DFlash2 speculative decoding for Qwen3.8-27B - #607
Open
spokvulcan wants to merge 3 commits into
Open
spokvulcan wants to merge 3 commits into
spokvulcan wants to merge 3 commits into
Conversation
spokvulcan
added a commit
to spokvulcan/tesseract
that referenced
this pull request
Sep 4, 2026
…in (ADR-0061) (#464) Moves the vendor pin onto the reshaped DFlash2 series (ADR-0061) and adapts the app to the new protocols. The 13 fork commits are now one upstream-shaped commit, which is what went to ml-explore/mlx-swift-lm as ml-explore/mlx-swift-lm#607. App side: DFlash2Support loads and 4-bit quantizes the drafter directly, LLMActor stacks same-input projections through the new protocol, the bench runs the fixed width (`--bench-blocks 8`, the old `8f` still parses), and ServerCompletion sizes the warm-start window from the drafter's context window. Gates on the docs prompt against the pre-reshape build: identical output, acceptance 115/532 bit-identical, per-round drafts identical, ABBA medians 32.8/30.1 vs 28.8/27.8 tok/s. Vendor tests 740/740, swift-format and verify-docs clean. Ledger R58 and ADR-0061 cover the numbers.
christopherbattlefrontlegal
added a commit
to christopherbattlefrontlegal/swift-mlx-forge
that referenced
this pull request
Sep 5, 2026
Port of ml-explore/mlx-swift-lm#607 onto the vendored tree: drafter model, block-parallel iterator, factory, registry and generate overloads, plus the Qwen3.5 verify pass written against the vendored eager layer bodies (the upstream compiled decode segments are not carried). Forge resolves a drafter next to a Qwen3.5-family target at load and routes chat decoding through it ahead of the native MTP head. Measured, greedy, 320 tokens: stock Qwen3.8-27B bf16 goes from 11.3 to 20.8 tok/s on prose and 38.4 on code. The Fable merges accept 3 to 7 percent of drafts and decode slower than plain; the drafter was distilled on the stock model. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds DFlash2 speculative decoding: a drafter model (DFlash2DraftModel), the iterator that drives it (DFlash2SpeculativeTokenIterator), a factory and registry in the MTP drafter's shape, generate/generateTokens overloads, and the target side on Qwen35TextModel/Qwen35Model. The drafter embeds [anchor, MASK x 7], attends over a sliding window of the target's captured hidden states and walks the top-16 candidates per position with its selector; the target verifies the block in one pass. The next round is built while that pass runs, from lazy accept-dependent arrays, and the host syncs once per round. Greedy rounds compare argmaxes; sampled rounds rejection-sample against the selector's candidate distribution, so output matches the target's own decoding either way. Logit processors run row by row on a copy, as SpeculativeTokenIterator does. Qwen3.5: the verify pass reuses the compiled decode segments with S rows, writes attention rows at a lazy position without moving the cache offset, and returns gated-delta captures so the iterator can replay the recurrent state to the accepted prefix. Rope now rides inside the decode traces with its offset as a [1] array input. Also: stackSameInputProjections(in:) folds gate|up and q|k|v into one quantized matmul each (plain QuantizedLinear only).
spokvulcan
force-pushed
the
dflash2-upstream-clean
branch
from
September 15, 2026 21:51
65c7f45 to
56a21b2
Compare
5 tasks
`stackSameInputProjections(in:)` iterated `modules()`, whose array holds every projection module, so the originals of every block stayed alive until the loop ended and the transient reached the sum of all stacked blocks (7 GB on Qwen3.8-27B) instead of one block. The loop now keeps only the stacking modules. `testSameInputStackingReleasesEachBlockBeforeTheNext` held 41.9 MB above the loaded weights for 8 blocks of 5.2 MB before, and passes after. (cherry picked from commit f8b4827)
Contributor
Author
|
One more commit. Qwen3.5-27B 4-bit in the app that embeds this library, MLX peak memory in GB:
|
The MTP `generate` overload takes a trailing `tools:` parameter, so the symbol link in the DFlash2 `generate` doc comment resolved to nothing and `verify-docs.sh` failed MLXLMCommon under --warnings-as-errors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed changes
Adds DFlash2 speculative decoding for the Qwen3.5 family. Closes #606.
Why this model in particular, Qwen3.8-27B scores 52 on the Artificial Analysis Intelligence Index (https://artificialanalysis.ai/models/qwen3-8-27b), the same as GPT-5.6 Luna and 14 points behind Claude Fable 5.1 at 66.
DFlash2 is the block drafter from inco.ai and z-lab (https://inco.ai/blog/dflash2/, reference code at https://github.com/z-lab/dflash). It's a separate small model, not a head inside the checkpoint: for Qwen3.8-27B it has five layers, reads the target's hidden states after layers 5, 19, 33, 47 and 61, and proposes 7 tokens at once, which the target then checks in a single forward pass. The drafter checkpoint is https://huggingface.co/incoai/Qwen3.8-27B-DFlash2. It has no embedding or output head of its own and borrows the target's. Output is token-for-token what the target would produce by itself.
What's in the PR
A drafter model and a speculative token iterator that follow the existing MTP drafter pattern (drafter protocol, factory, registry and
generateoverloads), plus a verify pass on the Qwen3.5 text model that checks a whole 8-token block in one forward pass and commits the caches to whatever prefix was accepted.Numbers
Measured on a MacBook Pro 16-inch, Apple M3 Max (16-core CPU, 40-core GPU), 48 GB, macOS 26.5.2, with Qwen3.8-27B quantized to 4 bits (group size 64) and the drafter above, also 4-bit. Greedy decoding, block size 8, 192 generated tokens per run, thinking enabled. One prompt per category rather than the full benchmark sets, so treat the right column as a spot check.
"Acceptance length" is the average number of tokens each verify pass yields, counting the one token the target produces itself: 1.0 is plain decoding, 8.0 is every proposal accepted. The comparison column is Table 4 of the blog post linked above, same model and drafter, block size 8.
Decode speed on that machine. The blog post doesn't publish tokens per second, so no comparison column:
TokenIteratorMath and code match the blog post, and the z-lab reference implementation (Python/MLX) gives the same acceptance on the same prompts. Chat and prose sit below the MT-Bench number: with thinking on, most of the 192 tokens are the model's planning text, which the DFlash paper measured at roughly a quarter lower acceptance than the answer itself, and a 4-bit target loses a few more points against the unquantized one the published numbers use. Every row is token-for-token identical to plain
TokenIteratoroutput, and greedy acceptance is stable run to run.Tests
Tests/MLXLMTests/DFlash2Tests.swift, all with tiny synthetic models so they run in the normal unit suite: config decoding and rejection of unsupported shapes, the context cache (placeholder rows, compaction), the visibility mask, the dynamic convolution against a naive loop, the selector, compiled versus eager drafter output, gated-delta replay against a prefix recompute, rejection sampling, sampling filter order againstTopPSampler, and the iterator against mock target and drafter models: accept and bonus bookkeeping, penalties matchingTokenIteratortoken for token, warm start over a prefilled cache matching a cold run, finalize rewinding undrained drafts, and the errors for unsupported inputs.Checklist
pre-commit run --all-filesAI usage
accurately describes the code changes.
implementation and our earlier port, reviewed and benchmarked by me line
by line.