Repository navigation
Add a NemotronH aggressive-mse NVFP4 PTQ recipe #2701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,9 +76,20 @@ def safe_nemotron_vl_forward(full_model: torch.nn.Module, batch: dict[str, Any]) | |
| flat_ids = input_ids.reshape(b * n) | ||
| selected = flat_ids == full_model.img_context_token_id | ||
|
|
||
| # Vision embeddings | ||
| # Vision embeddings. InternVL-style Nemotron VL exposes `extract_feature(pixel_values)`, but | ||
| # newer omni wrappers (e.g. NemotronH_Omni_Reasoning_V3 / nemotron_h_omni) do not: they merge the | ||
| # vision embeddings via masked_scatter inside their own forward. Calling the helper | ||
| # unconditionally raises AttributeError on those, aborting calibration. Fall back to the model's | ||
| # own forward when the helper is absent. | ||
| if not hasattr(full_model, "extract_feature"): | ||
| full_model(**{k: v for k, v in batch.items() if v is not None}) | ||
| return | ||
| vit_embeds = full_model.extract_feature(pixel_values) | ||
| vit_embeds = vit_embeds[image_flags_s == 1] | ||
| # Under a multi-GPU device_map the vision tower and the LLM embedding table can live on | ||
| # different devices, so the merge below would raise a cross-device error. Align explicitly. | ||
| if vit_embeds.device != flat_embeds.device: | ||
| vit_embeds = vit_embeds.to(flat_embeds.device) | ||
| try: | ||
| flat_embeds[selected] = flat_embeds[selected] * 0.0 + vit_embeds.reshape(-1, c) | ||
| except Exception: | ||
|
|
@@ -88,6 +99,14 @@ def safe_nemotron_vl_forward(full_model: torch.nn.Module, batch: dict[str, Any]) | |
|
|
||
| inputs_embeds = flat_embeds.reshape(b, n, c) | ||
|
|
||
| # Under a sharded device_map the LLM's first block may not sit on the embedding device. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The comment says this aligns to the LLM's first block, but |
||
| _lm_dev = getattr(full_model.language_model.get_input_embeddings().weight, "device", None) | ||
| if _lm_dev is not None: | ||
| if attention_mask is not None and attention_mask.device != _lm_dev: | ||
| attention_mask = attention_mask.to(_lm_dev) | ||
| if position_ids is not None and position_ids.device != _lm_dev: | ||
| position_ids = position_ids.to(_lm_dev) | ||
|
|
||
| # LLM forward (drives activation stats) | ||
| full_model.language_model( | ||
| inputs_embeds=inputs_embeds, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is only the SPDX part of |
||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
|
|
||
| # NemotronH -- aggressive NVFP4 tier, weight-MSE calibration. | ||
| # | ||
| # For hybrid Mamba-MoE NemotronH checkpoints, including the omni VL wrappers. Broader FP8 weight | ||
| # coverage than the conservative tier; the MTP rules apply only when the checkpoint carries an | ||
| # MTP tail and are inert otherwise. | ||
| # | ||
| # MoE routed + shared experts NVFP4 W4A4, static weight scales | ||
| # attention q/k/v/o, Mamba in/out_proj, lm_head FP8 | ||
| # MTP block-1 routed + shared experts NVFP4 W4A4, static weight scales | ||
| # latent MoE, embeddings, router, C-RADIO vision BF16 | ||
| # KV cache FP8 (see the kv rules below) | ||
| # | ||
| # quant_cfg is ordered and the LAST match wins, so the blanket disable comes first and the | ||
| # vision / MTP rules come last. Only configs/numerics/* is imported, so every rule that | ||
| # applies to this model is visible in this file. | ||
|
|
||
| imports: | ||
| nvfp4: configs/numerics/nvfp4 | ||
| nvfp4_static: configs/numerics/nvfp4_static | ||
| fp8: configs/numerics/fp8 | ||
|
|
||
| metadata: | ||
| recipe_type: ptq | ||
| description: >- | ||
| NemotronH aggressive tier, weight-MSE calibrated: NVFP4 routed and shared MoE experts with | ||
| static weight scales and dynamic NVFP4 activations, the same for the MTP block-1 experts; | ||
| FP8 attention projections, Mamba in/out_proj and lm_head; BF16 latent MoE and vision tower. | ||
| FP8 KV cache, with the backbone cast at a fixed range and MTP calibrated. | ||
| quantize: | ||
| algorithm: | ||
| method: mse | ||
| # Sweeps the 128 e4m3 scale values; requires static weight quantizers (below). | ||
| fp8_scale_sweep: true | ||
| # Required for VLMs: decoder layers sit under `model.language_model.layers`, | ||
| # where layerwise_calibrate cannot find them. | ||
| layerwise: | ||
| enable: false | ||
| quant_cfg: | ||
| - quantizer_name: '*' | ||
| enable: false | ||
| - quantizer_name: '*.experts.*weight_quantizer' | ||
| cfg: | ||
| $import: nvfp4_static | ||
| - quantizer_name: '*.experts.*input_quantizer' | ||
| cfg: | ||
| $import: nvfp4 | ||
| - quantizer_name: '*shared_experts*weight_quantizer' | ||
| cfg: | ||
| $import: nvfp4_static | ||
| - quantizer_name: '*shared_experts*input_quantizer' | ||
| cfg: | ||
| $import: nvfp4 | ||
| - quantizer_name: '*[qkvo]_proj.weight_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| - quantizer_name: '*[qkvo]_proj.input_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| - quantizer_name: '*mixer.in_proj.weight_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| - quantizer_name: '*mixer.in_proj.input_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| - quantizer_name: '*mixer.out_proj.weight_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| - quantizer_name: '*mixer.out_proj.input_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| # Backbone KV casts at a fixed FP8 range: no _amax buffer, so no k_scale/v_scale is | ||
| # exported, matching configs/ptq/units/kv_fp8_cast. MTP KV below stays calibrated and | ||
| # keeps its pair. | ||
| - quantizer_name: '*[kv]_bmm_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| use_constant_amax: true | ||
| - quantizer_name: '*mtp.*' | ||
| enable: false | ||
|
Comment on lines
+94
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '45,110p' modelopt_recipes/models/nvidia/Nemotron-3.5-Super-120B-A12B/ptq/nvfp4-aggressive-mse.yaml
sed -n '490,512p' modelopt_recipes/ptq.mdRepository: NVIDIA/Model-Optimizer Length of output: 3892 🏁 Script executed: set -o pipefail
printf '%s\n' '--- recipe with line numbers ---'
nl -ba modelopt_recipes/models/nvidia/Nemotron-3.5-Super-120B-A12B/ptq/nvfp4-aggressive-mse.yaml | sed -n '1,125p'
printf '%s\n' '--- documentation with line numbers ---'
nl -ba modelopt_recipes/ptq.md | sed -n '455,515p'
printf '%s\n' '--- exact PR diff for implicated files ---'
git diff --unified=12 90ba9fbc65d2be4788ce74a7184ec0709b22ffea 84b1d30dc7d9b5d6fef2c1fb9efd146427df477d -- \
modelopt_recipes/models/nvidia/Nemotron-3.5-Super-120B-A12B/ptq/nvfp4-aggressive-mse.yaml \
modelopt_recipes/ptq.md
printf '%s\n' '--- repository references to named checkpoint ---'
rg -n -F --glob '!*.lock' --glob '!*.jsonl' \
-e 'nvidia/Nemotron-Super-3.5-GA-FINAL-row105' \
-e 'Nemotron-Super-3.5-GA-FINAL-row105' .Repository: NVIDIA/Model-Optimizer Length of output: 17835 🏁 Script executed: set -o pipefail
nl -ba modelopt_recipes/models/nvidia/Nemotron-3.5-Super-120B-A12B/ptq/nvfp4-aggressive-mse.yaml | sed -n '1,125p'
nl -ba modelopt_recipes/ptq.md | sed -n '455,515p'
git diff --unified=12 90ba9fbc65d2be4788ce74a7184ec0709b22ffea 84b1d30dc7d9b5d6fef2c1fb9efd146427df477d -- modelopt_recipes/models/nvidia/Nemotron-3.5-Super-120B-A12B/ptq/nvfp4-aggressive-mse.yaml modelopt_recipes/ptq.md
rg -n -F --glob '!*.lock' -e 'nvidia/Nemotron-Super-3.5-GA-FINAL-row105' -e 'Nemotron-Super-3.5-GA-FINAL-row105' .Repository: NVIDIA/Model-Optimizer Length of output: 17669 Qualify the documented FP8 attention scope for MTP. The FP8 projection rules run before The selective MTP re-enablement indicates that this is an intentional unquantized exception. Qualify the recipe descriptions instead of restoring FP8 projections without a checkpoint contract. Suggested fix-# attention q/k/v/o, Mamba in/out_proj, lm_head FP8
+# backbone attention q/k/v/o, Mamba in/out_proj, lm_head FP8
+# MTP attention projections unquantized
...
- FP8 attention projections, Mamba in/out_proj and lm_head; BF16 latent MoE and C-RADIO vision.
+ FP8 backbone attention projections, Mamba in/out_proj and lm_head; MTP attention projections
+ remain unquantized; BF16 latent MoE and C-RADIO vision.- - attention q/k/v/out, Mamba `in/out_proj`, `lm_head` → FP8
+ - backbone attention q/k/v/out, Mamba `in/out_proj`, `lm_head` → FP8
+ - MTP attention projections remain unquantized🤖 Prompt for AI Agents |
||
| - quantizer_name: '*mtp.layers.1.mixer.experts.*weight_quantizer' | ||
| cfg: | ||
| $import: nvfp4_static | ||
| - quantizer_name: '*mtp.layers.1.mixer.experts.*input_quantizer' | ||
| cfg: | ||
| $import: nvfp4 | ||
| - quantizer_name: '*mtp.layers.1.mixer.shared_experts.*weight_quantizer' | ||
| cfg: | ||
| $import: nvfp4_static | ||
| - quantizer_name: '*mtp.layers.1.mixer.shared_experts.*input_quantizer' | ||
| cfg: | ||
| $import: nvfp4 | ||
| # Restated so this rule cannot inherit the backbone setting above. | ||
| - quantizer_name: '*mtp.layers.0.*[kv]_bmm_quantizer' | ||
| cfg: | ||
| $import: fp8 | ||
| use_constant_amax: false | ||
| - quantizer_name: '*embed_vision*' | ||
| enable: false | ||
| - quantizer_name: '*lm_head*weight_quantizer' | ||
| enable: true | ||
| cfg: | ||
| $import: fp8 | ||
| - quantizer_name: '*lm_head*input_quantizer' | ||
| enable: true | ||
| cfg: | ||
| $import: fp8 | ||
| - quantizer_name: '*vision_model*' | ||
| enable: false | ||
| - quantizer_name: '*vision_projector*' | ||
| enable: false | ||
| - quantizer_name: '*radio_model*' | ||
| enable: false | ||
| - quantizer_name: '*vision_tower*' | ||
| enable: false | ||
| - quantizer_name: '*visual*' | ||
| enable: false | ||
| - quantizer_name: '*multi_modal_projector*' | ||
| enable: false | ||
| - quantizer_name: 'mlp1.*' | ||
| enable: false | ||
| - quantizer_name: '*.mlp1.*' | ||
| enable: false | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check runs after
full_model.img_context_token_idandlanguage_model.get_input_embeddings()(input_ids)have already been used. Any wrapper that doesn't have those attributes still fails before it reaches the fallback, and the embedding lookup is wasted work. Move the check to just after theimage_flagshandling. Also, the fallback passes the originalbatch, so the dtype-castpixel_valuesabove is thrown away. It also doesn't passuse_cache=False. The module docstring says wrapperforward()is unsafe in calibration (dist.get_rank,past_key_values). Please add a short note on why that doesn't apply to omni wrappers, and add a unit test for this path using a mock.