mirror of
https://github.com/NVIDIA/Model-Optimizer.git
synced 2026-11-08 09:03:20 +08:00
### What does this PR do?
#### Summary
Redesigns the `quant_cfg` configuration format in ModelOpt's PyTorch
quantization stack, replacing the previous dict-based format with an
**ordered list of typed `QuantizerCfgEntry` dicts**.
##### Motivation
The old `quant_cfg` dict had several pain points:
- **Ambiguous precedence**: no explicit way to reason about which entry
wins when multiple keys match a quantizer
- **Mixed key namespaces**: wildcard paths and PyTorch class names lived
in the same dict level, requiring ad-hoc dispatch
- **Magic `"default"` key**: an implicit, undocumented catch-all that
was easy to misuse
- **Poor composability**: merging two configs required dict updates that
silently discarded keys
- **No YAML round-trip fidelity**: the nested structure couldn't be
expressed cleanly in YAML
##### New format
`quant_cfg` is now an ordered list of `QuantizerCfgEntry` TypedDicts.
Each entry has:
- `quantizer_name` *(required)*: `fnmatch` wildcard matched against
quantizer module names
- `cfg` *(optional)*: dict (or list of dicts) of
`QuantizerAttributeConfig` fields
- `enable` *(optional)*: toggles quantizer on/off independently of `cfg`
- `parent_class` *(optional)*: restricts match to quantizers whose
parent module is of the given PyTorch class (e.g. `"nn.BatchNorm2d"`)
Entries are applied in list order; later entries override earlier ones.
The canonical pattern is deny-all first (`_base_disable_all`), then
selectively re-enable and configure, then apply standard exclusions
(`_default_disabled_quantizer_cfg`).
##### Changes
**Core library (`modelopt/torch/quantization/`)**
- **`config.py`**:
- Added `QuantizerCfgEntry` TypedDict (line 163) and
`find_quant_cfg_entry_by_path()` helper for exact-match lookup of
entries by path.
- Added `normalize_quant_cfg_list()` (line 1539) that converts legacy
formats (flat dict, single-key dicts, `nn.*`-scoped dicts, `"default"`
key) to canonical `QuantizerCfgEntry` lists. After normalization every
entry is guaranteed to have explicit `quantizer_name`, `enable`, and
`cfg` keys.
- Converted `_default_disabled_quantizer_cfg` and
`_mamba_moe_disabled_quantizer_cfg` from dicts to lists of
`QuantizerCfgEntry`.
- Added `_base_disable_all` (line 205): canonical deny-all entry
(`[{"quantizer_name": "*", "enable": False}]`).
- Converted all ~30 built-in config constants (`INT8_DEFAULT_CFG`,
`FP8_DEFAULT_CFG`, `NVFP4_DEFAULT_CFG`, etc.) to list format using
`*_base_disable_all` and `*_default_disabled_quantizer_cfg` unpacking.
- KV-cache configs (`FP8_KV_CFG`, `NVFP4_KV_CFG`, etc.) are now minimal
lists designed to be concatenated with a primary config — they
intentionally omit `_base_disable_all` and `"algorithm"`.
- Added two `QuantizeConfig` Pydantic field validators: a
`mode="before"` validator that calls `normalize_quant_cfg_list()`, and a
`mode="after"` validator that validates `cfg` dicts against
`QuantizerAttributeConfig`.
- Updated `need_calibration()` to iterate the normalized list instead of
the old dict.
- Changed `QuantizeQuantCfgType` alias from `dict[str | Callable, ...]`
to `list[QuantizerCfgEntry]`.
- **`conversion.py`**:
- Rewrote `set_quantizer_by_cfg()` (line 217) to iterate the list
directly. Each entry's `parent_class` is resolved via
`QuantModuleRegistry[parent_class_name]` (the existing `_DMRegistryCls`
registry).
- Added `set_quantizer_attributes_full()` (line 314): full replacement
of quantizer attributes from a `QuantizerAttributeConfig`. Unspecified
fields revert to defaults, enforcing entry atomicity. Can also upgrade
`TensorQuantizer` → `SequentialQuantizer` or downgrade the reverse.
- Added `set_quantizer_attributes_partial()` (line 384): merges a
partial `dict` of attributes into existing quantizer state. Does NOT
change quantizer structure. Used for enable-only entries.
- Added `set_quantizer_by_cfg_context()` context manager (line 447) that
temporarily applies a `quant_cfg` list and restores original quantizer
state on exit.
- Deprecated `set_quantizer_attribute()` (line 525) with a
`DeprecationWarning` pointing to the new functions.
- **`tensor_quantizer.py`**:
- `TensorQuantizer.set_from_attribute_config()`: narrowed type hint from
`dict` to `dict[str, Any]`.
- Added `_axis_setter` and `_block_sizes_setter` custom setters so that
`axis` and `block_sizes` changes properly propagate to the calibrator
and maintain mutual exclusivity.
- `SequentialQuantizer.set_from_attribute_config()`: narrowed signature
to `list[QuantizerAttributeConfig] | list[dict[str, Any]]` (removed the
old union with single values).
- **`algorithms.py`**:
- Updated `_match_quantizer_cfg()` to iterate the list and return
`(matched_cfg, matched_enable)` tuple with last-match-wins.
- Updated `_cfg_to_dict()`, `estimate_quant_compression()`, and
`QuantRecipe` to work with the list-based format.
- Updated `get_auto_quantize_config()` to emit list-format `quant_cfg`.
- **`model_quant.py`**: `disable_quantizer()` / `enable_quantizer()` now
call `set_quantizer_attributes_partial()` directly instead of the
deprecated `set_quantizer_attribute()`. Updated docstrings and code
examples to show the list format.
- **`utils/core_utils.py`**: `disable_lora_quantizers_in_config()` and
`update_quant_cfg_with_kv_cache_quant()` updated to append
`QuantizerCfgEntry` dicts to the list.
- **Other**: minor updates to `backends/fp8_per_tensor_gemm.py`,
`backends/nvfp4_gemm.py`, `compress.py`, `model_calib.py`,
`export/unified_export_hf.py`, and
`sparsity/attention_sparsity/conversion.py` to use the list format.
- **`onnx/llm_export_utils/quantization_utils.py`**: Updated
quantization config construction to use list format.
**YAML recipes (`modelopt_recipes/`)**
- Converted all 5 general PTQ recipes to the new list format:
- `general/ptq/fp8_default-fp8_kv.yml`
- `general/ptq/nvfp4_default-fp8_kv.yml`
- `general/ptq/nvfp4_experts_only-fp8_kv.yml`
- `general/ptq/nvfp4_mlp_only-fp8_kv.yml`
- `general/ptq/nvfp4_omlp_only-fp8_kv.yml`
- Converted model-specific recipe:
`models/Step3.5-Flash/nvfp4-mlp-only.yaml`
**Documentation (`docs/`)**
- New guide: `docs/source/guides/_quant_cfg.rst` — comprehensive
reference covering entry format, ordering semantics, entry atomicity,
`enable` vs `cfg` independence, `parent_class` filtering, and common
patterns (deny-all-then-enable, customizing a built-in config, building
from scratch).
- Updated `_pytorch_quantization.rst` code examples to show the list
format with `copy.deepcopy` and `.append()`.
- Added `_quant_cfg.rst` to the quantization guide table of contents.
**Examples**
- Updated all quantization examples to use the list format:
`deepseek/ptq.py`, `diffusers/quantization/config.py`,
`llm_ptq/hf_ptq.py`, `llm_qat/main.py`, `vllm_serve/vllm_ptq_utils.py`,
`llm_autodeploy/run_auto_quantize.py`, `llm_eval/quantization_utils.py`,
`llm_ptq/example_utils.py`,
`windows/torch_onnx/diffusers/qad_example/sample_example_qad_diffusers.py`,
and 2 notebooks.
**Tests**
- New test file:
`tests/unit/torch/quantization/test_config_validation.py` — unit tests
for `need_calibration()`, `normalize_quant_cfg_list()` (new format,
legacy format conversions, error cases),
`find_quant_cfg_entry_by_path()`, `_match_quantizer_cfg()`, and
`QuantizeConfig` Pydantic validators.
- Extended `tests/unit/torch/quantization/test_quantize_cpu.py` with
tests for `set_quantizer_attributes_full()` (atomicity, parent_class
filtering, SequentialQuantizer creation), list ordering, enable-only
entry behavior, and end-to-end legacy dict format.
- Updated 20+ existing test files across `tests/unit/`, `tests/gpu/`,
`tests/gpu_megatron/`, and `tests/_test_utils/` to use the list format.
##### Backward compatibility
`normalize_quant_cfg_list()` is called automatically by the
`QuantizeConfig` Pydantic `mode="before"` validator, so existing code
passing the old dict-based format (flat dict like `{"*weight_quantizer":
{"num_bits": 8}}`, single-key dict lists, or `nn.*`-scoped dicts with
`parent_class` semantics) continues to work without modification. The
legacy `"default"` key is converted to `quantizer_name: "*"`.
`set_quantizer_attribute()` is preserved as a deprecated wrapper around
`set_quantizer_attributes_partial()`.
#### Test coverage
- **Unit tests**: new `test_config_validation.py` with tests for
normalization, validation, path lookup, and cfg matching. Extended
`test_quantize_cpu.py` with tests for full/partial attribute setting,
ordering, atomicity, and legacy backward compatibility.
- **System testing**:
```
python examples/llm_ptq/hf_ptq.py \
--model Qwen/Qwen3-8B \
--recipe general/ptq/fp8_default-fp8_kv \
--export_path=build/fp8_default-fp8_kv42 \
--calib_size=16 \
--batch_size=0 \
--trust_remote_code \
--export_fmt=hf
```
### Additional Information
<!-- E.g. related issue. -->
---------
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
157 lines
5.0 KiB
Python
157 lines
5.0 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2024 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.
|
|
|
|
"""High-level tests for quantization."""
|
|
|
|
import copy
|
|
|
|
import pytest
|
|
from _test_utils.torch.quantization.models import SimpleConv, SimpleConvLinear, SimpleLinear
|
|
from _test_utils.torch.quantization.quantize_common import (
|
|
FP4_SVDQUANT_CFG,
|
|
INT4_AWQ_CLIP_CFG,
|
|
INT4_AWQ_FULL_CFG,
|
|
quantize_model_and_forward,
|
|
save_restore_test,
|
|
)
|
|
|
|
import modelopt.torch.quantization as mtq
|
|
from modelopt.torch.quantization.extensions import get_cuda_ext_mx
|
|
|
|
NVFP4_WEIGHT_ACT_MSE_CFG = {
|
|
"quant_cfg": [
|
|
{
|
|
"quantizer_name": "*weight_quantizer",
|
|
"cfg": {
|
|
"num_bits": (2, 1),
|
|
"block_sizes": {-1: 16, "type": "static", "scale_bits": (4, 3)},
|
|
"axis": None,
|
|
},
|
|
"enable": True,
|
|
},
|
|
{
|
|
"quantizer_name": "*input_quantizer",
|
|
"cfg": {
|
|
"num_bits": (2, 1),
|
|
"block_sizes": {-1: 16, "type": "dynamic", "scale_bits": (4, 3)},
|
|
"axis": None,
|
|
},
|
|
"enable": True,
|
|
},
|
|
],
|
|
"algorithm": {
|
|
"method": "mse",
|
|
"step_size": 0.25,
|
|
"start_multiplier": 0.25,
|
|
"stop_multiplier": 2.0,
|
|
},
|
|
}
|
|
|
|
NVFP4_WEIGHT_MSE_FP8_SWEEP_CFG = {
|
|
"quant_cfg": [
|
|
{
|
|
"quantizer_name": "*weight_quantizer",
|
|
"cfg": {
|
|
"num_bits": (2, 1),
|
|
"block_sizes": {-1: 16, "type": "static", "scale_bits": (4, 3)},
|
|
"axis": None,
|
|
},
|
|
"enable": True,
|
|
},
|
|
{"quantizer_name": "*input_quantizer", "enable": False},
|
|
],
|
|
"algorithm": {
|
|
"method": "mse",
|
|
"fp8_scale_sweep": True,
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("model_cls", [SimpleLinear, SimpleConv, SimpleConvLinear])
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
mtq.INT8_DEFAULT_CFG,
|
|
mtq.FP8_DEFAULT_CFG,
|
|
mtq.W4A8_AWQ_BETA_CFG,
|
|
mtq.INT8_SMOOTHQUANT_CFG,
|
|
mtq.INT4_BLOCKWISE_WEIGHT_ONLY_CFG,
|
|
mtq.INT4_AWQ_CFG,
|
|
INT4_AWQ_CLIP_CFG,
|
|
INT4_AWQ_FULL_CFG,
|
|
mtq.NVFP4_DEFAULT_CFG,
|
|
FP4_SVDQUANT_CFG,
|
|
mtq.NVFP4_AWQ_LITE_CFG,
|
|
mtq.NVFP4_AWQ_CLIP_CFG,
|
|
mtq.NVFP4_AWQ_FULL_CFG,
|
|
mtq.NVFP4_W4A4_WEIGHT_LOCAL_HESSIAN_CFG,
|
|
mtq.MXFP8_DEFAULT_CFG,
|
|
mtq.MXFP6_DEFAULT_CFG,
|
|
mtq.MXFP4_DEFAULT_CFG,
|
|
mtq.MXINT8_DEFAULT_CFG,
|
|
mtq.NVFP4_KV_ROTATE_CFG,
|
|
mtq.FP8_2D_BLOCKWISE_WEIGHT_ONLY_CFG,
|
|
NVFP4_WEIGHT_ACT_MSE_CFG,
|
|
NVFP4_WEIGHT_MSE_FP8_SWEEP_CFG,
|
|
],
|
|
)
|
|
def test_quantize(model_cls, config):
|
|
"""Test quantize function can run without problems."""
|
|
if config in [
|
|
mtq.NVFP4_DEFAULT_CFG,
|
|
FP4_SVDQUANT_CFG,
|
|
mtq.NVFP4_AWQ_LITE_CFG,
|
|
mtq.NVFP4_AWQ_CLIP_CFG,
|
|
mtq.NVFP4_AWQ_FULL_CFG,
|
|
mtq.MXFP8_DEFAULT_CFG,
|
|
mtq.MXFP6_DEFAULT_CFG,
|
|
mtq.MXFP4_DEFAULT_CFG,
|
|
mtq.MXINT8_DEFAULT_CFG,
|
|
mtq.NVFP4_KV_ROTATE_CFG,
|
|
mtq.FP8_2D_BLOCKWISE_WEIGHT_ONLY_CFG,
|
|
NVFP4_WEIGHT_ACT_MSE_CFG,
|
|
NVFP4_WEIGHT_MSE_FP8_SWEEP_CFG,
|
|
mtq.NVFP4_W4A4_WEIGHT_LOCAL_HESSIAN_CFG,
|
|
]:
|
|
if get_cuda_ext_mx() is None:
|
|
pytest.skip("cuda_ext_mx is not available")
|
|
if model_cls in [SimpleConv, SimpleConvLinear]:
|
|
pytest.skip("Conv weight quantization will fail as the kernel_size < FP4 blocksize")
|
|
|
|
if config == mtq.FP8_2D_BLOCKWISE_WEIGHT_ONLY_CFG:
|
|
# reduce block sizes for simple testing models
|
|
config = copy.deepcopy(config)
|
|
for entry in config["quant_cfg"]:
|
|
if entry.get("quantizer_name") == "*weight_quantizer":
|
|
entry.setdefault("cfg", {})["block_sizes"] = {-1: 8, -2: 8}
|
|
model = model_cls().cuda()
|
|
calib_data = [model.get_input().cuda() for _ in range(8)]
|
|
quantize_model_and_forward(model, config, calib_data)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model_cls", "quant_config"),
|
|
[
|
|
(SimpleLinear, mtq.INT8_SMOOTHQUANT_CFG),
|
|
(SimpleLinear, mtq.W4A8_AWQ_BETA_CFG),
|
|
(SimpleConvLinear, mtq.INT8_DEFAULT_CFG),
|
|
(SimpleLinear, NVFP4_WEIGHT_MSE_FP8_SWEEP_CFG),
|
|
(SimpleLinear, NVFP4_WEIGHT_ACT_MSE_CFG),
|
|
],
|
|
)
|
|
def test_save_restore(model_cls, quant_config):
|
|
test_cpu_restore = quant_config == mtq.INT8_SMOOTHQUANT_CFG
|
|
save_restore_test(model_cls, "cuda", quant_config, test_cpu_restore=test_cpu_restore)
|