Skip to content

Commit e6b9cd6

Browse files
committed
Address some comments
Signed-off-by: Jingyu Xin <jingyux@nvidia.com>
1 parent 0024404 commit e6b9cd6

3 files changed

Lines changed: 43 additions & 33 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Changelog
1515
- Enable PTQ workflow for the Step3.5-Flash MoE model with NVFP4 W4A4 + FP8 KV cache quantization. See `modelopt_recipes/models/Step3.5-Flash/nvfp4-mlp-only.yaml <https://github.com/NVIDIA/Model-Optimizer/blob/main/modelopt_recipes/models/Step3.5-Flash/nvfp4-mlp-only.yaml>`_ for more details.
1616
- Add support for vLLM fakequant reload using ModelOpt state for HF models. See `examples/vllm_serve/README.md <https://github.com/NVIDIA/Model-Optimizer/tree/main/examples/vllm_serve#load-qatptq-model-and-serve-in-vllm-wip>`_ for more details.
1717
- [Early Testing] Add Claude Code PTQ skill (``.claude/skills/ptq/``) for agent-assisted post-training quantization. The skill guides the agent through environment detection, model support checking, format selection, and execution via the launcher or manual SLURM/Docker/bare GPU paths. Includes handling for unlisted models with custom module patching. This feature is in early testing — use with caution.
18+
- Add implicit GEMM CUDA kernel for Conv3D with fused NVFP4 fake quantization (``modelopt.torch.kernels.conv``). When NVFP4 quantization is applied to an ``nn.Conv3d`` layer via ModelOpt PTQ, the implicit GEMM path is used automatically instead of cuDNN. Uses BF16 WMMA tensor cores (SM80+) with FP32 accumulation and in-kernel FP4 (E2M1) activation quantization. Grouped convolution (``groups > 1``) falls back to the default cuDNN path. Inference only — training mode falls back to cuDNN with a warning.
1819

1920
**Backward Breaking Changes**
2021

examples/diffusers/quantization/quantize.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -116,41 +116,48 @@ def get_quant_config(self, n_steps: int, backbone: torch.nn.Module) -> Any:
116116

117117
if self.config.format == QuantFormat.INT8:
118118
if self.config.algo == QuantAlgo.SMOOTHQUANT:
119-
quant_config = mtq.INT8_SMOOTHQUANT_CFG
119+
base_cfg = mtq.INT8_SMOOTHQUANT_CFG
120120
else:
121-
quant_config = INT8_DEFAULT_CONFIG
121+
base_cfg = INT8_DEFAULT_CONFIG
122122
if self.config.collect_method != CollectMethod.DEFAULT:
123123
reset_set_int8_config(
124-
quant_config,
124+
base_cfg,
125125
self.config.percentile,
126126
n_steps,
127127
collect_method=self.config.collect_method.value,
128128
backbone=backbone,
129129
)
130130
elif self.config.format == QuantFormat.FP8:
131-
quant_config = FP8_DEFAULT_CONFIG
131+
base_cfg = FP8_DEFAULT_CONFIG
132132
elif self.config.format == QuantFormat.FP4:
133133
if self.model_config.model_type.value.startswith("flux"):
134-
quant_config = NVFP4_FP8_MHA_CONFIG
134+
base_cfg = NVFP4_FP8_MHA_CONFIG
135135
else:
136-
quant_config = NVFP4_DEFAULT_CONFIG
137-
# Override block size if non-default
138-
if self.config.block_size != 16:
139-
import copy
140-
141-
quant_config = copy.deepcopy(quant_config)
142-
for entry in quant_config["quant_cfg"]:
143-
if isinstance(entry, dict) and "block_sizes" in entry.get("cfg", {}):
144-
entry["cfg"]["block_sizes"][-1] = self.config.block_size
136+
base_cfg = NVFP4_DEFAULT_CONFIG
145137
else:
146138
raise NotImplementedError(f"Unknown format {self.config.format}")
139+
140+
# Build a fresh config dict so we never mutate the global constants.
141+
quant_cfg_list = list(base_cfg["quant_cfg"])
142+
143+
if self.config.format == QuantFormat.FP4:
144+
for i, entry in enumerate(quant_cfg_list):
145+
if isinstance(entry, dict) and "block_sizes" in entry.get("cfg", {}):
146+
new_block_sizes = {**entry["cfg"]["block_sizes"], -1: self.config.block_size}
147+
quant_cfg_list[i] = {
148+
**entry,
149+
"cfg": {**entry["cfg"], "block_sizes": new_block_sizes},
150+
}
151+
147152
if self.config.quantize_mha:
148-
quant_config["quant_cfg"].append(
153+
quant_cfg_list.append(
149154
{
150155
"quantizer_name": "*[qkv]_bmm_quantizer",
151156
"cfg": {"num_bits": (4, 3), "axis": None},
152157
}
153158
)
159+
160+
quant_config = {**base_cfg, "quant_cfg": quant_cfg_list}
154161
set_quant_config_attr(
155162
quant_config,
156163
self.model_config.trt_high_precision_dtype.value,
@@ -617,8 +624,8 @@ def main() -> None:
617624
logger.info(f"Quantizing backbone: {backbone_name}")
618625
backbone_quant_config = quantizer.get_quant_config(calib_config.n_steps, backbone)
619626

620-
# Full pipeline inference for calibration — cached modules
621-
# (transformer, video_decoder) are exercised during __call__
627+
# Calibration runs the full pipeline (not just `mod`), so the
628+
# closure intentionally ignores the backbone argument.
622629
def forward_loop(mod):
623630
calibrator.run_calibration(batched_prompts)
624631

modelopt/torch/quantization/nn/modules/quant_conv.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
"""Quantized convolution."""
1717

18+
import warnings
19+
1820
import torch.nn as nn
1921

2022
from ... import tensor_quant
@@ -90,34 +92,23 @@ class _QuantConv3d(QuantLinearConvBase):
9092

9193
def _should_use_implicit_gemm(self):
9294
"""Check if both quantizers are NVFP4 and the implicit GEMM kernel is available."""
93-
if hasattr(self, "_use_implicit_gemm"):
94-
return self._use_implicit_gemm
95-
result = False
96-
if (
95+
return (
9796
hasattr(self, "input_quantizer")
9897
and hasattr(self, "weight_quantizer")
9998
and _is_nvfp4_quantizer(self.input_quantizer)
10099
and _is_nvfp4_quantizer(self.weight_quantizer)
101100
and self.groups == 1
102-
):
103-
try:
104-
from modelopt.torch.kernels.conv.implicit_gemm_cuda import (
105-
conv3d_implicit_gemm_cuda, # noqa: F401
106-
)
107-
108-
result = True
109-
except ImportError:
110-
pass
111-
self._use_implicit_gemm = result
112-
return result
101+
)
113102

114103
def _implicit_gemm_forward(self, input):
115104
"""Run NVFP4 implicit GEMM kernel. Input may already be padded."""
116105
from modelopt.torch.kernels.conv.implicit_gemm_cuda import conv3d_implicit_gemm_cuda
117106

107+
# _get_amax is an internal TensorQuantizer method with no public equivalent;
108+
# block_sizes is a public property.
118109
act_amax = self.input_quantizer._get_amax(input)
119110
weight = _nvfp4_quantize_weight_along_k(self.weight, self.weight_quantizer)
120-
fp4_block_size = self.input_quantizer._block_sizes.get(-1, 16)
111+
fp4_block_size = self.input_quantizer.block_sizes.get(-1, 16)
121112

122113
output = conv3d_implicit_gemm_cuda(
123114
input,
@@ -137,7 +128,18 @@ def forward(self, input, *args, **kwargs):
137128
if not self._should_use_implicit_gemm():
138129
return super().forward(input, *args, **kwargs)
139130

131+
if self.training:
132+
warnings.warn(
133+
"Implicit GEMM Conv3D kernel is inference-only and does not support training. "
134+
"Falling back to the default cuDNN quantization path, which could produce "
135+
"different numerics.",
136+
stacklevel=2,
137+
)
138+
return super().forward(input, *args, **kwargs)
139+
140140
# During calibration, only collect amax — use the faster cuDNN path.
141+
# _if_calib/_if_quant are internal TensorQuantizer state with no public property;
142+
# toggled via enable_calib()/disable_calib()/enable_quant()/disable_quant().
141143
if self.input_quantizer._if_calib and not self.input_quantizer._if_quant:
142144
return super().forward(input, *args, **kwargs)
143145

0 commit comments

Comments
 (0)