[ModelRunner V2] Speculative Decoding NGram GPU Implementations - #40704
[ModelRunner V2] Speculative Decoding NGram GPU Implementations#40704PatchouliTIS wants to merge 79 commits into
Conversation
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a GPU-accelerated N-gram speculator (NgramGPUSpeculator) for speculative decoding in vLLM V2. Key changes include infrastructure to handle variable-length draft tokens, memory management updates to keep token IDs on the GPU for faster scanning, and performance optimizations for the Gumbel sampler by making FP64 precision optional. A review comment suggests improving the N-gram matching logic in the kernel to select the most recent occurrence of a pattern instead of the first, which better captures local context for prompt lookup.
| idx = matches.int().argmax(dim=1) | ||
| has_match = matches[batch_idx, idx] | ||
| first_match_pos[:, i] = torch.where(has_match, idx.long(), -1) |
There was a problem hiding this comment.
The current implementation uses argmax(dim=1) on the boolean matches tensor, which finds the first occurrence of the n-gram pattern in the sequence. In speculative decoding (specifically prompt lookup), it is standard practice and significantly more effective to use the most recent (last) occurrence of the pattern, as it better captures the local context.
You can find the last match by applying argmax to a tensor of indices where matches occur, which will return the largest index for each row.
| idx = matches.int().argmax(dim=1) | |
| has_match = matches[batch_idx, idx] | |
| first_match_pos[:, i] = torch.where(has_match, idx.long(), -1) | |
| # Find the last match by using argmax on indices to get the most recent occurrence | |
| matched_indices = torch.where(matches, window_pos.unsqueeze(0), -1) | |
| idx = matched_indices.argmax(dim=1) | |
| has_match = matches[batch_idx, idx] | |
| first_match_pos[:, i] = torch.where(has_match, idx, -1) |
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
…liTIS/vllm into patchy/async_ngram_v2_pr
|
@PatchouliTIS it seems like the FP64 -> FP32 gumbel sample changes are not necessary for enabling the ngram functionality. Would it be possible to separate those changes out into a separate PR? |
okay, I'm on my vacation now and I will handle this next week. |
Signed-off-by: PatchouliTaisa <patchychen@tencent.com>
|
Removed gumbel sampling modifications from this PR, ready for review. @TheEpicDolphin |
| "mtp", | ||
| "dflash", | ||
| "dspark", | ||
| "ngram_gpu", |
There was a problem hiding this comment.
| "ngram_gpu", | |
| "dspark", | |
| "ngram_gpu", |
it this an accident due to merge conflict?
There was a problem hiding this comment.
Yes, dspark was accidentally removed in an earlier commit. Fixed in the latest version.
|
@PatchouliTIS really sorry for the delay, we'll aim to get this merged this week! |
Signed-off-by: PatchouliTaisa <pyramkar@gmail.com>
…hy/async_ngram_v2_pr
…ification layout Replace the scheduler-notification design (per-step take_draft_token_ids RPC + D2H event sync) with GPU-side verification trimming reusing the DSpark adaptive-verification machinery: - The drafter records per-request valid draft counts in a persistent GPU tensor; the scheduler always schedules the full num_speculative_tokens. - At the next step, VariableDraftTrimmer clamps scheduled draft slots to min(num_valid, scheduled) and rebuilds cu_num_logits/query_start_loc on device via build_verification_layout (factored out of AdaptiveVerificationManager.reallocate_drafts). CPU totals remain upper bounds; the trimmed gap behaves as cudagraph padding downstream, and trimmed slots reconcile through the existing num_rejected accounting. - Falls back to pad-and-verify (still correct) when the attention backend or config cannot support device-side varlen trimming. - Ngram kernels now index all_token_ids rows in place via idx_mapping (no per-step [B, max_model_len] materialization), use persistent scratch, early-exit scan blocks past seq_len, and drop the torch.compile fallback. No CPU<->GPU syncs are introduced on any path. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
…mode Unset (auto) cudagraph_mode may resolve to full graphs after the trimmer is created, so treat it as full-graph usage when checking varlen decode capture support. Also log when GPU draft trimming is enabled. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
… the V2 runner On the V2 model runner, method="ngram" and method="ngram_gpu" now both use NgramGPUSpeculator (via a new SpeculativeConfig.use_ngram() helper); the V1 runner keeps its separate CPU and GPU proposers. Also scope the torch.compile cache disable to the V1 ngram-gpu proposer: it exists for V1's @support_torch_compile kernel, while the V2 implementation is pure Triton and does not need it. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
|
@PatchouliTIS I have reworked this a bit in another branch here: main...njhill:vllm:patchy/async_ngram_v2_pr to exploit some of what was added recently in #47808, it simplifies things quite a bit and does not require round-tripping draft tokens back to CPU. PTAL! (note it's still not in a final state, quite a bit more simplification/cleanup can be done espec. in model_runner.py) Output throughput (tok/s)Workloads (max_tokens=256,
|
|
@njhill LGTM. The truncation of n-gram draft tokens now runs completely on the GPU side, eliminating the data transfer overhead that my implementation introduced. Would you mind sharing the benchmark scripts and commands you used? I'd love to run them locally to reproduce the results—the MRV1 Also, please feel free to push directly to this branch, or let me know if you'd prefer me to pull your changes into this PR and help finish the remaining cleanup in |
f1c0a89 to
ee72c7a
Compare
…esolve_cudagraph_mode_and_sizes Both adaptive verification and variable-length drafters decide per-request query lengths on device, so decode batches are varlen and cudagraph capture needs a separate decode routine. Express that as a varlen_decode flag on resolve_cudagraph_mode_and_sizes, alongside the other backend-support downgrades, instead of mutating compilation_config.cudagraph_mode from the model runner beforehand. Only CUDAGraphMode.FULL actually needs to change (it has full cudagraphs but no separate decode routine); PIECEWISE/NONE capture no full decode graphs and FULL_DECODE_ONLY/FULL_AND_PIECEWISE already have one. This drops the adaptive-verification override of an explicitly requested PIECEWISE or FULL_DECODE_ONLY mode, which was a no-op for the default FULL_AND_PIECEWISE. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
Replace the getattr/hasattr injection of RequestState into the speculator with an explicit init_speculator parameter, handed to the speculators that draft from the persistent token store (currently only NgramGPUSpeculator). RequestState is now built before the speculator, which only needs config values that were already available at that point. NgramGPUSpeculator.req_states is consequently non-optional, so propose() drops its injection assert, and its tests exercise a real RequestState instead of a duck-typed stand-in. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
…draft_trimmer Pass vllm_config and RequestState instead of eight individually-derived values: LoRA/PP/CP/cudagraph-mode support all come from the config, and max_num_reqs, device and the logit chunk limit from RequestState. Declare trims_drafts_on_gpu and num_valid_drafts on BaseSpeculator so the factory reads them directly rather than through getattr, which also lets it accept a None speculator and drop that check from the call site. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
trims_drafts_on_gpu carried no information beyond "num_valid_drafts is set", so replace both with a single optional num_valid_drafts_for_trim tensor on BaseSpeculator: None means verify every scheduled draft, a tensor opts the drafter into device-side trimming. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Nick Hill <nickhill123@gmail.com>
|
Hi @PatchouliTIS, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
|
I pushed my changes along with a bit more rework, but I'm still not sure it's in the best state w.r.t. how the draft trimmer abstraction is structured.
@PatchouliTIS here is the benchmark script that was used. I hadn't actually read it, I guess the prompts aren't ideal and we could use some better test sets for this: bench_ngram_trim.py |
|
Hi @PatchouliTIS, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
Purpose
Added a new NGram GPU speculator.
The main feature is a new implementation at:
vllm/v1/worker/gpu/spec_decode/ngram/speculator.py, similiar to [Core] NGram GPU Implementation compatible with Async Scheduler #29184.Updated request state storage for NGram GPU.
vllm/v1/worker/gpu/states.pyvllm/v1/worker/gpu/model_runner.pyThis changes how RequestState is initialized so that all_token_ids can stay densely resident on GPU instead of defaulting to UVA when ngram_gpu is active. As discussed in [Core] NGram GPU Implementation compatible with Async Scheduler #29184, the new n-gram speculator repeatedly scans active request token history, doing that from GPU-resident dense storage is much more appropriate than pulling through UVA-backed memory, this is a performance-oriented architectural change supporting the new feature.
The
model_runner.pyalso injects req_states into speculators that need direct access to the persistent token store.Added variable-length draft token plumbing
Several files were updated to support draft proposals where different requests may have different numbers of valid draft tokens:
vllm/v1/outputs.pyvllm/v1/worker/gpu/spec_decode/utils.pyvllm/v1/core/sched/scheduler.pyvllm/v1/engine/core.pyvllm/v1/worker/gpu/model_runner.pyDraftTokenIds now includes:
num_valid_draft_tokens: list[int] | None.Scheduler logic now truncates speculative tokens based on
num_valid_draft_tokens.EngineCore adds _maybe_update_async_draft_token_ids()to consume draft metadata from async execution and update scheduler state at the right time.Test Plan
vllm bench cmd:
Test Result
Async NGram GPU V1 results:
Async NGram GPU V2 results:
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.