文章标题:用纯整数算术消除 LLM 推理中的非确定性
正文: 不久前,Thinking Machines 发表了《Defeating Nondeterminism in LLM Inference》,解释了为什么贪心解码在生产环境中并非确定性的。理论上,在温度 0 下,模型总是选择其最可能的下一个 token,因此同一个提示每次都会给出相同的答案。在实践中,当他们把同一个提示发送 1,000 次时,得到了 80 个不同的答案。1
问题在于浮点加法不满足结合律。(a+b)+c(a + b) + c(a+b)+c 和 a+(b+c)a + (b + c)a+(b+c) 的舍入结果可能不同。GPU 为每个 token 要对数千个数字求和,而求和的顺序取决于批次中还有多少其他请求等因素。改变顺序,就会改变舍入,偶尔还会改变哪个 token 胜出。
他们的解决办法是固定顺序:重写 kernel,使给定的 GPU 始终以相同的顺序相加数字,无论批次中还有什么其他内容。这确实有效,但只在那块 GPU 上、用那些 kernel 时才有效。H100 和 A100 使用不同的矩阵乘法 kernel,CPU 使用的指令则完全不同,每一种都有自己的舍入方式。在三种硬件上运行同一个模型,你仍然会得到三个不同的答案。
我一直想,如果我们干脆不用浮点数会怎样?整数加法是精确的,所以顺序不可能有影响。如果模型中的每个操作都使用整数,那么输出在任何地方都应当是逐位相同的:任何批次大小、任何 GPU、任何 CPU、任何厂商。
最终,我在一次 SpaceXAI hackathon 期间找到了时间来做这件事。这个项目名叫 DetLLM,其效果足够好,最终成为两场 NeurIPS 2026 workshop 的论文。2 在这篇文章中,我将带你了解为什么 LLM 推理是非确定性的、为什么整数能解决这个问题,以及如何在不进行任何一次浮点运算的情况下运行 LLM 的每一个部分(矩阵乘法、softmax、归一化、旋转嵌入)。
上面的动画就是全部结果。同一个提示在三台机器上的九种配置中运行;fp16 的运行从第一个 token 起就开始漂移,在第 457 步时,其中一个分叉到了不同的文本。整数运行则始终纹丝不动。
为什么 GPU 不是确定性的?
每一次浮点加法都会舍入
浮点数存储固定数量的有效数字和一个单独的指数。当你把两个指数相差很大的数相加时,较小的那个必须移位以便与较大的那个对齐,而从末尾掉落的数字就丢失了。每一次加法都会舍入。其结果是,(a+b)+c(a + b) + c(a+b)+c 并不总是等于 a+(b+c)a + (b + c)a+(b+c):
>>> (0.1 + 1e20) - 1e20
0.0
>>> 0.1 + (1e20 - 1e20)
0.1
LLM 的前向传播主要就是求和:每一次矩阵乘法、softmax 和层归一化都会把一长串数字加起来,每一步都在舍入。这些加法的顺序由 kernel 决定。下面,同样的八个 fp32 值以两种方式求和:从左到右,以及分成两半再合并(这就是“split-K”矩阵乘法 kernel 的做法)。3
为什么 kernel 不总是以相同顺序相加
自然的猜测是:GPU 线程以随机顺序完成,所以求和也以随机顺序落定。在使用原子加时确实可能发生这种情况,但这大多不是 LLM 前向传播中发生的事。在相同输入上运行同一个矩阵乘法一千次,你会一千次得到相同的比特。大多数推理 kernel 是运行间确定性的。
它们不是批次不变的。cuBLAS 并没有一个矩阵乘法 kernel;它有一个目录,会根据问题的形状挑选一个。小批次无法让每个流式多处理器都保持忙碌,所以库会把归约拆成块并分别求和。大批次有足够的行可供分配,所以每一行都在一趟中完成归约。你的行在两种情况下是一样的。它被相加的顺序则不是。
文章标题:用纯整数算术消除 LLM 推理中的非确定性
正文: 在服务端点上,批大小取决于那一刻有多少其他人在发送请求。所以你的输出取决于你看不到也无法控制的流量。批大小也不是唯一的原因。Prefill 一次性用一个大矩阵乘法处理你的整个提示词,而 decode 一次处理一个 token,因此相同的 token 会经过不同的 kernel。而如果 KV cache 以不同方式分块,注意力就会以不同的顺序求和。4
跨设备时,不存在共享的顺序
在单块 GPU 上,这个问题已经解决。Thinking Machines 的批不变 kernel 固定了每一次归约的顺序,无论批大小或分块方式如何,代价是矩阵乘法大约慢 20%,而他们全部 1,000 次补全返回的结果完全一致。5
跨设备时,没有任何东西可以固定。A100 和 H100 拥有不同的张量核心、累加宽度和 kernel 目录,而 CPU 根本不会像张量核心那样做 fp16 运算。PyTorch 的可复现性说明不承诺跨平台有任何保证。可复现的浮点库试图弥合这一差距,6 但前提是每个平台上的每个 kernel 都按照它们的标准编写。
如果我们根本不用浮点数呢?
整数加法是结合性的。它也是可交换的。(a+b)+c=a+(b+c)(a + b) + c = a + (b + c)(a+b)+c=a+(b+c) 对每个 aaa、bbb、ccc 都成立,只要你不溢出,而溢出是你可以提前界定范围的。这意味着每一次归约,无论以任何顺序、在任何正确的硬件上,都会产生相同的比特。下面是和之前相同的八个值,乘以 4 使其成为整数,经过相同的两个设备。
使用整数时,确定性来自算术本身。你不需要特殊的 kernel 或固定的顺序,两个厂商只需要就整数乘加是精确的这一点达成一致。
但 int8 推理已经存在(SmoothQuant、LLM.int8、OmniQuant),而它在不同硬件上并不具有确定性。这是因为它属于模拟量化:矩阵乘法以 int8 运行,但它们之间的一切(softmax、RMSNorm、SwiGLU、旋转嵌入)都退回浮点数。每一个这样的浮点步骤都是归约顺序重新泄漏进来的地方。
整数存储不是整数算术。任何一处浮点步骤都会破坏它,所以整个前向传播都必须是整数,从输入 id 到 logits。
DetLLM
DetLLM 以精确整数算术运行 Qwen3-0.6B,从输入 id 到 int32 logits 之间的每一个操作都是如此:每一次矩阵乘法、归一化、激活、softmax 和旋转嵌入。整数算子来自 I-LLM,属于一系列关于面向边缘硬件的纯整数 transformer 的工作(I-BERT、I-ViT、I-LLM),这些工作关心的是效率而非确定性。7 在 8 比特下它不需要微调,只需一次校准过程和两个闭式平滑技巧。8 关键的几个部分:
Dyadic 缩放。每个张量携带一个缩放因子 m/2km / 2^km/2k,其中 mmm 是整数尾数、kkk 是移位量,因此重新缩放就是一次整数乘法和一次移位,并遵循一条固定的舍入规则。Exp、softmax、RMSNorm 和 SwiGLU 都是对这些值进行的基于移位的整数算法,而 RoPE 使用预计算的 int16 正弦和余弦表。
构造上即批不变。每个 token 的激活值都由该 token 自身的值量化而来,因此它的比特不可能依赖于批次中的其余部分,也不需要特殊的 kernel。例外是 KV cache,因为按 token 的缩放因子会落入 softmax 或 probs·V 求和中,所以 K 和 V 使用离线校准的固定缩放因子。缓存值只量化一次、之后不再量化,因此 prefill 和 decode 看到的是相同的比特。
一个硬件相关的函数。只有 int8 × int8 → int32 矩阵乘法会根据硬件分派(CUDA 上为 torch._int_mm,CPU 上为普通的 int32 矩阵乘法)。由于整数累加是精确的,跨设备匹配就归结为那个矩阵乘法是否返回精确结果,这一点由测试检查。一个 TorchDispatchMode 守卫会把前向传播中的任何浮点张量变成错误,而 argmax 在平局时倾向于最小的 token id,而不是信任后端。
一个提示,九种配置,一个哈希
该演示取 Sonnet 18 的前 64 个 token,贪婪生成 512 个 token,并对每一步的 logits 进行链式哈希。每种配置针对不同的非确定性来源:batch 1;batch 8,其中七个随机提示共享该批次;预填充一分为二,后半部分逐个 token 输入;以及一个纯 CPU 后端,不使用 cuBLAS、Triton 或 CUDA graphs。它们在三台机器上运行:一台 A100 服务器、一台 H100 服务器和一台 MacBook。
| 设备 | 配置 | int8 哈希 | fp16 哈希 |
|---|---|---|---|
| NVIDIA A100 | batch 1 | 64430dd985f8 | af3ffdc1592d |
| NVIDIA A100 | batch 8,7 个随机共提示 | 64430dd985f8 | 12cf67ceecbb |
| NVIDIA A100 | 拆分预填充 + 逐 token | 64430dd985f8 | 229b14ee3995 |
| NVIDIA H100 | batch 1 | 64430dd985f8 | 88b7a5544ac5 |
| NVIDIA H100 | batch 8,7 个随机共提示 | 64430dd985f8 | b1a5a93c743d |
| NVIDIA H100 | 拆分预填充 + 逐 token | 64430dd985f8 | 246e001ebbfd |
| AMD EPYC 7J13 (CPU) | batch 1 | 64430dd985f8 | 02880fd41404 |
| Intel Xeon 8480+ (CPU) | batch 1 | 64430dd985f8 | 22af55b4f422 |
| Apple M1 Pro (CPU) | batch 1 | 64430dd985f8 | 54ed3db507f9 |
| 1 个唯一哈希 | 9 个唯一哈希 |
九次整数运行,一个哈希。九次 fp16 运行,九个哈希,每一对从第一个 token 起就不一致。四个厂商和三种指令集产生比特级相同的 logits,且无需针对每个平台进行调优。算术已没有产生分歧的余地。9
发散的 logits,一致的 token
fp16 列中有一个转折。该演示还对生成的 token id 进行哈希,九次 fp16 运行中有八次产生相同的 512 个 token,尽管每一步的 logits 都不一致。贪婪解码是 argmax,而 argmax 会忽略任何小于前两名候选之间差距的扰动。在此提示下,批次组成使第 0 步的 logits 偏移最多 0.094,而前两名差距为 2.66,有 28 倍的缓冲。
这种缓冲异常宽裕。大多数步骤要接近得多:512 步中有 45 步的前两名差距低于 0.5。在第 457 步,在 A100 上“ introduces”仅领先“ is” 0.03。在 M1 Pro 上,两者完全打平,argmax 选择较小的 token id“ is”,文本出现分叉:
A100:第一行介绍了这个问题……M1 Pro:第一行是“我可以把你比作夏日吗?”……
A100:第一行介绍了这个问题……
M1 Pro:第一行是“我可以把你比作夏日吗?”……
每个生成的 token 都是又一次接近平局的机会,因此生成越长,分叉的可能性越大。浮点非确定性通常是潜伏的,而非不存在。
困惑度与速度
明显的担忧是移除所有浮点数会损害模型。但影响甚微:WikiText2 困惑度为 20.72,而 fp16 为 20.95,C4 差了 0.4%。
| WikiText2 (A100) | WikiText2 (H100) | C4 | |
|---|---|---|---|
| fp16 | 20.9549 | 20.9552 | 31.5696 |
| int8 (DetLLM) | 20.7170 | 20.7170 | 31.6966 |
速度则是另一回事。fp16 基线运行在高度优化的内核上,如 cuBLAS 和 FlashAttention。整数推理几乎一无所有:PyTorch 只提供了一个 int8 matmul,仅此而已,因此 softmax、RMSNorm、SwiGLU 和 attention 都必须用小整数操作手工构建。在融合整数操作、添加 CUDA graphs 并编写融合 Triton attention 内核后,解码从每秒 0.8 个 token 提升到 106 个。
| 解码 tok/s | fp16 eager 10 | int8 eager | int8 compiled | int8 final |
|---|---|---|---|---|
| A100, batch 1 | 29.3 | 0.8 | 5.6 | 106.3 |
| A100, batch 8 | 233.8 | 5.5 | 42.9 | 783.3 |
| H100, batch 1 | 19.5 | 2.8 | 5.8 | 73.8 |
| H100, batch 8 | 157.9 | 22.3 | 43.4 | 566.7 |
局限性与下一步
这只是一个概念验证,但我对它表现得这么好感到意外:困惑度与 fp16 相当,而且只要有足够的 kernel 工作,decode 就不会卡在 eager 速度上。更大的模型、其他架构、整数采样、prefill 速度,以及手机或 NPU 硬件都还是开放问题,kernel 侧大概也还有很多技巧没用上。
这个保证绑定在一个预先准备好的 artifact 上,因为 float 到 int 的转换本身并不可复现。如果想试试,代码在 GitHub 上。在任何地方运行 int8 配置,你都应该得到 64430dd985f8。如果得不到,请告诉我。
引用
请按如下方式引用本工作:
Barry, Nathan. "Eliminating Nondeterminism in LLM Inference with Integer-Only Arithmetic".
nathan.rs (Nov 2026). https://nathan.rs/posts/detllm.
或使用 BibTex 引用:
@article{barry2026detllm,
title = "Eliminating Nondeterminism in LLM Inference with Integer-Only Arithmetic",
author = "Barry, Nathan",
journal = "nathan.rs",
year = "2026",
month = "Nov",
url = "https://nathan.rs/posts/detllm"
}
- Horace He 等,Defeating Nondeterminism in LLM Inference,Thinking Machines Lab,2025。本文前半部分大量依赖他们的诊断,这是我读过的对这个问题最清晰的解释。 ↩︎
- 研讨会论文是 Eliminating Nondeterminism in LLM Inference via Integer-Only Arithmetic,发表于 NeurIPS 2026 的 ML for Systems 和 On-Device Intelligence workshops。本文沿用论文的结构,但花更多时间讲“为什么”,较少讲工程附录。 ↩︎
- 这些量级是人为构造的,以便舍入在八次加法而不是八百万次加法中就可见。2242^{24}224 是 fp32 耗尽整数精度的地方,所以 224+12^{24} + 1224+1 会舍入回 2242^{24}224。展示的每个部分和都是真实的 Math.fround 结果;没有伪造任何东西。 ↩︎
- Thinking Machines 详细梳理了 attention 的情况:80 个 token 的 KV cache 加上 48 个新 token,会分成五个 32 的块进行归约,而同样的 128 个 token 一次性处理时只分成四个块归约,这改变了顺序。 ↩︎
- 同一篇文章还说明了为什么这不止关乎可复现性:当采样器和训练器逐位一致时,RL 才真正成为 on-policy,两者之间的 KL 散度会降到恰好为零。 ↩︎
- ReproBLAS 在 IEEE-754 假设下让求和与顺序无关,而 RepOps(来自 Verde 论文)固定了受支持硬件上每个浮点运算的顺序。两者都保留浮点模型,而整数执行不保留;作为交换,每个平台上的每个 kernel 都必须实现可复现的算子。 ↩︎
- 批不变性、prefill/decode 不变性以及跨设备逐位精确性,在那类文献中都不是已有主张。据我所知,DetLLM 是第一个同时针对这三点端到端构建并验证的流水线。 ↩︎
- 误差归因发现,QK⊤QK^\topQK⊤ 的 int8 量化是主要误差来源:在 QK-Norm 和 RoPE 之后,key 的逐通道范围跨度约为 85×,因此中位数通道只拿到其 127 个 int8 级别中的 1.5 个。按通道重新缩放 key,并对 query 应用逆缩放,可以解决这个问题,并且在点积中恰好抵消,使 ablation 子集上的困惑度从 44.9 降到 25.2。I-LLM 的 7 位 attention 概率又带来 2 点困惑度代价;DetLLM 使用 15 位概率,精确地以两个 int8 GEMM 计算。 ↩︎
- 表格背后是一套零容忍测试套件:十次重复运行,单独一个 prompt 对比填充到 2、8、32 的批次并搭配随机 co-prompt,prefill 对比逐 token 在每个位置进行比较,CUDA 对比 CPU 参考,以及一个 4k token 上下文
- 并非对等比较。fp16 基线没有使用 compile 或 CUDA graphs,而 int8 在 prefill 阶段没有 FlashAttention 风格的 kernel,该阶段仍以约 0.13× fp16 的速度运行。↩︎