跳至内容
17/30第 17 章,共 30 章

温度、top-p,以及你并不拥有的确定性

温度会在 softmax 前除以 logit,这个事实足以推翻“创意旋钮”的说法。同一次 greedy 调用,两次运行,两个答案。

本页内容

下面是同一个请求发给同一个模型五次。相同权重、相同 prompt、相同机器、相同随机种子。唯一变化的是一个数字。

TEXT
prompt: "Q: What is the capital of France?\nA:"

T = 0.0   " Paris\nWhat is the question and does the answer answer it? The
           question is: What is the capital of France?..."

T = 0.7   " Paris\nWhat is the question: Which city is the capital of
           France?..."

T = 1.0   " Paris\nWhat is a good geographical qualifier for describing
           Paris concerning its location?\nA: Near the Mediterranean Sea..."

T = 1.5   " Paris\nWhat clue from premise allows we to conclude that Godwin
           was &, He chose Healing Crimson Colour No:white flour Pure..."

T = 2.0   "安全感金华.ITEMT]]];\naims assume parental.st-importe.valtermination
           Screens قطر_Zeroหมายเลข-zA ('$ספטמבר..."

没有任何东西坏掉。最后一行里的每个 token 都是从模型自己在 151,936 个词表项上的概率分布中合法抽出来的。那个变化的数字叫温度,大多数文档把它描述成创意旋钮,而这个描述是错的;本章会演示这一点,而不是只做断言。

这也是前三个承诺兑现的一章。第 4 章定义了 logit,但一直没有真正用上它。第 2 章的浮点数框以一句提醒结束——等第 17 章问为什么相同 prompt、模型和种子仍可能产生不同 token 时,请记住这一点。而第 9 章的 mixture-of-experts 框承诺会列出四个非确定性的原因。三者都在下面出现。

第 4 章把 logit 介绍为未归一化的实数分数,每个类别一个。第 8 章让语言模型为词表中的每个条目生成一个 logit。softmax 会把这个向量 z\mathbf{z} 变成概率:

pi=ezijezjp_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

温度在这里进入——这个名字借自统计物理,在那里同一个参数控制 Boltzmann 分布在低能态上集中得有多尖锐1——而且它是在取指数之前除以 logits

pi(T)=ezi/Tjezj/Tp_i(T) = \frac{e^{z_i/T}}{\sum_j e^{z_j/T}}

这个位置就是整个机制,值得用两行代数说明它为什么不可能放在别处。假设你试图把温度应用到概率上——把它们按 1/T1/T 缩放再重新归一化。你会得到

pi/Tjpj/T=pijpj=pi\frac{p_i/T}{\sum_j p_j/T} = \frac{p_i}{\sum_j p_j} = p_i

常数抵消了。缩放概率完全没有效果;分布原样回来。温度之所以有效,只是因为它作用在指数上:在取指数前除以 TT,等价于把每个概率提升到 1/T1/T 次方——这是一种非线性重塑,改变的是条目之间的比值,而不是它们共同的尺度。

从这个位置出发,两个极限不需要额外推导就能得到。当 T0T \to 0 时,最大的 logit 会把其他 logit 远远甩开,pp 坍缩到单个最高分 token 上:greedy decoding。当 TT 增大时,每个 zi/Tz_i/T 都趋向于零,每个指数都趋向于 1,分布会向整个词表上的均匀分布变平。在恰好 T=0T = 0 时,公式会除以零,所以每个实现都会把它 special-case 为算术最大值——包括下面的小组件,它会在 T0.001T \le 0.001 切换到 argmax。

有一点警告,因为重名会造成真正的混淆。机器学习里还有另一个无关的东西也叫温度:temperature scaling,一种校准方法,会在验证集上拟合一个值,让分类器的置信度匹配它的准确率。2 公式相同,但与生成无关。论文里说“temperature”时经常指的是那个;本章从不指它。

下面就是这个分布,算术过程摆在你面前。logits 是固定且合理的,所以你可以把下文里的数字与看到的内容核对:

  • ␣Paris96.9%
  • ␣the1.3%
  • ␣located0.8%
  • ␣a0.5%
  • ␣Lyon0.2%
  • ␣called0.1%
  • ␣home0.1%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

10 个 token 中有 10 个通过截断,并共同分配概率。

以表格查看数据
Tokenlogit温度调整后截断后
␣Paris⁨9.4⁩96.90%96.90%
␣the⁨5.1⁩1.31%1.31%
␣located⁨4.6⁩0.80%0.80%
␣a⁨4.1⁩0.48%0.48%
␣Lyon⁨3.2⁩0.20%0.20%
␣called⁨2.9⁩0.15%0.15%
␣home⁨2.4⁩0.09%0.09%
␣Marseille⁨1.8⁩0.05%0.05%
␣not⁨1.1⁩0.02%0.02%
␣banana⁨-2.6⁩0.00%0.00%
采样:temperature、top-p 和 top-k

The capital of France is 的十个候选续写,在温度 1 且不裁剪时。␣Paris 占据了 96.90 % 的质量;排在底部、logit 为 2.6-2.6␣banana 得到 0.00 %。把温度滑到 0,只剩一个 token,概率 100 %。滑到 2,␣Paris 降到 69.81 %,而 ␣banana 升到 0.17 %——一个被模型拒绝的 token,因为读者转动的旋钮而获得了真实概率。

␣banana 这个数字就是整个论点的缩影:提高温度不能让模型产生它原本没有的想法。 logits 已经算出来了,排名已经固定,而温度会完全保留这个排名——再多的热量也不会让低分 token 超过高分 token。它所做的只是沿着模型自己给出的排名向下重新分配质量。高温不会让模型更有创造力;它只是让模型更可能输出那些它自己评为糟糕的 token。

在真实词表上,这不再只是个有趣现象,而会成为高温输出不可用的原因。在 Qwen/Qwen2.5-0.5B-Instruct 上测量,一次 forward pass,使用上面的 prompt,统计需要多少 token 才能累积到给定比例的概率质量:

temperaturetop-1 probabilityentropytokens holding 80 %90 %95 %99 %
0.599.98 %0.00 nats1111
0.799.65 %0.03 nats1111
1.096.01 %0.30 nats11114
1.288.20 %0.88 nats1213252
1.562.83 %3.07 nats293532,67226,787
2.016.62 %8.19 nats13,51632,96655,231101,205

慢慢读最后一行。在 T=2T = 2 时,对于一个只有一个正确答案的问题,32,966 个不同 token 共享了前 90 % 的概率质量。这不是更宽阔的创意空间。这是一个模型被算术告知:在 A: 后面的词位上,韩语助词和 C++ 标识符都该被当作可选项。开头代码块里的垃圾就是直接后果,而且这不是模型或库的 bug——这是请求本身要求得到的结果。

有用范围很窄,而且取决于任务,不取决于口味。对于事实性问题,答案就是一个 token,任何高于约 1.2 的热量都只是在无谓地注入错误。对于开放式问题,确实存在不止一个好的续写,适当的热量能换来仍然流畅的多样性:

TEXT
"Write a two-sentence story about a lighthouse."

T = 0.0  "The lighthouse stood tall and proud, its beacon illuminating the
          night sky above. A lone sailor, his eyes fixed on the distant
          horizon..."

T = 0.7  "In the quiet, stormy waters of the sea, a lighthouse stood
          sentinel over the horizon, its golden dome casting a warm glow
          on the fog-shrouded streets below..."

T = 1.0  "In the quiet night, a lone lighthouse stood sentinel over the
          sea, its shining beacon a beacon of hope and solace for sailors
          and fishermen across the vast and endless ocean..."

T = 1.3  "In the gentle sunlight, now reflecting upon the opening of Jack's
          lighthouse, Jim Trahan, a small-time individual difficult to
          define in paperwork, wondered about a career where simplicity
          reigns..."

在 1.3 时,模型发明了一个专有名词和一个无法解析的句子。对这个模型和这个任务来说,从“每次都一样”到“不连贯”的区间大约是 0.6 到 1.1;诚实的建议是,你应该在你的任务上测量得到它,而不是从一篇博客里抄一个数字。

这一切下面藏着一个显而易见的问题:如果模型有一个概率分布,而某个 token 最可能,为什么不总是选它?Greedy decoding 免费、可复现,而且不需要参数。

因为结果会是这样:

TEXT
prompt: "In a shocking finding, scientists discovered a herd of unicorns
         living in a remote valley."

greedy: " The unicorns were so rare that they were not even recognized by
         the local people. The unicorns were so rare that they were not
         even recognized by the local people. The unicorns were so rare
         that they were not even recognized by the local people. ..."

         repeated 4-grams: 87.6 %

八个句子,一个句子。几乎十个四-token 窗口里有九个已经在同一段输出的前面出现过。这就是 neural text degeneration,由 Holtzman 等人在提出 top-p 的论文中命名和解释。3 模型并没有坏;最大化序列概率只是不适合作为开放式文本的目标。人类写作不是最可能的词序列——它带有惊讶,每个 token 的概率会游走、下探再恢复——而最大概率路径是一个固定点,一旦进入,就没有离开的理由。

这就是为什么 sampling 存在。但同样重要、却常被省略的一点是:这不是普遍定律第 12 章测量过,两步文字题用普通 greedy decoding 得到 24/24 全对,而温度 0.8 的 sampling 会降到 81%;self-consistency 随后花掉六倍 token 才爬回 greedy 原本已经达到的位置。两个事实可以同时为真:

开放式生成。 没有单一正确续写,所以最可能的那个是一座陷阱——它会循环,而且 87.6 % 的内容是从自身复制来的。要 sample。

只有一个正确答案的任务。 这里确实存在单一正确续写,所以抽到其他任何东西就是抽到错误。第 12 章的 100 % 变成 81 %,正是这个原因。不要 sample。

大多数生产 prompt 都是第二类,却被配置得像第一类,因为 temperature 被留在了示例代码使用的那个值。

两种裁剪方式,只有一种会自适应

链接到此部分:两种裁剪方式,只有一种会自适应

没有人真的会从完整分布中 sampling,因为尾部巨大,而且充满胡言乱语。必须裁掉一些东西。经典答案有两个,它们在一个决定一切的方面不同。

Top-k 保留固定数量的候选。按概率排序,保留前 kk 个,丢弃其余,再重新归一化。4 Top-p,也叫 nucleus sampling,保留固定数量的质量:按降序取 token,直到它们的累积概率达到 pp,然后停止。3 形式上,nucleus 是满足下面条件的最小集合 VpV_p

iVppip\sum_{i \in V_p} p_i \ge p

这个差异听起来像是表面问题,但并不是,因为你在同一分钟发出的两个 prompt 会有完全不同形状的分布。下面两者都是同一个模型在温度 1 下的结果:

Q: What is the capital of France?\nA:Once upon a time,
top-1 probability96.01 %25.39 %
tokens holding 90 % of the mass1467
top-k = 40 keeps99.61 % of the mass78.87 % of the mass
mass in ranks 2 to 403.61 %53.48 %
token at rank 40␣Av, 0.0093 %␣Dr, 0.128 %

一个固定的 kk,两个方向相反的失败。在事实性 prompt 上,k=40k = 40 放进了 39 个 token,它们合起来只值 3.6 %——它在放垃圾通过,包括一个概率只有万分之九个百分点的候选,因为规则数的是槽位而不是证据。在故事 prompt 上,同一个 k=40k = 40 丢掉了模型真正分配的 21 % 质量,因为那里的真实 nucleus 有 467 个 token 宽。

Top-p 只用一个数字就能同时完成两件事。设 p=0.9p = 0.9,它会在第一个 prompt 上保留 1 个 token,在第二个上保留 467 个,因为它问的是分布本身的问题,而不是把一个数量强加给它。直接观察这种自适应——同一个裁剪,四个温度:

  • ␣Paris91.1%
  • ␣the5.2%
  • ␣located3.7%
  • ␣a0.0%
  • ␣Lyon0.0%
  • ␣called0.0%
  • ␣home0.0%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

10 个 token 中有 3 个通过截断,并共同分配概率。

以表格查看数据
Tokenlogit温度调整后截断后
␣Paris⁨9.4⁩85.03%91.10%
␣the⁨5.1⁩4.84%5.18%
␣located⁨4.6⁩3.47%3.71%
␣a⁨4.1⁩2.48%
␣Lyon⁨3.2⁩1.36%
␣called⁨2.9⁩1.12%
␣home⁨2.4⁩0.80%
␣Marseille⁨1.8⁩0.54%
␣not⁨1.1⁩0.34%
␣banana⁨-2.6⁩0.03%
采样:temperature、top-p 和 top-k

Top-p 为 0.90、温度为 1.5 时:十个 token 中有三个存活并共享质量,␣Paris 被重新归一化到 91.10 %。现在只移动温度。在 0.7 时,同样的 0.90 只留下一个幸存者——这么窄的 nucleus,只是换了名字的 greedy decoding。在 2.0 时,它留下五个。裁剪线从未移动;下面的形状变了。

这个小组件还澄清了一个值得点名的误解,因为它会让人花冤枉钱。在一个置信分布上,top_p = 0.9 并不是“有一点多样性”。它就是 greedy。 在温度 1 时,这里的领先 token 已经占 96.90 %,超过 0.9,所以 nucleus 宽度为一个 token,其他任何东西都永远抽不到。团队把 top_p 设为 0.9,以为自己放松了某些东西,然后困惑为什么每个回复都一模一样。

改用 top-k,则相反的失败也同样明显:

  • ␣Paris97.2%
  • ␣the1.3%
  • ␣located0.8%
  • ␣a0.5%
  • ␣Lyon0.2%
  • ␣called0.0%
  • ␣home0.0%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

10 个 token 中有 5 个通过截断,并共同分配概率。

以表格查看数据
Tokenlogit温度调整后截断后
␣Paris⁨9.4⁩96.90%97.20%
␣the⁨5.1⁩1.31%1.32%
␣located⁨4.6⁩0.80%0.80%
␣a⁨4.1⁩0.48%0.49%
␣Lyon⁨3.2⁩0.20%0.20%
␣called⁨2.9⁩0.15%
␣home⁨2.4⁩0.09%
␣Marseille⁨1.8⁩0.05%
␣not⁨1.1⁩0.02%
␣banana⁨-2.6⁩0.00%
采样:temperature、top-p 和 top-k

Top-k 为 5,不使用 top-p。每个温度下都有五个 token 存活,因为请求的就是五个。在温度 1 时,如图所示,␣Paris 下面的四个候选合起来值 2.79 %。降到 0.7 时,同样四个只值 0.38 %——裁剪只是表演,模型实际上是 greedy。升到 2.0 时,它们值 22.54 %。相同设置、相同幸存者数量,三种完全不同的行为,而请求里没有任何东西告诉你自己会得到哪一种。

各种惩罚项,连同公式,因为把它们混淆是常态

链接到此部分:各种惩罚项,连同公式,因为把它们混淆是常态

三个不同机制带着相似的名字流传,它们做的事情不同,而且差异可以测量。令 cic_i 表示 token ii 已经出现的次数。

ziziα1[ci>0]z_i \leftarrow z_i - \alpha \cdot \mathbb{1}[c_i > 0]

对任何已经出现过的 token 减去一个常数。出现一次和出现四十次受到的惩罚完全相同。它是开关,不是旋钮。

ziziβciz_i \leftarrow z_i - \beta \, c_i

出现次数成比例地减去。一个用过四次的 token 受到的惩罚是只用过一次的四倍,而且随着文本增长,压力会不断叠加。

zi{zi/ρif zi>0ziρif zi0z_i \leftarrow \begin{cases} z_i / \rho & \text{if } z_i > 0 \\ z_i \cdot \rho & \text{if } z_i \le 0 \end{cases}

原始版本来自 CTRL 论文。7 它是相除而不是相减,并且需要符号分支,因为除以负 logit 会让它变大。因此它的强度取决于 logit 的大小,这意味着同一个 ρ\rho 在同一句子的不同位置打击力度不同。

前面那个退化续写,分别应用每一种惩罚项。“Steps altered”统计 120 个生成步骤中,有多少步选择了与未惩罚模型不同的 token。这里的运行是 120 步,而上面的代码块是 140 步,所以未惩罚基线显示为 85.5 % 而不是 87.6 %:

settingrepeated 4-gramssteps altered
nothing85.5 %0 / 120
presence 0.565.0 %3 / 120
presence 1.03.4 %11 / 120
frequency 0.56.0 %12 / 120
frequency 1.00.0 %20 / 120
repetition 1.2 (CTRL)0.0 %35 / 120

可以看出三件事。Presence 为 0.5 时,在 120 个决策中改变了 3 个,把重复减少了四分之一——这个循环是由少数几个 token 维系的。Frequency 为 0.5 时,改变的决策数量是前者的四倍,效果也大得多,因为计数乘数会持续增长,而 presence 常数不会。CTRL 惩罚项在被广泛复制的 1.2 数值下重写了 120 个决策中的 35 个,这不是轻推;这是另一个模型。

最后这个数字铺垫了一个没人提醒你的失败模式。

惩罚项会怎样影响本来就应该重复的文本

链接到此部分:惩罚项会怎样影响本来就应该重复的文本

代码会重复。表格会重复。列表会重复。结构化输出按定义就会重复——这就是结构。惩罚项无法区分模型卡在循环里,和模型正在正确输出表格第四行,因为两者看起来都是某个 token 再次出现。

同样三个任务,用三种方式生成:

tasknothingfrequency 0.5repetition 1.2
markdown table, 6 rows0 / 56 steps altered0 / 562 / 62
Python function0 / 930 / 9310 / 110
bulleted list, 1 to 120 / 500 / 500 / 50

Frequency penalty 为 0.5 时,对三者都无害,这是一个有用且略令人意外的结果,它说明了一件精确的事:既然没有决策改变,那么结构性 token 在自己的位置上赢出的幅度,一定大于惩罚项减去的量,即使它们已经出现了五六次。CTRL 惩罚项是相除,它确实会把这些 token 撬开,下面是它生成的内容:

TEXT
repetition 1.2, markdown table:
  | n | 2^n |
  | --- | --- |
  | 0 | 1      |
  | 1 | 2       |
  | 2 | 4       |

对齐崩掉了:每个单元格里的 padding 量逐行变化,因为 closing pipe 前的一串空格正是惩罚项要打断的那类重复。这只是外观问题,而且多花了六个 token。Python 例子就不是外观问题了:

TEXT
nothing / frequency 0.5:
      total = 0
      for i in range(1, n + 1):
          total += i ** 2
      return total

repetition 1.2:
      # Initialize total_sum with 0
      total_sum = 0
      # Loop through numbers from 1 to n, incrementing by 2 each time
      for i in range(1, n + 1,

惩罚项把模型从 total 推开——它已经在 docstring 里用过——转向 total_sum,又用虚构注释填充输出,把预算花在未使用的 token 上,最后走进了一个带 stride 的三参数 range。注释说 incrementing by 2 each time,这对于从 1 到 nn 的平方和是错的。一个 repetition penalty 让原本无需它就能正确回答的 prompt 生成了错误代码。

接下来的规则很短:惩罚项适用于开放式散文,应该在代码、结构化输出、表格数据以及任何带 schema 的内容上关闭。第 18 章讲的正是第二类。

应用顺序,以及为什么它会改变答案

链接到此部分:应用顺序,以及为什么它会改变答案

每个真实实现都会按一个具体顺序应用这些机制:

penalties → temperature → top-k → top-p → sample

这不是任意记账,交换两个阶段会产生真正不同的分布。下面两个测量都在事实性 prompt 上进行。

在温度前还是温度后裁剪。 nucleus 会在它拿到的分布上计算,而温度会彻底改变这个分布:

top-p 0.9 after temperaturetop-p 0.9 before temperature
T=1.0T = 1.01 token1 token
T=1.5T = 1.5353 tokens1 token
T=2.0T = 2.032,966 tokens1 token

T=2T = 2 时,相同的名义设置会得到 32,966 个候选或 1 个候选,纯粹取决于哪个阶段先运行。如果你曾经疑惑为什么在一家提供商上提高温度“毫无作用”,而在另一家提供商上用同样两个数字却毁掉输出,这张表就是一个合理答案。

在温度前还是温度后惩罚。 先减去惩罚项 α\alpha 再除以 TT,会得到有效惩罚 α/T\alpha/T;先相除再相减,则得到 α\alpha。对领先 token 应用 1.0 的 presence penalty:

temperaturepenalise, then tempertemper, then penalise
0.599.858 %99.948 %
1.089.839 %89.839 %
2.010.783 %6.830 %

T=1T = 1 时它们完全相同,也必须相同。在 T=2T = 2 时相差 1.58 倍。“Presence penalty 1.0”并不是一个定义良好的惩罚量,除非你还知道温度在哪里应用,而没有 API 会记录这一点。

查看详情

可选:按上述顺序展示完整 pipeline。

十六行,本章的一切都在里面。这与小组件执行的是同一套计算,只是用真实 logit 向量,而不是十个固定数字。

sample.pyPYTHON
def sample(logits, counts, presence=0.0, frequency=0.0,
           temperature=1.0, top_k=0, top_p=1.0, generator=None):
    z = logits.clone()

    idx = torch.tensor(list(counts))                       # 1. penalties
    if len(idx):
        z[idx] -= presence
        z[idx] -= frequency * torch.tensor([float(c) for c in counts.values()])

    if temperature <= 0:                                   # 2. temperature
        return int(z.argmax())                             #    T=0 is argmax
    p = torch.softmax(z / temperature, -1)

    p, order = p.sort(descending=True)
    if top_k:                                              # 3. top-k
        p[top_k:] = 0
    p = p * ((p.cumsum(0) - p) < top_p)                     # 4. top-p

    p = p / p.sum()                                        # 5. renormalise
    return int(order[torch.multinomial(p, 1, generator=generator)])

Top-p 行里的 cumsum(0) - p不包含当前 token 的累积质量,这使得 nucleus 会包含那个跨过阈值的 token,而不是在它前面就停止。这里差一位,top_p = 0.9 就会悄悄变成比其他实现略紧的裁剪。

这是课程后半部分里少数 Python 是正确语言的地方之一,原因是结构性的,而不是风格性的:上面的每一行都需要你手里有完整的 logits 向量,而通过 HTTP API,这个向量并不存在。你可以把 temperaturetop_p 发给提供商;你无法实现它们,也看不到它们做了什么。

每家提供商接受这些控制项的不同子集,范围不同,并且会静默忽略其余部分。这不是抽象抱怨。任何提供模型选择的应用,都必须在某处写下这些差异,而写下它的那个文件就是一张不兼容地图。下面是一个这样的目录对其支持的九个文本来源,在单个参数上声明的内容:

declared temperature rangesources
0 to 1Anthropic, Google, Meta, Cerebras, PaLM
0 to 1.5Mistral
0 to 2OpenAI, DeepSeek, xAI

词是同一个,尺度不是。某处的“temperature of 1”是未修改分布,另一处则是允许的最高热量,而目录中一半来源无法表达另一半视为“中性再加一点”的值。其他旋钮也一样不均衡:OpenAI、DeepSeek 和 xAI 条目接受 presence 与 frequency penalties,但没有 topK;Google、Meta、Cerebras 和 PaLM 条目接受 topK,但没有 penalties;Anthropic 接受 topKtopP 和 stop sequences,但没有 penalties;而九个里面只有一个——Mistral——接受种子。发送提供商未实现的参数通常完全不会报错:请求成功,旋钮没有作用,然后你得出“这个设置没效果”的结论。

还要注意,这样一个文件是什么:它是在某一天写下的、关于别人 API 的一个声明,之后没有东西验证它。一个目录如果对某家现在已经接受 0 到 2 的提供商仍写着 0 到 1,就会静默截断每个请求。

还有两个控制项属于同一类。logprobs 在可用时,会返回所选 token 的 log-probabilities,通常还会返回前几个替代项——这是你窥见本章所讨论分布的唯一窗口,也是基于闭源模型构建任何置信度启发式的基础。而 maximum tokensstop sequences 会完全不参考概率地结束生成:一个硬上限和一个字符串匹配。两者都会表现为第 14 章里的 finish_reason,其中 length 意味着你的答案是被预算在句中截断的,不是模型自己完成的。

设置种子后,sampling 会变得可复现。这部分是真的,而且很容易验证:

TEXT
seed = 1234  " Paris\nWhat is a good geographical qualifier for describing
               Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 1234  " Paris\nWhat is a good geographical qualifier for describing
               Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 7     " Paris is the capital of France. The appellation of Paris is
               \"Île de Paris\"."
seed = 7     " Paris is the capital of France. The appellation of Paris is
               \"Île de Paris\"."

同一个种子内字节完全一致,不同种子之间不同,正如宣传所说。因此种子固定的是那个 sample 函数最后一行里的随机抽样——在给定分布下选择哪个 token。

它没有固定的是分布。 麻烦就在这里,因为你的模型产生的 logits 向量不是一个数学对象;它是数十亿次浮点加法的输出,而这些加法有顺序。

第 2 章已经准备好了这个实验。同样的一百万个 float32 数字,用不同分组求和:

TEXT
sequential          998.564270020    error vs float64: 6.393e-03
pairwise (numpy)    998.570556641    error vs float64: 1.061e-04
in 4 chunks         998.570495605    error vs float64: 1.672e-04
in 8 chunks         998.570556641    error vs float64: 1.061e-04
in 16 chunks        998.570678711    error vs float64: 1.594e-05

sequential == pairwise?  False
4 chunks == 8 chunks?    False

看最后一行。chunk 数量会改变答案。 这不是 numpy 的趣闻;这就是机制,因为当推理服务器把 reduction 拆到更多或更少的并行单元上时,它做的正是这件事。而服务器会根据它正在服务多少请求来拆分。

下面是这种影响在模型本身上的表现。同一个 prompt,同一个 forward pass,唯一不同是 batch 里当时碰巧有多少其他请求:

TEXT
20 identical forward passes, batch of 1:  20 / 20 bit-for-bit identical

the same prompt inside a batch of  2:  147,321 of 151,936 logits differ
the same prompt inside a batch of  4:  146,515 of 151,936 logits differ
the same prompt inside a batch of  8:  146,515 of 151,936 logits differ
the same prompt inside a batch of 16:  147,321 of 151,936 logits differ

largest change to any logit: 2.5e-05

单独运行时,模型完全确定——二十次 pass,bit 级相同。把相同 prompt 与无关请求放进同一个 batch,97 % 的 logits 会改变。你的请求没有任何变化。别人的请求到了。

现在说诚实的部分,因为这件事通常被讲得好像故事到这里就结束了。2.5×1052.5 \times 10^{-5} 的变化只有在两个候选 token 的距离也在这个量级内时,才会改变输出。在十二个 prompt 的 717 个生成步骤中,前两个 logits 之间最小的间隔是 2.5×1032.5 \times 10^{-3}——比扰动大一百倍——而且没有一步接近到足以翻转。所以在这个模型、float32、笔记本电脑上,batching 改变了每个 logit,却没有改变任何 token。

这是对有利条件的描述,不是安慰;只要条件变一个就够了:

TEXT
same weights, same prompts, greedy decoding, no seed involved
float32 vs bfloat16:   6 of 8 answers diverge
                       first divergence at step 23, on average

  float32: "...it is scattered and dispersed into different colors,
            including blue. The blue light is scattered more than other
            colors, so it appears to come from the sky."

  bfloat16: "...it is scattered and scattered, causing the colors of the
             sun to be scattered and scattered, creating the appearance
             of a blue color."

八个答案里有六个分歧,其中一个严重退化。第 2 章的表解释了原因:bfloat16 保留 7 个尾数位,所以在 logit 大小接近 16 时,可表示值之间相隔 0.125——16.0,然后 16.125,然后 16.25——舍入可以让一个 logit 移动最多 0.0625。与此同时,上面测得的生成步骤中有 4.7 % 的 top-two gap 低于 0.1。这就是两个实验的全部差异:在 float32 中,扰动比最近的决策小一百倍;而在 bfloat16 中,它与决策处于同一量级。生产推理运行在 16-bit 上,硬件使用 fused kernels 和没人承诺保持的 reduction 顺序。“数值噪声是否可以忽略”是关于精度和硬件的问题,不是关于模型的问题。

所以,按第 9 章承诺,四个原因列举如下:

第 2 章的框。求和顺序会改变数值,因此任何 reduction 拆分方式的变化都会改变 logits。这是底层基质;另外三个原因都是改变顺序的方式。

动态 batching 会把你的请求和陌生人的请求分到一起

链接到此部分:动态 batching 会把你的请求和陌生人的请求分到一起

来自第 13 章的 continuous batching 是推理变得负担得起的原因——但它也意味着你的 token 流过的矩阵形状取决于流量。上面测得:batch size 改变导致 147,321 个 logits 移动。

第 9 章的框已经说过了。router 会对每个 token、每一层做出离散选择,并受制于基于整个 batch 计算的每个 expert 容量限制。一个单独时会去 expert 7 的 token,在有其他请求陪同时会去 expert 12。这不是舍入差异;这是不同的一组权重。

-latest 这样的版本字符串是一个指针,而指针会被重新指向。提供商也会在固定版本标识符下更新 serving stack。两者都不会以足够细的粒度公告,让你能把它与你自己的输出变化关联起来。

OpenAI 的 seed 参数以它唯一能做到的方式诚实面对这一点:它会随同一个标识后端配置的 system_fingerprint 字段一起返回,文档也说明确定性是 best-effort,且 fingerprint 改变意味着结果可能不同。请把它按原意理解——提供商告诉你,以上四个原因都由它控制,你一个都控制不了,而它唯一能提供的是在事后告诉你:有东西变了。

到这里为止,所有内容都围绕一个旋钮及其后果。退后一层,更难的问题就出现了:我们一直在调的是一个概率分布,而概率分布没有接口。

function call 有接口。数据库行有接口。一个期待带三个必填字段 JSON body 的 POST handler 有接口,而且会拒绝其他任何东西。在模型与你系统里的每个其他组件之间,都坐着一个契约,而其中一方无法做出承诺:模型会产生某些东西,它来自一个你塑形过但没有固定的分布;另一边的代码需要一个已知类型的值,否则就会抛错。

连接这两个世界的桥,不是由 parsing 和 retry 搭起来的,而是由本章的材料搭起来的。如果某个 token 会破坏所需结构,你不应该 sample 它再祈祷——你应该在 softmax 看到它之前,把它的 logit 设为 -\infty。Constrained decoding 是覆盖在同一个向量上的 mask,而我们花了一整章重塑的正是这个向量;它把“请用 JSON 回复”从请求变成保证。

第 18 章就是这个契约:tool calling、JSON Schema、structured outputs,以及要在概率系统之上安全构建确定性系统需要什么。


本章所有测量都来自 CPU 上的 Qwen/Qwen2.5-0.5B-Instruct,除非特别说明均为 float32,sampling 按可选小节中的写法实现,而不是委托给库。它们来自一个小模型,具体数值属于它;机制不属于它。Von Platen 的 How to generate text with different decoding methods(Hugging Face,2020)是本文对照测量的文章,也仍然是同一材料最好的简短入门。关于确定性部分:PyTorch 的可复现性说明描述了种子在单机上能固定什么、不能固定什么;OpenAI 关于 seedsystem_fingerprint 的文档描述了提供商能承诺什么、不能承诺什么;Thinking Machines 在 2025 年关于 batch-invariant kernels 的讨论,是目前最清楚的公开解释:为什么在推理服务器层面修复这件事是可能的,但并不免费。

  1. Ackley, D. H., Hinton, G. E. and Sejnowski, T. J. A Learning Algorithm for Boltzmann Machines. Cognitive Science 9(1), pp. 147–169 (1985),其中 softmax 里的温度来自统计物理。Hinton, G., Vinyals, O. and Dean, J., Distilling the Knowledge in a Neural Network, arXiv:1503.02531 (2015),第 2 节,是同一个参数在现代深度学习中重新出现的地方——作为暴露 teacher 完整分布的一种方式,也就是第 13 章的 soft labels,而不是本章的 sampling。

  2. Guo, C., Pleiss, G., Sun, Y. and Weinberger, K. Q. On Calibration of Modern Neural Networks. arXiv:1706.04599 (2017). 不要把它和本章的温度混淆。 Temperature scaling 会在验证集上拟合单个值,让模型置信度匹配准确率;它是应用于分类器输出的事后校准方法。Temperature sampling 是运行时控制生成器如何抽取 tokens 的机制。公式相同,目的不同,也没有共享数值。

  3. Holtzman, A., Buys, J., Du, L., Forbes, M. and Choi, Y. The Curious Case of Neural Text Degeneration. arXiv:1904.09751 (2019). 提出了 nucleus sampling,并给出了这样一项测量:基于最大化的 decoding 会产生概率画像完全不像人类文本的文本。 2

  4. Fan, A., Lewis, M. and Dauphin, Y. Hierarchical Neural Story Generation. arXiv:1805.04833 (2018). 让 top-k sampling 流行起来的论文。

  5. Nguyen, M. et al. Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs. arXiv:2407.01082 (2024).

  6. Meister, C., Pimentel, T., Wiher, G. and Cotterell, R. Locally Typical Sampling. arXiv:2202.00666 (2022).

  7. Keskar, N. S., McCann, B., Varshney, L. R., Xiong, C. and Socher, R. CTRL: A Conditional Transformer Language Model for Controllable Generation. arXiv:1909.05858 (2019). 第 4.1 节是原始的 repetition penalty——那个使用相除的版本。


作者

David Vicente Campos

NeuraLIA Labs 创始人、MyRealFood 联合创始人

我是莱昂大学毕业的计算机工程师。我共同创立了 MyRealFood,并在那里作为 CTO 打造了一款数百万人用来吃得更健康的应用;我还创立了 NeuraLIA Labs,在这里我打造 AI 产品。我在本站写下一路走来所必须理解的内容,就像我希望当初有人向我讲解的那样。

了解作者更多信息

由 NeuraLIA Labs 发布。

新文章直达你的收件箱

AI 新闻、指南和产品更新——有值得你花时间阅读的内容时,我们会发一封简短邮件。

更喜欢用消息接收?同样的内容,也在这里:WhatsApp 社群 (在新标签页打开)Telegram 频道 (在新标签页打开)

课程目录

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev10 分钟阅读

Jev AI 模型为决策而生,而非写作

TypeSafe AI 的 Jev 正受到关注,因为它把软件智能视为一个概率问题:选择正确分支,附上置信度,并避免在代码只需要决策时还花钱让 LLM 写文本。

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering12 分钟阅读

Context engineering for long-horizon AI agents

Long-running agents do not fail only because the window is small. They fail when files, tool outputs and stale history crowd out the task the agent was supposed to finish.

准备好让 LIA 替你选模型了吗?

所有 AI 模型都在一处——今天就免费开始。