-
Notifications
You must be signed in to change notification settings - Fork 502
feat(quant): skip max-calib forward when no activation needs data #1963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,13 +23,15 @@ | |
| from _test_utils.torch.quantization.quantize_common import get_awq_config | ||
|
|
||
| import modelopt.torch.quantization as mtq | ||
| from modelopt.torch.quantization.config import QuantizerAttributeConfig | ||
| from modelopt.torch.quantization.config import MaxCalibConfig, QuantizerAttributeConfig | ||
| from modelopt.torch.quantization.model_calib import ( | ||
| _needs_activation_forward_for_max_calib, | ||
| apply_pre_quant_scale_and_smooth, | ||
| disable_pre_quant_scale_and_resmooth, | ||
| layerwise_calibrate, | ||
| max_calibrate, | ||
| ) | ||
| from modelopt.torch.quantization.nn import TensorQuantizer | ||
| from modelopt.torch.quantization.nn import QuantLinear, TensorQuantizer | ||
| from modelopt.torch.quantization.utils.layerwise_calib import LayerActivationCollector | ||
|
|
||
|
|
||
|
|
@@ -583,3 +585,119 @@ def _pre_hook(_module, args): | |
| assert torch.allclose(observed_layer_inputs[1][0], torch.tensor([[2.0, 4.0]])) | ||
| # Layer 2 gets output of layer 1 (prev * mask1 * scale1 = [2,4] * 0.5 * 0.5 = [0.5,1.0]) | ||
| assert torch.allclose(observed_layer_inputs[2][0], torch.tensor([[0.5, 1.0]])) | ||
|
|
||
|
|
||
| def _make_quant_linear(input_cfg, fi=16, fo=8): | ||
| """QuantLinear with an int8 weight quantizer and a caller-specified input quantizer.""" | ||
| lin = QuantLinear(fi, fo, bias=False) | ||
| lin.weight_quantizer.set_from_attribute_config(QuantizerAttributeConfig(num_bits=8)) | ||
| lin.input_quantizer.set_from_attribute_config(input_cfg) | ||
| return lin | ||
|
|
||
|
|
||
| def test_max_calib_skips_forward_when_all_activations_constant(): | ||
| """constant_amax activation quantizer => no data forward; weights still calibrated.""" | ||
| lin = _make_quant_linear(QuantizerAttributeConfig(num_bits=8, constant_amax=127.0)) | ||
| assert not _needs_activation_forward_for_max_calib(lin) | ||
|
|
||
| calls = [] | ||
|
|
||
| def forward_loop(model): | ||
| calls.append(1) | ||
| model(torch.randn(4, 16)) | ||
|
|
||
| max_calibrate( | ||
| lin, forward_loop, distributed_sync=False, skip_forward_without_activation_calib=True | ||
| ) | ||
| assert calls == [] # forward skipped entirely | ||
| assert hasattr( | ||
| lin.weight_quantizer, "_amax" | ||
| ) # weight still calibrated via weight_only_quantize | ||
| assert float(lin.input_quantizer._amax) == 127.0 # constant amax preserved | ||
|
|
||
|
|
||
| def test_max_calib_runs_forward_when_activation_needs_data(): | ||
| """A normal (non-constant) input quantizer still triggers the calibration forward.""" | ||
| lin = _make_quant_linear(QuantizerAttributeConfig(num_bits=8)) | ||
| assert _needs_activation_forward_for_max_calib(lin) | ||
|
|
||
| calls = [] | ||
|
|
||
| def forward_loop(model): | ||
| calls.append(1) | ||
| model(torch.randn(4, 16)) | ||
|
|
||
| max_calibrate( | ||
| lin, forward_loop, distributed_sync=False, skip_forward_without_activation_calib=True | ||
| ) | ||
| assert calls == [1] | ||
| assert lin.input_quantizer.amax is not None | ||
|
|
||
|
|
||
| def test_max_calib_default_does_not_skip_forward(): | ||
| """Default flag is opt-out=False for direct callers: forward always runs (backward compat).""" | ||
| lin = _make_quant_linear(QuantizerAttributeConfig(num_bits=8, constant_amax=127.0)) | ||
|
|
||
| calls = [] | ||
|
|
||
| def forward_loop(model): | ||
| calls.append(1) | ||
| model(torch.randn(4, 16)) | ||
|
|
||
| max_calibrate(lin, forward_loop, distributed_sync=False) # skip flag defaults to False | ||
| assert calls == [1] | ||
|
|
||
|
|
||
| def test_needs_activation_forward_ignores_disabled_and_weight_quantizers(): | ||
| """Disabled activation quantizers (weight-only recipe) need no forward.""" | ||
| lin = _make_quant_linear(QuantizerAttributeConfig(num_bits=8, enable=False)) | ||
| assert not _needs_activation_forward_for_max_calib(lin) | ||
|
|
||
|
|
||
| def test_needs_activation_forward_ignores_mx_and_dynamic_quantizers(): | ||
| """MX (block-dynamic E8M0, no per-tensor amax) and dynamic activations need no forward.""" | ||
| # MXFP8: block-dynamic with E8M0 scales; top-level ``_dynamic`` is False, so it must be | ||
| # excluded via ``is_mx_format`` rather than the ``_dynamic`` check. | ||
| mx = _make_quant_linear( | ||
| QuantizerAttributeConfig( | ||
| num_bits=(4, 3), block_sizes={-1: 32, "type": "dynamic", "scale_bits": (8, 0)} | ||
| ) | ||
| ) | ||
| assert mx.input_quantizer.is_mx_format | ||
| assert not mx.input_quantizer._dynamic | ||
| assert not _needs_activation_forward_for_max_calib(mx) | ||
|
|
||
| # Top-level dynamic activation quantization also needs no calibration forward. | ||
| dyn = _make_quant_linear(QuantizerAttributeConfig(num_bits=8, type="dynamic")) | ||
| assert not _needs_activation_forward_for_max_calib(dyn) | ||
|
|
||
|
|
||
| def test_needs_activation_forward_for_static_bias_calibrator(): | ||
| """A static bias calibrator collects data during the forward, even on MX/dynamic amax.""" | ||
| static_bias = {-1: None, "type": "static"} | ||
| mx_cfg = {"num_bits": (4, 3), "block_sizes": {-1: 32, "type": "dynamic", "scale_bits": (8, 0)}} | ||
|
|
||
| # MX amax (no per-tensor amax) but a *static* bias -> the forward is still required. | ||
| mx_static_bias = _make_quant_linear(QuantizerAttributeConfig(**mx_cfg, bias=static_bias)) | ||
| assert mx_static_bias.input_quantizer.bias_type == "static" | ||
| assert _needs_activation_forward_for_max_calib(mx_static_bias) | ||
|
|
||
| # A *dynamic* bias is computed on the fly, so no forward is needed. | ||
| mx_dynamic_bias = _make_quant_linear( | ||
| QuantizerAttributeConfig(**mx_cfg, bias={-1: None, "type": "dynamic"}) | ||
| ) | ||
| assert not _needs_activation_forward_for_max_calib(mx_dynamic_bias) | ||
|
|
||
| # constant_amax exempts the quantizer from calibration entirely (bias included). | ||
| const_static_bias = _make_quant_linear( | ||
| QuantizerAttributeConfig(num_bits=8, constant_amax=127.0, bias=static_bias) | ||
| ) | ||
| assert not _needs_activation_forward_for_max_calib(const_static_bias) | ||
|
Comment on lines
+680
to
+695
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Exercise the static-bias path through These assertions only test the private predicate. Add a CPU test using the real 🤖 Prompt for AI AgentsSources: Coding guidelines, Path instructions |
||
|
|
||
|
|
||
| def test_max_calib_config_skip_is_opt_in(): | ||
| """The flag is opt-in (default False) so it does not change behavior for direct callers.""" | ||
| assert MaxCalibConfig().skip_forward_without_activation_calib is False | ||
| assert MaxCalibConfig( | ||
| skip_forward_without_activation_calib=True | ||
| ).skip_forward_without_activation_calib | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RB: Can you give us a plan on how to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RB: Never mind, I think
module._dynamiccaptures that. Is that correct?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question — actually
module._dynamicdoes not capture MXFP4/MXFP8._dynamicis only set from a top-leveltype: dynamic, but the MX numerics configs settype: dynamicinsideblock_sizes(block-level E8M0 scales), leaving the top-level_dynamic == False:So an MX activation quantizer would currently fall through to the
_calibrator is not Nonecheck and be treated as needing the forward. MX formats carry no per-tensor amax at all (is_mx_formatmakesamaxalwaysNone), so they're safe to skip. I'll addmodule.is_mx_formatto the skip conditions in_needs_activation_forward_for_max_calibso MXFP4/MXFP8 are covered too. Thanks for catching this!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🐝 Looking into this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edward’s answer is correct. Concretely, the plan is:
_dynamicfor top-level dynamic quantizers.module.is_mx_formatas a separate skip condition, because MX’stype: dynamiclives insideblock_sizesand leaves_dynamic == False.is_mx_format == True,_dynamic == False, and makes_needs_activation_forward_for_max_calib()returnFalse; retain the normal data-driven case returningTrue.This is already implemented at
3f9d4e28, and the required unit/GPU/regression checks are green.