Measured
[spec-phase] on the 27B lane, steady state:
backbone=27.44ms sample=10.50ms logits=3724800
backbone=27.36ms sample=10.49ms logits=3724800
The sequential Markov sampling is 10.5 ms per draft step, 28% of the step,
and it is pure host-side work:
- every block-forward variant returns
std::vector<float>
(qwen3_dflash.h:155,202,248,284), so the logits are ALWAYS downloaded —
nqpr x draft_vocab = 15 x 248320 = 3,724,800 floats = 14.9 MB per draft
step on the 27B;
SampleDsparkBlockDrafts then runs, per step i of k, a device->host Markov
bias download plus a host argmax over the full 248320 vocab
(dspark/speculator.cpp:44-80).
The 35B lane's draft is reduced-vocab (32000), so it moves 9 x 32000 = 1.15 MB
instead of 14.9 MB — which is part of why that lane already sits at 0.92-0.98x
while the 27B does not.
This is risk R5 in .agents/specs/dspark-spec-decode.md, recorded when the
lane landed and never closed: "An N-iteration host loop with a device round-trip
per step would be a decode-path regression at exactly the point the feature is
supposed to win." Upstream captures the whole draft step, backbone plus the
N-step Markov loop, in ONE CUDA graph (dspark/speculator.py:22-24).
The fix
Keep the loop on device. Every primitive already exists, so this is composition,
not a new kernel:
- a device-returning block-forward variant (today's four all download);
- per step:
IndexSelect(markov_w1, prev) -> GEMV against markov_w2 -> add the
step's base row -> vt::GreedyArgmax (already lowest-index tie-break, the same
convention the host loop uses) -> d2t map (identity when the checkpoint ships
no d2t, as the 27B does) -> prev stays on device;
- download only the final
[num_reqs, k] ids.
prev must stay device-resident or the per-step sync defeats the point.
Gate
Device path must be token-identical to the host path on both lanes (same drafts,
same acceptance), then the speed delta measured on matched content. Correctness
first: a faster drafter that changes drafts is not a win.
Context: this is the real lever behind #430, whose premise (a draft-quality gap)
was refuted by direct measurement — our proposals are nearly token-identical to
upstream's and acceptance is at parity on matched content.
Measured
[spec-phase]on the 27B lane, steady state:The sequential Markov sampling is 10.5 ms per draft step, 28% of the step,
and it is pure host-side work:
std::vector<float>(
qwen3_dflash.h:155,202,248,284), so the logits are ALWAYS downloaded —nqpr x draft_vocab= 15 x 248320 = 3,724,800 floats = 14.9 MB per draftstep on the 27B;
SampleDsparkBlockDraftsthen runs, per step i of k, a device->host Markovbias download plus a host argmax over the full 248320 vocab
(
dspark/speculator.cpp:44-80).The 35B lane's draft is reduced-vocab (32000), so it moves 9 x 32000 = 1.15 MB
instead of 14.9 MB — which is part of why that lane already sits at 0.92-0.98x
while the 27B does not.
This is risk R5 in
.agents/specs/dspark-spec-decode.md, recorded when thelane landed and never closed: "An N-iteration host loop with a device round-trip
per step would be a decode-path regression at exactly the point the feature is
supposed to win." Upstream captures the whole draft step, backbone plus the
N-step Markov loop, in ONE CUDA graph (
dspark/speculator.py:22-24).The fix
Keep the loop on device. Every primitive already exists, so this is composition,
not a new kernel:
IndexSelect(markov_w1, prev)-> GEMV againstmarkov_w2-> add thestep's base row ->
vt::GreedyArgmax(already lowest-index tie-break, the sameconvention the host loop uses) -> d2t map (identity when the checkpoint ships
no d2t, as the 27B does) ->
prevstays on device;[num_reqs, k]ids.prevmust stay device-resident or the per-step sync defeats the point.Gate
Device path must be token-identical to the host path on both lanes (same drafts,
same acceptance), then the speed delta measured on matched content. Correctness
first: a faster drafter that changes drafts is not a win.
Context: this is the real lever behind #430, whose premise (a draft-quality gap)
was refuted by direct measurement — our proposals are nearly token-identical to
upstream's and acceptance is at parity on matched content.