Dev Study
← AIを部品として組み込む仕組み
レッスン 1 / 12·約 9 分·第20期 AIを部品として組み込む仕組み

呼ぶたびに引き直している — temperature 0 でも同じ答えは約束されない

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

ここからは、確率で答える部品をアプリに組み込む側に立ちます。最初に、同じ要求を送り直すと何が起きるかを見ます。次回は「スキーマが縛るのは形まで」です。

まず結論

LLM を呼ぶコードは、呼ぶたびに分布から1つを引いてくる部品を抱えています。temperature を0にすると取り出す工程の乱数は消えますが、2026-09 時点の Anthropic の API 文書は、それでも結果は完全には決定的にならないと書いています。

Thinking Machines の記事は、自前の推論サーバでの揺れを、同時に処理される要求の数で計算の順序が変わることから説明しています。ベンダーは原因を書いていません。同じ要求に同じ答えが返ることを前提にせず、確かめたいことは部品の外側のコードに書きます。

? 読む前に予測

公開重みの大きなモデルを自前の推論サーバで動かし、temperature 0 で同じ依頼文から1000トークンずつ、1000回生成しました。何通りの文が出たでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

取り出す工程と、分布を作る計算

LLM を呼ぶコードは、関数を呼ぶコードと同じ見た目をしています。しかし返ってくるのは、分布から引いた1つです。第4期「AIはなぜ次の語を選べるのか」で見たとおり、モデルは候補全体の確率分布を作り、1語を取り出す工程は別にあります。

temperature が0でなければ、取り出す工程に乱数が入ります。では0なら。Thinking Machines の記事は temperature 0 を、最も確率の高い候補を常に選ぶ貪欲選択だと説明し、thus making the sampling theoretically deterministic と書きます。取り出す工程から乱数は消えます。

残るのは分布を作る計算です。候補ごとの点数(ロジット)は膨大な掛け算と足し算の結果で、浮動小数点の足し算は、足す順序を変えると結果が変わりえます。記事はこれを The original sin: floating-point non-associativity という節で扱います。上位2つの候補が僅差なら、下位の数ビットの違いで最大が入れ替わります。

同じ要求でも、揺れの入口は2つある
同じ依頼文呼ぶ側が書いた入力
候補ごとの点数を計算
分布を作る計算足す順序が変わると下位の数ビットが変わる
1つ取り出す
temperature が0でない乱数で引く
temperature が0最大を選ぶ。乱数は無い
  • ・temperature 0 で消えるのは取り出す工程の乱数だけ
  • ・分布を作る計算の揺れは、僅差の候補がいれば選択を入れ替える
具体実物

足す順序だけを変えた模型

標準ライブラリだけの模型です。4096個の float32 を、16個ずつと256個ずつの塊に分けて足します。もう1つの候補の点数は、2つの合計の中間に置きました。

import random, struct
f32=lambda x: struct.unpack('f',struct.pack('f',x))[0]
def sum32(v):
    s=0.0
    for x in v: s=f32(s+x)
    return s
def chunked32(v,c):
    return sum32([sum32(v[i:i+c]) for i in range(0,len(v),c)])
random.seed(3)
v=[f32(random.gauss(0,1)) for _ in range(4096)]
for c in (1,16,256,4096): print(f"chunk {c:5d}: {chunked32(v,c)!r}")
# two candidate tokens: logit_A computed by reduction, logit_B a fixed value between the variants
a1=chunked32(v,16); a2=chunked32(v,256)
b=(a1+a2)/2
print("logit_B =",b)
for c,a in ((16,a1),(256,a2)): print(f"chunk {c}: greedy picks", "A" if a>b else "B")
$ python3 l1-d1b.py(macOS 26.5.2・Python 3.14.6。2回実行し2回とも同一)コマンド出力
chunk 1: 19.455772399902344
chunk 16: 19.45569610595703
chunk 256: 19.455686569213867
chunk 4096: 19.455772399902344
logit_B = 19.45569133758545
chunk 16: greedy picks A
chunk 256: greedy picks B

同じ4096個なのに、塊16と塊256で合計が約0.0000095違います。その差だけで、貪欲選択は A から B に変わりました。塊1と塊4096が一致するのは、どちらも先頭から順に足すからです。種を0〜9に変えても、10通りすべてで3つの塊の大きさが3つの違う値を出しました。

ただし相手の点数はわざと中間に置いています。示せるのは「順序が変わると値が変わり、僅差なら選択が入れ替わりうる」という機構までで、実モデルで僅差がどれだけ起きるかの証拠ではありません。

✓ ここまでの確認

本文の模型では、同じ4096個の float32 を塊16と塊256で足すと合計の下位の数ビットが変わり、間にわざと置いた候補との大小が入れ替わりました。この実演から言えることはどれですか。

抽象原理

順序を変えているのは、他人の要求

では、実際の推論で足す順序を変えているのは何か。よく語られるのは「GPU の並行処理と浮動小数点のせい」という説明です。記事はこれを While this hypothesis is not entirely wrong, it doesn’t reveal the full picture. と扱います。同じ行列積を GPU で繰り返すとビット単位で同じ結果になり、forward も “run-to-run deterministic.” だと書きます。

揺れを生むのはバッチです。推論サーバは同時に来た要求をまとめて計算し、記事によれば forward は batch invariance を欠くため、自分の要求の出力がバッチの大きさに依存します。そして each individual request has no way of knowing what the parallel requests will be 。呼ぶ側から見ると、同時に来た他人の要求が、書いてもいないのに結果を決める入力になります。

ベンダーの文書も約束はしていません。2026-09 時点で、Anthropic は temperature が 0.0 でも the results will not be fully deterministic と書き、OpenAI の Chat Completions の seed は Determinism is not guaranteed です。記事自身は the primary reason nearly all LLM inference endpoints are nondeterministic is that the load (and thus batch-size) nondeterministically varies! と、推論エンドポイント全般へ一般化しています。ただし実験は自前の推論環境でのもので、商用 API の内部を確かめたものではなく、ベンダーも原因は書いていません。「商用 API が揺れるのはバッチのせい」と断定はできません。

さらに同じ Anthropic の文書では、Claude Opus 4.6 より後に出たモデルは temperature を 1.0 しか受け付けません。0にする手段がそもそも無いモデルもあるのです。

具体実物

1000回のうち、分かれたのは103トークン目

記事の実験は、Qwen3-235B-A22B-Instruct-2507 に “Tell me about Richard Feynman” を temperature 0 で1000回、各1000トークン生成させたものです。

Thinking Machines の記事(2025-09-10)の実測表
条件 結果
通常のカーネル 80通り。最も多い文は78回
最初の102トークン 1000件すべて同一
103トークン目 992件が Queens, New York / 8件が New York City
バッチ不変のカーネル 1000件すべて同一

全件が Feynman was born on May 11, 1918, in まで同じで、その先の地名で割れました。そしてバッチ不変のカーネルに替えると1000件すべてが同じになりました。

揺れは消せたのです。ただし消したのは推論サーバを動かす側で、カーネルを差し替えています。API を呼ぶだけの側には、同じバッチに誰が乗るかを選ぶ手段がありません。

✓ ここまでの確認

Thinking Machines の記事は、推論サーバの forward は同じバッチなら毎回同じ結果になると分析しています。それでも要求を送る側から見て出力が揺れるのはなぜですか。

⇄ 2つを見比べる
具体例 A

temperature 0 で1000回の推論

同じ依頼文から80通りの文が出ました。記事の分析では、結果を決めていたのは同時に処理された要求の数です。バッチ不変のカーネルで揺れは消えましたが、それをしたのはサーバを動かす側で、要求を送る側ではありません。

具体例 B

時々落ちるテスト(第16期)

同じテストが TZ=Asia/Tokyo で緑、TZ=Europe/London で赤になりました。結果を決めていたのは、テストに書かれていないタイムゾーンでした。テストの中で時計と TZ を固定すると、両方で緑になりました。

2つとも「同じなのに違う」結果です。隠れた入力の見つけ方と、それを止められる人は同じでしょうか。

隠れた入力を固定する手段が、結果を受け取る側の手の中にあるかどうかを比べてください。

この章のまとめ

  • LLM は呼ぶたびに分布から1つを引く。temperature 0 は取り出す工程の乱数を消すだけ。
  • 分布を作る計算は足す順序で下位の数ビットが変わり、僅差の候補がいれば選択が入れ替わる(模型で確認。実モデルの頻度の証拠ではない)。
  • Thinking Machines の分析では、推論サーバの揺れの主因はバッチ不変性の欠如。「並行処理と浮動小数点」は not entirely wrong だが全体像ではない。
  • 自前の推論環境で temperature 0・1000回が80通り。バッチ不変のカーネルで1000件同一。止めたのはサーバ側。
  • 2026-09 時点で、Anthropic も OpenAI も決定性を約束していない。原因は書いていない。
  • 同じ答えを前提にせず、確かめたいことは部品の外側のコードに書く。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

OpenAI の OpenAPI 定義(2026-09 取得)は、Chat Completions の `seed` をどう説明していますか。

用語集(3語)
temperature
分布の鋭さを変える API の引数。0に近いほど確率の高い候補に偏る。Anthropic の文書は 0.0 でも完全には決定的でないと書く。2026-09 時点では 1.0 しか受け付けないモデルもある。
貪欲選択 (greedy sampling)
毎回、最も確率の高い候補を選ぶ取り出し方。Thinking Machines の記事は temperature 0 をこれと説明する。取り出す工程には乱数が無い。
バッチ不変性 (batch invariance)
同時に処理する要求の数が変わっても、各要求の計算結果が変わらない性質。Thinking Machines の記事は、これを欠くことを推論サーバの非決定性の主因とする。