去年の正解が、今年の間違いになる
同じ計測をやり直したら、ボトルネックは動いていた
数か月後に同じ計測をやり直したら、内訳は真逆になっていた。生成は全体の1割。残る9割は、待ち行列と距離だった。
前回いちばん効いたレバーは、もう的外れだった。ボトルネックは動く——正解には賞味期限がある。
そこで往復を消そうとユーザーの近くでの推論(エッジ推論)を試すと、今度は全体が3倍遅くなった。部分最適は、全体では負ける。
前回、遅さの原因はモデルの生成——正確には、モデルが答える前に”考えて”いたことだった、と書いた。思考をオフにして(reasoning=none)、生成は一気に軽くなった。当時はそれで十分だった。
問題は、正解に賞味期限があることだ。
習慣が、昔の結論を疑わせる
一度「分解して測る」を覚えると、それが癖になる。別の作業のついでに、私はまた同じ内訳計測を回した。深い意図はなく、ただ「いまはどうなってるか」を見たかっただけだ。
出てきた数字を見て、手が止まった。
| 区間 | p50 | 割合 |
|---|---|---|
| ネットワーク往復 | 275 ms | ~55% |
| queue(待ち行列) | 167 ms | ~33% |
| 生成(generation) | 50 ms | ~10% |
| prompt(入力処理) | 7 ms | ~1% |
かつて時間を支配していた生成が、いまや10%まで縮んでいた。しかも新旧2機種(各38サンプル)でほぼ一致しているので、これは測定ノイズではなく構造だ。
理由はすぐ分かった。前回モデルの”考え”を止め、その後も出力を絞り込んで、生成そのものが軽くなっていた。生成を軽くしたぶん、相対的に「待ち」と「距離」が浮上したのだ。支配項が入れ替わっていた。
待ち行列は、平均で語ってはいけない
さっきの167msは、待ち行列の中央値にすぎない。そして待ち行列は、平均で語ってはいけないものの典型だった。それを痛感したのは、いまの現行モデルではなく、その一つ前に本番候補だった別のモデルでのことだ。
そのモデルは、共有インフラ上の巨大なモデルだった。混雑時に、同じ待ち行列を測ると——中央値からして、桁が違った。
| パーセンタイル | 待ち行列(別モデル・混雑時) |
|---|---|
| p50(中央値) | 988 ms |
| p90 | 3,438 ms |
| p99 | 4,883 ms |
このモデルは、中央値ですでに約1秒——現行モデル(167ms)の、およそ6倍だ。そして100回に1回は、約5秒待たされる。しかも厄介なのは、いつそうなるかが読めないことだ——空いている時間帯なら150ms、混雑すればこの分布。「いつも速い」でも「いつも遅い」でもなく、読めない。
平均だけ見ていたら、このテールはまったく見えない。だが、ユーザーが覚えているのは平均ではなく、その最悪の一回だ——マニフェストの3番目の原則——良い代理指標は、平均でなく最悪の一回(tail)を見る——そのものである。そしてこのテールは、外部の共有インフラを使う限り消せない——並ぶ相手を、こちらは選べないからだ。(専有すれば、このテールごと消える。それが第5回。)
ネットワークの275msをさらに分解すると、接続はコネクションプールで再利用されていて確立コストはほぼゼロ、応答も小さく転送も一瞬。つまり275msはほぼ純粋な地理的往復——端末から遠い場所(当時は米国)にある推論サーバまでの、物理的な往復時間だった。(世界各地のAWSリージョンで実測した詳細は、深掘り「地理は、レイテンシの動かせない床だ」に。)
だから、打つべきレバーも入れ替わる
前回の正解「モデルに考えさせない」は、もうこれ以上は効かない。10%しかない生成をどういじっても、体感はほとんど動かない。いま支配的なのは、
- 距離(往復275ms)→ 推論をユーザーの近くに置けば消える。この275msは、動かせない物理の”床”だ
- queue(167ms)→ 共有インフラの待ち行列。専有すれば消える
つまりレバーは「モデルを速く」でも「出力を削る」でもなく、推論サーバーを物理的に近く・専有で持つことに移っていた。
仮説を検証する(そして、また負ける)
「往復さえ消せば速くなる」なら、ユーザーの近くのエッジで推論すればいい。都合よく、あるエッジ推論サービスにまったく同じモデルが載っていた。すでにそのエッジ基盤を経由していたので、叩き先を差し替えるだけで比較できる。理屈の上では完璧だった。
結果はこうだ。
| 経路 | 翻訳 wall p50 | max | 結果 |
|---|---|---|---|
| 現行(遠いが専用の高速推論) | 502 ms | — | ✅ 字幕が出る |
| エッジ推論(近いが汎用GPU・同一モデル) | 1500 ms(制限時間に到達) | 2610 ms | ❌ 字幕が出ない |
決定的な負け。往復を250msも消したのに、全体は遅くなった。
原因は、分解表のおかげで即座に判明した。近くても、エッジの汎用GPUでは生成が間に合わなかった。暖機後の純粋な生成でも現行より遅く(測れた範囲で約1.5倍)、実運用ではコールドスタートや待ちも重なって、翻訳が終わる前に制限時間(1500ms)で打ち切られた——だから字幕が出ない。表の「1500ms」は生成の速さではなく、クライアント側の制限時間そのものだ。往復をいくら消しても、生成が締め切りを越えれば、全体では負ける。10%まで小さくなっていたはずの生成が、遅いハードに載せた瞬間に勝敗を決めたのだ。
唯一の出口は「近い×速い」の両立
ここで、前回のもうひとつの教訓が効いてくる。部分最適は罠だ。往復だけ消しても、生成が遅ければ意味がない。逆に、生成だけ速くても、遠ければ意味がない。
だから条件は「近接かつ高速推論」の両立になる。試算では、専用の高速チップをユーザーに近い地域(例:国内)に置ければ、往復は十数ミリ秒、全体は100ms前後まで落ちる見込みだった。これが、自前で推論を持つ(セルフホスト)を検討し始めた本当の起点だ。速いモデルが欲しかったのではない。近くて速い場所が欲しかった。
(では、なぜ最初から自前にしなかったのか——それは供給側の事情という別の物語で、第4回に続く。)
教訓
- ボトルネックは動く。だから測り続ける。 一度の分解で得た正解は、状況が変われば失効する。ズレに気づけたのは計測が習慣になっていたからで、前回の結論を「もう分かったこと」として棚に上げていたら、いまも効かないレバーを引いていた。
- 相対値で考える。 生成が「軽くなった」のではなく、他が相対的に「重くなった」。支配項は、絶対値でなく割合で入れ替わる。
- 部分最適は罠。 往復を消しても生成が遅ければ逆効果。全体(end-to-end)で、複数の区間を同時に満たさないと勝てない。
付録:生データ
| 項目 | 実測 | 条件・留保 |
|---|---|---|
| 翻訳 wall p50 | 502ms | Pixel 10 / 7a 各 n=38、cross-device で一致 |
| 内訳(P10 / 7a) | ネットワーク往復 275 / 268ms、queue 167 / 200ms、generation 50 / 66ms、prompt 7ms | 生成は全体の約1割 |
| ネットワーク内訳 | 接続はプール再利用で +0〜2ms、転送 +1〜3ms | 275ms は純・地理往復 |
| エッジ推論 A/B(同一モデル) | wall p50 1500ms(全サイクルが上限に到達)/ max 2610ms(現行 502ms) | 暖機後でもエッジ生成は現行の約1.5倍遅い |
| 近接×高速の試算 | 往復 ~13ms、全体 ~97ms | 体感の壁 ~177ms を初めて下回る見込み(試算) |