The vault was 155 pages / 31,210 lines, most of it measurement ledgers for payloads that no longer run. A throughput figure for a runtime nobody operates is not a record: AI moves faster than the vault, so the number is wrong before anyone reads it, and a wrong number in a runbook gets believed. Per the new purpose test in `AGENTS.md` → *Documentation Policy*, it is destroyed. Gone (`.AGENTTRASH/20261019-destruction/`, KILL-LIST.txt is the manifest): - the `frank-medium` endpoint and its five era pages, `frank-npu`, FastFlowLM, the ikllama/kingjones/dealign/frank-redo eras, the ling/swift15/freetoken/ubatch campaign pages, qwen3.8 spec-matrix and strix-optimizations, the two "Running Qwen3.6 …" pages; - the whole `Pipeline Parallelism/` folder (no live script passes a pipeline flag) and the multi-device/x4-pair/hybrid/offload experiment pages; - every vLLM page — no vLLM endpoint exists on this fleet; - `LLM/deployment.md` (`Architecture/Deployment Flow.md` is the test-referenced owner) and the external surveys (SGLang, Lemonade, llama.cpp forks/diff, self-hosted app census, the TTS survey) that were obsolete by construction; - `log.md`, 2,380 lines. Git holds the chronology; a causal law belongs to the page that owns the thing it governs, never to a diary. Survivors: 101 pages, 17,542 lines, 0 OKF violations, 0 unresolved links. Eras tables keep the window, payload, runtime, reason and rollback pointer; the rate columns are blanked. The live shapes stay verbatim — those numbers are what the deployer and `models.yml` consume.
176 lines
9.9 KiB
Markdown
176 lines
9.9 KiB
Markdown
---
|
||
type: reference
|
||
title: Llama Cpp Serving Sizing
|
||
description: "Measurement-first sizing for llama.cpp shapes: slot pools against a headroom band, per-device accounting, prompt cache."
|
||
tags: [llama.cpp, vram, kv-cache, slots, flash-attn, sizing]
|
||
timestamp: 2026-10-05T00:00:00-07:00
|
||
---
|
||
|
||
# Llama Cpp Serving Sizing
|
||
|
||
## 1. Slot-pool sizing against a headroom band
|
||
|
||
Goal: choose `CTX_SIZE`/`PARALLEL`/KV types so N conversations fit a GPU while
|
||
keeping a requested amount of VRAM free — from measurement, never arithmetic
|
||
guesswork.
|
||
|
||
1. **Fix the invariants.** Read the payload's KV geometry (attention layers
|
||
only, KV heads, head_dim, KV dtype bytes) and the per-slot rule
|
||
`n_ctx_slot = CTX_SIZE / PARALLEL`. A hybrid model with
|
||
`kv_unified = 'false'` gives each slot its own cache AND a fixed per-slot
|
||
state buffer — slot count is a memory term, not free.
|
||
2. **Two boots on the real card** (production stopped, per the bench law)
|
||
each near the target; record `nvidia-smi` used/total plus the boot log's
|
||
`n_slots`/`n_ctx_slot`: shape A = current/cheap, shape B = new demand.
|
||
`non-KV = used − (tokens × KiB/token)`; the two rows separate
|
||
weights/buffers/graphs from the new demand; a projector is billed after the
|
||
fit at roughly 1.5x its file size ([[frank-zealot]] Contract has that
|
||
measurement).
|
||
3. **Solve the band:** `KV_tokens ≤ (card − non-KV − headroom_low) /
|
||
KiB/token`, round the pool DOWN to a multiple of `PARALLEL × 4096` so every
|
||
slot gets an equal integer window. Boot and measure; if outside the band,
|
||
walk one step — never re-derive from a formula.
|
||
4. **Propagate the per-slot number — it is the client contract:**
|
||
`models.yml` `contextWindow` = `n_ctx_slot` (NOT the model's native window)
|
||
or long conversations die with `400 exceed_context_size_error` instead of
|
||
compacting; compaction/summary budgets sized to the old window get
|
||
re-checked; endpoint docs and the rollback table move too.
|
||
5. **Prove behaviour:** boot-log slot count, `nvidia-smi` free in band, N
|
||
concurrent requests answered in parallel with N+1 queued, and — if a
|
||
projector ships — a solid-color image probe through an **`image_url`
|
||
content part** expecting the exact color. The legacy top-level `images`
|
||
field is silently ignored, so a probe that uses it measures a
|
||
hallucination, not vision.
|
||
|
||
Gotchas: shorter slots can make decode FASTER (~5 % measured); the KV pool is
|
||
allocated whole at boot so post-boot `memory.used` IS the ceiling; record both
|
||
measured rows in the service `.env` comment block and mirror the non-KV price
|
||
in the bats budget gate so a shape that busts the band fails the suite instead
|
||
of the boot.
|
||
|
||
## 2. Per-device VRAM accounting ("where did the memory go?")
|
||
|
||
First decide which kind of memory is in question:
|
||
|
||
| device | kind | read it with |
|
||
|---|---|---|
|
||
| RTX 3090 (Dankclaw) | discrete GDDR6X | `nvidia-smi --query-gpu=memory.total,memory.used,memory.free` |
|
||
| gfx1151 iGPU (Dankclaw) | **UMA** — "VRAM" is host RAM | `/sys/class/drm/card1/device/mem_info_gtt_{total,used}` vs `mem_info_vram_*`; the aperture and carve-out values are [[Dankclaw System Specs]]'s |
|
||
| R9700s (Dankserver) | discrete | `rocm-smi --showmeminfo vram`; the per-card byte/MiB value (and the stale "64 GiB SVM-backed" figure it replaced) is [[Dankserver System Specs]]'s |
|
||
|
||
Per-process truth on NVIDIA:
|
||
`nvidia-smi --query-compute-apps=pid,process_name,used_memory`. Never add the
|
||
iGPU's GTT to host RAM as a separate pool; `free -g` is the arbiter for
|
||
co-tenants.
|
||
|
||
- **Engine ledger first.** Halogen prints weights pinned (+ remaining 2 MiB
|
||
contiguous blocks — the honest fragmentation signal: a block census that drops
|
||
across a restart means the model did not come back into the same hole; the
|
||
before/after census this was learned on is that era's, on [[frank-dragoon]])
|
||
device budget, KV pool/working split, "left for everything
|
||
else". llama.cpp prints them at `llama-model.cpp:1827` (per-backend model buffer)
|
||
and `llama-context.cpp:692` (compute buffer) on the era's build; on today's
|
||
`master` the same `LLAMA_LOG_INFO` lines sit at `llama-model.cpp:2009`
|
||
("model buffer size =") and `llama-context.cpp:842` ("compute buffer size =")
|
||
([source](https://raw.githubusercontent.com/ggml-org/llama.cpp/master/src/llama-context.cpp)
|
||
re-read 2026-10-18) — but **`llama-bench`
|
||
suppresses loader INFO and a systemd unit did not put them in the journal**
|
||
(verified `grep -c` = 0). Recipe: one launch through `llama-cli`/`llama-server`
|
||
keeping the first ~200 lines.
|
||
- **Otherwise compose from tensor sizes.** Group the GGUF tensor table by
|
||
role (Flash-Next example: 75.0 GiB experts at 1.5625 GiB/layer, 32.8 GiB PLE
|
||
table, 3.2 GiB everything else), KV ≈ 2 KiB/position per f16 attention
|
||
layer, × derived context. Vendor delta is real: same layers+KV cost 12.80
|
||
GiB under Vulkan vs 14.99 GiB under CUDA (~3.3 vs ~1.1 GiB of
|
||
graph/compute pool).
|
||
- **Traps:** mmap'd PLE is reclaimable page cache; `-ot` makes it pinned —
|
||
"move layers to the card" converts reclaimable cache into device memory and
|
||
frees nothing for co-tenants. A served model's footprint is not its weight
|
||
size: the MiMo Q5_K_M era booted a 6.40 GiB payload to **20,530 MiB used** of the
|
||
3090's addressable 24,576 — the Eras row and its addressable ≠ advertised law are on
|
||
[[frank-zealot]]. What the card reports used is weights + KV + compute buffers +
|
||
projector, never weights alone. Do
|
||
the capacity subtraction before designing, on the card number the owner page
|
||
quotes, not the advertised one.
|
||
`n_ctx = n_prompt + n_gen + n_depth` (defaults 512/128/0, type_k/v F16,
|
||
flash_attn AUTO, split_mode LAYER). `-p 512,16384 -n 64` = contexts
|
||
576/16,448; a plain `tg` row has ~zero depth — `-d` is the only thing that
|
||
puts KV in the picture.
|
||
- **Label every number** measured / derived / inference, and state the context
|
||
each figure was taken at — 12.8 GiB at 65,568 tokens is not the same claim
|
||
as at 200k.
|
||
|
||
## 3. Prompt cache and state sizing
|
||
|
||
`--cache-ram` (alias `-cram`, `common/arg.cpp:1719`) is HOST RAM for saved
|
||
prompt states — unpinned, on demand, moves no VRAM. `--ctx-checkpoints` (alias
|
||
`-ctxcp`/`--swa-checkpoints`, `common/arg.cpp:1701`) is a different mechanism:
|
||
per-slot KV snapshots; `0` disables them. Both flags are current upstream; the
|
||
two defaults quoted here (8192 MiB and 32) are the era's values and were not
|
||
re-readable in the upstream sources checked *(unverified 2026-10-18)* — print
|
||
them from the binary (`llama-server --help`) before sizing on them.
|
||
|
||
**State law** (compute before trusting any cap): a state is the serialized KV
|
||
of the whole sequence + constant recurrent state + the draft context's KV:
|
||
`per_token = 2 × n_full_attn_layers × n_kv_heads × head_dim × bytes (K and V) +
|
||
draft_per_token`. The worked qwen35-hybrid instance of that formula (full layer
|
||
count, f16 KV, MTP draft) and the slot-size table it prices are on
|
||
; cross-check any such arithmetic against the
|
||
journal's `prompt state size` lines — the two must agree.
|
||
|
||
**The journal is the instrument:** `exceeds cache size limit … skipping` =
|
||
cap binding in production; `making room … removing oldest` / `cache size
|
||
limit reached` = eviction, and the printed size proves the active cap. Do NOT
|
||
claim a raise is proven by `prompt_tokens_cached_total` rising — on exact
|
||
repeats that counter is 0 by construction.
|
||
|
||
**Hybrid + zero-checkpoint trap:** for a hybrid (DeltaNet/SSM + attention)
|
||
model, `seq_pos_min` sits at the END of the slot's own prompt (a recurrent
|
||
cell stores only the last position), so with `--ctx-checkpoints 0` there is no
|
||
checkpoint to fall back on and the server logs `forcing full prompt
|
||
re-processing … (likely due to SWA or hybrid/recurrent memory)`. Consequences:
|
||
intra-slot reuse only survives when the request echoes the assistant's
|
||
previous reply verbatim (a pure user resend is always a full re-prefill even
|
||
at f_sim 1.000); the RAM prompt cache becomes the ONLY carrier of a
|
||
conversation state ACROSS slots, so a cap that refuses deep states costs
|
||
minutes of re-prefill whenever slots are reassigned; state restoration only
|
||
runs via the LRU path (or LCP with f_keep < 0.5) — a plain prefix hit never
|
||
calls `prompt_load`.
|
||
|
||
**Verify a change:** live argv carries the flag (not the deploy log); `/health`
|
||
200 + real completion; VRAM unchanged and host RAM mostly free; no new
|
||
`exceeds cache size limit` lines, with a `making room` entry larger than the
|
||
old cap as direct proof. If the ceiling itself was not exercised live, say so —
|
||
crossing it needs a >120k-token conversation and must not run beside a live
|
||
service (bench law).
|
||
|
||
## 4. QSA flash-attn assert fix (qwen4exp)
|
||
|
||
`--flash-attn on` aborts graph build for qwen4exp models:
|
||
`GGML_ASSERT(a->type == GGML_TYPE_F32)` at `ggml.c:5371` through
|
||
`build_attn_qsa`. Root cause: `build_attn_qsa` calls `ggml_fill` on the
|
||
F16-cast kq mask returned by `get_kq_mask()` when flash attn is on, but
|
||
`ggml_fill` requires F32.
|
||
|
||
Fix (mirrors the MLA sparse path in `llm_graph_context::build_attn`): run the
|
||
top-k mask surgery on the F32 base mask (`inp->self_kq_mask`), cast to F16
|
||
only at the attention call when `flash_attn` is enabled
|
||
(`kq_mask_attn = cparams.flash_attn ? ggml_cast(ctx0, kq_mask_top_k,
|
||
GGML_TYPE_F16) : kq_mask_top_k`). The patch lives at
|
||
`scripts/patches/qwen4exp-qsa-f32-mask.patch`.
|
||
|
||
Only the charlie12345/ROCmFPX build-qwen38 (PR #27742 qwen4exp merged) takes
|
||
it; CIRU, upstream and stock builds do not. Validate live: load with
|
||
`--flash-attn on`, health 200, a real completion finishing `"stop"`, journal
|
||
clean of `GGML_ASSERT`/`cudaMalloc`/`missing tensor`, and flash-attn engaged
|
||
in the timings. The context cap that build reached for full-model residency is
|
||
part of that era's record on [[frank-dragoon]].
|
||
|
||
## Related
|
||
|
||
- [[GGUF Compatibility]] — what a payload is before it is priced
|
||
- [[frank-zealot]] — the endpoint whose Eras rows are the two-boot priced shapes this method produced
|
||
- the state-law worked example behind section 3
|
||
- [[DFlash DSpark Speculative Decoding]] — the spec A/B protocol on llama.cpp
|
||
- / — the multi-device side
|