vLLM Explained

Quantization methods as plugins

AdvancedvLLM conceptsabout 11 min read + 3 min deep dives

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#

Figure: the front end chooses the method and makes one config object (panel 1); each layer gets a quantize method when the worker builds the model (panel 2); apply() runs in every step (panel 3).
Open full size ↗

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
  1. 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.
  2. Lines 97-100. A new name goes into QUANTIZATION_METHODS. If the platform has a non-empty supported_quantization list, 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).
  3. Lines 102-105. A class that is not a QuantizationConfig subclass raises ValueError. The check comes after line 97, so the new name is in the list already.
  4. 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):

MethodKindCalled byPurpose
get_name()instancelogs, checksthe method name
get_supported_act_dtypes()instancevllm/config/vllm.py:919-925the model dtype must be in this list, else ValueError
get_min_capability()classvllm/config/vllm.py:908-918for example 80 for Ampere; a GPU below it raises ValueError
get_config_filenames()staticvllm/model_executor/model_loader/weight_utils.py:352extra JSON files to read when config.json has no quantization config
from_config(dict)classvllm/model_executor/model_loader/weight_utils.py:237makes the object from the checkpoint dict
get_quant_method(layer, prefix)instancevllm/model_executor/layers/quantization/base_config.py:312returns 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
  1. Line 1392. The checkpoint names a method in quant_method, for example "awq".
  2. Lines 1396-1416. Seventeen built-in names are "overrides": their class has its own override_quantization_method. The order matters, "particularly ... for GPTQ".
  3. 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".
  4. Lines 1429-1433. The first method whose override_quantization_method returns a name wins. For an AWQ checkpoint on a GPU, AutoAWQConfig answers "auto_awq" (vllm/model_executor/layers/quantization/auto_awq.py:263-286).
  5. 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_config

get_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):

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 None

Note 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 dive

The 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--quantizationResultWhy
"awq"not setauto_awqAutoAWQConfig 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 setfp8No override answers, so line 1455 copies the checkpoint name.
none (BF16)fp8fp8, then an online configThe checkpoint branch does not run (line 1391). Step 5 turns the name into OnlineQuantizationConfig.
"fp8"awqValueError"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 dive

Which process does what

WorkProcessWhere
register the name (decorator)every process that runs the general pluginvllm/model_executor/layers/quantization/__init__.py:88-107
choose the name, check the platformfront end, ModelConfig.__post_init__vllm/config/model.py:1383-1470
make the config object, check capability and dtypefront end, VllmConfig.__post_init__vllm/config/vllm.py:1586-1589
receive the objectengine core and workers (pickled VllmConfig)vllm/v1/engine/utils.py:256-287
get_quant_method, create_weightsworker, model buildvllm/model_executor/layers/linear.py:293, vllm/model_executor/layers/linear.py:379
process_weights_after_loadingworker, after weight loadvllm/model_executor/model_loader/utils.py:186
applyworker, every stepvllm/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 dive

KV 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:

  1. ModelOpt checkpoints. EngineArgs resolves auto before it makes CacheConfig (vllm/engine/arg_utils.py:2285-2288). resolve_kv_cache_dtype_string reads the KV scheme only when quant_method starts with "modelopt" (vllm/utils/torch_utils.py:457-466, vllm/utils/torch_utils.py:509-521).
  2. compressed-tensors (llm-compressor) checkpoints. The Attention layer sees kv_cache_scheme on the quant config and turns auto into fp8 while the model is built (vllm/model_executor/layers/attention/attention.py:287-291).
  3. Other checkpoints. auto stays 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 dive

Error paths

SituationResultWhere
Unknown name in get_quantization_configValueError "Invalid quantization method"vllm/model_executor/layers/quantization/__init__.py:113-114
Name not in QUANTIZATION_METHODS after the checksValueError "Unknown quantization method"vllm/config/model.py:1464-1469
Platform list does not contain the nameValueError "... quantization is currently not supported in ..."vllm/platforms/interface.py:1153-1158
GPU capability below get_min_capability()ValueErrorvllm/config/vllm.py:912-918
Model dtype not in get_supported_act_dtypes()ValueErrorvllm/config/vllm.py:919-925
Config files expected but missing, or more than oneValueErrorvllm/model_executor/model_loader/weight_utils.py:375-385
Linear layer gets NoneValueError "All linear layers should support quant method."vllm/model_executor/layers/linear.py:295-296
Re-registering a nameDEBUG log, overwritevllm/model_executor/layers/quantization/__init__.py:89-95

What can go wrong#

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#

  1. 1
    Which decorator adds a new quantization method, and what must the class subclass?
    easy
    Show answer

    @register_quantization_config("name"); the class must subclass QuantizationConfig, else ValueError (vllm/model_executor/layers/quantization/__init__.py:61, vllm/model_executor/layers/quantization/__init__.py:102-105).

  2. 2
    Which 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_weights runs once when the layer is built.

  3. 3
    A checkpoint says "quant_method": "awq" and you pass no flag. Which method name does vLLM use on a GPU, and which code decides?
    medium
    Show answer

    auto_awq. _verify_quantization asks each method's override_quantization_method (vllm/config/model.py:1429-1450), and AutoAWQConfig answers for "awq" when the platform is not CPU (vllm/model_executor/layers/quantization/auto_awq.py:263-284).

  4. 4
    Your plugin registers "my_quant" only in a script that runs in the API server process. With TP = 2, do the workers fail?
    medium
    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.

  5. 5
    You run --quantization custom_quant on a BF16 checkpoint, and get_config_filenames() returns []. How is the config object made, and what follows for the constructor?
    hard
    Show answer

    config.json has no quantization config, so get_quant_config goes past line 279. The class names no files, and custom_quant is neither fp8 nor an online shorthand, so vLLM calls quant_cls() with no arguments (vllm/model_executor/model_loader/weight_utils.py:352-368). The constructor must work without arguments; the test class has num_bits: int = 8 (tests/quantization/test_register_quantization_config.py:71-74).