概要
ローカルLLMを業務に組み込む前には、「このGPUで何人が同時に使えるか」「長い資料を渡したときに最初の文字が出るまで何秒かかるか」を見積もる必要があります。ところが、推論性能の計測は条件の違いで結果が大きく変わります。同じプロンプトを繰り返し送るだけの計測では、実運用より大幅に良い数字が出ることがあります。
本記事では、推論性能を 生成(デコード)・プリフィル・並列スループット の3つに分けて測る方法と、それぞれの数字が妥当かを確かめる手順を整理します。数値はすべて社内の検証機(RTX 5060 Ti、Qwen3.5-4B-FP8、vLLM 0.32)での実測です。
測る指標を分ける
LLMの応答時間は、性質の異なる2つの処理からなります。
| 指標 | 意味 | 支配的な要因 |
|---|---|---|
| TTFT(Time To First Token) | リクエストから最初のトークンが届くまでの時間 | 入力トークンを一括で処理するプリフィル(計算量) |
| ITL(Inter-Token Latency) | トークンとトークンの間隔 | 1トークンごとに重みを読み出すデコード(メモリ帯域) |
| 並列スループット | 同時C本のリクエストで1秒あたりに出る出力トークン数 | バッチ化によるデコードの効率 |
「1秒あたり何トークン」という1つの数字で比べると、入力が長い用途と短い用途の違いが見えなくなります。TTFTとITLは分けて記録し、並列時の数字はさらに別に測ります。
生成速度:メモリ帯域から上限を見積もる
デコードでは、1トークンを生成するたびにモデルの重みをGPUメモリから読み出します。そのため、生成速度の上限はおおよそ次の式で見積もれます。
社内の検証機では、毎ステップ読む重みが5.86 GB、RTX 5060 Tiのメモリ帯域が448 GB/sです。
実測の生成速度は 68.8 tok/s(ITL 14.5 ms) でした。理論値13.1 msに対して実測14.5 msなので、メモリ帯域の約90%で動いている計算になります。
この比較を一度しておくと、計測結果の妥当性を判断できます。理論値より速い結果が出たら計測方法を疑い、理論値の半分程度しか出ていなければ設定や量子化の経路を疑います。
プリフィル:入力長を変えて傾向をつかむ
プリフィルは入力トークンをまとめて処理するため、デコードより桁違いに速く進みます。一方で、アテンションの計算量は入力長の2乗で増えるため、入力が長くなるほど1トークンあたりの処理は遅くなります。
社内の検証機では、プリフィル速度は1kトークンで約9,000 tok/sで、入力が長くなるほど下がりました。入力長 を変えて測ったTTFTは、次の式によく当てはまりました(決定係数 )。
定数項はリクエストの受付やスケジューリングなどの固定コスト、1次の項はトークン数に比例する処理、2次の項はアテンションの計算に対応すると読めます。このような式を一度求めておくと、「社内規程のPDFを丸ごと渡すとTTFTは何秒か」といった見積もりが、計測し直さなくてもできます。
並列スループット:ステップ時間の伸びを見る
vLLMは複数のリクエストを1つのバッチにまとめてデコードします。重みの読み出しはバッチ内で共有されるため、同時実行数 を増やしても1ステップの時間はわずかしか伸びません。
社内の検証機(入力256トークン、出力128トークン)では、1ステップの時間は次のように近似できました。
| 同時実行数 | 実測 |
|---|---|
| C=1 | 68.8 tok/s(1本の生成速度) |
| C=16 | 731 tok/s(出力スループット) |
| C=32 | 1,059 tok/s(出力スループット) |
1本あたりの速度は落ちますが、全体のスループットは1並列の10倍以上に伸びます。なお、実測値は「全リクエストの出力トークン数の合計 ÷ 壁時計の経過時間」で求めています。経過時間にはプリフィルや最後のリクエストの完了待ちも含まれるため、ステップ時間の式だけから求めた値より小さくなります。
落とし穴1:prefix cacheで結果が良く見える
vLLMには、過去のリクエストと先頭が同じ入力について、計算済みのKVキャッシュを再利用するprefix cachingがあります。実運用では有用な機能ですが、同じプロンプトを繰り返し送るベンチマークでは、プリフィルがほぼ省略される ため、TTFTと並列スループットが実際より良く出ます。
社内の検証機では、入力512トークンでは差が見えませんでした(このモデルは32層のうち24層が線形アテンションです)。しかし入力2048トークン以上では、TTFTと並列スループットが大きくずれました。短い入力で問題がなかったからといって、長い入力でも同じとは限りません。
対策は、リクエストごとに入力の先頭を変える ことです。vLLMの vllm bench serve がランダムなデータセットで共有prefixを持たせないのと同じ考え方です。
import uuid FILLER = "The quick brown fox jumps over the lazy dog and then runs away. " def build_messages(reps: int) -> list[dict]: return [{"role": "user", "content": f"{uuid.uuid4().hex} " + FILLER * reps}]
入力長はトークナイザで事前に数えるより、最初のウォームアップの応答に含まれる usage.prompt_tokens を見て繰り返し回数を補正する方が簡単で正確です。
落とし穴2:thinkingの有無とトークンの数え方
Qwen3.5は既定で思考(thinking)を行います。thinkingの有無でトークンの生成速度自体は変わりませんでしたが、出力トークン数が大きく変わるため、応答全体の時間は比較できなくなります。計測では、チャットテンプレートの引数で明示的にオフにします。
{ "model": "<model>", "messages": [{"role": "user", "content": "..."}], "max_tokens": 128, "stream": true, "ignore_eos": true, "chat_template_kwargs": {"enable_thinking": false} }
あわせて、次の点を揃えます。
- 出力長を固定する:vLLMでは
ignore_eosを指定し、モデルが途中で生成を終えても指定トークン数まで出させる - トークン数は推定しない:応答の
usage.completion_tokensを使う - reasoningもトークンとして数える:thinkingを切れないモデルでは、
reasoning_contentの到着もトークンの到着として扱う - デコード速度は最初と最後のトークンの間で測る:(出力トークン数 − 1) ÷ ITLの合計。最後の
usageや[DONE]の受信処理を含めない - ウォームアップを捨てる:最初の数回は初期化の影響を受けるため集計しない
落とし穴3:クライアント側の待ちがTTFTに混ざる
計測用のクライアント側の処理も、TTFTに混ざることがあります。
- HTTPクライアントの同時接続数の上限:たとえばPythonのhttpxは既定で同時接続数に上限があります。並列数がそれを超えると、接続待ちの時間がTTFTに加算されます。接続数の上限は同時実行数に合わせて設定します。
- 送信前の前処理:コンテキスト長に合わせて
max_tokensを調整するために、送信前に/tokenizeを呼ぶような処理があると、その往復がTTFTに入ります。計測時は入力長を呼び出し側で管理し、この処理を外します。
DigitalBaseに組み込んでいる性能計測も、こうした点を反映しています。リクエストごとに先頭を変えた入力を作り、ウォームアップ2回を捨てたうえで、1並列のレイテンシとC並列のスループットを分けて記録します。
サーバー側の数字と突き合わせる
クライアントで測った数字は、最後にvLLMの /metrics と突き合わせます。vLLMはPrometheus形式でTTFTのヒストグラムやprefix cacheのヒット数を出しています。
curl -s http://localhost:8000/metrics | grep -E "time_to_first_token_seconds_(sum|count)|prefix_cache_(queries|hits)"
計測の前後で値を取り、差分を見ます。
- TTFTの合計 ÷ 件数 がクライアント側のTTFTと大きく離れていれば、クライアント側の待ちが混ざっています
- prefix_cache_hits が計測中に増えていれば、入力の先頭が重なっており、prefix cacheの影響を受けています
まとめ
ローカルLLMの推論性能は、次の手順で測ると実運用に近い数字が得られます。
- 指標を分ける:TTFT(プリフィル)、ITL(デコード)、並列スループットを別々に記録する
- 理論値と比べる:重みのサイズ ÷ メモリ帯域で、生成速度の上限を見積もる
- 入力長を変えて式にする:TTFTを入力長の関数として当てはめ、見積もりに使う
- prefix cacheを避ける:リクエストごとに入力の先頭を変える。短い入力で差がなくても、長い入力で確認する
- 条件を揃える:thinkingを明示的にオフにし、出力長を固定し、トークン数は
usageから取る - サーバー側と突き合わせる:
/metricsのTTFTとprefix cacheのヒット数で、計測が正しいかを確かめる
社内の検証機では、生成はメモリ帯域の約90%で動き、32並列で1,059 tok/sまで伸びました。GPUの選定や同時利用人数の見積もりには、このように前提を揃えて測った数字を使うことが大切です。