Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
12e1540
Reuse a whole recipe via $import, deprecate recipe_type, and backfill…
shengliangxu Sep 15, 2026
80d7d17
Swap the NVIDIA Nemotron backfill for Kimi-K2.6 and Qwen3.5-397B-A17B…
shengliangxu Sep 15, 2026
aabfe5c
Keep the schema regex line-scoped and the delegation helper private
shengliangxu Sep 15, 2026
32bc5dd
Merge main into shengliangx/batch-backfill-recipe
shengliangxu Sep 16, 2026
90126a5
Merge main into shengliangx/batch-backfill-recipe
shengliangxu Sep 16, 2026
95f7ab6
Merge main into shengliangx/batch-backfill-recipe
shengliangxu Sep 16, 2026
1cc02bf
Address review: phantom doc rows, alias validation, and kind resolution
shengliangxu Sep 16, 2026
8bdcb88
Merge main into shengliangx/batch-backfill-recipe
shengliangxu Sep 17, 2026
c4e3592
Sweep recipe_type out of the recipes main added in this merge
shengliangxu Sep 17, 2026
6dd443c
Merge origin-oss/main into shengliangx/batch-backfill-recipe
shengliangxu Sep 18, 2026
82b35cb
Fix the windows unit job: read the docs as UTF-8, not the locale code…
shengliangxu Sep 18, 2026
5c41145
Address review: cycle-specific error, honest hook docs, leaner changelog
shengliangxu Sep 18, 2026
35f9009
Address review: UTF-8 recipe reads, snippet-aware hook, MTP order tests
shengliangxu Sep 18, 2026
6f11724
Cover every recipe whose mtp.* disable is load-bearing, not a listed …
shengliangxu Sep 18, 2026
502ded4
Correct a discovery message left saying 'ends with' after the filter …
shengliangxu Sep 18, 2026
632e8ef
Drop the MTP fix from this PR: no semantic changes to pre-existing re…
shengliangxu Sep 19, 2026
e709fc4
Merge origin-oss/main into shengliangx/batch-backfill-recipe
shengliangxu Sep 19, 2026
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
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Changelog

*Quantization*

- A recipe can now **delegate its whole body to another recipe** with a top-level ``$import``; any top-level key given alongside it overrides the imported one. ``metadata.recipe_type`` became optional along with it: a recipe states its kind with a ``# modelopt-schema:`` comment, with ``metadata.recipe_type``, or by delegating to a recipe that does, and only a recipe that another file imports has to carry the schema comment. Whatever a recipe does state must be true: a schema comment and a ``recipe_type`` must agree, and so must a recipe and the recipe it delegates to. ``modelopt_recipes/models/`` uses this for checkpoint entries that a portable recipe already reproduces: the entry aliases that recipe instead of copying it.
- Backfill the recipes behind NVIDIA's already-published checkpoints under ``modelopt_recipes/models/``, so a released checkpoint's quantization scheme is reachable from its own model-hub path rather than only from the general tier. For example, ``moonshotai/Kimi-K2.6`` (published as ``nvidia/Kimi-K2.6-NVFP4``) and ``nvidia/Qwen3.5-397B-A17B`` (``nvidia/Qwen3.5-397B-A17B-NVFP4-V2``) each alias a portable recipe wholesale -- the general expert-only NVFP4 recipe and the ``qwen3_5_moe`` architecture recipe respectively -- rather than copying its body; other checkpoints follow in separate changes.
- Add ``layerwise.export_dir``: layerwise calibration writes each decoder layer to its own quantized checkpoint shard as it finishes, so no separate ``export_hf_checkpoint()`` pass is needed and, with ``layerwise.checkpoint_dir``, an interrupted run resumes without redoing finished layers. Calibration writes the layer shards; ``finalize()`` on the exporter left on the model adds the tail shard, the index and the config artifacts, and the checkpoint does not load until it runs. ``examples/hf_ptq`` does this for you. Supports FP8 and NVFP4 on single-process models, resident or offloaded, including multimodal models and models with MTP layers; other formats and placements raise ``NotImplementedError`` before calibration starts.
- Add support for quantizing and calibrating enabled operators outside the transformer layers, such as ``lm_head``, when using layerwise calibration.
- Add an end-to-end BEVFormer ONNX PTQ example with temporal calibration data generation, INT8 and FP8 quantization, TensorRT engine building, and nuScenes accuracy evaluation. See `examples/onnx_ptq/bevformer/README.md <https://github.com/NVIDIA/Model-Optimizer/tree/main/examples/onnx_ptq/bevformer>`_ for details.
Expand Down Expand Up @@ -64,6 +66,7 @@ Changelog
- The single-format quantization CLI flags are deprecated in favour of ``--recipe`` and will be removed in a future release; passing one now emits a ``FutureWarning``. ``examples/hf_ptq``: ``--qformat`` and ``--kv_cache_qformat``. ``examples/megatron_bridge/quantize.py``: ``--quant_cfg``, ``--kv_cache_quant`` and ``--weight_only``. ``examples/torch_onnx/torch_quant_to_onnx.py``: ``--qformat``. A recipe carries the quantization config, the calibration algorithm and the KV-cache setting in one file, so they cannot drift apart the way separate flags can -- and ``--recipe`` already took precedence over all six, silently on ``hf_ptq`` and with a warning on ``megatron_bridge`` -- with one gap the recipe closes rather than inherits: a weight AutoQuantize recipe that omits ``kv_cache`` still falls back to ``--kv_cache_qformat``, so set ``kv_cache`` in the recipe when migrating. Use a recipe from ``modelopt_recipes/general/ptq/``, an architecture-specific one under ``modelopt_recipes/model_type/<model_type>/``, or a checkpoint-specific one under ``modelopt_recipes/models/``. The warning fires only when a flag is passed explicitly: ``--qformat`` defaults to ``fp8`` and ``--kv_cache_qformat`` to ``fp8_cast``, so warning on the defaults would fire on every run, including runs that correctly use ``--recipe``. ``examples/speculative_decoding/scripts/quantize_drafter.py`` keeps ``--qformat`` undeprecated: it has no ``--recipe`` alternative yet.
- The TensorRT-LLM checkpoint export format is deprecated and will be removed in 0.49.0: ``export_tensorrt_llm_checkpoint`` and ``torch_to_tensorrt_llm_checkpoint`` now emit a ``DeprecationWarning`` on use. Use ``export_hf_checkpoint``, which exports a unified Hugging Face checkpoint deployable on TensorRT-LLM, vLLM and SGLang. Its implementation moved to ``modelopt.torch.export.trtllm``, so import those two functions from there and the ``ModelConfig`` dataclasses from ``modelopt.torch.export.trtllm.model_config``; both functions remain importable from ``modelopt.torch.export`` for this release only.
- The ``evaluation`` agent skill no longer supports GDPVal: its task recipe, example config and Apptainer SIF helper are removed, and the shared NeMo Gym machinery it carried now lives at ``references/gym.md`` with the launcher wrapper renamed ``scripts/nel-gym.sh``. GDPVal is an AA-suite member, so an "AA" request now generates the ``aa/`` tasks only -- report per-task scores rather than an aggregate compared against a published AA Index.
- Deprecate ``metadata.recipe_type`` in recipe YAML. A recipe now says what kind it is with a ``# modelopt-schema:`` comment naming its schema class, or by delegating to a recipe that does; ``recipe_type`` is still read and still honoured, so a recipe outside this repo keeps working unchanged, but new recipes should leave it out and every recipe shipped here has been converted. Where both are present they must agree, and so must a recipe and the recipe it delegates to -- a disagreement is an error rather than a silent preference.

**Bug Fixes**

Expand Down
57 changes: 53 additions & 4 deletions docs/source/guides/10_recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,9 @@ Reusable snippets are stored under ``modelopt_recipes/configs/``:
Metadata section
================

Every recipe must contain a ``metadata`` mapping with at least a ``recipe_type`` field:
Every recipe contains a ``metadata`` mapping. ``recipe_type`` is optional and
**deprecated** -- a recipe may instead declare its kind with a ``# modelopt-schema:``
comment, or inherit it from the recipe it delegates to; see `Declaring a recipe's kind`_:

.. list-table::
:header-rows: 1
Expand All @@ -403,7 +405,7 @@ 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.
schema depend on the recipe's kind, however it is declared.

PTQ (``recipe_type: ptq``)
--------------------------
Expand Down Expand Up @@ -516,18 +518,28 @@ General PTQ recipes are model-agnostic and apply to any supported architecture:
* - ``general/ptq/nvfp4_weight_only-kv_fp8_cast``
- NVFP4 W4A16 weight-only, FP8 KV cache with constant amax

See `modelopt_recipes/ptq.md <https://github.com/NVIDIA/Model-Optimizer/blob/main/modelopt_recipes/ptq.md>`_
for the full list and for guidance on choosing between them.

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
checkpoint entries 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.

A checkpoint entry comes in two forms. A **mirror** carries its own body, because the
release uses a per-layer scheme no portable recipe expresses. An **alias** has no body of
its own: a general or architecture recipe already produces that scheme, so the entry
imports that recipe wholesale and exists to make it findable from the checkpoint's hub
path. See `modelopt_recipes/ptq.md <https://github.com/NVIDIA/Model-Optimizer/blob/main/modelopt_recipes/ptq.md>`_
for what each checkpoint entry does.

.. note::

``model_type/`` was previously named ``huggingface/``. Old
Expand All @@ -547,6 +559,43 @@ for the layout conventions and recipe-lookup order.
- MXFP8 language-model base with MSE-calibrated NVFP4 routed experts for MiniMax-M3


Delegating to another recipe
----------------------------

A recipe can hand its whole body to another recipe with a top-level ``$import`` and keep
only its own ``metadata``. Keys given alongside the ``$import`` override the imported
ones, so the body -- ``quantize``, its algorithm and every ``quant_cfg`` entry -- is
inherited unchanged:

.. code-block:: yaml

imports:
base: general/ptq/nvfp4_default-kv_fp8_cast

$import: base
metadata:
description: What this checkpoint uses the base recipe for.

.. _Declaring a recipe's kind:

Note the missing ``recipe_type``. A recipe states its kind in whichever of these it
likes, and the loader takes the first that answers: a ``# modelopt-schema:`` comment
naming its schema class, ``metadata.recipe_type``, or -- as here -- the recipe it
delegates to. ``metadata.recipe_type`` is **deprecated**: it is still read and still
honoured, so no existing recipe needs changing, but new recipes should declare their
schema instead. It remains the only option for a directory-format recipe's
``metadata.yml``, which has no comment preamble to read.
Comment thread
shengliangxu marked this conversation as resolved.

Stating more than one is allowed, but they must agree, and that extends across the
import: a recipe and the recipe it delegates to must be the same kind, since the import
takes over the whole body. A PTQ recipe importing an EAGLE one is rejected as a kind
mismatch rather than left to fail on whatever the spliced sections do to the schema.

What *is* required: a recipe that another file imports must carry the schema comment,
since ``$import`` resolution needs it to validate the imported payload. A recipe nothing
imports needs no comment at all. This is how the checkpoint aliases under ``models/``
name the recipe behind a release without duplicating its body.

Loading recipes
===============

Expand All @@ -555,7 +604,7 @@ 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:
type depends on the recipe's kind, however it is declared:

.. code-block:: python

Expand Down
66 changes: 55 additions & 11 deletions modelopt/recipe/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import warnings
from enum import Enum
from typing import Literal
from typing import ClassVar, Literal

from pydantic import Field, field_validator, model_validator

Expand Down Expand Up @@ -67,9 +67,16 @@ class RecipeType(str, Enum):
class RecipeMetadataConfig(ModeloptBaseConfig):
"""YAML shape of the recipe metadata section."""

recipe_type: RecipeType = Field(
recipe_type: RecipeType | None = ModeloptField(
default=None,
title="Recipe type",
description="The type of the recipe (e.g. PTQ).",
description="The type of the recipe (e.g. PTQ). **Deprecated** in recipe YAML: "
"the ``# modelopt-schema:`` comment naming the recipe's schema class already says "
"which kind it is -- and it is the same declaration that makes the file "
"``$import``-able -- so the class fills this in. Still read and still honoured, so "
"no existing recipe needs changing, but new recipes should leave it out. It remains "
"the only option where there is no schema comment to read, as in a directory-format "
"recipe's ``metadata.yml``. When both are present they must agree.",
)
description: str = ModeloptField(
default=_DEFAULT_RECIPE_DESCRIPTION,
Expand All @@ -78,10 +85,10 @@ class RecipeMetadataConfig(ModeloptBaseConfig):
)


def _metadata_field(recipe_type: RecipeType):
"""Build the metadata Pydantic field with the recipe_type baked into the default."""
def _metadata_field():
"""Build a metadata Pydantic field that defaults to the owning class's recipe type."""
return ModeloptField(
default={"recipe_type": recipe_type, "description": _DEFAULT_RECIPE_DESCRIPTION},
default={"description": _DEFAULT_RECIPE_DESCRIPTION},
title="Metadata",
description="Recipe metadata containing the recipe type and description.",
validate_default=True,
Expand All @@ -94,16 +101,43 @@ class ModelOptRecipeBase(ModeloptBaseConfig):
If a layer name matches ``"*output_layer*"``, the attributes will be replaced with ``{"enable": False}``.
"""

#: The kind of recipe this class *is*. Set on every concrete subclass; it is the
#: single source of truth for ``metadata.recipe_type``, which the validator below
#: fills in so a recipe file never has to repeat what its schema already states.
RECIPE_TYPE: ClassVar[RecipeType | None] = None

metadata: RecipeMetadataConfig = Field(
title="Metadata",
description="Recipe metadata containing the recipe type and description. "
"Required: a recipe without a ``metadata`` section is rejected so that a "
"missing section can't silently fall back to a default recipe type.",
"recipe always says what it is for.",
)

@model_validator(mode="after")
def _resolve_recipe_type(self):
"""Fill ``metadata.recipe_type`` from the schema class, or reject a mismatch.

The schema class already determines the kind, so a recipe file that declares its
schema needs no ``recipe_type``. One that states it anyway must state the truth --
a silent disagreement between the two would make the file mean different things
to the loader and to a reader.
"""
if self.RECIPE_TYPE is None:
return self
if self.metadata.recipe_type is None:
self.metadata.recipe_type = self.RECIPE_TYPE
elif self.metadata.recipe_type != self.RECIPE_TYPE:
raise ValueError(
f"metadata.recipe_type is {self.metadata.recipe_type.value!r} but this recipe "
f"is a {type(self).__name__}, which is {self.RECIPE_TYPE.value!r}. Drop the "
"recipe_type (the schema declares it) or correct it."
)
return self

@property
def recipe_type(self) -> RecipeType:
"""Return the recipe type from metadata."""
assert self.metadata.recipe_type is not None, "recipe_type was not resolved"
return self.metadata.recipe_type

@property
Expand All @@ -115,6 +149,8 @@ def description(self) -> str:
class ModelOptPTQRecipe(ModelOptRecipeBase):
"""Our config class for PTQ recipes."""

RECIPE_TYPE: ClassVar[RecipeType] = RecipeType.PTQ

quantize: QuantizeConfig = Field(
title="PTQ config",
description="PTQ config containing quant_cfg and algorithm. Required: a PTQ "
Expand Down Expand Up @@ -308,7 +344,9 @@ def _has_search_space(self):
class ModelOptAutoQuantizeRecipe(ModelOptRecipeBase):
"""Our config class for AutoQuantize recipes."""

metadata: RecipeMetadataConfig = _metadata_field(RecipeType.AUTO_QUANTIZE)
RECIPE_TYPE: ClassVar[RecipeType] = RecipeType.AUTO_QUANTIZE

metadata: RecipeMetadataConfig = _metadata_field()

quantize: QuantizeConfig | None = ModeloptField(
default=None,
Expand Down Expand Up @@ -380,7 +418,9 @@ class ModelOptSpeculativeRecipeBase(ModelOptRecipeBase):
class ModelOptEagleRecipe(ModelOptSpeculativeRecipeBase):
"""Our config class for EAGLE speculative decoding recipes."""

metadata: RecipeMetadataConfig = _metadata_field(RecipeType.SPECULATIVE_EAGLE)
RECIPE_TYPE: ClassVar[RecipeType] = RecipeType.SPECULATIVE_EAGLE

metadata: RecipeMetadataConfig = _metadata_field()

eagle: EagleConfig = ModeloptField(
default=EagleConfig(),
Expand Down Expand Up @@ -409,7 +449,9 @@ def _warn_rope_vs_training_seq_len(self) -> ModelOptEagleRecipe:
class ModelOptDFlashRecipe(ModelOptSpeculativeRecipeBase):
"""Our config class for DFlash speculative decoding recipes."""

metadata: RecipeMetadataConfig = _metadata_field(RecipeType.SPECULATIVE_DFLASH)
RECIPE_TYPE: ClassVar[RecipeType] = RecipeType.SPECULATIVE_DFLASH

metadata: RecipeMetadataConfig = _metadata_field()

dflash: DFlashConfig = ModeloptField(
default=DFlashConfig(),
Expand All @@ -431,7 +473,9 @@ def _derive_dflash_offline(self) -> ModelOptDFlashRecipe:
class ModelOptMedusaRecipe(ModelOptSpeculativeRecipeBase):
"""Our config class for Medusa speculative decoding recipes."""

metadata: RecipeMetadataConfig = _metadata_field(RecipeType.SPECULATIVE_MEDUSA)
RECIPE_TYPE: ClassVar[RecipeType] = RecipeType.SPECULATIVE_MEDUSA

metadata: RecipeMetadataConfig = _metadata_field()

medusa: MedusaConfig = ModeloptField(
default=MedusaConfig(),
Expand Down
Loading
Loading