Skip to content

feat(quant): add constant_amax to pin activation input_scale (NVFP4 experts input_scale=1.0)#1947

Open
Edwardf0t1 wants to merge 2 commits into
mainfrom
feat/nvfp4-constant-amax-input-scale
Open

feat(quant): add constant_amax to pin activation input_scale (NVFP4 experts input_scale=1.0)#1947
Edwardf0t1 wants to merge 2 commits into
mainfrom
feat/nvfp4-constant-amax-input-scale

Conversation

@Edwardf0t1

@Edwardf0t1 Edwardf0t1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: new feature

Adds a constant_amax field to QuantizerAttributeConfig that pins a quantizer's amax to a fixed value and skips activation calibration (no forward statistics collected). Unlike the existing use_constant_amax — which hardcodes the amax to the FP8 E4M3 range (448.0) for KV-cache cast math and registers no _amax buffer — constant_amax stores the constant on the _amax buffer so it is used by both the fake-quant forward pass and the exported scaling factor (e.g. input_scale).

Motivation: pin the NVFP4 routed-expert activation input_scale to exactly 1.0 in the exported checkpoint, with no activation calibration. For NVFP4 the exported activation scale is input_scale = amax / (E2M1_MAX * E4M3_MAX) = amax / (6 * 448), so constant_amax = 2688.0 yields input_scale == 1.0.

Changes:

  • config.py: new constant_amax: float | None field + positive-value validator.
  • tensor_quantizer.py: setter registers the constant on the _amax buffer.
  • model_calib.py: enable_stats_collection / finish_stats_collection exclude constant-amax quantizers from calibration (so the fixed amax is never overwritten).
  • New recipe modelopt_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml applying it to the MoE expert activation quantizers.

Usage

from modelopt.torch.quantization.config import QuantizerAttributeConfig

# NVFP4 activation quantizer with a fixed input_scale == 1.0, no calibration:
cfg = QuantizerAttributeConfig(
    num_bits=(2, 1),
    block_sizes={-1: 16, "type": "dynamic", "scale_bits": (4, 3)},
    constant_amax=2688.0,  # 6 * 448 = E2M1_MAX * E4M3_MAX -> exported input_scale == 1.0
)

Or via the shipped recipe:

# modelopt_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml
python examples/hf_ptq/hf_ptq.py --recipe nvfp4_experts_only_input_scale1-kv_fp8_cast ...

Testing

  • New CPU unit tests in tests/_test_utils/torch/quantization/tensor_quantizer_common.py (run via test_tensor_quantizer_cpu.py):
    • test_constant_amax_registers_amax_amax buffer pinned to the constant; default (unset) registers no buffer.
    • test_constant_amax_nvfp4_input_scaleconstant_amax=2688 ⇒ exported NVFP4 input_scale == 1.0.
    • test_constant_amax_skips_calibration — quantizer excluded from calibration; amax not overwritten.
    • test_constant_amax_must_be_positive — validator rejects non-positive values.
  • pytest tests/unit/torch/quantization/test_tensor_quantizer_cpu.py tests/unit/torch/quantization/test_config_validation.py — 109 passed.
  • pre-commit run --files ... — all hooks pass (incl. validate modelopt recipes).
  • Verified the new recipe loads through modelopt.recipe.loader.load_recipe, with $import: nvfp4 correctly merging constant_amax: 2688.0.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ (new opt-in field defaulting to None; no behavior change when unset)
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅
  • Did you get Claude approval on this PR?: ❌ (pending)

Additional Information

The exported per-block E4M3 activation scales remain dynamic; only the per-tensor global scale (input_scale) is pinned to 1.0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for pinning a quantizer’s activation amax to a user-specified constant, keeping behavior consistent for export and inference.
    • Added a new expert-only NVFP4 PTQ recipe with FP8 KV-cache casting and fixed expert activation scaling.
  • Bug Fixes
    • Improved calibration/stat handling so constant-amax quantizers are excluded during calibration and restored afterward.
  • Tests
    • Added coverage for fixed-amax behavior, validation rules (positive value, mutual exclusivity), and NVFP4 scaling/export results.

@Edwardf0t1 Edwardf0t1 requested review from a team as code owners July 8, 2026 20:56
@Edwardf0t1 Edwardf0t1 requested a review from cjluo-nv July 8, 2026 20:56
@coderabbitai

coderabbitai Bot commented Jul 8, 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: 1f3ce1d5-162e-4e4d-9ea5-fb8ef1bc193a

📥 Commits

Reviewing files that changed from the base of the PR and between 932c30e and f224624.

📒 Files selected for processing (3)
  • modelopt/torch/quantization/config.py
  • modelopt/torch/quantization/nn/modules/tensor_quantizer.py
  • tests/_test_utils/torch/quantization/tensor_quantizer_common.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/_test_utils/torch/quantization/tensor_quantizer_common.py
  • modelopt/torch/quantization/config.py
  • modelopt/torch/quantization/nn/modules/tensor_quantizer.py

📝 Walkthrough

Walkthrough

Adds constant_amax to QuantizerAttributeConfig, wires it through TensorQuantizer and calibration handling, adds an NVFP4 experts-only PTQ recipe using it, and covers the new behavior with tests and changelog notes.

Changes

Constant amax feature

Layer / File(s) Summary
Config field and validator for constant_amax
modelopt/torch/quantization/config.py
Adds constant_amax to QuantizerAttributeConfig and validates that it is positive and not combined with use_constant_amax.
TensorQuantizer wiring for constant_amax
modelopt/torch/quantization/nn/modules/tensor_quantizer.py
Initializes _constant_amax, routes constant_amax through a custom setter, pins self.amax when set, and aligns _amax buffers to the input device.
Calibration exclusion for constant-amax quantizers
modelopt/torch/quantization/model_calib.py
Treats quantizers with _constant_amax is not None the same as _use_constant_amax during stats collection enable/finish handling.
NVFP4 experts-only recipe using constant_amax
modelopt_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml
Adds a PTQ recipe that applies NVFP4 to expert quantizers, pins expert and block-sparse MoE activation quantizers with constant_amax: 2688.0, and composes KV FP8 cast imports.
Tests and changelog for constant_amax
tests/_test_utils/torch/quantization/tensor_quantizer_common.py, CHANGELOG.rst
Adds coverage for _amax registration, NVFP4 input scale behavior, calibration skipping, validation errors, and documents the new field and recipe.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • NVIDIA/Model-Optimizer#1935: Touches the same constant-amax quantizer path in TensorQuantizer, overlapping with this PR’s new constant_amax wiring and behavior.
  • Suggested reviewers: kinjalpatel27
🚥 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: adding constant_amax to pin activation input_scale for NVFP4 experts.
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 PASS: The PR-touched Python files add no forbidden torch.load/numpy.load/trust_remote_code/eval/exec/nosec patterns, and no new pip deps were added.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/nvfp4-constant-amax-input-scale

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

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-1947/

Built to branch gh-pages at 2026-07-09 20:47 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modelopt/torch/quantization/config.py (1)

665-696: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Forbid setting both constant-amax modes modelopt/torch/quantization/config.py:665-696use_constant_amax and constant_amax can both be set today. That makes fake-quant use FP8 E4M3 max while export still uses the pinned _amax value, so the simulated and exported scales can silently diverge. Add a model_validator to reject the combination.

🤖 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/quantization/config.py` around lines 665 - 696, The
quantization config currently allows both use_constant_amax and constant_amax to
be set at the same time, which can make the fake-quant path and exported scaling
diverge. Add a model-level validator on the config class in config.py that
checks these two fields together and raises a clear validation error when both
are provided, while keeping the existing field-level validate_constant_amax
check for positivity.
🧹 Nitpick comments (1)
modelopt_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml (1)

46-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated constant_amax/nvfp4 block across experts.* and block_sparse_moe* patterns.

Both quantizer-name blocks apply identical cfg payloads (nvfp4 import + constant_amax: 2688.0) just to cover two different MoE naming conventions. A YAML anchor could remove the duplication and prevent the two copies from drifting if the constant ever needs updating.

♻️ Optional refactor using YAML anchors
   quant_cfg:
     - $import: base_disable_all
-    - quantizer_name: '*.experts.*weight_quantizer'
-      cfg:
-        $import: nvfp4
+    - quantizer_name: '*.experts.*weight_quantizer'
+      cfg: &nvfp4_weight
+        $import: nvfp4
     - quantizer_name: '*.experts.*input_quantizer'
-      cfg:
-        $import: nvfp4
-        # 2688 = E2M1_MAX * E4M3_MAX (6 * 448) -> exported input_scale == 1.0, no calibration.
-        constant_amax: 2688.0
+      cfg: &nvfp4_input_pinned
+        $import: nvfp4
+        # 2688 = E2M1_MAX * E4M3_MAX (6 * 448) -> exported input_scale == 1.0, no calibration.
+        constant_amax: 2688.0
     - quantizer_name: '*block_sparse_moe*weight_quantizer'
-      cfg:
-        $import: nvfp4
+      cfg: *nvfp4_weight
     - quantizer_name: '*block_sparse_moe*input_quantizer'
-      cfg:
-        $import: nvfp4
-        # 2688 = E2M1_MAX * E4M3_MAX (6 * 448) -> exported input_scale == 1.0, no calibration.
-        constant_amax: 2688.0
+      cfg: *nvfp4_input_pinned
     - $import: kv_fp8_cast
     - $import: default_disabled_quantizers
🤖 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_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml`
around lines 46 - 61, Refactor the duplicated MoE quantizer config by defining a
shared YAML anchor for the repeated nvfp4 payload and constant_amax value, then
reuse it for both the '*.experts.*input_quantizer' and
'*block_sparse_moe*input_quantizer' entries (and the matching weight_quantizer
blocks if appropriate) so the two naming conventions stay in sync. Locate the
duplicated configuration in the ptq recipe section that sets $import: nvfp4 and
constant_amax: 2688.0, and replace the repeated blocks with aliases while
preserving the existing matching patterns and behavior.
🤖 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 `@tests/_test_utils/torch/quantization/tensor_quantizer_common.py`:
- Around line 294-307: The test method `test_constant_amax_nvfp4_input_scale`
imports `NVFP4QTensor` locally without any stated reason, which violates the
test import convention here. Move `from
modelopt.torch.quantization.qtensor.nvfp4_tensor import NVFP4QTensor` to the
module-level imports in `tensor_quantizer_common.py` with the other test
dependencies, and remove the in-method import so the test uses the shared
top-level symbol.

---

Outside diff comments:
In `@modelopt/torch/quantization/config.py`:
- Around line 665-696: The quantization config currently allows both
use_constant_amax and constant_amax to be set at the same time, which can make
the fake-quant path and exported scaling diverge. Add a model-level validator on
the config class in config.py that checks these two fields together and raises a
clear validation error when both are provided, while keeping the existing
field-level validate_constant_amax check for positivity.

---

Nitpick comments:
In
`@modelopt_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml`:
- Around line 46-61: Refactor the duplicated MoE quantizer config by defining a
shared YAML anchor for the repeated nvfp4 payload and constant_amax value, then
reuse it for both the '*.experts.*input_quantizer' and
'*block_sparse_moe*input_quantizer' entries (and the matching weight_quantizer
blocks if appropriate) so the two naming conventions stay in sync. Locate the
duplicated configuration in the ptq recipe section that sets $import: nvfp4 and
constant_amax: 2688.0, and replace the repeated blocks with aliases while
preserving the existing matching patterns and behavior.
🪄 Autofix (Beta)

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: 6f80259d-94df-4eaa-94a2-3ec0bf4d4b80

📥 Commits

Reviewing files that changed from the base of the PR and between 2d4e472 and 932c30e.

📒 Files selected for processing (6)
  • CHANGELOG.rst
  • modelopt/torch/quantization/config.py
  • modelopt/torch/quantization/model_calib.py
  • modelopt/torch/quantization/nn/modules/tensor_quantizer.py
  • modelopt_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml
  • tests/_test_utils/torch/quantization/tensor_quantizer_common.py

Comment thread tests/_test_utils/torch/quantization/tensor_quantizer_common.py
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 74.61%. Comparing base (0593df0) to head (4e293de).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
.../torch/quantization/nn/modules/tensor_quantizer.py 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1947      +/-   ##
==========================================
- Coverage   77.80%   74.61%   -3.19%     
==========================================
  Files         519      519              
  Lines       57960    57977      +17     
==========================================
- Hits        45095    43262    -1833     
- Misses      12865    14715    +1850     
Flag Coverage Δ
examples 41.73% <89.47%> (-1.67%) ⬇️
gpu 49.38% <94.73%> (-9.08%) ⬇️
unit 55.25% <94.73%> (+0.01%) ⬆️

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.

if val is not None:
# Pin amax to the constant on the _amax buffer so it is used by both the
# fake-quant forward pass and export; calibration is skipped in model_calib.
self.amax = float(val)

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.

Hi Zhiyu, the pinned _amax buffer is registered on CPU and never moved to the model's device, will this have any implications. the test does the .to(device) so no errors are caught

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 catch — you're right. The buffer was registered on CPU at config time and would not follow a later model.to(device), so the first post-calibration GPU forward would hit the amax.is_cuda assert in the dynamic-block-quant kernel (the .to(device) in the test masked it). Fixed in f224624: _get_amax now aligns the buffer with the input device on the fly. Thanks!

@Edwardf0t1 Edwardf0t1 requested a review from realAsma July 9, 2026 05:08
Comment on lines +682 to +685
This differs from ``use_constant_amax``, which pins amax to the FP8 E4M3 range (448.0)
for KV-cache cast math and does not register an ``_amax`` buffer. For NVFP4 activations
the exported ``input_scale`` equals ``amax / (E2M1_MAX * E4M3_MAX) = amax / (6 * 448)``;
setting ``constant_amax`` to ``2688.0`` therefore yields an exported ``input_scale`` of

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.

Can the old use_constant_amax behavior be replicated by setting constant_amax = 448? If so, could we unify these args?

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.

Not exactly today — they overlap in intent but differ in mechanism:

  • use_constant_amax=True pins amax to 448 only in the forward path (_get_amax returns it on the fly) and deliberately registers no _amax buffer. It's used for the KV *[kv]_bmm_quantizer, whose scale is exported through the dedicated kv_cache_scaling_factor path, not the _amaxinput_scale path. It's also format-agnostic (always 448).
  • constant_amax=448 would register _amax=448 as a real buffer, so it lands in the state_dict and gets exported as an input_scale (= amax/maxbound). For a KV bmm quantizer that's an extra/unexpected buffer next to kv_cache_scaling_factor.

So constant_amax=448 replicates the fake-quant behavior but not the export/state behavior. Unifying is feasible as a follow-up, but it touches the KV-cache export path (regression risk), so I'd want to first confirm the KV cast recipes still export byte-identical checkpoints before collapsing the two flags — I'd prefer to keep that out of this PR. Happy to file a follow-up issue to unify (likely: make constant_amax able to skip buffer registration / forward-only mode, then have the KV recipe set constant_amax: 448). WDYT?

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

Looks great!

Edwardf0t1 and others added 2 commits July 9, 2026 13:41
…xperts input_scale=1.0)

Add a `constant_amax` field to QuantizerAttributeConfig that pins a
quantizer's amax to a fixed value and skips activation calibration (no
forward statistics collected). Unlike `use_constant_amax` (hardcoded to
448.0 for KV-cache cast math, registers no buffer), `constant_amax`
stores the constant on the `_amax` buffer so it is used by both the
fake-quant forward and the exported scaling factor.

For NVFP4 activations, input_scale = amax / (E2M1_MAX * E4M3_MAX) =
amax / (6 * 448), so constant_amax=2688.0 yields input_scale == 1.0.

Ships recipe nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml that pins
the MoE expert activation quantizers to input_scale=1.0 with no
activation calibration, and unit tests covering buffer registration,
calibration skip, the NVFP4 input_scale=1.0 mapping, and the positive
validator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
Address PR review feedback:
- Fix device mismatch: the constant_amax _amax buffer is registered on
  CPU at config time and may not follow a later model.to(device); align
  it with the input device in _get_amax so the post-calibration GPU
  forward doesn't hit the dynamic-block-quant is_cuda assert (juhi).
- Add a model_validator forbidding use_constant_amax + constant_amax
  together, which would otherwise diverge fake-quant (448.0) from the
  exported pinned amax (CodeRabbit).
- Move the NVFP4QTensor import in the test to module level per the repo
  test-import convention (CodeRabbit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
@Edwardf0t1 Edwardf0t1 force-pushed the feat/nvfp4-constant-amax-input-scale branch from f224624 to 4e293de Compare July 9, 2026 20:42
@Edwardf0t1 Edwardf0t1 enabled auto-merge (squash) July 9, 2026 20:43
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.

4 participants