A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 7, 16:30 UTC
add mixed precision support for modelopt (#35047)
Signed-off-by: Shiyang Chen <shiychen@nvidia.com>
sychen52 committed
Feb 26, 2026 at 13:56 UTC
d0105b84f00fadc18d9e7859d3e76887b7f5772c
4 files changed
+250
-83
vllm/config/model.py
+1
@@ -883,6 +883,7 @@ class ModelConfig:
883
"modelopt",
884
"modelopt_fp4",
885
"modelopt_mxfp8",
886
+ "modelopt_mixed",
887
"petit_nvfp4",
888
# Ensure heavy backends are probed last to avoid unnecessary
889
# imports during override detection (e.g., MXFP4 imports Triton)
vllm/model_executor/layers/quantization/__init__.py
+8
-1
@@ -18,6 +18,7 @@ QuantizationMethods = Literal[
18
"modelopt",
19
"modelopt_fp4",
20
"modelopt_mxfp8",
21
+ "modelopt_mixed",
22
"gguf",
23
"gptq_marlin",
24
"awq_marlin",
@@ -120,7 +121,12 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
121
from .gptq import GPTQConfig
122
from .gptq_marlin import GPTQMarlinConfig
123
from .inc import INCConfig
123
- from .modelopt import ModelOptFp8Config, ModelOptMxFp8Config, ModelOptNvFp4Config
124
+ from .modelopt import (
125
+ ModelOptFp8Config,
126
+ ModelOptMixedPrecisionConfig,
127
+ ModelOptMxFp8Config,
128
+ ModelOptNvFp4Config,
129
+ )
130
from .moe_wna16 import MoeWNA16Config
131
from .mxfp4 import Mxfp4Config
132
from .petit import PetitNvFp4Config
@@ -135,6 +141,7 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
141
"modelopt": ModelOptFp8Config,
142
"modelopt_fp4": ModelOptNvFp4Config,
143
"modelopt_mxfp8": ModelOptMxFp8Config,
144
+ "modelopt_mixed": ModelOptMixedPrecisionConfig,
145
"gguf": GGUFConfig,
146
"gptq_marlin": GPTQMarlinConfig,
147
"awq_marlin": AWQMarlinConfig,
vllm/model_executor/layers/quantization/modelopt.py
+228
-79
@@ -114,6 +114,8 @@ QUANT_ALGOS = [
114
"NVFP4",
115
# MXFP8
116
"MXFP8",
117
+ # MIXED_PRECISION,
118
+ "MIXED_PRECISION",
119
]
120
KV_CACHE_QUANT_ALGOS = ["FP8"]
121
@@ -235,6 +237,26 @@ class ModelOptQuantConfigBase(QuantizationConfig):
237
238
self.exclude_modules = hf_to_vllm_mapper.apply_list(new_exclude_modules)
239
240
+ @staticmethod
241
+ def _extract_modelopt_quant_algo(
242
+ hf_quant_cfg: dict[str, Any] | None,
243
+ ) -> str | None:
244
+ """Extract upper-cased quant_algo from a modelopt config.
245
+
246
+ Returns the quant_algo string (upper-cased), or None if the config
247
+ is not a modelopt config.
248
+ """
249
+ if hf_quant_cfg is None:
250
+ return None
251
+ if hf_quant_cfg.get("quant_method", "").lower() != "modelopt":
252
+ return None
253
+ if "quantization" in hf_quant_cfg:
254
+ quant_config = hf_quant_cfg["quantization"]
255
+ if isinstance(quant_config, dict):
256
+ return str(quant_config.get("quant_algo", "")).upper()
257
+ return None
258
+ return str(hf_quant_cfg.get("quant_algo", "")).upper()
259
+
260
@staticmethod
261
def get_config_filenames() -> list[str]:
262
return ["hf_quant_config.json"]
@@ -272,10 +294,20 @@ class ModelOptQuantConfigBase(QuantizationConfig):
294
# "exclude_modules" is the key in the legacy hf_quant_config.json
295
exclude_modules = quant_config.get("exclude_modules", [])
296
else:
275
- # Compressed-tensors style format:
297
+ # Compressed-tensors style format (config.json quantization_config):
298
# {"quant_algo": "...", "quant_method": "modelopt"}
299
quant_method = config.get("quant_algo")
278
- kv_cache_quant_method = config.get("kv_cache_quant_algo")
300
+
301
+ # "kv_cache_scheme" (a dict) instead of "kv_cache_quant_algo" (a string).
302
+ kv_cache_scheme = config.get("kv_cache_scheme")
303
+ if isinstance(kv_cache_scheme, dict) and (
304
+ kv_cache_scheme.get("type") == "float"
305
+ and kv_cache_scheme.get("num_bits") == 8
306
+ ):
307
+ kv_cache_quant_method = "FP8"
308
+ else:
309
+ kv_cache_quant_method = None
310
+
311
# "ignore" is the key in config.json
312
exclude_modules = config.get("ignore", [])
313
group_size_raw = config.get("group_size")
@@ -379,32 +411,9 @@ class ModelOptFp8Config(ModelOptQuantConfigBase):
411
def override_quantization_method(
412
cls, hf_quant_cfg, user_quant
413
) -> QuantizationMethods | None:
382
- """Detect if this ModelOpt config should be used based on
383
- quantization config."""
384
-
385
- if hf_quant_cfg is None:
386
- return None
387
-
388
- # Use the community standard 'quant_method'
389
- quant_method = hf_quant_cfg.get("quant_method", "").lower()
390
-
391
- # Only proceed if the method is explicitly "modelopt"
392
- if quant_method != "modelopt":
393
- return None
394
-
395
- # Look for ModelOpt-specific config structure
396
- if "quantization" in hf_quant_cfg:
397
- quant_config = hf_quant_cfg["quantization"]
398
- if isinstance(quant_config, dict):
399
- quant_algo = str(quant_config.get("quant_algo", ""))
400
- if quant_algo.upper() == "FP8":
401
- return "modelopt"
402
- else:
403
- # Check for compressed-tensors style config with specific quant_algo
404
- quant_algo = str(hf_quant_cfg.get("quant_algo", ""))
405
- if quant_algo.upper() == "FP8":
406
- return "modelopt"
407
-
414
+ algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
415
+ if algo is not None and algo == "FP8":
416
+ return "modelopt"
417
return None
418
419
@classmethod
@@ -1031,32 +1040,9 @@ class ModelOptNvFp4Config(ModelOptQuantConfigBase):
1040
def override_quantization_method(
1041
cls, hf_quant_cfg, user_quant
1042
) -> QuantizationMethods | None:
1034
- """Detect if this ModelOpt FP4 config should be used based on
1035
- quantization config."""
1036
- if hf_quant_cfg is None:
1037
- return None
1038
-
1039
- # Use the community standard 'quant_method'
1040
- quant_method = hf_quant_cfg.get("quant_method", "").lower()
1041
-
1042
- # Only proceed if the method is explicitly "modelopt"
1043
- if quant_method != "modelopt":
1044
- return None
1045
-
1046
- # Look for ModelOpt-specific config structure
1047
- if "quantization" in hf_quant_cfg:
1048
- quant_config = hf_quant_cfg["quantization"]
1049
- if isinstance(quant_config, dict):
1050
- quant_algo = quant_config.get("quant_algo", "")
1051
- if "NVFP4" in quant_algo:
1052
- return "modelopt_fp4"
1053
- else:
1054
- # Check for compressed-tensors style config with specific
1055
- # quant_algo field
1056
- quant_algo = hf_quant_cfg.get("quant_algo", "")
1057
- if isinstance(quant_algo, str) and "FP4" in quant_algo.upper():
1058
- return "modelopt_fp4"
1059
-
1043
+ algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
1044
+ if algo is not None and ("NVFP4" in algo or "FP4" in algo):
1045
+ return "modelopt_fp4"
1046
return None
1047
1048
@classmethod
@@ -1619,31 +1605,9 @@ class ModelOptMxFp8Config(ModelOptQuantConfigBase):
1605
def override_quantization_method(
1606
cls, hf_quant_cfg, user_quant
1607
) -> QuantizationMethods | None:
1622
- """Detect if this ModelOpt MXFP8 config should be used based on
1623
- quantization config."""
1624
- if hf_quant_cfg is None:
1625
- return None
1626
-
1627
- # Use the community standard 'quant_method'
1628
- quant_method = hf_quant_cfg.get("quant_method", "").lower()
1629
-
1630
- # Only proceed if the method is explicitly "modelopt"
1631
- if quant_method != "modelopt":
1632
- return None
1633
-
1634
- # Look for ModelOpt-specific config structure
1635
- if "quantization" in hf_quant_cfg:
1636
- quant_config = hf_quant_cfg["quantization"]
1637
- if isinstance(quant_config, dict):
1638
- quant_algo = str(quant_config.get("quant_algo", "")).upper()
1639
- if "MXFP8" in quant_algo:
1640
- return "modelopt_mxfp8"
1641
- else:
1642
- # Check for compressed-tensors style config with specific quant_algo
1643
- quant_algo = str(hf_quant_cfg.get("quant_algo", "")).upper()
1644
- if "MXFP8" in quant_algo:
1645
- return "modelopt_mxfp8"
1646
-
1608
+ algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
1609
+ if algo is not None and "MXFP8" in algo:
1610
+ return "modelopt_mxfp8"
1611
return None
1612
1613
@classmethod
@@ -1841,3 +1805,188 @@ class ModelOptMxFp8LinearMethod(LinearMethodBase):
1805
# Register the method classes for ModelOptMxFp8Config
1806
ModelOptMxFp8Config.LinearMethodCls = ModelOptMxFp8LinearMethod
1807
ModelOptMxFp8Config.KVCacheMethodCls = ModelOptFp8KVCacheMethod
1808
+
1809
+
1810
+class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
1811
+ """Config class for ModelOpt MIXED_PRECISION.
1812
+
1813
+ Supports checkpoints where different layers use different quantization
1814
+ algorithms (e.g., FP8 for dense layers and NVFP4 for MoE experts).
1815
+ The per-layer algorithm is specified in the ``quantized_layers`` dict
1816
+ inside ``config.json``'s ``quantization_config`` (preferred) or the
1817
+ legacy ``hf_quant_config.json``.
1818
+ """
1819
+
1820
+ def __init__(
1821
+ self,
1822
+ kv_cache_quant_method: str | None,
1823
+ exclude_modules: list[str],
1824
+ quantized_layers: dict[str, dict[str, Any]],
1825
+ fp8_config: ModelOptFp8Config,
1826
+ nvfp4_config: ModelOptNvFp4Config,
1827
+ ) -> None:
1828
+ super().__init__(exclude_modules)
1829
+ self.kv_cache_quant_method = kv_cache_quant_method
1830
+ self.quantized_layers = quantized_layers
1831
+ self.fp8_config = fp8_config
1832
+ self.nvfp4_config = nvfp4_config
1833
+
1834
+ def get_name(self) -> QuantizationMethods:
1835
+ return "modelopt_mixed"
1836
+
1837
+ def get_supported_act_dtypes(self) -> list[torch.dtype]:
1838
+ return [torch.bfloat16, torch.half]
1839
+
1840
+ @classmethod
1841
+ def get_min_capability(cls) -> int:
1842
+ return 89
1843
+
1844
+ @classmethod
1845
+ def override_quantization_method(
1846
+ cls, hf_quant_cfg, user_quant
1847
+ ) -> QuantizationMethods | None:
1848
+ algo = cls._extract_modelopt_quant_algo(hf_quant_cfg)
1849
+ if algo is not None and algo == "MIXED_PRECISION":
1850
+ return "modelopt_mixed"
1851
+ return None
1852
+
1853
+ @classmethod
1854
+ def _from_config(
1855
+ cls,
1856
+ *,
1857
+ quant_method: str,
1858
+ kv_cache_quant_method: str | None,
1859
+ exclude_modules: list[str],
1860
+ original_config: dict[str, Any],
1861
+ group_size: int | None,
1862
+ **kwargs: Any,
1863
+ ) -> "ModelOptMixedPrecisionConfig":
1864
+ if "quantization" in original_config:
1865
+ quantized_layers = original_config["quantization"].get(
1866
+ "quantized_layers", {}
1867
+ )
1868
+ else:
1869
+ quantized_layers = original_config.get("quantized_layers", {})
1870
+
1871
+ if not quantized_layers:
1872
+ raise ValueError(
1873
+ "MIXED_PRECISION quant_algo requires a non-empty "
1874
+ "'quantized_layers' mapping in the quantization config."
1875
+ )
1876
+
1877
+ # Determine group_size from the first NVFP4 entry if not provided.
1878
+ if group_size is None:
1879
+ for layer_info in quantized_layers.values():
1880
+ if layer_info.get("quant_algo", "").upper() == "NVFP4":
1881
+ group_size = layer_info.get("group_size", 16)
1882
+ break
1883
+ if group_size is None:
1884
+ group_size = 16
1885
+
1886
+ fp8_config = ModelOptFp8Config(
1887
+ quant_method="FP8",
1888
+ is_checkpoint_fp8_serialized=True,
1889
+ kv_cache_quant_method=kv_cache_quant_method,
1890
+ exclude_modules=[],
1891
+ )
1892
+ nvfp4_config = ModelOptNvFp4Config(
1893
+ is_checkpoint_nvfp4_serialized=True,
1894
+ kv_cache_quant_algo=kv_cache_quant_method,
1895
+ exclude_modules=[],
1896
+ group_size=group_size,
1897
+ )
1898
+
1899
+ return cls(
1900
+ kv_cache_quant_method=kv_cache_quant_method,
1901
+ exclude_modules=exclude_modules,
1902
+ quantized_layers=quantized_layers,
1903
+ fp8_config=fp8_config,
1904
+ nvfp4_config=nvfp4_config,
1905
+ )
1906
+
1907
+ def _resolve_quant_algo(self, prefix: str) -> str | None:
1908
+ """Look up the quant_algo for a vLLM-side layer prefix.
1909
+
1910
+ Tries three strategies in order:
1911
+ 1. Direct lookup in ``quantized_layers``.
1912
+ 2. Packed/fused-layer lookup (unfuse via ``packed_modules_mapping``).
1913
+ 3. Prefix-based lookup for FusedMoE (any child key starts with
1914
+ ``prefix + "."``).
1915
+
1916
+ Returns the upper-cased quant_algo string, or *None* if the prefix
1917
+ is not found.
1918
+ """
1919
+ # 1. Direct lookup
1920
+ if prefix in self.quantized_layers:
1921
+ return self.quantized_layers[prefix]["quant_algo"].upper()
1922
+
1923
+ # 2. Packed / fused layer lookup
1924
+ proj_name = prefix.rsplit(".", 1)[-1]
1925
+ if self.packed_modules_mapping and proj_name in self.packed_modules_mapping:
1926
+ algos: set[str] = set()
1927
+ base = prefix.rsplit(".", 1)[0]
1928
+ for shard_name in self.packed_modules_mapping[proj_name]:
1929
+ shard_prefix = f"{base}.{shard_name}"
1930
+ if shard_prefix in self.quantized_layers:
1931
+ algos.add(self.quantized_layers[shard_prefix]["quant_algo"].upper())
1932
+ if len(algos) == 1:
1933
+ return algos.pop()
1934
+ if len(algos) > 1:
1935
+ raise ValueError(
1936
+ f"Mixed quant_algo within fused layer {prefix}: "
1937
+ f"{algos}. All shards must use the same quantization."
1938
+ )
1939
+
1940
+ # 3. Prefix-based lookup (for FusedMoE / parent modules)
1941
+ prefix_dot = prefix + "."
1942
+ for key, info in self.quantized_layers.items():
1943
+ if key.startswith(prefix_dot):
1944
+ return info["quant_algo"].upper()
1945
+
1946
+ return None
1947
+
1948
+ def get_quant_method(
1949
+ self, layer: torch.nn.Module, prefix: str
1950
+ ) -> "QuantizeMethodBase | None":
1951
+ """Return quantize-method based on layer."""
1952
+ # KV-cache quantization
1953
+ if isinstance(layer, Attention):
1954
+ if self.kv_cache_quant_method:
1955
+ return ModelOptFp8KVCacheMethod(self)
1956
+ return None
1957
+
1958
+ # Excluded layers
1959
+ if self.is_layer_excluded(prefix):
1960
+ if isinstance(layer, LinearBase):
1961
+ return UnquantizedLinearMethod()
1962
+ return None
1963
+
1964
+ quant_algo = self._resolve_quant_algo(prefix)
1965
+
1966
+ if isinstance(layer, LinearBase):
1967
+ if quant_algo == "FP8":
1968
+ return ModelOptFp8LinearMethod(self.fp8_config)
1969
+ if quant_algo == "NVFP4":
1970
+ return ModelOptNvFp4LinearMethod(self.nvfp4_config)
1971
+ # Layer not in quantized_layers — leave unquantized
1972
+ return UnquantizedLinearMethod()
1973
+
1974
+ if isinstance(layer, FusedMoE):
1975
+ if quant_algo == "FP8":
1976
+ return ModelOptFp8MoEMethod(
1977
+ quant_config=self.fp8_config,
1978
+ moe_config=layer.moe_config,
1979
+ )
1980
+ if quant_algo == "NVFP4":
1981
+ return ModelOptNvFp4FusedMoE(
1982
+ quant_config=self.nvfp4_config,
1983
+ moe_config=layer.moe_config,
1984
+ )
1985
+ return None
1986
+
1987
+ return None
1988
+
1989
+ def apply_vllm_mapper(self, hf_to_vllm_mapper: "WeightsMapper"):
1990
+ super().apply_vllm_mapper(hf_to_vllm_mapper)
1991
+ if self.quantized_layers:
1992
+ self.quantized_layers = hf_to_vllm_mapper.apply_dict(self.quantized_layers)
vllm/model_executor/model_loader/weight_utils.py
+13
-3
@@ -287,7 +287,17 @@ def get_quant_config(
287
)
288
289
if hf_quant_config is not None:
290
- return quant_cls.from_config(hf_quant_config)
290
+ # For modelopt_mixed, config.json's quantization_config may or may
291
+ # not contain the per-layer quantized_layers map. Newer checkpoints
292
+ # embed it directly; older ones keep it only in hf_quant_config.json.
293
+ # If it is missing, fall through to the file-based loading path.
294
+ if (
295
+ model_config.quantization == "modelopt_mixed"
296
+ and "quantized_layers" not in hf_quant_config
297
+ ):
298
+ pass # fall through to file-based loading below
299
+ else:
300
+ return quant_cls.from_config(hf_quant_config)
301
302
# if hf_quant_config is None, we will try to get config from
303
# hf_overrides
@@ -365,8 +375,8 @@ def get_quant_config(
375
376
if model_config.quantization == "bitsandbytes":
377
config["adapter_name_or_path"] = model_config.model
368
- elif model_config.quantization == "modelopt":
369
- if config["producer"]["name"] == "modelopt":
378
+ elif model_config.quantization in ("modelopt", "modelopt_mixed"):
379
+ if config.get("producer", {}).get("name") == "modelopt":
380
return quant_cls.from_config(config)
381
else:
382
raise ValueError(