0%

TurboQuant 详解

本文从基础的量化开始,介绍 TurboQuant(arXiv:2504.19874)的核心设计。

量化到底在做什么

一句话讲清楚

量化就是把连续实数映射成有限个离散值,用一点误差换更小存储和更高速度。

更形式化地说,它是一个映射:

其中 $B = b \cdot d$,$b$ 是每个坐标的平均 bit 数。还需要一个反映射(反量化):

这个变换天生有损——$Q$ 不是双射,所以永远不可能完美还原原始向量。能做的,是让损失变得可控。

为什么量化在 LLM 里很重要

大模型推理有三个硬约束:

显存:可近似写成 $\text{参数个数} \times \text{每个参数的字节数}$。

  • 同样 10 亿参数,FP16 约 2 GB,INT4 只需约 0.5 GB。

带宽:很多推理场景的瓶颈不在算力,而在 HBM 带宽(GPU 的”传送带”)。

  • 量化后需要搬运的数据变少,传送带压力下降,速度就上去了。

KV cache:Decoder-only 模型必须把每一层的 key/value embedding 存在 cache 里。

  • cache 大小随层数、注意力头数、上下文长度三重增长,是长上下文推理的核心瓶颈。

所以量化可以省显存,是否加速取决于硬件与实现。

例子

假设有一个长度为 4 的向量:$x = [0.8, -0.5, 0.2, -0.9]$,每个数字原本是 32-bit 的浮点数。

为了省显存,我们决定把每个数字压缩到 1-bit

1-bit 意味着我们只能使用 $2^1 = 2$ 种状态(0 或 1)。这就引出了量化的四个关键步骤:

1. Codebook(定义码本)

既然只有 2 种状态,我们就只能选出 2 个“代表值”来近似所有可能的数字。

这两个代表值可以人为规定,也可以用 K-Means 对数据进行聚类求中心点。

假设经过统计,得出最优的两个代表值是 {-0.6, 0.6}。这就是码本(Codebook)

2. Quant(量化/编码)

接下来,把原向量 [0.8, -0.5, 0.2, -0.9] 中的每个数,就近映射到码本里,并记录它的索引

  • 0.80.6 更近 $\to$ 选大号代表值,存为索引 1

  • -0.5-0.6 更近 $\to$ 选小号代表值,存为索引 0

  • 0.20.6 更近 $\to$ 选大号代表值,存为索引 1

  • -0.9-0.6 更近 $\to$ 选小号代表值,存为索引 0

最终存进显存的只有比特串:1010

这一步,32-bit 变成了 1-bit,这就是空间被省下来的地方。

3. Dequant(反量化/解码)

当 GPU 需要拿这串数字去做矩阵乘法时,它会根据索引去查码本,把比特串 1010 还原出来。

还原出的近似值是:$\hat{x} = [0.6, -0.6, 0.6, -0.6]$。可以看到,它与原向量 x 接近,但不完全一致。

4. Metric(评估误差)

因为还原回来的值与原值不相等,误差就产生了。

怎么评估这个量化算法好不好?

最常用的指标有两个:

  • MSE(均方误差):算一下原值 $x$ 和近似值 $\hat{x}$ 之间差异的平方和的平均值。

    即 $D_{\text{mse}} = \mathbb{E}\left[\lVert x - \hat{x}\rVert_2^2\right]$。

    这是评估失真最基础的指标。

  • IP-RMSE / IP-Bias(内积误差):在 LLM 的注意力机制和向量检索中,真正频繁做的是点积计算

    如果还原后的点积和真实的点积 $\langle x, y \rangle$ 总是有系统性地偏高或偏低,这就是 Bias(偏差)。

注意:不要只看 MSE。

MSE 低不等于内积一定准确,TurboQuant 的重要贡献之一就是修正内积偏差。

有损 vs 无损(容易混淆的地方)

在量化的语境里,很多人会把一些概念搞混:

  • 有损(Lossy):从 0.8 变成 0.6 这个过程(即 Quant),误差是绝对不可逆转的。因此,量化本身天生是有损的

  • 无损(Lossless):针对索引串 1010,如果能用类似 ZIP 压缩(如 Huffman 编码或 ANS 等信息论方法)把它压得更小,解压后依然是完美还原的 1010 索引串,这就叫无损压缩。

所以”无损压缩”不是说量化无损,而是指在索引编码层的无损存储压缩


论文的动机

在长上下文大模型推理(KV Cache 压缩)和亿级向量检索场景中,传统的量化方法面临着一个不可调和的矛盾。可以把现有方法简单分为两派:

痛点 1:简单方法(如均匀量化)速度快,但精度差、空间利用率低

均匀量化直接把数据按等间距切分(就像尺子上的等距刻度)。计算极快,不需要提前分析数据分布。但这在数据分布不均匀时会带来巨大的误差。

例子:

2-bit 意味着每个坐标只能落在 $2^2 = 4$ 个代表值之一。

如果用均匀码本(等间距):

最近邻量化后:

如果用非均匀码本(码本点更贴近数据密集的区域):

量化后:

这就是均匀量化最大的缺点:真实世界的数据(如模型激活值)往往集中在某一区域(比如高斯分布的数值大多集中在 0 附近),等间距切分会把大量宝贵的编码空间(bit)浪费在几乎没有数据的边缘区域。

就像把一条路分成 16 段测车流密度,如果车都挤在市中心,你却把郊区和市中心切得一样细,这就是在浪费分辨率。

这种浪费导致了均方误差(MSE)极大,同等空间下的精度极低

痛点 2:高级方法(如乘积量化 PQ / K-Means)精度高,但耗时且难以在线流式处理

既然均匀量化不好,那怎么找到上面的“非均匀最优码本”呢?

最经典的方法是 Lloyd-Max 算法(可以把它理解为一维数据上的连续 K-Means 聚类)。

它的核心思想是“两步交替优化”。用一个包含 4 个数字的极简数组 [1, 2, 8, 9] 来演示,假设把它量化成 $K=2$ 个代表值(1-bit):

初始化:瞎猜两个初始代表值(码本),比如 $c_1 = 3$ 和 $c_2 = 6$。

第一轮迭代:

  1. 固定代表值,更新边界(划定地盘):
    计算 3 和 6 的中点边界:$t = (3 + 6) / 2 = 4.5$。

    这就把数据切成了两块:小于 4.5 的归左边,大于 4.5 的归右边。

    所以:左阵营分到了 {1, 2},右阵营分到了 {8, 9}

  2. 固定边界,更新代表值(选新代表):
    计算每个阵营内部的“平均值”(均值能让该阵营内部的 MSE 最小):

    左边的新代表值:$c_1 = (1 + 2) / 2 = 1.5$

    右边的新代表值:$c_2 = (8 + 9) / 2 = 8.5$

第二轮迭代:

  1. 更新边界:计算新中点 $t = (1.5 + 8.5) / 2 = 5$。
  2. 更新代表值:小于 5 的还是 {1, 2},大于 5 的还是 {8, 9}

    阵营没有变化,新的代表值依然是 1.58.5

    由于没有变化,所以算法收敛,到此结束!

最终,找到了完美贴合数据分布的最优码本 {1.5, 8.5}

通过这种聚类找码本的方法,空间利用率极高,MSE 极低。

但这带来一个致命缺陷:需要在线聚类拟合(Online Fitting)!

每次来新数据,必须先攒齐一大批数据,跑一遍耗时的迭代算法才能得到最优码本。

在 KV Cache 这种 Token 逐字输出的场景下,根本等不起聚类的时间;在向量数据库中,这也意味着极其漫长的数据建索引时间。

痛点 3:指标错位,MSE 低不代表“内积”一定准

几乎所有的传统量化方法都在死磕如何降低 MSE(重建误差)。

但是在 LLM 的注意力机制(Attention)和向量检索中,底层频繁计算的其实是两个向量的内积(Inner Product),而不是简单衡量它们长得有多像。

还是刚刚的 2-bit

假设现在来了一个查询向量(Query)$y$:

我们先算一下 $y$ 和刚才那个真实原始向量 $x$ 的点积:

真实内积 $\langle y, x \rangle = -0.782$

在实际的量化检索中,我们拿不到真实的 $x$,只能用量化后的 $\hat{x}$ 来算内积,结果会怎样?

  • $y$ 与均匀量化结果 $\hat{x}_{\text{uniform}}$ 的内积 = -0.700 (内积绝对误差 $\Delta = 0.082$)
  • $y$ 与非均匀量化结果 $\hat{x}_{\text{non-uniform}}$ 的内积 = -0.710 (内积绝对误差 $\Delta = 0.072$)

尽管聚类优化把 MSE 降低了 41%,但内积误差只从 0.082 降到 0.072,改善幅度有限。

这种因为有损压缩引发的内积计算误差,往往不是正负抵消的随机噪声,而是会引入系统性的内积偏差(Bias)

如果内积估计有系统偏差,在向量检索的 Top-K 排序或 Attention 打分中就会出现持续的定向偏移


TurboQuant 的思路

论文尝试用一套方法同时处理上述三个痛点:

  1. 精度:要达到高级聚类算法的极限压缩精度(逼近信息论的理论下界);
  2. 速度:接近均匀量化的在线效率,免去在线拟合(Zero online fitting),支持数据的即写即查;
  3. 无偏:通过巧妙的残差补偿算法,彻底消除内积计算的系统性偏差

关键洞察:先旋转,让分布变得可预测

TurboQuant 的第一步是在量化前做一次随机正交旋转

核心直觉是:如果原始分布形态复杂(长尾、极值),在线切分和聚类会很慢;那能否先通过变换,把未知分布转成一种稳定、可建模的分布?

只要分布稳定,就可以离线(Offline)预计算码本,数据到达后直接查表(Look-up),从而去掉耗时的在线拟合。

发现:对于任意高维向量,乘上随机正交矩阵后,其坐标分布会收敛到固定形式,即(缩放/平移后的)Beta 分布。

TurboQuant_mse:例子

假设有一个极度“偏科”(长尾极值)的输入向量:

所有的能量都集中在第一维,第二维是 0。

直接拿它去量化,码本很容易“跑偏”。对它进行 1-bit(即 2 个代表值)的量化。

Step 1:准备旋转矩阵 $R$

系统预先生成好了一个随机正交矩阵 $R$。在 2D 里,我们可以假设它恰好是一个逆时针旋转 45 度的矩阵:

Step 2:旋转打散偏置($y = Rx$)

对 $x$ 乘以旋转矩阵 $R$:

可以看到,原本偏向单一维度的 [1.0, 0.0] 被打散成 [0.707, 0.707]

Step 3:查表量化($\hat{y}$)

既然现在的分布变成了极其标准的稳定分布,根据事先针对 Beta 分布离线算好的 1-bit 最优码本,假设算出来是 {-0.6, 0.6}

  • $y_1 = 0.707$ 离 $0.6$ 更近 $\to$ 映射为 $0.6$
  • $y_2 = 0.707$ 离 $0.6$ 更近 $\to$ 映射为 $0.6$

所以中间向量就变成了 $\hat{y} = [0.6,\ 0.6]$(存进显存的只有比特串,即索引 [1, 1])。

Step 4:反旋转重建($\hat{x} = R^\top \hat{y}$)

当矩阵乘法需要用到原始向量时,GPU 根据索引查出 [0.6, 0.6],再乘以反向旋转矩阵 $R^\top$ 进行恢复:

整个过程很快,旋转矩阵和码表都可以预先准备好,查表的复杂度是 O(1),量化的时候做一次矩阵乘法,重建的时候也是做一次矩阵乘法。


再看 TurboQuant_mse 的官方步骤会更直接:

  1. 生成随机正交矩阵 $R \in \mathbb{R}^{d \times d}$(对高斯矩阵做 QR 分解)。
  2. 旋转:$y = Rx$。
  3. 查表量化:对每维做标量量化 $yi \to c{k^}$,其中 $k^ = \arg\min_k |y_i - c_k|$。

    这里的码本 $C$ 就是针对 Beta 分布预先离线计算好的最优码本。

  4. 获取存储形式:得到 $\hat{y} = [c{k^*(1)}, \dots, c{k^*(d)}]$。
  5. 反旋转(重建):$\hat{x} = R^\top \hat{y}$。

再回过头来看其他收益

  • 打散方向性偏置:原始向量某些维度可能异常值很大,某些维度很平,这会让逐维量化不稳定。旋转可以把这种偏置分散到各维度。

  • 让逐维独立量化更合理:论文推导表明,随机旋转后各维度不仅分布稳定,而且近似相互独立

正交矩阵满足 $R^\top R = I$,因此 $|x|_2 = |Rx|_2$,向量长度和内积结构都保持不变。

更重要的一点是:旋转保持了优化目标不变——

在旋转域最小化 MSE,完全等价于在原始域最小化 MSE。

Q:针对 Beta 分布离线计算的最优码本,是否意味着所有数据都可共用一个固定码本?

A:是的。

这正是 TurboQuant 摆脱“在线拟合”的关键。

无论输入是文本 embedding 还是视觉特征,只要乘上随机旋转矩阵 $R$,其坐标分布就会落到固定的 Beta 形式。

所以只需准备这一个全局码本。这个全局码本只和两个参数有关:向量的维度 $d$目标压缩位数 $b$完全与输入数据无关

唯一需要注意的是,由于 Beta 分布的推导前提是向量在单位球面上(即长度为 1,$|x|_2 = 1$)。

所以在工程实现中,我们只需要把真实向量的“长度(L2 Norm)”单独抽出来存成一个 16-bit 浮点数,然后把剩下的方向向量(即除以长度归一化后的向量)交给这个固定的码本去查表量化,最后反量化重建时再把长度乘回去即可。

通过这种“前处理 + 静态全局码表”的设计,论文 Theorem 1 证明:对任意 $|x|_2 = 1$ 的向量,均方误差存在明确上界:

$b$ $D_{\text{mse}}$ 上界
1 ≈ 0.36
2 ≈ 0.117
3 ≈ 0.03
4 ≈ 0.009

这个上界已经非常接近信息论下界,两者仅差约 2.7 倍常数因子。

也就是说,TurboQuant_mse 在 MSE 方向已经接近理论极限。

到这里,通过前处理预旋转 + 全局固定离线码本查表,TurboQuant 已经解决前两个痛点:既保留免拟合的在线效率,也接近高级聚类方法的压缩精度。

TurboQuant_prod:用残差补偿修正内积偏差

论文提出两阶段方案来解决第三个痛点:指标错位,内积偏差。

假设真实向量是 $x = [0.8, 0.5]$。

有个查询向量 $y = [1.0, 0.0]$ 算内积,真实内积是 $\langle y, x \rangle = 0.8 \times 1.0 + 0.5 \times 0.0 = 0.8$。

假设用刚才讲的 TurboQuantmse 算法,把 $x$ 量化成了 $\hat{x}{\text{mse}} = [0.7, 0.6]$。

例子

约定把随机矩阵 $S$ 取成一个便于手算的 45 度旋转矩阵,并约定 $\operatorname{sign}(0)=+1$。

计算“漏掉的误差”

  • 残差 $r$:

  • 残差长度 $\gamma$:

压缩误差方向(QJL)

不能直接存残差 $r$,因为那太占空间了。

于是改用 QJL,只存它的“方向”。

  1. 随机旋转:把残差 $r$ 乘以矩阵 $S$。

    这里取

  2. 只取符号:不再存具体数值,只看正负。

    • $0.1414$ 是正的,对应 +1
    • $0$ 在这个例子里按 +1 处理

    所以得到

现在,原本复杂的残差向量就被压成了两个 1-bit 符号。

还原“补丁向量”

当你需要用它修正内积时,算法会根据 $q$ 和前面存下来的长度 $\gamma$ 还原出一个补丁:

  1. 先乘回 $S^\top$:

  2. 再乘上缩放系数:

    所以补丁向量约为

把补丁加回去

最终重建向量变成:

如果查询向量仍取 $y = [1.0, 0.0]$,那么新的内积就是:

真实内积是 0.8,而如果只用 MSE 量化,内积就是 $0.7 \times 1.0 + 0.6 \times 0.0 = 0.7$。

  • 阶段 A(大头):用 MSE 量化保证尽量靠近真实向量;
  • 阶段 B(修正):通常带有系统方向偏差,因此先计算残差,再用极小代价(1-bit)提取偏差方向,修复偏差。

工程落地:如何选 bit-width

核心框架

bit-width 选择本质是一个有约束的优化问题:

Cost 包含显存、时延、吞吐;Quality 包含任务分数、召回率、内积误差、生成质量。

最低 bit 不是最优方案,达标集合里成本最低才是。

先定三类门槛

上线前先写死:

  1. 质量门槛:任务分数下降不超过 N%,或检索 Hit@k 不低于阈值
  2. 性能门槛:P95 时延下降至少 M%,或在时延持平前提下降低显存
  3. 稳定性门槛:长上下文任务不能出现系统性退化

没有门槛,就没办法做理性比较。

论文给出的关键数据点

来自 KV cache 量化实验:

  • 3.5 bit/channel:可实现质量中性(quality neutrality)
  • 2.5 bit/channel:只有轻微质量退化(marginal degradation)

这两个数据点是论文的核心工程结论,可以直接作为初始实验的基准配置。

一个具体例子:KV Cache 在 prefill 和 decode 里怎么量化

prefill 阶段:写入多,读取少

假设用户一次性输入了 1024 个 prompt tokens。

prefill 的特点是:这 1024 个 token 的 K/V 要批量生成并写入 cache,但它们在这一轮里主要是“刚算出来就立刻参与注意力”,不会像 decode 那样被反复从历史 cache 中读取。

工程上比较常见的做法是:

  1. 模型先正常算出这一批 token 的 KV,这时它们还在 BF16/FP16
  2. 当前层 attention 直接消费这批高精度 K/V,完成本轮 prefill 计算
  3. 等本轮该写 cache 时,再对每个 token、每个 head 的 K/V 做 TurboQuant 压缩
  4. 压缩后的 bit-packed 结果写进持久化 KV cache,供后面的 decode 使用

这里的关键点是:prefill 阶段量化主要发生在“写 cache”这一步。

拿其中某一层、某一个 head、某一个 token 的 key/value 向量举例。原始向量长度是 128

用 TurboQuant_mse 时,这个 token 的 key/value 向量大致会经历下面几步:

  1. 先取 L2 norm:$s = |k|_2$
  2. 做归一化:$u = k / s$
  3. 乘随机正交矩阵:$y = Ru$
  4. y 的每个坐标查离线码本,得到 3-bit 索引
  5. 128 个 3-bit 索引打包成紧凑 bitstream
  6. 单独存下 s

这时候,prefill 的收益主要有两个:

  • 省显存1024 个 prompt tokens 如果都写成压缩 cache,后续 decode 的历史负担会小很多
  • 减少后续带宽:虽然 prefill 当下不一定明显变快,但后面每一步 decode 都要反复读取这批历史 KV

但要注意,prefill 本身不一定因为 KV 量化就显著加速

因为 prefill 往往更像一个大 batch 的矩阵计算,瓶颈常常在 GEMM,而不是在读历史 cache。

它更直接的收益是把“后面要背着走的包袱”先减掉。

decode 阶段:写入少,但每步都要读取历史 cache

decode 的情况完全不一样。

假设 prompt 已经是 1024 个 tokens,现在模型开始一个一个往后生成。

当生成第 1025 个 token 时,会发生两件事:

  1. 当前 token 会新产生一份 K/V,并立刻追加到 cache 末尾
  2. 当前 token 的 query 要和前面 1024 个 token 的所有 key 做注意力,再用所有 value 做加权求和

也就是说,decode 每走一步,都要把“历史上所有 token 的 KV cache”重新读出来一遍。

到了第 2048 个 token,这个读取量会比刚开始大很多。

所以 decode 更像是带宽受限的问题,而不是纯算力问题。

这正是 KV cache 量化最容易见到真实加速的地方。

对某一层来说:

  • 不压缩时,读 $1024$ 个历史 tokens 的 KV,需要约 $1024 \times 4\,\mathrm{KB} = 4\,\mathrm{MB}$
  • 压缩后,读同样 $1024$ 个历史 tokens,只需要约 $1024 \times 672\,\mathrm{B} \approx 672\,\mathrm{KB}$

读流量一下子从 4 MB 降到不到 0.7 MB

如果 decode 的瓶颈本来就在 HBM 带宽,这种下降就很可能直接转成时延收益。

decode 阶段到底怎么“用”这些压缩后的 KV

读压缩 cache 时,工程上一般有两条路:

第一条路:先反量化,再算 attention。

流程是:

  1. 从 HBM 读出 bit-packed 的 key/value
  2. unpack 出索引
  3. 按码本查表,恢复旋转域里的近似向量 $\hat{y}$
  4. 乘回 $R^\top$,再乘 norm,得到近似的 $\hat{k}, \hat{v}$
  5. 用这些近似值去做标准 attention

这条路实现最直接,但有个问题:如果先把整段历史 cache 全部恢复成 FP16 再送去 attention,中间会产生额外访存,容易抵消前面节省的带宽。

第二条路:把反量化融合进 attention kernel。

更实用的实现通常是:

  1. 读压缩 bitstream
  2. 在 kernel 内部边 unpack、边查表、边做缩放
  3. 立刻和当前 query 做点积,或者立刻参与 value 的加权累加

这样就不会在显存里额外 materialize 一份完整的 FP16 KV cache。

很多 KV cache 量化实现能否真正加速,差别往往在这里:不仅要能压缩,还要避免先解压成大块中间张量再计算。

为什么 prefill 和 decode 的优化重点不同

把前面的流程压缩成一句话:

  • prefill 更像“批量生产 + 一次性写库”,重点是低开销写入压缩 cache
  • decode 更像“每步反复读取历史”,重点是低带宽读取和融合式反量化

所以在工程上,大家通常会看到这样的现象:

  • prefill 的主要收益是显存下降,速度提升未必明显
  • decode 的主要收益是带宽压力下降,因此更容易带来实际吞吐提升

总结来说,KV cache 量化在 prefill 中更偏向“降低存储负担”,在 decode 中更偏向“换取时延与吞吐收益”。

放到 TurboQuant 里,这套流程有什么特别之处

TurboQuant 比较适合这个场景,不是因为它只追求压缩率,而是因为它有两个很工程化的性质:

  1. 零在线拟合:不会像 PQ / K-Means 那样,来了新 token 还得先聚类再编码。

    token 一算出来就能立即量化写 cache。

  2. 码本固定、查表简单:旋转后坐标分布稳定,码本可以离线预计算。

    在线阶段只做旋转、查表和 bit packing。

这两点对 decode 很关键。

因为 decode 是一个 token 一个 token 往前走的流式过程,任何需要“攒一批数据再拟合”的方案,到了这里都会非常难用。


参考文献

  • TurboQuant: “TurboQuant: Online Vector Quantization with Near-optimal Distortion Rate” (Zandieh et al., 2025) — arXiv:2504.19874
  • Lloyd-Max: “Least squares quantization in PCM” (Lloyd, 1982); “Quantizing for minimum distortion” (Max, 1960)
  • QJL: “Quantized Johnson-Lindenstrauss” (arXiv:2411.10355)
  • Shannon Source Coding: Shannon, C.E. (1948). “A Mathematical Theory of Communication”
  • KV Cache 量化: “KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cache” (Liu et al., 2024)