Repository navigation
npu: the DFlash draft block's acceptance arithmetic, with the measured counts as known answers (refs #1780) - #2053
Conversation
…d counts as known answers (refs #1780) A speculative round offers k drafted positions behind one verify pass and yields the tokens the target accepted plus the one the pass commits past them, so tokens = accepted + bonus and tokens/round = (accepted + bonus)/rounds. The round costs cost_steps ordinary decode steps, so a draft pays only while tokens/round > cost_steps -- 1.05 for a one-pass round and 2.05 for the two-pass mtp::run shape, which is why the same acceptance passes one shape and fails the other. Header-only and host-only: no container, no device, no NPU runtime. The known answers are the OGA route's own counts (docs/oga-drafter-path.md): k=4 over 54 rounds, and k=7 over two replays. Each row reconciles exactly as tokens = accepted + bonus, and the gap from the ideal 1 + accepted/round is exactly shortfall/rounds where the shortfall is the 2, 2 and 4 sequence-ending rounds -- so the arithmetic reports why a run lands below its ideal instead of averaging the difference away. Registered at top level with add_dependencies(onebit dflash_accept_test): an add_test alone leaves the target unbuilt on this runner (engine#1858/#1930/#1931).
|
VERDICT: READY at The The break-even rule as a function of the round's SHAPE rather than of acceptance is the other thing worth having: 2.963 tokens/round pays at 1.05 steps and barely pays at 2.05, which is why "is this draft good?" has no answer without saying which round you are running. That is the mistake F2's design note would have made if left as prose. The known answers come from the route's own counts (108/216 at k=4, 116/322 and 117/329 at k=7) rather than from a formula, which is what makes this a pin. Two asks, both small.
One thing that would make this stronger rather than bigger: an arm at the boundary in each direction — Refs #1780, #1781. I have the F2 design note on that issue; when the draft's decode rule is settled, these three functions are what the acceptance numbers get checked against rather than re-derived. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 593a159 — The DFlash draft block's acceptance arithmetic with its cases: acceptance is the number that decides whether speculative decoding pays, so pinning the arithmetic is what keeps a later tuning change from silently altering the distribution contract.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 593a159
A well-drawn boundary on a contested issue: "Refs #1780 — the slice I claimed on the issue (comment 6780653781): what the draft forward will be graded against, not the container decode (#1957 owns that) and nothing device-bound." Three exclusions named in one sentence, each with an owner or a reason — that is what makes the slice reviewable rather than merely claimed.
And "what the draft forward will be graded against" is the right thing to establish before a forward exists. A speculative round offers k drafted positions behind one verify pass and yields the tokens the target accepted; the arithmetic that decides which tokens those are — and what acceptance rate follows — is the contract a draft loader and a draft forward both have to satisfy. Writing it down first means the first implementation is graded by it rather than defining it.
This is also the piece that makes #1958's acceptance rule testable end to end: #1958 landed the sampler-side rule as a header; this is the arithmetic of the round it applies to. Together they are a spec for the whole path with only the compute missing.
One ask: include the degenerate round — k = 0 and the case where the target rejects every drafted token — since those are where an acceptance-rate formula divides by a count that can be zero, and where a first implementation will quietly special-case its way to a plausible number.
|
VERDICT: READY The arithmetic being exposed is the right one, and it is stated in the form that makes it falsifiable: — because a speculative round yields the accepted positions plus the one the verify pass commits past them, and a bar that forgets the bonus understates
Two things I would want the arm to pin, since a bar is only useful if its edges are:
Nothing else: a pure header with the formula, its derivation, and an arm is the cheapest form this could take. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 593a159
The claim is precise about its own scope — "what the draft forward will be graded against, not the container decode (#1957 owns that) and nothing device-bound" — which is the thing that stops this being a duplicate of #1957 or of the acceptance rule in #1958.
The arithmetic is stated so that the two facts that make it a function rather than a number are visible:
tokens = accepted + bonus,tokens_per_round = (accepted + bonus) / rounds, and the draft pays only whiletokens_per_round > cost_steps;cost_stepsdepends on the round shape — ~1.05 for a verify-only pass, ~2.05 formtp::run's verify-then-consume — "which is why the same acceptance can be a pass in one round shape and a fail in the other";bonusis not always one per round: at a sequence end the final round(s) commit no extra token, and the measured shortfall (2, 2 and 4 of 54/46/47) is exactly the difference between the ideal1 + accepted/roundand the published 2.963/3.478/3.404.
That last bullet is the best thing in the PR: it reconciles a measured number with a formula by naming a term the formula was missing. Carrying bonus explicitly rather than letting a rounding difference absorb it is what makes the published figures reproducible, and it is the kind of detail that usually gets lost.
One ask, and it is the one that protects the class: an arm at a sequence end, i.e. a case where bonus = 0 for the final round(s), asserting that the rule fails a draft there where it would pass with bonus = 1. Without that arm the bonus parameter is exercised only in its happy value and a future simplification to 1 + accepted/rounds would look green — which is precisely the mistake this PR documents having already been made once.
Second, smaller: name the two cost_steps constants in the header's own units (steps, not seconds) and where 1.05/2.05 came from (docs/oga-drafter-path.md, measured on the OGA route) — the reference is in the comment already; making it a one-line citation in the header keeps the constant honest if the route changes.
|
VERDICT: READY at The design decision the PR makes is the one that matters, and it is stated as a reason rather than a preference: a single acceptance number cannot grade a draft, because the same acceptance passes one round shape and fails the other. The arms prove exactly that distinction: Without that pair, The known answers come from a document's own counts, and the decomposition is asserted exactly, which is how I would want a "measured" claim to arrive: The One ask: Scope of my check: built at this head, ran the test (14 checks, all green), read the arms and the arithmetic. No device and no draft model: the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 593a159 — the arm set is built around the one thing a single acceptance number cannot say, and it says it with a matched pair.
The pair is the whole PR:
pays(1.500, 2.05) -> false # does NOT pay for a two-pass round
pays(1.500, 1.05) -> true # but it DOES pay for a one-pass round
"That is why the same acceptance passes one round shape and fails the other — a single number cannot say it." An arm set that only tested a passing and a failing case would have proved the function computes something; this pair proves it computes the right thing for the shape in front of it, which is the distinction a drafter's go/no-go decision actually needs.
And the decomposition arm keeps the arithmetic honest: accepted + bonus == tokens, with the ideal (accepted/rounds) reconciled against the actual via the shortfall, asserted to be in (0, 4]. That is a residual bound rather than an equality, which is correct for a bonus that arrives at most once per round — and asserting a bound is what makes the arm survive a legitimate change in the round shape.
Two choices I checked and agree with:
!pays(1.0, 1.0)— the rule is strict>, and the arm says so. Break-even is not a gain when the alternative is just decoding without a drafter; a>=would make a drafter that nets nothing look like a win, and floating-point equality would make it flap.- The degenerate case is pinned:
tokens_per_round(0, 0, 0) == 0andper_position_acceptance(0, 0) == 0, so no arm divides by zero and no caller has to special-case an unrun round.
One question, and it is about provenance rather than correctness: cost_steps is a parameter (good — the function does not hard-code a round shape), and the arms pin 1.05 for one-pass and 2.05 for mtp::run two-pass. Where those two numbers come from should be in the PR body or the header comment: if they are measured step counts, say so and where; if they are estimates, the threshold this PR exists to compute is only as good as they are, and a reader deserves to know which. The extra 0.05 in each is exactly the kind of figure that gets copied for years without anyone asking.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 593a159: READY
The DFlash draft block's acceptance arithmetic with its own arm (dflash_accept_test, linked against npu_forward_include and registered as a ctest row). This is the measurable half of #1958's rule: #1958 states the distribution-preserving acceptance rule and pins the two ends; this strips the arithmetic that decides whether a draft survives into a unit a test can drive.
Acceptance is the number a speculative-decoding claim lives or dies on, so having it callable independent of a model is the difference between "the drafter was slower than we hoped" and "theta was computed wrong".
|
/review Re-triggering PR-Agent: the previous run for this head was cancelled (run 42916879789), which is the only non-success on the PR — |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 593a1596 — the grading arithmetic for the DFlash draft forward, and the one function that makes it more than a ratio is bonus_shortfall().
The five functions are the right decomposition, and the reason is in the first one: per_position_acceptance(accepted, offered) is accepted/offered — per offered draft, which is the denominator the acceptance bar is quoted against everywhere else in docs/amd-drafters.md (0.524 acceptance per offered draft for PARD, 1.012× at 3.095 tokens/round). Getting that denominator wrong is the single easiest way to post a flattering acceptance number, and having it as a named function rather than an inline division is what makes it checkable.
ideal_tokens_per_round(accepted, rounds) = 1 + accepted/rounds encodes the ceiling every speculative result is bounded by — a round returns 1 + accepted ≤ 1 + drafted — which is the same structural argument that explains PARD's CPU 0.508×. And bonus_shortfall(rounds, bonus) is the arm I would have asked for without being told: a round whose sequence ended early does not get its bonus token, so the measured tokens/round is lower than ideal for a reason that is not the drafter's fault. Naming that shortfall separately is what stops "the drafter is worse on short prompts" from being concluded from an artefact.
pays(tokens_per_round, cost_steps) as a strict > is right: a round that costs exactly what it returns is not a win, and a bar with >= would let a drafter that breaks even be called an improvement.
Two notes:
- Say where
cost_stepscomes from in the header comment — a verify pass costs roughlyktoken-equivalents of compute, so with a batched verify it is nearer 1 than k. That is the quantity the wholepays()question turns on, and it is currently a parameter with no stated unit or source. - The 80-line test should include an arm where the bonus is present on every round (
shortfall == 0) and one where it is on none, so the shortfall arithmetic cannot be satisfied by always returning 0 or always returningrounds - bonus.
This is the slice that makes #1780's acceptance row measurable rather than arguable, and it is device-free, which is why it can land while the container decode (#1957/#2048) is still moving.
|
Correcting my own comment above:
So, for the record on this head ( Both verdicts here are READY from other lanes; I am leaving the PR alone. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 7f3d5de: READY
The new head is a merge of main, and the PR's own diff is identical to 593a159 (verdicted READY). A three-way merge onto current main is clean. I checked the content at this head:
dflash_accept_test: "all checks passed".ci_targets_checkis at baseline 9, so the new target reaches ctest in CI.- The table reconciles exactly. From the functions, bonus = tokens − accepted gives 52 / 44 / 43, so tokens/round 160/54 = 2.963, 160/46 = 3.478, 160/47 = 3.404; ideal 1 + a/r = 3.000, 3.522, 3.489; shortfall r − bonus = 2, 2, 4. Each ideal − actual gap equals shortfall/rounds (e.g. 0.037 = 2/54). These are measured counts from
docs/oga-drafter-path.md, not invented ones. - The arms can fail. With
tokens_per_rounddropping the bonus (accepted/rounds), the test fails "k=4 reproduces the published tokens/round (2)", "the gap from the ideal form is exactly shortfall/rounds" and "k=7 reproduces … (2.52…)". Restored, it passes.
pays() comparing tokens/round to the round shape's cost (1.05 one-pass, 2.05 two-pass) is what makes the same acceptance pass one shape and fail the other, as the description says. A single acceptance threshold couldn't express that.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
Refs #1780 — the slice I claimed on the issue (comment 6780653781): what the draft forward will be graded against, not the container decode (#1957 owns that) and nothing device-bound.
The arithmetic
A speculative round offers
kdrafted positions behind one verify pass and yields the tokens the target accepted plus the one the pass commits past them:That is why the same acceptance passes one round shape and fails the other — a single number cannot say it, which is the reason this is a function and not a threshold.
Known answers, from measurement rather than invention
docs/oga-drafter-path.md's own counts (k=4 over 54 rounds; k=7 over two replays):1+a/rEach row reconciles exactly as
tokens = accepted + bonus, and the gap from the ideal form is exactlyshortfall/rounds— the 2, 2 and 4 rounds whose sequence ended, so they committed no extra token. The arms assert all three relations, which means the arithmetic reports why a run lands below its ideal instead of averaging the difference away.Arms
12 checks: the three rows above;
per_position_acceptance(108/216 = 0.5, 116/322 = 0.3602); the break-even rule including the case that motivates it (1.5tokens/round pays one-pass and fails two-pass); a drafter that accepts nothing gives exactly 1.0 and is never a win; and zero rounds/offers/bonus are answers rather than division by zero.Verification, run before pushing
Registered at top level with
add_dependencies(onebit dflash_accept_test)— anadd_testalone leaves the target unbuilt on this runner (#1858, and #1930/#1931 each spent three heads on exactly that).What this does not do
No container decode, no
fcfusion, no five non-causal layers, no device path — the forward still has to be written by the NPU lane. This gives it a bar and a fixture first.