Files
serverAppConfigs/serverAppConfigs-docs/LLM/frank-medium-llamacpp-dflash2.md
T
Frank f917a2ef55 docs: the runbook records the DSpark excursion as an excursion
The note described DSpark as the deployed drafter and 40:25 as the split; both
were true for a few hours. It now leads with DFlash2 at 36:29 and keeps the
excursion as the record it is:

- the drafter comparison table with DSpark's REAL size (1,455,376,576, not the
  1,359,284,737 that went into the fetch script), and why it lost: no sliding
  window, so its draft KV spanned the whole context.
- the split's two moves as one story: the batch pushed it to 40:25, the drafter
  swap pulled it back to 36:29, with both measurement tables.
- the layer-size law (0.43 GiB DeltaNet, 1.43 GiB attention) as the reason the
  balance is measured and not interpolated.
- the verified-behaviour section now carries the LIVE numbers: 4.71 / 4.91 GiB
  free (4.43 / 4.61 under load, 0.18 GiB gap), the DFlash2 init lines, and
  draft acceptance from both slots (0.63 / 0.48 on slot 1, 0.39 on slot 0).
- the fault note scopes its own evidence: 39:26 was loaded, not exercised.
2026-09-16 21:01:45 -07:00

26 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
configuration frank-medium llama.cpp Runbook (DFlash2 drafter) 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 z-lab Qwen3.8-27B-DFlash2 Q8_0 drafter, as ONE layer-split process across both Radeon AI PRO R9700 cards at a 36:29 split, 262144 ctx over two slots.
frank-medium
dankserver
llama-cpp
dflash2
dspark
qwen35
gfx1201
r9700
layer-split
2026-09-16

frank-medium — llama.cpp (DFlash2 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 z-lab DFlash2 block-diffusion drafter, as one process across both Radeon AI PRO R9700 cards, layer-split. DSpark was tried in the drafter's place for a few hours the same day and reverted — see The DSpark excursion. 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 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-DFlash2-Q8_0.gguf 2,056,414,816 DFlash2 drafter, 1.92 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-dflash those tensors are deliberately not loaded — the log says model has unused tensor blk.64.nextn.eh_proj.weight ... ignoring — because DFlash2 drafts from its own 5-layer sidecar. Switching (or chaining) --spec-type draft-mtp,draft-dflash is a one-line env change that pulls the head in; it is not enabled because the DFlash2 path is the measured one (below) and because a chain cannot hold two context-backed drafters at all.

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
./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 / 36,29 both cards, one model, no collective; the split is skewed because ROCm1 carries the drafter, and it is one LAYER of granularity — 0.43 GiB on a DeltaNet boundary, 1.43 GiB on an attention one
--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-dflash,ngram-mod / 7 DFlash2 blocks are 8 tokens, so 7 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 excursion (2026-09-16, tried and reverted)

The endpoint went live on z-lab DFlash2, was switched to magnitudedev/Qwen3.8-27B-DSpark for a few hours the same day, and was put back. Both are 5-layer block-diffusion sidecars, and their differences are what decided it:

DFlash2 (deployed) DSpark (tried, reverted)
file 2,056,414,816 B — 1.92 GiB 1,455,376,576 B — 1.36 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

A fetch-script bug this threw up. The DSpark file's size was recorded as 1,359,284,737 when it was fetched — 96 MB short of the Hub's 1,455,376,576 — and that wrong number became the fetch script's declared size, so --verify-only failed on a payload that was in fact byte-correct. Both drafters' sha256 have since been checked against the Hub's LFS oids and match.

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 moved twice: once for the batch, once for the drafter

Under DSpark. 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:

-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 1.88 GiB 1.88 GiB

Then DSpark came off, which frees ~4.3 GiB on ROCm1 (5.0 GiB of draft KV out, 0.65 GiB of heavier DFlash2 weights in) and moves the balance the other way. Re-measured at the same batch, the same two slots and the same window:

-ts GPU 0 free GPU 1 free gap
39,26 2.42 GiB 7.20 GiB 4.78
38,27 3.85 GiB 5.77 GiB 1.92
37,28 4.28 GiB 5.34 GiB 1.06
36,29 — deployed 4.71 GiB 4.91 GiB 0.20
35,30 5.14 GiB 4.48 GiB 0.66
34,31 6.57 GiB 3.05 GiB 3.52

One -ts step is one layer, and this stack's layers are not the same size. qwen35 is 16 x (3 Gated DeltaNet + 1 Gated Attention), so a step is worth ~0.43 GiB on a DeltaNet boundary and ~1.43 GiB on a full-attention one (0.45 GiB of weights plus ~1.0 GiB of its own f16 KV at 262144). Both fell out of that table — 37,28 -> 38,27 moves a DeltaNet layer, 38,27 -> 39,26 an attention layer. Balance by measurement, never by interpolating.

36:29 is both the minimum and the point that leaves the most room: it clears the operator's 1.5 GiB-per-card floor by 3.2 GiB, so no headroom has to be bought out of CTX_SIZE and the model's full 262144-token window stays.

A batch of 8192/2048 failed here under DSpark — its 2020 MiB pp buffer could not be allocated on ROCm1, which had 0.08 GiB free. That was a contiguity failure, not a total-bytes one, and it has not been re-measured with DFlash2's ~4.9 GiB of headroom. (tests/frank_medium_unit.bats fits the deployed point and predicts it still would not fit, but that is an extrapolation of a coefficient fitted at ubatch 1536.)

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 (36:29 has since served the whole verification battery and the e2e gate; 39:26 itself was only loaded at the deployed batch, not exercised), 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.

  • DFlash2 engaged — the init lines are the proof, not the flags:

    common_speculative_init_result: loading draft model '/home/dank/models/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q8_0.gguf'
    common_speculative_impl_draft_dflash: adding speculative implementation 'draft-dflash'
    common_speculative_impl_draft_dflash: - n_max=7, n_min=0, p_min=0.00
    common_speculative_impl_draft_dflash: - block_size=8, mask_token_id=248070, n_extract=5, sample_from_anchor=true
    

    block_size=8 and n_extract=5 are DFlash2's own metadata; the candidate selector is what distinguishes it from DFlash 1. DSpark logs under the same draft_dflash symbol with block_size=7 and a Markov head, so read the numbers, not the implementation name.

  • Both cards hold the model and they are balanced: at 262144 with the deployed batch and split, rocm-smi reports 4.71 GiB free on GPU 0 and 4.91 GiB on GPU 1 of 31.86 GiB each — the same figures the balance search predicted to 0.01 GiB. Under load (two concurrent requests, one per slot) they sit at 4.43 / 4.61, a 0.18 GiB gap, and both are far above the operator's 1.5 GiB floor.

  • Drafting is live on both slots, which is what a 2-slot deployment has to prove: slot print_timing reported draft acceptance = 0.62857 (44 accepted / 70 generated), mean len = 5.40 and 0.48214 (27 / 56) on slot 1, and 0.38776 (57 / 147), mean len = 3.71 on slot 0, from one concurrent pair. Acceptance moves with the text; the point is that neither slot is idle.

  • 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 — the one deployed now: 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.

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 deployed configuration. (During the DSpark excursion the chain was draft-dspark,ngram-mod; the matcher's behaviour is a property of the matcher, not of the drafter beside it.)

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-dflash. 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