Skip to content

feat(quant): skip max-calib forward when no activation needs data#1963

Open
Edwardf0t1 wants to merge 3 commits into
mainfrom
feat/skip-forward-loop-constant-amax
Open

feat(quant): skip max-calib forward when no activation needs data#1963
Edwardf0t1 wants to merge 3 commits into
mainfrom
feat/skip-forward-loop-constant-amax

Conversation

@Edwardf0t1

@Edwardf0t1 Edwardf0t1 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: new feature (performance)

Addresses @realAsma's review comment on #1947: when all enabled input/KV activation quantizers use a constant amax, max calibration still runs the full forward_loop, which is wasted — the constant/dynamic quantizers collect no data-driven statistics.

This adds _needs_activation_forward_for_max_calib(model) and gates the forward_loop in max_calibrate():

  • Skips the forward when no enabled quantizer needs data-driven activation stats — i.e. every enabled non-weight quantizer is dynamic, use_constant_amax, or constant_amax. Weight quantizers are still calibrated on the weight tensors directly via weight_only_quantize(), so results are unchanged; only the wasted forward is avoided.
  • Runs the forward whenever any enabled activation quantizer still needs data (normal PTQ → identical behavior).

Safety / scoping. The gate is behind an opt-in flag max_calibrate(..., skip_forward_without_activation_calib=False). max_calibrate has many internal callers — the advanced algorithms that always need activations (MSE, local Hessian, SmoothQuant, SVDQuant, GPTQ) call it directly and keep the default False, so they are unaffected. Only the top-level max path opts in, via MaxCalibConfig.skip_forward_without_activation_calib (default True).

Also simplifies a default-arg lambda in core_utils.sync_moe_expert_amax (lambda m, w=weight: m(w)lambda m: m(weight)) so mypy can infer its type — surfaced by the max_calibrate signature change; the forward_loop is invoked synchronously so closure capture is safe.

Usage

Automatic for the top-level max path — no user action needed. For the nvfp4_experts_only_input_scale1-kv_fp8_cast recipe (all expert inputs + KV pinned to constant amax), calibration now performs weight-only calibration and skips the model forward:

max_calibrate: all enabled activation quantizers use constant/dynamic amax; skipping the calibration forward pass (weight-only calibration).

To force the old behavior: set skip_forward_without_activation_calib: false on the max algorithm config.

Testing

New CPU unit tests in tests/unit/torch/quantization/test_calib.py:

  • test_max_calib_skips_forward_when_all_activations_constant — forward not called; weight quantizer still gets _amax; constant input amax preserved.
  • test_max_calib_runs_forward_when_activation_needs_data — a normal input quantizer still triggers the forward.
  • test_max_calib_default_does_not_skip_forward — direct callers (default flag) keep running the forward.
  • test_needs_activation_forward_ignores_disabled_and_weight_quantizers.
  • test_max_calib_config_enables_skip_by_default.

pytest tests/unit/torch/quantization/test_calib.py → 17 passed; test_quantize_cpu.py + test_tensor_quantizer_cpu.py → 79 passed. pre-commit on all changed files passes (ruff, mypy, bandit, …).

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ (opt-in flag; default preserves behavior for direct callers; normal max PTQ unchanged)
  • 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

Follow-up to #1947 (now merged) per reviewer request. Closes the optimization discussion at #1947 (comment).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an opt-in configuration flag to let max calibration skip the activation calibration forward pass when no enabled activation quantizer needs runtime statistics.
    • Weight calibration still runs when the forward pass is skipped.
    • Activation-data-required advanced calibration flows remain unaffected.
  • Documentation
    • Updated the changelog with the new option and the NVFP4 PTQ recipe now enables it.
  • Bug Fixes
    • Improved expert amax synchronization to ensure the correct weight is captured during calibration.
  • Tests
    • Added unit coverage for the skip behavior and activation-calibration detection logic.

@Edwardf0t1 Edwardf0t1 requested review from a team as code owners July 10, 2026 22:28
@Edwardf0t1 Edwardf0t1 requested review from chadvoegele and removed request for a team July 10, 2026 22:28
@coderabbitai

coderabbitai Bot commented Jul 10, 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: 3ea63bd9-cc62-4f5e-a173-e7c5eb8e8ff9

📥 Commits

Reviewing files that changed from the base of the PR and between 3f9d4e2 and dec014d.

📒 Files selected for processing (3)
  • CHANGELOG.rst
  • modelopt/torch/quantization/model_calib.py
  • tests/unit/torch/quantization/test_calib.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • CHANGELOG.rst
  • tests/unit/torch/quantization/test_calib.py
  • modelopt/torch/quantization/model_calib.py

📝 Walkthrough

Walkthrough

Max calibration gains configurable detection of activation-statistics requirements. When none are needed, the activation forward_loop can be skipped while weight calibration continues. The option defaults to False, with coverage for quantizer types and execution behavior.

Changes

Max calibration forward control

Layer / File(s) Summary
Calibration configuration and API
modelopt/torch/quantization/config.py, modelopt/torch/quantization/model_calib.py
Adds opt-in skip_forward_without_activation_calib with default False to MaxCalibConfig and max_calibrate.
Activation requirement detection
modelopt/torch/quantization/model_calib.py, tests/unit/torch/quantization/test_calib.py
Detects whether enabled activation quantizers require data-driven calibration, including static-bias calibrator cases.
Conditional execution and validation
modelopt/torch/quantization/model_calib.py, modelopt/torch/quantization/utils/core_utils.py, modelopt_recipes/general/ptq/nvfp4_experts_only_input_scale1-kv_fp8_cast.yaml, tests/unit/torch/quantization/test_calib.py, CHANGELOG.rst
Conditionally skips forward_loop, preserves weight calibration, updates the MoE calibration lambda, enables the option in the NVFP4 recipe, and adds tests and release notes.

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

Sequence Diagram(s)

sequenceDiagram
  participant max_calibrate
  participant TensorQuantizer
  participant forward_loop
  participant WeightCalibration
  max_calibrate->>TensorQuantizer: Inspect enabled activation quantizers
  TensorQuantizer-->>max_calibrate: Report calibration requirement
  alt Activation statistics required
    max_calibrate->>forward_loop: Run calibration forward loop
  else No activation statistics required and skip enabled
    max_calibrate->>WeightCalibration: Continue without forward loop
  end
  max_calibrate->>WeightCalibration: Calibrate weight tensors
Loading

Possibly related PRs

Suggested reviewers: chadvoegele, realasma, rohansjoshi, shengliangxu

🚥 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: skipping max-calibration forward passes when activation data is unnecessary.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
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 No banned load/eval/nosec patterns appear in the PR’s touched Python files, and no dependency manifests were changed.
✨ 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/skip-forward-loop-constant-amax

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

@codecov

codecov Bot commented Jul 10, 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 76.28%. Comparing base (dca6ecd) to head (dec014d).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/quantization/utils/core_utils.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1963      +/-   ##
==========================================
- Coverage   76.40%   76.28%   -0.12%     
==========================================
  Files         522      522              
  Lines       58413    58821     +408     
==========================================
+ Hits        44632    44874     +242     
- Misses      13781    13947     +166     
Flag Coverage Δ
examples 33.07% <21.05%> (-8.01%) ⬇️
gpu 57.88% <21.05%> (-0.71%) ⬇️
regression 15.08% <10.52%> (+0.14%) ⬆️
unit 55.39% <94.73%> (-0.03%) ⬇️

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.

Base automatically changed from feat/nvfp4-constant-amax-input-scale to main July 10, 2026 23:14
@Edwardf0t1 Edwardf0t1 requested a review from a team as a code owner July 10, 2026 23:14
@Edwardf0t1 Edwardf0t1 force-pushed the feat/skip-forward-loop-constant-amax branch from be9b904 to 977b74c Compare July 11, 2026 00:33
@github-actions

github-actions Bot commented Jul 11, 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-1963/

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

or getattr(module, "_constant_amax", None) is not None
):
continue
if getattr(module, "_calibrator", None) is not None:

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 we also check if the model is mx formats like MXFP4? if the activation quantizer is MXFP4/MXFP8 we can skip calibration on that as well.

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.

RB: Can you give us a plan on how to do this?

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.

RB: Never mind, I think module._dynamic captures that. Is that correct?

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 question — actually module._dynamic does not capture MXFP4/MXFP8. _dynamic is only set from a top-level type: dynamic, but the MX numerics configs set type: dynamic inside block_sizes (block-level E8M0 scales), leaving the top-level _dynamic == False:

# configs/numerics/mxfp8.yaml
num_bits: e4m3
block_sizes: {-1: 32, type: dynamic, scale_bits: e8m0}

So an MX activation quantizer would currently fall through to the _calibrator is not None check and be treated as needing the forward. MX formats carry no per-tensor amax at all (is_mx_format makes amax always None), so they're safe to skip. I'll add module.is_mx_format to the skip conditions in _needs_activation_forward_for_max_calib so MXFP4/MXFP8 are covered too. Thanks for catching this!

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.

🤖 Bot comment.

🐝 Looking into this.

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.

🤖 Bot comment.

Edward’s answer is correct. Concretely, the plan is:

  1. Keep _dynamic for top-level dynamic quantizers.
  2. Add module.is_mx_format as a separate skip condition, because MX’s type: dynamic lives inside block_sizes and leaves _dynamic == False.
  3. Add a regression test proving an MX quantizer has is_mx_format == True, _dynamic == False, and makes _needs_activation_forward_for_max_calib() return False; retain the normal data-driven case returning True.

This is already implemented at 3f9d4e28, and the required unit/GPU/regression checks are green.

@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!! Thanks for this PR

Edwardf0t1 added a commit that referenced this pull request Jul 13, 2026
Address PR #1963 review feedback:

- Make MaxCalibConfig.skip_forward_without_activation_calib opt-in
  (default False). Defaulting it True silently changed calibration for
  every max-PTQ user and broke weight-only quantization under DeepSpeed
  ZeRO-3 (test_nested_deepspeed_backward[3-quant_cfg0]): the calibration
  forward materializes sharded params, so skipping it left the ZeRO-3
  param shards out of sync with the quantized weights. Enable it
  explicitly in the nvfp4_experts_only_input_scale1-kv_fp8_cast recipe.

- Also skip the forward for MX formats (MXFP4/MXFP8) in
  _needs_activation_forward_for_max_calib (realAsma). MX configs set
  type: dynamic inside block_sizes (E8M0 per-block scales) but leave the
  top-level _dynamic False, so the _dynamic check did not catch them;
  they carry no per-tensor amax (is_mx_format -> amax is None), so add an
  explicit is_mx_format check.

Update tests (opt-in default, MX/dynamic coverage) and changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
@Edwardf0t1

Copy link
Copy Markdown
Contributor Author

Pushed 33cebce with two changes:

  1. MX coverage (per @realAsma): added module.is_mx_format to _needs_activation_forward_for_max_calib, so MXFP4/MXFP8 activation quantizers are also skipped. As noted above, _dynamic alone didn't catch them (MX sets type: dynamic only inside block_sizes).

  2. Made the optimization opt-in (MaxCalibConfig.skip_forward_without_activation_calib default TrueFalse). The prior default-on broke a GPU test — test_nested_deepspeed_backward[3-quant_cfg0] (weight-only, ZeRO-3): the calibration forward materializes ZeRO-3 sharded params, so skipping it left the shards out of sync with the quantized weights (ZeRO-1/2 passed; only ZeRO-3 failed). Making it opt-in keeps all existing max-PTQ behavior unchanged; the nvfp4_experts_only_input_scale1-kv_fp8_cast recipe now sets the flag explicitly to get the optimization.

Tests updated (opt-in default + MX/dynamic coverage), changelog updated, pre-commit + recipe validation green. CI re-running.

Edwardf0t1 and others added 2 commits July 13, 2026 23:04
When all enabled activation quantizers use a constant amax (constant_amax
/ use_constant_amax) or dynamic quantization, max calibration's forward
pass collects no data-driven statistics and is pure overhead. Add
_needs_activation_forward_for_max_calib() and gate the forward_loop in
max_calibrate() behind a new opt-in flag
skip_forward_without_activation_calib.

Weight quantizers are still calibrated on the weight tensors directly via
weight_only_quantize(), so results are unchanged; only the wasted forward
is avoided (e.g. the nvfp4_experts_only_input_scale1 recipe).

The flag defaults to False in max_calibrate() so the advanced algorithms
that call it directly and always need activations (MSE, local Hessian,
SmoothQuant, SVDQuant, GPTQ) are unaffected; it is enabled via
MaxCalibConfig.skip_forward_without_activation_calib (default True) so
only the top-level max path opts in.

Also simplify a default-arg lambda in core_utils.sync_moe_expert_amax so
mypy can infer its type (surfaced by the max_calibrate signature change);
the forward_loop is invoked synchronously so closure capture is safe.

Stacked on #1947 (depends on the constant_amax attribute).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
Address PR #1963 review feedback:

- Make MaxCalibConfig.skip_forward_without_activation_calib opt-in
  (default False). Defaulting it True silently changed calibration for
  every max-PTQ user and broke weight-only quantization under DeepSpeed
  ZeRO-3 (test_nested_deepspeed_backward[3-quant_cfg0]): the calibration
  forward materializes sharded params, so skipping it left the ZeRO-3
  param shards out of sync with the quantized weights. Enable it
  explicitly in the nvfp4_experts_only_input_scale1-kv_fp8_cast recipe.

- Also skip the forward for MX formats (MXFP4/MXFP8) in
  _needs_activation_forward_for_max_calib (realAsma). MX configs set
  type: dynamic inside block_sizes (E8M0 per-block scales) but leave the
  top-level _dynamic False, so the _dynamic check did not catch them;
  they carry no per-tensor amax (is_mx_format -> amax is None), so add an
  explicit is_mx_format check.

Update tests (opt-in default, MX/dynamic coverage) and changelog.

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/skip-forward-loop-constant-amax branch from 33cebce to 3f9d4e2 Compare July 14, 2026 06:04
:func:`weight_only_quantize`, so they never need the data forward. Activation-side
quantizers (input/output/BMM) need it only when they collect data-driven statistics:
i.e. they are enabled, have a calibrator, and are none 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.

should we also check for the bias_calibrator?

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 — yes, we should. A quantizer with a static bias_calibrator collects data during the forward (finish_stats_collectionload_calib_bias), so it needs the forward even when its amax path is dynamic/MX (which carry no per-tensor amax). My original helper would have wrongly skipped that case.

Fixed in dec014d: _needs_activation_forward_for_max_calib now mirrors finish_stats_collection exactly — a non-constant quantizer with a static bias forces the forward:

if not is_constant and module.bias_calibrator is not None and module.bias_type == "static":
    return True

Constant-amax quantizers stay exempt (they continue before the bias block in finish_stats_collection, so their bias isn't calibrated either), and a dynamic bias is computed on the fly so still needs no forward. Added tests for MX+static-bias (→ forward runs), MX+dynamic-bias, and constant+static-bias. Thanks!

Address review feedback (juhi10071998): a quantizer with a static
bias_calibrator collects data during the calibration forward
(finish_stats_collection -> load_calib_bias), so it needs the forward
even when its amax is dynamic/MX (which carry no per-tensor amax).

_needs_activation_forward_for_max_calib now mirrors
finish_stats_collection exactly: a non-constant quantizer with a static
bias_calibrator forces the forward, while constant-amax quantizers (which
are exempted from bias calibration) and dynamic-bias quantizers still do
not. Add tests for MX + static/dynamic bias and constant + static bias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
@Edwardf0t1 Edwardf0t1 enabled auto-merge (squash) July 14, 2026 19:14
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.

3 participants