はじめに
LLMを学習することとサービングすることは、まったく別の問題です。学習は一度しっかり終わらせればよいですが、サービングは毎瞬間ユーザーのリクエストを受け取ってトークンを生成しなければなりません。同じGPUを使っても、サービングスタックをどう構成するかによってスループット(throughput)は何倍も差がつきます。同じH100一枚で毎秒数百トークンを出す人もいれば、数千トークンを出す人もいます。
その差を生み出すのが、まさに推論サービングエンジンです。2026年現在、実務で最もよく挙げられる三つがvLLM、SGLang、TensorRT-LLMです。この記事ではまずLLM推論がなぜ厄介なのか、その根本原理を押さえ、三つのエンジンがそれぞれどのような哲学でこの問題を解くのかを比較したうえで、実際に何を選びどうデプロイするかまで整理します。
この記事の目的は、「無条件でこれが正解」と言うことではありません。ワークロードの性質によって正解が変わるからです。その判断基準を立てることが核心です。
核心原理: 推論は二つの段階からなる
LLMがテキストを生成する過程は、性質がまったく異なる二つの段階に分かれます。この区別を理解しないと、サービング最適化の半分を見落とすことになります。
prefill段階
prefillは入力プロンプト全体を一度に処理する段階です。ユーザーが送ったプロンプトが1,000個のトークンなら、この1,000個を同時にアテンションに通して各位置のKV(key/value)を計算し、最初の出力トークンを作り出します。
prefillは入力トークン数だけの演算を並列に実行できます。つまりGPUの演算ユニットを満たすことができるので、**演算バウンド(compute-bound)**の性質が強いです。行列積が大きく密に起きるからです。
decode段階
decodeはトークンを一個ずつ順番に生成する段階です。一つのトークンを作るには、直前まで作られたすべてのトークンのKVを読み込んでアテンションを計算しなければなりません。ところが毎ステップで新たに処理するトークンはたった一つだけです。
問題はここにあります。トークン一つを作るためにモデルの全重み(数十GB)とそれまで積み上がったKVキャッシュをメモリから読み込まなければならないのに、実際の計算量はトークン一つ分だけです。つまり**メモリバウンド(memory-bound)**です。GPUの演算ユニットはほとんど遊び、メモリ帯域がボトルネックになります。
prefill: 入力Nトークンを一度に処理 -> 演算バウンド (GPU演算ユニット飽和)
decode: トークン1個ずつ生成 -> メモリバウンド (重み/KV読み込みがボトルネック)
一リクエストの一生:
[プロンプト] --prefill--> [最初のトークン] --decode--> [トークン] --decode--> ... [EOS]
この二つの段階の性質の違いが、サービング最適化のすべてを決めます。decodeがメモリバウンドだからこそ、バッチングで複数のリクエストをまとめてGPUを満たし、量子化でメモリ読み込み量を減らし、KVキャッシュを効率的に管理することがこれほど重要なのです。
掘り下げ1: continuous batching
伝統的なバッチング(static batching)は、複数のリクエストを一つのまとまりに集めて同時に処理し、まとまりの中のすべてのリクエストが終わるまで待ってから次のまとまりを始めます。問題はリクエストごとに生成長が千差万別だという点です。あるリクエストはトークン10個、あるリクエストは1,000個を作ります。static batchingでは短いリクエストが早く終わっても、長いリクエストを待つせいでGPUが遊んでしまいます。
continuous batching(in-flight batchingとも呼びます)はこの問題を優雅に解決します。毎デコードステップごとにバッチを動的に再構成します。終わったリクエストは即座にバッチから外し、待機していた新しいリクエストをその場所に入れます。
static batching (無駄が発生):
ステップ-> 1 2 3 4 5 6 7 8
req A ■ ■ ✓ . . . . . <- 3ステップで終わったがスロットが8まで縛られる
req B ■ ■ ■ ■ ■ ■ ■ ✓
req C ■ ■ ■ ✓ . . . . <- 空きスロットがGPUを遊ばせる
continuous batching (スロット即時再利用):
ステップ-> 1 2 3 4 5 6 7 8
req A ■ ■ ✓
req D ■ ■ ■ ✓ <- Aが空けた場所にDを投入
req B ■ ■ ■ ■ ■ ■ ■ ✓
req C ■ ■ ■ ✓
req E ■ ■ ■ ■ <- Cが空けた場所にEを投入
2026年現在、continuous batchingはすべての主要サービングエンジンの標準機能です。これがスループットを引き上げる最も基本的でありながら強力な技法です。
掘り下げ2: paged KVキャッシュ
decode段階でKVキャッシュはシーケンスが長くなるほど大きくなり続けます。伝統的な方式は各リクエストに対して「最大長」分の連続したメモリブロックをあらかじめ確保しておきます。ところが実際の生成長は事前にわかりません。最大2,048トークンを確保しておいたのに、実際には50トークンしか生成しなければ、残りの空間はまるごと無駄になります。
PagedAttentionはオペレーティングシステムの仮想メモリの概念をKVキャッシュに適用します。KVキャッシュを小さな固定サイズのブロック(page)に分け、必要なたびにブロックを割り当てます。論理的には連続でも物理的には散らばっていてもかまいません。ブロックテーブルが論理位置と物理位置をマッピングします。
伝統方式 (連続事前割り当て):
req A: [■■■□□□□□□□□□□□□□] <- 16マス確保し3マスのみ使用、13マス無駄
req B: [■■■■■□□□□□□□□□□□] <- 16マス確保し5マスのみ使用
paged方式 (ブロック単位の動的割り当て):
ブロックプール: [b0][b1][b2][b3][b4][b5]...
req A ブロックテーブル: b0 -> b3 (必要な分だけ)
req B ブロックテーブル: b1 -> b2 -> b4 (物理的に散らばってもOK)
この方式の効果は二つです。第一に、メモリの断片化がほぼなくなり、同じGPUメモリではるかに多くの同時リクエストを処理できます。第二に、ブロック単位で管理するので、複数のリクエストが同じprefixを共有するとき、そのブロックを共有できます。PagedAttentionはvLLMが最初に普及させた技法で、今では事実上の標準になりました。
フレームワーク比較
さて、三つのエンジンを一つずつ見ていきます。それぞれ出発点と強みが異なります。
vLLM
vLLMはPagedAttentionを世に知らしめたプロジェクトであり、最も汎用的な選択肢です。幅広いモデルアーキテクチャを素早くサポートし、多様なハードウェア(NVIDIAだけでなくAMD、その他のアクセラレータ)で動きます。OpenAI互換APIサーバーを標準で提供するので統合が容易です。
特徴は「バランス」です。ある一つのワークロードに極端に最適化するよりも、多様な状況でまんべんなく良い性能を出します。新しいモデルが出ると最初にサポートされる場合が多く、最新モデルを素早く立ち上げる必要があるときに頼りになります。よくチューニングされたTensorRT-LLMと比べると、特定のシナリオでスループットがやや低いことがありますが、その差はたいてい大きくありません。
TensorRT-LLM
TensorRT-LLMはNVIDIAが作ったコンパイルベースのエンジンです。モデルをNVIDIA GPUに最適化されたエンジンに事前コンパイルし、カーネル融合と精度最適化を極限まで推し進めます。その結果、よくサポートされたモデルと適切にチューニングされた設定では、H100のようなNVIDIA GPUでvLLM比でおよそ15〜30%高いスループットを示す場合が報告されています(モデルと設定によって偏差が大きいです)。
代償は柔軟性です。エンジンをコンパイルする段階が必要で、新しいモデルや特殊な構成へのサポートがvLLMほど速くないことがあります。またNVIDIAエコシステムに縛られます。「決まったモデルをNVIDIA GPUで最大効率で長く回す」のであれば強力な選択肢です。
SGLang
SGLangはRadixAttentionという技法でprefixキャッシュ再利用を最大化します。複数のリクエストが共通の前部分(システムプロンプト、few-shot例、マルチターン会話履歴)を持つとき、その部分のKV計算を再利用します。RadixAttentionはprefixをradix treeで管理し、共有可能な部分を自動的に見つけ出します。
そのためSGLangはマルチターン会話や同じシステムプロンプトを繰り返し使うエージェントワークロードで特に輝きます。こうした環境ではprefix再利用のおかげで、およそ10〜20%程度の追加の利得を得る場合があります(共有されるprefixの割合に大きく左右されます)。構造化生成や複雑なプロンプトプログラムにも強いです。
比較テーブル
| 項目 | vLLM | TensorRT-LLM | SGLang |
| --- | --- | --- | --- |
| 核心の強み | 汎用性、広いモデル/ハードウェア対応 | コンパイル最適化、最高スループット | RadixAttention prefix再利用 |
| スループット | 基準線 (高い) | NVIDIAで約15〜30%高い | prefix共有が多いと追加利得 |
| ハードウェア | NVIDIA、AMDなど多様 | NVIDIA中心 | 主にNVIDIA |
| 新規モデル対応 | 非常に速い | 相対的に遅いことがある | 速い |
| 設定難度 | 低い | コンパイル段階でやや高い | 中程度 |
| 適合ワークロード | 汎用、素早いプロトタイピング | 固定モデルの大規模運用 | マルチターン、エージェント、共有prefix |
選択ガイド
テーブルだけ見ても途方に暮れるかもしれないので、実際の状況別に整理します。
- **素早く立ち上げて多様なモデルを実験したいなら**: vLLM。統合が容易でほぼすべてのモデルがすぐ動きます。
- **モデルが固定されていてNVIDIA GPUでコストを最大限に絞り出したいなら**: TensorRT-LLM。コンパイルコストを払う価値があります。
- **マルチターンチャットボットやエージェントのようにprefixが多く重なるなら**: SGLang。RadixAttentionの効果が大きいです。
- **よくわからずとにかく始めるべきなら**: vLLMで始めて、ボトルネックが明確になったら他のエンジンをベンチマークしてください。
重要なのは自分のワークロードで直接測定することです。他人のベンチマーク数値は出発点にすぎず、入力/出力長の分布と同時実行レベルによって結果が大きく変わります。
実務: デプロイ設定の例
次はvLLMのOpenAI互換サーバーを立ち上げる例です。
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--max-model-len 8192 \
--gpu-memory-utilization 0.90 \
--max-num-seqs 256 \
--enable-chunked-prefill
Kubernetesにデプロイするなら、次のようなDeployment形式になります。
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-llama
spec:
replicas: 2
selector:
matchLabels:
app: vllm-llama
template:
metadata:
labels:
app: vllm-llama
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- "--model"
- "meta-llama/Llama-3.1-8B-Instruct"
- "--gpu-memory-utilization"
- "0.90"
- "--max-num-seqs"
- "256"
resources:
limits:
nvidia.com/gpu: "1"
ports:
- containerPort: 8000
SGLangサーバーも同様に立ち上げられます。
python -m sglang.launch_server \
--model-path meta-llama/Llama-3.1-8B-Instruct \
--mem-fraction-static 0.85 \
--context-length 8192
核心のチューニングパラメータは共通して三つです。GPUメモリ使用比率(高めるほどKVキャッシュ空間が増え同時実行が増加)、最大同時シーケンス数、最大コンテキスト長です。メモリ使用比率を高くしすぎると、突然の長いリクエストでOOMが起きることがあるので余裕を持たせるべきです。
掘り下げ3: 量子化とサービング
decodeがメモリバウンドだという事実を改めて思い出すと、メモリから読み込むデータの量を減らすあらゆる技法がそのまま速度向上につながります。量子化がまさにそれです。重みをFP16の代わりにより低い精度で保存すると、同じ重みを読むのに必要なメモリ帯域が減ります。
精度別の重みメモリ (8Bモデルと仮定、概算値):
FP16 : 要素あたり2バイト -> 約16 GB
INT8 : 要素あたり1バイト -> 約8 GB
INT4 : 要素あたり0.5バイト -> 約4 GB
decodeは毎トークンごとに重みを読むので、
読むデータが半分なら、メモリボトルネックは半分に緩和。
2026年現在、サービングでよく使われる精度はFP8とINT4です。FP8はH100以降の世代のGPUのハードウェアサポートを受け、精度損失が小さいながらもスループットの利得が大きいです。INT4はメモリを最も多く節約しますが、精度損失がより大きいので、作業によって品質を検証する必要があります。
量子化選択の直観:
品質最優先 -> FP16またはFP8
メモリ/コスト最優先 -> INT4 (品質検証必須)
バランス -> FP8 (ハードウェアサポート時に有利)
重要な点は、量子化が重みだけでなくKVキャッシュにも適用されるということです。長いコンテキストと高い同時実行ではKVキャッシュがメモリの大きな割合を占めるので、KVをFP8で保存すると同時実行をさらに引き上げられます。ただし重みの量子化とKVの量子化は別個の設定であり、両方をオンにすると効果が累積する傾向があります。
掘り下げ4: 並列化 — モデルがGPU一枚に入らないとき
モデルが大きくてGPU一枚のメモリに入らないなら、複数のGPUにモデルを分けなければなりません。サービングでよく使う二つの方式がテンソル並列(tensor parallel)とパイプライン並列(pipeline parallel)です。
テンソル並列 (tensor parallel, TP):
一つのレイヤーの重み行列を複数のGPUに横に分割。
各GPUが部分計算をし結果を合わせる(all-reduce通信)。
GPU間の通信が頻繁なので速いインターコネクト(NVLink)が重要。
[GPU0: 重みの半分] --合体-- [GPU1: 重みの半分]
同じレイヤーを二つで分けて計算
パイプライン並列 (pipeline parallel, PP):
レイヤーをグループに分けてGPUごとに異なるレイヤーを任せる。
GPU0が前のレイヤー、GPU1が後ろのレイヤーを処理。
通信は少ないがパイプラインバブル(遊ぶ区間)が生じうる。
[GPU0: 1〜16層] --伝達--> [GPU1: 17〜32層]
実務の直観は次のとおりです。一ノード内のGPU同士のようにインターコネクトが速ければテンソル並列が有利です。通信が頻繁でも速いリンクが支えてくれるからです。ノードをまたぐ場合のようにリンクが遅ければ、通信が少ないパイプライン並列や二つの方式の組み合わせを検討します。ほとんどのサービングエンジンはテンソル並列の次数(例: TP=2、TP=4)を設定一行で指定できます。
vLLMでテンソル並列4で大きなモデルをサービング
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.90
並列化はタダではありません。通信オーバーヘッドが追加されるので、モデルが一枚に入るなら、あえて分割しないほうが速いです。並列化は「どうしても大きくて分けなければならないとき」または「遅延を減らすためにより多くのGPUを投入するとき」の道具です。
実務: オートスケーリングと可観測性
サービングをプロダクションに乗せると、単一インスタンスの性能だけが問題ではありません。トラフィックは時間帯によって揺れ動き、それに合わせてインスタンスを増やしたり減らしたりしてこそコストを制御できます。
オートスケーリングの基準となる信号:
- GPU使用率 / KVキャッシュ占有率
- キューの長さ(pendingリクエスト数)
- TTFT(最初のトークン遅延)がSLAを超えるか
スケールアウトのトリガー例:
KVキャッシュ占有率 > 85% が一定時間継続 -> インスタンス +1
ここで注意すべき点は、LLMインスタンスは起動に時間がかかるということです。モデルの重みをメモリに乗せてエンジンを初期化するのに数十秒から数分かかることがあります。したがってトラフィックが集中してからスケールすると遅すぎます。予測的スケーリングや十分な余裕(headroom)を持たせる設計が必要です。
可観測性の面では、先に述べたTTFT、トークンあたり時間、スループットをダッシュボードで常時追跡しなければなりません。またリクエスト失敗率、OOMの発生、キューの滞留を併せて見てこそ問題を早期に捉えられます。
サービングダッシュボードの核心パネル:
1) TTFT分布 (p50, p95, p99)
2) トークンあたり時間(TPOT)分布
3) スループット(毎秒トークン)
4) KVキャッシュ占有率 / GPU使用率
5) キューの長さ / 失敗率
ワークロード測定: スループットを見積もる方法
エンジンを選び設定を決めたら、実際にどれだけ受けられるかを見積もる必要があります。正確な数値は測定でしかわかりませんが、おおよその上限を推定する考え方は有用です。
同時実行上限の直観:
使えるKVメモリ / リクエストあたりKVメモリ = 同時リクエスト数の上限
例) KVに使えるメモリ = 40 GB
リクエストあたり平均KV(コンテキスト4K基準) = 0.5 GB
-> 同時リクエスト上限 約80個
コンテキストを32Kに伸ばすとリクエストあたりKVが8倍 -> 同時約10個
こうした推定は正確ではありませんが、「コンテキストを伸ばすと同時実行が急減する」というトレードオフを数字で体感させてくれます。実際の運用では負荷テストで同時実行を段階的に上げながらTTFTとスループットの変曲点を見つけるのが定石です。変曲点を超えて同時実行をさらに上げると、スループットは停滞し遅延だけが悪化します。
負荷テストで見つける動作点:
同時実行を上げながら測定
-> スループットはある地点まで上がって停滞
-> その地点以降は遅延だけ悪化
-> 停滞直前が効率的な運用点
落とし穴とトラブルシューティング
- **OOM(メモリ不足)**: gpu-memory-utilizationを高くしすぎるか、max-model-lenが過剰な場合がよくあります。同時リクエストがすべて最大長でデコードする最悪の場合を想定して余裕を持たせてください。
- **スループットは良いのに遅延が悪い**: バッチが大きすぎて個別リクエストの応答が遅くなった場合です。スループットと遅延はトレードオフの関係であることを覚えておいてください。
- **TensorRT-LLMのコンパイル失敗**: モデル構造や精度設定がサポート範囲を外れている可能性があります。サポートマトリクスをまず確認してください。
- **SGLangなのにprefixキャッシュの効果がない**: リクエストが実際にprefixを共有しなければRadixAttentionの利得はありません。システムプロンプトを統一するなど、ワークロードの構造を点検してください。
- **ベンチマークが実際と異なる**: 合成負荷の入出力分布が実際のトラフィックと異なる場合です。できるかぎり実際のトラフィックサンプルで測定してください。
おわりに
LLM推論サービングの核心は、結局二つに要約されます。第一に、decodeがメモリバウンドだという事実を受け入れ、バッチング、量子化、KVキャッシュ最適化でメモリボトルネックを攻略すること。第二に、自分のワークロードの性質に合ったエンジンを選ぶことです。
vLLMはバランスの取れた汎用デフォルト、TensorRT-LLMは固定モデルの極限スループット、SGLangはprefix共有ワークロードの強者です。どれも絶対的な正解ではなく、自分のトラフィックで測定した数字が最終判断の根拠になるべきです。推論サービングは急速に進化する分野なので、公式ドキュメントを地道に追っていくことをお勧めします。
参考資料
- [vLLM公式ドキュメント](https://docs.vllm.ai/)
- [vLLM GitHub](https://github.com/vllm-project/vllm)
- [SGLang GitHub](https://github.com/sgl-project/sglang)
- [TensorRT-LLM GitHub](https://github.com/NVIDIA/TensorRT-LLM)
- [Hugging Faceドキュメント](https://huggingface.co/docs)
- [PyTorch](https://pytorch.org/)
- [Attention Is All You Need (arXiv:1706.03762)](https://arxiv.org/abs/1706.03762)
- [FlashAttention (arXiv:2205.14135)](https://arxiv.org/abs/2205.14135)
현재 단락 (1/176)
LLMを学習することとサービングすることは、まったく別の問題です。学習は一度しっかり終わらせればよいですが、サービングは毎瞬間ユーザーのリクエストを受け取ってトークンを生成しなければなりません。同じG...