夜雨聆风学习资料网

ARTICLE · 1168376

AI 为什么两次回答不一样?真凶可能是服务器太忙了

AI 为什么两次回答不一样?真凶可能是服务器太忙了

跟 ChatGPT 问同一个问题,两遍答案不一样,这事大家早就习惯了。采样嘛,本来就要按概率随机挑 token。

但有个更瘆人的事实。

把 temperature 调到 0,理论上模型会沿着概率最高的路径贪婪解码,输出应该被彻底锁死。实际上它照样不稳。你在网页版试是这样,用 vLLM、SGLang 这类开源工具在自己机器上跑,也是这样。

这件事困扰了工程圈很久。直到,Thinking Machines 的 Horace He 团队发了篇博文,把真凶揪了出来。

结论有点反直觉:浮点数是背锅侠,真凶是服务器的忙闲。

Thinking Machines 是 OpenAI 前成员 Mira Murati 参与创立的公司,Horace He 是这篇技术博客的第一作者。

先说清楚,为什么会有误差

要让一组数加出完全一样的结果,有个隐含前提:加法顺序不能变。

浮点数偏偏做不到。受限于显示位数,两个相差好几个数量级的数相加,小的那个会被吃掉几位。

拿十进制打个比方。1230 加 23.4,精确答案是 1253.4。但系统只留 3 位精度,结果只能存成 1250。那个 23.4 在被加上之前,先被舍成了 20。

1230 + 23.4 = 1253.4(精确值)

→ 3 位精度下 = 1250

→ 23.4 先被舍成 20,才参与相加

更极端的例子在这:同一组数,只打乱相加顺序,能跑出 102 种不同的和。

这就是浮点数非结合性,(a+b)+c 和 a+(b+c) 可以不一样。误差的根源确实在这。

但背锅侠这次真没背稳

业内的传统解释是「并发 + 浮点数」。GPU 并行计算,谁先算完谁先加,累加顺序一变,结果就飘。

这个假设解释不了一件事。

同一块 GPU 上,同两个矩阵相乘,跑一千遍,结果位位相同。浮点数在用,并发也在跑,这一次为什么稳如老狗?

那篇被引用的 arXiv 预印本就是这套假设的代表,把非确定性一股脑归给浮点非结合性和执行顺序。眼看圆不回来,真凶必然藏在别的地方。

服务器忙不忙,决定了你的答案

真正的机制叫批处理不变性。先说两个容易混的概念。

运行间确定性
同一批请求,每次跑结果都一样。推理服务器能做到。对你完全相同的请求,它每次都吐出相同的输出。
批处理不变性
一次处理 1 个请求和一次处理 100 个请求时,单个请求的结果完全一样。这个推理服务器做不到。

从服务器的视角看,它是确定性的。但站在单个用户的视角,系统的输入只有你的请求,其他并发用户带来的只是波动属性。你问什么不重要,那会儿有几个人在跟你抢 GPU,才重要。

想象一个快递分拣站,分拣员是 GPU 核心,原则是把算力榨干。

快递堆成山时,三个分拣员各管一片,各自扫码、各自归类,互不打扰。这对应数据并行。

快递少的时候,一个区的活不够一个人忙,就把这堆货拆开:一个人扫码,一个人归类,最后再把结果汇总。这对应拆分归约。

两种做法都在优化吞吐。麻烦在于汇总方式变了,浮点误差的路径跟着变。

LLM 里到处是这种「汇总」,专业说法叫归约(reduction)。RMSNorm、矩阵乘、注意力,三个核心运算全都要归约。而推理引擎会根据当前 batch 大小,悄悄切换归约策略。

高峰时段你的请求可能和一百个人挤在同一批,闲时可能单独成批。batch 大小变了,归约方式就变,同一个问题于是有了不同的答案。

它不是抽风。它只是那会儿正好有别人在蹭车。

怎么修:让内核变得批处理不变

思路直接摊牌。不管 batch 多大,三个核心运算的归约策略全部锁死。

RMSNorm
归一化要算均值,批量小的时候引擎会从单核归约切换成多核拆分。修法是固定一种归约方式,宁可损失小批量下的性能。
矩阵乘
批量变小的时候,引擎会切更小的计算块(tile),甚至直接关掉张量核。修法是编译一份内核配置,所有形状共用,tile 大小一致,张量核不轻易关。
注意力
这个最麻烦,得同时处理特征和序列,还带着 KV Cache。修法是固定大小的 KV 分片:KV 长度 1000,以前切成四段 250,现在切成三段 256 加一段 232。分片大小写死,归约策略就不再随 batch 波动。

效果:1000 次生成,一次分叉都没了

实验用的是 Qwen3-235B-A22B-Instruct-2507,temperature=0,同一个 输入「谁是费曼?」生成 1000 次,每次 1000 个 token。

结果先泼一盆冷水。

1000 次跑出 80 种不同的输出,最常见的那个出现了 78 次。前 102 个 token 完全一致,第 103 个开始分叉。

所有输出都写到了「费曼出生于1918年5月11,在......」,然后开始兵分两路。

992 次Queens, New York
8 次New York City

一个稳如泰山的 235B 大模型,在 1000 次生成里,连费曼的出生地都拿不准。而它抽不抽风,取决于当晚服务器的并发量。

换上批处理不变内核之后,同样的 1000 次生成,输出完全一致。模型没抽风。它那会儿正好有别人在蹭车。

代价:慢了 16 秒

用 Qwen3-8B 单卡压测,1000 个请求各输出约 100 token。

默认 vLLM 26 秒
确定性版,未优化 55 秒
优化注意力内核后 42 秒

用百分之六十的性能余量,换一份可复现。在强化学习和在线评测这类场景,这笔账划算。

真正该紧张的是 RL 圈子

这篇博客最重的落点其实在这。

做强化学习的都知道一个难言之隐:采样器在跑模型,训练器也在跑同一份模型,两边对同一个 token 算出来的概率永远差一点。你的 on-policy RL,其实是偷偷降级成了 off-policy RL,得靠重要性权重硬掰回来。

而确定性的逻辑链是这样的:如果两次相同的推理请求都拿不到位位相同的结果,训练和推理之间更不可能对上。只有把推理侧也做到位级一致,采样和训练之间才能谈得上真正重合。

KL 散度归零,重要性权重归 1,on-policy 才是字面意义上的 on-policy。

Horace He 团队的实现已经开源在 thinking-machines-lab/batch-invariant-ops。做法是借 vLLM 的 FlexAttention 后端,再用 torch.Library 非侵入式替换掉相关算子。

相关学习资料