Skip to content

perf(dspark): propose one token, not three, once the draft is carrying real context (+9.4% dspark-decode@4k) - #878

Merged
skyrocket2026 merged 1 commit into
gittensor-ai-lab:mainfrom
rsnetworkinginc:perf/dspark-proposal-depth
Aug 19, 2026
Merged

perf(dspark): propose one token, not three, once the draft is carrying real context (+9.4% dspark-decode@4k)#878
skyrocket2026 merged 1 commit into
gittensor-ai-lab:mainfrom
rsnetworkinginc:perf/dspark-proposal-depth

Conversation

@rsnetworkinginc

Copy link
Copy Markdown
Contributor

Summary

Below the deep threshold the draft proposes 3 tokens. At this model's acceptance those extra
proposals are never accepted, so they are pure draft cost — and the cost is not constant: each
proposal is another row of the draft's own attention over the same KV, so it grows with context
while what it buys stays at zero.

The measurements the depth-3 choice rests on were taken with the harness's NSPLITS=1 pin in
place. At the split counts the server actually runs, the attention that pin inflates is cheap, the
draft's fixed cost is a much larger share of the step, and the balance moves. Measured on RTX 5090
at production split counts, one binary, LOSSLESS 1 on every run:

ctx depth 1 depth 3 mean accept (d1 / d3)
128 81.94 91.69 1.1034 / 1.0159
512 92.32 91.95 1.0000 / 1.0000
1024 78.72 68.30 1.1429 / 1.1566
2048 83.04 78.35 1.0435 / 1.0435
4096 81.23 74.82 1.0756 / 1.0756

Acceptance is identical at 4k and 2048 — depth 3 buys exactly nothing there. 128 is the one
context that prefers the wide block, and its acceptance column says why: at depth 3 the draft
accepts essentially nothing (1.0159), the idle-draft rule stops drafting, and the stream runs at
AR speed. Narrowing raises acceptance just enough (1.1034) to keep the draft alive without being
worth its cost. So this does not narrow always — it narrows once the draft is carrying a context
whose per-row cost the deeper proposals cannot repay, which is what the 512 tie and everything
above it show. Below kNarrowMinSeq (512) nothing changes, and the >= kDeepMinSeq branch keeps
its 7 — long context is where acceptance actually climbs and it is measured separately.

Decided once per generation, not per step, and that distinction is load-bearing. The verify
graph dflash_warm_verify captures is sized from this depth; narrowing per step under a warm
graph built for 4 rows recovers only 0.7% of the 9.4%, because the mismatched graph costs more
than the rows it saves. Both were measured; the static form is the one that pays.

Proof of speedup

  • Tested on RTX 5090 (sm_120)

Decode tok/s (dspark_tau_check <modelopt> <dspark> 128 <4096-token prompt>, METRIC DSPARK_TPS,
production split counts):

decode tok/s
before (main) 73.87
after (this PR) 80.82

+9.4% dspark-decode@4k. Main and PR binaries interleaved round-robin:

round 1: main=73.7986  pr=80.8077   (accept 1.0756, lossless)
round 2: main=73.8362  pr=80.8594   (accept 1.0756, lossless)
round 3: main=73.9694  pr=80.7974   (accept 1.0756, lossless)

main 73.868 | pr 80.822 | +9.41%, 3/3 rounds

No context regresses, and the two below the band are unchanged by construction:

ctx=128   main 90.92  pr 91.30    (+0.4%, depth unchanged)
ctx=512   main 91.89  pr 91.86    (-0.0%, tie)
ctx=1024  main 68.27  pr 78.73    (+15.3%)
ctx=2048  main 78.45  pr 83.04    (+5.9%)

Correctness

LOSSLESS 1 on every run above, including the interleaved rounds and all four guard contexts.

Mean accept is unchanged: 1.0756 on main, 1.0756 with this PR, at 4k — this PR does not buy
throughput with acceptance, it removes draft rows that acceptance was never using. That is the
tau floor's own question, answered by identity rather than by tolerance.

Multi-rep losslessness at the scored context is in a comment below (running as this was written;
SPARKINFER_DSPARK_SPEC_REPS at ctx=4096).

SPARKINFER_DFLASH_NARROW_MINSEQ pins the band boundary (default 512) and
SPARKINFER_DFLASH_PROPOSALS still overrides the depth outright, so both arms come out of one
binary.

…g real context (+9.4% dspark-decode@4k, tau unchanged)
@rsnetworkinginc

Copy link
Copy Markdown
Contributor Author

Multi-rep losslessness at the scored context, as promised in the description — same build, ctx=4096, production split counts:

SPEC-REPS 12 reps, 0 differ-from-first, 0 not-lossless-vs-AR
METRIC DSPARK_TPS 80.7754
METRIC MEAN_ACCEPT 1.0756
METRIC LOSSLESS 1
METRIC LOSSLESS_RUNS 12

Every repeat re-runs the whole generation including the token-loop prompt prefill, and the mean accept lands on 1.0756 again — the same value main measures, which is the point: this PR removes proposals acceptance was not using rather than trading acceptance for throughput.

@skyrocket2026 skyrocket2026 added eval-dspark:S DSpark decode@128 eval tier eval:S sparkinfer auto-eval verdict: S labels Aug 19, 2026
@skyrocket2026

Copy link
Copy Markdown
Member

sparkinfer DSpark auto-eval — eval-dspark:S

metric value
label eval-dspark:S
scored at DSpark speculative decode @ ctx=4k on the ModelOpt NVFP4 checkpoint
PR DSpark tok/s 45.36
main DSpark tok/s 43.06
speedup vs main 1.053× (+5.3%)
PR AR tok/s (floor) 47.40
main AR tok/s (floor) 47.39
AR vs main (floor) +0.0%
DSpark vs AR 0.957× — above 1.0 means speculation finally pays
mean accept τ 1.085 (main 1.085, ceiling 7)
accuracy gate ✅ top1=1.000 (bar >=0.9) · KL=0.0000 (bar <=0.1)
losslessness gate ✅ DSpark matches the AR reference token-for-token, verified across 3 independent runs
mean accept τ floor ✅ 1.0847 vs main 1.0847 (bar ≥95%)
qwen3.8 (ModelOpt) guard @16k ✅ no regression (decode+prefill) — decode 89.5 tok/s · prefill 13555 pp
qwen3.6 guard @16k ✅ no regression (decode+prefill) — decode 474.5 tok/s · prefill 28368 pp
PPL PR / main 3.204 / 3.204
Polaris receipt collected, not signed (no key configured)
commit b545b4f2a

ok

Scored on the pinned eval box vs same-box origin/main: DSpark speculative decode throughput at ctx=4k on the ModelOpt NVFP4 checkpoint, with the AR reference measured in the same process and the same model load. Both a regression in AR decode and any divergence from the AR token sequence are hard REJECTs — a speculative decoder that is fast because it skips verification is not faster, it is wrong. τ is the lever, and the row above reports it against a block_size of 7. This is informational, not a judgment on your PR: a none label just means no measurable DSpark decode@4k speedup was verified, which is expected and fine if that isn't what your change is about. Automated — merge behaviour depends on SPARKINFER_DSPARK_AUTOMERGE.

@skyrocket2026 skyrocket2026 added dspark-needs-rebase DSpark eval merge ordering eval-dspark:REJECT DSpark decode@128 eval tier eval:REJECT sparkinfer auto-eval verdict: REJECT and removed eval-dspark:S DSpark decode@128 eval tier eval:S sparkinfer auto-eval verdict: S labels Aug 19, 2026
@skyrocket2026

Copy link
Copy Markdown
Member

Closed: sparkinfer DSpark auto-eval — eval-dspark:REJECT

This PR's Qwen3.8-27B prefill@16k speed measured -47.5% vs main, (dspark decode@4k regression) — closing automatically. This bot evaluates every eligible PR in the repo against Qwen3.8-27B's decode AND prefill@128 speed specifically, regardless of what the PR is actually about — a close here isn't a judgment on the PR's purpose, just that it didn't move these particular metrics. Reopen (or open a fresh PR) if you have a fix or a different approach.

@skyrocket2026 skyrocket2026 reopened this Aug 19, 2026
skyrocket2026 added a commit that referenced this pull request Aug 19, 2026
…ref's own checkout

The harness lives in the repo, so each ref was being measured with its own copy of it. That is
harmless while the harness is stable and catastrophic the moment it changes: every PR branched
before the change gets compared against a baseline measured by a different instrument.

It cost us the best PR of the day within an hour of the pin removal. #878 was branched before
f3b2e04, so its checkout still carried dspark_tau_check WITH the NSPLITS=1 pin. It measured
dspark 45.37 / ar 47.40 -- the pinned regime -- against a main baseline of 74.07 / 90.16 measured
without the pin. The bot read that as -47.5% on ar-decode@4k, labelled it REJECT and auto-closed
it. Nothing was wrong with the PR: it was the one change all day that closed the DSpark/AR gap
(0.909 -> 0.957) with AR flat and tau held. Reopened by hand.

Now both refs take runtime/examples/dspark_tau_check.cpp and bench/scripts/bench_prompt_4k.txt from
origin/main after checkout, so a round always compares two builds through one ruler. Same pattern
the bot already uses for eval/polaris, and consistent with HARNESS_PATHS -- a PR may not change
these files anyway, so sourcing them from main costs a contributor nothing.

Fails the run loudly if the pin cannot be applied, rather than silently measuring with whatever the
ref happened to carry.
@skyrocket2026 skyrocket2026 removed eval-dspark:REJECT DSpark decode@128 eval tier eval:REJECT sparkinfer auto-eval verdict: REJECT dspark-needs-rebase DSpark eval merge ordering labels Aug 19, 2026
@skyrocket2026

Copy link
Copy Markdown
Member

Reopened — the REJECT was our bug, not yours. Apologies for the noise.

An hour before that round, we removed a SPARKINFER_NSPLITS=1 pin from dspark_tau_check (the harness), because it was measuring decode in a regime the server never runs. That moved the baseline by roughly 72%: dspark@4k 43.06 → 74.05, ar@4k 47.39 → 90.19.

The harness lives in the repo, and the bot was measuring each ref with its own copy of it. Your branch predates the removal, so it was measured with the pin while main was measured without it:

#878   dspark 45.37   ar 47.40    ← pinned harness (your checkout)
main   dspark 74.07   ar 90.16    ← unpinned harness (main)
       → read as -47.5% on ar-decode@4k → REJECT → auto-closed

Two builds, two different rulers. Nothing about your change caused it.

For the record, your PR was the best result of the day. Under the v1 numbers it scored S at +5.3%, but the tier undersold it: it was the only change that moved DSpark toward break-even — ratio 0.909 → 0.957, with AR flat and τ held at 1.085. The three attention PRs that outscored it (L, XL, L) each lifted AR more than the speculative path, so they widened the gap; and measured at production split counts, all three delivered ~0% (production decode at 4k went 93.69 → 93.64 across the day). Yours was the one that made speculation genuinely cheaper.

Two things have been fixed:

  1. The harness is now pinned to origin/main for every ref, so a round always compares two builds through one ruler. It fails loudly rather than silently measuring with whatever a ref happened to carry.
  2. The scoring schema is bumped to v2, so this will be re-evaluated from scratch rather than compared against a v1 number.

Your reasoning about the depth-3 choice resting on pin-inflated measurements was also correct, and it is part of why the pin was removed. Nothing is needed from you — it will be re-scored automatically on the next round.

@skyrocket2026 skyrocket2026 added eval-dspark:M DSpark decode@128 eval tier eval:M sparkinfer auto-eval verdict: M labels Aug 19, 2026
@skyrocket2026

Copy link
Copy Markdown
Member

sparkinfer DSpark auto-eval — eval-dspark:M

metric value
label eval-dspark:M
scored at DSpark speculative decode @ ctx=4k on the ModelOpt NVFP4 checkpoint
PR DSpark tok/s 82.12
main DSpark tok/s 74.82
speedup vs main 1.097× (+9.7%)
PR AR tok/s (floor) 90.07
main AR tok/s (floor) 90.23
AR vs main (floor) -0.1%
DSpark vs AR 0.912× — above 1.0 means speculation finally pays
mean accept τ 1.076 (main 1.076, ceiling 7)
accuracy gate ✅ top1=1.000 (bar >=0.9) · KL=0.0000 (bar <=0.1)
losslessness gate ✅ DSpark matches the AR reference token-for-token, verified across 3 independent runs
mean accept τ floor ✅ 1.0756 vs main 1.0756 (bar ≥95%)
qwen3.8 (ModelOpt) guard @16k ✅ no regression (decode+prefill) — decode 89.0 tok/s · prefill 12706 pp
qwen3.6 guard @16k ✅ no regression (decode+prefill) — decode 472.7 tok/s · prefill 28365 pp
PPL PR / main 3.204 / 3.204
Polaris receipt collected, not signed (no key configured)
commit b545b4f2a

ok

Scored on the pinned eval box vs same-box origin/main: DSpark speculative decode throughput at ctx=4k on the ModelOpt NVFP4 checkpoint, with the AR reference measured in the same process and the same model load. Both a regression in AR decode and any divergence from the AR token sequence are hard REJECTs — a speculative decoder that is fast because it skips verification is not faster, it is wrong. τ is the lever, and the row above reports it against a block_size of 7. This is informational, not a judgment on your PR: a none label just means no measurable DSpark decode@4k speedup was verified, which is expected and fine if that isn't what your change is about. Automated — merge behaviour depends on SPARKINFER_DSPARK_AUTOMERGE.

@skyrocket2026 skyrocket2026 added the dspark-merge-first DSpark eval merge ordering label Aug 19, 2026
@skyrocket2026
skyrocket2026 merged commit 237071a into gittensor-ai-lab:main Aug 19, 2026
11 of 13 checks passed
@skyrocket2026

Copy link
Copy Markdown
Member

Auto-merged as the round's dspark-merge-first winner — verified same-box 128-token decode speedup over main, accuracy-gated vs llama.cpp.

@skyrocket2026 skyrocket2026 removed the dspark-merge-first DSpark eval merge ordering label Aug 19, 2026
skyrocket2026 added a commit that referenced this pull request Aug 20, 2026
…s (+25% at ctx=4k)

The narrow band existed because a 4-wide verify could not pay for itself. #878 measured depth 1
beating depth 3 by 9.4%, correctly, when a 4-row batched verify cost 2.62 target forwards against
a mean accept of 1.92 -- C > tau, so every extra proposal lost. The kernel work in this branch
took that to 1.85, C < tau, and the sign of the trade flips.

Re-measured at ctx=4096, same prompt, same build, LOSSLESS in every case:

    depth 1 (2-wide)   tau 1.3611   72.28 tok/s   0.798x AR    <- what #878 chose, still the default
    depth 3 (4-wide)   tau 1.9231   90.57 tok/s   1.010x AR    <- and what now wins
    depth 6 (7-wide)   tau 1.9231   53.81 tok/s   0.600x AR

tau is identical at 4-wide and 7-wide -- acceptance saturates at ~1.92 -- so 4 is the widest block
that buys anything and 7 only buys cost.

Without this the whole branch is invisible: every cost improvement lands on a batched verify the
default configuration barely uses, and the scored dimension keeps reading 0.798x.

The band narrows to what it was really describing -- the range where the batched verify CANNOT arm.
Below kEngageMinSeq the token loop runs one target forward per kept token, exactly what AR runs, so
proposing more is pure draft overhead and depth 1 stays. At or above it the batched pass arms and
depth 3 is the operating point. The >= kDeepMinSeq branch keeps its 7, unmeasured here and
measured separately.
skyrocket2026 added a commit that referenced this pull request Aug 20, 2026
…rify arms"

This reverts d37034c. The measurement behind it was invalid.

It was taken on a synthetic prompt -- token ids 1000..5095, i.e. gibberish -- which drives the
target into degenerate repetition (at ctx=128 the AR output is literally '197 320 197 320 ...').
Repetition is trivially predictable, so tau inflates to 1.9231 and the 4-wide block looks like it
pays. It does not. Re-measured at ctx=4096 on the corpus the eval bot actually scores
(bench/scripts/bench_prompt_4k.txt, tokenized with the checkpoint's own tokenizer):

    prompt                  tau       DSpark      vs AR
    prose (bot's corpus)    1.4118    66.57       0.736x
    code (C++)              1.1163    68.13       0.759x
    eval_corpus             1.2973    69.24       0.772x
    synthetic ids           1.9231    90.66       1.011x   <- what d37034c measured

and on that corpus the depth sweep runs the other way:

    depth 1 (2-wide)   tau 1.2632   78.80 tok/s   0.877x AR   <- #878's choice, correct
    depth 3 (4-wide)   tau 1.4118   66.23 tok/s   0.739x AR
    depth 6 (7-wide)   tau 1.4545   40.44 tok/s   0.451x AR

#878's narrow band was right and stays. The verify-cost work in this branch is unaffected -- it is
kernel-level and prompt-independent, and C at N=2 still improved 1.49 -> 1.23 forwards -- but it is
not enough to make a wider block pay at the acceptance this model actually achieves on real text.

Lesson worth keeping: never measure tau on synthetic token ids. Speculative-decoding throughput is
a function of how predictable the generated text is, so a degenerate continuation measures the
harness, not the decoder.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

eval:M sparkinfer auto-eval verdict: M eval-dspark:M DSpark decode@128 eval tier

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants