Skip to content

lane host: three fairness fixes to #1188's prompt steps - #1287

Closed
jschmied wants to merge 3 commits into
ashhart:mainfrom
jschmied:pf1188-sched
Closed

jschmied wants to merge 3 commits into
ashhart:mainfrom
jschmied:pf1188-sched

Conversation

@jschmied

Copy link
Copy Markdown
Contributor

Follow-ups to #1188 (prompt steps beside decoding streams), found in a review of main. Each commit has a test that fails on main and passes here. The Gemma 4 fix from the same review is a separate PR.

1. A burst of short prompts could pause decoding without bound (4c22c055b)
The short-prompt loop admits and fills until no short prompt waits. If a reply ends together with its prompt (max_tokens 1, an embedding, a cancel), it frees its lane inside the loop. Under steady arrivals of those, decoders got no round. Now, after a lane count of short prompts have begun, a round goes first. A burst that fits the lanes still goes in together, so normal traffic is unchanged. Test: 40 one-token requests beside a decoder on 2 lanes. Main fills all 40 back to back; now at most 2 in a row.

2. Short prompts waited behind an aged long prompt's debt (3dc3997d5)
pick() let a long prompt that had waited 8 choices win. The short loop then ended, and with decode time owed, the long step wasn't due either, so a new short prompt waited out the whole debt. Short prompts are now picked among themselves (same order: aged, foreground, least left). The long step picks among long prompts. A burst past the lane count yields to a waiting long prompt as it does to a decoder, so aging still holds with no decoder. Tests: share 0.01, a decoder, a 1000-row prompt and 30 one-token requests. On main one short prompt waited 63 decoder rounds; now at most 1. A second test checks that with no decoder, a long prompt beside 40 one-token arrivals still goes in before the last of them.

3. Debt survived its decoders (138e7a400)
If every decoder finished or paused, the debt stayed, and the next decoder was paid it before the long prompt's next step. The debt is now cleared whenever no stream is runnable. Test: a 3-token decoder ends after a long prompt's first step at share 0.01, then a second decoder arrives. On main it ran 50 rounds before the long prompt's next step; now 1 (its first token).

Gates: full zig build test -Dnvcc exits 0. Lean: 0 new problems against the merge base. The fake-backend equality tests are unchanged and pass. With the default share (1), scheduling changes only when lanes free up inside a burst (point 1).

…hile streams decode

The short-prompt loop admitted and filled prompts until none was short. A
reply that ends with its prompt (max_tokens 1, an embedding, a cancel) frees
its lane inside the loop, so under steady arrivals of those the decoders got
no round at all. After a lane count of short prompts begun, a round goes
first; a burst of arrivals that fits the lanes still goes in together.

Test: 40 one-token requests beside a decoder on 2 lanes; main fills all 40
with no decoder round between them, now at most 2 in a row.
…ged long one's debt

The short-prompt loop asked the one fill pick: once a long prompt had waited
eight choices it won, the loop ended, and with decode time owed its step was
not due, so a short prompt arriving then waited out the whole debt. Short
prompts are now picked among themselves (the same order: aged, foreground,
least left); the long step after them picks among long prompts, and a burst
past the lane count yields to a waiting long prompt as to a decoder.

Tests: a decoder, a 1,000-row prompt and 30 one-token requests at share
0.01; on main one short prompt waited 63 decoder rounds behind the debt, now
at most 1. With no decoder, a long prompt beside 40 one-token arrivals still
goes in before the last of them.
…opped

A long prompt's step stays due while no stream decodes, but the debt it
owed earlier decoders stayed too, and the next decoder to start was paid
it before the long prompt's next step. The debt is now cleared whenever no
stream is runnable, as it already was when no prompt waits.

Test: a 3-token decoder ends after a long prompt's first step at share
0.01, a second decoder arrives; on main the second ran 50 rounds before the
long prompt's next step, now 1 (its first token).
@jschmied

Copy link
Copy Markdown
Contributor Author

Metal check, since lane_host is shared. M3 Pro (36 GB), macOS 26.7.1, main 8086c205c against this branch 138e7a400:

  • Full zig build test: no new failures. One core.paced_read test failed once while an 18 GB download was loading the disk. It passed in 5 reruns, and this branch doesn't touch that file.
  • Two-stream smoke: tensorfold-native serve --parallel 2 --context 4096 --temperature 0, drafts on. Two prompts, solo, then two concurrent rounds.
    • Models: Qwen3.5-2B 4-bit, and Nemotron 3.5 Lightning 30B-A3B 4-bit with MTP.
    • Every concurrent reply equals its solo reply, and the replies are identical on main and this branch.
    • Timing is within noise. On Nemotron the concurrent rounds took 49 steps instead of 36 with fewer accepted drafts, so the streams did overlap.

@ashhart

ashhart commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Landed on main (84e0d56..24072ec), with your three commits as author. Thank you! On a GB10 (Qwen3.8-27B CUDA) against main: tokens identical, the 1-8 stream sweep exactly flat, and prefill within 0.3% across six alternating runs. On an M3 Ultra it is flat as well, and with the rest of the batch, Flash Next MLX on CUDA is flat to +2.8%.

- Synthema, Ash's AI Assistant.

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