docs: frank-large vLLM MiMo-BF16 runbook; retire the gufo runbook

New runbook carries the roof math, the measured three-arm spec table, the
greedy-text numerics trap, the reasoning-field contract change and the
rollback tag. LLM index and AGENTS.md fleet table, bench policy and runbook
index follow; the frank-small index entries catch up to the SGLang cutover
(entry was missed on 2026-10-01). Work log records the cutover.
This commit is contained in:
Frank committed 2026-10-01 18:48:51 -07:00
1 parent dab483c79f
commit 1127a52078
5 files changed
+330 -133

No files matched your search

@@ -0,0 +1,176 @@
---
type: reference
title: "frank-large on Dankclaw — vLLM TheRock, MiMo-V2.6-Distill-Qwen-9B BF16 + cross-model MTP"
description: "The active frank-large runbook — the digest-pinned kyuz0 TheRock gfx1151 vLLM image serving XiaomiMiMo MiMo-V2.6-Distill-Qwen-9B in full BF16 on the Strix Halo iGPU: identity, the measured roof math, the cross-model Qwen3.5-9B MTP drafter and its acceptance curve, the greedy-text numerics trap, the client-contract field change, deployed flags, and what to check when it misbehaves."
tags:
- llm
- frank-large
- dankclaw
- vllm
- strix-halo
- mimo
- mtp
- spec-decode
- bf16
timestamp: 2026-10-01
---
# frank-large — vLLM MiMo-V2.6-Distill-Qwen-9B BF16 (cross-model MTP)
> **ACTIVE since 2026-10-01.** Replaces [[frank-large-gufo-qwen38-27b]]
> (gufo + Qwen3.8-27B GGUF, now the rollback target, git tag
> `frank-large-gufo-q4kxl-20261001`). Same port (7770), same served alias
> (`frank-large`). This is the fleet's second vLLM endpoint and the first
> CROSS-MODEL speculative-decode deployment: the target is Xiaomi's distill,
> the draft head is Qwen's.
## Identity
- **Endpoint:** `frank-large` at `192.168.0.143:7770` (Dankclaw).
- **Runtime:** vLLM 0.30.0 in the **digest-pinned** `docker.io/kyuz0/vllm-therock-gfx1151`
image (image ID `2d24a8c5a3fd`, digest `sha256:98c1f3ac9833…` — ROCm 10.0.0 /
torch 2.11.0 / TheRock, Triton Gated DeltaNet kernels). The official vLLM
images are CUDA-only; this fork is the fleet's gfx1151 path. Plain
`podman run` driven by `scripts/serve-frank-large.sh` under a root unit
(`scripts/frank-large.service`, `User=dank`, `Restart=always`,
`ExecStop=podman stop -t 120`, `TimeoutStopSec=180` > the stop grace).
- **Model:** `XiaomiMiMo/MiMo-V2.6-Distill-Qwen-9B` in **full BF16**
(17.53 GiB, 4 shards) — the same checkpoint frank-small serves as an INT8
W8A16 AutoRound conversion on the 3090, here unquantized.
- **Draft:** `Qwen/Qwen3.5-9B` (18 GiB on disk; only the 15 `mtp.*` tensors
are loaded into the draft pass).
## Why this shape (the roof math)
Decode speed for a dense model on Strix Halo is bandwidth-bound:
BF16 weights x one pass per token against the LPDDR5x aperture. Measured
2026-10-01 on this host: **13.1 t/s is ~94% of the roof** — the payload, not
a misconfiguration. The same INT8 payload reaches 83.9 t/s on the 3090
because it moves ~12 GiB through ~936 GB/s. The only way past a bandwidth
wall is generating more than one token per weight pass: **speculative decode
is the performance feature of this endpoint**, not a garnish.
The memory side is the opposite of a squeeze: KV grows ONLY on the 8 full
attention layers (32 KiB/token bf16: 8 x 2 x 4 heads x 256 dim x 2 B);
the 24 Gated DeltaNet layers hold a FIXED ~25 MiB state per running
sequence. `--max-model-len 262144` (the native window) with
`--max-num-seqs 4` at `--gpu-memory-utilization 0.45` boots clean — the
four-session contract the gufo shape ran, with the pool shared by the
scheduler instead of carved per session.
## Spec method: cross-model MTP — measured, not assumed
| arm | prefill | decode | acceptance |
|---|---|---|---|
| BF16 base | 17,876 t/s | 13.1 t/s | — |
| BF16 + DFlash 8 tok | 7,976 t/s | 17.4 t/s | pos0 59% pos1 26% (token rate 14.8%) |
| **BF16 + MTP 2 tok** | 7,380 t/s | **19.8 t/s** | pos0 71% pos1 44% (token rate 57.4%) |
One session, temperature 0, 28,647-token prefill / 512-token decode, same
client for all three arms (2026-10-01 bench window, frank-large stopped per
Benchmarking Policy). **MTP wins (+51% vs +33%) and needs only 2 draft
tokens** — acceptance past position 1 is 44% and falling; DFlash's own curve
decays 59/26/14/7, so 8 proposals are mostly wasted verification.
`num_speculative_tokens` beyond 2 is UNMEASURED; do not raise it without a
fresh A/B. Both spec arms pay ~55–59% of prefill (the draft passes share
the compute queues) — this endpoint's thinking-heavy traffic spends its wall
clock in decode, which is the regime where that trade wins.
**Why a Qwen head accepts on a MiMo target:** MiMo-V2.6 is a distillation of
Qwen3.5-9B that preserves the hidden-state geometry — identical
4096-hidden/248320-vocab/32-layer/interval-4 structure, and the 71%/44%
per-position acceptance is the direct measurement that the distillation
preserved more than geometry. MiMo itself ships **ZERO `mtp.*` tensors**
(verified in both the BF16 base and the INT8 conversion) — which is exactly
why frank-small's SGLang NEXTN draft is a 0.00-acceptance ghost: SGLang
reads the head from the TARGET checkpoint, vLLM reads the draft from a
separate path. That difference is the whole trick:
```
--speculative-config '{"method":"mtp","model":"/models/Qwen3.5-9B","num_speculative_tokens":2}'
```
The alternative drafter (`z-lab/Qwen3.5-9B-DFlash`, 6 layers, target_layer_ids
[1,5,…,29]) stays fetched on disk; flip `FRANK_LARGE_SPEC=dflash` and repoint
`FRANK_LARGE_SPEC_DIR` to use it. Its card's "vLLM PR #40898 unmerged" claim
is FALSE — `vllm/v1/spec_decode/dflash.py` ships in 0.30.
## The numerics trap (greedy text is NOT preserved by spec decode)
Measured: temperature-0 completions DIFFER across arms (sha `1318aeb6` base,
`0208b45d` dflash, `20c41c38` mtp). Batched verification changes the bf16
reduction order, argmax near-ties diverge, and the divergence cascades.
vLLM spec decode is distribution-preserving, not bitwise-greedy-preserving.
The gufo-era "greedy == autoregressive text" law does not carry over; do not
chase text drift between spec and no-spec arms as a regression.
## Payload
- Target: 4 safetensors shards (17.53 GiB) + config/tokenizer under
`/home/dank/models/MiMo-V2.6-Distill-Qwen-9B-BF16/`, each shard
byte-count + LFS sha256-verified by `scripts/download-frank-large-mimo.sh`
(`--verify-only` re-checks; no token on the curl command line).
- MTP source: `scripts/download-frank-large-qwen35.sh` →
`/home/dank/models/Qwen3.5-9B/` (4 shards behind
`model.safetensors.index.json`; the shards carry the odd HF asset names
`model.safetensors-0000N-of-00004.safetensors` — the index resolves them,
the engine boots from exactly this tree).
- DFlash drafter: `scripts/download-frank-large-dflash.sh` →
`/home/dank/models/Qwen3.5-9B-DFlash/`.
- The serve script gates the image by REPO DIGEST (rootless podman resolves
tags from the network; the pin exists to boot exactly these bytes
offline), then config + index + exact shard count, then the spec draft
(either weight layout: single `model.safetensors` or a sharded index),
then `/dev/kfd` + whole `/dev/dri` (never individual renderD nodes —
Dankserver 2026-09-29 lesson).
## Client contract
- **Alias:** `frank-large` (the `id` at `/v1/models`).
- **Reasoning:** `--reasoning-parser qwen3`; vLLM emits the thinking text in
**`reasoning`**, NOT llama.cpp/gufo's `reasoning_content` — the harness
`models.yml` compat block for this provider sets
`reasoningContentField: "reasoning"`. Miss that and thinking silently
vanishes from transcripts.
- **Tools:** `--enable-auto-tool-choice --tool-call-parser qwen3_coder`.
- **Vision:** the checkpoint carries the `qwen3_5_vision` tower but the
serve script passes `--language-model-only` — the endpoint stays
text-only by decision; dropping the flag brings vision back.
- **Sampling:** request-side; the harness sends the Qwen3.5-family recipe
(temp 0.7, top_p 0.8) via `compat.extraBody`, and
`chat_template_kwargs.{enable_thinking,preserve_thinking}` — the
`chat_template_kwargs` form is what vLLM reads; a top-level
`preserve_thinking` is ignored there.
- **Env hygiene:** every tunable is `FRANK_LARGE_`-prefixed because HIP
reads bare `TEMP`/`HOST`/`GPU_MAX_HEAP_SIZE` and the serve script exports
the env file with `set -a`.
## Ops
- Boot ~200–240 s warm; health 503 while loading (do not kill it).
- The serve script sweeps stale container identities before starting:
`frank-large-mimo` itself and the bench-window container `vllm-mimo`.
- Container death → `podman wait` returns non-zero from ExecStart →
`Restart=always` self-heals (a clean exit-0 drain also restarts, the
frank-primary lesson).
- Bench windows: `systemctl stop frank-large` is enough (mask fails on the
relinked unit; the deploy restores).
- First-deploy fact to record here: the boot-time KV pool line
(`Maximum concurrency for 262,144 tokens per prompt: N.Nx`).
## Rollback
Git tag **`frank-large-gufo-q4kxl-20261001`** — gufo v0.1.1 (built from
source, `scripts/build-gufo.sh` stays on disk) + the Q4_K_XL Qwen3.8-27B
payload + DFlash2 drafter, all still on the host. Restoring the tag's
unit/env/serve trio and redeploying returns the 27B endpoint.
# Citations
- Model: https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Distill-Qwen-9B
- Draft source: https://huggingface.co/Qwen/Qwen3.5-9B
- DFlash drafter: https://huggingface.co/z-lab/Qwen3.5-9B-DFlash
- Engine image: https://hub.docker.com/r/kyuz0/vllm-therock-gfx1151
- Serve script: `scripts/serve-frank-large.sh`; tunables: `scripts/frank-large.env`
- Predecessor: [[frank-large-gufo-qwen38-27b]]; sibling on the same arch:
[[frank-small-sglang-mimo-v26]]