
Horace He:2025年LLM推理非确定性?批次不变性来破局
Benjamin zhong
1
9-28晓曼: 你可能觉得,当你在和ChatGPT这样的语言模型聊天时,把那个叫做“温度”的参数调到零,就意味着它每次都会给你完全相同的、最可预测的答案。理论上确实如此,这叫“贪婪采样”。但奇怪的是,现实并非如此。你可能已经发现了,即使在最严格的设置下,你问同一个问题,两次得到的答案也可能不一样。
晓曼: 这到底是怎么回事?很多人,包括一些技术文章,都会给出一个听起来很专业的解释:并发加浮点数假说。简单来说,就是GPU为了快,会并行处理很多计算,而浮点数的加法顺序一变,结果就会有微小的差异,这些差异累积起来,就导致了最终输出的不同。这个解释听起来很有道理,但它并不完整,甚至在某种程度上具有误导性。因为如果你在GPU上反复运行同一个矩阵乘法,你会发现结果总是精确到每一个比特都完全相同。这就有意思了,既然都是在GPU上用浮点数做并行计算,为什么矩阵乘法是确定的,而整个语言模型的推理却不是呢?今天,我们就来当一回侦探,揭开大型语言模型推理中“非确定性”这个谜题背后的真正元凶。
晓曼: 那么,既然常见的“并发加浮点数”假说无法完全解释这种现象,我们首先需要深入了解浮点数非结合性这一“原罪”,才能探寻真正的幕后黑手。
晓曼: 在数字世界里,浮点数计算存在一个与生俱来的“原罪”,叫做非结合性。意思就是,在数学里天经地义的 `(a+b)+c` 等于 `a+(b+c)`,到了计算机里,尤其是用浮点数计算时,就不一定成立了。
晓曼: 这背后的原因其实也挺好理解。你可以把浮点数想象成一种科学记数法,它用有限的位数去表示一个可能非常大或非常小的数。比如说,我们只能保留3位有效数字。现在让你计算100万加上0.1,然后再减去100万。如果你先算 `(100万 + 0.1)`,由于精度限制,结果还是约等于100万,那点0.1就被“舍入”掉了,就像往大海里扔了颗沙子。然后用这个结果再减去100万,答案就是0。但如果你换个顺序,先算 `(100万 - 100万)`,结果是0,然后再加0.1,答案就是0.1。你看,完全一样的数字,只是计算顺序不同,结果就天差地别。
晓曼: 这种特性,其实是浮点数为了在有限的存储空间里表示巨大范围的数字而做出的权衡,它保证了“有效数字”的数量。但也正因如此,任何涉及浮点数加法顺序改变的计算,都可能产生微小的数值差异。原文里有个很绝的例子,一个只包含8个数字的数组,仅仅因为求和顺序的不同,竟然能产生102种不同的结果。这为我们理解LLM推理中的数值差异打下了基础。
晓曼: 不过,请注意,这只是差异的来源,它本身并不等于“非确定性”。非确定性意味着加法顺序会随机改变。很多人自然而然地会想到,GPU那么多核心在同时工作,是不是哪个核心先算完,哪个结果就先被加上去,从而导致顺序随机?这个过程通常依赖一个叫“原子加”的操作。
晓曼: 然而,这里的关键反转来了。文章明确指出,在大型语言模型进行推理的前向传播过程中,绝大多数的核心计算,也就是所谓的kernel,根本就用不到这个会导致顺序随机的“原子加”。换句话说,从一次运行到下一次运行,这些核心计算本身是确定性的。
晓曼: 既然原子加在LLM前向传播中并非主导因素,那么真正导致“加法顺序”在推理过程中变化的深层原因究竟是什么呢?
晓曼: 好了,我们现在知道,单个计算核心(kernel)的运行是确定性的,那为什么把它们组合在一起,整个语言模型的推理就变得不确定了呢?这里的元凶,是一个听起来有点陌生的概念,叫做“批量不变性”的缺失。
晓曼: 什么叫批量不变性?从数学上讲,一个操作的结果不应该受到它和谁一起被处理的影响。我给你举个例子,假设你是一个老师,正在批改一叠试卷。理论上,张三的试卷分数,不应该因为你是单独批改他这一份,还是把他放在一百份试卷里一起批改而发生变化。这就是批量不变性。
晓曼: 但诡异的是,在GPU的实际计算中,这个原则被打破了。文章里给了一个非常直观的代码例子:我们先做一个单行的矩阵乘法,得到结果A;然后再做一个包含很多行的、更大批量的矩阵乘法,再从结果中取出对应的那一行,得到结果B。按理说,A和B应该完全一样,对吧?但实验结果显示,它们的差值可能非常大。
晓曼: 这就好比说,张三的试卷,如果单独批改是90分,但如果把他和全班同学的试卷一起批改,他的分数就可能变成89.5分。这就是缺乏“批量不变性”。
晓曼: 现在,我们把这个概念和语言模型推理服务器联系起来。你每次向ChatGPT提问,你的请求都会和其他成百上千个用户的请求被打包在一起,形成一个“批次”送到GPU上处理。服务器忙不忙,决定了这个“批次”有多大。今天可能你的请求和10个其他请求一起处理,明天可能就和100个一起。
晓曼: 因为核心计算操作缺乏“批量不变性”,批次大小的变化,直接导致了每个请求计算过程中的浮点数加法顺序发生了变化,最终让你的输出也变了。所以,对于推理服务器本身来说,如果给它两次完全一样的批次,它的输出是确定的。但对于你,一个普通用户来说,你无法控制每次跟你“拼单”的是谁、有多少人,所以从你的视角看,整个系统就是非确定性的。
晓曼: 这个发现彻底颠覆了之前的“并发+浮点数”假说。问题的根源不在于单个计算内部的随机性,而在于计算策略会随着外部负载(也就是批次大小)的变化而变化。这不仅让模型的行为变得不可预测,难以调试,更严重的是,它破坏了科学研究的可复现性。
晓曼: 既然批量不变性是导致LLM推理非确定性的关键,那么如何在实际的LLM核心操作,比如RMSNorm、矩阵乘法和注意力机制中,实现这种至关重要的不变性呢?
晓曼: 要实现真正的确定性,就得从根源上修复这个“批量不变性”的问题,这意味着要改造Transformer模型里那些最核心的、涉及规约操作的部件:RMSNorm、矩阵乘法和注意力机制。
晓曼: 我们一个一个来看。首先是RMSNorm,这相对简单。它的主要工作是做一些标准化的计算。通常的并行策略是数据并行,也就是一个核心处理一个批次里的一个元素。只要批次够大,每个核心都有活干,各干各的,互不影响,规约顺序自然就固定了。问题出在批次很小的时候,比如GPU有100个核心,但批次里只有10个元素,为了不让90个核心闲着,聪明的工程师就会让多个核心去合作处理一个元素,这就叫“分裂规约”。这么一来,性能保住了,但规约顺序变了,批量不变性也就丢了。解决方案呢?嗯,有点“笨”,就是强制规定,无论批次大小,都用同一种规约策略。即使小批次时性能差点,也要保证一致性。
晓曼: 接下来是矩阵乘法,情况类似但更复杂。为了高效利用GPU里的张量核心(Tensor Core),计算不是逐个元素进行的,而是按“块”进行的。当批次大小变化时,为了效率最大化,核心操作可能会选择不同尺寸的指令“块”,甚至在批次极小的时候干脆放弃使用张量核心。这同样会改变内部的规约顺序。所以,解决方案还是类似的“一刀切”:编译一个固定配置的核心操作,不管来的是什么尺寸的矩阵,都用这一套,牺牲一部分灵活性来换取绝对的一致性。
晓曼: 最难啃的骨头是注意力机制。它有两个额外的麻烦。第一,它不仅要在特征维度上规约,还要在序列长度这个维度上规约。第二,现代推理引擎为了优化,会玩出各种花样,比如把一个长句子的计算拆成几块(chunked prefill),或者利用之前算过的结果(KV Caching)。
晓曼: 这带来了新的挑战。想象一下,要计算一个序列里第1000个词的注意力,它的计算方式不能因为“这是在一次性处理1000个词”,还是“这是在处理了999个词的缓存后,再处理这最后一个词”而有所不同。很多现有实现,比如vLLM里的注意力核,就是因为分开处理缓存和新来的词,导致了规约顺序不一致。
晓曼: 要解决这个问题,就必须在计算注意力之前,先把缓存和新来的词“合并同类项”,让它们在内存里看起来就像一个完整的、连续的序列。此外,在处理长序列需要分裂规约时,不能像之前那样“固定分割数量”,比如把1000个元素分成4份,每份250个;而要采用“固定分割大小”,比如规定每份最多256个,那1000个元素就会被分成三份256的和一份232的。这样,无论序列多长,每个块内部的计算方式都是一样的,从而保证了批量不变性。
晓曼: 这些改造可以说是深入到了GPU编程的“无人区”,充满了各种工程上的权衡。但它真的能解决问题吗?这又会给整个领域带来什么影响呢?
晓曼: 通过这些听起来就非常复杂的改造,我们真的能驯服LLM的非确定性吗?答案是肯定的。文章里的实验结果非常惊人。他们用Qwen模型,在温度为零的设置下生成1000次答案。在默认的vLLM配置下,竟然产生了80个各不相同的答案。而当他们启用了自己改造的、具有批量不变性的核心操作后,1000次运行,得到了1000个完全相同的、逐比特一致的答案。
晓曼: 当然,天下没有免费的午餐。实现这种绝对的确定性,是有性能代价的。在他们的测试中,完成同样任务的时间从26秒增加到了42秒。虽然速度变慢了,但性能并没有到无法接受的地步,而且这还是未经深度优化的版本。
晓曼: 这个成果的意义,远远不止是让“复制粘贴”变得更可靠。它对一个前沿领域——强化学习——可能具有革命性的影响。怎么说呢?在强化学习里,模型通过自己“尝试-反馈-学习”的循环来提升能力,这叫“在线策略学习”(on-policy RL)。但研究者早就发现,如果模型在“尝试”(推理采样)和“学习”(训练更新)这两个阶段,底层的数值计算存在微小差异,那就好比你用A球拍练球,却用B球拍去比赛。虽然看起来差不多,但你的学习策略已经悄悄偏离了真实情况,变成了“离线策略”(off-policy)。
晓曼: 而确定性推理的实现,意味着我们可以让采样器和训练器之间的计算结果做到逐比特相同。这就实现了“真正的在线策略学习”。实验也证明了这一点:在没有这种确定性保证的情况下,模型的奖励在训练中途会突然崩溃;而有了它,训练过程就变得非常平稳。从技术图表上看,衡量策略偏离程度的KL散度,在“真·在线学习”的设置下,是一条完美的、平坦的零直线。
晓曼: 这种对系统深层理解和问题解决的追求,不仅仅是一次技术上的胜利,它更像是一种哲学层面的反思。
晓曼: 那么,回顾我们今天讨论的这一切,可以总结出几个核心的洞察。首先,大型语言模型推理的非确定性,它的根源并不是我们通常以为的采样随机性,也不是那个听起来很专业的“并发加浮点数”假说,而是因为核心计算操作缺乏一种叫做“批量不变性”的特质。
晓曼: 其次,浮点数计算顺序不同导致结果不同的“非结合性”,确实是所有数值差异的基础。但是,真正驱动这个顺序发生变化的,是推理服务器为了应对动态变化的负载,而改变了批次大小,进而改变了内部的计算策略。
晓曼: 再次,通过对RMSNorm、矩阵乘法,尤其是复杂的注意力机制进行精心的、甚至是有些“笨拙”的改造,我们确实可以在实践中实现LLM推理的完全确定性。
晓曼: 最后,实现这种确定性虽然可能带来一些性能开销,但它换来的科学可复现性,以及为“真·在线强化学习”这类高级应用铺平道路的价值,可能是无法估量的。
晓曼: 在如今这些极其复杂的软件系统里,当我们遇到非确定性、遇到那些微小又恼人的数值差异时,我们很容易选择妥协,告诉自己“反正模型本来就是概率性的,多一点随机性也无妨”。然而,今天我们探讨的这篇文章,展示了另一条路。那就是拒绝这种“失败主义”,通过抽丝剥茧,去真正理解我们所构建的系统的每一个细节,并从根源上解决问题。这最终不仅是关于代码和数字,更是关于我们对“理解”和“控制”这些复杂系统的承诺。在追求通用人工智能的漫漫长路上,每一次对深层确定性的执着探索,既是对科学精神的捍卫,也是对我们能否真正驾驭自己创造的技术力量的一次深刻反思。