Skip to content

lanes: Drafted passes held windows to a target that reads them itself - #1378

Closed
BlivionIaG wants to merge 1 commit into
ashhart:mainfrom
BlivionIaG:series/lanes-held
Closed

BlivionIaG wants to merge 1 commit into
ashhart:mainfrom
BlivionIaG:series/lanes-held

Conversation

@BlivionIaG

@BlivionIaG BlivionIaG commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Branch series/lanes-held, 1 commit on main 9a8d654ef: 9a8d654ef. Sits on main. Part of #1374.

What this changes

Drafted.held_on_target (default false). When a target reads held drafts from the drafter on the device, Drafted.verify
gives it the windows with Window.held as the round loop made them, so the host does not wait to read the drafts back.
A stream with fillers still gets host tokens. With the flag off, verify runs as before; the new check is not evaluated.

Nothing on main sets the flag. It lands with the LFM2 DSpark drafter PR, which copies held drafts on the GPU. It is a
separate PR so the lane-core change can be reviewed on its own.

Receipts at this head

  • Head 9a8d654ef is unchanged by the review and mergeable with main. The consumer of Drafted.held_on_target is the LFM2 Metal backend only: it reads the drafter's held picks on the GPU through held_source and lfm2_draft_ids after an unwaited hold. No other family sets it.

  • Device check, M4 Pro, the DSpark stack head (bf16 drafter): exact.py 8 of 8 drafted equal "draft": false; bench_concurrent --alone --serial at 1, 4, 8 and 16 streams, same and distinct prompts, 0 unequal, 0 failed; sampled (temperature 1.0, seeded) distinct prompts at 4 and 8 streams equal. Speed in those cells is not valid (other builds ran).

  • Test that fails before the change (drafted_held_test.zig, run with the flag kept but the pass-through removed):
    242 pass, 1 fail: "held_on_target passes held windows through ..." expected 3, found 0. With the change: 243/243.

  • Host gates on this branch (fresh cache; main: lanes 242/242, server-cpu 792/795 (3 skipped), cuda-host 2021/2033
    (12 skipped), golden 322/335 and 26/26 equal with 0 unexpected, lean 433 problems):

fmt exit=0 files=3
diff-check exit=0
lean exit=1 4083 files, 433 problems
lanes exit=0 secs=24 Build Summary: 3/3 steps succeeded; 243/243 tests passed
lane-api exit=0 secs=9 Build Summary: 3/3 steps succeeded; 297/297 tests passed
lane-lone exit=0 secs=2 Build Summary: 3/3 steps succeeded; 19/19 tests passed
lane-halt exit=0 secs=2 Build Summary: 3/3 steps succeeded; 18/18 tests passed
lane-interrupted exit=0 secs=3 Build Summary: 3/3 steps succeeded; 18/18 tests passed
root-copy exit=0 secs=2 Build Summary: 5/5 steps succeeded; 31/31 tests passed
server-cpu exit=0 secs=130 Build Summary: 5/5 steps succeeded; 792/795 tests passed (3 skipped)
golden exit=0 secs=84 Build Summary: 5/5 steps succeeded
cuda-host exit=0 secs=36 Build Summary: 258/258 steps succeeded; 2021/2033 tests passed (12 skipped)
check-cuda exit=0 secs=2 Build Summary: 7/7 steps succeeded
consumers exit=0 secs=70 Build Summary: 7/7 steps succeeded
hip-host exit=0 secs=2 Build Summary: 8/8 steps succeeded; 52/52 tests passed
hip-qwen35 exit=0 secs=4 Build Summary: 4/4 steps succeeded; 32/33 tests passed (1 skipped)
hip-check exit=0 secs=2 Build Summary: 5/5 steps succeeded
native exit=0 secs=107 Build Summary: 57/57 steps succeeded
dev-tools exit=0 secs=160 Build Summary: 213/213 steps succeeded

Lean: 433 problems, 433 on main, 0 new (tools/zig/lean_new.py). The 433 are inherited.

  • Prompt speed: n/a. The change is in Drafted.verify only.

A/B on current main (other families and builds)

M4 Pro, LFM2.5-24B-A2B 4-bit, no drafter, 4 streams, a different prompt on each, greedy 256 tokens, 4 alternated rounds, a fresh server per cell, a quiet machine (load under 2, no Zig build), every build merged with main 8c36359d5; aggregate tok/s by round:

build round 1 round 2 round 3 round 4
main 193.4 194.1 193.3 193.2
held (#1378) 194.8 192.9 193.5 193.4
guard (#1379) 193.2 193.4 193.5 193.3
chances (#1381) 193.1 193.3 193.4 193.3
lone guard (#1382) 193.3 194.0 193.2 193.0

Every build is within 0.5% of main and every reply token-equal to main's. An earlier single-round run showed #1378 at -6% and #1379 at -4%; four rounds showed nothing, so those were session effects.

M5 Ultra, Qwen3.8-27B + DFlash2, 4 streams, distinct prompts, greedy 256 tokens, 6 rotated rounds, 3 reps a cell, token_sha equal across all four binaries. main2 is 8c36359d5 built a second time in a separate worktree. Median of the 3 reps, tok/s:

round (order) main main2 held lane (6539b72be)
1 (main, main2, held, lane) 321.6 318.8 322.9 318.6
2 (lane, held, main2, main) 317.1 319.7 323.2 318.5
3 (main2, main, lane, held) 327.2 318.3 323.6 328.5
4 (held, lane, main, main2) 326.4 319.4 324.8 319.2
5 (main, main2, held, lane) 322.5 318.8 320.2 318.5
6 (lane, held, main2, main) 317.4 318.3 322.6 321.6
median of round medians 322.0 318.8 (-1.0%) 323.1 (+0.3%) 318.9 (-1.0%)

1 stream: 296.0, 296.5, 296.0 and 295.8. The two builds of identical source differ as much as the lane head does, and the spread inside a cell is 304 to 333, so the gap follows the build or the session, not the code. An earlier session had reported 1.8% to 2.7% lower at this cell; I did not reproduce it.

The 6-round table above includes the held head; the other families are in #1382.

Not measured

  • M1 and M2 Macs. Nothing here depends on chip-specific kernels (this PR changes no kernel).
  • CUDA and HIP at run time: only host tests and compile checks. Linux CI is run by the PR checks.

Related PRs

Sits on main. Merges cleanly with #1379 and #1380. The DSpark drafter PR (#1391) sits on it.

With Drafted.held_on_target set, Drafted.verify gives the target its windows with Window.held as the round loop
made them, so a target that reads held drafts from the drafter on the device skips the host readback. A window
of a stream with fillers still gets host tokens. The flag is off by default; without it verify runs as before
and the new check is not evaluated.

drafted_held_test.zig checks the three cases: held windows pass through, a stream with fillers gets its pending
token as host tokens, and with the flag off the drafter's drafts are read back as host tokens.
@ashhart

ashhart commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Thanks @BlivionIaG. This one reviewed clean on its own: default-off, no source defect found in its commit. It lands after a device check showing unchanged tokens and a per-family A/B. Please qualify the actual asynchronous consumer before anything enables it.

@BlivionIaG

BlivionIaG commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. The consumer of held_on_target is the LFM2 Metal backend only (held_source and lfm2_draft_ids, after an unwaited hold). Device check on an M4 Pro at the stack head: drafted equals "draft": false on exact.py 8 of 8 and on bench_concurrent --alone --serial at 1, 4, 8 and 16 streams (same and distinct prompts, and sampled at 4 and 8), 0 unequal. Held head against main, M4 Pro, LFM2 4 streams, 4 alternated rounds: within 0.5% of main, tokens equal. M5, Qwen3.8-27B with DFlash2, 4 streams, 6 rotated rounds: held 323.1 against main 322.0 tok/s (+0.3%), tokens equal.

@ashhart

ashhart commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Landed on main as ed3c8ba, in 1.1.7, together with #1380. Nothing sets the flag yet; on a GB10 every cell equals main. Thank you!

@ashhart ashhart closed this Oct 27, 2026
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.

2 participants