Generated banner for Nemo Automodel Model Onboarding
Generated logo for Nemo Automodel Model Onboarding

Nemo Automodel Model Onboarding

NVIDIAOfficialCreated by NVIDIA

  • Skill
  • Workflow
  • Software development
  • Apache-2.0
  • Medium risk

From the author

Guide for onboarding new model architectures into NeMo AutoModel, including architecture discovery, implementation patterns, registration, and validation.
— the description by NVIDIA

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured, five-phase procedure for integrating new model architectures—including LLMs, MoE models, and VLMs—into NeMo AutoModel. It covers the entire lifecycle from initial architecture discovery and implementation of custom components to registration, validation, and parity testing. The guide emphasizes correct weight mapping, state-dict adaptation, capability declaration, and rigorous numerical verification to ensure seamless integration with existing infrastructure.

What it does

  • Guides the classification and implementation of new model architectures.
  • Provides checklists for MoE state-dict adaptation and VLM integration.
  • Details the registration process for new models and custom configurations.
  • Defines requirements for weight tying and precision-sensitive parameter handling.
  • Outlines mandatory unit and parity testing phases for numerical equivalence.
  • Specifies documentation and coverage update requirements.

When to use it

  • When adding support for a new Hugging Face causal LM architecture.
  • When mapping MoE router and expert weights from a Hugging Face checkpoint.
  • When registering a new model class or custom configuration in NeMo AutoModel.
  • When modifying existing model architecture support, such as custom layers or adapters.

Advantages

  • Ensures consistent implementation patterns across different model types.
  • Reduces integration errors through mandatory parity and numerical equivalence testing.
  • Provides clear guidance on handling complex architectures like MoE and VLM.
  • Standardizes the registration and capability declaration process.

Usage examples

“How do I add support for a new Hugging Face causal LM architecture?”
The agent will guide you through fetching the config.json, creating the directory structure, implementing the model class, registering it in the registry, and adding the required unit and parity tests.
“What steps are needed to map MoE router and expert weights?”
The agent will provide the MoE state-dict adapter checklist, ensuring you map router tensors, preserve expert index order, and verify mappings with numerical equivalence tests.
“How do I register a new model class in NeMo AutoModel?”
The agent will instruct you to update the MODEL_ARCH_MAPPING in the registry and, if necessary, register custom configurations in the _CUSTOM_CONFIG_REGISTRATIONS dictionary.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/nvidia/nemo-automodel-model-onboarding/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip nemo-automodel-model-onboarding.zip -d .agents/skills

Documentation by NVIDIA

Adding Model Support to NeMo AutoModel

Purpose

This skill guides implementation of new model architectures in NeMo AutoModel. Follow the five phases in order.

<!-- NVSkills signature refresh requested after PR #2998 (2026-07-31). -->

Instructions

When answering an onboarding question, keep the response in this order:

  1. Classify the architecture from config.json.

  2. Name the exact implementation files under components/models/<name>/.

  3. Identify registry and optional custom-config updates.

  4. State the validation tests that must be added before full checkpoint use.

For conceptual onboarding questions, answer from this skill without opening the pattern files unless the user asks you to edit code. Mention pattern filenames as references, then give the direct checklist.

Use direct action verbs: classify the model, name the files, map the weights, register the class, and add tests. Do not discuss distributed strategy, launcher configuration, or general recipe authoring unless the user explicitly connects it to onboarding a new architecture.

Examples

Use these compact answer patterns for common questions:

  • Dense causal LM: classify as dense only when architectures contains a ForCausalLM class and expert fields such as num_local_experts, n_routed_experts, or num_experts_per_tok are absent. Create components/models/<name>/model.py and __init__.py; add state_dict_adapter.py only for checkpoint weight conversion and config.py only if needed. Register MODEL_ARCH_MAPPING in _transformers/registry.py, add example YAML, and add tiny-config unit tests plus layer-equivalence tests for rewritten layers.

  • MoE state dict: identify expert fields in config.json, reference moe-patterns.md, map router tensors separately, preserve routed-expert index order, map routed experts, shared experts, and gate/up/down projections, add adapter key-map tests and tiny-config numerical equivalence tests, and do not rely only on from_pretrained() or silent tensor reshapes.

  • VLM onboarding: classify as VLM only when vision_config, text_config, and a ForConditionalGeneration architecture are present. Reference vlm-patterns.md and existing VLM implementations such as mistral4, kimivl, or kimi_k25_vl; check text backbone, vision tower, projector, processor assumptions, text and vision checkpoint compatibility (adapter mappings when needed), registry registration, and tiny image-text tests before full checkpoints. Do not treat VLM onboarding as a pure causal-LM path or skip processor/image tests.

For MoE state-dict and VLM questions, apply the checklists in Sections 2.4 and 2.5.

Routing Boundary

Use this skill only when the user is adding or modifying model architecture support: model files, custom layers, state-dict adapters, Hugging Face config mapping, registry entries, or model capability flags.

Do not use this skill for standalone training recipe YAML questions about optimizers, datasets, schedulers, validation datasets, or trainer wiring unless they are explicitly part of onboarding a new model architecture. Those recipe questions belong to the nemo-automodel-recipe-development skill.

In-scope examples:

  • "Add support for a new Hugging Face causal LM architecture."

  • "Map MoE router and expert weights from a Hugging Face checkpoint."

  • "Register a new model class in NeMo AutoModel."

Out-of-scope examples:

  • "Write a finetuning recipe YAML with optimizer and dataset sections."

  • "Choose FSDP2, DDP, tensor parallel, or context parallel settings."

  • "Configure Slurm, SkyPilot, containers, mounts, or launch dispatch."

Phase 1: Discovery

Before writing code, gather information about the target model.

1.1 Fetch HuggingFace config.json

Download the model's config.json from the HuggingFace Hub (or use AutoConfig.from_pretrained). Key fields to extract:

  • architectures -- determines the class name and registration key (e.g., "LlamaForCausalLM", "Qwen3MoeForCausalLM", "Mistral3ForConditionalGeneration")

  • model_type -- used for custom config registration in _CUSTOM_CONFIG_REGISTRATIONS if HF does not have a built-in config class

  • hidden_size, intermediate_size, num_hidden_layers, num_attention_heads, num_key_value_heads -- sizing

  • vocab_size -- needed for tiny test configs

  • tie_word_embeddings -- the saved setting in each supported checkpoint; do not infer it from a bare config constructor

  • hidden_act -- activation function (e.g., "silu" for SwiGLU)

1.2 Determine model type
TypeIndicatorsPattern file
Dense LLMForCausalLM in architectures, no expert fieldsllm-patterns.md
MoE LLMn_routed_experts, num_local_experts, num_experts_per_tok in configmoe-patterns.md
VLMForConditionalGeneration in architectures, has vision_config + text_configvlm-patterns.md
1.3 Check for existing similar architectures

Look in components/models/ for architectures with similar attention or MLP patterns:

components/models/
  llama/           # Standard GQA + SwiGLU with separate HF-compatible projections
  qwen2/           # Same as Llama but with attention bias + QKV bias
  baichuan/        # ALiBi attention variant
  deepseek_v3/     # MLA attention + MoE (DeepSeek-style grouped experts)
  mistral4/        # MLA + MoE + VLM (Pixtral vision)
  kimivl/          # DeepSeek-V3 backbone + MoonVit vision
  kimi_k25_vl/     # Updated KimiVL with different projector
  qwen3_moe/       # Qwen3 with MoE layers
  nemotron_v3/     # Hybrid mamba-attention
1.4 Identify custom components

Check whether the model needs:

  • Custom attention: GQA (standard), MLA (DeepSeek/Mistral4), sliding window, bidirectional

  • Custom RoPE: Standard (Llama), YaRN scaling, NTK-aware, complex-number (DeepSeek)

  • Custom normalization: RMSNorm (standard), LayerNorm, different eps values

  • Custom MLP: SwiGLU (standard), GeGLU, ReLU-squared, MoE routing

  • Custom config class: Needed only if HF AutoConfig cannot parse the model's config.json (check auto_map field)

1.5 Note dimensions for test config

For unit tests, create a tiny config. Target: ~1M parameters or less.

# Example tiny config for a Llama-like model:
tiny_config = LlamaConfig(
    hidden_size=64,
    intermediate_size=128,
    num_hidden_layers=2,
    num_attention_heads=4,
    num_key_value_heads=2,
    vocab_size=256,
    max_position_embeddings=128,
)

Phase 2: Implementation

2.1 Create directory structure
components/models/<name>/
  __init__.py
  model.py
  state_dict_adapter.py # Only if HF weight names or tensor layouts need conversion
  config.py            # Only if HF config is insufficient
  layers.py            # Only for MoE / MLA / other non-standard layers
  rope_utils.py        # Only for custom RoPE
2.2 Implementation order

Implement files in dependency order:

  1. config.py (if needed) -- Custom PretrainedConfig subclass

  2. rope_utils.py (if needed) -- RoPE implementation

  3. layers.py (if needed) -- Attention, MLP, decoder block classes

  4. model.py -- The main ForCausalLM (or ForConditionalGeneration) class

  5. state_dict_adapter.py (if needed) -- HF weight conversion. Treat checkpoint I/O performance as part of the implementation, and evaluate the low-memory DCP capability as described in Section 2.6.

  6. init.py -- Re-export the main model class

See the pattern files for detailed implementation guidance:

Most custom models need state_dict_adapter.py for HF weight conversion. Omit the file and attribute only when HF names and tensor layouts already match across supported backend/config variants, as in Llama, Qwen2, and Qwen3. Weight tying remains the model's responsibility (Section 2.3).

2.3 Causal LM weight tying

Every registered model class with a causal lm_head must:

  • Declare tie_word_embeddings_support: TieSupport as BOTH, TIED_ONLY, or UNTIED_ONLY.

  • Call reject_unsupported_tie_word_embeddings(type(self), config) at the top of __init__, using the original config before unwrapping text_config or thinker_config.

Only classes with no causal LM head may be explicitly exempted from the registry test.

Choose the policy from the implementation and the actual supported checkpoint configs, not from a bare config constructor:

  • BOTH: tied and untied configurations are both supported.

  • TIED_ONLY: only a tied configuration is supported.

  • UNTIED_ONLY: only an untied configuration is supported.

Runtime helpers must treat TIED_ONLY and UNTIED_ONLY as authoritative and only resolve a per-checkpoint config flag for BOTH. All current BOTH VLMs honor the outer tie_word_embeddings flag, so do not add a model-specific resolver until a supported BOTH model actually requires another config path.

For BOTH and TIED_ONLY, always declare _tied_weights_keys and implement tie_weights() with the actual lm_head and input-embedding FQNs. Do not rely on inherited Hugging Face tying, and re-tie after any language-model swap.

Add policy-specific tests:

  • BOTH: tied aliases; untied does not alias.

  • TIED_ONLY: tied aliases; untied is rejected.

  • UNTIED_ONLY: weights stay separate; tied is rejected.

Do not tie architectures with intentionally separate heads, asymmetric vocab sizes, or stages that do not own both tensors.

For from_pretrained, the checkpoint's saved tie_word_embeddings value is authoritative, even for BOTH. The NeMoAuto* bridge rejects flips in either direction. A model-owned from_pretrained that bypasses that bridge must call reject_tie_word_embeddings_flip(checkpoint_config, requested_config, model_class_name).

2.4 MoE state-dict adapter checklist

For MoE models, verify all weights below. When their HF and native layouts differ, the adapter must explicitly map:

  • Router weights, including gate bias or correction-bias tensors when the Hugging Face model has them.

  • Expert weights, preserving expert index order across local and routed experts.

  • Gate/up/down projections, including combined or split projection layouts.

  • Shared experts separately from routed experts when the architecture has both.

Add tests that assert expected key mappings and run numerical equivalence with tiny configs before trying full checkpoints.

For MoE PEFT, preserve pretrained router correction biases and verify adapter save/reload/resume using the router-bias checks.

Do not use these shortcuts:

  • Do not validate the adapter only by calling from_pretrained().

  • Do not accept missing or extra expert keys without an explicit mapping reason.

  • Do not change dtype, transpose dimensions, or reshape tensors unless the HF and NeMo layouts require it and a test proves the conversion is reversible.

  • Do not skip router or shared-expert tests because dense-layer tests pass.

2.5 VLM onboarding checklist

For VLMs, confirm the Hugging Face config has vision_config and text_config and that architectures points to a conditional-generation class. Start from the closest VLM pattern file, usually vlm-patterns.md, and compare existing implementations such as mistral4, kimivl, or kimi_k25_vl.

The implementation should explicitly cover:

  • Text backbone, vision tower, projector, and processor or image preprocessing assumptions.

  • Checkpoint compatibility for both text and vision modules, with adapter mappings where needed.

  • Registration of the ForConditionalGeneration class in _transformers/registry.py.

  • Tiny tests that exercise image-text inputs and verify checkpoint load/export, plus adapter round-trip when present.

2.6 Checkpoint I/O performance

Treat checkpoint performance as an implementation requirement, not a later optimization. For every new or materially changed state-dict adapter, evaluate both latency and peak host/device memory for loading and for any save or export path the change affects. In particular:

  • Avoid full-checkpoint or model-sized temporary copies when tensors can load directly into final model storage or be transformed in bounded parts.

  • Keep distributed reads and conversions rank-local when a rank needs only its shard; do not materialize a global tensor on every rank unnecessarily.

  • Avoid repeated tensor merges, copies, full-heap garbage collections, shard scans, or file opens inside model-sized loops.

  • Record representative before/after latency and peak-memory evidence for an optimized path, including the model, dtype, backend, and topology.

Every adapter must evaluate supports_low_memory_dcp_load. Set _supports_low_memory_dcp_load = True only when most checkpoint tensors write directly into final model storage and every remaining allocating conversion has a small, bounded temporary footprint for every runtime variant that reports support. Keep it false when a backend, topology, dtype, quantization mode, or model option requires model-sized rebuilding. A false value selects the safe fallback; it does not mean checkpoint loading is unsupported.

An opt-in needs focused tests that write sentinel values through direct destinations and prove the final model storage changes, bound any allocating conversions, and verify unsafe runtime variants report the capability as false. This storage test is also a correctness requirement: a false positive can cause the adapter to treat a temporary tensor as loaded in place and skip rebuilding the real parameter.

2.7 Register in registry

Add the model to MODEL_ARCH_MAPPING in _transformers/registry.py:

# In _transformers/registry.py
MODEL_ARCH_MAPPING = OrderedDict([
    # ... existing entries ...
    (
        "NewModelForCausalLM",
        ("nemo_automodel.components.models.new_model.model", "NewModelForCausalLM"),
    ),
])

If the model has a custom config class with auto_map in its config.json, also register in _CUSTOM_CONFIG_REGISTRATIONS:

_CUSTOM_CONFIG_REGISTRATIONS: Dict[str, Tuple[str, str]] = {
    # ... existing entries ...
    "new_model": ("nemo_automodel.components.models.new_model.configuration", "NewModelConfig"),
}
2.8 Declare capabilities and precision-sensitive params

Every class registered in MODEL_ARCH_MAPPING must declare parallelism capabilities, either with a static nested ModelCapabilities dataclass or a variant-aware get_capabilities(cls, config) method. Pick exactly one pattern. Capabilities should reflect recipe YAMLs that have been validated end to end.

If the model has precision-sensitive parameters such as Mamba A_log / dt_bias, MoE sigmoid gate bias, attention-sink bias, or per-head scale, declare _keep_in_fp32_modules_strict so sharding keeps those params in fp32 compute. See capabilities-and-precision.md for examples, variant dispatch rules, and frozen-submodule dtype guidance.


Phase 3: Onboarding Example Config

This phase is only for adding a minimal example config that proves the newly onboarded architecture can load and run. Use nemo-automodel-recipe-development for general recipe authoring or existing recipe modifications.

3.1 Create example YAML config

Create an example config under examples/llm_finetune/<name>/ (or examples/vlm_finetune/<name>/):

For new full-parameter Adam/AdamW examples, set model.dtype: float32. See training precision for compute precision and other training modes.

model:
  _target_: nemo_automodel.NeMoAutoModelForCausalLM.from_pretrained
  pretrained_model_name_or_path: <org>/<model-name>
  dtype: float32

trainer:
  max_steps: 100
  gradient_clip_val: 1.0
  accumulate_grad_batches: 1

# ... data, optimizer config ...
3.2 Verify model loads

Test that the model loads from a HuggingFace checkpoint:

from nemo_automodel import NeMoAutoModelForCausalLM

model = NeMoAutoModelForCausalLM.from_pretrained("<org>/<model-name>")
3.3 Test with tiny config first

Before using full-size models, verify with a tiny config (1-2 layers, small hidden dim) to catch shape mismatches early.

Phase 4: Tests

Create tests/unit_tests/models/<name>/ and cover the checks below before loading full checkpoints:

  • Forward-shape smoke test with a tiny config.

  • State-dict adapter round-trip, when present: from_hf -> to_hf preserves mapped names, shapes, dtypes, and values.

  • HF load/export and native save/reload preserve weights and ties, with or without an adapter; see tests/unit_tests/checkpoint/test_native_hf_state_dict.py.

  • Layer equivalence tests for every rewritten attention, MLP, normalization, RoPE, or MoE layer. Use the model dtype from config, identical seeded weights, identical inputs, and dtype-appropriate torch.allclose tolerances.

  • Short functional test that verifies loss decreases over a few training steps.


Phase 5: Documentation

5.1 Update model coverage page

Edit the appropriate file in docs/model-coverage/:

  • LLM/MoE: docs/model-coverage/llm/index.md

  • VLM: docs/model-coverage/vlm/index.md

Add a row with the model name, supported features (TP, PP, FSDP, LoRA, QLoRA), and any limitations.


Phase 6: Parity Testing

After implementation and unit tests are complete, run the full parity-testing workflow to verify that the new model produces numerically equivalent results to the reference HuggingFace implementation.

Run three levels of comparison:

  1. State-dict round-trip: load a reference HuggingFace checkpoint, convert it into the NeMo AutoModel layout, export it back, and verify that all mapped tensors match the reference names, shapes, dtypes, and values within the expected tolerance.

  2. Component-level parity: compare rewritten attention, MLP, normalization, RoPE, and MoE components against the HuggingFace implementation with fixed seeds and identical dtype.

  3. End-to-end forward pass: run the full NeMo AutoModel and HuggingFace model on the same tokenized input and compare logits, hidden states, and loss.

Do not skip this phase. A model that passes unit tests can still diverge from HF due to subtle weight-conversion bugs, backend differences, or RoPE mismatches that only surface in a full parity comparison.


Key Files Reference

FilePurpose
_transformers/registry.pyMODEL_ARCH_MAPPING and _CUSTOM_CONFIG_REGISTRATIONS
components/models/common/__init__.pyExports BackendConfig, HFCheckpointingMixin, and backend construction utilities
components/models/llama/model.pySeparate attention and MLP projections with HF-compatible weights
components/checkpoint/state_dict_adapter.pyOptional StateDictAdapter conversion contract
components/models/common/hf_checkpointing_mixin.pyHFCheckpointingMixin for save/load
components/models/common/utils.pyBackendConfig, initialize_rms_norm_module, initialize_linear_module, get_rope_config
components/moe/config.pyMoEConfig dataclass
components/moe/fsdp_mixin.pyMoEFSDPSyncMixin for distributed expert handling
components/moe/layers.pyMoE layer, MLP (dense) for MoE blocks
components/moe/experts.pyGroupedExperts, GroupedExpertsDeepEP, GroupedExpertsTE

Checklist

  • [ ] Fetched and analyzed config.json from HuggingFace

  • [ ] Determined model type (dense LLM / MoE / VLM)

  • [ ] Identified custom components (attention, RoPE, normalization, MLP)

  • [ ] Created components/models/<name>/ directory

  • [ ] Implemented config.py (if custom config needed)

  • [ ] Implemented layers.py (if custom layers needed)

  • [ ] Implemented rope_utils.py (if custom RoPE needed)

  • [ ] Implemented model.py with HFCheckpointingMixin

  • [ ] Implemented state_dict_adapter.py if needed; evaluated checkpoint latency and peak memory per Section 2.6

  • [ ] For adapters, evaluated supports_low_memory_dcp_load; any opt-in proves direct destinations reach model storage, bounds allocating conversions, and reports False for unsafe variants

  • [ ] Implemented init.py with re-export

  • [ ] Registered in MODEL_ARCH_MAPPING in _transformers/registry.py

  • [ ] Registered custom config in _CUSTOM_CONFIG_REGISTRATIONS (if applicable)

  • [ ] Declared ModelCapabilities nested dataclass (static) OR get_capabilities(cls, config) classmethod (variant dispatch, e.g. ERNIE-4.5 MoE vs dense) — never both, never neither

  • [ ] Declared TieSupport and called the constructor guard for every class with a causal lm_head (or added an explicit no-head exemption) -- see §2.3

  • [ ] Added explicit _tied_weights_keys and tie_weights() for BOTH / TIED_ONLY, plus policy-specific alias and rejection tests -- see §2.3

  • [ ] Guarded any model-owned from_pretrained that bypasses the NeMoAuto* bridge against checkpoint flips -- see §2.3

  • [ ] Created example YAML config

  • [ ] Verified model loads via NeMoAutoModelForCausalLM.from_pretrained()

  • [ ] Created unit tests (forward shape, state_dict round-trip)

  • [ ] Verified MoE PEFT router freezing and adapter save/reload/resume per LoRA for MoE

  • [ ] Declared _keep_in_fp32_modules_strict for every intrinsically-fp32 param (SSM A_log/dt_bias, Mamba D when reference-fp32, MoE gate bias, attention-sink bias, scale, …) — see §2.8

  • [ ] Created layer equivalence tests for every rewritten layer (matching model dtype)

  • [ ] Created functional tests (training loss decreases)

  • [ ] Updated docs/model-coverage page

  • [ ] Ran state-dict round-trip, component parity, and E2E forward-pass parity checks

  • [ ] Set ModelClass = <Name>ForCausalLM at module bottom

Files

moe-patterns.md

MoE (Mixture of Experts) Implementation Patterns

This document describes the patterns for adding a Mixture-of-Experts model to NeMo AutoModel.

Reference implementations:

  • components/models/deepseek_v3/model.py -- canonical MoE LLM with MLA + grouped experts

  • components/models/mistral4/model.py -- MoE with Mistral4-specific MLA and VLM wrapping

  • components/models/qwen3_moe/model.py -- Qwen3 MoE variant


MoE vs Dense: Key Differences

AspectDense LLMMoE LLM
Base classesHFCheckpointingMixin, PreTrainedModelHFCheckpointingMixin, nn.Module, MoEFSDPSyncMixin
MLPModel-owned dense MLP, such as separate SwiGLU projectionsMLP for dense layers, MoE for expert layers
ConfigHF config onlyHF config + MoEConfig dataclass
State dict adapterOmit for HF-compatible weights; model-owned conversion for different layoutsCustom adapter for differing expert layouts, often with MoESplitExpertsStateDictMixin
ParallelismFSDP + TP + PPFSDP + TP + PP + Expert Parallelism (EP)
Forward signatureStandard HF-compatibleCustom (no CausalLMOutputWithPast, returns raw tensors)

MoE implementations also need explicit initialize_weights() handling, initialize_linear_module() for lm_head, gate bias updates via update_moe_gate_bias(), and variable-length thd sequence packing through squeeze_input_for_thd.


MoEFSDPSyncMixin (Required)

Every MoE ForCausalLM class MUST inherit MoEFSDPSyncMixin. This mixin manages FSDP synchronization state during training with gradient accumulation:

from nemo_automodel.components.moe.fsdp_mixin import MoEFSDPSyncMixin

class NewMoEForCausalLM(HFCheckpointingMixin, nn.Module, MoEFSDPSyncMixin):
    ...

The mixin provides:

  • prepare_for_grad_accumulation(pp_enabled=False) -- defers sync/resharding at start of accumulation

  • prepare_for_final_backward(pp_enabled=False) -- enables sync/resharding for the last backward pass

It also integrates with patched_backward_maybe_with_nosync for pipeline parallelism support.

Note: the mixin accesses self.backend.enable_fsdp_optimizations to check whether optimizations are active.


MoEConfig Dataclass

MoE models need a MoEConfig in addition to the HF config. Build it from the HF config fields:

from nemo_automodel.components.moe.config import MoEConfig

def _build_moe_config(config) -> MoEConfig:
    return MoEConfig(
        dim=config.hidden_size,
        inter_dim=config.intermediate_size,
        moe_inter_dim=config.moe_intermediate_size,
        n_routed_experts=config.n_routed_experts,       # or config.num_local_experts
        n_shared_experts=config.n_shared_experts,        # 0 if no shared experts
        n_activated_experts=config.num_experts_per_tok,
        n_expert_groups=config.n_group,                  # grouping for top-k routing
        n_limited_groups=config.topk_group,
        train_gate=True,
        gate_bias_update_factor=1e-3,
        score_func="sigmoid",          # or "softmax", "softmax_with_bias"
        route_scale=config.routed_scaling_factor,
        aux_loss_coeff=0,              # auxiliary load balancing loss coefficient
        norm_topk_prob=config.norm_topk_prob,
    )

All MoE models support moe_overrides — a dict that merges into the default MoEConfig construction:

model = NeMoAutoModelForCausalLM.from_pretrained("model", moe_overrides={"gate_bias_update_factor": 1e-4})
Model MoE defaults
Modelscore_funcaux_loss_coeffgate_bias_update_factore_score_correction_bias
DeepSeek V3sigmoid01e-3yes
DeepSeek V3.2sigmoid01e-3yes
GLM4 MoEsigmoid0.01e-3yes
GLM4 MoE Litesigmoid0.01e-3yes
GLM MoE DSAsigmoid0.01e-3yes
Mistral4softmax_with_bias01e-3yes
MiniMax-M2sigmoid01e-3yes
NemotronV3sigmoid0.00.0yes
Qwen3 MoEsoftmaxfrom config (0.0)0.0no
Qwen3.5 MoEsoftmaxfrom config (0.001)0.0no
Qwen3 Nextsoftmaxfrom config0.0no
Qwen3 Omni MoEsoftmaxfrom config (0.0)0.0no
Qwen3 VL MoEsoftmaxfrom config (0.0)0.0no
Gemma4 MoEsoftmax0.00.0no
GPT-OSSsoftmaxfrom config0no
Step3.5config-dependent0.00.0no

Models with e_score_correction_bias=yes use gate bias updates for load balancing. Models with e_score_correction_bias=no may use auxiliary loss (aux_loss_coeff) instead. All defaults are overridable via moe_overrides.

MoEConfig fields
FieldTypeDescription
dimintModel hidden dimension
inter_dimintDense MLP intermediate dimension
moe_inter_dimintExpert MLP intermediate dimension
n_routed_expertsintTotal number of routed experts
n_shared_expertsintNumber of shared (always-active) experts
n_activated_expertsintNumber of experts activated per token
n_expert_groupsintNumber of expert groups for group-limited routing
n_limited_groupsintTop-k groups selected in group-limited routing
train_gateboolWhether the gating network is trainable
gate_bias_update_factorfloatStep size for auxiliary gate bias updates
score_funcstrRouting score function: "sigmoid", "softmax", "softmax_with_bias"
route_scalefloatScaling factor for routed expert outputs
aux_loss_coefffloatCoefficient for auxiliary load balancing loss
norm_topk_probboolWhether to normalize top-k routing probabilities
router_biasboolWhether router has bias (default False)
expert_biasboolWhether expert MLPs have bias (default False)
expert_activationstrExpert activation: "swiglu", "quick_geglu", "relu2"
moe_latent_size`intNone`

Block Class with Conditional MLP

MoE models typically have dense MLP for the first first_k_dense_replace layers and MoE for the rest:

from nemo_automodel.components.moe.layers import MLP, MoE

class Block(nn.Module):
    def __init__(self, layer_idx: int, config, moe_config: MoEConfig, backend: BackendConfig):
        super().__init__()
        self.self_attn = SomeAttention(config, backend)

        # Dense layers use standard MLP, expert layers use MoE
        if layer_idx < config.first_k_dense_replace:
            self.mlp = MLP(config.hidden_size, config.intermediate_size, backend.linear)
        else:
            self.mlp = MoE(moe_config, backend)

        self.input_layernorm = initialize_rms_norm_module(
            backend.rms_norm, config.hidden_size, eps=config.rms_norm_eps,
        )
        self.post_attention_layernorm = initialize_rms_norm_module(
            backend.rms_norm, config.hidden_size, eps=config.rms_norm_eps,
        )
        self.layer_idx = layer_idx

    def forward(self, x, freqs_cis, attention_mask=None, padding_mask=None, **attn_kwargs):
        # Convert attention_mask to padding_mask for MoE routing
        if attention_mask is not None and padding_mask is None:
            padding_mask = attention_mask.bool().logical_not()

        # Pre-norm attention
        attn_out = self.self_attn(
            x=self.input_layernorm(x), freqs_cis=freqs_cis,
            attention_mask=attention_mask, **attn_kwargs,
        )
        x = x + attn_out

        # Pre-norm MLP (dense or MoE)
        mlp_out = self._mlp(x=self.post_attention_layernorm(x), padding_mask=padding_mask)
        x = x + mlp_out
        return x

    def _mlp(self, x, padding_mask):
        if isinstance(self.mlp, MLP):
            return self.mlp(x)
        else:
            assert isinstance(self.mlp, MoE)
            return self.mlp(x, padding_mask)  # MoE needs padding_mask for routing

    def init_weights(self, buffer_device):
        for norm in (self.input_layernorm, self.post_attention_layernorm):
            norm.reset_parameters()
        self.self_attn.init_weights(buffer_device)
        self.mlp.init_weights(buffer_device)
Why padding_mask matters

The MoE routing layer uses padding_mask to exclude padding tokens from expert assignment. Without it, padding tokens consume expert capacity and waste compute.


MoE Model Backbone

MoE backbones use nn.ModuleDict (not nn.ModuleList) for layers:

class NewMoEModel(nn.Module):
    def __init__(self, config, backend: BackendConfig, *, moe_config=None):
        super().__init__()
        self.config = config
        self.backend = backend
        self.moe_config = moe_config or _build_moe_config(config)

        self.embed_tokens = nn.Embedding(
            config.vocab_size, config.hidden_size,
            dtype=get_dtype(config.torch_dtype, torch.bfloat16),
        )

        # ModuleDict (not ModuleList) for layer-indexed access
        self.layers = torch.nn.ModuleDict()
        for layer_id in range(config.num_hidden_layers):
            self.layers[str(layer_id)] = Block(layer_id, config, self.moe_config, backend)

        self.norm = initialize_rms_norm_module(
            backend.rms_norm, config.hidden_size, eps=config.rms_norm_eps,
        )

        # Precompute RoPE frequencies
        self.max_seq_len = config.max_position_embeddings
        rope_theta, rope_scaling, _ = get_rope_config(config)
        self.register_buffer(
            "freqs_cis",
            precompute_freqs_cis(config.qk_rope_head_dim, self.max_seq_len, rope_theta, rope_scaling),
            persistent=False,
        )
Gate bias update

MoE models with adaptive router correction-bias buffers need an update_moe_gate_bias() method. The update must honor a zero gate_bias_update_factor, including when PEFT freezes bias adaptation:

def update_moe_gate_bias(self) -> None:
    with torch.no_grad():
        for _, block in self.layers.named_children():
            if isinstance(block.mlp, MoE) and block.mlp.gate.bias_update_factor > 0:
                block.mlp.gate.update_bias()

ForCausalLM for MoE

class NewMoEForCausalLM(HFCheckpointingMixin, nn.Module, MoEFSDPSyncMixin):
    # Pin every intrinsically-fp32 param (sigmoid-gate bias here; add SSM A_log/dt_bias,
    # attention-sink bias, scale, etc. as applicable). See capabilities-and-precision.md.
    # This keeps
    # them in fp32 compute even under fp32 master weights.
    _keep_in_fp32_modules_strict = ["e_score_correction_bias"]  # if using sigmoid routing

    @classmethod
    def from_config(cls, config, moe_config=None, backend=None, **kwargs):
        return cls(config, moe_config, backend, **kwargs)

    def __init__(self, config, moe_config=None, backend=None, **kwargs):
        super().__init__()
        self.config = config
        self.backend = backend or BackendConfig()
        # Router scoring is selection-sensitive and HF computes it in fp32; default the gate to
        # fp32 unless the user overrides it (the gate is tiny, so the cost is negligible).
        if self.backend.gate_precision is None:
            self.backend.gate_precision = torch.float32
        self.model = NewMoEModel(config, backend=self.backend, moe_config=moe_config)
        self.lm_head = initialize_linear_module(
            self.backend.linear, config.hidden_size, config.vocab_size, bias=False,
        )
        if self.backend.enable_hf_state_dict_adapter:
            self.state_dict_adapter = NewMoEStateDictAdapter(
                self.config, self.model.moe_config, self.backend,
                dtype=get_dtype(config.torch_dtype, torch.bfloat16),
            )

    def forward(self, input_ids, *, position_ids=None, attention_mask=None,
                padding_mask=None, **attn_kwargs):
        # Handle thd format for variable-length sequences
        if "qkv_format" in attn_kwargs and attn_kwargs["qkv_format"] == "thd":
            input_ids, position_ids, padding_mask, attn_kwargs = squeeze_input_for_thd(
                input_ids, position_ids, padding_mask, attn_kwargs,
            )
            attention_mask = None

        logits = self.model(
            input_ids, position_ids=position_ids,
            attention_mask=attention_mask, padding_mask=padding_mask,
            **attn_kwargs,
        )
        logits = self.lm_head(logits) if self.lm_head else logits

        if "qkv_format" in attn_kwargs and attn_kwargs["qkv_format"] == "thd":
            logits = logits.unsqueeze(0)
        return logits

    def update_moe_gate_bias(self) -> None:
        with torch.no_grad():
            for _, block in self.model.layers.named_children():
                if isinstance(block.mlp, MoE) and block.mlp.gate.bias_update_factor > 0:
                    block.mlp.gate.update_bias()

    @torch.no_grad()
    def initialize_weights(self, buffer_device=None, dtype=torch.bfloat16):
        buffer_device = buffer_device or torch.device(f"cuda:{torch.cuda.current_device()}")
        # Sampling init directly in bf16 distorts its variance/mean and causes exploding
        # first-step gradients in from-scratch pretraining; yield_fp32_model samples in fp32
        # and casts back to the resident dtype on exit (see its docstring for the full rationale).
        with yield_fp32_model(self, dtype):
            with buffer_device:
                self.model.init_weights(buffer_device=buffer_device)
                final_out_std = self.config.hidden_size ** -0.5
                cutoff_factor = 3
                if self.lm_head is not None:
                    nn.init.trunc_normal_(
                        self.lm_head.weight, mean=0.0, std=final_out_std,
                        a=-cutoff_factor * final_out_std, b=cutoff_factor * final_out_std,
                    )

ModelClass = NewMoEForCausalLM

Expert Parallelism

Expert parallelism (EP) distributes experts across devices. The MoE layer handles this internally via moe_mesh:

from nemo_automodel.components.moe.experts import GroupedExperts, GroupedExpertsDeepEP
GroupedExperts implementations
ImplementationImportDescription
GroupedExpertscomponents/moe/experts.pyDefault: torch grouped matmul
GroupedExpertsTEcomponents/moe/experts.pyTransformer Engine grouped GEMM
GroupedExpertsDeepEPcomponents/moe/experts.pyDeepEP all-to-all dispatch

The MoE layer selects the implementation based on BackendConfig and available libraries.


State Dict Adapter for MoE

MoE state dict adapters must handle expert weight conversion. The base pattern uses MoESplitExpertsStateDictMixin:

from nemo_automodel.components.checkpoint.state_dict_adapter import StateDictAdapter
from nemo_automodel.components.moe.state_dict_mixin import MoESplitExpertsStateDictMixin

class NewMoEStateDictAdapter(MoESplitExpertsStateDictMixin, StateDictAdapter):
    def __init__(self, config, moe_config, backend, dtype=torch.bfloat16):
        self.config = config
        self.moe_config = moe_config
        self.backend = backend
        self.dtype = dtype

    def from_hf(self, hf_state_dict, **kwargs):
        # 1. Rename keys from HF format to NeMo format
        # 2. Handle expert weight stacking (HF stores per-expert, NeMo stores grouped)
        # 3. Handle MLA weight conversion if applicable
        custom_state_dict = {}
        # ... key renaming and conversion logic ...
        return custom_state_dict

    def to_hf(self, state_dict, exclude_key_regex=None, **kwargs):
        # Reverse of from_hf
        hf_state_dict = {}
        # ... key renaming and conversion logic ...
        return hf_state_dict
Expert weight format

HF stores expert weights as separate tensors per expert:

model.layers.N.mlp.experts.0.gate_proj.weight
model.layers.N.mlp.experts.0.up_proj.weight
model.layers.N.mlp.experts.0.down_proj.weight
model.layers.N.mlp.experts.1.gate_proj.weight
...

NeMo AutoModel stores them as stacked tensors:

model.layers.N.mlp.experts.gate_up_weight   # [n_experts, 2*moe_inter_dim, dim]
model.layers.N.mlp.experts.down_weight       # [n_experts, dim, moe_inter_dim]

The state dict adapter must stack/unstack these during conversion.


LoRA for MoE

MoE models support LoRA through specialized expert-aware implementations:

from nemo_automodel.components.moe.experts import GroupedExperts
# LoRA variants available:
# - GroupedExpertsLoRA (standard LoRA on expert weights)
# - GroupedExpertsDeepEPLoRA (LoRA with DeepEP dispatch)

LoRA on MoE typically targets the gate/up/down projections within experts, as well as attention projections (q, k, v, o).

Preserve router state during LoRA

LoRA trains adapter weights while freezing the base model's parameters. Some MoE routers also update correction-bias buffers outside the optimizer; freezing parameters does not stop those updates.

NeMo AutoModel's PEFT checkpoints save only adapter weights, so reloading the base model plus adapters would lose any bias changes. Keep these pretrained biases fixed during LoRA training. For the shared Gate with a pretrained e_score_correction_bias, use:

model:
  moe_overrides:
    gate_bias_update_factor: 0.0
    force_e_score_correction_bias: true

Do not add a correction bias to architectures whose base checkpoint has none. Keep full-training defaults unchanged. See the checkpointing guide for checkpoint limitations.

Before advertising MoE PEFT support, verify:

  • Both backbone and outer-model update hooks honor disabled adaptation.

  • With real routing and nonzero pretrained biases, adapters change while the biases stay fixed; fake balanced routing can hide this failure.

  • Actual adapter checkpoint save/reload/resume matches uninterrupted losses, model parameters and buffers, and optimizer state.


Imports Summary

# Core MoE components
from nemo_automodel.components.moe.config import MoEConfig
from nemo_automodel.components.moe.fsdp_mixin import MoEFSDPSyncMixin
from nemo_automodel.components.moe.layers import MoE, MLP
from nemo_automodel.components.moe.experts import GroupedExperts, GroupedExpertsDeepEP, GroupedExpertsTE

# Common model components
from nemo_automodel.components.models.common import (
    BackendConfig,
    get_rope_config,
    initialize_linear_module,
    initialize_rms_norm_module,
)
from nemo_automodel.components.models.common.hf_checkpointing_mixin import HFCheckpointingMixin

# Utilities
from nemo_automodel.components.utils.model_utils import squeeze_input_for_thd
from nemo_automodel.shared.utils import dtype_from_str as get_dtype

Checklist (MoE-Specific)

In addition to the standard checklist in SKILL.md:

  • [ ] Built MoEConfig from HF config fields

  • [ ] Implemented Block class with conditional MLP (dense for early layers, MoE for later)

  • [ ] ForCausalLM inherits MoEFSDPSyncMixin

  • [ ] ForCausalLM has update_moe_gate_bias() method

  • [ ] ForCausalLM has initialize_weights() method

  • [ ] initialize_weights() wraps init in yield_fp32_model(self, dtype) (fp32 init, then cast)

  • [ ] Gate defaults to fp32 (gate_precision) if routing is precision-sensitive (sigmoid/softmax)

  • [ ] Forward handles thd format via squeeze_input_for_thd

  • [ ] Forward passes padding_mask to MoE layers

  • [ ] State dict adapter handles expert weight stacking/unstacking

  • [ ] _keep_in_fp32_modules_strict set for every intrinsically-fp32 param (sigmoid-gate bias e_score_correction_bias, and any others) — see capabilities-and-precision.md

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

Security

What our scanner noticed· Medium risk

  • Large base64-encoded blobs

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
NVIDIA
Publisher
NVIDIA (Official)
Source
NVIDIA/skills · skills/nemo-automodel-model-onboarding · commit fce3cca
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE-APACHE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by NVIDIA.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download nemo-automodel-model-onboarding.zip