Files
serverAppConfigs/serverAppConfigs-docs/LLM/RDNA4 MoE Expert Offload VRAM Budget.md
T
Frank a61308d72e docs(vault): audit tranche 1 — one-owner dedupe of the observability cluster, dead-shape numbers destroyed
Purpose-test pass over the audit findings (every verdict re-verified
against the file before touching it; three scout findings were hallucinated
— a nonexistent filename, my own changelog line, and sanctioned Eras rows):

- Observability Provisioning: destroyed the 63-line 'Current delivered
  state' block — chain topology/relay/delivery restates [[Observability]]
  and [[Alertmanager]] (those are the owners). Its three sole-custody laws
  MOVED to Stale Signal Audit as 'Three rules that outlive the incident':
  down-beside-healthy -> read lastError; Prometheus alert state in process
  vs Alertmanager's --storage.path persistence; candidate-arm metrics are
  never fleet state.
- RDNA4 MoE VRAM budget: kept the laws (registry over-prices the pool;
  never reuse a B/token ratio across geometries; pin pool bytes
  floor-sized), era-labelled the rates, destroyed the measured ladder
  table for the superseded shape-G/bf16 rungs.
- Stale Signal Audit: new rules section + Provisioning Related link.

check-okf 109/0, vault links 0 unresolved, docs seams green.
2026-10-21 02:35:44 -07:00

211 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: reference
title: RDNA4 MoE Expert Offload VRAM Budget
description: "Expert-offload arithmetic on the RDNA4 fork: movable terms, the utilisation law, graph charge, ceilings, context ladder."
tags: [vllm, expert-offload, vram, arena, kv-cache, ladder, tcclaviger]
timestamp: 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
## 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
(era G measured ~14 % under the planned B/token — the gap is the lesson, not
the number; **never reuse a B/token ratio across geometries**), and there is a
~4 MB block-alignment loss to subtract when converting a token target to
bytes. The ask lands block-aligned: verify against the engine's own
`GPU KV cache size: N tokens, Maximum concurrency … X.Xx` line at boot.
**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. The B/token rate is a property
of the GEOMETRY: era G (3 × 200,704 fp8, vLLM fork) measured **7,891.8**;
the radiance-era hybrid measured ~7,700 fp8 / ~13,800 bf16 at its own shape.
Read your live rate from the boot ledger every time; the decomposition (a
dtype-independent mamba/side stream rides on top of the attention KV) is why
the ratio moves. 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-size` refused for GQA when `TP ≤ 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.
1. **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 covers
`seqs × MAXLEN` from the engine's own line.
2. **Boot under the PRODUCTION environment, not a shell probe** — a rootless
shell lacks `LimitMEMLOCK=infinity` and 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_FILE` hook.
3. **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.
4. **Capture the engine log per rung BEFORE teardown** (`podman logs` dies
with `podman rm`; a `podman logs` issued too soon after `podman run -d`
fails with *no container with that name* — record UNKNOWN, never FAIL).
5. **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 PyTorch` died in graph CAPTION;
`Tried to allocate 10.28 GiB … 9.46 GiB free` died in KV ALLOCATION.
6. **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 for `MemAvailable` to recover.
**The hazard that cost production:** a leftover ladder script whose teardown
`podman rm -f`s 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.
## 7. Arena audit procedure (when asked for "more experts")
1. Read the live plan before proposing anything — `grep -oE` with a
digit-anchored tail (plain grep/journalctl truncate these columns to
`17.`):
```
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})"'
```
2. Classify each term (§1 table); take only the two reachable levers.
3. Verify after deploy: plan lines show new budget/graphs/arena;
`GPU KV cache size` matches the ask; graph-estimate vs post-capture;
`rocm-smi` high-water per card = the real margin;
`systemctl show frank-archon -p NRestarts --value` = 0; a real chat
completion from a PEER (health-probe via `192.168.0.235:7770/health`, not
`localhost` inside an ssh session on the host — the latter returns `000`
and looks like an outage).
## Related
- the bring-up path and container traps
- capture and contracts
- [[frank-archon]] — measured history
- [[Llama Cpp Serving Sizing]] — the llama.cpp equivalent