Course based on Defeating Nondeterminism in LLM Inference

Page 1: 第1页 各位同学,欢迎。今天我们探讨一个在人工智能领域,尤其是大语言模型中,基础而又棘手的问题:结果的可复现性。科学进步的基石之一,便是在相同条件下能够重复实验并得到相同结果。然而,对于大语言模型,这却成了一个巨大的挑战。 你们可能已经发现,向ChatGPT这类模型多次提出同一个问题,会得到不同的回答。这本身不奇怪,因为模型生成答案的过程涉及一个叫做“采样”的随机过程。但令人困惑的是,即便我们试图通过将“温度”参数设为0来消除这种随机性,使其在理论上变为确定性的“贪婪采样”,实际结果依然是不可复现的。 一个广为流传的假说将此归咎于“并发与浮点数”的共同作用。这个假说认为,GPU的高度并行计算,加上浮点数运算不满足结合律(即(a+b)+c不恒等于a+(b+c)),导致了计算结果会因线程完成的先后顺序而产生微小差异。这就像让一群会计师同时计算一长串数字,每个人加总的顺序不同,由于每一步都可能存在四舍五入,最终的总和也可能出现细微偏差。然而,这个假说并不完整,我们将深入探究其背后的真正原因。 Page 2: 第2页 现在,我们来追溯问题的根源,我称之为计算领域的“原罪”:浮点数的非结合律。在理想的数学世界里,加法满足结合律,(a+b)+c 总是等于 a+(b+c)。但在计算机中,由于浮点数的表示方式,这个定律被打破了。 浮点数,好比是用科学计数法来表示数字,它由一个“尾数”和一个“指数”组成,比如 $3.45 \times 10^3$。这种表示法的优势在于能用固定的位数表示极大或极小的数,但代价是精度有限。 当两个指数(即“标度”)差异巨大的浮点数相加时,问题就出现了。比如,一个非常大的数加上一个非常小的数。为了对齐小数点,计算机必须牺牲小数的精度。这就好比一位亿万富翁在计算总资产时,他账户里的零头(比如几块钱)很可能在与他的巨额财富相加时被“四舍五入”掉了。如果你先将两个亿万富翁的资产相加,再加零头,这个零头可能就被忽略了。但如果你先将零头与其中一位的资产相加,它或许还能保留一部分。 因此,运算的顺序决定了精度损失的方式,从而导致了不同的最终结果。这虽然解释了数值差异的存在,但尚未揭示这些不同运算顺序是如何在LLM推理中随机发生的。 Page 3: 第3页 为了解开这个谜题,我们需要像剥洋葱一样,层层剖析LLM推理系统。我们会发现一个有趣的悖论,以下四个陈述同时为真: 首先,在最底层的硬件执行层面,部分GPU计算单元(我们称之为“核”或Kernel)确实是“非确定性”的,比如那些使用了“原子加法”的核。 其次,尽管如此,在LLM模型进行一次“前向传播”(即从输入到输出的计算过程)时,所用到的所有核,实际上都是“确定性”的。它们不使用那些会导致随机性的操作。 再次,从整个推理服务器的角度看,它的行为也可以声称是“确定性”的。只要给它完全相同的一批用户请求,它总会产生完全相同的输出。 最后,然而,对于任何一个单独的用户来说,他们感受到的结果却是“非确定性”的。 这个悖论是理解问题的关键。它告诉我们,非确定性并非源于模型计算过程本身固有的随机性,而是源于更高层面的系统行为。我们将逐一解开这些看似矛盾的陈述。 Page 4: 第4页 我们来审视一下“原子加法”这个概念,它常常被误认为是罪魁祸首。想象一下,一个班的学生需要将各自的得分加到一个总分牌上。如果大家一拥而上,同时去写,就可能出现混乱。原子加法就像一个神奇的裁判,它保证每个人的分数最终都会被加上,但它不保证相加的顺序,谁先算完谁先加。由于浮点数的非结合律,这个不确定的顺序就会导致最终总分可能每次都略有不同。 然而,在LLM的推理(即前向传播)过程中,这种混乱的场面几乎不会发生。为什么呢? 第一,因为通常服务器会同时处理很多用户的请求(即一个“批次”),GPU的计算核心可以被分配去独立处理每个请求,就像给每个学生一个独立的计分板,互不干扰,也就不需要争抢同一个总分牌了。 第二,即便需要并行计算一个总和,现代计算库也采用了更聪明的、确定性的方法,比如“分治”策略。这好比将学生分成小组,先组内汇总,再由组长将小组总分汇总,整个过程的顺序是固定的,从而避免了随机性。 因此,原子加法虽然本身具有非确定性,但它在LLM推理的前向传播中几乎不被使用。这说明,我们必须在别处寻找非确定性的真正来源。 Page 5: 第5页 现在,我们终于触及了问题的核心。真正的罪魁祸首,是计算核缺乏“批次不变性”(Batch Invariance)。 这是一个关键概念。从数学上讲,一个矩阵乘法中,批次里的每个元素的计算应该是相互独立的。计算第一个请求的结果,不应该受到同时还在计算第二、第三个请求的影响。然而在实际的硬件实现中,为了最大化效率,这个理想的独立性被打破了。 我们可以用一个比喻来理解。想象一个大型面包店,有一个巨大的烤箱。理论上,烤箱里每个面包的烘焙过程应该是独立的。但实际上,烤箱里的总面包数量(即批次大小)会影响烤箱内部的热量分布和循环。因此,即使每个面包都用了完全相同的配方和时间,一个只烤了10个面包的批次,和一个烤了100个面包的批次,其出品的面包在口感上可能会有微小的差异。 对于LLM推理服务器而言,道理是相通的。你发送一个请求,但你无法知道服务器此刻正在同时处理多少其他人的请求。这个“同时处理的请求数量”就是批次大小,它对于你来说是随机的。由于计算核的输出会随着批次大小变化,你每次请求得到的结果也就会随机变化。这才是用户感受到非确定性的根本原因。 Page 6: 第6页 为了实现完全的确定性,我们必须确保模型中的每一个计算操作都具备“批次不变性”。我们从最简单的操作——RMSNorm开始。RMSNorm是一种归一化技术,它需要对一个向量内的所有元素进行求和计算(即“规约”操作)。 问题出在哪里呢?当服务器处理的批次很大时,GPU的每个计算核心可以被分配去独立完成一个或多个请求的RMSNorm计算。每个请求的计算都在自己的核心内部完成,互不干扰,其计算顺序是固定的。这就像每个学生都有自己的计算器,计算过程是独立的。 但是,当批次非常小,比如只有一个请求时,为了不让GPU的大量核心闲置,一个聪明的工程师可能会让多个核心协作来完成这一个请求的计算。这就叫“分裂规约”。多个核心同时计算,再把结果汇总。这改变了计算顺序,从而破坏了批次不变性。 如何解决呢?最简单的方法是“以不变应万变”。我们强制规定,无论批次大小,所有RMSNorm计算都必须采用同一种固定的计算策略,即那种为大批次设计的、每个请求独立计算的策略。虽然这在处理小批次时会牺牲一些性能(因为部分核心会闲置),但它保证了计算顺序的绝对一致,从而实现了批次不变性。 Page 7: 第7页 接下来是矩阵乘法,情况更为复杂。矩阵乘法本质上也是一系列乘法和加法(规约)的组合。 同样地,处理大型矩阵时,我们可以将输出矩阵切分成许多小“瓦片”,每个GPU核心负责计算一整块瓦片的结果。这样,每个核心独立工作,计算顺序固定,保证了批次不变性。 但当矩阵尺寸较小,无法切分出足够多的瓦片来喂饱所有GPU核心时,系统就会采用“Split-K”策略。它会将规约维度(即矩阵乘法中相乘后相加的那个维度)也进行切分,让多个核心协同计算。这再次破坏了批次不变性。 更麻烦的是,现代GPU有专门用于矩阵乘法的硬件单元,叫做“张量核心”(Tensor Cores)。它们像专门处理大宗订单的流水线,效率极高,但只接受特定尺寸的“订单”(即瓦片)。如果你的矩阵尺寸不合适,系统可能会切换到不同规格的流水线,甚至改用“人工”(即普通计算单元)处理。不同的处理方式,其内部的计算顺序天差地别,这又是一个破坏批次不变性的因素。 解决方案依然是“标准化作业”:我们强制规定,无论矩阵大小,都使用同一种固定的计算核配置和同一种张量核心指令。这就像规定面包店只用一种尺寸的烤盘和一种烘焙程序,虽然对小订单来说可能有点浪费,但保证了所有产品的品质一致。 Page 8: 第8页 注意力机制是使LLM具备上下文理解能力的核心,也是实现批次不变性最困难的一环。它引入了两个新的难题。 第一,它的规约操作更为复杂,不仅涉及特征维度,还涉及序列维度。 第二,也是最关键的,它与推理过程中的各种优化紧密相连,尤其是“KV缓存”。KV缓存就像一个速记本,记录了模型已经处理过的信息,避免重复计算。 问题在于,为了效率,很多推理引擎在计算当前词的注意力时,会把“速记本”里的旧信息和当前的新信息分开处理,然后再合并。这就像一个侦探在分析案情,他先回顾了一遍旧的案卷(KV缓存),然后又看了一遍新送来的证据(当前词元),最后在脑中整合。 这种“分开处理再合并”的策略,其具体的计算步骤和顺序,会因为旧案卷的厚度(KV缓存的长度)而发生变化。例如,处理一个长篇小说的第一个字(此时KV缓存为空),和处理第一千个字(此时KV缓存很长),其底层的计算块划分和合并顺序是完全不同的。这就破坏了批次不变性,因为对于同一个词,其计算方式会因其在序列中的位置和处理方式(是作为长提示一次性输入,还是逐词生成)而改变。 Page 9: 第9页 为了解决注意力机制的难题,我们必须重新设计它的并行计算策略,尤其是在处理KV缓存时。 在逐词生成(解码)阶段,由于每次只处理一个词,并行度很低。为了压榨GPU性能,我们必须采用“分裂KV”策略,即让多个核心共同处理长长的KV缓存。传统的做法是,根据KV缓存的总长度和我们需要的并行核心数,来决定每个核心分摊多少计算量。比如,缓存长度1000,需要4个核心,每个核心就处理250个单位。但当缓存增长到1200时,每个核心就要处理300个单位。这种“动态均分”的策略,其计算顺序会随总长度变化,因此破坏了批次不变性。 正确的做法是采用“固定分块大小”的策略。我们不再关心总长度和核心数,而是规定每个核心处理的“数据块”大小是固定的,比如256个单位。对于一个长度为1000的KV缓存,我们就会把它切分成三个完整的256大小的块,和一个232大小的“零头”块。 - 无论KV缓存多长,前面那些完整的256大小的块,其内部的计算方式永远是相同的。这样,我们就保证了计算顺序的一致性,从而实现了注意力机制的批次不变性。 Page 10: 第10页 理论必须经过实践的检验。研究人员基于主流的vLLM推理框架,通过其FlexAttention后端和torch.Library技术,将标准的计算核替换为了我们前面讨论的批次不变性计算核。 他们进行了一项实验:使用一个大型Qwen模型,在温度为0的确定性采样模式下,对同一个提示“告诉我关于理查德·费曼的事”请求生成1000次。 结果令人震惊。在标准模式下,1000次请求竟然产生了80个独一无二的回答版本。最常见的那个版本也只出现了78次。这有力地证明了非确定性问题在实践中是多么严重。 而当我们切换到批次不变性模式后,1000次请求得到了1000个完全相同、逐字不差的回答。 通过对比这些不同的回答,我们发现分歧点出现在第103个词元。所有回答都同样生成了“费曼出生于1918年5月11日,在”,但随后,992个回答生成了“纽约皇后区”,而另外8个则生成了“纽约市”。这清晰地展示了微小的数值差异如何级联放大,最终导致语义上的不同。 Page 11: 第11页 确定性推理的一个重要应用领域,是强化学习(RL)。在强化学习中,我们让模型(即“策略”)与环境互动,根据得到的奖励来优化自身。 其中,“在线策略”(On-Policy)RL要求产生行为的策略和被优化的策略必须是同一个。然而,由于推理(采样)和训练过程中的数值差异,我们实际上是在用一个略有不同的“旧”策略去收集数据,来更新“新”的策略。这在不经意间把在线策略变成了“离线策略”(Off-Policy),如果不加以校正(比如通过重要性采样),训练过程很容易崩溃。 这就像一位射箭运动员,他练习时用的弓(采样策略)和比赛时用的弓(训练策略)有微不可察的差异。短期内可能没问题,但长期来看,基于练习手感形成的肌肉记忆,用在比赛的弓上就可能导致成绩突然大幅下滑。 而确定性推理,通过确保采样和训练过程中的数值计算完全一致,就相当于保证了运动员练习和比赛用的是同一把弓。实验结果清晰地显示,在实现了“真正的在线策略”RL后,我们不再需要任何离线校正技术,训练过程依然平稳,并且采样策略和训练策略之间的KL散度(一种衡量分布差异的指标)始终为零,证明了二者的完全一致。 Page 12: 第12页 最后,我们进行总结。在机器学习的复杂系统中,当我们遇到非确定性和微小的数值差异时,一种常见的态度是将其归咎于模型的“概率性”本质,并选择容忍甚至忽视它。这是一种技术上的“失败主义”。 本文的核心论点,正是要反击这种失败主义。我们已经论证,LLM推理中的非确定性,其根源并非某些不可避免的随机过程,而是完全可以理解和解决的工程问题——计算核缺乏“批次不变性”。 通过深入分析,我们为RMSNorm、矩阵乘法和注意力机制等关键操作设计并实现了批次不变性的版本。这证明了,只要我们付出努力,完全可以驯服非确定性这头猛兽,获得可复现、可信赖的系统。 我们希望这项工作能启发社区,鼓励大家不再满足于对系统行为的模糊认知,而是深入探究其根本原因,构建更强大、更可靠的智能系统。谢谢大家。

Course based on Defeating Nondeterminism in LLM Inference