diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3e10c786753..1486f98ace9 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,10 @@ Changelog **Backward Breaking Changes** +- Layerwise calibration now uses prior-layer QDQ activations by default + (``layerwise.get_qdq_activations_from_prev_layer=True``). Set it to ``False`` to + preserve full-precision activations for subsequent layers (the default behavior for + max calibration without layerwise calibration). - Unified HuggingFace export now fails with ``NotImplementedError`` when it meets an MoE block whose expert projection names it does not know, instead of assuming Mixtral's ``w1``/``w2``/``w3``. If you hit this, register a ``ModelSpec`` for the model under ``modelopt/torch/models/``. Every MoE architecture ModelOpt exported correctly before this change is registered, so no supported model regresses. **Deprecations** diff --git a/docs/source/_static/announcements.css b/docs/source/_static/announcements.css index 4614336e55d..331588a58e7 100644 --- a/docs/source/_static/announcements.css +++ b/docs/source/_static/announcements.css @@ -142,3 +142,65 @@ .toctree-wrapper.compound:empty { display: none; } + +/* Numbered equations: Sphinx's default `float: right` on the equation number + cannot share a line with MathJax's full-width display block, so the number + lands above the equation. Anchor it to the right of the equation instead. */ +div.math { + position: relative; +} + +div.math span.eqno { + float: none; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +/* Display equations: shibuya gives `.yue .math-wrapper` a 2rem margin top and + bottom, and MathJax adds its own margin on the container inside it. The + wrapper sets `overflow-x: auto`, so none of it collapses with the + neighbouring paragraph margins and the gap around every equation ends up + oversized. Drop the MathJax margin and set the spacing on the wrapper. */ +.yue .math-wrapper { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.yue .math-wrapper mjx-container[display="true"] { + margin: 0; +} + +.yue strong.local-hessian-result { + color: #76b900; + font-size: 1.15em; + font-weight: 900; +} + +.table-header-note { + font-size: 0.75em; + font-weight: 400; +} + +.table-note { + font-size: 0.75rem; + line-height: 1.4; + color: var(--sy-c-light); +} + +/* A `.table-note` right after a table is that table's caption, so pull it up + against the table. Shibuya gives `.table-wrapper` a 2rem bottom margin; this + margin collapses with the note's own, so the negative value here nets out to + a small gap. The `.yue .table-wrapper +` prefix is needed to outrank the + theme's paragraph spacing, and keeps figure captions on default spacing. */ +.yue .table-wrapper + p.table-note { + margin-top: -1.75rem; +} + +.table-note strong, +.table-note code, +.table-note .math, +.table-note mjx-container { + font-size: inherit !important; +} diff --git a/docs/source/announcements/assets/qwen3-27b-scaled-weight-distribution.png b/docs/source/announcements/assets/qwen3-27b-scaled-weight-distribution.png new file mode 100644 index 00000000000..78305f4ab57 Binary files /dev/null and b/docs/source/announcements/assets/qwen3-27b-scaled-weight-distribution.png differ diff --git a/docs/source/announcements/assets/qwen3-27b-w4a4-scale-rule-accuracy.png b/docs/source/announcements/assets/qwen3-27b-w4a4-scale-rule-accuracy.png new file mode 100644 index 00000000000..453cc480b4b Binary files /dev/null and b/docs/source/announcements/assets/qwen3-27b-w4a4-scale-rule-accuracy.png differ diff --git a/docs/source/announcements/local-hessian.rst b/docs/source/announcements/local-hessian.rst new file mode 100644 index 00000000000..3720add20c4 --- /dev/null +++ b/docs/source/announcements/local-hessian.rst @@ -0,0 +1,330 @@ +:orphan: + +Improving NVFP4 Accuracy with Local-Hessian Weight Scales +######################################################### + +:Author: Model Optimizer Team +:Date: September 9, 2026 +:Tags: local-hessian, quantization, nvfp4, calibration, modelopt + +.. role:: local-hessian-result(strong) +.. role:: table-header-note + +In this blog, we share about Model Optimizer 'Local-Hessian', an algorithm for NVFP4 per-block scale selection +to minimize the output error. We used this algorithm to create a low loss checkpoint +`nvidia/Qwen3.8-27B-NVFP4 `_ which can leverage NVFP4 tensor cores for performant inference on Blackwell GPUs. +Here is a comparison of accuracy results we observed for 'Local-Hessian' algorithm compared to the default max algorithm: + +.. image:: assets/qwen3-27b-w4a4-scale-rule-accuracy.png + :alt: Qwen3.8-27B scores by NVFP4 weight-scale rule, BF16 baseline in gray + :width: 100% + +**Figure 1. Qwen3.8-27B NVFP4 accuracy comparison between the default NVFP4 algorithm (max) and 'Local-Hessian'.** + +Scale Selection For NVFP4 +************************** + +NVFP4 represents each group of 16 weights with FP4 values and an FP8 block +scale [1]_. This block scale is used to scale the per-block values so to NVFP4 E2M1 range (-6.0, 6.0). +The default way is to set the block scale based on the per-block maximum value (max scaling) [1]_. + +As originally shown in the 'Four-Over-Six' paper [2]_, this block scale can be selected based on other +criteria such as per-block error. 'Four-Over-Six' selects the per-block scale from 2 candidates, while the +:func:`Model-Optimizer Mean Square Error (MSE) ` algorithm uses an exhaustive sweep over all positive, non-zero FP8 +scales (126 values). + +Both of these approaches for scale selection consider only weight tensor-level error, which we find does not correlate +well with downstream accuracy evaluation results. + +How Local-Hessian Works +*********************** + +NVFP4 **Local-Hessian** chooses each per-block weight scale to minimize +the *output* error of the matrix multiplication rather than the weight +error. Nothing about the format changes -- we just compute the per-block +scales differently from max scaling. + +Consider a linear layer :math:`Y=WX` with weights +:math:`W\in\mathbb{R}^{C_{\mathrm{out}}\times C_{\mathrm{in}}}` and +calibration inputs +:math:`X\in\mathbb{R}^{C_{\mathrm{in}}\times N}`, where :math:`N` is the +number of calibration tokens. Quantizing divides the weights by a scale +and casts, where the cast rounds each value onto the grid of the +low-precision format: + +.. math:: + :label: lh-quant + + \mathcal{Q}(W,s)=\operatorname{Cast}(W/s)\cdot s. + +This leaves a quantization error + +.. math:: + :label: lh-quant-error + + \Delta(W,s)=\mathcal{Q}(W,s)-W. + +What we actually care about is the error this puts on the layer output, +measured over the calibration set: + +.. math:: + :label: lh-output-error + + E(s) &= \lVert WX-\mathcal{Q}(W,s)\,X\rVert_F^2 + = \lVert \Delta(W,s)\,X\rVert_F^2 \\ + &= \operatorname{tr}\!\left(\Delta(W,s)\,(XX^{\top})\, + \Delta(W,s)^{\top}\right). + +The input second-moment matrix +:math:`XX^{\top}\in\mathbb{R}^{C_{\mathrm{in}}\times C_{\mathrm{in}}}` is +half the Hessian of the output error, +:math:`\partial^2E(s)/\partial\Delta(W,s)^2=2XX^{\top}`: it weights each +weight error by how much that input coordinate actually moves the +output. The trace sums one such quadratic form per output channel, so +the output channels are independent and can be handled separately. + +For NVFP4, :math:`s` is not a scalar: each output channel has +:math:`C_{\mathrm{in}}/16` blocks, one scale each. With :math:`M` +candidates per block, minimizing :math:`E(s)` jointly means searching +:math:`M^{C_{\mathrm{in}}/16}` combinations -- this is not tractable. To make +the search tractable, Local-Hessian uses a block-diagonal approximation: it adds +the output error from each block independently and ignores interactions between +quantization errors in different blocks. For block :math:`b`, + +.. math:: + :label: lh-block-error + + E_b(s_b) = \Delta(W_b,s_b)\,(X_bX_b^{\top})\,\Delta(W_b,s_b)^{\top}, + +where :math:`W_b\in\mathbb{R}^{1\times16}` is one NVFP4 block of weights, +:math:`s_b` is that block's scale, and +:math:`X_b\in\mathbb{R}^{16\times N}` holds the rows of :math:`X` the +block multiplies, so the local Hessian :math:`X_bX_b^{\top}` is only +:math:`16\times16`. We sweep all 126 candidate FP8 scales per block, just +as the MSE algorithm does; because blocks are independent, a +:func:`Triton kernel ` +does the whole layer at once. See the +:func:`Model Optimizer Local-Hessian code ` +for details. + +Local-Hessian Results +********************** + +Accuracy Comparison +==================== + +In Table 1 we compare Local-Hessian against other scale-selection algorithms for weights on Qwen3.5-9B. + +Local-Hessian gives the overall best accuracy among the NVFP4 +weight-scale selection methods, cutting the average drop from 5.10 to 3.10 +points against the default max rule. We get that from nothing but a +smarter way of computing the weight scale -- which says something about +micro-block formats like NVFP4: **the scale carries a lot of +information, and it pays to set it diligently.** + +.. list-table:: + :header-rows: 1 + + * - Weight scale selection method + - MMLU + - HellaSwag + - WinoGrande + - GSM8K + - Average drop :table-header-note:`(lower is better)` + - WikiText PPL :table-header-note:`(lower is better)` + * - BF16 reference + - 78.69 + - 78.04 + - 73.40 + - 87.64 + - 0.00 + - 9.20 + * - Max scale + - 75.81 + - 76.33 + - 70.64 + - 74.60 + - 5.10 + - 10.08 + * - MSE scale + - 76.49 + - 76.61 + - **72.45** + - 76.72 + - 3.87 + - 9.98 + * - Four-over-six scale + - 75.32 + - **76.62** + - 70.40 + - 76.42 + - 4.75 + - 10.02 + * - Local-Hessian scale + - **76.81** + - 76.50 + - 71.19 + - **80.89** + - :local-hessian-result:`3.10` + - :local-hessian-result:`9.90` + +.. rst-class:: table-note + +**Table 1: Scale selection algorithm comparison.** All layers except the final +output layer (``lm_head``) use NVFP4 weight and activation quantization (W4A4). + + +Local-Hessian With GPTQ +================================= + +Local-Hessian changes scales; GPTQ [3]_ changes weight rounding to +minimize per-layer output error. The two are orthogonal, so they +compose: Local-Hessian rounds to nearest (RTN) by default, and GPTQ can +replace that rounding step once the scales are set. In Table 2, we show +that Local-Hessian scales improve GPTQ as well. + +Two things stand out: + +#. Local-Hessian scale selection alone (3.10 average drop) beats GPTQ with + max scales (4.84). +#. Combining Local-Hessian scales with GPTQ improves the average drop further, + from 3.10 to 2.94. + +.. list-table:: + :header-rows: 1 + + * - Method + - MMLU + - HellaSwag + - WinoGrande + - GSM8K + - Average drop + - WikiText PPL + * - Max scale + GPTQ + - 75.77 + - 76.51 + - 70.17 + - 75.97 + - 4.84 + - 10.02 + * - Local-Hessian scale + GPTQ + - **76.98** + - **76.59** + - 70.96 + - 81.50 + - :local-hessian-result:`2.94` + - :local-hessian-result:`9.91` + +.. rst-class:: table-note + +**Table 2: GPTQ composition on Qwen3.5-9B, NVFP4 W4A4.** + +.. note:: + + Composition is not always a win: on Qwen3.8-27B, Local-Hessian + GPTQ scored + below Local-Hessian alone, so the published checkpoint uses Local-Hessian + only. This shows the best algorithm could vary depending on the model. + +Scale Selection Reshapes Distribution +************************************* + +Figure 2 plots the scaled weights :math:`W/s` -- the values handed to the +E2M1 cast. Max scaling piles the mass up near 6.0, the largest E2M1 value, +while MSE and Local-Hessian both cluster it on the representable E2M1 grid +values. Grid alignment alone does not explain the accuracy gains. Although +MSE and Local-Hessian both align weights to the E2M1 grid, Local-Hessian's +layer-output-error objective yields larger downstream accuracy improvements +in our results. + + +.. image:: assets/qwen3-27b-scaled-weight-distribution.png + :alt: Scaled weight distribution for max, MSE and Local-Hessian scales against the NVFP4 E2M1 grid + :width: 100% + +.. rst-class:: table-note + +**Figure 2. Scaled weight distribution across algorithms.** Blue +dashed lines are the NVFP4 E2M1 representable values. Distribution is for the +first gate-projection layer of Qwen3.8-27B. + +Just Better Scales, No Runtime Cost +*********************************** + +Local-Hessian and the other ModelOpt scale-selection algorithms for +NVFP4 weight scales are free. Weight scales are computed only once, at +checkpoint creation, and that same scale is reused on every +deployment. Selecting scales this way improves accuracy without +incurring any deployment throughput penalty. + + +Using Local-Hessian +******************* + +See the +:func:`local_hessian_calibrate API ` +for the calibration entry point. + +To use it in your own configuration, set the ``algorithm`` field: + +.. code-block:: python + + import modelopt.torch.quantization as mtq + + config = { + "quant_cfg": [...], # quantizer configuration + "algorithm": { + "method": "local_hessian", + "layerwise": {"enable": True}, + }, + } + + model = mtq.quantize(model, config, forward_loop) + +See :ref:`quant-cfg` for how to write the ``quant_cfg`` field. + +To reproduce the published Qwen3.8-27B checkpoint end to end: + +.. code-block:: bash + + python examples/hf_ptq/hf_ptq.py \ + --pyt_ckpt_path Qwen/Qwen3.8-27B \ + --recipe modelopt_recipes/models/Qwen/Qwen3.8-27B/ptq/nvfp4_local_hessian-fp8_attn-kv_fp8_cast.yaml \ + --dataset nemotron-post-training-v3 \ + --calib_size 512 \ + --calib_seq 2048 \ + --batch_size 1 \ + --export_path + +.. note:: + + For Local-Hessian and GPTQ, we recommend enabling + :func:`layerwise_calibrate ` + with ``"layerwise": {"enable": True}`` and using calibration batch size 1. + Layerwise calibration exposes each layer to the fake-quantized outputs of the + preceding layer, approximating quantized deployment, while batch size 1 prevents + padding tokens from contaminating activation statistics. + + +Next steps +********** + +- **Adapt Local-Hessian for sparse MoEs.** Many experts in a sparse MoE + see very little calibration data. Local-Hessian workflow needs to be adapted to that + low-data regime. + +.. _local-hessian-references: + +References +********** + +.. [1] E. Alvarez, O. Almog, E. Chung, S. Layton, D. Stosic, R. Krashinsky, + and K. Aubrey. `Introducing NVFP4 for Efficient and Accurate Low-Precision + Inference `_. + NVIDIA Technical Blog, 2025. +.. [2] J. Cook, J. Guo, G. Xiao, Y. Lin, K. Wyss, M. Nazemi, A. Mishra, + C. del Mundo, T. Blankevoort, and S. Han. `Four Over Six: More Accurate + NVFP4 Quantization with Adaptive Block Scaling + `_. arXiv:2512.02010, 2025. +.. [3] E. Frantar, S. Ashkboos, T. Hoefler, and D. Alistarh. `GPTQ: Accurate + Post-Training Quantization for Generative Pre-trained Transformers + `_. ICLR, 2023. diff --git a/docs/source/index.rst b/docs/source/index.rst index e2a871b5fe0..86b4312a561 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,6 +13,7 @@ Release notes, technical updates, examples, and deployment stories from the Mode + @@ -24,6 +25,12 @@ Release notes, technical updates, examples, and deployment stories from the Mode
+
+
September 9, 2026 · Model Optimizer Team
+

Improving NVFP4 Accuracy with Local-Hessian Weight Scales

+

How Local-Hessian selects NVFP4 block scales to minimize layer output error, and how it compares with max, MSE, Four-over-six, and GPTQ.

+
local-hessianquantizationnvfp4calibrationmodelopt
+
August 24, 2026 · Model Optimizer Team

AutoQuantize: A Fast Automatic Mixed-Precision Assignment

diff --git a/modelopt/torch/quantization/config.py b/modelopt/torch/quantization/config.py index 541cf5cefdf..f8847aa1fc1 100644 --- a/modelopt/torch/quantization/config.py +++ b/modelopt/torch/quantization/config.py @@ -723,13 +723,14 @@ class LayerwiseConfig(ModeloptBaseConfig): ) get_qdq_activations_from_prev_layer: bool = ModeloptField( - default=False, + default=True, title="Cache next-layer inputs from QDQ outputs of prior layers.", description=( - "If True (GPTQ default), capture each layer's next-layer inputs " + "If True (default), capture each layer's next-layer inputs " "after it is calibrated, so QDQ error and in-place weight updates " - "propagate forward. If False (max/mse default), capture before, so " - "the next layer sees the same FP activations as a non-layerwise pass." + "propagate forward. If False, capture before calibration, so " + "the next layer sees the same FP activations as a non-layerwise pass. " + "We recommend setting this to True for GPTQ and Local-Hessian." ), ) @@ -1242,21 +1243,6 @@ class GPTQCalibConfig(QuantizeAlgorithmConfig): per-column error propagation into one launch per GPTQ block.""", ) - @model_validator(mode="after") - def _gptq_qdq_default(self): - """Inject ``get_qdq_activations_from_prev_layer=True`` unless the user set it. - - GPTQ's Hessian correctness depends on prior-layer QDQ activations, so the - default differs from the base class. Uses ``model_fields_set`` to detect - whether the user explicitly set the field — covers every input shape - (empty constructor, bool, dict) without a per-shape special case. - """ - if "get_qdq_activations_from_prev_layer" not in self.layerwise.model_fields_set: - self.layerwise = self.layerwise.model_copy( - update={"get_qdq_activations_from_prev_layer": True} - ) - return self - _ScaleCalibConfig: TypeAlias = MaxCalibConfig | MseCalibConfig | LocalHessianCalibConfig diff --git a/modelopt/torch/quantization/mode.py b/modelopt/torch/quantization/mode.py index db7704e89b5..1b4fb0dcbe3 100644 --- a/modelopt/torch/quantization/mode.py +++ b/modelopt/torch/quantization/mode.py @@ -231,7 +231,7 @@ def wrapped_calib_func( layerwise = layerwise_cfg.get("enable", False) checkpoint_dir = layerwise_cfg.get("checkpoint_dir") export_dir = layerwise_cfg.get("export_dir") - qdq_from_prev = layerwise_cfg.get("get_qdq_activations_from_prev_layer", False) + qdq_from_prev = layerwise_cfg.get("get_qdq_activations_from_prev_layer", True) save_every = layerwise_cfg.get("save_every", 1) calib_mutates_weights = layerwise_cfg.get("calib_mutates_weights", True) if method is not None and "awq" in method: diff --git a/modelopt/torch/quantization/model_calib.py b/modelopt/torch/quantization/model_calib.py index 73f6f01df9c..6855d546bc2 100644 --- a/modelopt/torch/quantization/model_calib.py +++ b/modelopt/torch/quantization/model_calib.py @@ -1030,6 +1030,9 @@ def local_hessian_calibrate( experts), plain MSE otherwise. Other quantizer types (e.g. SequentialQuantizer) are unsupported and left at their max-calibrated scale. + We recommend using Local-Hessian with layerwise calibration enabled + (``"layerwise": {"enable": True}``) and a calibration batch size of 1. + Args: model: Model to be calibrated. forward_loop: A callable which takes the model as argument and @@ -2063,13 +2066,14 @@ def layerwise_calibrate( skip / run / capture strategy so that inter-layer logic in parent modules (e.g. mask construction) executes naturally without model-specific hooks. - Every knob arrives through ``calib_kwargs`` from :class:`LayerwiseConfig`, which + Every knob arrives through ``calib_kwargs`` from + :class:`LayerwiseConfig `, which documents them; ``export_dir`` additionally leaves the model in export form, so it must not be used for inference afterwards. """ checkpoint_dir = calib_kwargs.pop("checkpoint_dir", None) export_dir = calib_kwargs.pop("export_dir", None) - qdq_from_prev = calib_kwargs.pop("get_qdq_activations_from_prev_layer", False) + qdq_from_prev = calib_kwargs.pop("get_qdq_activations_from_prev_layer", True) save_every = calib_kwargs.pop("save_every", 1) calib_mutates_weights = calib_kwargs.pop("calib_mutates_weights", True) @@ -2248,6 +2252,9 @@ def gptq( * **Non-layerwise** (``layerwise.enable=False``): called once on the full model. All layers are quantized in parallel from the original activations. + We recommend enabling layerwise calibration + (``"layerwise": {"enable": True}``) and using a calibration batch size of 1. + Per-module steps: 1. ``max_calibrate`` to set amax values from the current activations. diff --git a/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise.yaml b/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise.yaml index f86afb2d2ee..8bdd8c6a025 100644 --- a/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise.yaml +++ b/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise.yaml @@ -28,6 +28,7 @@ quantize: # Max calibration is fast and does not typically need checkpointing. layerwise: enable: true + get_qdq_activations_from_prev_layer: false quant_cfg: - $import: base_disable_all - quantizer_name: '*.experts.*weight_quantizer' diff --git a/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_export.yaml b/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_export.yaml index 049922cc4fb..0deaf3f6bc7 100644 --- a/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_export.yaml +++ b/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_export.yaml @@ -42,6 +42,7 @@ quantize: method: max layerwise: enable: true + get_qdq_activations_from_prev_layer: false # max only updates _amax, so the exported shard stays valid for its layer. calib_mutates_weights: false # Presence enables per-layer export; the value is replaced with --export_path. diff --git a/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_offload.yaml b/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_offload.yaml index aa525cb4188..002c941f604 100644 --- a/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_offload.yaml +++ b/modelopt_recipes/general/ptq/nvfp4_experts_only-kv_fp8_layerwise_offload.yaml @@ -30,6 +30,7 @@ quantize: method: max layerwise: enable: true + get_qdq_activations_from_prev_layer: false calib_mutates_weights: false quant_cfg: - $import: base_disable_all diff --git a/modelopt_recipes/huggingface/minimax_m3_vl/ptq/mxfp8_nvfp4_experts.yaml b/modelopt_recipes/huggingface/minimax_m3_vl/ptq/mxfp8_nvfp4_experts.yaml index a7a14dcdfd1..77555897a5c 100644 --- a/modelopt_recipes/huggingface/minimax_m3_vl/ptq/mxfp8_nvfp4_experts.yaml +++ b/modelopt_recipes/huggingface/minimax_m3_vl/ptq/mxfp8_nvfp4_experts.yaml @@ -34,6 +34,7 @@ quantize: fp8_scale_sweep: false layerwise: enable: true + get_qdq_activations_from_prev_layer: false quant_cfg: - $import: base_disable_all - quantizer_name: "*weight_quantizer" diff --git a/tests/unit/torch/quantization/test_config_validation.py b/tests/unit/torch/quantization/test_config_validation.py index cd08959a976..a2450601a73 100644 --- a/tests/unit/torch/quantization/test_config_validation.py +++ b/tests/unit/torch/quantization/test_config_validation.py @@ -613,23 +613,17 @@ def test_directories_are_inert_without_enable(self): """Not an error: with enable=False nothing reads them.""" assert MaxCalibConfig(layerwise={"checkpoint_dir": "/x"}).layerwise.enable is False - @pytest.mark.parametrize( - ("cfg_cls", "expected_qdq"), - [(MaxCalibConfig, False), (GPTQCalibConfig, True)], - ) - def test_per_algorithm_qdq_default(self, cfg_cls, expected_qdq): - assert cfg_cls().layerwise.get_qdq_activations_from_prev_layer is expected_qdq + @pytest.mark.parametrize("cfg_cls", [MaxCalibConfig, GPTQCalibConfig, LocalHessianCalibConfig]) + def test_qdq_default(self, cfg_cls): + assert cfg_cls().layerwise.get_qdq_activations_from_prev_layer is True @pytest.mark.parametrize( ("layerwise_input", "expected_qdq"), [ - # GPTQ default kicks in for user dict that doesn't mention qdq. + # The layerwise default applies when a user dict does not mention QDQ. ({"enable": True}, True), - # User-explicit False overrides the GPTQ default. + # An explicit False overrides the layerwise default. ({"enable": True, "get_qdq_activations_from_prev_layer": False}, False), - # ``LayerwiseConfig`` instance: ``_coerce_layerwise_input`` must - # preserve ``model_fields_set`` so the GPTQ default still kicks in - # for fields the user didn't explicitly set. (LayerwiseConfig(enable=True), True), ( LayerwiseConfig(enable=True, get_qdq_activations_from_prev_layer=False), @@ -637,15 +631,15 @@ def test_per_algorithm_qdq_default(self, cfg_cls, expected_qdq): ), ], ) - def test_gptq_qdq_default_respects_user_explicit_value(self, layerwise_input, expected_qdq): - cfg = GPTQCalibConfig(layerwise=layerwise_input) + def test_qdq_default_respects_user_explicit_value(self, layerwise_input, expected_qdq): + cfg = MaxCalibConfig(layerwise=layerwise_input) assert cfg.layerwise.get_qdq_activations_from_prev_layer is expected_qdq def test_default_dump_shape(self): dumped = MaxCalibConfig().model_dump() assert dumped["layerwise"] == { "enable": False, - "get_qdq_activations_from_prev_layer": False, + "get_qdq_activations_from_prev_layer": True, "checkpoint_dir": None, "save_every": 1, "export_dir": None,