Skip to content

dflash: the chain verify builds pipelined and defers the qwen4 PLE gather, as the MTP verify does - #937

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

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

Conversation

@davidtai

Copy link
Copy Markdown
Contributor

Stacked on #935 (this branch is #935's head plus one commit; the diff shows both until #935 merges).

Why

dflashVerify built its [1, 1+m] verify forward whole and, on qwen4_exp, stopped at trunk layer 1 for the n-gram PLE host gather. nextMtp and the tree round already hand the graph to the GPU every VERIFY_PIPELINE_LAYERS layers (pipeline_build) and defer the PLE leaf (ple_defer), so the host builds while the GPU runs; the leaf is filled once after the build and before anything downstream is evaluated. The chain verify now does the same. Outputs are unchanged by construction: the same ops, submitted earlier.

Measured

Flash-Next on an M5 Max (128 GB), DFlash2 adaptive block 8 with typical 0.2, card sampling (T 1.0, top-p 0.95, top-k 20, seed 20260829), the same seeded 1K-token task on every prefill rung, 512 output tokens, one warm-up request per server, one binary per set. Same draws on both sets (identical acceptance per rung), so the delta is round time alone:

prefill without (tok/s) with (tok/s) delta
1K 120 122 +2%
2K 107 116 +8%
4K 100 106 +6%
8K 117 121 +3%
16K 119 121 +2%
32K 118 122 +3%
64K 99 104 +5%
128K 116 122 +5%
256K (YaRN 4x) 115 115 0%
mean 1K-128K 111.7 116.8 +4.6%

An earlier A/B on the standard coding cell (16K prefill, 1,024 tokens, seeds 1234/4321, A B B A) read +4.8%. The MTP lane is untouched by this change (its verify already pipelines).

Tests

zig build test-build -Dtest-filter=dflash: 77 passed, 1 skipped.

…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.
…ther, as the MTP verify does

`dflashVerify` built its `[1, 1+m]` verify forward whole and, on qwen4_exp, stopped
at trunk layer 1 for the n-gram PLE host gather. `nextMtp` and the tree round
already hand the graph to the GPU every VERIFY_PIPELINE_LAYERS layers
(`pipeline_build`) and defer the PLE leaf (`ple_defer`) so the host builds while
the GPU runs, filling the leaf once after the build and before anything
downstream is evaluated (the tree round measured 52.8 -> 50.3 ms a round). The
chain verify now does the same. The leaf's ids are host values on this path
(the draft ids were read for the verify input), so the flush is a gather, not a
sync. Outputs are unchanged by construction: the same ops, submitted earlier.

ddalcu commented Oct 22, 2026

Copy link
Copy Markdown
Owner

🤖 MLXServe-PRBot — code review

  1. Wrong shape. This is dflash(qwen4): DFlash2 on Qwen3.8-Flash-Next (incoai/Qwen3.8-Flash-Next-DFlash2) #935's branch plus one commit, the same feature (DFlash2 on Flash-Next) split across two PRs. Fold 7aa7803 into dflash(qwen4): DFlash2 on Qwen3.8-Flash-Next (incoai/Qwen3.8-Flash-Next-DFlash2) #935 and close this one, or hold it until dflash(qwen4): DFlash2 on Qwen3.8-Flash-Next (incoai/Qwen3.8-Flash-Next-DFlash2) #935 merges and rebase onto main.

  2. The numbers measure code this tree doesn't have. The table says "DFlash2 adaptive block 8 with typical 0.2". --dflash-typical was dropped from dflash(qwen4): DFlash2 on Qwen3.8-Flash-Next (incoai/Qwen3.8-Flash-Next-DFlash2) #935 and lives only on your side branch. So "without" and "with" are both a binary carrying a lossy verify rule that isn't here. Re-run on this branch's code, with exact acceptance. Every cell is also one run. Your own dflash(qwen4): DFlash2 on Qwen3.8-Flash-Next (incoai/Qwen3.8-Flash-Next-DFlash2) #935 data shows 10-20% pass-to-pass swings, and the deltas claimed here are 2-8%. Interleave reps (A B B A, as you did for the 16K cell) on every rung you claim.

  3. Copy-paste. The new block at src/generate.zig:6049-6069 repeats the tree round's sequence at :9232-9265 line for line:

    • set ple_defer and pipeline_build;
    • forward, with a catch that resets the flags and calls discardDeferredPle;
    • reset the flags;
    • errdefer discardDeferredPle;
    • if (!deferredPleOnGpu) flushDeferredPle.

    Two copies of an ordering contract ("must precede the first eval downstream of the leaf and rollbackSsmFromCapture") will drift. Factor one helper that runs a verify forward with the leaf deferred and settles it, and call it from both rounds.

  4. "Outputs are unchanged by construction" needs a test. AGENTS.md: a refactor gets a characterization test first. Pin the chain verify's logits, or its accepted tokens, against the unpipelined, eagerly gathered build on a qwen4 fixture.

No conflict with main.


Generated by Claude Code

@davidtai

Copy link
Copy Markdown
Contributor Author

All four points taken. Re-measured on this branch's code, exact acceptance, DFlash2 fixed block 8, the standard coding cell (16K prefill, 1,024 tokens), one warm-up request per server, two interleaved A B B A quads (seeds 1234 / 4321), PR head (22f3e8e) as A and this branch as B. Same text and the same accepted-per-round on both arms at a seed:

rows A (PR head) B (this branch) delta
greedy 94.0, 92.5, 89.4, 92.3 (mean 92.0) 92.3, 93.0, 95.7, 93.6 (mean 93.7) +1.7%
card (T 1.0, top-p 0.95, top-k 20) 87.6, 84.6, 74.8, 87.4 (mean 83.6) 74.3, 83.1, 75.8, 83.3 (mean 79.1) -5.4%

The +4.6% in the description came from a binary carrying --dflash-typical and one run per rung; it does not hold under this instrument. The pipelining is flat on greedy and negative on sampled rows, so it has no claim to make.

What is still worth keeping, as a refactor with no performance claim: Generator.verifyForwardDeferred is now the one place that sets pipeline_build and ple_defer, runs the forward, and fills the leaf before anything downstream; the chain verify, the tree round and the MTP verify all call it, and MLX_SERVE_VERIFY_PIPELINE=0 builds eagerly. A real-pack characterization test (QWEN4_TEST_MODEL + QWEN4_DFLASH: greedy fixed-4 decode, pipelined vs eager, byte-identical ids and round/accepted counts) is on the branch and runs on the box now. If it passes, the helper and test go onto #935 as its fourth commit and this PR closes.

@davidtai

Copy link
Copy Markdown
Contributor Author

Closed: the speed claim did not survive exact-acceptance interleaved reps (see above). The helper refactor and characterization test go to #935 as a no-claim refactor once the real-pack test passes.

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