攻克 LLM 推理中的非确定性
LLM 推理的非确定性是一个系统性问题。它源于为追求极致性能而设计的、对批次大小敏感的底层计算库,与现实世界中动态变化的服务器负载之间的矛盾。解决方案是存在的,即强制使用批次不变的计算内核,但这通常需要以牺牲部分性能为代价。
LLM(大语言模型)推理结果的不可复现性(非确定性),并非像通常认为的那样,是由于 GPU 并行计算的随机性与浮点数计算误差的简单结合。真正的罪魁祸首是:核心计算操作(Kernel)缺乏“批次不变性”(Batch Invariance),再结合服务器上不断变化的负载(即变化的批处理大小 Batch Size)。
主要内容解析
Section titled “主要内容解析”-
普遍的误解 vs. 事实
- 普遍误解(“并发+浮点数”假说):人们普遍认为,由于浮点数加法不满足结合律(即
(a+b)+c ≠ a+(b+c)),而 GPU 又以不确定的顺序并行执行这些加法,导致了结果的随机性。 - 文章指出的事实:这个假说并不完全。虽然浮点数非结合律是产生数值差异的根源,但 LLM 推理(前向传播)中使用的绝大多数计算核心(如矩阵乘法)本身是 “运行确定” 的。即对于一个固定批次的输入,多次运行会得到完全相同的结果。
- 普遍误解(“并发+浮点数”假说):人们普遍认为,由于浮点数加法不满足结合律(即
-
真正的非确定性来源
- 缺乏“批次不变性”:尽管单个计算核心是确定性的,但其计算结果会受到 批处理大小(Batch Size) 的影响。例如,对一个向量进行计算,当它被单独处理(batch size=1)与和其他上千个向量一起处理(batch size=1000)时,得到的数值结果会有微小的差异。这是因为为了优化不同批次大小下的性能,底层会采用不同的计算策略和指令,从而改变了浮点数的累加顺序。
- 可变的服务器负载:从用户的角度来看,他们发送的请求会被推理服务器与其他用户的请求动态地组合成一个批次进行处理。服务器的负载是实时变化的,这意味着用户的同一个请求,这次可能在一个大小为 8 的批次中处理,下次可能在一个大小为 128 的批次中处理。
- 两者结合的结果:一个缺乏“批次不变性”的计算核心,被应用在一个“批次大小不确定”的系统中,最终导致了用户感知的 非确定性。
如何实现确定性推理(即实现“批次不变性”)
Section titled “如何实现确定性推理(即实现“批次不变性”)”文章指出,要实现完全可复现的推理,必须让模型中的每一个计算环节都做到批次不变,主要涉及以下三个部分:
- RMSNorm:相对容易实现。只需固定使用一种并行化策略,即使在小批量时性能稍差,也要避免切换到会改变计算顺序的策略。
- 矩阵乘法(Matrix Multiplication):挑战更大。高性能的矩阵乘法库会根据输入尺寸选择不同的 Tensor Core 指令或并行策略(如 Split-K)。要实现确定性,必须强制使用同一种内核配置,这会牺牲在某些尺寸下的极致性能。
- 注意力机制(Attention):最复杂。不仅要对批次大小保持不变,还要对序列的处理方式(如分块处理 Prefill、使用 KV Cache 解码)保持不变。这意味着一个 token 在计算注意力时,无论其上下文(KV Cache)有多少,其内部的计算顺序都必须完全一致。