docs: vault link integrity restored after rogue-session doc WIP landed
The parallel session's stash pop left GGUF Compatibility, Multi-Device Splits, Serving Sizing, PCIe Link and Atomics, RDNA4 Container Serving, vLLM Porting triage and RDNA4 MoE Budget unindexed and carrying 9 unresolved wikilinks (LLM Bench Windows / Model Swap / AGENTS targets that never existed, a [[nodiscard]] C++ attribute parsed as a wiki link, and two multi-line-wrapped link tokens). All repointed to real pages; six rows added to the vault index; seam green.
This commit is contained in:
1 parent
380aafa1cc
commit
a6ad9af708
10 files changed
+2013
-1
No files matched your search
@@ -0,0 +1,219 @@
|
||||
---
|
||||
type: reference
|
||||
title: RDNA4 MoE Expert Offload VRAM Budget
|
||||
description: 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.
|
||||
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 (frank-primary: Qwen3.8-
|
||||
Flash-Next FP8, TP=2, 2x R9700 at 31.86 GiB each, expert offload to host RAM).
|
||||
Tuning history and the deployed shape live in
|
||||
[[dankserver-qwen38-fp8-expert-offload-tuning]]; 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-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.
|
||||
|
||||
**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")
|
||||
|
||||
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-primary-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-primary -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
|
||||
|
||||
- [[RDNA4 Container Serving]] — the bring-up path and container traps
|
||||
- [[vLLM Boot Forensics and Config Interrogation]] — capture and contracts
|
||||
- [[dankserver-qwen38-fp8-expert-offload-tuning]] — measured history
|
||||
- [[Llama Cpp Serving Sizing]] — the llama.cpp equivalent
|
||||
Reference in new issue
Block a user