-
Notifications
You must be signed in to change notification settings - Fork 7.3k
Enable Piecewise CUDA Graph for NemotronH Hybrid (Mamba+Attention) Models #19903
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
Changes from 13 commits
ddf7e6b
16b5e3d
da7daae
02b793e
3319a59
2459197
84a5e5c
4044f86
c890ee8
b27fe6d
92e0065
fbc5332
fecd99c
92fd376
4893a0e
e3dcd06
98675b1
6445583
522c707
29092e0
5a0ad2f
6faece1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,8 @@ | |
| import torch | ||
| from torch import nn | ||
|
|
||
| from sglang.srt.compilation.compilation_config import register_split_op | ||
| from sglang.srt.compilation.piecewise_context_manager import get_forward_context | ||
| from sglang.srt.configs import NemotronHConfig | ||
| from sglang.srt.configs.nemotron_h import ATTENTION, MAMBA, MLP, MOE | ||
| from sglang.srt.distributed import ( | ||
|
|
@@ -69,6 +71,7 @@ | |
| is_cuda, | ||
| make_layers, | ||
| ) | ||
| from sglang.srt.utils.custom_op import register_custom_op | ||
| from sglang.utils import logger | ||
|
|
||
| _is_cuda = is_cuda() | ||
|
|
@@ -391,6 +394,23 @@ def __init__( | |
|
|
||
| self.norm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon) | ||
|
|
||
| def _forward_mamba( | ||
| self, hidden_states: torch.Tensor, forward_batch: ForwardBatch | ||
| ) -> torch.Tensor: | ||
| """Core Mamba forward logic, called directly or via split op.""" | ||
| output = torch.empty_like(hidden_states) | ||
| attn_backend = forward_batch.attn_backend | ||
| assert isinstance(attn_backend, HybridLinearAttnBackend) | ||
| assert isinstance(attn_backend.linear_attn_backend, Mamba2AttnBackend) | ||
| attn_backend.linear_attn_backend.forward( | ||
| mixer=self.mixer, | ||
| layer_id=self.layer_id, | ||
| hidden_states=hidden_states, | ||
| output=output, | ||
| use_triton_causal_conv=True, | ||
| ) | ||
| return output | ||
|
|
||
| def forward( | ||
| self, | ||
| *, | ||
|
|
@@ -404,18 +424,13 @@ def forward( | |
| else: | ||
| hidden_states, residual = self.norm(hidden_states, residual) | ||
|
|
||
| output = torch.empty_like(hidden_states) | ||
| attn_backend = forward_batch.attn_backend | ||
| assert isinstance(attn_backend, HybridLinearAttnBackend) | ||
| assert isinstance(attn_backend.linear_attn_backend, Mamba2AttnBackend) | ||
| attn_backend.linear_attn_backend.forward( | ||
| mixer=self.mixer, | ||
| layer_id=self.layer_id, | ||
| hidden_states=hidden_states, | ||
| output=output, | ||
| use_triton_causal_conv=True, # TODO: investigate need of `use_triton_causal_conv` | ||
| ) | ||
| return output, residual | ||
| if forward_batch.forward_mode.is_extend() and get_forward_context() is not None: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use is_in_piecewise_cuda_graph() context |
||
| output = torch.empty_like(hidden_states) | ||
| nemotron_mamba2_with_output(hidden_states, output, self.layer_id) | ||
| return output, residual | ||
| else: | ||
| output = self._forward_mamba(hidden_states, forward_batch) | ||
| return output, residual | ||
|
|
||
|
|
||
| class NemotronHAttention(nn.Module): | ||
|
|
@@ -526,12 +541,12 @@ def forward( | |
|
|
||
|
|
||
| Layers = ( | ||
| NemotronHAttentionDecoderLayer | ||
| | NemotronHMLPDecoderLayer | ||
| | NemotronHMambaDecoderLayer | ||
| | NemotronHMoEDecoderLayer | ||
| NemotronHAttentionDecoderLayer, | ||
| NemotronHMLPDecoderLayer, | ||
| NemotronHMambaDecoderLayer, | ||
| NemotronHMoEDecoderLayer, | ||
| ) | ||
| ALL_DECODER_LAYER_TYPES: dict[str, type[Layers]] = { | ||
| ALL_DECODER_LAYER_TYPES: dict[str, type] = { | ||
| ATTENTION: NemotronHAttentionDecoderLayer, | ||
| MLP: NemotronHMLPDecoderLayer, | ||
| MAMBA: NemotronHMambaDecoderLayer, | ||
|
|
@@ -861,3 +876,35 @@ def load_weights( | |
|
|
||
|
|
||
| EntryClass = [NemotronHForCausalLM] | ||
|
|
||
|
|
||
| @register_custom_op(mutates_args=["output"]) | ||
| @register_split_op() | ||
| def nemotron_mamba2_with_output( | ||
| hidden_states: torch.Tensor, | ||
| output: torch.Tensor, | ||
| layer_id: int, | ||
| ) -> None: | ||
| """Split op for Mamba2 forward in piecewise CUDA graph mode.""" | ||
| context = get_forward_context() | ||
| forward_batch = context.forward_batch | ||
| attention_layers = context.attention_layers | ||
| mamba_layer = attention_layers[layer_id] | ||
|
|
||
| # In piecewise CUDA graph mode, hidden_states may be padded to the | ||
| # captured graph size. Slice to actual token count for Mamba forward. | ||
| attn_backend = forward_batch.attn_backend | ||
| metadata = attn_backend.linear_attn_backend.forward_metadata | ||
| num_actual_tokens = metadata.num_prefill_tokens + ( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does only mamba need this special shape handle, can't we know the exact output shape before?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. During CUDA graph replay, [hidden_states] is padded to the captured graph batch size. Attention handles this naturally via KV cache and masks, but Mamba processes tokens sequentially through conv/SSM states and asserts [num_actual_tokens == projected_states.shape[0]]. The slicing must happen inside the split op (not the caller) because [torch.compile(fullgraph=True)] requires static tensor shapes within each compiled segment — the split op acts as the graph break where we can access runtime metadata. |
||
| metadata.num_decodes * metadata.draft_token_num | ||
| if metadata.is_target_verify | ||
| else metadata.num_decodes | ||
| ) | ||
| if hidden_states.shape[0] != num_actual_tokens: | ||
| hidden_states = hidden_states[:num_actual_tokens] | ||
|
|
||
| ret = mamba_layer._forward_mamba(hidden_states, forward_batch) | ||
|
|
||
| # Copy result back; output may be larger (padded) so only fill actual tokens | ||
| output[:num_actual_tokens].view(ret.shape).copy_(ret) | ||
| return | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need to store non-attention layers as None in
attention_layers? Could we only store attention layers as previously, but we could insert mamba attention layers intoattention_layersfor mamba models or change the field name innemotron_hto make it compatible with existing logic.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
attention_layers is indexed by layer_id in the split ops (e.g., attention_layers[layer_id]). NemotronH's layer_id is the absolute model layer index (0–51), so each position must map correctly. Without None placeholders, layer_id=10 (a Mamba layer) would index into the wrong entry.
For non-hybrid models this is backward-compatible — every entry is an attention layer and no None values appear. Open to alternatives if you have a preferred approach (e.g., using a dict instead of a list).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good design. Should keep it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only concern here is we're changing the
attention_layercapturing logic for all other models as well, i.e. we now are addingNonefor non-attention decoder layers. But theoretically it should be fine since all other models should only have attention/linear-attention decoder layers. Onlynemotron_his havingMLPin their decoder layer. We can verify it by checking if all PCG CIs of other models pass.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree @zminglei