-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Enable CUDA graphs for MTP inference #4260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
148 commits
Select commit
Hold shift + click to select a range
bde9685
Add nvtx ranges for mtp
santhnm2 7587f2f
perf benchmarking
santhnm2 b8bdd12
Remove simpy dependency
santhnm2 e948bc9
cuda graphs for mtp
santhnm2 6c60f34
mtp cuda graphs
santhnm2 90ffe8a
fix cuda graphs
santhnm2 8ce0a4b
cuda graph fix
santhnm2 84129e7
Fix dummy_position_ids dtype
santhnm2 9bfa0fc
Try to compile more
santhnm2 cbc1270
remove extra ep sync
santhnm2 7687743
Add nvtx ranges
santhnm2 407e12d
Remove cpu sync
santhnm2 e54458c
Avoid h2d syncs
santhnm2 70afe4c
Linting
santhnm2 5e84f85
compile rewind_kv_cache
santhnm2 e2020b2
Linting
santhnm2 56b93ff
Fix verify_speculative_tokens
santhnm2 98a32ef
More fixes
santhnm2 3fefb93
Add torch.compile back
santhnm2 96668d6
Linting
santhnm2 9a95577
Try to fix tests
santhnm2 74edd58
Fix test
santhnm2 b6466d2
More test fixes
santhnm2 6941afb
Bug fix
santhnm2 94452d5
Linting
santhnm2 cd981ea
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 774ec53
kernelize
santhnm2 d9c2902
more perf optimizations
santhnm2 45cc1eb
Fix dtype
santhnm2 6c57910
Fix dtype
santhnm2 68923e2
Fix cuda graph
santhnm2 0066e42
Clean up CUDA graph
santhnm2 0a235ff
Clean up graph
santhnm2 0d6243a
Fix graph
santhnm2 5bfd41c
Debugging
santhnm2 95ed9a0
Fix graphs
santhnm2 4b1d093
Fix graphs
santhnm2 7913032
Remove debug prints
santhnm2 819ec4a
Merge remote-tracking branch 'upstream/main' into mtp_perf_v2
santhnm2 a39dffd
Clean up
santhnm2 9e6680c
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 b1e44cf
Merge branch 'mtp_perf_v2' into mtp_perf
santhnm2 9bd8aa4
Add unit test
santhnm2 e8690f8
Linting
santhnm2 89c4ced
Zero out padded values in test
santhnm2 610d434
Fix test
santhnm2 0656373
fix test
santhnm2 93f5a34
Fix test
santhnm2 bd8c4b9
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 bbbb92d
Fix test
santhnm2 24d481e
Fix tests
santhnm2 e872366
Fix tests
santhnm2 aacefb4
Linting
santhnm2 ac2048c
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 13122e7
Mark flaky tests
santhnm2 8dacd28
Merge with main
santhnm2 8d3a063
Fix rewind_kv_cache_bug
santhnm2 210415a
Move MTP cuda graph warmup inline
santhnm2 bcebf41
Linting
santhnm2 c82b70a
Add Triton tests
santhnm2 3a3bbdc
Linting
santhnm2 c05d8a5
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 43de7aa
NVLS all gathers
santhnm2 78fba30
Fix gather_output
santhnm2 4c0ac1a
Expand graph scope
santhnm2 03d52ab
Update tests
santhnm2 4abc573
Fix graph manager
santhnm2 d8b08ac
Fix chaining
santhnm2 836a914
Fix keys
santhnm2 bb597ca
Inference RS
santhnm2 3c3b23b
Fix flag
santhnm2 5391d6c
Merge remote-tracking branch 'upstream/main' into mtp_perf_v2
santhnm2 bc214cf
Formatting
santhnm2 2a77e5b
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 91029d4
Merge branch 'mtp_perf_v2' into mtp_perf
santhnm2 c0f4fff
Clean up and fixes
santhnm2 c39c8e5
Fix tests
santhnm2 084e605
Make MTP cuda graphs test engine level
santhnm2 432722d
Fix dtype
santhnm2 1a59fcc
Cast model to params_dtype
santhnm2 2f8e0c8
Fixes
santhnm2 57771f9
Try again to fix tests
santhnm2 314a2ee
Fix 0 token count graph error
santhnm2 f0a87e0
Fix casting
santhnm2 168028b
Fix input dtype
santhnm2 3d61c13
Add depth
santhnm2 43176d1
Delete cuda graphs for eager
santhnm2 d76da59
Fix delete cuda graphs
santhnm2 248d761
Fix deletion path
santhnm2 187b00d
Linting
santhnm2 4676931
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 aa1e8f6
Cleanup
santhnm2 1bc4a87
Linting
santhnm2 f97e3f6
Remove dead code
santhnm2 fcb2ec3
Clean up resources for test_parallel_inference
santhnm2 259c1f5
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 f2504c5
Fix EP in test
santhnm2 3d21b5d
Fix is_expert arg
santhnm2 fa22202
Fix case where weight is None
santhnm2 389d83e
Add depth kwarg
santhnm2 0825420
Nits
santhnm2 ffdefc5
Fix normalization in unit test
santhnm2 dbd9f20
Fix bias in unit test
santhnm2 9b6a2c4
Add no-op get_extra_state function
santhnm2 343c6bb
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 79c871d
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 2d8b102
Linting
santhnm2 6d2ace5
Address review comments
santhnm2 53bd51d
Address reviewer comments
santhnm2 157c0c5
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 8f5e7d3
Revert unnecessary changes
santhnm2 6d1f0b6
Linting
santhnm2 c5a2d5f
Clean up MTP graph manager tracking
santhnm2 b074789
More cleanup
santhnm2 ed0e028
Restore docstring
santhnm2 ffe4780
Fix skip_weight_param_allocation
santhnm2 3119052
Revert InferenceColumnParallelLinear changes
santhnm2 d991265
Linting
santhnm2 9a72316
Standardize inference_reduce_scatter_first_dim API
santhnm2 0c4b56c
Merge
santhnm2 071ab7f
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 ba6de96
Merge with main
santhnm2 4e56aee
Remove is_expert setting
santhnm2 9d90639
Address reviewer comments
santhnm2 adafdd1
Linting
santhnm2 1700d02
Add mtp_utils_pytorch.py
santhnm2 d75ed23
Linting
santhnm2 4bab4be
setup_method -> setup_cls
santhnm2 0afeb18
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 61bde08
Add classmethod decorator
santhnm2 652c9b5
More test NCCL cleanup
santhnm2 06e6907
Refactor test_dynamic_engine.py
santhnm2 96a01ea
clear nvte env vars
santhnm2 77473c3
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 490165b
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 8eb0c80
Fix test
santhnm2 ac467b1
Refactor test_text_generation_controller.py
santhnm2 3e28f26
Explicitly delete cuda graphs
santhnm2 15d0e83
Add cuda graph deletion to static engine
santhnm2 b4d0d69
More cleanup
santhnm2 1a8f7da
Add clear_nvte_env_vars to static test
santhnm2 9f64401
Add garbage collection to test_moe_inference.py
santhnm2 f193a01
Linting
santhnm2 5266808
Merge remote-tracking branch 'upstream/main' into mtp_perf
santhnm2 c6ad4f3
More cuda graph deletion
santhnm2 2cae20f
Fix copyright
santhnm2 f537865
Revert unnecessary cuda graph changes
santhnm2 84b1ef5
Merge with main
santhnm2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
244 changes: 244 additions & 0 deletions
244
megatron/core/inference/text_generation_controllers/mtp_utils_pytorch.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,244 @@ | ||
| # Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
|
|
||
| import torch | ||
|
|
||
|
|
||
| def rewind_kv_cache( | ||
| accepted_counts, | ||
| prefill_status, | ||
| last_kv_block_offset, | ||
| kv_length_offsets, | ||
| kv_block_counts, | ||
| last_kv_block_id, | ||
| kv_block_ids, | ||
| num_speculative_tokens, | ||
| block_size_tokens, | ||
| num_active_requests=None, | ||
| ): | ||
| """Update the KV cache bookkeeping for speculative decoding. | ||
|
|
||
| After forward pass with speculative tokens, some tokens may be rejected. | ||
| This function "rewinds" the KV cache bookkeeping to reflect only the accepted tokens. | ||
|
|
||
| When speculative tokens are rejected, we need to: | ||
| 1. Update kv_length_offsets (total sequence length) | ||
| 2. Update last_kv_block_offset (position within last block) | ||
| 3. If rewinding crosses a block boundary: | ||
| - Reduce kv_block_counts | ||
| - Update last_kv_block_id to point to the previous block | ||
| - Clear the entry in kv_block_ids for the released block | ||
|
|
||
| Mutates the input tensors in-place. | ||
|
|
||
| Returns (blocks_to_release, remove_mask). | ||
| """ | ||
| N = accepted_counts.shape[0] | ||
| if num_active_requests is None: | ||
| num_active_requests = N | ||
|
|
||
| blocks_to_release = torch.empty_like(last_kv_block_id) | ||
| remove_mask = torch.empty(N, device=accepted_counts.device, dtype=torch.bool) | ||
|
|
||
| for i in range(N): | ||
| if i >= num_active_requests: | ||
| blocks_to_release[i] = 0 | ||
| remove_mask[i] = False | ||
| continue | ||
|
|
||
| accepted = accepted_counts[i].item() | ||
| prefill = prefill_status[i].item() | ||
| last_offset = last_kv_block_offset[i].item() | ||
| kv_length = kv_length_offsets[i].item() | ||
| block_count = kv_block_counts[i].item() | ||
| last_block = last_kv_block_id[i].item() | ||
|
|
||
| # Number of tokens to rewind (rejected speculative tokens). | ||
| # For prefill requests, no speculative tokens were forwarded through the model, | ||
| # so there is nothing to rewind. | ||
| num_to_rewind = 0 if prefill == 1 else num_speculative_tokens - accepted | ||
|
|
||
| # Save the original offset BEFORE modifying to correctly detect block boundary crossing. | ||
| # A request crosses back to a previous block if: original_offset - num_to_rewind < 0 | ||
| diff = last_offset - num_to_rewind | ||
| remove = diff < 0 | ||
|
|
||
| # Update the offsets | ||
| new_offset = diff % block_size_tokens | ||
| last_kv_block_offset[i] = new_offset | ||
| kv_length_offsets[i] = kv_length - num_to_rewind | ||
|
|
||
| # For requests that crossed back to a previous block, we need to: | ||
| # 1. Reduce the block count by 1 | ||
| # 2. Get the block ID to release (current last_kv_block_id) | ||
| # 3. Update last_kv_block_id to point to the previous block | ||
| # 4. Clear the entry in kv_block_ids for the released block | ||
| # 5. Release the block back to the allocator | ||
| blocks_to_release[i] = last_block | ||
|
|
||
| # Reduce block counts for requests that crossed back | ||
| new_block_count = block_count - 1 if remove else block_count | ||
| kv_block_counts[i] = new_block_count | ||
|
|
||
| # Update last_kv_block_id to point to the previous block (at index new_count - 1) | ||
| prev_idx = max(new_block_count - 1, 0) | ||
| prev_block_id = kv_block_ids[i, prev_idx].item() | ||
| last_kv_block_id[i] = prev_block_id if remove else last_block | ||
|
|
||
| # Clear the released block entry (at index new_count, which was the old last block) | ||
| scatter_idx = min(new_block_count, kv_block_ids.shape[1] - 1) | ||
| if remove: | ||
| kv_block_ids[i, scatter_idx] = -1 | ||
|
|
||
| remove_mask[i] = remove | ||
|
|
||
| return blocks_to_release, remove_mask | ||
|
|
||
|
|
||
| # pylint: disable=line-too-long | ||
| def verify_speculative_tokens( | ||
| input_tokens, output_tokens, num_decode_requests, num_prefill_requests, num_speculative_tokens | ||
| ): | ||
| """Verify speculative tokens against input tokens and compute acceptance. | ||
|
|
||
| Creates an accepted tokens mask where: | ||
| - For prefill requests, the token is always accepted. | ||
| - For decode requests, the first token (base token) is always accepted, then we compare | ||
| sampled tokens with input tokens and accept consecutive matches. | ||
| Then finds the index of the last accepted token per request. | ||
|
|
||
| Example (assume 1, 2, and 0 spec tokens are accepted in the first 3 decode requests): | ||
| input_tokens_required: [ a5 a6s a7s | b3 b4s b5s | c6 c7s c8s | d2 | e4 ] # Size 11 | ||
| Output tokens [ a6o a7o a8o | b40 b5o b6o | c7o c8o c9o | d3o | e5o ] | ||
| Output tokens right shift [ d3o a6o a7o | a8o b40 b5o | b6o c7o c8o | c9o | d3o ] | ||
| Accepted tokens mask [ 1 1 0 | 1 1 1 | 1 0 0 | 1 | 1 ] | ||
| Last one indices [ 1 | 5 | 6 | 9 | 10 ] | ||
|
|
||
| Returns: | ||
| tuple: (last_one_indices, accepted_tokens_mask, input_tokens) where | ||
| last_one_indices contains the index of the last accepted token per request. | ||
| """ | ||
| if input_tokens.ndim == 2: | ||
| input_tokens = input_tokens.squeeze(0) | ||
|
|
||
| stride = num_speculative_tokens + 1 | ||
| active_request_count = num_decode_requests + num_prefill_requests | ||
| decode_len = num_decode_requests * stride | ||
|
|
||
| # Initialize mask with False to prevent boundary bleed | ||
| accepted_tokens_mask = torch.zeros_like(input_tokens, dtype=torch.bool) | ||
|
|
||
| # Safe decode token verification without cross-batch boundary contamination | ||
| decode_mask_2d = None | ||
| if num_decode_requests > 0: | ||
| decode_inputs = input_tokens[:decode_len].reshape(num_decode_requests, stride) | ||
| decode_outputs = output_tokens[:decode_len].reshape(num_decode_requests, stride) | ||
|
|
||
| # Shift outputs right by 1 *within* each request to align sampled tokens with input targets | ||
| decode_outputs_shifted = decode_outputs.roll(1, dims=1) | ||
| decode_mask_2d = decode_inputs == decode_outputs_shifted | ||
| # The first token (base token) is always accepted | ||
| decode_mask_2d[:, 0] = True | ||
| # Enforce consecutive acceptance: cummin propagates False to the right | ||
| decode_mask_2d = decode_mask_2d.cummin(dim=1).values | ||
| accepted_tokens_mask[:decode_len] = decode_mask_2d.flatten() | ||
|
|
||
| # Make all prefill tokens accepted | ||
| if num_prefill_requests > 0: | ||
| accepted_tokens_mask[decode_len:] = True | ||
|
|
||
| last_one_indices = torch.full( | ||
| (active_request_count,), -1, device=input_tokens.device, dtype=torch.long | ||
| ) | ||
|
|
||
| if num_decode_requests > 0: | ||
| # Summing the consecutive mask gives the count; subtract 1 for the local index | ||
| local_last_indices = decode_mask_2d.sum(dim=1) - 1 | ||
| row_offsets = torch.arange(num_decode_requests, device=input_tokens.device) * stride | ||
| last_one_indices[:num_decode_requests] = row_offsets + local_last_indices | ||
|
|
||
| if num_prefill_requests > 0: | ||
| prefill_valid = torch.nonzero(accepted_tokens_mask[decode_len:]).squeeze(-1) + decode_len | ||
| last_one_indices[num_decode_requests:] = prefill_valid | ||
|
|
||
| return last_one_indices, accepted_tokens_mask, input_tokens | ||
|
|
||
|
|
||
| # pylint: disable=line-too-long | ||
| def prepare_next_forward_pass( | ||
| num_decode_requests, | ||
| output_tokens, | ||
| required_logit_indices, | ||
| last_one_indices, | ||
| accepted_tokens_mask, | ||
| input_tokens, | ||
| sampled_tokens_buf, | ||
| last_accepted_seq_buf, | ||
| accepted_tokens_per_request, | ||
| accepted_token_counts, | ||
| num_speculative_tokens, | ||
| ): | ||
| """Prepare data for the next forward pass after speculative token verification. | ||
|
|
||
| For each active request: | ||
| - Store the final sampled tokens for the next forward pass. | ||
| - Store the last accepted positions in the packed sequence for serial | ||
| MTP computation after verification. | ||
|
|
||
| For decode requests, extract accepted tokens and counts: | ||
| input_tokens_required: [ a5 a6s a7s | b3 b4s b5s | c6 c7s c8s | d2 | e4 ] | ||
| Accepted tokens mask [ 1 1 0 | 1 1 1 | 1 0 0 | 1 | 1 ] | ||
| Accepted tokens [ [a6s -1] | [b4s b5s] | [-1 -1] ] # Only decode requests (prefill defaults to -1) | ||
| Accepted token counts [ 1 | 2 | 0 ] # Prefill defaults to 0 | ||
|
|
||
| Writes results into the pre-allocated buffers provided by the caller. | ||
| """ | ||
| active_request_count = last_one_indices.shape[0] | ||
| stride = num_speculative_tokens + 1 | ||
|
|
||
| for pid in range(active_request_count): | ||
| idx = last_one_indices[pid].item() | ||
|
|
||
| # Store the final sampled tokens for the next forward pass. | ||
| sampled_tokens_buf[pid] = output_tokens[idx] | ||
|
|
||
| # Store the last accepted positions in the packed sequence for serial | ||
| # MTP computation after verification. | ||
| last_accepted_seq_buf[pid] = required_logit_indices[idx] | ||
|
|
||
| # Extract accepted tokens and counts for decode requests. | ||
| # For prefill it is always set to 1. For decode, the first token is always accepted, | ||
| # then we compare with input tokens and accept the next tokens if its a match. | ||
| if pid < num_decode_requests: | ||
| base = pid * stride | ||
| # Skip the first token of every decode request (i.e a5, b3, c6) | ||
| for s in range(num_speculative_tokens): | ||
| pos = base + 1 + s | ||
| if accepted_tokens_mask[pos]: | ||
| accepted_tokens_per_request[pid, s] = input_tokens[pos] | ||
| else: | ||
| accepted_tokens_per_request[pid, s] = -1 | ||
|
|
||
| count = 0 | ||
| for s in range(num_speculative_tokens): | ||
| if accepted_tokens_per_request[pid, s].item() != -1: | ||
| count += 1 | ||
| accepted_token_counts[pid] = count | ||
|
|
||
|
|
||
| def mamba_state_selective_copy( | ||
| intermediate_states, current_states, prefill_status, state_idx, accepted_counts, num_layers | ||
| ): | ||
| """Mamba speculative rewind state update. | ||
|
|
||
| For each decode request, copies | ||
| `intermediate[layer, slot, accepted_count, ...]` → | ||
| `current[layer, slot, ...]` for every Mamba layer. | ||
| """ | ||
| N = prefill_status.shape[0] | ||
| for i in range(N): | ||
| if prefill_status[i].item() == 1: | ||
| continue | ||
| slot = state_idx[i].item() | ||
| accepted = accepted_counts[i].item() | ||
| for layer in range(num_layers): | ||
| current_states[layer, slot] = intermediate_states[layer, slot, accepted] |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.