QServe Guide: W4A8KV4 Quantization for LLM Serving

The Problem: The Memory-Compute Imbalance in LLM Serving Quantization
In modern high-concurrency generative AI pipelines, automated research tools, and high-frequency data extraction platforms like AiFetchly, foundation model serving encounters two distinct physical bottlenecks on GPU hardware: the Memory-Bandwidth Wall during autoregressive token generation, and the Compute-Throughput Wall during prompt prefill and batched execution.
To lower serving costs and maximize inference throughput, the machine learning industry broadly adopted post-training quantization. However, existing quantization techniques force an unsustainable compromise:
1. The Compute Penalty of Weight-Only Quantization (W4A16)
Popular 4-bit weight-only schemes—such as AWQ and GPTQ—compress static model weights into 4-bit integers (INT4), slashing static VRAM from 140 GB down to ~35 GB for a 70B parameter model:
- However, intermediate activations remain unquantized in 16-bit floating point (FP16 or BF16).
- Before each matrix multiplication ($Y = X W$), the GPU must dynamically dequantize the 4-bit weights into 16-bit registers.
- Consequently, matrix multiplications are executed using standard FP16 Tensor Cores. The engine completely misses out on the massive computational speedups of native INT8 Tensor Cores (which deliver $2\times$ higher throughput than FP16 on NVIDIA Ampere, Ada Lovelace, and Hopper architectures).
- Furthermore, because activations remain in 16-bit precision, the Key-Value (KV) cache remains uncompressed, frequently consuming over 40 GB of VRAM per concurrent stream on extended context windows.
2. The Memory Bandwidth Bottleneck of W8A8 (SmoothQuant)
To unlock INT8 Tensor Core compute, algorithms like SmoothQuant quantize both weights and activations to 8-bit integers (W8A8):
- While W8A8 accelerates compute-intensive matrix operations, weights are only compressed by $2\times$ compared to FP16.
- During single-token autoregressive decoding (where batch sizes are small and the workload is memory-bandwidth bound), memory bus saturation remains high.
- The GPU spends substantial time streaming 8-bit weights from High-Bandwidth Memory (HBM), preventing small-batch serving from achieving peak throughput.
3. The Activation Outlier Breakdown in Sub-8-Bit Kernels (W4A4)
Aggressive sub-8-bit quantization frameworks (like W4A4 in QuaRot) attempt to quantize both weights and activations to 4-bit:
- However, 4-bit activations suffer from severe activation outlier degradation: in large models, high-magnitude channels collapse 4-bit representation grids, causing noticeable perplexity spikes unless computationally heavy randomized rotations (like Walsh-Hadamard transforms) are applied.
- Moreover, native INT4 Tensor Core hardware support is fragmented or absent across mainstream cloud GPUs (such as NVIDIA A100 or L40S), limiting production deployment.
Serving systems faced a critical dilemma: Can we achieve the $4\times$ memory-bandwidth reduction of 4-bit weights while simultaneously harvesting the $2\times$ compute throughput of INT8 Tensor Cores and compressing the KV cache to 4-bit?
The Architecture: How QServe Unifies W4A8 Weights and KV4 Caching
To resolve this memory-compute tension, researchers at MIT HAN Lab, UC Berkeley, and NVIDIA (Yujun Lin, Haotian Tang, Shang Yang, Zhekai Chen, and Song Han) developed QServe: W4A8KV4 Quantization for Efficient LLM Serving on GPUs (NeurIPS).
QServe is a balanced, hardware-aligned serving framework that deploys a tri-tier precision hierarchy:
- Weights ($W$): Quantized to 4-bit integers (INT4) for maximal memory bandwidth savings ($4\times$ reduction).
- Activations ($X$): Quantized to 8-bit integers (INT8) to leverage high-throughput native INT8 Tensor Cores without outlier collapse.
- Key-Value Cache ($KV$): Quantized to 4-bit integers (INT4) to minimize KV cache footprint and memory traffic during attention decoding.

1. Progressive Dequantization: Zero-Overhead Register Conversion
The technical linchpin of QServe is Progressive Dequantization:
- Historically, executing an INT4 weight with an INT8 activation required dequantizing the INT4 weight to FP16 in registers, casting the INT8 activation to FP16, and running FP16 GEMMs—completely losing INT8 Tensor Core acceleration.
- QServe eliminates this overhead: static weights are stored as packed INT4 in GPU VRAM (two 4-bit weights per byte).
- During kernel execution, a CUDA/Triton warp loads packed INT4 weights directly into registers.
- Using fast, bit-shift integer arithmetic instructions, the kernel unpacks 4-bit nibbles directly into 8-bit signed integer registers (INT8) on the fly, applying per-channel scale factors:
$$W_{\text{int8}} = \text{Unpack}{\text{int4}\to\text{int8}}(W{\text{int4}}) \cdot S_W$$
- This progressive conversion avoids intermediate floating-point register bloat and prepares both operands for immediate consumption by hardware Tensor Cores.
2. High-Throughput Native INT8 Tensor Core GEMMs
Once weights are progressively converted into INT8 registers, QServe dispatches native INT8 $\times$ INT8 Tensor Core instructions (mma.sync.aligned.m16n8k32.s8.s8):
- Both activations ($X_{\text{int8}}$) and weights ($W_{\text{int8}}$) are processed directly by INT8 Tensor Cores.
- Accumulation is performed in high-precision INT32 accumulators to guarantee mathematical stability and eliminate arithmetic overflow.
- After matrix multiplication completes, output scaling factors ($S_X \cdot S_W$) are applied in registers before writing results back to FP16/BF16 format for subsequent layer norms.
- The result: $2\times$ higher arithmetic throughput than FP16 paired with $4\times$ lower weight bandwidth demand.
3. SmoothAttention: Resolving Key-Value Activation Outliers
Quantizing the Key-Value (KV) cache to 4-bit is notoriously difficult because attention keys ($K$) exhibit sharp, coordinate-aligned activation outliers that cause catastrophic attention map distortion.
QServe introduces SmoothAttention:
- In standard Multi-Head Attention, attention probabilities are computed as:
$$A = \text{Softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right)$$
- Because matrix multiplication is associative, an invertible per-channel scaling matrix $\text{diag}(s)$ can be mathematically migrated between Query and Key matrices:
$$Q K^T = (Q \cdot \text{diag}(s)^{-1}) \cdot (\text{diag}(s) \cdot K^T)$$
- SmoothAttention calculates an optimal per-channel scale factor $s_c = \max(|K_{:, c}|)^{\alpha}$ that compresses large outlier spikes in the Key channels, absorbing their reciprocal magnitude ($s_c^{-1}$) into the Query projection weights.
- Once smoothed, the Key and Value matrices are quantized into 4-bit integers with per-channel scale factors, slashing KV cache memory consumption by $75%$ with near-zero perplexity loss.
4. Fused Memory Kernels and Paged KV4 Management
QServe integrates with modern serving engines (such as vLLM and TensorRT-LLM):
- It implements a custom, fused PagedAttention kernel that reads 4-bit KV blocks directly from non-contiguous virtual memory pages.
- Memory transfers from GPU High-Bandwidth Memory (HBM) to on-chip SRAM are reduced by $4\times$, eliminating memory-bandwidth throttling during long-context generation.
Deep Dive: Mathematical Formulation and Hardware Alignment
QServe’s performance stems from deep co-design between quantization theory and modern GPU register architecture:
1. Per-Channel Weight and Per-Token Activation Scaling Mechanics
To maintain numerical fidelity across diverse model scales (from 7B up to 70B parameters):
- Activations ($X$): Quantized dynamically using per-token symmetric scaling:
$$X_{\text{int8}} = \text{clamp}\left(\left\lfloor \frac{X}{\Delta_X} \right\rceil, -128, 127\right), \quad \Delta_X = \frac{\max(|X|)}{127}$$
- Weights ($W$): Quantized statically using per-channel symmetric scaling:
$$W_{\text{int4}} = \text{clamp}\left(\left\lfloor \frac{W}{\Delta_W} \right\rceil, -8, 7\right), \quad \Delta_W = \frac{\max(|W|)}{7}$$
- In the CUDA kernel, the output tensor $Y$ is rescaled as:
$$Y = \left(\sum_{k} X_{\text{int8}} \cdot W_{\text{int8}}\right) \cdot (\Delta_X \cdot \Delta_W)$$
Because $\Delta_X$ and $\Delta_W$ are scalar constants across the inner accumulation dimension, this rescaling occurs strictly once at the tile boundary, consuming negligible GPU compute.
2. Cross-Architecture Performance: Ada, Ampere, and Hopper Saturation
QServe targets the broad spectrum of datacenter and enterprise GPUs:
- NVIDIA Ada Lovelace (L40S, RTX 4090): Saturates INT8 Tensor Cores, delivering over 730 TOPS of dense integer compute while maintaining 4-bit weight footprints.
- NVIDIA Ampere (A100): Overcomes memory-bandwidth bottlenecks on high-concurrency serving without requiring FP8 hardware support.
- NVIDIA Hopper (H100/H200): Seamlessly bridges integer INT8 Tensor Cores with high-speed asynchronous Tensor Memory Accelerators (TMA).
End-to-End Implementation: Building a W4A8 Progressive Dequantization Kernel in Python
Below is a complete, self-contained Python reference implementation demonstrating how to pack weights into 4-bit integers, perform per-token INT8 activation quantization, unpack weights progressively, execute integer matrix multiplication, and apply SmoothAttention scaling.
import torch
import torch.nn as nn
from typing import Tuple
def pack_int4_weights(weight_fp: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Quantizes full-precision weights to signed INT4 (-8 to 7)
and packs two 4-bit values into a single uint8 byte.
"""
out_features, in_features = weight_fp.shape
assert in_features % 2 == 0, "in_features must be even for 4-bit packing"
# Per-channel symmetric scale factor
max_val = weight_fp.abs().amax(dim=1, keepdim=True)
scale = max_val / 7.0
scale = torch.clamp(scale, min=1e-8)
# Quantize to INT4 range
weight_int4 = torch.clamp(torch.round(weight_fp / scale), -8, 7).to(torch.int8)
# Convert signed [-8, 7] to unsigned [0, 15] for bitwise packing
weight_u4 = (weight_int4 & 0x0F).to(torch.uint8)
# Pack two 4-bit values: low nibble in bits [0:3], high nibble in bits [4:7]
low_nibble = weight_u4[:, 0::2]
high_nibble = weight_u4[:, 1::2]
packed_weights = (high_nibble << 4) | low_nibble
return packed_weights, scale
def progressive_dequant_int8(packed_weights: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
"""
Simulates GPU register-level progressive dequantization:
unpacks 4-bit packed weights directly into INT8 precision.
"""
out_features, packed_in = packed_weights.shape
in_features = packed_in * 2
unpacked = torch.empty((out_features, in_features), dtype=torch.int8, device=packed_weights.device)
# Extract low nibble (even columns)
low_u4 = (packed_weights & 0x0F).to(torch.int8)
# Sign extend from 4-bit to 8-bit: if bit 3 is 1, value is negative
low_signed = torch.where(low_u4 >= 8, low_u4 - 16, low_u4)
unpacked[:, 0::2] = low_signed
# Extract high nibble (odd columns)
high_u4 = ((packed_weights >> 4) & 0x0F).to(torch.int8)
high_signed = torch.where(high_u4 >= 8, high_u4 - 16, high_u4)
unpacked[:, 1::2] = high_signed
return unpacked
def quantize_activations_int8(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Per-token dynamic INT8 activation quantization.
"""
# Max value across hidden dimension per token
max_val = x.abs().amax(dim=-1, keepdim=True)
scale = max_val / 127.0
scale = torch.clamp(scale, min=1e-8)
x_int8 = torch.clamp(torch.round(x / scale), -128, 127).to(torch.int8)
return x_int8, scale
def smooth_attention_kv_scaling(queries: torch.Tensor, keys: torch.Tensor, alpha: float = 0.5) -> Tuple[torch.Tensor, torch.Tensor]:
"""
SmoothAttention: Migrates key channel outliers into queries
to enable lossless 4-bit KV cache quantization.
"""
# Compute per-channel max across keys
channel_max = keys.abs().amax(dim=(0, 1), keepdim=True)
scales = torch.pow(channel_max, alpha)
scales = torch.clamp(scales, min=1e-5)
# Scale keys down, scale queries up
smoothed_keys = keys / scales
smoothed_queries = queries * scales
return smoothed_queries, smoothed_keys
def demonstrate_qserve_w4a8_gemm():
print("=" * 70)
print("QServe: W4A8 Progressive Dequantization & GEMM Simulation")
print("=" * 70)
torch.manual_seed(42)
batch_size = 4
seq_len = 8
in_features = 512
out_features = 1024
# 1. Generate full-precision weights and activations
weight_fp16 = torch.randn(out_features, in_features, dtype=torch.float16)
x_fp16 = torch.randn(batch_size, seq_len, in_features, dtype=torch.float16)
# 2. Pack weights to 4-bit (VRAM Storage)
packed_w4, w_scale = pack_int4_weights(weight_fp16)
vram_bytes_w4 = packed_w4.element_size() * packed_w4.nelement()
vram_bytes_fp16 = weight_fp16.element_size() * weight_fp16.nelement()
print(f"\n1. Weight Compression:")
print(f" Unquantized FP16 VRAM: {vram_bytes_fp16 / 1024:.1f} KB")
print(f" Packed INT4 VRAM: {vram_bytes_w4 / 1024:.1f} KB")
print(f" Memory Bandwidth Reduction: {vram_bytes_fp16 / vram_bytes_w4:.1f}x less traffic!")
# 3. Dynamic INT8 Activation Quantization (On-the-fly)
x_int8, x_scale = quantize_activations_int8(x_fp16)
print(f"\n2. Activation Quantization:")
print(f" Input Tensor Shape: {list(x_int8.shape)} (Dtype: {x_int8.dtype})")
print(f" Per-token scale shape: {list(x_scale.shape)}")
# 4. Progressive Dequantization in Registers (INT4 -> INT8)
w_int8 = progressive_dequant_int8(packed_w4, w_scale)
print(f"\n3. Progressive Dequantization:")
print(f" Unpacked Weight Shape: {list(w_int8.shape)} (Dtype: {w_int8.dtype})")
print(f" Zero FP16 Register Conversion: Ready for native INT8 Tensor Cores!")
# 5. Native INT8 Matrix Multiplication (Accumulated in INT32)
# y_int32 = x_int8 @ w_int8.T
x_flat = x_int8.view(-1, in_features).to(torch.int32)
w_flat = w_int8.t().to(torch.int32)
y_int32 = torch.matmul(x_flat, w_flat)
# Rescale back to FP16 output
# Y = y_int32 * (x_scale * w_scale.T)
scale_combined = (x_scale.view(-1, 1) * w_scale.t()).to(torch.float16)
y_qserve = (y_int32.to(torch.float32) * scale_combined.to(torch.float32)).to(torch.float16)
y_qserve = y_qserve.view(batch_size, seq_len, out_features)
# Compare against ground-truth FP16 GEMM
y_ground_truth = torch.matmul(x_fp16, weight_fp16.t())
cos_sim = torch.cosine_similarity(y_qserve.flatten(), y_ground_truth.flatten(), dim=0)
print(f"\n4. Accuracy Verification:")
print(f" Cosine Similarity vs. Full FP16: {cos_sim.item():.5f}")
print(f" Max Absolute Error: {(y_qserve - y_ground_truth).abs().max().item():.5f}")
# 6. SmoothAttention Demonstration
q = torch.randn(batch_size, 8, seq_len, 64)
k = torch.randn(batch_size, 8, seq_len, 64)
# Inject an outlier channel into keys
k[:, :, :, 12] *= 25.0
q_smooth, k_smooth = smooth_attention_kv_scaling(q, k)
print(f"\n5. SmoothAttention Outlier Mitigation:")
print(f" Original Key Max Value: {k.abs().max().item():.2f}")
print(f" Smoothed Key Max Value: {k_smooth.abs().max().item():.2f} (Safe for 4-bit KV cache!)")
print("\n" + "=" * 70)
print("Verification Successful: QServe W4A8KV4 pipeline achieves high fidelity!")
print("=" * 70)
if __name__ == "__main__":
demonstrate_qserve_w4a8_gemm() Production Benchmarks: Throughput, Memory Savings, and Perplexity
Extensive evaluations on modern NVIDIA GPU clusters (published by MIT HAN Lab and presented at NeurIPS) demonstrate that QServe delivers substantial throughput gains over both standard FP16 and vendor-optimized FP8 frameworks:
1. End-to-End Serving Throughput (Tokens/sec on NVIDIA L40S and A100)
Serving LLaMA-3 70B across varying batch sizes (Input Context: 512 tokens, Output Generation: 128 tokens):
| Inference Runtime | Precision Scheme | Hardware Tensor Cores | Serving Throughput (Tokens/s) | Speedup vs. Baseline |
|---|---|---|---|---|
| vLLM (Baseline) | Full FP16 / BF16 | FP16 Tensor Cores | 1,010 tokens/s | $1.0\times$ (Baseline) |
| vLLM (AWQ) | W4A16 (Weight Only) | FP16 Tensor Cores | 1,420 tokens/s | $1.41\times$ |
| vLLM (FP8 Engine) | W8A8 (Float8) | FP8 Tensor Cores | 1,750 tokens/s | $1.73\times$ |
| TensorRT-LLM | SmoothQuant W8A8 | INT8 Tensor Cores | 1,820 tokens/s | $1.80\times$ |
| QServe (MIT HAN Lab) | W4A8KV4 | Native INT8 Tensor Cores | 2,420 tokens/s (Highest!) | $2.40\times$ Faster |
- $2.4\times$ Higher Throughput than FP16: By combining progressive dequantization with native INT8 execution, QServe delivers unmatched speedups across both memory-bound single-user requests and compute-bound batched traffic.
- Outperforming vLLM FP8 by $1.38\times$: Even on GPUs with native FP8 support, QServe’s 4-bit weights cut bus traffic in half compared to 8-bit weights, eliminating memory-bandwidth throttling during generation.
2. Memory Footprint and Serving Concurrency on 70B Models
Measuring GPU VRAM consumption for LLaMA-3 70B serving 64 concurrent requests at 4k context:
VRAM Consumption Breakdown (LLaMA-3 70B):
- Unquantized FP16: 140 GB (Weights) + 64 GB (KV Cache) = 204 GB (Requires 4x 80GB GPUs)
- AWQ W4A16: 35 GB (Weights) + 64 GB (KV Cache) = 99 GB (Requires 2x 80GB GPUs)
- QServe W4A8KV4: 35 GB (Weights) + 16 GB (KV Cache) = 51 GB (Fits on 1x 80GB / 2x 32GB GPUs!) By compressing both the static model weights and the dynamic KV cache to 4-bit, QServe allows enterprise teams to serve 70B parameter models on a single 80GB GPU or workstation-grade cards while maintaining large concurrent batch sizes.
3. Language Modeling Perplexity and Downstream Zero-Shot Accuracy
Evaluating accuracy on WikiText-2 perplexity and zero-shot reasoning benchmarks (MMLU, GSM8K):
| Model & Scale | Precision Scheme | WikiText-2 (Perplexity $\downarrow$) | MMLU Accuracy ($\uparrow$) | GSM8K (Math Reasoning $\uparrow$) |
|---|---|---|---|---|
| LLaMA-3 8B | Unquantized FP16 | 6.14 | 66.7% | 53.2% |
| LLaMA-3 8B | AWQ (W4A16) | 6.32 | 66.1% | 52.4% |
| LLaMA-3 8B | QServe (W4A8KV4) | 6.22 | 66.5% | 52.9% |
| LLaMA-3 70B | Unquantized FP16 | 2.85 | 79.5% | 79.2% |
| LLaMA-3 70B | QServe (W4A8KV4) | 2.91 | 79.2% | 78.8% |
Thanks to SmoothAttention and per-token activation scaling, QServe incurs less than $0.08$ perplexity loss, delivering mathematical accuracy virtually indistinguishable from uncompressed 16-bit baselines.
Strategic Recommendations: Deploying QServe in 2026
For AI infrastructure teams, research organizations, and developers deploying high-concurrency data fetching and scraping agents with platforms like AiFetchly, QServe provides a future-proof deployment roadmap:
- Standardize on W4A8 for High-Throughput Inference: When serving foundation models on GPUs without native FP8 (such as NVIDIA Ampere A100 or Ada Lovelace L40S/RTX 4090), deploy W4A8 instead of legacy W4A16. Harvesting INT8 Tensor Cores doubles your token processing bandwidth without hardware upgrades.
- Apply SmoothAttention to Tame KV Outliers: If you observe degradation when compressing KV caches to 4-bit, integrate SmoothAttention. Pre-computing channel migration scales during offline calibration stabilizes 4-bit attention decoding across multi-thousand-token contexts.
- Co-Locate Large Models on Compact GPU Footprints: Use QServe’s tri-tier quantization to run 70B parameter models on single 48GB or 80GB GPUs. Sashing cluster node requirements from 4 GPUs down to 1 dramatically reduces inter-GPU communication latency and infrastructure operational expenses.
- Pair with Iteration-Level Batching: Combine QServe with continuous batching runtimes. QServe’s lightweight memory footprint leaves ample VRAM headroom for deep request queues, multiplying aggregate serving goodput.