The 2026-10-09 wire aliases become the service names everywhere: systemd units, serve scripts, env files and FRANK_* variables, deploy-plan/deploy-remote service keys, container names, Prometheus job names + alert rules, Grafana dashboard queries, bats seams, runbook filenames, homepage/AGENTS lore, and historical prose. Rollback tags renamed in lockstep. Residue in the tree: zero. Retired frank-medium/small/npu names are history and stay. Known casualty: scripts/patches/*.patch context lines were rewritten; those retired-era vLLM-fork patches no longer apply byte-exact.
15 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| reference | Tcclaviger Fork Ops | Operating the tcclaviger RDNA4 vLLM fork on dankserver (shape G era, rollback target since the 2026-10-05 FreeToken cutover) — the ad-hoc sweep harness, measured knob verdicts, sweep hygiene, the large-MoE serving recipe, and compile-cache persistence. |
|
2026-10-05T00:00:00-07:00 |
Tcclaviger Fork Ops
The tcclaviger RDNA4 vLLM fork (docker.io/tcclaviger/vllm@sha256:da4029666…,
vLLM 0.29.0.dev, GLM-5.3/Qwen-specialised with native MoE quant kernels and a
first-class expert-offload planner) served frank-archon at shape G until
the 2026-10-05 FreeToken cutover; it remains the rollback target
(git tag frank-archon-vllm-shapeG-20261003). Full plan law and VRAM
arithmetic: RDNA4 MoE Expert Offload VRAM Budget; boot forensics:
vLLM Boot Forensics and Config Interrogation; window discipline:
LLM Bench Windows and Measurement.
The ad-hoc sweep harness (dankserver)
Serve script /home/dank/serve_qwen_fp8.sh with env hooks: SPEC (MTP
depth), TUNABLE (TunableOp), PREFETCH, UTIL, MAXLEN, KV_BYTES,
MNBT, UVA_TOK, GATHER_C/GATHER_L (R4D_LRU gather), TELEM
(R4D_LRU_TELEMETRY), PROFDIR (torch profiler bind-mount + --profiler-config
JSON). The script re-copies the harness (mtp_measure.py, bench_iter.py)
into every fresh container — podman cp files die on container recreate.
- Measure ONLY via usage counters:
mtp_measure.py(3 depths) ormtp_measure_short.py(166 ctx).decode = completion_tokens/(wall − ttft); chunk counting is INVALID under spec decode. - Jobs:
bash /home/dank/job.sh start NAME CMD…/wait NAME SECS 40/out NAME(Remote Job Runner). - Validity check on ANY speed result:
completion_tokens == max_tokens(else repetition-halt or early EOS), finish_reason, and inspect the text —bench_iterprompts are repeated word-lists +!padding, loop-prone. A loop makes LRU hits ~100% → PCIe traffic vanishes → fake 3–4× decode. Acceptance swings 48–61% run-to-run — noise; decode is the metric. - Each arm = full reload: ~8.5 min cold, ~12 min with measurement.
- Readiness gate:
podman logs <c> 2>&1(stdout+stderr merged) forApplication startup complete— it is on STDERR; an unmerged grep can never match and burns the whole wait timeout. - GPU util ~0% during load/compile is NORMAL (weights + CUDA-graph capture are CPU-side); VRAM climbing is the load indicator.
- Config edit discipline: the serve script is patched by scp'd python
scripts with asserts (anchor must exist, addition must not);
bash -nafter every patch.
Measured knob verdicts (2026-09-29 campaign)
Incumbent: SPEC=4 PREFETCH=1 UTIL=0.98 MAXLEN=32768 KV_BYTES=2147483648 TUNABLE=0, stock gather 8×16 → 26.80/25.15/26.81 tok/s @ 166/1087/4064 ctx.
| Knob | Verdict | Evidence |
|---|---|---|
Gather geometry R4D_LRU_GATHER_{CHUNKS,LANES} |
RULED OUT | 16×32 worse (~24); 32×64 tie (26.6). The 9.5 vs 22.7 GB/s read-path gap is per-transfer structure (~105 µs/slab vs 78 ideal), not a tunable knob |
| MTP depth | RULED OUT | SPEC=3 ~9% worse; SPEC=6 collapses (13.5–19 tok/s, acceptance 17.4%) — also proves UVA read-through (crosses at step tokens > R4D_LRU_UVA_TOKENS=5) is SLOWER than copy-in |
| TunableOp | REJECTED | CLAV_TUNABLEOP_SWEEP=1 silently SKIPPED under expert offload unless CLAV_TUNABLEOP_WITH_OFFLOAD=1; adds ~7 min/load, persists nothing (container-local), and CORRUPTS NUMERICS (loops, repetition-halt, fake 79.8 tok/s @ 96.5% acceptance on degenerate completions). Any "3× win" with early-EOS + absurd acceptance = the loop artifact |
| MNBT | neutral at 8192 vs 2048 | small values re-chunk prefill only (hurts TTFT, never decode — decode step = max-num-seqs × (spec+1) tokens). No ubatch concept in vLLM |
Decode on this stack is PCIe-gather-bound (~30% GPU duty cycle), so batch-shaped compute knobs move ~nothing.
The R4D_*/CLAV_* knob family (QSA_, MHC, ROUTE, ACT_QUANT, AR_QUANT,
SILU_QUANT) are kill-switches defaulting ON, not perf dials; real levers are
the R4D_LRU_ family, TunableOp (rejected), R4D_LRU_TELEMETRY, and spec
depth.
Telemetry gotchas: --profiler-config takes a JSON blob, not separate
flags; torch_profiler_with_stack defaults TRUE — a full measure inside a
profile window OOM-kills the engine worker (~40 GB trace buffer; kernel
kills python3 at ~95 GB anon-rss). Keep windows to ~15 s / one depth
(mtp_measure_short.py). telemetry_report (expert_cache_rank<N>.json)
fires ONLY from the worker's profiler-stop path; worker-side stop can hang
5 min on ROCm/kineto. No alternative trigger exists. The LRU hit rate measured 82.45% — NOT the ~27% the arena-fraction arithmetic implies; temporal reuse dominates, so never infer miss rates from resident-slot fractions.
Sweep hygiene (fork-specific preflight)
- Knob landed: for each
-eenv knob, confirm in-containerpodman exec … env(or the engine log's confirmed-values line). A knob the fork silently ignores produces a "parity" arm that is really the default — the v1 gather sweep lost all three arms to exactly this (GATHER_C/GATHER_L silently dropped, all arms ran stock 8×16). - Prefetch uniformity: every arm in a sweep MUST carry the same expert-prefetch setting. The 2026-09-29 campaign measured losing arms with prefetch ON and incumbents OFF; untaxed, its verdicts inverted (no-spec 12.11→41.47; gather 32×64 parity→+18.7%). Never re-read a taxed verdict.
- Spec arms must prove engagement: engine log
SpecDecoding metrics: Mean acceptance length …andcurl :7770/metrics | grep vllm:spec_decode_. Zero such lines = MTP off. - Boot-fail fast-detection: poll
podman psINSIDE the health loop; a refused boot dies in one poll instead of 20 min of health waits. Capture8 lines of
podman logson failure — the root cause (e.g. a SEQS init refusal) lives higher up. - Report per-depth tables, never one number: g32×64's win grows with depth (54@1k, 39.5@short) — a single 4064 median hides the short-ctx cost. TTFT rows are noisy ±0.15 s and non-monotonic across depths; compare same-ctx rows across arms only.
- Generate ≥256 tokens per sample (at 8 tokens inter_token is first-token noise).
Boot-log facts survive measure failures: KV pool size (GPU KV cache size),
arena slots (expert cache: … -> N slots), and the spec-reservation ladder
(MTP3 766,666 / MTP4 729,545 / MTP6 665,263 tokens) print at BOOT — a
measure-stage bug costs decode tables but not boot-log findings.
MTP depth-brittleness law (2026-10-02, all arms rc=0): MTP3 on this fork
is depth-brittle — 47.01 t/s at 166 tokens (47.88 at temp 0.7) but 23.09 at
4064, vs no-spec baseline 37.02 / 33.99. MTP3 + SEQS 16 + MNBT 4096 + util
0.95 + ladder [4..32] = best overall 37.04 / 38.61 t0.7 and keeps 33.15 at
4064. --expert-precise-memory is load-bearing for MTP (same arm with it
OFF = worst arm, 19.81 at 4064). Every spec arm costs KV (398,658 → 313,002
tokens). Acceptance is real: mean length 2.4–2.8, per-position
0.76/0.55/0.33, avg 52–57%. Next lever named by the engine: MNBT 4096 is
exactly SEQS×(n+1) (suboptimal warning) → try 8192.
Serving a model larger than VRAM (dankserver recipe)
Dankserver: 2× R9700 (gfx1201), 31.86 GiB each = 63.7 GiB real VRAM, 246 GiB
RAM. torch.cuda.device_count() returns 3 and summing total_memory gives
a bogus 186.7 GiB (the Raphael iGPU is in the list — see
Llama Cpp Serving Sizing for the reading discipline).
Kernel gate — this decides which checkpoint can work at all.
/app/r4dhip/r4d.so exports gemm_moe_mxfp4a8_* and gemm_moe_dsfp4a8_*
but NO gemm_moe_w4a16_*. Expert offload requires an experts class with
BOTH a compiled kernel AND an offload_recipe; the planner aborts startup
otherwise. Usable: MXFP4, DSFP4, FP8-HIP (R4dMxfp4MoEExperts,
R4dDsfp4MoEExperts, Fp8HipMoEExperts). R4dW4a16MoEExperts has no
kernel (the class exists, the r4d.so entry does not); RFA has neither. Check
the checkpoint's quant against this list BEFORE downloading anything.
Entrypoint is /app/tools/image_entrypoint.sh, ending exec vllm serve "$@" — pass the model path as the FIRST argument and do NOT pass serve.
Known-good invocation:
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 \
docker.io/tcclaviger/vllm:latest /models \
--served-model-name <name> --port 7770 \
--tensor-parallel-size 2 \
--max-model-len 32768 \
--kv-cache-memory-bytes 2147483648 \
--max-num-seqs 4 \
--gpu-memory-utilization 0.95 \
--enable-prefix-caching --trust-remote-code
-p 7770:7770 is not included, so the API is only reachable via
podman exec — add -p if host-side curl is needed. Load ~5 min;
/health answers connection-refused the whole time; watch for
Application startup complete in podman logs <ctr>. Verified working:
orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8 (gated; 172.8 GiB, 131
shards, Qwen4ExpForConditionalGeneration, 48 layers, 512 experts top-10,
multimodal, native 262,144).
Traps that each cost a boot:
- Mount the whole
/dev/dri, never individual render nodes. Render nodes without the primarycard*nodes → HIP enumerates two agents and refuses a context on either (RuntimeError: No CUDA GPUs are available) whiledevice_count()cheerfully returns 2. Confirmed by direct A/B. - memlock must be raised (done on Dankserver via
/etc/security/limits.d/99-memlock.conf). Host default 8 MiB makes any large pinned allocation fail as a misleadingtorch.AcceleratorError: CUDA error: out of memoryfrompin_memory(); it cannot be raised from inside a container or root's own shell — only PAM applies it at login. Matters because--expert-host-mlockdefaults True. --gpu-memory-utilizationdoes NOT buy KV under expert offload — the planner sizes the arena against the budget and leaves KV the remainder; raising util 0.95→0.98 grew the arena and the KV pool stayed at 0.64 GiB.- Lowering
--max-model-lenis the WRONG lever and backfires — the budget subtracts the KV reserve first, so a smaller context shrinks the reserve and hands the space to the arena (measured: 65536 → 0.64 GiB KV; 32768 → 0.34 GiB). - The KV reserve undercounts hybrid-attention models ~2× (9190 B/token
planned vs ~18,000 real: the Mamba state is a fixed per-sequence cost).
Fix by pinning the pool:
--kv-cache-memory-bytes. --expert-offload-memcaps the HOST side, not the VRAM arena — structurally orthogonal to--expert-precise-memory(verified in-container 2026-10-05: precise-memory only swaps the GPU overhead term, expert_offload_mem only sets the host bound; they rejoin only insideplace_experts, and the plan rewritesexpert_offload_memback to the cap the plan met, so plan and allocation cannot drift).--ipc=hostand--shm-sizeare mutually exclusive in podman; pick one.podman run -dwithout-asends no stdout to a redirect — the log file only receives the container ID; usepodman logs.
Status reads: use scripts/dankstatus.sh, never hand-rolled
/proc/meminfo parsing — /proc/meminfo separates key from value with a
TAB, so grep MemAvailable | tr -s " " | cut -d" " -f2 returns empty and
awk prints 0.0 — a false "host out of memory" that invites an unnecessary
abort. Gated checkpoints: token via a mode-600 curl -K config, never argv
(pgrep -a curl prints it); always --fail; compare each file's byte count
to the Hub's declared size; -C - resumes truncated shards.
Check whether the stack supports the model natively before porting an
overlay. The image already registered Glm5NextForConditionalGeneration,
Qwen4ExpForCausalLM and both MTP variants — chasing four missing symbols
through two image tags to bring an overlay up was entirely wasted work.
Compile-cache persistence (survives podman rm -f)
Serve scripts that recreate the container discard the writable layer, so
every boot re-pays compile/JIT (frank-archon: torch.compile took 181.20 s
cold, of which 173 s was one graph).
- Prove it and locate it: fresh container vs live
(
podman run --rm --entrypoint bash IMG -c "ls -la /root/.cache"vspodman exec LIVE bash -c "du -sb /root/.cache/*; find /root/.cache -type f | wc -l"). vLLM cache root:VLLM_CACHE_ROOT(default/root/.cache/vllm,torch_compile_cache/); a HIP runtime adds/root/.cache/comgr. - Mount the PARENT, not the leaf — one bind on
/root/.cachepersists both trees, safe whenever the image ships no such directory (step 1 proves it; otherwise you would be hiding baked state). Wire as an opt-in tunable:mkdir -p "$CACHE_DIR"BEFORE the mount, then${CACHE_DIR:+-v "$CACHE_DIR":/root/.cache}. Host path outside the checkout. - Verify with two consecutive boots — cold
Compiling a graph … takes N s, warmDirectly load the compiled graph(s) for compile range (…) from the cachewithtorch.compile tookin single-digit seconds. - Traps: the cache key includes the compile RANGE — an MNBT change costs
ONE recompile, not a permanent tax; do not claim a proportional boot cut
(time to
/healthis dominated by weight loading: 490 s warm vs 188 s cold compile); stale entries are misses, not wrong answers (version + config hashed); if the serve script is seam-gated, assert the mkdir and the exact mount target.
Plan-line verification checklist (after any fork deploy)
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})"'
grep -oE with a digit-anchored tail — plain grep/journalctl truncate
these columns to 17.. Verify: new budget/graphs/arena in the plan;
GPU KV cache size matches the requested token count; Estimated CUDA graph memory vs the capture line (raise _PER_GRAPH_MB toward 52 if capture
exceeds the charge); rocm-smi --showmeminfo vram high-water per card = the
real margin; systemctl show frank-archon -p NRestarts --value = 0;
VLLM_CUDAGRAPH_* present in podman exec … env; then a real chat
completion from a PEER — curl localhost:7770/health inside an ssh session
on the host can return 000 and looks like an outage that isn't.
Related
- RDNA4 MoE Expert Offload VRAM Budget — the plan law this harness tunes
- vLLM Boot Forensics and Config Interrogation — journal forensics for these boots
- LLM Bench Windows and Measurement — window discipline and bash laws
- Model Swap and Rollback — where the fork sits in the swap history
- Remote Job Runner — the job.sh harness referenced above