Skip to content

dflash(qwen4): DFlash2 on Qwen3.8-Flash-Next (incoai/Qwen3.8-Flash-Next-DFlash2) - #935

Open
davidtai wants to merge 4 commits into
ddalcu:mainfrom
davidtai:qwen4-dflash2
Open

davidtai wants to merge 4 commits into
ddalcu:mainfrom
davidtai:qwen4-dflash2

Conversation

@davidtai

@davidtai davidtai commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

incoai published a DFlash2 drafter for Qwen3.8-Flash-Next (incoai/Qwen3.8-Flash-Next-DFlash2: 5 layers, block 8, selector rank 256 / top-16, two-tap grouped dynamic convolutions, mask token 248077, target layers 5/15/25/35/45). The host already parses, loads and runs DFlash2 drafters (GLM-5.3-Flash-DFlash2, Qwen3.8-27B-DFlash2), and Flash-Next passes every bind check (hidden 2560, vocab 248320, 48 layers, capture-capable). Three things stood in the way:

  1. The capture tap. forwardQwen4With handed a drafter the raw [B, L, hc*hidden] hyper-connection stream, four times the drafter's encoder width. DFlash2 is trained on the per-token mean of the four streams (its SGLang capture patch: unflatten(-1, (hc, hidden)).mean(-2)), the same tap forwardGlm5With already gives its drafter.
  2. The round policy (dflash_policy.zig) was gated to GLM-5.3. Flash-Next is the same kind of sparse target: every extra verify row routes to more distinct experts.
  3. The scheduler's spec wiring (specInitWiring) forced DFlash off for every module-class arch, and Flash-Next is one (its n-gram table is module-shared state). With --drafter the sidecar loaded, every request logged drafter=enabled, and the slot decoded plain at serial speed (65-69 tok/s on the 1K and 16K cells) with no [spec-stats] line. The rule already arms the arch's own MTP head when it can rewind its state across a verify (moduleStateSpecRollback); the DFlash block verify rolls back through the same per-position capture (rollbackSsmFromCapture), and DflashModel.bind refuses the archs with no capture seam. A bound sidecar now outranks the head there too, as it does on a shell-owned arch; the gemma drafter and PLD stay off. Four wiring tests added.

What

  • CaptureLayers.hc_mean: a DFlash consumer asks for the hyper-connection mean ([B, L, hidden], via Transformer.hcStreamMean, unit-tested on a CPU stream); the qwen4 parity oracles keep the raw [B, L, hc*hidden] stream in the slot. Every DFlash capture site in generate.zig sets the flag. A new real-pack guard (QWEN4_TEST_MODEL) checks both shapes from one layer, prefill width and the one-token path, and that the mean slot equals the mean over the raw slot's streams.
  • dflashPolicyFor drives Flash-Next too (dflashPolicyWanted, unit-tested); MLX_SERVE_DFLASH_POLICY=0 keeps the fixed block.
  • specInitWiring: a module-class arch with state rollback runs a bound DFlash sidecar (tests cover both sidecar states, the opt-out, and the no-rollback case).

Measurements

128 GB M5 Max, the Q4 mixed-bit Flash-Next pack (Youssofal/Qwen3.8-Flash-Next-MTPLX-Optimized-Speed, converted to the mlx-serve layout), production contract: --ctx-size 262144 --max-concurrent 1 --temp 1.0 --top-p 0.95 --top-k 20, thinking on, MLX_SERVE_NOCACHE_LOAD=1 MLX_SERVE_NGRAM_WARM=0. Prompts are the standard code-ctx seeds (1K = 1,052 tokens, 16K = 16,296), max_tokens 512, one fresh server per configuration, rows in the order 1K card, 1K greedy, 16K card, 16K greedy (so the greedy rows prefill from the card row's prefix cache; decode is unaffected). Card = T 1.0 / top-p 0.95 / top-k 20 / seed 20260829; greedy = T 0. Rates and acceptance are the server's own <- N+512 tokens and [spec-stats] lines. MTP control: the pack's own head, --mtp-max-depth 3 --mtp-typical 0.2. DFlash rows: --drafter <DFlash2> --no-mtp --draft-block-size N, fixed = MLX_SERVE_DFLASH_POLICY=0, adaptive = the round policy (default on Flash-Next with this PR).

1. Block-size sweep (one pass)

config cell decode tok/s prefill tok/s accepted/round per-draft % rounds gate
mtp 1K card 114.9 1815.7 2.38 82.8 151 on
mtp 1K greedy 107.4 303.1 (cached) 2.02 67.3 167 on
mtp 16K card 116.2 2678.8 2.59 88.9 142 on
mtp 16K greedy 104.8 288.4 (cached) 2.12 71.6 164 on
b3-fixed 1K card 57.8 896.0 1.67 83.3 9 disabled
b3-fixed 1K greedy 70.5 300.3 (cached) 1.63 81.3 8 disabled
b3-fixed 16K card 65.6 2217.7 1.50 75.0 8 disabled
b3-fixed 16K greedy 68.1 273.5 (cached) 1.63 81.3 8 disabled
b4-fixed 1K card 104.0 1804.6 2.44 81.2 149 on
b4-fixed 1K greedy 105.1 302.4 (cached) 2.19 73.1 160 on
b4-fixed 16K card 95.8 2549.7 2.19 73.1 160 on
b4-fixed 16K greedy 101.7 287.2 (cached) 2.30 76.8 155 on
b5-fixed 1K card 74.9 1807.6 2.26 56.5 157 on
b5-fixed 1K greedy 101.5 303.8 (cached) 2.57 64.3 143 on
b5-fixed 16K card 88.2 2678.1 2.68 66.9 139 on
b5-fixed 16K greedy 109.3 291.8 (cached) 2.88 72.0 132 on
b6-fixed 1K card 73.6 1816.0 2.61 52.1 142 on
b6-fixed 1K greedy 94.9 302.7 (cached) 2.87 57.4 132 on
b6-fixed 16K card 77.6 2677.0 2.61 52.1 142 on
b6-fixed 16K greedy 99.5 289.3 (cached) 3.06 61.3 126 on
b7-fixed 1K card 86.3 890.8 3.45 57.5 115 on
b7-fixed 1K greedy 100.0 303.8 (cached) 3.30 55.0 119 on
b7-fixed 16K card 91.5 2730.1 3.45 57.5 115 on
b7-fixed 16K greedy 99.7 287.9 (cached) 3.34 55.6 118 on
b8-fixed 1K card 92.9 1814.7 3.97 56.7 103 on
b8-fixed 1K greedy 102.4 302.7 (cached) 3.92 56.0 104 on
b8-fixed 16K card 81.4 2671.0 3.45 49.3 115 on
b8-fixed 16K greedy 109.7 291.2 (cached) 4.28 61.1 97 on
b8-adaptive 1K card 110.3 1817.1 3.56 50.9 107 on
b8-adaptive 1K greedy 127.6 300.6 (cached) 3.81 54.4 104 on
b8-adaptive 16K card 106.3 2775.5 3.30 47.1 111 on
b8-adaptive 16K greedy 113.1 289.2 (cached) 3.31 47.3 113 on

Reading it:

  • Block 3 is disabled by the MoE economics gate (dflashGateMinimum: the sparse-target floor is 1.80 accepted/round, and two drafts/round accept 1.5-1.67). That is the gate working as designed, not a bug; block 3 falls back to plain decode at 58-70 tok/s.
  • Per-draft acceptance falls with width (81% at block 4, 56% at block 8) while accepted/round rises (2.2-2.4 → 3.5-4.3). On this MoE target the wider verify costs more experts than the extra accepted drafts return, so the fixed blocks are flat-to-worse past 4: block 4 is the best fixed width (96-105 tok/s), block 8 fixed is 81-110.
  • The adaptive policy at block 8 is the best DFlash configuration (106-128 tok/s) and the only one that beats the MTP head on the greedy rows.

2. Verification on the 1K / 16K track (two passes each, same window)

config cell decode tok/s prefill tok/s accepted/round per-draft % rounds gate
mtp 1K card 110.4 1806.4 2.38 82.8 151 on
mtp 1K greedy 88.6 230.5 (cached) 2.02 67.3 167 on
mtp 16K card 91.4 2121.8 1.83 82.5 181 on
mtp 16K greedy 90.2 229.3 (cached) 2.08 75.7 166 on
mtp-r2 1K card 116.9 1820.2 2.38 82.8 151 on
mtp-r2 1K greedy 92.3 247.0 (cached) 2.02 67.3 167 on
mtp-r2 16K card 94.8 2183.1 2.12 81.0 163 on
mtp-r2 16K greedy 90.5 237.2 (cached) 1.94 72.8 174 on
b4-fixed 1K card 107.8 1801.5 2.44 81.2 149 on
b4-fixed 1K greedy 104.2 302.4 (cached) 2.19 73.1 160 on
b4-fixed 16K card 95.3 2454.0 2.19 73.1 160 on
b4-fixed 16K greedy 99.9 286.3 (cached) 2.30 76.8 155 on
b4-fixed-r2 1K card 98.5 1056.2 2.44 81.2 149 on
b4-fixed-r2 1K greedy 93.2 265.3 (cached) 2.19 73.1 160 on
b4-fixed-r2 16K card 87.4 2176.3 2.19 73.1 160 on
b4-fixed-r2 16K greedy 92.4 245.1 (cached) 2.30 76.8 155 on
b8-fixed 1K card 89.8 1806.6 3.97 56.7 103 on
b8-fixed 1K greedy 82.2 285.3 (cached) 3.92 56.0 104 on
b8-fixed 16K card 66.4 2100.7 3.45 49.3 115 on
b8-fixed 16K greedy 89.1 260.5 (cached) 4.28 61.1 97 on
b8-fixed-r2 1K card 91.1 1820.7 3.97 56.7 103 on
b8-fixed-r2 1K greedy 91.9 257.3 (cached) 3.92 56.0 104 on
b8-fixed-r2 16K card 78.9 2185.7 3.45 49.3 115 on
b8-fixed-r2 16K greedy 95.3 243.0 (cached) 4.28 61.1 97 on
b8-adaptive 1K card 119.8 1410.5 3.74 53.4 104 on
b8-adaptive 1K greedy 126.4 297.7 (cached) 3.81 54.4 104 on
b8-adaptive 16K card 109.7 2237.2 3.52 50.3 108 on
b8-adaptive 16K greedy 113.6 283.2 (cached) 3.31 47.3 113 on
b8-adaptive-r2 1K card 108.3 1813.3 3.74 53.4 104 on
b8-adaptive-r2 1K greedy 111.3 256.0 (cached) 3.83 54.8 103 on
b8-adaptive-r2 16K card 88.3 2160.5 3.20 45.7 105 on
b8-adaptive-r2 16K greedy 97.2 234.7 (cached) 3.38 48.3 108 on
config 1K card 1K greedy 16K card 16K greedy mean
mtp 110/117 89/92 91/95 90/90 96.9
b4-fixed 108/98 104/93 95/87 100/92 97.3
b8-fixed 90/91 82/92 66/79 89/95 85.6
b8-adaptive 120/108 126/111 110/88 114/97 109.3

Pass 2 of every configuration ran slower than pass 1 (the box warms over a 6-minute window), so compare within a pass. The adaptive policy wins 7 of 8 cells against MTP and all 8 against fixed block 8; its mean over both passes is 109 tok/s against 97 for MTP and 97 for fixed block 4. Pick: DFlash2 adaptive, block 8 (the drafter's trained block). Fixed block 4 ties MTP; fixed block 8 loses to it.

Greedy outputs are not byte-identical across configurations: on the 1K cell MTP, fixed 4 and fixed 5 produce one text (a thinking preamble), fixed 6/7 diverge at character 820, and fixed 8 / adaptive open straight into code. The verify width changes the bf16 reduction order, and the opening token of this prompt is a near-tie; the 16K greedy rows agree through the first 86-341 characters the same way. Same class of drift as the GLM-5.3 DFlash rows.

3. Context ladder, 1K..128K, three tracks

One window, one fresh server per track (ctx 262,144 for every rung), the standard code-ctx prompts 1K..128K (the model's window is 262,144 tokens, so 256K and up do not fit with headroom), card then greedy at max_tokens 512 per rung. MTP = the pack's head at typical 0.2; fixed = DFlash2 block 4 with MLX_SERVE_DFLASH_POLICY=0; adaptive = DFlash2 block 8 under the round policy. Logs: reports/qwen-ladder-20261021-1154/<track>/server.log.

Decode tok/s, card / greedy, with accepted drafts per round in brackets:

prompt MTP typical 0.2 DFlash2 fixed block 4 DFlash2 adaptive block 8
1K 118 / 107 [2.38 / 2.02] 108 / 104 [2.44 / 2.19] 119 / 128 [3.74 / 3.81]
2K 119 / 104 [2.61 / 1.96] 99 / 100 [2.25 / 2.15] 87 / 115 [2.27 / 3.34]
4K 120 / 107 [2.63 / 2.26] 67 / 69 [1.78 / 1.63] (gate off) 114 / 112 [3.88 / 3.22]
8K 108 / 105 [2.32 / 2.16] 101 / 103 [2.32 / 2.26] 122 / 114 [4.16 / 3.27]
16K 103 / 104 [1.94 / 2.12] 96 / 104 [2.19 / 2.30] 108 / 113 [3.39 / 3.31]
32K 109 / 101 [2.58 / 2.12] 106 / 96 [2.58 / 2.08] 83 / 106 [2.35 / 3.24]
64K 104 / 103 [2.39 / 2.22] 100 / 100 [2.35 / 2.20] 117 / 112 [3.99 / 3.29]
128K 99 / 93 [2.47 / 2.19] 65 / 96 [1.67 / 2.10] (gate off) 116 / 114 [4.05 / 3.57]

Card row (cold prefix): prefill tok/s and wall seconds for prompt + 512 tokens:

prompt MTP prefill MTP wall fixed-4 prefill fixed-4 wall adaptive prefill adaptive wall
1K 1,405 5.2 1,814 5.4 1,158 5.3
2K 1,363 5.8 2,107 6.2 2,253 6.8
4K 1,884 6.5 2,385 9.4 2,595 6.1
8K 2,075 8.7 2,840 8.0 2,771 7.1
16K 2,046 13.0 2,814 11.1 2,637 11.0
32K 2,041 16.6 2,653 14.0 2,602 15.6
64K 2,026 25.0 2,531 21.2 2,590 20.1
128K 2,104 39.8 2,451 37.6 2,460 34.1

Over the 16 rows per track:

mtp: mean decode 106.4 tok/s (min 93, max 120), mean per-draft acceptance 77%, gate off in 0/16 rows
dflash-fixed: mean decode 94.5 tok/s (min 65, max 108), mean per-draft acceptance 72%, gate off in 3/16 rows
dflash-adaptive: mean decode 111.1 tok/s (min 83, max 128), mean per-draft acceptance 49%, gate off in 0/16 rows

Reading it:

  • The adaptive policy holds its lead across the whole ladder: 106-128 tok/s on 13 of 16 rows and the best cell at 128K (116 / 114 against MTP's 99 / 93). Its per-draft acceptance is the lowest of the three (49%) because the policy spends the full 7-draft width whenever it pays; accepted/round stays at 3.2-4.2 wherever it is winning.
  • Its two weak rows are sampled (card) rows at 2K and 32K (87 and 83 tok/s, accepted/round 2.3): those two generations are thinking preambles where the drafter accepts less, and the policy narrows. The greedy rows at the same rungs are 115 and 106.
  • Fixed block 4 is the loser on a long ladder. It ties MTP where its acceptance holds (2.2-2.6/round) but the MoE economics gate (floor 1.80 accepted/round) switched it off on three rows (4K card and greedy, 128K card: 1.63-1.78/round), after which those rows decoded plain at 65-69 tok/s. The adaptive policy never tripped the gate.
  • Both DFlash tracks prefill 20-35% faster than the MTP track (2,450-2,840 tok/s against 2,030-2,100 from 8K up): with --no-mtp the prompt pass does not also run the MTP head. At 128K that is 34.1 s against 39.8 s wall for prompt + 512 tokens.
  • Prompt-length drift in decode is small for every track (1K → 128K: MTP 118 → 99, adaptive 119 → 116).

Reading: on Flash-Next the DFlash2 drafter under the round policy (--drafter <DFlash2> --no-mtp --draft-block-size 8, policy on by default with this PR) is a second decode lane beside the MTP head, not a replacement: +4% mean decode over the ladder, +17% at 128K, +20-35% prefill, no gate trips, but the sampled rows at 2K and 32K and the 256K card row still go to MTP (see section 5). The MTP lane stays the default; --drafter opts a server in. The fixed block is not worth shipping on this target without lowering the MoE floor, which this PR does not touch.

4. DFlash2 with typical acceptance at 0.2 (--dflash-typical 0.2, side branch qwen4-dflash2-typical, NOT in this PR)

Measured for the dataset only. The knob is a lossy acceptance rule, so it stays on a side branch (commit 21ae60f there) until it has its own PR; the rows below were produced by that build, not by the three commits here.

The DFlash verify had no typical mode: on this row-exact trunk a sampled request drew every verify row with the keyed sampler and kept a draft only while it matched, so sections 1-3 compare MTP under --mtp-typical 0.2 against DFlash2 under an exact rule. --dflash-typical <d> gives the DFlash verify the MTP lane's floor (keep a draft while its filtered target probability clears min(1, d * exp(-H)), a miss resamples the target row). Same window shape as section 2, two passes, binary with this commit. Greedy rows are unaffected by construction and reproduce section 2's counts exactly.

config cell decode tok/s prefill tok/s accepted/round per-draft % rounds accept rule gate
mtp 1K card 100.5 1810.8 2.38 82.8 151 mtp typical 0.2 on
mtp 1K greedy 87.0 232.2 (cached) 2.02 67.3 167 mtp typical 0.2 on
mtp 16K card 96.7 2077.7 2.38 87.6 151 mtp typical 0.2 on
mtp 16K greedy 87.4 217.3 (cached) 2.03 74.6 169 mtp typical 0.2 on
mtp-r2 1K card 109.6 1804.0 2.38 82.8 151 mtp typical 0.2 on
mtp-r2 1K greedy 96.6 257.3 (cached) 2.02 67.3 167 mtp typical 0.2 on
mtp-r2 16K card 101.8 2284.2 2.30 85.4 155 mtp typical 0.2 on
mtp-r2 16K greedy 95.5 245.3 (cached) 1.90 76.0 176 mtp typical 0.2 on
b4-fixed-typ0.2 1K card 105.6 1817.3 2.41 80.4 150 typical:0.20 on
b4-fixed-typ0.2 1K greedy 104.0 304.6 (cached) 2.19 73.1 160 typical:0.20 on
b4-fixed-typ0.2 16K card 102.8 2620.4 2.46 82.0 148 typical:0.20 on
b4-fixed-typ0.2 16K greedy 102.6 287.3 (cached) 2.30 76.8 155 typical:0.20 on
b4-fixed-typ0.2-r2 1K card 95.8 1815.8 2.41 80.4 150 typical:0.20 on
b4-fixed-typ0.2-r2 1K greedy 92.8 256.1 (cached) 2.19 73.1 160 typical:0.20 on
b4-fixed-typ0.2-r2 16K card 98.3 2289.0 2.46 82.0 148 typical:0.20 on
b4-fixed-typ0.2-r2 16K greedy 98.4 268.9 (cached) 2.30 76.8 155 typical:0.20 on
b8-fixed-typ0.2 1K card 104.5 1809.5 4.57 65.2 92 typical:0.20 on
b8-fixed-typ0.2 1K greedy 101.8 302.7 (cached) 3.92 56.0 104 typical:0.20 on
b8-fixed-typ0.2 16K card 88.2 2634.9 3.92 56.0 104 typical:0.20 on
b8-fixed-typ0.2 16K greedy 107.7 291.1 (cached) 4.28 61.1 97 typical:0.20 on
b8-fixed-typ0.2-r2 1K card 108.1 1115.7 4.57 65.2 92 typical:0.20 on
b8-fixed-typ0.2-r2 1K greedy 100.3 291.3 (cached) 3.92 56.0 104 typical:0.20 on
b8-fixed-typ0.2-r2 16K card 93.9 2491.4 3.92 56.0 104 typical:0.20 on
b8-fixed-typ0.2-r2 16K greedy 103.5 271.4 (cached) 4.28 61.1 97 typical:0.20 on
b8-adaptive-typ0.2 1K card 95.3 243.5 4.08 58.3 90 typical:0.20 on
b8-adaptive-typ0.2 1K greedy 111.9 300.3 (cached) 3.83 54.8 103 typical:0.20 on
b8-adaptive-typ0.2 16K card 120.1 714.1 4.34 62.1 93 typical:0.20 on
b8-adaptive-typ0.2 16K greedy 110.7 291.1 (cached) 3.31 47.3 113 typical:0.20 on
b8-adaptive-typ0.2-r2 1K card 102.3 1814.7 3.90 55.7 98 typical:0.20 on
b8-adaptive-typ0.2-r2 1K greedy 109.6 245.3 (cached) 3.83 54.8 103 typical:0.20 on
b8-adaptive-typ0.2-r2 16K card 107.9 2112.5 4.34 62.1 93 typical:0.20 on
b8-adaptive-typ0.2-r2 16K greedy 95.6 240.6 (cached) 3.29 47.0 110 typical:0.20 on
config 1K card 1K greedy 16K card 16K greedy mean
mtp 100/110 87/97 97/102 87/96 96.9
b4-fixed-typ0.2 106/96 104/93 103/98 103/98 100.0
b8-fixed-typ0.2 104/108 102/100 88/94 108/104 101.0
b8-adaptive-typ0.2 95/102 112/110 120/108 111/96 106.7

Reading it:

  • Typical lifts sampled-row acceptance at block 8 (card rows, accepted/round, exact → typical): fixed 8 at 1K 3.97 → 4.57 and at 16K 3.45 → 3.92; adaptive at 1K 3.74 → 3.90-4.08 and at 16K 3.52 → 4.34. Block 4 barely moves (2.44 → 2.41, 2.19 → 2.46): three drafts per round were already accepting 73-81%.
  • Decode does not move outside the window's noise. This window ran slower across the board (MTP 97 tok/s mean against 111 in section 2's window), and within it the adaptive policy keeps its lead (107 against 97, +10%) by about the margin it had under the exact rule (+12% in section 2). The extra accepted drafts per round buy little on this target because a wider accepted span costs more experts in the verify, the same economics that flattened the fixed-block sweep.
  • Typical rows are not reproducible across servers (pass 1 and pass 2 of a card cell produce different texts), where the keyed serial rule reproduced them byte for byte. Expected: the typical path leaves the keyed sampler for the stochastic arm.
  • The first two card rows of the first server in this window (adaptive, 1K and 16K) show a low prefill rate (244 and 714 tok/s) because the n-gram table was evicted from the page cache right before the window and the first long prompt faults it in from SSD. Their decode figures are unaffected; pass 2 shows the normal rate.

Takeaway: on Flash-Next the exact rule costs DFlash2 nothing measurable, so the recommendation stays the adaptive policy under exact acceptance. --dflash-typical is there for a matched comparison against --mtp-typical and for targets where the verify is cheap enough for acceptance to be the bound.

5. Standard coding cell, the YaRN ladder to 256K, and where the round's time goes

Standard cell (the GLM program's coding workload: a 1,024-token task on a CPython module, 16,387 rendered tokens, max_tokens 1024, T 1.0 / top-p 0.95, seeds 1234 and 4321, one fresh server per run, lane order balanced across the seeds; rates from the server's own line, peak RSS sampled at 2 s):

lane seed prompt out prefill tok/s TTFT s decode tok/s accepted/round per-draft % gate wall s peak RSS GB text sha
MTP typical 0.2 1234 17743 1024 2,115 8.41 108.3 2.35 79.9 on 17.9 76.1 3c70514d9cf1
MTP typical 0.2 4321 17743 1024 2,276 7.81 97.6 2.46 83.8 on 18.3 81.1 82e975ab19f6
DFlash2 fixed 4 1234 17743 1024 2,768 6.43 93.5 2.11 70.4 on 17.4 80.0 7c9f8130fc87
DFlash2 fixed 4 4321 17743 1024 2,103 8.45 80.0 2.03 67.7 on 21.3 80.0 7eab4bce2eb6
DFlash2 adaptive 8 1234 17743 1024 2,768 6.43 95.3 2.84 40.6 on 17.2 80.1 9c96a9bb1d2e
DFlash2 adaptive 8 4321 17743 1024 2,750 6.47 106.2 3.53 50.4 on 16.2 80.1 a13f22c87f4e
DFlash2 adaptive 8 typical 0.2 1234 17743 1024 2,756 6.45 98.1 3.76 53.7 on 17.0 80.1 c6e6ec3b5a92
DFlash2 adaptive 8 typical 0.2 4321 17743 1024 2,738 6.50 101.6 3.17 45.3 on 16.6 80.1 2ba7f908f690
lane decode mean (2 seeds) prefill mean TTFT mean
MTP typical 0.2 102.9 2,196 8.11
DFlash2 fixed 4 86.8 2,435 7.44
DFlash2 adaptive 8 100.8 2,759 6.45
DFlash2 adaptive 8 typical 0.2 99.8 2,747 6.48

On this cell DFlash2 adaptive ties the MTP head on decode (100.8 vs 102.9) and wins time-to-first-token (6.5 vs 8.1 s: the prompt pass skips the head). Acceptance is lower than on the code-ctx cells because the output is a long reasoning preamble (2.8-3.5 accepted/round under the policy, 40-50% per draft).

Ladder to 256K. The 256K prompt is 262,144 tokens, the whole shipped window, so this ladder runs under Qwen's extension recipe (--config-overrides YaRN factor 4, 262,144 -> 1,048,576; the server advertises 270,336 as its session window) with the hot prefix cache capped at 1 GB so the 256K rung fits beside the 83 GB model under a 112 GiB guard. Decode tok/s, card / greedy, accepted drafts per round in brackets:

prompt MTP typical 0.2 DFlash2 fixed 4 DFlash2 adaptive 8 DFlash2 adaptive 8 typical 0.2
1K 117 / 121 [2.40 / 2.41] 69 / 71 [1.67 / 1.75] (gate off) 116 / 124 [3.56 / 3.64] 122 / 121 [4.14 / 3.64]
2K 111 / 108 [2.39 / 2.21] 67 / 68 [1.67 / 1.63] (gate off) 117 / 117 [3.82 / 3.49] 120 / 116 [4.42 / 3.49]
4K 116 / 108 [2.54 / 2.29] 86 / 106 [1.82 / 2.35] 106 / 101 [3.55 / 2.73] 102 / 102 [3.32 / 2.71]
8K 119 / 108 [2.67 / 2.36] 97 / 108 [2.23 / 2.41] 119 / 116 [3.98 / 3.69] 115 / 116 [4.05 / 3.69]
16K 108 / 111 [2.35 / 2.32] 90 / 68 [2.01 / 1.63] (gate off) 106 / 103 [3.20 / 2.77] 123 / 106 [4.32 / 2.77]
32K 108 / 113 [2.36 / 2.37] 84 / 103 [1.82 / 2.28] 114 / 114 [3.48 / 3.46] 116 / 114 [4.15 / 3.46]
64K 104 / 106 [2.07 / 2.30] 88 / 102 [1.97 / 2.30] 88 / 108 [2.31 / 3.44] 121 / 110 [4.54 / 3.44]
128K 108 / 99 [2.72 / 2.28] 94 / 101 [2.20 / 2.30] 109 / 111 [3.74 / 3.47] 118 / 114 [4.99 / 3.47]
256K 91 / 91 [2.46 / 2.31] 86 / — [1.96 / —] 101 / — [3.07 / —] 116 / — [4.12 / —]

Reading it:

  • Under YaRN the MTP head is unchanged (1K-128K rows match the shipped-RoPE ladder in section 3) and reaches 256K at 91 / 91 tok/s (prefill 2,187 tok/s, 117 s for prompt + 512).
  • The DFlash2 drafter was trained against the shipped RoPE: under YaRN its acceptance drops at short context (fixed block 4 trips the MoE gate at 1K, 2K and 16K at 1.6-1.8 accepted/round) while the adaptive policy keeps 3.2-4.0 and its lead from 8K up.
  • --dflash-typical 0.2 shows up in tok/s here where it did not in section 4: the card rows accept 4.1-5.0 per round and run 5-15% faster than exact adaptive at 16K-128K.
  • DFlash2 adaptive at 256K: 100.7 tok/s on the card row (3.07 accepted/round, prefill 2,076 tok/s, 130 s wall). The greedy row, a second cold 256K prefill on the same server, exceeds the box's memory guard with the drafter's context on board, so DFlash lanes have the card row only at 256K. Card row at 256K, decode tok/s: MTP 91, fixed 4 86, adaptive 101, adaptive typical 0.2 116 (prefill 2,076-2,117 tok/s, 127-130 s for prompt + 512).

Where a round's time goes (M5 Max, 16K KV, MLX_SERVE_DECODE_FWD_UBENCH: the trunk's pure forward, no host work):

rows ms / forward ops bytes read (est.)
1 15.4 4,001 ~6.3 GB
4 27.3 3,546
8 42.3 5,000 ~15.7 GB
16 68.8 5,923

The pack is 77 GB of experts (3.07 MB per expert at 4-bit with group-32 scales and biases) and about 4.8 GB of dense weights per forward. One row routes to 10 experts per layer; 8 rows route to about 74 distinct experts per layer, so the verify reads 2.5x the bytes of a serial step for 8x the positions, at roughly 68% of a 546 GB/s bandwidth floor. A traced DFlash round on the coding cell (MLX_SERVE_DFLASH_TRACE=1, which adds its own evals) is 42-52 ms of verify plus about 6 ms of drafter forward, draft head, acceptance and context append, for 3.2 to 5.6 accepted tokens; the adaptive policy's confident stretches reach ~145 tok/s and its narrow rounds pull the mean to 95-117. The draft-tree verify never engages on this arch (the GDN layers hold no per-branch state), so tokens per round are bounded by the single 8-token path. The n-gram table page-warm (29.8 GB) does not fit beside the model under the guard on a 128 GB box.

Existing switches on the coding cell (one fresh server per run; seed 1234, and 4321 where two rows show). None moves the ceiling: a deeper MTP round accepts more but costs more (round 30 -> 43 ms at depth 4-5), pinning the policy at 8 rows loses a quarter, and the NAX verify lane's width threshold is already right.

config seed decode tok/s prefill accepted/round per-draft % depth round ms gate
mtp-d3 1234 108.4 2,116 2.35 79.9 3 30.42 on
mtp-d4 1234 98.2 2,777 2.78 75.7 4 30.23 on
mtp-d4 4321 99.9 2,740 3.05 78.7 4 42.65 on
mtp-d5 1234 104.5 2,629 2.97 77.4 5 31.67 on
mtp-d5 4321 101.6 2,737 3.30 79.5 5 43.78 on
mtp-d5-min3 1234 97.2 2,429 3.04 76.9 5 33.53 on
mtp-d6 1234 90.9 2,398 1.96 77.3 6 28.75 on
dflash-adaptive 1234 89.9 2,190 2.91 41.5 on
dflash-rows8 1234 72.4 2,305 3.56 50.9 on
dflash-rows8 4321 72.4 2,325 3.63 51.8 on
dflash-nax0 1234 92.4 2,334 2.99 42.7 on
dflash-naxm4 1234 86.9 2,343 2.90 41.4 on

Reaching 140 tok/s with exact acceptance therefore needs a cheaper 8-row verify (fewer expert bytes per round: a row-aware MoE verify that computes only the rows routed to each expert, and the group-32 scale/bias overhead) plus overlapping the drafter forward with the verify. That is follow-up kernel work, outside this PR.

@davidtai
davidtai marked this pull request as ready for review October 21, 2026 17:04

ddalcu commented Oct 21, 2026

Copy link
Copy Markdown
Owner

🤖 MLXServe-PRBot — code review

  1. The capture change breaks Flash-Next's parity oracle. ctx.capture_layers on forwardQwen4With is not a DFlash-only tap: the three qwen4 fixture tests read the raw [B, L, hc*H] residual stream through it and compare it to the HF reference's stream_i. src/transformer.zig:26506 now writes the hc-mean [B, L, H] into that slot. So:

    • qwen4 fixture bf16 skips every layer on the length mismatch (:67512) and then fails checked >= n_layers - 2 (:67518).
    • qwen4 fixture (:68532) and qwen4 fixture vision (:67296) print "size vs ref" and stop checking the streams, silently.

    They're env-gated, so CI didn't see it. Don't change what the shared tap means. Reduce at the DFlash consumer, or put the reduction on CaptureLayers so the drafter asks for the mean and the oracle keeps the raw stream. Then add the guard GLM has (glm5_next DFlash capture): a qwen4 test that the slot DFlash binds is [B, L, hidden] and equals the stream mean. hcStreamMean's unit test only proves the reshape works, not that DFlash gets the mean.

  2. --dflash-typical is a speculative lossy knob, and your own section 4 says so: "the exact rule costs DFlash2 nothing measurable, so the recommendation stays the adaptive policy under exact acceptance." AGENTS.md: no speculative knobs, and a lossy lever needs a measured second arm worth comparing. It's also a second copy of the typical rule: typicalFloor (src/generate.zig:13603) is, by its own comment, the GPU twin of mtp_acceptance.typicalThreshold and the .typical arm of mtpBatchedLossyGraph. It adds a host eval per verify row on top of probAt's. And it's inserted between probAt and its doc comment (:13597), so probAt loses its doc and typicalFloor starts with the wrong line. Drop 21ae60f. If a target ever shows that typical acceptance pays, reuse the MTP typical graph instead of hand-rolling a per-row CPU loop.

  3. The win is inside the noise you measured. MTP 1K greedy is 107.4 tok/s in section 1 and 88.6 / 92.3 in section 2. Pass-to-pass swings are 10-20%, and the context ladder is one pass per track. "+4% mean decode over the ladder" doesn't establish "replaces the MTP head". Nothing here changes a default (DFlash only arms with a bound drafter), so this doesn't block. But either cut that recommendation, or back it with interleaved same-session reps (/bench: spec cells are variance, same-session ratios only).

  4. Every commit carries a Co-Authored-By: trailer, and the body ends with a "Generated with Claude Code" line. AGENTS.md Conventions forbids both. Also remove the stray shell output pasted into section 2 (/Users/davidtai/.../qwen-dflash2-sweep-20261021-1147, ready).

The specInitWiring change and the policy gate look right, and both are tested. No conflict with main.


Generated by Claude Code

ddalcu commented Oct 21, 2026

Copy link
Copy Markdown
Owner

🤖 MLXServe-PRBot — code review

Delta since 21ae60f: bb87e0a brings one specInitWiring test case in line with the new wiring (a bound sidecar runs on a module-class arch with no head) and adds the no-head, no-sidecar case. The test is correct. It also carries a Co-Authored-By: trailer.

None of the earlier review is addressed:

  1. src/transformer.zig:26506 still writes the hc-mean into the shared capture_layers slot. That breaks the env-gated qwen4 fixture, qwen4 fixture bf16 and qwen4 fixture vision stream checks. Reduce in the DFlash consumer instead, and add a qwen4 capture-shape guard.
  2. --dflash-typical (21ae60f) is still in. It's a lossy knob your own measurements say buys nothing, and a second copy of the MTP typical rule.
  3. The "replaces the MTP head" recommendation still sits inside your measured pass-to-pass noise.
  4. The Co-Authored-By: trailers on every commit and the generated-by line in the body are still there.

No conflict with main.


Generated by Claude Code

…ection streams; the round policy drives Flash-Next

incoai's Qwen3.8-Flash-Next-DFlash2 is trained on the per-token MEAN of the four hyper-connection streams at its target
layers (its SGLang capture patch: unflatten(-1, (hc, hidden)).mean(-2)), the tap GLM-5.3's forward already gives its
drafter. forwardQwen4With handed the raw [B, L, hc*hidden] stream, 4x the drafter's encoder width. `hcStreamMean` is
the shared reshape + mean, unit-tested on a CPU stream.

The DFlash round policy (dflash_policy.zig) now drives Flash-Next as it drives GLM-5.3: both are sparse targets where
every extra verify row routes to more distinct experts. MLX_SERVE_DFLASH_POLICY=0 keeps the fixed block.
…decar (Flash-Next)

`specInitWiring` forced DFlash off for every module-class arch. Flash-Next (qwen4_exp) is one (its n-gram table is
module-shared state), so with --drafter the sidecar loaded, every request logged "drafter=enabled", and the slot
decoded plain: block 3 on the 1K and 16K cells ran at 65-69 tok/s, serial speed, with no [spec-stats] line. The rule
already arms the arch's own MTP head when it can rewind its state across a verify (`moduleStateSpecRollback`); the
DFlash block verify rolls back through the same per-position capture (`rollbackSsmFromCapture`), and `DflashModel.bind`
refuses the archs with no capture seam. So a bound sidecar now outranks the head there too, as on a shell-owned arch;
the gemma drafter and PLD stay off. Tests cover the four cases.
… module-class arch with no head

The upstream case "rollback capability alone never arms a head that is not
loaded" asserted use_dflash false on a module-class arch with a bound sidecar
and no MTP head, which was the old veto, not a property of the head. With a
sidecar bound and the arch able to rewind, DFlash runs there whether or not a
head is loaded (the sidecar outranks the head either way). A new case covers
no head and no sidecar: nothing arms, and no intent bit.

ddalcu commented Oct 21, 2026

Copy link
Copy Markdown
Owner

🤖 MLXServe-PRBot approved — reviewed at head 22f3e8e, no issues found.

All four earlier findings are fixed:

  • CaptureLayers.hc_mean keeps the raw stream for the qwen4 parity oracles.
  • Every DFlash capture site in generate.zig sets the flag.
  • The new qwen4_exp DFlash capture guard checks both shapes and the mean, at prefill width and on the one-token path.
  • --dflash-typical is gone, the trailers are gone, and the body no longer claims a ladder-wide win.

CI on 22f3e8e was still running at review time.


Generated by Claude Code

@davidtai

Copy link
Copy Markdown
Contributor Author

Addressed in the force-push (22f3e8e4, three commits, no trailers):

  1. Capture tap. The reduction is on CaptureLayers now: hc_mean: bool = false. The qwen4 capture site writes the raw [B, L, hc*hidden] stream unless the caller asks for the mean, so the parity oracles keep their slot; every DFlash capture site in generate.zig sets .hc_mean = true. New guard beside GLM's: qwen4_exp DFlash capture: the slot DFlash binds is [B, L, hidden] and equals the stream mean; the oracle slot is the raw stream (QWEN4_TEST_MODEL) reads both shapes from one layer, prefill width and the one-token path, and checks the mean slot equals the mean over the raw slot's streams.
  2. --dflash-typical is out of the PR (kept on a side branch); the body's section 4 says the numbers came from that build.
  3. "replaces the MTP head" is gone from the body: the lane is opt-in via --drafter, MTP stays the default, and the rows where MTP still wins are named.
  4. Trailers stripped from all three commits; the body's generated-by line and the two stray report lines are removed.

Laptop: zig build test-build -Dtest-filter=qwen4 96 passed / 15 skipped (the real-pack tests skip without QWEN4_TEST_MODEL); the hyper-connection filter 8 passed / 2 skipped.

… leaf deferred; a characterization test pins the chain verify

`Generator.verifyForwardDeferred` is the one place that sets `pipeline_build` and `ple_defer`, runs the forward
(plain or capture-all), clears the flags, and fills the deferred n-gram leaf before anything downstream or any
`rollbackSsmFromCapture` can see it. The MTP verify and the tree round built this sequence by hand (two copies of
one ordering contract); the chain verify built eagerly and stopped at the layer-1 host gather. All three call the
helper now. `MLX_SERVE_VERIFY_PIPELINE=0` (and `verify_pipeline_override`) builds eagerly; the MTP batched arm keeps
its eager build through the same switch.

This is a refactor, not a speedup: exact-acceptance A B B A x2 on the fixed-8 lane measured +1.7% greedy and -5.4%
on sampled rows against the eager chain verify, so no claim is made for the pipelined chain verify. The ops are the
same either way; only their submission differs.

Real-pack test (QWEN4_TEST_MODEL + QWEN4_DFLASH, GPU): greedy DFlash decode at fixed block 4 on the repeating prompt,
pipelined against eager, emits byte-identical ids with the same round and accepted counts (96 ids, 8 rounds, 15
accepted on the M5 Max). The qwen4 test rig loads through the server's loader so `MLX_SERVE_NOCACHE_LOAD` applies.

ddalcu commented Oct 22, 2026

Copy link
Copy Markdown
Owner

🤖 MLXServe-PRBot — code review

Delta since 22f3e8e: 4fa39f1 folds #937 in (good, that's the right shape) and factors the deferred-PLE verify into verifyForwardDeferred. The refactor changes behaviour on the default Flash-Next path.

  1. Regression: the MTP tree round on qwen4 now gathers the PLE leaf eagerly.

    • Before: ple_defer was true unconditionally. Only pipeline_build depended on mtp_serial_accept.
    • Now: src/generate.zig:9258 sets verify_pipeline_override = false when !mtp_serial_accept, and verifyForwardDeferred (:6510) derives both ple_defer and pipeline_build from that one bool.
    • mtp_serial_accept requires rowExactDecode(), which requires dflash_bound (model.zig:1225). So on the stock Flash-Next setup (its own MTP head, no DFlash drafter), every MTP round now does the layer-1 host gather against still-lazy draft ids. That's exactly the barrier the deferral exists to avoid (the comment right above it says so).

    The pipeline width and the leaf deferral are two decisions; keep them as two parameters of the helper.

  2. A process-global used as a function argument. The tree round saves verify_pipeline_override, overwrites it and restores it, just to tell its own callee one bool. Pass it as a parameter. The global then exists only for the test, and that test can pass the bool too.

  3. MLX_SERVE_VERIFY_PIPELINE=0 is an env lever on a path you state is output-identical ("the ops are the same either way"). AGENTS.md: env levers only for paths with two arms worth comparing. The characterization test uses the override, not the env. Drop the env.

  4. Your own numbers say the chain verify shouldn't pipeline yet. The commit measures "+1.7% greedy and -5.4% on sampled rows against the eager chain verify". Shipping it on by default ships a measured sampled-row loss. Either keep the chain verify eager and land only the dedup, or show that the -5.4% is noise with interleaved reps.

  5. Minor: ple_lap now starts before the forward (:9260), so the .ple sub-lap includes the graph build that .build already counts. Start it after the forward returns, as before.

The characterization test (dflash chain verify: the pipelined, leaf-deferred build emits the eager build's tokens) is the right bar. Add the same check for the qwen4 MTP tree round, which is the path item 1 breaks. No conflict with main.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants