一个长上下文服务里的显存账
假设有一台单卡 80GB 的推理服务器,跑一个 70B 级别的模型,上下文窗口开到 128K。权重按 fp16 存放已经占去大部分显存,剩下给 KV Cache 的空间非常有限。随着对话轮次增加,KV Cache 按 token 线性增长,很快就把可用显存吃满,服务开始拒绝新请求或被迫截断上下文。
直觉上的解法是把 KV Cache 压到更低精度。权重和激活的量化在工业界已经比较成熟,4-bit 甚至更低的方案都有落地案例。于是很自然的想法是:KV Cache 也是张量,为什么不能照搬同样的逐 token 量化?
问题恰恰出在这里。把 KV Cache 按 token 逐个量化,在短上下文、低压缩率下看起来还行,一旦上下文拉长、比特数降到 4-bit 以下,生成质量会明显下滑:长文档问答开始答非所问,代码补全出现重复片段,多轮对话丢失早期约束。这个失效不是调参能解决的,它和旋转位置编码(RoPE)的数学结构直接相关。
长上下文推理的瓶颈与基线量化
KV Cache 是自回归解码时缓存的历史 Key 和 Value 张量。每生成一个新 token,模型只需要计算当前 token 的 Query,再和缓存的全部 Key 做注意力,避免重复计算历史 token。代价是缓存随序列长度线性增长。
以常见的多头注意力为例,单个 token 的 KV Cache 大小约为 2 × 层数 × 头数 × 头维度 × 数据类型字节数。层数、头数、头维度由模型结构固定,所以显存占用基本正比于序列长度。在长上下文场景里,这一项往往超过权重本身,成为显存的主要消耗者。
量化的基本思路是给一组数值找一个缩放因子(scale)和零点(zero point),把浮点数映射到低位整数区间。反量化时用同样的参数还原。误差来源有两类:一是映射本身的舍入误差,二是量化范围(range)被少数极端值撑大,导致其余数值挤在很窄的区间里,分辨率不足。
逐 token 量化(per-token quantization)把每个 token 的 KV 向量当作一个量化组,为这个向量算一组 scale。它的假设是:同一个 token 内部的数值分布比较集中,用一组参数描述足够。这个假设在激活量化里经常成立,但放到经过 RoPE 处理的 Key 上就不成立了。
RoPE 在维度间制造了数值差异
RoPE 的核心操作是把位置信息编码成旋转。具体做法是把 Key 向量的维度两两配对,每一对看作二维平面上的一个点,然后按 token 的位置乘以一个旋转角度。位置越靠后,旋转角度越大。
旋转本身不改变向量的长度,只改变方向。但它带来的一个副作用是:同一对维度里的两个分量,会随着位置变化在数值上此消彼长。原本在某个位置可能两个分量都接近零,旋转到另一个位置后,一个分量变大、另一个变小。
更关键的是不同维度对的旋转频率不同。低频维度对旋转慢,高频维度对旋转快。RoPE 论文(Su 等,RoFormer,arXiv:2104.09864)指出,这种设计让注意力具备随相对距离衰减的依赖关系,并支持序列长度外推。代价是:同一个 token 的 Key 向量内部,不同维度对的数值分布差异被拉大。
这就破坏了逐 token 量化的前提。逐 token 量化假设一个 token 内部数值集中,但 RoPE 之后,某些维度对可能落在很大的数值区间,另一些维度对落在很小的区间。用一组 scale 去覆盖整个向量,要么被大值撑大范围、小值分辨率不足,要么为了照顾小值而让大值溢出。
用一个具体场景说明。假设一个 128K 上下文的企业知识库问答服务,用户上传了一份长合同,提问“第 37 条里的违约金比例是多少”。模型需要在注意力里精确定位到第 37 条附近的 token。这些 token 的 Key 经过 RoPE 后,位置编码差异体现在旋转角度上。如果逐 token 量化把 Key 的数值误差放大,注意力分数就会偏移,模型可能把注意力落到相邻条款上,答案里的比例数字随之出错。
旋转感知量化的两条路线
既然误差来自 RoPE 之后的数值分布,一个直接的思路是:不要在 RoPE 之后量化,而是在 RoPE 之前量化。
KVQuant(Hooper 等,arXiv:2401.18079)提出的 Pre-RoPE Key Quantization 就是这个思路。它在旋转位置编码施加之前对 Key 做量化,避开 RoPE 对数值分布的扰动。同一篇工作还提出逐通道 Key 量化(Per-Channel Key Quantization),即改变量化的维度方向,沿着通道而不是沿着 token 来分组,让量化范围更好地匹配分布。此外它用非均匀数据类型和逐向量稠密稀疏量化来处理离群值。资料显示,这些方法组合后,在 3-bit 量化下对 Wikitext-2 和 C4 的困惑度退化小于 0.1,并支持在单张 A100-80GB 上服务 LLaMA-7B 的百万级上下文。
另一条路线是旋转感知量化,代表工作是 QuaRot(Ashkboos 等,arXiv:2404.00456)。它的做法不是回避旋转,而是主动引入旋转来消除离群值。QuaRot 对隐藏状态、前馈组件、注意力机制的部分以及 KV Cache 施加旋转,这种旋转在数学上不改变模型输出,但能把原本集中在少数维度的离群值摊开到所有维度上,让量化范围更均匀。资料显示,QuaRot 能对权重、激活和 KV Cache 做端到端 4-bit 量化,4-bit 的 LLaMa2-70B 在 WikiText-2 上困惑度损失最多 0.47,保留 99% 的零样本性能;对 6-bit 和 8-bit 模型,用最近舍入量化可以做到无校准数据的无损压缩。
两条路线的共同点是:都承认 RoPE 之后的 Key 分布不适合直接逐 token 量化,区别在于一个选择在旋转前量化,另一个选择用额外的旋转把分布“熨平”。
逐通道、分组与旋转量化的对比
理解这几种方案的差异,需要先明确量化分组的维度。假设 Key 张量的形状是 [序列长度, 头数, 头维度],那么:
- 逐 token 量化沿头维度分组,每个 token 一组 scale。
- 逐通道量化沿序列维度分组,每个通道(头维度上的一个位置)一组 scale。
- 分组量化在头维度上切出若干连续片段,每段一组 scale。
RoPE 的旋转是作用在头维度上的成对维度,所以它主要扰动的是头维度方向的分布。逐 token 量化恰好沿着这个被扰动的方向分组,因此受影响最大。逐通道量化沿着序列方向分组,每个通道独立统计,受 RoPE 影响较小,但需要为每个通道存一组 scale,元数据开销随头维度线性增长。分组量化是折中,组越小越接近逐通道,元数据越多;组越大越接近逐 token,RoPE 误差越明显。
| 方案 | 分组维度 | 对 RoPE 误差的敏感度 | 元数据开销 | 典型适用条件 |
|---|---|---|---|---|
| 逐 token 量化 | 沿头维度 | 高,旋转直接扰动分组内分布 | 低,每 token 一组 | 短上下文、8-bit 以上、对精度要求宽松 |
| 逐通道量化 | 沿序列维度 | 低,各通道独立统计 | 高,随头维度增长 | 长上下文、低比特、显存允许存 scale |
| 分组量化 | 头维度切段 | 中,取决于组大小 | 中,组数决定 | 需要在精度和开销间平衡 |
| 旋转感知量化 | 先旋转再分组 | 低,旋转摊平离群值 | 中到高,需存旋转相关参数 | 端到端低比特、可接受额外计算 |
这张表里的“敏感度”是定性判断,来自 RoPE 作用维度与量化分组维度的关系,不是某个基准上的实测数字。实际选择还要看硬件对元数据读取的支持、kernel 实现成熟度以及是否允许校准。
数据流与实现边界
下面用一个流程图描述长上下文服务里,Key 从生成到被量化存储、再被注意力读取的路径,以及旋转感知量化在哪个环节介入。
flowchart TD
A[新 token 的隐藏状态] --> B[线性投影得到 Key]
B --> C{量化时机}
C -->|逐 token 基线| D[施加 RoPE 旋转]
D --> E[按 token 分组量化]
E --> F[存入 KV Cache]
C -->|Pre-RoPE 路线| G[旋转前量化 Key]
G --> H[施加 RoPE 旋转]
H --> F
C -->|旋转感知路线| I[施加额外旋转摊平离群值]
I --> J[按通道或分组量化]
J --> F
F --> K[解码时读取并反量化]
K --> L[与 Query 做注意力]
流程里有几个关键转折点。第一,量化时机决定了 RoPE 是否参与误差。逐 token 基线在旋转之后量化,旋转引入的维度间差异直接进入量化范围。Pre-RoPE 路线把量化提前到旋转之前,代价是缓存里存的是旋转前的 Key,读取后还要补做旋转。
第二,旋转感知路线在量化前插入一次额外旋转。这个旋转必须和模型结构配合,保证旋转后的计算结果与原始模型等价。QuaRot 的资料强调这种旋转不改变输出,这是它能做端到端 4-bit 的前提。如果旋转矩阵实现有误,等价性被破坏,量化误差反而会被放大。
第三,反量化发生在注意力计算之前。长上下文下,每个解码步都要读取全部历史 Key,反量化的计算量和内存带宽开销随序列长度增长。KVQuant 的资料提到它为 KVQuant 开发了定制 CUDA kernel,相比 fp16 矩阵向量乘法有约 1.7 倍加速,这说明反量化开销可以通过 kernel 优化摊薄,但需要专门的工程投入。
生成质量影响与失败模式
量化误差对生成质量的影响不是均匀的。短上下文、事实性问答这类任务,注意力分布比较集中,少量 Key 的数值偏移可能不改变最终选择。长上下文、需要精确定位的任务更容易暴露问题:
- 长文档问答里,答案依赖某个具体位置的 token,Key 误差让注意力偏移到相邻位置,答案出现细节错误。
- 代码补全里,缩进和括号匹配依赖局部结构,Key 误差可能让模型重复生成同一段代码。
- 多轮对话里,早期轮次设定的约束(比如“只输出 JSON”)如果对应的 Key 被量化误差淹没,后续生成会违反约束。
这些失败模式的共同点是:误差发生在注意力定位环节,而不是生成环节。模型仍然能流畅输出,但输出的内容偏离了上下文里的具体事实。这类问题在困惑度指标上不一定明显,因为困惑度衡量的是整体分布,而定位错误只影响少数 token。
另一个失败模式是离群值导致的量化范围失衡。如果某个 token 的 Key 恰好包含一个极端大的值,逐 token 量化的 scale 会被这个值撑大,同一 token 内其他维度的有效分辨率下降。RoPE 的旋转会让这种极端值在不同位置反复出现,因为旋转把数值在维度对之间搬运。
适用边界与仍未解决的问题
旋转感知量化不是无条件有效的。它的前提是模型结构允许插入等价旋转,且旋转后的分布确实更均匀。对于已经用了其他位置编码方案、或者注意力结构有特殊修改的模型,这套方法需要重新推导等价性。
Pre-RoPE 量化的前提是缓存里存旋转前的 Key。这要求推理框架在读取 KV Cache 后、做注意力之前补做旋转,增加了 kernel 的复杂度。如果框架原本把 RoPE 融合在 Key 投影里,改成 Pre-RoPE 需要拆开这个融合,可能影响计算效率。
低比特量化的收益和风险都随比特数下降而放大。8-bit 量化下,逐 token 方案的误差通常可以接受;4-bit 以下,RoPE 误差开始主导,需要旋转感知或逐通道方案。资料里 KVQuant 在 3-bit 下做到小于 0.1 的困惑度退化,QuaRot 在 4-bit 下端到端量化保留 99% 零样本性能,但这些结论都有具体的模型和数据集条件,不能直接外推到所有模型和任务。
还没有完全解决的问题包括:旋转感知量化在超长上下文(百万 token 以上)下的误差累积规律;不同注意力变体(比如分组查询注意力、多查询注意力)对量化分组的适配;以及量化误差在多次解码步之间是否会通过 KV Cache 的读写形成反馈放大。这些方向需要结合具体部署场景做验证,而不是靠单一指标判断。
回到开头那台 80GB 服务器。如果要在 128K 上下文下稳定服务,逐 token 量化在低比特下大概率不够用。可行的路径是:先确认模型是否支持等价旋转,再决定走 Pre-RoPE 还是旋转感知路线;如果都不满足,退回到逐通道或小分组量化,用元数据换精度。选择哪一种,取决于显存预算、kernel 实现成本和任务对定位精度的要求。