ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型推理输出不稳定?采样参数与硬件浮点误差全解析

大模型推理输出不稳定?采样参数与硬件浮点误差全解析 最近在做模型效果评估的时候遇到一件让我琢磨了一阵子的事同一份测试集、同一个 checkpoint、同一条 prompt前后两次跑推理拿到的输出居然不完全一样。一开始我以为是代码里有隐藏的随机操作没注意到后来排查了一圈才发现这个现象在大模型推理里其实非常正常甚至可以说如果你从来没遇到过反而有点反常。这篇内容就是想把“推理阶段同一样本 LLM 输出结果不同”这件事彻底拆开讲清楚。我会从数学模型层面的随机性来源讲起再逐层拆解到采样参数、输入侧 padding、KV Cache、浮点数精度这些容易被忽略的细节最后给出我在实际项目里总结的一套稳定复现和排查方法。适合正在学习大模型原理、做模型评估、或者上线 LLM 服务的同学参考。1. 先别急着怀疑模型坏了LLM 推理输出背后的概率机制1.1 你看到的输出本质是从概率分布里“抽”出来的大多数第一次接触 LLM 的朋友会下意识把大模型当成一个“输入一句话输出一句话”的确定性函数同一个输入结果就应该一样。但如果你看过模型源码或者自己手动加载过一次权重就会发现完全不是这么回事。LLM 的核心是预测下一个 token 的概率分布。模型内部实际上是一个巨大的条件概率函数 P(y_t | x, y_1, y_2, ..., y_{t-1})也就是给定前面的输入 x 和已经生成的历史 token计算词表 V 中每个 token 出现的概率。每一步生成都是在整个词表上做一次概率分布预测分布里包含的可能不是某一个 token 一枝独秀而是很多 token 都有不小的概率。举个例子输入“今天天气真”模型可能认为“好”的概率是 0.45“棒”的概率是 0.2“不错”的概率是 0.15剩下 0.2 分布在其他几千个 token 上。这时如果直接用 argmax 取概率最大的那个得到的一定是“好”。但如果用了采样sampling结果就变成“从整个分布中按照概率抽样”这次抽到“好”下次也可能抽到“棒”。所以同一样本输出不同首先要去查的就是你的解码策略是不是采样模式。这一步可以直接解释掉 80% 以上的“结果不稳定”现象。1.2 解码策略的分岔路口贪心搜索与随机采样推理阶段常用的解码方式大致可以分成两派。一派是贪心搜索Greedy Search每一步都取概率最高的 token因此如果模型计算完全确定输出就是确定的。这种方式适合要求稳定输出的场景比如信息抽取、结构化数据生成。缺点是容易陷入重复和呆板缺乏多样性。另一派是随机采样Sampling按照概率分布随机抽 token。为了让采样质量更高又衍生出了 temperature、top_p、top_k 这些控制参数。采样模式天然带有随机性即使模型权重、输入完全相同只要随机种子或者底层随机状态不一致输出就可能不同。还有介于两者之间的 beam search它每一轮保留 top-K 条概率最高的路径最终输出得分最高的一条。理论上 beam search 是确定的但如果实现里对相同分数的路径做了不稳定排序或者浮点计算有细微差异也可能出现不同。我自己的经验是遇到“输出结果对不上”第一步就是看一眼代码里 generation 的配置# HuggingFace transformers 常见的参数 outputs model.generate( input_ids, do_sampleTrue, # 开启采样 temperature0.8, top_p0.9, top_k50 )只要do_sampleTrue结果就天然具备随机性这不是 bug而是设计如此。你想完全复现就要把do_sampleFalse或者temperature0才能让输出变成贪心结果。1.3 为什么有些场景下“随机”反而是刚需聊到这里可能有人会问既然随机性会导致结果不稳定为什么还要用采样因为语言生成任务本身是“一个输入多种合理输出”的问题。比如你让模型写一段宣传文案、写一首诗、或者给一个开放性问题做头脑风暴如果每次都返回同一句话那模型生成能力就被浪费了。采样的意义在于让模型在概率分布中“有选择地探索”增加输出的多样性和丰富度。日常开发中我一般这样区分使用场景稳定性优先抽取、分类、翻译、代码生成→ 关闭采样用贪心或固定 seed多样性优先文案写作、对话交互、头脑风暴→ 开启采样并调节 temperature 和 top_p评测对比AB 两个模型或多个 checkpoint 对比→ 要么全关采样要么固定所有随机因素否则结果没有可比性理解了这一层后面讲的所有“输出来源差异”才能落到实际场景里去判断该不该处理。2. 温度、top_p、top_k、seed这四个参数是怎么联手制造“随机”的2.1 温度不是“热度”是概率分布的形态开关temperature温度是采样模式下影响最大的参数。它的数学本质很简单在 softmax 层里把模型输出的 logits 除以 T 再做归一化。softmax 的标准形式是softmax(z_i) exp(z_i) / Σ_j exp(z_j)加了温度之后变成softmax(z_i / T) exp(z_i / T) / Σ_j exp(z_j / T)当 T 1 时分布不变T 越小分布越“尖锐”高概率 token 的概率被进一步放大T 趋近于 0 时分布逐渐退化成 one-hot也就是 argmax 贪心T 越大分布越“平坦”低概率 token 被抽中的概率变大输出越随机。从工程角度看temperature0通常并不是真的做除法而是框架在内部直接切成贪心路径。HuggingFace 和 vLLM 在检测到temperature0时会自动使用 argmax 替代采样。但如果你设置的是temperature1e-8这种“微量非零”的值框架还是走采样逻辑只是在几乎所有情况下都抽到概率最大的 token理论上仍有极小概率出现不同。所以做实验的时候想要绝对稳定建议直接设置do_sampleFalse不要依赖“温度极低”来近似贪心。2.2 top_p 和 top_k候选词的闸门top_k 的思路最简单每次只保留概率最高的 K 个 token把它们重新归一化再采样。K 越小候选越少输出越保守K 越大候选越多越容易出现冷门词。top_p也叫 nucleus sampling稍微绕一点把 token 按概率从高到低排序然后从累计概率超过阈值 p 的那一批 token 里采样。比如 top_p0.9表示只保留累计概率达到 90% 的那一小撮 token剩下的低概率尾巴直接砍掉。这两个参数本身不直接制造随机性但它们决定了“采样池”的大小。池子越大采样结果的分布越宽同一样本多次运行时输出差异越明显。换句话说如果你发现两次输出差异太大除了种子问题还可能是 top_p 或 top_k 设置得太宽松。我踩过的一个实际坑是微调后的对话模型在开放域问答里表现很好但每次输出都不一样用户反馈“同一个问题问两次答案前后矛盾”。最终定位到是配置里同时开了 temperature0.95、top_p0.95、top_k60随机性叠加后非常强。后来把 top_k 关掉设为 0 或 None、top_p 降到 0.85、temperature 降到 0.7输出稳定性明显提升多样性也没有损失太多。2.3 随机数种子能固定一部分随机但不是全部很多人在代码里加了torch.manual_seed(42)之后发现结果还是对不上此时容易陷入重启循环“这 seed 到底有用没用”实际上seed 只对你当前进程里调用随机数生成器的顺序和初始状态有影响。如果模型推理过程中有多个随机源比如 dropout 在推理模式本来应该关闭但某些实现不小心开了或者采样时用了 NumPy 的np.random而你不小心在别处先调用了一次np.random导致采样时拿到的是“偏移后的随机流”——这些都会让同一个 seed 得到不同结果。更隐蔽的是 GPU 算子的非确定性。在 GPU 上做并行矩阵运算时浮点数累加的顺序不是固定的同一份数据在不同线程调度下计算出来的浮点结果可能有一个非常微小的差异。这个差异在逐 token 生成时会被放大因为下一步的语言模型预测拿到的是上一步那个“有一丁点不同”的隐藏状态几步之后就可能选出完全不同的 token。所以更准确的说法是seed 能固定逻辑层面的随机序列但固定不了硬件层面的浮点不确定性。这在 CPU 上通常不是问题在 GPU 上就需要额外手段下面会讲。2.4 一组对比同样参数下配置差异如何影响稳定性我把常见配置下的稳定性直观列一下方便对照配置是否稳定说明do_sampleFalse / temperature0基本稳定贪心路径输出确定性最高temperature0.7, top_p0.9随机常见生成配置稳定性一般temperature1.0随机性较大标准 softmax 采样分布未压缩temperature0.1较稳定但极小概率仍会变beam search, num_beams4基本稳定但浮点误差可能导致不稳定采样 seed 固定部分稳定代码逻辑内可复现硬件层面不保证这张表是我多次实验后的经验总结不是官方定义但足够帮你在排查问题时快速定位方向。3. 比采样更隐蔽的变量batch padding、KV Cache 和浮点数运算如果说采样是明面上导致输出随机的“主犯”那这一节要讲的三个因素就是妥妥的“帮凶”。它们在代码里很难一眼看出来而且往往在你想复现别人的实验结果时突然冒出来给你一拳。3.1 同一条样本放进不同 batch结果竟然变了这个现象我第一次遇到时真的很困惑单独跑一条样本输出稳定和其他样本拼成一个 batch 跑输出就不一样了。更奇怪的是batch 里的其他样本顺序一换结果又变了。原因出在 padding 和 attention mask 上。LLM 训练时通常以 batch 为单位一个 batch 里的样本长度不一需要把短的样本 pad 到和最长样本一致然后用 attention mask 告诉模型哪些位置是真实 token、哪些位置是 padding token。推理时很多框架为了高速利用 GPU也会把多条请求拼在一起做 batch 推理。问题在于padding 的位置虽然被 mask 掉但在很多实现里padding 位置的 embedding 向量依然会被灌进注意力计算只是最终被 mask 遮住这会产生一个极其微小的数值噪音。另外attention 的计算里分母需要对整行做 sum不同 batch 大小、不同 padding 形状会导致浮点累加顺序不同结果自然有细微差异。这种情况下表现在最终输出上可能是相同的也可能是某个概率本来就很接近的 token 位置突然成了分水岭导致输出从“很好”变成“还不错”。所以在做严格对比实验时我会把每条测试样本单独推理或者保证所有对比实验使用完全相同的 batch composition相同样本、相同顺序、相同 padding 策略否则你没法判断分数差异来自模型能力还是 batch 构造差异。3.2 KV Cache 截断对长文本推理的影响第二个隐蔽变量是 KV Cache。生成阶段每个 token 都要关注历史所有 token 的 Key 和 ValueKV Cache 就是把之前算好的 K、V 缓存下来避免重复计算。对于超长输入有些框架会做 KV Cache 逐出eviction或压缩比如 H2O、StreamingLLM 这类方法把部分历史 token 的 KV 丢弃。一旦发生丢弃后续 token 的注意力计算就只基于剩余的部分历史结果和“完整历史”肯定不一样。更麻烦的是不同框架、不同版本的 KV Cache 策略可能不同同一份 prompt 在不同框架里推理结果不同很可能就是这个原因。如果你只是做一般长度的文本生成KV Cache 截断未必会触发但如果你在做长文档 summarization 或者多轮对话这类长上下文场景建议先确认你用的推理框架是否默认开启了 KV Cache 淘汰以及它的触发条件是什么。3.3 浮点数精度与算子的非确定性GPU 并行累加的顺序问题这一节稍微硬核但值得花两分钟理解。GPU 的矩阵计算是大规模并行的以 attention 里的缩放点积为例要把一个向量里的元素累加成一个标量。并行时多个线程分别算部分和再逐层合并。不同线程调度顺序下累加结果在浮点数层面可能不一样。浮点数的运算是非结合律的(ab)c 的结果不一定等于 a(bc)因为中间步骤可能发生舍入误差。所以同样的公式在不同批次、不同线程调度下结果可能差个 1e-7 级别。这个极小差异经过多层 Transformer 的矩阵乘法传递后会被放大最终在 softmax 上变成概率分布里一个很微小的波动。如果此刻 model 的高概率 token 之间本来差距就不大就可能出现抽样结果翻转。需要强调的是这类非确定性在推理阶段大概率不会有肉眼可感知的质量差异但如果你在做“同输入必同输出”的强一致性要求比如用 LLM 做语义检索排序、评分卡、或者代理决策就需要意识到这一点。4. 几个真实场景里的“输出不稳定”排查案例前面讲理论这一节直接用实际案例走一遍完整的排查链路。这些案例都是我在日常开发和复现别人项目时真实遇到过的踩坑过程比直接给结论更有参考价值。4.1 案例一微调后评估同样的测试集两次分数波动背景我在用 LoRA 微调一个 7B 模型做文本分类评估时用固定测试集跑准确率。第一次跑了 86.3%第二次重跑变成了 85.9%。排查过程检查代码里是否固定了评价 seed发现没有。但即便如此分类任务如果不采样理论上结果应该稳定。查看生成配置发现do_sampleTrue且temperature0.9。这是从对话生成脚本里继承下来的配置评估时忘了改。把所有评估统一改成do_sampleFalse并固定 batch composition 后两次结果完全一致。结论评估类任务必须关闭采样否则每次跑出来的指标都是带随机噪声的。4.2 案例二线上服务与离线脚本结果不一致背景模型在离线脚本里输出“好的”线上 API 却返回“没问题”prompt 明明一样。排查过程对比了 prompt 字符串本身发现离线脚本会额外加一个系统提示词线上没有。对比采样参数线上服务的 temperature0.7离线脚本是默认的 1.0。对比框架版本线上用的是 vLLM离线是 HuggingFace transformers两者的 KV Cache 实现、算子融合策略不同即使参数一致也可能出现微小差异。结论线上和离线结果不一致先查 prompt 和生成参数再查框架差异。这个案例最终改成了两套环境都固定同一份参数配置并在 prompt 里显式加入同样的系统提示词。4.3 案例三vLLM 重启后相同请求输出不同背景用 vLLM 部署服务第一次发请求返回 A重启服务进程后发相同请求返回 B。排查过程排除了采样随机性因为温度设为 0理论上走贪心。排除了输入差异请求内容完全一样。怀疑 vLLM 的 prefix cache 在重启前后状态不同。vLLM 会对请求做 KV Cache 自动缓存如果前一次请求的部分计算被缓存命中数值累加顺序和完全重新计算时有差异导致后续生成出现微小分层。使用相同模型权重、相同参数配置在两台机器上分别测试结果稳定。再确认代码里没有未固定的随机源后基本可以确定是 GPU 算子浮点非确定性和 cache 机制共同作用导致的极小概率差异。结论这种差异概率极低也不会对文本质量产生明显影响。如果业务对一致性要求极高可以考虑锁卡、固定 batch shape、并做多次自洽校验。5. 想要结果可复现实操层面该怎么做前面讲清楚了“为什么不同”最后分享一套我项目里用得比较顺手的复现和稳定方案。它不保证 100% 绝对一致但能让你在绝大多数场景下得到可复现的结果。5.1 固定随机种子的标准写法代码层面建议在推理脚本入口统一固定所有随机源import os import random import numpy as np import torch def set_seed(seed42): os.environ[PYTHONHASHSEED] str(seed) random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 尽量让 cudnn 使用确定性算法 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False # 开启确定性算法注意部分算子会稍微变慢 torch.use_deterministic_algorithms(True, warn_onlyTrue) set_seed(42)这里最容易被忽略的是torch.backends.cudnn.benchmark False。如果 benchmarkTruecuDNN 会在第一次运行时自动选择最快的卷积/注意力算子不同运行可能选到不同实现结果会有细微差异。同时torch.use_deterministic_algorithms(True)会强制 PyTorch 优先选择确定性实现但部分算子比如某些 attention kernel不支持时会直接报错所以加上warn_onlyTrue让它降级为警告而不是中断。5.2 采样参数检查清单每次跑实验前建议先确认这一组配置do_sampleTrue 还是 False。希望稳定就 False。temperature是否等于 0。等于 0 等价于贪心。top_p、top_k是否关闭或设置了一个足够大的范围。repetition_penalty、no_repeat_ngram_size这些也会改变每一步 token 的概率影响最终输出。max_new_tokens长度不同也会导致后续生成路径不同。我在团队里放了一个配置模板每次跑生成任务前复制一份并记录 git commit id 和模型文件 hash这样即使结果不一致也能快速定位是代码改了还是模型换了。5.3 对比实验的规范固定 batch、固定 padding、固定后端如果你要做模型对比或者算法消融实验强烈建议按下面这套规范执行单条样本单独推理避免 batch 内 padding 引入差异。如果必须 batch 推理固定 batch 内样本的顺序和 padding 方式不要让 dataloader 每次 shuffle。所有对比跑在同一个 GPU 型号和同一个推理框架版本上不要一台机器 A100、一台机器 4090 做横向对比。记录每个实验的推理框架版本、CUDA 版本、模型 dtypefp16/bf16/fp32。其中 dtype 的影响很大。同一个模型用 fp16 和 bf16 推理结果大概率不一样因为两者的数值范围和舍入方式不同。有些模型在低精度下高概率 token 之间的差距会被抹平导致采样结果更随机。5.4 要不要在“绝对一致”上死磕我的建议最后聊点个人的判断。追求“绝对一致”在某些场景下是合理的比如跑自动化测试、写 RAG 管道的确定性评测、或者做需要审计的决策系统。但更多场景下LLM 的多样性和轻微随机性反而是优势。我的做法是分层看待对基线评测和质量回归统一用贪心解码或固定 seed保证结果可复现对线上生成保留适度的采样随机性再做规则层面的后处理比如关键词兜底来控制质量底线。这样既不会因为每次输出都一样而损失生成的自然度也不会因为完全随机而让下游系统无所适从。另外有一个实战小技巧如果你在调试 prompt想让模型反馈更稳定可以把 temperature 从 0.7 降到 0.4然后同时调低 top_p 到 0.85往往比直接改 prompt 效果更明显。反过来如果觉得模型回复太死板、缺乏灵活性优先调大 temperature而不是动 top_p因为 top_p 的变化更容易让输出突然“崩坏”。说实话刚开始接触 LLM 推理时我也被“同一样本输出不同”这件事折腾过不少时间。后来想明白了这不是模型出 Bug而是概率生成模型天然的工作方式。真正需要做的不是消灭随机性而是搞清楚随机性来自哪里、什么时候该控制它、什么时候该利用它。希望这篇文章能帮你在遇到类似问题时少走一些弯路。
返回列表