推論コスト削減の実務 — 何から手を付けるか
量子化も蒸留も効きますが、たいていの現場ではその前に「品質を1ミリも賭けずに済む手」が残っています。請求書を4つの数に分解し、キャッシュ→バッチング→短縮→圧縮の順に並べ替える。各手の効き方と、順序を間違えたときに何が壊れるかまで。
要点
- 推論コストは「単価×(件数×1件の仕事量)÷同時に処理できる件数」に分解でき、打てる手は4種類しかない。
- 優先順位は効果の大きさではなく、品質を賭けるかどうかで決める。
- 出力を変えずに効くキャッシュとバッチングが先で、量子化・蒸留は品質を賭けるため後に回す。
- 最大コンテキスト長を「念のため最大値」で設定すると、同時実行数が落ちてBが下がる。
- 投機デコードはバッチで演算器が埋まっていると効かず、順位は環境で入れ替わる。
電球を替える前に、消し忘れた部屋を消す
電気代が想定の3倍になったとき、最初にやることは家じゅうの電球をLEDに替えることではありません。まずどの部屋が食っているかを見て、次につけっぱなしを消す。器具を買い替えるのはその後です。順番を逆にすると、金と手間をかけたのに請求書がほとんど動かない、ということが起きます。
推論コストの削減も同じ形をしています。「量子化しよう」「小さいモデルに蒸留しよう」は目を引く手ですが、多くの現場ではそれより先に効いて、しかも品質を1ミリも賭けなくていい手が2つも3つも残っています。この記事は個々の技術の解説ではなく、どれから手を付けるかと、それぞれの副作用の話です。
請求書は4つの数でできている
自社でGPUを持っていても、APIを叩いていても、支払いの構造は変わりません。借りている計算資源の単価に、それを占有した時間を掛けたものです。1か月ぶんを式にすると、こうなります。
が月額のコスト、 はGPU1時間あたりの値段、 は月のリクエスト件数、 は1件のプロンプトを読み込む(プレフィルの)時間、 は平均の出力トークン数、 は1トークン吐くのにかかる時間、 は同時に何件をまとめて処理できているか(実効バッチサイズ)です。
言い換えると、「単価 × (何件来るか × 1件ぶんの仕事量) ÷ 何件を同時に片づけられるか」。それだけです。
この式が便利なのは、打てる手が4種類しかないとすぐ分かることです。 を減らす(呼ばない)、 を減らす(短くする)、 を減らす(1トークンを軽くする)、 を増やす(まとめる)。世に出ている削減テクニックは、必ずこのどれかに落ちます。逆に言えば、施策を持ってこられたときは「これはどの文字を動かすのか」と聞けば筋の良し悪しが判定できます。
そして優先順位を決める軸は、効果の大きさではありません。品質を賭けるかどうかです。 を増やす手は出力を1文字も変えませんが、 を減らす手の多くは出力そのものを変えます。効果が同じなら、賭けない方から順にやる。これが全体の骨格です。
手0: まず、どこが食っているかを見る
削減に入る前の測定は省略できません。見るのは3つです。
リクエストの内訳。 エンドポイント別・機能別に、件数ではなくGPU秒で並べます。件数で見ると小さく見える機能が、出力が長いせいで請求の半分を占めていることがよくあります。
入出力トークン長の分布。 平均ではなく中央値とp95を出します。平均だけ見ていると、ごく一部の極端に長いリクエストがGPUを長時間占有している構図が見えません。式の は平均ですが、現場で効くのはたいてい裾の方です。
呼び出しの重複。 同じ入力が何度来ているか、リトライやエージェントのループで二重三重に呼んでいないか。ここが数十%あることは珍しくありません。
手1: そもそも呼ばない
いちばん安い推論は、実行しない推論です。しかも品質は定義上まったく落ちません。三段階あります。
完全一致キャッシュ。 入力を正規化して、同じなら保存済みの出力を返す。実装が数十行で済むわりに、FAQ的な使われ方をする機能では効きます。条件が1つあって、temperature を0にして決定的にしておくこと。乱数で毎回違う出力を返す設定のままキャッシュを挟むと、「同じ質問なのに人によって答えが違う」という現象が固定化されます。
プレフィックスキャッシュ。 同じシステムプロンプトで始まるリクエストが100件あるなら、その部分の計算結果は使い回せます。過去のトークンのKとVを取っておく仕組み自体はKVキャッシュを1から理解するで扱ったものですが、それをリクエストをまたいで共有するのがこれです。vLLMなら --enable-prefix-caching の1行。出力は1ビットも変わらないのに、長いシステムプロンプトを使っている構成ではプレフィル時間がまるごと消えます。最初にやるべき設定変更はこれです。
セマンティックキャッシュ。 入力を埋め込みに変え、十分近い過去の質問があればその回答を返す。ヒット率は上がりますが、ここから先は品質を賭けています。
事故の形は決まっています。「東京の明日の天気」と「大阪の明日の天気」は埋め込み空間では非常に近い。「この契約は自動更新されますか」と「この契約は自動更新されませんか」も近い。固有名詞・数値・否定が入れ替わっただけの質問は、意味が正反対でも距離は近いのです。実務的な妥協点は、しきい値をきつめに取ったうえで、固有名詞や数値を含むリクエストはキャッシュ対象から外すこと。ヒット率は落ちますが、間違った回答を自信満々に返すコストの方がずっと高くつきます。
手2: まとめる — 品質に触らずに効く最大の手
キャッシュの次は です。ここが多くの現場でいちばん大きく空いています。
理由は推論の性質にあります。1トークン吐くたびに、モデルの重みを全部メモリから読み出さなければなりません。数十GBを読んで、掛け算はごくわずか。つまりGPUの演算器は待ち時間で埋まっています。ここで別のリクエストの計算を相乗りさせても、読むバイト数は増えません。1件ぶんとほぼ同じ時間で、32件が終わる。「サービングを入れたら何倍になった」の正体はほぼこれです。
だから、まだ静的バッチング(一定数揃うまで待って一斉に処理)で回しているなら、連続バッチングを持つサービング基盤に載せ替えるだけで倍以上動くことがあります。モデルの重みも出力も変えないので、評価をやり直す必要すらありません。
ただし副作用が2つあります。1つはレイテンシとのトレードオフ。バッチを大きくすると1ステップが伸び、待ち行列も伸びます。全体の処理量が増えても、1人あたりの体感は遅くなる。もう1つはばらつき。メモリが逼迫すると走行中のリクエストが一時的に押し出され、特定の人だけ途中で長く固まります。平均レイテンシは良くなっているのに苦情が来る、という形で表面化します。必ずp95/p99で見てください。
触るノブは3つに集約されます。KVキャッシュに回すメモリ比率(--gpu-memory-utilization)、1件あたりの予約長を決める最大コンテキスト長(--max-model-len)、同時実行数の上限(--max-num-seqs)。とくに2つ目、「念のため最大値」で設定するのが最も多い設定ミスです。実需が2,000トークンなのに128,000で確保すると、1件あたりの予約が膨らんで同時実行数が落ち、 が下がります。
手3: 短くする
はコストに素直に効きます。出力が半分なら、デコード時間も半分です。
出力側。 max_tokens に現実的な上限を置く。「簡潔に答えて」を指示に入れる。JSONなどの構造化出力を強制して、前置きと言い訳を消す。副作用は明確で、上限で切られた出力はJSONとして壊れます。切り詰めるなら、パースエラー時のリトライ(=コスト増)とセットで考える必要があります。
入力側。 RAGで10件詰めているものを3件にする、few-shot例を減らす、会話履歴を要約に置き換える。入力短縮はプレフィル時間 に効きますが、効き方が線形ではないのが面白いところです。プロンプト長に対して、モデル本体の計算は比例で増える一方、attentionの部分は長さの2乗で増えます。長いプロンプトを少し削るだけで、最初の1文字が出るまでの時間が目に見えて縮むことがあるのはこのためです。
副作用は気づきにくい形で出ます。few-shot例を削ると、全体の正答率はほとんど動かないのに、その例が担当していた特定パターン(珍しい書式、特定の言語)だけが落ちる。総平均の指標では見えません。削るときは、削った要素が守っていたケースを名指しで含む評価セットを先に作っておくことです。
手4: 小さくする
ここからが の領域で、初めて品質を賭けます。
量子化が第一候補です。重みをfp16からINT4に落とせば、読み出すバイト数が4分の1になる。デコードは帯域待ちで埋まっているので、読むバイト数の削減がそのまま速度になります。手順と落とし穴はLLM量子化を1から解説にありますが、優先順位の観点で重要なのは1点だけです。perplexityだけ見て通してはいけない。 perplexityは全トークンの平均なので、まれにしか起きない失敗の増加は薄まって見えません。実際に先に崩れるのは、長い文脈での指示追従、コード生成、ツール呼び出しの引数の整合といった「一発でも外すと台無しになる」種類の仕事です。
KVキャッシュの量子化は別物として扱ってください。重みではなく文脈そのものを粗くするので、長い会話で効く一方、劣化はattentionの参照先の取り違えという形で出ます。重みの量子化より慎重に。
蒸留と小型モデルへの置き換えは、単位あたりの効きは最大です。同時に、必要な工数と不可逆性も最大です。教師の出力を集め、訓練を回し、評価を作り、本番の入力分布が変わるたびに作り直す。だから最後に置きます。先にルーティングを試す価値があります。難しいリクエストだけ大きいモデルに送り、残りは小さいモデルで済ませる。判定器自体のコストと、判定を外したときのコストを見積もってから入れてください。
手5: 賢く回す — ただし空いているときだけ
投機デコードは珍しい性質を持っています。小さいモデルに数トークン先読みさせ、大きいモデルがまとめて検証する。標準的な検証手続きを使う限り、出力の分布は元のモデルと変わりません。品質を賭けずに速くなる、数少ない手です。詳しくは投機的デコーディングにあります。
ただし、これを優先順位の上に置いてはいけません。投機デコードは「遊んでいる演算器に検証をやらせる」手法だからです。すでにバッチで演算器が埋まっているなら、埋める余地がありません。社内利用や低トラフィックの環境( が小さく、1件あたりのレイテンシが問題になる状況)では大きく効き、高負荷の共有サーバでは得が消える。順位が状況で入れ替わる代表例です。
順序を間違えると何が起きるか
同じ施策でも、順序が違うと結果が変わります。
量子化を先にやると。 稼働率が低いままなので、体感ほどには請求書が動きません。GPUが遊んでいる時間は、モデルを軽くしても遊んでいるだけです。しかも品質は賭け済みで、後からバッチングを入れて が変わると、量子化の効果測定をやり直すことになります。
キャッシュを最後にやると。 消せたはずのリクエストまで含めてチューニングしていたことになり、キャッシュを入れた瞬間に前提が崩れます。
全部同時にやると。 品質が落ちたときに原因が特定できません。評価セットを固定し、1つずつ入れて、そのたびに測る。 遠回りに見えて、これがいちばん速い道です。
現場ではこう使う
誰がいつ触るか。 推論基盤・MLOpsのエンジニアと、AI機能を持つプロダクトのバックエンド担当が、GPU予算を超過したとき、ユーザーが増えて単価を下げる必要が出たとき、あるいは「同じ品質でいくら下げられるか」を経営から問われたときに触ります。
実際に触るもの。 サーバ側は --enable-prefix-caching / --gpu-memory-utilization / --max-model-len / --max-num-seqs / --enable-chunked-prefill。アプリ側は temperature=0 とキャッシュキーの正規化、max_tokens、RAGの取得件数。計測は TTFT(最初の1トークンまで)・TPOT(以降の1トークンあたり)・システム全体のトークン毎秒、そしてSLOを満たしたリクエストだけ数えた実効スループット(goodput)。評価は本番ログから作った固定セットで、施策を入れるたびに同じものを流します。
知らないと事故になる落とし穴。
- キャッシュヒット率を削減率として報告する。 キャッシュに乗る質問は短くて安いものに偏りがちで、ヒット率30%がコスト30%減にはなりません。必ずGPU秒で数えること。
- 平均レイテンシだけ見る。 連続バッチングは平均を良くしつつ、ばらつきを広げます。苦情はp99から来ます。
- 「トークン毎秒」の主語を確認しない。 1リクエストあたりか、サーバ全体の合計か。同じ構成でも10倍以上違います。
- リトライとエージェントのループを数えていない。 失敗時の自動リトライや多段のツール呼び出しが、請求の相当部分を占めていることがあります。 の削減はここから始まります。
- 最大コンテキスト長を「念のため最大」にする。 同時実行数を静かに削り、 を下げます。
問われたときの答え方。 「推論コストを半分にしろ」と言われたら、いきなり手法名を挙げないこと。まず に分解し、測定でどの文字がいちばん膨らんでいるかを示し、品質を賭けない順に並べて提示する。これが筋の通った答えです。
まとめ
- コストは「単価 × 件数 × 1件の仕事量 ÷ まとめ具合」に分解でき、打てる手は4種類しかない
- 優先順位は効果の大きさではなく品質を賭けるかどうかで決める。キャッシュとバッチングは出力を変えずに効く
- 量子化・蒸留は効くが不可逆で、評価をやり直す義務が付いてくる。だから後
- 投機デコードのように、バッチが空いているときだけ効く手もある。順位は環境で入れ替わる
- 1つずつ入れて、固定した評価セットで毎回測る。同時に入れると原因が分からなくなる
コメント
コメントにはログインが必要です