Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d69ab24
Backfill recipes for NVIDIA's published quantized checkpoints
shengliangxu Sep 10, 2026
9fbd855
Add CHANGELOG entries for the published-checkpoint recipe backfill
shengliangxu Sep 10, 2026
28675dc
Give every published checkpoint an entry under modelopt_recipes/models/
shengliangxu Sep 10, 2026
5c7aba0
Key the DeepSeek-V4-Flash W4A16 alias by its source weights
shengliangxu Sep 10, 2026
6669884
Document recipe delegation and the models/ alias tier in the recipes …
shengliangxu Sep 10, 2026
549b4cd
Make a recipe's kind declarable once, in whichever place fits
shengliangxu Sep 10, 2026
312e7e3
Reject a recipe that delegates to a different kind of recipe
shengliangxu Sep 10, 2026
944bec7
Deprecate metadata.recipe_type and cover the recipe-loader changes wi…
shengliangxu Sep 10, 2026
d725b91
Merge remote-tracking branch 'origin-oss/main' into shengliangx/batch…
shengliangxu Sep 10, 2026
a4ad0ad
Fold the Step-3.7 checkpoint mirror into the step3p7 architecture tier
shengliangxu Sep 10, 2026
ea93894
Limit the backfill to checkpoints NVIDIA publishes
shengliangxu Sep 10, 2026
b9a20c8
Narrow the backfill batch to the NVIDIA-source checkpoints
shengliangxu Sep 10, 2026
53e62b8
Document $import as aliasing a recipe, not as carrying a recipe_type
shengliangxu Sep 11, 2026
1d08d0e
Move the KV-unquantized general recipes to the batches that need them
shengliangxu Sep 11, 2026
49fd58b
Stop using the deprecated recipe_type in the shipped recipes
shengliangxu Sep 11, 2026
43dbce5
Remove the recipe_backfill tool and its generated index
shengliangxu Sep 11, 2026
2e173b7
Revert the minimax_m3_vl "fix": recipes match in-memory, not on-disk,…
shengliangxu Sep 11, 2026
6eb514f
Drop the Nano-30B NVFP4 recipe: that release is PTQ followed by QAD
shengliangxu Sep 11, 2026
ee6c531
Frame the models/ tier change as a backfill in the changelog
shengliangxu Sep 11, 2026
8bb64f5
Drop the import-mechanism explanation from the alias recipe headers
shengliangxu Sep 11, 2026
0074e25
Merge main into shengliangx/batch-backfill-recipe
shengliangxu Sep 11, 2026
33861e0
Drop too-old Llama-Nemotron checkpoint alias recipes
shengliangxu Sep 11, 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
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Changelog

*Quantization*

- A recipe can now **delegate its whole body to another recipe** with a top-level ``$import``, overriding only ``metadata``. ``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. This batch covers the NVIDIA-source releases: NVIDIA-Nemotron-3-Nano-30B-A3B FP8, NVIDIA-Nemotron-3-Super-120B-A12B FP8, and Nemotron-3-Nano-Omni-30B-A3B-Reasoning (FP8 and NVFP4); the remaining source-model orgs follow in separate changes. A release is only backfilled when its model card supports a post-training quantization recipe -- one whose card documents quantization-aware distillation after PTQ is deliberately left out, since no PTQ recipe reproduces it.
- 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.

*Misc*
Expand All @@ -21,11 +23,13 @@ Changelog
**Deprecations**

- 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.
- 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**

- Fix ``--use_fsdp2`` HuggingFace checkpoint export gathering the whole model onto rank 0, which made export the dominant phase of a PTQ run and could exhaust host memory on large models. The model is now split into per-decoder-layer units dealt round-robin across ranks; each rank gathers every unit but keeps, packs, and writes only the ones it owns, so a rank buffers roughly ``model / world_size`` instead of the whole checkpoint, and rank 0 writes the combined index. Export configurations that cannot be split this way now raise instead of producing a mismatched checkpoint: FSDP2 combined with another DTensor parallelism (for example FSDP2 + tensor parallel on a 2-D mesh; HSDP is supported), models whose decoder layers cannot be discovered, a decoder layer object reused across layers, and a module that holds the decoder layers while owning parameters of its own.
- Speed up ``mtq.quantize`` on FSDP2-sharded fused-MoE models. Promoting static-block weight quantizers gathered each expert's slice of the fused weight across ranks even though only quantizer state is read, adding a collective per expert to calibration.
- Fix the NVIDIA-Nemotron-3 Super and Ultra NVFP4 recipes quantizing the MTP block on the Megatron-Core path, where it is a live ``model.mtp`` submodule the recipes' broad ``*mixer.*`` patterns matched into, contrary to their own descriptions. Hugging Face runs were unaffected: the ``nemotron_h`` class discards the MTP tensors on load.
- Add FP8 and INT8 recipes that quantize timm ResNet shortcut inputs immediately before residual adds. The torch ONNX example now accepts PTQ and AutoQuantize recipes through ``--recipe`` and uses ``--qformat`` when no recipe is provided. ResNet supports only FP8 and INT8 because TensorRT has limited convolution kernel support; AutoQuantize and other quantization formats are no longer supported for ResNet.
- Fix a DDP hang in DFlash training at scale where a rank whose batch contained no valid anchor skipped the draft forward, leaving its rotary buffer list shorter than other ranks' and causing ``broadcast_buffers`` to hang. The buffer is now created during ``modify()`` before training begins.
- Fix ``megatron_generate`` dropping the VLM vision inputs (``pixel_values`` / ``image_grid_thw`` / ``image_sizes``) after the first generated token when KV-cache decoding is off, including the automatic fallback under sequence parallelism, which made generation silently ignore the image. No other ModelOpt feature is affected.
Expand Down
55 changes: 54 additions & 1 deletion docs/source/guides/10_recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -515,19 +515,37 @@ General PTQ recipes are model-agnostic and apply to any supported architecture:
- 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
* - ``general/ptq/fp8_default-kv_fp16``
- FP8 per-tensor W8A8, KV cache left unquantized
* - ``general/ptq/nvfp4_default-kv_fp16``
- NVFP4 W4A4, KV cache left unquantized
* - ``general/ptq/nvfp4_mlp_only-kv_fp16``
- NVFP4 for MLP layers only, KV cache left unquantized
* - ``general/ptq/nvfp4_experts_only-kv_fp16``
- NVFP4 for MoE expert layers only, KV cache left unquantized

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 ``huggingface/<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/huggingface/README.md <https://github.com/NVIDIA/Model-Optimizer/blob/main/modelopt_recipes/huggingface/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.

.. list-table::
:header-rows: 1
:widths: 40 60
Expand All @@ -540,6 +558,41 @@ 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.

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.

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 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