推理软件栈的微小差异,就能改变输出 token
梦晨 发自凹非寺
丸辣!辛辛苦苦本地部署的大模型,怎么就是比官方版更笨??
即使是同一张显卡、完全相同的权重,推理软件栈中的微小差异,也可能让模型在关键位置输出完全不同的 token,甚至导致工具调用彻底失败。
这种陷阱很容易触发,却很难排查,简直让人怀疑是不是三体人来“智子封锁”了。

Level1Techs 论坛用户 thr3e 做了一系列实验,使用 Qwen3.6-27B 在 RTX PRO 6000 Blackwell 显卡上完成了超过 10 万个 token 的全量 logit 捕获测试。

Logits 是针对所有候选 token 计算出的一组原始分数,再经过采样器(sampler)输出下一个 token。
关键在于,logits 是纯粹的数学产物,是矩阵乘法、注意力计算、激活函数等层层叠加后的浮点数结果。如果两套系统使用同一份权重和同一段输入,理论上应该计算出完全相同的 logits。
但在现实中,浮点运算精度、累加顺序以及硬件指令集的差异,都会让最终数值产生微小偏移。
当这种偏移大到足以改变概率最高的 token 时,模型的表现就会出现差异。
虽然你的本地部署很烂,但别伤心,其他人的部署也各有各的烂法。

权重不变,换个注意力后端就变傻
推理流程中的一个关键环节,是“注意力后端”。
vLLM 为 Qwen3.6-27B 提供了三种可选的全注意力后端:FlashAttention 2、Flash Inference 和 Triton Attention。
除了切换这一项配置外,其余硬件、软件、权重以及 KV 缓存精度全部保持不变。

实验使用的输入是一段约 10 万个 token 的真实工作场景数据,来自一个包含多次工具调用的 Agent 工作流。
thr3e 特别强调,这段数据不属于任何公开基准或训练集,没有人能够针对它进行 benchmark 优化或量化校准。
测试方法是每隔 32 个 token 采样一次全词表 logit,事后使用 FP64 精度计算 KL 散度和 Top-1 一致性。
实验观察到了“Top-1 翻转”现象,也就是切换后端后,模型会选出与基线不同的贪心解码 token。
例如,在一个具体的出错案例中,模型执行了一次工具调用,目标是 Cisco 路由器上的接口 GigabitEthernet0/0/1.201。
FlashAttention 2 出现错误,导致接口变成了 GigabitEthernet0/1/4。随后,模型又在两次后续工具调用中执行了错误的命令。

为保持数学上的可比性,所有后端共享同一份强制 token 历史。翻转只记录“原本会选错”的情况,不让错误继续传播。
在最初的几千个 token 中,三个后端的输出完全一致。但随着上下文增长,分歧开始出现,而且分布并不均匀。

thr3e 同时进行了同一后端多次运行的重复性对照。仅切换 vLLM 的一个配置项,在同一块 GPU、同一份操作系统和驱动、同一份权重以及同一段 prompt 下,每一个隐藏状态的 logit 在多次运行之间都逐位相同。
这意味着,观察到的分歧完全来自不同 CUDA 核函数在 prefill 阶段执行矩阵乘法和累加时产生的数值差异。
KV 缓存量化导致智商断崖式下跌
接下来的实验保持权重为 BF16 不变,注意力后端固定为 Triton,仅改变 KV 缓存的量化精度,分别测试 BF16、INT8 和 INT4 三种配置。
结果显示,INT4 KV 缓存在长上下文中的 Top-1 翻转率急剧攀升,最终导致工具调用无法恢复。INT8 KV 缓存虽然也出现了翻转,但模型最终设法回到了正确轨道。只有 BF16 KV 缓存全程保持稳定。

thr3e 让翻转后的生成自由运行,而不是将其拉回基线,以观察实际后果。
BF16 正常完成了所有调用;INT8 在出错后“最终挣扎着恢复了”;而 INT4 则彻底偏离,工具调用失败且无法自我纠正。

这个问题尤其容易影响那些为了节省显存、将 KV 缓存压缩至 INT4 的本地用户。
在短上下文中,你可能感知不到差异;但一旦对话或 Agent 工作流延长至数万个 token,累积的数值漂移就足以让模型做出致命的错误决策。
权重量化横评:英伟达官方 FP4 垫底
最后一组实验将 KV 缓存统一为 BF16,转而比较五种不同的权重量化方案。
参赛方案包括:Qwen 官方 BF16 基线、Qwen 官方 FP8(W8A8)、TheHouseOfTheDude 发布的 INT8(W8A16,无校准数据集的一次性量化)、英伟达官方 NVFP4,以及 cyankiwi 发布的 AWQ INT4(W4A16,使用 STEM 和 Agentic 数据集进行校准)。
五种方案分别调用不同的 CUDA 核函数完成矩阵运算。BF16 使用标准 torch 线性层;FP8 使用 CUTLASS 的 FP8 分块缩放核;INT8 和 AWQ 均使用 Marlin 核。
NVFP4 则采用混合路径:208 个目标使用 FlashInfer 的 FP8 缩放核,193 个 MLP 投影使用 Marlin 的 NvFp4 核。
在本次测试所使用的 vLLM nightly 版本中,GPU 路径被判定为不支持原生 FP4 运算。因此,NVFP4 实际执行的是通过 Marlin 内核进行的仅权重 FP4 解压缩,而不是真正的 FP4 运算。

结果中最亮眼的是 TheHouseOfTheDude 的 INT8(W8A16)。这是一个未使用任何校准数据集、仅进行通道级对称量化的社区版本,其 Top-1 一致性明显优于 Qwen 官方 FP8 和英伟达官方 NVFP4。
经分析,这得益于 W8A16 保留了 BF16 激活精度,同时该量化方案排除了 Gated DeltaNet 投影层和 lm_head 层。
英伟达 NVFP4 在这组测试中的综合表现最差。当上下文长度达到约 8.8 万个 token 时,其 Top-1 翻转率逼近 50%,相当于模型在一半的位置上会选出不同的 token。
在实际工具调用测试中,NVFP4 和 AWQ W4A16 均未能正确结束工具调用,并且弄错了 Cisco 命令行语法:执行了 show run,而非正确的 show arp。FP8 和 INT8 则均顺利完成。
thr3e 还展示了张量并行带来的诡异现象:同一份 BF16 权重,TP1 单卡能够正确完成工具调用,切换到 TP2 双卡后反而失败,再切换到 TP4 四卡又成功了。
经过进一步调试和 NCCL 通信图捕获,这通常是 NCCL 跨卡归约操作中的数值差异导致的。
734 个依赖包,每一个都可能有坑
thr3e 表示,他随手下载的 vLLM nightly 容器镜像中包含 734 个软件包,其中 252 个是 Python 的 uv/pip 包。
这 734 个代码库各自存在不同的 bug 和未记录的行为特性。特定的硬件和模型配置在这座代码山中所走的路径,都是独一无二的。
这也是为什么不能轻信 Hugging Face 模型卡上标注的极低 KL 散度数值。
除非作者完整披露参考检查点、完整运行时环境、评估文本、校准数据、上下文长度、采样位置、KL 方向、词表截断方式以及聚合方法,否则这个数字根本无法解读。
目前,thr3e 已经完成了不同权重、不同模型(包括 Qwen3.6 与 Qwen3.8 的跨模型对比)、不同 KV 缓存量化方式、不同张量并行度、不同 NCCL 配置、不同注意力后端以及不同显卡(RTX PRO 6000 与 RTX 5090,同属 SM120 架构)等维度的全量 logit 捕获与分叉追踪。
他正在将测试工具和数据集打包成可分发版本,供其他用户在自己的设备上运行并上报结果。

如果你听说某个模型“炸裂、震撼、无敌”,下载到本地后却觉得它很笨,可能是因为你的推理栈中,从注意力核函数、KV 缓存精度、权重量化方案到多卡通信协议,每一层都在制造与原始基准不同的数学结果。
而这些差异在长上下文中会像滚雪球一样累积,直到模型在关键时刻做出完全错误的决定。
参考链接:
[1] https://forum.level1techs.com/t/why-your-local-llm-feels-dumber-than-it-is/253917/4