Skip to content

feat: DFlash2 block-diffusion drafter for Qwen 3.8 (opt-in, byte-identical) - #22

Merged
NeelM0906 merged 5 commits into
mainfrom
claude/qwen38-dflash2-drafter
Aug 28, 2026
Merged

feat: DFlash2 block-diffusion drafter for Qwen 3.8 (opt-in, byte-identical)#22
NeelM0906 merged 5 commits into
mainfrom
claude/qwen38-dflash2-drafter

Conversation

@NeelM0906

Copy link
Copy Markdown
Owner

Stacked on #21 (retarget as the chain merges). Adopts the DFlash2 drafter (z-lab/Inco, ICML 2026) as an alternative draft source for the Qwen 3.8 speculative rounds — the verify/accept/rollback machinery and therefore every emitted byte is unchanged for any draft quality.

What's here

  • Drafter core (Qwen38DFlash2Drafter, Metal/DFlash2/): 5-layer block-diffusion forward with KV-injected target taps, bidirectional block attention, grouped dynamic conv, top-16 + CPU path selector, mmap safetensors loader. FP32 residual stream (o/down projections overflow FP16 on outlier channels).
  • INT4 at load (DFlash2Int4Slab): projections quantize into the production multi-x format, disk-cached beside the checkpoint. The BF16 drafter (3.85 GB) thrashed the 24 GB host and reproduced the DSV4 file-backed wiring collapse — draft rounds went 2.13 s → 32 ms with the slab.
  • Integration: verify stages tap rows; prefill primes the prompt's trailing context window; accept shrinks to the GDN restore; continuity breaks reset the drafter (quality-only). MFERENCE_DFLASH2_DIR enables; per-draft-position accept telemetry added to MFERENCE_PHASES=1.

Gates

  • Every kernel CPU-reference tested; assembled core + selector match the fp32 MLX reference across two rounds (env-gated fixtures under scratch/).
  • Full suite: 1093 tests green. Byte-identity confirmed on every A/B.

Measured (M5 24 GB, same-session A/B vs MTP k=3, greedy)

Case MTP DFlash2
bench chat prompt 11.38 11.81 (+3.7%)
raw code prompt 15.58 13.76

Tokens/round 3.53 vs 2.27 on the bench prompt — the drafts are genuinely deeper (accept profile 75/53/42/33/25/25 vs 75/34/18), consistent with llama.cpp's independent +3%-on-prose finding. The limiter is the verify pass's linear per-row cost (~15 ms/row of sequential attention+GDN dispatches), which taxes k=6 harder than k=3.

Follow-ups (docs/QWEN38_DFLASH2.md)

  • Batch verify rows (multi-row attention + GDN) — the unlock, benefits MTP too
  • Real-tap fixture comparison vs the MLX reference to close the late-position acceptance gap
  • Chunked top-16 head to halve the 32 ms draft cost

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76b711b25b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Mference/Runtime/Inference/Qwen38DFlash2Drafter.swift
NeelM0906 added a commit that referenced this pull request Aug 26, 2026
The config guards accepted a checkpoint with the expected vocabulary and
target-layer count but a different hidden_size (Codex P2 on PR #22). The
drafter shares the target's embedding/LM head and consumes tap captures
of the target's hidden rows, so the mismatch would mis-stride those
kernels or read out of bounds on the GPU instead of failing cleanly.
Validate hidden_size against the target before any buffer is built.

Config-only test fixtures prove the guards fire before model.safetensors
is opened; without the guard the mismatched checkpoint sails through to
the weights load.
@NeelM0906
NeelM0906 force-pushed the claude/qwen38-dflash2-drafter branch from 76b711b to db38166 Compare August 26, 2026 17:36
NeelM0906 added a commit that referenced this pull request Aug 26, 2026
The config guards accepted a checkpoint with the expected vocabulary and
target-layer count but a different hidden_size (Codex P2 on PR #22). The
drafter shares the target's embedding/LM head and consumes tap captures
of the target's hidden rows, so the mismatch would mis-stride those
kernels or read out of bounds on the GPU instead of failing cleanly.
Validate hidden_size against the target before any buffer is built.

Config-only test fixtures prove the guards fire before model.safetensors
is opened; without the guard the mismatched checkpoint sails through to
the weights load.
@NeelM0906
NeelM0906 force-pushed the claude/qwen38-dflash2-drafter branch from db38166 to e919d79 Compare August 26, 2026 18:15
Base automatically changed from claude/streaming-io-overlap to main August 26, 2026 20:13
Ports z-lab model_mlx.py semantics to Metal: BF16 multi-x GEMV, block
attention over [sliding ctx | bidirectional block], grouped dynamic conv
(prepare/finish), top-16 + CPU path selector, tap-gather staging, and an
mmap safetensors loader. FP32 residual stream (o/down projections
overflow FP16 on outlier channels — the Inkling residual lesson). Every
kernel is CPU-reference tested; the assembled core and selector match
the fp32 MLX reference (fixtures under scratch/, env-gated) — the bf16
reference disagrees with itself-in-fp32 by more than this port does.
Not wired into decode yet.
Verify stages tap rows for the drafter; accept shrinks to the GDN
restore + tap commit; continuity breaks (plain-decode gaps, rewinds)
reset the drafter with positions realigned — quality-only, bytes
unchanged. Draft capacity defaults to 6 when the drafter is active.
… taps

The BF16 drafter (3.85 GB) both thrashed the 24 GB host against the
15 GB target and ran hand-rolled scalar GEMVs at ~8.6 GB/s — decode
collapsed to 0.6 tok/s. Projections now quantize at load into the
production multi-x INT4 format (disk-cached beside the checkpoint),
small tensors copy to an anonymous GPU buffer, and no command buffer
touches file-backed memory. Draft rounds: 2.13 s -> 32 ms.

Prefill now stages the prompt's trailing context window of tap rows so
the first rounds draft warm, and spec stats gain a per-draft-position
accept profile.

Measured (M5 24 GB, greedy 96-128 tok, same-session A/B vs MTP k=3):
bench-protocol chat prompt 11.81 vs 11.38 tok/s (+3.7%), accept profile
75/53/42/33/25/25 vs 75/34/18; raw code prompt 13.8 vs 15.6 (-11%).
Bytes identical everywhere. The limiter is the verify pass's linear
per-row cost (~15 ms/row of sequential attention+GDN dispatches), which
taxes k=6 harder than MTP's k=3 — batching verify rows is the unlock.
The config guards accepted a checkpoint with the expected vocabulary and
target-layer count but a different hidden_size (Codex P2 on PR #22). The
drafter shares the target's embedding/LM head and consumes tap captures
of the target's hidden rows, so the mismatch would mis-stride those
kernels or read out of bounds on the GPU instead of failing cleanly.
Validate hidden_size against the target before any buffer is built.

Config-only test fixtures prove the guards fire before model.safetensors
is opened; without the guard the mismatched checkpoint sails through to
the weights load.
@NeelM0906
NeelM0906 force-pushed the claude/qwen38-dflash2-drafter branch from e919d79 to 505f83c Compare August 26, 2026 20:13
@NeelM0906
NeelM0906 merged commit 297c008 into main Aug 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant