ステータス: ドラフト(2026-10-27)、実測値。 8GB級ノートPC用GPU(Ada世代)1基、vLLM 0.31.0、Qwen3-Reranker-0.6B で2026-10-27に計測しました。同じ3機能の原理と1.7Bモデルの初回の計測は「LLM KV Cache Anatomy」の第2部と「Toss ML/AI Infra Preparation」の第14部にあるため、この記事ではそれらの記事で行っていないこと(繰り返し測定、機能をオフにしたベースライン、チャンクの無効化、cache_salt)のみを扱います。 2026-10-27 追加。 同日に vLLM 0.32.0 で同じテストを再実行した結果を6-1節に追加しました。0.31.0の数値はそのまま残してあります。 作成ルール: 実測データを基本とし、測定できなかったものは「未計測」、確認できなかったものは「未確認」と表記します。
目次
- 1. なぜ再計測するのか
- 2. 環境と方法
- 3. 連続バッチ処理: オフにするとスループットが1/10になる
- 4. chunked prefill: デコードが停止する時間はチャンクサイズに比例する
- 5. プレフィックスキャッシュ: 繰り返しても18.9ms
- 6. cache_salt: 同じプレフィックスでもテナント間で共有しない
- 6-1. 2026-10-27 再計測: 0.32.0でも同じか
- 7. 運用における示唆
- 8. 確認できなかったこと
- 参考
1. なぜ再計測するのか
PagedAttention、連続バッチ処理、プレフィックスキャッシュの原理はサービングエンジン内のKVキャッシュで、1.7Bモデルで最初に計測した値は小さなモデルでのvLLM直接計測で取り上げました。その計測には3つの隙がありました。
- 条件ごとに1回ずつしか計測せず、ばらつきが分かりませんでした。
- 連続バッチ処理はオフにできなかったため、「なければどうなるか」を示せませんでした。
- チャンクを完全にオフにした場合と、複数のユーザーがキャッシュを共有する際の分離(cache_salt)を確認していませんでした。
今回はすべての条件を3〜4回繰り返し、最初のバッチはウォームアップとして除外し、中央値から差し引きました。機能をオフにしたサーバーは同じ日の同じポッドで続けて立ち上げました。
2. 環境と方法
- GPU: 8GB級ノートPC用GPU(Ada世代)1基、ドライバー 595.99。コンテナのメモリ上限は4GiB。
- vLLM 0.31.0公式コンテナ(PyTorch 2.13.0、CUDA 13.0)、アテンションバックエンドはログ上 FLASH_ATTN。
- モデル: Qwen3-Reranker-0.6B、bf16、重み 1.12GiB。28層、KVヘッド8個、ヘッド次元128であるため、1トークンのKVは 2 × 28 × 8 × 128 × 2バイト = 114,688バイトです。ログの「KV 4.3GiB に 40,208トークン」と一致します。
- 共通サーバーオプション:
--max-model-len 4096 --gpu-memory-utilization 0.80。その他はデフォルト値です。このバージョンはGPUメモリが70GB未満の場合、サーバーのmax_num_batched_tokensのデフォルトを2,048、max_num_seqsを256に設定します(ソースコードvllm/engine/arg_utils.pyで確認)。 - クライアント: 同じポッド内で aiohttp を用いてストリーミングリクエストを送信し、最初のチャンク到着までの時間(TTFT)とチャンク間の間隔(ITL)を計測しました。入力は30個の英単語をランダムにつなげた文章であり、出力は
ignore_eosで長さを固定しました。 - 繰り返し: 負荷テストは同時実行数ごとに4つのバッチ(最初のバッチはウォームアップ)を送信し、残りの3つのバッチの中央値を採用しました。プレフィックステストは新しいプレフィックス5個に対して、それぞれコールドリクエスト1回とウォームリクエスト4回を送信しました。
3. 連続バッチ処理: オフにするとスループットが1/10になる
連続バッチ処理自体をオフにするオプションはありません。代わりに --max-num-seqs 1 を用いて1ステップに1リクエストのみ処理させることで、「リクエストを1つずつ処理するサーバー」になります。入力256トークン、出力128トークンのリクエストを同時実行数 c で 2c 個ずつ送信しました。
| サーバー | 同時リクエスト | スループット(tok/s) | TTFT中央値 | ITL中央値 | ITL p99 |
|---|---|---|---|---|---|
1度に1つ(--max-num-seqs 1) |
1 | 174.4 | 18.9ms | 5.6ms | 6.2ms |
| 1度に1つ | 16 | 174.6 | 11,008ms | 5.6ms | 6.5ms |
| デフォルト | 1 | 174.2 | 18.8ms | 5.6ms | 6.7ms |
| デフォルト | 4 | 593.8 | 50.7ms | 6.4ms | 7.5ms |
| デフォルト | 16 | 1,683.1 | 131.0ms | 8.2ms | 9.4ms |
| デフォルト | 64 | 2,899.3 | 272.2ms | 16.6ms | 90.6ms |
3つのバッチのスループットは互いに0.3%の誤差範囲内で一致していました(同時16デフォルト: 1,681.3、1,683.1、1,685.4)。
- 1つのリクエストだけを処理するサーバーは、同時実行数を増やしてもスループットが変わりません。 デコードの1ステップは重み全体をメモリから読み込む処理が大半を占めるため、1つのリクエストを処理しようが16個を処理しようがステップにかかる時間は大きく変わりません。デフォルトサーバーで同時16のトークン間隔は8.2msとなり、単独の場合(5.6ms)の1.5倍に伸びただけである一方、スループットは9.6倍になりました。
- その代償となるのが待ち時間です。 1度に1つだけ処理するサーバーにおいて、同時16リクエストの最初のトークンの中央値は11秒でした。前のリクエストが完了するまで行列に並んだためです。
- 同時64ではITL p99が90.6msに跳ね上がりました。新しいリクエストのプレフィルがデコードステップの間に割り込むためであり、次節で述べるチャンクサイズがこのテールの長さを決定します。
4. chunked prefill: デコードが停止する時間はチャンクサイズに比例する
1つのデコードリクエスト(900トークン)が処理されている間に、約3,700トークンの入力を5回割り込ませ、割り込ませた区間におけるデコードトークン間隔の最大値を計測しました。
| サーバー | 1ステップあたりの入力トークン上限 | 割り込み区間の最大ITL(5回) | その他のITL中央値 | 割り込ませた入力のTTFT中央値 |
|---|---|---|---|---|
--max-num-batched-tokens 512 |
512 | 35.6ms(35.5, 35.5, 35.6, 35.6, 35.6) | 5.7ms | 232.0ms |
| デフォルト | 2,048 | 109.8ms(108.3, 108.6, 109.1, 109.2, 109.8) | 5.7ms | 216.2ms |
--no-enable-chunked-prefill --max-num-batched-tokens 4096 |
4,096(チャンクなし) | 197.6ms(191.8, 195.7, 197.1, 197.4, 197.6) | 5.8ms | 214.1ms |
このGPUにおけるプレフィックス速度は、3,200トークンを174.7msで処理する程度、すなわち秒間約18,000トークンです。2,048トークンのチャンク1つには約111ms、3,700トークン全体には約200msかかるという計算は、表の最大ITLと一致します。5回の測定値が1ms前後の誤差に収まっており、偶然ではありません。
- 小さなチャンクはデコードを保護し、プレフィックス処理を小幅に遅らせます。 512トークンのチャンクは割り込ませた入力のTTFTを216msから232msへと7%増加させる代わりに、デコードの最大停止時間を3分の1に減少させました。
- チャンクをオフにすると、V0時代の動作に近づきます。 このとき、
max_num_batched_tokensはmax_model_len以上でなければサーバーが起動しません(ドキュメントの警告通り)。 - 同時16の負荷(入力256トークン)において、512トークンのチャンクはスループットを変えず(1,682.7対 1,683.1 tok/s)、TTFTの中央値は131.0msから86.3msに減少したものの、ITL p99は9.4msから22.9msに増加しました。短い入力が多い負荷では、チャンクサイズは「どちらのテールを削減するか」を選択するノブにすぎず、無条件の改善ではありません。この違いの原因はスケジュール履歴を確認していないため未確認です。
1.7Bモデルで計測した以前の値(512トークン 69ms、2,048トークン 227ms)と傾向は同じで、サイズはモデルの計算量分だけ小さくなりました。
5. プレフィックスキャッシュ: 繰り返しても18.9ms
3,000単語(約3,200トークン)の共通プレフィックスに対し、リクエストごとに異なる32単語を付与して送信しました。プレフィックスを変更して5つのバッチを実行しました。
| サーバー | コールドな初回のリクエストTTFT(5個のプレフィックス) | ウォームなリクエストTTFT(20回) |
|---|---|---|
| デフォルト(プレフィックスキャッシュ有効) | 中央値 174.7ms(174.6〜175.3) | 中央値 18.9ms(最小 18.0、最大 19.9) |
--no-enable-prefix-caching |
中央値 173.9ms、サーバーの初回のリクエストのみ 236.0ms | 中央値 173.8ms(最小 173.0、最大 178.2) |
ウォームなリクエストは3,200トークンのうち最後のブロックとテールのみを計算するため、9.2倍高速化されました。プレフィックスキャッシュをオフにしたサーバーの最初の1回目のリクエストが236msとなり、他のコールドなリクエストより60ms遅くなりましたが、これがサーバー起動後のウォームアップ分です。計測において最初の値を切り離さなければならない理由はここにあります。
キャッシュがブロック単位である点も覚えておきましょう。vLLM は埋まったブロック(デフォルト16トークン)のみをハッシュ化して保存するため、共通部分がブロック境界で途切れた場合、最後の断片は再計算されます。ハッシュは0.11からデフォルトが sha256 であり、 --prefix-caching-hash-algo に変更できます。
6. cache_salt: 同じプレフィックスでもテナント間で共有しない
プレフィックスキャッシュにはセキュリティ上の問題が1つあります。他のユーザーが送信した文書と同じ前半部分を送信してみて、最初のトークンが早ければ「誰かがこの文書をすでに送信した」ことがわかります。時間差を利用して情報を盗み出すタイミング攻撃です。vLLM ではリクエストに cache_salt を付与すると、その値を最初のブロックのハッシュに混ぜ、同じソルト同士でのみキャッシュを共有させることができます。
--enable-prompt-tokens-details を用いてレスポンスにキャッシュされたトークン数が含まれるようにし、同じ3,000単語のプレフィックスに対してソルトを変更しながら5つのバッチを送信しました。
1つのバッチは同じプレフィックスに対する5つのリクエストです。ソルト a で2回、ソルト b で2回、ソルトなしで1回送信しました。
| 順序 | cache_salt | キャッシュされたトークン(レスポンスの cached_tokens) |
TTFT中央値(2〜5番目のバッチ) |
|---|---|---|---|
| 1 | tenant-a | 0 | 173.9ms |
| 2 | tenant-a | 3,184 または 3,200 | 17.9ms |
| 3 | tenant-b | 0 | 173.7ms |
| 4 | tenant-b | 3,184 または 3,200 | 18.3ms |
| 5 | なし | 0 | 173.9ms |
- 同じソルト同士では、3,200トークンのうちブロック境界まで(16の倍数である3,184または3,200トークン)が再利用されました。
- ソルトが異なると、1文字たりとも違わないプレフィックスであってもキャッシュを利用できず、時間もコールドなリクエストと同等でした。ソルトのないリクエストも、ソルトが指定されたリクエストのキャッシュを利用できませんでした。
- 一番最初のバッチの最初の1回目のリクエストは228.5ms、その次のリクエストは27.6msと、他のバッチよりも低速でした。サーバー起動後の最初のリクエストにおけるウォームアップ分です。
cached_tokens はサーバーを --enable-prompt-tokens-details で起動しなければレスポンスに表示されません。オフにするとこのフィールドが空になり、キャッシュヒットを時間のみで推測する必要があります。サーバー全体のヒット率は /metrics の vllm:prefix_cache_hits_total と vllm:prefix_cache_queries_total で確認します。
6-1. 2026-10-27 再計測: 0.32.0でも同じか
同じGPU、同じリランカーモデル、同じクライアントを使用して vLLM 0.32.0公式コンテナで再計測しました(「2026-10-27 0.31.0 計測 → 0.32.0 再計測」)。
| 項目 | 0.31.0 | 0.32.0 |
|---|---|---|
| スループット、同時1 | 174.2 tok/s | 174.2 tok/s |
| スループット、同時16(デフォルト) | 1,683.1 tok/s | 1,683.6 tok/s |
| スループット、同時16(一度に1つ) | 174.6 tok/s | 174.3 tok/s |
| スループット、同時64 | 2,899.3 tok/s | 2,899.7 tok/s |
| 3,700トークンが割り込む際のデコード最大停止(デフォルトチャンク) | 109.8ms | 108.2ms |
| 3,200トークンプレフィックス最初のトークン(コールド → ウォーム) | 174.7 → 18.9ms | 175.2 → 18.9ms |
すべての値が1.5%の範囲内で同じでした。0.32.0リリースノートの大きな変更(Model Runner V2の拡大、カスケードアテンションの削除、 max_tokens をコンテキストから事前に除外しないなど)は、この小規模モデルのデフォルト負荷では数値を変えませんでした。チャット用のQwen3-0.6B(同構造)も同時1・16・64で174.1、1,678.0、2,898.7 tok/s と同じでした。
7. 運用における要点
- 同時リクエストが多いサービスであれば、連続バッチングがスループットの大部分を占めます。
max_num_seqsを下げると、KVを節約できる代わりにこのメリットを直接削ることになります。 - デコードのテールレイテンシが重要である場合は
max_num_batched_tokensを減らし、長い入力のTTFTが重要である場合は大きくします。このGPUにおいて、1つのチャンクの時間は大体チャンクのトークン数を秒間18,000で割った値でした。自身のGPUのプリフィル速度を一度測定しておけば、チャンクサイズをレイテンシの目標から逆算して選択できます。 - 複数の顧客が1つのエンジンを共有する場合、顧客単位で
cache_saltを付与します。同じ顧客内での再利用はそのまま残り、顧客間のタイミングのリークのみを防ぎます。 - 測定の際は、サーバー起動後の初回のリクエストを除外し、同じ条件を複数回実行してばらつきも合わせて記録します。今回も初回の1つのリクエストが他の値より35%遅くなりました。
8. 検証できなかったこと
- 大規模なモデル、複数のGPU、実際のチャットデータ(プレフィックスの長さと再利用の間隔が混在する負荷)での値は未計測です。
- 512トークンチャンクにおいて短い入力負荷のITL p99が増加した原因は、スケジュールログを確認していないため未確認です。
- 0.28.0で大規模GPUのデフォルト
max_num_batched_tokensが8,192から16,384に引き上げられたというリリースノートは確認しましたが、そのサイズのGPUでの動作は未計測です。
参考
- Kwon他、「Efficient Memory Management for Large Language Model Serving with PagedAttention」、SOSP 2023。同一レイテンシでFasterTransformer・Orcaと比較してスループット2~4倍(アブストラクト基準)。https://arxiv.org/abs/2309.06180
- Yu他、「Orca: A Distributed Serving System for Transformer-Based Generative Models」、OSDI 2022。https://www.usenix.org/conference/osdi22/presentation/yu
- Agrawal他、「Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve」、arXiv 2403.02310。テールレイテンシの制約下でvLLMと比較した単一A100のMistral-7Bサービング容量2.6倍(アブストラクト基準)。https://arxiv.org/abs/2403.02310
- vLLMドキュメント、Optimization and Tuning(Chunked Prefill)、Automatic Prefix Cachingの設計(cache_salt): https://docs.vllm.ai/en/latest/configuration/optimization.html, https://docs.vllm.ai/en/latest/design/prefix_caching.html
- 以前の測定:サービングエンジン内のKVキャッシュ、小型モデルでvLLMを直接再検証する