Repository navigation
[Perf][Spec Decode] Filter rejection sampler output rows in Python instead of per-row NumPy - #57888
ZhengGong-amd wants to merge 1 commit into
Conversation
Impact surface and end-to-endWho reaches it. Which denominator. The two call sites pass different tensors. Under async scheduling
End-to-end: no signal, as expected. Qwen3-8B + ngram (
The spread is larger than the effect in both shapes — ngram acceptance varies run to run — No GSM8K run, deliberately. The two implementations are not statistically equivalent, |
CUDA sideThis change is platform-independent and does affect CUDA, in the same direction and by
Async scheduling is the case that matters most on CUDA: I do not have an NVIDIA GPU here, so there is no CUDA timing in this PR. I would rather |
RejectionSampler.parse_output filtered each request's row with a NumPy fancy index plus a tolist(), two dispatches per request to select a handful of tokens. Convert the batch once and filter in Python instead, which is faster at every batch size measured (median -64.6% over a 90-shape grid, no regressions), and skip building valid_mask entirely when logprobs are off, since nothing else reads it. Generated-by: Hyperloom Co-authored-by: Claude Signed-off-by: Zheng Gong <Zheng.Gong@amd.com>
d30c7ef to
bf74f8e
Compare
Purpose
RejectionSampler.parse_outputbuilds its result with a NumPy fancy index and atolist()per row:A row holds
max_spec_len + 1tokens — typically 2 to 6 — so each request pays two NumPydispatches to filter a handful of values, and the cost grows linearly with the running
batch. Convert the batch once and filter in Python instead, which is faster at every batch
size measured, and drop the
valid_maskallocation entirely when logprobs are off, sincenothing else reads it.
The function is the
max_gen_len > 1branch ofget_output()andexecute_model(), so itruns once per speculative-decoding step for ngram, EAGLE/EAGLE3, MTP and draft-model alike.
Test Plan
Both versions are lifted out of their trees by text and run in one interpreter so a single
loop alternates between them. Grid: batch 1–512 ×
max_spec_len1/3/5 × acceptance0.3/0.6/0.9,
vocab_size=151936, int32, 5 interleaved pairs per shape with the orderalternated, min-of-pairs, within-arm spread ≤ 1.5 %.
Equivalence: 2560 cases (batch 0–64, 1–6 columns, two vocab sizes, all-placeholder rows,
out-of-vocabulary and negative ids,
discard_req_indicesas list / ndarray / tensor / everyrow, with and without
logprobs_tensors) through the real function in both trees.Test Result
Equivalence: 0 mismatches over 2560 cases, byte-identical outputs including the
logprobs path.
parse_outputwith a host-side input,max_spec_len=3, acceptance 0.6:Over the full 90-shape grid: median −64.6 %, worst case −48.3 %, no shape regresses.
Under async scheduling
get_output()passes a host tensor, so the table above is the wholefunction. The synchronous path passes a device tensor and also pays
.cpu(); the samechange reads −12.6 % at batch 4, −25.2 % at 16, −39.8 % at 64, −56.5 % at 256.
End-to-end (Qwen3-8B, ngram, TP=1, 4 interleaved pairs, pair 0 dropped) is flat: +4.2 %
at concurrency 16 and +0.6 % at 64, against within-arm spreads of 6.4 % and 4.4 %. No
end-to-end speedup is claimed — the saving is microseconds against a multi-millisecond
decode step. The claim is the host-side cost of the function, and that it no longer grows
with the running batch.
The obvious alternative — one masked gather plus a Python split — is better at batch 512
(33 µs against 67 µs) but costs +15 % at batch 1 and regresses 18 of 90 grid shapes.
Scope
Platform-independent — no custom op, no backend dispatch, no
current_platformbranch — soCUDA gets the same change by the same mechanism. The filter predicate, the empty rows for
discarded requests and the pre-discard logprobs boundaries are unchanged, and the
max_gen_len == 1path does not reach this function.Reproducing the numbers
Patch
bf74f8e39con9598487bc1. Measured on vLLMv0.29.0(98dff2a81d), torch2.12.0+git6bbd260;parse_output's body is identical between that tag andmainapartfrom docstring blank lines added by #52136.
Found by Hyperloom; reviewed and measured by hand.