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).
This commit is contained in:
Frank
2026-10-19 01:33:47 -07:00
parent eb21dbc705
commit 04f4e2c6d4
18 changed files with 348 additions and 425 deletions
@@ -14,7 +14,7 @@ timestamp: 2026-09-03
DFlash and DSpark are the two newest speculative decoding methods. They draft many tokens in one parallel pass instead of one token at a time. This reduces decode latency on memory-bound workloads.
DFlash comes from Z Lab at UC Berkeley. DSpark comes from DeepSeek. Both work with vLLM and llama.cpp. Both have draft models for the Qwen3.6 models that Dankserver and Dankclaw run.
DFlash comes from Z Lab at UC Berkeley. DSpark comes from DeepSeek. Both work with vLLM and llama.cpp. Both publish draft models for the Qwen3.5/Qwen3.6 families (table below); what any of them pairs with on this fleet is an era on [[frank-dragoon]] or [[frank-zealot]].
## How DFlash Works
@@ -115,12 +115,12 @@ unconditionally, while **context-backed** types (`draft-mtp`, `draft-dflash`,
- Internal order is fixed (ngram types first, then drafters); CLI order is
irrelevant.
- **A legal chain can still lose on a recurrent model.** `draft-mtp,ngram-mod`
on the qwen35 hybrid (frank-dragoon era) measured −15 % prose / −7 %
block-copy (2026-10-03): adding the ngram layer logs `recurrent state
rollback is not compatible with 'ngram-mod' - disabling rollback support`,
and MTP verification depends on that rollback, so acceptance drops on ALL
traffic even where the matcher fires. Measure chains per architecture,
never by analogy.
on the qwen35 hybrid loses on both traffic shapes (2026-10-03): adding the
ngram layer logs `recurrent state rollback is not compatible with
'ngram-mod' - disabling rollback support`, and MTP verification depends on
that rollback, so acceptance drops on ALL traffic even where the matcher
fires. The measured size of each loss is [[frank-dragoon]]'s (Laws). Measure
chains per architecture, never by analogy.
- ngram-mod prints NO startup banner — the load log shows only the drafter's
`adding speculative implementation` line. Never conclude "chain not
engaged" from the load log; identify behaviourally (`draft_n` > 0 only
@@ -198,7 +198,10 @@ Compatible MTP-only draft GGUFs on Hugging Face:
Speculative decoding is lossless under rejection sampling: the target verifies every draft token. A draft head from the base model works with a derivative target; wrong proposals just lower the acceptance rate. The accepted MTP draft lengths are 2-3 tokens; the "MTP Defaults Are a Trap" writeup (thefrontierlab.ai) documents the tuning pitfalls.
On Dankclaw, `draft-dflash` remains the benchmarked winner (20.2 t/s at 4k vs 16.9 for DFlash+ngram and 8.7 for ngram-only). MTP is a viable alternative to test, not a bug fix: the parallel-stream releases investigated on 2026-08-13 were client-side aborts and a Dankserver crash, not speculative decoding behavior (see [[deployment]]).
On the mid-August 2026 Dankclaw shape `draft-dflash` was the benchmarked winner
at 4k — 20.2 t/s, against 16.9 with ngram layered on and 8.7 ngram-only; those
three figures are that retired llama.cpp era's property ([[frank-dragoon]] owns
the era and its shape). MTP is a viable alternative to test, not a bug fix: the parallel-stream releases investigated on 2026-08-13 were client-side aborts and a Dankserver crash, not speculative decoding behavior (see [[deployment]]).
## Draft Models
@@ -213,13 +216,12 @@ DFlash draft model sizes for the models on these servers:
The DFlash draft is not a standalone model. It must pair with its target model. The model cards recommend block size 8 for higher concurrency and 16 for single-stream throughput.
## Deployment on Dankserver (NVIDIA RTX 3090)
## DFlash on an RTX 3090 (Ampere) — the archived vLLM path
The Dankserver llama.cpp deployment (`frank-zealot`, Qwen3.6-35B-A3B) is **active** on Dankserver (port 7770) as of the 2026-08-17 redo (the earlier 2026-08-16/17 retirement of this service was reversed when the PP split was retired). The vLLM material below documents the archived AWQ/DFlash path on the same hardware.
> **DFlash retired from active service.** `DFlash` (draft-dflash) was tested initially but did not deliver the desired speed gains under the `frank-zealot` workload (see ngram-mod benchmarks below). The `Alittlehammmer/Qwen3.6-35B-A3B-DFlash` draft GGUF remains in `/home/dank/models/` as reference material only.
The vLLM material below documents the archived AWQ/DFlash path on the same hardware. The RTX 3090 is Ampere (compute capability 8.6). Use these conditions:
DFlash was tried on the 3090 line, lost there to the ngram chain, and was never
kept deployed; that card's payloads, shapes and rates are [[frank-zealot]]'s
(its Eras rows are the llama.cpp arms the draft was tested against). What
transfers is the Ampere arithmetic — compute capability 8.6, so:
- FP8 KV cache is valid on CUDA 12.
- FP8 weights compute through a fallback path; no native FP8 tensor cores.
@@ -248,11 +250,15 @@ docker run --rm --name vllm-qwen36-dflash \
--gpu-memory-utilization 0.95
```
## Deployment on Dankclaw (AMD Strix Halo gfx1151)
## DFlash and DSpark on Strix Halo (gfx1151) — what killed them
`frank-dragoon` on Dankclaw ran the llama.cpp pipeline-parallel head (Qwen3.8-27B-Uncensored Q6_K) on port 7770, RPC-split with the Dankserver worker. Spec was the standalone MTP draft over the ngram-mod fallback, 1 slot at 262144 ctx, q8_0/f16 KV, head GPU side ROCm0. This page is a historical record of that era (mid-August 2026); for the current deployment see [[frank-dragoon]].
History: before `--spec-default`, the box ran DSpark (`draft-dspark`, retired 2026-08-15 after a ROCm illegal-memory-access crash under two concurrent 16k prompts) and before that bundled MTP (`draft-mtp`, retired same day). DFlash was retired on 2026-08-14 (premature-EOS under parallel load). The old DFlash draft GGUF (`Qwen3.6-27B-DFlash-Q8_0.gguf`), the MTP-only drafter (`Qwen3.6-27B-MTP-ONLY-Q8_0.gguf`), and the DSpark draft (`Qwen3.8-27B-DSpark-Q8_0.gguf`) remain in `/home/dank/models/` as reference only.
Both context-backed drafters died the same way on the iGPU: under parallel load.
DSpark took `ROCm error: an illegal memory access` inside the shared draft
context with two concurrent 16k prompts; DFlash corrupted multi-slot into
premature EOS and was retired first. The timeline, the crash class, the
"concurrency is the trigger, not size" finding and the `PARALLEL=1` workaround
are on [[qwen3.8-spec-matrix]]; the shape they were measured on is
[[frank-dragoon]]'s mid-August 2026 era.
## A/B protocol on a live llama.cpp endpoint
@@ -359,16 +365,18 @@ pay for K sequential forwards.
Trained heads are target-specific: EAGLE/DFlash consume the TARGET's hidden
states, so a dense-27B DFlash2 is useless for a 512-expert MoE trunk.
**Step 4 — acceptance reality checks.** Fleet datum: a trained z-lab DFlash2
at depth 7 accepted ~41 % on its matched target (`draft-dflash 7 → 43.5
tok/s`); treat 40–60 % as the honest band for a matched pair. Cheapest proxy
for `p` at temp 0 when both models are greedy and share a tokenizer: greedy
continuation agreement on identical prompts, two API calls, no weights moved.
llama.cpp can also synthesize the projection without any draft artifact:
**Step 4 — acceptance reality checks.** Treat 40–60 % as the honest band for a
matched pair; the arm that anchors the bottom of it on this fleet is the
depth-7 `draft-dflash` row of [[frank-medium-llamacpp-dflash2]]'s spec table
(accepted share, rate and VRAM for every depth are in that table). Cheapest
proxy for `p` at temp 0 when both models are greedy and share a tokenizer:
greedy continuation agreement on identical prompts, two API calls, no weights
moved. llama.cpp can also synthesize the projection without any draft artifact:
`--spec-synth-rates P0,P1,...` / `--spec-synth-len L`. Speculation pays IN
PROPORTION TO HOW SLOW THE TARGET IS — the same 41 % gave 2.3x on an
18.8 tok/s layer-split target and a loss on a 25 tok/s one; fast targets are
usually sparse MoE, the worst case for drafting.
PROPORTION TO HOW SLOW THE TARGET IS — that band's midpoint was worth 2.3× on
the slow two-card layer-split target and a LOSS on the 25 tok/s target of the
worked example above; fast targets are usually sparse MoE, the worst case for
drafting.
**Step 5 — check the attachment surface BEFORE the arithmetic.** Closed
engines refuse external drafters by design: halogen-flash-server's
@@ -17,66 +17,59 @@ Dates: researched 2026-08. Hardware is verified on the live machine unless
marked "verify first". Re-verified 2026-08-15/16; corrections marked below.
**Live today (2026-10-18).** The era described below — llama.cpp on the 8060S
iGPU behind :7770 — is retired. Dankclaw now serves
[[frank-dragoon]] (halogen-flash-server 0.16.2, port
7770) and [[frank-zealot]] (llama.cpp PrismML fork on
the RTX 3090, port 7771 — drift today: that unit is `failed` since 2026-10-18
03:40 and a hand-run 9B-MTP process answers the port), and boots `iommu=pt` since 2026-10-18
([[dankclaw-iommu-translation-off]]). The numbers below are the measurements of
the era they were taken in; the `ttm` ceiling and the NVMe scheduler rows were
re-checked 2026-10-18 and still hold.
iGPU — is retired; what runs on this box now (both endpoints, their engines and
ports) is [[frank-dragoon]]'s and [[frank-zealot]]'s pages. Drift worth knowing
before trusting either page: the frank-zealot unit is `failed` since 2026-10-18
03:40 and a hand-run 9B-MTP process answers its port. The OS facts are this
page's: `iommu=pt` since 2026-10-18 ([[dankclaw-iommu-translation-off]]), and
the `ttm` ceiling and NVMe scheduler rows below were re-checked 2026-10-18 and
still hold. Every benchmark number below is a measurement of the era it was
taken in.
## Corrections (re-verified 2026-08-15/16)
- **Backend**: that era's production was **Vulkan0**, not ROCm. Measured on this box
(b10433): Vulkan 25.13 t/s vs ROCm 24.33 t/s decode at 16k; at 262k ctx
Vulkan decodes 48% faster (5.68 vs 3.83 t/s). ROCm wins prefill (410 vs
302 t/s @64k). The "HIP out-performs Vulkan for decode" claim did not
hold on the 8060S. See [[frank-dragoon]].
- **Backend**: that era's production was **Vulkan0**, not ROCm. On this box ROCm
wins prefill and Vulkan wins decode, and the decode gap widens with context:
the 64k A/B is [[qwen3.8-strix-optimizations]] §4, the 262k A/B is
[[qwen3.8-spec-matrix]] §High-Context Backend Comparison. The "HIP
out-performs Vulkan for decode" claim did not hold on the 8060S.
- **Power profile**: the `/sys/class/drm/card0/device/power_profile` file
does not exist on this machine (checked 2026-08-15). GPU DPM is already
`performance`, forced level `auto`. This item is N/A.
- **KV cache**: production ran `q8_0` K **and** `q8_0` V (both quantized). The law
still holds on `frank-zealot` (`CACHE_TYPE_K=q8_0`, `CACHE_TYPE_V=q8_0` —
`scripts/frank-zealot.env:55-56`).
- **KV cache**: production ran `q8_0` K **and** `q8_0` V (both quantized). What
`frank-zealot` runs now is its own Current shape — this row is not it.
- **Spec**: DFlash/DSpark are retired. That era's production was `--spec-default`
(ngram-mod) with draft-mtp first. See [[qwen3.8-spec-matrix]]. Today `frank-zealot`
is configured with speculation **off** (`SPEC_TYPE=none` — `scripts/frank-zealot.env:67`).
- **TTM**: raised to 114 GiB on 2026-08-25, set live (no reboot) and
persisted in `/etc/modprobe.d/ttm.conf`. The effective VRAM ceiling that
once held the service at ~15 GiB is gone.
(ngram-mod) with draft-mtp first — timeline on [[qwen3.8-spec-matrix]]; the
spec setting each endpoint runs today is on its own page.
- **TTM**: the `pages_limit` raise that removed the ~15 GiB ceiling is recorded
once on [[Dankclaw System Specs]] (Memory).
## Verified Memory Picture (measured 2026-08-11)
| Figure | Value | Source |
| ------ | ----- | ------ |
| OS physical RAM | 30.5 GiB | `/proc/meminfo` |
| TTM pages_limit | 114 GiB (29,883,536 x 4KiB) — raised 2026-08-25 | `/sys/module/ttm/parameters/pages_limit` |
| ROCm VRAM total | 96.0 GiB | `rocm-smi --showmeminfo vram` under the then firmware VGM carve |
| ROCm VRAM used | 76.2 GiB | `rocm-smi` while serving 3x200700 |
That 2026-08 reading is the firmware-VGM era: the BIOS carved ~96 GiB as
"dedicated" iGPU VRAM, so the kernel saw only ~30 GiB (the same split is
recorded in [[ling-3.0-flash-trial]] §Why Vulkan + GTT). The OS now accounts
**121 GiB** (`MemTotal 127108080 kB`, 2026-10-18) and `rocm-smi
--showmeminfo vram` reports **2.0 GiB** (2,147,483,648 B) of dedicated VRAM —
the carve is gone and the GPU budget is the GTT aperture, not the SMI figure
([[Dankclaw System Specs]]).
The TTM row above is still live: `pages_limit = 29883536` (114 GiB) and
`/etc/modprobe.d/ttm.conf` still carries the `options ttm` line (both read
2026-10-18).
recorded in [[ling-3.0-flash-trial]] §Why Vulkan + GTT; the 76.2 GiB row is an
era [[frank-dragoon]] load reading). Every live number on this box — `MemTotal`,
the ~114 GiB GTT aperture that is the real GPU budget, the 2.0 GiB dedicated
carve and the `ttm` page cap — belongs to [[Dankclaw System Specs]], re-read
2026-10-18.
This split is the Strix Halo GTT/VGM design. It drives the batch/ubatch memory
limits documented in [[frank-dragoon]].
## 1. Self-Hosted Agentic Tools
All of these connect to the local OpenAI-compatible endpoint
(`http://127.0.0.1:7770/v1`). Today that port is **halogen-flash-server**, not a
llama.cpp server, and the address must be the IPv4 literal: `curl
http://localhost:7770/v1/models` returns nothing while
`http://127.0.0.1:7770/v1/models` answers 200 (checked 2026-10-18 — the rootless
podman publish is IPv4-only). None need NVIDIA.
All of these connect to the local OpenAI-compatible endpoint on
`:7770` (which engine owns that port is [[frank-dragoon]]'s to say), and the
address must be the IPv4 literal: `curl http://localhost:7770/v1/models` returns
nothing while `http://127.0.0.1:7770/v1/models` answers 200 (checked 2026-10-18
— the rootless podman publish is IPv4-only). None need NVIDIA.
| Tool | Use | Endpoint config | Priority |
| ---- | --- | --------------- | -------- |
@@ -90,12 +83,13 @@ podman publish is IPv4-only). None need NVIDIA.
| Qdrant | Vector store for RAG | Standalone, CPU-native | Low |
The pragmatic first installs are Open WebUI, Cline, and Aider. They all work
with the endpoint on 7770 and need no extra GPU. The 7770 endpoint today is
halogen-flash-server, not llama.cpp.
with the endpoint on 7770 and need no extra GPU.
## 2. Inference Runtime Tuning (llama.cpp on gfx1151)
Verified against the running service and llama.cpp source.
Verified against that era's running service and llama.cpp source; the era is
retired ([[frank-dragoon]]), so this table is history and a method, not a live
TODO list.
| Change | Recommendation | Priority |
| ------ | -------------- | -------- |
@@ -104,8 +98,8 @@ Verified against the running service and llama.cpp source.
| Flash attention | Leave `--flash-attn auto`. It helps prompt processing, not decode. | Keep |
| Load mode | Start with mmap (default). Enable `--mlock` only if page-fault stutter appears. | Optional |
| Threads | Use `--threads 12 --threads-batch 16 --threads-http 1` for 16 Zen5 cores + 3 slots. Frees cores for the ROCm driver. | Medium |
| DFlash n_max | Current 8 (block 16) is high. Try `--spec-draft-n-max 5`; it may reduce rejection cost at the same throughput. | Verify |
| ngram layering | Measured slower than DFlash alone at 4k (16.9 vs 20.2 t/s). Not enabled. | Keep off |
| DFlash n_max | Depth 8 (block 16) was high; a shallower draft cuts rejection cost at the same throughput. Sweep depth per the A/B protocol in [[DFlash DSpark Speculative Decoding]]. | Verify |
| ngram layering | Layering ngram on the DFlash draft measured slower than the draft alone at 4k — figures on [[DFlash DSpark Speculative Decoding]]. | Keep off |
## 3. Server / OS / Hardware Settings
@@ -113,7 +107,7 @@ All require root and a reboot for the modprobe change.
| Setting | Action | Effect | Priority |
| ------- | ------ | ------ | -------- |
| TTM GPU pool | `/etc/modprobe.d/ttm.conf` → `options ttm pages_limit=29883536 page_pool_size=29883536` (applied 2026-08-25, set live then persisted) | Lifts the 15 GiB cap to 114 GiB; the full 262k KV + Q8 AGENT payload fit in one slot | Done |
| TTM GPU pool | Raised 2026-08-25; the knob, its value and its persistence file are [[Dankclaw System Specs]]'s (Memory row) | Lifted the ~15 GiB kernel-page ceiling so a full 262k KV plus a ~30 GB payload fits one slot | Done |
| Power profile | **N/A on this machine** (2026-08-15; re-checked 2026-10-18: no `power_profile` under `/sys/class/drm/card0/device/` or `card1/device/`); GPU DPM already `performance` | none | ~~Medium~~ |
| NVMe scheduler | Already `none` (`[none] mq-deadline` in `/sys/block/nvme0n1/queue/scheduler`, re-read 2026-10-18) | No change | Done |
| BIOS VGM | Set Variable Graphics Memory / UMA to "High" in the EVO-X2 BIOS | Max dedicated VRAM for the GPU carve-out; verify in the live BIOS | Verify |
@@ -66,6 +66,7 @@ The NPU endpoint is retired (2026-09-11; [[frank-npu]]). The `frank-zealot` name
| System RAM | 121 GiB (shared CPU + GPU; `free -g` total 121, `MemTotal 127108080 kB`, 2026-10-18) |
| GPU-Visible (ROCm) | ~114 GiB GTT aperture (`mem_info_gtt_total` 122,402,963,456 B — the budget) |
| Swap | 64 GB `/swap.img` (`swapon`, 2026-10-18; `free` reports 63 GiB) |
| TTM page cap | `ttm.pages_limit = 29883536` (114 GiB of 4 KiB pages) and `page_pool_size=29883536`, persisted in `/etc/modprobe.d/ttm.conf`, applied live 2026-08-25 and re-read 2026-10-18 (`/sys/module/ttm/parameters/pages_limit`). The kernel default is half of RAM, so in the firmware-VGM era (~30 GiB visible) the cap was ~15 GiB and a 28 GB payload plus a 262k KV cache could not load — this is the row every other page should cite |
| /dev/shm | 61 GiB tmpfs (half of RAM, `df /dev/shm`, 2026-10-18) |
### Storage
@@ -147,10 +147,10 @@ LAN/SATA/WiFi:
they were in separate IOMMU groups (15 and 20 — re-read 2026-10-18: still 15
and 20), and both root ports had ACS `ReqRedir+`, so this page inferred P2P
would hairpin at the root complex. **That inference is superseded:** the live
engine says the opposite about the same pair — "I device: the 2 cards reach
each other's memory directly (peer to peer)" (`podman logs frank-archon`,
2026-10-15 boot). Plain P2P traffic measured 45.27 GB/s bidirectional on the
*old* board — DMA was never the gate, atomics were.
engine reports the two cards reaching each other's memory directly, which
[[frank-archon]] and [[Dankserver System Specs]] record. Plain P2P traffic
measured 45.27 GB/s bidirectional on the *old* board — DMA was never the gate,
atomics were.
- Kernel 7.0.0-31-generic with `iommu=pt` then; this host runs 7.0.0-38-generic
with `iommu=pt` today (`/proc/cmdline`, read 2026-10-18). Both were ruled out
as causes ([above](#ruled-out-empirically-do-not-re-derive)) and this host's
@@ -163,10 +163,9 @@ launched since the swap. Until the 2-rank probe runs — or a `--split-mode row`
boot that does *not* log `internal AllReduce init failed ... falling back to
meta-backend butterfly` — "collectives work here" was a strong expectation, not a
measurement. **RCCL never got that measurement**; what settled the operational
question instead is production traffic: radiance's TP=2 wire moves cross-rank
all-reduces over the direct P2P path and reports its own precision — "tp wire:
LOSSY (--tp-wire wht6) at 128 KiB a message and above … Steps of at most 25 rows
are exact" (`podman logs frank-archon`, 2026-10-15 boot).
question instead is production traffic — radiance's TP=2 wire carries cross-rank
all-reduces over the direct P2P path and prints its own precision, and the
measured `wht6` banner with its exact-step bound is on [[frank-archon]].
## Measured matrix
@@ -233,35 +232,34 @@ landed, at x8/x8, clears the arithmetic with room to spare.
Both cooperation modes were run on this host on 2026-09-20 — the same salted
10-prompt battery (`scripts/bench-conversation.py`), the service stopped and
masked between arms. Decode/prefill are geometric means over the ten rows;
burst-4 is four concurrent copies of one prompt, reported as aggregate tokens/s.
masked between arms. The arm table is [[frank-medium-radiance-mxfp4]]'s bench of
record (arms R = radiance TP=2 + DFlash2 as deployed, I = the llama.cpp layer
split that preceded it, L = llama.cpp `--split-mode tensor` against RCCL); this
page keeps only the verdict. **TP=2 won on every axis — roughly 2.1× decode and
3.6× at 4-way concurrency over the layer split — and replaced the layer split the
day of measurement.**
| Shape | Cross-device path | Decode | TTFT | Prefill | burst-4 |
|---|---|---|---|---|---|
| **radiance TP=2 + DFlash2 — deployed** | R4D one-shot allreduce | **95.8 t/s** | **0.21 s** | **1493.6 t/s** | **259.4 t/s (65.6/stream)** |
| llama.cpp layer split 32:33 + DFlash2 — predecessor | none, boundary activation copy | 45.7 t/s | 0.72 s | ~530 t/s | 71.7 t/s |
| llama.cpp `--split-mode tensor` 1:1 | RCCL allreduce | 30.5 t/s | 0.71 s | n/r | 54.3 t/s |
Row 3 carries a caveat that no flag can remove: llama.cpp's tensor mode could not
load the DFlash2 drafter (it aborts at `ggml-backend.cpp:941` — a draft weight
Arm L carries a caveat no flag can remove: llama.cpp's tensor mode could not load
the DFlash2 drafter (it aborts at `ggml-backend.cpp:941` — a draft weight
pre-allocated on the non-primary device has no backend once `n_copies == 1`), so
it decoded **without** speculation while the other two rows decoded with it. The
comparison that decides anything is row 1 against row 2, and some of that gap is
the 4-bit MXFP4 checkpoint rather than the parallelism.
it decoded **without** speculation while the other arms decoded with it; and part
of R's margin over I is the 4-bit MXFP4 checkpoint, not the parallelism.
Two numbers elsewhere on this page are therefore not interchangeable:
- The 272 t/s recorded 2026-09-16 is **prefill**, on a **butterfly** allreduce,
with RCCL off, on the old board. It was never a TP decode number.
- 26.3 t/s from the same table is that butterfly tensor's decode. Row 3's 30.5 is
an RCCL tensor decode a day later: same shape family, different collective path.
with RCCL off, on the old board. It was never a TP decode number, and its
paired decode figure is in "The third path" below.
- The butterfly tensor arm and the RCCL tensor arm a day later are the same shape
family on different collective paths; the measured pair lives with
[[frank-medium-radiance-mxfp4]]'s arms.
RCCL needs PCIe atomics on *every* participating GPU — the gate the old board
failed. vLLM's R4D one-shot allreduce dispatches no collective at all, which is
why TP=2 runs today rather than merely "being unblocked": the image ships TheRock
RCCL, and TheRock carries no gfx1201 device code (`invalid device function`), so
radiance could not have used RCCL on this host even with atomics routed. Ubuntu's
RCCL 2.27 does carry gfx1201 kernels, and that is what row 3 linked against.
RCCL 2.27 does carry gfx1201 kernels, and that is what arm L linked against.
**Rule for any future multi-GPU collective experiment: stop and mask the
production service first**, per the benchmarking policy, or run it on a host that
@@ -292,12 +290,11 @@ on the second card, so the atomics gate never comes up.
That is what `frank-medium` ran from 2026-09-16 to 09-20
([[frank-medium-llamacpp-dflash2]]): one process, one model, both cards,
262144 context. Measured against tensor split
on this host: 1991 t/s prefill / 18.8 tok/s decode layer-split, against 272 /
26.3 tensor (butterfly fallback, no RCCL). **That tensor number is not a
prediction for an RCCL-backed build** — it is what tensor split costs when the
allreduce degrades to a meta-backend butterfly, and it is the number the
board-swap path exists to retire.
262144 context. Its measured layer-split rates are that page's; this host's
tensor arm under the butterfly fallback measured 272 t/s prefill / 26.3 tok/s
decode. **That tensor number is not a prediction for an RCCL-backed build** — it
is what tensor split costs when the allreduce degrades to a meta-backend
butterfly, and it is the number the board-swap path exists to retire.
The ranking therefore changed twice. On 2026-09-16: software already gets both
cards onto one model, so the hardware fix stopped being a capability question
@@ -20,8 +20,8 @@ Sweep decode across context depth. **Flat** ⇒ attention over history is not
the cost; the problem is per-token expert movement and KV/attention tuning is
ruled out entirely. A genuinely context-bound server shows a clear downward
slope. Record cold vs warm separately — a small gap means the cache is
working. Measured on the fp8 expert-offload shape: 12.11 tok/s FLAT from 304
to 16,095 tokens.
working. The canonical flat run (fp8 expert-offload shape, a few hundred to
sixteen thousand tokens of context) is in [[Tcclaviger Fork Ops]]'s profile.
## 2. Duty-cycle arithmetic (the "GPU not fully utilized" answer)
@@ -161,7 +161,7 @@ of the same RAM, not extra memory.
## 7. IBS model-access attribution (AMD CPUs)
Real CPU access patterns for mmap'd weights, live, no stop (established on
frank-archon, 2026-09-26, ~2.7M attributed samples in 75 s). Reference:
[[frank-archon]], 2026-09-26, ~2.7M attributed samples in 75 s). Reference:
`scripts/frank-archon-expert-activity.py` (self-contained dump + sample +
analyze; `--analyze-only`).
@@ -23,8 +23,9 @@ guesswork.
each near the target; record `nvidia-smi` used/total plus the boot log's
`n_slots`/`n_ctx_slot`: shape A = current/cheap, shape B = new demand.
`non-KV = used − (tokens × KiB/token)`; the two rows separate
weights/buffers/graphs from the new demand (a 918 MB projector bills
~1.4 GiB).
weights/buffers/graphs from the new demand; a projector is billed after the
fit at roughly 1.5x its file size ([[frank-zealot]] Contract has that
measurement).
3. **Solve the band:** `KV_tokens ≤ (card − non-KV − headroom_low) /
KiB/token`, round the pool DOWN to a multiple of `PARALLEL × 4096` so every
slot gets an equal integer window. Boot and measure; if outside the band,
@@ -54,8 +55,8 @@ First decide which kind of memory is in question:
| device | kind | read it with |
|---|---|---|
| RTX 3090 (Dankclaw) | discrete GDDR6X | `nvidia-smi --query-gpu=memory.total,memory.used,memory.free` |
| gfx1151 iGPU (Dankclaw) | **UMA** — "VRAM" is host RAM | `/sys/class/drm/card1/device/mem_info_gtt_{total,used}` (114 GiB) vs `mem_info_vram_*` (2 GiB carve-out) |
| R9700s (Dankserver) | discrete | `rocm-smi --showmeminfo vram` → 34,208,743,424 B = 32,624 MiB (31.86 GiB) per card, read 2026-10-18 — **not** the 64 GiB "SVM-backed" figure this row once carried ([[Dankserver System Specs]]) |
| gfx1151 iGPU (Dankclaw) | **UMA** — "VRAM" is host RAM | `/sys/class/drm/card1/device/mem_info_gtt_{total,used}` vs `mem_info_vram_*`; the aperture and carve-out values are [[Dankclaw System Specs]]'s |
| R9700s (Dankserver) | discrete | `rocm-smi --showmeminfo vram`; the per-card byte/MiB value (and the stale "64 GiB SVM-backed" figure it replaced) is [[Dankserver System Specs]]'s |
Per-process truth on NVIDIA:
`nvidia-smi --query-compute-apps=pid,process_name,used_memory`. Never add the
@@ -63,9 +64,10 @@ iGPU's GTT to host RAM as a separate pool; `free -g` is the arbiter for
co-tenants.
- **Engine ledger first.** Halogen prints weights pinned (+ remaining 2 MiB
contiguous blocks — the honest fragmentation signal: 240 blocks/12.3 GiB
after reboot vs 475/14.7 GiB before means the model did not come back into
the same hole), device budget, KV pool/working split, "left for everything
contiguous blocks — the honest fragmentation signal: a block census that drops
across a restart means the model did not come back into the same hole; the
before/after census this was learned on is that era's, on [[frank-dragoon]]),
device budget, KV pool/working split, "left for everything
else". llama.cpp prints them at `llama-model.cpp:1827` (per-backend model buffer)
and `llama-context.cpp:692` (compute buffer) on the era's build; on today's
`master` the same `LLAMA_LOG_INFO` lines sit at `llama-model.cpp:2009`
@@ -83,13 +85,13 @@ co-tenants.
graph/compute pool).
- **Traps:** mmap'd PLE is reclaimable page cache; `-ot` makes it pinned —
"move layers to the card" converts reclaimable cache into device memory and
frees nothing for co-tenants. A served model's footprint ≠ weight size
(a retired frank-zealot shape: 22.6 of 24.1 GiB for an 11.29 GiB model; the card
today reads 24,576 MiB = 24.0 GiB total, `nvidia-smi --query-gpu=memory.total`,
2026-10-18, and the deployed payload is the PQ2_0 pair
[[frank-zealot]]). Do the capacity
subtraction before designing: 12 attn layers × 1.62 GiB + 200k f16 KV =
28.6 GiB > 24.1 GiB usable → KV-in-VRAM on that pair only reaches ~64k.
frees nothing for co-tenants. A served model's footprint is not its weight
size: the MiMo Q5_K_M era booted a 6.40 GiB payload to **20,530 MiB used** of the
3090's addressable 24,576 — the Eras row and its addressable ≠ advertised law are on
[[frank-zealot]]. What the card reports used is weights + KV + compute buffers +
projector, never weights alone. Do
the capacity subtraction before designing, on the card number the owner page
quotes, not the advertised one.
- **Context lengths: derive, never remember.** `llama-bench` computes
`n_ctx = n_prompt + n_gen + n_depth` (defaults 512/128/0, type_k/v F16,
flash_attn AUTO, split_mode LAYER). `-p 512,16384 -n 64` = contexts
@@ -112,10 +114,10 @@ them from the binary (`llama-server --help`) before sizing on them.
**State law** (compute before trusting any cap): a state is the serialized KV
of the whole sequence + constant recurrent state + the draft context's KV:
`per_token = 2 × n_full_attn_layers × n_kv_heads × head_dim × bytes (K and V) +
draft_per_token`. Measured on frank-medium (qwen3.5 hybrid, f16, MTP draft):
16×4×256×2×2 = 64 KiB/token + 4 KiB draft = **68 KiB/token**; a full
196,608-token slot state ≈ 12.8 GiB. Cross-check against the journal's
`prompt state size` lines — the two must agree.
draft_per_token`. The worked qwen35-hybrid instance of that formula (full layer
count, f16 KV, MTP draft) and the slot-size table it prices are on
[[frank-medium-llamacpp-dflash2]]; cross-check any such arithmetic against the
journal's `prompt state size` lines — the two must agree.
**The journal is the instrument:** `exceeds cache size limit … skipping` =
cap binding in production; `making room … removing oldest` / `cache size
@@ -162,13 +164,13 @@ Only the charlie12345/ROCmFPX build-qwen38 (PR #27742 qwen4exp merged) takes
it; CIRU, upstream and stock builds do not. Validate live: load with
`--flash-attn on`, health 200, a real completion finishing `"stop"`, journal
clean of `GGML_ASSERT`/`cudaMalloc`/`missing tensor`, and flash-attn engaged
in the timings. Context cap for full-model residency on that shape stayed
32,768.
in the timings. The context cap that build reached for full-model residency is
part of that era's record on [[frank-dragoon]].
## Related
- [[GGUF Compatibility]] — what a payload is before it is priced
- [[frank-zealot]] — the two-boot priced shape of the retired MiMo era (live shape today: [[frank-zealot]])
- [[frank-medium-llamacpp-dflash2]] — where the 68 KiB/token datum was measured
- [[frank-zealot]] — the endpoint whose Eras rows are the two-boot priced shapes this method produced
- [[frank-medium-llamacpp-dflash2]] — the state-law worked example behind section 3
- [[DFlash DSpark Speculative Decoding]] — the spec A/B protocol on llama.cpp
- [[Dual-R9700-Cooperation]] / [[x4-pair-experiment]] — the multi-device side
@@ -65,13 +65,11 @@ LPDDR5X on the Strix Halo — 128 GB LPDDR5x-8000 on a 256-bit bus,
streaming loses by construction and no tuning rescues it; doubling the link
only doubles a losing number.**
Worked example (Dankclaw 2026-09-16): Gen4 x4 measured H2D **6.72 / D2H 6.60
GB/s** (85 % of spec) — pinned H2D ≈ async, pageable within 1 %, i.e.
link-limited not staging-limited. A 3 MiB slab (the expert-streaming
granularity) = 474 µs. A3B MoE at Q4_K_M (~1.70 GiB active/token) → 272
ms/token ⇒ **3.7 tok/s** (4.6 with 20 % resident) vs measured **20.46 tok/s**
with experts computed on the CPU, 25–26.5 on the iGPU. The streaming design
loses 3–5x.
Worked example: the Dankclaw Gen4 x4 run of 2026-09-16 — measured H2D/D2H
rates, the 3 MiB slab cost, and the streaming-vs-compute comparison it decided
— is recorded on [[x4-pair-experiment]] §The link, measured. Read it once and
apply the rule above: on that link the streaming design lost by 3–5x, and the
x8 ceiling would only have halved the loss.
## 3. Measure the link: two probes, same mechanics
@@ -150,9 +148,9 @@ int main(int argc, char **argv) {
(the usual "pageable costs 2x" rule does not apply at this link).
- Small-transfer penalty (2.345 MiB: ~106 µs vs ~78 µs ideal, ~21 %) is
per-transfer overhead — recoverable only by BATCHING transfers, not by a
faster link. (Dankserver 2026-09-29: bulk 22.7 GB/s at the 2.344 MiB slab =
91 % of Gen5 x8, while the engine's effective read path ran ~9.5 GB/s — the
gap is the fork's gather software path, not the wire.)
faster link. The engine-vs-wire version of that reading on Dankserver
(bulk slab rate against the engine's effective gather rate, same
granularity) is [[frank-archon]]'s tcclaviger-era record.
- Both GPUs symmetric ⇒ no cross-device rebalance is available; measure both.
- Don't quote a derived line from inside a probe without hand-checking it —
a printf format/argument mismatch in one of these printed a nonsense
@@ -186,7 +184,8 @@ sudo lspci -vv -s <bridge> | grep AtomicOpsCap # want Routing+
`Routing-` is a read-only hardware capability. Typical AM5 layout: one x16
slot on CPU root ports (atomics fine), the rest behind the chipset (usually
not). Bandwidth for TP is small anyway: ~10 MB per token-step for a 27B at
batch 4 ≈ 1.4 ms at Gen4 x4 — compute-bound, not interconnect-bound.
batch 4 is a millisecond-scale figure at x4 — compute-bound, not
interconnect-bound (full derivation: [[Dual-R9700-Cooperation]] §Bandwidth).
**Probe matrix (bare 2-rank `torch.distributed`, one process per GPU, 25 s PG
timeout) — one probe answers several design questions:**
@@ -215,13 +214,12 @@ reporting an outage (`gitea` vs `gitea-server`).
1. CPU-attached M.2 → PCIe x4 riser (~$30) — puts the card on the CPU root
complex; x4 suffices for TP.
2. Board whose CPU x16 bifurcates x8/x8 (done 2026-09-20 on dankserver — both
R9700s on root ports `00:01.1`/`00:01.3`, every bridge `Routing+`, both
endpoints `ReqEn+`). Re-checked 2026-10-18 without root: both root ports
report `current_link_width=8` at `32.0 GT/s`
2. Board whose CPU x16 bifurcates x8/x8 (done 2026-09-20 on dankserver — the
per-device chain census, including both endpoints reading `ReqEn+`, is
[[Dual-R9700-Cooperation]]'s). Re-checked 2026-10-18 without root: both root
ports report `current_link_width=8` at `32.0 GT/s`
(`/sys/bus/pci/devices/0000:00:01.{1,3}/current_link_width`), and the live
engine states direct P2P between the cards
([[Dual-R9700-Cooperation]]).
engine states direct P2P between the cards.
3. llama.cpp `--split-mode layer` — no collectives at all ([[Llama Cpp Multi-Device Splits]]); costs the GGUF conversion plus paged attention /
continuous batching / spec decode.
4. Independent TP=1 replicas + routing proxy — cards never share one model.
@@ -43,8 +43,16 @@ state, verified against the live journal and output smoke tests
the mismatch (`resolve_fused_ops: Flash Attention enabled`) via the
GGML_CUDA_FA_ALL_QUANTS rebuild — see "FA-all-quants rebuild" below.
- Pool `262144`, `PARALLEL=1`, live `n_ctx_slot = 262144`.
- MTP: standalone draft head, acceptance 0.83 then 0.75, mean len
4.0-4.3, per-position ~0.86/0.71/0.71/0.71.
* MTP: standalone draft head, acceptance 0.83 then 0.75, mean len 4.0-4.3,
per-position ~0.86/0.71/0.71/0.71 — stronger at every position than the old bundled
head, which measured 0.739/0.554/0.427/0.340.
- Spec chain as deployed: `--spec-draft-n-max 4` on the standalone draft, with
the `--spec-default` ngram-mod fallback at n_match 24 / n_min 48 / n_max 64
(the preset that survived the DSpark/DFlash retirements —
[[qwen3.8-spec-matrix]]).
- Batch 8192 / ubatch 256, `--direct-io`, `--fit off`, `--gpu-layers all`. The
pi `models.json` declared 262144 (`contextWindow = maxTokens`, the house
convention of that era).
- Head GPU side: **ROCm0** (no `--device` flag; HIP registers before
Vulkan).
- Worker steady state: 23854/24576 MiB used, ~273 MiB free (tight but
@@ -215,8 +223,11 @@ head env ([[frank-dragoon]]).
faster prefill, ~3% slower decode (measured on this box).
3. **Pin `--tensor-split 1,1`.** The default split is proportional to
*reported free memory*: the Strix reports up to 96 GiB (GTT) vs the
3090's 24 GB, so the default lands ~20/80 on the slow device (verified
with a 4B smoke test). 1,1 = 50/50 = balanced per-layer latency.
3090's 24 GB, so the default lands ~20/80 on the slow device. 1,1 = 50/50 =
balanced per-layer latency. The 2026-08-15 4B smoke test that proved it
(production untouched): at a 0.2/0.8 split only 4 of 36 layers reached the
3090, and at a real split worker VRAM rose ~819 MiB with 95 t/s prefill /
58.8 t/s decode on the tiny model; link RTT ~0.3–0.5 ms.
4. **`-dio` on the head.** Large model + RPC hangs at load without direct
I/O.
5. **RPC is unauthenticated and unencrypted.** LAN only. The worker binds
@@ -8,8 +8,8 @@ timestamp: 2026-10-05T00:00:00-07:00
# RDNA4 Container Serving
Bring-up path for Dankserver (2x Radeon AI PRO R9700, gfx1201, 31.86 GiB each =
**63.7 GiB real VRAM**, 246 GiB RAM, 7700X) with
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
@@ -155,7 +155,9 @@ podman run -d --name <ctr> --ipc=host --ulimit memlock=-1:-1 \
## 5. Memory budgeting: the levers are not where they look
The planner builds `budget = total × util`, subtracts a KV reserve, and gives
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
@@ -163,11 +165,14 @@ the REMAINDER to the expert arena. Consequences, all measured:
- **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 (9190
B/token for Qwen4Exp) misses the large fixed per-sequence Mamba state
- **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>` (2 GiB →
113,595 tokens / 3.47x concurrency at 32k). For long context,
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
@@ -201,18 +206,21 @@ 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 (118
GiB offload request → ~238 GiB) and is invisible in the weight count. Read the
log for `Using 'EMULATION' Mxfp4 MoE backend` before concluding "doesn't fit".
**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**: 12.11
tok/s FLAT from 304 to 16,095 context tokens — KV attention is not the cost.
With ~29 % of experts arena-resident, ~0.84 GB 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.
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:**
@@ -8,10 +8,12 @@ timestamp: 2026-10-05T00:00:00-07:00
# RDNA4 MoE Expert Offload VRAM Budget
For VRAM-bound vLLM on the tcclaviger RDNA4 fork (frank-archon: 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
[[frank-archon]]; boot-failure capture lives in
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
@@ -36,7 +36,7 @@ In unified memory:
### ROCm Version
vLLM supports AMD GPUs with ROCm 6.3 or above *(unverified 2026-10-18)*. Dankclaw has ROCm 7.2.4 from AMD repos (`/etc/apt/sources.list.d/rocm.list` → `deb https://repo.radeon.com/rocm/apt/7.2.4 noble main`; `rocm-core 7.2.4.70204-93~24.04`, read 2026-10-18). No vLLM endpoint runs on this host today — 7770 is halogen-flash-server and 7771 is llama.cpp ([[Dankclaw System Specs]]) — so this page is reference material, not the serving path.
vLLM supports AMD GPUs with ROCm 6.3 or above *(unverified 2026-10-18)*. Dankclaw's ROCm version and which engine owns which port today are [[Dankclaw System Specs]]'s — so this page is reference material, not the serving path.
Check ROCm version:
@@ -166,7 +166,7 @@ Unified memory has lower bandwidth than dedicated VRAM:
### ROCm Overcommit
ROCm reports ~114 GiB of GPU memory on Strix Halo APU (the GTT aperture: `mem_info_gtt_total` = 122402963456 bytes = 114 GiB, read 2026-10-18 under `/sys/class/drm/card1/device/`; `llama-server --list-devices` shows 116,732 MiB — the 2026-08-17 "96 GiB" figure was stale). Physical RAM visible to the OS is **121 GiB** today (`free -g` total 121, read 2026-10-18; 64 GiB in the earlier reading on this page, upgraded from 32 GB). The GPU can address more than physical RAM exists because the GTT aperture maps over the shared LPDDR. vLLM will see ~114 GiB and try to allocate accordingly. Set `--gpu-memory-utilization` to 0.80 or lower to prevent OOM.
ROCm reports the GTT aperture as "GPU memory" on this APU; the aperture size, the physical `MemTotal` behind it and the firmware carve-out are [[Dankclaw System Specs]]'s numbers, and that page also carries the 2026-08-17 "96 GiB" reading this page used to quote (stale). One reporting quirk worth keeping here: `llama-server --list-devices` prints 116,732 MiB for the same device the sysfs file calls 114 GiB — engines round the aperture differently, so read the sysfs value when sizing. The GPU can address more than physical RAM exists because the GTT maps over the shared LPDDR. vLLM will see the aperture and try to allocate accordingly; set `--gpu-memory-utilization` to 0.80 or lower to prevent OOM.
Lower bandwidth means slower inference. Plan for lower throughput.
@@ -232,9 +232,8 @@ FP8 quantization depends on GPU architecture:
the matmul claim as the settled half.)
For FP4 on Dankclaw the ROCmFPX llama.cpp fork (`Q2_0_ROCMFP4`) provided native
ROCm FP4 kernels; that runtime line is **retired** (CIRU/Kairic eras,
[[frank-dragoon]], [[frank-dragoon]]) and no
ROCmFPX build serves this host today.
ROCm FP4 kernels; that runtime line is **retired** — the CIRU and Kairic eras on
[[frank-dragoon]] — and no ROCmFPX build serves this host today.
## Example Commands
+38 -100
View File
@@ -60,6 +60,8 @@ TUNABLE=0`, stock gather 8×16 → 26.80/25.15/26.81 tok/s @ 166/1087/4064 ctx.
Decode on this stack is PCIe-gather-bound (~30% GPU duty cycle), so
batch-shaped compute knobs move ~nothing.
Measured with no spec: **12.11 tok/s FLAT from 304 to 16,095 context tokens** — KV
attention is not the cost, so a context-slope hunt on this stack is null by construction.
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
@@ -120,95 +122,36 @@ 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).
Dankserver's card count, per-card VRAM and host RAM are in [[Dankserver System Specs]];
`torch.cuda.device_count()` returns 3 on this box and summing `total_memory` overstates it
badly (the Raphael iGPU is in the list — reading discipline: [[Llama Cpp Serving Sizing]]).
**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.
Everything else about running this image — the quant-kernel gate that decides
which checkpoints can serve at all, the entrypoint contract, the known-good
invocation, the `/dev/dri` and memlock traps, the util and max-model-len arena
traps, the `--ipc=host` / `-d` stdout traps and the gated-fetch discipline — is
written once on [[RDNA4 Container Serving]], and the plan arithmetic behind the
arena/KV traps is [[RDNA4 MoE Expert Offload VRAM Budget]]'s. Three findings
stay here because this harness produced them:
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:
- **The KV reserve undercounts hybrid-attention models ~2×** (the planner's
B/token figure against ~18,000 real: the Mamba state is a fixed per-sequence
cost). Fix by pinning the pool: `--kv-cache-memory-bytes`.
- **`--expert-offload-mem` caps 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 inside
`place_experts`, and the plan rewrites `expert_offload_mem` back to the cap
the plan met, so plan and allocation cannot drift).
- **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.
```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 \
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:
1. **Mount the whole `/dev/dri`, never individual render nodes.** Render
nodes without the primary `card*` nodes → HIP enumerates two agents and
refuses a context on either (`RuntimeError: No CUDA GPUs are available`)
while `device_count()` cheerfully returns 2. Confirmed by direct A/B.
2. **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 misleading
`torch.AcceleratorError: CUDA error: out of memory` from `pin_memory()`;
it cannot be raised from inside a container or root's own shell — only
PAM applies it at login. Matters because `--expert-host-mlock` defaults
True.
3. **`--gpu-memory-utilization` does 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.
4. **Lowering `--max-model-len` is 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).
5. **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`.
6. **`--expert-offload-mem` caps 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 inside
`place_experts`, and the plan rewrites `expert_offload_mem` back to the
cap the plan met, so plan and allocation cannot drift).
7. `--ipc=host` and `--shm-size` are mutually exclusive in podman; pick one.
8. `podman run -d` without `-a` sends no stdout to a redirect — the log file
only receives the container ID; use `podman 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.
The checkpoint this recipe was verified on is
`orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8` (gated; 172.8 GiB, 131 shards,
`Qwen4ExpForConditionalGeneration`, 48 layers, 512 experts top-10, multimodal,
native 262,144) — an [[frank-archon]] era payload.
## Compile-cache persistence (survives `podman rm -f`)
@@ -240,20 +183,15 @@ cold, of which 173 s was one graph).
## 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.
The journal one-liner that reads the plan lines (digit-anchored `grep -oE`,
because plain grep truncates those columns to `17.`) and the post-deploy
verification order — plan vs captured graph memory, `GPU KV cache size` vs the
ask, per-card `rocm-smi` high-water, `NRestarts` = 0, `VLLM_CUDAGRAPH_*` present
in the container env, then a real completion from a PEER because
`curl localhost:7770/health` inside an ssh session on the host can return `000`
and look like an outage — are step 3 of
[[RDNA4 MoE Expert Offload VRAM Budget]] §7. Run that; do not re-derive it
here.
## Related
@@ -241,6 +241,11 @@ Before 2026-09-26 the port belonged to the **`frank-medium`** service: [[frank-m
seqs 2 and seqs 3 are statistically tied (30.9-33.1 t/s, inside the ±3 % floor). The "seqs 2
cannot boot" law belonged to the retired tcclaviger fork's `torch.compile` shape
specialization, and radiance booted seqs 2 on its first fp8-native start.
**Same era, separate law: a dequantize-to-compute path bills host RAM twice.** On the
EMULATION backend an offload request of **118 GiB** landed as **~238 GiB** of real host RAM —
the dequantized copy rides on top of the pinned weights, and the doubling is invisible in the
weight count. Read the engine log for `Using 'EMULATION' Mxfp4 MoE backend` before concluding
"doesn't fit"; the container-side procedure and the kernel gate are [[RDNA4 Container Serving]].
- **One encoding per expert table** (`rad_arch_declare`): a whole expert kept bf16 is the only
thing legal beside the table, so a split tier (down=fp8, gate_up=w4) is unservable.
- **Descriptor-selection law.** The plugin is picked by the exact `(arch_id, quant descriptor)`
@@ -25,6 +25,12 @@ what each buys. The answer is that `--ubatch-size` has an **optimum** on this
stack, that the logical batch does not matter at all, and that the optimum is
much cheaper than what was deployed — which then moved the layer split.
The deployment this sweep tuned (`frank-medium`, 2x R9700, llama.cpp layer
split) is retired; its shape and boot ledger are
[[frank-medium-llamacpp-dflash2]]'s, and the endpoint lineage continues on
[[frank-archon]]. What this page owns is the sweep, the harness traps and the
coupling law between batch and split.
## The shape of the measurement
A cell is a **64k prefill measured with 64k of KV already resident**, plus a
@@ -104,7 +110,7 @@ kill — no requests):
|---|---|---|---|---|
| 36,29 — deployed before | 1536 | 4.71 GiB | 4.91 GiB | 0.20 GiB |
| 36,29 | **512** | 6.01 GiB | 7.32 GiB | 1.31 GiB |
| **35,30 — deployed now** | **512** | **6.44 GiB** | **6.90 GiB** | **0.46 GiB** |
| **35,30 — deployed after** | **512** | **6.44 GiB** | **6.90 GiB** | **0.46 GiB** |
Two things follow.
@@ -153,7 +159,7 @@ other (measured 2.41 GiB vs 1.30 GiB going 1536 → 512). Balance by
measurement only: one `-ts` step is one layer, worth ~0.43 GiB on a DeltaNet
boundary and ~1.43 GiB on an attention one, so the reachable minimum gap is
about one DeltaNet layer, never zero. Reference: 36,29 @1536 → 4.71/4.91;
36,29 @512 → 6.01/7.32; **35,30 @512 → 6.44/6.90 (deployed)**.
36,29 @512 → 6.01/7.32; **35,30 @512 → 6.44/6.90 (the shape that deployed)**.
Re-fit both ubatch coefficients in `tests/frank_medium_unit.bats` at the new
deployed point and re-anchor the per-card constants; check neighbouring
@@ -14,54 +14,28 @@ timestamp: 2026-09-03
# Qwen3.8-27B Speculative-Decoding Matrix
This page records the speculative-decoding benchmark history and the
production tuning for the Qwen3.8-27B service (`frank-dragoon`),
now a pipeline-parallel head with a Dankserver worker.
production tuning for the then-Qwen3.8-27B service (`frank-dragoon`), at the
time a pipeline-parallel head with a Dankserver worker.
## Production state (2026-08-17: Q6_K, q8_0/f16 KV, split 0.68)
## Production state (2026-08-17)
Current production, verified against the live process and journal
(2026-08-17, load 02:06 + smoke tests). Topology detail:
[[Pipeline Parallelism/Qwen3.8 PP Deployment]].
- Devices: `RPC0` (Dankserver RTX 3090) + `ROCm0` (Strix iGPU). The head
passes no `--device` flag. Default selection puts RPC first and picks
ROCm for the iGPU (HIP registers before Vulkan). The Vulkan0 side stays
an option via `HEAD_BIN=.../build-vulkan-rpc`.
- Model: `Qwen3.8-27B-Uncensored-Q6_K.gguf` (JonathanColetti, 22.43 GB,
imatrix, 65 blocks, 866 tensors, NO bundled MTP head). The repo's
standalone draft sidecar `Qwen3.8-27B-Uncensored-draft-Q8_0.gguf`
(3.16 GB) is loaded via `--spec-draft-model`; it runs on ROCm0 only
(1718 MiB, zero worker impact).
- Spec: **MTP** (standalone draft) `--spec-draft-n-max 4` + the
`--spec-default` ngram-mod fallback (48/64/24). Measured acceptance
0.83 then 0.75, mean len 4.0-4.3, per-position ~0.86/0.71/0.71/0.71 —
stronger than the old bundled head (0.739/0.554/0.427/0.340).
- KV: K **q8_0** + V **f16** (mismatched). 3136 B/token per FA layer =
784 MiB at 262144 (K 1088 + V 2048). RPC0 8624 MiB (11 FA), ROCm0
3920 MiB (5 FA); total 12544 = 16 x 784. **Flash Attention stays
enabled with the mismatch** (`resolve_fused_ops: Flash Attention
enabled`) via the GGML_CUDA_FA_ALL_QUANTS rebuild of both binaries
at 9b05354ec — see the PP doc.
- Split: `--tensor-split 0.68,0.32` = 45 of 65 blocks (0-44) on the
3090, 69% of the layer weight. The maximum: 46 blocks -> ~0.3 GiB
margin; 48 blocks (f >= 0.7197) adds FA block 47 -> KV 9408 -> OOM by
~0.85 GiB. Boundary math: the PP doc.
- Context: pool 262144, 1 slot, `n_ctx_slot = 262144`. The pi
`models.json` declares 262144 (contextWindow = maxTokens, house
convention).
- Worker steady state: 23854/24576 MiB used on the 3090, ~273 MiB free
(tight but stable; all allocations happen at load).
- Batch 8192 / ubatch 256, `--direct-io`, `--fit off`,
`--gpu-layers all`.
The pipeline-parallel shape this matrix was tuned for — Q6_K target, standalone
Q8_0 draft on ROCm0 only, `--tensor-split 0.68,0.32` (45 of 65 blocks), K `q8_0`
/ V `f16`, pool 262144 at one slot, worker steady at 23854/24576 MiB — is
recorded once, in [[Pipeline Parallelism/Qwen3.8 PP Deployment]] §Production
state, together with the split-limit arithmetic (why 45 blocks is the maximum),
the KV-per-layer bill and the FA-all-quants rebuild the mismatch needed. That
era's endpoint identity is an [[frank-dragoon]] era. This page keeps only the
speculative-decoding measurements.
## How MTP works on Qwen3.8 (bundled-head era)
The unsloth `UD-*` GGUFs keep the MTP (Multi-Token Prediction) head inside
the model file. llama.cpp b10411+ auto-detects it and creates the draft
context from the target model. Do not pass `--spec-draft-model` for those
files. This is superseded: the production `Qwen3.8-27B-Uncensored-Q6_K.gguf`
has NO bundled head, and the serve script passes the standalone draft
sidecar with `--spec-draft-model` (see Production state above).
files. This is superseded: the production target of 2026-08-17 carried no
bundled head, and the serve script passed the standalone draft sidecar with
`--spec-draft-model` (shape: [[Pipeline Parallelism/Qwen3.8 PP Deployment]]).
The stock llama.cpp build must be b10411 or newer. The 2026-08-14 rebuild
on Dankclaw is b10433 (`9b05354ec`) with `GGML_HIP` + `gfx1151` + Vulkan.
@@ -83,14 +57,14 @@ standalone configuration only; the PP head runs ROCm0.
6/ABRT, systemd restart, ~503 during reload). DSpark reuses the DFlash
block-diffusion machinery, which shares one draft context across
parallel slots; concurrent block drafting faults in HIP stream sync.
This is the same multi-slot bug class that retired `draft-dflash`
(premature EOS). The crash shows on 16k concurrent streams, not on a
67k pair — concurrency is the trigger, not size. Fix: PARALLEL=1
(requests queue safely). Retired the same day.
This is the same multi-slot bug class that retired `draft-dflash` (premature
EOS, retired 2026-08-14). The crash shows on 16k concurrent streams, not on a
67k pair — concurrency is the trigger, not size. Fix: PARALLEL=1 (requests
queue safely). Retired the same day.
- **2026-08-15 to 08-16: `--spec-default` (ngram-mod), Vulkan0** — n_match
24 / n_min 48 / n_max 64, draftless, PARALLEL=2 (2 x 32k parallel stream
test passes). Chosen for parallel safety after DSpark; kept as the
fallback under MTP in the current config.
fallback under MTP in the 2026-08-17 shape.
## Benchmark Method
@@ -146,19 +120,19 @@ ubatch 256, single slot, on scratch port 7892.
Verdict: at very high context, Vulkan0 decodes **48% faster** (5.68 vs
3.83 t/s); ROCm0 prefills 12% faster. Generation is the interactive cost
(users stream tokens). This verdict applies to the standalone
configuration; the PP head runs ROCm0 (HIP registers before Vulkan).
(users stream tokens). This verdict applies to the standalone configuration; the PP
head runs ROCm0 — HIP registers before Vulkan and the iGPU dedup rule (the llama.cpp
PR #23897 workaround) keeps the first backend's device.
Both `tg_3s` values were rock-steady across the run (3.83 and 5.67 t/s).
High-context decode collapses on both backends versus ~25 t/s at 16k ctx;
that is the long-context attention penalty, not a spec artifact.
## Production Configuration
## Where the tunables live
The live tunables live in `scripts/frank-dragoon.env` (head) and
`scripts/frank-pp-worker.env` (worker) in this repo. The Production
state section at the top of this page records the current values: Q6_K
model + standalone MTP draft, split 0.68/0.32, 262144 ctx, PARALLEL=1,
q8_0/f16 KV.
That era's head and worker env/units are listed (and marked deleted) in
[[Pipeline Parallelism/Qwen3.8 PP Deployment]] §Files. `scripts/frank-dragoon.env`
is no longer a PP-head env at all — it is the halogen-flash-server trio of the
current era, which [[frank-dragoon]] owns.
## See Also
@@ -28,7 +28,7 @@ Related pages:
| 2 | MTP `n_max` explicit 4–5 + Q4_0 draft head | +5–15% on the spec share | restart, 1.56 GB file | test 08-16 |
| 3 | Plain `Q8_0` A/B vs UD-Q8_K_XL (same 8 bits) | dequant ALUs may differ | restart, 29 GB file | test 08-16 |
| 4 | Prefill path: ROCm backend + tuned ubatch/threads-batch | 2–3× prefill for agentic turns | per-workload backend choice | partial data |
| 5 | TTM `pages_limit` raised to 114 GiB | removes the hard VRAM ceiling; the full 262k f16 KV plus the Q8 AGENT payload, MTP draft, and mmproj fit in one slot | modprobe, set live 2026-08-25 | applied (29,883,536 pages) |
| 5 | TTM `pages_limit` raised | removes the hard VRAM ceiling; the full 262k f16 KV plus the Q8 AGENT payload, MTP draft, and mmproj fit in one slot | modprobe, set live 2026-08-25 | applied — value and file: [[Dankclaw System Specs]] (Memory) |
| 6 | Vulkan ICD pinning + loader-layer disable | robustness; small | service env vars | test 08-16 |
Everything else (quantization downgrade, NVFP4, larger drafts) is documented
@@ -62,32 +62,32 @@ Consequences of the hybrid design:
cannot be prefix-cached the way attention KV can. Every agentic turn
re-prefills the whole conversation. **Prefill is the hidden cost of
agentic use**, and it is the dominant cost we have not yet optimized.
3. **MTP is native and strong.** The model ships a nextn head; our matrix
measured 13.3 t/s vs 7.2 t/s (no spec) at 16k on Vulkan.
3. **MTP is native and strong.** The model ships a nextn head; on Vulkan at 16k
it roughly doubled the no-spec rate (the pair of figures is
[[qwen3.8-spec-matrix]]'s realistic-prompt table).
## Current production state (baseline)
## Baseline this page optimized (2026-08-15)
`frank-dragoon` (Dankclaw, Vulkan0, 2026-08-15):
- Model: UD-Q8_K_XL, 31.5 GB, bundled MTP head.
- `--spec-default` (ngram-mod) with draft-mtp first in the chain.
- 2 parallel slots × 262144 ctx, KV q8_0, batch 8192, ubatch 256,
threads 12, `-dio` off, `--fit off`.
- Measured: ~25 t/s at 16k ctx; 5.68 t/s at 262k ctx; prefill 302 t/s @64k
(Vulkan) vs 410 t/s @64k (ROCm, same build).
- GPU: 28 GiB of 96 GiB GTT used. **74 GiB sits idle.**
`frank-dragoon` then ran llama.cpp on the iGPU with the UD-Q8_K_XL target
(bundled MTP head), `--spec-default` ngram-mod under a draft-mtp first link, two
262144 slots, q8_0 KV, batch 8192 / ubatch 256, threads 12. That era's ledger is
[[frank-dragoon]]'s; what this page keeps is the shape of the problem: the
service occupied **28 GiB of the then 96 GiB firmware VRAM carve, ~74 GiB
idle**, decode held ~25 t/s at 16k and collapsed at 262k depth
([[qwen3.8-spec-matrix]] §High-Context Backend Comparison), and prefill was the
unoptimized agentic cost (section 4).
## The levers, in detail
### 1. Pipeline-parallel to the RTX 3090 (biggest win)
The 3090 on Dankserver is idle (serves a 22 GB MoE small-model service with
~2.4 GB free). llama.cpp RPC splits the model by **layers**: each device
runs its layer range, hidden states cross the 2.5 GbE ethernet once per
token. The per-token transfer is ~41 KiB (5120-wide f32 hidden state, both
directions); at ~0.5 ms LAN RTT the bandwidth cost is negligible — the split
is latency-bound, not bandwidth-bound (see
[Ethernet Bandwidth Math](Pipeline%20Parallelism/Ethernet%20Bandwidth%20Math.md)).
The 3090 on Dankserver had headroom that night (it carried the day's small-model
service with a couple of GiB free — that card's eras are [[frank-zealot]]'s).
llama.cpp RPC splits the model by **layers**: each device runs its layer range,
hidden states cross the 2.5 GbE ethernet once per token. The per-position wire
bill is measured in [[Pipeline Parallelism/Ethernet Bandwidth Math]] §0; at
~0.5 ms LAN RTT the bandwidth cost is negligible — the split is latency-bound,
not bandwidth-bound.
Why this is a real speedup and not just "two GPUs": the 3090 does
matmuls roughly 2–3× faster per layer than the 8060S (936 vs 512 GB/s
@@ -95,58 +95,32 @@ memory, 80 vs 40 CUs). A 50/50 layer split balances per-layer latency, so
each token step takes ≈ max(half-on-3090, half-on-Strix) + one RTT instead
of all-64-layers-on-Strix.
Design decisions (verified in b10433 source, 2026-08-15):
- **Head on Dankclaw, worker on Dankserver.** The 31.5 GB model file stays
on the head; the worker receives its tensor share over the wire at load
(with `-c` it caches the tensors locally after the first load).
- **Head must NOT pass `--device`.** When `params.devices` is set,
`llama_prepare_model_devices` builds the device list from exactly those
devices and the RPC device is silently dropped (`--rpc` becomes a no-op).
Default device selection inserts RPC servers **at the front** of the list.
- **The local iGPU side runs on ROCm, not Vulkan.** Default selection walks
registered backends in order; HIP registers before Vulkan, and the iGPU
dedup rule (PR #23897 workaround) keeps the first backend's device.
Verified live 2026-08-15: device list `[RPC0, ROCm0]`, Vulkan0 never
listed. Consequence: the Strix half runs on the ROCm backend — better
prefill, ~3% slower decode than Vulkan0 (measured on this box).
A second head build (`/home/dank/llama.cpp/build-vulkan-rpc`, HIP off,
Vulkan on, RPC on) was built the same night for an A/B; with it the
device list becomes `[RPC0, Vulkan0]`.
- **Default split is proportional to *reported free memory*, which is wrong
here.** The Strix reports up to 96 GiB (GTT ceiling) vs the 3090's 24 GB,
so the default lands ~20/80 in favor of the slow device (verified with a
4B smoke test: 4 of 36 layers on the 3090). We pin `--tensor-split 1,1`
(50/50) explicitly.
- **`-dio` (direct I/O) on the head.** Large model + RPC load hangs without
it (visorcraft Strix finding; matches our 28 GiB load-stall history).
- **Version skew hangs the handshake.** Worker built at the identical
commit (9b05354ec / b10433) in a worktree
`/home/dank/llama-cpp-rpc` on Dankserver — production b10121 build
untouched.
Smoke test (2026-08-15, 4B model, 0.2/0.8 split, production untouched):
worker VRAM rose ~819 MiB (real split), 95 t/s prefill, 58.8 t/s decode on
the tiny model. RTT on the link: ~0.3–0.5 ms.
Full design, scripts, and morning runbook:
[Qwen3.8 PP Deployment](Pipeline%20Parallelism/Qwen3.8%20PP%20Deployment.md).
Every decision this led to is written once, in
[[Pipeline Parallelism/Qwen3.8 PP Deployment]] §Gotchas (with the 4B smoke test
that proved each): head on the box that already holds the model file, **no
`--device` on the head** (setting it silently drops RPC), the fixed
HIP-before-Vulkan device selection, the reported-free-memory split trap, the
identical-build requirement, and `-dio` on the head — the last one visorcraft's
Strix finding, matching our own 28 GiB load-stall history. Scripts and the
morning runbook are on the same page.
### 2. Speculative-decoding tuning (same bits, free tokens)
- **MTP `n_max`.** Production leaves `--spec-draft-n-max` unset. Our matrix
(Vulkan, 16k) found 13.3 t/s at the tested setting vs 7.2 without spec;
the published Strix tuning curve peaks at n_max 3–4 (30.3 t/s at 4 vs
28.9 at 3 vs 29.9 at 5). The PP env sets **4** explicitly; the matrix
re-tests 3/4/5 on the real split.
- **MTP `n_max`.** Production left `--spec-draft-n-max` unset; the fleet matrix
(Vulkan, 16k, realistic prompt) found the depth-5 peak against the no-spec
baseline — both figures are [[qwen3.8-spec-matrix]]'s realistic-prompt table.
The published Strix tuning curve peaks at n_max 3–4 (30.3 t/s at 4 vs 28.9 at
3 vs 29.9 at 5). The PP env set **4** explicitly; the matrix re-tested 3/4/5
on the real split.
- **Q4_0 draft head.** The published Strix card measured a standalone
Q4_0 MTP head ~6–10% faster than Q8_0 for the same acceptance (the draft
head is a 248k-vocab output projection; cheaper dequant, same accuracy).
The bundled head inside UD-Q8_K_XL is Q8-class; the standalone
`ggml-org/mtp-Qwen3.8-27B-Q4_0.gguf` (1.56 GB) is being downloaded for
an A/B via `--spec-draft-model`.
- **Ngram-mod fallback stays.** It is the only spec type that survived
parallel load (DSpark crashed ROCm, DFlash corrupts multi-slot).
- **Ngram-mod fallback stays.** It is the only spec type that survived parallel
load; the DSpark ROCm fault and the DFlash multi-slot corruption are timed and
explained on [[qwen3.8-spec-matrix]].
### 3. Same-bit quant A/B: UD-Q8_K_XL vs plain Q8_0
@@ -162,10 +136,10 @@ zero quality cost.
Every agentic turn re-prefills the whole conversation (no prefix caching for
hybrid state, upstream). Levers:
- **Backend choice per workload.** Measured @64k on this box (b10433):
ROCm 410 t/s vs Vulkan 302 t/s prefill; Vulkan 25.1 vs ROCm 24.3 t/s
decode. For chat-heavy use: Vulkan. For agentic/batch-heavy use: ROCm.
The PP head (ROCm side) gets this for free.
- **Backend choice per workload.** Measured @64k on this box (b10433, the era
[[frank-dragoon]] owns): ROCm 410 t/s vs Vulkan 302 t/s prefill; Vulkan 25.1
vs ROCm 24.3 t/s decode. For chat-heavy use: Vulkan. For agentic/batch-heavy
use: ROCm. The PP head (ROCm side) gets this for free.
- **ubatch/threads-batch.** Production runs ubatch 256. Larger ubatches
(512/1024) fill the iGPU better during prefill; `--threads-batch` for
the CPU-bound tail. Untested at prefill scale on this box — matrix item.
@@ -175,14 +149,14 @@ hybrid state, upstream). Levers:
### 5. OS level (from the Improvements doc, re-verified 2026-08-15)
- **TTM `pages_limit` is now 114 GiB (29,883,536 pages)** — applied 2026-08-25.
Written live to `/sys/module/ttm/parameters/pages_limit` (no reboot required)
and persisted in `/etc/modprobe.d/ttm.conf`. The full 262k f16 KV cache plus
the ~28 GB Q8 AGENT payload, MTP draft, and mmproj now fit in one slot. The
load-stall risk from the old 15 GiB cap is resolved. Note: 96 GiB is the
driver-reported VRAM carve-out (VGM/SVM pool); the TTM pages_limit is the
kernel page map that was the real ceiling.
- `iommu=pt` — verified present on the kernel cmdline. Done.
- **TTM `pages_limit`** — the raise itself (value, live write, modprobe file,
date) is one row on [[Dankclaw System Specs]] (Memory). What matters to this
page is the mechanism: the driver-reported 96 GiB VRAM carve-out (VGM/SVM
pool) was never the ceiling — the TTM kernel page map was, and until it was
raised a 262k f16 KV cache plus a ~28 GB payload could not load in one slot.
- **Boot line** — IOMMU policy on this host is [[dankclaw-iommu-translation-off]]'s
and [[Dankclaw System Specs]]'s; it has changed twice since this lever was
written, so no era-scoped reading of `/proc/cmdline` belongs here.
- **Vulkan ICD pinning** — `radeon_icd.json` exists; no `VK_ICD_FILENAMES`
or loader-layer env is set. Pinning the radeon ICD and disabling loader
layers (`VK_LOADER_LAYERS_DISABLE=all`) is the recommended Strix
@@ -240,8 +214,8 @@ Per config: `pp-test.sh bench 4` (4k prompt / 256 out) and `bench 16`
(16k / 256). Report: decode t/s (server print_timing), prefill t/s, MTP
acceptance, worker VRAM, head GTT usage.
Success bar: **≥ 30 t/s at 4k and ≥ 20 t/s at 16k decode** (vs 25 t/s
production @16k) with the same 8-bit model.
Success bar: **≥ 30 t/s at 4k and ≥ 20 t/s at 16k decode**, against the
production @16k bar in the Baseline section, with the same 8-bit model.
## Sources
@@ -8,8 +8,9 @@ timestamp: 2026-10-05T00:00:00-07:00
# vLLM Boot Forensics and Config Interrogation
For serve-script vLLM endpoints (frank-archon/qwen-fp8 on dankserver, any
`podman rm -f` + restart service).
For serve-script vLLM endpoints (the vLLM eras of [[frank-archon]] — that page's
Eras table is which ones; any endpoint that runs `podman rm -f` + a restart
service).
## 1. Capture the true cause — the journal, not `podman logs`
+25 -20
View File
@@ -41,7 +41,7 @@ From the 111 GiB GGUF (`mradermacher/Qwen3.8-Flash-Next-Uncensored-i1-Q4_K_M`,
- 48 layers, of which **12 are full attention** (indices `[3,7,11,15,19,23,27,31,35,39,43,47]`,
every 4th — `full_attention_interval = 4`) and 36 are GDN/linear.
- Only the 12 attention layers carry KV. At the engine's measured 28 KiB per
position, a 200k-token conversation reads **5.5 GiB of KV per token**.
position, a 200k-token conversation holds **5.5 GiB of KV**.
- A layer split moves **whole layers**, so it moves ~1.62 GiB of experts with
every layer (75 GiB / 48). Attention layers are spread across the whole depth
@@ -82,10 +82,12 @@ Real placement in the split runs, sampled from `nvidia-smi` during the run:
split is genuine, not a mislabelled single-device run (see Finding 5 for why
that check is mandatory here).
Reference points on the same silicon: halogen (deployed) ≈ **25 tok/s** at real
contexts; the retired ik hybrid 3.8–4.6 tok/s; mainline with the trunk on the
3090 and CPU experts 1.01 tok/s (2026-09-15); the CPU expert path's prefill
ceiling 192 tok/s.
Reference points, all era readings rather than measurements of this run: the
deployed halogen shape at ≈ **25 tok/s** at real contexts and the retired ik
hybrid at 3.8–4.6 tok/s (both are [[frank-dragoon]] eras, and that page's Eras
rows are where those shapes live); mainline with the trunk on the 3090 and CPU
experts at 1.01 tok/s (2026-09-15, this page's own earlier pass); the CPU expert
path's prefill ceiling at 192 tok/s.
## Finding 1 — per-tensor placement across devices is killed by one flag: `-ot`
@@ -248,15 +250,16 @@ distinguishes a real split from a mislabelled sweep.
## Conclusion
The fleet's answer is unchanged — **two independent endpoints**
([[frank-dragoon]] on the iGPU via halogen,
[[frank-zealot]] holding a whole 27B in the 3090's 24 GB) — but two
claims from the first round of this experiment are now corrected:
([[frank-dragoon]] on the iGPU, [[frank-zealot]] alone on the 3090; each page
owns what its card holds) — but two claims from the first round of this
experiment are now corrected:
1. **Cross-vendor layer split is possible** (measured: CUDA0 + Vulkan1, 27.23
tok/s and 268.5 t/s prefill at 64k). It is not a regime change (+7 % decode),
and it costs the 3090 ~15 GiB — which this fleet spends on the vision endpoint
(`frank-zealot` holds 22.4 of the card's 24.6 GiB). An operator who wants the
7 % must give up the fleet's only image-capable endpoint.
and it costs the 3090 ~15 GiB — memory this fleet instead spends on the
vision endpoint, whose settled footprint is an Eras row on
[[frank-zealot]]. An operator who wants the 7 % must give up the fleet's only
image-capable endpoint.
2. **Per-tensor placement across two devices is impossible while `-ot` is used**,
for a scheduler-mechanical reason, regardless of driver or vendor mix.
@@ -316,11 +319,13 @@ Operational notes for the next run:
- Restarting frank-dragoon needs the quadlet source back in
`~/.config/containers/systemd/` plus `daemon-reload`; the generator re-creates
the unit from that file only. After the restore (and the host reboot, see the
landmines) the ledger read `400384 positions, ~27.2 GiB` against a 33.5 GiB
budget — no clamp — with 12.3 GiB left for everything else (14.7 GiB before
the reboot: the same model, a more fragmented host afterwards — the engine
reported 240 free contiguous 2 MiB blocks against 475 earlier). Both endpoints
answered `/health` and served `/v1/models` under their names.
landmines) its boot ledger read `400384 positions, ~27.2 GiB` against a 33.5
GiB budget — no clamp — with 12.3 GiB left for everything else (14.7 GiB
before the reboot: the same model, a more fragmented host afterwards — the
engine reported 240 free contiguous 2 MiB blocks against 475 earlier). That
ledger is an era reading of [[frank-dragoon]]; the block-census lesson it
teaches is on [[Llama Cpp Serving Sizing]]. Both endpoints answered `/health`
and served `/v1/models` under their names.
## Smaller-quant check: Q4_K_S against the recorded Q4_K_M numbers
@@ -393,10 +398,10 @@ expert mass) — 580 × 3 MiB slabs:
| *the same bytes computed in host RAM at 256 GB/s* | 7.1 ms | the link is **31× slower** than the memory behind it |
Against this host's measured alternatives — **20.46 tok/s** for llama.cpp with
the experts *computed* on the CPU (link carries activations only), 25 for
halogen, 26.5 for mainline on the iGPU — an engine that *streams* experts across
this link loses by 3–5×, and the port's x8 ceiling would only double it (≈7–9
tok/s).
the experts *computed* on the CPU (link carries activations only, config B′
above), 25 for the halogen shape deployed that day ([[frank-dragoon]]), 26.5 for
mainline on the iGPU — an engine that *streams* experts across this link loses
by 3–5×, and the port's x8 ceiling would only double it (≈7–9 tok/s).
For the specific engine that raised the question: FreeToken v0.1.2 keeps expert
weights in host RAM with an LRU GPU cache — exactly this streaming shape — and