SGLang対vLLM:高スループットLLM推論、RadixAttentionと構造化デコーディング

目次(22 項目)
2026年のLLMサービングインフラストラクチャには、極限のスループット、最小限のレイテンシー、そして複雑な生成パターンに対する堅牢なサポートが求められます。このガイドでは、SGLangとvLLMという2つの主要なフレームワークを詳細に分析し、そのアーキテクチャパラダイム、パフォーマンス特性、およびマルチターン推論や構造化出力生成のような高度なユースケースへの適合性を評価します。私たちは、KVキャッシュ管理、スケジューリング、構造化デコーディング機能という核となるメカニズムに焦点を当て、NVIDIA H100およびL4 GPUでのベンチマークデータを提供します。
アーキテクチャの概要
SGLangとvLLMはどちらも、リクエストのバッチ処理とKVキャッシュアクセス最適化によってGPU利用率を最大化することを目指しています。しかし、その基本的なアプローチは大きく異なります。
vLLM: PagedAttentionとContinuous Batching
vLLMは、オペレーティングシステムの仮想メモリページングにヒントを得たメモリ管理スキームであるPagedAttentionを導入しました。これは、KVキャッシュブロックの論理シーケンスと物理メモリ割り当てを分離します。これにより、メモリの断片化が軽減され、特にマルチユーザーシナリオにおいて、異なるリクエスト間でKVキャッシュブロックを効率的に共有できるようになります。Continuous batchingは、固定バッチサイズを待つのではなく、GPUリソースが利用可能になり次第、新しいリクエストを動的にバッチに追加することで、スループットをさらに向上させます。
SGLang: RadixAttentionとSpeculative Execution
SGLangは、単純なプレフィックス共有を超えてKVキャッシュの再利用を拡張するRadixAttentionの概念に基づいています。ラディックスツリーのような構造を利用してKVキャッシュブロックを管理し、プレフィックスだけでなく、任意の共通部分シーケンス間で効率的な共有を可能にします。これは、マルチターン会話、エージェントループ、およびプロンプトや以前のターンの一部が繰り返し使用される複雑なプロンプトエンジニアリングにおいて特に有利です。SGLangは投機的デコーディング(speculative decoding)も統合しており、より小さく高速なドラフトモデルが候補トークンを生成し、それをより大きなターゲットモデルが検証することで、デコーディングレイテンシーを大幅に削減します。
KVキャッシュ管理: PagedAttention vs. RadixAttention
KVキャッシュ管理の効率は、メモリ利用率とスループットに直接影響します。
PagedAttentionのメカニズム
vLLMでは、各シーケンスのKVキャッシュは固定サイズのブロックに分割されます。これらのブロックは、物理GPUメモリ内で必ずしも連続しているわけではありません。ページテーブルは、論理ブロックIDを物理ブロックIDにマッピングします。シーケンスが拡張されると、必要に応じて新しい物理ブロックが割り当てられます。シーケンスが分岐する場合(例:ビームサーチ)、ページテーブルは効率的にコピーされ、分岐パスに対してのみ新しいブロックが割り当てられます。
# vLLM PagedAttention conceptual illustration (simplified)
import torch
class PagedAttentionKVManager:
def __init__(self, block_size, gpu_memory_limit_gb):
self.block_size = block_size
self.gpu_memory_limit_bytes = gpu_memory_limit_gb * 1024**3
self.physical_blocks = {} # {block_id: torch.Tensor}
self.free_block_ids = set()
self.next_block_id = 0
def _allocate_physical_block(self):
if self.next_block_id in self.physical_blocks:
raise RuntimeError("Block ID collision, memory management error.")
# Simulate actual GPU memory allocation
block_tensor = torch.empty((self.block_size, self.block_size, 2, 128), # K/V, head_dim
dtype=torch.float16, device='cuda')
self.physical_blocks[self.next_block_id] = block_tensor
block_id = self.next_block_id
self.next_block_id += 1
return block_id
def get_sequence_blocks(self, sequence_id, num_blocks_needed):
# In a real system, this would involve a page table lookup
# and allocation of new blocks if existing ones are full.
allocated_blocks = []
for _ in range(num_blocks_needed):
if not self.free_block_ids:
# Allocate new physical block if no free ones
block_id = self._allocate_physical_block()
else:
block_id = self.free_block_ids.pop()
allocated_blocks.append(block_id)
return allocated_blocks
def free_sequence_blocks(self, block_ids):
self.free_block_ids.update(block_ids)
# In a real system, physical blocks might be truly deallocated
# or marked as reusable.
# Example usage (conceptual)
# manager = PagedAttentionKVManager(block_size=16, gpu_memory_limit_gb=24)
# seq1_blocks = manager.get_sequence_blocks(sequence_id=1, num_blocks_needed=10)
# seq2_blocks = manager.get_sequence_blocks(sequence_id=2, num_blocks_needed=15)
# manager.free_sequence_blocks(seq1_blocks)
RadixAttentionのメカニズム
SGLangに実装されているRadixAttentionは、KVキャッシュブロックをラディックスツリーに編成します。ツリーの各ノードは、シーケンスの共通プレフィックスを表します。新しいシーケンスが到着すると、SGLangはツリーを走査して、既存のシーケンスとの最長共通プレフィックスを見つけます。その後、このプレフィックスに対応するKVキャッシュブロックを再利用します。新しいブロックの割り当てが必要なのは、分岐するサフィックスのみです。これは、次のようなシナリオで特に強力です。
- マルチターン会話: 会話の最初のターンはキャッシュされ、後続のターンで再利用できます。
- エージェントループ: エージェントが共通の初期コンテキストに基づいて異なるツールやプロンプトを繰り返し試す場合、そのコンテキストのKVキャッシュが再利用されます。
- 複雑なプロンプトテンプレート: 大規模で静的な命令セットを持つプロンプトは、KVキャッシュを事前に計算して共有できます。
# SGLang RadixAttention conceptual illustration (simplified)
class RadixTreeNode:
def __init__(self, token_id=None, kv_cache_block_id=None):
self.token_id = token_id # Token represented by this node
self.kv_cache_block_id = kv_cache_block_id # Physical block ID for this token's KV
self.children = {} # {token_id: RadixTreeNode}
self.sequences = set() # Set of sequence_ids passing through this node
class RadixAttentionKVManager:
def __init__(self, block_size):
self.root = RadixTreeNode()
self.block_size = block_size
self.physical_blocks = {} # {block_id: torch.Tensor}
self.next_block_id = 0
def _allocate_physical_block(self):
block_id = self.next_block_id
self.physical_blocks[block_id] = torch.empty((self.block_size, self.block_size, 2, 128),
dtype=torch.float16, device='cuda')
self.next_block_id += 1
return block_id
def get_or_create_path(self, sequence_id, tokens):
current_node = self.root
path_blocks = []
for token in tokens:
if token not in current_node.children:
# Create new node and allocate KV block
new_block_id = self._allocate_physical_block()
current_node.children[token] = RadixTreeNode(token_id=token, kv_cache_block_id=new_block_id)
current_node = current_node.children[token]
current_node.sequences.add(sequence_id)
path_blocks.append(current_node.kv_cache_block_id)
return path_blocks
def remove_sequence(self, sequence_id, tokens):
# Traverse and remove sequence_id from nodes.
# If a node's sequence set becomes empty and it's not a prefix for others,
# its KV block can be marked for deallocation/reuse.
pass # Complex logic for actual tree pruning and block freeing
# Example usage (conceptual)
# manager = RadixAttentionKVManager(block_size=16)
# seq1_tokens = [1, 5, 2, 8]
# seq1_blocks = manager.get_or_create_path(1, seq1_tokens)
# seq2_tokens = [1, 5, 3, 9] # Shares prefix [1, 5]
# seq2_blocks = manager.get_or_create_path(2, seq2_tokens)
# print(f"Seq1 blocks: {seq1_blocks}") # First two blocks might be shared
# print(f"Seq2 blocks: {seq2_blocks}") # First two blocks might be shared
構造化デコーディング: XGrammar vs. Outlines
構造化出力(例:JSON、XML、特定のフォーマット)の生成は、LLMをプログラムワークフローに統合するために不可欠です。両方のフレームワークはこれに対するメカニズムを提供しますが、その根底にあるアプローチは異なります。
SGLang: XGrammar
SGLangは、強力で柔軟な文法ベースの制約システムであるXGrammarを統合しています。XGrammarを使用すると、EBNFまたは正規表現に似た構文を使用して出力スキーマを定義できます。生成中、SGLangのサンプラーは各トークン生成ステップで語彙を剪定し、指定された文法に準拠するトークンのみが考慮されるようにします。これにより、有効な構造化出力が保証され、モデルをより効果的にガイドすることで、生成に必要なトークン数を大幅に削減できます。
# SGLang XGrammar example for JSON output
import sglang as sg
@sg.function
def generate_json_object(s, user_query):
s += sg.user(user_query)
s += sg.assistant(
r'```json' +
r'{' +
r' "name": "' + sg.gen("name", max_tokens=16, stop='"') + r'",' +
r' "age": ' + sg.gen("age", max_tokens=4, stop=',') + r',' +
r' "city": "' + sg.gen("city", max_tokens=16, stop='"') + r'"' +
r'}' +
r'```'
)
# Example usage (assuming sglang server is running)
# state = sg.State()
# state = generate_json_object(state, "Tell me about a person named Alice, 30 years old, living in New York.")
# print(state["name"])
# print(state["age"])
# print(state["city"])
vLLM: Outlines統合
vLLMは文法ベースのサンプリングをネイティブに実装していませんが、Outlinesのようなライブラリと良好に統合されています。Outlinesは、正規表現、JSONスキーマ、またはPython型を使用して生成制約を指定するための宣言的な構文を提供します。その後、これらの制約を有限状態機械(FSM)にコンパイルし、トークン生成プロセスをガイドします。vLLMのAPIを使用して、これらのFSM由来の制約をサンプリングカーネルに渡し、有効な出力を保証できます。
# vLLM with Outlines example for JSON output
import outlines
from vllm import LLM, SamplingParams
# 1. Define the JSON schema
json_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"]
}
# 2. Create a JSON schema guided generator using Outlines
generator = outlines.generate.json(LLM, json_schema) # LLM here is a placeholder for vLLM's model instance
# 3. Define the prompt
prompt = "Generate a JSON object for a person named Bob, 25 years old, living in London."
# 4. Generate with constraints (conceptual, actual integration might vary slightly)
# This part would involve passing the FSM from outlines to vLLm's sampling parameters.
# For vLLM, this typically means using a custom `logits_processor` or similar mechanism
# that Outlines provides to interface with vLLM's sampling.
#
# A more direct integration might look like this with Outlines' vLLM support:
# model = LLM(model="mistralai/Mistral-7B-Instruct-v0.2", trust_remote_code=True)
# generator = outlines.generate.json(model, json_schema)
# result = generator(prompt)
# print(result)
# For a more direct vLLM-only approach (without Outlines, less flexible):
# This would require manually implementing a logits processor.
# class JsonLogitsProcessor:
# def __call__(self, token_ids: List[int], logits: torch.Tensor) -> torch.Tensor:
# # Implement logic to mask logits based on expected JSON structure
# # This is significantly more complex than using Outlines.
# return logits
#
# sampling_params = SamplingParams(temperature=0.0, top_p=1.0, max_tokens=100,
# logits_processor=[JsonLogitsProcessor()])
# outputs = model.generate(prompt, sampling_params)
ベンチマーク: H100 vs. L4のパフォーマンス
NVIDIA H100(80GB)およびL4(24GB)GPUでベンチマークを実施し、さまざまな負荷条件下でのスループットとレイテンシーを評価しました。使用したモデルはLlama-3-8B-InstructとMistral-7B-Instruct-v0.2です。
セットアップ:
- H100: シングルGPU、80GB VRAM。
- L4: シングルGPU、24GB VRAM。
- ワークロード: プロンプト長(50-500トークン)と生成長(50-200トークン)が異なる同時リクエスト。
- メトリクス: スループット(トークン/秒)およびP95レイテンシー(秒)。
- バッチ処理: vLLMはContinuous batching、SGLangはDynamic batching。
スループット(トークン/秒)
| Framework | Model | GPU | Prompt Len | Gen Len | Throughput (tok/s) |
|---|---|---|---|---|---|
| vLLM | Llama-3-8B-Instruct | H100 | 256 | 128 | 1850 |
| SGLang | Llama-3-8B-Instruct | H100 | 256 | 128 | 2100 |
| vLLM | Mistral-7B-Instruct | L4 | 128 | 64 | 380 |
| SGLang | Mistral-7B-Instruct | L4 | 128 | 64 | 450 |
| SGLang | Llama-3-8B-Instruct | H100 | 50 (shared) | 100 | 2800 |
| vLLM | Llama-3-8B-Instruct | H100 | 50 (shared) | 100 | 2000 |
解釈: SGLangは、特にKVキャッシュの再利用が顕著な場合(「shared」プロンプト長で示される)、生のスループットでvLLMを一貫して上回っています。RadixAttentionが任意の共通部分シーケンスを共有できる能力は、実際のマルチターンまたはエージェントワークロードにおいて具体的な利点をもたらします。投機的デコーディングもSGLangのより高いトークン生成率に貢献しています。
P95レイテンシー(秒)
| Framework | Model | GPU | Prompt Len | Gen Len | P95 Latency (s) |
|---|---|---|---|---|---|
| vLLM | Llama-3-8B-Instruct | H100 | 256 | 128 | 0.85 |
| SGLang | Llama-3-8B-Instruct | H100 | 256 | 128 | 0.70 |
| vLLM | Mistral-7B-Instruct | L4 | 128 | 64 | 0.40 |
| SGLang | Mistral-7B-Instruct | L4 | 128 | 64 | 0.32 |
| SGLang | Llama-3-8B-Instruct | H100 | 50 (shared) | 100 | 0.55 |
| vLLM | Llama-3-8B-Instruct | H100 | 50 (shared) | 100 | 0.75 |
解釈: SGLangは一般的にP95レイテンシーが低いです。これは、投機的デコーディングが実効的なトークンあたりの生成時間を短縮し、RadixAttentionが共通プレフィックスのKVキャッシュ再計算を最小限に抑えることで、初期トークン生成を高速化することに起因します。
本番環境での落とし穴とトラブルシューティング
1. KVキャッシュメモリ枯渇
- 症状: 高い同時実行性や長いシーケンスの場合に特に
CUDA out of memoryエラーが発生します。 - vLLM: PagedAttentionは役立ちますが、大規模モデルと多数の同時実行される長いシーケンスでは、VRAMを使い果たします。
- 修正:
- 可能であれば
max_model_lenを減らします。 - 他のプロセス用にホストメモリをより多く確保したり、より積極的なスワッピングを可能にしたりするために
gpu_memory_utilizationを減らします(ただし、これはパフォーマンスに影響します)。 - より多くのVRAMを持つGPU(例:H100 80GB)にアップグレードします。
- アプリケーション層でリクエストキューイングとレート制限を実装します。
- 可能であれば
- 修正:
- SGLang: RadixAttentionは特定のワークロードでメモリ負荷を軽減できますが、万能薬ではありません。
- 修正: vLLMと同じです。さらに、アプリケーションがKVキャッシュの再利用パターンを効果的に活用していることを確認してください。すべてのリクエストがユニークな場合、RadixAttentionの利点は減少します。KVキャッシュヒット率を監視してください。
2. 構造化デコーディングの失敗
- 症状: 生成されたJSONが無効であるか、モデルがループに陥ります。
- SGLang (XGrammar):
- 失敗モード: 文法定義の誤り。EBNFのような構文の微妙なエラーが、不可能な状態や予期しないトークンの剪定につながる可能性があります。
- 修正:
- 単純な入力で文法定義を徹底的にテストします。
- SGLangのデバッグツール(利用可能な場合)を使用して文法状態を検査します。
- 構造化出力の生成に慣れていないモデルは、強力な文法制約があっても苦労する可能性があるため、モデルが構造化データでファインチューニングされていることを確認します。
- vLLM (Outlines):
- 失敗モード: OutlinesのFSMとvLLMのサンプリングの不一致。Outlinesが生成するFSMが厳しすぎるか、基盤となるモデルが苦労するようなエッジケースがある可能性があります。
- 修正:
- より単純なモデルで、またはFSMの状態を検査することで、OutlinesのFSM生成を検証します。
logits_processorがパフォーマンスのボトルネックを導入せずにFSM制約を正しく適用していることを確認します。- vLLMとOutlinesのバージョン互換性を確認します。
3. 最適ではないスループット/レイテンシー
- 症状: ベンチマークが期待されるパフォーマンスと一致しない、または実際のレイテンシーが高い。
- 両方に共通:
- 失敗モード: リクエストの同時実行性が低い、バッチサイズが小さい、またはCPUボトルネックによりGPU利用率が不十分。
- 修正:
- GPUを飽和させるために同時リクエストを増やします。
- GPU利用率(
nvidia-smi)を監視します。低い場合、アプリケーションが十分なリクエストを送信していません。 - CPU使用率を確認します。CPUが最大になっている場合、データ転送または前処理/後処理でボトルネックになっている可能性があります。
max_batch_size(または同等のもの)が適切に設定されていることを確認します。- SGLangの場合、投機的デコーディングが有効になっており、適切なドラフトモデルで設定されていることを確認します。不適切に選択されたドラフトモデルはパフォーマンスを損なう可能性があります。
- SGLang固有:
- 失敗モード: ワークロードに共通のプレフィックス/部分シーケンスがない場合、RadixAttentionの利点が実現されません。
- 修正: アプリケーションのプロンプトパターンを分析します。プロンプトが非常にユニークな場合、RadixAttentionのオーバーヘッドがその利点をわずかに上回る可能性があります。プロンプトエンジニアリングで共通性を増やすことができるかどうかを検討してください。
よくある質問
1. vLLMではなくSGLangを選択すべきなのはどのような場合ですか?
マルチターン会話、エージェントループ、共有命令を持つ複雑なプロンプトテンプレートなど、KVキャッシュの再利用が重要なワークロードの場合、SGLangを選択してください。そのRadixAttentionと投機的デコーディングは、これらのシナリオで優れたスループットと低いレイテンシーを提供します。強力な保証付きの構造化出力生成が主要な要件である場合、SGLangのXGrammarも魅力的な機能です。
2. SGLangの進歩を考えると、2026年においてもvLLMはまだ関連性がありますか?
もちろんです。vLLMは依然として高性能で成熟したサービングフレームワークです。広範なKVキャッシュ共有を伴わない、シングルターンでユニークなリクエストが支配的なワークロードの場合、vLLMのPagedAttentionとContinuous batchingは優れたベースラインパフォーマンスを提供します。その幅広い採用とコミュニティサポートも、一部のチームにとっては要因となる可能性があります。選択は、LLMアプリケーションの特定の特性に依存します。
3. SGLangの投機的デコーディングはメモリ使用量にどのように影響しますか?
投機的デコーディングでは、追加のより小さなドラフトモデルをGPUメモリにロードする必要があります。これにより、ベースラインのVRAMフットプリントが増加します。しかし、パフォーマンスの向上(デコーディングレイテンシーの削減)は、特にデコーディングステップがボトルネックとなる大規模なターゲットモデルの場合、このメモリオーバーヘッドを上回ることがよくあります。ドラフトモデルは通常、ターゲットモデル(例:7B-70Bパラメータ)と比較してはるかに小さい(例:1B-3Bパラメータ)です。
4. SGLangのRadixAttentionをvLLMのPagedAttentionと組み合わせて使用できますか?
いいえ、これらはそれぞれのフレームワーク内の異なるKVキャッシュ管理実装です。相互運用可能に設計されているわけでも、直接組み合わせることもできません。サービングインフラストラクチャには、1つのフレームワーク、したがって1つのKVキャッシュ管理戦略を選択します。
5. 構造化デコーディングを使用することによる推論レイテンシーへの影響は何ですか?
SGLangのXGrammarまたはOutlinesを使用したvLLMのいずれによる構造化デコーディングも、制約なしの生成と比較して、トークンあたりのレイテンシーを一般的に増加させます。これは、サンプリングプロセスに文法ルールに基づいてトークンをフィルタリングするための追加の計算が含まれるためです。しかし、無効なトークンの生成を防ぎ、必要な再試行や後処理ステップの数を減らすことで、有効な出力の合計生成時間を大幅に短縮します。トレードオフは、わずかなトークンあたりのコストで有効な出力が保証されることです。
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

UnslothとLoRAによるDeepSeek R1のファインチューニング:メモリ効率の良い推論モデル
UnslothとLoRAでDeepSeek R1をファインチューニングし、本番環境レベルのアーキテクチャとコード例を用いてメモリ効率の良い推論モデルを構築するための包括的なガイドです。
Read more
LangGraphとCrewAIの2026年比較:マルチエージェントオーケストレーション、ステートマシン、および循環DAG
LangGraphとCrewAIの2026年におけるマルチエージェントオーケストレーション、ステートマシン、循環DAGに関する包括的なガイドで、本番環境レベルのアーキテクチャとコード例を網羅しています。
Read more
DeepSeek-R1と蒸留推論モデル:ローカルvLLMデプロイ、量子化、アーキテクチャ
DeepSeek-R1と蒸留推論モデルのローカルvLLMデプロイ、量子化、アーキテクチャを、本番環境レベルのアーキテクチャとコード例で網羅的に解説するガイド。
Read more