Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ddf7e6b
fix nemotron pcg
vedantjh2 Mar 4, 2026
16b5e3d
resolver merge conflict
vedantjh2 Mar 5, 2026
da7daae
fix rmerge conflict
vedantjh2 Mar 5, 2026
02b793e
fix lint
vedantjh2 Mar 5, 2026
3319a59
Merge branch 'main' into vjhaveri/fix_nemotron
vedantjh2 Mar 5, 2026
2459197
make consistent
vedantjh2 Mar 5, 2026
84a5e5c
Merge branch 'main' into vjhaveri/fix_nemotron
zminglei Mar 5, 2026
4044f86
fix lint
vedantjh2 Mar 5, 2026
c890ee8
Merge branch 'vjhaveri/fix_nemotron' of https://github.com/vedantjh2/…
vedantjh2 Mar 5, 2026
b27fe6d
Merge branch 'main' into vjhaveri/fix_nemotron
vedantjh2 Mar 5, 2026
92e0065
put back comment
vedantjh2 Mar 5, 2026
fbc5332
Merge branch 'vjhaveri/fix_nemotron' of https://github.com/vedantjh2/…
vedantjh2 Mar 5, 2026
fecd99c
Merge branch 'main' into vjhaveri/fix_nemotron
vedantjh2 Mar 5, 2026
92fd376
use is_in_piecewise_cuda_graph instead of if forward_batch.forward_m…
vedantjh2 Mar 5, 2026
4893a0e
Merge branch 'vjhaveri/fix_nemotron' of https://github.com/vedantjh2/…
vedantjh2 Mar 5, 2026
e3dcd06
Merge branch 'main' into vjhaveri/fix_nemotron
vedantjh2 Mar 6, 2026
98675b1
Merge branch 'main' into vjhaveri/fix_nemotron
vedantjh2 Mar 6, 2026
6445583
fix nemotron moe
vedantjh2 Mar 7, 2026
522c707
Merge branch 'vjhaveri/fix_nemotron' of https://github.com/vedantjh2/…
vedantjh2 Mar 7, 2026
29092e0
fix failing test
vedantjh2 Mar 9, 2026
5a0ad2f
Merge branch 'main' into vjhaveri/fix_nemotron
vedantjh2 Mar 10, 2026
6faece1
add guard for pcg
vedantjh2 Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions python/sglang/srt/model_executor/model_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2195,24 +2195,34 @@ def init_piecewise_cuda_graphs(self):
self.moe_layers = []
self.moe_fusions = []
for layer in language_model.model.layers:
attn_layer = None
if hasattr(layer, "self_attn"):
if hasattr(layer.self_attn, "attn"):
self.attention_layers.append(layer.self_attn.attn)
attn_layer = layer.self_attn.attn
elif hasattr(layer.self_attn, "attn_mqa"):
# For DeepSeek model
self.attention_layers.append(layer.self_attn.attn_mqa)
attn_layer = layer.self_attn.attn_mqa
# For hybrid model
elif hasattr(layer, "attn"):
self.attention_layers.append(layer.attn)
attn_layer = layer.attn
elif hasattr(layer, "linear_attn"):
if hasattr(layer.linear_attn, "attn"):
self.attention_layers.append(layer.linear_attn.attn)
attn_layer = layer.linear_attn.attn
else:
self.attention_layers.append(layer.linear_attn)
attn_layer = layer.linear_attn
# For InternVL model
elif hasattr(layer, "attention"):
if hasattr(layer.attention, "attn"):
self.attention_layers.append(layer.attention.attn)
attn_layer = layer.attention.attn
# For NemotronH and similar hybrid models using 'mixer' attribute
elif hasattr(layer, "mixer"):
if hasattr(layer.mixer, "attn"):
attn_layer = layer.mixer.attn
elif hasattr(layer, "_forward_mamba"):
# Mamba layer with split op support - store the layer itself
attn_layer = layer
# attn_layer is None for non-attention layers (e.g. Mamba, MLP-only)

Copy link
Copy Markdown
Collaborator

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 into attention_layers for mamba models or change the field name in nemotron_h to make it compatible with existing logic.

Copy link
Copy Markdown
Contributor Author

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).

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Collaborator

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_layer capturing logic for all other models as well, i.e. we now are adding None for non-attention decoder layers. But theoretically it should be fine since all other models should only have attention/linear-attention decoder layers. Only nemotron_h is having MLP in their decoder layer. We can verify it by checking if all PCG CIs of other models pass.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree @zminglei

self.attention_layers.append(attn_layer)

moe_block = None
moe_fusion = None
Expand Down
81 changes: 64 additions & 17 deletions python/sglang/srt/models/nemotron_h.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
*,
Expand All @@ -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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 + (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Loading