执行摘要
- 一句话:TurboQuant 注意力后端实现 2-4 倍 KV 缓存压缩
- 推荐动作:此 PR 引入了 KV 缓存压缩的一个有前途的方向。由于其巨大的性能和影响范围,建议在充分审查后合并。社区讨论揭示了关键的质量问题和修复,应优先集成已验证的修复(FP8 值默认,3位打包错误,Ampere FP8 兼容性)。考虑将 TurboQuant 作为 v1 的可选功能,并在文档中明确限制(仅全注意力,不适用于混合模型)。对于内存受限的生产场景,它提供了巨大的价值。
功能与动机
主存容量限制了 LLM 服务的典型上下文长度。在线 KV 缓存压缩允许相同硬件支持 2-4 倍的上下文长度,对内存受限场景(长序列、大批量)至关重要。TurboQuant 利用 WHT 旋转,使旋转后的各坐标近似独立,实现高效的标量量化,同时保持高精度。社区发现 QJL(有损压缩)有损精度,因此省略;WHT 旋转 + Lloyd-Max 提供了更好的质量/压缩平衡。
实现拆解
- 配置系统:在
vllm/model_executor/layers/quantization/turboquant/config.py 中定义 4 个命名预设,每个预设指定 key_quant_bits、value_quant_bits 和 norm_correction。通过 --kv-cache-dtype 选择预设,由 TurboQuantConfig 类解析并计算槽大小和位数。
- 量化基础:在
centroids.py 中实现 Lloyd-Max 最优量化器,使用梯形法则积分替代 scipy 依赖。在 quantizer.py 中生成 WHT 旋转所需的随机符号。
- 注意力后端:在
turboquant_attn.py 中实现 TurboQuantAttentionBackend,负责预填充(标准注意力 + 量化存储)和解码(分步 Triton 内核,逐步解码注意力和值累积)。后端注册为 TURBOQUANT 并支持 4 种 CacheDType。
- Triton 内核:在
triton_turboquant_store.py 中实现融合存储内核(_tq_fused_store_fp8 和 _tq_fused_store_mse),将桶化、残差范数、索引打包和值量化融合为单个内核。在 triton_turboquant_decode.py 中实现分步解码内核(stage1 完成评分和值累积,stage2 执行 log-sum-exp 约简)。
- 引擎集成:修改
attention.py 以在层初始化时调用 _init_turboquant_buffers(分配符号、质心和预分析缓冲区)。在 kv_cache_interface.py 中添加 TQFullAttentionSpec,覆盖 real_page_size_bytes。更新 cache.py 中的 CacheDType 字符串映射。更新 arg_utils.py 以支持边界层跳过(通过 kv_cache_dtype_skip_layers 或环境变量)。
- 测试和文档:添加
tests/quantization/test_turboquant.py,验证配置解析、打包大小、量化器正确性和 WHT 符号。在 docs/design/attention_backends.md 中记录新的后端。
关键文件:
vllm/model_executor/layers/quantization/turboquant/config.py(模块 配置层;类别 source;类型 data-contract;符号 TurboQuantConfig, TQ_PRESETS, key_fp8, mse_bits): 核心配置,定义 TQ 预设和参数计算,决定所有量化行为
vllm/v1/attention/backends/turboquant_attn.py(模块 注意力后端;类别 source;类型 core-logic;符号 TurboQuantAttentionBackend, TurboQuantAttentionImpl, _build_hadamard, get_kv_cache_shape): 注意力后端核心实现,管理预填充存储和解码流程
vllm/model_executor/layers/quantization/turboquant/centroids.py(模块 量化器;类别 source;类型 data-contract;符号 solve_lloyd_max, get_centroids, _gaussian_pdf, _trapz): Lloyd-Max 量化器实现,替代 scipy 依赖
tests/quantization/test_turboquant.py(模块 测试;类别 test;类型 test-coverage;符号 TestTurboQuantConfig, TestLloydMax, TestWhtSigns): 单元测试验证配置、打包大小、量化器正确性和 WHT 符号
vllm/model_executor/layers/attention/attention.py(模块 注意力层;类别 source;类型 data-contract;符号 _init_turboquant_buffers): 集成 TQ 缓冲区初始化到注意力层,预分配解码中间缓冲区
vllm/v1/attention/ops/triton_turboquant_decode.py(模块 操作核;类别 infra;类型 infrastructure;符号 triton_turboquant_decode_attention, _tq_decode_stage1): Triton 解码内核,分时注意力和值累积
vllm/v1/attention/ops/triton_turboquant_store.py(模块 操作核;类别 infra;类型 infrastructure;符号 _tq_fused_store_fp8, _tq_fused_store_mse, triton_turboquant_store): Triton 存储内核,将量化融合为单个启动
vllm/v1/kv_cache_interface.py(模块 接口层;类别 source;类型 core-logic;符号 TQFullAttentionSpec, real_page_size_bytes, merge): 添加 TQFullAttentionSpec,支持紧凑槽大小
关键符号:TurboQuantConfig.from_cache_dtype, TurboQuantAttentionImpl.forward, solve_lloyd_max, triton_turboquant_store, triton_turboquant_decode_attention, _init_turboquant_buffers
关键源码片段
vllm/v1/attention/backends/turboquant_attn.py
注意力后端核心实现,管理预填充存储和解码流程
# SPDFileCopyrightText: Copyright contributors to the vLLM project
"""TurboQuant attention backend for vLLM.
Prefill: Standard scaled dot-product attention on uncompressed K/V,
then quantize K and store K+V into combined cache slot.
Decode: Compute TQ attention scores from compressed cache,
unpack FP16 values, softmax + weighted sum.
Cache layout (no leading 2 dimension):
(num_blocks, block_size, num_kv_heads, slot_size)
where slot_size = key_packed_size + value_fp16_size
"""
import functools
import math
from dataclasses import dataclass
from typing import ClassVar
import torch
from vllm.config.cache import CacheDType
from vllm.v1.attention.backend import AttentionBackend, AttentionImpl, AttentionType, MultipleOf
from vllm.v1.attention.ops.triton_turboquant_decode import triton_turboquant_decode_attention
from vllm.v1.attention.ops.triton_turboquant_store import triton_turboquant_store
class TurboQuantAttentionBackend(AttentionBackend):
"""Attention backend using TurboQuant KV-cache compression."""
accept_output_buffer: bool = True
forward_includes_kv_cache_update: bool = False
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
"turboquant_k8v4",
"turboquant_4bit_nc",
"turboquant_k3v4_nc",
"turboquant_3bit_nc",
]
@staticmethod
def get_name() -> str:
return "TURBOQUANT"
@staticmethod
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
return [16, 32, 64, 128]
@classmethod
def supports_attn_type(cls, attn_type: str) -> bool:
return attn_type == AttentionType.DECODER
@staticmethod
def get_impl_cls() -> type["TurboQuantAttentionImpl"]:
return TurboQuantAttentionImpl
@staticmethod
def get_builder_cls() -> type["TurboQuantMetadataBuilder"]:
return TurboQuantMetadataBuilder
@staticmethod
def get_kv_cache_shape(
num_blocks: int,
block_size: int,
num_kv_heads: int,
head_size: int,
cache_dtype_str: str = "turboquant_4bit_nc",
) -> tuple[int, ...]:
"""Combined K+V cache shape — no leading 2 dimension.
Standard backends use (2, num_blocks, block_size, num_kv_heads, head_size).
TQ packs K+V into one slot per head: (num_blocks, block_size, num_kv_heads, slot_size_aligned).
"""
from vllm.model_executor.layers.quantization.turboquant.config import TurboQuantConfig
from vllm.model_executor.layers.quantization.turboquant.config import TQ_PRESETS
tq_config = TurboQuantConfig.from_cache_dtype(cache_dtype_str, head_size)
slot = tq_config.slot_size
# Round up to even boundary for alignment
aligned_slot = ((slot + 1) // 2) * 2
return (num_blocks, block_size, num_kv_heads, aligned_slot)
vllm/model_executor/layers/quantization/turboquant/centroids.py
Lloyd-Max 量化器实现,替代 scipy 依赖
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Lloyd-Max optimal scalar quantizer for TurboQuant.
After rotating a d-dimensional unit vector by a random orthogonal matrix,
each coordinate approximately follows N(0, 1/d) for d >= 64.
We solve the Lloyd-Max conditions to find optimal centroids.
Based on: turboquant-pytorch/lloyd_max.py (Zandieh et al.)
"""
import math
from functools import lru_cache
import torch
def _gaussian_pdf(x: float, sigma2: float) -> float:
return (1.0 / math.sqrt(2 * math.pi * sigma2)) * math.exp(-x * x / (2 * sigma2))
def _trapz(f, a: float, b: float, n: int = 200) -> float:
"""Trapezoidal numerical integration (replaces scipy.integrate.quad)."""
h = (b - a) / n
result = 0.5 * (f(a) + f(b))
for i in range(1, n):
result += f(a + i * h)
return result * h
def solve_lloyd_max(
d: int,
bits: int,
max_iter: int = 200,
tol: float = 1e-10,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Solve Lloyd-Max optimal quantizer for N(0, 1/d) distribution.
Args:
d: Vector dimension (determines variance = 1/d).
bits: Number of quantization bits.
max_iter: Maximum Lloyd-Max iterations.
tol: Convergence tolerance.
Returns:
centroids: Sorted tensor of 2^bits optimal centroids.
boundaries: Sorted tensor of 2^bits - 1 decision boundaries.
"""
n_levels = 2 ** bits
sigma2 = 1.0 / d
sigma = math.sqrt(sigma2)
def pdf(x):
return _gaussian_pdf(x, sigma2)
lo, hi = -3.5 * sigma, 3.5 * sigma
centroids = [lo + (hi - lo) * (i + 0.5) / n_levels for i in range(n_levels)]
for _ in range(max_iter):
boundaries = [
(centroids[i] + centroids[i + 1]) / 2.0 for i in range(n_levels - 1)
]
edges = [lo * 3] + boundaries + [hi * 3]
new_centroids = []
for i in range(n_levels):
a, b = edges[i], edges[i + 1]
num = _trapz(lambda x: x * pdf(x), a, b)
den = _trapz(pdf, a, b)
new_centroids.append(num / den if den > 1e-15 else centroids[i])
if max(abs(new_centroids[i] - centroids[i]) for i in range(n_levels)) < tol:
break
centroids = new_centroids
boundaries = [(centroids[i] + centroids[i + 1]) / 2.0 for i in range(n_levels - 1)]
return (
torch.tensor(centroids, dtype=torch.float32),
torch.tensor(boundaries, dtype=torch.float32),
)
@lru_cache(maxsize=32)
def get_centroids(d: int, bits: int) -> torch.Tensor:
"""Get precomputed Lloyd-Max centroids (cached)."""
centroids, _ = solve_lloyd_max(d, bits)
return centroids
vllm/model_executor/layers/attention/attention.py
集成 TQ 缓冲区初始化到注意力层,预分配解码中间缓冲区
def _init_turboquant_buffers(
self, cache_dtype: str, head_size: int, prefix: str
) -> None:
"""Initialize TurboQuant rotation/projection matrices and centroids.
Registers buffers so model.to(device) moves them to GPU *before*
the memory profiler runs. Pre-allocate decode intermediate buffers
to avoid OOMs on the first decoder call.
"""
from vllm.model_executor.layers.quantization.turboquant.centroids import get_centroids
from vllm.model_executor.layers.quantization.turboquant.config import TurboQuantConfig
from vllm.model_executor.layers.quantization.turboquant.quantizer import generate_wht_signs
from vllm.model_executor.models.utils import extract_layer_index
tq_config = TurboQuantConfig.from_cache_dtype(cache_dtype, head_size)
# Each layer uses a unique seed (stride 1337) to decorrelate quantization errors
layer_idx = extract_layer_index(prefix)
seed = tq_config.seed + layer_idx * 1337
self.register_buffer(
"_tq_signs",
generate_wht_signs(head_size, seed=seed),
)
self.register_buffer(
"_tq_centroids",
get_centroids(head_size, tq_config.centroid_bits),
)
self._tq_config = tq_config
# Pre-allocate decode intermediate buffers so the profiler doesn't give all memory to KV cache
_vllm_cfg = get_current_vllm_config()
B = _vllm_cfg.scheduler_config.max_num_seqs
Hq = self.num_heads
S = _vllm_cfg.attention_config.tq_max_kv_splits_for_cuda_graph
D = head_size
self.register_buffer("_tq_mid_o_buf", torch.empty(B, Hq, S, D + 1, dtype=torch.float32), persistent=False)
self.register_buffer("_tq_output_buf", torch.empty(B, Hq, D, dtype=torch.float32), persistent=False)
self.register_buffer("_tq_lse_buf", torch.empty(B, Hq, dtype=torch.float32), persistent=False)
评论区精华
1. 独立后端 vs 集成:@mgoin 最初希望集成到现有后端,但经过 sig-quantization 会议讨论,决定采用独立后端以隔离开发和追求极端性能。
2. 质量控制:早期测试显示 GSM8K 得分为 0%,社区追查到默认 4 位值量化过度。@varjoranta 建议 FP8 值默认(k8v4),@MidasMining 测试显示值比键对精度更敏感,支持非对称 K/V 位分配。
3. 3 位 MSE 打包错误:@varjoranta 和 @Cklaus1 发现 tq4 预设中 3 位 MSE 路径损坏,原因为线性桶化扫描和硬编码 2 位中心数。作者确认修复。
4. 边界层保护:@mgoin 质疑将 TQ_BOUNDARY_LAYERS 作为环境变量的合理性,作者解释首尾层精度敏感性的必要性。最终改用已有的 kv_cache_dtype_skip_layers 参数。
5. 代码优化讨论:@lishunyang12 指出 .item() 导致主机-设备同步、流重叠方向倒置、_ensure_on_device 损坏 register_buffer 语义、连续预填充缓冲区重复分配等问题。作者逐一修复。
6. 范围削减:作者决定将首次合并限制为仅全注意力和统一滑动窗口模型,排除混合模型以加速审查(基于与 @mgoin 的讨论)。
7. KV cache spec 兼容性:@lishunyang12 指出 isinstance 检查可能意外包含 MLAAttentionSpec,要求显式白名单。
- 独立后端 vs 集成 (design): 采用独立后端策略,作为首个版本。
- 质量控制:默认值量化位宽 (correctness): 默认预设改为 k8v4(FP8 键+4 位值),修复质量问题。
- 3 位 MSE 打包错误 (correctness): 修复 3 位打包路径,线性桶化改为通用 constexpr 二进制搜索。
- 边界层保护设计 (design): 迁移至 kv_cache_dtype_skip_layers,并添加边界层跳过文档。
- KV cache spec 兼容性:isinstance 回归 (correctness): 改为显式白名单 FullAttentionSpec 和 TQFullAttentionSpec。
风险与影响
- 风险:
- 性能回归:解码吞吐量比基线低 20-30%(k8v4 为 79%),但长序列预填充持平或略快。TPOT 和 TTFT 在短序列上更高。对于内存受限的长上下文场景,整体收益仍可观。
- 质量风险:极端预设(3bit_nc)显示 GSM8K 下降 5-10%(82% vs 87.5%)。边界层跳过可缓解,但环境变量默认值(最早/最后 2 层)可能不足以保护所有模型。
- 兼容性风险:仅与 v1 注意力后端配合使用。缓存布局无前导 2 维度,可能破坏 KV 卸载、前缀缓存等功能。不适用于混合注意力模型(Mamba+Attention)。未测试与 LoRA、推测解码的互操作。
- 维护风险:独立后端复制了其他后端的部分逻辑,增加未来重构和 API 变更的维护成本。
- 影响:
- 用户:简单启用(
--kv-cache-dtype turboquant_k8v4)可获得 2.6 倍压缩,适合长上下文和大批量场景。用户可能需要调节环境变量优化特定模型的质量/性能。
- 系统:对于典型模型,内存节省约 2.6x,可立即扩展上下文长度或批量大小。对于内存受限的部署(如单 GPU),其影响可达数倍。
- 团队:需要维护独立注意力后端和 Triton 内核。社区贡献活跃,需要整合。当前范围限制(仅全注意力)降低了覆盖面,但为后续扩展留下了空间。
- 风险标记:质量退化在极端预设下, 解码性能降级 20-30%, 独立后端兼容性未知, MLA 回归因 isinstance 更改, 内置基准测试代码不应合并
关联脉络
- PR #39064 [Bugfix] Fix IMA when grouped shared KV layers exist: 在 PR 评论中被 @vibhavagarwal5 引用,用于修复 Qwen3.5 非法内存访问问题
- PR #33695 [Feature] Add
kv_cache_dtype_skip_layers engine parameter: TurboQuant 的边界层保护机制利用此已有参数
- PR #38280 [Attention Backend] TurboQuant 2-bit KV cache (closed): 早期的 TurboQuant PR,已关闭,部分设计讨论被合并至当前 PR
参与讨论