Repository navigation
perf(qwen27): a drafted round overlaps its cache commit with the next draft, and the drafter's attention and top-k use more of the GPU - #713
Merged
Conversation
…e next draft accept() split the round's one command buffer in two: the target's picks with the accepted rows' taps and head row (all the host waits for), then the keep (KV rows, recurrent state, history), which nothing reads before the next verify. The keep is committed once the picks end and is waited for just before that verify (and before a run returns), so the drafter's forward overlaps it. The served path (runRound) finishes the keep at once, so the overlap with the drafter's forward is in the CLI run path (runImpl) only. The drafter's first command buffer commits after six ops instead of forty, so the GPU starts while the rest is still being encoded, and its row copies go out as one dispatch instead of 128-row chunks. Tokens, rounds, verified rows and accepted drafts are unchanged.
…olds them Each key's score and value loaded and folded before the next key's load started, so a lane waited on memory once per key. A tile of four keys now loads its scores and values together, then folds them in key order with the same fp32 chain: the maximum after each key, both weights and the sums are computed exactly as before, so every output byte is unchanged (390 masked, fully masked and unmasked cases at 1 to 2063 keys, bf16 and fp32 outputs, equal to the old kernel's bytes; wider tiles measured slower). The 16-row draft attention at 1000 keys runs in 0.31 ms from 1.00 ms.
One threadgroup scanned a whole row, so a 15-row draft head over 98592 columns used 15 of the GPU's cores. Rows of 4096 columns or more now scan in sixteen spans, each leaving its top k in a small buffer, and a second dispatch merges them with the same tournament and the same (score, lower token) order, so the entries, counts and non-finite masks are byte-equal to the single-group kernel's (576 cases over widths 4096 to 248320, ties, both zeros, NaN and infinities, padded strides). The draft's top-16 runs in 0.34 ms from 0.57 ms.
jschmied
pushed a commit
to jschmied/TensorFold
that referenced
this pull request
Oct 13, 2026
Owner
|
Thank you, @jkuepker. Three careful speed changes to the 27B on Metal, each with its exactness argument written down, and all three hold on our machines: the in-place DeltaNet scan (#711), the decode attention without the tail kernel (#712), and the draft-side changes (#713). Your ten commits landed in 1.0.9 under your name, rebased onto 1.0.8 (only #713's first commit changed: three What we ran, with all three together against the tree they started from:
|
Open
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.
Three changes to DFlash2 drafted decode on Metal. Tokens, draft rounds, verified rows and accepted drafts are unchanged, and the plain decode binary is byte-identical to main's.
4f5f387b).accept()now splits the round's command buffer in two: the target's picks with the accepted rows' taps and head row (all the host waits for), then the keep (KV rows, recurrent state, history), which nothing reads before the next verify. The keep is committed without a wait and waited for just before that verify, and before a run returns, so the drafter's forward overlaps it. This is only correct if nothing encoded in between reads or writes what the keep writes. The drafter's forward, the taps ring andRunner.ensurefrom Qwen3.8-27B serves concurrent requests: shared rounds across runner slots, DFlash2 for one stream, and tf-qwen27-bench #578 don't touch those buffers, and debug asserts at the target's entry points (commitDevice,accept,absorb,advance,prefill,promptChunk,decodeChunk) check that no keep is still pending. The served path (runRound) finishes the keep at once, so the overlap is in the CLI run path only. The drafter's first command buffer also commits after six ops instead of forty, and its row copies go out as one dispatch.654c85b5). Each lane loaded one key's score and value and folded it before the next key's load started. A tile of four keys now loads together, then folds in key order with the same fp32 chain, so every output byte is unchanged.6ca24f79). One threadgroup scanned a whole row, so a 15-row draft head over 98592 columns used 15 of the GPU's cores. Rows of 4096 columns or more now scan in sixteen spans, and a second dispatch merges them with the same tournament and the same (score, lower token) order. A compile-time check ties the kernel's lane, span andmax_kconstants to the Zig side, so a change to one of them fails the build instead of the merge.zig/tests/bf16_topk_bench.zig, whichtools/zig/bf16_topk_bench.shbuilds, now dispatches the single-group kernel directly as its control.The three can land separately. 2 and 3 also run on the served path; 1 doesn't.
Exactness
All on one Apple M5 Max, against origin/main
e7c523eb(1.0.7) built the same way.tools/zig/bf16_topk_bench.shon the GPUqwen27_gate.py check(4 prompts × 256 greedy tokens,--chunk 128 --compare-chunk 16)prompt_state_byte_equal4/4tf-qwen27-dflash-run --prototype q4 --compare-plain, DFlash2)qwen27_http_gate.py(raw/chat × plain/draft)bench_concurrent.py --levels 1,4 --alone --serial, code and chat)tf-qwen27-streams(real weights), 4 streamstf-qwen27-synthetic,-lanes-check,-served-check,-dflash-served-check,-dflash-syntheticThe gate exits 1 on main too: story (token 68) and code (token 21) leave the Python 0.6.6 oracle at a BF16 near-tie. This branch gives exactly main's tokens on all four.
Speed
Same Mac and settings throughout, with no other model process running. Builds of origin/main
e7c523eband of this branch, run alternately (ABBA).Drafted decode, CLI (
tf-qwen27-dflash-run --prototype q4, DFlash2 4-bit, 256 tokens, 5 interleaved pairs per prompt). Tokens, rounds, verified rows and accepted drafts are equal in every run.At 16K main's runs spread 6.3%, with two fast runs; against main's fastest run the branch's median is still 6.9% faster.
Served drafted (native server,
tools/zig/qwen27_served_bench.py, DFlash2 4-bit, 3 repeats, 2 sessions per build, alternated with main and two other branches): copy −1.1%, arithmetic +0.9%, story −0.4%, code +0.6%.tools/bench_openai.pywith the drafter: fibonacci −0.2%, chat +2.1%. All are inside main's own session-to-session spread, so I'm not claiming a served change. The served code prompt is the same 24 tokens as the CLI's short case. The likely difference is commit 1, which only the CLI path uses, but I haven't measured the commits separately on this base. The served prompts are all short (14–31 tokens), so there is no served long-context number.Cold prefill (fresh KV,
--prompt-cache-gib 0, 2k/8k/32k): no change. Plain decode wasn't timed:tf-qwen27-runbuilt from this branch is byte-identical to main's.Host checks
zig build test test-golden -Dcpu=apple_m1: 143/143 steps, 1290/1292 tests passed, 0 failed, 2 skipped.test-golden: 309/319 equal, the 10 known differences, 0 unexpected. All the same as main.python3 tools/zig/lean_check.py: 534 problems on main and 534 on this branch, 0 new.zig fmt --checkis clean on the touched files.Setup: Apple M5 Max (40-core GPU, 128 GB), macOS 27.0 (26A428), Xcode 27.0, Zig 0.17.0. Checkpoint
TensorFold/Qwen3.8-27B-MLX-4bit@22d8d538; drafterz-lab/Qwen3.8-27B-DFlash2@50307d4c, 4-bit. Not run: CUDA and ROCm (the changed kernels are Metal), and Apple M1–M4.