Files
Model-Optimizer/docs/source/guides/10_recipes.rst
T
Shengliang Xu c7ed23a103 Rename modelopt_recipes/huggingface to model_type with backward-compat alias (#2328)
### What does this PR do?

Type of change: Refactor + deprecation (recipe-library restructure,
backward compatible), plus an unrelated transformers-compat test fix.

Rename the architecture-specific recipe tier
`modelopt_recipes/huggingface/` to
`modelopt_recipes/model_type/`, making explicit that it holds recipes
**shared across
every checkpoint of a Hugging Face `model_type`** — as opposed to the
checkpoint-mirror
`models/<org>/<model_id>/` tier. The old `huggingface/` path keeps
working as a
deprecated backward-compat alias (a source-tree symlink plus a loader
alias), so no
saved `--recipe` path breaks.

- **Loader alias** (`modelopt/recipe/loader.py`): generalized so saved
`--recipe huggingface/<model_type>/...` paths rewrite to
`model_type/...`, alongside
the existing `huggingface/models/... -> models/...` rewrite (checked
first as the more
specific prefix). This keeps old paths resolving for pip-installed
wheels, where the
  source-tree symlinks don't survive.
- **Internal `$import`s**: rewritten from `huggingface/... ->
model_type/...` inside the
shipped recipes so they resolve without the symlink — mandatory for
wheels, since
  `$import` resolution goes through `config_loader` (no alias there).
- **Packaging** (`pyproject.toml`, `MANIFEST.in`): extended the
symlink-exclusion globs
so the recursive `**/*.yaml` package-data glob doesn't double-ship
recipes through the
`huggingface -> model_type` and `model_type/models -> ../models`
symlinks.
- **Docs / examples / skills / tests**: migrated all internal references
to the canonical
`model_type/`; `huggingface/` remains only in the deprecated-alias tests
and explanatory
  notes.
- **Unrelated fix (2nd commit):**
`tests/unit/torch/export/test_quant_aware_conversion.py`
  failed on transformers>=5.9, which dropped `base_model_prefix` from
`WeightTransform.__slots__` (the scoped-rule tests assigned it on the
now-slotted
object). Production `_scope_prefixes` already reads it via `getattr(...,
None)` and
degrades correctly, so there is no runtime change — the tests now set it
through a
helper that suppresses `AttributeError` across the supported
transformers range.

### Usage

```bash
# New canonical path
python examples/hf_ptq/hf_ptq.py --model <ckpt> \
    --recipe model_type/qwen3_vl/ptq/fp8_vision-kv_none

# Old path still works (deprecated backward-compat alias)
python examples/hf_ptq/hf_ptq.py --model <ckpt> \
    --recipe huggingface/qwen3_vl/ptq/fp8_vision-kv_none
```

```python
from modelopt.recipe import load_recipe

load_recipe("model_type/vit/ptq/fp8")    # canonical
load_recipe("huggingface/vit/ptq/fp8")   # deprecated alias, resolves to the same recipe
```

### Testing

- `tests/unit/recipe/` — **336 passed**, including the new
`test_load_recipe_huggingface_arch_backward_compat_alias` and the
updated
  structural/doc tests (`test_recipe_docs.py`).
- `tests/unit/torch/export/test_quant_aware_conversion.py` — **16
passed** (was 4 failed
  on transformers 5.9.0).
- Built an sdist **and** a wheel and inspected both manifests: each
recipe ships exactly
once (29 `model_type/`, 13 `models/`, 2 `timm/`, 162 total) with
**zero** `huggingface/` or
  `model_type/models/` duplicates and no build error on the symlinks.
- Simulated a wheel install (symlink-free extracted tree) and confirmed
`huggingface/<arch>/...`, `model_type/...`, and `huggingface/models/...`
all resolve via
  the loader alias — including a recipe that pulls internal `$import`s.

### Before your PR is "*Ready for review*"

- Is this change backward compatible?: ✅ — old `huggingface/...` recipe
paths keep resolving via the symlink + loader alias.
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: ✅ — backward-compat alias test
added; structural/doc tests updated to the new layout.
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
✅ — Deprecations entry under 0.48.0. (The transformers-compat test fix
is not changelog-worthy.)
- Did you get Claude approval on this PR?: ❌ — not yet.

### Additional Information

The `model_type/models -> ../models` symlink is kept purely as a
backward-compat alias for
old `huggingface/models/<org>/<model_id>/...` paths; `model_type/` is
otherwise
architecture-only. If we ever want it strictly architecture-only, that
symlink can be
dropped later without breaking anything, since the loader rewrites
`huggingface/models/...`
straight to the top-level `models/` tier.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added post-training quantization recipes for Gemma, Gemma 4,
MiniMax-M3, Nemotron, Qwen, Step-3.7, ViT, and other architectures.
- Added vision, multimodal, mixed-precision, and experts-only
quantization options.

- **Documentation**
- Standardized architecture-specific recipes under `model_type/` and
updated examples and guidance.

- **Compatibility**
- Legacy `huggingface/` recipe paths remain supported with deprecation
warnings.
  - Local recipe files now take precedence over built-in recipes.
  - Deprecated quantization-format flags warn when explicitly provided.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
2026-09-15 12:16:12 -07:00

765 lines
27 KiB
ReStructuredText

.. _recipes:
Recipes
#######
A **recipe** is a declarative specification that fully describes how to optimize a model.
A recipe can be a single YAML file or a directory containing YAML configs and other files
that together define a model optimization workflow.
Recipes decouple optimization settings from Python code, enabling reuse, sharing, version
control, and reproducibility. Instead of editing Python scripts to change optimization
parameters, you author (or select) a recipe and pass it to the ModelOpt tooling.
While the examples below focus on PTQ (the first supported recipe type), the recipe
system is designed to support any optimization technique.
.. contents:: On this page
:local:
:depth: 2
Motivation
==========
Without recipes, optimization settings are scattered across command-line arguments, Python
constants, and ad-hoc code edits. This makes it difficult to:
* **Reproduce** a published result -- the exact configuration is buried in script arguments.
* **Share** a configuration -- there is no single artifact to hand off.
* **Version-control** changes -- diffs are mixed in with unrelated code changes.
* **Onboard new models** -- engineers must read source code to discover which
settings to tweak.
Recipes solve these problems by capturing **all** the configuration needed to optimize a
model in a single, portable artifact -- either a YAML file or a directory of files.
Design overview
===============
The recipe system is part of the :mod:`modelopt.recipe` package and consists of three
layers:
1. **Recipe sources** -- YAML files or directories stored in the ``modelopt_recipes/``
directory (shipped with the package) or on the user's filesystem.
2. **Config loader** -- :func:`~modelopt.recipe.load_config` reads YAML files, resolves
paths, and performs automatic ``ExMy`` floating-point notation conversion.
3. **Recipe loader** -- :func:`~modelopt.recipe.load_recipe` validates the loaded
configuration against Pydantic models and returns a typed recipe object ready for use.
Recipe format
=============
A recipe contains two top-level sections: ``metadata`` and a type-specific
configuration section (for example, ``quantize`` for PTQ recipes). These can live
in a single YAML file or be split across files in a directory.
Recipes support two authoring styles: **inline** (all values written directly)
and **import-based** (reusable snippets referenced via ``$import``). Both
styles can be used in a single-file or directory layout.
Single-file format
------------------
The simplest form is a single ``.yaml`` file.
**Inline style** — all config values are written directly:
.. code-block:: yaml
metadata:
recipe_type: ptq
description: FP8 per-tensor weight and activation (W8A8), FP8 KV cache, max calibration.
quantize:
algorithm: max
quant_cfg:
- quantizer_name: '*'
enable: false
- quantizer_name: '*input_quantizer'
cfg:
num_bits: e4m3
axis:
- quantizer_name: '*weight_quantizer'
cfg:
num_bits: e4m3
axis:
- quantizer_name: '*[kv]_bmm_quantizer'
cfg:
num_bits: e4m3
# ... standard exclusions omitted for brevity
**Import style** — the same recipe using reusable config snippets:
.. code-block:: yaml
imports:
base_disable_all: configs/ptq/units/base_disable_all
default_disabled: configs/ptq/units/default_disabled_quantizers
fp8: configs/numerics/fp8
metadata:
recipe_type: ptq
description: FP8 per-tensor weight and activation (W8A8), FP8 KV cache, max calibration.
quantize:
algorithm: max
quant_cfg:
- $import: base_disable_all
- quantizer_name: '*input_quantizer'
cfg:
$import: fp8
- quantizer_name: '*weight_quantizer'
cfg:
$import: fp8
- quantizer_name: '*[kv]_bmm_quantizer'
cfg:
$import: fp8
- $import: default_disabled
Both styles produce identical results at load time. The import style reduces
duplication when multiple recipes share the same numeric formats or exclusion
lists. See :ref:`composable-imports` below for the full ``$import`` specification.
Directory format
----------------
For larger recipes or when you want to keep metadata separate from the
optimization configuration, use a directory with multiple files. Here is a PTQ
example:
.. code-block:: text
my_recipe/
metadata.yaml # metadata section body
quantize.yaml # quantize section (+ optional imports)
``metadata.yaml``:
.. code-block:: yaml
recipe_type: ptq
description: My custom NVFP4 recipe.
``quantize.yaml``:
.. code-block:: yaml
algorithm: max
quant_cfg:
- quantizer_name: '*'
enable: false
- quantizer_name: '*weight_quantizer'
cfg:
num_bits: e2m1
block_sizes: {-1: 16, type: dynamic, scale_bits: e4m3}
- quantizer_name: '*input_quantizer'
cfg:
num_bits: e4m3
axis:
Both inline and import styles work with the directory format. Imports are
scoped to the file that declares them; for PTQ quantization snippets, declare
the relevant ``imports`` section in ``quantize.yaml``.
.. _composable-imports:
Composable imports
------------------
Recipes can import **reusable config snippets** via the ``imports`` section.
This eliminates duplication — numeric format definitions and standard exclusion
lists are authored once and referenced by name across recipes.
The ``imports`` section is a dict mapping short names to config file paths.
References use the explicit ``{$import: name}`` marker so they are never
confused with literal values.
.. note::
``imports`` (no ``$``) is a **top-level structural section** — like
``metadata`` or ``quantize``, it declares the recipe's dependencies.
``$import`` (with ``$``) is an **inline directive** that appears inside
data values and gets resolved at load time.
The ``$import`` marker can appear anywhere in the recipe:
- As a **dict value** — the marker is replaced with the snippet content, or
merged with inline overrides when sibling keys are present.
- As a **list element** — the surrounding list's schema and the imported
snippet's ``modelopt-schema`` determine whether the imported snippet is
appended as one element or spliced as multiple elements.
As a **dict value**, ``$import`` supports composition with clear override
precedence (lowest to highest):
1. **Imports in list order** — ``$import: [base, override]``: later snippets
override earlier ones on key conflicts.
2. **Inline keys** — extra keys alongside ``$import`` override all imported
values.
This is equivalent to calling ``dict.update()`` in order: imports first (in
list order), then inline keys last.
.. code-block:: yaml
# Single import
cfg:
$import: nvfp4
# Import + override — import nvfp4, then override type inline
cfg:
$import: nvfp4 # imports {num_bits: e2m1, block_sizes: {-1: 16, type: dynamic, ...}}
block_sizes:
-1: 16
type: static # overrides type: dynamic → static calibration
# Multiple imports — later snippet overrides earlier on conflict
cfg:
$import: [base_format, kv_tweaks] # kv_tweaks wins on shared keys
# All three: multi-import + inline override
cfg:
$import: [bits, scale]
axis: 0 # highest precedence
As a **list element**, ``$import`` must be the only key — extra keys alongside
a list import are not supported. List imports require a typed containing list
and a schema-declared snippet:
* If the snippet schema is the same list type as the containing list, its
entries are spliced into the surrounding list.
* If the snippet schema is the list element type, it is appended as one list
item.
.. code-block:: yaml
imports:
base_disable_all: configs/ptq/units/base_disable_all
default_disabled: configs/ptq/units/default_disabled_quantizers
fp8: configs/numerics/fp8
metadata:
recipe_type: ptq
description: FP8 W8A8, FP8 KV cache.
quantize:
algorithm: max
quant_cfg:
- $import: base_disable_all # appended from a single-entry snippet
- quantizer_name: '*weight_quantizer'
cfg:
$import: fp8 # cfg value replaced with imported dict
- $import: default_disabled # spliced from a multi-element list snippet
In this example:
- ``$import: base_disable_all`` and ``$import: default_disabled`` are **list elements**
— ``base_disable_all`` is appended as one entry, while ``default_disabled`` is
a YAML list spliced into ``quant_cfg``.
- ``$import: fp8`` under ``cfg`` is a **dict value** — the snippet (a YAML dict of
quantizer attributes) replaces the ``cfg`` field.
Import paths are resolved via :func:`~modelopt.recipe.load_config` — the
built-in ``modelopt_recipes/`` library is checked first, then the filesystem.
**Recursive imports:** An imported snippet may itself contain an ``imports``
section. Each file's imports are scoped to that file — the same name can be
used in different files without conflict. Circular imports are detected and
raise ``ValueError``.
Multi-document snippets
^^^^^^^^^^^^^^^^^^^^^^^
Dict-valued snippets (e.g., numeric format definitions) can use ``imports``
directly because the ``imports`` key and the snippet content are both part of
the same YAML mapping. List-valued snippets have a problem: YAML only allows
one root node per document, so a file cannot be both a mapping (for
``imports``) and a list (for entries) at the same time.
The solution is **multi-document YAML**: the first document holds the
``imports``, and the second document (after ``---``) holds the list content.
The loader parses both documents, resolves ``$import`` markers in the content,
and returns the resolved list:
.. code-block:: yaml
# configs/ptq/units/kv_fp8.yaml — list snippet that imports a dict snippet
# modelopt-schema: modelopt.torch.quantization.config.QuantizerCfgListConfig
imports:
fp8: configs/numerics/fp8
---
- quantizer_name: '*[kv]_bmm_quantizer'
cfg:
$import: fp8
This enables full composability — list snippets can reference dict snippets,
dict snippets can reference other dict snippets, and recipes can reference
any of them. All import resolution happens at load time with the same
precedence rules.
Schema modelines
^^^^^^^^^^^^^^^^^
Reusable snippets referenced from an ``imports`` section must declare the
Pydantic-compatible schema they are expected to satisfy using a
``modelopt-schema`` comment preamble. The comment is ignored by YAML itself,
but ModelOpt's loader reads it before parsing and validates the resolved
snippet payload after any imports have been expanded:
.. code-block:: yaml
# modelopt-schema: modelopt.torch.quantization.config.QuantizerAttributeConfig
num_bits: e2m1
block_sizes:
-1: 16
type: dynamic
scale_bits: e4m3
The schema comment itself is not returned as part of the loaded config. The
declared schema is the validation contract: after imports are resolved, the
loader validates the payload against that schema and returns the result --
a Pydantic model instance for ``BaseModel`` schemas (with defaults populated)
or a validated ``dict``/``list`` for ``TypedDict`` schemas. The schema can
also be supplied at the call site via ``load_config(path, schema_type=...)``,
which takes precedence over an in-file comment when both are present.
Top-level recipe files are validated by :func:`~modelopt.recipe.load_recipe`;
they do not need ``modelopt-schema`` comments. The comments are the contract
for reusable snippets, especially snippets under ``modelopt_recipes/configs/``:
every file referenced from an ``imports`` section must declare
``modelopt-schema``, whether it is imported into a dict value or a list.
Schemas should be concrete ModelOpt config types, Pydantic models,
``TypedDict`` classes, or explicitly typed container aliases such as
``list[QuantizerCfgEntry]``. Untyped list schemas are not supported for list
imports because the loader must know the element type. For safety,
``modelopt-schema`` paths must resolve under the ``modelopt.`` package.
List imports are schema-driven. When a typed list field such as
``quant_cfg: list[QuantizerCfgEntry]`` contains a bare import entry, the
imported snippet must declare its own ``modelopt-schema``:
* If the snippet schema matches the containing list type
(``QuantizerCfgListConfig``, i.e. ``list[QuantizerCfgEntry]``), the imported
entries are spliced into the containing list.
* If the snippet schema matches the element type (``QuantizerCfgEntry``), the
imported entry is appended as a single list item.
* If the containing list or imported snippet has no schema, or the snippet
schema is neither the list type nor the element type, loading raises
``ValueError``.
Built-in config snippets
^^^^^^^^^^^^^^^^^^^^^^^^
Reusable snippets are stored under ``modelopt_recipes/configs/``:
.. list-table::
:header-rows: 1
:widths: 45 55
* - Snippet path
- Description
* - ``configs/numerics/fp8``
- FP8 E4M3 quantizer attributes
* - ``configs/numerics/nvfp4``
- NVFP4 E2M1 blockwise, dynamic calibration, FP8 scales (default)
* - ``configs/numerics/nvfp4_static``
- NVFP4 E2M1 blockwise, static calibration, FP8 scales
* - ``configs/ptq/units/base_disable_all``
- Disable all quantizers (deny-all-then-configure pattern)
* - ``configs/ptq/units/default_disabled_quantizers``
- Standard exclusions (LM head, routers, BatchNorm, etc.)
* - ``configs/ptq/units/kv_fp8``
- FP8 E4M3 KV cache quantization (multi-document, imports ``fp8``)
* - ``configs/ptq/units/kv_fp8_cast``
- FP8 E4M3 KV cache with constant amax (skips KV calibration)
* - ``configs/ptq/units/kv_nvfp4_cast``
- NVFP4 KV cache with constant amax (skips KV calibration)
Metadata section
================
Every recipe must contain a ``metadata`` mapping with at least a ``recipe_type`` field:
.. list-table::
:header-rows: 1
:widths: 20 15 65
* - Field
- Required
- Description
* - ``recipe_type``
- Yes
- The optimization category. Determines which configuration sections are
expected (e.g., ``"ptq"`` expects a ``quantize`` section). See
:class:`~modelopt.recipe.config.RecipeType` for supported values.
* - ``description``
- No
- A human-readable summary of what the recipe does.
Type-specific configuration sections
=====================================
Each recipe type defines its own configuration section. The section name and
schema depend on the ``recipe_type`` value in the metadata.
PTQ (``recipe_type: ptq``)
--------------------------
PTQ recipes contain a ``quantize`` mapping with:
.. list-table::
:header-rows: 1
:widths: 20 15 65
* - Field
- Required
- Description
* - ``quant_cfg``
- Yes
- An ordered list of
:class:`~modelopt.torch.quantization.config.QuantizerCfgEntry` entries.
In YAML each entry is authored as a mapping; after loading they are
validated Pydantic instances. See :ref:`quant-cfg` for the full
specification of entries, ordering semantics, and atomicity rules.
* - ``algorithm``
- No
- The calibration algorithm: ``"max"`` (default), ``"mse"``, ``"smoothquant"``,
``"awq_lite"``, ``"awq_full"``, ``"awq_clip"``, ``"gptq"``, or ``null`` for
formats that need no calibration (e.g. MX formats).
ExMy floating-point notation
=============================
The config loader supports a convenient shorthand for floating-point bit formats.
This is primarily used in PTQ recipes for ``num_bits`` and ``scale_bits`` fields,
but applies to any YAML value loaded through :func:`~modelopt.recipe.load_config`.
Instead of writing a Python tuple, you write the format name directly:
.. code-block:: yaml
num_bits: e4m3 # automatically converted to (4, 3)
scale_bits: e8m0 # automatically converted to (8, 0)
The notation is case-insensitive (``E4M3``, ``e4m3``, ``E4m3`` all work). The
conversion is performed by :func:`~modelopt.recipe.load_config` when loading any
YAML file, so it works in both recipe files and standalone config files.
Common formats:
.. list-table::
:header-rows: 1
:widths: 15 15 70
* - Notation
- Tuple
- Description
* - ``e4m3``
- ``(4, 3)``
- FP8 E4M3 -- standard FP8 weight/activation format
* - ``e5m2``
- ``(5, 2)``
- FP8 E5M2 -- wider dynamic range, used for gradients
* - ``e2m1``
- ``(2, 1)``
- FP4 E2M1 -- NVFP4 weight format
* - ``e8m0``
- ``(8, 0)``
- E8M0 -- MX block scaling format
Built-in recipes
================
ModelOpt ships a library of built-in recipes under the ``modelopt_recipes/`` package.
These are bundled with the Python distribution and can be referenced by their relative
path (without the ``modelopt_recipes/`` prefix).
PTQ recipes
-----------
General PTQ recipes are model-agnostic and apply to any supported architecture:
.. list-table::
:header-rows: 1
:widths: 40 60
* - Recipe path
- Description
* - ``general/ptq/fp8_default-kv_fp8_cast``
- FP8 per-tensor W8A8, FP8 KV cache with constant amax, max calibration
* - ``general/ptq/fp8_default-kv_fp8``
- FP8 per-tensor W8A8, FP8 KV cache with data-driven calibration
* - ``general/ptq/nvfp4_default-kv_fp8_cast``
- NVFP4 W4A4, FP8 KV cache with constant amax, max calibration
* - ``general/ptq/nvfp4_default-kv_fp8``
- NVFP4 W4A4, FP8 KV cache with data-driven calibration
* - ``general/ptq/nvfp4_default-kv_nvfp4_cast``
- NVFP4 W4A4, NVFP4 KV cache with constant amax, max calibration
* - ``general/ptq/nvfp4_mlp_only-kv_fp8_cast``
- NVFP4 for MLP layers only, FP8 KV cache with constant amax
* - ``general/ptq/nvfp4_mlp_only-kv_fp8``
- NVFP4 for MLP layers only, FP8 KV cache
* - ``general/ptq/nvfp4_experts_only-kv_fp8_cast``
- NVFP4 for MoE expert layers only, FP8 KV cache with constant amax
* - ``general/ptq/nvfp4_experts_only-kv_fp8``
- NVFP4 for MoE expert layers only, FP8 KV cache
* - ``general/ptq/nvfp4_experts_only-kv_fp8_layerwise``
- NVFP4 for MoE expert layers only, FP8 KV cache, layerwise calibration
* - ``general/ptq/nvfp4_omlp_only-kv_fp8_cast``
- NVFP4 for output projection + MLP layers, FP8 KV cache with constant amax
* - ``general/ptq/nvfp4_omlp_only-kv_fp8``
- NVFP4 for output projection + MLP layers, FP8 KV cache
* - ``general/ptq/nvfp4_weight_only-kv_fp8_cast``
- NVFP4 W4A16 weight-only, FP8 KV cache with constant amax
Model-specific recipes
----------------------
Model-specific recipes come in two tiers: architecture recipes keyed by a
Hugging Face ``model_type`` under ``model_type/<model_type>/<task>/``, and
checkpoint mirrors keyed by a model-hub path under
``models/<org>/<model_id>/<task>/``. See
`modelopt_recipes/model_type/README.md <https://github.com/NVIDIA/Model-Optimizer/blob/main/modelopt_recipes/model_type/README.md>`_
and
`modelopt_recipes/models/README.md <https://github.com/NVIDIA/Model-Optimizer/blob/main/modelopt_recipes/models/README.md>`_
for the layout conventions and recipe-lookup order.
.. note::
``model_type/`` was previously named ``huggingface/``. Old
``huggingface/<model_type>/...`` recipe paths still resolve for backward
compatibility, but ``model_type/`` is the canonical location — prefer it in
new ``--recipe`` flags and ``load_recipe`` calls.
.. list-table::
:header-rows: 1
:widths: 40 60
* - Recipe path
- Description
* - ``models/stepfun-ai/Step-3.5-Flash/ptq/nvfp4-mlp-only``
- NVFP4 MLP-only for Step 3.5 Flash MoE model
* - ``model_type/minimax_m3_vl/ptq/mxfp8_nvfp4_experts``
- MXFP8 language-model base with MSE-calibrated NVFP4 routed experts for MiniMax-M3
Loading recipes
===============
Python API
----------
Use :func:`~modelopt.recipe.load_recipe` to load a recipe. The path is resolved
against the built-in library first, then the filesystem. The returned object's
type depends on the ``recipe_type`` in the metadata:
.. code-block:: python
from modelopt.recipe import load_recipe
# Load a built-in recipe by relative path (suffix optional)
recipe = load_recipe("general/ptq/fp8_default-kv_fp8_cast")
# For PTQ recipes, the quantize dict can be passed directly to mtq.quantize()
import modelopt.torch.quantization as mtq
model = mtq.quantize(model, recipe.quantize, forward_loop)
.. code-block:: python
# Load a custom recipe from the filesystem (file or directory)
recipe = load_recipe("/path/to/my_custom_ptq.yaml")
# or: recipe = load_recipe("/path/to/my_recipe_dir/")
Command-line usage
------------------
Some example scripts accept a ``--recipe`` flag. For instance, the PTQ example:
.. code-block:: bash
python examples/hf_ptq/hf_ptq.py \
--model Qwen/Qwen3-8B \
--recipe general/ptq/fp8_default-kv_fp8_cast \
--export_path build/fp8 \
--calib_size 512 \
--export_fmt hf
When ``--recipe`` is provided, the script loads the recipe and uses its configuration
directly, bypassing format-specific flags (e.g., ``--qformat`` / ``--kv_cache_qformat``
for PTQ).
Loading standalone configs
--------------------------
:func:`~modelopt.recipe.load_config` loads arbitrary YAML config files with
automatic ``ExMy`` conversion and built-in path resolution. This is useful
for loading shared configuration fragments:
.. code-block:: python
from modelopt.recipe import load_config
cfg = load_config("configs/some_shared_config")
Path resolution
===============
Both :func:`~modelopt.recipe.load_recipe` and :func:`~modelopt.recipe.load_config`
resolve paths using the same strategy:
1. If the path is absolute, use it directly.
2. If relative, check the **built-in recipes library** first
(``modelopt_recipes/``), probing ``.yml`` and ``.yaml`` suffixes as well as
directories.
3. Then check the **filesystem**, probing the same suffixes and directories.
This means built-in recipes can be referenced without any prefix:
.. code-block:: python
# These are all equivalent:
load_recipe("general/ptq/fp8_default-kv_fp8_cast")
load_recipe("general/ptq/fp8_default-kv_fp8_cast.yaml")
Writing a custom recipe
=======================
To create a custom recipe:
1. Start from an existing recipe that is close to your target configuration.
2. Copy it and modify the type-specific configuration as needed (for PTQ recipes,
see :ref:`quant-cfg` for ``quant_cfg`` entry format details).
3. Update the ``metadata.description`` to describe your changes.
4. Save the file (or directory) and pass its path to ``load_recipe()`` or ``--recipe``.
Example -- creating a custom PTQ recipe using imports:
.. code-block:: yaml
# my_int8_ptq.yaml
imports:
base_disable_all: configs/ptq/units/base_disable_all
default_disabled: configs/ptq/units/default_disabled_quantizers
metadata:
recipe_type: ptq
description: INT8 per-channel weight, per-tensor activation.
quantize:
algorithm: max
quant_cfg:
- $import: base_disable_all
- quantizer_name: '*weight_quantizer'
cfg:
num_bits: 8
axis: 0
- quantizer_name: '*input_quantizer'
cfg:
num_bits: 8
axis:
- $import: default_disabled
The built-in snippets (``base_disable_all``, ``default_disabled``) handle the
deny-all prefix and standard exclusions. Only the format-specific entries need
to be written inline.
Recipe repository layout
========================
The ``modelopt_recipes/`` package is organized as follows:
.. code-block:: text
modelopt_recipes/
+-- __init__.py
+-- general/ # Model-agnostic recipes
| +-- ptq/
| +-- fp8_default-kv_fp8_cast.yaml
| +-- fp8_default-kv_fp8.yaml
| +-- nvfp4_default-kv_fp8_cast.yaml
| +-- nvfp4_default-kv_fp8.yaml
| +-- nvfp4_default-kv_nvfp4_cast.yaml
| +-- nvfp4_mlp_only-kv_fp8_cast.yaml
| +-- nvfp4_mlp_only-kv_fp8.yaml
| +-- nvfp4_experts_only-kv_fp8_cast.yaml
| +-- nvfp4_experts_only-kv_fp8.yaml
| +-- nvfp4_experts_only-kv_fp8_layerwise.yaml
| +-- nvfp4_omlp_only-kv_fp8_cast.yaml
| +-- nvfp4_omlp_only-kv_fp8.yaml
| +-- nvfp4_weight_only-kv_fp8_cast.yaml
+-- model_type/ # Architecture-specific recipes (by model_type)
| +-- <model_type>/ # see modelopt_recipes/model_type/README.md
| +-- <task>/
| +-- <recipe>.yaml
+-- models/ # Checkpoint-specific recipes (by model-hub path)
| +-- <org>/<model_id>/ # see modelopt_recipes/models/README.md
| +-- <task>/
| +-- <recipe>.yaml
+-- configs/ # Reusable config snippets (imported via $import)
+-- numerics/ # Numeric format definitions
| +-- fp8.yaml
| +-- nvfp4_static.yaml
| +-- nvfp4.yaml
+-- ptq/
+-- units/ # Reusable quant_cfg building blocks
| +-- base_disable_all.yaml
| +-- default_disabled_quantizers.yaml
| +-- kv_fp8.yaml
| +-- kv_fp8_cast.yaml
| +-- kv_nvfp4_cast.yaml
| +-- w8a8_fp8_fp8.yaml
| +-- w4a4_nvfp4_nvfp4.yaml
+-- presets/ # Complete configs (backward compat with *_CFG dicts)
+-- model/
| +-- fp8.yaml
+-- kv/
+-- fp8.yaml
Recipe data model
=================
Recipes are validated at load time using Pydantic models:
:class:`~modelopt.recipe.config.ModelOptRecipeBase`
Base class for all recipe types. Contains a required ``metadata`` field
typed as :class:`~modelopt.recipe.config.RecipeMetadataConfig` -- a
:class:`~modelopt.torch.opt.config.ModeloptBaseConfig` subclass exposing
``recipe_type`` and ``description`` as Pydantic fields.
:class:`~modelopt.recipe.config.ModelOptPTQRecipe`
PTQ-specific recipe. Adds a required ``quantize`` field typed as
:class:`~modelopt.torch.quantization.config.QuantizeConfig` (also a
``ModeloptBaseConfig`` subclass, containing ``quant_cfg`` and ``algorithm``).
:class:`~modelopt.recipe.config.RecipeType`
Enum of supported recipe types.
Future directions
=================
The recipe system is designed to grow:
* **QAT recipes** -- ``recipe_type: qat`` with training hyperparameters, distillation
settings, and dataset configuration.
* **Sparsity recipes** -- structured and unstructured pruning configurations.
* **Speculative decoding recipes** -- draft model and vocabulary calibration settings.
* **Composite recipes** -- chaining multiple optimization stages
(e.g., quantize then prune) in a single recipe.
* **Dataset configuration** -- standardized ``dataset`` section for calibration data
specification.
* **Recipe merging and override utilities** -- programmatic tools to compose and
customize recipes.
* **Unified entry point** -- a ``nv-modelopt`` CLI that accepts ``--recipe`` as the
primary configuration mechanism, replacing per-example scripts.