From 5facaa9f5a06998029042bce5aef8249aa57c774 Mon Sep 17 00:00:00 2001 From: weimingc <17592131+meenchen@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:13:27 -0700 Subject: [PATCH 1/7] feat(autoquant): add fixed MTP mixed-precision recipes Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> --- CHANGELOG.rst | 1 + .../configs/numerics/nvfp4_static.yaml | 2 + .../auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml | 29 ++++++ .../nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml | 19 ++++ .../auto_quantize/nvfp4_mse_fp8_search.yaml | 92 +++++++++++++++++++ tests/unit/recipe/test_loader.py | 29 ++++++ 6 files changed, 172 insertions(+) create mode 100644 modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml create mode 100644 modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml create mode 100644 modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 62b8d12b615..884454e8045 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -32,6 +32,7 @@ Changelog *Quantization* - Add Q8_0 weight-only quantization with 32-value GGML blocks, packed unified HF and Megatron export, and a built-in ``q8_0`` PTQ recipe. +- Add a Nemotron-H AutoQuantize recipe that searches NVFP4, FP8, and BF16 at 4.9 effective bits while keeping MTP routed and shared experts at NVFP4 W4A4. - Add Hugging Face PTQ calibration and export support for Nemotron-H MTP modules, preserving calibrated expert input scales. - Add the ``models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/ptq/nvfp4-mse`` PTQ recipe, the PTQ stage for NVIDIA Nemotron 3.5 Super VL: weight-MSE calibrated NVFP4 routed and shared MoE experts, FP8 attention projections, Mamba ``in_proj``/``out_proj`` and ``lm_head``, and an FP8 KV cache. The latent MoE, router, embeddings and the vision and audio encoders stay in BF16, and the MTP rules apply only to checkpoints that carry an MTP tail. It reproduces the released precision assignment, not the released weights: the published NVFP4 checkpoint adds quantization-aware distillation after this stage. - Backfill checkpoint aliases for three more published NVFP4 releases: ``moonshotai/Kimi-K2.7-Code``, ``google/gemma-4-26B-A4B-it`` and ``google/diffusiongemma-26B-A4B-it``. Each imports an existing general recipe wholesale rather than copying its body. diff --git a/modelopt_recipes/configs/numerics/nvfp4_static.yaml b/modelopt_recipes/configs/numerics/nvfp4_static.yaml index 9f6ac62e11e..4f2ca57eb90 100644 --- a/modelopt_recipes/configs/numerics/nvfp4_static.yaml +++ b/modelopt_recipes/configs/numerics/nvfp4_static.yaml @@ -21,3 +21,5 @@ block_sizes: -1: 16 type: static scale_bits: e4m3 +# Four value bits plus one FP8 scale per 16-element block. +effective_bits: 4.5 diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml new file mode 100644 index 00000000000..57722559f5a --- /dev/null +++ b/modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Fixed Nemotron-H MTP policy: NVFP4 W4A4 MoE projections and FP8 attention KV-cache BMMs. + +# modelopt-schema: modelopt.torch.quantization.config.QuantizeConfig +imports: + nvfp4: configs/numerics/nvfp4 + fp8: configs/numerics/fp8 + +algorithm: max +quant_cfg: + - quantizer_name: "*" + enable: false + - quantizer_name: "*mtp.layers.1.mixer.experts.*weight_quantizer" + enable: true + cfg: {$import: nvfp4} + - quantizer_name: "*mtp.layers.1.mixer.experts.*input_quantizer" + enable: true + cfg: {$import: nvfp4} + - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*weight_quantizer" + enable: true + cfg: {$import: nvfp4} + - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*input_quantizer" + enable: true + cfg: {$import: nvfp4} + - quantizer_name: "*mtp.layers.0.*[kv]_bmm_quantizer" + enable: true + cfg: {$import: fp8} diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml new file mode 100644 index 00000000000..adfd9b36b01 --- /dev/null +++ b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# modelopt-schema: modelopt.recipe.config.ModelOptAutoQuantizeRecipe +imports: + mtp: model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8 + search: model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search + +metadata: + recipe_type: auto_quantize + description: >- + Nemotron-H AutoQuantize at 4.9 effective bits over weight-MSE NVFP4, FP8, and BF16, + with a fixed MTP quantization policy and vision/audio paths kept in BF16. + +quantize: {$import: mtp} +auto_quantize: + $import: search + constraints: + effective_bits: 4.9 diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml new file mode 100644 index 00000000000..cf2cb6eb7af --- /dev/null +++ b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Nemotron-H language-path search over weight-MSE NVFP4, FP8, and implicit BF16. + +# modelopt-schema: modelopt.recipe.config.AutoQuantizeConfig +imports: + base_cost_excluded_layers: configs/auto_quantize/units/base_cost_excluded_layers + nvfp4: configs/numerics/nvfp4 + nvfp4_static: configs/numerics/nvfp4_static + fp8: configs/numerics/fp8 + kv_fp8: configs/ptq/presets/kv/fp8 + +constraints: + effective_bits: 4.9 +module_search_spaces: + - module_name_patterns: + - "*language_model.layers*" + - "*language_model.model.layers*" + - "*language_model.backbone.layers*" + candidate_formats: &search_candidates + - algorithm: + method: mse + fp8_scale_sweep: true + quant_cfg: + - quantizer_name: "*" + enable: false + - quantizer_name: "*weight_quantizer" + enable: true + cfg: {$import: nvfp4_static} + - quantizer_name: "*input_quantizer" + enable: true + cfg: {$import: nvfp4} + - algorithm: max + quant_cfg: + - quantizer_name: "*" + enable: false + - quantizer_name: "*weight_quantizer" + enable: true + cfg: {$import: fp8} + - quantizer_name: "*input_quantizer" + enable: true + cfg: {$import: fp8} + allow_no_quant: true + - module_name_patterns: + - "*lm_head*" + - "*output_layer*" + candidate_formats: *search_candidates + allow_no_quant: true +auto_quantize_method: gradient +score_size: 128 +kv_cache: {$import: kv_fp8} +disabled_layers: + - "*block_sparse_moe.gate*" + - "*linear_attn.conv1d*" + - "*linear_attn.in_proj_a*" + - "*linear_attn.in_proj_b*" + - "*mixer.conv1d*" + - "*mlp.gate.*" + - "*mlp.shared_expert_gate.*" + - "*shared_expert_gate*" + - "*proj_out.*" + - "*router*" + - "*vision*" + - "*visual*" + - "*image*" + - "*radio*" + - "*encoder*" + - "*model_encoder*" + - "*video*" + - "*audio*" + - "*speech*" + - "*multimodal_projector*" + - "*multi_modal_projector*" + - "*vision_projector*" + - "*audio_projector*" +cost_excluded_layers: + - $import: base_cost_excluded_layers + - "*vision*" + - "*visual*" + - "*image*" + - "*radio*" + - "*encoder*" + - "*model_encoder*" + - "*video*" + - "*audio*" + - "*speech*" + - "*multimodal_projector*" + - "*multi_modal_projector*" + - "*vision_projector*" + - "*audio_projector*" + - "*mtp*" diff --git a/tests/unit/recipe/test_loader.py b/tests/unit/recipe/test_loader.py index e1add8623bf..7ebc62c2f29 100644 --- a/tests/unit/recipe/test_loader.py +++ b/tests/unit/recipe/test_loader.py @@ -2632,6 +2632,35 @@ def test_load_recipe_autoquantize_builtin_active_moe(): assert all(c.effective_bits is None for c in aq.candidate_formats) +def test_nvfp4_static_autoquant_cost_includes_scale_storage(): + """Static NVFP4 uses the packed-value and FP8-scale storage cost.""" + config = load_config("configs/numerics/nvfp4_static") + assert config.effective_bits == 4.5 + + +def test_load_nemotron_h_mse_autoquant_recipe(): + recipe = load_recipe("model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits") + + assert recipe.auto_quantize.constraints.effective_bits == 4.9 + assert recipe.auto_quantize.auto_quantize_method == "gradient" + assert len(recipe.auto_quantize.module_search_spaces) == 2 + assert recipe.quantize.algorithm == "max" + fixed_mtp_entries = [ + entry.model_dump() + for entry in recipe.quantize.quant_cfg + if entry.enable and "mtp.layers.1" in entry.quantizer_name + ] + assert len(fixed_mtp_entries) == 4 + assert all(entry["cfg"]["num_bits"] == (2, 1) for entry in fixed_mtp_entries) + assert all(entry["cfg"]["block_sizes"][-1] == 16 for entry in fixed_mtp_entries) + assert "*mtp*" in recipe.auto_quantize.cost_excluded_layers + assert all( + "mtp" not in pattern + for search_space in recipe.auto_quantize.module_search_spaces + for pattern in search_space.module_name_patterns + ) + + def test_load_recipe_autoquantize_module_search_spaces(): """Qwen recipe separates its fixed PTQ baseline from explicit search spaces.""" recipe = load_recipe( From 341415fe7fb81d82e494578fbb6b3362540abf1d Mon Sep 17 00:00:00 2001 From: weimingc <17592131+meenchen@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:57:29 -0700 Subject: [PATCH 2/7] refactor(recipe): keep one MSE AutoQuant recipe with fixed MTP Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> --- CHANGELOG.rst | 1 - .../configs/numerics/nvfp4_static.yaml | 2 - .../auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml | 29 ----- .../nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml | 112 +++++++++++++++++- .../auto_quantize/nvfp4_mse_fp8_search.yaml | 92 -------------- tests/unit/recipe/test_loader.py | 29 ----- 6 files changed, 107 insertions(+), 158 deletions(-) delete mode 100644 modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml delete mode 100644 modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 884454e8045..62b8d12b615 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -32,7 +32,6 @@ Changelog *Quantization* - Add Q8_0 weight-only quantization with 32-value GGML blocks, packed unified HF and Megatron export, and a built-in ``q8_0`` PTQ recipe. -- Add a Nemotron-H AutoQuantize recipe that searches NVFP4, FP8, and BF16 at 4.9 effective bits while keeping MTP routed and shared experts at NVFP4 W4A4. - Add Hugging Face PTQ calibration and export support for Nemotron-H MTP modules, preserving calibrated expert input scales. - Add the ``models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/ptq/nvfp4-mse`` PTQ recipe, the PTQ stage for NVIDIA Nemotron 3.5 Super VL: weight-MSE calibrated NVFP4 routed and shared MoE experts, FP8 attention projections, Mamba ``in_proj``/``out_proj`` and ``lm_head``, and an FP8 KV cache. The latent MoE, router, embeddings and the vision and audio encoders stay in BF16, and the MTP rules apply only to checkpoints that carry an MTP tail. It reproduces the released precision assignment, not the released weights: the published NVFP4 checkpoint adds quantization-aware distillation after this stage. - Backfill checkpoint aliases for three more published NVFP4 releases: ``moonshotai/Kimi-K2.7-Code``, ``google/gemma-4-26B-A4B-it`` and ``google/diffusiongemma-26B-A4B-it``. Each imports an existing general recipe wholesale rather than copying its body. diff --git a/modelopt_recipes/configs/numerics/nvfp4_static.yaml b/modelopt_recipes/configs/numerics/nvfp4_static.yaml index 4f2ca57eb90..9f6ac62e11e 100644 --- a/modelopt_recipes/configs/numerics/nvfp4_static.yaml +++ b/modelopt_recipes/configs/numerics/nvfp4_static.yaml @@ -21,5 +21,3 @@ block_sizes: -1: 16 type: static scale_bits: e4m3 -# Four value bits plus one FP8 scale per 16-element block. -effective_bits: 4.5 diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml deleted file mode 100644 index 57722559f5a..00000000000 --- a/modelopt_recipes/model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Fixed Nemotron-H MTP policy: NVFP4 W4A4 MoE projections and FP8 attention KV-cache BMMs. - -# modelopt-schema: modelopt.torch.quantization.config.QuantizeConfig -imports: - nvfp4: configs/numerics/nvfp4 - fp8: configs/numerics/fp8 - -algorithm: max -quant_cfg: - - quantizer_name: "*" - enable: false - - quantizer_name: "*mtp.layers.1.mixer.experts.*weight_quantizer" - enable: true - cfg: {$import: nvfp4} - - quantizer_name: "*mtp.layers.1.mixer.experts.*input_quantizer" - enable: true - cfg: {$import: nvfp4} - - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*weight_quantizer" - enable: true - cfg: {$import: nvfp4} - - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*input_quantizer" - enable: true - cfg: {$import: nvfp4} - - quantizer_name: "*mtp.layers.0.*[kv]_bmm_quantizer" - enable: true - cfg: {$import: fp8} diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml index adfd9b36b01..e751515d14d 100644 --- a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml +++ b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml @@ -3,17 +3,119 @@ # modelopt-schema: modelopt.recipe.config.ModelOptAutoQuantizeRecipe imports: - mtp: model_type/nemotron_h/auto_quantize/mtp_w4a4_nvfp4-kv_fp8 - search: model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search + base_cost_excluded_layers: configs/auto_quantize/units/base_cost_excluded_layers + nvfp4: configs/numerics/nvfp4 + nvfp4_static: configs/numerics/nvfp4_static + fp8: configs/numerics/fp8 + kv_fp8: configs/ptq/presets/kv/fp8 metadata: recipe_type: auto_quantize description: >- Nemotron-H AutoQuantize at 4.9 effective bits over weight-MSE NVFP4, FP8, and BF16, - with a fixed MTP quantization policy and vision/audio paths kept in BF16. + with MSE-calibrated MTP experts fixed at NVFP4 W4A4 and vision/audio in BF16. + +quantize: + algorithm: &mse + method: mse + fp8_scale_sweep: true + quant_cfg: + - quantizer_name: "*" + enable: false + - quantizer_name: "*mtp.layers.1.mixer.experts.*weight_quantizer" + enable: true + cfg: &nvfp4_weight + $import: nvfp4_static + # Four value bits plus one FP8 scale per 16-element block. + effective_bits: 4.5 + - quantizer_name: "*mtp.layers.1.mixer.experts.*input_quantizer" + enable: true + cfg: {$import: nvfp4} + - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*weight_quantizer" + enable: true + cfg: *nvfp4_weight + - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*input_quantizer" + enable: true + cfg: {$import: nvfp4} + - quantizer_name: "*mtp.layers.0.*[kv]_bmm_quantizer" + enable: true + cfg: {$import: fp8} -quantize: {$import: mtp} auto_quantize: - $import: search constraints: effective_bits: 4.9 + module_search_spaces: + - module_name_patterns: + - "*language_model.layers*" + - "*language_model.model.layers*" + - "*language_model.backbone.layers*" + candidate_formats: &search_candidates + - algorithm: *mse + quant_cfg: + - quantizer_name: "*" + enable: false + - quantizer_name: "*weight_quantizer" + enable: true + cfg: *nvfp4_weight + - quantizer_name: "*input_quantizer" + enable: true + cfg: {$import: nvfp4} + - algorithm: max + quant_cfg: + - quantizer_name: "*" + enable: false + - quantizer_name: "*weight_quantizer" + enable: true + cfg: {$import: fp8} + - quantizer_name: "*input_quantizer" + enable: true + cfg: {$import: fp8} + allow_no_quant: true + - module_name_patterns: + - "*lm_head*" + - "*output_layer*" + candidate_formats: *search_candidates + allow_no_quant: true + auto_quantize_method: gradient + score_size: 128 + kv_cache: {$import: kv_fp8} + disabled_layers: + - "*block_sparse_moe.gate*" + - "*linear_attn.conv1d*" + - "*linear_attn.in_proj_a*" + - "*linear_attn.in_proj_b*" + - "*mixer.conv1d*" + - "*mlp.gate.*" + - "*mlp.shared_expert_gate.*" + - "*shared_expert_gate*" + - "*proj_out.*" + - "*router*" + - "*vision*" + - "*visual*" + - "*image*" + - "*radio*" + - "*encoder*" + - "*model_encoder*" + - "*video*" + - "*audio*" + - "*speech*" + - "*multimodal_projector*" + - "*multi_modal_projector*" + - "*vision_projector*" + - "*audio_projector*" + cost_excluded_layers: + - $import: base_cost_excluded_layers + - "*vision*" + - "*visual*" + - "*image*" + - "*radio*" + - "*encoder*" + - "*model_encoder*" + - "*video*" + - "*audio*" + - "*speech*" + - "*multimodal_projector*" + - "*multi_modal_projector*" + - "*vision_projector*" + - "*audio_projector*" + - "*mtp*" diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml deleted file mode 100644 index cf2cb6eb7af..00000000000 --- a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_search.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Nemotron-H language-path search over weight-MSE NVFP4, FP8, and implicit BF16. - -# modelopt-schema: modelopt.recipe.config.AutoQuantizeConfig -imports: - base_cost_excluded_layers: configs/auto_quantize/units/base_cost_excluded_layers - nvfp4: configs/numerics/nvfp4 - nvfp4_static: configs/numerics/nvfp4_static - fp8: configs/numerics/fp8 - kv_fp8: configs/ptq/presets/kv/fp8 - -constraints: - effective_bits: 4.9 -module_search_spaces: - - module_name_patterns: - - "*language_model.layers*" - - "*language_model.model.layers*" - - "*language_model.backbone.layers*" - candidate_formats: &search_candidates - - algorithm: - method: mse - fp8_scale_sweep: true - quant_cfg: - - quantizer_name: "*" - enable: false - - quantizer_name: "*weight_quantizer" - enable: true - cfg: {$import: nvfp4_static} - - quantizer_name: "*input_quantizer" - enable: true - cfg: {$import: nvfp4} - - algorithm: max - quant_cfg: - - quantizer_name: "*" - enable: false - - quantizer_name: "*weight_quantizer" - enable: true - cfg: {$import: fp8} - - quantizer_name: "*input_quantizer" - enable: true - cfg: {$import: fp8} - allow_no_quant: true - - module_name_patterns: - - "*lm_head*" - - "*output_layer*" - candidate_formats: *search_candidates - allow_no_quant: true -auto_quantize_method: gradient -score_size: 128 -kv_cache: {$import: kv_fp8} -disabled_layers: - - "*block_sparse_moe.gate*" - - "*linear_attn.conv1d*" - - "*linear_attn.in_proj_a*" - - "*linear_attn.in_proj_b*" - - "*mixer.conv1d*" - - "*mlp.gate.*" - - "*mlp.shared_expert_gate.*" - - "*shared_expert_gate*" - - "*proj_out.*" - - "*router*" - - "*vision*" - - "*visual*" - - "*image*" - - "*radio*" - - "*encoder*" - - "*model_encoder*" - - "*video*" - - "*audio*" - - "*speech*" - - "*multimodal_projector*" - - "*multi_modal_projector*" - - "*vision_projector*" - - "*audio_projector*" -cost_excluded_layers: - - $import: base_cost_excluded_layers - - "*vision*" - - "*visual*" - - "*image*" - - "*radio*" - - "*encoder*" - - "*model_encoder*" - - "*video*" - - "*audio*" - - "*speech*" - - "*multimodal_projector*" - - "*multi_modal_projector*" - - "*vision_projector*" - - "*audio_projector*" - - "*mtp*" diff --git a/tests/unit/recipe/test_loader.py b/tests/unit/recipe/test_loader.py index 7ebc62c2f29..e1add8623bf 100644 --- a/tests/unit/recipe/test_loader.py +++ b/tests/unit/recipe/test_loader.py @@ -2632,35 +2632,6 @@ def test_load_recipe_autoquantize_builtin_active_moe(): assert all(c.effective_bits is None for c in aq.candidate_formats) -def test_nvfp4_static_autoquant_cost_includes_scale_storage(): - """Static NVFP4 uses the packed-value and FP8-scale storage cost.""" - config = load_config("configs/numerics/nvfp4_static") - assert config.effective_bits == 4.5 - - -def test_load_nemotron_h_mse_autoquant_recipe(): - recipe = load_recipe("model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits") - - assert recipe.auto_quantize.constraints.effective_bits == 4.9 - assert recipe.auto_quantize.auto_quantize_method == "gradient" - assert len(recipe.auto_quantize.module_search_spaces) == 2 - assert recipe.quantize.algorithm == "max" - fixed_mtp_entries = [ - entry.model_dump() - for entry in recipe.quantize.quant_cfg - if entry.enable and "mtp.layers.1" in entry.quantizer_name - ] - assert len(fixed_mtp_entries) == 4 - assert all(entry["cfg"]["num_bits"] == (2, 1) for entry in fixed_mtp_entries) - assert all(entry["cfg"]["block_sizes"][-1] == 16 for entry in fixed_mtp_entries) - assert "*mtp*" in recipe.auto_quantize.cost_excluded_layers - assert all( - "mtp" not in pattern - for search_space in recipe.auto_quantize.module_search_spaces - for pattern in search_space.module_name_patterns - ) - - def test_load_recipe_autoquantize_module_search_spaces(): """Qwen recipe separates its fixed PTQ baseline from explicit search spaces.""" recipe = load_recipe( From 321360e2ce646a3cb0a70631cabcda837aab1233 Mon Sep 17 00:00:00 2001 From: weimingc <17592131+meenchen@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:40:31 -0700 Subject: [PATCH 3/7] fix(recipe): cast KV and search standalone Nemotron-H decoders Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> --- .../nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml | 23 ++- .../test_nemotron_h_autoquant_recipe.py | 161 ++++++++++++++++++ 2 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 tests/unit/recipe/test_nemotron_h_autoquant_recipe.py diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml index e751515d14d..742ac49e490 100644 --- a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml +++ b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml @@ -1,5 +1,17 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# 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. # modelopt-schema: modelopt.recipe.config.ModelOptAutoQuantizeRecipe imports: @@ -7,13 +19,14 @@ imports: nvfp4: configs/numerics/nvfp4 nvfp4_static: configs/numerics/nvfp4_static fp8: configs/numerics/fp8 - kv_fp8: configs/ptq/presets/kv/fp8 + kv_fp8_cast: configs/ptq/presets/kv/fp8_cast metadata: recipe_type: auto_quantize description: >- Nemotron-H AutoQuantize at 4.9 effective bits over weight-MSE NVFP4, FP8, and BF16, - with MSE-calibrated MTP experts fixed at NVFP4 W4A4 and vision/audio in BF16. + with MSE-calibrated MTP experts fixed at NVFP4 W4A4, FP8 KV casting, + and vision/audio in BF16. quantize: algorithm: &mse @@ -39,13 +52,15 @@ quantize: cfg: {$import: nvfp4} - quantizer_name: "*mtp.layers.0.*[kv]_bmm_quantizer" enable: true - cfg: {$import: fp8} + cfg: {$import: fp8, use_constant_amax: true} auto_quantize: constraints: effective_bits: 4.9 module_search_spaces: - module_name_patterns: + - "model.layers.*" + - "backbone.layers.*" - "*language_model.layers*" - "*language_model.model.layers*" - "*language_model.backbone.layers*" @@ -78,7 +93,7 @@ auto_quantize: allow_no_quant: true auto_quantize_method: gradient score_size: 128 - kv_cache: {$import: kv_fp8} + kv_cache: {$import: kv_fp8_cast} disabled_layers: - "*block_sparse_moe.gate*" - "*linear_attn.conv1d*" diff --git a/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py b/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py new file mode 100644 index 00000000000..77bed68b9e6 --- /dev/null +++ b/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""Resolve the fixed-MTP recipe on native modules without running GPU calibration.""" + +import pytest +import torch + +native = pytest.importorskip("transformers.models.nemotron_h.modeling_nemotron_h") + +import modelopt.torch.quantization as mtq +from modelopt.recipe import load_recipe +from modelopt.torch.models.nemotron_h.mtp import _NemotronHMTP +from modelopt.torch.opt.conversion import apply_mode +from modelopt.torch.opt.utils import named_hparams +from modelopt.torch.quantization._auto_quantize_cost import get_auto_quantize_cost_model +from modelopt.torch.quantization.algorithms import AutoQuantizeGradientSearcher, QuantRecipe +from modelopt.torch.quantization.mode import QuantizeModeRegistry +from modelopt.torch.quantization.nn import TensorQuantizer + + +@pytest.mark.parametrize("wrapped", [False, True], ids=["standalone", "wrapped"]) +@pytest.mark.parametrize("decoder_name", ["model", "backbone"]) +@pytest.mark.parametrize("with_mtp", [False, True], ids=["no-mtp", "mtp"]) +def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, with_mtp): + """Search decoder/head groups at 4.5/8/16 bits, fix MTP experts, and cast KV.""" + config = native.NemotronHConfig( + vocab_size=32, + hidden_size=32, + intermediate_size=32, + layers_block_type=["attention", "moe"], + num_attention_heads=2, + num_key_value_heads=1, + head_dim=16, + n_routed_experts=2, + num_experts_per_tok=1, + moe_intermediate_size=32, + moe_shared_expert_intermediate_size=32, + use_mamba_kernels=False, + attn_implementation="eager", + mtp_layers_block_type=["attention", "moe"], + ) + language_model = native.NemotronHForCausalLM(config).eval() + # Keep real decoder modules while exercising both native and remote-code namespaces. + current_name = "model" if "model" in language_model._modules else "backbone" + if current_name != decoder_name: + language_model.add_module(decoder_name, language_model._modules.pop(current_name)) + if with_mtp: + language_model.mtp = _NemotronHMTP(config) + model = language_model + prefix = "" + if wrapped: + model = torch.nn.Module() + model.language_model = language_model + model.vision_model = torch.nn.Linear(32, 32) + prefix = "language_model." + + recipe = load_recipe("model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits") + aq = recipe.auto_quantize + fixed = QuantRecipe(recipe.quantize.model_dump(), name="fixed_mtp") + assert fixed.config.algorithm == {"method": "mse", "fp8_scale_sweep": True} + assert aq.auto_quantize_method == "gradient" + assert aq.constraints.effective_bits == 4.9 + + model = apply_mode(model, mode="auto_quantize", registry=QuantizeModeRegistry) + mtq.set_quantizer_by_cfg(model, fixed.config.quant_cfg) + searcher = AutoQuantizeGradientSearcher() + searcher.model = model + searcher.constraints = aq.constraints.model_dump(exclude_none=True) + searcher.config = {"cost": {"excluded_module_name_patterns": aq.cost_excluded_layers}} + searcher._cost_model = get_auto_quantize_cost_model("weight") + spaces = searcher._normalize_module_search_spaces( + [ + { + "module_name_patterns": space.module_name_patterns, + "quantization_formats": [ + (fmt.model_dump(), f"candidate_{index}") + for index, fmt in enumerate(space.candidate_formats) + ], + "allow_no_quant": space.allow_no_quant, + } + for space in aq.module_search_spaces + ] + ) + searcher.insert_hparams_after_merge_rules(model, [], aq.disabled_layers, spaces, fixed) + groups = [hparam for _, hparam in named_hparams(model, unique=True)] + searcher._verify_resolved_constraint(groups) + + searched_names, fixed_expert_names = set(), set() + for group in groups: + names = group.quant_module_names + bits = [choice.compression * 16 for choice in group.solver_choices] + if any(".mixer.router" in name or name.startswith("vision_model") for name in names): + assert bits == [16] + elif all( + name.startswith(prefix + decoder_name + ".layers.") for name in names + ) or names == [prefix + "lm_head"]: + assert bits == [4.5, 8, 16] + assert group.allow_no_quant and not group.is_fixed + assert group.cost_weight == 1 + assert group.solver_choices[0].config.algorithm == fixed.config.algorithm + searched_names.update(names) + elif all("mtp.layers.1.mixer." in name for name in names): + assert bits == [4.5] + assert group.is_fixed and not group.allow_no_quant + assert group.cost_weight == 0 + fixed_expert_names.update(names) + for module in group.quant_modules: + quantizers = [ + (name, q) + for name, q in module.named_modules() + if isinstance(q, TensorQuantizer) and ("weight" in name or "input" in name) + ] + assert quantizers + for name, quantizer in quantizers: + assert quantizer.is_enabled and quantizer.num_bits == (2, 1) + assert quantizer.block_sizes[-1] == 16 + assert quantizer.block_sizes["type"] == ( + "static" if "weight" in name else "dynamic" + ) + else: + assert all(name.startswith(prefix + "mtp.") for name in names) + assert bits == [16] and group.is_fixed and group.cost_weight == 0 + + assert prefix + "lm_head" in searched_names + assert any(".layers.0.mixer.q_proj" in name for name in searched_names) + assert any(".layers.1.mixer.experts" in name for name in searched_names) + assert bool(fixed_expert_names) == with_mtp + if with_mtp: + assert any(".mixer.experts" in name for name in fixed_expert_names) + assert any(".mixer.shared_experts" in name for name in fixed_expert_names) + + # The baseline already casts MTP KV; the post-search preset also casts base-model KV. + for stage in ("baseline", "post-search"): + if stage == "post-search": + mtq.set_quantizer_by_cfg(model, aq.kv_cache.quant_cfg) + kv = { + name: q + for name, q in model.named_modules() + if name.endswith(("k_bmm_quantizer", "v_bmm_quantizer")) + } + assert len(kv) == 2 + 2 * with_mtp + for name, quantizer in kv.items(): + enabled = stage == "post-search" or "mtp." in name + assert quantizer.is_enabled == enabled + if enabled: + assert quantizer.num_bits == (4, 3) and quantizer._use_constant_amax + assert quantizer._get_amax(torch.tensor([0.01, 1000.0])).item() == 448 + assert not hasattr(quantizer, "_amax") From 1ea057cf06f937f8737ff309839a600d1fed7595 Mon Sep 17 00:00:00 2001 From: weimingc <17592131+meenchen@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:13:01 -0700 Subject: [PATCH 4/7] fix(recipe): limit MTP quantization to routed experts Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> --- .../nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml | 21 +++++++------------ .../test_nemotron_h_autoquant_recipe.py | 19 +++++++++++------ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml index 742ac49e490..561afdc92d0 100644 --- a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml +++ b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml @@ -19,14 +19,14 @@ imports: nvfp4: configs/numerics/nvfp4 nvfp4_static: configs/numerics/nvfp4_static fp8: configs/numerics/fp8 - kv_fp8_cast: configs/ptq/presets/kv/fp8_cast + kv_fp8_cast: configs/ptq/units/kv_fp8_cast metadata: recipe_type: auto_quantize description: >- Nemotron-H AutoQuantize at 4.9 effective bits over weight-MSE NVFP4, FP8, and BF16, - with MSE-calibrated MTP experts fixed at NVFP4 W4A4, FP8 KV casting, - and vision/audio in BF16. + with MSE-calibrated MTP routed experts fixed at NVFP4 W4A4 and base-model FP8 KV casting. + Other MTP modules and vision/audio remain in BF16; MTP KV quantization is disabled. quantize: algorithm: &mse @@ -44,15 +44,6 @@ quantize: - quantizer_name: "*mtp.layers.1.mixer.experts.*input_quantizer" enable: true cfg: {$import: nvfp4} - - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*weight_quantizer" - enable: true - cfg: *nvfp4_weight - - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*input_quantizer" - enable: true - cfg: {$import: nvfp4} - - quantizer_name: "*mtp.layers.0.*[kv]_bmm_quantizer" - enable: true - cfg: {$import: fp8, use_constant_amax: true} auto_quantize: constraints: @@ -93,7 +84,11 @@ auto_quantize: allow_no_quant: true auto_quantize_method: gradient score_size: 128 - kv_cache: {$import: kv_fp8_cast} + kv_cache: + quant_cfg: + - $import: kv_fp8_cast + - quantizer_name: "*mtp.*[kv]_bmm_quantizer" + enable: false disabled_layers: - "*block_sparse_moe.gate*" - "*linear_attn.conv1d*" diff --git a/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py b/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py index 77bed68b9e6..47b32916c84 100644 --- a/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py +++ b/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py @@ -35,7 +35,7 @@ @pytest.mark.parametrize("decoder_name", ["model", "backbone"]) @pytest.mark.parametrize("with_mtp", [False, True], ids=["no-mtp", "mtp"]) def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, with_mtp): - """Search decoder/head groups at 4.5/8/16 bits, fix MTP experts, and cast KV.""" + """Search decoder/head groups, fix MTP routed experts, and cast only base-model KV.""" config = native.NemotronHConfig( vocab_size=32, hidden_size=32, @@ -98,7 +98,7 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit groups = [hparam for _, hparam in named_hparams(model, unique=True)] searcher._verify_resolved_constraint(groups) - searched_names, fixed_expert_names = set(), set() + searched_names, fixed_expert_names, bf16_mtp_names = set(), set(), set() for group in groups: names = group.quant_module_names bits = [choice.compression * 16 for choice in group.solver_choices] @@ -112,7 +112,7 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit assert group.cost_weight == 1 assert group.solver_choices[0].config.algorithm == fixed.config.algorithm searched_names.update(names) - elif all("mtp.layers.1.mixer." in name for name in names): + elif all("mtp.layers.1.mixer.experts" in name for name in names): assert bits == [4.5] assert group.is_fixed and not group.allow_no_quant assert group.cost_weight == 0 @@ -133,6 +133,7 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit else: assert all(name.startswith(prefix + "mtp.") for name in names) assert bits == [16] and group.is_fixed and group.cost_weight == 0 + bf16_mtp_names.update(names) assert prefix + "lm_head" in searched_names assert any(".layers.0.mixer.q_proj" in name for name in searched_names) @@ -140,12 +141,18 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit assert bool(fixed_expert_names) == with_mtp if with_mtp: assert any(".mixer.experts" in name for name in fixed_expert_names) - assert any(".mixer.shared_experts" in name for name in fixed_expert_names) + assert any(".mixer.shared_experts" in name for name in bf16_mtp_names) - # The baseline already casts MTP KV; the post-search preset also casts base-model KV. + # Post-search KV casting must preserve the routed-experts-only MTP policy. for stage in ("baseline", "post-search"): if stage == "post-search": mtq.set_quantizer_by_cfg(model, aq.kv_cache.quant_cfg) + for name, quantizer in model.named_modules(): + if isinstance(quantizer, TensorQuantizer) and name.startswith(prefix + "mtp."): + routed_io = name.startswith(prefix + "mtp.layers.1.mixer.experts.") and ( + "weight_quantizer" in name or "input_quantizer" in name + ) + assert quantizer.is_enabled == routed_io kv = { name: q for name, q in model.named_modules() @@ -153,7 +160,7 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit } assert len(kv) == 2 + 2 * with_mtp for name, quantizer in kv.items(): - enabled = stage == "post-search" or "mtp." in name + enabled = stage == "post-search" and "mtp." not in name assert quantizer.is_enabled == enabled if enabled: assert quantizer.num_bits == (4, 3) and quantizer._use_constant_amax From c916c0d209948c2e50413e97e3f883dafddb940c Mon Sep 17 00:00:00 2001 From: weimingc <17592131+meenchen@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:47:39 -0700 Subject: [PATCH 5/7] fix(recipe): calibrate MTP KV and quantize shared experts Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> --- .../nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml | 19 ++++- .../test_nemotron_h_autoquant_recipe.py | 79 ++++++++++++++++--- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml index 561afdc92d0..a0f5f8211d7 100644 --- a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml +++ b/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml @@ -25,8 +25,8 @@ metadata: recipe_type: auto_quantize description: >- Nemotron-H AutoQuantize at 4.9 effective bits over weight-MSE NVFP4, FP8, and BF16, - with MSE-calibrated MTP routed experts fixed at NVFP4 W4A4 and base-model FP8 KV casting. - Other MTP modules and vision/audio remain in BF16; MTP KV quantization is disabled. + with MSE-calibrated MTP routed/shared experts fixed at NVFP4 W4A4, calibrated MTP FP8 KV, + and base-model FP8 KV casting. Other MTP modules and vision/audio remain in BF16. quantize: algorithm: &mse @@ -44,6 +44,18 @@ quantize: - quantizer_name: "*mtp.layers.1.mixer.experts.*input_quantizer" enable: true cfg: {$import: nvfp4} + - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*weight_quantizer" + enable: true + cfg: *nvfp4_weight + - quantizer_name: "*mtp.layers.1.mixer.shared_experts.*input_quantizer" + enable: true + cfg: {$import: nvfp4} + # Collect MTP KV scales in the fixed calibration pass, before base-model KV casting. + - quantizer_name: "*mtp.*[kv]_bmm_quantizer" + enable: true + cfg: &mtp_kv_fp8 + $import: fp8 + use_constant_amax: false auto_quantize: constraints: @@ -88,7 +100,8 @@ auto_quantize: quant_cfg: - $import: kv_fp8_cast - quantizer_name: "*mtp.*[kv]_bmm_quantizer" - enable: false + enable: true + cfg: *mtp_kv_fp8 disabled_layers: - "*block_sparse_moe.gate*" - "*linear_attn.conv1d*" diff --git a/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py b/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py index 47b32916c84..6fd084fb863 100644 --- a/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py +++ b/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py @@ -22,12 +22,14 @@ import modelopt.torch.quantization as mtq from modelopt.recipe import load_recipe -from modelopt.torch.models.nemotron_h.mtp import _NemotronHMTP +from modelopt.torch.export.quant_utils import postprocess_state_dict +from modelopt.torch.models.nemotron_h.mtp import _NemotronHMTP, prepare_for_calibration from modelopt.torch.opt.conversion import apply_mode from modelopt.torch.opt.utils import named_hparams from modelopt.torch.quantization._auto_quantize_cost import get_auto_quantize_cost_model from modelopt.torch.quantization.algorithms import AutoQuantizeGradientSearcher, QuantRecipe from modelopt.torch.quantization.mode import QuantizeModeRegistry +from modelopt.torch.quantization.model_calib import max_calibrate from modelopt.torch.quantization.nn import TensorQuantizer @@ -35,7 +37,7 @@ @pytest.mark.parametrize("decoder_name", ["model", "backbone"]) @pytest.mark.parametrize("with_mtp", [False, True], ids=["no-mtp", "mtp"]) def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, with_mtp): - """Search decoder/head groups, fix MTP routed experts, and cast only base-model KV.""" + """Search decoder/head groups, fix MTP experts, and preserve calibrated MTP KV scales.""" config = native.NemotronHConfig( vocab_size=32, hidden_size=32, @@ -112,7 +114,10 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit assert group.cost_weight == 1 assert group.solver_choices[0].config.algorithm == fixed.config.algorithm searched_names.update(names) - elif all("mtp.layers.1.mixer.experts" in name for name in names): + elif all( + "mtp.layers.1.mixer.experts" in name or "mtp.layers.1.mixer.shared_experts." in name + for name in names + ): assert bits == [4.5] assert group.is_fixed and not group.allow_no_quant assert group.cost_weight == 0 @@ -141,18 +146,49 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit assert bool(fixed_expert_names) == with_mtp if with_mtp: assert any(".mixer.experts" in name for name in fixed_expert_names) - assert any(".mixer.shared_experts" in name for name in bf16_mtp_names) + for projection in ("up_proj", "down_proj"): + assert prefix + f"mtp.layers.1.mixer.shared_experts.{projection}" in fixed_expert_names + assert bf16_mtp_names + + calibrated_amax = {} + # Exercise actual KV activation collection without CPU NVFP4 weight calibration. + if with_mtp and decoder_name == "model": + prepare_for_calibration(model) + with mtq.set_quantizer_by_cfg_context( + model, + [ + {"quantizer_name": "*weight_quantizer*", "enable": False}, + {"quantizer_name": "*input_quantizer", "enable": False}, + ], + ): + max_calibrate( + model, + lambda _: language_model(torch.tensor([[1, 2, 3, 4]]), use_cache=False), + distributed_sync=False, + ) + calibrated_amax = { + name: quantizer.amax.clone() + for name, quantizer in model.named_modules() + if "mtp." in name and name.endswith(("k_bmm_quantizer", "v_bmm_quantizer")) + } + assert len(calibrated_amax) == 2 + for amax in calibrated_amax.values(): + assert torch.isfinite(amax).all() and amax.max() > 0 - # Post-search KV casting must preserve the routed-experts-only MTP policy. + # Post-search casting must retain the fixed MTP quantizers and their collected scales. for stage in ("baseline", "post-search"): if stage == "post-search": mtq.set_quantizer_by_cfg(model, aq.kv_cache.quant_cfg) for name, quantizer in model.named_modules(): if isinstance(quantizer, TensorQuantizer) and name.startswith(prefix + "mtp."): - routed_io = name.startswith(prefix + "mtp.layers.1.mixer.experts.") and ( - "weight_quantizer" in name or "input_quantizer" in name - ) - assert quantizer.is_enabled == routed_io + expert_io = name.startswith( + ( + prefix + "mtp.layers.1.mixer.experts.", + prefix + "mtp.layers.1.mixer.shared_experts.", + ) + ) and ("weight_quantizer" in name or "input_quantizer" in name) + mtp_kv = name.endswith(("k_bmm_quantizer", "v_bmm_quantizer")) + assert quantizer.is_enabled == (expert_io or mtp_kv) kv = { name: q for name, q in model.named_modules() @@ -160,9 +196,26 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit } assert len(kv) == 2 + 2 * with_mtp for name, quantizer in kv.items(): - enabled = stage == "post-search" and "mtp." not in name + is_mtp = "mtp." in name + enabled = stage == "post-search" or is_mtp assert quantizer.is_enabled == enabled if enabled: - assert quantizer.num_bits == (4, 3) and quantizer._use_constant_amax - assert quantizer._get_amax(torch.tensor([0.01, 1000.0])).item() == 448 - assert not hasattr(quantizer, "_amax") + assert quantizer.num_bits == (4, 3) + assert quantizer._use_constant_amax == (not is_mtp) + if not is_mtp: + assert quantizer._get_amax(torch.tensor([0.01, 1000.0])).item() == 448 + assert not hasattr(quantizer, "_amax") + elif name in calibrated_amax: + torch.testing.assert_close(quantizer.amax, calibrated_amax[name]) + + if calibrated_amax: + exported = postprocess_state_dict(model.state_dict(), maxbound=448, quantization="FP8") + for name, amax in calibrated_amax.items(): + projection = "k_proj.k_scale" if name.endswith("k_bmm_quantizer") else "v_proj.v_scale" + torch.testing.assert_close( + exported[name.rsplit(".", 1)[0] + "." + projection], amax / 448 + ) + assert not any( + name.startswith(prefix + decoder_name + ".") and name.endswith(("k_scale", "v_scale")) + for name in exported + ) From 8ca7e5b46ae0aa7af2a1cdecd360533622c3d147 Mon Sep 17 00:00:00 2001 From: weimingc <17592131+meenchen@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:54:51 -0700 Subject: [PATCH 6/7] refactor(recipe): scope AutoQuant to Nemotron 3.5 Super VL Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> --- CHANGELOG.rst | 1 + modelopt_recipes/README.md | 5 ++++ .../auto_quantize/README.md | 26 +++++++++++++++++++ .../nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml | 7 +++-- ...nemotron_3_5_super_vl_autoquant_recipe.py} | 7 +++-- 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md rename modelopt_recipes/{model_type/nemotron_h => models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16}/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml (92%) rename tests/unit/recipe/{test_nemotron_h_autoquant_recipe.py => test_nemotron_3_5_super_vl_autoquant_recipe.py} (97%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 62b8d12b615..53cf26feb20 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Changelog *Quantization* - Add imatrix calibration (``algorithm: {method: imatrix}``) for the GGML formats with a weighted search: each weight is encoded once, its search weighted by the square root of its inputs' per-column ``E[x^2]`` (llama.cpp's imatrix statistic), and HF fused-MoE experts get one statistic per expert. A weight that already holds final codes is left alone, so ``[{method: gptq}, {method: imatrix}]`` runs GPTQ on the dense linears and imatrix on the experts GPTQ skips; the built-in IQ1 and IQ2 recipes now do this, and new ``_experts_only`` recipes apply imatrix to the routed MoE experts alone. +- Add a checkpoint-specific NVIDIA Nemotron 3.5 Super VL AutoQuantize recipe with a 4.9-effective-bit NVFP4-MSE/FP8/BF16 search and fixed NVFP4 MTP experts. Select it under ``models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/``. **Backward Breaking Changes** diff --git a/modelopt_recipes/README.md b/modelopt_recipes/README.md index 0d945dcfc8b..eb4526361ab 100644 --- a/modelopt_recipes/README.md +++ b/modelopt_recipes/README.md @@ -92,6 +92,11 @@ Hugging Face Hub, ModelScope, etc.). Browse [`models/`](models/) for the available checkpoints; see [`models/README.md`](models/README.md) for the naming convention. +For NVIDIA Nemotron 3.5 Super VL, the checkpoint-specific +[AutoQuantize recipe](models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/) +searches NVFP4/FP8/BF16 placements with fixed MTP expert quantization. It produces +a search candidate, not the released QAD-refined weights. + --- ## Adding a recipe diff --git a/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md b/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md new file mode 100644 index 00000000000..8c13395b7db --- /dev/null +++ b/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md @@ -0,0 +1,26 @@ +# NVIDIA Nemotron 3.5 Super VL AutoQuantize + +`nvfp4_mse_fp8_mtp_fixed_at_4p9bits` searches decoder modules and `lm_head` over +weight-MSE NVFP4, FP8, and BF16 using gradient-based sensitivity scores. It is +tuned for `nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16`, not a default for +all Nemotron-H checkpoints. The search generates a mixed-precision candidate; +it does not reproduce the published QAD-refined NVFP4 weights. + +MTP routed/shared experts stay fixed at MSE-calibrated NVFP4 W4A4. MTP KV uses +calibrated FP8 scales, while backbone KV uses fixed-range FP8 casting. Other MTP +modules and the vision/audio stack remain BF16. Fixed MTP and non-language +modules are excluded from the search cost, so the 4.9-bit budget is not the +whole-checkpoint bits per parameter. + +```bash +python examples/hf_ptq/hf_ptq.py \ + --pyt_ckpt_path nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16 \ + --recipe models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits \ + --calib_size 128 \ + --use_seq_device_map \ + --export_path +``` + +Review the checkpoint's modeling code before adding `--trust_remote_code` if +required. The example AutoQuantize CLI currently accepts text calibration only; +do not copy the sibling PTQ recipe's `--calib_with_images` invocation. diff --git a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml b/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml similarity index 92% rename from modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml rename to modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml index a0f5f8211d7..da923620b4c 100644 --- a/modelopt_recipes/model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml +++ b/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits.yaml @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# AutoQuantize search for nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16. +# This selects a mixed-precision candidate, not the released QAD-refined weights. # modelopt-schema: modelopt.recipe.config.ModelOptAutoQuantizeRecipe imports: base_cost_excluded_layers: configs/auto_quantize/units/base_cost_excluded_layers @@ -22,11 +24,12 @@ imports: kv_fp8_cast: configs/ptq/units/kv_fp8_cast metadata: - recipe_type: auto_quantize description: >- - Nemotron-H AutoQuantize at 4.9 effective bits over weight-MSE NVFP4, FP8, and BF16, + AutoQuantize for nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16 at + 4.9 effective bits over weight-MSE NVFP4, FP8, and BF16, with MSE-calibrated MTP routed/shared experts fixed at NVFP4 W4A4, calibrated MTP FP8 KV, and base-model FP8 KV casting. Other MTP modules and vision/audio remain in BF16. + This search does not reproduce the released QAD-refined weights. quantize: algorithm: &mse diff --git a/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py b/tests/unit/recipe/test_nemotron_3_5_super_vl_autoquant_recipe.py similarity index 97% rename from tests/unit/recipe/test_nemotron_h_autoquant_recipe.py rename to tests/unit/recipe/test_nemotron_3_5_super_vl_autoquant_recipe.py index 6fd084fb863..1f44b3e64c8 100644 --- a/tests/unit/recipe/test_nemotron_h_autoquant_recipe.py +++ b/tests/unit/recipe/test_nemotron_3_5_super_vl_autoquant_recipe.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Resolve the fixed-MTP recipe on native modules without running GPU calibration.""" +"""Resolve the Super 3.5 VL AutoQuantize recipe without running GPU calibration.""" import pytest import torch @@ -69,7 +69,10 @@ def test_4p9_mse_recipe_resolves_search_and_fixed_mtp(wrapped, decoder_name, wit model.vision_model = torch.nn.Linear(32, 32) prefix = "language_model." - recipe = load_recipe("model_type/nemotron_h/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits") + recipe = load_recipe( + "models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/" + "nvfp4_mse_fp8_mtp_fixed_at_4p9bits" + ) aq = recipe.auto_quantize fixed = QuantRecipe(recipe.quantize.model_dump(), name="fixed_mtp") assert fixed.config.algorithm == {"method": "mse", "fp8_scale_sweep": True} From d241cef4c49ab4800d913870db72c63baff9abcb Mon Sep 17 00:00:00 2001 From: weimingc <17592131+meenchen@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:41:17 -0700 Subject: [PATCH 7/7] docs(recipe): omit optional AutoQuant README Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> --- modelopt_recipes/README.md | 5 ---- .../auto_quantize/README.md | 26 ------------------- 2 files changed, 31 deletions(-) delete mode 100644 modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md diff --git a/modelopt_recipes/README.md b/modelopt_recipes/README.md index eb4526361ab..0d945dcfc8b 100644 --- a/modelopt_recipes/README.md +++ b/modelopt_recipes/README.md @@ -92,11 +92,6 @@ Hugging Face Hub, ModelScope, etc.). Browse [`models/`](models/) for the available checkpoints; see [`models/README.md`](models/README.md) for the naming convention. -For NVIDIA Nemotron 3.5 Super VL, the checkpoint-specific -[AutoQuantize recipe](models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/) -searches NVFP4/FP8/BF16 placements with fixed MTP expert quantization. It produces -a search candidate, not the released QAD-refined weights. - --- ## Adding a recipe diff --git a/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md b/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md deleted file mode 100644 index 8c13395b7db..00000000000 --- a/modelopt_recipes/models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# NVIDIA Nemotron 3.5 Super VL AutoQuantize - -`nvfp4_mse_fp8_mtp_fixed_at_4p9bits` searches decoder modules and `lm_head` over -weight-MSE NVFP4, FP8, and BF16 using gradient-based sensitivity scores. It is -tuned for `nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16`, not a default for -all Nemotron-H checkpoints. The search generates a mixed-precision candidate; -it does not reproduce the published QAD-refined NVFP4 weights. - -MTP routed/shared experts stay fixed at MSE-calibrated NVFP4 W4A4. MTP KV uses -calibrated FP8 scales, while backbone KV uses fixed-range FP8 casting. Other MTP -modules and the vision/audio stack remain BF16. Fixed MTP and non-language -modules are excluded from the search cost, so the 4.9-bit budget is not the -whole-checkpoint bits per parameter. - -```bash -python examples/hf_ptq/hf_ptq.py \ - --pyt_ckpt_path nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16 \ - --recipe models/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16/auto_quantize/nvfp4_mse_fp8_mtp_fixed_at_4p9bits \ - --calib_size 128 \ - --use_seq_device_map \ - --export_path -``` - -Review the checkpoint's modeling code before adding `--trust_remote_code` if -required. The example AutoQuantize CLI currently accepts text calibration only; -do not copy the sibling PTQ recipe's `--calib_with_images` invocation.