Skip to content

qwen27: score and decide passes queued onto the lane host's idle engine thread - #1189

Closed
grantoverton wants to merge 16 commits into
ashhart:mainfrom
grantoverton:ornith-pr1-decide-queue
Closed

grantoverton wants to merge 16 commits into
ashhart:mainfrom
grantoverton:ornith-pr1-decide-queue

Conversation

@grantoverton

@grantoverton grantoverton commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1190.

Stacked on #1125 (the qwen27 Metal family); base for review: 30e5dcb, the #1125 head. Review the last two commits: 146a658 (the queue and the pass) and 227b961 (lean-check comments).

The qwen27 Metal lane engines take decision work beside text generation: an HTTP thread enqueues a score or decide task and the lane host's own engine thread runs the pass when the engine is idle. One resident model serves both streams.

What changes

  • core/engine_api.zig: DecisionTask (.score / .decide), Decision (trunk + branches + counts + label/logit spans), and a Decider vtable entry on the lane host's backend (run only; ready arrives in the follow-up).
  • core/lane_host_decide.zig (new): the host-side queue. Callers append a task and wait on h.decided; step takes the queue's head only when the engine is idle (no admitted or filling streams, nothing active on the lane core) and runs it on the engine thread.
  • core/lane_host.zig / lane_rounds.zig: step is driven from the host's idle points; a stale host fails the waiters instead of hanging them.
  • native/qwen27_decide.zig (new): the 27B lane backend's pass. A score prefills the prompt and reads labels' logits at the row after it (a prefill's .last head leaves them in logits row 0). A decide prefills the shared trunk once, keeps its state, and restores it a branch at a time so every branch's rows stand on the same trunk state.
  • native/qwen27_served.zig: the served engine registers the decider.
  • core/lane_host_decide_test.zig (new): queue/wait/step/fail coverage (3 tests).

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).
  • The decision pass does not touch the drafted path: drafted replies stay identical to undrafted ones (the family gate from qwen27: serve Qwen3.5-MoE (Ornith-1.5-122B) on Metal: routed experts, its DFlash2 drafter and the publisher's 4-bit weights #1125 is unaffected — this PR adds no scheduler beside the engine; the host's own thread owns the work).

Receipts (serving validation, receipts/DECIDE-RECEIPTS.md in the work's record; Apple M3 Ultra, 256 GB, macOS 26.4.1, zig 0.17.0; local Ornith-1.5-122B A1-affine 4-bit checkpoint + local DFlash2 drafter, temperature 1.0, no-think; recorded at feature-branch 98a0ce4f, which this stack replays with comment-only changes)

  • Decisions return 200 with full answers while chats stream (4-way and 8-way loads, prompt_tokens=270 each).
  • A decision landing mid-drafted-round does not corrupt the emitted token stream: a deterministic constrained prompt produces one token sha across solo / during / after runs.

Known limits (deliberate)

  • Decisions serialize behind live generation — they run only at idle points (~12 s wait at 4-way load). The lane runner is single-owner by design; taking a free slot beside the streams is the follow-up PR in this stack.
  • A long-trunk decision re-prefills the trunk per question; trunk KV reuse across questions is future work.

Not run

No GPU jobs were run for this PR cut: the numbers above are cited from the recorded serving validation named in Receipts. CONTRIBUTING's performance receipts (bench_openai / bench_concurrent / cold prefill tables) are unchanged from #1125's run — this PR adds no decode-path work; they would be re-run at file time if the maintainer wants them on this exact head.

BobClawblaw and others added 16 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.

@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. Your two commits don't need the #1125 Ornith stack, so we rebased just them onto current main (one conflict in lane_host.zig, plus main's new spares argument to saveSlot). Host suites pass, including the three decide tests. Main has since landed its own decision-score queue (lane_host_score.zig), so:

  1. Please build on that queue instead of adding a second one (decisions/decided): give the qwen27 lanes backend a score entry (as cuda_bound.zig does) and add decide to lane_host_score.zig.
  2. The decider is set for every qwen27 host (qwen27_served.zig), so default Qwen3.8-27B serving starts answering /v1/systemone and /v1/decisions instead of 404. New routes need to be opt-in.
  3. lane_host_decide.zig waits with no cancel or timeout, so a client that disconnects can't abandon a decision, and decisions can wait indefinitely under load.
  4. Smaller: settle() errors are dropped in qwen27_decide.zig, and score checks r.capacity while decide checks window.

Once reworked, we'll check on a Mac that text tokens are unchanged, each branch's label logits equal a score of trunk+branch bit for bit, streams during a decision equal their solo runs, and the next reply after a decision equals a fresh server's.

@ashhart

ashhart commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Review of the qwen27 decision stack (#1189 → #1191 → #1192 → #1215, tracking #1190) against main 3f408b6. It's about 860 commits behind but #1189's own two commits apply cleanly, and host checks pass on the rebase (test-lane-api-host 300/300 including the decide tests, native build, fmt, lean). The open points from the earlier review still stand, and there's a newer piece of context: gilby's #1361 (Clef) adds a hidden call that returns a prompt's final rows, and we've asked it to admit decision prompts into the shared rounds rather than running them one after another. It would be best if this stack and #1361 converge on one shared decision path in core:

@ashhart

ashhart commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Thanks @grantoverton for the decision queue. We're closing this for now, as our 26 Oct review hasn't had a reply. Please do reopen it, or send a fresh PR, once it builds on main's score queue as an opt-in with cancel and timeout, ideally converging with #1361's shared decision path.

@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