Nemo Automodel Model Onboarding
NVIDIAOfficialCreated by NVIDIA
- Skill
- Workflow
- Software development
- Apache-2.0
- Medium risk
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
Download the skill as a ZIP:
https://autogineer.com/agent-skills/nvidia/nemo-automodel-model-onboarding/downloadUnzip 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:
Classify the architecture from
config.json.Name the exact implementation files under
components/models/<name>/.Identify registry and optional custom-config updates.
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
architecturescontains aForCausalLMclass and expert fields such asnum_local_experts,n_routed_experts, ornum_experts_per_tokare absent. Createcomponents/models/<name>/model.pyand__init__.py; addstate_dict_adapter.pyonly for checkpoint weight conversion andconfig.pyonly if needed. RegisterMODEL_ARCH_MAPPINGin_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, referencemoe-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 onfrom_pretrained()or silent tensor reshapes.VLM onboarding: classify as VLM only when
vision_config,text_config, and aForConditionalGenerationarchitecture are present. Referencevlm-patterns.mdand existing VLM implementations such asmistral4,kimivl, orkimi_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_REGISTRATIONSif HF does not have a built-in config classhidden_size,intermediate_size,num_hidden_layers,num_attention_heads,num_key_value_heads-- sizingvocab_size-- needed for tiny test configstie_word_embeddings-- the saved setting in each supported checkpoint; do not infer it from a bare config constructorhidden_act-- activation function (e.g.,"silu"for SwiGLU)
1.2 Determine model type
| Type | Indicators | Pattern file |
|---|---|---|
| Dense LLM | ForCausalLM in architectures, no expert fields | llm-patterns.md |
| MoE LLM | n_routed_experts, num_local_experts, num_experts_per_tok in config | moe-patterns.md |
| VLM | ForConditionalGeneration in architectures, has vision_config + text_config | vlm-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-attention1.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
AutoConfigcannot parse the model'sconfig.json(checkauto_mapfield)
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 RoPE2.2 Implementation order
Implement files in dependency order:
config.py (if needed) -- Custom
PretrainedConfigsubclassrope_utils.py (if needed) -- RoPE implementation
layers.py (if needed) -- Attention, MLP, decoder block classes
model.py -- The main
ForCausalLM(orForConditionalGeneration) classstate_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.
init.py -- Re-export the main model class
See the pattern files for detailed implementation guidance:
Dense LLM: llm-patterns.md
MoE: moe-patterns.md
VLM: vlm-patterns.md
Capabilities and fp32 precision: capabilities-and-precision.md
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: TieSupportasBOTH,TIED_ONLY, orUNTIED_ONLY.Call
reject_unsupported_tie_word_embeddings(type(self), config)at the top of__init__, using the original config before unwrappingtext_configorthinker_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
ForConditionalGenerationclass 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_hfpreserves 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.allclosetolerances.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.mdVLM:
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:
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.
Component-level parity: compare rewritten attention, MLP, normalization, RoPE, and MoE components against the HuggingFace implementation with fixed seeds and identical dtype.
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
| File | Purpose |
|---|---|
_transformers/registry.py | MODEL_ARCH_MAPPING and _CUSTOM_CONFIG_REGISTRATIONS |
components/models/common/__init__.py | Exports BackendConfig, HFCheckpointingMixin, and backend construction utilities |
components/models/llama/model.py | Separate attention and MLP projections with HF-compatible weights |
components/checkpoint/state_dict_adapter.py | Optional StateDictAdapter conversion contract |
components/models/common/hf_checkpointing_mixin.py | HFCheckpointingMixin for save/load |
components/models/common/utils.py | BackendConfig, initialize_rms_norm_module, initialize_linear_module, get_rope_config |
components/moe/config.py | MoEConfig dataclass |
components/moe/fsdp_mixin.py | MoEFSDPSyncMixin for distributed expert handling |
components/moe/layers.py | MoE layer, MLP (dense) for MoE blocks |
components/moe/experts.py | GroupedExperts, GroupedExpertsDeepEP, GroupedExpertsTE |
Checklist
[ ] Fetched and analyzed
config.jsonfrom 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 reportsFalsefor unsafe variants[ ] Implemented init.py with re-export
[ ] Registered in
MODEL_ARCH_MAPPINGin_transformers/registry.py[ ] Registered custom config in
_CUSTOM_CONFIG_REGISTRATIONS(if applicable)[ ] Declared
ModelCapabilitiesnested dataclass (static) ORget_capabilities(cls, config)classmethod (variant dispatch, e.g. ERNIE-4.5 MoE vs dense) — never both, never neither[ ] Declared
TieSupportand called the constructor guard for every class with a causallm_head(or added an explicit no-head exemption) -- see §2.3[ ] Added explicit
_tied_weights_keysandtie_weights()forBOTH/TIED_ONLY, plus policy-specific alias and rejection tests -- see §2.3[ ] Guarded any model-owned
from_pretrainedthat bypasses theNeMoAuto*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_strictfor every intrinsically-fp32 param (SSMA_log/dt_bias, MambaDwhen 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>ForCausalLMat module bottom
Files
- ATTRIBUTION.mdAdded by AutoGineer951 B
- BENCHMARK.md7.6 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md22.3 KiB
- capabilities-and-precision.md4.9 KiB
- evals/evals.json3.7 KiB
- llm-patterns.md11.9 KiB
- moe-patterns.md19.5 KiB
- skill-card.md4.2 KiB
- skill.oms.sig5.4 KiB
- vlm-patterns.md16.5 KiB
vlm-patterns.md
VLM (Vision-Language Model) Implementation Patterns
This document describes the patterns for adding a VLM (ForConditionalGeneration) to NeMo AutoModel.
Reference implementations:
components/models/mistral4/model.py--Mistral3ForConditionalGeneration(Pixtral vision + MoE text)components/models/kimivl/model.py--KimiVLForConditionalGeneration(MoonVit + DeepSeek-V3 text)components/models/kimi_k25_vl/model.py--KimiK25VLForConditionalGeneration
Architecture Overview
A VLM in NeMo AutoModel follows this structure:
ForConditionalGeneration
+-- model (VLM wrapper, plain nn.Module)
| +-- vision_tower (vision encoder)
| +-- multi_modal_projector (maps vision features to text dim)
| +-- language_model (text backbone wrapper)
| +-- model (the actual text model, e.g., DeepseekV3Model)
| +-- lm_head (optional, some put it here)
+-- lm_head (or as property proxying to language_model.lm_head)The key design constraint: the top-level class and the VLM wrapper inherit from nn.Module (NOT PreTrainedModel) to avoid FSDP conflicts from PreTrainedModel's module registration hooks.
Nested Config
VLMs have a nested config with vision_config and text_config:
class NewVLMConfig(PretrainedConfig):
model_type = "new_vlm"
def __init__(
self,
vision_config=None,
text_config=None,
ignore_index=-100,
media_placeholder_token_id=128256, # Model-specific image token ID
pad_token_id=0,
tie_word_embeddings=False, # MUST be at top level, NOT in text_config
**kwargs,
):
if vision_config is None:
vision_config = SomeVisionConfig()
elif isinstance(vision_config, dict):
vision_config = SomeVisionConfig(**vision_config)
self.vision_config = vision_config
if text_config is None:
text_config = SomeTextConfig()
elif isinstance(text_config, dict):
text_config = SomeTextConfig(**text_config)
self.text_config = text_config
self.ignore_index = ignore_index
self.media_placeholder_token_id = media_placeholder_token_id
super().__init__(
pad_token_id=pad_token_id,
tie_word_embeddings=tie_word_embeddings,
**kwargs,
)Critical: tie_word_embeddings placement
tie_word_embeddings MUST be set on the top-level VLM config, NOT inside text_config. The CombinedProjectionStateDictAdapter reads it from the config it receives, and for VLMs that config is the top-level one. If it is only set in text_config, tied weight handling breaks.
Vision Tower
Two approaches for the vision encoder:
Option A: Use HF vision model (Mistral4/Pixtral pattern)
from transformers import AutoModel
vision_tower = AutoModel.from_config(config.vision_config)This is the simplest approach when HF already has the vision model.
Option B: Custom vision encoder (KimiVL/MoonVit pattern)
class MoonVitPretrainedModel(nn.Module):
def __init__(self, config):
super().__init__()
self.patch_embed = PatchEmbed(...)
self.encoder = VisionEncoder(...)
self.merge_kernel_size = config.merge_kernel_size
def forward(self, pixel_values, grid_hws):
hidden_states = self.patch_embed(pixel_values, grid_hws)
hidden_states = self.encoder(hidden_states, grid_hws)
return patch_merger(hidden_states, grid_hws, self.merge_kernel_size)Custom vision encoders use standard PyTorch attention (flash_attn or SDPA), not the CombinedQKV mixin.
Multi-Modal Projector
Projects vision features into the language model's hidden dimension:
class NewVLMMultiModalProjector(nn.Module):
def __init__(self, config):
super().__init__()
vision_config = config.vision_config
text_config = config.text_config
# Compute input size (depends on patch merging)
input_size = vision_config.hidden_size * merge_factor
self.pre_norm = nn.LayerNorm(vision_config.hidden_size)
self.linear_1 = nn.Linear(input_size, input_size, bias=True)
self.act = nn.GELU()
self.linear_2 = nn.Linear(input_size, text_config.hidden_size, bias=True)
def forward(self, image_features):
hidden_states = self.pre_norm(image_features)
hidden_states = self.linear_1(hidden_states.view(-1, self.hidden_size))
hidden_states = self.act(hidden_states)
return self.linear_2(hidden_states)Or use HF's built-in projector if available:
from transformers.models.mistral3.modeling_mistral3 import Mistral3MultiModalProjector
multi_modal_projector = Mistral3MultiModalProjector(config)VLM Model Wrapper (nn.Module, not PreTrainedModel)
The wrapper composes vision tower + projector + language model. It is a plain nn.Module:
class NewVLMModel(nn.Module):
def __init__(self, config, vision_tower, multi_modal_projector, language_model):
super().__init__()
self.config = config
self.vision_tower = vision_tower
self.multi_modal_projector = multi_modal_projector
self.language_model = language_model
# Property aliases for parallelizer access
@property
def layers(self):
return self.language_model.layers
@property
def embed_tokens(self):
return self.language_model.embed_tokens
@property
def norm(self):
return self.language_model.norm
def get_input_embeddings(self):
return self.language_model.get_input_embeddings()
def _get_image_features(self, pixel_values, image_sizes, vision_feature_layer=-1):
"""Encode images through vision tower + projector."""
image_outputs = self.vision_tower(pixel_values, image_sizes=image_sizes, ...)
# Select vision feature layer
selected = image_outputs.hidden_states[vision_feature_layer]
image_features = self.multi_modal_projector(selected)
return image_features
def forward(self, input_ids=None, pixel_values=None, attention_mask=None,
position_ids=None, inputs_embeds=None, image_sizes=None, **kwargs):
if (input_ids is None) == (inputs_embeds is None):
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
if inputs_embeds is None:
inputs_embeds = self.language_model.get_input_embeddings()(input_ids)
# Merge image features into text embeddings
if pixel_values is not None and self.vision_tower is not None:
image_features = self._get_image_features(pixel_values, image_sizes)
image_features = torch.cat(image_features, dim=0).to(inputs_embeds.device, inputs_embeds.dtype)
image_token_index = getattr(self.config, "image_token_index", 10)
special_image_mask = (
(input_ids == image_token_index)
.unsqueeze(-1)
.expand_as(inputs_embeds)
.to(inputs_embeds.device)
)
inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features)
hidden_states = self.language_model(
input_ids=None, # Pass embeddings, not ids
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
position_ids=position_ids,
**kwargs,
)
return hidden_statesLanguage Model Backend Wrapper
The text backbone is wrapped to provide a uniform interface and avoid FSDP double-root-init:
class NewVLMLanguageModelBackend(nn.Module):
def __init__(self, config, backend, *, moe_config=None):
super().__init__()
# Wrap the actual text model (e.g., DeepseekV3Model, Mistral4Model)
self.model = TextModel(config, backend, moe_config=moe_config)
self.moe_config = self.model.moe_config # If MoE
self.lm_head = initialize_linear_module(
backend.linear, config.hidden_size, config.vocab_size, bias=False,
)
# Property aliases so parallelizer can find layers
@property
def embed_tokens(self):
return self.model.embed_tokens
@property
def layers(self):
return self.model.layers
@property
def norm(self):
return self.model.norm
def get_input_embeddings(self):
return self.embed_tokens
def set_input_embeddings(self, value):
self.model.embed_tokens = value
def forward(self, input_ids=None, *, inputs_embeds=None, attention_mask=None,
position_ids=None, **kwargs):
h = self.model(
input_ids=input_ids,
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
position_ids=position_ids,
**kwargs,
)
return BaseModelOutputWithPast(last_hidden_state=h, past_key_values=None)ForConditionalGeneration (Top-Level)
class NewVLMForConditionalGeneration(HFCheckpointingMixin, nn.Module, MoEFSDPSyncMixin):
# Optional: filter out configs where this model should not be used
@classmethod
def supports_config(cls, config) -> bool:
text_config = getattr(config, "text_config", None)
return text_config is not None and getattr(text_config, "model_type", None) == "expected_type"
@classmethod
def from_config(cls, config, moe_config=None, backend=None, **kwargs):
return cls(config, moe_config=moe_config, backend=backend, **kwargs)
def __init__(self, config, moe_config=None, backend=None, **kwargs):
super().__init__()
backend = backend or BackendConfig()
self.config = config
self.backend = backend
text_config = config.text_config
# Build components
vision_tower = build_vision_tower(config.vision_config)
multi_modal_projector = build_projector(config)
language_model = NewVLMLanguageModelBackend(
text_config, backend=backend, moe_config=moe_config,
)
self.model = NewVLMModel(
config=config,
vision_tower=vision_tower,
multi_modal_projector=multi_modal_projector,
language_model=language_model,
)
self.vocab_size = text_config.vocab_size
self.image_token_index = getattr(config, "image_token_index", 10)
if backend.enable_hf_state_dict_adapter:
self.state_dict_adapter = NewVLMStateDictAdapter(config, ...)
def get_input_embeddings(self):
return self.model.language_model.embed_tokens
def set_input_embeddings(self, value):
self.model.language_model.set_input_embeddings(value)
@property
def lm_head(self):
return self.model.language_model.lm_head
def get_output_embeddings(self):
return self.model.language_model.lm_head
def set_output_embeddings(self, new_embeddings):
self.model.language_model.lm_head = new_embeddings
def forward(self, input_ids=None, *, position_ids=None, attention_mask=None,
pixel_values=None, image_sizes=None, inputs_embeds=None, **kwargs):
# PP VLM support: retrieve pixel_values from stored chunks
if (
pixel_values is None
and hasattr(self, "_vlm_pixel_values_chunks")
and self._vlm_pixel_values_chunks is not None
):
has_media_tokens = (
input_ids is not None
and self.image_token_index is not None
and (input_ids == self.image_token_index).any()
)
if has_media_tokens:
chunk_idx = getattr(self, "_vlm_chunk_idx", 0)
if chunk_idx < len(self._vlm_pixel_values_chunks):
pixel_values = self._vlm_pixel_values_chunks[chunk_idx]
# Also handle image_grid_hws if needed
self._vlm_chunk_idx = chunk_idx + 1
outputs = self.model(
input_ids=input_ids,
pixel_values=pixel_values,
attention_mask=attention_mask,
position_ids=position_ids,
inputs_embeds=inputs_embeds,
image_sizes=image_sizes,
**kwargs,
)
hidden_states = outputs.last_hidden_state if hasattr(outputs, "last_hidden_state") else outputs
logits = self.lm_head(hidden_states) if self.lm_head is not None else hidden_states
return logits
ModelClass = NewVLMForConditionalGenerationReturn Types
VLMs may use the HF LlavaCausalLMOutputWithPast return type for HF-compatible output:
from transformers.models.llava.modeling_llava import LlavaCausalLMOutputWithPastHowever, most NeMo AutoModel VLM implementations return raw logits tensors from forward() for simplicity and compatibility with the training loop.
State Dict Adapter for VLMs
VLM state dict adapters must handle both vision and language weights. Two patterns:
Pattern A: Separate adapters (Mistral4)
class NewVLMMultimodalStateDictAdapter(StateDictAdapter):
def __init__(self, config, moe_config, backend, dtype):
self.text_adapter = NewVLMTextStateDictAdapter(config.text_config, moe_config, backend, dtype)
def from_hf(self, hf_state_dict, **kwargs):
# Text keys: prefix "model.language_model.model." or "model.text_model."
# Vision keys: prefix "model.vision_tower." or "vision_model."
# Projector keys: prefix "model.multi_modal_projector."
return self.text_adapter.from_hf(hf_state_dict, **kwargs)
def to_hf(self, state_dict, **kwargs):
return self.text_adapter.to_hf(state_dict, **kwargs)Pattern B: Delegate to language adapter (KimiVL)
If the language model is an existing architecture (e.g., DeepSeek-V3), reuse its adapter:
# KimiVL reuses DeepSeekV3StateDictAdapter directly
self.state_dict_adapter = DeepSeekV3StateDictAdapter(
self.config, self.model.language_model.moe_config, self.backend, dtype=...
)pixel_values / Image Inputs Handling
VLMs receive image data through pixel_values (and optionally image_sizes / image_grid_hws). The flow is:
Processor/collator packs images into
pixel_valuestensorVision tower encodes
pixel_valuesintoimage_featuresProjector maps
image_featuresto text hidden dimImage features are merged into text embeddings at special token positions via
masked_scatter
# Standard image-text merge pattern:
image_token_index = getattr(self.config, "image_token_index", 10)
special_image_mask = (
(input_ids == image_token_index)
.unsqueeze(-1)
.expand_as(inputs_embeds)
.to(inputs_embeds.device)
)
inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features)Pipeline Parallelism Support for VLMs
VLMs need special handling for PP because vision inputs are only relevant at the first stage. The pattern uses _vlm_pixel_values_chunks and _vlm_chunk_idx to pass pixel values across micro-batches:
# In forward():
if (
pixel_values is None
and hasattr(self, "_vlm_pixel_values_chunks")
and self._vlm_pixel_values_chunks is not None
):
has_media_tokens = (
input_ids is not None
and self.image_token_index is not None
and (input_ids == self.image_token_index).any()
)
if has_media_tokens:
chunk_idx = getattr(self, "_vlm_chunk_idx", 0)
if chunk_idx < len(self._vlm_pixel_values_chunks):
pixel_values = self._vlm_pixel_values_chunks[chunk_idx]
self._vlm_chunk_idx = chunk_idx + 1Registration
VLMs are registered in MODEL_ARCH_MAPPING just like LLMs:
(
"NewVLMForConditionalGeneration",
("nemo_automodel.components.models.new_vlm.model", "NewVLMForConditionalGeneration"),
),If the model has a custom config class (not natively supported by HF's AutoConfig), also register in _CUSTOM_CONFIG_REGISTRATIONS:
_CUSTOM_CONFIG_REGISTRATIONS = {
"new_vlm": ("nemo_automodel.components.models.new_vlm.model", "NewVLMConfig"),
}supports_config Pattern
When a single HF architecture name maps to multiple possible backends (e.g., Mistral3 can be dense Ministral3 or MoE Mistral4), use supports_config to disambiguate:
@classmethod
def supports_config(cls, config) -> bool:
"""Only handle configs whose text backbone is the expected type."""
text_config = getattr(config, "text_config", None)
return text_config is not None and getattr(text_config, "model_type", None) == "mistral4"The registry calls supports_config(config) before returning the model class. If it returns False, the registry falls back to HF's default implementation.
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 or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download nemo-automodel-model-onboarding.zip