Files
serverAppConfigs/serverAppConfigs-docs/LLM/frank-medium-llamacpp-dflash2.md
T
Frank 718a283afc docs: the frank-medium runbook catches up with DSpark, 2 slots and the rebalance
The note still described DFlash2, one slot at 262144 and a 1:1 split -- all of
which the day's work replaced. It now carries:

- the DSpark swap with the table that matters (1.27 vs 1.92 GiB, block_size 7 vs
  8 so n_max 6 vs 7, a Markov head instead of the candidate selector, and NO
  sliding window) and the rule behind the sizing: the draft context is built
  with n_ctx = llama_n_ctx(ctx_tgt), so a drafter without a window caches the
  whole target context -- 5.0 GiB here, against DFlash2's ~40 MiB.
- the failure that rule produces when ignored: load succeeds, /health answers,
  and the first draft decode dies with an OOM at 1:1.
- why the split moved a second time: tripling the batch costs ROCm1 about twice
  what it costs ROCm0, with the 38,27 -> 41,24 -> 40,25 measurement table.
- the deployed flags, the 2-slot shape (n_slots = 2, n_ctx_slot = 131072), the
  DSpark init lines, and balanced 29.98 / 29.98 GiB cards.
- honest scoping on the older measurements: the throughput, drafter-worth and
  ngram tables were taken on DFlash2, and the agent-loop numbers on one slot.
- the 39:26 page-fault signature as a thing to watch, marked as NOT an OOM.
2026-09-16 20:21:31 -07:00

449 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: configuration
title: frank-medium llama.cpp Runbook (DSpark drafter)
description: The ACTIVE frank-medium deployment since 2026-09-16 — mainline llama.cpp on Dankserver serving ajgazin/Swift-Qwen3.8-27B-Uncensored-Dynamic-MTP at UD-Q8_K_XL with the magnitudedev Qwen3.8-27B-DSpark Q8_0 drafter, as ONE layer-split process across both Radeon AI PRO R9700 cards, 262144 ctx over two slots.
tags:
- frank-medium
- dankserver
- llama-cpp
- dspark
- qwen35
- gfx1201
- r9700
- layer-split
timestamp: 2026-09-16
---
# frank-medium — llama.cpp (DSpark drafter)
The ACTIVE frank-medium deployment since 2026-09-16: **mainline llama.cpp**
serving `ajgazin/Swift-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF` at `UD-Q8_K_XL`
with the **magnitudedev DSpark** block-diffusion drafter (it replaced z-lab
DFlash2 the same day — see the swap section below), as **one process across
both Radeon AI PRO R9700 cards**, layer-split.
It replaced the vllm-radiance MXFP4 replica pair + routing proxy
([[frank-medium-radiance-mxfp4]], the record and the PCIe-atomics evidence) in
the same day the pair went live. It is also the box's **only** LLM runtime: the
replicas, the proxy and their unit files are retired.
## Why one process and layer split
The endpoint's whole history is one constraint: **the two cards cannot take
part in an RCCL collective.** RCCL's kernels declare a hidden hostcall buffer,
and ROCr refuses to supply it to a device without PCIe atomics; the second
R9700 hangs off the B650 chipset (`AtomicOpsCap: Routing-`, read-only), so
every collective that touches it dies with `hipErrorIllegalState`. That rules
out tensor parallelism — and it rules out **vLLM's pipeline parallelism too**,
whose stage handoff is a `send`/`recv` of the hidden state on the device
backend (`vllm/distributed/parallel_state.py`), the very primitive that fails.
The full matrix is in [[Dual-R9700-Cooperation]].
llama.cpp's `--split-mode layer` needs **no collective at all**: layers are
distributed across the cards in order and the boundary activation is a plain
per-backend copy, once per forward pass. Measured on this host with the
predecessor runtime (llama-bench, pp4096 / tg128, f16 KV, flash-attn on):
| Split | pp4096 | tg128 |
|---|---:|---:|
| one R9700 (`-sm none`) | 1160.9 | 19.42 |
| **two cards, layer (`-sm layer`)** | **1991.2** | **18.76** |
| two cards, tensor (`-sm tensor`, no RCCL: butterfly fallback) | 271.8 | 26.26 |
Tensor split buys ~40 % decode and loses 7× prefill. The workload here is
prefill-heavy (agentic, long prompts, reasoning traces), so layer split is the
shape — prefill nearly doubles and one model serves from both cards.
## Hardware
| Property | Value |
|---|---|
| GPUs | 2x AMD Radeon AI PRO R9700 (DID 0x7551), gfx1201 (Navi 48, RDNA4) |
| VRAM | 34,208,743,424 B (31.86 GiB) each, as `rocm-smi` reports it |
| Link | PCIe, no XGMI; card #2 behind the chipset, **no atomic routing** |
| iGPU | Raphael (gfx1036) enumerates as HIP device 2 — hidden via `HIP_VISIBLE_DEVICES=0,1` |
| CPU / RAM | Ryzen 7 7700X (8c/16t), 60 GiB RAM, 63 GiB swap |
## Runtime
Mainline llama.cpp at **`fb27a525`** (2026-09-16), built for **gfx1201 only** by
`scripts/build-llama-rdna4.sh` against a private ROCm `7.14.0a20260624`
toolchain (TheRock nightly) that is **bundled next to the binary** —
`RPATH=$ORIGIN/../lib`, no `LD_LIBRARY_PATH`, nothing from the host's ROCm 7.1
packages.
Two features pin that ref, and neither is in the ROCmFPX fork the endpoint used
earlier the same day (its tip, `c49ebdb`, predates both):
- **DFlash2** (`ggml-org/llama.cpp#27342`): grouped dynamic depthwise
convolution plus the candidate selector, detected from the draft GGUF
(`selector_top_k > 0`) and served through the ordinary
`--spec-type draft-dflash` path.
- **qwen35 MTP**: the `draft-mtp` implementation covers this architecture's
single trained head.
Build with:
```bash
bash scripts/build-llama-rdna4.sh # ~25 min on this box, JOBS=$(nproc)
```
> **Bundling trap, measured.** rocBLAS and hipBLASLt look for their device code
> at a fixed depth under the binary's lib dir (`../lib/rocblas/library`,
> `../lib/hipblaslt/library/<arch>`). `cp -r SRC DST/` with a missing `DST`
> creates `DST` as a copy of `SRC` and **flattens it one level**, and the
> failure is invisible at startup: the engine loads 29 GiB of weights, answers
> `/health`, and dies on the **first request** with
> `rocBLAS error: Cannot read .../lib/rocblas/library/TensileLibrary.dat: No
> such file or directory for GPU arch : gfx1201`. The build script now declares
> the destination and copies contents.
## Model and drafter
| File | Bytes | Role |
|---|---:|---|
| `Swift-Qwen3.8-27B-Uncensored-Dynamic-MTP-UD-Q8_K_XL.gguf` | 31,457,990,112 | target, 29.29 GiB |
| `Qwen3.8-27B-DSpark-Q8_0.gguf` | 1,359,284,737 | DSpark drafter, 1.27 GiB |
Fetched by `scripts/download-frank-medium-gguf.sh` (resumable, byte-verified —
a truncated 31 GB GGUF is otherwise a load-time crash).
The target is Unsloth Dynamic 3.0 (per-tensor types from Unsloth's own quant of
the same size, on Unsloth's importance matrix) of an abliterated Swift
fine-tune: orcarouter's single refusal direction projected out of 131 tensors,
vision tower untouched. **Text only here** (`--no-mmproj`); the fleet's image
endpoint is frank-small.
**The file also carries the model's MTP head** (`blk.64.nextn.*`). Under
`--spec-type draft-dspark` those tensors are deliberately **not** loaded — the
log says `model has unused tensor blk.64.nextn.eh_proj.weight ... ignoring` —
because DSpark drafts from its own 5-layer sidecar. Switching (or chaining)
`--spec-type draft-mtp,draft-dspark` is a one-line env change that pulls the
head in; it is not enabled because the DSpark path is the measured one and
because a chain cannot hold two context-backed drafters at all (below).
## Deployment
| File | Role |
|---|---|
| `scripts/frank-medium.env` | every tunable, with the sizing arithmetic in comments |
| `scripts/serve-frank-medium.sh` | env-driven llama-server launcher; no arguments |
| `scripts/frank-medium.service` | ROOT systemd unit, `User=dank`, `WantedBy=multi-user.target` |
```bash
./scripts/deploy.sh dankserver --service frank-medium
```
The handler verifies the runtime and payload exist (and refuses otherwise),
retires the three vLLM quadlets into `~/.AGENTTRASH/20260916-vllm-radiance-retire/`,
waits for **port 7770 to be released** (the retired proxy held it on the host
network), relinks and restarts the unit, and gates on
`http://127.0.0.1:7770/health`.
Deployed flags:
| Flag | Value | Why |
|---|---|---|
| `--split-mode` / `--device` / `--tensor-split` | `layer` / `ROCm0,ROCm1` / `40,25` | both cards, one model, no collective; the split is skewed because ROCm1 carries the drafter (see the swap section) |
| `--gpu-layers` / `--fit` | `999` / `off` | everything on the cards; auto-fit must not resize a context we set |
| `--ctx-size` / `--parallel` | `262144` / `2` | the model's whole window over two slots of 131072; `--parallel` **divides** the context |
| `--batch-size` / `--ubatch-size` | `6144` / `1536` | 3x the defaults; `--ubatch-size` is what the compute buffers cost, and it is what forced the rebalance |
| `--ctx-checkpoints` / `--cache-ram` | `0` / `8192` | no per-slot KV snapshots (the build's default); the RAM-side prompt cache is capped at 8 GiB |
| `--flash-attn` / cache types | `on` / `f16` K and V | an agentic checkpoint is the wrong place to spend KV precision |
| `--spec-type` / `--spec-draft-n-max` | `draft-dspark,ngram-mod` / `6` | DSpark blocks are 7 tokens, so 6 drafts is the ceiling; ngram-mod rides the chain as a draftless second proposer |
| `--spec-draft-device` | `ROCm1` | the drafter rides the card that ends up holding the target's last layers |
| `--temp` / `--top-p` / `--top-k` | `0.7` / `0.95` / `20` | the values the vLLM predecessor served on this endpoint |
| `--reasoning-format` / `--reasoning-budget` | `deepseek` / `-1` | trace into `reasoning_content`, unlimited |
| `--no-mmproj` | — | text only |
## The DSpark swap (2026-09-16, hours after the cutover)
The endpoint went live on **z-lab DFlash2** and was switched to
**magnitudedev/Qwen3.8-27B-DSpark** the same day. Both are 5-layer
block-diffusion sidecars, but they differ in ways that decide the deployment:
| | DFlash2 (before) | DSpark (deployed) |
|---|---|---|
| file | 2,056,414,816 B — 1.92 GiB | 1,359,284,737 B — 1.27 GiB |
| draft depth | `block_size 8` -> `n_max 7` | `block_size 7` -> `n_max 6` (llama.cpp asserts `n_max <= block_size - 1`) |
| attention | `sliding_window 2048`, `causal false`, candidate selector (rank 256, top-k 16) | **no sliding window**; a Markov head in place of the selector |
| draft KV at 262144 | ~40 MiB — the window bounds it | **5.0 GiB** — the whole context |
**The draft context is sized from the target's.** `cparams.n_ctx =
llama_n_ctx(ctx_tgt)` in `common/speculative.cpp`, with no flag to cap it
independently, so a drafter that declares no sliding window caches the entire
target window on whichever device `--spec-draft-device` names. DSpark's KV is
5 layers x 2 x 8 kv heads x 128 dim x 2 B = 20 KiB per position.
That cost has to be paid somewhere, and the failure was instructive: at a 1:1
split the model **loaded fine, answered `/health`, and then died on the first
draft decode** with `ROCm error: out of memory` inside
`common_speculative_impl_draft_dflash::process` — ROCm1 sat at 33.9 GB of
34.2 GB. A unit that crash-loops with an allocation failure *after* a
successful load is a sizing failure, not a broken drafter.
Read `sliding_window` out of a drafter's GGUF header before sizing a
spec-decode deployment — 40 lines of `struct` parsing is enough, and the
flag-level configuration looks identical either way.
### The split then had to move again for the batch
Tripling the batch (2048/512 -> 6144/1536) made the two cards diverge: at
262144 the bigger `--ubatch-size` cost **ROCm1 ~3.3 GiB against ROCm0's
~1.7 GiB** — ROCm1 holds the output layers *and* the drafter's graphs — which
left the draft-carrying card at **0.08 GiB free** against ROCm0's 3.78 under
the 38:27 split that had been balanced at the smaller batch. One `-ts` step is
worth ~1.5 GiB at this size, so the balance was found by measurement, not
arithmetic:
| `-ts` | GPU 0 free | GPU 1 free |
|---|---:|---:|
| 38,27 | 3.78 GiB | 0.08 GiB |
| 41,24 | 1.47 GiB | 2.38 GiB |
| **40,25 — deployed** | **1.88 GiB** | **1.88 GiB** |
The balance is also what **maximises the context**: an unbalanced split has to
buy its headroom out of `CTX_SIZE`, and `CTX_SIZE` is already at the model's
trained window. The operator's floor for this configuration is 1.5 GiB per
card, and `scripts/frank-medium.env`'s sizing table carries the measurements
that 8192/2048 does *not* fit here at all (its 2020 MiB pp buffer fails to
allocate on ROCm1) while 4096/1024 fits but leaves 1.11 GiB.
> **A fault to watch.** A 39:26 split, at 1x batch and one slot, died on its
> eighth request: the amdgpu driver logged SDMA0 UTCL2 page faults and the
> engine surfaced `ROCm error: an illegal memory access was encountered` ->
> `ggml_abort` — a fault in the cross-card boundary copy, which is this layer
> split's hot path. It has not reproduced (38:27 served the whole e2e gate and
> a burst of requests; 40:25 has served the verification battery and a deploy
> gate), and it is **not** an OOM: no amount of headroom would prevent it.
## Verified serving behaviour (2026-09-16)
- Load: `model loaded` ~25 s after start with a warm page cache, then
`listening on http://0.0.0.0:7770`. The drafter adds ~2.5 s.
- Slot shape: `n_slots = 2, n_ctx_slot = 131072`; `/props` reports
`modalities: {vision: false}` and the sampler defaults above.
- DSpark engaged — the init lines are the proof, not the flags:
```
common_speculative_init_result: loading draft model '.../Qwen3.8-27B-DSpark-Q8_0.gguf'
common_speculative_impl_draft_dflash: adding speculative implementation 'draft-dspark'
common_speculative_impl_draft_dflash: - n_max=6, n_min=0, p_min=0.00
common_speculative_impl_draft_dflash: - block_size=7, mask_token_id=248077, n_extract=5, sample_from_anchor=true
```
`block_size=7` and `mask_token_id=248077` are DSpark's own metadata. The
implementation still logs under the `draft_dflash` symbol because dspark *is*
that code path plus the Markov head — the loader prefers a dspark sidecar
when one is present.
- Both cards hold the model and they are **balanced**: at 262144 with the
deployed batch, `rocm-smi` reports **29.98 GiB used on each card** of
31.86 GiB — 1.88 GiB free on each, measured with two concurrent 1832-token
prompts in flight. The 38:27 split that preceded the batch change was
lopsided (3.78 / 0.08 GiB free); see the swap section above.
- **140,069 prompt tokens prefilled in 134 s** (1043 tok/s) was measured
through a *single* slot, at the previous configuration: the raised window is
real rather than declared, and it is 22 % beyond the 114688 the vLLM
predecessor could offer. The window is now split over two slots.
- **Nothing is replicated.** The loader does not print a per-device ledger at
this verbosity, so this is measured instead of read: raising CTX_SIZE from
131072 to 262144 added **4.83 GB to GPU 0 and 4.84 GB to GPU 1** — both cards
carry the KV for their own attention layers, about half each — and neither
card's *total* usage (24.94 / 29.42 GB at 262144) could hold the 31.46 GB
file, so no card carries a full copy of the weights. Compare the shape this
replaced: two vLLM replicas, i.e. two complete copies of the model, one per
card, which is exactly why a 29.8 GB FP8 checkpoint could not be served there
at any usable context.
### Measured throughput (2026-09-16, deployed service, `temperature 0`)
Same filler corpus for every shape, so these are shapes rather than a
benchmark: a large prompt with a tiny budget isolates prefill, a small prompt
with a long budget isolates decode. `draft`/`acc` are per request; acceptance
moves with the text (filler prose accepts far worse than code or a real agent
loop).
| Shape | Prefill | Decode | Acceptance |
|---|---|---|---|
| 4k prompt / 32 out | 3994 tok in 3.25 s — 1230 tok/s | 59.8 tok/s | 71 % |
| 1k prompt / 512 out | 998 tok in 1.41 s — 707 tok/s | **43.8 tok/s** | 41 % |
| 16k prompt / 16 out | 12,528 tok in 8.94 s — **1402 tok/s** | — | 100 % (13/13) |
| 64k prompt / 16 out | 48,508 tok in 39.8 s — 1219 tok/s | — | 53 % |
| 140k prompt / 16 out | 140,069 tok in 134.3 s — 1043 tok/s | — | — |
| same 1k prompt again | **4 tokens prefilled** | 43.8 tok/s | 41 % |
The last row is the prefix cache doing its job: the second request of the same
conversation re-prefilled 4 of 998 tokens. Prefill holds ~1.0-1.4k tok/s from
4k to 140k, and decode sits at ~44 tok/s on a long generation — against the
vLLM predecessor's 42.8 tok/s single-stream (two replicas) and the ROCmFPX
llama.cpp measured here at 35.7-46.2 tok/s. One process, one model, both cards,
no proxy.
### What the drafter is worth — and why MTP is not the alternative
Measured 2026-09-16 with `scripts/bench-spec-ab.py` on the **DFlash2** drafter:
four configurations of the same binary, identical prompts at `temperature 0`,
unit stopped between runs (one host, one copy of a 29 GiB model). 1k-token
prompt, 512-token budget. These are the numbers that selected a trained
multi-token drafter over the model's own MTP head; DSpark replaced DFlash2 for
the reasons in the swap section, and it is that conclusion which carried over.
| Configuration | Decode | Drafting | VRAM (card 1) |
|---|---:|---|---:|
| `none` — autoregressive | 17.7 tok/s | — | 26.07 GB |
| `draft-mtp` n_max 3 (the model's own head) | 32.4 tok/s | 58 % of 72 | 29.35 GB |
| `draft-mtp` n_max 7 | 26.7 tok/s | 33 % of 147 | 29.65 GB |
| **`draft-dflash` n_max 7 — deployed** | **43.5 tok/s** | 41 % of 119 | 29.42 GB |
- **DFlash2 is worth 2.46× over autoregressive and 34 % over the best MTP.**
Deeper MTP drafting costs more than it returns: at depth 7 only a third of
the proposals survive, at depth 3 more than half do.
- **All four produced identical greedy text** (`2, 3, 5, 7, 11`): speculation
buys speed, never a different answer. The harness warns loudly if a future
configuration ever breaks that.
- **MTP is not the cheaper option.** Selecting it loads `blk.64.nextn.*` from
the target and adds ~3.3 GB to the card that holds it, against 2.06 GB for
the drafter's file — which is also why the load log says
`unused tensor blk.64.nextn... ignoring` under `draft-dflash`: correct
behaviour, not a truncated download.
- **They cannot be chained.** `--spec-type draft-mtp,draft-dflash` dies at load
with `context type MTP requested but model doesn't contain MTP layers`. A
chain has ONE draft context, and MTP builds against whatever model that
context holds — here the DFlash2 file. MTP's head is loaded only when MTP *is*
the draft model (`creating MTP draft context against the target model ...`).
That is structural, not a tuning question.
### Chaining the draftless n-gram matcher (deployed)
`ngram-mod` **can** ride beside a context-backed drafter, and it is enabled.
The reason is in `common/speculative.cpp`: the n-gram types are added to the config vector
unconditionally while every model-backed drafter is gated on
`ctx_dft != nullptr` — so n-gram needs no draft context and cannot collide with
the drafter over one. (MTP is the opposite: it *is* context-backed, which is why
`draft-mtp,draft-dflash` cannot load.) The vector's fixed order — ngram types
first, then the drafters — also means CLI order is irrelevant.
The measurements below are DFlash2's; the chain is now `draft-dspark,ngram-mod`.
The matcher's behaviour — zero drafts on prose, 100 % on echo — is a property
of the matcher, not of the drafter beside it, but re-run
`scripts/bench-spec-ab.py` if the DSpark numbers themselves matter.
Measured with `scripts/bench-spec-ab.py`, decode tok/s, three shapes that a
proposer can and cannot match:
| Configuration | prose 1k/512 | echo 1.5k/400 (copy 0.94) | structured/400 | identity |
|---|---:|---:|---:|---:|
| `draft-dflash@7` | 43.77 | 76.82 | 91.08 | 70.37 |
| `ngram-mod` alone | 17.72 *(0 drafts)* | 102.50 | 17.80 *(0 drafts)* | 17.77 |
| **`draft-dflash,ngram-mod@7`** | **43.65** | **200.58** | 91.22 | 70.40 |
- **On prompt-echo traffic the chain is 2.6× the drafter alone** (200.6 vs 76.8)
and twice `ngram-mod` by itself, at 100 % acceptance.
- **On prose it is indistinguishable from DFlash2 alone** — the same 119 drafts
and 49 accepts, the same 43.7 tok/s. That is the property that makes it safe
as a default: where the matcher cannot match, it costs nothing.
- `ngram-mod` alone is a *negative control*: zero drafts on prose and identity
(17.7 tok/s = the autoregressive baseline), 100 % acceptance on echo. Nothing
else could have proposed those tokens, which is how its drafts are identified
at all — see the trap below.
- VRAM is unchanged (29.42 GB on card 1 with or without it): the matcher is a
64 MiB host-RAM hash table.
- `structured` (25 records with unique ids) rewards no proposer: its lines
differ, so there is no long span to match. It is a negative control for
*self-repetition*, not evidence about it.
**The window matters, and shorter is not better.** The chain at three windows:
| `SPEC_NGRAM_N_MIN`/`N_MAX` | prose 1k/512 | echo 1.5k/400 |
|---|---:|---:|
| **48/64 — deployed** | **43.9** | **200.6** |
| 16/32 | 36.0 | 161.0 |
| 8/16 | 36.2 | 113.8 |
A short window makes the matcher propose on ordinary prose fragments, where the
proposals are rejected and the verification is still paid — the "ngram hurts"
result this fleet measured twice before on other payloads. At 48/64 the chain
keeps DFlash2's exact numbers on prose and wins on echo.
> **Operational trap: `ngram-mod` prints no startup banner in this build.**
> `journalctl -u frank-medium` shows `adding speculative implementation
> 'draft-dflash'` and nothing for the matcher — the only evidence it is running
> is behavioural (`draft_n` > 0 where text repeats) or the per-task line
> `slot print_timing: ... draft acceptance = 1.00000 (256 accepted / 256
> generated), mean len = 65.00`. Do not conclude from the load log that the
> chain is not engaged.
### The agent-loop shape (fleet harness, 2 families × 2 turns)
`scripts/bench-turn-latency.py http://127.0.0.1:7770 2 2 LAYERSPLIT` — two
agent loops re-sending a ~30k-token document concurrently:
| | turn 1 | turn 2 |
|---|---:|---:|
| family 1 | 24,799 ms | 30,697 ms |
| family 0 | 51,424 ms | 8,015 ms |
Read that against the prefill table, not as a latency regression: with **one**
slot the second family's first turn waited for the first (25 s of cold 30k
prefill + the wait ≈ 51 s), and the last number — 8.0 s — was a re-prefill the
cache could only partly serve. That was the 1-slot configuration. The
deployment now runs **two slots of 131072**, so a second agent loop has a slot
of its own instead of queueing, at the cost of halving the window each one
gets. `--parallel` splits the context and the KV between slots; it does not add
throughput, so the trade is concurrency against conversation length.
## Observability
The endpoint exports Prometheus metrics under the **`llamacpp:`** namespace
(`--metrics`), which is what the fleet's scrape and dashboard read — the
`vllm:` series the predecessor exported no longer exist:
| Need | Series |
|---|---|
| Throughput | `llamacpp:prompt_tokens_seconds`, `llamacpp:predicted_tokens_seconds` |
| Queue | `llamacpp:requests_processing`, `llamacpp:requests_deferred` |
| Cache reuse | `llamacpp:prompt_tokens_cached_total` vs `llamacpp:prompt_tokens_total` |
| **Speculative decoding is live** | `llamacpp:spec_decode_num_drafts_total` and `llamacpp:spec_decode_num_accepted_tokens_total` (their ratio is the acceptance rate) |
`spec_decode_num_drafts_total` staying at 0 is the one-line answer to "is the
drafter actually running": the flags can all be right while the draft model
failed to load.
## Client contract
OpenAI-compatible `/v1` on **7770**, alias **`frank-medium`**, streaming and
`/health`, `/props`, `/metrics` as llama.cpp provides them. Reasoning traces
are projected into `reasoning_content`. A request that sends no sampler fields
gets the server defaults above; a client that sends its own wins. Text only.
## Troubleshooting
- **Loads, then dies on the first request with a rocBLAS Tensile error** — the
bundled device-library depth (see the bundling trap above); check
`build-rdna4-rocm714/lib/rocblas/library/` exists and is populated.
- **`hipErrorNoDevice` / a card missing** — `HIP_VISIBLE_DEVICES` must be
`0,1`; the iGPU enumerates as device 2 and must stay hidden.
- **Drafting not engaged** — read the journal for
`common_speculative_impl_draft_dflash` (DFlash2 and DSpark both log under that
symbol); a missing drafter file makes the serve script exit before
llama-server starts, and a drafter whose GGUF the build does not recognise
fails the load instead.
- **Why is `blk.64.*` "unused"?** That is the model's own MTP head and it is
correct for `draft-dspark`. It is not a truncated download:
`bash scripts/download-frank-medium-gguf.sh --verify-only` proves the bytes.
- **A rebuild segfaults at load** — the build is gfx1201-only on purpose; a
gfx1200 sibling binary links, loads the model and then segfaults on these
cards.
## See Also
- [[frank-medium-radiance-mxfp4]] — the retired vllm-radiance MXFP4 deployment
(replicas + proxy) and the PCIe-atomics evidence
- [[frank-medium-rocmfpx]] — the llama.cpp/ROCmFPX deployment earlier on
2026-09-15, whose `--split-mode layer` measurements this service reuses
- [[Dual-R9700-Cooperation]] — why the two cards cannot take part in a
collective, and every software escape that is closed
- [[frank-small-qwen38-gsq-rco]] — the fleet's other llama.cpp endpoint (RTX
3090, MTP, the image path)
- [[Dankserver System Specs]] — hardware inventory