Skip to content
Qubitium edited this page Nov 2, 2026 · 6 revisions

VPTQ

Documentation home · Quickstart · Lifecycle

On this page

Overview

VPTQ (Vector Post-Training Quantization) compresses a linear layer with learned vector codebooks rather than independently rounding every scalar weight. Each small weight vector is represented by an index into a table of centroids; an optional residual codebook refines the reconstruction. GPT-QModel learns these tables from model weights and calibration activations, then applies GPTQ-style Hessian error compensation across input columns.

The implementation follows the recipe described by microsoft/VPTQ and VPTQ: Extreme Low-bit Vector Post-Training Quantization for Large Language Models. It contains a Torch quantizer, a calibration looper processor, a portable Torch runtime, and a hybrid native CUDA runtime. No external VPTQ package or pre-generated Hessian files are required by this implementation.

Identifier Value Purpose
METHOD.VPTQ "vptq" Quantization algorithm
FORMAT.VPTQ "vptq" Codebooks, indices, and reconstruction metadata stored in checkpoints
BACKEND.VPTQ_TORCH "vptq_torch" Portable reference inference
BACKEND.VPTQ_CUDA "vptq_cuda" Fused CUDA small-row inference; CUDA dequantization plus GEMM for other cases
BACKEND.TORCH "torch" Method-aware alias resolved to VPTQ_TORCH for VPTQ

Both VPTQ runtimes consume the same checkpoint layout. The method is calibration-based, even though the inference layer is a weight-only representation and its base class is named WeightOnlyQuantLinear. VPTQConfig.uses_weight_only_lifecycle() explicitly returns False.

Workflow at a glance

One codebook stream or residual refinement. Residual settings change the vector recipe, not the calibration requirement.

Reading the diagram: Alternative recipes using calibration. Residuals are disabled with num_res_centroids=[-1, -1]; outlier residual codebooks are unsupported.

Text version of the workflow
  • Residual disabled: Learn main codebooks → Assign and compensate → Pack main indices.
  • Residual enabled: default: Learn main codebooks → Run first pass → Fit residual codebooks → Rerun and pack.

Open full-size SVG

Concepts

Vectors, centroids, and codebooks

A linear weight is treated as [out_features, in_features]. For each input column, VPTQ splits the output-feature axis into vectors of vector_lens[1] elements. With vector length 8, each index reconstructs eight adjacent output weights from one centroid, rather than one scalar weight.

Term Meaning in GPT-QModel
Vector A slice down one input column along out_features; the last slice is zero-padded when necessary
Centroid A learned floating vector of the same length as the weight vector
Codebook The table of centroids shared by vectors in one input-channel group
Main index The integer selecting a main-codebook centroid
Residual centroid/index An optional second table/index representing the remaining vector error after the main centroid
Group Contiguous input columns that share a codebook, not a group of scalar quantization scales
Outlier group A leading set of input columns with its own vector length and codebook

Conceptually, a vector is reconstructed as main_codebook[main_index], or as main_codebook[main_index] + residual_codebook[residual_index] when residual quantization is enabled. Normalization and input permutation, if configured, are reversed during reconstruction.

Residual quantization is optional but enabled by default: num_res_centroids=[-1, 16] adds a 16-entry residual codebook for each main input-channel group. Use num_res_centroids=[-1, -1] to disable it. This removes the residual codebook/index stream, not calibration or Hessian compensation; a one-entry residual codebook is rejected rather than treated as disabled.

Vector length is along the output axis. group_size is along the input axis. Confusing the two produces an incompatible layout.

Nominal bits versus actual storage

VPTQConfig.bits is derived, not an independent scalar quantization width. Let C be the main centroid count, R the residual count when enabled, and L the main vector length. The index-only rate is:

index_bits_per_vector = log2(C) + (log2(R) if residual is enabled else 0)
index_only_bits_per_weight = index_bits_per_vector / L
bits = clamp(ceil(index_only_bits_per_weight), 1, 8)

The default C=4096, R=16, L=8 uses 12 main-index bits plus 4 residual-index bits per eight-weight vector: 2 index bits per weight. A 4096-centroid codebook without residuals over length-8 vectors has a 1.5-bit index rate, but its integer bits label is still 2.

Neither number includes codebook storage, word padding, outlier indices, normalization tensors, or bias. Small layers and configurations with many codebooks can have substantial centroid overhead. bits=2 alone does not prove a two-bit total checkpoint size.

To choose the compression recipe, set vector_lens, num_centroids, and num_res_centroids. Passing a contradictory bits value logs that it is ignored and replaces it with the derived label. Labels are restricted to (1, 2, 3, 4, 5, 6, 7, 8); combined index width is independently limited to 32 bits.

Groups and outliers

The two entries in each paired configuration mean [outlier, main]. -1 disables an optional component.

Configuration Result
group_size=-1, group_num=-1 One main codebook for all non-outlier input channels
Positive group_size, group_num=-1 Enough codebooks to cover the non-outlier channels with that input-channel group size; last group may be padded
group_size=-1, positive group_num Requested number of main codebooks; group width is resolved from the input width and outlier policy
Both positive Rejected

Outliers are enabled only when npercent > 0, the outlier vector length is positive, and the outlier centroid count is greater than one. The initial outlier size is ceil(npercent / 100 * in_features), clamped to the input width. npercent=1.0 means one percent, not a fraction of one.

With an active outlier codebook and explicit group_num, remainder columns are added to the outlier set so every main group has equal width. Without an outlier codebook, the helper keeps outlier_size=0 and rounds the group width up; the runtime drops trailing padding during reconstruction. It does not silently create an outlier codebook merely to absorb a remainder. Keep every requested group nonempty: group_num <= remaining_columns alone does not guarantee a nonempty final group after rounding. See Group-count edge case.

The quantizer assigns the leading columns in its working order to the outlier group. enable_perm=True orders input columns by descending Hessian diagonal first; without it, the leading columns retain their original order. npercent alone is not an independent magnitude-based outlier search. Residual codebooks for the outlier group are not supported.

Calibration, normalization, and permutation

Calibration captures the per-module input Gram matrix H = mean(xᵀx). The accumulator uses FP64 and counts flattened input tokens, not dataset rows. This is built online: pass ordinary calibration data to model.quantize() instead of a hessian_path.

The optional transforms are distinct:

Option Effect
kmeans_mode="hessian" Uses Hessian-diagonal importance as sample weights when learning centroids
kmeans_mode="" Uniform k-means weighting; does not disable the later Hessian compensation pass
enable_perm=True Sorts input columns by descending Hessian diagonal and permutes the Hessian consistently; the inverse permutation restores the original layout
enable_norm=True, norm_dim=0 Per-input-column min/range normalization across output rows
enable_norm=True, norm_dim=1 Per-output-row min/range normalization across input columns

VPTQ's enable_perm is its own stored reconstruction feature. It is not an orthogonal model rotation and is not a reason to enable the unrelated shared rotation field.

Usage and configuration

Quantize, save, and reload

This uses a supported model from the repository's VPTQ example. Access to the model may require accepting its upstream license and authenticating with Hugging Face.

import torch
from datasets import load_dataset
from gptqmodel import GPTQModel
from gptqmodel.quantization import VPTQConfig
from gptqmodel.utils.backend import BACKEND

model_id = "meta-llama/Llama-3.2-1B-Instruct"
output_path = "Llama-3.2-1B-Instruct-VPTQ"

calibration = load_dataset(
    "allenai/c4",
    data_files="en/c4-train.00001-of-01024.json.gz",
    split="train",
).select(range(256))["text"]

qcfg = VPTQConfig(
    vector_lens=[-1, 8],
    num_centroids=[-1, 4096],
    num_res_centroids=[-1, 16],
    group_size=-1,
    kmeans_mode="hessian",
)
model = GPTQModel.load(model_id, qcfg, dtype=torch.float16)
model.quantize(calibration, batch_size=1, backend=BACKEND.VPTQ_TORCH)
model.save(output_path)

# Native CUDA inference consumes the same saved codebooks/indices.
cuda_model = GPTQModel.load(
    output_path, device="cuda:0", backend=BACKEND.VPTQ_CUDA
)

For portable or CPU inference:

from gptqmodel import GPTQModel
from gptqmodel.utils.backend import BACKEND

model = GPTQModel.load(
    "Llama-3.2-1B-Instruct-VPTQ",
    device="cpu",
    backend=BACKEND.VPTQ_TORCH,
)

The quantization lifecycle uses VPTQProcessor and packs VPTQLinear with BACKEND.VPTQ_TORCH. Choosing a CUDA inference backend later does not require relearning the codebooks. For generation and tokenizer usage, see Loading and Inference.

Configuration reference

VPTQConfig derives from GPTQConfig; these are its method-specific fields and defaults. List defaults are independently created for each config instance.

Field Default Meaning/validation
method METHOD.VPTQ Only VPTQ accepted by this class
format FORMAT.VPTQ Only VPTQ export layout
bits 2, derived Replaced by the main/residual index-rate calculation; labels 1 through 8
vector_lens [-1, 8] [outlier_vector_len, main_vector_len]; main positive, outlier -1 or positive
num_centroids [-1, 4096] [outlier, main] counts; main must be a power of two at least 2
num_res_centroids [-1, 16] Main residual -1 disables it; enabled count must be a power of two at least 2. Outlier residual must remain -1
npercent 0.0 Nonnegative percentage of input channels for the outlier codebook; positive values require a usable outlier vector/codebook
group_size -1 Input channels per main codebook; -1 lets group_num/single-group layout decide
group_num -1 Number of main codebooks; otherwise positive; cannot be set together with group_size
kmeans_mode "hessian" Only "hessian" and "" accepted
kmeans_iters 100 Maximum Lloyd iterations; at least 1
kmeans_tol 1e-5 Relative inertia-change tolerance; strictly between 0 and 1
kmeans_seed 0 Seed for device-local centroid initialization
enable_norm False Min/range normalization before learning/quantizing vectors
norm_dim 0 Only 0 or 1 accepted
enable_perm False Hessian-diagonal input ordering with saved permutation
block_size 128 Input-column block size for error propagation; at least 1
percdamp 0.01 Inverse-Hessian damping fraction; 0 <= percdamp < 1

All three paired fields must be two-item lists/tuples. Centroid counts are -1 or positive powers of two, subject to the stricter main/outlier requirements above. A one-entry main residual table is rejected because it would have no index bits. Main plus residual index width must be at most 32.

vptq_group_setting() additionally requires positive input width, group_size=-1 or positive, group_num=-1 or positive, and positive resolved group width/count. An explicit group count cannot exceed the initial non-outlier channel count. An outlier percentage leaving no main channels is therefore unusable even though the config validates npercent primarily as nonnegative. For explicit group counts without outliers, also check the nonempty-group invariant below; it is not fully enforced by the helper.

Shared/inherited field Default or relevant rule VPTQ interpretation
desc_act False after normalization Use enable_perm, not GPTQ activation ordering, for VPTQ's saved permutation
sym True Not a selector between scalar symmetric/asymmetric VPTQ codebooks
pack_dtype torch.int32 Runtime-supported packed index dtype
dynamic None Layer exclusion can be honored; nonempty per-layer override dictionaries are rejected
fallback Fallback(strategy=FallbackStrategy.RTN, threshold="0.5%", smooth=None) VPTQ interprets insufficient coverage as Hessian-free vector quantization, not scalar RTN
true_sequential True Shared sequential replay lifecycle
lm_head False Shared output-head selection
offload_to_disk True Shared quantization memory management
rotation None Not the VPTQ permutation feature; keep unset for this recipe
mse, damp_percent, damp_auto_increment, static_groups, GPTAQ/FOEM options Inherited GPTQ-family surface Not the VPTQ codebook-learning controls. VPTQ uses its own k-means fields, block_size, and percdamp

The runtime validator deliberately does not apply the scalar GPTQ group-size, symmetry, or activation-order contract to VPTQ. It checks the VPTQ format contract, shared device/dtype/adapter constraints, and nominal bit labels instead. See QuantizeConfig Reference for the shared config surface.

Group-count edge case

For K input channels and resolved (O, S, G) = (outlier_size, group_size, group_num), every main group needs at least one real column:

(G - 1) * S < K - O <= G * S

In this revision, K=5, group_num=4, group_size=-1 without outliers resolves to (O=0, S=2, G=4). Although 4 <= 5 passes the helper's group-count check, the fourth group starts beyond the matrix. The quantizer leaves that group's codebook absent and packing raises a missing-codebook error. Prefer an explicit positive group_size with group_num=-1, or choose a group count satisfying the invariant for every selected layer. Automatic padding covers partial groups, not empty codebooks.

Alternative recipes

Disable main residual quantization explicitly:

from gptqmodel.quantization import VPTQConfig

qcfg = VPTQConfig(
    vector_lens=[-1, 8],
    num_centroids=[-1, 4096],
    num_res_centroids=[-1, -1],
)
# Index-only rate: 12/8 = 1.5 bits/weight; nominal qcfg.bits is 2.

The repository guide also gives this larger-codebook, three-bit index-rate recipe:

qcfg = VPTQConfig(
    vector_lens=[-1, 8],
    num_centroids=[-1, 65536],
    num_res_centroids=[-1, 256],
    enable_perm=True,
    enable_norm=True,
    kmeans_mode="hessian",
)
# (16 + 8) / 8 = 3 index bits/weight, before codebooks and metadata.

Larger codebooks increase centroid storage and distance-search work; this is not a free accuracy upgrade. For a concrete outlier/grouped layout:

qcfg = VPTQConfig(
    vector_lens=[4, 8],
    num_centroids=[256, 4096],
    num_res_centroids=[-1, 16],
    npercent=1.0,
    group_size=128,
    group_num=-1,
    enable_perm=True,
)

The actual group/outlier sizes are resolved separately for each layer's input width. The bits label continues to describe the main index-rate recipe, not an outlier-weighted checkpoint-size estimate.

Implementation internals

Quantizer APIs

The source exposes these VPTQ method signatures:

def __init__(self, module: nn.Module, qcfg: Optional[VPTQConfig] = None):
def add_batch(self, inp: torch.Tensor, out: Optional[torch.Tensor] = None) -> None:
def materialize_hessian(self, target_device: Optional[torch.device] = None) -> torch.Tensor:
def quantize(self) -> Tuple[VPTQQuantResult, float, float]:
def quantize_weight(
    self,
    weight: torch.Tensor,
    hessian: torch.Tensor,
    target_dtype: Optional[torch.dtype] = None,
) -> Tuple[VPTQQuantResult, float]:

These are signatures for methods in gptqmodel/quantization/vptq.py, not standalone runnable definitions. quantize() returns (result, duration_seconds, hessian_weighted_loss). quantize_weight() operates on [out_features, in_features] weights and a matching input Hessian and returns (result, loss).

VPTQ accepts nn.Linear and Transformers Conv1D. The latter stores its weight transposed, so quantization and replay explicitly convert layouts. Actual nn.Conv1d and nn.Conv2d modules are rejected: reconstructing a dense F.linear weight would not preserve their convolution semantics.

Quantization needs FP64 Hessian accumulation and Cholesky support on its execution devices. The portable inference-device declarations do not guarantee that every such device can execute calibration and quantization.

Learning and sequential quantization

Step Implementation behavior
Capture Flatten inputs into tokens, accumulate FP64 xᵀx under a lock in per-device buffers, then merge/divide by sample count
Coverage/dead channels Use identity Hessian if no usable calibration Hessian or configured low-coverage fallback applies; otherwise identify exactly zero diagonal channels and zero those weight columns while repairing the diagonal
Scale Hessian Normalize by its mean diagonal
Normalize/permutate Apply optional weight min/range normalization, form Hessian-diagonal k-means weights, then optionally reorder input columns and Hessian
Main codebooks Split each group's columns into output-axis vectors and run weighted Lloyd k-means
First pass Quantize successive input columns using nearest centroids and update unprocessed columns with the upper inverse-Hessian Cholesky factor
Residual codebooks When enabled, learn main-group residual centroids from first-pass error and rerun the pass with main-plus-residual lookup
Reconstruction Restore original input order, undo normalization, and round consistently with stored codebook/model dtype
Reporting Return reconstructed replay weights, codebooks/indices/metadata, normalized Hessian-weighted loss, and stats

_weighted_kmeans() seeds a device-local generator, initializes from a random permutation of training vectors, assigns nearest centroids in chunks, and updates centroids with weighted means. Empty clusters retain their previous centroids. The stopping condition compares inertia change to kmeans_tol * max(abs(previous_inertia), epsilon).

When a layer has fewer training vectors than requested centroids, initialization repeats vectors so the stored codebook still has exactly the configured number of entries. This preserves the layout contract; it does not reduce the codebook's storage cost. The algorithm is seeded, but that is not a guarantee of bitwise-identical floating arithmetic across devices.

Distance assignment sizes its chunks against a fixed 256 MiB workspace budget instead of constructing the complete vector-by-codebook distance matrix. Hessian-weighted loss evaluation sizes row chunks against a fixed 64 MiB budget. These are internal chunk targets, not total-memory limits: dense Hessians still scale quadratically with input width, and weights, codebooks, indices, and other temporaries consume additional memory.

_inverse_hessian_cholesky() adds percdamp * mean(diag(H)) to the diagonal before computing the upper Cholesky factor of the inverse. If the damped matrix is not positive definite, the error explicitly recommends increasing percdamp or supplying more calibration data. There is no adaptive damping retry loop here.

The reported loss is the ratio of summed Hessian-weighted squared reconstruction error to the corresponding weight energy, evaluated in FP64 with the absolute denominator clamped away from zero. It is not unnormalized scalar weight MSE, and it is not a model perplexity score.

VPTQQuantResult

The intermediate result is a dataclass, not the saved module itself:

Field Contents
centroids, indices Dictionaries for main/outlier centroid tensors and unpacked vector indices
res_centroids, res_indices Dictionaries for optional main residual tables and indices
weight_scale, weight_bias Optional normalization reversal tensors; None when disabled
perm Optional input permutation; None when disabled
outlier_size, group_size, group_num Resolved layer-specific layout
index_bits, res_index_bits Main/residual widths per vector index
stats Includes loss, sample count, duration, and fallback reporting as populated by the quantizer
quantized_weight Temporary floating replay weight, not a persistent inference weight cache

Dictionary key 0 is reserved for the outlier codebook when active; 1..group_num identify main groups.

VPTQProcessor lifecycle

Stage Behavior
Construction Receives calibration data/tokenizer/preparation controls; requires calibration forwards
Dataset verification Rejects missing calibration data; the processor does not support replacing its captured calibration dataset later
Preprocess Honors a layer exclusion, deep-copies config, creates VPTQ, resolves fallback and expected token count
Forward hook Passes captured input/output into add_batch()
Process Calls quantize(), releases task statistics, moves codebooks/indices/scales/permutation to CPU, logs duration/loss/sample counts, and installs reconstructed weights for replay
Conv1D replay Transposes the quantized linear-layout weight back to Transformers Conv1D storage order
Submodule finalization Creates VPTQLinear using qcfg.quant_linear_init_kwargs(), then calls pack(linear=..., vptq_result=result)
Finalization Marks model.quantized=True and sets method/format to VPTQ, then invokes shared finalization

The original dense weight and intermediate result are released as the packed module takes ownership of codebooks and indices. See Looper Processors and Memory Offload and Multi-GPU.

Coverage fallback is still vector quantization

VPTQ substitutes an identity Hessian when the captured matrix is unusable or the configured coverage threshold triggers. That keeps vector-codebook quantization available without inter-column Hessian compensation and avoids zeroing an entirely unvisited layer as if all its input channels were individually dead. The quantizer sets its fallback_used attribute and stores the value in result.stats["fallback"].

This is not scalar RTN, regardless of the shared fallback object's default strategy name. The VPTQ algorithm uses the threshold/coverage policy to choose Hessian-free vector quantization; it does not switch to QQQ/GPTQ scalar fallback math.

If fallback is disabled with fallback=None, the surrounding subset executor can remove never-invoked modules from quantization and record exclusions. Do not assume every MoE expert received the full dataset's token count. Review capture counts and exclusions when auditing coverage.

The inherited default fallback is enabled. Under that default, an unvisited module can still be packed using identity-Hessian vector quantization; pruning uncovered modules is specifically the disabled-fallback policy, not an unconditional behavior.

FORMAT.VPTQ and packed storage

Index stream

Main indices use a contiguous little-endian bitstream stored in signed torch.int32 words. Residual indices share the same stream:

merged = (residual_index << index_bits) | main_index
words_per_row = ceil(group_size * (index_bits + res_bits) / 32)

Indices may cross word boundaries. Signed int32 word values are storage containers for unsigned bit patterns; a negative packed word is not a negative centroid index. The layout matches the reference VPTQ bitstream, but bitstream compatibility alone is not full model-checkpoint compatibility.

The helpers are defined in gptqmodel/utils/vptq.py:

def vptq_index_bits(num_centroids: int) -> int:
def vptq_packed_words(num_elements: int, total_bits: int) -> int:
def vptq_group_setting(
    in_features: int,
    *,
    npercent: float,
    group_size: int,
    group_num: int,
    enable_outlier: bool = True,
) -> Tuple[int, int, int]:
def pack_vptq_indices(
    indices: torch.Tensor,
    index_bits: int,
    *,
    res_indices: Optional[torch.Tensor] = None,
    res_bits: int = 0,
) -> torch.Tensor:
def unpack_vptq_indices(
    packed: torch.Tensor,
    index_bits: int,
    num_elements: int,
    *,
    res_bits: int = 0,
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
Helper Contract
vptq_index_bits Computes ceil(log2(num_centroids)); count must be positive
vptq_packed_words Computes the int32 word count; element count nonnegative and total bits positive
vptq_group_setting Returns (outlier_size, resolved_group_size, resolved_group_num)
vptq_outlier_enabled Checks positive percentage/vector length and more than one outlier centroid
vptq_group_for_column Returns 0 for outlier columns and 1-based main group ids thereafter
pack_vptq_indices Validates index ranges/shapes, optional residual width, and total width no greater than 32
unpack_vptq_indices Reconstructs requested main indices and optional residual indices from packed words

For example, the default 12-bit main and 4-bit residual indices round-trip as follows:

import torch
from gptqmodel.utils.vptq import pack_vptq_indices, unpack_vptq_indices

main = torch.tensor([[0, 17, 4095]], dtype=torch.int64)
residual = torch.tensor([[1, 2, 15]], dtype=torch.int64)
packed = pack_vptq_indices(main, 12, res_indices=residual, res_bits=4)
decoded_main, decoded_residual = unpack_vptq_indices(
    packed, 12, num_elements=3, res_bits=4
)
assert torch.equal(main, decoded_main)
assert torch.equal(residual, decoded_residual)

Runtime state

Let G be resolved main group count, S resolved group size, N output width, L main vector length, C main centroid count, and R enabled residual centroid count. I = ceil(N / L) and P = ceil(S * (index_bits + res_bits) / 32).

State Shape Storage
centroids.weight (G, C * L) Floating, frozen embedding parameter; viewed as (G, C, L) during reconstruction
res_centroids.weight (G, R * L) when enabled Floating, frozen embedding parameter
indices (G, I, P) Packed torch.int32 main/residual indices
outlier_centroids.weight (1, outlier_centroids * outlier_vector_len) when enabled Floating, frozen embedding parameter
outlier_indices (1, ceil(N / outlier_vector_len), outlier_size) Unpacked torch.int32 indices; not a 16-bit buffer
weight_scale, weight_bias (in_features,) for norm_dim=0, (out_features,) for norm_dim=1 Floating normalization metadata when enabled
perm (in_features,) Persistent torch.int32 permutation when enabled
inverse_perm (in_features,) Non-persistent torch.int32, rebuilt from perm
bias (out_features,) when present Floating linear bias

Padding of output vectors and the last input group is removed during dequantization. Outlier indices use int32 to avoid wrapping codebook entries above the signed-int16 range. Codebooks and normalization metadata are stored in the module's codebook dtype, not universally FP32.

VPTQLinear.pack() requires a quantization result; it does not learn centroids merely from the dense linear argument:

def pack(self, linear: nn.Module = None, *, vptq_result=None, **kwargs):
def load_quant_result(self, result, bias: Optional[torch.Tensor] = None) -> None:
def dequantize(self) -> torch.Tensor:
def forward(self, x: torch.Tensor) -> torch.Tensor:

Packing rejects missing group codebooks, required residual codebooks/indices, and a missing required permutation. post_init() calls _validate_loaded_state(), which checks the packed indices shape and rebuilds the inverse permutation with argsort. It does not comprehensively validate every loaded tensor shape, index range, or permutation bijectivity; externally assembled checkpoints must satisfy the full layout contract rather than relying on this check to detect every malformed tensor.

For developers constructing VPTQLinear directly, its low-level defaults are not the VPTQConfig recipe: the constructor defaults to vector_lens=(8, 8), num_centroids=(4096, 4096), num_res_centroids=(-1, -1), and kmeans_mode="". The model lifecycle passes qcfg.quant_linear_init_kwargs() explicitly; do the same instead of assuming the default config and default runtime constructor select identical codebooks. The constructor uses FP16 codebooks unless dtype is one of FP16/BF16/FP32.

Inference backends

Shared contract and portable Torch

Property VPTQLinear / VPTQ_TORCH VPTQCudaLinear / VPTQ_CUDA
Method/format VPTQ / VPTQ Same
Nominal labels 1 through 8 Same
Packed dtype torch.int32 Same
Floating dtypes FP16, BF16, FP32 Same declared contract
Devices All Torch device types, subject to available operations CUDA
Platforms All declared Inherits same declaration; requires usable CUDA extension/toolchain
Feature divisibility 1; automatic padding supported Same layout/padding support
Shards Supported Supported
Training Not supported Not supported
Adapters Lora Lora

Torch inference unpacks indices, gathers main/residual/outlier centroids, removes padding, restores input order and normalization, then calls F.linear using the input dtype/device. Bias is converted to the activation dtype if needed; an adapter is applied afterward. Empty inputs return an appropriately shaped empty linear output.

optimize() compiles the dequantization graph, not its dense output. It chooses inductor by default except on MPS (aot_eager), and skips compilation on NPU. Every forward still reconstructs weights from the live packed indices/codebooks; no persistent dense weight cache is retained. This portable reference path can be costly for large matrices and very small batches because it reconstructs a dense weight for the current operation.

CUDA dispatch

VPTQCudaLinear.validate_once() rejects ROCm with an actionable NotImplementedError pointing at VPTQ_TORCH: the extension is built from CUDA sources and CUDA-only intrinsics that hipcc cannot compile, and because ROCm exposes the CUDA device namespace, torch.cuda.is_available() alone cannot be the gate. AUTO on AMD GPUs therefore selects the portable VPTQ_TORCH.

VPTQCudaLinear subclasses the Torch layer and reuses its state/packing. Its optimize() marks the layer optimized without compiling the portable decoder, because native inference does not use that graph.

The dispatch counts flattened activation rows, so [batch, sequence, hidden] contributes batch * sequence rows:

Condition Route
Zero rows Empty-output fast path
Non-CUDA activations Runtime error recommending VPTQ_TORCH
Activations and main VPTQ buffers on different CUDA devices Runtime error
At most 4 rows and activation dtype equals centroid dtype Fused vptq_linear native kernel; input is made contiguous
More than 4 rows, or activation/codebook dtype mismatch Native vptq_dequantize, convert reconstructed weight to input dtype, then F.linear/CUDA GEMM

The four-row threshold is _VPTQ_CUDA_FUSED_MAX_ROWS = 4; it applies across vector lengths and outlier configurations in the Python dispatcher, not just the vector-length-4 optimized native case. A dtype mismatch selects the reconstruction route instead of the fused route. Optional bias and adapter application are preserved.

The fused route reads packed indices/codebooks without first materializing a persistent full weight. The larger-row route temporarily reconstructs a dense weight once for that forward so GEMM can reuse it across rows. It does not cache the dense result between calls. Do not describe the backend as fused for every batch size or as uniformly avoiding temporary dense allocations.

VPTQ_CUDA is a CUDA backend, not a declared ROCm backend. On other accelerators use VPTQ_TORCH where the required Torch operations are available. The VPTQ Python contract does not specify a QQQ/Marlin-style minimum sm_80 capability check; a working target-compatible build of the native extension is still required.

Native extension and diagnostics

gptqmodel/utils/vptq_cuda.py defines TorchOpsJitExtension named gptqmodel_vptq_ops, namespace gptqmodel_vptq, with required operations vptq_dequantize and vptq_linear. Its native sources are gptqmodel_ext/vptq/vptq.cpp and vptq_kernel.cu. The central registry exposes vptq with vptq_cuda as an alias.

def clear_vptq_extension_cache() -> None:
def vptq_cuda_runtime_available() -> bool:
def vptq_cuda_runtime_error() -> str:
def prewarm_vptq_extension() -> bool:
Function/control Behavior
vptq_cuda_runtime_available() Returns False without an available CUDA device; otherwise asks the extension registry
vptq_cuda_runtime_error() Returns a no-CUDA message, empty string when available, or the registry/runtime diagnostic
prewarm_vptq_extension() Returns False without CUDA; otherwise loads registry extension vptq and returns its status
clear_vptq_extension_cache() Clears the extension helper's cache
GPTQMODEL_VPTQ_BUILD_ROOT Overrides build location
GPTQMODEL_VPTQ_FORCE_REBUILD Force-rebuild control
GPTQMODEL_EXT_VERBOSE Verbose extension build output

The operation wrappers vptq_cuda_dequantize() and vptq_cuda_linear() forward packed state plus keyword-only layout dimensions to the central registry. A representative source signature is:

def vptq_cuda_linear(
    x,
    indices,
    centroids,
    res_centroids,
    outlier_indices,
    outlier_centroids,
    inverse_perm,
    weight_scale,
    weight_bias,
    bias,
    *,
    group_size: int,
    in_features: int,
    out_features: int,
    vector_len: int,
    outlier_size: int,
    outlier_vector_len: int,
    num_centroids: int,
    num_res_centroids: int,
    index_bits: int,
    res_bits: int,
    norm_dim: int,
):

These are internal kernel wrappers, not codebook-learning APIs. Native validation checks CUDA placement/contiguity, int32 packed indices, consistent optional tensor pairs, dimension/index widths, and matching activation/codebook dtype for the fused op. Prefer the quantized module rather than manually assembling calls.

The repository's dependency baseline is Python >=3.10, PyTorch >=2.14.1, and Transformers >=5.20.2. Portable VPTQ does not require the native CUDA extension; CUDA inference additionally requires an appropriate compiler/toolkit, Ninja, and a successfully built/loaded extension. Refer to Installation and Quickstart for the supported installation procedure.

Checkpoint compatibility and limitations

Concern Current behavior
Torch versus CUDA Same FORMAT.VPTQ; choose the runtime at load time
Configuration persistence VPTQ serializes its vector/centroid/group/k-means/normalization/permutation/block/damping fields so the layer can reconstruct the same layout
Generic GPTQ loaders Not compatible: these are codebooks and indices, not GPTQ scalar qweight/qzeros/scales
External VPTQ checkpoints A compatible per-row index bitstream alone does not establish complete checkpoint compatibility; use the layout and metadata expected by GPT-QModel
Stock Transformers/vLLM/SGLang The repository guide does not expose these as supported VPTQ checkpoint runtimes; load through GPT-QModel's VPTQ backends
Mid-quantization checkpoint/resume Not available for the VPTQ processor chain; it is absent from the supported checkpoint adapter chains
Dynamic mixed recipes Nonempty per-layer overrides rejected; configure codebooks globally or use supported layer exclusion
Training Not supported by either VPTQ kernel contract; adapter application at inference is separate

The implementation has no separate persistent Hessian-file requirement or external VPTQ calibration phase. Normal model saving after a completed quantization is supported; that is different from resuming an interrupted quantization run.

Validation and benchmarking tools

docs/vptq.md is the repository-local companion guide. It covers the same algorithm/configuration, recipes, module restrictions, backend dispatch, and checkpoint limitations, and documents two benchmark entry points:

Script Purpose
scripts/benchmark_vptq_cuda.py Compare dispatch, direct fused native inference, native reconstruction plus GEMM, and portable Torch on identical layer state/activations
scripts/benchmark_vptq.py Model-level dense/VPTQ/GPTQ comparison, including perplexity and optional ARC/speed measurements
tests/test_vptq.py Independent quantization/index-layout oracles and runtime coverage

For a CUDA checkpoint/backend comparison:

python scripts/benchmark_vptq_cuda.py /path/to/checkpoint --dtype float16

For seeded synthetic packed layers without a model download:

python scripts/benchmark_vptq_cuda.py \
  --synthetic \
  --shapes 2048x2048,4096x4096 \
  --batch-sizes 1,8,32,128 \
  --dtype bfloat16 \
  --output ./vptq-backend-bf16.json

The script reports synchronized GPU-event timing and wall-clock latency, including median/minimum/p90 by layer and row count. Run FP16 and BF16 separately when comparing those deployment dtypes; retain the same codebook/activation state for backend comparisons. Synthetic performance is not a model-quality result. For model quality and baseline methodology, see Evaluation.

Pitfalls and edge cases

Problem Explanation/action
Setting only bits does not change compression Choose vector length and centroid counts; bits is derived
Actual checkpoint is larger than the nominal rate Include centroid tables, outlier indices, padding, normalization, and bias in storage accounting
group_num plus group_size fails Set only one; leave the other -1
Outliers unexpectedly consume extra channels Explicit group count with enabled outliers moves remainder columns into the outlier group
Nondivisible width creates a remainder Without outliers, the final main group is padded and trimmed; no implicit outlier codebook is invented
Packing reports a missing codebook despite group_num <= in_features Explicit group count without outliers can leave an empty trailing group after width rounding; use positive group_size or satisfy the nonempty-group invariant
Outlier residual configuration fails Keep num_res_centroids[0] = -1
nn.Conv1d/nn.Conv2d fails Only linear and Transformers Conv1D module semantics are supported
Cholesky factorization fails Increase VPTQ percdamp or improve calibration coverage; inherited GPTQ damping controls are not its retry mechanism
Unvisited/rare MoE expert Inspect counts, fallback stats, and exclusions; Hessian-free vector fallback differs from scalar RTN
CUDA path unexpectedly calls GEMM Expected for more than four flattened rows or mismatched activation/codebook dtypes
CUDA device mismatch Keep all packed state/metadata and activations on the same CUDA device
Portable inference uses substantial temporary memory It reconstructs a dense weight for the current forward; packed checkpoint size is not peak execution memory
Codebook count exceeds training-vector count Duplicate initial centroids preserve configured table size; overhead can dominate small layers
Attempting interrupted-run resume VPTQ does not implement the quantization checkpoint adapter chain

Source files

The behavior above follows the implementation at eb90dbdd. Key source paths:

Path Responsibility
gptqmodel/quantization/config.py VPTQConfig, derived bit labels, method/format values, validation and persistence
gptqmodel/quantization/vptq.py VPTQ, VPTQQuantResult, Hessian collection, weighted k-means, error compensation, loss
gptqmodel/looper/vptq_processor.py Calibration, quantization/replay, packed-module replacement
gptqmodel/looper/stage_subset.py Calibration coverage and uncovered-module policy
gptqmodel/nn_modules/qlinear/vptq_torch.py VPTQLinear, codebook/index state, pack/load/dequantize/Torch inference
gptqmodel/nn_modules/qlinear/vptq_cuda.py VPTQCudaLinear, small-row fused/native-GEMM dispatch
gptqmodel/utils/vptq.py Index packing/unpacking, bit counts, input grouping and outlier helpers
gptqmodel/utils/vptq_cuda.py Extension definition, diagnostics, native operation wrappers
gptqmodel_ext/vptq/vptq.cpp, vptq_kernel.cu, vptq.h Native operation registration, validation, dequantization, and linear kernels
gptqmodel/extension.py Extension registry and vptq_cuda alias
gptqmodel/utils/backend.py, gptqmodel/utils/importer.py Backend names, alias normalization, kernel selection
gptqmodel/models/base.py VPTQ processor dispatch and quantization entry point
gptqmodel/looper/gptq_checkpoint.py Supported mid-quantization checkpoint processor chains
docs/vptq.md Repository guide and recipes
scripts/benchmark_vptq.py, scripts/benchmark_vptq_cuda.py Quality and backend benchmarks
tests/test_vptq.py Reference/oracle and integration coverage
pyproject.toml, requirements.txt Dependency baseline

See also

Clone this wiki locally