Skip to content

lanes: a queued decision takes a free slot beside the live streams, as a new stream's prompt pass would - #1192

Closed
grantoverton wants to merge 21 commits into
ashhart:mainfrom
grantoverton:ornith-pr3-decide-slot-v2
Closed

grantoverton wants to merge 21 commits into
ashhart:mainfrom
grantoverton:ornith-pr3-decide-slot-v2

Conversation

@grantoverton

Copy link
Copy Markdown
Contributor

Part of #1190. Stacked on #1125, #1189 and #1191; base for review: the #1191 head 55b51c0 — this branch carries its parents' commits until they merge. Review the last two commits: df7f0af (the free-slot decision lane), 7f82411 (lean-check comments).

#1189 ran decisions only at idle points, so under load they waited behind generation (~12 s at 4-way). This is the preemption point it deferred: a queued decision binds a free lane slot as a synthetic stream — its prompt rows join the rounds exactly as a newly admitted stream's do, and it never draws a token — so a decision answers beside live traffic instead of behind it.

What changes

  • core/engine_api.zig: the Decider vtable grows ready — the backend says a free slot exists — and run may fail with error.Busy, which leaves the task at the queue's head for the next gap.
  • core/lane_host_decide.zig: head returns the queue's front when the engine is idle as before, or when a slot is free and no queued request is waiting for that turn (a decision takes the same turn a new stream would — requests keep priority). waiting reports a decision a free slot could start now, so the lone driver hands its stream to the lane core rather than hold the engine through the whole reply.
  • native/qwen27_served.zig: decideRun may return error.Busy, and the host wires the decideReady gate into decider.ready.
  • core/lane_host.zig / lane_rounds.zig: comment lines follow the wider rule (no code change).
  • native/qwen27_decide.zig: ready is b.freeSlot() != null; bind admits the synthetic stream the way a new stream's admission binds a slot — buffers grown where needed, held drafts dropped, runner state reset — and error.Busy covers "no slot" and "slot's buffers do not fit yet". unbind frees it on the caller's defer.
  • native/qwen27_lanes.zig / qwen27_slots.zig / lane_host_lone.zig / lane_rounds.zig: the slot accounting and the lone-driver handoff.
  • core/lane_host_decide_test.zig: coverage for the free-slot path, the busy retry, and request priority.

Checks

  • zig build native -Dcpu=apple_m1 -Dversion=1.0.0 passes clean on the head.
  • zig build test test-golden -Dcpu=apple_m1: test-golden 0 unexpected differences (306/319 + 26/26 equal). The test step's device-dependent binaries fail on this machine while its GPU is serving a model — the same test binaries fail identically on the base head 30e5dcb (run for comparison), so the failure is inherited, not introduced by this diff.
  • tools/zig/lean_check.py: 444 findings, as on the base (zero added).
  • Same-trunk branch semantics are unchanged: each branch still scores on the shared trunk's kept state, now read through the bound slot's rows.

Receipts (Apple M3 Ultra, 256 GB, macOS 26.4.1, zig 0.17.0; local Ornith-1.5-122B A1-affine 4-bit + DFlash2 drafter)

  • Decision latency under load: ~12 s at 4-way chat on the idle-path serve (qwen27: score and decide passes queued onto the lane host's idle engine thread #1189's B3 receipt) → ~0.5 s solo / ~0.9 s under load on the production serve with this path (the work's measured profile at adopted tip 8e8a2d4a; trunk-prefill-bound — the head's read is 0.24 ms).
  • Live chat traffic is unaffected: the synthetic stream draws no tokens and requests keep admission priority — a queued request's turn comes first.

Not run

As #1189: no new GPU jobs for this cut. The wall-time figure is from the recorded production serve; a CONTRIBUTING-style bench_concurrent receipt on the exact head would be produced at file time if wanted.

BobClawblaw and others added 21 commits October 20, 2026 12:36
… 32-over-2 attention

WIP: the text path matches an fp32-routing reference at every depth on the 4-bit checkpoint
(KL 0.0023 at 48 layers; decode top-1 98-100%); host registration, DFlash2 and images follow.
…routed block (route, experts) and TF_PROFILE_PROMPT
…d five of Ornith's eight); selector options by env
…erts' kernel (core: indexed_extra row kernel)
… the reference config and the CUDA family read it
…ne thread

Ornith serves /v1/decisions and /v1/systemone while ordinary text submits keep
running: info.decides stays off and the hooks appear only when a decider is set.

A lane host's runner is owned by its engine thread, so HTTP threads cannot run
the Metal passes themselves. engine_api gains a DecisionTask/Decider pair; the
host queues a Work per score/decide call, the round and generic driver loops run
the oldest once no stream is admitted, filling or active, and shutdown or a
stale device fails every waiter instead of sleeping past the thread's end.

qwen27's passes run one prompt each on the existing LM head path: score reads
the labels' logits at the row after its prompt plus the vocabulary logsumexp,
and decide keeps the trunk's slot state once, restoring it per branch so every
branch's last-row label logits stand on the same trunk. Pictures and requests
past the context window are refused.
…s a new stream's prompt pass would

A score/decide call no longer waits for an idle engine: the decider's
`ready` hook says when a slot is free, and the queued pass runs beside the
live rounds — its prompt prefilled in the backend's usual chunks, its
label rows read, its slot unbound without a drawn token. A queued
request a lane is free for still goes first; a slot that will not fit yet
leaves the pass queued (error.Busy) for the next gap. An idle engine
runs a decision exactly as before, and a decider without `ready` keeps
the idle-only rule.

qwen27 binds a synthetic stream (id "decision", max_new 0, drafts off)
to a free slot for the pass's length — the same bind an admission makes:
buffers grown where needed, held drafts dropped, runner slot reset,
drafter context cleared — and `unbind` frees it, promoting a live stream
into slot 0 when the pass held the drafter's. The pass never overlaps a
planned round, so draft-slot ownership and acceptance are untouched.

@ashhart ashhart left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks @grantoverton. This sits on the unmerged #1125 stack and on #1189, which was sent back, and it conflicts in 9 files on current main. It keeps #1189's second decision queue (decisions/decided) instead of main's lane_host_score.zig, sets the decider on every qwen27 host (so /v1/decisions and /v1/systemone answer by default), and its wait has no cancel or timeout. Here also the lone driver now yields whenever a decision is queued (changing default scheduling), and `unbind` moves a live stream into slot 0 (the drafter's slot) mid-reply. Once #1189 is reworked on main's score queue as an opt-in, please rebase this onto it; we'd then check that drafted equals plain and concurrent equals solo with a decision in flight.

@ashhart

ashhart commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Review is on #1189, covering the whole stack #1189–#1215.

@ashhart

ashhart commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Thanks @grantoverton for the free-slot decision lane. We're closing this with #1189, as our 26 Oct review hasn't had a reply. A version that steps decisions one max_rows piece at a time through main's score queue would be very welcome as a fresh PR.

@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.

3 participants