# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# 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.
"""Lightweight quantization config resolver usable by both Megatron and vLLM workers."""
from __future__ import annotations
from fnmatch import fnmatchcase
from typing import Any, Iterator
_QUANT_IGNORE_NAME_SUFFIXES = (
".weight",
".weight_scale",
".weight_scale_2",
)
# Layers kept in native dtype by the real-quant vLLM rollout. Shared between the
# vLLM quantization_config and the Megatron export-side ignore patterns.
DEFAULT_NVFP4_IGNORE = [
"lm_head",
"*output_layer*",
"*mlp.gate",
"*router*",
"*block_sparse_moe.gate*",
"*self_attention*",
"*self_attn*",
]
[docs]
def _iter_quant_ignore_suffix_variants(name: str) -> Iterator[str]:
"""Yield ``name`` and, if it ends in a known quant suffix, the stripped form."""
yield name
for suffix in _QUANT_IGNORE_NAME_SUFFIXES:
if name.endswith(suffix):
yield name[: -len(suffix)]
break
[docs]
def iter_quant_ignore_name_candidates(name: str) -> Iterator[str]:
"""Yield name variants matched by ModelOpt real-quant ignore patterns."""
yield from _iter_quant_ignore_suffix_variants(name)
alternate = (
name.removeprefix("model.") if name.startswith("model.") else f"model.{name}"
)
if alternate == name:
return
yield from _iter_quant_ignore_suffix_variants(alternate)
[docs]
def matches_quant_ignore_pattern(name: str, patterns: list[str]) -> bool:
"""Return whether ``name`` matches any ModelOpt real-quant ignore pattern."""
return any(
fnmatchcase(candidate, pattern)
for candidate in iter_quant_ignore_name_candidates(name)
for pattern in patterns
)
[docs]
def build_vllm_modelopt_nvfp4_config(
*,
ignore: list[str] | None = None,
) -> dict[str, Any]:
"""Build the HuggingFace quantization_config consumed by vLLM ModelOpt NVFP4.
NeMo-RL's ``quant_cfg`` recipes are ModelOpt PTQ/QAT configs consumed by
``mtq.quantize``. vLLM expects the deployment/export-side
``quantization_config`` shape instead.
"""
return {
"quant_method": "modelopt",
"config_groups": {
"group_0": {
"input_activations": None,
"weights": {
"dynamic": False,
"num_bits": 4,
"type": "float",
"group_size": 16,
},
"targets": ["Linear"],
}
},
"ignore": ignore if ignore is not None else list(DEFAULT_NVFP4_IGNORE),
"quant_algo": "NVFP4",
"quant_mode": "w4a16_nvfp4",
"weight_only": True,
"group_size": 16,
"producer": {"name": "modelopt"},
}
[docs]
def resolve_quant_cfg(quant_cfg: str) -> dict[str, Any]:
"""Resolve a quantization config string into a dict consumable by ``mtq.quantize``.
Resolution order:
1. Built-in ModelOpt config constant exposed on ``modelopt.torch.quantization``
(e.g. ``"NVFP4_DEFAULT_CFG"``, ``"FP8_DEFAULT_CFG"``).
2. A ModelOpt PTQ recipe — either the name of a built-in recipe shipped under
``modelopt_recipes/`` (e.g. ``"general/ptq/nvfp4_default-fp8_kv"``; the
``.yml`` / ``.yaml`` suffix is optional) or the path to a user-authored
YAML recipe. Resolution is performed by ``modelopt.recipe.load_config``,
which searches the filesystem first and then the built-in recipe library.
For Ray/container workers, use an absolute path for user-authored recipe
files; NeMo-RL repo-relative recipe paths are not resolved here.
YAML recipes are expected to follow the standard ModelOpt PTQ recipe layout
with a top-level ``quantize:`` section in the ``{"quant_cfg": [...],
"algorithm": ...}`` shape that ``mtq.quantize`` expects. A bare
``{"quant_cfg": [...], "algorithm": ...}`` document (without a wrapping
``quantize:`` key) is also accepted for convenience. If ``algorithm`` is
omitted, it defaults to ``"max"`` so ModelOpt's calibration helpers see the
same normalized config as ``mtq.quantize``. The extracted dict — not the full
recipe — is returned.
See ``modelopt_recipes/general/ptq/`` in the NVIDIA/Model-Optimizer repo
(https://github.com/NVIDIA/Model-Optimizer) for the canonical format and
``examples/modelopt/quant_configs/`` for a user-authored example.
"""
import modelopt.torch.quantization as mtq
from modelopt.recipe import load_config
def _normalize_mtq_cfg(config: dict[str, Any]) -> dict[str, Any]:
if not isinstance(config, dict):
raise ValueError(
f"Quantization recipe '{quant_cfg}' must resolve to a dict."
)
mtq_cfg = config.get("quantize", config)
if not isinstance(mtq_cfg, dict) or "quant_cfg" not in mtq_cfg:
raise ValueError(
f"Quantization recipe '{quant_cfg}' must contain a 'quant_cfg' "
f"entry (optionally nested under a top-level 'quantize:' section)."
)
if "algorithm" not in mtq_cfg:
mtq_cfg = {**mtq_cfg, "algorithm": "max"}
return mtq_cfg
builtin = getattr(mtq, quant_cfg, None)
if builtin is not None:
return _normalize_mtq_cfg(builtin)
try:
loaded = load_config(quant_cfg)
except (ValueError, FileNotFoundError) as e:
raise ValueError(
f"Unknown quant_cfg '{quant_cfg}'. Must be either a built-in "
f"ModelOpt config name (e.g. 'NVFP4_DEFAULT_CFG'), a built-in "
f"ModelOpt PTQ recipe name (e.g. "
f"'general/ptq/nvfp4_default-fp8_kv'), or an absolute path to a "
f"YAML quantization recipe."
) from e
return _normalize_mtq_cfg(loaded)