Skip to content

Export quantized/co-trained MTP weights instead of copying BF16 - #2174

Open
yeyu-nvidia wants to merge 2 commits into
NVIDIA:mainfrom
yeyu-nvidia:yeyu/mtp-quant-export
Open

Export quantized/co-trained MTP weights instead of copying BF16#2174
yeyu-nvidia wants to merge 2 commits into
NVIDIA:mainfrom
yeyu-nvidia:yeyu/mtp-quant-export

Conversation

@yeyu-nvidia

@yeyu-nvidia yeyu-nvidia commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

GPTModelExporter._get_mtp_state_dict copied the MTP (multi-token prediction) head verbatim from the BF16 pretrained model instead of exporting the live model's MTP weights. _get_state_dict only walks model.decoder.layers and never model.mtp, so self._state_dict never contains any mtp.* keys and the key not in self._state_dict guard was always true — every MTP tensor came from the pretrained safetensors. There was a standing # TODO Implement MTP export for quantized MTP.

Consequence: any quantization or co-training applied to the MTP head during QAD was silently discarded at export; the exported draft head was always the original BF16 weights. This makes it impossible to evaluate MTP quantization or MTP co-training downstream.

Fix

  • Rewrite _get_mtp_state_dict to walk the live MCore model.mtp module and apply the same quantization rules used for the base decoder (mirroring _get_eagle_module_state_dict). The MTP inner attention/MoE layers are structurally identical to backbone hybrid layers, so the base layer walker (_get_transformer_layer_state_dict / _get_mamba_layer_state_dict) is reused with a restricted set of mtp.* naming rules aliased onto the standard rule keys — emitting mtp.layers.{}. HF keys. Restricted on purpose: any base rule key the walker references but that has no mtp. variant is simply absent (guarded), rather than silently emitting a wrong backbone. prefix.
  • Keep the old copy behavior as _copy_mtp_state_dict_from_pretrained, used only when the live model has no mtp module (e.g. exporting a base-only checkpoint that grafts a pretrained head).
  • Add the missing MTP inner-layer export rules to nemotron_h_causal_lm_export (attention qkv/o_proj/norm, MoE router/experts/shared_experts). The predictor projection rules (mtp.enorm/hnorm/eh_proj/final_layernorm) already existed.

Validation

  • For a NemotronHForCausalLM model (num_nextn_predict_layers=1, hybrid *E), the walker reproduces exactly the expected HF key layout — mtp.layers.0 = attention (enorm/hnorm/eh_proj/norm/mixer.{q,k,v,o}_proj), mtp.layers.1 = MoE (norm/final_layernorm/mixer.gate/shared_experts/experts.{e}) — matching what the existing nemotron_h_causal_lm_import (is_mtp keys) reads back.
  • pre-commit clean (ruff / ruff-format / mypy / bandit).
  • End-to-end re-export + downstream MTP spec-decode eval on a co-trained checkpoint is in progress; will post the before/after tensor diff here.

Note for reviewer

@jenchen13 — this is the MTP-export bug you flagged (the L599 BF16 copy). Would appreciate your review, especially on the assumption that the MTP inner layers can be driven through the base layer walker with only a prefix swap, and whether any non-*E MTP configurations need additional inner-layer rules.

Summary by CodeRabbit

  • New Features

    • Improved live MTP export support across Transformer, Mamba, attention, MLP, and expert-based architectures.
    • Added support for MTP-specific mappings, projections, final normalization, fused layers, and packed experts.
    • Preserved pretrained safetensor fallback when a live MTP module is unavailable.
  • Bug Fixes

    • Added validation for supported MTP layer types.
    • Ensured exporter state is restored after MTP export operations.

`_get_mtp_state_dict` previously copied the MTP (multi-token prediction)
head verbatim from the BF16 pretrained model (`# TODO Implement MTP
export for quantized MTP`), because `_get_state_dict` only walks
`model.decoder.layers` and never `model.mtp` — so the `key not in
self._state_dict` guard was always true. Any quantization or co-training
applied to the MTP head during QAD was silently discarded at export; the
draft head shipped as the original BF16 weights.

This walks the live MCore `model.mtp` module and applies the same
quantization rules used for the base decoder, mirroring
`_get_eagle_module_state_dict`. The MTP inner attention/MoE layers are
structurally identical to backbone hybrid layers, so the base layer
walker is reused with a restricted set of `mtp.*` naming rules aliased
onto the standard rule keys (emitting `mtp.layers.{}.` HF keys). The old
BF16-copy path is kept as `_copy_mtp_state_dict_from_pretrained`, used
only when the live model has no `mtp` module.

Adds the missing `mtp.*` inner-layer export rules to
`nemotron_h_causal_lm_export` (attention qkv/o_proj/norm, MoE
router/experts/shared_experts); the predictor projection rules
(`mtp.enorm/hnorm/eh_proj/final_layernorm`) already existed. Round-trips
with the `is_mtp` keys in `nemotron_h_causal_lm_import`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yeyu-nvidia
yeyu-nvidia requested a review from a team as a code owner August 12, 2026 14:02
@yeyu-nvidia
yeyu-nvidia requested a review from cjluo-nv August 12, 2026 14:03
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5b97b575-859d-402e-8b60-f9cd13f34eaa

📥 Commits

Reviewing files that changed from the base of the PR and between 54eaceb and 89ef596.

📒 Files selected for processing (2)
  • modelopt/torch/export/plugins/mcore_nemotron.py
  • modelopt/torch/export/unified_export_megatron.py

📝 Walkthrough

Walkthrough

The PR adds live MTP traversal to Megatron export. It propagates MTP-specific prefixes through shared layer and projection rules, preserves the pretrained BF16 fallback, and restores exporter state after export.

Changes

MTP export

Layer / File(s) Summary
Live MTP traversal and state handling
modelopt/torch/export/unified_export_megatron.py
The exporter traverses live MTP Transformer and Mamba layers, exports predictor and normalization tensors, validates supported layers, reports tensor counts, and restores exporter state.
MTP prefix remapping across export rules
modelopt/torch/export/unified_export_megatron.py
Shared attention, Mamba, MLP, expert, QKV, KV-scaling, and packing rules generate MTP-prefixed entries when is_mtp is enabled.
Fallback handling and Nemotron mapping documentation
modelopt/torch/export/unified_export_megatron.py, modelopt/torch/export/plugins/mcore_nemotron.py
The exporter retains the pretrained BF16 fallback when no live MTP module exists. The Nemotron comment documents shared inner-layer mappings and dedicated predictor mappings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: ⚪ Minimal · up to 89ef5

The PR changes MTP export to preserve live quantized or co-trained weights while retaining a pretrained fallback when no live MTP module exists; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant MegatronModel
  participant MTPExporter
  participant ExportRules
  participant ExportState
  MegatronModel->>MTPExporter: provide live MTP layers
  MTPExporter->>ExportState: enable MTP export mode
  MTPExporter->>ExportRules: traverse layers with is_mtp=true
  ExportRules->>MTPExporter: return mtp-prefixed tensor entries
  MTPExporter->>ExportState: restore prior exporter state
Loading

Suggested reviewers: cjluo-nv

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: exporting quantized or co-trained MTP weights instead of copying BF16 weights.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns ✅ Passed The PR changes only two Python files and adds no prohibited security patterns, no # nosec comments, and no dependency files; existing torch.load(..., weights_only=False) is unchanged.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@yeyu-nvidia
yeyu-nvidia requested a review from jenchen13 August 12, 2026 14:04

@coderabbitai coderabbitai 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modelopt/torch/export/plugins/mcore_nemotron.py`:
- Around line 163-179: Add MTP-prefixed entries to the MTP rule mapping for
every Mamba walker key accessed by _get_mamba_layer_state_dict: norm,
mixer_norm, A_log, D, dt_bias, conv1d, in_proj, and out_proj. Map them to the
corresponding mtp.layers.{}.mixer.* paths, preserving the existing NameRemapping
or slicing behavior used by the base Mamba rules so _get_mtp_state_dict can
export Mamba-based MTP layers without KeyError.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b9290d4b-2650-402d-ba4e-c438024d580d

📥 Commits

Reviewing files that changed from the base of the PR and between a21173a and 54eaceb.

📒 Files selected for processing (2)
  • modelopt/torch/export/plugins/mcore_nemotron.py
  • modelopt/torch/export/unified_export_megatron.py

Comment on lines +163 to +179
# MTP inner attention / MoE layers. Structurally identical to the backbone hybrid
# layers, so these mirror the base rules above with the `mtp.layers.{}` prefix; the
# `mtp.` namespace is aliased onto the standard rule keys by _get_mtp_state_dict.
"mtp.input_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.fused_norm": NameRemapping("mtp.layers.{}.norm.weight"),
"mtp.linear_qkv": QKVSlicing("mtp.layers.{}.mixer."),
"mtp.linear_proj": NameRemapping("mtp.layers.{}.mixer.o_proj."),
"mtp.pre_mlp_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.router": NameRemapping(
"mtp.layers.{}.mixer.gate.", {"mapping": {"expert_bias": "e_score_correction_bias"}}
),
"mtp.shared_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.shared_experts.up_proj."),
"mtp.shared_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.shared_experts.down_proj."),
"mtp.local_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.experts.{}.up_proj."),
"mtp.local_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.experts.{}.down_proj."),
"mtp.experts.linear_fc1": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.up_proj"),
"mtp.experts.linear_fc2": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.down_proj"),

@coderabbitai coderabbitai Bot Aug 12, 2026

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add MTP mappings for all Mamba walker rules.

_get_mtp_state_dict aliases only mtp.* rules. If an MTP inner layer is a MambaLayer, _get_mamba_layer_state_dict accesses norm, mixer_norm, A_log, D, dt_bias, conv1d, in_proj, and out_proj without guards. This mapping adds none of their mtp.* variants. Export then raises KeyError instead of exporting live MTP weights.

Add the corresponding mtp.* mappings with the mtp.layers.{}.mixer. prefix.

Proposed mapping additions
+    "mtp.norm": NameRemapping("mtp.layers.{}.norm."),
+    "mtp.mixer_norm": NameRemapping("mtp.layers.{}.mixer.norm."),
+    "mtp.A_log": NameRemapping("mtp.layers.{}.mixer.A_log"),
+    "mtp.D": NameRemapping("mtp.layers.{}.mixer.D"),
+    "mtp.dt_bias": NameRemapping("mtp.layers.{}.mixer.dt_bias"),
+    "mtp.conv1d": NameRemapping("mtp.layers.{}.mixer.conv1d."),
+    "mtp.in_proj": NameRemapping("mtp.layers.{}.mixer.in_proj."),
+    "mtp.out_proj": NameRemapping("mtp.layers.{}.mixer.out_proj."),

This conflicts with the PR objective to reuse Mamba layer walkers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# MTP inner attention / MoE layers. Structurally identical to the backbone hybrid
# layers, so these mirror the base rules above with the `mtp.layers.{}` prefix; the
# `mtp.` namespace is aliased onto the standard rule keys by _get_mtp_state_dict.
"mtp.input_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.fused_norm": NameRemapping("mtp.layers.{}.norm.weight"),
"mtp.linear_qkv": QKVSlicing("mtp.layers.{}.mixer."),
"mtp.linear_proj": NameRemapping("mtp.layers.{}.mixer.o_proj."),
"mtp.pre_mlp_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.router": NameRemapping(
"mtp.layers.{}.mixer.gate.", {"mapping": {"expert_bias": "e_score_correction_bias"}}
),
"mtp.shared_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.shared_experts.up_proj."),
"mtp.shared_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.shared_experts.down_proj."),
"mtp.local_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.experts.{}.up_proj."),
"mtp.local_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.experts.{}.down_proj."),
"mtp.experts.linear_fc1": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.up_proj"),
"mtp.experts.linear_fc2": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.down_proj"),
# MTP inner attention / MoE layers. Structurally identical to the backbone hybrid
# layers, so these mirror the base rules above with the `mtp.layers.{}` prefix; the
# `mtp.` namespace is aliased onto the standard rule keys by _get_mtp_state_dict.
"mtp.input_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.fused_norm": NameRemapping("mtp.layers.{}.norm.weight"),
"mtp.linear_qkv": QKVSlicing("mtp.layers.{}.mixer."),
"mtp.linear_proj": NameRemapping("mtp.layers.{}.mixer.o_proj."),
"mtp.pre_mlp_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.router": NameRemapping(
"mtp.layers.{}.mixer.gate.", {"mapping": {"expert_bias": "e_score_correction_bias"}}
),
"mtp.shared_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.shared_experts.up_proj."),
"mtp.shared_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.shared_experts.down_proj."),
"mtp.local_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.experts.{}.up_proj."),
"mtp.local_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.experts.{}.down_proj."),
"mtp.experts.linear_fc1": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.up_proj"),
"mtp.experts.linear_fc2": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.down_proj"),
"mtp.norm": NameRemapping("mtp.layers.{}.norm."),
"mtp.mixer_norm": NameRemapping("mtp.layers.{}.mixer.norm."),
"mtp.A_log": NameRemapping("mtp.layers.{}.mixer.A_log"),
"mtp.D": NameRemapping("mtp.layers.{}.mixer.D"),
"mtp.dt_bias": NameRemapping("mtp.layers.{}.mixer.dt_bias"),
"mtp.conv1d": NameRemapping("mtp.layers.{}.mixer.conv1d."),
"mtp.in_proj": NameRemapping("mtp.layers.{}.mixer.in_proj."),
"mtp.out_proj": NameRemapping("mtp.layers.{}.mixer.out_proj."),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelopt/torch/export/plugins/mcore_nemotron.py` around lines 163 - 179, Add
MTP-prefixed entries to the MTP rule mapping for every Mamba walker key accessed
by _get_mamba_layer_state_dict: norm, mixer_norm, A_log, D, dt_bias, conv1d,
in_proj, and out_proj. Map them to the corresponding mtp.layers.{}.mixer.*
paths, preserving the existing NameRemapping or slicing behavior used by the
base Mamba rules so _get_mtp_state_dict can export Mamba-based MTP layers
without KeyError.

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.

if the MTP layers are structurally identical to the base model layers, then maybe we don't need to add mtp.* rules that are duplicates of the base model rules? It should be possible to just use the base model rules. Adding these mtp rules also makes the import & export rules diverge.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="modelopt/torch/export/unified_export_megatron.py"

ast-grep outline "$target" --items all --type function --match '_get_mtp_state_dict|_get_mamba_layer_state_dict|_get_transformer_layer_state_dict' --view expanded

rg -n -A100 -B15 \
  'def _get_mtp_state_dict\b|def _get_mamba_layer_state_dict\b|def _get_transformer_layer_state_dict\b' \
  "$target"

rg -n -A5 -B5 \
  'mtp\.\*|is_mtp|_get_mtp_state_dict\s*\(|_get_mamba_layer_state_dict\s*\(' \
  modelopt/torch/export

Length of output: 56329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A80 -B15 \
  'self\.rules\s*=|CustomModuleMapping|target_name_or_prefix|func_kwargs|name_remapping' \
  modelopt/torch/export/unified_export_megatron.py \
  modelopt/torch/export/plugins/mcore_custom.py \
  modelopt/torch/export/plugins

rg -n -A80 -B15 \
  'def _name_remapping\b|def _qkv_slicing\b|def _grouped_mlp_slicing\b|def .*remapp' \
  modelopt/torch/export

rg -n -A5 -B5 \
  'replace\("backbone", "mtp"\)|replace\("model", "mtp"\)|is_mtp' \
  modelopt/torch/export

Length of output: 201035


@jenchen13, agreed. The current mtp.* aliases are needed by the export control flow, but they should not be manually duplicated.

The existing code creates self.rules from only mtp.* entries in _get_mtp_state_dict. Therefore, the Mamba walker cannot currently reuse the base rules. It raises KeyError for an MTP MambaLayer.

A better fix is to derive the MTP inner-layer export mappings from the base layer mappings. A helper can clone each layer-scoped CustomModuleMapping, replace the backbone.layers. prefix with mtp.layers., and add the cloned rule under mtp.<base-rule-key>. This follows the existing prefix-rewrite pattern in modelopt/torch/export/plugins/mcore_qwen3vl.py.

This change gives the MTP walker all base-layer rules, including Mamba rules, without a second hand-maintained mapping list. It also prevents future base Mamba or Transformer mapping changes from diverging from MTP export mappings.

I will withdraw the proposed manual mapping additions. The underlying export failure remains open until the aliases are generated from the base mappings or the exporter uses an equivalent prefix-rewrite mechanism.

🐇


🧬 Code Graph Analysis Results

modelopt/torch/export/plugins/mcore_custom.py

Lines 70–79 — CustomModuleMapping

class CustomModuleMapping:
    """A custom module mapping from Megatron Core to its HF counter part."""

    def __init__(
        self, func_name: str = "", target_name_or_prefix: str = "", func_kwargs: dict[str, Any] = {}
    ):
        """Create a custom module mapping."""
        self.func_name = func_name
        self.target_name_or_prefix = target_name_or_prefix
        self.func_kwargs = func_kwargs

Stores the mapping function name, target name/prefix, and optional keyword arguments such as is_mtp.

Lines 82–91 — NameRemapping

class NameRemapping(CustomModuleMapping):
    """A custom module mapping that renames of the modules."""

    def __init__(self, target_name_or_prefix: str = "", func_kwargs: dict[str, Any] = {}):
        """Create a custom module mapping that renames of the modules."""
        super().__init__(
            func_name="name_remapping",
            target_name_or_prefix=target_name_or_prefix,
            func_kwargs=func_kwargs,
        )

Lines 142–151 — QKVSlicing

class QKVSlicing(CustomModuleMapping):
    """A custom module mapping that slices Q, K, V."""

    def __init__(self, target_name_or_prefix: str = "", func_kwargs: dict[str, Any] = {}):
        """Create a custom module mapping that slices Q, K, V."""
        super().__init__(
            func_name="qkv_slicing",
            target_name_or_prefix=target_name_or_prefix,
            func_kwargs=func_kwargs,
        )

Lines 118–127 — GroupedMLPSlicing

class GroupedMLPSlicing(CustomModuleMapping):
    """A custom module mapping that slices fused grouped MLP weights into per-expert weights."""

    def __init__(self, target_name_or_prefix: str = "", func_kwargs: dict[str, Any] = {}):
        """Create a custom module mapping that slices grouped MLP weights for export."""
        super().__init__(
            func_name="grouped_mlp_slicing",
            target_name_or_prefix=target_name_or_prefix,
            func_kwargs=func_kwargs,
        )

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — done in 89ef596. I removed the duplicate mtp.* inner-layer rules and instead thread an is_mtp flag through the base layer walker + remapping helpers, which rewrite the target root backbone/modelmtp — mirroring exactly what the importer does. Only enorm/hnorm/eh_proj/final_layernorm stay as dedicated mtp.* rules now, so the import/export rule books stay symmetric.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 12.79070% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.16%. Comparing base (a21173a) to head (89ef596).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/export/unified_export_megatron.py 12.79% 75 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (a21173a) and HEAD (89ef596). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (a21173a) HEAD (89ef596)
unit 2 1
gpu 5 3
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2174       +/-   ##
===========================================
- Coverage   78.74%   66.16%   -12.59%     
===========================================
  Files         522      522               
  Lines       60368    61519     +1151     
===========================================
- Hits        47538    40705     -6833     
- Misses      12830    20814     +7984     
Flag Coverage Δ
examples-gpt-oss 13.27% <9.30%> (-0.01%) ⬇️
examples-llm_distill 13.33% <9.30%> (-0.01%) ⬇️
examples-llm_qat 17.59% <9.30%> (-0.02%) ⬇️
examples-llm_sparsity 15.92% <9.30%> (-0.01%) ⬇️
examples-specdec_bench 13.00% <9.30%> (-0.01%) ⬇️
examples-speculative_decoding 17.52% <9.30%> (-0.08%) ⬇️
gpu 21.08% <9.30%> (-38.24%) ⬇️
unit 55.26% <9.30%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines +163 to +179
# MTP inner attention / MoE layers. Structurally identical to the backbone hybrid
# layers, so these mirror the base rules above with the `mtp.layers.{}` prefix; the
# `mtp.` namespace is aliased onto the standard rule keys by _get_mtp_state_dict.
"mtp.input_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.fused_norm": NameRemapping("mtp.layers.{}.norm.weight"),
"mtp.linear_qkv": QKVSlicing("mtp.layers.{}.mixer."),
"mtp.linear_proj": NameRemapping("mtp.layers.{}.mixer.o_proj."),
"mtp.pre_mlp_layernorm": NameRemapping("mtp.layers.{}.norm."),
"mtp.router": NameRemapping(
"mtp.layers.{}.mixer.gate.", {"mapping": {"expert_bias": "e_score_correction_bias"}}
),
"mtp.shared_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.shared_experts.up_proj."),
"mtp.shared_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.shared_experts.down_proj."),
"mtp.local_experts.linear_fc1": NameRemapping("mtp.layers.{}.mixer.experts.{}.up_proj."),
"mtp.local_experts.linear_fc2": NameRemapping("mtp.layers.{}.mixer.experts.{}.down_proj."),
"mtp.experts.linear_fc1": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.up_proj"),
"mtp.experts.linear_fc2": GroupedMLPSlicing("mtp.layers.{}.mixer.experts.{{}}.down_proj"),

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.

if the MTP layers are structurally identical to the base model layers, then maybe we don't need to add mtp.* rules that are duplicates of the base model rules? It should be possible to just use the base model rules. Adding these mtp rules also makes the import & export rules diverge.

# A *restricted* alias set is used on purpose: any base rule key the walker
# references but that has no ``mtp.`` variant is simply absent (and its call is
# guarded), rather than silently emitting a wrong ``backbone.`` prefix.
mtp_rules = {

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.

you can just reuse the base layer rules + add the mtp specific ones for enorm, hnorm, eh_proj, final_layernorm

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — done in 89ef596. I removed the duplicate mtp.* inner-layer rules and instead thread an is_mtp flag through the base layer walker + remapping helpers, which rewrite the target root backbone/modelmtp — mirroring exactly what the importer does. Only enorm/hnorm/eh_proj/final_layernorm stay as dedicated mtp.* rules now, so the import/export rule books stay symmetric.

@yeyu-nvidia

Copy link
Copy Markdown
Contributor Author

End-to-end validation (NemotronH, num_nextn_predict_layers=1, hybrid *E)

Re-exported a co-trained checkpoint with this branch and compared the mtp.* tensors against the BF16 pretrained model that the old code copied from:

tensor fixed export vs BF16 pretrained source
mtp.layers.0.eh_proj.weight differs
mtp.layers.0.enorm / hnorm / norm differs
mtp.layers.0.mixer.{q,k,v,o}_proj.weight differs

Before this PR every mtp.* tensor was byte-identical to the pretrained model regardless of training (the copy path), so any QAD co-training of the MTP head was discarded at export. After the fix the exported head reflects the trained weights. Full layout (270 mtp.* tensors, layers.0=attention, layers.1=MoE) matches the nemotron_h_causal_lm_import round-trip.

Note on precision: in this particular checkpoint the MTP head was left unquantized (BF16, like the lm_head), so the exported MTP is BF16 with no weight_scale. The walker runs the same quantization rules as the base decoder, so it will emit NVFP4 weights + scales whenever the MTP module is quantized in the checkpoint — this validation just didn't exercise that path. @jenchen13 flagging in case the MTP head is expected to be quantized by the recipe.

…e mtp rules

Per review (@jenchen13): the MTP inner attention/MoE layers are structurally
identical to the backbone hybrid layers, so instead of adding duplicate
`mtp.*` inner-layer rules, thread an `is_mtp` flag through the base layer
walker (`_get_transformer_layer_state_dict` / `_get_mamba_layer_state_dict`)
and the remapping helpers. When set, the helper rewrites the target root
(`backbone`/`model` -> `mtp`), exactly mirroring the importer. Only the
predictor-specific keys (enorm/hnorm/eh_proj/final_layernorm) remain dedicated
`mtp.*` rules. This keeps the import and export rule books symmetric and avoids
rule duplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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