Quantization methods as plugins
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
A quantization method is a named QuantizationConfig class; each layer asks the config object for a quantize method that owns its weights.
The idea#
A quantized checkpoint stores weights in fewer bits, with scales (Number formats). The page Quantized checkpoints explains the formats and the memory that they save. This page explains the code path: how vLLM knows the format, and how a layer gets the right weights and kernel.
Each format is a quantization method. In code it is a class pair. The QuantizationConfig holds the settings of one checkpoint, for example the group size of AWQ. The quantize method works on one layer: it makes the layer's weight tensors, and it runs the layer's matrix multiply. The config chooses a quantize method for each layer, so one model can mix methods, for example a quantized MLP and an unquantized lm_head.
vLLM keeps a table from a name, such as "fp8" or "awq", to a config class. A plugin adds a name with the decorator @register_quantization_config. Then --quantization my_quant, or a checkpoint whose config says "quant_method": "my_quant", uses the new class. No file of vLLM changes.
An everyday comparison. Think of a print shop that receives files in many formats. At the front desk, a clerk reads the file header and picks the matching instruction sheet. The sheet says, for each page, which machine and which paper to use. The machine operators only follow the sheet. A new format needs a new sheet, not new machines. The comparison stops here: in vLLM, the front desk (the API server) passes a copy of the filled-in sheet to every worker.
The picture#
Read the panels from top to bottom. Panel 1 runs once, in the API server, while it builds VllmConfig. Panel 2 runs in each worker. Panel 3 is the hot path. The callout is the main rule for plugins.
In the code#
Step 1: the names#
The built-in names are a Literal type, copied into a list that plugins can extend:
QuantizationMethods = Literal[
"awq",
"auto_awq",
"fp8",
"fbgemm_fp8",
"fp_quant",
"modelopt",
"modelopt_fp4",
"modelopt_mxfp8",
"modelopt_mixed",
"auto_gptq",
"gptq",
"gptq_marlin",
"awq_marlin",
"humming",
"compressed-tensors",
"experts_int8",
"quark",
"moe_wna16",
"torchao",
"inc",
"mxfp4",
"gpt_oss_mxfp4",
"deepseek_v4_fp8",
"online",
# Below are online quant shorthand names (see vllm.config.quantization).
# Listed here as strings to avoid a circular import; kept in sync with
# _ONLINE_SHORTHANDS by the assertion in get_quantization_config().
"fp8_per_tensor",
"fp8_per_block",
"fp8_per_channel",
"int8_per_channel_weight_only",
"nvfp4_per_token",
"mxfp8",
]
QUANTIZATION_METHODS: list[str] = list(get_args(QuantizationMethods))
DEPRECATED_QUANTIZATION_METHODS = [
"fbgemm_fp8",
"fp_quant",
]
# The customized quantization methods which will be added to this dict.
_CUSTOMIZED_METHOD_TO_QUANT_CONFIG = {}The list has 30 names: 24 methods and 6 "online quant shorthand names" (lines 40-48). Two names are deprecated (lines 52-55). The dict _CUSTOMIZED_METHOD_TO_QUANT_CONFIG holds the plugin classes (line 58). The CLI flag accepts any string, because the field type is QuantizationMethods | str | None (vllm/config/model.py:264).
Step 2: register_quantization_config#
def _wrapper(quant_config_cls):
if quantization in QUANTIZATION_METHODS:
logger.debug(
"The quantization method '%s' already exists and will be "
"overwritten by the quantization config %s.",
quantization,
quant_config_cls,
)
else:
QUANTIZATION_METHODS.append(quantization)
# Automatically assume the custom quantization config is supported
if sq := current_platform.supported_quantization:
sq.append(quantization)
if not issubclass(quant_config_cls, QuantizationConfig):
raise ValueError(
"The quantization config must be a subclass of `QuantizationConfig`."
)
_CUSTOMIZED_METHOD_TO_QUANT_CONFIG[quantization] = quant_config_cls
return quant_config_cls
return _wrapper- Lines 89-95. A name that exists already is not an error. vLLM logs at DEBUG level and overwrites it. A plugin can replace a built-in method this way.
- Lines 97-100. A new name goes into
QUANTIZATION_METHODS. If the platform has a non-emptysupported_quantizationlist, the name goes there too. ROCm has such a list (vllm/platforms/rocm.py:625-649); CUDA has none, and an empty list means "no limit" (vllm/platforms/interface.py:1153-1158). - Lines 102-105. A class that is not a
QuantizationConfigsubclass raisesValueError. The check comes after line 97, so the new name is in the list already. - Line 106. The class goes into the plugin dict.
Lookups go through get_quantization_config(name). It builds the built-in table, then applies the plugin dict last, so a plugin class wins over a built-in class of the same name:
# Register online shorthands (e.g. "fp8_per_tensor") as quant methods.
# setdefault so a shorthand that is also a checkpoint method (e.g. "mxfp8")
# keeps its checkpoint config; the shorthand still works via the
# `--quantization` CLI path in `resolve_quantization_config`.
for shorthand in _ONLINE_SHORTHANDS:
method_to_config.setdefault(shorthand, OnlineQuantizationConfig)
# Update the `method_to_config` with customized quantization methods.
method_to_config.update(_CUSTOMIZED_METHOD_TO_QUANT_CONFIG)
return method_to_config[quantization]Step 3: the interface of a QuantizationConfig#
Six methods are abstract (vllm/model_executor/layers/quantization/base_config.py:130-217):
| Method | Kind | Called by | Purpose |
|---|---|---|---|
get_name() | instance | logs, checks | the method name |
get_supported_act_dtypes() | instance | vllm/config/vllm.py:919-925 | the model dtype must be in this list, else ValueError |
get_min_capability() | class | vllm/config/vllm.py:908-918 | for example 80 for Ampere; a GPU below it raises ValueError |
get_config_filenames() | static | vllm/model_executor/model_loader/weight_utils.py:352 | extra JSON files to read when config.json has no quantization config |
from_config(dict) | class | vllm/model_executor/model_loader/weight_utils.py:237 | makes the object from the checkpoint dict |
get_quant_method(layer, prefix) | instance | vllm/model_executor/layers/quantization/base_config.py:312 | returns a quantize method for one layer, or None |
One class method is optional: override_quantization_method. It returns None by default (vllm/model_executor/layers/quantization/base_config.py:163-182). The docstring says that a subclass should override it "only ... in exceptional circumstances". Step 4 shows what it does. maybe_update_config is a hook that runs after the object is made (vllm/model_executor/layers/quantization/base_config.py:275-292).
Step 4: how vLLM picks the method name#
ModelConfig._verify_quantization runs in ModelConfig.__post_init__ (vllm/config/model.py:922), in the front end. It reads the checkpoint's quant_method and asks every method whether it wants to take over:
quant_cfg = self.model_arch_config.quantization_config
if quant_cfg is not None:
quant_method = quant_cfg["quant_method"]
# Quantization methods which are overrides (i.e. they have a
# `override_quantization_method` method) must be checked in order
# of preference (this is particularly important for GPTQ).
overrides = [
"auto_gptq",
"gptq",
"gptq_marlin",
"auto_awq",
"awq",
"awq_marlin",
"inc",
"moe_wna16",
"modelopt",
"modelopt_fp4",
"modelopt_mxfp8",
"mxfp8",
"modelopt_mixed",
# Ensure heavy backends are probed last to avoid unnecessary
# imports during override detection (e.g., MXFP4 imports Triton)
"mxfp4",
"gpt_oss_mxfp4",
"deepseek_v4_fp8",
"humming",
]
# if the user specifies humming, we should always use humming
if self.quantization == "humming":
overrides = ["humming"] + overrides
quantization_methods = [
q for q in supported_quantization if q not in overrides
]
# Any custom overrides will be in quantization_methods so we place
# them at the start of the list so custom overrides have preference
# over the built-in ones.
quantization_methods = quantization_methods + overrides
# Detect which checkpoint is it
for name in quantization_methods:
method = me_quant.get_quantization_config(name)
quantization_override = method.override_quantization_method(
quant_cfg, self.quantization, hf_config=self.hf_config
)
if quantization_override is not None:
# Raise error if the override is not custom (custom would
# be in QUANTIZATION_METHODS but not QuantizationMethods)
# and hasn't been added to the overrides list.
if (
name in get_args(me_quant.QuantizationMethods)
and name not in overrides
):
raise ValueError(
f"Quantization method {name} is an override but "
"is has not been added to the `overrides` list "
"above. This is necessary to ensure that the "
"overrides are checked in order of preference."
)
quant_method = quantization_override
self.quantization = quantization_override
break- Line 1392. The checkpoint names a method in
quant_method, for example"awq". - Lines 1396-1416. Seventeen built-in names are "overrides": their class has its own
override_quantization_method. The order matters, "particularly ... for GPTQ". - Lines 1420-1426. All other names come first, then the overrides. Plugin names are in the first part, so "custom overrides have preference over the built-in ones".
- Lines 1429-1433. The first method whose
override_quantization_methodreturns a name wins. For an AWQ checkpoint on a GPU,AutoAWQConfiganswers"auto_awq"(vllm/model_executor/layers/quantization/auto_awq.py:263-286). - Lines 1438-1447. A built-in method that overrides but is missing from the list raises
ValueError. This guards vLLM's own code, not plugins.
The rest of the method checks the result (vllm/config/model.py:1452-1485). --quantization must match the checkpoint, unless an override changed it. An unknown name raises "Unknown quantization method". The platform can refuse the name in verify_quantization. A deprecated name raises unless --allow-deprecated-quantization is set.
Step 5: one config object, made in the front end#
VllmConfig.__post_init__ makes the object once:
if self.quant_config is None and self.model_config is not None:
self.quant_config = VllmConfig._get_quantization_config(
self.model_config, self.load_config
) if model_config.quantization is not None:
from vllm.model_executor.model_loader.weight_utils import get_quant_config
quant_config = get_quant_config(model_config, load_config)
capability_tuple = current_platform.get_device_capability()
if capability_tuple is not None:
capability = capability_tuple.to_int()
if capability < quant_config.get_min_capability():
raise ValueError(
f"The quantization method {model_config.quantization} "
"is not supported for the current GPU. Minimum "
f"capability: {quant_config.get_min_capability()}. "
f"Current capability: {capability}."
)
supported_dtypes = quant_config.get_supported_act_dtypes()
if model_config.dtype not in supported_dtypes:
raise ValueError(
f"{model_config.dtype} is not supported for quantization "
f"method {model_config.quantization}. Supported dtypes: "
f"{supported_dtypes}"
)
quant_config.maybe_update_config(
model_config.model,
hf_config=model_config.hf_config,
revision=model_config.revision,
)
return quant_configget_quant_config finds the settings. It reads quantization_config from the HF config, or from its text_config, or compression_config, and calls from_config with that dict (vllm/model_executor/model_loader/weight_utils.py:239-279). Without one, it looks for the files that get_config_filenames() names. If that list is empty, it calls the class with no arguments (vllm/model_executor/model_loader/weight_utils.py:352-368). So a plugin that quantizes a BF16 checkpoint at load time needs a constructor without required arguments.
Step 6: how the workers get the config#
The worker does not look up the name again. quant_config is a field of VllmConfig, and the engine core and the workers get the whole VllmConfig pickled, as an argument of the new process (vllm/v1/engine/utils.py:256-287). Python imports the module of the config class while it unpickles the object. So a worker needs your module importable, but not your registration. (Derived from the pickling.)
Two worker paths do look up a name, so register in every process to be safe. A draft model for speculative decoding makes its own config (vllm/model_executor/models/utils.py:1003-1007). The torchao loader calls get_quant_config again (vllm/model_executor/model_loader/default_loader.py:491-492). A general plugin runs the decorator in every process.
Step 7: each layer asks for its quantize method#
The model reads the object from the config, for example in Llama (vllm/model_executor/models/llama.py:370), and passes it to each layer with a prefix. A linear layer asks it in its constructor:
self.quant_method: QuantizeMethodBase
if quant_config is None:
self.quant_method = UnquantizedLinearMethod()
elif quant_method := resolve_quant_method(quant_config, self, prefix=prefix):
self.quant_method = quant_method
else:
raise ValueError("All linear layers should support quant method.")resolve_quant_method calls get_quant_method(layer, prefix), and adds online quantization on top when it is configured (vllm/model_executor/layers/quantization/base_config.py:295-356). The prefix is the module name, so the config can skip layers by name. A linear layer must get a method; None raises "All linear layers should support quant method." Other layer types, such as attention, accept None.
The quantize method has two required methods and one important optional one (vllm/model_executor/layers/quantization/base_config.py:43-85):
create_weights(layer, ...)makes the parameters and sets them on the layer. A replicated linear layer calls it with the sizes and itsweight_loader(vllm/model_executor/layers/linear.py:379-387).apply(layer, x, bias)runs in every forward (vllm/model_executor/layers/linear.py:420).process_weights_after_loading(layer)runs once after the weights are loaded, for example to repack them for a kernel. The loader calls it for every module that has aquant_method(vllm/model_executor/model_loader/utils.py:157-186).
Step 8: a minimal out-of-tree method#
The test suite has a complete plugin. It fakes 8-bit quantization of the activations and keeps BF16 weights:
class FakeQuantLinearMethod(UnquantizedLinearMethod):
"""Fake quantization linear method for per-token dynamic quantization."""
def __init__(self, num_bits: int = 8) -> None:
"""Initialize the quantization method."""
super().__init__()
self.num_bits = num_bits
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
"""Perform fake quantization before the linear layer."""
# Calculate the scales dynamically
max_val = torch.amax(x, dim=(0, -1), keepdims=True)
min_val = torch.amin(x, dim=(0, -1), keepdims=True)
scales = (max_val - min_val) / (2**self.num_bits - 1)
# Fake quantize the input
quant_x = torch.clamp(
torch.round(x / scales),
-(2 ** (self.num_bits - 1)),
2 ** (self.num_bits - 1) - 1,
)
dequant_x = quant_x * scales
return F.linear(dequant_x, layer.weight, bias)
@register_quantization_config("custom_quant")
class CustomQuantConfig(QuantizationConfig):
"""Custom quantization config for per-token dynamic fake quantization."""
def __init__(self, num_bits: int = 8) -> None:
"""Initialize the quantization config."""
super().__init__()
self.num_bits = num_bits
def get_name(self) -> QuantizationMethods:
"""Name of the quantization method."""
return "custom_quant" # type: ignore[return-value] # Runtime registration extends built-in names.
def get_supported_act_dtypes(self) -> list[torch.dtype]:
"""List of supported activation dtypes."""
return [torch.float16, torch.bfloat16]
@classmethod
def get_min_capability(cls) -> int:
"""Minimum GPU capability to support the quantization method."""
return -1
@staticmethod
def get_config_filenames() -> list[str]:
"""List of filenames to search for in the model directory."""
return []
@classmethod
def from_config(cls, config: dict[str, Any]) -> "CustomQuantConfig":
"""Create a config class from the model's quantization config."""
return CustomQuantConfig(num_bits=config.get("num_bits", 8))
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> FakeQuantLinearMethod | None:
"""Get the quantize method to use for the quantized layer."""
if isinstance(layer, LinearBase):
return FakeQuantLinearMethod(num_bits=self.num_bits)
return NoneNote the choices. FakeQuantLinearMethod subclasses UnquantizedLinearMethod, so it reuses its create_weights and only changes apply. get_config_filenames() returns [] and the constructor has a default, so --quantization custom_quant works on a normal BF16 checkpoint. The test loads Llama-3.2-1B with it and checks that qkv_proj uses the method (tests/quantization/test_register_quantization_config.py:130-140).
To use such a class with vllm serve, publish a general plugin that imports the module, so that the decorator runs:
# my_quant_pkg/__init__.py (illustrative, not in the vLLM repository)
def register():
from my_quant_pkg import config # noqa: F401 (runs @register_quantization_config("custom_quant"))
# pyproject.toml
# [project.entry-points."vllm.general_plugins"]
# my_quant = "my_quant_pkg:register"
vllm serve meta-llama/Llama-3.2-1B-Instruct --quantization custom_quant
The guide in the docs shows the same pattern, and a MoE method too (docs/features/quantization/README.md:93-148).
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveThe override loop, line by line, for three checkpoints
The loop of vllm/config/model.py:1420-1450 with the default settings, on a CUDA GPU:
Checkpoint quant_method | --quantization | Result | Why |
|---|---|---|---|
"awq" | not set | auto_awq | AutoAWQConfig takes "awq" when the user value is unset or an AWQ name, and the platform is not CPU (vllm/model_executor/layers/quantization/auto_awq.py:267-284). |
"fp8" | not set | fp8 | No override answers, so line 1455 copies the checkpoint name. |
| none (BF16) | fp8 | fp8, then an online config | The checkpoint branch does not run (line 1391). Step 5 turns the name into OnlineQuantizationConfig. |
"fp8" | awq | ValueError | "Quantization method specified in the model config (fp8) does not match ..." (vllm/config/model.py:1456-1462). |
A plugin can take over a checkpoint format. Its class overrides override_quantization_method and returns its own name for, say, quant_method == "awq". Its name is in the first part of the loop, so it answers before auto_awq. (Derived from lines 1420-1433.)
Deep diveWhich process does what
| Work | Process | Where |
|---|---|---|
| register the name (decorator) | every process that runs the general plugin | vllm/model_executor/layers/quantization/__init__.py:88-107 |
| choose the name, check the platform | front end, ModelConfig.__post_init__ | vllm/config/model.py:1383-1470 |
| make the config object, check capability and dtype | front end, VllmConfig.__post_init__ | vllm/config/vllm.py:1586-1589 |
| receive the object | engine core and workers (pickled VllmConfig) | vllm/v1/engine/utils.py:256-287 |
get_quant_method, create_weights | worker, model build | vllm/model_executor/layers/linear.py:293, vllm/model_executor/layers/linear.py:379 |
process_weights_after_loading | worker, after weight load | vllm/model_executor/model_loader/utils.py:186 |
apply | worker, every step | vllm/model_executor/layers/linear.py:420 |
With TP = 2, the front end makes one object, and each of the 2 workers unpickles its own copy. The quantize methods are new objects per layer and per worker. (Derived.)
A platform has its own hook for this: the docstring of pre_register_and_update says it can import and register "the out-of-tree quantization config" (vllm/platforms/interface.py:639-651). It runs in the front end twice: when the CLI builds the parser (vllm/engine/arg_utils.py:3257), and again when create_engine_config starts (vllm/engine/arg_utils.py:2246).
Deep diveKV cache quantization at a high level
The KV cache data type is a separate setting, --kv-cache-dtype. Its default auto is resolved in two places, and the result depends on the checkpoint format:
- ModelOpt checkpoints.
EngineArgsresolvesautobefore it makesCacheConfig(vllm/engine/arg_utils.py:2285-2288).resolve_kv_cache_dtype_stringreads the KV scheme only whenquant_methodstarts with"modelopt"(vllm/utils/torch_utils.py:457-466,vllm/utils/torch_utils.py:509-521). - compressed-tensors (llm-compressor) checkpoints. The
Attentionlayer seeskv_cache_schemeon the quant config and turnsautointofp8while the model is built (vllm/model_executor/layers/attention/attention.py:287-291). - Other checkpoints.
autostays the model dtype (vllm/utils/torch_utils.py:527-529).
The scales come through the same quant config. Each attention layer asks resolve_quant_method for a KV cache method. A quantized one must be a BaseKVCacheMethod, and its create_weights makes k_scale and v_scale parameters that the checkpoint fills (vllm/model_executor/layers/attention/attention.py:194-226). The KV dtype also limits the attention backends (How vLLM chooses an attention backend).
A new --kv-cache-dtype value has its own registry, register_kv_cache_dtype (vllm/config/cache.py:111-155). No code in vllm/ calls it at this commit; the page Hooks in the engine core and the workers explains it.
Deep diveError paths
| Situation | Result | Where |
|---|---|---|
Unknown name in get_quantization_config | ValueError "Invalid quantization method" | vllm/model_executor/layers/quantization/__init__.py:113-114 |
Name not in QUANTIZATION_METHODS after the checks | ValueError "Unknown quantization method" | vllm/config/model.py:1464-1469 |
| Platform list does not contain the name | ValueError "... quantization is currently not supported in ..." | vllm/platforms/interface.py:1153-1158 |
GPU capability below get_min_capability() | ValueError | vllm/config/vllm.py:912-918 |
Model dtype not in get_supported_act_dtypes() | ValueError | vllm/config/vllm.py:919-925 |
| Config files expected but missing, or more than one | ValueError | vllm/model_executor/model_loader/weight_utils.py:375-385 |
Linear layer gets None | ValueError "All linear layers should support quant method." | vllm/model_executor/layers/linear.py:295-296 |
| Re-registering a name | DEBUG log, overwrite | vllm/model_executor/layers/quantization/__init__.py:89-95 |
What can go wrong#
- A plugin hides a built-in method. A name collision only logs at DEBUG level, and the plugin class wins (
vllm/model_executor/layers/quantization/__init__.py:89-95,vllm/model_executor/layers/quantization/__init__.py:195). - The method is refused on ROCm. A platform with a list accepts only listed names. The decorator appends a new name to the list (lines 97-100). It does not append a name that exists already. So ROCm still refuses a plugin that replaces a built-in method missing from its list, such as
experts_int8. (Derived fromvllm/model_executor/layers/quantization/__init__.py:89-100andvllm/platforms/rocm.py:625-649.) - The config class needs arguments. On a checkpoint without quantization settings, vLLM calls your class with no arguments (
vllm/model_executor/model_loader/weight_utils.py:368). - A deprecated method.
fbgemm_fp8andfp_quantneed--allow-deprecated-quantization(vllm/config/model.py:1472-1485).
Why it matters. Most production models are quantized. When a checkpoint loads with the wrong method, or with no method, the model gives wrong answers or uses too much memory. The startup log and the error messages above show which method vLLM picked and why.
Check yourself#
- 1Which decorator adds a new quantization method, and what must the class subclass?easy
Show answer
@register_quantization_config("name"); the class must subclassQuantizationConfig, elseValueError(vllm/model_executor/layers/quantization/__init__.py:61,vllm/model_executor/layers/quantization/__init__.py:102-105). - 2Which method of the quantize method runs in every forward of a linear layer?easy
Show answer
apply(layer, x, bias)(vllm/model_executor/layers/linear.py:420).create_weightsruns once when the layer is built. - 3A checkpoint saysmedium
"quant_method": "awq"and you pass no flag. Which method name does vLLM use on a GPU, and which code decides?Show answer
auto_awq._verify_quantizationasks each method'soverride_quantization_method(vllm/config/model.py:1429-1450), andAutoAWQConfiganswers for"awq"when the platform is not CPU (vllm/model_executor/layers/quantization/auto_awq.py:263-284). - 4Your plugin registersmedium
"my_quant"only in a script that runs in the API server process. With TP = 2, do the workers fail?Show answer
Not for the main model: the workers get the config object inside the pickled
VllmConfig(vllm/v1/engine/utils.py:256-287), and need only your module importable. But a draft model or the torchao loader looks the name up again in the worker (vllm/model_executor/models/utils.py:1003-1007,vllm/model_executor/model_loader/default_loader.py:491-492). A general plugin avoids the difference. - 5You runhard
--quantization custom_quanton a BF16 checkpoint, andget_config_filenames()returns[]. How is the config object made, and what follows for the constructor?Show answer
config.jsonhas no quantization config, soget_quant_configgoes past line 279. The class names no files, andcustom_quantis neitherfp8nor an online shorthand, so vLLM callsquant_cls()with no arguments (vllm/model_executor/model_loader/weight_utils.py:352-368). The constructor must work without arguments; the test class hasnum_bits: int = 8(tests/quantization/test_register_quantization_config.py:71-74).