Files
serverAppConfigs/serverAppConfigs-docs/LLM/RDNA4 Container Serving.md
T
Frank 04f4e2c6d4 docs(vault): strip restated fleet facts from 17 technique pages
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).
2026-10-19 01:33:47 -07:00

259 lines
14 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 Container Serving
description: Bringing a model larger than VRAM onto Dankserver's 2x R9700 (gfx1201) with the tcclaviger vLLM stack — quant-class gating, fetch verification, container GPU-init triage, memory budgeting and the expert-offload plan.
tags: [rdna4, gfx1201, r9700, vllm, expert-offload, podman, rocm]
timestamp: 2026-10-05T00:00:00-07:00
---
# RDNA4 Container Serving
Bring-up path for Dankserver (2x Radeon AI PRO R9700, gfx1201 — card count,
per-card VRAM and host RAM are [[Dankserver System Specs]]'s) with
`docker.io/tcclaviger/vllm` (vLLM 0.29.0.dev, GLM/Qwen-specialised RDNA4 build
with native MoE quant kernels and a first-class expert offload planner). The
tuning history and current deployed shape live in
[[frank-archon]]; this page is the bring-up law.
## 0. Know the box — and its phantom third GPU
`torch.cuda.device_count()` returns **3**; summing `total_memory` overstates
the box as 186.7 GiB. The third device is the 7700X's integrated Vega
(gfx1036): torch reports ~123 GiB for it, `rocm-smi` reports 0.50 GiB,
allocating on it moves system `MemAvailable` 1:1 (it is GTT over DDR5, not
dedicated VRAM), and it dies with `hipErrorInvalidKernelFile` when the image
ships no code for that arch. **Never sum device `total_memory`; never count
the iGPU as headroom or a workload.**
## 1. Preflight: is the quant class EXECUTABLE, not merely present?
Expert offload is mandatory when weights exceed VRAM, and it requires the
checkpoint's experts class to have BOTH a compiled kernel AND an
`offload_recipe` ("a pinned expert has no host copy, so there is no
read-through fallback" — startup aborts otherwise). Check BEFORE downloading
anything:
```bash
podman run --rm --device /dev/kfd --device /dev/dri --device /dev/mem \
-e HIP_VISIBLE_DEVICES=0,1 --entrypoint bash $IMAGE -c '
nm -D /app/r4dhip/r4d.so | grep -oE "gemm_moe_[a-z0-9_]+" | sort -u
grep -rl offload_recipe /opt/vllm/lib/python*/site-packages/vllm/model_executor/layers/fused_moe/'
```
| quant | kernel in `r4d.so` | experts class | verdict |
|---|---|---|---|
| FP8 | yes | `Fp8HipMoEExperts` | **serves** |
| MXFP4 / DSFP4 | yes (`gemm_moe_mxfp4a8_*` / `dsfp4a8_*`) | `R4dMxfp4MoEExperts` / `R4dDsfp4MoEExperts` | **serves** |
| W4A16 | **no** `gemm_moe_w4a16_*` | `R4dW4a16MoEExperts` exists | silently falls back to generic `TritonWNA16Experts`, no recipe → startup halts |
| RFA | none | no class at all | out |
Also confirm the architecture routes natively with NO overlay
(`ModelRegistry.get_supported_archs()` for `Qwen4Exp`/`Glm5`). The stock image
natively carries Qwen3.8/Qwen4Exp and GLM-5.x including MTP heads. **Do not
apply vendor overlays** against a fast-moving pre-release image — the breakage
misleads (a missing symbol in the overlay's OWN copy of a module masks the
real cause, then moves to another symbol each patch;
`cannot import name 'checkpoint_has_lm_head'` = overlay clobbered
`transformers_utils/config.py`). The image ships quantizers for
`rfa|rfi|mxfp4|mxfp4ocp|paro5` under `/app/tools/quantize.py`
(`--fp8-attn --quant-only --dry-run` first) if a re-quant is needed — note it
can run MXFP4/DSFP4/FP8 but only PRODUCE rfa/rfi/mxfp4/paro.
## 2. Fetch with verification, always
Gated repos: token never in argv (`pgrep -a curl` prints command lines).
Build a mode-600 curl config once:
```bash
{ printf 'header = "Authorization: Bearer '; cat ~/.cache/huggingface/token; printf '"\n'; } \
> ~/.cache/huggingface/curl.conf && chmod 600 ~/.cache/huggingface/curl.conf
curl -K ~/.cache/huggingface/curl.conf -f -sS -L -C - -o "$out" "$url"
```
`--fail` is MANDATORY — `curl -o FILE URL` exits 0 on HTTP 401/403 and writes
the error body as a "successful artifact" (a gated repo once yielded three
153-byte shards with identical sha256s and a cheerful DONE banner). `-C -`
resumes torn shards; HTTP/2 cancels leave silently truncated files, so
re-verify EVERY shard's byte count against the Hub manifest (and the LFS
sha256) before serving. The AGENTS.md Secret Rules carry the token-hygiene
law; [[frank-medium-model-swap]] has the same detail for GGUFs and
`.partNofM` fragments.
## 3. Serve — the known-good invocation and its traps
```bash
sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'
podman run -d --name <ctr> --ipc=host --ulimit memlock=-1:-1 \
--cap-add SYS_PTRACE \
--device /dev/kfd --device /dev/dri --device /dev/mem \
-e HIP_VISIBLE_DEVICES=0,1 -e VLLM_ROCM_USE_AITER=0 \
-v /home/dank/models/<model>:/models:ro \
$IMAGE /models --served-model-name <name> --port 7770 \
--tensor-parallel-size 2 --max-model-len 65536 \
--kv-cache-memory-bytes 2147483648 \
--max-num-seqs 4 --gpu-memory-utilization 0.95 \
--enable-prefix-caching --trust-remote-code
```
- **Entrypoint already execs `vllm serve "$@"`** — pass the model path as the
first argument and do NOT pass `serve` (podman then tries to pull `serve`
as an image name). `-e VAR=1` must come BEFORE the image name.
- **Mount the whole `/dev/dri`, never individual render nodes** (§4).
- `--ipc=host` and `--shm-size` are mutually exclusive in podman.
- `podman run -d` without `-a` sends container stdout nowhere — read
`podman logs <ctr>`; and vLLM writes `Application startup complete` to
**stderr**, so a `podman logs X 2>/dev/null | grep -q` readiness gate never
fires.
- Start at 65536, not the native ceiling; prove the plan, then scale.
- Load takes ~5 min with `/health` refusing the whole time — do not kill the
process (deploy.sh waits 20 min).
## 4. Container GPU-init triage (ordered)
1. **Enumerate-vs-context trap (most common):** individual
`renderD*` nodes give HIP the render nodes WITHOUT the primary `card*`
nodes — it enumerates agents (`device_count()` returns N) and then refuses
every context: `RuntimeError: No CUDA GPUs are available` on the first
real op. A/B-confirmed 2026-09-19. Rule: `--device /dev/kfd --device
/dev/dri --device /dev/mem`; restrict GPUs with `HIP_VISIBLE_DEVICES`,
never with device nodes.
2. **`device_count()` is not evidence.** Prove usability with a real op
(`(a@a).sum().item()` on a fresh `torch.randn`), then repeat via
`mp.spawn`, `mp.fork`, and `subprocess.run([sys.executable, ...])`, and
cross-check `rocm-smi --showmeminfo vram`.
3. **memlock rlimit → a MISLEADING CUDA OOM.** Host default 8192 KB; a pin
larger than the limit fails as
`torch.AcceleratorError: CUDA error: out of memory` raised from
`pin_memory()` — it names the accelerator but the fault is host pinned
memory. It cannot be raised from inside a container or even from root's
own shell (`Operation not permitted`); only PAM applies it at login, so
fixes only affect NEW sessions:
`/etc/security/limits.d/99-memlock.conf` with
`* - memlock unlimited` + `root - memlock unlimited`; the container needs
`--ulimit memlock=-1:-1` to inherit. Discriminating test after the fix:
the container reports `unlimited` AND small pins succeed; if pins still
fail, memlock was never the cause.
4. **`/proc/meminfo` TAB trap:** the key/value separator is a TAB, so
`grep MemAvailable /proc/meminfo | tr -s " " | cut -d" " -f2` returns
EMPTY and a following awk prints `0.0` — a false "host out of memory" on a
host with hundreds of GiB free, which invites aborting something healthy.
Tab-safe: `awk -v k='MemAvailable:' '$1==k {printf "%.1f", $2/1048576;
exit}' /proc/meminfo`, cross-check `<= MemTotal`, refuse to print a zero.
Use `scripts/dankstatus.sh` instead of hand-rolling.
5. **Silent-parse traps:** `grep -c PATTERN file || echo 0` yields TWO lines
(grep prints 0 AND exits 1) — `[: 0\n0: integer expected`; use
`| head -1` or `|| true`. `systemctl is-enabled` can emit multiple lines.
`rocm-smi --showmeminfo vram --csv` column order is `device, VRAM Total
Memory (B), VRAM Total Used Memory (B)` — reading $3 as total/$4 as used
yields 0.00; filter to `^card[0-9]+$`.
6. **Guard the box:** long loads with co-tenants can drive RAM to 0 — abort
before the OOM killer (`MemAvailable < 4 GiB → podman rm -f`, exit 1).
7. **Drop page cache before a large load** (`echo 3 > /proc/sys/vm/drop_caches`)
and confirm the drop was actually in effect for the run before treating a
failure as a capacity wall — a stale "it doesn't fit" from a contaminated
run is expensive to unwind.
## 5. Memory budgeting: the levers are not where they look
Every figure in this section is a tcclaviger-era boot on the port that is
[[frank-archon]]'s today; the laws they teach are general. The planner builds
`budget = total × util`, subtracts a KV reserve, and gives
the REMAINDER to the expert arena. Consequences, all measured:
- **`--gpu-memory-utilization` grows the ARENA, not the KV pool** (KV stayed
0.64 GiB at util 0.95 and 0.98). The arena is the only elastic term.
- **Lowering `--max-model-len` is the WRONG lever and backfires:** a smaller
context shrinks the reserve, handing the space to the arena — measured 65536
→ 0.64 GiB KV, 32768 → 0.34 GiB KV.
- **Hybrid-attention undercount:** the planner's per-token KV figure for
Qwen4Exp is an undercount — the printed figure and the true one are both on
[[frank-archon]] — because it misses the large fixed per-sequence Mamba state
(~2x), so the reserve is always short and the arena silently covers it.
Fix by pinning the pool: `--kv-cache-memory-bytes <BYTES>` (era reading: a
2 GiB pin = 113,595 tokens ≈ 3.47x concurrency at 32k — that pair is this
page's record of the tcclaviger boots, not [[frank-archon]]'s). For long
context,
`--kv-offloading-size` (GiB) spills KV to host.
- **`--expert-offload-mem` caps the HOST side, not the VRAM arena**
(`cap = int(x * GIB)` against `host_memory_available()`). Do not reach for
it to fix a VRAM problem; to grow the arena, shrink the KV pin or raise
util (see [[frank-archon]] for the
util=1.0 unbootability law).
**Read the plan — it is the real acceptance test:**
```
expert offload: AUTO-ENABLED, weights 62.54 GiB per rank exceed the 30.27 GiB budget (TP2)
budget (total x util) 30.27 GiB
non-expert weights 6.28 GiB
KV reserve 0.36 GiB <- sanity-check against the model's own per-token figure
CUDA graphs 0.66 GiB
runtime overhead 7.60 GiB
expert cache space 15.36 GiB 6712 slots of 2.344 MiB
host-backed experts/rank 52.74 GiB 23040 experts
host: 105.48 GiB across 2 ranks of 160.89 GiB usable
```
Host fits when `host_total < usable`. The PLE n-gram table rides memfd and is
SHARED across TP ranks.
**Loading mechanism** (for reasoning about peak RAM): weights never materialise
in VRAM — expert Parameters start as CPU staging tensors; each layer's
`weight_loader` counts shard events and, on layer completion,
`ExpertTopology.split_layer` moves that layer's experts into the VRAM arena
and compacts the rest into exact-size pinned host tensors behind UVA. Peak
host RAM = `host-backed set + layers still loading`, not the whole model. At
runtime `apply_hot_cold` sends arena-resident experts down a hot GEMM and the
rest straight over PCIe via UVA views — no staging copy in the critical path.
**Emulation check:** a dequantize-to-compute path doubles real host RAM — the
measured pair (offload request vs host footprint) is on [[frank-archon]] — and
the doubling is invisible in the weight count. Read the log for `Using
'EMULATION' Mxfp4 MoE backend` before concluding "doesn't fit".
## 6. Performance profile and honest measurement
Decode on this pair is **PCIe-gather-bound, context-independent**: the rate is
flat from a few hundred to sixteen thousand context tokens, so KV attention is
not the cost (the flat figure and its MTP/gather deltas are
[[Tcclaviger Fork Ops]]'s profile of the same shape). With ~29 % of experts
arena-resident, under a gigabyte crosses PCIe per token at the engine's
effective read rate; cold-vs-warm differing ~10 % confirms the LRU works;
growth is linear in hit rate, and only speculative decode cuts sequential STEPS
rather than making each cheaper.
**Benchmark traps that produced fake numbers here:**
- `max_tokens=1` is not TTFT (the server can satisfy it without doing the
work — prefill looked flat at 1.26 s for 16k AND 30k prompts). A 64-token
non-streamed reply is time-to-64-tokens (decode-bound, flat ~9.6 s at every
length). Correct TTFT: stream and time the FIRST content-bearing chunk.
- A small repeating word loop is prefix-cacheable AND n-gram-predictable — a
43-word cycle reported 22,897 tok/s prefill, almost entirely cache hit. Use
a large vocabulary in shuffled order and print `cached_tokens`.
- ~1.2 s fixed per-request overhead swamps prefill below ~16k tokens; compare
prefill at 30k+ only.
- Size prompts with `/v1/messages/count_tokens` (a chat call for counting
pollutes the metric); guessing words-per-token silently overran the window
(HTTP 400).
- Always run a canary: plant a unique token at the END of a long prompt and
ask the model to quote it — proves prompts reach the model at all.
- Measure warm AND long (`podman exec` + urllib at 127.0.0.1:7770): a 33-token
reply read 14.6 cold / 20.3 warm; a 768-token generation hit 25.7; two
concurrent streams scaled to 47 aggregate.
**Levers (all opt-in):** `--expert-prefetch N` takes an INT depth, not a
boolean (default 0; at depth 1 only ~10 % cold-vs-warm difference — the cache
already works); `CLAV_TUNABLEOP_SWEEP=1` autotunes GEMM shapes (silently
skipped under expert offload unless `CLAV_TUNABLEOP_WITH_OFFLOAD=1` too —
see [[frank-archon]] for why it was rejected);
`--expert-precise-memory` swaps the fitted overhead figure in.
## Related
- [[frank-archon]] — deployed shape, plan law,
measured campaigns
- [[PCIe Link and Atomics]] — why decode is gather-bound and what cannot fix it
- [[RDNA4 MoE Expert Offload VRAM Budget]] — arena/plan arithmetic deep-dive
- [[frank-dragoon]] and [[frank-zealot]] — window discipline on live hosts