-
Notifications
You must be signed in to change notification settings - Fork 492
feat(quant): add constant_amax to pin activation input_scale (NVFP4 experts input_scale=1.0) #1947
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -202,6 +202,7 @@ def __init__( | |
| self.amax = amax | ||
|
|
||
| self._use_constant_amax = False | ||
| self._constant_amax = None | ||
| self.set_from_attribute_config(quant_attribute_cfg) | ||
|
|
||
| self._if_quant = if_quant | ||
|
|
@@ -244,6 +245,13 @@ def _block_sizes_setter(val): | |
| self._calibrator._axis = None | ||
| return val | ||
|
|
||
| def _constant_amax_setter(val): | ||
| 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) | ||
|
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. 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
Contributor
Author
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. Good catch — you're right. The buffer was registered on CPU at config time and would not follow a later |
||
| return val | ||
|
|
||
| # Some attributes need custom handling. | ||
| # By default, attributes from config are mapped to a name ``f"_{attribute}"`` | ||
| _custom_setters: dict[str, tuple[str, Callable]] = { | ||
|
|
@@ -255,6 +263,7 @@ def _block_sizes_setter(val): | |
| "backend": ("backend", lambda val: val), | ||
| "backend_extra_args": ("backend_extra_args", lambda val: val or {}), | ||
| "use_constant_amax": ("_use_constant_amax", lambda val: val), | ||
| "constant_amax": ("_constant_amax", _constant_amax_setter), | ||
| } | ||
|
|
||
| for attribute, val in attribute_cfg.items(): | ||
|
|
@@ -722,6 +731,10 @@ def _get_amax(self, inputs): | |
| return torch.tensor(torch.finfo(torch.float8_e4m3fn).max, device=inputs.device) | ||
| if hasattr(self, "_amax"): | ||
| amax = self._amax | ||
| # A constant_amax buffer is registered at config time (on CPU) and may not have | ||
| # followed a later `model.to(device)`; align it with the input device on the fly. | ||
| if amax.device != inputs.device: | ||
| amax = amax.to(inputs.device) | ||
| else: | ||
| reduce_axis = quant_utils.convert_quantization_axis_to_reduce_axis(inputs, self._axis) | ||
| amax = quant_utils.reduce_amax(inputs, axis=reduce_axis, keepdims=True).detach() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # 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. | ||
|
|
||
| # Composed PTQ recipe for expert-only NVFP4 quantization with a fixed activation input_scale of | ||
| # 1.0 (no activation calibration), plus FP8 KV-cache cast mode. | ||
| # | ||
| # The expert weight quantizers use standard NVFP4 (per-tensor weight scale + dynamic block | ||
| # scales, calibrated via max). The expert input (activation) quantizers pin the per-tensor amax | ||
| # to a constant 2688.0 = E2M1_MAX * E4M3_MAX (6 * 448) so the exported ``input_scale`` is exactly | ||
| # ``amax / (E2M1_MAX * E4M3_MAX) = 1.0``; the per-block E4M3 activation scales remain dynamic. | ||
| # No activation statistics are collected for these quantizers. | ||
|
|
||
| imports: | ||
| base_disable_all: configs/ptq/units/base_disable_all | ||
| default_disabled_quantizers: configs/ptq/units/default_disabled_quantizers | ||
| nvfp4: configs/numerics/nvfp4 | ||
| kv_fp8_cast: configs/ptq/units/kv_fp8_cast | ||
|
|
||
| metadata: | ||
| recipe_type: ptq | ||
| description: >- | ||
| Applies NVFP4 only to expert-layer weight and input quantizers, pinning the expert activation | ||
| input_scale to 1.0 (constant amax = 2688, no activation calibration), plus FP8 KV-cache cast | ||
| mode using constant amax; uses max calibration for the remaining (weight) quantizers. | ||
| quantize: | ||
| algorithm: | ||
| method: max | ||
| # Max calibration is fast and does not typically need checkpointing. | ||
| # layerwise=false required for VLMs where the decoder layers are nested under | ||
| # `model.language_model.layers` (layerwise_calibrate can't find them otherwise). | ||
|
realAsma marked this conversation as resolved.
|
||
| layerwise: false | ||
| quant_cfg: | ||
| - $import: base_disable_all | ||
| - quantizer_name: '*.experts.*weight_quantizer' | ||
| cfg: | ||
| $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 | ||
| - quantizer_name: '*block_sparse_moe*weight_quantizer' | ||
| cfg: | ||
| $import: nvfp4 | ||
| - 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 | ||
| - $import: kv_fp8_cast | ||
| - $import: default_disabled_quantizers | ||
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 the old
use_constant_amaxbehavior be replicated by settingconstant_amax = 448? If so, could we unify these args?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.
Not exactly today — they overlap in intent but differ in mechanism:
use_constant_amax=Truepins amax to 448 only in the forward path (_get_amaxreturns it on the fly) and deliberately registers no_amaxbuffer. It's used for the KV*[kv]_bmm_quantizer, whose scale is exported through the dedicatedkv_cache_scaling_factorpath, not the_amax→input_scalepath. It's also format-agnostic (always 448).constant_amax=448would register_amax=448as a real buffer, so it lands in the state_dict and gets exported as aninput_scale(= amax/maxbound). For a KV bmm quantizer that's an extra/unexpected buffer next tokv_cache_scaling_factor.So
constant_amax=448replicates 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: makeconstant_amaxable to skip buffer registration / forward-only mode, then have the KV recipe setconstant_amax: 448). WDYT?