Spaces:
Running on Zero
VRAM Calculator v3: YaRN coherence ladder, real spec enum, live UI + graphs
Browse filesPre-flight VRAM predictor + llama-server command-builder. See PLAN.md for
the full design spine from the grilling session.
Core logic:
- yarn.py: add yarn_coherence_warnings + extension_ratio (monotonic ladder:
<=2x fine, >2-8x tail degradation, >=8x incoherent/not-recommended), wired
into estimate() warnings.
- report.py: command_preview emits --fit on --fit-target <margin MiB>
(complement path; margin from smallest GPU's VRAM share).
- draft.py: rewrite spec surface to the real llama.cpp --spec-type enum
(draft-simple/eagle3/mtp/dflash + ngram-*). Split weightless (n-gram,
zero VRAM, no -md) vs weighted (draft, weights+KV, full --spec-draft-*
namespace). Fixes invalid --spec-type draft shipped previously.
- quant.py: nvfp4 documented as commented placeholder (VERIFY before ship);
not selectable as a floor until a verified bpw is sourced.
Tests: 67 passing (real-enum spec, YaRN coherence ladder, --fit/--fit-target,
weightless/weighted branches).
UI (app.py): two-phase (fetch once -> live estimate() recompute on every
change) + two matplotlib graphs (VRAM-vs-n_ctx by quant; VRAM-vs-quant with
quality/bpw color) + "assumes your build supports the chosen quant" caveat.
Co-Authored-By: Claude <noreply@anthropic.com>
- .gitignore +5 -1
- PLAN.md +141 -0
- app.py +254 -43
- tests/test_vramcalc.py +190 -6
- vramcalc/__init__.py +10 -1
- vramcalc/draft.py +50 -2
- vramcalc/quant.py +5 -0
- vramcalc/report.py +42 -8
- vramcalc/yarn.py +55 -1
|
@@ -18,4 +18,8 @@ venv/
|
|
| 18 |
# OS / editor
|
| 19 |
.DS_Store
|
| 20 |
.idea/
|
| 21 |
-
.vscode/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# OS / editor
|
| 19 |
.DS_Store
|
| 20 |
.idea/
|
| 21 |
+
.vscode/
|
| 22 |
+
|
| 23 |
+
# Local Claude Code tooling
|
| 24 |
+
.claude/
|
| 25 |
+
skills-lock.json
|
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# VRAM Calculator — v1 Plan
|
| 2 |
+
|
| 3 |
+
Pre-flight VRAM predictor + `llama-server` command-builder. Single-GPU and
|
| 4 |
+
mixed-GPU. Hero use case: "I want 1M context on a model trained for 262k —
|
| 5 |
+
give me the command and tell me it'll still be coherent."
|
| 6 |
+
|
| 7 |
+
This document is the spine agreed in the /grilling session. The grilling is
|
| 8 |
+
closed; everything below is decided.
|
| 9 |
+
|
| 10 |
+
## Objective
|
| 11 |
+
|
| 12 |
+
**Maximize quality at a VRAM ceiling.** Quality proxy = highest-bpw quant
|
| 13 |
+
that fits. Floor = the 4-bit class (q4_0 / rocmfp4 / nvfp4, ~4.0+ bpw).
|
| 14 |
+
Sub-4-bit quants (2-bit/3-bit family) are offered only on explicit user
|
| 15 |
+
override and flagged "you asked for it." Backend-agnostic.
|
| 16 |
+
|
| 17 |
+
## The fit loop (core)
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
target = user_target_n_ctx
|
| 21 |
+
yarn = auto_configure_yarn(training_ctx, target) # rope_freq_scale
|
| 22 |
+
fit = max_context(arch, floor_quant, gpus) # hard VRAM ceiling
|
| 23 |
+
result = min(target, fit)
|
| 24 |
+
if fit < target: present "best you can do = fit, because <reason>"
|
| 25 |
+
if extension_ratio crosses ladder: warn (never hard-block)
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
Says "no" cleanly when impossible at floor + hardware, and offers the
|
| 29 |
+
floor-quant max_context as the honest fallback.
|
| 30 |
+
|
| 31 |
+
## YaRN coherence ladder (warnings only, never a hard block)
|
| 32 |
+
|
| 33 |
+
Monotonic — more extension always means same-or-more concern, never goes quiet:
|
| 34 |
+
|
| 35 |
+
- **≤ 2× extension** — fine, no warning.
|
| 36 |
+
- **> 2× up to 8×** — "expect some quality degradation at the tail."
|
| 37 |
+
- **≥ 8× extension** — "likely incoherent at long range; not recommended."
|
| 38 |
+
|
| 39 |
+
Source: YaRN paper behavior + community experience, not measured. Extension
|
| 40 |
+
ratio = target_n_ctx / training_ctx (1.0 when target ≤ training or training
|
| 41 |
+
unknown). Implemented as `yarn_coherence_warnings(training_ctx, target_ctx)`
|
| 42 |
+
in `yarn.py`, surfaced in `estimate()` warnings and the UI.
|
| 43 |
+
|
| 44 |
+
## Margin = the honesty knob
|
| 45 |
+
|
| 46 |
+
`safety_margin_pct` is the user-facing "how scared am I" knob. **Complement
|
| 47 |
+
path:** the predictor emits *fully explicit* flags (`-c`, `-ngl`, `-sm`,
|
| 48 |
+
`-ts`, `-mg`, `-ctk`, `-ctv`, `--flash-attn`, YaRN flags) **plus**
|
| 49 |
+
`--fit on --fit-target <margin MiB>`. Because everything is already explicit,
|
| 50 |
+
runtime `--fit` is a no-op safety net — the user gets exactly what the
|
| 51 |
+
predictor said. Our number *is* the guarantee, with the margin as the dial.
|
| 52 |
+
Never use the word "guarantee" in UI copy; the margin is the honest version.
|
| 53 |
+
|
| 54 |
+
## Speculative decoding — two branches (this is a bugfix)
|
| 55 |
+
|
| 56 |
+
The shipped `--spec-type draft` is **invalid**. The real enum (from the
|
| 57 |
+
llama.cpp server README) is:
|
| 58 |
+
|
| 59 |
+
```
|
| 60 |
+
none, draft-simple, draft-eagle3, draft-mtp, draft-dflash,
|
| 61 |
+
ngram-simple, ngram-map-k, ngram-map-k4v, ngram-mod, ngram-cache
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
Rewrite the spec surface to this enum, then split the add-on flow:
|
| 65 |
+
|
| 66 |
+
- **Weightless** (`ngram-*`): ~zero extra VRAM. Auto-offer when headroom
|
| 67 |
+
exists; no "acknowledge the tradeoff" checkbox (there's no VRAM tradeoff).
|
| 68 |
+
- **Weighted** (`draft-simple` / `draft-eagle3` / `draft-mtp` /
|
| 69 |
+
`draft-dflash`): a draft model with weights + its own KV. Headroom-gated +
|
| 70 |
+
an acknowledge checkbox. Draft rides the main GPU. Emit the full
|
| 71 |
+
`--spec-draft-*` namespace (`-md`, `--spec-draft-ngl`, `--spec-draft-device`,
|
| 72 |
+
`--spec-draft-n-max`, `--spec-draft-p-min`, `--spec-draft-p-split`), not
|
| 73 |
+
the old `-md`-only stub.
|
| 74 |
+
|
| 75 |
+
`draft-mtp` (a spec *type*) is distinct from `--mtp N` (MTP head count on
|
| 76 |
+
the target). Keep both but emit the right flag for each.
|
| 77 |
+
|
| 78 |
+
## Backend scope
|
| 79 |
+
|
| 80 |
+
**Dropped.** Assume it runs. One displayed caveat: "fits in VRAM assuming
|
| 81 |
+
your build supports the chosen quant." Keep the existing stock-build
|
| 82 |
+
ROCmFP4/ROCmFPX incompatibility warning (that's not a backend issue — no
|
| 83 |
+
stock build can read those tensor types at all).
|
| 84 |
+
|
| 85 |
+
## Quarantined unverified data
|
| 86 |
+
|
| 87 |
+
Do **not** block v1 on these:
|
| 88 |
+
|
| 89 |
+
- **nvfp4** (llama.cpp PR #22196): not yet in `QUANT_BPW`. Add a provisional
|
| 90 |
+
row with a `# VERIFY before ship` comment once we have a verified
|
| 91 |
+
bits-per-weight; until then it's absent from the table and can't be
|
| 92 |
+
selected as a floor.
|
| 93 |
+
- **dSpark** (#25167 / #25173 / DeepSeek-V4-Flash-DSpark): folds into the
|
| 94 |
+
existing draft-model VRAM path and emits `--spec-type draft-mtp`
|
| 95 |
+
provisionally. For a *predictor*, dSpark's VRAM cost is just
|
| 96 |
+
draft-weights + draft-KV, same as any MTP draft; the spec-type label only
|
| 97 |
+
affects the command string.
|
| 98 |
+
- **Vulkan quant matrix:** dropped (consequence of dropping backends).
|
| 99 |
+
|
| 100 |
+
## UI — two phases, two graphs
|
| 101 |
+
|
| 102 |
+
1. **Fetch phase:** repo ID + HF token → `parse_hf_range` once → cache
|
| 103 |
+
`ModelArch`. Network happens here, once, never again.
|
| 104 |
+
2. **Live phase:** everything is `estimate()` recompute on the cached arch
|
| 105 |
+
(microseconds). Context slider, quant selector, KV-dtype, GPU sizes,
|
| 106 |
+
margin. The recommended command updates live.
|
| 107 |
+
3. **Graph 1:** VRAM vs `n_ctx`, one curve per quant — the context ceiling,
|
| 108 |
+
with the slider sitting on it.
|
| 109 |
+
4. **Graph 2:** VRAM vs quant at fixed `n_ctx`, **with quality as
|
| 110 |
+
color/rank** — the cost+quality frontier (where "maximize quality at the
|
| 111 |
+
ceiling" becomes visible).
|
| 112 |
+
|
| 113 |
+
## Implementation order
|
| 114 |
+
|
| 115 |
+
Done:
|
| 116 |
+
|
| 117 |
+
1. ✅ **`yarn.py`** — `yarn_coherence_warnings(training_ctx, target_ctx)` +
|
| 118 |
+
`extension_ratio`; wired into `yarn_warnings()` (and thus `estimate()`).
|
| 119 |
+
2. ✅ **`report.py`** — emits `--fit on --fit-target <margin MiB>` via
|
| 120 |
+
`_fit_target_mib()` (margin from smallest GPU's VRAM share).
|
| 121 |
+
3. ✅ **`draft.py` + `report.py`** — spec surface rewritten to the real
|
| 122 |
+
llama.cpp enum (`draft-simple`, `draft-eagle3`, `draft-mtp`,
|
| 123 |
+
`draft-dflash`, `ngram-*`); weightless (ngram, zero VRAM) vs weighted
|
| 124 |
+
(draft, weights+KV) split; `command_preview` emits `-md` + the full
|
| 125 |
+
`--spec-draft-*` namespace for weighted, only `--spec-type` for
|
| 126 |
+
weightless.
|
| 127 |
+
4. ✅ **`quant.py`** — `nvfp4` documented as a commented placeholder with a
|
| 128 |
+
VERIFY note; not selectable as a floor until a verified bpw is sourced.
|
| 129 |
+
5. ✅ **Tests** — 67 passing (real-enum spec tests, YaRN coherence-ladder
|
| 130 |
+
tests, `--fit`/`--fit-target` tests, weightless/weighted branch tests).
|
| 131 |
+
6. ✅ **`app.py`** — two-phase UI (fetch once → live `estimate()` recompute
|
| 132 |
+
on every change), two graphs (VRAM-vs-n_ctx by quant; VRAM-vs-quant with
|
| 133 |
+
quality color), the "assumes your build supports the chosen quant"
|
| 134 |
+
caveat, and the sub-floor "you asked for it" warning. Syntax-checked;
|
| 135 |
+
graph math verified against presets (gradio not installable locally).
|
| 136 |
+
|
| 137 |
+
## Out of scope for v1
|
| 138 |
+
|
| 139 |
+
Per-backend quant compatibility matrices, MLX, Vulkan-specific quants,
|
| 140 |
+
runtime auto-tuning (that's llama.cpp's `--fit`), measured perplexity/WikiText
|
| 141 |
+
quality tables, throughput/token-per-second modeling.
|
|
@@ -1,15 +1,25 @@
|
|
| 1 |
"""llama.cpp VRAM Calculator — Hugging Face Space.
|
| 2 |
|
| 3 |
-
A
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
| 13 |
from huggingface_hub import HfApi
|
| 14 |
import spaces # noqa: F401 — present so ZeroGPU detects a GPU-aware Space
|
| 15 |
|
|
@@ -29,7 +39,10 @@ from vramcalc import (
|
|
| 29 |
min_gpu_setup,
|
| 30 |
mmproj_bytes_from_tensors,
|
| 31 |
auto_configure_yarn,
|
|
|
|
|
|
|
| 32 |
)
|
|
|
|
| 33 |
from vramcalc.presets import PRESETS, PRESET_NAMES
|
| 34 |
|
| 35 |
|
|
@@ -37,9 +50,18 @@ QUANT_CHOICES = list(QUANT_BPW.keys())
|
|
| 37 |
CACHE_DTYPES = ["f16", "bf16", "f32", "q8_0", "q8_1", "q4_0", "q4_1", "q5_0", "q5_1"]
|
| 38 |
COMPUTE_DTYPES = ["f16", "bf16", "f32"]
|
| 39 |
SPLIT_MODES = ["layer", "tensor", "none"]
|
| 40 |
-
SPEC_TYPES =
|
| 41 |
MTP_CACHE_DTYPES = ["(same as target)", "f16", "bf16", "f32", "q8_0"]
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
def list_gguf_files(repo_id: str, hf_token: str):
|
| 45 |
"""List *.gguf files in a repo and auto-detect their quant names.
|
|
@@ -312,10 +334,15 @@ def _parse_tensor_split(text: str) -> list[float] | None:
|
|
| 312 |
return out or None
|
| 313 |
|
| 314 |
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
n_batch, n_prompt,
|
| 320 |
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 321 |
yarn_beta_fast, yarn_beta_slow,
|
|
@@ -324,15 +351,14 @@ def compute(
|
|
| 324 |
spec_type, draft_quant, draft_params, draft_n_layer, draft_n_max,
|
| 325 |
draft_p_min, draft_p_split,
|
| 326 |
mmproj_enabled, mmproj_file, mmproj_offload, mmproj_bytes_text,
|
| 327 |
-
):
|
| 328 |
arch = _fields_to_arch(arch_fields)
|
| 329 |
-
# GPU text may carry a 'u' suffix for unified memory; strip it for vram_gb
|
| 330 |
gpus = _parse_gpu_list(gpu_vram_text.replace("u", "").replace("U", ""))
|
| 331 |
unified = _parse_unified_flags(gpu_vram_text)
|
| 332 |
tensor_split = _parse_tensor_split(tensor_split_text)
|
| 333 |
|
| 334 |
draft = None
|
| 335 |
-
if spec_type and spec_type != "none"
|
| 336 |
draft = DraftInputs(
|
| 337 |
spec_type=spec_type,
|
| 338 |
quant=draft_quant,
|
|
@@ -377,11 +403,135 @@ def compute(
|
|
| 377 |
mmproj=mmproj,
|
| 378 |
safety_margin_pct=float(safety_margin),
|
| 379 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
if arch.n_layer <= 0 or arch.n_embd <= 0 or arch.params <= 0:
|
| 381 |
return (
|
| 382 |
-
"⚠️
|
| 383 |
-
"
|
| 384 |
-
"",
|
| 385 |
)
|
| 386 |
bd = estimate(arch, inp)
|
| 387 |
|
|
@@ -406,12 +556,19 @@ def compute(
|
|
| 406 |
f"| {a} | {b} |" for a, b in rows
|
| 407 |
)
|
| 408 |
|
| 409 |
-
# warnings
|
| 410 |
-
|
| 411 |
-
if
|
| 412 |
-
|
| 413 |
-
f"
|
|
|
|
| 414 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
eff_md = (
|
| 416 |
f"\n\nEffective context (training_ctx / rope_freq_scale): "
|
| 417 |
f"**{bd.effective_context}**"
|
|
@@ -449,10 +606,21 @@ def compute(
|
|
| 449 |
f"**{arch.name or arch.architecture or 'Model'}** @ {quant}, "
|
| 450 |
f"ctx {inp.n_ctx} ({cache_dtype} KV"
|
| 451 |
+ (", FA" if inp.flash_attn else ", no FA")
|
| 452 |
-
+ f"), {len(
|
| 453 |
f"**{format_bytes(bd.total_bytes)}** total"
|
| 454 |
)
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
|
| 457 |
|
| 458 |
def build_ui():
|
|
@@ -556,16 +724,25 @@ def build_ui():
|
|
| 556 |
)
|
| 557 |
yarn_auto_status = gr.Markdown("")
|
| 558 |
|
| 559 |
-
with gr.Accordion("Speculative decoding (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
spec_type = gr.Dropdown(
|
| 561 |
label="Spec type", choices=SPEC_TYPES, value="none"
|
| 562 |
)
|
| 563 |
with gr.Row():
|
| 564 |
draft_quant = gr.Dropdown(
|
| 565 |
-
label="Draft quant
|
|
|
|
| 566 |
)
|
| 567 |
draft_params = gr.Number(
|
| 568 |
-
label="Draft params
|
|
|
|
| 569 |
)
|
| 570 |
draft_n_layer = gr.Number(
|
| 571 |
label="Draft n_layer (1 for EAGLE-3)",
|
|
@@ -611,13 +788,20 @@ def build_ui():
|
|
| 611 |
safety_margin = gr.Number(label="Safety margin %", value=5.0)
|
| 612 |
|
| 613 |
compute_btn = gr.Button("Compute VRAM", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
|
| 615 |
gr.Markdown("### Results")
|
| 616 |
result_md = gr.Markdown("")
|
| 617 |
-
with gr.
|
| 618 |
-
|
|
|
|
|
|
|
| 619 |
cmd_text = gr.Textbox(
|
| 620 |
-
label="Command (copyable)", lines=
|
| 621 |
)
|
| 622 |
|
| 623 |
with gr.Tab("Auto-fit"):
|
|
@@ -689,21 +873,48 @@ def build_ui():
|
|
| 689 |
outputs=[mmproj_bytes_box, fetch_status],
|
| 690 |
)
|
| 691 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
compute_btn.click(
|
| 693 |
-
fn=
|
| 694 |
-
inputs=
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
)
|
| 708 |
|
| 709 |
# Auto-fit wiring
|
|
|
|
| 1 |
"""llama.cpp VRAM Calculator — Hugging Face Space.
|
| 2 |
|
| 3 |
+
A pre-flight VRAM predictor + llama-server command-builder. Hero use case:
|
| 4 |
+
"I want N context on a model trained for less — give me the command and
|
| 5 |
+
warn me about coherence." Two-phase UI:
|
| 6 |
+
|
| 7 |
+
1. Fetch phase (network, once): repo id + GGUF file -> parse_hf_range ->
|
| 8 |
+
cached ModelArch. Network happens here and never again.
|
| 9 |
+
2. Live phase (microseconds): every input change re-runs estimate() on the
|
| 10 |
+
cached arch; the command, breakdown, and graphs update live.
|
| 11 |
+
|
| 12 |
+
Two graphs: VRAM-vs-n_ctx (one curve per quant, the context ceiling) and
|
| 13 |
+
VRAM-vs-quant at fixed n_ctx (the cost+quality frontier, quality as color).
|
| 14 |
+
See PLAN.md for the full design spine.
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
import gradio as gr
|
| 20 |
+
import matplotlib
|
| 21 |
+
matplotlib.use("Agg") # headless: render to PNG, no display needed
|
| 22 |
+
import matplotlib.pyplot as plt
|
| 23 |
from huggingface_hub import HfApi
|
| 24 |
import spaces # noqa: F401 — present so ZeroGPU detects a GPU-aware Space
|
| 25 |
|
|
|
|
| 39 |
min_gpu_setup,
|
| 40 |
mmproj_bytes_from_tensors,
|
| 41 |
auto_configure_yarn,
|
| 42 |
+
yarn_coherence_warnings,
|
| 43 |
+
extension_ratio,
|
| 44 |
)
|
| 45 |
+
from vramcalc.draft import SPEC_TYPES as REAL_SPEC_TYPES
|
| 46 |
from vramcalc.presets import PRESETS, PRESET_NAMES
|
| 47 |
|
| 48 |
|
|
|
|
| 50 |
CACHE_DTYPES = ["f16", "bf16", "f32", "q8_0", "q8_1", "q4_0", "q4_1", "q5_0", "q5_1"]
|
| 51 |
COMPUTE_DTYPES = ["f16", "bf16", "f32"]
|
| 52 |
SPLIT_MODES = ["layer", "tensor", "none"]
|
| 53 |
+
SPEC_TYPES = list(REAL_SPEC_TYPES) # the real llama.cpp --spec-type enum
|
| 54 |
MTP_CACHE_DTYPES = ["(same as target)", "f16", "bf16", "f32", "q8_0"]
|
| 55 |
|
| 56 |
+
# The 4-bit-class floor quants (q4_0 / ROCmFP4 / ~nvfp4). Sub-4-bit quants
|
| 57 |
+
# (2-bit/3-bit family) are offered only on explicit override and flagged
|
| 58 |
+
# "you asked for it." See PLAN.md "Objective".
|
| 59 |
+
FLOOR_QUANTS = {"q4_0", "ROCmFP4", "ROCmFPX"} # ~4.0+ bpw
|
| 60 |
+
|
| 61 |
+
# n_ctx sweep for the VRAM-vs-context graph (log-ish, capped at 1M).
|
| 62 |
+
GRAPH_CTX_GRID = [1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144,
|
| 63 |
+
524288, 1048576]
|
| 64 |
+
|
| 65 |
|
| 66 |
def list_gguf_files(repo_id: str, hf_token: str):
|
| 67 |
"""List *.gguf files in a repo and auto-detect their quant names.
|
|
|
|
| 334 |
return out or None
|
| 335 |
|
| 336 |
|
| 337 |
+
# --- Live recompute + graphs -------------------------------------------------
|
| 338 |
+
#
|
| 339 |
+
# The fetch phase caches a ModelArch; everything below is pure estimate()
|
| 340 |
+
# math on that cached arch, so it runs in microseconds and can fire on every
|
| 341 |
+
# input change (live sliders). No network in the live phase. See PLAN.md.
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def _build_inputs(
|
| 345 |
+
arch_fields, quant, n_ctx, cache_dtype, flash_attn, compute_dtype,
|
| 346 |
n_batch, n_prompt,
|
| 347 |
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 348 |
yarn_beta_fast, yarn_beta_slow,
|
|
|
|
| 351 |
spec_type, draft_quant, draft_params, draft_n_layer, draft_n_max,
|
| 352 |
draft_p_min, draft_p_split,
|
| 353 |
mmproj_enabled, mmproj_file, mmproj_offload, mmproj_bytes_text,
|
| 354 |
+
) -> tuple[ModelArch, Inputs]:
|
| 355 |
arch = _fields_to_arch(arch_fields)
|
|
|
|
| 356 |
gpus = _parse_gpu_list(gpu_vram_text.replace("u", "").replace("U", ""))
|
| 357 |
unified = _parse_unified_flags(gpu_vram_text)
|
| 358 |
tensor_split = _parse_tensor_split(tensor_split_text)
|
| 359 |
|
| 360 |
draft = None
|
| 361 |
+
if spec_type and spec_type != "none":
|
| 362 |
draft = DraftInputs(
|
| 363 |
spec_type=spec_type,
|
| 364 |
quant=draft_quant,
|
|
|
|
| 403 |
mmproj=mmproj,
|
| 404 |
safety_margin_pct=float(safety_margin),
|
| 405 |
)
|
| 406 |
+
return arch, inp
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def graph_vram_vs_ctx(arch: ModelArch, inp: Inputs) -> plt.Figure | None:
|
| 410 |
+
"""Graph 1: VRAM vs n_ctx, one curve per quant.
|
| 411 |
+
|
| 412 |
+
Shows the context ceiling each quant reaches on the configured GPUs. The
|
| 413 |
+
user's current n_ctx + the budget total are overlaid so the slider sits
|
| 414 |
+
on the curve. Returns None when the arch is incomplete.
|
| 415 |
+
"""
|
| 416 |
+
if arch.n_layer <= 0 or arch.n_embd <= 0 or arch.params <= 0:
|
| 417 |
+
return None
|
| 418 |
+
budget_gb = sum(inp.gpu_vram_gb)
|
| 419 |
+
# sweep the grid up to max(training_ctx*4, current n_ctx, 1M)
|
| 420 |
+
cap = max(arch.training_ctx * 4, inp.n_ctx, 1 << 20)
|
| 421 |
+
grid = [c for c in GRAPH_CTX_GRID if c <= cap] or [cap]
|
| 422 |
+
|
| 423 |
+
# order quants low->high bpw so the legend reads bottom-to-top
|
| 424 |
+
quants = sorted(QUANT_CHOICES, key=lambda q: QUANT_BPW[q])
|
| 425 |
+
fig, ax = plt.subplots(figsize=(7, 4.2))
|
| 426 |
+
palette = plt.cm.viridis
|
| 427 |
+
n_q = max(1, len(quants))
|
| 428 |
+
for i, q in enumerate(quants):
|
| 429 |
+
ys = []
|
| 430 |
+
for c in grid:
|
| 431 |
+
from dataclasses import replace
|
| 432 |
+
bd = estimate(arch, replace(inp, quant=q, n_ctx=c))
|
| 433 |
+
ys.append(bd.total_bytes / (1 << 30))
|
| 434 |
+
color = palette(i / max(1, n_q - 1))
|
| 435 |
+
ax.plot(grid, ys, marker="o", markersize=3, linewidth=1.6,
|
| 436 |
+
label=q, color=color)
|
| 437 |
+
|
| 438 |
+
# current config marker + budget line
|
| 439 |
+
cur = estimate(arch, inp)
|
| 440 |
+
ax.axhline(budget_gb, color="crimson", linestyle="--", linewidth=1.2,
|
| 441 |
+
label=f"budget {budget_gb:.0f} GB")
|
| 442 |
+
ax.scatter([inp.n_ctx], [cur.total_bytes / (1 << 30)],
|
| 443 |
+
color="crimson", zorder=5, s=55,
|
| 444 |
+
label=f"current ({inp.quant})")
|
| 445 |
+
ax.set_xscale("log")
|
| 446 |
+
ax.set_xlabel("context length (n_ctx)")
|
| 447 |
+
ax.set_ylabel("total VRAM (GiB)")
|
| 448 |
+
ax.set_title("VRAM vs context, by quant")
|
| 449 |
+
ax.grid(True, which="both", alpha=0.25)
|
| 450 |
+
ax.legend(fontsize=7, ncol=2, loc="upper left")
|
| 451 |
+
fig.tight_layout()
|
| 452 |
+
return fig
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def graph_vram_vs_quant(arch: ModelArch, inp: Inputs) -> plt.Figure | None:
|
| 456 |
+
"""Graph 2: VRAM vs quant at fixed n_ctx, colored by quality (bpw).
|
| 457 |
+
|
| 458 |
+
The cost+quality frontier: each bar's height is VRAM, its color is the
|
| 459 |
+
quant's bpw (proxy for quality). The budget is a horizontal line; bars
|
| 460 |
+
below it fit. Returns None when the arch is incomplete.
|
| 461 |
+
"""
|
| 462 |
+
if arch.n_layer <= 0 or arch.n_embd <= 0 or arch.params <= 0:
|
| 463 |
+
return None
|
| 464 |
+
budget_gb = sum(inp.gpu_vram_gb)
|
| 465 |
+
from dataclasses import replace
|
| 466 |
+
quants = sorted(QUANT_CHOICES, key=lambda q: QUANT_BPW[q])
|
| 467 |
+
ys, bpws, labels = [], [], []
|
| 468 |
+
for q in quants:
|
| 469 |
+
bd = estimate(arch, replace(inp, quant=q))
|
| 470 |
+
ys.append(bd.total_bytes / (1 << 30))
|
| 471 |
+
bpws.append(QUANT_BPW[q])
|
| 472 |
+
labels.append(q)
|
| 473 |
+
|
| 474 |
+
fig, ax = plt.subplots(figsize=(7, 4.2))
|
| 475 |
+
cmap = plt.cm.plasma
|
| 476 |
+
norm = plt.Normalize(vmin=min(bpws), vmax=max(bpws))
|
| 477 |
+
colors = [cmap(norm(b)) for b in bpws]
|
| 478 |
+
bars = ax.bar(range(len(quants)), ys, color=colors, edgecolor="black",
|
| 479 |
+
linewidth=0.4)
|
| 480 |
+
ax.axhline(budget_gb, color="crimson", linestyle="--", linewidth=1.2,
|
| 481 |
+
label=f"budget {budget_gb:.0f} GB")
|
| 482 |
+
# highlight the currently-selected quant
|
| 483 |
+
if inp.quant in quants:
|
| 484 |
+
idx = quants.index(inp.quant)
|
| 485 |
+
bars[idx].set_edgecolor("crimson")
|
| 486 |
+
bars[idx].set_linewidth(2.0)
|
| 487 |
+
ax.set_xticks(range(len(quants)))
|
| 488 |
+
ax.set_xticklabels(labels, rotation=55, ha="right", fontsize=7)
|
| 489 |
+
ax.set_ylabel("total VRAM (GiB)")
|
| 490 |
+
ax.set_title(f"VRAM vs quant @ ctx {inp.n_ctx} (color = quality/bpw)")
|
| 491 |
+
ax.grid(True, axis="y", alpha=0.25)
|
| 492 |
+
ax.legend(fontsize=8, loc="upper left")
|
| 493 |
+
# colorbar for quality
|
| 494 |
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
| 495 |
+
sm.set_array([])
|
| 496 |
+
fig.colorbar(sm, ax=ax, label="bits per weight (quality)")
|
| 497 |
+
fig.tight_layout()
|
| 498 |
+
return fig
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def live_compute(
|
| 502 |
+
arch_fields,
|
| 503 |
+
quant, n_ctx, cache_dtype, flash_attn, compute_dtype,
|
| 504 |
+
n_batch, n_prompt,
|
| 505 |
+
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 506 |
+
yarn_beta_fast, yarn_beta_slow,
|
| 507 |
+
gpu_vram_text, split_mode, main_gpu, tensor_split_text, safety_margin,
|
| 508 |
+
mtp_cache_dtype,
|
| 509 |
+
spec_type, draft_quant, draft_params, draft_n_layer, draft_n_max,
|
| 510 |
+
draft_p_min, draft_p_split,
|
| 511 |
+
mmproj_enabled, mmproj_file, mmproj_offload, mmproj_bytes_text,
|
| 512 |
+
):
|
| 513 |
+
"""Live recompute: estimate() on the cached arch + graphs + command.
|
| 514 |
+
|
| 515 |
+
Returns (summary_markdown, command_text, graph1, graph2). No network —
|
| 516 |
+
the arch was fetched once and stored in arch_state. Fires on every input
|
| 517 |
+
change so sliders are live.
|
| 518 |
+
"""
|
| 519 |
+
arch, inp = _build_inputs(
|
| 520 |
+
arch_fields, quant, n_ctx, cache_dtype, flash_attn, compute_dtype,
|
| 521 |
+
n_batch, n_prompt,
|
| 522 |
+
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 523 |
+
yarn_beta_fast, yarn_beta_slow,
|
| 524 |
+
gpu_vram_text, split_mode, main_gpu, tensor_split_text, safety_margin,
|
| 525 |
+
mtp_cache_dtype,
|
| 526 |
+
spec_type, draft_quant, draft_params, draft_n_layer, draft_n_max,
|
| 527 |
+
draft_p_min, draft_p_split,
|
| 528 |
+
mmproj_enabled, mmproj_file, mmproj_offload, mmproj_bytes_text,
|
| 529 |
+
)
|
| 530 |
if arch.n_layer <= 0 or arch.n_embd <= 0 or arch.params <= 0:
|
| 531 |
return (
|
| 532 |
+
"⚠️ Fetch a GGUF (or load a preset) to populate the architecture, "
|
| 533 |
+
"then adjust the sliders.",
|
| 534 |
+
"", None, None,
|
| 535 |
)
|
| 536 |
bd = estimate(arch, inp)
|
| 537 |
|
|
|
|
| 556 |
f"| {a} | {b} |" for a, b in rows
|
| 557 |
)
|
| 558 |
|
| 559 |
+
# warnings (incl. the YaRN coherence ladder) + the build-support caveat
|
| 560 |
+
warns = list(bd.warnings)
|
| 561 |
+
if quant not in FLOOR_QUANTS and QUANT_BPW.get(quant, 99) < 4.0:
|
| 562 |
+
warns.append(
|
| 563 |
+
f"{quant} ({QUANT_BPW[quant]} bpw) is below the 4-bit quality "
|
| 564 |
+
f"floor — you asked for it."
|
| 565 |
)
|
| 566 |
+
warns.append(
|
| 567 |
+
"Estimate assumes your llama.cpp build supports the chosen quant "
|
| 568 |
+
"and KV dtype. ROCmFP4/ROCmFPX need the ciru-ai/ROCmFPX runner."
|
| 569 |
+
)
|
| 570 |
+
warn_md = "\n\n**⚠️ Notes:**\n" + "\n".join(f"- {w}" for w in warns) if warns else ""
|
| 571 |
+
|
| 572 |
eff_md = (
|
| 573 |
f"\n\nEffective context (training_ctx / rope_freq_scale): "
|
| 574 |
f"**{bd.effective_context}**"
|
|
|
|
| 606 |
f"**{arch.name or arch.architecture or 'Model'}** @ {quant}, "
|
| 607 |
f"ctx {inp.n_ctx} ({cache_dtype} KV"
|
| 608 |
+ (", FA" if inp.flash_attn else ", no FA")
|
| 609 |
+
+ f"), {len(inp.gpu_vram_gb)} GPU(s) → "
|
| 610 |
f"**{format_bytes(bd.total_bytes)}** total"
|
| 611 |
)
|
| 612 |
+
md = summary + "\n\n" + breakdown_md + eff_md + warn_md + "\n\n" + gpu_md
|
| 613 |
+
|
| 614 |
+
g1 = graph_vram_vs_ctx(arch, inp)
|
| 615 |
+
g2 = graph_vram_vs_quant(arch, inp)
|
| 616 |
+
return md, cmd, g1, g2
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
# The live handler is CPU-only estimate math (no GPU needed). `spaces` is
|
| 620 |
+
# imported above so ZeroGPU detects a GPU-aware Space; we don't decorate the
|
| 621 |
+
# hot path with @spaces.GPU because that would queue every slider drag.
|
| 622 |
+
def compute(*args, **kwargs): # noqa: D401 — backward-compat name
|
| 623 |
+
return live_compute(*args, **kwargs)
|
| 624 |
|
| 625 |
|
| 626 |
def build_ui():
|
|
|
|
| 724 |
)
|
| 725 |
yarn_auto_status = gr.Markdown("")
|
| 726 |
|
| 727 |
+
with gr.Accordion("Speculative decoding (speed add-on)", open=False):
|
| 728 |
+
gr.Markdown(
|
| 729 |
+
"_Two branches. **Weightless** (`ngram-*`): ~zero extra "
|
| 730 |
+
"VRAM, no draft model — auto-offered when headroom exists. "
|
| 731 |
+
"**Weighted** (`draft-*`): a draft model with its own "
|
| 732 |
+
"weights + KV; only enable when you have headroom and "
|
| 733 |
+
"accept the tradeoff._"
|
| 734 |
+
)
|
| 735 |
spec_type = gr.Dropdown(
|
| 736 |
label="Spec type", choices=SPEC_TYPES, value="none"
|
| 737 |
)
|
| 738 |
with gr.Row():
|
| 739 |
draft_quant = gr.Dropdown(
|
| 740 |
+
label="Draft quant (weighted only)",
|
| 741 |
+
choices=QUANT_CHOICES, value="Q4_K_M"
|
| 742 |
)
|
| 743 |
draft_params = gr.Number(
|
| 744 |
+
label="Draft params (weighted only)",
|
| 745 |
+
value=0, precision=0
|
| 746 |
)
|
| 747 |
draft_n_layer = gr.Number(
|
| 748 |
label="Draft n_layer (1 for EAGLE-3)",
|
|
|
|
| 788 |
safety_margin = gr.Number(label="Safety margin %", value=5.0)
|
| 789 |
|
| 790 |
compute_btn = gr.Button("Compute VRAM", variant="primary")
|
| 791 |
+
gr.Markdown(
|
| 792 |
+
"_Live: every slider/dropdown above updates the results "
|
| 793 |
+
"and graphs on change. The Compute button is a manual "
|
| 794 |
+
"trigger for the same recompute._"
|
| 795 |
+
)
|
| 796 |
|
| 797 |
gr.Markdown("### Results")
|
| 798 |
result_md = gr.Markdown("")
|
| 799 |
+
with gr.Row():
|
| 800 |
+
graph_ctx = gr.Plot(label="VRAM vs context (by quant)")
|
| 801 |
+
graph_quant = gr.Plot(label="VRAM vs quant @ ctx (color = quality)")
|
| 802 |
+
with gr.Accordion("llama.cpp launch command preview", open=True):
|
| 803 |
cmd_text = gr.Textbox(
|
| 804 |
+
label="Command (copyable)", lines=10, interactive=False
|
| 805 |
)
|
| 806 |
|
| 807 |
with gr.Tab("Auto-fit"):
|
|
|
|
| 873 |
outputs=[mmproj_bytes_box, fetch_status],
|
| 874 |
)
|
| 875 |
|
| 876 |
+
# Live recompute inputs — every input that affects the estimate.
|
| 877 |
+
live_inputs = [
|
| 878 |
+
arch_state, quant, n_ctx, cache_dtype, flash_attn, compute_dtype,
|
| 879 |
+
n_batch, n_prompt,
|
| 880 |
+
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 881 |
+
yarn_beta_fast, yarn_beta_slow,
|
| 882 |
+
gpu_vram_text, split_mode, main_gpu, tensor_split_text,
|
| 883 |
+
safety_margin,
|
| 884 |
+
mtp_cache_dtype,
|
| 885 |
+
spec_type, draft_quant, draft_params, draft_n_layer,
|
| 886 |
+
draft_n_max, draft_p_min, draft_p_split,
|
| 887 |
+
mmproj_enabled, mmproj_file, mmproj_offload, mmproj_bytes_box,
|
| 888 |
+
]
|
| 889 |
+
live_outputs = [result_md, cmd_text, graph_ctx, graph_quant]
|
| 890 |
+
|
| 891 |
compute_btn.click(
|
| 892 |
+
fn=live_compute,
|
| 893 |
+
inputs=live_inputs,
|
| 894 |
+
outputs=live_outputs,
|
| 895 |
+
)
|
| 896 |
+
|
| 897 |
+
# Live: fire on every input change (estimate() is microseconds on the
|
| 898 |
+
# cached arch — no network). See PLAN.md "Live phase".
|
| 899 |
+
live_comps = [
|
| 900 |
+
quant, n_ctx, cache_dtype, flash_attn, compute_dtype,
|
| 901 |
+
n_batch, n_prompt,
|
| 902 |
+
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 903 |
+
yarn_beta_fast, yarn_beta_slow,
|
| 904 |
+
gpu_vram_text, split_mode, main_gpu, tensor_split_text,
|
| 905 |
+
safety_margin,
|
| 906 |
+
mtp_cache_dtype,
|
| 907 |
+
spec_type, draft_quant, draft_params, draft_n_layer,
|
| 908 |
+
draft_n_max, draft_p_min, draft_p_split,
|
| 909 |
+
mmproj_enabled, mmproj_file, mmproj_offload, mmproj_bytes_box,
|
| 910 |
+
]
|
| 911 |
+
for comp in live_comps:
|
| 912 |
+
comp.change(
|
| 913 |
+
fn=live_compute, inputs=live_inputs, outputs=live_outputs,
|
| 914 |
+
)
|
| 915 |
+
# arch_state changes (fetch/preset) also trigger a recompute.
|
| 916 |
+
arch_state.change(
|
| 917 |
+
fn=live_compute, inputs=live_inputs, outputs=live_outputs,
|
| 918 |
)
|
| 919 |
|
| 920 |
# Auto-fit wiring
|
|
@@ -10,6 +10,8 @@ from vramcalc import (
|
|
| 10 |
compute_scratch_bytes,
|
| 11 |
yarn_effective_context,
|
| 12 |
yarn_warnings,
|
|
|
|
|
|
|
| 13 |
auto_configure_yarn,
|
| 14 |
gpu_split,
|
| 15 |
estimate,
|
|
@@ -320,7 +322,7 @@ def test_estimate_includes_draft_and_mmproj():
|
|
| 320 |
from vramcalc import Inputs, DraftInputs, Mmproj
|
| 321 |
arch = PRESETS["Llama-3 8B"]
|
| 322 |
base = estimate(arch, Inputs(quant="Q4_K_M", n_ctx=8192, gpu_vram_gb=[24.0]))
|
| 323 |
-
draft = DraftInputs(spec_type="draft", quant="Q4_K_M",
|
| 324 |
params=1_000_000_000, n_layer=4, cache_dtype="f16")
|
| 325 |
mm = Mmproj(enabled=True, bytes_=500 * 1e6)
|
| 326 |
with_extras = estimate(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
|
@@ -342,8 +344,8 @@ def test_mmproj_bytes_from_tensors():
|
|
| 342 |
|
| 343 |
def test_draft_bytes_basic():
|
| 344 |
from vramcalc import DraftInputs, draft_bytes
|
| 345 |
-
d = DraftInputs(spec_type="draft", quant="Q4_K_M",
|
| 346 |
-
n_layer=4, cache_dtype="f16")
|
| 347 |
bd = draft_bytes(draft=d, target_n_ctx=8192, target_n_embd=4096,
|
| 348 |
target_n_head=32, target_n_head_kv=8)
|
| 349 |
# weights ~0.6 GB for 1B @ Q4_K_M
|
|
@@ -437,14 +439,14 @@ def test_command_preview_none_mode_main_gpu():
|
|
| 437 |
def test_command_preview_draft_and_mmproj():
|
| 438 |
from vramcalc import Inputs, DraftInputs, Mmproj
|
| 439 |
arch = PRESETS["Llama-3 8B"]
|
| 440 |
-
draft = DraftInputs(spec_type="draft", quant="Q4_K_M",
|
| 441 |
params=1_000_000_000, n_layer=4, cache_dtype="f16")
|
| 442 |
mm = Mmproj(filename="mmproj-F16.gguf", enabled=True, offload=True)
|
| 443 |
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 444 |
gpu_vram_gb=[24.0],
|
| 445 |
draft=draft, mmproj=mm))
|
| 446 |
assert "-md draft-Q4_K_M.gguf" in cmd
|
| 447 |
-
assert "--spec-type draft" in cmd
|
| 448 |
assert "--mmproj mmproj-F16.gguf" in cmd
|
| 449 |
|
| 450 |
|
|
@@ -620,4 +622,186 @@ def test_gguf_tensor_section_unreachable_returns_minus_one():
|
|
| 620 |
meta, total = parse_header_with_tensors(buf)
|
| 621 |
assert total == -1
|
| 622 |
arch = metadata_to_arch(meta, total)
|
| 623 |
-
assert arch.params == 0 # negative fallback is treated as no fallback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
compute_scratch_bytes,
|
| 11 |
yarn_effective_context,
|
| 12 |
yarn_warnings,
|
| 13 |
+
yarn_coherence_warnings,
|
| 14 |
+
extension_ratio,
|
| 15 |
auto_configure_yarn,
|
| 16 |
gpu_split,
|
| 17 |
estimate,
|
|
|
|
| 322 |
from vramcalc import Inputs, DraftInputs, Mmproj
|
| 323 |
arch = PRESETS["Llama-3 8B"]
|
| 324 |
base = estimate(arch, Inputs(quant="Q4_K_M", n_ctx=8192, gpu_vram_gb=[24.0]))
|
| 325 |
+
draft = DraftInputs(spec_type="draft-simple", quant="Q4_K_M",
|
| 326 |
params=1_000_000_000, n_layer=4, cache_dtype="f16")
|
| 327 |
mm = Mmproj(enabled=True, bytes_=500 * 1e6)
|
| 328 |
with_extras = estimate(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
|
|
|
| 344 |
|
| 345 |
def test_draft_bytes_basic():
|
| 346 |
from vramcalc import DraftInputs, draft_bytes
|
| 347 |
+
d = DraftInputs(spec_type="draft-simple", quant="Q4_K_M",
|
| 348 |
+
params=1_000_000_000, n_layer=4, cache_dtype="f16")
|
| 349 |
bd = draft_bytes(draft=d, target_n_ctx=8192, target_n_embd=4096,
|
| 350 |
target_n_head=32, target_n_head_kv=8)
|
| 351 |
# weights ~0.6 GB for 1B @ Q4_K_M
|
|
|
|
| 439 |
def test_command_preview_draft_and_mmproj():
|
| 440 |
from vramcalc import Inputs, DraftInputs, Mmproj
|
| 441 |
arch = PRESETS["Llama-3 8B"]
|
| 442 |
+
draft = DraftInputs(spec_type="draft-simple", quant="Q4_K_M",
|
| 443 |
params=1_000_000_000, n_layer=4, cache_dtype="f16")
|
| 444 |
mm = Mmproj(filename="mmproj-F16.gguf", enabled=True, offload=True)
|
| 445 |
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 446 |
gpu_vram_gb=[24.0],
|
| 447 |
draft=draft, mmproj=mm))
|
| 448 |
assert "-md draft-Q4_K_M.gguf" in cmd
|
| 449 |
+
assert "--spec-type draft-simple" in cmd
|
| 450 |
assert "--mmproj mmproj-F16.gguf" in cmd
|
| 451 |
|
| 452 |
|
|
|
|
| 622 |
meta, total = parse_header_with_tensors(buf)
|
| 623 |
assert total == -1
|
| 624 |
arch = metadata_to_arch(meta, total)
|
| 625 |
+
assert arch.params == 0 # negative fallback is treated as no fallback
|
| 626 |
+
|
| 627 |
+
# --- YaRN coherence ladder (warnings only, never a hard block) -----------------
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def test_extension_ratio():
|
| 631 |
+
# no extension -> 1.0
|
| 632 |
+
assert extension_ratio(8192, 8192) == 1.0
|
| 633 |
+
# 4x extension
|
| 634 |
+
assert extension_ratio(8192, 32768) == pytest.approx(4.0)
|
| 635 |
+
# within training context -> 1.0 (no extension counted)
|
| 636 |
+
assert extension_ratio(131072, 8192) == 1.0
|
| 637 |
+
# unknown training ctx -> 1.0 (can't grade)
|
| 638 |
+
assert extension_ratio(0, 32768) == 1.0
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def test_yarn_coherence_no_warning_within_2x():
|
| 642 |
+
# 1.5x: fine, no warning
|
| 643 |
+
assert yarn_coherence_warnings(8192, 12288) == []
|
| 644 |
+
# exactly 2x: fine, no warning (boundary)
|
| 645 |
+
assert yarn_coherence_warnings(8192, 16384) == []
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def test_yarn_coherence_warns_above_2x():
|
| 649 |
+
# 4x: tail degradation warning
|
| 650 |
+
w = yarn_coherence_warnings(8192, 32768)
|
| 651 |
+
assert len(w) == 1
|
| 652 |
+
assert "degradation" in w[0].lower() or "tail" in w[0].lower()
|
| 653 |
+
|
| 654 |
+
|
| 655 |
+
def test_yarn_coherence_warns_at_8x():
|
| 656 |
+
w = yarn_coherence_warnings(8192, 65536) # exactly 8x
|
| 657 |
+
assert len(w) == 1
|
| 658 |
+
assert "incoherent" in w[0].lower() or "not recommended" in w[0].lower()
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
def test_yarn_coherence_monotonic_ladder():
|
| 662 |
+
# more extension => same-or-stronger warning severity (never goes quiet)
|
| 663 |
+
w2x = yarn_coherence_warnings(8192, 16384) # 2x (boundary, fine)
|
| 664 |
+
w6x = yarn_coherence_warnings(8192, 49152) # 6x
|
| 665 |
+
w8x = yarn_coherence_warnings(8192, 65536) # 8x
|
| 666 |
+
w16x = yarn_coherence_warnings(8192, 131072) # 16x
|
| 667 |
+
assert w2x == []
|
| 668 |
+
assert len(w6x) == 1 and "degradation" in w6x[0].lower() or "tail" in w6x[0].lower()
|
| 669 |
+
assert len(w8x) == 1 and "incoherent" in w8x[0].lower()
|
| 670 |
+
assert len(w16x) == 1 and "incoherent" in w16x[0].lower()
|
| 671 |
+
|
| 672 |
+
|
| 673 |
+
def test_yarn_coherence_no_warning_when_within_training():
|
| 674 |
+
assert yarn_coherence_warnings(131072, 8192) == []
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
def test_yarn_coherence_invalid_inputs():
|
| 678 |
+
assert yarn_coherence_warnings(0, 32768) == []
|
| 679 |
+
assert yarn_coherence_warnings(8192, 0) == []
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def test_yarn_warnings_includes_coherence():
|
| 683 |
+
# yarn_warnings should fold in coherence warnings at high extension
|
| 684 |
+
w = yarn_warnings(
|
| 685 |
+
training_ctx=262144, target_ctx=1048576, # 4x
|
| 686 |
+
rope_freq_scale=0.25, yarn_ext_factor=-1.0, yarn_attn_factor=1.0,
|
| 687 |
+
)
|
| 688 |
+
assert any("degradation" in x.lower() or "tail" in x.lower() for x in w)
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
# --- Speculative decoding: real enum + weightless/weighted split ----------------
|
| 692 |
+
|
| 693 |
+
|
| 694 |
+
def test_spec_types_real_enum():
|
| 695 |
+
from vramcalc.draft import SPEC_TYPES, is_weightless, is_weighted
|
| 696 |
+
# invalid legacy value must not be present
|
| 697 |
+
assert "draft" not in SPEC_TYPES
|
| 698 |
+
# real enum members are present
|
| 699 |
+
for t in ("none", "draft-simple", "draft-eagle3", "draft-mtp",
|
| 700 |
+
"draft-dflash", "ngram-simple", "ngram-map-k", "ngram-mod",
|
| 701 |
+
"ngram-cache"):
|
| 702 |
+
assert t in SPEC_TYPES
|
| 703 |
+
# weightless classification
|
| 704 |
+
assert is_weightless("ngram-simple")
|
| 705 |
+
assert not is_weightless("draft-simple")
|
| 706 |
+
assert is_weighted("draft-simple")
|
| 707 |
+
assert not is_weighted("ngram-simple")
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def test_draft_weightless_zero_vram():
|
| 711 |
+
from vramcalc import DraftInputs, draft_bytes
|
| 712 |
+
# ngram spec types: enabled with params=0, zero VRAM
|
| 713 |
+
d = DraftInputs(spec_type="ngram-simple", params=0)
|
| 714 |
+
assert d.enabled is True
|
| 715 |
+
bd = draft_bytes(draft=d, target_n_ctx=8192)
|
| 716 |
+
assert bd.enabled is True
|
| 717 |
+
assert bd.total_bytes == 0.0
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
def test_draft_weighted_requires_params():
|
| 721 |
+
from vramcalc import DraftInputs
|
| 722 |
+
# weighted type with no params -> disabled
|
| 723 |
+
d = DraftInputs(spec_type="draft-simple", params=0)
|
| 724 |
+
assert d.enabled is False
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
def test_command_preview_ngram_no_md_flag():
|
| 728 |
+
# ngram (weightless) emits --spec-type but no -md draft model
|
| 729 |
+
from vramcalc import Inputs, DraftInputs
|
| 730 |
+
arch = PRESETS["Llama-3 8B"]
|
| 731 |
+
d = DraftInputs(spec_type="ngram-simple", params=0, n_max=4)
|
| 732 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 733 |
+
gpu_vram_gb=[24.0], draft=d))
|
| 734 |
+
assert "--spec-type ngram-simple" in cmd
|
| 735 |
+
assert "-md" not in cmd
|
| 736 |
+
assert "--spec-draft-ngl" not in cmd
|
| 737 |
+
|
| 738 |
+
|
| 739 |
+
def test_command_preview_weighted_emits_full_spec_draft_namespace():
|
| 740 |
+
from vramcalc import Inputs, DraftInputs
|
| 741 |
+
arch = PRESETS["Llama-3 8B"]
|
| 742 |
+
d = DraftInputs(spec_type="draft-eagle3", quant="Q4_K_M",
|
| 743 |
+
params=1_000_000_000, n_layer=1, n_max=5, p_min=0.5,
|
| 744 |
+
p_split=0.1)
|
| 745 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 746 |
+
gpu_vram_gb=[24.0], draft=d))
|
| 747 |
+
assert "--spec-type draft-eagle3" in cmd
|
| 748 |
+
assert "-md draft-Q4_K_M.gguf" in cmd
|
| 749 |
+
assert "--spec-draft-ngl 999" in cmd
|
| 750 |
+
assert "--spec-draft-device all" in cmd
|
| 751 |
+
assert "--spec-draft-n-max 5" in cmd
|
| 752 |
+
assert "--spec-draft-p-min 0.5" in cmd
|
| 753 |
+
assert "--spec-draft-p-split 0.1" in cmd
|
| 754 |
+
|
| 755 |
+
|
| 756 |
+
# --- --fit complement path ---------------------------------------------------
|
| 757 |
+
|
| 758 |
+
|
| 759 |
+
def test_command_preview_emits_fit_on():
|
| 760 |
+
from vramcalc import Inputs
|
| 761 |
+
arch = PRESETS["Llama-3 8B"]
|
| 762 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 763 |
+
gpu_vram_gb=[24.0],
|
| 764 |
+
safety_margin_pct=5.0))
|
| 765 |
+
assert "--fit on" in cmd
|
| 766 |
+
# --fit-target derived from 5% of 24GB = ~1228 MiB
|
| 767 |
+
assert "--fit-target" in cmd
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
def test_command_preview_fit_target_scales_with_margin():
|
| 771 |
+
from vramcalc import Inputs
|
| 772 |
+
arch = PRESETS["Llama-3 8B"]
|
| 773 |
+
small = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 774 |
+
gpu_vram_gb=[24.0],
|
| 775 |
+
safety_margin_pct=2.0))
|
| 776 |
+
big = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 777 |
+
gpu_vram_gb=[24.0],
|
| 778 |
+
safety_margin_pct=10.0))
|
| 779 |
+
def ft_target(cmd):
|
| 780 |
+
import re
|
| 781 |
+
m = re.search(r"--fit-target (\d+)", cmd)
|
| 782 |
+
return int(m.group(1)) if m else 0
|
| 783 |
+
assert ft_target(big) > ft_target(small) > 0
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
def test_command_preview_fit_target_uses_smallest_gpu():
|
| 787 |
+
from vramcalc import Inputs
|
| 788 |
+
arch = PRESETS["Llama-3 8B"]
|
| 789 |
+
# mixed GPU: margin should be based on the 8GB device, not the 24GB one
|
| 790 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 791 |
+
gpu_vram_gb=[24.0, 8.0],
|
| 792 |
+
safety_margin_pct=10.0))
|
| 793 |
+
import re
|
| 794 |
+
m = re.search(r"--fit-target (\d+)", cmd)
|
| 795 |
+
assert m
|
| 796 |
+
# 10% of 8GB = ~819 MiB
|
| 797 |
+
assert 800 < int(m.group(1)) < 840
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
def test_command_preview_no_fit_target_when_margin_zero():
|
| 801 |
+
from vramcalc import Inputs
|
| 802 |
+
arch = PRESETS["Llama-3 8B"]
|
| 803 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 804 |
+
gpu_vram_gb=[24.0],
|
| 805 |
+
safety_margin_pct=0.0))
|
| 806 |
+
assert "--fit on" in cmd
|
| 807 |
+
assert "--fit-target" not in cmd
|
|
@@ -5,7 +5,14 @@ Pure calculation logic, importable without Gradio so it can be unit-tested.
|
|
| 5 |
|
| 6 |
from .quant import QUANT_BPW, weight_bytes, quant_from_filename
|
| 7 |
from .kv import kv_cache_bytes, cache_dtype_bytes, compute_scratch_bytes
|
| 8 |
-
from .yarn import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
from .gpu import GpuBudget, GpuSpec, gpu_split, fit_gpus, GpuSplitResult
|
| 10 |
from .gguf import (
|
| 11 |
GGUFMetadata,
|
|
@@ -36,6 +43,8 @@ __all__ = [
|
|
| 36 |
"compute_scratch_bytes",
|
| 37 |
"yarn_effective_context",
|
| 38 |
"yarn_warnings",
|
|
|
|
|
|
|
| 39 |
"YarnConfig",
|
| 40 |
"auto_configure_yarn",
|
| 41 |
"GpuBudget",
|
|
|
|
| 5 |
|
| 6 |
from .quant import QUANT_BPW, weight_bytes, quant_from_filename
|
| 7 |
from .kv import kv_cache_bytes, cache_dtype_bytes, compute_scratch_bytes
|
| 8 |
+
from .yarn import (
|
| 9 |
+
yarn_effective_context,
|
| 10 |
+
yarn_warnings,
|
| 11 |
+
yarn_coherence_warnings,
|
| 12 |
+
extension_ratio,
|
| 13 |
+
YarnConfig,
|
| 14 |
+
auto_configure_yarn,
|
| 15 |
+
)
|
| 16 |
from .gpu import GpuBudget, GpuSpec, gpu_split, fit_gpus, GpuSplitResult
|
| 17 |
from .gguf import (
|
| 18 |
GGUFMetadata,
|
|
|
|
| 43 |
"compute_scratch_bytes",
|
| 44 |
"yarn_effective_context",
|
| 45 |
"yarn_warnings",
|
| 46 |
+
"yarn_coherence_warnings",
|
| 47 |
+
"extension_ratio",
|
| 48 |
"YarnConfig",
|
| 49 |
"auto_configure_yarn",
|
| 50 |
"GpuBudget",
|
|
@@ -21,7 +21,45 @@ from .quant import weight_bytes, QUANT_BPW
|
|
| 21 |
from .kv import kv_cache_bytes, cache_dtype_bytes
|
| 22 |
|
| 23 |
|
| 24 |
-
SPEC_TYPES = (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
@dataclass
|
|
@@ -38,7 +76,13 @@ class DraftInputs:
|
|
| 38 |
|
| 39 |
@property
|
| 40 |
def enabled(self) -> bool:
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
@dataclass
|
|
@@ -66,6 +110,10 @@ def draft_bytes(
|
|
| 66 |
if not draft.enabled:
|
| 67 |
return DraftBreakdown(enabled=False)
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
w = weight_bytes(draft.params, draft.quant)
|
| 70 |
|
| 71 |
n_embd = draft.n_embd if getattr(draft, "n_embd", 0) else target_n_embd
|
|
|
|
| 21 |
from .kv import kv_cache_bytes, cache_dtype_bytes
|
| 22 |
|
| 23 |
|
| 24 |
+
SPEC_TYPES = (
|
| 25 |
+
"none",
|
| 26 |
+
# weighted (draft model: weights + own KV)
|
| 27 |
+
"draft-simple",
|
| 28 |
+
"draft-eagle3",
|
| 29 |
+
"draft-mtp",
|
| 30 |
+
"draft-dflash",
|
| 31 |
+
# weightless (statistical / n-gram; no draft model, ~zero extra VRAM)
|
| 32 |
+
"ngram-simple",
|
| 33 |
+
"ngram-map-k",
|
| 34 |
+
"ngram-map-k4v",
|
| 35 |
+
"ngram-mod",
|
| 36 |
+
"ngram-cache",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Spec types that need no draft model — pure statistical speculation. These
|
| 40 |
+
# have ~zero extra VRAM cost (no draft weights, no draft KV), so they are
|
| 41 |
+
# auto-offered when headroom exists without an "acknowledge the tradeoff"
|
| 42 |
+
# gate. See PLAN.md "Speculative decoding".
|
| 43 |
+
WEIGHTLESS_SPEC_TYPES = (
|
| 44 |
+
"ngram-simple",
|
| 45 |
+
"ngram-map-k",
|
| 46 |
+
"ngram-map-k4v",
|
| 47 |
+
"ngram-mod",
|
| 48 |
+
"ngram-cache",
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Spec types backed by a draft model (-md) with its own weights + KV.
|
| 52 |
+
WEIGHTED_SPEC_TYPES = ("draft-simple", "draft-eagle3", "draft-mtp", "draft-dflash")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def is_weightless(spec_type: str) -> bool:
|
| 56 |
+
"""True if this spec type needs no draft model (n-gram family)."""
|
| 57 |
+
return spec_type in WEIGHTLESS_SPEC_TYPES
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def is_weighted(spec_type: str) -> bool:
|
| 61 |
+
"""True if this spec type needs a draft model (-md)."""
|
| 62 |
+
return spec_type in WEIGHTED_SPEC_TYPES
|
| 63 |
|
| 64 |
|
| 65 |
@dataclass
|
|
|
|
| 76 |
|
| 77 |
@property
|
| 78 |
def enabled(self) -> bool:
|
| 79 |
+
# Weightless (n-gram) spec types need no draft model: enabled as long
|
| 80 |
+
# as a type is chosen. Weighted types need params > 0.
|
| 81 |
+
if self.spec_type == "none":
|
| 82 |
+
return False
|
| 83 |
+
if is_weightless(self.spec_type):
|
| 84 |
+
return True
|
| 85 |
+
return self.params > 0
|
| 86 |
|
| 87 |
|
| 88 |
@dataclass
|
|
|
|
| 110 |
if not draft.enabled:
|
| 111 |
return DraftBreakdown(enabled=False)
|
| 112 |
|
| 113 |
+
# Weightless spec types (n-gram family) have no draft model: zero VRAM.
|
| 114 |
+
if is_weightless(draft.spec_type):
|
| 115 |
+
return DraftBreakdown(enabled=True)
|
| 116 |
+
|
| 117 |
w = weight_bytes(draft.params, draft.quant)
|
| 118 |
|
| 119 |
n_embd = draft.n_embd if getattr(draft, "n_embd", 0) else target_n_embd
|
|
@@ -59,6 +59,11 @@ QUANT_BPW: dict[str, float] = {
|
|
| 59 |
# ciru-ai/ROCmFPX runner; see command_preview for the warning.
|
| 60 |
"ROCmFP4": 4.34, # 27B Strix Lean: 14.82 GB / 27.32B params
|
| 61 |
"ROCmFPX": 7.08, # 35B A3B MoEQuality: model card states 7.08 BPW
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
|
|
|
|
| 59 |
# ciru-ai/ROCmFPX runner; see command_preview for the warning.
|
| 60 |
"ROCmFP4": 4.34, # 27B Strix Lean: 14.82 GB / 27.32B params
|
| 61 |
"ROCmFPX": 7.08, # 35B A3B MoEQuality: model card states 7.08 BPW
|
| 62 |
+
# nvfp4 (NVIDIA FP4, llama.cpp PR #22196): NOT yet in this table. The
|
| 63 |
+
# bits-per-weight is unverified from here, so it cannot be selected as a
|
| 64 |
+
# floor quant. Add it only once a verified effective bpw is sourced; see
|
| 65 |
+
# PLAN.md "Quarantined unverified data".
|
| 66 |
+
# "nvfp4": 4.0, # VERIFY before ship
|
| 67 |
}
|
| 68 |
|
| 69 |
|
|
@@ -220,6 +220,22 @@ def _gpu_specs(inp: Inputs) -> list[GpuSpec]:
|
|
| 220 |
]
|
| 221 |
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
def format_bytes(n: float) -> str:
|
| 224 |
"""Human-readable byte size."""
|
| 225 |
n = float(n)
|
|
@@ -253,6 +269,16 @@ def command_preview(arch: ModelArch, inp: Inputs) -> str:
|
|
| 253 |
if inp.flash_attn:
|
| 254 |
parts.append("--flash-attn")
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
# Multi-GPU: emit --split-mode / --main-gpu / --tensor-split only when the
|
| 257 |
# user has more than one GPU or chose a non-default mode.
|
| 258 |
multi = len(inp.gpu_vram_gb) > 1
|
|
@@ -272,17 +298,25 @@ def command_preview(arch: ModelArch, inp: Inputs) -> str:
|
|
| 272 |
f"# MTP draft KV dtype: {inp.mtp_cache_dtype} (target: {inp.cache_dtype})"
|
| 273 |
)
|
| 274 |
|
| 275 |
-
# Speculative decoding
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
if inp.draft is not None and inp.draft.enabled:
|
| 277 |
d = inp.draft
|
| 278 |
-
parts.append(f"-md draft-{d.quant}.gguf")
|
| 279 |
parts.append(f"--spec-type {d.spec_type}")
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
parts.append(
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
# Multimodal projector
|
| 288 |
if inp.mmproj is not None and inp.mmproj.enabled:
|
|
|
|
| 220 |
]
|
| 221 |
|
| 222 |
|
| 223 |
+
def _fit_target_mib(inp: Inputs) -> int:
|
| 224 |
+
"""Per-device --fit-target margin in MiB, from safety_margin_pct.
|
| 225 |
+
|
| 226 |
+
--fit-target is a *per-device* MiB reserve. We derive one from the safety
|
| 227 |
+
margin as a share of the smallest configured GPU's VRAM (the tightest
|
| 228 |
+
device is what binds). Returns 0 to omit the flag (margin disabled).
|
| 229 |
+
"""
|
| 230 |
+
if not inp.gpu_vram_gb:
|
| 231 |
+
return 0
|
| 232 |
+
if inp.safety_margin_pct <= 0:
|
| 233 |
+
return 0
|
| 234 |
+
smallest_gb = min(inp.gpu_vram_gb)
|
| 235 |
+
margin_bytes = smallest_gb * (1 << 30) * (inp.safety_margin_pct / 100.0)
|
| 236 |
+
return int(round(margin_bytes / (1 << 20)))
|
| 237 |
+
|
| 238 |
+
|
| 239 |
def format_bytes(n: float) -> str:
|
| 240 |
"""Human-readable byte size."""
|
| 241 |
n = float(n)
|
|
|
|
| 269 |
if inp.flash_attn:
|
| 270 |
parts.append("--flash-attn")
|
| 271 |
|
| 272 |
+
# Complement path: we emit fully explicit flags, then --fit on with a
|
| 273 |
+
# per-device margin target so runtime --fit is a no-op safety net that
|
| 274 |
+
# can only *reduce* usage, never exceed our estimate. The margin is the
|
| 275 |
+
# user's "how scared am I" knob (safety_margin_pct). We never claim a
|
| 276 |
+
# guarantee; the margin is the honest version.
|
| 277 |
+
parts.append("--fit on")
|
| 278 |
+
margin_mib = _fit_target_mib(inp)
|
| 279 |
+
if margin_mib:
|
| 280 |
+
parts.append(f"--fit-target {margin_mib}")
|
| 281 |
+
|
| 282 |
# Multi-GPU: emit --split-mode / --main-gpu / --tensor-split only when the
|
| 283 |
# user has more than one GPU or chose a non-default mode.
|
| 284 |
multi = len(inp.gpu_vram_gb) > 1
|
|
|
|
| 298 |
f"# MTP draft KV dtype: {inp.mtp_cache_dtype} (target: {inp.cache_dtype})"
|
| 299 |
)
|
| 300 |
|
| 301 |
+
# Speculative decoding. Two branches:
|
| 302 |
+
# - weightless (n-gram family): no draft model, ~zero extra VRAM. Emit
|
| 303 |
+
# only --spec-type (no -md).
|
| 304 |
+
# - weighted (draft-*): a draft model with weights + KV. Emit -md plus
|
| 305 |
+
# the full --spec-draft-* namespace, not the old -md-only stub.
|
| 306 |
if inp.draft is not None and inp.draft.enabled:
|
| 307 |
d = inp.draft
|
|
|
|
| 308 |
parts.append(f"--spec-type {d.spec_type}")
|
| 309 |
+
from .draft import is_weightless
|
| 310 |
+
if not is_weightless(d.spec_type):
|
| 311 |
+
parts.append(f"-md draft-{d.quant}.gguf")
|
| 312 |
+
parts.append("--spec-draft-ngl 999") # full draft offload
|
| 313 |
+
parts.append("--spec-draft-device all")
|
| 314 |
+
if d.n_max:
|
| 315 |
+
parts.append(f"--spec-draft-n-max {d.n_max}")
|
| 316 |
+
if d.p_min:
|
| 317 |
+
parts.append(f"--spec-draft-p-min {d.p_min}")
|
| 318 |
+
if d.p_split:
|
| 319 |
+
parts.append(f"--spec-draft-p-split {d.p_split}")
|
| 320 |
|
| 321 |
# Multimodal projector
|
| 322 |
if inp.mmproj is not None and inp.mmproj.enabled:
|
|
@@ -110,4 +110,58 @@ def yarn_warnings(
|
|
| 110 |
"normal long-context YaRN (lower only for high-context YaRN fine-tunes)."
|
| 111 |
)
|
| 112 |
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
"normal long-context YaRN (lower only for high-context YaRN fine-tunes)."
|
| 111 |
)
|
| 112 |
|
| 113 |
+
warns.extend(yarn_coherence_warnings(training_ctx, target_ctx))
|
| 114 |
+
|
| 115 |
+
return warns
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# Extension-ratio thresholds for the coherence ladder. These are warnings
|
| 119 |
+
# only — never a hard block — and come from YaRN-paper behavior and community
|
| 120 |
+
# experience, not measurement. The ladder is monotonic: more extension means
|
| 121 |
+
# same-or-more concern. See PLAN.md "YaRN coherence ladder".
|
| 122 |
+
_YARN_FINE_RATIO = 2.0 # <= 2x: no warning
|
| 123 |
+
_YARN_BAD_RATIO = 8.0 # > 2x..8x: tail degradation; >= 8x: incoherent
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def extension_ratio(training_ctx: int, target_ctx: int) -> float:
|
| 127 |
+
"""How far the target context stretches beyond the training context.
|
| 128 |
+
|
| 129 |
+
Returns 1.0 when target <= training (no extension) or when training_ctx
|
| 130 |
+
is unknown/invalid.
|
| 131 |
+
"""
|
| 132 |
+
if not training_ctx or training_ctx <= 0:
|
| 133 |
+
return 1.0
|
| 134 |
+
if target_ctx <= 0:
|
| 135 |
+
return 1.0
|
| 136 |
+
if target_ctx <= training_ctx:
|
| 137 |
+
return 1.0
|
| 138 |
+
return target_ctx / training_ctx
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def yarn_coherence_warnings(training_ctx: int, target_ctx: int) -> list[str]:
|
| 142 |
+
"""Warn about coherence risk when extending context beyond training.
|
| 143 |
+
|
| 144 |
+
These are heuristic warnings, not guarantees. The ladder is monotonic:
|
| 145 |
+
<= 2x : fine, no warning
|
| 146 |
+
> 2x..8x : expect some degradation at the tail of the context window
|
| 147 |
+
>= 8x : likely incoherent at long range; not recommended
|
| 148 |
+
"""
|
| 149 |
+
if training_ctx <= 0 or target_ctx <= 0:
|
| 150 |
+
return []
|
| 151 |
+
if target_ctx <= training_ctx:
|
| 152 |
+
return []
|
| 153 |
+
ratio = target_ctx / training_ctx
|
| 154 |
+
w: list[str] = []
|
| 155 |
+
if ratio >= _YARN_BAD_RATIO:
|
| 156 |
+
w.append(
|
| 157 |
+
f"Target context {target_ctx} is {ratio:.1f}x the training "
|
| 158 |
+
f"context {training_ctx}. Likely incoherent at long range; "
|
| 159 |
+
f"not recommended."
|
| 160 |
+
)
|
| 161 |
+
elif ratio > _YARN_FINE_RATIO:
|
| 162 |
+
w.append(
|
| 163 |
+
f"Target context {target_ctx} is {ratio:.1f}x the training "
|
| 164 |
+
f"context {training_ctx}. Expect some quality degradation at "
|
| 165 |
+
f"the tail of the context window."
|
| 166 |
+
)
|
| 167 |
+
return w
|