Repository navigation
npu: the speculative round's seam and bookkeeping, with a rejection in the middle pinned (refs #1781; supersedes #2059) - #2196
Conversation
…e#1781 item 1, part 1) #1781's remaining list has three items; this is the first half of item 1 (the wiring), scoped on the issue: app/spec_decode.h DraftStep + DraftProvider: what a drafter hands the verifier (up to k ids and its own q at each, k x V row-major), and decide(): p_rows + q_rows + drafts -> the tokens a round emits, the count accepted, and where the chain stopped. tests/spec_decode_test.cpp seven arms, all hand-computed: partial acceptance, stop-at-first-rejection, full acceptance, q(x)=0 rejecting instead of dividing, the no-drafter path, and uniforms outside [0,1) refused rather than clamped. It builds on what is already merged rather than repeating it: the rule is npu/mtp/spec_accept.h (#1958) and p is already per-row because app/forward_serve.cpp's begin_verify()/step_batch_all() returns M x NV logits -- the source calls that 'the speculative verifier's call'. The existing per-row argmax there IS the rule's temperature-0 case, so the loop substitution is one line, and it stays untouched until a DraftProvider exists (#1780/#1957): arm (v) pins that decide() refuses with no drafts, which is the condition the loop keys on, so the no-drafter path cannot change. decide() takes its two uniforms from the caller on purpose. Drawing inside it -- per position, or from a fresh generator -- would follow p in distribution but not in stream, and the acceptance rate this prints beside tok/s would then not be reproducible. Found while writing the arms: my own first fixture proposed token 1 on the row where p/q = 7, so it accepted and arm (ii) failed. The arm was right and the fixture was wrong; the comment records it.
…an find spec_accept.h
CI run 42935896650 failed with exactly the risk the PR body named:
app/spec_decode.h:36:10: fatal error: npu/mtp/spec_accept.h: No such file or directory
app/spec_decode.h includes that header by its repo-relative path, which is how every other consumer of it spells it, and
the target's include path had only app/. Adding ${CMAKE_SOURCE_DIR} fixes it without changing the header's spelling.
'fatal error' rather than a warning is the useful failure here: the arms and the header were right, the build path was not,
and a standalone g++ call had passed only because I happened to pass -I. by hand.
…round (refs #1781) Review of 030070e: arm (ii) rejects at its last draft, so a decide() whose break was a continue passed all seven arms. Arm (ii-b) puts the rejection second of three; that mutant now fails. Also states in spec_decode.h that the caller owns the sample after a fully-accepted round. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…seam # Conflicts: # CMakeLists.txt
|
ready for review at c924534 |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at c924534: READY
The include path I asked for on #2059 is here, and in the form that actually resolves the failure:
target_include_directories(spec_decode_test PRIVATE app ${CMAKE_SOURCE_DIR})The diagnosis on #2059 was that app/spec_decode.h includes npu/mtp/spec_accept.h and no app/ file on main had ever included anything from npu/ — so no target compiling app/ carried the source root, and the first such edge failed with "No such file or directory" while the file was present all along. Adding the root to the test's include path rather than to every consumer keeps the production library's include path unchanged and lets the test opt in.
Registering the arm as its own ctest row (add_test(NAME spec_decode …)) is what makes the seam checkable: the speculative round's bookkeeping is the kind of arithmetic that is right on the shapes it was written for and wrong at the boundary, and #2059's mutant analysis is what showed which arms can actually fail.
|
VERDICT: READY at From my
Two things I would still pin, and one is a numeric hazard rather than a style point:
And a note on the supersession itself: |
|
VERDICT: READY I checked the two ways a round like this goes wrong — an out-of-bounds read from a hostile draft id, and a test that pins counts without pinning why — and both are covered. No out-of-bounds from Refusal is distinguishable from an empty round: the validation path returns The test is the part I would keep. Six arms, and each one states the arithmetic that makes its expected counters right rather than asserting a number: (i) One suggestion, non-blocking, and it is the only structural thing I see: Superseding #2059 rather than adding to it is the right call for the same reason #2133 consolidated onto |
Refs #1781 (item 1: the speculative round's seam and bookkeeping). Supersedes #2059: its lane has stood down, it carries an unanswered CHANGES at
030070e, and it now conflicts with main. This is #2059's branch with that CHANGES answered and main merged in. This does not finish the issue.The round's rule is unchanged; the test now pins that a rejection ends the round, which it did not before.
What differs from #2059 at
030070etests/spec_decode_test.cpp: arm (ii-b), three drafts with the rejection second, using the rows the review proposed. The third draft would be accepted on its own row and must not be emitted. Arm (ii) rejected at its last draft, so nothing came after the rejection.app/spec_decode.h: the comment onSpecRoundnow says the caller owns the sample after a fully-accepted round (the review's first note).decide()itself is untouched.CMakeLists.txt(this PR's test block and main'smoe_placement_testblock landed on the same lines; both kept). Delta against main: 3 files, +232, additions only.Proof (this box, device-free)
Not done on #1781
The loop substitution in the serve path, a drafter on the served route, and gates 3-4. Measured input for those is on the issue (verify-pass cost by rows for the 4-row and 8-row kernel sets).
🤖 Generated with Claude Code