vLLM Explained

Quantized checkpoints

IntermediateLearn the basicsabout 9 min read + 5 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

In one sentence

A quantized checkpoint stores weights in 8 or 4 bits with scales. vLLM reads the format from config.json; the saved memory goes to the KV cache.

The idea#

BF16, FP8 and FP4 are number formats with fewer bits per value, and block scales map the small values back to real values (Number formats). A quantized checkpoint is a model folder that stores some tensors in such a small format. This page explains which tensors, how the checkpoint says it, and what that does to memory in a server.

Three families#

  • Weight-only (W4A16, W8A16). Only the weights are small, for example 4-bit integers. The kernel expands them to 16 bits and multiplies with 16-bit activations. AWQ and GPTQ are the common 4-bit methods. This saves memory and memory bandwidth, so it helps decode, which reads all weights for each step. It does not make the multiply itself cheaper.
  • Weight and activation (W8A8). Weights and the activations that enter each linear layer are 8-bit, FP8 or INT8. The GPU then runs the multiply in 8 bits. On GPUs with FP8 tensor cores (for example NVIDIA Hopper), the FP8 peak is about twice the BF16 peak. This is general knowledge, not from the vLLM repository. This helps prefill and large batches.
  • FP4 (NVFP4, MXFP4). 4-bit floating point with a scale for each small block of values. On the newest GPUs, weights and activations can both be FP4 (W4A4). On other GPUs, vLLM can run some FP4 checkpoints weight-only.

Scales: per tensor, per channel, per group, per block#

A small format has a small range. So each group of values gets a scale: real value ≈ stored value × scale. The checkpoint stores the scales next to the weights. The group size is a trade-off: more scales give better accuracy but use more memory.

GranularityOne scale forExample for a weight [4096, 4096]
per tensorthe whole tensor1 scale
per channeleach output row4,096 scales
per groupeach run of g input values in a row (g = 128 is common for AWQ and GPTQ)4,096 × 32 = 131,072 scales
per blockeach 2-D tile, for example 128 × 128 (block FP8)32 × 32 = 1,024 scales
micro-block (FP4)each run of 16 (NVFP4) or 32 (MXFP4) values4,096 × 256 = 1,048,576 scales (NVFP4)

Activations change with every request. So their scales come from calibration data (static, one fixed scale), or from the live values (dynamic). A dynamic scheme computes, for example, one scale per token in the kernel.

The KV cache is separate#

KV cache quantization stores the keys and values in a small format, normally FP8. It is a separate choice from the weight format. You can serve a BF16 model with an FP8 KV cache, or an AWQ model with a BF16 KV cache. FP8 KV halves the bytes per token, so the same memory holds twice the tokens.

How a checkpoint says that it is quantized#

A Hugging Face model folder has a config.json (Hugging Face checkpoints). A quantized checkpoint adds a quantization_config object to it. The key quant_method names the format. The other keys depend on the format:

"quantization_config": {
  "quant_method": "awq",
  "bits": 4,
  "group_size": 128,
  "zero_point": true
}

Common values of quant_method are awq, gptq, fp8, compressed-tensors (from llm-compressor) and modelopt (from NVIDIA ModelOpt). A compressed-tensors config has config_groups. Each group says, for a list of target layers, how the weights and the input_activations are quantized.

Memory math: an 8B model#

Take a Llama-3.1-8B-shaped model: 8.03 billion weights. 6.98 billion are in the linear layers of the 32 decoder layers. 1.05 billion are in the embedding and the LM head, which many quantized checkpoints keep in BF16.

WeightsBytes per linear weightWeights in GBFree for KV on a 24 GB GPUKV tokens (BF16 KV)KV tokens (FP8 KV)
BF16216.16.0 GB45,90091,900
FP8 (W8A8)1 (+ scales)9.113.0 GB99,200198,300
INT4, group 128 (W4A16)0.5 + 2/128 + 0.5/1285.716.4 GB124,800249,500

The assumptions: 1 GB = 109 bytes, and the GPU has 24 GB. The model is untied, with a vocabulary of 128,256 tokens. The computation: the budget is 24 GB × 0.92 (the default gpu_memory_utilization, vllm/config/cache.py:212) = 22.1 GB. The KV cache needs 32 layers × 8 KV heads × 128 × 2 (K and V) × 2 bytes = 128 KiB per token in BF16. For FP8 weights, 22.1 − 9.1 = 13.0 GB, and 13.0 GB / 131,072 bytes = 99,200 tokens. The INT4 row counts one 16-bit scale and one 4-bit zero point per group of 128 weights, as AWQ stores them.

An everyday comparison#

Think of a photo library. Weight-only quantization is like small JPEG files that you open to full size when you look at them. They use less disk space, but the viewing work is the same. W8A8 is like a viewer that can show the small files directly, without the expand step. The KV cache is the stack of photos on your desk: if each one is smaller, more of them fit.

The comparison stops here: a JPEG loses detail in a fixed way. Quantization methods such as AWQ and GPTQ choose the scales with calibration data, to keep the error small where the model is sensitive.

The picture#

Figure: each scheme makes different tensors small; the KV cache format is a separate choice (panel 1). Smaller weights leave more memory for the KV cache (panel 2, derived numbers for a Llama-3.1-8B-shaped model).
Open full size ↗

Read panel 1 row by row. The colour of a cell gives the number of bits. Panel 2 is the memory table of this page as a picture: the weight format moves the line between weight memory and KV cache memory.

In vLLM#

This is a basics page, so this part gives only pointers. The page Quantization methods as plugins explains how a quantization method plugs into the layers.

  1. The flag. --quantization / -q selects a method (vllm/engine/arg_utils.py:985). Its default is None. Then vLLM reads quantization_config from the model config; without one, the weights are not quantized (vllm/config/model.py:264-268).
  2. Detection. ModelConfig._verify_quantization() runs in __post_init__ (vllm/config/model.py:922). It reads quant_method and asks each method class whether it wants to take over the checkpoint (vllm/config/model.py:1388-1462).
  3. The list. QuantizationMethods has 30 names: 24 methods such as awq, gptq, fp8, compressed-tensors, modelopt_fp4 and mxfp4, and 6 short names for online quantization such as fp8_per_tensor (vllm/model_executor/layers/quantization/__init__.py:15-48).
  4. The KV cache. --kv-cache-dtype has the default "auto" (vllm/config/cache.py:232). A KV format in the checkpoint wins first; otherwise "auto" means the model dtype (next deep dives).
        # Parse quantization method from the HF model config, if available.
        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
The start of the detection: the quantization config from config.json, its quant_method, and the ordered list of methods that can override it.

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

From config.json to a quantization method, step by step

  1. Read the file. get_config() takes quantization_config from config.json. Older ModelOpt checkpoints keep it in a separate hf_quant_config.json, and vLLM reads that file instead (vllm/transformers_utils/config.py:945-958).
  2. Normalize. _normalize_quantization_config() also accepts the key compression_config of compressed-tensors. It sets quant_method for ModelOpt checkpoints from their quant_algo (FP8 → modelopt, NVFP4 → modelopt_fp4). It lower-cases the name and writes compressed_tensors as compressed-tensors (vllm/transformers_utils/model_arch_config_convertor.py:241-281). For multimodal models it also looks in the text config (vllm/transformers_utils/model_arch_config_convertor.py:283-290).
  3. Overrides. _verify_quantization() tries all methods; the methods in the overrides list go last, in a fixed order (vllm/config/model.py:1396-1426). Each class can return a better name for the checkpoint (vllm/config/model.py:1429-1450). For example, on a GPU an AWQ checkpoint becomes auto_awq (vllm/model_executor/layers/quantization/auto_awq.py:262-286), and a GPTQ checkpoint becomes auto_gptq (vllm/model_executor/layers/quantization/auto_gptq.py:214-233).
  4. Compare with the flag. Without --quantization, vLLM uses the detected method. With a flag that does not match the checkpoint, it raises ValueError (vllm/config/model.py:1452-1462).
  5. Check the platform. An unknown name raises ValueError, and the platform can reject a method (vllm/config/model.py:1464-1470). Deprecated methods (fbgemm_fp8, fp_quant) need --allow-deprecated-quantization (vllm/config/model.py:1472-1485, vllm/model_executor/layers/quantization/__init__.py:52-55).
  6. Build the config object. VllmConfig._get_quantization_config() makes the QuantizationConfig from the checkpoint dict. It raises if the GPU compute capability is too low or the model dtype is not supported (vllm/config/vllm.py:898-931).

The name maps to a class in get_quantization_config(): for example awq, awq_marlin and auto_awq all map to AutoAWQConfig (vllm/model_executor/layers/quantization/__init__.py:156-185). A plugin can add a new name with register_quantization_config() (vllm/model_executor/layers/quantization/__init__.py:61).

Deep dive

What each method reads from quantization_config

MethodKeys that vLLM readsSource
AWQw_bit or bits, q_group_size or group_size, zero_point, optional lm_head, modules_to_not_convertvllm/model_executor/layers/quantization/auto_awq.py:241-248
GPTQbits, group_size, desc_act, sym, optional lm_head, dynamicvllm/model_executor/layers/quantization/auto_gptq.py:191-198
FP8activation_scheme, optional ignored_layers, weight_block_sizevllm/model_executor/layers/quantization/fp8.py:158-163
compressed-tensorsconfig_groups: for each group, targets, weights, input_activations, formatvllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors.py:319-346

compressed-tensors schemes. One checkpoint format covers many schemes. vLLM has one scheme class for each, for example W8A8Fp8, W8A8Int8, WNA16, W4A4Fp4 (NVFP4) and W4A4Mxfp4 (vllm/model_executor/layers/quantization/compressed_tensors/schemes/). The NVFP4 scheme uses a group size of 16 and packs two FP4 values in one byte (vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w4a4_nvfp4.py:32, vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w4a4_nvfp4.py:52-58). The MXFP4 class comment says: E8M0 scales for groups of 32, true W4A4 on SM100+ with FlashInfer, otherwise W4A16 with Marlin (vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w4a4_mxfp4.py:25-37).

Deep dive

Quantizing a BF16 checkpoint at load time

You can also quantize a normal BF16 checkpoint while vLLM loads it. This is online quantization. The 6 short names in QuantizationMethods are for this: fp8_per_tensor, fp8_per_block, fp8_per_channel, int8_per_channel_weight_only, nvfp4_per_token and mxfp8 (vllm/model_executor/layers/quantization/__init__.py:43-48). mxfp4 and mxfp8 are also checkpoint formats. They mean online quantization only when the checkpoint has no quantization metadata (vllm/config/quantization.py:233-235).

vllm serve meta-llama/Llama-3.1-8B-Instruct --quantization fp8_per_tensor

Online quantization saves weight memory, but it uses no calibration data. A checkpoint that a tool such as llm-compressor or ModelOpt made with calibration is usually more accurate.

Deep dive

--kv-cache-dtype and its real default

The flag is --kv-cache-dtype (vllm/engine/arg_utils.py:1387). The field default is "auto", and the allowed names include fp8, fp8_e4m3, fp8_e5m2, nvfp4 and several per-head INT and FP formats (vllm/config/cache.py:44-63, vllm/config/cache.py:232-244). "auto" goes through three places:

  1. ModelOpt checkpoints. EngineArgs resolves "auto" before it makes the CacheConfig (vllm/engine/arg_utils.py:2285-2288, vllm/engine/arg_utils.py:2300). resolve_kv_cache_dtype_string() reads the KV algorithm from quantization_config (vllm/utils/torch_utils.py:503-521). But get_kv_cache_quant_algo_string() reads it only when quant_method starts with modelopt; for other methods it returns None (vllm/utils/torch_utils.py:457-458, vllm/utils/torch_utils.py:500).
  2. llm-compressor checkpoints. The attention layer checks the kv_cache_scheme of the quantization config. If the dtype is still "auto", it sets "fp8" (vllm/model_executor/layers/attention/attention.py:281-291). An explicit --kv-cache-dtype wins.
  3. Everything else. "auto" means the model dtype, for example BF16 (vllm/utils/torch_utils.py:524-529).

Scales. An FP8 KV cache needs a scale for K and V. A checkpoint can bring them; the attention layer reads the scheme, for example attn_head strategy for one scale per head (vllm/model_executor/layers/attention/attention.py:293-297). The page Quantization methods as plugins explains how the scales load.

What can go wrong#

  • The flag does not match the checkpoint. --quantization fp8 on an AWQ checkpoint raises "Quantization method specified in the model config (awq) does not match ..." (vllm/config/model.py:1456-1462). Leave the flag out for a quantized checkpoint; vLLM detects the method.
  • --quantization fp8 on a BF16 model. It still works, but it logs a deprecation warning and runs as fp8_per_tensor (vllm/model_executor/model_loader/weight_utils.py:356-363). Use fp8_per_tensor or another online short name.
  • The GPU is too old. Each method has a minimum compute capability; vLLM stops at start-up below it (vllm/config/vllm.py:910-918).
  • The dtype does not fit the method. Some methods support only FP16 or only BF16 activations. Start-up then fails with "is not supported for quantization method" (vllm/config/vllm.py:919-925). Set --dtype to a supported type.
  • An FP8 KV cache without good scales. FP8 KV halves KV memory, but without calibrated scales the accuracy can drop. Test the quality of your task before you use it in production.
  • Docs that show old flags. Guides from older versions use --quantization fp8 for online quantization. The code marks it as deprecated (vllm/model_executor/model_loader/weight_utils.py:357-360). Use the new short names.

Why it matters. The weight format decides how much GPU memory is left for the KV cache, and so how many requests the server can hold at once. The activation format decides how fast prefill and large batches run. Pick both on purpose, and keep the KV cache format as a separate decision.

Check yourself#

  1. 1
    What is the difference between W4A16 and W8A8?
    easy
    Show answer

    W4A16 stores only the weights in 4 bits; the activations and the multiply stay in 16 bits. W8A8 stores weights and activations in 8 bits, so the multiply runs in 8 bits too.

  2. 2
    You start vllm serve with an AWQ checkpoint and no --quantization flag. How does vLLM know that the model is quantized?
    easy
    Show answer

    It reads quantization_config.quant_method from config.json (vllm/config/model.py:1389-1392). On a GPU, the AWQ class takes over the name and the method becomes auto_awq (vllm/model_executor/layers/quantization/auto_awq.py:267-284).

  3. 3
    An 8B model has 6.98 billion linear weights and 1.05 billion embedding and LM-head weights in BF16. About how many GB do the weights need with FP8 linear layers?
    medium
    Show answer

    The FP8 linear weights need 6.98 GB (1 byte each). The BF16 embedding and LM head need 1.05 × 2 = 2.1 GB. Together that is about 9.1 GB, plus a little for the scales.

  4. 4
    A compressed-tensors checkpoint has an FP8 kv_cache_scheme. You start vLLM without --kv-cache-dtype. What KV dtype do you get, and which code decides it?
    hard
    Show answer

    FP8. resolve_kv_cache_dtype_string() returns "auto" for this checkpoint, because it reads only ModelOpt configs (vllm/utils/torch_utils.py:457-458). The attention layer then sees the kv_cache_scheme and the dtype "auto", and it sets "fp8" (vllm/model_executor/layers/attention/attention.py:287-291).

  5. 5
    Why does a BF16 KV cache hold only 45,900 tokens next to BF16 weights on a 24 GB GPU, in this page's example? Give the computation.
    hard
    Show answer

    The budget is 24 GB × 0.92 = 22.1 GB. The BF16 weights take 16.1 GB, so 6.0 GB are left. One token needs 32 × 8 × 128 × 2 × 2 = 131,072 bytes, and 6.0 GB / 131,072 B ≈ 45,900 tokens. The default 0.92 comes from the config (vllm/config/cache.py:212). Activations and other buffers make the real number lower.