Repository navigation
qwen27 DFlash: compile the drafter head's lane pipeline at load - #1734
Conversation
|
Thanks @kieranscotney, another clean one. Moving a compile from the first draft round to load, with the same key, source and options, is exactly the kind of change we like. No ATC claim is needed for a fix this size. We've rebased it onto main (clean) with a changelog line, and are running the 27B with DFlash2 on an M5, main against this: tokens equal, decode, and the startup time, since the compile now lands there. An M3 Ultra run follows when that box is free again. |
|
@kieranscotney, our M5 Ultra run (Qwen3.8-27B MLX 4-bit with DFlash2, main against this, fresh servers, order main, this, this, main) found something we didn't expect from a compile move. Tokens are equal to main (0 of 20 prompt rows differ), but decode after some prompts is slower in both runs of this PR, and level in both runs of main:
The stream sweep is level at one stream (-0.4%) and -1.9% at four. Since the pipeline should be the same object, our guess is that the one built at load differs from the one the first head launch would have built in some way the key doesn't capture. That could be a compile option, the function constants, or the library it joins. Or the early |
|
Update from our M5 Ultra, @kieranscotney. We re-ran your change against main 94b90c9 in both orders (main, PR, PR, main and then PR, main, main, PR), fresh process per run, with the same checkpoint, drafter and seeds. All 768 replies are token-exact. Decode after a 16k prompt from offset 5000 is slower in both orders, -8.6% and -8.4%, with every PR run below every main run; after the first 2k prompt it's -5.2% in one order; 2k from offset 5000 doesn't reproduce. A host review found the same head key, generated source and pipeline inputs on both sides, so we can't point to a cause in your code yet. We've also just found that on this machine a comment-only change can move one cell by about 7%, because the binary's layout shifts. So we're checking whether that explains this before asking you for anything. It stays parked until then. |
|
Good news, @kieranscotney: we found the cause of the M5 slowdown, and it isn't your change. The engine calibrates its draft pricing once per executable and keeps it, and the two builds calibrated slightly different cost curves. With that, your build did one extra draft round at 16k (11 against main's 10) for the same exact reply. When we gave both executables main's retained prices, the two were level: -0.6% at 16k from offset 5000 and +0.2% on the first 2k, with the same rounds and proposals. That calibration spread is ours to fix, and it's in hand separately. One thing before this can land: #1738 (just merged) rewrote the same projection path in |
|
Thanks @ashhart, and thanks for tracking down the calibration spread. Rebased onto current main (after #1738); the branch is one commit again,
|
f46cbb2 to
f3b551c
Compare
|
@kieranscotney, to save you the rebase: we put your commit on current main as If that looks right to you, either reset your branch to it, or just say so here and we'll land it from there with your authorship as it is. Either way, it's credited to you. |
f3b551c to
1465084
Compare
|
Reset my branch to |
|
Thanks @kieranscotney, and sorry for the wait. Our M3 Ultra run (Qwen3.8-27B MLX 4-bit with the DFlash2 drafter, main against this branch, warm-up then base, new, new, base) is green: prompt tokens identical in all 20 rows, decode level at every prompt size (2K 80.6 -> 80.4, 64K 19.5 -> 19.5 tok/s, runs overlapping), served chat at 1 to 8 streams within 1%, and sampled code at 8 streams up 5.2% (163.4 -> 172.0 tok/s, both new runs above both base runs). An independent review found the first draft round reuses the pipeline built at load, so draft and accepted tokens can't change, and a failed compile now refuses the drafter by name at start-up. It lands in the next cycle. |
The drafter's trimmed-vocabulary head runs through the target's quant kernels, whose lane pipelines compile from source on first use. Its shape is unique, so it compiled inside the first draft round after a prompt. Backend.init now asks Kernels.prepareDecode for that pipeline; the lookup moves into laneEntry with the same key, so every later launch selects the same pipeline as before.
Exercise the actual preparation, cache lookup and lane encoder with a recording Metal boundary. Check that short and long target prompts leave first head launches on the prepared decode entry, while long prompt projections retain their own key and use the entry's row capacity. Cover preparation rollback, reuse and no-op guards, and run the regression in the default CPU test graph as well as test-qwen27-quant-host. The supplied head preparation already preserves main's long key and max_rows dispatch; keep that production change intact. Co-authored-by: kieranscotney <346361909+kieranscotney@users.noreply.github.com>
1465084 to
8b9d68c
Compare
|
Thank you @kieranscotney. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes. |
What this changes
The DFlash drafter's head is the target's quantized lm_head trimmed to the draft vocabulary (
dflash/runtime_head.zig).It runs through the target's
quant_gpu.Kernels.quant, which compiles one specialised lane-projection pipeline fromsource the first time each
(n, k, slices, ...)key is seen. No target layer has the trimmed head'sn, so its librarywas compiled inside the first draft round after a prompt.
quant_gpu.zig: the lookup inquantOne(key, ranges,getOrPut,lane.Projection.init,joined) moves unchangedinto
laneEntry(p, sk, skip, cut).quantOnecalls it with the same arguments. A newprepareDecode(p)callslaneEntry(p, p.slices, 0, .{ 0, 0 }), the key a decode-rowquantofpbuilds. It encodes nothing and returnsearly for stacked or raw-layout weights and during prompt passes.
dflash/runtime_backend.zig:Backend.initcallstarget.kernels.prepareDecode(head.linear)afterHead.init.The first head launch now finds the same pipeline object it would have built (same key, source and compile options).
Dispatches, buffers and arithmetic are unchanged.
No issue exists for this, so there is no ATC claim; happy to open one and claim it if you prefer.
atc overlaplistsakol1's open stack #1433-#1437 as sharing
quant_gpu.zig. Their hunks touchquantOne's split and raw paths, not thelane lookup moved here, and this commit cherry-picks cleanly onto #1437.
Receipt
mainat94b90c9db; Apple M5 Max, 128 GB; Zig 0.17.0; Qwen3.8-27B MLX 4-bit target(revision
22d8d538) with the stock DFlash2 drafter at depth 1. Measurements came from the same change applied to theYukon mlx.fast Qwen3.8-27B engine copy, whose
quant_gpulookup and drafter head matchmain's.1,065-token public long-copy prompt against the plain target's golden tokens) passed on every run.
and by about 25-40 ms on an idle machine. Encode of the head call was 179.7 ms of it, with a 1.8 ms GPU wait.
Later rounds stay at about 4 ms. Over a 128-token decode window: +0.6% to +1.8% across seven alternating pairs on an
idle machine, +4.7% under load.
zig build test -Dcpu=apple_m1on this branch exits 0; summary7 passed; 0 skipped; 0 failed. Thefailed commandlines in the log come from tests that exercise failure paths, andmainprints the same kind.tools/bench_openai.pyandtools/bench_concurrent.py(the numbers above come from the Yukon harness on the same engine code).Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (Draftedequals the plain target's golden tokens on the exactness gate above; resumed and concurrent cases were not run. The
change builds the same pipeline object earlier, so it cannot affect them.)
change is a compile-time move with no observable output difference.)