Skip to content

Commit 426fcc5

Browse files
committed
Refactor AutoQuantize cost model handling
Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
1 parent b663485 commit 426fcc5

5 files changed

Lines changed: 259 additions & 215 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Changelog
3232
- Add ``--cast_mxfp4_to_nvfp4`` flag to ``examples/llm_ptq/hf_ptq.py`` for closed-form, bit-exact MXFP4 → NVFP4 weight conversion. Supports the GPT-OSS family (``openai/gpt-oss-20b``, ``openai/gpt-oss-120b``). See `examples/llm_ptq/README.md <https://github.com/NVIDIA/Model-Optimizer/tree/main/examples/llm_ptq#mxfp4--nvfp4-cast-for-gpt-oss>`__ for usage.
3333
- DeepSeek PTQ (``examples/deepseek/ptq.py``) now defaults to native top-k calibration with post-hoc per-layer peer-max sync of expert ``input_quantizer.amax``; the all-experts path is preserved behind ``--calib_all_experts``.
3434
- Add NVFP4 W4A16 weight-only quantization (``w4a16_nvfp4``): FP4 weights with group_size=16, BF16 activations, no calibration forward pass required. Use ``mtq.W4A16_NVFP4_CFG`` or ``--qformat w4a16_nvfp4`` in ``hf_ptq.py``. vLLM deployment support is in progress.
35+
- Add active-MoE cost accounting for ``mtq.auto_quantize`` effective-bits search. Set ``constraints={"effective_bits": ..., "cost_model": "active_moe", "cost": {"active_moe_expert_ratio": ...}}`` to weight routed MoE expert costs by active experts per token while keeping shared experts fully counted. The ``hf_ptq.py`` AutoQuant path exposes this via ``--auto_quantize_cost_model active_moe`` and ``--auto_quantize_active_moe_expert_ratio``.
3536
- Add ``DATASET_COMBOS`` to ``modelopt.torch.utils.dataset_utils`` — single ``--dataset`` tokens that fan out to multiple registered datasets; per-entry ``num_samples`` is split evenly across the members. Initial combos: ``cnn_nemotron_v2_mix`` (``cnn_dailymail`` + ``nemotron-post-training-dataset-v2``, used by ``hf_ptq.py`` when no ``--dataset`` is provided) and ``nemotron-post-training-v3`` (the seven ``nvidia/Nemotron-*`` SFT datasets added in #1498, mirroring the `nemotron-post-training-v3 collection <https://huggingface.co/collections/nvidia/nemotron-post-training-v3>`_). Combo names are listed by ``get_supported_datasets()`` and surfaced in ``--dataset`` help. ``get_dataset_dataloader`` rejects inputs that mix a combo with one of its member datasets (e.g. ``cnn_dailymail,cnn_nemotron_v2_mix``) to avoid double-sampling, and ``get_dataset_samples`` rejects combo names so callers route through the dataloader. ``hf_ptq.py`` default ``--calib_size`` is bumped from ``512`` to ``1024`` so the total calibration sample count under the new default combo matches the previous two-dataset fallback.
3637
- The ``nemotron-sft-agentic-v2`` registered dataset (added in #1498) now uses only the ``search`` split. The previously configured ``interactive_agent`` and ``tool_calling`` splits contain content-level defects (heterogeneous schema and a malformed JSON row, respectively) that cause pyarrow's streaming JSON reader to fail deterministically.
3738
- Add shared Megatron-Core calibration forward loop: ``modelopt.torch.utils.plugins.megatron_calibration.get_megatron_calibration_forward_loop`` produces the ``forward_loop`` callable expected by ``mtq.quantize`` / ``mtp.prune``. Replaces the bespoke calibration loops in Megatron-LM and Megatron-Bridge for quantization and pruning with a single canonical implementation.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Cost models for AutoQuantize effective-bits accounting."""
17+
18+
from collections.abc import Callable, Iterable, Sequence
19+
from typing import Any, Final
20+
21+
import regex as re
22+
import torch.nn as nn
23+
24+
# Default target used by historical AutoQuantize calls when no explicit effective-bits
25+
# constraint is supplied. The value is intentionally kept for backward compatibility.
26+
DEFAULT_AUTO_QUANTIZE_EFFECTIVE_BITS: Final = 4.8
27+
28+
AUTO_QUANTIZE_CONSTRAINT_KEYS: Final = frozenset({"effective_bits", "cost_model", "cost"})
29+
ACTIVE_MOE_EXPERT_RATIO_KEY: Final = "active_moe_expert_ratio"
30+
COST_MODEL_WEIGHT: Final = "weight"
31+
COST_MODEL_ACTIVE_MOE: Final = "active_moe"
32+
33+
_ROUTED_MOE_EXPERT_NAME_RE = re.compile(r"(^|\.)experts(\.|$)")
34+
_ACTIVE_MOE_TOP_K_ATTRS = (
35+
"num_experts_per_tok",
36+
"num_experts_per_token",
37+
"moe_top_k",
38+
"top_k",
39+
"num_selected_experts",
40+
)
41+
_ACTIVE_MOE_NUM_EXPERTS_ATTRS = (
42+
"num_experts",
43+
"num_local_experts",
44+
"n_routed_experts",
45+
"moe_num_experts",
46+
"num_routed_experts",
47+
)
48+
49+
50+
def _iter_model_configs(model: nn.Module):
51+
seen = set()
52+
for obj in (model, getattr(model, "model", None), getattr(model, "language_model", None)):
53+
config = getattr(obj, "config", None)
54+
if config is None or id(config) in seen:
55+
continue
56+
seen.add(id(config))
57+
yield config
58+
for nested_attr in ("text_config", "language_config"):
59+
nested_config = getattr(config, nested_attr, None)
60+
if nested_config is None or id(nested_config) in seen:
61+
continue
62+
seen.add(id(nested_config))
63+
yield nested_config
64+
65+
66+
def _get_first_numeric_config_attr(config: Any, attr_names: tuple[str, ...]) -> float | None:
67+
for attr_name in attr_names:
68+
value = getattr(config, attr_name, None)
69+
if isinstance(value, (int, float)) and not isinstance(value, bool):
70+
return float(value)
71+
return None
72+
73+
74+
def infer_active_moe_expert_ratio(model: nn.Module) -> float | None:
75+
"""Infer top-k / num-experts from a single model config object when possible."""
76+
for config in _iter_model_configs(model):
77+
num_active_experts = _get_first_numeric_config_attr(config, _ACTIVE_MOE_TOP_K_ATTRS)
78+
num_experts = _get_first_numeric_config_attr(config, _ACTIVE_MOE_NUM_EXPERTS_ATTRS)
79+
if num_active_experts is None or num_experts is None or num_experts <= 0:
80+
continue
81+
ratio = num_active_experts / num_experts
82+
if ratio <= 0.0:
83+
continue
84+
return min(ratio, 1.0)
85+
return None
86+
87+
88+
def is_routed_moe_module_name(name: str) -> bool:
89+
"""Return True for routed MoE expert modules, excluding shared experts."""
90+
return "shared_expert" not in name and _ROUTED_MOE_EXPERT_NAME_RE.search(name) is not None
91+
92+
93+
class AutoQuantizeCostModel:
94+
"""Base class for AutoQuantize effective-bits cost accounting."""
95+
96+
name: str
97+
supported_cost_keys: frozenset[str] = frozenset()
98+
99+
def normalize_cost_constraints(
100+
self, model: nn.Module, cost_constraints: dict[str, Any]
101+
) -> dict[str, Any]:
102+
"""Validate and normalize cost-model-specific constraints."""
103+
unknown_cost_keys = set(cost_constraints) - self.supported_cost_keys
104+
if unknown_cost_keys:
105+
raise ValueError(f"Unsupported auto_quantize cost constraints: {unknown_cost_keys}.")
106+
return cost_constraints
107+
108+
def module_cost_weight(
109+
self, module_names: Sequence[str], cost_constraints: dict[str, Any]
110+
) -> float:
111+
"""Return the cost multiplier for a group of modules."""
112+
return 1.0
113+
114+
def total_weight_size(
115+
self,
116+
named_modules: Iterable[tuple[str, nn.Module]],
117+
is_auto_quantize_module: Callable[[nn.Module], bool],
118+
cost_constraints: dict[str, Any],
119+
) -> float:
120+
"""Return the cost denominator for the effective-bits constraint."""
121+
return sum(
122+
module.weight.numel() * self.module_cost_weight([name], cost_constraints)
123+
for name, module in named_modules
124+
if is_auto_quantize_module(module)
125+
)
126+
127+
128+
class WeightCostModel(AutoQuantizeCostModel):
129+
"""Count all quantizable weights equally."""
130+
131+
name = COST_MODEL_WEIGHT
132+
133+
134+
class ActiveMoECostModel(AutoQuantizeCostModel):
135+
"""Scale routed MoE expert weights by the active experts per-token ratio."""
136+
137+
name = COST_MODEL_ACTIVE_MOE
138+
supported_cost_keys = frozenset({ACTIVE_MOE_EXPERT_RATIO_KEY})
139+
140+
def normalize_cost_constraints(
141+
self, model: nn.Module, cost_constraints: dict[str, Any]
142+
) -> dict[str, Any]:
143+
cost_constraints = super().normalize_cost_constraints(model, cost_constraints)
144+
active_moe_expert_ratio = cost_constraints.get(ACTIVE_MOE_EXPERT_RATIO_KEY)
145+
if active_moe_expert_ratio is None:
146+
active_moe_expert_ratio = infer_active_moe_expert_ratio(model)
147+
if active_moe_expert_ratio is None:
148+
raise ValueError(
149+
"Could not infer active_moe_expert_ratio from model.config. "
150+
"Pass it via constraints['cost']['active_moe_expert_ratio']."
151+
)
152+
153+
if not (
154+
isinstance(active_moe_expert_ratio, (int, float))
155+
and not isinstance(active_moe_expert_ratio, bool)
156+
and 0.0 < active_moe_expert_ratio <= 1.0
157+
):
158+
raise ValueError(
159+
"constraints['cost']['active_moe_expert_ratio'] must be in (0.0, 1.0]."
160+
)
161+
cost_constraints[ACTIVE_MOE_EXPERT_RATIO_KEY] = float(active_moe_expert_ratio)
162+
return cost_constraints
163+
164+
def module_cost_weight(
165+
self, module_names: Sequence[str], cost_constraints: dict[str, Any]
166+
) -> float:
167+
if any(is_routed_moe_module_name(n) for n in module_names):
168+
return cost_constraints[ACTIVE_MOE_EXPERT_RATIO_KEY]
169+
return 1.0
170+
171+
172+
_COST_MODELS: Final = {
173+
COST_MODEL_WEIGHT: WeightCostModel(),
174+
COST_MODEL_ACTIVE_MOE: ActiveMoECostModel(),
175+
}
176+
177+
178+
def get_auto_quantize_cost_model(name: str) -> AutoQuantizeCostModel:
179+
"""Return the registered AutoQuantize cost model."""
180+
try:
181+
return _COST_MODELS[name]
182+
except KeyError as e:
183+
raise ValueError(
184+
f"Invalid constraints['cost_model']: {name}. "
185+
f"Valid options are {tuple(_COST_MODELS)}."
186+
) from e
187+
188+
189+
def normalize_auto_quantize_constraints(
190+
model: nn.Module, constraints: dict[str, Any] | None
191+
) -> dict[str, Any]:
192+
"""Validate and normalize AutoQuantize constraints."""
193+
constraints = (
194+
{"effective_bits": DEFAULT_AUTO_QUANTIZE_EFFECTIVE_BITS}
195+
if constraints is None
196+
else dict(constraints)
197+
)
198+
unexpected_constraint_keys = set(constraints) - AUTO_QUANTIZE_CONSTRAINT_KEYS
199+
if unexpected_constraint_keys:
200+
raise ValueError(
201+
f"Unsupported auto_quantize constraints: {unexpected_constraint_keys}. "
202+
"Supported constraints are 'effective_bits', 'cost_model', and 'cost'."
203+
)
204+
205+
cost_model_name = constraints.get("cost_model", COST_MODEL_WEIGHT)
206+
if not isinstance(cost_model_name, str):
207+
raise ValueError("constraints['cost_model'] must be a string when provided.")
208+
cost_model = get_auto_quantize_cost_model(cost_model_name)
209+
210+
cost_constraints = constraints.get("cost", {})
211+
if cost_constraints is None:
212+
cost_constraints = {}
213+
if not isinstance(cost_constraints, dict):
214+
raise ValueError("constraints['cost'] must be a dict when provided.")
215+
cost_constraints = cost_model.normalize_cost_constraints(model, dict(cost_constraints))
216+
217+
constraints["cost_model"] = cost_model.name
218+
if cost_constraints or cost_model.name == COST_MODEL_ACTIVE_MOE:
219+
constraints["cost"] = cost_constraints
220+
else:
221+
constraints.pop("cost", None)
222+
return constraints

0 commit comments

Comments
 (0)