Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Changelog
- Add **Domino** speculative-decoding training: the parallel DFlash draft backbone plus a lightweight GRU causal correction head, selected via ``dflash_architecture_config.projector_type=domino``. Trained with a base/final dual loss whose ``dflash_lambda_base_start``/``dflash_lambda_base_decay_ratio`` curriculum decays the base-loss weight 1→0. Exports in the z-lab drafter format; recipe at ``modelopt_recipes/general/speculative_decoding/domino.yaml``. Training only — the inference path is not wired up yet.
- Add Torch-TensorRT FP8 deployment example for HuggingFace ViT (``examples/torch_trt/``): ``torch_tensorrt_ptq.py`` covers ``mtq.quantize`` → ``torch_tensorrt.compile(ir="dynamo")``, and ``torch_tensorrt_accuracy.py`` reports the compiled model's ImageNet-1k top-1/top-5 accuracy via the ``onnx_ptq`` ``evaluate`` harness (the unquantized baseline is Torch-TensorRT-compiled too, for an apples-to-apples comparison). Ships a ViT-tuned FP8 PTQ recipe under ``modelopt_recipes/huggingface/vit/ptq/`` (``fp8.yaml``) composed from the shared ``modelopt_recipes/configs/`` units: it quantizes the encoder Linears, patch-embed ``nn.Conv2d``, ``classifier``, and per-block LayerNorm inputs plus the attention Q/K/V BMMs and softmax. Verified on ``google/vit-base-patch16-224`` (ImageNet-1k 50k validation): FP8 stays within 0.13 pp Top-1 of the FP16 baseline.
- Add **AutoQuantize recipe** support: ``mtq.auto_quantize`` can be driven declaratively from a YAML recipe (``RecipeType.AUTO_QUANTIZE`` / ``AutoQuantizeConfig``) specifying candidate formats, the ``effective_bits`` target, cost model (incl. ``active_moe`` and ``excluded_module_name_patterns``), scoring method, and disabled layers. Adds an ``effective_bits`` cost-model override on ``QuantizeConfig`` / ``QuantizerAttributeConfig`` (block-scale-accurate NVFP4 = 4.5 via ``configs/numerics/nvfp4``). Shipped recipes live under ``modelopt_recipes/general/auto_quantize/`` and model-specific ones under ``modelopt_recipes/huggingface/<model>/auto_quantize/``.
- Add module-specific AutoQuantize search spaces through ``mtq.auto_quantize(..., module_search_spaces=...)`` and recipe-level ``auto_quantize.module_search_spaces``. Glob-matched runtime decision groups can override the global candidate formats and control whether BF16/no-quant is solver-selectable with ``allow_no_quant``; one candidate with ``allow_no_quant: false`` fixes the matching group to that format while retaining its effective-bits cost. Rules cannot partially split runtime-fused groups, fixed groups are isolated from unrelated calibration algorithms, and checkpoint replay validates the complete module-specific candidate configuration before reusing calibration or sensitivity state.
- Add ``rotate.mode`` to torch quantizer configs. The default ``"rotate"`` keeps the existing rotate-before-quantize behavior; ``"rotate_back"`` enables fake-quant rotate → quantize → rotate-back for TensorQuantizer.

**Bug Fixes**
Expand Down
16 changes: 10 additions & 6 deletions examples/hf_ptq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,16 @@ The recipe quantizes the less accuracy-sensitive layers with the more aggressive
keeps the more sensitive ones at higher precision (or unquantized), so the model meets the recipe's
`effective_bits` target. To author your own, copy a shipped recipe and adjust `candidate_formats`,
`constraints.effective_bits`, `auto_quantize_method` (`gradient` / `kl_div`), `score_size`,
`disabled_layers` (excluded from the search), and `cost_excluded_layers` (kept out of the bit-budget
accounting — e.g. VL vision towers). Recipes can splice a shared base `disabled_layers` set via
`$import` (see `modelopt_recipes/configs/auto_quantize/units/base_disabled_layers`).

bf16 (no quantization) is always an implicit per-layer choice, so `candidate_formats` need only list
the quantized options — a single format (e.g. `[fp8]`) gives a `{fp8, bf16}` per-layer search.
`module_search_spaces` (optional per-module candidate overrides), `disabled_layers` (excluded from
the search), and `cost_excluded_layers` (kept out of the bit-budget accounting — e.g. VL vision
towers). Recipes can splice a shared base `disabled_layers` set via `$import` (see
`modelopt_recipes/configs/auto_quantize/units/base_disabled_layers`).

bf16 (no quantization) is an implicit per-layer choice for the top-level `candidate_formats`, so a
single format (e.g. `[fp8]`) gives a `{fp8, bf16}` per-layer search. A `module_search_spaces` rule can
set `allow_no_quant: false` to exclude bf16 from the solver choices for matching modules. A rule with
one candidate format then fixes those modules to that format while retaining their uncompressed
weight in the effective-bit denominator and their selected-format cost in the numerator.

For models without backprop support (e.g. Llama-4), use the `kl_div` scoring method — see the shipped
`general/auto_quantize/nvfp4_fp8_kl_div_at_5p4bits` recipe.
Expand Down
44 changes: 30 additions & 14 deletions examples/hf_ptq/hf_ptq.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,25 @@ def _match_candidate_to_preset(fmt) -> tuple[str | None, dict]:
return None, fmt.model_dump()


def _mtq_candidate_formats(formats) -> list[dict]:
"""Translate recipe candidate formats to export-compatible mtq configs."""
quantization_formats = []
for fmt in formats:
preset_name, quant_cfg = _match_candidate_to_preset(fmt)
if preset_name is not None and preset_name not in _AUTO_QUANTIZE_QFORMATS:
raise ValueError(
f"AutoQuantize candidate_formats entry '{preset_name}' is not supported for "
"unified checkpoint export. Use an export-compatible format."
)
if preset_name is None:
warnings.warn(
"An AutoQuantize candidate_formats entry matches no shipped preset; its export "
"compatibility cannot be verified. Ensure it is safe for HF checkpoint export."
)
quantization_formats.append(quant_cfg)
return quantization_formats


def _mtq_inputs_from_auto_quantize_config(aq_config, args: argparse.Namespace) -> dict:
"""Map a resolved AutoQuantizeConfig to mtq.auto_quantize inputs.

Expand All @@ -327,23 +346,19 @@ def _mtq_inputs_from_auto_quantize_config(aq_config, args: argparse.Namespace) -
# Translate each candidate to its mtq preset dict and, in the same pass, guard export
# compatibility (fails fast, before the expensive search). Custom configs matching no shipped
# preset can't be verified, so warn rather than block.
quantization_formats = []
for fmt in aq_config.candidate_formats:
preset_name, quant_cfg = _match_candidate_to_preset(fmt)
if preset_name is not None and preset_name not in _AUTO_QUANTIZE_QFORMATS:
raise ValueError(
f"AutoQuantize candidate_formats entry '{preset_name}' is not supported for "
"unified checkpoint export. Use an export-compatible format."
)
if preset_name is None:
warnings.warn(
"An AutoQuantize candidate_formats entry matches no shipped preset; its export "
"compatibility cannot be verified. Ensure it is safe for HF checkpoint export."
)
quantization_formats.append(quant_cfg)
quantization_formats = _mtq_candidate_formats(aq_config.candidate_formats)
module_search_spaces = [
{
"module_name_patterns": search_space.module_name_patterns,
"quantization_formats": _mtq_candidate_formats(search_space.candidate_formats),
"allow_no_quant": search_space.allow_no_quant,
}
for search_space in aq_config.module_search_spaces
]
return {
"constraints": constraints,
"quantization_formats": quantization_formats,
"module_search_spaces": module_search_spaces,
"disabled_layers": aq_config.disabled_layers,
"kv_cache_quant_cfg": kv_cache_quant_cfg,
"method": aq_config.auto_quantize_method,
Expand Down Expand Up @@ -467,6 +482,7 @@ def forward_step(model, batch):
forward_step=forward_step,
loss_func=loss_func,
quantization_formats=inputs["quantization_formats"],
module_search_spaces=inputs["module_search_spaces"],
num_calib_steps=len(calib_dataloader),
num_score_steps=min(len(calib_dataloader), max(inputs["score_size"] // args.batch_size, 1)),
verbose=True,
Expand Down
47 changes: 47 additions & 0 deletions modelopt/recipe/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"AutoQuantizeConfig",
"AutoQuantizeConstraints",
"AutoQuantizeCost",
"AutoQuantizeModuleSearchSpace",
"ModelOptAutoQuantizeRecipe",
"ModelOptDFlashRecipe",
"ModelOptEagleRecipe",
Expand Down Expand Up @@ -191,6 +192,46 @@ def _validate_effective_bits(cls, v: float) -> float:
return v


class AutoQuantizeModuleSearchSpace(ModeloptBaseConfig):
"""Candidate formats selectable for modules matching one or more name patterns."""

module_name_patterns: LayerPatternList = ModeloptField(
default=[],
title="Module name patterns",
description="Glob patterns matched against quantizable module names. A grouped AutoQuantize "
"decision must match a rule for every module in the group or for none of them.",
validate_default=True,
)
candidate_formats: list[QuantizeConfig] = ModeloptField(
default=[],
title="Module candidate quantization formats",
description="Formats selectable for matching modules. These override the top-level "
"candidate_formats for the matching AutoQuantize decision group.",
validate_default=True,
)
allow_no_quant: bool = ModeloptField(
default=True,
title="Allow no-quant selection",
description="Whether BF16/no-quant is selectable for matching modules. AutoQuantize keeps "
"an internal no-quant baseline for sensitivity scoring and cost normalization even when "
"this is false.",
)

@field_validator("module_name_patterns")
@classmethod
def _at_least_one_module_pattern(cls, v: list[str]) -> list[str]:
if not v:
raise ValueError("module_search_spaces requires at least 1 module_name_pattern")
return v

@field_validator("candidate_formats")
@classmethod
def _at_least_one_module_candidate(cls, v: list[QuantizeConfig]) -> list[QuantizeConfig]:
if not v:
raise ValueError("module_search_spaces requires at least 1 candidate_format")
return v


class AutoQuantizeConfig(ModeloptBaseConfig):
"""Schema for the ``auto_quantize`` block of an AutoQuantize recipe."""

Expand All @@ -206,6 +247,12 @@ class AutoQuantizeConfig(ModeloptBaseConfig):
"(e.g. [fp8]) yields a {fp8, bf16} per-layer search.",
validate_default=True,
)
module_search_spaces: list[AutoQuantizeModuleSearchSpace] = ModeloptField(
default=[],
title="Module-specific search spaces",
description="Optional per-module overrides for candidate formats and BF16/no-quant "
"selectability. Matching is performed after runtime-fusion grouping.",
)
auto_quantize_method: Literal["gradient", "kl_div"] = ModeloptField(
default="gradient",
title="Sensitivity scoring method",
Expand Down
Loading
Loading