1616"""Calibrator that returns the MSE amax of all collected tensors."""
1717
1818import math
19+ import os
1920from collections .abc import Callable
2021
2122import torch
2425from .. import utils as quant_utils
2526from .calibrator import _Calibrator
2627
27- __all__ = ["MseCalibrator" , "NVFP4MSECalibrator" , "TritonNVFP4MSECalibrator" ]
28+ __all__ = ["MseCalibrator" , "NVFP4MSECalibrator" ]
2829
2930
3031class MseCalibrator (_Calibrator ):
@@ -172,7 +173,15 @@ def compute_amax(self, verbose: bool = False):
172173
173174
174175class NVFP4MSECalibrator (MseCalibrator ):
175- """Per-block FP8 scale sweep calibrator for NVFP4 static quantization."""
176+ """Per-block FP8 scale sweep calibrator for NVFP4 static quantization.
177+
178+ Uses a fused Triton kernel as an internal fast path on the first ``collect`` call
179+ when (a) ``error_func is None``, (b) the input tensor is on CUDA in the standard
180+ blocked ``[n_blocks, block_size]`` layout, and (c) Triton + the kernel package are
181+ importable. Falls back to the reference 126-step Python sweep otherwise (custom
182+ ``error_func`` users, multi-``collect`` activation flows, CPU inputs, or when the
183+ fast path is disabled via ``MODELOPT_NVFP4_TRITON_SWEEP=0``).
184+ """
176185
177186 def __init__ (
178187 self ,
@@ -185,6 +194,8 @@ def __init__(
185194 """Initialize NVFP4 MSE calibrator with per-block and global amax."""
186195 super ().__init__ (amax = amax , axis = axis , quant_func = quant_func , error_func = error_func )
187196 self ._global_amax = global_amax
197+ # Set by the Triton fast path on its (one-shot) collect; consumed by compute_amax.
198+ self ._best_amax_fast : torch .Tensor | None = None
188199
189200 def _compute_candidate_amax (self , candidates : torch .Tensor ) -> torch .Tensor :
190201 if candidates .ndim != 0 : # Called during final compute amax
@@ -204,94 +215,70 @@ def _generate_candidates(self, device: torch.device) -> torch.Tensor:
204215 fp8_values = fp8_values [valid_mask ]
205216 return fp8_values / 448.0
206217
218+ def _can_use_triton_fast_path (self , x : torch .Tensor ) -> bool :
219+ """Whether the Triton fast path is usable for this ``collect`` input.
207220
208- class TritonNVFP4MSECalibrator (NVFP4MSECalibrator ):
209- """Triton-fused FP8 scale sweep calibrator for NVFP4 weight MSE.
210-
211- Numerically equivalent to :class:`NVFP4MSECalibrator` but evaluates all 126
212- candidates in a single fused Triton kernel — one weight read instead of 126.
213-
214- Limitation: a single ``collect()`` call is supported per ``compute_amax`` cycle.
215- This matches the static weight-MSE flow (``mse_calibrate``'s weight loop), where
216- the calibrator is collected once per weight and immediately consumed. For
217- activation calibration (multiple ``collect`` calls), use :class:`NVFP4MSECalibrator`.
218- Call :meth:`reset` to free internal state and re-enable :meth:`collect`.
219- """
220-
221- def __init__ (
222- self ,
223- amax : torch .Tensor ,
224- global_amax : torch .Tensor ,
225- axis : int | tuple | list | None = None ,
226- quant_func : Callable | None = None ,
227- error_func : Callable | None = None ,
228- ):
229- """Initialize the Triton-fused NVFP4 MSE calibrator.
230-
231- See :class:`NVFP4MSECalibrator`. ``quant_func``/``error_func`` are unused by
232- the kernel path but accepted for API parity. Tile shape and ``num_warps`` are
233- autotuned by the kernel per ``N_BLOCKS``.
221+ The kernel produces the final per-block amax in one shot, so it's only usable
222+ when the caller wants the standard squared-error sweep on a single CUDA tensor
223+ whose layout already matches the per-block amax.
234224 """
235- super ().__init__ (
236- amax = amax ,
237- global_amax = global_amax ,
238- axis = axis ,
239- quant_func = quant_func ,
240- error_func = error_func ,
241- )
242- # Stash shape metadata so collect() can keep working after reset() releases
243- # the (potentially large) _initial_amax buffer.
244- self ._initial_amax_shape = tuple (amax .shape )
245- self ._initial_amax_dtype = amax .dtype
246- self ._n_blocks = int (amax .numel ())
247- self ._best_amax : torch .Tensor | None = None
225+ if self ._error_func is not None :
226+ return False
227+ if not x .is_cuda :
228+ return False
229+ if os .environ .get ("MODELOPT_NVFP4_TRITON_SWEEP" , "1" ) == "0" :
230+ return False
231+ if self ._initial_amax is None :
232+ return False
233+ if x .ndim != 2 or x .shape [0 ] != int (self ._initial_amax .numel ()):
234+ return False
235+ try :
236+ from modelopt .torch .kernels .quantization .gemm import nvfp4_fp8_scale_sweep # noqa: F401
237+ except ImportError :
238+ return False
239+ return True
248240
249241 @torch .no_grad ()
250242 def collect (self , x : torch .Tensor ):
251- """Run the fused FP8 sweep kernel and store the resulting per-block amax."""
252- from modelopt .torch .kernels .quantization .gemm import nvfp4_fp8_scale_sweep
253-
254- if self ._best_amax is not None :
243+ """Collect input statistics. Uses the Triton fast path when eligible."""
244+ if self ._best_amax_fast is not None :
255245 raise RuntimeError (
256- "TritonNVFP4MSECalibrator.collect() is one-shot; call reset() to "
257- "discard the previous result before collecting again."
258- )
259-
260- x = x .detach ()
261- # The weight quantizer reshapes its input to [n_blocks, block_size] before
262- # calling collect (see TensorQuantizer._process_for_blockquant). Validate
263- # via ValueError so the contract still holds under ``python -O``.
264- if x .ndim != 2 :
265- raise ValueError (
266- f"Expected x to be [n_blocks, block_size]; got shape { tuple (x .shape )} ."
246+ "NVFP4MSECalibrator: the Triton fast path produced a final amax on a "
247+ "previous collect() call; multi-collect after the fast path is not "
248+ "supported. Call reset() to start a fresh cycle, set "
249+ "MODELOPT_NVFP4_TRITON_SWEEP=0, or pass a non-None error_func to force "
250+ "the reference path for activation-style accumulation."
267251 )
268- block_size = x .shape [- 1 ]
269- if block_size <= 0 :
270- raise ValueError (f"x.shape[-1] must be positive; got { block_size } ." )
271- n_blocks = x .shape [0 ]
272- if n_blocks != self ._n_blocks :
273- raise ValueError (
274- f"initial amax.numel() ({ self ._n_blocks } ) does not match the number "
275- f"of NVFP4 blocks in x ({ n_blocks } )."
252+ # Fast path is eligible only on the first call, before the reference accumulator
253+ # has produced any state.
254+ if self ._losses_sum is None and self ._can_use_triton_fast_path (x ):
255+ from modelopt .torch .kernels .quantization .gemm import nvfp4_fp8_scale_sweep
256+
257+ best_flat = nvfp4_fp8_scale_sweep (x .detach (), self ._global_amax , block_size = x .shape [- 1 ])
258+ # Match the original shape/dtype of the initial amax so downstream
259+ # load_calib_amax behaves identically to the reference path.
260+ self ._best_amax_fast = best_flat .reshape (self ._initial_amax .shape ).to (
261+ self ._initial_amax .dtype
276262 )
277-
278- best_amax_flat = nvfp4_fp8_scale_sweep (
279- x ,
280- self ._global_amax ,
281- block_size = block_size ,
282- )
283- # Match the original shape/dtype of the initial amax so downstream
284- # load_calib_amax behaves identically to the reference path.
285- self ._best_amax = best_amax_flat .reshape (self ._initial_amax_shape ).to (
286- self ._initial_amax_dtype
287- )
263+ return
264+ super ().collect (x )
288265
289266 @torch .no_grad ()
290267 def compute_amax (self , verbose : bool = False ):
291- """Return the per-block amax computed during ``collect``."""
292- return self ._best_amax
268+ """Return the per-block amax — from the fast path if it ran, else from the reference sweep."""
269+ if self ._best_amax_fast is not None :
270+ return self ._best_amax_fast
271+ return super ().compute_amax (verbose = verbose )
293272
294273 def reset (self ):
295- """Reset the stored best amax. Subsequent ``collect`` calls are allowed."""
296- self ._best_amax = None
297- super ().reset ()
274+ """Reset per-cycle state. Keep ``_initial_amax`` so the calibrator stays reusable.
275+
276+ ``MseCalibrator.reset()`` intentionally drops ``_initial_amax`` to free memory in
277+ the multi-step search, but the NVFP4 per-block amax is shape ``[num_blocks]`` —
278+ small enough to keep so a follow-up ``collect()`` can run again on the same
279+ calibrator instance.
280+ """
281+ self ._best_amax_fast = None
282+ self ._losses_sum = None
283+ self ._candidates = None
284+ self ._amax = None
0 commit comments