Spaces:
Running on Zero
Running on Zero
Add llama.cpp VRAM calculator app and core logic
Browse filesGradio UI (app.py) plus the pure vramcalc package: quant BPW table,
KV cache + compute-scratch math, multi-GPU proportional split, YaRN
context-extension checks, minimal GGUF v3 header parser (HF range-read),
model presets, and 23 unit tests.
Co-Authored-By: Claude <noreply@anthropic.com>
- .gitignore +21 -0
- app.py +377 -0
- requirements.txt +3 -0
- tests/test_vramcalc.py +259 -0
- vramcalc/__init__.py +50 -0
- vramcalc/gguf.py +218 -0
- vramcalc/gpu.py +104 -0
- vramcalc/kv.py +106 -0
- vramcalc/presets.py +62 -0
- vramcalc/quant.py +86 -0
- vramcalc/report.py +199 -0
- vramcalc/yarn.py +59 -0
.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.egg-info/
|
| 5 |
+
.eggs/
|
| 6 |
+
build/
|
| 7 |
+
dist/
|
| 8 |
+
|
| 9 |
+
# Test / tooling caches
|
| 10 |
+
.pytest_cache/
|
| 11 |
+
.coverage
|
| 12 |
+
htmlcov/
|
| 13 |
+
|
| 14 |
+
# venv
|
| 15 |
+
.venv/
|
| 16 |
+
venv/
|
| 17 |
+
|
| 18 |
+
# OS / editor
|
| 19 |
+
.DS_Store
|
| 20 |
+
.idea/
|
| 21 |
+
.vscode/
|
app.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""llama.cpp VRAM Calculator — Hugging Face Space.
|
| 2 |
+
|
| 3 |
+
A Gradio app that estimates VRAM usage for a Hugging Face GGUF model given
|
| 4 |
+
quantization type, context length, KV cache options, YaRN context-extension
|
| 5 |
+
parameters, MTP heads, and a multi-GPU budget. Architecture is auto-fetched
|
| 6 |
+
from the GGUF header (range-read, no full download) with a manual-override
|
| 7 |
+
tab and presets for offline use.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
from huggingface_hub import HfApi
|
| 14 |
+
|
| 15 |
+
from vramcalc import (
|
| 16 |
+
QUANT_BPW,
|
| 17 |
+
ModelArch,
|
| 18 |
+
Inputs,
|
| 19 |
+
estimate,
|
| 20 |
+
command_preview,
|
| 21 |
+
format_bytes,
|
| 22 |
+
quant_from_filename,
|
| 23 |
+
parse_hf_range,
|
| 24 |
+
)
|
| 25 |
+
from vramcalc.presets import PRESETS, PRESET_NAMES
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
QUANT_CHOICES = list(QUANT_BPW.keys())
|
| 29 |
+
CACHE_DTYPES = ["f16", "bf16", "f32", "q8_0", "q8_1", "q4_0", "q4_1", "q5_0", "q5_1"]
|
| 30 |
+
COMPUTE_DTYPES = ["f16", "bf16", "f32"]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def list_gguf_files(repo_id: str, hf_token: str):
|
| 34 |
+
"""List *.gguf files in a repo and auto-detect their quant names."""
|
| 35 |
+
if not repo_id or not repo_id.strip():
|
| 36 |
+
return [], "Enter a Hugging Face repo id."
|
| 37 |
+
try:
|
| 38 |
+
api = HfApi(token=hf_token or None)
|
| 39 |
+
files = api.list_repo_files(repo_id=repo_id.strip())
|
| 40 |
+
except Exception as e: # noqa: BLE001
|
| 41 |
+
return [], f"Error listing repo: {e}"
|
| 42 |
+
ggufs = sorted(f for f in files if f.lower().endswith(".gguf"))
|
| 43 |
+
if not ggufs:
|
| 44 |
+
return [], f"No .gguf files found in {repo_id!r}."
|
| 45 |
+
choices = []
|
| 46 |
+
for f in ggufs:
|
| 47 |
+
q = quant_from_filename(f)
|
| 48 |
+
label = f"{f}" + (f" [{q}]" if q else "")
|
| 49 |
+
choices.append((label, f))
|
| 50 |
+
return choices, f"Found {len(ggufs)} GGUF file(s)."
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def fetch_arch(repo_id: str, filename: str, hf_token: str):
|
| 54 |
+
"""Range-read a GGUF header from HF and return editable arch fields."""
|
| 55 |
+
if not repo_id or not filename:
|
| 56 |
+
return _empty_arch_fields(), "Pick a GGUF file first."
|
| 57 |
+
try:
|
| 58 |
+
meta = parse_hf_range(
|
| 59 |
+
repo_id.strip(), filename, token=hf_token or None
|
| 60 |
+
)
|
| 61 |
+
except Exception as e: # noqa: BLE001
|
| 62 |
+
return _empty_arch_fields(), f"Error reading GGUF header: {e}"
|
| 63 |
+
if not meta.n_layer:
|
| 64 |
+
return _arch_to_fields(meta), (
|
| 65 |
+
"Parsed header but architecture fields look empty; "
|
| 66 |
+
"edit them manually below."
|
| 67 |
+
)
|
| 68 |
+
return _arch_to_fields(meta), (
|
| 69 |
+
f"Fetched {meta.architecture or 'model'}: "
|
| 70 |
+
f"{meta.n_layer} layers, {meta.n_embd} embd, "
|
| 71 |
+
f"{meta.n_head}/{meta.n_head_kv} heads, ctx {meta.training_ctx}, "
|
| 72 |
+
f"params {meta.params or 'n/a'}."
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _empty_arch_fields():
|
| 77 |
+
return _arch_to_fields(ModelArch())
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _arch_to_fields(m: ModelArch):
|
| 81 |
+
return [
|
| 82 |
+
m.name, m.architecture, m.n_layer, m.n_embd, m.n_head, m.n_head_kv,
|
| 83 |
+
m.training_ctx, m.params, m.rope_freq_base, m.n_expert,
|
| 84 |
+
m.n_expert_used, m.n_mtp,
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
ARCH_FIELD_NAMES = [
|
| 89 |
+
"name", "architecture", "n_layer", "n_embd", "n_head", "n_head_kv",
|
| 90 |
+
"training_ctx", "params", "rope_freq_base", "n_expert",
|
| 91 |
+
"n_expert_used", "n_mtp",
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _fields_to_arch(fields) -> ModelArch:
|
| 96 |
+
values = {k: v for k, v in zip(ARCH_FIELD_NAMES, fields)}
|
| 97 |
+
return ModelArch(
|
| 98 |
+
name=str(values["name"] or ""),
|
| 99 |
+
architecture=str(values["architecture"] or ""),
|
| 100 |
+
n_layer=int(values["n_layer"] or 0),
|
| 101 |
+
n_embd=int(values["n_embd"] or 0),
|
| 102 |
+
n_head=int(values["n_head"] or 0),
|
| 103 |
+
n_head_kv=int(values["n_head_kv"] or 0),
|
| 104 |
+
training_ctx=int(values["training_ctx"] or 0),
|
| 105 |
+
params=int(values["params"] or 0),
|
| 106 |
+
rope_freq_base=float(values["rope_freq_base"] or 10000.0),
|
| 107 |
+
n_expert=int(values["n_expert"] or 0),
|
| 108 |
+
n_expert_used=int(values["n_expert_used"] or 0),
|
| 109 |
+
n_mtp=int(values["n_mtp"] or 0),
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def load_preset(name: str):
|
| 114 |
+
if name and name in PRESETS:
|
| 115 |
+
return _arch_to_fields(PRESETS[name]), f"Loaded preset: {name}"
|
| 116 |
+
return _empty_arch_fields(), ""
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _parse_gpu_list(text: str) -> list[float]:
|
| 120 |
+
out = []
|
| 121 |
+
for tok in (text or "").replace(";", ",").split(","):
|
| 122 |
+
tok = tok.strip()
|
| 123 |
+
if tok:
|
| 124 |
+
try:
|
| 125 |
+
out.append(float(tok))
|
| 126 |
+
except ValueError:
|
| 127 |
+
pass
|
| 128 |
+
return out or [24.0]
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def compute(
|
| 132 |
+
arch_fields,
|
| 133 |
+
quant, n_ctx, cache_dtype, flash_attn, compute_dtype,
|
| 134 |
+
n_batch, n_prompt,
|
| 135 |
+
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 136 |
+
yarn_beta_fast, yarn_beta_slow,
|
| 137 |
+
gpu_vram_text, kv_on_largest, safety_margin,
|
| 138 |
+
):
|
| 139 |
+
arch = _fields_to_arch(arch_fields)
|
| 140 |
+
gpus = _parse_gpu_list(gpu_vram_text)
|
| 141 |
+
inp = Inputs(
|
| 142 |
+
quant=quant,
|
| 143 |
+
n_ctx=int(n_ctx),
|
| 144 |
+
cache_dtype=cache_dtype,
|
| 145 |
+
flash_attn=bool(flash_attn),
|
| 146 |
+
compute_dtype=compute_dtype,
|
| 147 |
+
n_batch=int(n_batch),
|
| 148 |
+
n_prompt=int(n_prompt),
|
| 149 |
+
rope_freq_scale=float(rope_freq_scale),
|
| 150 |
+
yarn_ext_factor=float(yarn_ext_factor),
|
| 151 |
+
yarn_attn_factor=float(yarn_attn_factor),
|
| 152 |
+
yarn_beta_fast=float(yarn_beta_fast),
|
| 153 |
+
yarn_beta_slow=float(yarn_beta_slow),
|
| 154 |
+
gpu_vram_gb=gpus,
|
| 155 |
+
kv_on_largest=bool(kv_on_largest),
|
| 156 |
+
safety_margin_pct=float(safety_margin),
|
| 157 |
+
)
|
| 158 |
+
if arch.n_layer <= 0 or arch.n_embd <= 0 or arch.params <= 0:
|
| 159 |
+
return (
|
| 160 |
+
"⚠️ Model architecture is incomplete. Fill n_layer, n_embd, and "
|
| 161 |
+
"params (or fetch from a GGUF / load a preset).",
|
| 162 |
+
"", "",
|
| 163 |
+
)
|
| 164 |
+
bd = estimate(arch, inp)
|
| 165 |
+
|
| 166 |
+
# breakdown table
|
| 167 |
+
rows = [
|
| 168 |
+
["Weights (GGUF, " + quant + ")", format_bytes(bd.weights_bytes)],
|
| 169 |
+
["KV cache (" + cache_dtype + ")", format_bytes(bd.kv_cache_bytes)],
|
| 170 |
+
["Compute / scratch", format_bytes(bd.compute_scratch_bytes)],
|
| 171 |
+
["MTP overhead", format_bytes(bd.mtp_overhead_bytes)],
|
| 172 |
+
["GGUF header / overhead", format_bytes(bd.gguf_overhead_bytes)],
|
| 173 |
+
["Safety margin (" + str(inp.safety_margin_pct) + "%)",
|
| 174 |
+
format_bytes(bd.safety_margin_bytes)],
|
| 175 |
+
["**Total**", f"**{format_bytes(bd.total_bytes)}**"],
|
| 176 |
+
]
|
| 177 |
+
breakdown_md = "| Component | Size |\n|---|---|\n" + "\n".join(
|
| 178 |
+
f"| {a} | {b} |" for a, b in rows
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# warnings
|
| 182 |
+
warn_md = ""
|
| 183 |
+
if bd.warnings:
|
| 184 |
+
warn_md = "\n\n**⚠️ YaRN / context notes:**\n" + "\n".join(
|
| 185 |
+
f"- {w}" for w in bd.warnings
|
| 186 |
+
)
|
| 187 |
+
eff_md = (
|
| 188 |
+
f"\n\nEffective context (training_ctx / rope_freq_scale): "
|
| 189 |
+
f"**{bd.effective_context}**"
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# per-GPU table
|
| 193 |
+
gpu_md = ""
|
| 194 |
+
if bd.gpu and bd.gpu.assignments:
|
| 195 |
+
a = bd.gpu.assignments
|
| 196 |
+
header = "| GPU | VRAM | Weights | KV+Compute | Used | Free | Fits? |"
|
| 197 |
+
sep = "|---|---|---|---|---|---|---|"
|
| 198 |
+
body = []
|
| 199 |
+
for g in a:
|
| 200 |
+
kv = format_bytes(g.kv_compute_bytes) if g.kv_compute_bytes else "—"
|
| 201 |
+
badge = "✅" if g.fits else "❌"
|
| 202 |
+
body.append(
|
| 203 |
+
f"| {g.index} | {format_bytes(g.vram_bytes)} | "
|
| 204 |
+
f"{format_bytes(g.weight_bytes)} | {kv} | "
|
| 205 |
+
f"{format_bytes(g.used_bytes)} | {format_bytes(g.free_bytes)} "
|
| 206 |
+
f"| {badge} |"
|
| 207 |
+
)
|
| 208 |
+
total_badge = "✅ all fit" if bd.gpu.all_fit else "❌ over budget"
|
| 209 |
+
gpu_md = (
|
| 210 |
+
"**Per-GPU split (estimate, proportional weight split):**\n\n"
|
| 211 |
+
+ header + "\n" + sep + "\n" + "\n".join(body)
|
| 212 |
+
+ f"\n\nTotal VRAM: {format_bytes(bd.gpu.total_vram_bytes)} · "
|
| 213 |
+
f"Total used: {format_bytes(bd.gpu.total_used_bytes)} · "
|
| 214 |
+
f"{total_badge} · KV on GPU {bd.gpu.kv_gpu_index}"
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
cmd = command_preview(arch, inp)
|
| 218 |
+
summary = (
|
| 219 |
+
f"**{arch.name or arch.architecture or 'Model'}** @ {quant}, "
|
| 220 |
+
f"ctx {inp.n_ctx} ({cache_dtype} KV"
|
| 221 |
+
+ (", FA" if inp.flash_attn else ", no FA")
|
| 222 |
+
+ f"), {len(gpus)} GPU(s) → "
|
| 223 |
+
f"**{format_bytes(bd.total_bytes)}** total"
|
| 224 |
+
)
|
| 225 |
+
return summary + "\n\n" + breakdown_md + eff_md + warn_md + "\n\n" + gpu_md, cmd, cmd
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def build_ui():
|
| 229 |
+
with gr.Blocks(title="llama.cpp VRAM Calculator") as demo:
|
| 230 |
+
gr.Markdown(
|
| 231 |
+
"# 🦀 llama.cpp VRAM Calculator\n"
|
| 232 |
+
"Estimate VRAM for a Hugging Face GGUF model: quant size, "
|
| 233 |
+
"context, KV cache options, YaRN context extension, MTP heads, "
|
| 234 |
+
"and multi-GPU split. Architecture is auto-fetched from the GGUF "
|
| 235 |
+
"header (range-read — no full model download)."
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
arch_state = gr.State(_empty_arch_fields())
|
| 239 |
+
|
| 240 |
+
with gr.Row():
|
| 241 |
+
with gr.Column(scale=1):
|
| 242 |
+
gr.Markdown("### 1. Model source")
|
| 243 |
+
with gr.Tab("Auto-fetch from HF"):
|
| 244 |
+
repo_id = gr.Textbox(
|
| 245 |
+
label="HF repo id",
|
| 246 |
+
placeholder="e.g. bartowski/Llama-3-8B-Instruct-GGUF",
|
| 247 |
+
)
|
| 248 |
+
hf_token = gr.Textbox(
|
| 249 |
+
label="HF token (optional, for gated/private repos)",
|
| 250 |
+
type="password",
|
| 251 |
+
)
|
| 252 |
+
list_btn = gr.Button("List GGUF files")
|
| 253 |
+
file_picker = gr.Dropdown(
|
| 254 |
+
label="GGUF file", choices=[], interactive=True
|
| 255 |
+
)
|
| 256 |
+
fetch_btn = gr.Button("Fetch architecture from GGUF header")
|
| 257 |
+
fetch_status = gr.Markdown("")
|
| 258 |
+
|
| 259 |
+
with gr.Tab("Presets / manual"):
|
| 260 |
+
preset_dd = gr.Dropdown(
|
| 261 |
+
label="Quick preset", choices=PRESET_NAMES, interactive=True
|
| 262 |
+
)
|
| 263 |
+
load_preset_btn = gr.Button("Load preset")
|
| 264 |
+
|
| 265 |
+
gr.Markdown("### Architecture (editable)")
|
| 266 |
+
arch_inputs = [
|
| 267 |
+
gr.Textbox(label="name", value=""),
|
| 268 |
+
gr.Textbox(label="architecture", value=""),
|
| 269 |
+
gr.Number(label="n_layer", value=0, precision=0),
|
| 270 |
+
gr.Number(label="n_embd", value=0, precision=0),
|
| 271 |
+
gr.Number(label="n_head", value=0, precision=0),
|
| 272 |
+
gr.Number(label="n_head_kv", value=0, precision=0),
|
| 273 |
+
gr.Number(label="training_ctx", value=0, precision=0),
|
| 274 |
+
gr.Number(label="params", value=0, precision=0),
|
| 275 |
+
gr.Number(label="rope_freq_base", value=10000.0),
|
| 276 |
+
gr.Number(label="n_expert (MoE)", value=0, precision=0),
|
| 277 |
+
gr.Number(label="n_expert_used", value=0, precision=0),
|
| 278 |
+
gr.Number(label="n_mtp", value=0, precision=0),
|
| 279 |
+
]
|
| 280 |
+
|
| 281 |
+
with gr.Column(scale=1):
|
| 282 |
+
gr.Markdown("### 2. Inference options")
|
| 283 |
+
quant = gr.Dropdown(
|
| 284 |
+
label="Quantization", choices=QUANT_CHOICES, value="Q4_K_M"
|
| 285 |
+
)
|
| 286 |
+
n_ctx = gr.Number(label="Target context (n_ctx)", value=8192, precision=0)
|
| 287 |
+
with gr.Row():
|
| 288 |
+
cache_dtype = gr.Dropdown(
|
| 289 |
+
label="KV cache dtype", choices=CACHE_DTYPES, value="f16"
|
| 290 |
+
)
|
| 291 |
+
compute_dtype = gr.Dropdown(
|
| 292 |
+
label="Compute dtype", choices=COMPUTE_DTYPES, value="f16"
|
| 293 |
+
)
|
| 294 |
+
flash_attn = gr.Checkbox(label="Flash attention", value=True)
|
| 295 |
+
with gr.Row():
|
| 296 |
+
n_batch = gr.Number(label="n_batch", value=512, precision=0)
|
| 297 |
+
n_prompt = gr.Number(label="n_prompt (active)", value=0, precision=0)
|
| 298 |
+
|
| 299 |
+
gr.Markdown("### YaRN / RoPE context extension")
|
| 300 |
+
with gr.Row():
|
| 301 |
+
rope_freq_scale = gr.Number(label="rope_freq_scale", value=1.0)
|
| 302 |
+
yarn_ext_factor = gr.Number(label="yarn_ext_factor", value=-1.0)
|
| 303 |
+
yarn_attn_factor = gr.Number(label="yarn_attn_factor", value=1.0)
|
| 304 |
+
with gr.Row():
|
| 305 |
+
yarn_beta_fast = gr.Number(label="yarn_beta_fast", value=32.0)
|
| 306 |
+
yarn_beta_slow = gr.Number(label="yarn_beta_slow", value=1.0)
|
| 307 |
+
|
| 308 |
+
gr.Markdown("### 3. Multi-GPU budget")
|
| 309 |
+
gpu_vram_text = gr.Textbox(
|
| 310 |
+
label="Per-GPU VRAM (GB, comma-separated)",
|
| 311 |
+
value="24",
|
| 312 |
+
placeholder="e.g. 24,24,16",
|
| 313 |
+
)
|
| 314 |
+
with gr.Row():
|
| 315 |
+
kv_on_largest = gr.Checkbox(
|
| 316 |
+
label="Place KV cache on largest GPU", value=False
|
| 317 |
+
)
|
| 318 |
+
safety_margin = gr.Number(label="Safety margin %", value=5.0)
|
| 319 |
+
|
| 320 |
+
compute_btn = gr.Button("Compute VRAM", variant="primary")
|
| 321 |
+
|
| 322 |
+
gr.Markdown("### Results")
|
| 323 |
+
result_md = gr.Markdown("")
|
| 324 |
+
with gr.Accordion("llama.cpp launch command preview", open=False):
|
| 325 |
+
cmd_md = gr.Markdown("")
|
| 326 |
+
cmd_text = gr.Textbox(
|
| 327 |
+
label="Command (copyable)", lines=8, interactive=False
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
# --- wiring ---
|
| 331 |
+
def _store_arch(*fields):
|
| 332 |
+
return list(fields)
|
| 333 |
+
|
| 334 |
+
arch_inputs_and_state = [*arch_inputs, arch_state]
|
| 335 |
+
# keep arch_state synced whenever arch fields change
|
| 336 |
+
for comp in arch_inputs:
|
| 337 |
+
comp.change(
|
| 338 |
+
fn=_store_arch, inputs=arch_inputs, outputs=arch_state
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
list_btn.click(
|
| 342 |
+
fn=list_gguf_files, inputs=[repo_id, hf_token],
|
| 343 |
+
outputs=[file_picker, fetch_status],
|
| 344 |
+
)
|
| 345 |
+
fetch_btn.click(
|
| 346 |
+
fn=fetch_arch, inputs=[repo_id, file_picker, hf_token],
|
| 347 |
+
outputs=[*arch_inputs, fetch_status],
|
| 348 |
+
).then(
|
| 349 |
+
fn=_store_arch, inputs=arch_inputs, outputs=arch_state
|
| 350 |
+
)
|
| 351 |
+
load_preset_btn.click(
|
| 352 |
+
fn=load_preset, inputs=[preset_dd],
|
| 353 |
+
outputs=[*arch_inputs, fetch_status],
|
| 354 |
+
).then(
|
| 355 |
+
fn=_store_arch, inputs=arch_inputs, outputs=arch_state
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
compute_btn.click(
|
| 359 |
+
fn=compute,
|
| 360 |
+
inputs=[
|
| 361 |
+
arch_state, quant, n_ctx, cache_dtype, flash_attn, compute_dtype,
|
| 362 |
+
n_batch, n_prompt,
|
| 363 |
+
rope_freq_scale, yarn_ext_factor, yarn_attn_factor,
|
| 364 |
+
yarn_beta_fast, yarn_beta_slow,
|
| 365 |
+
gpu_vram_text, kv_on_largest, safety_margin,
|
| 366 |
+
],
|
| 367 |
+
outputs=[result_md, cmd_md, cmd_text],
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
return demo
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
demo = build_ui()
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
if __name__ == "__main__":
|
| 377 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.20.0
|
| 2 |
+
huggingface_hub>=0.20.0
|
| 3 |
+
requests>=2.28.0
|
tests/test_vramcalc.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the pure calculation logic in vramcalc."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from vramcalc import (
|
| 6 |
+
QUANT_BPW,
|
| 7 |
+
weight_bytes,
|
| 8 |
+
quant_from_filename,
|
| 9 |
+
kv_cache_bytes,
|
| 10 |
+
compute_scratch_bytes,
|
| 11 |
+
yarn_effective_context,
|
| 12 |
+
yarn_warnings,
|
| 13 |
+
gpu_split,
|
| 14 |
+
estimate,
|
| 15 |
+
command_preview,
|
| 16 |
+
format_bytes,
|
| 17 |
+
)
|
| 18 |
+
from vramcalc.presets import PRESETS
|
| 19 |
+
from vramcalc.gguf import parse_header_bytes, metadata_to_arch
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_quant_table_has_common_types():
|
| 23 |
+
for q in ["Q4_K_M", "Q5_K_M", "Q8_0", "Q2_K", "F16", "BF16", "Q6_K"]:
|
| 24 |
+
assert q in QUANT_BPW
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def test_quant_from_filename():
|
| 28 |
+
assert quant_from_filename("Llama-3-8B-Instruct-Q4_K_M.gguf") == "Q4_K_M"
|
| 29 |
+
assert quant_from_filename("model.Q5_K_S.gguf") == "Q5_K_S"
|
| 30 |
+
assert quant_from_filename("model.Q8_0.gguf") == "Q8_0"
|
| 31 |
+
assert quant_from_filename("model.F16.gguf") == "F16"
|
| 32 |
+
assert quant_from_filename("model.gguf") is None
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_weight_bytes():
|
| 36 |
+
# 8B params at Q4_K_M (4.84375 bpw) -> ~4.84 GB
|
| 37 |
+
b = weight_bytes(8_000_000_000, "Q4_K_M")
|
| 38 |
+
assert 4.7e9 < b < 4.9e9
|
| 39 |
+
# F16 -> 16 bytes/param
|
| 40 |
+
assert weight_bytes(1_000_000, "F16") == pytest.approx(2_000_000)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_weight_bytes_unknown_raises():
|
| 44 |
+
with pytest.raises(ValueError):
|
| 45 |
+
weight_bytes(1, "BOGUS")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_kv_cache_basic():
|
| 49 |
+
# Llama-3 8B: 32 layers, 4096 embd, 32 heads, 8 kv heads, ctx 8192, f16
|
| 50 |
+
# head_dim = 128; per layer = 8192*2*8*128*2 = 33,554,432 bytes
|
| 51 |
+
# * 32 layers = ~1.07 GB
|
| 52 |
+
b = kv_cache_bytes(
|
| 53 |
+
n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 54 |
+
n_ctx=8192, cache_dtype="f16",
|
| 55 |
+
)
|
| 56 |
+
assert 1.0e9 < b < 1.1e9
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_kv_cache_scales_with_ctx():
|
| 60 |
+
b1 = kv_cache_bytes(n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 61 |
+
n_ctx=4096, cache_dtype="f16")
|
| 62 |
+
b2 = kv_cache_bytes(n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 63 |
+
n_ctx=8192, cache_dtype="f16")
|
| 64 |
+
assert b2 == pytest.approx(2 * b1)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_kv_cache_mtp():
|
| 68 |
+
base = kv_cache_bytes(n_layer=61, n_embd=7168, n_head=128, n_head_kv=128,
|
| 69 |
+
n_ctx=8192, cache_dtype="f16", n_mtp=0)
|
| 70 |
+
mtp = kv_cache_bytes(n_layer=61, n_embd=7168, n_head=128, n_head_kv=128,
|
| 71 |
+
n_ctx=8192, cache_dtype="f16", n_mtp=1)
|
| 72 |
+
# n_mtp=1 adds one layer's worth: ratio = 62/61
|
| 73 |
+
assert mtp == pytest.approx(base * 62 / 61)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_kv_cache_quantized_smaller():
|
| 77 |
+
f16 = kv_cache_bytes(n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 78 |
+
n_ctx=8192, cache_dtype="f16")
|
| 79 |
+
q8 = kv_cache_bytes(n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 80 |
+
n_ctx=8192, cache_dtype="q8_0")
|
| 81 |
+
q4 = kv_cache_bytes(n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 82 |
+
n_ctx=8192, cache_dtype="q4_0")
|
| 83 |
+
assert q8 < f16
|
| 84 |
+
assert q4 < q8
|
| 85 |
+
assert q8 == pytest.approx(f16 / 2)
|
| 86 |
+
assert q4 == pytest.approx(f16 / 4)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_compute_scratch_positive():
|
| 90 |
+
s = compute_scratch_bytes(
|
| 91 |
+
n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 92 |
+
n_batch=512, compute_dtype="f16",
|
| 93 |
+
)
|
| 94 |
+
assert s > 0
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_compute_scratch_no_fa_quantized_adds_dequant():
|
| 98 |
+
base = compute_scratch_bytes(
|
| 99 |
+
n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 100 |
+
n_batch=512, compute_dtype="f16", cache_dtype="q8_0", flash_attn=True,
|
| 101 |
+
)
|
| 102 |
+
nofa = compute_scratch_bytes(
|
| 103 |
+
n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 104 |
+
n_batch=512, compute_dtype="f16", cache_dtype="q8_0", flash_attn=False,
|
| 105 |
+
)
|
| 106 |
+
assert nofa > base
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_yarn_effective_context():
|
| 110 |
+
# scale 0.5 doubles effective context
|
| 111 |
+
assert yarn_effective_context(8192, 0.5) == 16384
|
| 112 |
+
assert yarn_effective_context(8192, 1.0) == 8192
|
| 113 |
+
# zero/negative scale falls back to training ctx
|
| 114 |
+
assert yarn_effective_context(8192, 0.0) == 8192
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def test_yarn_warnings_target_exceeds():
|
| 118 |
+
w = yarn_warnings(
|
| 119 |
+
training_ctx=8192, target_ctx=32768,
|
| 120 |
+
rope_freq_scale=1.0, yarn_ext_factor=-1.0, yarn_attn_factor=1.0,
|
| 121 |
+
)
|
| 122 |
+
assert any("exceeds training context" in x for x in w)
|
| 123 |
+
assert any("Effective context" in x for x in w)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_yarn_warnings_no_warning_when_within():
|
| 127 |
+
w = yarn_warnings(
|
| 128 |
+
training_ctx=131072, target_ctx=8192,
|
| 129 |
+
rope_freq_scale=1.0, yarn_ext_factor=-1.0, yarn_attn_factor=1.0,
|
| 130 |
+
)
|
| 131 |
+
assert w == []
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def test_gpu_split_proportional_sums():
|
| 135 |
+
res = gpu_split(
|
| 136 |
+
gpu_vram_bytes=[24 << 30, 24 << 30, 16 << 30],
|
| 137 |
+
weights_bytes=40 << 30,
|
| 138 |
+
kv_compute_bytes=4 << 30,
|
| 139 |
+
)
|
| 140 |
+
# weight shares sum to weights
|
| 141 |
+
total_w = sum(a.weight_bytes for a in res.assignments)
|
| 142 |
+
assert total_w == pytest.approx(40 << 30, rel=1e-6)
|
| 143 |
+
# KV on exactly one GPU (default GPU 0)
|
| 144 |
+
kv_gpus = [a for a in res.assignments if a.kv_compute_bytes > 0]
|
| 145 |
+
assert len(kv_gpus) == 1
|
| 146 |
+
assert kv_gpus[0].index == 0
|
| 147 |
+
# used+free == vram for each gpu
|
| 148 |
+
for a in res.assignments:
|
| 149 |
+
assert a.used_bytes + a.free_bytes == pytest.approx(a.vram_bytes, rel=1e-6)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def test_gpu_split_kv_on_largest():
|
| 153 |
+
res = gpu_split(
|
| 154 |
+
gpu_vram_bytes=[16 << 30, 24 << 30],
|
| 155 |
+
weights_bytes=10 << 30,
|
| 156 |
+
kv_compute_bytes=3 << 30,
|
| 157 |
+
kv_on_largest=True,
|
| 158 |
+
)
|
| 159 |
+
kv_gpus = [a for a in res.assignments if a.kv_compute_bytes > 0]
|
| 160 |
+
assert len(kv_gpus) == 1
|
| 161 |
+
assert kv_gpus[0].index == 1
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def test_gpu_split_fit_badge():
|
| 165 |
+
res = gpu_split(
|
| 166 |
+
gpu_vram_bytes=[4 << 30], # tiny
|
| 167 |
+
weights_bytes=10 << 30,
|
| 168 |
+
kv_compute_bytes=1 << 30,
|
| 169 |
+
)
|
| 170 |
+
assert res.all_fit is False
|
| 171 |
+
assert res.assignments[0].fits is False
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_estimate_llama3_8b():
|
| 175 |
+
arch = PRESETS["Llama-3 8B"]
|
| 176 |
+
from vramcalc import Inputs
|
| 177 |
+
bd = estimate(arch, Inputs(quant="Q4_K_M", n_ctx=8192, cache_dtype="f16",
|
| 178 |
+
flash_attn=True, compute_dtype="f16",
|
| 179 |
+
n_batch=512, gpu_vram_gb=[24.0]))
|
| 180 |
+
# weights ~4.8 GB
|
| 181 |
+
assert 4.5e9 < bd.weights_bytes < 5.0e9
|
| 182 |
+
# KV ~1 GB
|
| 183 |
+
assert 0.9e9 < bd.kv_cache_bytes < 1.1e9
|
| 184 |
+
# total fits on 24 GB
|
| 185 |
+
assert bd.gpu.all_fit is True
|
| 186 |
+
assert bd.total_bytes < 24 << 30
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def test_estimate_mtp_overhead():
|
| 190 |
+
arch = PRESETS["DeepSeek-V3 (MoE)"]
|
| 191 |
+
from vramcalc import Inputs
|
| 192 |
+
bd = estimate(arch, Inputs(quant="Q4_K_M", n_ctx=8192, cache_dtype="f16",
|
| 193 |
+
gpu_vram_gb=[80.0]))
|
| 194 |
+
assert bd.mtp_overhead_bytes > 0
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def test_command_preview_basic():
|
| 198 |
+
arch = PRESETS["Llama-3 8B"]
|
| 199 |
+
from vramcalc import Inputs
|
| 200 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 201 |
+
cache_dtype="q8_0", flash_attn=True,
|
| 202 |
+
gpu_vram_gb=[24.0]))
|
| 203 |
+
assert "llama-server" in cmd
|
| 204 |
+
assert "-c 8192" in cmd
|
| 205 |
+
assert "--cache-type-k q8_0" in cmd
|
| 206 |
+
assert "--flash-attn" in cmd
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def test_command_preview_multigpu_split():
|
| 210 |
+
arch = PRESETS["Llama-3 8B"]
|
| 211 |
+
from vramcalc import Inputs
|
| 212 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=8192,
|
| 213 |
+
gpu_vram_gb=[24.0, 24.0]))
|
| 214 |
+
assert "--tensor-split 24.0,24.0" in cmd
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def test_command_preview_yarn_when_target_exceeds():
|
| 218 |
+
arch = PRESETS["Llama-3 8B"] # training_ctx 8192
|
| 219 |
+
from vramcalc import Inputs
|
| 220 |
+
cmd = command_preview(arch, Inputs(quant="Q4_K_M", n_ctx=32768,
|
| 221 |
+
rope_freq_scale=0.5,
|
| 222 |
+
gpu_vram_gb=[24.0]))
|
| 223 |
+
assert "--rope-scaling yarn" in cmd
|
| 224 |
+
assert "--rope-freq-scale 0.5" in cmd
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def test_format_bytes():
|
| 228 |
+
assert "GiB" in format_bytes(5 << 30)
|
| 229 |
+
assert "MiB" in format_bytes(5 << 20)
|
| 230 |
+
assert "KiB" in format_bytes(5 << 10)
|
| 231 |
+
assert format_bytes(0).endswith("B")
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def test_gguf_parse_header_minimal():
|
| 235 |
+
# Build a tiny valid GGUF header in memory: magic, version, tensor_count=0,
|
| 236 |
+
# kv_count=2, with one string and one int metadata entry.
|
| 237 |
+
import struct
|
| 238 |
+
def s(x): return x.encode("utf-8")
|
| 239 |
+
buf = b""
|
| 240 |
+
buf += struct.pack("<I", 0x46554747) # GGUF
|
| 241 |
+
buf += struct.pack("<I", 3) # version
|
| 242 |
+
buf += struct.pack("<Q", 0) # tensor_count
|
| 243 |
+
buf += struct.pack("<Q", 2) # kv_count
|
| 244 |
+
# kv 1: "general.architecture" = "llama"
|
| 245 |
+
name = s("general.architecture")
|
| 246 |
+
buf += struct.pack("<Q", len(name)) + name
|
| 247 |
+
buf += struct.pack("<I", 8) # STRING
|
| 248 |
+
val = s("llama")
|
| 249 |
+
buf += struct.pack("<Q", len(val)) + val
|
| 250 |
+
# kv 2: "llama.block_count" = 32
|
| 251 |
+
name2 = s("llama.block_count")
|
| 252 |
+
buf += struct.pack("<Q", len(name2)) + name2
|
| 253 |
+
buf += struct.pack("<I", 4) # UINT32
|
| 254 |
+
buf += struct.pack("<I", 32)
|
| 255 |
+
meta = parse_header_bytes(buf)
|
| 256 |
+
assert meta["general.architecture"] == "llama"
|
| 257 |
+
arch = metadata_to_arch(meta)
|
| 258 |
+
assert arch.architecture == "llama"
|
| 259 |
+
assert arch.n_layer == 32
|
vramcalc/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""llama.cpp VRAM estimation helpers.
|
| 2 |
+
|
| 3 |
+
Pure calculation logic, importable without Gradio so it can be unit-tested.
|
| 4 |
+
"""
|
| 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 yarn_effective_context, yarn_warnings
|
| 9 |
+
from .gpu import GpuBudget, gpu_split, fit_gpus, GpuSplitResult
|
| 10 |
+
from .gguf import (
|
| 11 |
+
GGUFMetadata,
|
| 12 |
+
parse_header_bytes,
|
| 13 |
+
parse_local_file,
|
| 14 |
+
parse_hf_range,
|
| 15 |
+
metadata_to_arch,
|
| 16 |
+
)
|
| 17 |
+
from .report import (
|
| 18 |
+
ModelArch,
|
| 19 |
+
Inputs,
|
| 20 |
+
Breakdown,
|
| 21 |
+
estimate,
|
| 22 |
+
command_preview,
|
| 23 |
+
format_bytes,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
__all__ = [
|
| 27 |
+
"QUANT_BPW",
|
| 28 |
+
"weight_bytes",
|
| 29 |
+
"quant_from_filename",
|
| 30 |
+
"kv_cache_bytes",
|
| 31 |
+
"cache_dtype_bytes",
|
| 32 |
+
"compute_scratch_bytes",
|
| 33 |
+
"yarn_effective_context",
|
| 34 |
+
"yarn_warnings",
|
| 35 |
+
"GpuBudget",
|
| 36 |
+
"GpuSplitResult",
|
| 37 |
+
"gpu_split",
|
| 38 |
+
"fit_gpus",
|
| 39 |
+
"GGUFMetadata",
|
| 40 |
+
"parse_header_bytes",
|
| 41 |
+
"parse_local_file",
|
| 42 |
+
"parse_hf_range",
|
| 43 |
+
"metadata_to_arch",
|
| 44 |
+
"ModelArch",
|
| 45 |
+
"Inputs",
|
| 46 |
+
"Breakdown",
|
| 47 |
+
"estimate",
|
| 48 |
+
"command_preview",
|
| 49 |
+
"format_bytes",
|
| 50 |
+
]
|
vramcalc/gguf.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal GGUF v3 header parser.
|
| 2 |
+
|
| 3 |
+
Reads only the metadata key/value section (no tensor data) from a GGUF file,
|
| 4 |
+
either from a local path or via a ranged HTTP GET against the Hugging Face
|
| 5 |
+
resolve URL so we never download the whole model.
|
| 6 |
+
|
| 7 |
+
GGUF layout (little-endian):
|
| 8 |
+
magic: u32 = 'GGUF'
|
| 9 |
+
version: u32
|
| 10 |
+
tensor_count: u64
|
| 11 |
+
metadata_kv_count: u64
|
| 12 |
+
metadata_kv[]: { name_len: u64, name: bytes, value_type: u32, value: ... }
|
| 13 |
+
tensor_info[]: (not parsed here)
|
| 14 |
+
|
| 15 |
+
We parse just enough metadata to recover architecture parameters.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import io
|
| 21 |
+
import struct
|
| 22 |
+
from dataclasses import dataclass, field
|
| 23 |
+
|
| 24 |
+
GGUF_MAGIC = 0x46554747 # "GGUF"
|
| 25 |
+
GGUF_DEFAULT_VERSION = 3
|
| 26 |
+
|
| 27 |
+
GGUF_TYPE_UINT8 = 0
|
| 28 |
+
GGUF_TYPE_INT8 = 1
|
| 29 |
+
GGUF_TYPE_UINT16 = 2
|
| 30 |
+
GGUF_TYPE_INT16 = 3
|
| 31 |
+
GGUF_TYPE_UINT32 = 4
|
| 32 |
+
GGUF_TYPE_INT32 = 5
|
| 33 |
+
GGUF_TYPE_FLOAT32 = 6
|
| 34 |
+
GGUF_TYPE_BOOL = 7
|
| 35 |
+
GGUF_TYPE_STRING = 8
|
| 36 |
+
GGUF_TYPE_ARRAY = 9
|
| 37 |
+
GGUF_TYPE_UINT64 = 10
|
| 38 |
+
GGUF_TYPE_INT64 = 11
|
| 39 |
+
GGUF_TYPE_FLOAT64 = 12
|
| 40 |
+
|
| 41 |
+
_SCALAR_FMT = {
|
| 42 |
+
GGUF_TYPE_UINT8: ("<B", 1),
|
| 43 |
+
GGUF_TYPE_INT8: ("<b", 1),
|
| 44 |
+
GGUF_TYPE_UINT16: ("<H", 2),
|
| 45 |
+
GGUF_TYPE_INT16: ("<h", 2),
|
| 46 |
+
GGUF_TYPE_UINT32: ("<I", 4),
|
| 47 |
+
GGUF_TYPE_INT32: ("<i", 4),
|
| 48 |
+
GGUF_TYPE_FLOAT32: ("<f", 4),
|
| 49 |
+
GGUF_TYPE_BOOL: ("<?", 1),
|
| 50 |
+
GGUF_TYPE_UINT64: ("<Q", 8),
|
| 51 |
+
GGUF_TYPE_INT64: ("<q", 8),
|
| 52 |
+
GGUF_TYPE_FLOAT64: ("<d", 8),
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class GGUFMetadata:
|
| 58 |
+
raw: dict[str, object] = field(default_factory=dict)
|
| 59 |
+
architecture: str = ""
|
| 60 |
+
n_layer: int = 0
|
| 61 |
+
n_embd: int = 0
|
| 62 |
+
n_head: int = 0
|
| 63 |
+
n_head_kv: int = 0
|
| 64 |
+
training_ctx: int = 0
|
| 65 |
+
rope_freq_base: float = 10000.0
|
| 66 |
+
n_expert: int = 0
|
| 67 |
+
n_expert_used: int = 0
|
| 68 |
+
n_mtp: int = 0
|
| 69 |
+
params: int = 0
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class _Reader:
|
| 73 |
+
def __init__(self, buf: bytes):
|
| 74 |
+
self.buf = buf
|
| 75 |
+
self.pos = 0
|
| 76 |
+
|
| 77 |
+
def eof(self) -> bool:
|
| 78 |
+
return self.pos >= len(self.buf)
|
| 79 |
+
|
| 80 |
+
def need(self, n: int):
|
| 81 |
+
if self.pos + n > len(self.buf):
|
| 82 |
+
raise EOFError("buffer exhausted while parsing GGUF header")
|
| 83 |
+
|
| 84 |
+
def u32(self) -> int:
|
| 85 |
+
self.need(4)
|
| 86 |
+
v = struct.unpack_from("<I", self.buf, self.pos)[0]
|
| 87 |
+
self.pos += 4
|
| 88 |
+
return v
|
| 89 |
+
|
| 90 |
+
def u64(self) -> int:
|
| 91 |
+
self.need(8)
|
| 92 |
+
v = struct.unpack_from("<Q", self.buf, self.pos)[0]
|
| 93 |
+
self.pos += 8
|
| 94 |
+
return v
|
| 95 |
+
|
| 96 |
+
def string(self) -> str:
|
| 97 |
+
n = self.u64()
|
| 98 |
+
self.need(n)
|
| 99 |
+
s = self.buf[self.pos:self.pos + n].decode("utf-8", errors="replace")
|
| 100 |
+
self.pos += n
|
| 101 |
+
return s
|
| 102 |
+
|
| 103 |
+
def scalar(self, t: int):
|
| 104 |
+
fmt, size = _SCALAR_FMT[t]
|
| 105 |
+
self.need(size)
|
| 106 |
+
v = struct.unpack_from(fmt, self.buf, self.pos)[0]
|
| 107 |
+
self.pos += size
|
| 108 |
+
return v
|
| 109 |
+
|
| 110 |
+
def value(self, t: int):
|
| 111 |
+
if t == GGUF_TYPE_STRING:
|
| 112 |
+
return self.string()
|
| 113 |
+
if t in _SCALAR_FMT:
|
| 114 |
+
return self.scalar(t)
|
| 115 |
+
if t == GGUF_TYPE_ARRAY:
|
| 116 |
+
inner = self.u32()
|
| 117 |
+
count = self.u64()
|
| 118 |
+
return [self.value(inner) for _ in range(count)]
|
| 119 |
+
raise ValueError(f"unsupported GGUF value type {t}")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def parse_header_bytes(buf: bytes) -> dict[str, object]:
|
| 123 |
+
"""Parse the metadata KV map from a buffer containing the GGUF header."""
|
| 124 |
+
r = _Reader(buf)
|
| 125 |
+
magic = r.u32()
|
| 126 |
+
if magic != GGUF_MAGIC:
|
| 127 |
+
raise ValueError(f"not a GGUF file (magic={magic:#x})")
|
| 128 |
+
version = r.u32()
|
| 129 |
+
if version < 1 or version > 3:
|
| 130 |
+
raise ValueError(f"unsupported GGUF version {version}")
|
| 131 |
+
# tensor_count (u64) - skipped
|
| 132 |
+
r.u64()
|
| 133 |
+
kv_count = r.u64()
|
| 134 |
+
meta: dict[str, object] = {}
|
| 135 |
+
for _ in range(kv_count):
|
| 136 |
+
if r.eof():
|
| 137 |
+
break
|
| 138 |
+
key = r.string()
|
| 139 |
+
t = r.u32()
|
| 140 |
+
try:
|
| 141 |
+
meta[key] = r.value(t)
|
| 142 |
+
except (EOFError, ValueError):
|
| 143 |
+
# ran out of buffered bytes (range read too small) - stop
|
| 144 |
+
break
|
| 145 |
+
return meta
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _coerce_int(v: object) -> int:
|
| 149 |
+
if isinstance(v, bool):
|
| 150 |
+
return int(v)
|
| 151 |
+
if isinstance(v, (int, float)):
|
| 152 |
+
return int(v)
|
| 153 |
+
return 0
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _coerce_float(v: object) -> float:
|
| 157 |
+
if isinstance(v, (int, float)):
|
| 158 |
+
return float(v)
|
| 159 |
+
return 0.0
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def metadata_to_arch(meta: dict[str, object]) -> GGUFMetadata:
|
| 163 |
+
"""Project raw GGUF metadata into the architecture fields we need."""
|
| 164 |
+
arch_name = str(meta.get("general.architecture", "") or "")
|
| 165 |
+
p = f"{arch_name}." if arch_name else ""
|
| 166 |
+
|
| 167 |
+
def g(key: str, default=0):
|
| 168 |
+
return meta.get(f"{p}{key}", meta.get(key, default))
|
| 169 |
+
|
| 170 |
+
m = GGUFMetadata(raw=meta, architecture=arch_name)
|
| 171 |
+
m.n_layer = _coerce_int(g("block_count"))
|
| 172 |
+
m.n_embd = _coerce_int(g("embedding_length"))
|
| 173 |
+
m.n_head = _coerce_int(g("attention.head_count"))
|
| 174 |
+
n_head_kv = g("attention.head_count_kv")
|
| 175 |
+
m.n_head_kv = _coerce_int(n_head_kv) if n_head_kv is not None else m.n_head
|
| 176 |
+
if m.n_head_kv == 0:
|
| 177 |
+
m.n_head_kv = m.n_head
|
| 178 |
+
m.training_ctx = _coerce_int(g("context_length"))
|
| 179 |
+
m.rope_freq_base = _coerce_float(g("rope.freq_base", 10000.0))
|
| 180 |
+
m.n_expert = _coerce_int(g("expert_count"))
|
| 181 |
+
m.n_expert_used = _coerce_int(g("expert_used_count"))
|
| 182 |
+
# MTP: not always in GGUF metadata; leave 0 unless present
|
| 183 |
+
m.n_mtp = _coerce_int(g("mtp.count") or g("n_mtp"))
|
| 184 |
+
# parameter count
|
| 185 |
+
m.params = _coerce_int(meta.get("general.parameter_count", 0))
|
| 186 |
+
return m
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def parse_local_file(path: str, max_bytes: int = 1 << 20) -> GGUFMetadata:
|
| 190 |
+
with open(path, "rb") as f:
|
| 191 |
+
buf = f.read(max_bytes)
|
| 192 |
+
return metadata_to_arch(parse_header_bytes(buf))
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def parse_hf_range(
|
| 196 |
+
repo_id: str,
|
| 197 |
+
filename: str,
|
| 198 |
+
*,
|
| 199 |
+
max_bytes: int = 1 << 20,
|
| 200 |
+
token: str | None = None,
|
| 201 |
+
revision: str = "main",
|
| 202 |
+
timeout: float = 30.0,
|
| 203 |
+
) -> GGUFMetadata:
|
| 204 |
+
"""Range-read the first `max_bytes` of a GGUF file from the HF Hub.
|
| 205 |
+
|
| 206 |
+
Uses a HTTP Range request against the resolve URL so we never download the
|
| 207 |
+
full model. Requires the `requests` package.
|
| 208 |
+
"""
|
| 209 |
+
import requests
|
| 210 |
+
|
| 211 |
+
url = f"https://huggingface.co/{repo_id}/resolve/{revision}/{filename}"
|
| 212 |
+
headers = {"Range": f"bytes=0-{max_bytes - 1}"}
|
| 213 |
+
if token:
|
| 214 |
+
headers["Authorization"] = f"Bearer {token}"
|
| 215 |
+
resp = requests.get(url, headers=headers, timeout=timeout, stream=True)
|
| 216 |
+
resp.raise_for_status()
|
| 217 |
+
buf = resp.content
|
| 218 |
+
return metadata_to_arch(parse_header_bytes(buf))
|
vramcalc/gpu.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multi-GPU VRAM budgeting and tensor-split estimation.
|
| 2 |
+
|
| 3 |
+
llama.cpp's real `--tensor-split` distributes tensor rows across devices; we
|
| 4 |
+
approximate it by splitting the **weights** proportionally to each GPU's VRAM
|
| 5 |
+
(this is the common case when users want an even load). The KV cache and
|
| 6 |
+
compute scratch are placed on a single device (GPU 0 by default, or the GPU
|
| 7 |
+
with the most free space) because llama.cpp keeps the cache on the first
|
| 8 |
+
device unless manually offloaded.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from dataclasses import dataclass, field
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class GpuBudget:
|
| 18 |
+
vram_bytes: list[int]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class GpuAssignment:
|
| 23 |
+
index: int
|
| 24 |
+
vram_bytes: int
|
| 25 |
+
weight_bytes: float
|
| 26 |
+
kv_compute_bytes: float
|
| 27 |
+
used_bytes: float
|
| 28 |
+
free_bytes: float
|
| 29 |
+
fits: bool
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class GpuSplitResult:
|
| 34 |
+
assignments: list[GpuAssignment] = field(default_factory=list)
|
| 35 |
+
total_vram_bytes: int = 0
|
| 36 |
+
total_used_bytes: float = 0.0
|
| 37 |
+
total_weights_bytes: float = 0.0
|
| 38 |
+
total_kv_compute_bytes: float = 0.0
|
| 39 |
+
all_fit: bool = False
|
| 40 |
+
kv_gpu_index: int = 0
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def gpu_split(
|
| 44 |
+
*,
|
| 45 |
+
gpu_vram_bytes: list[int],
|
| 46 |
+
weights_bytes: float,
|
| 47 |
+
kv_compute_bytes: float,
|
| 48 |
+
kv_on_largest: bool = False,
|
| 49 |
+
) -> GpuSplitResult:
|
| 50 |
+
"""Split weights proportionally to VRAM; place KV+compute on one GPU."""
|
| 51 |
+
if not gpu_vram_bytes:
|
| 52 |
+
return GpuSplitResult()
|
| 53 |
+
|
| 54 |
+
total_vram = sum(gpu_vram_bytes)
|
| 55 |
+
if total_vram <= 0:
|
| 56 |
+
return GpuSplitResult(
|
| 57 |
+
assignments=[
|
| 58 |
+
GpuAssignment(i, v, 0.0, 0.0, 0.0, float(v), True)
|
| 59 |
+
for i, v in enumerate(gpu_vram_bytes)
|
| 60 |
+
],
|
| 61 |
+
total_vram_bytes=total_vram,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# weight share per gpu proportional to vram
|
| 65 |
+
weight_shares = [weights_bytes * (v / total_vram) for v in gpu_vram_bytes]
|
| 66 |
+
|
| 67 |
+
# choose KV host
|
| 68 |
+
if kv_on_largest:
|
| 69 |
+
kv_idx = max(range(len(gpu_vram_bytes)), key=lambda i: gpu_vram_bytes[i])
|
| 70 |
+
else:
|
| 71 |
+
kv_idx = 0
|
| 72 |
+
|
| 73 |
+
assignments = []
|
| 74 |
+
for i, vram in enumerate(gpu_vram_bytes):
|
| 75 |
+
w = weight_shares[i]
|
| 76 |
+
kv = kv_compute_bytes if i == kv_idx else 0.0
|
| 77 |
+
used = w + kv
|
| 78 |
+
free = vram - used
|
| 79 |
+
fits = used <= vram
|
| 80 |
+
assignments.append(
|
| 81 |
+
GpuAssignment(
|
| 82 |
+
index=i,
|
| 83 |
+
vram_bytes=vram,
|
| 84 |
+
weight_bytes=w,
|
| 85 |
+
kv_compute_bytes=kv,
|
| 86 |
+
used_bytes=used,
|
| 87 |
+
free_bytes=free,
|
| 88 |
+
fits=fits,
|
| 89 |
+
)
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
return GpuSplitResult(
|
| 93 |
+
assignments=assignments,
|
| 94 |
+
total_vram_bytes=total_vram,
|
| 95 |
+
total_used_bytes=sum(a.used_bytes for a in assignments),
|
| 96 |
+
total_weights_bytes=weights_bytes,
|
| 97 |
+
total_kv_compute_bytes=kv_compute_bytes,
|
| 98 |
+
all_fit=all(a.fits for a in assignments),
|
| 99 |
+
kv_gpu_index=kv_idx,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def fit_gpus(result: GpuSplitResult) -> bool:
|
| 104 |
+
return result.all_fit
|
vramcalc/kv.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""KV cache and compute-scratch memory math.
|
| 2 |
+
|
| 3 |
+
Based on llama.cpp's KV cache layout:
|
| 4 |
+
|
| 5 |
+
KV = n_layer * n_ctx * 2 * n_head_kv * head_dim * sizeof(cache_dtype)
|
| 6 |
+
|
| 7 |
+
where head_dim = n_embd / n_head. The factor 2 covers both K and V. With MTP
|
| 8 |
+
heads (e.g. DeepSeek-V3 n_mtp=1) each head adds ~one extra layer's worth of
|
| 9 |
+
KV, so the layer count becomes (n_layer + n_mtp).
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# cache dtype -> bytes per element (effective, including scales for quantized)
|
| 16 |
+
CACHE_DTYPE_BYTES: dict[str, float] = {
|
| 17 |
+
"f16": 2.0,
|
| 18 |
+
"bf16": 2.0,
|
| 19 |
+
"f32": 4.0,
|
| 20 |
+
"q8_0": 1.0,
|
| 21 |
+
"q8_1": 1.0625,
|
| 22 |
+
"q4_0": 0.5,
|
| 23 |
+
"q4_1": 0.5625,
|
| 24 |
+
"q5_0": 0.625,
|
| 25 |
+
"q5_1": 0.6875,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
# compute dtype -> bytes per element
|
| 29 |
+
COMPUTE_DTYPE_BYTES: dict[str, float] = {
|
| 30 |
+
"f16": 2.0,
|
| 31 |
+
"bf16": 2.0,
|
| 32 |
+
"f32": 4.0,
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def cache_dtype_bytes(dtype: str) -> float:
|
| 37 |
+
if dtype not in CACHE_DTYPE_BYTES:
|
| 38 |
+
raise ValueError(f"Unknown cache dtype: {dtype!r}")
|
| 39 |
+
return CACHE_DTYPE_BYTES[dtype]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _head_dim(n_embd: int, n_head: int) -> int:
|
| 43 |
+
if n_head <= 0:
|
| 44 |
+
return 0
|
| 45 |
+
return n_embd // n_head
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def kv_cache_bytes(
|
| 49 |
+
*,
|
| 50 |
+
n_layer: int,
|
| 51 |
+
n_embd: int,
|
| 52 |
+
n_head: int,
|
| 53 |
+
n_head_kv: int,
|
| 54 |
+
n_ctx: int,
|
| 55 |
+
cache_dtype: str,
|
| 56 |
+
n_mtp: int = 0,
|
| 57 |
+
flash_attn: bool = True,
|
| 58 |
+
) -> float:
|
| 59 |
+
"""Total KV cache size in bytes for the whole context window."""
|
| 60 |
+
head_dim = _head_dim(n_embd, n_head)
|
| 61 |
+
per_layer = n_ctx * 2 * n_head_kv * head_dim * cache_dtype_bytes(cache_dtype)
|
| 62 |
+
layers = n_layer + max(0, n_mtp)
|
| 63 |
+
base = per_layer * layers
|
| 64 |
+
|
| 65 |
+
# When flash attention is off AND the cache is quantized, llama.cpp keeps a
|
| 66 |
+
# dequantized f32 scratch for the active batch slice. We approximate the
|
| 67 |
+
# worst-case scratch as one batch-sized slice in f32, per layer (K+V).
|
| 68 |
+
if not flash_attn and cache_dtype not in ("f16", "bf16", "f32"):
|
| 69 |
+
# scratch is per-batch, not per-ctx; caller passes batch separately via
|
| 70 |
+
# compute_scratch_bytes. Here we leave it out to avoid double counting.
|
| 71 |
+
pass
|
| 72 |
+
return base
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def compute_scratch_bytes(
|
| 76 |
+
*,
|
| 77 |
+
n_layer: int,
|
| 78 |
+
n_embd: int,
|
| 79 |
+
n_head: int,
|
| 80 |
+
n_head_kv: int,
|
| 81 |
+
n_batch: int,
|
| 82 |
+
compute_dtype: str = "f16",
|
| 83 |
+
cache_dtype: str = "f16",
|
| 84 |
+
flash_attn: bool = True,
|
| 85 |
+
n_mtp: int = 0,
|
| 86 |
+
) -> float:
|
| 87 |
+
"""Working/compute scratch memory in bytes.
|
| 88 |
+
|
| 89 |
+
Includes the batch activations and, when FA is off with a quantized cache,
|
| 90 |
+
the per-batch f32 dequantization scratch for K and V across all layers.
|
| 91 |
+
"""
|
| 92 |
+
if compute_dtype not in COMPUTE_DTYPE_BYTES:
|
| 93 |
+
raise ValueError(f"Unknown compute dtype: {compute_dtype!r}")
|
| 94 |
+
cb = COMPUTE_DTYPE_BYTES[compute_dtype]
|
| 95 |
+
|
| 96 |
+
head_dim = _head_dim(n_embd, n_head)
|
| 97 |
+
layers = n_layer + max(0, n_mtp)
|
| 98 |
+
|
| 99 |
+
# activations: roughly batch * n_embd * compute_dtype (a couple of buffers)
|
| 100 |
+
activations = n_batch * n_embd * cb * 2
|
| 101 |
+
|
| 102 |
+
scratch = 0.0
|
| 103 |
+
if not flash_attn and cache_dtype not in ("f16", "bf16", "f32"):
|
| 104 |
+
# f32 dequant buffer for one batch across all layers, K and V
|
| 105 |
+
scratch = layers * n_batch * 2 * n_head_kv * head_dim * 4.0
|
| 106 |
+
return activations + scratch
|
vramcalc/presets.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Built-in model architecture presets for offline quick-start."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from .report import ModelArch
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
PRESETS: dict[str, ModelArch] = {
|
| 9 |
+
"Llama-3 8B": ModelArch(
|
| 10 |
+
name="Llama-3 8B", architecture="llama",
|
| 11 |
+
n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 12 |
+
training_ctx=8192, params=8_030_000_000, rope_freq_base=500000.0,
|
| 13 |
+
),
|
| 14 |
+
"Llama-3 70B": ModelArch(
|
| 15 |
+
name="Llama-3 70B", architecture="llama",
|
| 16 |
+
n_layer=80, n_embd=8192, n_head=64, n_head_kv=8,
|
| 17 |
+
training_ctx=8192, params=70_000_000_000, rope_freq_base=500000.0,
|
| 18 |
+
),
|
| 19 |
+
"Llama-3.1 8B": ModelArch(
|
| 20 |
+
name="Llama-3.1 8B", architecture="llama",
|
| 21 |
+
n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 22 |
+
training_ctx=131072, params=8_030_000_000, rope_freq_base=500000.0,
|
| 23 |
+
),
|
| 24 |
+
"Llama-3.1 70B": ModelArch(
|
| 25 |
+
name="Llama-3.1 70B", architecture="llama",
|
| 26 |
+
n_layer=80, n_embd=8192, n_head=64, n_head_kv=8,
|
| 27 |
+
training_ctx=131072, params=70_000_000_000, rope_freq_base=500000.0,
|
| 28 |
+
),
|
| 29 |
+
"Mistral 7B": ModelArch(
|
| 30 |
+
name="Mistral 7B", architecture="llama",
|
| 31 |
+
n_layer=32, n_embd=4096, n_head=32, n_head_kv=8,
|
| 32 |
+
training_ctx=32768, params=7_240_000_000, rope_freq_base=10000.0,
|
| 33 |
+
),
|
| 34 |
+
"Qwen2 7B": ModelArch(
|
| 35 |
+
name="Qwen2 7B", architecture="qwen2",
|
| 36 |
+
n_layer=28, n_embd=3584, n_head=28, n_head_kv=4,
|
| 37 |
+
training_ctx=32768, params=7_620_000_000, rope_freq_base=1000000.0,
|
| 38 |
+
),
|
| 39 |
+
"Qwen2 72B": ModelArch(
|
| 40 |
+
name="Qwen2 72B", architecture="qwen2",
|
| 41 |
+
n_layer=80, n_embd=8192, n_head=64, n_head_kv=8,
|
| 42 |
+
training_ctx=131072, params=72_700_000_000, rope_freq_base=1000000.0,
|
| 43 |
+
),
|
| 44 |
+
"DeepSeek-V3 (MoE)": ModelArch(
|
| 45 |
+
name="DeepSeek-V3", architecture="deepseek2",
|
| 46 |
+
n_layer=61, n_embd=7168, n_head=128, n_head_kv=128,
|
| 47 |
+
training_ctx=131072, params=671_000_000_000, rope_freq_base=10000.0,
|
| 48 |
+
n_expert=256, n_expert_used=8, n_mtp=1,
|
| 49 |
+
),
|
| 50 |
+
"Phi-3 mini 3.8B": ModelArch(
|
| 51 |
+
name="Phi-3 mini", architecture="phi3",
|
| 52 |
+
n_layer=32, n_embd=3072, n_head=32, n_head_kv=32,
|
| 53 |
+
training_ctx=131072, params=3_800_000_000, rope_freq_base=10000.0,
|
| 54 |
+
),
|
| 55 |
+
"Gemma-2 9B": ModelArch(
|
| 56 |
+
name="Gemma-2 9B", architecture="gemma2",
|
| 57 |
+
n_layer=42, n_embd=3584, n_head=16, n_head_kv=8,
|
| 58 |
+
training_ctx=8192, params=9_240_000_000, rope_freq_base=10000.0,
|
| 59 |
+
),
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
PRESET_NAMES = list(PRESETS.keys())
|
vramcalc/quant.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Quantization -> bits-per-weight table and weight memory math.
|
| 2 |
+
|
| 3 |
+
Values are the effective bits per weight (including block scales/min) as used
|
| 4 |
+
by llama.cpp's k_quants / imatrix quants. Sourced from the llama.cpp docs and
|
| 5 |
+
the widely-cited community tables. These are estimates; real file sizes vary a
|
| 6 |
+
little due to alignment and metadata.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# quant name -> effective bits per weight
|
| 15 |
+
QUANT_BPW: dict[str, float] = {
|
| 16 |
+
# 2-bit
|
| 17 |
+
"Q2_K": 2.5625,
|
| 18 |
+
"IQ2_XXS": 2.0625,
|
| 19 |
+
"IQ2_XS": 2.3125,
|
| 20 |
+
"IQ2_S": 2.5,
|
| 21 |
+
"IQ2_M": 2.7,
|
| 22 |
+
# 3-bit
|
| 23 |
+
"Q3_K_S": 3.4375,
|
| 24 |
+
"Q3_K_M": 3.84375,
|
| 25 |
+
"Q3_K_L": 4.125,
|
| 26 |
+
"IQ3_XXS": 3.0625,
|
| 27 |
+
"IQ3_XS": 3.25,
|
| 28 |
+
"IQ3_S": 3.5,
|
| 29 |
+
"IQ3_M": 3.7,
|
| 30 |
+
# 4-bit
|
| 31 |
+
"Q4_0": 4.5,
|
| 32 |
+
"Q4_1": 5.0,
|
| 33 |
+
"Q4_K_S": 4.5,
|
| 34 |
+
"Q4_K_M": 4.84375,
|
| 35 |
+
"IQ4_NL": 4.5,
|
| 36 |
+
"IQ4_XS": 4.25,
|
| 37 |
+
# 5-bit
|
| 38 |
+
"Q5_0": 5.5,
|
| 39 |
+
"Q5_1": 6.0,
|
| 40 |
+
"Q5_K_S": 5.5,
|
| 41 |
+
"Q5_K_M": 5.6875,
|
| 42 |
+
# 6-bit
|
| 43 |
+
"Q6_K": 6.5625,
|
| 44 |
+
# 8-bit
|
| 45 |
+
"Q8_0": 8.5,
|
| 46 |
+
"Q8_1": 9.0,
|
| 47 |
+
# unquantized
|
| 48 |
+
"F16": 16.0,
|
| 49 |
+
"BF16": 16.0,
|
| 50 |
+
"F32": 32.0,
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass(frozen=True)
|
| 55 |
+
class QuantInfo:
|
| 56 |
+
name: str
|
| 57 |
+
bpw: float
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def quant_from_filename(filename: str) -> str | None:
|
| 61 |
+
"""Detect a quant name from a GGUF filename like `Model-Q4_K_M.gguf`.
|
| 62 |
+
|
| 63 |
+
Returns the canonical quant name (matching a key in QUANT_BPW) or None.
|
| 64 |
+
Matching is case-insensitive on the quant token.
|
| 65 |
+
"""
|
| 66 |
+
import re
|
| 67 |
+
|
| 68 |
+
upper = filename.upper()
|
| 69 |
+
# Try longer / more specific names first so e.g. Q4_K_S isn't caught by Q4_K.
|
| 70 |
+
ordered = sorted(QUANT_BPW.keys(), key=len, reverse=True)
|
| 71 |
+
for q in ordered:
|
| 72 |
+
# match as a token: surrounded by non-alphanumeric or string edges
|
| 73 |
+
if re.search(rf"(^|[^A-Z0-9]){re.escape(q.upper())}([^A-Z0-9]|$)", upper):
|
| 74 |
+
return q
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def weight_bytes(params: int, quant: str) -> float:
|
| 79 |
+
"""Estimated weight memory in bytes for `params` parameters at `quant`.
|
| 80 |
+
|
| 81 |
+
`params` is the total parameter count (including all MoE experts; the GGUF
|
| 82 |
+
already encodes them so this is the natural input).
|
| 83 |
+
"""
|
| 84 |
+
if quant not in QUANT_BPW:
|
| 85 |
+
raise ValueError(f"Unknown quant type: {quant!r}")
|
| 86 |
+
return params * QUANT_BPW[quant] / 8.0
|
vramcalc/report.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Assemble a full VRAM breakdown and a llama.cpp launch-command preview."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
|
| 7 |
+
from .quant import weight_bytes
|
| 8 |
+
from .kv import kv_cache_bytes, compute_scratch_bytes
|
| 9 |
+
from .yarn import yarn_effective_context, yarn_warnings
|
| 10 |
+
from .gpu import gpu_split, GpuSplitResult
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class ModelArch:
|
| 15 |
+
"""Architecture parameters (fetched from GGUF or entered manually)."""
|
| 16 |
+
name: str = ""
|
| 17 |
+
architecture: str = ""
|
| 18 |
+
n_layer: int = 0
|
| 19 |
+
n_embd: int = 0
|
| 20 |
+
n_head: int = 0
|
| 21 |
+
n_head_kv: int = 0
|
| 22 |
+
training_ctx: int = 0
|
| 23 |
+
params: int = 0 # total params including MoE experts
|
| 24 |
+
rope_freq_base: float = 10000.0
|
| 25 |
+
n_expert: int = 0 # MoE
|
| 26 |
+
n_expert_used: int = 0
|
| 27 |
+
n_mtp: int = 0 # MTP heads (e.g. DeepSeek-V3 = 1)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class Inputs:
|
| 32 |
+
quant: str = "Q4_K_M"
|
| 33 |
+
n_ctx: int = 8192
|
| 34 |
+
cache_dtype: str = "f16"
|
| 35 |
+
flash_attn: bool = True
|
| 36 |
+
compute_dtype: str = "f16"
|
| 37 |
+
n_batch: int = 512
|
| 38 |
+
n_prompt: int = 0
|
| 39 |
+
# YaRN
|
| 40 |
+
rope_freq_scale: float = 1.0
|
| 41 |
+
yarn_ext_factor: float = -1.0
|
| 42 |
+
yarn_attn_factor: float = 1.0
|
| 43 |
+
yarn_beta_fast: float = 32.0
|
| 44 |
+
yarn_beta_slow: float = 1.0
|
| 45 |
+
# GPU
|
| 46 |
+
gpu_vram_gb: list[float] = field(default_factory=lambda: [24.0])
|
| 47 |
+
kv_on_largest: bool = False
|
| 48 |
+
# margin
|
| 49 |
+
safety_margin_pct: float = 5.0
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class Breakdown:
|
| 54 |
+
weights_bytes: float = 0.0
|
| 55 |
+
kv_cache_bytes: float = 0.0
|
| 56 |
+
compute_scratch_bytes: float = 0.0
|
| 57 |
+
mtp_overhead_bytes: float = 0.0
|
| 58 |
+
gguf_overhead_bytes: float = 0.0
|
| 59 |
+
safety_margin_bytes: float = 0.0
|
| 60 |
+
total_bytes: float = 0.0
|
| 61 |
+
effective_context: int = 0
|
| 62 |
+
warnings: list[str] = field(default_factory=list)
|
| 63 |
+
gpu: GpuSplitResult | None = None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def estimate(arch: ModelArch, inp: Inputs) -> Breakdown:
|
| 67 |
+
weights = weight_bytes(arch.params, inp.quant)
|
| 68 |
+
|
| 69 |
+
# KV cache (with MTP layers folded in)
|
| 70 |
+
kv = kv_cache_bytes(
|
| 71 |
+
n_layer=arch.n_layer,
|
| 72 |
+
n_embd=arch.n_embd,
|
| 73 |
+
n_head=arch.n_head,
|
| 74 |
+
n_head_kv=arch.n_head_kv,
|
| 75 |
+
n_ctx=inp.n_ctx,
|
| 76 |
+
cache_dtype=inp.cache_dtype,
|
| 77 |
+
n_mtp=arch.n_mtp,
|
| 78 |
+
flash_attn=inp.flash_attn,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# compute / activation scratch
|
| 82 |
+
scratch = compute_scratch_bytes(
|
| 83 |
+
n_layer=arch.n_layer,
|
| 84 |
+
n_embd=arch.n_embd,
|
| 85 |
+
n_head=arch.n_head,
|
| 86 |
+
n_head_kv=arch.n_head_kv,
|
| 87 |
+
n_batch=inp.n_batch,
|
| 88 |
+
compute_dtype=inp.compute_dtype,
|
| 89 |
+
cache_dtype=inp.cache_dtype,
|
| 90 |
+
flash_attn=inp.flash_attn,
|
| 91 |
+
n_mtp=arch.n_mtp,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# isolate MTP overhead for display: the extra layer's weights + its KV share
|
| 95 |
+
mtp_overhead = 0.0
|
| 96 |
+
if arch.n_mtp > 0:
|
| 97 |
+
# approximate extra weight as one layer's worth: params/n_layer * bpw/8
|
| 98 |
+
if arch.n_layer > 0:
|
| 99 |
+
per_layer_params = arch.params / arch.n_layer
|
| 100 |
+
mtp_weights = per_layer_params * (
|
| 101 |
+
__import__("vramcalc.quant", fromlist=["QUANT_BPW"]).QUANT_BPW[inp.quant] / 8.0
|
| 102 |
+
)
|
| 103 |
+
else:
|
| 104 |
+
mtp_weights = 0.0
|
| 105 |
+
# KV for the extra layers
|
| 106 |
+
head_dim = arch.n_embd // arch.n_head if arch.n_head > 0 else 0
|
| 107 |
+
mtp_kv = (
|
| 108 |
+
arch.n_mtp * inp.n_ctx * 2 * arch.n_head_kv * head_dim
|
| 109 |
+
* __import__("vramcalc.kv", fromlist=["cache_dtype_bytes"]).cache_dtype_bytes(inp.cache_dtype)
|
| 110 |
+
)
|
| 111 |
+
mtp_overhead = mtp_weights + mtp_kv
|
| 112 |
+
|
| 113 |
+
# GGUF header / alignment overhead: small constant per file, rough estimate
|
| 114 |
+
gguf_overhead = max(arch.n_layer * 4096, 1 << 20) # >= 1 MiB
|
| 115 |
+
|
| 116 |
+
subtotal = weights + kv + scratch + gguf_overhead
|
| 117 |
+
margin = subtotal * (inp.safety_margin_pct / 100.0)
|
| 118 |
+
total = subtotal + margin
|
| 119 |
+
|
| 120 |
+
eff_ctx = yarn_effective_context(arch.training_ctx, inp.rope_freq_scale)
|
| 121 |
+
warns = yarn_warnings(
|
| 122 |
+
training_ctx=arch.training_ctx,
|
| 123 |
+
target_ctx=inp.n_ctx,
|
| 124 |
+
rope_freq_scale=inp.rope_freq_scale,
|
| 125 |
+
yarn_ext_factor=inp.yarn_ext_factor,
|
| 126 |
+
yarn_attn_factor=inp.yarn_attn_factor,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
gpu_vram_bytes = [int(g * (1 << 30)) for g in inp.gpu_vram_gb]
|
| 130 |
+
gpu = gpu_split(
|
| 131 |
+
gpu_vram_bytes=gpu_vram_bytes,
|
| 132 |
+
weights_bytes=weights,
|
| 133 |
+
kv_compute_bytes=kv + scratch,
|
| 134 |
+
kv_on_largest=inp.kv_on_largest,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
return Breakdown(
|
| 138 |
+
weights_bytes=weights,
|
| 139 |
+
kv_cache_bytes=kv,
|
| 140 |
+
compute_scratch_bytes=scratch,
|
| 141 |
+
mtp_overhead_bytes=mtp_overhead,
|
| 142 |
+
gguf_overhead_bytes=gguf_overhead,
|
| 143 |
+
safety_margin_bytes=margin,
|
| 144 |
+
total_bytes=total,
|
| 145 |
+
effective_context=eff_ctx,
|
| 146 |
+
warnings=warns,
|
| 147 |
+
gpu=gpu,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def format_bytes(n: float) -> str:
|
| 152 |
+
"""Human-readable byte size."""
|
| 153 |
+
n = float(n)
|
| 154 |
+
if n < 0:
|
| 155 |
+
return "-" + format_bytes(-n)
|
| 156 |
+
units = [("GiB", 1 << 30), ("MiB", 1 << 20), ("KiB", 1 << 10)]
|
| 157 |
+
for label, size in units:
|
| 158 |
+
if n >= size:
|
| 159 |
+
return f"{n / size:.2f} {label}"
|
| 160 |
+
return f"{n:.0f} B"
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def command_preview(arch: ModelArch, inp: Inputs) -> str:
|
| 164 |
+
"""Generate a llama.cpp launch command from the current inputs."""
|
| 165 |
+
parts = ["llama-server"]
|
| 166 |
+
parts.append(f"-m model-{inp.quant}.gguf")
|
| 167 |
+
parts.append(f"-c {inp.n_ctx}")
|
| 168 |
+
parts.append("-ngl 999") # full offload assumption
|
| 169 |
+
parts.append(f"-b {inp.n_batch}")
|
| 170 |
+
if inp.cache_dtype != "f16":
|
| 171 |
+
parts.append(f"--cache-type-k {inp.cache_dtype}")
|
| 172 |
+
parts.append(f"--cache-type-v {inp.cache_dtype}")
|
| 173 |
+
if inp.flash_attn:
|
| 174 |
+
parts.append("--flash-attn")
|
| 175 |
+
if len(inp.gpu_vram_gb) > 1:
|
| 176 |
+
# tensor-split proportional to vram
|
| 177 |
+
split = ",".join(f"{g}" for g in inp.gpu_vram_gb)
|
| 178 |
+
parts.append(f"--tensor-split {split}")
|
| 179 |
+
# YaRN / rope
|
| 180 |
+
yarn_args = []
|
| 181 |
+
if inp.rope_freq_scale != 1.0:
|
| 182 |
+
yarn_args.append(f"--rope-freq-scale {inp.rope_freq_scale}")
|
| 183 |
+
if arch.rope_freq_base != 10000.0:
|
| 184 |
+
yarn_args.append(f"--rope-freq-base {arch.rope_freq_base}")
|
| 185 |
+
if inp.yarn_ext_factor >= 0.0:
|
| 186 |
+
yarn_args.append(f"--yarn-ext-factor {inp.yarn_ext_factor}")
|
| 187 |
+
if inp.yarn_attn_factor != 1.0:
|
| 188 |
+
yarn_args.append(f"--yarn-attn-factor {inp.yarn_attn_factor}")
|
| 189 |
+
if inp.yarn_beta_fast != 32.0:
|
| 190 |
+
yarn_args.append(f"--yarn-beta-fast {inp.yarn_beta_fast}")
|
| 191 |
+
if inp.yarn_beta_slow != 1.0:
|
| 192 |
+
yarn_args.append(f"--yarn-beta-slow {inp.yarn_beta_slow}")
|
| 193 |
+
if inp.n_ctx > arch.training_ctx and arch.training_ctx > 0:
|
| 194 |
+
yarn_args.append("--rope-scaling yarn")
|
| 195 |
+
if yarn_args:
|
| 196 |
+
parts.extend(yarn_args)
|
| 197 |
+
if arch.n_mtp > 0:
|
| 198 |
+
parts.append(f"--mtp {arch.n_mtp}")
|
| 199 |
+
return " \\\n ".join(parts)
|
vramcalc/yarn.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""YaRN / RoPE context-extension sanity checks.
|
| 2 |
+
|
| 3 |
+
YaRN changes how the model *interprets* positions, not the per-token KV size,
|
| 4 |
+
so KV cache memory is driven by the target context length the user requests.
|
| 5 |
+
These helpers surface warnings when the target context exceeds the model's
|
| 6 |
+
training context (YaRN scaling required) and show the effective context as a
|
| 7 |
+
sanity check.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def yarn_effective_context(training_ctx: int, rope_freq_scale: float) -> int:
|
| 14 |
+
"""Effective (interpolated) context given a rope_freq_scale.
|
| 15 |
+
|
| 16 |
+
effective = training_ctx / rope_freq_scale (rope_freq_scale < 1 extends).
|
| 17 |
+
"""
|
| 18 |
+
if rope_freq_scale <= 0:
|
| 19 |
+
return training_ctx
|
| 20 |
+
return int(round(training_ctx / rope_freq_scale))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def yarn_warnings(
|
| 24 |
+
*,
|
| 25 |
+
training_ctx: int,
|
| 26 |
+
target_ctx: int,
|
| 27 |
+
rope_freq_scale: float,
|
| 28 |
+
yarn_ext_factor: float,
|
| 29 |
+
yarn_attn_factor: float,
|
| 30 |
+
) -> list[str]:
|
| 31 |
+
"""Return human-readable warnings about the YaRN / context config."""
|
| 32 |
+
warns: list[str] = []
|
| 33 |
+
|
| 34 |
+
if target_ctx > training_ctx:
|
| 35 |
+
warns.append(
|
| 36 |
+
f"Target context {target_ctx} exceeds training context "
|
| 37 |
+
f"{training_ctx}; YaRN/RoPE scaling is required for coherent "
|
| 38 |
+
f"long-context output."
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
eff = yarn_effective_context(training_ctx, rope_freq_scale)
|
| 42 |
+
if target_ctx > training_ctx and eff < target_ctx:
|
| 43 |
+
warns.append(
|
| 44 |
+
f"Effective context from rope_freq_scale={rope_freq_scale} is "
|
| 45 |
+
f"{eff}, which is below the target {target_ctx}. The model may "
|
| 46 |
+
f"not fully cover the requested window; lower rope_freq_scale "
|
| 47 |
+
f"to interpolate more aggressively."
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
if target_ctx > training_ctx and yarn_ext_factor < 0.0:
|
| 51 |
+
warns.append("yarn_ext_factor is negative; YaRN extrapolation is off.")
|
| 52 |
+
|
| 53 |
+
if yarn_attn_factor < 1.0:
|
| 54 |
+
warns.append(
|
| 55 |
+
"yarn_attn_factor < 1.0 scales attention down; use ~1.0 for "
|
| 56 |
+
"normal long-context YaRN (lower only for high-context YaRN fine-tunes)."
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
return warns
|