Enforces "one fact, one owner": technique pages keep mechanism, laws, traps and verdicts; per-endpoint shapes, digests and era measurements are cited to the endpoint page that owns them. Also repairs the four pointers the pass left empty (each verified against the cited owner before landing): - EMULATION dequant doubles host RAM (118 GiB -> ~238 GiB) -> [[frank-archon]] - 12.11 tok/s flat 304 -> 16,095 tokens -> [[Tcclaviger Fork Ops]] - bundled-head acceptance ladder 0.739/0.554/0.427/0.340 -> [[Qwen3.8 PP Deployment]] - llama.cpp PR #23897 dedup provenance -> [[qwen3.8-spec-matrix]] Replaces the retired-shape illustration in Llama Cpp Serving Sizing with the owned MiMo Q5_K_M row (6.40 GiB payload -> 20,530 MiB used), and unwraps a hard-wrapped [[Dankserver System Specs]] link that failed link integrity. Orphan census over the diff: 0 numbers lost (comma-insensitive match against the whole vault plus AGENTS.md).
12 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| reference | RDNA4 MoE Expert Offload VRAM Budget | The expert-offload plan arithmetic on the tcclaviger fork — which terms a deploy can move, the util law, the CUDA-graph charge, what free VRAM really is, knife-edge survivability, parallelism ceilings and the context ladder. |
|
2026-10-05T00:00:00-07:00 |
RDNA4 MoE Expert Offload VRAM Budget
For VRAM-bound vLLM on the tcclaviger RDNA4 fork. Every boot ledger, arena figure and rate below is from frank-archon's tcclaviger era (2026-09-29 → 10-05: Qwen3.8-Flash-Next FP8, TP=2, 2x R9700 at 31.86 GiB each, expert offload to host RAM) — that page owns the endpoint's shape, tuning history and era record; this page owns only the plan arithmetic, the ladder method and the audit procedure. Boot-failure capture lives in vLLM Boot Forensics and Config Interrogation.
1. The plan law
arena = budget − non_expert − kv_reserve − cuda_graphs − runtime_overhead
budget = device_total × gpu_memory_utilization # expert_plan.py build_plan
slots = arena // expert_bytes # 2.344 MiB/slot ⇒ 437 slots/GiB
The arena is DEVICE-resident, so the plan predicts reality: the fp8
3-window shape's plan totalled 28.27 GiB and rocm-smi measured exactly
28.27 GiB. weights + pinned_pool + graphs + arena + non-torch (≈1.45 + 0.57 driver reserve) vs 31.86 GiB predicts a boot failure before it happens.
Which terms a deploy can move:
| term | reachable? | how |
|---|---|---|
| budget (util) | yes, law-capped | --gpu-memory-utilization; +0.01 = +0.32 GiB on 31.86 GiB cards |
| CUDA graphs | yes | container env VLLM_CUDAGRAPH_MEM_BASE_MB / _PER_GRAPH_MB (or _ESTIMATE_MB for an explicit total) |
| non-expert weights | no | checkpoint census |
| KV reserve | no | registry constant: 9190 B/token × max_model_len × 1.08 + per-seq mamba pages |
| runtime overhead | partly | --expert-precise-memory swaps default 7.60 GiB for the registry-fitted 5.25 GiB |
Anything else is an IMAGE EDIT, not a deploy — say so; do not invent a flag
(verify with podman exec <ctr> grep -n getenv …/offloader/expert_activation_registry.py).
UTIL LAW: 0.982, NEVER 1.00 (measured 2026-10-03). The CLI field is
Field(gt=0, le=1), but the worker init demands free > budget: at 1.0
budget == device total and BOTH ranks raise
ValueError: Free memory on device cuda:N (31.29/31.86 GiB …) BEFORE a
single weight loads — a crash loop (NRestarts ~2 min apart, zero payload
progress). Not a knife-edge: unbootable. Even bootable near-ceiling
shapes die under real load (0.13 GiB free bf16 boot; 0 bytes free on a real
995-token request).
2. The CUDA-graph term: charge the measured capture
Defaults (256 MiB base + 52 MiB/graph) over-charge badly. Measure with the
engine's own post-capture line Graph capturing finished … took X GiB (per
rank; heavier rank binds). Measured on the no-spec piecewise plan: 0.31/0.17
GiB, and one boot reported a NEGATIVE −0.06 GiB delta (capture fit inside
pages the profiler had already counted) — the analytic base pool is pure
over-charge. Shipped: BASE_MB=0, PER_GRAPH_MB=12 = 0.07 GiB — per-graph,
so a spec-decode shape (18 graphs) still pays (0.21 GiB). An explicit
_ESTIMATE_MB total does NOT scale with ladder changes; base+per-graph does.
Verify every deploy: Estimated CUDA graph memory line vs the Graph capturing finished line; if capture exceeds the charge, raise _PER_GRAPH_MB
back toward 52. Check VLLM_CUDAGRAPH_* actually present in
podman exec … env.
3. The pool is invisible to the plan; free VRAM is the estimator gap
--kv-cache-memory-bytes does NOT appear in the arena arithmetic — the plan
reserves the registry price, not the actual pool. Raising the pool consumes
real free VRAM without growing the arena; the registry over-prices the pool
(9190 B/token planned vs 7,891.8 measured at 200704 fp8 — 7,701 at 262144;
never reuse a B/token ratio across geometries, and subtract ~3.87 MB
block-alignment loss when converting a token target to bytes). The ask lands
block-aligned: 3,166,500,000 B → 401,408 tokens; verify with
GPU KV cache size: N tokens, Maximum concurrency … X.Xx.
Driver-reported free VRAM after hours of traffic is NOT allocatable arena. It is the estimator gap: the registry over-prices KV and charges 5.25 GiB overhead against ~2.5 measured; torch's allocator does not return slabs, so it reads as the high-water mark. The only reachable levers are util (within the law) and the graph charge; everything else needs a registry edit inside the image — the fork's own log line says as much.
MNBT is arena-free up to the anchor: registry overhead =
base + 0.22 GiB × max(0, (nbt−4096)/1024) anchored at nbt 4096, so rungs
1024/2048/4096 are plan-identical (the fitted 5.25 GiB was fitted AT 4096).
MNBT changes re-key the compile range (one recompile, ~24 s at (1,2048) with
the mounted cache). 512 doubles TTFT at 4064 ctx; 12288 costs ~2.5 % plus
arena. MNBT does not move the KV pool.
4. Registry "compile headroom" is not reclaimable slack
--expert-precise-memory selects the fitted overhead (5.25 GiB, this arch at
TP=2) over the default (7.60 GiB = fitted + 2.35 GiB of compile and
graph-capture headroom the registry documents as "transient VRAM the
profiler never sees"). It moves exactly one term — the plan's room — so the
arena grows by that 2.35 GiB (~1,025 slots). That headroom is the budget
for the phase that allocates LAST: reclaiming it requires paying elsewhere
(smaller pinned pool, fewer sessions, shorter context). Measured 2026-10-01:
bf16 2×262,144 + precise put the total at 31.73 of 31.86 GiB and the boot
crash-looped at graph capture; dropping the flag restored ~2.4 GiB and booted.
5. Boot ≠ survival (knife-edge shapes)
A shape can pass EVERY boot gate — weights, profile, KV allocation, graph
capture, /health 200 — and die on the first real request. Measured
2026-10-01: 5 × 262144 @ fp8 booted (KV 1,350,182 tokens / 5.15x) and was
killed seconds later by a 995-token prompt with max_tokens=261149:
torch.OutOfMemoryError: Tried to allocate 50.00 MiB … 0 bytes free. After
any knife-edge change: send a LARGE-max_tokens request before calling it
done, and read post-boot free VRAM per rank (rocm-smi --showmeminfo vram)
— the survivor had ~3.6 GiB free; the corpse had 0.
KV accounting — pin the pool BYTES, floor-sized: set
--kv-cache-memory-bytes PER RANK to the contract floor
(seqs × MAXLEN × measured B/token), not the native window — extra pool bytes
come out of the arena/headroom and buy nothing. Measured B/token on this
hybrid: 7,701 fp8 / 13,839 bf16 (decomposed: attention 6,138/12,276 +
dtype-independent mamba/side 1,563). Measured rates are shape-dependent —
7,891.8 at the 200704×3 shape. Capacity does NOT scale with SEQS: the
planner's KV reserve is for ONE max-model-len request.
Parallelism ceilings you cannot flag past:
--decode-context-parallel-sizerefused for GQA whenTP ≤ num_kv_heads: "requires TP (2) to be greater than the model's total number of KV heads (2)" — DCP shards KV along the sequence dimension AFTER KV heads spread across TP ranks (it reuses TP ranks, not TP×DCP), so a 2-KV-head model needs TP ≥ 4 = four GPUs.- EP (
--enable-expert-parallel+--all2all-backend) exists but REPLACES the fork's local tiered-offload gather with a per-layer PCIe all-to-all. - DP requires a full model copy per replica — on a model larger than the card it is impossible, not slow.
6. The context ladder (find the max bootable window by measurement)
"Max possible" = the largest value that BOOTS, measured — never the model's spec maximum, never hand arithmetic.
- Scale the pool with the window. If KV_BYTES is pinned, it must shrink
in lockstep with MAXLEN or the rung tests nothing:
KV_BYTES = round(KV_BYTES_ref × (seqs×MAXLEN_new) / (seqs×MAXLEN_ref))off a booted reference rung. Verify the rung's capacity still coversseqs × MAXLENfrom the engine's own line. - Boot under the PRODUCTION environment, not a shell probe — a rootless
shell lacks
LimitMEMLOCK=infinityand can pass where production OOMs. Transient unit:sudo systemd-run --uid=<svcuser> --unit=probe-$ML -p LimitMEMLOCK=infinity -p Restart=no env <ENVFILE_OVERRIDE> <serve-script>pointing the serve script at a scratch env file via its*_ENV_FILEhook. - Judge on health + unit liveness: poll
/health(200 up / 503 loading / 000 not bound); break early when the unit goes inactive — that is the failure signal, not a timeout. - Capture the engine log per rung BEFORE teardown (
podman logsdies withpodman rm; apodman logsissued too soon afterpodman run -dfails with no container with that name — record UNKNOWN, never FAIL). - Ladder down on FAIL, one bonus step UP on PASS. Measured steps on this
box: OOM −16,384 tokens, bonus +4,096. Tiny margins (76 MiB class) mean
the next rung down almost certainly passes:
Tried to allocate 76.00 MiB … 0 bytes free … 30.43 GiB allocated by PyTorchdied in graph CAPTION;Tried to allocate 10.28 GiB … 9.46 GiB freedied in KV ALLOCATION. - Clean between rungs: stop the transient unit,
podman rm -f, kill orphans by explicit PID (ps aux --sort=-rss | awk '/vllm\/bin\/python3/ && $6 > 50000000 {print $2}'), wait forMemAvailableto recover.
The hazard that cost production: a leftover ladder script whose teardown
podman rm -fs the production container fires against a freshly DEPLOYED
engine. Teardown runs ONLY on failure, never after a pass; kill the whole
scratch process tree and verify none remain BEFORE deploying. On PASS: print
the measured GPU KV cache size / Maximum concurrency line, LEAVE IT
SERVING, and pin the number into the repo (env + seam + harness
contextWindow) before deploying.
Cutover order: ladder → winner passes and serves → repo env + seam + harness
→ tests → commit+push → verify no scratch scripts remain → stash foreign work
(git stash -u) if the tree is dirty → deploy (one restart) → verify health
- engine flags in the log.
Measured ladder on this box (200704×3, fp8 KV, precise memory):
| shape | util | graphs | arena | result |
|---|---|---|---|---|
| E | 0.98 | 0.55 | 17.22 GiB / 7,522 slots | boot ok |
| F | 0.982 | 0.32 | 17.52 GiB / 7,653 slots | boot ok |
| 1.0 | 1.00 | 0.07 | — | UNBOOTABLE (worker-init ValueError, both ranks) |
| G (deployed) | 0.982 | 0.07 | 17.77 GiB / 7,762 slots | KV 401,408 tokens / 2.00x; 0.89 GiB real free/card held through a live 4,096-token generation at ~38–39 tok/s |
bf16 ceilings at util 0.98: 3 seqs → 241,664 passes / 245,760 and 262,144 OOM at graph capture; 6 seqs fp8 → 10.70 GiB ask with 9.18 GiB free (fails) — ~207k per context max.
7. Arena audit procedure (when asked for "more experts")
-
Read the live plan before proposing anything —
grep -oEwith a digit-anchored tail (plain grep/journalctl truncate these columns to17.):ssh dankserver 'sudo journalctl CONTAINER_NAME=frank-archon-qwen -b -o cat --no-pager \ | grep -aoE "(budget \(total x util\) +[0-9.]+|expert cache space +[0-9.]+ GiB +[0-9,]+ slots|Estimated CUDA graph memory.{0,80}|Graph capturing finished.{0,40}|GPU KV cache size.{0,90})"' -
Classify each term (§1 table); take only the two reachable levers.
-
Verify after deploy: plan lines show new budget/graphs/arena;
GPU KV cache sizematches the ask; graph-estimate vs post-capture;rocm-smihigh-water per card = the real margin;systemctl show frank-archon -p NRestarts --value= 0; a real chat completion from a PEER (health-probe via192.168.0.235:7770/health, notlocalhostinside an ssh session on the host — the latter returns000and looks like an outage).
Related
- RDNA4 Container Serving — the bring-up path and container traps
- vLLM Boot Forensics and Config Interrogation — capture and contracts
- frank-archon — measured history
- Llama Cpp Serving Sizing — the llama.cpp equivalent