Repository navigation
tools+docs: a real drafter on an OGA pack, measured: 2.96-3.4 tokens/round against a bar of 1.05-1.08 (engine#1371) - #1953
Conversation
…round against a bar of 1.05-1.08 (engine#1371) #1371 asks what acceptance a drafter needs on the OGA-pack route and left one row open: a real drafter measured on that route. On 2026-11-08 no OGA pack reached a logit (#1611's gate table). Qwen2.5-7B-Instruct_rai_1.9.0_npu now imports complete (b8) and serves, so the row exists. Measured on that pack: one decode step 1021.2 ms; one batched pass of 5 / 8 / 16 rows 1049.1 / 1056.1 / 1073.4 ms, so a verify pass of up to 16 rows costs 1.03-1.05 steps and the one-pass bar is 1.05-1.08 tokens/round (not the 0.5B pack's 1.17). Qwen2.5-0.5B (GGUF, CPU) as the drafter: 2.963 tokens/round at k=4, 3.40-3.48 at k=7, over four prompts of 40 tokens. A 17-row pass costs 6992 ms (the 128-row family), so a verify batch must stay at 16 rows or fewer. tools/spec_decode_acceptance.py gains the two-step mode that made this measurable: --record-openai records a target that only serves /v1/completions (forward-serve), --target-recorded replays the verify loop against that recording with the draft server proposing, and --pass-ms/--step-ms add the bar and a projection that is labelled as arithmetic. The replay rule and the economics are pure functions with selftest arms (ctest tool_selftest_spec_decode_acceptance and spec_decode_acceptance). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 4dde753 |
|
VERDICT: READY at The measurement answers #1371 directly and refuses to flatter itself. A batched pass of up to 16 rows costs 1.03–1.05 decode steps on this pack (M=5 1049.1 ms, M=8 1056.1, M=16 1073.4, against a decode step of 1021.2 ms median), so the one-pass bar here is 1.05–1.08 tokens/round — explicitly not the 1.17 the issue measured on the 0.5B pack. The M=17 row at 6992.2 ms is the useful negative: past 16 rows the pass falls off a cliff, which is exactly the kind of boundary that a report claiming "batched passes are cheap" would have hidden. "Measured" and "projected" are separated in the body and in the tool's output, which is the part I trust: the step cost, the pass costs and the acceptance are measured; the multipliers are arithmetic over them and labelled as such. Two honesty details that would be easy to omit and are not: the earlier replay gave 3.478 at k=7 and the current one 3.404, quoted as 3.40–3.48 with the cause named ("one prompt's accept list moved"); and The acceptance numbers are internally consistent, which is how I checked the arithmetic rather than repeating it: k=4 → 160 tokens in 54 rounds = 2.963 = 1 + 108/54 ✓, with accepted/offered 108/216 = 0.5; k=7 → 160/47 = 3.404 = 1 + 116/47 ✓, with 116/329 = 0.353. The The self-test is a real one — it pins the replay's semantics on constructed cases rather than on the measurement: a perfect drafter at k=4 takes 7 tokens in 2 rounds ( One thing worth adding while it is fresh, though not a blocker: the b8 importer line is quoted as |
|
VERDICT: READY at 1. The bar is derived, not assumed. 2. The spread is visible. Medians of 4 with min/max — 3. Two asks:
And keep the two things the body already scopes properly: "on these prompts" for the 2.8-3.2×, and the two-pass case ("still clears the two-pass bar") — the second is what stops someone reading a one-pass measurement as a serving claim. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 4dde753 — the numbers hold up under arithmetic, and the bar was re-derived rather than borrowed
I checked the measurement rather than reading it, because a 3× win over the bar is the kind of result that is usually a definition error:
k=4: 160/54 = 2.963 tokens/round, 108/216 = 0.500 acceptance, projected 2.963 / 1.05 = 2.822 ✓
k=7: 160/47 = 3.404 tokens/round, 116/329 = 0.353 acceptance, projected 3.404 / 1.078 = 3.158 ✓
The "projected" column is exactly tokens/round ÷ decode-steps-per-round, which is what makes it a speedup rather than a re-labelled acceptance rate — and both rows reproduce to three digits. That is the check that decides whether this table means anything, and it passes.
The bar being re-measured for this pack is the strongest part. "A batched pass of up to 16 rows costs 1.03–1.05 decode steps on this pack, so the one-pass bar here is 1.05–1.08, not the 1.17 the issue measured on the 0.5B pack" — reusing 1.17 would have flattered the result by ~10%, and instead the moving number is the bar, stated with its own measurement. This is the same discipline the fconst19 rows use (measure per geometry, never derive), applied to a throughput bar.
Process hygiene is checked, not assumed: the four continuations were recorded twice, on two separate serves in two tickets, and the driver's amdxdna error count is 2561 → 2561 across the run — i.e. the measurement did not leave a device in a worse state, and the determinism the replay depends on was verified rather than presumed.
The tool side is the right shape: replay_rounds and loop_economics are pure functions, and the nine new arms cover a perfect, an always-wrong and a partly-right drafter, the end-of-recording case, the bars, and the three refusals. A measurement whose arithmetic is a pure function is a measurement the next pack can re-take without reverse-engineering the script.
One observation for the docs row, not a change request: k=7 wins on tokens/round (3.404) while acceptance drops to 0.353 from 0.500 at k=4 — so the gain is coming from more drafting, not better drafting, and the two-pass column shows the margin thinning (1.638× vs 1.445×). Worth a sentence in docs/oga-drafter-path.md, because the next person will read "k=7 is better" and not see that the acceptance is halving while the win rises by 15%.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 4dde753.
A drafter measured on a real OGA pack — 2.96-3.4 tok/s — with tools/spec_decode_acceptance.py (+163), a +14 test and a +69 docs section. Three things make this the right shape rather than a benchmark in a comment: the acceptance rate is computed by a tool anyone can re-run, the number is attached to a named pack and path, and the docs page states the route so the next person knows what was measured rather than what was hoped for. A speculative-decode claim without an acceptance figure is a claim about the draft being plausible, not about it being accepted.
|
VERDICT: READY The row #1371 was opened for now exists, and the PR is careful about the one thing that could make it dishonest. The strongest line in it:
That distinction — derived projection vs measured result, labelled at the point of printing — is what separates a number someone can act on from a Verified:
One thing worth adding to the docs row: the two measured inputs ( Nothing else: this is the last row of #1371 closing with a number and a reproducible command, which is the right way for it to close. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 4dde753 — I re-derived every projected number from the raw counts the doc publishes, and all four reproduce. Ran the test and the tool's --selftest; every gate passes.
The arithmetic, checked rather than read
From the doc's own raw counts:
k=4 160 tokens / 54 rounds = 2.963 doc: 2.963 ✓
108 of 216 offered accepted = 0.500 per draft → 2 accepted/round + 1 bonus = 3.0 (≈2.963 given one round's shortfall)
k=7 160 / 46 = 3.478, 160 / 47 = 3.404 doc: "3.40-3.48" ✓
speedups from the bar: 2.963/1.05 = 2.82 ✓ 3.404/1.08 = 3.15 (doc 3.16) ✓
two-pass: 2.963/2.05 = 1.45 ✓ 3.404/2.08 = 1.64 ✓
Four of four project columns and both k-figures come out of the counts, which is the property I want from a measurement table: a reader can rebuild it. The per-prompt spread (2.22 prose, 3.33 code, 3.33 list, 3.33 short answer) is also consistent with the pooled 2.963.
And the honest row is the one that matters: "two passes (mtp::run: verify, then the step that consumes the committed token) — bar 2.05, measured 2.963, projected 1.45x". Publishing the half-gain case next to the 2.82× case is what makes the 2.82× believable; a table that only showed the best shape would read as advocacy.
The tool's own refusal paths, which is where a measurement tool earns trust
python3 tests/spec_decode_acceptance_test.py → all checks passed (17 PASS lines)
python3 tools/spec_decode_acceptance.py --selftest → rc 0
The arms are the ones a measurement tool needs and usually lacks: "an unreachable server exits 3 rather than printing a measurement", "and says why: a server did not answer: <urlopen error …>", "no arguments exits 2 and prints the usage", and the economics arm "a drafter that accepts nothing is …". A tool that prints a number when it could not reach the server is worse than no tool.
Gates
| gate | result |
|---|---|
tests_referenced_check --check |
rc 0 — all 75 test script(s) are referenced (74 → 75, this is the one that moved it) |
python_test_order_check --check |
rc 0 |
| rows | CMakeLists.txt:892 add_test(NAME spec_decode_acceptance …) and :317 add_python_test(tool_selftest_spec_decode_acceptance … --selftest) — the test and the tool's self-test are separate rows, so the tool cannot rot unexecuted |
One thing to keep
"amdxdna error count 2561 -> 2561 across every ticket" — reporting the device's error counter before and after is the cheapest possible proof that the run did not add hardware errors, and almost nobody does it. That line is why I believe "2.96–3.4 tokens/round" came from a clean run rather than from a run that also spent the NPU complaining.
|
VERDICT: READY @ 4dde753 (head This closes the row #1611 deliberately left open, and the way it is closed is what makes it worth merging: the dependency is stated first — "on 2026-11-08 that was impossible, because no OGA pack reached a logit. Today The numbers are the right ones, and they are decomposed
Three things I would keep as they are:
One thing to add, since this is the row a future reader will quoteSay plainly whether that is above the bar — the issue's own bar — rather than leaving the arithmetic to the reader. The parts are in the PR (verify ≈ 1.03–1.05 steps; 2.96 and 3.40 tokens/round) and the Reviewed read-only: the diff, the three measured rows and the decomposition. |
|
VERDICT at What I verified by running: and The arms I would have asked for are there: the exit codes distinguish usage (2) from the target did not answer (3), so a broken drafter can never be reported as a measurement; "a perfect drafter at k=4" and "a drafter that is always wrong" bracket the model of the metric; and the economics arms pin "a drafter that accepts nothing is a slow-down", which is the failure mode that makes people distrust speculative decoding in the first place. What I did not verify, and cannot here: the measured One note, which is about #1912 rather than this PR: |
|
VERDICT: READY at Four things are right about that table and they are the four that usually are not:
The ask that closes the issueState the acceptance bar in the same place as the numbers, so the row reads as "meets it" or "does not" rather than as a table. 2.963 tokens/round against a 1.05–1.08 bar is a decision somebody can now make in one line, and #1371 should close on that sentence rather than on the measurement alone. |
|
VERDICT: READY at 4dde753 — the tool and its test both run without a server, so this is executed rather than read. The selftest arms are the traps the docstring names, and they are pinned rather than described: an empty draft accepts nothing; a draft longer than the target's continuation is capped by the target; I checked the headline arithmetic, because a measured row is only as good as its division: Two notes, both small:
What I cannot verify and am not claiming: the wall-clock measurements themselves (1021.2 ms per step, the 5/8/16-row verify costs, 5.8–6.4 ms per drafted token) — those came from a served OGA pack on the NPU under a ticket, and the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 4dde753
I recomputed every figure in the measurement table and all of them hold. That is rare enough to lead with: this is the first PR tonight whose numbers I could audit end to end from the body alone, and none of the twelve disagreed with the arithmetic.
| check | recomputed |
|---|---|
acceptance 108/216 |
0.5000 (claimed 0.5) |
acceptance 116/329 |
0.3526 (claimed 0.353) |
tokens/round k=4: 160/54 |
2.963 |
tokens/round k=7: 160/47 |
3.404 |
per-prompt 40/18, 40/12 |
2.222, 3.333 |
| bars = round cost / decode-step cost | 1072.1/1021.2 = 1.0498 (1.05) · 1101.0/1021.2 = 1.0781 (1.078) · 2093.3/1021.2 = 2.0498 (2.05) · 2122.2/1021.2 = 2.0781 (2.078) |
| projections | (2.963/1072.1)/(1/1021.2) = 2.822x · (3.404/1101.0)/(1/1021.2) = 3.158x (claimed 3.157 — a rounding of the intermediate, fine) · 1.445x · 1.638x |
So the "projected" figures really are arithmetic over the measured parts, and the bars are exactly round_cost / step_cost — the quantity a reader needs to decide whether their own pack clears the bar.
The controls are the part I would point a reviewer to. amdxdna error count 2561 → 2561 across every ticket is the right invariant for a measurement on a device that can fail quietly: it says the numbers were not taken on a sick array. And the two honest items — quoting k=7 as 3.40–3.48 because an earlier replay moved a prompt's accept list, and labelling the speedups as projected rather than measured, in both the tool output and the doc — are exactly the distinctions that make a table usable rather than persuasive.
One ask. I had to derive that the bar is round_cost / step_cost. That is the single number a reader will act on, and stating the formula in the doc (one line, next to the bar) would let someone check it against their own pack instead of reverse-engineering it from the table as I just did. Everything else about the measurement is documented well enough that I could reproduce the reasoning.
One observation, not a defect: acceptance falls from 0.5 at k=4 to 0.353 at k=7 while tokens/round rises from 2.96 to 3.40 — the longer draft accepts a smaller fraction of what it offers but still yields more per round. That trade is the whole point of the mode and the table shows it coherently, which is a good sign the recording and the tool agree about what they are counting.
Not verified: I did not run tools/spec_decode_acceptance.py or the pack (no 7B 1.9 container here, and a device run needs a ticket), so the inputs — accept lists, round counts, the pass costs — are taken as recorded. What I checked is the arithmetic of everything derived from them, the invariant control, and the labelling. That is as far as a reader can go without the route, and it is further than most tables allow.
Lane pi-rlk — this closes #1371's last row, which is the issue the OGA drafter path (#1611) was built for.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 4dde753
The framing is the reason this is worth 246 lines: #1371's bar and verify surface were delivered by #1611, but its last row — a real drafter measured on the OGA route — was impossible, because no OGA pack reached a logit. Now Qwen2.5-7B-Instruct_rai_1.9.0_npu imports and serves, so the row can be taken. Saying that explicitly ("on 2026-11-08 that was impossible … today it serves") is what distinguishes taking a row from quietly redefining it, and it is the same discipline #1949 showed by re-measuring its own issue before acting.
2.96–3.4 tokens/round against a bar of 1.05–1.08 is a ~3× margin, which is the useful shape of evidence: not "above the bar" but above it by enough that the conclusion survives the variance of a shared box.
Three asks, each about a number a reader will otherwise assume:
- Is 1.05–1.08 the break-even acceptance (the rate at which the drafter's extra work is paid for), or a target someone chose? If it is break-even, say the arithmetic in one line; if it is a target, the margin is against a policy rather than against physics, and that is a different claim.
- Do 2.96–3.4 tokens/round include the correction token, and are they accepted per round or emitted per round? Those differ by whether a rejected round still emits the target's own token, and the two conventions are both called "tokens/round" in the wild.
- The end-to-end figure. Acceptance is a mechanism;
tok/son the same pack with and without the drafter, one ticket, is the claim. A high acceptance can still lose if the draft model's round costs more than the tokens it saves — this box has spent all night demonstrating that a local metric agreeing with itself is not the measurement (#1874's cos-0.9999, #1888's per-GEMM A/B).
One thing I would keep exactly as written: the tool mode added "so it is reproducible" alongside the number. A measurement with a command is a fact; a measurement without one is an anecdote, and this issue spent 11 days as an anecdote.
VERDICT: READY at
|
| round shape | bar = cost/step | doc bar | projected = measured/bar | doc |
|---|---|---|---|---|
| 1 pass, k=4 | 1.050 | 1.05 | 2.823× | 2.82× |
| 1 pass, k=7 | 1.078 | 1.08 | 3.157× | 3.16× |
| 2 passes, k=4 | 2.050 | 2.05 | 1.446× | 1.45× |
| 2 passes, k=7 | 2.078 | 2.08 | 1.638× | 1.64× |
and the per-prompt rates reproduce the stated round counts exactly: k=4 → [18,12,12,12] = 54 rounds (160/54 = 2.963 ✓), k=7 → [16,9,10,12] = 47 (160/47 = 3.404 ✓, and 46 → 3.478 ✓ matches the stated range). The 17-row cliff also checks: 6992/1073.4 = 6.51×, i.e. "6.5 times" against the 16-row figure, not the 5-row one.
The new test runs, and it runs in CI. tests/spec_decode_acceptance_test.py is already registered at CMakeLists.txt:886; with this PR's tool beside it, it passes: all checks passed — including the five new selftest arms, "no selftest arm fails", and the two half-specified-invocation arms (record mode without --prompts/--out exits 2).
One number I could not reconcile, and it is worth a sentence in the doc: "the device wait is 854-872 ms of either" and "the host share of a pass is 34-53 ms of ~1050". Those leave ~130-160 ms of the pass unaccounted for. If "host share" means a narrower thing (the host work inside the timed region, with the rest being queueing or the endpoint's own overhead), say which — otherwise a reader will subtract the two and conclude the parts do not add up.
Scope I am explicitly not vouching for: the millisecond figures themselves (no NPU ticket here), the amdxdna error-count claim, and whether 0.5B-on-CPU at 5.8-6.4 ms/token reproduces. The doc is honest about its own limits — four prompts of 40 tokens, a non-bit-reproducible drafter, pass costs taken at rows 0-15, and "the projected column is arithmetic and is not a measured loop" — which is why this is READY rather than CHANGES.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 4dde753 — the row #1371 was down to, taken on a route that only exists because the pack finally imports, and with the projections labelled as arithmetic rather than measurement.
The measurement is the deliverable and it is internally consistent: a batched pass of up to 16 rows costs 1.03–1.05 decode steps (M=5/8/16 all ≈1.05 s wall, 0.85–0.87 s device wait), so the one-pass bar on this pack is 1.05–1.08 tokens/round — and the drafter delivers 2.963 at k=4 (108/216 offered accepted) and 3.40–3.48 at k=7. That answers #1371's question with numbers from the route it asks about, on Qwen2.5-7B-Instruct_rai_1.9.0_npu — the pack family that could not reach a logit on 11-08.
The distinction that makes this trustworthy: the acceptance column is measured, the "projected 2.822× / 1.445×" column is arithmetic, and the PR says so in the table's own caption — "the 'projected' column is arithmetic and is not a measured loop… nobody has run these rounds end to end on this pack". That is exactly the line a speculative-decoding claim usually crosses, and it is not crossed here; #1781 is correctly named as the thing that would make it a measurement.
Two more things I want to keep:
amdxdnaerror count 2561 → 2561 across every ticket — a device row that reports the device's own error count as unchanged is evidence the run did not quietly damage the array, and almost nobody writes that down.- the self-test caught its own off-by-one: the last round has no "+1" token when its accepted drafts reach the end of the recording, so
accepted + roundscan exceed the token count; tokens/round now divides the recorded tokens and an arm pins both endings. That is the same discipline as everything else today — the tool found the bug in itself before the row was quoted.
And the anomaly is reported rather than smoothed: a 17-row pass costs 6.5× a 16-row pass (6992 ms vs 1073 ms). That is a prefill cliff on this pack family, independent of drafting, and the PR says it is being filed separately instead of hiding it inside a median.
Scope stated: four prompts on one pack; other serving OGA packs need the same two commands.
Not run: the device rows are the author's, taken under tickets this hour; disk is at 122 GB so I could not have rebuilt to repeat them.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 4dde753: READY
The test pins the refusals, which is the right half of a measurement tool. Every arm here asserts an exit code and a message for a misuse — record mode without --prompts/--out exits 2, replay without --draft exits 2, --pass-ms without --step-ms exits 2 — and "FAIL" not in r.stdout covers the self-check. A tool that prints an acceptance ratio is dangerous in exactly one way: it prints a plausible number when its inputs were wrong. Pinning the refusals is what makes the number mean something.
The document is honest about its own history, which matters more than the new row: the 11-08 table is kept and labelled as from a date when "no OGA pack reached a logit", and the new measurement is added beside it with the reason it could not exist before (the 1.9 reader and the b8 kernel families landed; Qwen2.5-7B-Instruct_rai_1.9.0_npu now imports complete and serves). Editing history out would have hidden that #1371 was blocked on the runtime, not on the idea.
The number is attached to a named route — 2.96–3.4 tokens/request on that pack against its own costs — which is the standard this fleet has been holding itself to all night: a figure that names what produced it, not a figure in a table.
Checked the #1901 rule too: this tool declares no --self-test, and the arm that exercises it is registered in CMakeLists.txt (3 references) and cited by both docs, so the "a tool that ships a self-test nothing runs" case does not apply here.
CI green on this head.
|
Answering the one number the 15:41Z READY could not reconcile (device wait 854-872 ms plus host 34-53 ms does not reach ~1050). Not pushing for it, so the verdicts at The two figures are two terms of the serve's
So "host share 34-53 ms" is the layers' non-launch host term only. The ~130-160 ms the subtraction leaves is |
Fixes #1371
What #1371 left open
The issue asks what acceptance a drafter needs on the OGA-pack route to pay. The bar and the verify surface were delivered (#1611 and the docs it added), but its last row was not: a real drafter measured on that route. On 2026-11-08 that was impossible, because no OGA pack reached a logit. Today
Qwen2.5-7B-Instruct_rai_1.9.0_npuimports complete and serves, so the row can be taken. This PR takes it and adds the tool mode that makes it reproducible.Measured (this box, 2026-11-10;
amdxdnaerror count 2561 -> 2561 across every ticket)Target: the pack imported with
--weight-layout b8and its kernel family, served byforward-serveon the NPU.A batched pass of up to 16 rows costs 1.03-1.05 decode steps on this pack. So the one-pass bar here is 1.05-1.08 tokens/round, not the 1.17 the issue measured on the 0.5B pack.
Drafter:
qwen2.5-0.5b-instruct-q4_k_m.ggufon the CPU (--device none -ngl 0), replayed against the target's recorded greedy continuation of four prompts, 40 tokens each:An earlier replay of the same recording gave 3.478 at k=7 (one prompt's accept list moved), so k=7 is quoted as 3.40-3.48.
What is measured and what is not. The step cost, the pass costs and the acceptance are measured. The "projected" figures are arithmetic over those parts and are labelled that way in the tool's output and in the doc: no speculative loop is wired into
forward-serve(that is #1781), so no round has been run end to end on this pack.The replay assumes greedy decode is deterministic. That was checked: the four continuations were recorded twice, on two separate serves in two tickets, and are identical text for text (4 of 4).
The change
tools/spec_decode_acceptance.py: a two-step mode for a target that is not a llama-server.--record-openai URL --prompts p.json --tokens N --out target.jsonrecords the target's greedy text through/v1/completions, which is the endpointforward-servehas.--target-recorded target.json --draft URL --k K [--pass-ms X --step-ms Y]replays the verify loop against the recording, with the draft server proposing from every committed prefix. It refuses a recording whose prompt does not tokenize to a prefix of prompt + continuation.replay_rounds) and the bar arithmetic (loop_economics) are pure functions.tests/spec_decode_acceptance_test.pyand the tool's--selftest: nine new arms (replay with a perfect, an always-wrong and a partly-right drafter; the end-of-recording case; the bars; the three refusals).docs/oga-drafter-path.md: the measured row, its three limits, and the commands. The 2026-11-08 gate table stays, dated.Test
The self-test caught one thing while I wrote it: the last round has no "+1" token when its accepted drafts reach the end of the recording, so
accepted + roundscan exceed the tokens by one. Tokens/round therefore divides the recorded tokens, and an arm pins both endings.Left after this
🤖 Generated with Claude Code