# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from typing import Annotated, Any from pydantic import Field, GetPydanticSchema, ValidationInfo, field_validator from pydantic_core import core_schema from vllm.config.utils import config from vllm.model_executor.layers.quantization.utils.quant_utils import ( QuantKey, kFp8Dynamic128Sym, kFp8DynamicTensorSym, kFp8DynamicTokenSym, kFp8Static128BlockSym, kFp8StaticChannelSym, kFp8StaticTensorSym, kInt8StaticChannelSym, kMxfp4Dynamic, kMxfp8Dynamic, ) # User-facing names addressable from quantization_config. QUANT_KEY_NAMES: dict[str, QuantKey] = { "fp8_per_tensor_static": kFp8StaticTensorSym, "fp8_per_tensor_dynamic": kFp8DynamicTensorSym, "fp8_per_token": kFp8DynamicTokenSym, "fp8_per_channel_static": kFp8StaticChannelSym, "fp8_per_block_static": kFp8Static128BlockSym, "fp8_per_block_dynamic": kFp8Dynamic128Sym, "mxfp8": kMxfp8Dynamic, "mxfp4": kMxfp4Dynamic, "int8_per_channel_static": kInt8StaticChannelSym, } def _coerce_quant_key(v: Any) -> QuantKey | None: if v is None or isinstance(v, QuantKey): return v if not isinstance(v, str): raise TypeError(f"expected str or QuantKey, got {type(v).__name__}") try: return QUANT_KEY_NAMES[v] except KeyError: raise ValueError( f"unknown quantization name {v!r}; " f"expected one of {sorted(QUANT_KEY_NAMES)}" ) from None # Stop pydantic from introspecting QuantKey: it transitively contains a # NamedTuple with `ClassVar[GroupShape]` declarations that pydantic refuses. QuantKeyField = Annotated[ QuantKey | None, GetPydanticSchema( lambda _src, _handler: core_schema.no_info_plain_validator_function( _coerce_quant_key ) ), ] @config class QuantSpec: """Quantization spec for one layer kind (linear or MoE). `None` on either side means the method class falls back to its own default (typically inherited from the checkpoint, or unquantized for online). """ weight: QuantKeyField = None """Weight quantization key, or a name from QUANT_KEY_NAMES.""" activation: QuantKeyField = None """Activation quantization key, or a name from QUANT_KEY_NAMES.""" @config class QuantizationConfigArgs: """User-facing quantization configuration. See `docs/features/quantization/online.md` for the schema and shorthand string forms accepted on `linear` and `moe`. """ linear: QuantSpec | None = None """Spec applied to ``LinearBase`` layers.""" moe: QuantSpec | None = None """Spec applied to ``FusedMoE`` layers.""" ignore: list[str] = Field(default_factory=list) """Layers to skip quantization for.""" @field_validator("linear", "moe", mode="before") @classmethod def _coerce_spec(cls, v: Any, info: ValidationInfo) -> Any: if not isinstance(v, str): return v field_name = info.field_name assert field_name is not None if v in _ONLINE_SHORTHANDS: spec = getattr(_ONLINE_SHORTHANDS[v], field_name) if spec is None: raise ValueError( f"online shorthand {v!r} does not define a {field_name} spec" ) return spec return QuantSpec(weight=_coerce_quant_key(v)) # CLI shorthands accepted by `--quantization`. Each desugars to a full # QuantizationConfigArgs; activation overrides go through quantization_config. _ONLINE_SHORTHANDS: dict[str, QuantizationConfigArgs] = { "fp8_per_tensor": QuantizationConfigArgs( linear=QuantSpec(weight=kFp8StaticTensorSym), moe=QuantSpec(weight=kFp8StaticTensorSym), ), "fp8_per_block": QuantizationConfigArgs( linear=QuantSpec(weight=kFp8Static128BlockSym), moe=QuantSpec(weight=kFp8Static128BlockSym), ), # Per-output-channel weight scale + dynamic per-token activation. # Same shape as llmcompressor's FP8_DYNAMIC recipe. "fp8_per_channel": QuantizationConfigArgs( linear=QuantSpec(weight=kFp8StaticChannelSym), moe=QuantSpec(weight=kFp8StaticChannelSym), ), "mxfp8": QuantizationConfigArgs( linear=QuantSpec(weight=kMxfp8Dynamic), moe=QuantSpec(weight=kMxfp8Dynamic), ), # INT8 weight-only on MoE; linear stays unquantized (no `linear` field). "int8_per_channel_weight_only": QuantizationConfigArgs( moe=QuantSpec(weight=kInt8StaticChannelSym), ), } # Names accepted by `--quantization`; "online" means "use quantization_config". ONLINE_QUANT_SHORTHAND_NAMES: tuple[str, ...] = ( *_ONLINE_SHORTHANDS.keys(), "online", ) def resolve_quantization_config( quantization: str | None, quantization_config: dict[str, Any] | QuantizationConfigArgs | None, ) -> QuantizationConfigArgs | None: """Resolve `--quantization` shorthand and `--quantization-config` into a QuantizationConfigArgs. `quantization` is a CLI shorthand that desugars into a base config via `_ONLINE_SHORTHANDS`. `quantization_config` is a dict or pre-built args object. When both are given, fields explicitly set in `quantization_config` take precedence over the shorthand. """ if quantization is not None and quantization not in ONLINE_QUANT_SHORTHAND_NAMES: if quantization_config is not None: raise ValueError( f"quantization_config is only supported when quantization is " f"one of {sorted(ONLINE_QUANT_SHORTHAND_NAMES)}, " f"got quantization={quantization!r}" ) return None base = _ONLINE_SHORTHANDS.get(quantization) if quantization else None if quantization_config is None: return base if isinstance(quantization_config, dict): quantization_config = QuantizationConfigArgs(**quantization_config) if base is None: return quantization_config return QuantizationConfigArgs( linear=quantization_config.linear or base.linear, moe=quantization_config.moe or base.moe, ignore=quantization_config.ignore or base.ignore, )