bugfix: separate greedy and probabilistic draft proposals for rejection sampling. - #2175
Conversation
f50b95d to
34e2f7b
Compare
2e9bbec to
6337bec
Compare
…on sampling. Encapsulate the speculative draft proposal as drafted tokens plus an optional draft distribution, so the presence of draft_probs alone selects the probabilistic acceptance path and greedy proposals carry tokens only. Validate the proposal shape against the target vocabulary before rejection sampling. Enable probabilistic draft sampling for reduced-vocab EAGLE-3 (d2t) by scattering the draft distribution into the full target vocabulary in the draft worker, so q aligns with the target p; the (p-q)+ residual keeps full target mass on tokens the draft cannot propose. Greedy remains the default.
6337bec to
697531a
Compare
Benchmark:
|
| metric | before | after | Δ |
|---|---|---|---|
| throughput | 280.8 tok/s | 308.8 tok/s | +10.0% |
| accept length | 1.69 | 1.64 | ≈ same (run-to-run 1.63–1.69) |
TPOT (inter-token latency, ms/token)
| concurrency | before | after | Δ |
|---|---|---|---|
| 8 (no queueing) | 51 | 46 | −9.8% |
| 64 (saturated) | 81 | 76 | −6% |
Where the gain comes from
The gain is not from acceptance quality — accept length is unchanged. It is per-step compute, normalized per decode step:
| stage | before | after | Δ |
|---|---|---|---|
| draft exec / step | 2.62 ms | 1.99 ms | −24% |
| target (verify) exec / step | 2.52 ms | 2.51 ms | ≈ flat |
Greedy proposals skip the entire probabilistic-draft machinery: the full-vocab (248320) softmax that produces draft_probs, restoring them to a dense [B, n_spec, V] tensor, and resampling from the draft distribution. msprof op-level over a decode window confirms it — the sampling-path ops all shrink (device ms, directional; the before window contains more steps):
| op (device ms, decode window) | before | after |
|---|---|---|
| ScatterElementsV2 (dense probs materialize) | 1844 | 957 |
| SoftmaxV2 (draft vocab softmax) | 60 | 27 |
| Cumsum | 159 | 65 |
| Sort | 126 | 49 |
| DSARandomUniform | 91 | 62 |
Net: at the throughput-optimal operating point, the greedy default gives +10% throughput / −6–10% TPOT, entirely from eliminating draft-side probability computation, with no change in acceptance length. The gain scales with vocab size and narrows at very large (GEMM-bound) batch.
Summary
DraftProposal.draft_sampling_modethrough runtime options and the spawned-worker protocol.Validation
clang-format --dry-run --Werroron changed C/C++ filesgit diff --checkclang-tidyon sampler, DFlash, and DSpark paths/varscounters (see Benchmark)Benchmark
Env: Qwen3.5-2B (target) + Qwen3.5-2B-mtp (draft), MTP
num_speculative_tokens=2,single Ascend 910 die (TP1), chunked prefill,
max_memory_utilization=0.85,block_size=128.Workload: gsm8k 128-prompt corpus,
temperature=0.7,top_p=0.9,seed=42,max_tokens=2048, streaming. Acceptance is read from xLLM/varscounters (ground truth);accept_length = accepted/steps + 1(max 3.0 at num_spec=2).before= upstream3906f5a9(default temperature-following draft, no
draft_sampling_modeflag);after= this branch.Server launch (per die;
ASCEND_RT_VISIBLE_DEVICESselects the die;beforeomits--draft_sampling_mode):Client (evalscope, per cell):
evalscope perf --api openai --url .../v1/chat/completions --dataset line_by_line --dataset-path <gsm8k-128.jsonl> --number 128 --parallel <conc> --max-tokens 2048 --temperature 0.7 --top-p 0.9 --seed 42 --stream. Acceptance is readfrom the server
/varscounters around each run.Performance (
number=128per cell, one Ascend 910 die, one server at a time,sequential, warm-up discarded). conc=1 (no queueing) and conc=32 (saturated)
bracket the range:
at conc=32 (780.7 vs 511.9 tok/s; 33.0 vs 52.2 ms; 15.42 vs 21.10 s; 142.7 vs 184.6 ms),
and +46% throughput / −32% TPOT / −36% E2E / −11% TTFT at conc=1. Greedy skips the
per-step dense
[B, n_spec, vocab=248320]draft_probs materialization and themin(1, p/q)+ residual recovery; because the first streamed token already includes onespeculative decode step, the lighter step lowers TTFT too (hence the modest but real
conc=1 TTFT gain with no queueing involved).
noise); conc=32 187.2 vs 184.6 ms / 52.2 vs 52.2 ms. The refactor preserves the old
temperature-following path on the sampler side; opt back in with
--draft_sampling_mode=probabilistic./vars) ≈ 2.54 (greedy) vs 2.63 (before /probabilistic) — greedy accepts marginally fewer draft tokens, but the per-step savings
outweigh it. Acceptance is draft-model-determined and otherwise unchanged by this PR.
Accuracy note (gsm8k,
temperature=0, mean_acc = 0.72 for all configs): attemperature=0the target uses all-greedy verify, so speculative decoding emits the target model's greedy argmax
regardless of
draft_sampling_mode/num_spec/ spec on-off / binary version; the target modelis byte-identical before↔after, so accuracy is invariant. Measured identical (0.72) for
after-greedy and after-probabilistic.
Losslessness of the probabilistic fix (Monte-Carlo)
gsm8k accuracy cannot expose the rejection-sampling defect: at
temperature=0the targetverifies greedily (argmax) and bypasses rejection entirely, and at
temperature>0accuracy istoo noisy to resolve a residual-distribution error. The correct probe is a distribution-match
test — fix a target distribution
pand a draft distributionq, draw the rejection samplerNtimes, and compare the empirical output distribution top(lossless ⇔ total-variationdistance
→ 0). Both variants share the same acceptance ruleu < min(1, p[x]/q[x]); theydiffer only in the recovery distribution sampled on a rejected token.
With
p = [.10 .20 .30 .20 .20],q = [.50 .20 .10 .10 .10],num_spec=1,N=2e6(reproducible, seed-fixed):
pp[.100 .200 .300 .200 .200](p − q)+, full draft dist[.100 .1996 .3003 .1997 .2005]pwith only the drafted token reduced byq[x][.100 .2879 .2335 .1887 .1899]afterconverges to the target to the sampling-noise floor (lossless);beforedeviatessystematically (token 1 over-weighted 0.29 vs 0.20, token 2 under-weighted 0.23 vs 0.30) because
its
(p−q)+residual only subtracts draft mass at the single drafted token, not the full draftdistribution. On the shipped code, the
RejectionSamplerTest.RandomandRandomFusedRecoveryDistributionunit tests assert the real sampler's recovery distribution.Reduced-vocab EAGLE-3 (d2t) validation
The new-capability path in this PR — probabilistic rejection sampling for a reduced-vocab
EAGLE-3 draft — is validated separately on a dense single-die setup:
Qwen3-8B(target, TP1,one Ascend 910 die) +
Qwen3-8B-EAGLE3(draft,draft_vocab_size=32000→ full targetvocab_size=151936),speculative_algorithm=Eagle3,num_speculative_tokens=3. The draftworker logs
Loaded d2t tensor from state_dict, hot_token_id size: 32000, confirming thereduced→full scatter map is active. gsm8k corpus,
temperature=0.7,top_p=0.9,seed=42,max_tokens=1024, 32 requests at concurrency 8; acceptance from/vars(
accept_length = accepted/steps + 1, max 4.0 atnum_spec=3).Both modes produce coherent output with no init/runtime error, and acceptance is invariant
(~2.1 at
num_spec=3) across before/after — the draft model is unchanged, so the PR changesper-step cost and rejection-sampling correctness, not accept length. The new
greedydefaulttrims TPOT vs the pre-PR default (16.6 vs 17.9 ms) by dropping draft-probs materialization
entirely.
probabilisticexercises the reduced→full scatter: pre-PR, the draft distribution wascompressed to the selected token's probability before rejection sampling (measured 2.08 /
18.1 ms), so the
(p−q)+recovery on a rejected token sawqmass only at the drafted token;this PR carries the full draft distribution — for reduced-vocab EAGLE-3, scattered from the
32000 draft vocab into the full 151936 target vocab — so recovery uses the true
q.