Files
vllm-omni/vllm_omni/quantization/component_config.py
T
2026-08-15 23:01:26 +08:00

149 lines
5.3 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Per-component quantization routing for multi-stage models.
Routes get_quant_method() to different configs based on longest-prefix match:
{"transformer": fp8_config, "vae": None}
"transformer.blocks.0.attn.to_q" -> fp8_config
"vae.encoder.conv_in" -> None
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import torch
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig,
)
if TYPE_CHECKING:
from vllm.model_executor.layers.quantization.base_config import (
QuantizeMethodBase,
)
# Pre-quantized checkpoints (modelopt FP8/FP4/MXFP8/mixed) only quantize the
# Thinker LM. Vision and audio encoder weights remain in BF16 with no
# corresponding scale tensors in the checkpoint.
PRE_QUANTIZED_METHODS: frozenset[str] = frozenset({"modelopt", "modelopt_fp4", "modelopt_mxfp8", "modelopt_mixed"})
def resolve_component_quant_config(
quant_config: QuantizationConfig | None,
component: str,
) -> QuantizationConfig | None:
"""Resolve one pipeline component from a global or component config.
A plain config is global and therefore applies unchanged to every
quantization-aware component. Only ``ComponentQuantizationConfig`` narrows
the scope through its explicit prefix map.
"""
if isinstance(quant_config, ComponentQuantizationConfig):
return quant_config.resolve(component)
return quant_config
def resolve_encoder_quant_config(
quant_config: QuantizationConfig | None,
) -> QuantizationConfig | None:
"""Resolve quantization config for vision / audio encoders.
Returns *None* for pre-quantized methods so that FP8 kernels are never
applied to BF16 encoder weights (which lack scale tensors). All other
configs — including ``ComponentQuantizationConfig`` and ``None`` — are
returned as-is so the caller can handle them.
"""
if (
quant_config is not None
and not isinstance(quant_config, ComponentQuantizationConfig)
and quant_config.get_name() in PRE_QUANTIZED_METHODS
):
return None
return quant_config
def safe_quant_config(
quant_config: QuantizationConfig | None,
) -> QuantizationConfig | None:
"""Return *quant_config* only if it is safe for norm/modulation layers.
Norm and modulation layers (LayerNorm, RMSNorm, AdaLayerNorm, img_mod,
txt_mod, etc.) produce precision-sensitive shift/scale/gate values and
should not receive FP8 quant configs (see #2728). Pre-quantized methods
like INC/AutoRound W4A16 need the config propagated so packed weights
load correctly.
This is the inverse of :func:`resolve_encoder_quant_config`: that function
strips pre-quantized configs from encoders, while this one strips
*every config except* pre-quantized configs from norm/mod layers.
"""
if quant_config is None:
return None
from vllm.model_executor.layers.quantization.inc import INCConfig
if isinstance(quant_config, INCConfig):
return quant_config
return None
class ComponentQuantizationConfig(QuantizationConfig):
"""Routes quantization to different configs by layer prefix."""
def __init__(
self,
component_configs: dict[str, QuantizationConfig | None],
default_config: QuantizationConfig | None = None,
) -> None:
super().__init__()
self._components = component_configs
self._default = default_config
self._sorted_prefixes = sorted(self._components.keys(), key=len, reverse=True)
def resolve(self, prefix: str) -> QuantizationConfig | None:
"""Find the config for a given layer prefix (longest-prefix match).
Note: vLLM may remap quantization prefixes vs model definition
prefixes (e.g. via WeightsMapper). If prefixes don't match after
remapping, layers may fall through to the default config.
"""
for comp_prefix in self._sorted_prefixes:
if prefix.startswith(comp_prefix):
return self._components[comp_prefix]
return self._default
def get_name(self) -> str:
return "component"
def get_quant_method(self, layer: torch.nn.Module, prefix: str) -> QuantizeMethodBase | None:
config = self.resolve(prefix)
if config is None:
return None
return config.get_quant_method(layer, prefix)
@classmethod
def get_supported_act_dtypes(cls) -> list[torch.dtype]:
return [torch.bfloat16, torch.float16]
def get_min_capability(self) -> int:
"""Return the minimum capability across all component configs."""
caps = [c.get_min_capability() for c in self._components.values() if c is not None]
if self._default is not None:
caps.append(self._default.get_min_capability())
return min(caps) if caps else 0
@classmethod
def from_config(cls, config: dict[str, Any]) -> ComponentQuantizationConfig:
raise NotImplementedError("Use build_quant_config() instead")
def get_config_filenames(self) -> list[str]:
return []
@property
def component_configs(self) -> dict[str, QuantizationConfig | None]:
return self._components
@property
def default_config(self) -> QuantizationConfig | None:
return self._default