3.2 数值稳定性与共享边界:PagedAttention 在共享场景下结果还准吗 读者读完这一节,应该能回答一句:PagedAttention 只改变了 KV Cache 的“内存排布”,没有改注意力的“数学”,只要归一化基准一致,分页结果与连续结果数值等价;而在并行采样 / beam search 这类共享前缀的场景里,它的正确性边界是“共享的是已生成的前缀,一旦序列分叉便各自私有、互不影响”。 第 3.1 节解决了“为什么快”。这一节解决更让人睡不着的问题:“它改了内存排布、还把 KV 切成块、多序列共享,那算出来的东西还是原来那个注意力吗?” 这种担忧非常合理——在工程里,“快但算错”比“慢但正确”更可怕。本节给你明确结论、边界条件,以及一套上线前可操作的验证方法。 3.2.
读者读完这一节,应该能回答一句:PagedAttention 只改变了 KV Cache 的“内存排布”,没有改注意力的“数学”,只要归一化基准一致,分页结果与连续结果数值等价;而在并行采样 / beam search 这类共享前缀的场景里,它的正确性边界是“共享的是已生成的前缀,一旦序列分叉便各自私有、互不影响”。
第 3.1 节解决了“为什么快”。这一节解决更让人睡不着的问题:“它改了内存排布、还把 KV 切成块、多序列共享,那算出来的东西还是原来那个注意力吗?” 这种担忧非常合理——在工程里,“快但算错”比“慢但正确”更可怕。本节给你明确结论、边界条件,以及一套上线前可操作的验证方法。
这是理解本节的总开关:PagedAttention 本质是“显存管理策略”,不是“新的注意力公式”。 它把连续的 KV 张量切成固定大小的块(block),用一张块表(block table)把逻辑上连续的序列映射到物理上可能离散的块。注意力计算时,内核按块表把需要的 KV 块取出来,做的数学运算和“KV 在一整块连续显存里”时完全一样。
类比操作系统:虚拟内存把进程的连续地址空间映射到离散的物理页,但进程算出来的结果不会因为“页散落在不同物理位置”而改变。PagedAttention 同理——分页是地址翻译层的事,注意力核看到的逻辑 K/V 序列没变,softmax 没变,加权求和没变。所以从数学定义上,分页结果应当和连续结果严格等价。这正是它和第 2 章 2.2 节“像操作系统管内存一样管显存”那句话的呼应:管法变了,语义没变。而第 2 章 2.3 节我们讲过 FA-2 与 PagedAttention 如何在一次推理里协同分工;这一节则从数值层面为那次协同担保——只要归一化基准统一,协同就不会悄悄引入误差。
PagedAttention 最亮眼的能力,是让多个序列共享同一个 prompt 前缀的 KV 块。这在两类场景极常见:
正确性边界必须说清楚:共享的只是“已经生成、且内容相同的那段前缀”。 一旦某个序列在某一位置走出不同分支(采样出了不同的 token),从该位置往后的 KV 就必须各自私有,不能再共享,否则就会把 A 序列的 KV 喂给 B 序列、造成污染。
工程上这是靠**写时复制(copy-on-write)**实现的:多个序列的逻辑块表同时指向同一物理块,只读时共用;当某序列要写入新生成的 KV(即生成了新 token、需要新块)时,才真正复制该块,断开共享。这与操作系统里 fork 之后的 COW 是同一思路。因此共享的“安全边界”等价于:在分叉点之前共享,分叉点之后隔离。只要块表维护正确,N 个序列的输出,和它们各自独立运行(不共享)的输出,在数学上应当一致。
尽管数学等价,实际部署里你可能发现分页实现和连续实现的输出有 1e-3 量级的细微差别。这不是“算错了”,而是浮点实现的客观现象,常见来源有:
① 归一化基准:块内 softmax 若各自减了不同的局部 max,再跨块拼接时若没做校正, 会产生偏差——这是实现 bug,不是算法缺陷。 ② 累加精度:长序列下 fp16 的 running sum 有舍入漂移;稳健实现会在关键路径用 fp32 累加。 ③ 并行 reduce 次序:不同内核把多行/多块的部分和合并时,浮点加法不满足严格结合律, 合并顺序不同,末位有差异。 ④ 与 FlashAttention-2 的 online softmax 叠加:两者都引入了“分块增量更新”, 须保证 FA-2 的块级校正因子和 PagedAttention 的分块基准相互对齐。
关键的工程保障是:分页结果和连续结果之间必须存在一个“归一化不变量”——无论 KV 怎么切块、怎么跨块拼接,最终 softmax 的分母和每行的归一化结果应逐位等价(在浮点容差内)。 主流推理引擎(如 vLLM)正是通过在拼接时用统一的 running max/sum 基准、并在敏感路径使用 fp32 累加,来守住这条不变量的。所以“分页会算错”这个说法不成立;成立的是“分页若实现粗糙、跨块没校正,才会引入可观测偏差”。
为了让上面这套“不变量”不悬空,我再用一个具体场景说明偏差会从哪钻出来。假设分块大小是 16,序列长度 64,那么 KV 被切成 4 块。某实现为了省事,在每块内部各自减掉“本块 max”做局部 exp,再直接拼接 4 块的结果——这时每块减去的是不同的常数,而 softmax 的平移不变性要求“减去同一个全局 max 才不变”。这种实现就会在边界处累积出可见偏差,且序列越长(块越多)偏差越大。正确做法是维护一个跨块的全局 running max/sum,每块用“全局基准 + 校正因子”增量合并,这样无论怎么切块,结果都收敛到与连续实现一致。这就是为什么 3.2.5 的验证清单里,“一致性比对”是必做项——它能把这类实现 bug 在发版前就逮出来。
顺带把第 2 章埋的点补全:FA-2 不仅快,数值上也很稳,秘密在 online softmax。
朴素 softmax 要先算出整张分数矩阵 S,再减 max、再 exp、再归一化——这要求把 S 完整物化,且 exp 对大数极敏感,容易溢出/下溢。FA-2 改为分块流式:逐块读入局部分数,维护 running max(m)和 running sum(l),用校正因子把各块的局部结果增量合并成全局等价的 softmax。
为什么等价?因为 softmax 具有平移不变性:分子分母同减一个常数(这里是全局 max),结果不变。online softmax 只是把“先减全局 max”拆解成“边读边更新 max、边校正前面已累加的部分”,数学上严格等价,但不再需要物化整张 S。这带来双重收益:显存更省(不物化 O(n²) 矩阵)+ 数值更稳(避免了一次性的大数 exp)。所以 FA-2 不是“用精度换速度”,而是“顺手把数值稳定性也解决了”。这一点和 3.2.3 的“统一归一化基准”是一脉相承的——无论分块在哪一维发生,只要全局基准一致,数学等价就守得住。
3.2.2 提到共享靠写时复制(COW)守住边界,这里再补一句机制层面的话,避免你误以为“共享”会引入不确定性。COW 的核心保证是:只读期间,多个序列的物理块完全相同,读取结果必然一致;一旦任一序列要写入(生成新 token),立刻复制出独立副本,此后它的修改对其他序列不可见。 这和我们写多线程程序时“共享只读、写前拷贝”的纪律完全一致。因此“N 个序列共享前缀”不会造成任何一个序列读到别的序列的 KV;分叉之后的私有块从复制那一刻起就是隔离的。只要块表的引用计数和复制触发逻辑正确(这是推理引擎内核要守的契约),共享就是零风险的。
当你要把 PagedAttention / FlashAttention 真正推上生产,别只信文档说“等价”。用下面这套清单做一遍,结论才有底气:
① 基线测量:固定输入,分别跑“连续 KV”与“分页 KV”两组,记录 吞吐、峰值显存、TTFT(首 token 延迟)。 ② 一致性比对:同一个 prompt,对比两组输出的 logits / 生成结果差异, 应小于设定容差(如 1e-3);差异过大先怀疑跨块归一化基准没对齐。 ③ 共享增益实测:并行采样 N 条 vs 单条,观察显存是否“近线性增长” 而非 N 倍翻倍——这是共享生效的直接证据。 ④ 长序列压测:逐步拉长上下文(如 2K→8K→32K),记录分页优势从哪个长度开始显著。 ⑤ 边界回归:不同块大小(如 16/32)、不同 dtype(fp16/bf16)各留一组输出快照, 防止后续升级内核时产生隐性退化。
这套方法的灵魂在于:把“等价”从口号变成可观测、可回归的断言。 一旦你有了“分页 vs 连续”的基线快照,以后任何引擎升级、内核切换,只要重跑这组对照、差异在容差内,就可以放心;差异突然变大,立刻能定位是归一化 bug 还是精度策略变了。
前面两节是分开讲的,但真实引擎里它们同时生效,于是出现一个耦合问题:两个都做分块增量更新,归一化基准必须对得上。 FA-2 的 online softmax 在“注意力算子的行方向”上维护 running max/sum;PagedAttention 在“KV 的块方向”上拼接。两者各自都“等价”,但若 FA-2 的块级校正因子和 PagedAttention 的分块基准用了不同的全局参照,叠加后末位就可能漂移放大。
工程上的解法是统一归一化基准:让 PagedAttention 在取 KV 块时,沿用 FA-2 的 running max/sum 语义,而不是各自为政。这也是为什么“同时开 FA-2 和 PagedAttention”不能简单理解为“两个独立优化叠加”——它们在内核层面是协作的,正确性依赖于这条统一的数值契约。第 4 章你会看到,推理引擎正是把这两者打包在同一个融合内核里,才既快又稳。
这一节的结论可以压缩成三句话带走:
读懂 3.1 和 3.2,你手里就有了“加速是否真的有效、结果是否依然可信”的两把尺子。下一章(第 4 章实战)我们不再讲原理,而是把这些尺子用到 vLLM 与 HuggingFace 的真实配置里——那时你会发现,本章的块大小、fp32 累加、共享显存,全部会变成你能解释、能调优的具体参数。
最后泼一盆冷静水。本章说“分页与连续数学等价”,是说在正确实现的前提下等价。工程现实是:等价性依赖那一连串不易被肉眼发现的细节——跨块归一化基准、fp32 累加开关、与 FA-2 的数值契约。任何一环实现偷懒,等价性就会破功,且破得悄无声息(只差 1e-3,却可能在长链推理里被放大)。
所以请把 3.2.5 的验证清单当成正经的发版门槛,而不是“可选的最佳实践”。我自己的习惯是:每次升级推理引擎、或切换内核版本,都重跑一次“分页 vs 连续”的一致性快照,差异在容差内才敢上线。这半小时的验证,能帮你避开无数个“线上输出莫名其妙变差”的深夜故障。把稳定性当成资产经营,它就会在你每次改动时替你站岗。