Skip to content

Add NemotronH hybrid Mamba2-Attention model (Nemotron-3-Nano-4B) - #3464

Open
OmarAzizi wants to merge 3 commits into
mlc-ai:mainfrom
OmarAzizi:add-nemotron-h-hybrid-mamba2-model
Open

Add NemotronH hybrid Mamba2-Attention model (Nemotron-3-Nano-4B)#3464
OmarAzizi wants to merge 3 commits into
mlc-ai:mainfrom
OmarAzizi:add-nemotron-h-hybrid-mamba2-model

Conversation

@OmarAzizi

@OmarAzizi OmarAzizi commented Mar 27, 2026

Copy link
Copy Markdown

Summary

Adds support for the NemotronH architecture family to MLC-LLM, resolving #3458.

The first supported model is nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16.

Architecture

NemotronH uses a hybrid layer pattern defined by hybrid_override_pattern in the HF config:

  • 21 Mamba2 layers: Selective state space model, linear complexity, no KV cache
  • 17 MLP layers: Standard non-gated feed-forward
  • 4 Attention layers: GQA with RoPE, uses KV cache

Only the 4 attention layers allocate KV cache slots, keeping memory usage low.

Implementation

Pure Relax/TVM - no custom CUDA kernels required.

The Mamba2 chunked SSD algorithm is implemented entirely in TVM Relax + TIR ops, translated from the reference torch_forward path in HuggingFace transformers.

Key implementation decisions:

  • hybrid_override_pattern string (M/*/- chars) auto-converted to layers_block_type list in __post_init__.
  • HF uses backbone.* weight prefix instead of model.*; handled in loader.
  • HF field names differ from our conventions (conv_kernel, n_groups, chunk_size, etc); mapped in __post_init__.
  • Hybrid runtime path via kHybrid KVStateKind: provides both create_paged_kv_cache (for the 4 attention layers) and create_rnn_state (for the Mamba2 layers, currently a stub matching this PR's prefill-only design — proper recurrent caching is a follow-up).
  • Mamba2 forward is prefill-only for now. Decode works but recomputes from scratch. Future PR can add proper RNN state caching similar to Add Qwen3.5 GatedDeltaNet hybrid model + kHybrid KVStateKind #3449.
  • SSM math runs in fp32 internally (matching HF reference) — fp16 overflows for NemotronH's high dt_bias/A_log values, producing NaN in dA_cumsum and breaking inference.

Changes

  • python/mlc_llm/model/nemotron_h/: new model package
  • python/mlc_llm/model/model.py: register nemotron_h model type
  • python/mlc_llm/model/model_preset.py: add nemotron_h_4b preset
  • python/mlc_llm/interface/compile.py: route nemotron_h through the kHybrid runtime path
  • tests/python/model/test_nemotron_h.py: 4 pytest tests

Test Plan

Unit tests (no download required):

python3 -m pytest tests/python/model/test_nemotron_h.py -v

Full pipeline test (requires ~8GB download, GPU recommended):

# 1. Download weights
python3 -c "
from huggingface_hub import snapshot_download
snapshot_download(
    'nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16',
    local_dir='./dist/models/nemotron-h-4b',
    ignore_patterns=['*.md', 'LICENSE', '*.txt']
)
"

# 2. Convert weights to MLC format (q4f16_1)
python3 -m mlc_llm convert_weight \
    ./dist/models/nemotron-h-4b \
    --quantization q4f16_1 \
    --model-type nemotron_h \
    -o ./dist/nemotron-h-4b-q4f16_1-MLC

# 3. Generate chat config
python3 -m mlc_llm gen_config \
    ./dist/models/nemotron-h-4b \
    --quantization q4f16_1 \
    --model-type nemotron_h \
    --conv-template chatml \
    -o ./dist/nemotron-h-4b-q4f16_1-MLC

# 4. Compile for CUDA. --overrides keep activation buffers small enough for a 6 GB card (my RTX 3050)
python3 -m mlc_llm compile \
    ./dist/nemotron-h-4b-q4f16_1-MLC/mlc-chat-config.json \
    --device cuda \
    --overrides "max_batch_size=1;prefill_chunk_size=512;context_window_size=4096" \
    -o ./dist/nemotron-h-4b-q4f16_1-MLC/lib-cuda.so

# 5. Chat
python3 -m mlc_llm chat \
    ./dist/nemotron-h-4b-q4f16_1-MLC \
    --model-lib ./dist/nemotron-h-4b-q4f16_1-MLC/lib-cuda.so \
    --device cuda \
    --overrides "prefill_chunk_size=512;context_window_size=4096"

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the NemotronH hybrid Mamba2-Attention-MoE architecture to MLC-LLM. The changes include the core model implementation, a weight loader for HuggingFace models, a 4B model preset, and unit tests. Feedback identifies an unused variable in the Mamba2 mixer, suggests consolidating redundant dtype casts for better clarity, and recommends using identity checks for booleans in the test suite.

proj = self.in_proj(hidden_states)
i0 = self.intermediate_size
i1 = i0 + self.conv_dim
i2 = i1 + self.num_heads

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The variable i2 is defined but never used. It can be removed to improve code clarity.

Comment on lines +285 to +287
x = op.reshape(x.astype(dtype), (b, s, self.num_heads, self.head_dim))
B = op.reshape(B, (b, s, self.n_groups, self.ssm_state_size))
C = op.reshape(C, (b, s, self.n_groups, self.ssm_state_size))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There are several inconsistencies and redundancies in dtype casting within this function. To improve clarity and correctness, I suggest consolidating the dtype conversions.

  1. B and C are used as float32 and then cast to dtype multiple times later. It would be cleaner to cast them here along with x.
  2. There are several redundant .astype(dtype) calls on tensors that are already of dtype (e.g., on L at line 324, dx_t at line 346, and new_states at line 384).

By casting B and C here, you can then remove the subsequent casts on G (line 333), B_t (line 342), and C_t (line 383), as well as the redundant casts mentioned above.

Suggested change
x = op.reshape(x.astype(dtype), (b, s, self.num_heads, self.head_dim))
B = op.reshape(B, (b, s, self.n_groups, self.ssm_state_size))
C = op.reshape(C, (b, s, self.n_groups, self.ssm_state_size))
x = op.reshape(x.astype(dtype), (b, s, self.num_heads, self.head_dim))
B = op.reshape(B.astype(dtype), (b, s, self.n_groups, self.ssm_state_size))
C = op.reshape(C.astype(dtype), (b, s, self.n_groups, self.ssm_state_size))

assert config.mamba_d_conv == 4 # mapped from conv_kernel
assert config.mamba_chunk_size == 256 # mapped from chunk_size
assert config.mamba_n_groups == 8 # mapped from n_groups
assert config.mamba_conv_bias == True # mapped from use_conv_bias

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For better adherence to Python style conventions, it's recommended to use is for boolean identity checks instead of ==.

Suggested change
assert config.mamba_conv_bias == True # mapped from use_conv_bias
assert config.mamba_conv_bias is True # mapped from use_conv_bias

@babusid

babusid commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Hey @OmarAzizi thanks for the PR! I'll try and get some tests run / code review done on this soon.

Just note that it sometimes takes us a second to get model PRs merged (our team is small), so just keep an eye on this branch, and make sure to rebase it off of main in case anything relevant ends up landing before it. This way, it'll keep it easy to merge in.

Thanks again!

@babusid

babusid commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Also out of curiosity, when you were developing this PR, did you feel like there were any particular sources of significant boilerplate that you had to contend with? Just collecting some feedback from the community!

@OmarAzizi

OmarAzizi commented Mar 30, 2026

Copy link
Copy Markdown
Author

Hi @babusid, thanks for the heads up, I have been busy lately, but I’ll make sure to keep the branch rebased on main 👍

Regarding testing, I actually tried running the model on a more powerful machine (both CPU and CUDA), but I ran into quite a few errors during execution. I’m still digging into them and would really appreciate your help.

@OmarAzizi

Copy link
Copy Markdown
Author

Also out of curiosity, when you were developing this PR, did you feel like there were any particular sources of significant boilerplate that you had to contend with? Just collecting some feedback from the community!

Regarding this, to be honest, I’m still quite new to these types of projects, so I’m not entirely sure.

@babusid

babusid commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Hi @babusid, thanks for the heads up, I have been busy lately, but I’ll make sure to keep the branch rebased on main 👍

Regarding testing, I actually tried running the model on a more powerful machine (both CPU and CUDA), but I ran into quite a few errors during execution. I’m still digging into then abd would really appreciate your help.

no worries! I'll try to take a look at the branch sometime this week.

OmarAzizi added 3 commits May 8, 2026 17:48
Resolves mlc-ai#3458

Adds support for the NemotronH (nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16)
hybrid architecture to MLC-LLM.

Architecture:
- 42 layers: 21 Mamba2 SSM + 17 MLP + 4 GQA Attention
- Layer pattern from hybrid_override_pattern in HF config
- Only 4 attention layers use KV cache slots (not all 42)
- Pure Relax/TVM implementation of chunked SSD algorithm
- No custom CUDA kernels required

Changes:
- python/mlc_llm/model/nemotron_h/: new model package
  - nemotron_h_model.py: NemotronHConfig, NemotronHMamba2Mixer,
    NemotronHAttention, NemotronHMLP, NemotronHMoE (for future 30B),
    NemotronHDecoderLayer, NemotronHForCausalLM
  - nemotron_h_loader.py: HF weight name mapping (backbone.* prefix)
- python/mlc_llm/model/model.py: register nemotron_h model type
- python/mlc_llm/model/model_preset.py: add nemotron_h_4b preset
- tests/python/model/test_nemotron_h.py: 5 pytest tests

Implementation notes:
- HF config uses hybrid_override_pattern (M/*/- chars) instead of
  layers_block_type list; __post_init__ handles both formats
- HF field names differ from our internal names (conv_kernel ->
  mamba_d_conv, n_groups -> mamba_n_groups, etc); mapped in __post_init__
- HF weight prefix is backbone.* not model.*; handled in loader
- MoE layer type included for forward compatibility with 30B Nano model
  which uses Mamba2+MoE+Attention; not used by 4B model

Tested:
- All 5 pytest tests pass
- mlc_llm convert_weight: 263/263 params, 3.97B total, 2.08GB q4f16_1
- mlc_llm gen_config: succeeds
- mlc_llm compile --device cpu: succeeds, lib.so generated

Signed-off-by: OmarAzizi <oalazizi75@gmail.com>
…cal fixes

After mlc-ai#3449 introduced kHybrid KVStateKind, NemotronH crashes at runtime
with an unrecognized AttnKind. This PR migrates to the new runtime path
and fixes several numerical bugs in the chunked SSD math that were
causing all inputs to map to a single special token.

Runtime / compile:
- Route nemotron_h through "hybrid" in _infer_kv_state_kind.
- Add create_rnn_state and thread rnn_state through batch_*; update spec.
- Drop layer_partition arg in create_paged_kv_cache (was [0, 42], should
  default to [0, num_attention_layers] = [0, 4]; was the actual cause of
  the AttnKind crash).
- Migrate tir -> tirx (catches up with mlc-ai#3462).

GPU compile (Mamba2-specific):
- Replace concat+split-based dc construction with a direct te.compute
  (avoids buffer aliasing during lowering).
- Hand-schedule inter-chunk state propagation as a T.prim_func with
  tirx.is_scheduled=1; mirrors qwen35's GatedDeltaNet pattern. Dlight's
  GPU reduction rule fails to bind threads when the reduction dim is
  small for short prefills.
- Fix dtype mismatch in causal_conv1d's te.if_then_else.

SSD numerical fixes:
- Add causal mask on intra-chunk L. Without it, exp(cumsum[i] - cumsum[j])
  for j > i blows up to inf and propagates NaN through Y_diag.
- Compute dt/A/dA, Y_diag, Y_off, D skip in fp32 (HF runs SSM in fp32).
  NemotronH's dt_bias up to 33.5 + A_log up to 8.6 makes |dA| ~2e5 which
  overflows fp16.
- Use cumulative LC for inter-chunk decay; raw per-chunk lc didn't
  correspond to any physical decay.
- Fix off-by-one in dc and add causal mask in the chunk dimension.

Validated: 4 unit tests pass; convert_weight, gen_config, compile (CPU +
CUDA) all succeed; GPU inference runs at ~20 tok/s on RTX 3050.

Known follow-up: output is varied real text (no longer a special-token
loop) but still not coherent. Suspect q4f16_1 quantization aggression on
Mamba2's sensitive matrices or one more subtle bug; needs HF transformers
reference comparison to localize.
@OmarAzizi
OmarAzizi force-pushed the add-nemotron-h-hybrid-mamba2-model branch from 7e7286a to 788964c Compare May 8, 2026 16:57
@OmarAzizi

Copy link
Copy Markdown
Author

Hi @babusid! Quick update. I rebased onto main and pushed the fixes (force push since the rebase rewrote history). Sorry for the rebase noise.

I dug into the runtime errors I mentioned. Main one was the kHybrid KVStateKind transition from #3449: NemotronH was still using kv_state_kind=kv_cache, which now fails the runtime's AttnKind check. New commit migrates it to "hybrid", adds create_rnn_state, threads rnn_state through batch_* methods, and removes a buggy layer_partition arg.

Other fixes in the same commit:

  • GPU compile: Dlight couldn't auto-schedule the inter-chunk SSM matmul for short prefills. Replaced with a hand-scheduled T.prim_func, same pattern as qwen35's GatedDeltaNet kernel.
  • SSD numerical bugs: missing causal mask on L, fp16 overflow in dA, and an off-by-one in the inter-chunk decay. These were causing all inputs to deterministically map to one special token. SSM math now runs in fp32 internally to match the HF reference.

Pipeline runs end-to-end on CUDA at ~20 tok/s on my RTX 3050. Output varies per input, but still isn't coherent, likely q4f16_1 being aggressive on Mamba2's sensitive matrices, or one more subtle bug.

I would appreciate your review whenever you have time!

@babusid

babusid commented May 8, 2026

Copy link
Copy Markdown
Contributor

Hi @babusid! Quick update. I rebased onto main and pushed the fixes (force push since the rebase rewrote history). Sorry for the rebase noise.

I dug into the runtime errors I mentioned. Main one was the kHybrid KVStateKind transition from #3449: NemotronH was still using kv_state_kind=kv_cache, which now fails the runtime's AttnKind check. New commit migrates it to "hybrid", adds create_rnn_state, threads rnn_state through batch_* methods, and removes a buggy layer_partition arg.

Other fixes in the same commit:

  • GPU compile: Dlight couldn't auto-schedule the inter-chunk SSM matmul for short prefills. Replaced with a hand-scheduled T.prim_func, same pattern as qwen35's GatedDeltaNet kernel.

  • SSD numerical bugs: missing causal mask on L, fp16 overflow in dA, and an off-by-one in the inter-chunk decay. These were causing all inputs to deterministically map to one special token. SSM math now runs in fp32 internally to match the HF reference.

Pipeline runs end-to-end on CUDA at ~20 tok/s on my RTX 3050. Output varies per input, but still isn't coherent, likely q4f16_1 being aggressive on Mamba2's sensitive matrices, or one more subtle bug.

I would appreciate your review whenever you have time!

Hey sorry for the slow review, it's been a bit hectic. I will try to get to this soon! I will try some known tricks and see if I can spot where a bug might be happening. Question for you - is the output sometimes coherent and sometimes not? Do you observe any crashes?

@OmarAzizi

Copy link
Copy Markdown
Author

Hey sorry for the slow review, it's been a bit hectic. I will try to get to this soon! I will try some known tricks and see if I can spot where a bug might be happening. Question for you - is the output sometimes coherent and sometimes not? Do you observe any crashes?

No problem at all, I have been really busy myself lately. To answer your question, the model output always seems to be incoherent, but I experienced no crashes on my end.

@babusid

babusid commented May 11, 2026

Copy link
Copy Markdown
Contributor

Hey sorry for the slow review, it's been a bit hectic. I will try to get to this soon! I will try some known tricks and see if I can spot where a bug might be happening. Question for you - is the output sometimes coherent and sometimes not? Do you observe any crashes?

No problem at all, I have been really busy myself lately. To answer your question, the model output always seems to be incoherent, but I experienced no crashes on my end.

okay. This could point to numerical issues / errors that still manages to emit token ids within the valid vocabulary range. I will take a look and run some tests, but for your reference we have seen slight errors cause this type of behavior before. A potential source of error (not sure if it's core to this model, just something I've seen before) is incorrect positional encoding leading to incorrect attention outputs. Dumping and numerically comparing tensors to see where divergence happens is a potential strategy to fixing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants