0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

# vLLM × SGLang でLLMベンチマーク計測 — 12GB GPU で同じモデルを公平に比較する準備編

0
Posted at

はじめに

ローカルで LLM を動かす手段は、用途によっていくつかレイヤーがあります。

  • 手軽に触りたい (個人利用 / 試作): OllamaLM Studio。インストール 1 ステップでモデルが動き、GGUF / Q4_K_M などの量子化も自動で扱ってくれる
  • 本格運用したい (複数ユーザー / API サーバ化 / スループット重視): vLLMSGLang。OpenAI 互換 API を喋り、continuous batching と KV cache 最適化で並列処理に耐える
  • 極限まで最適化したい (商用 / 大規模 GPU クラスタ): TensorRT-LLM。NVIDIA 公式で性能トップクラスだが、エンジンビルドと量子化キャリブレーションに専門知識が必要で、モデルごとに builder スクリプトを書くフェーズが発生する

この記事で扱う vLLM と SGLang は、ちょうど真ん中のレイヤーです。Ollama より速く、TensorRT-LLM より圧倒的にセットアップが軽い (どちらも基本は docker run 一発)。Hugging Face のモデルをほぼそのまま投入でき、量子化済みチェックポイント (FP8, AWQ, GPTQ) もそのまま読めます。**「個人開発から本格運用までを 1 つのスタックでカバーできる」**という位置づけが、両エンジンが二大選択肢になっている理由です。

ただ、どちらも論文と GitHub では華々しい数字が並ぶ一方、いざ手元の RTX 4070 12GB に同じモデルを載せて比べようとすると、起動オプションも測定指標もそれぞれ違うため、「正しく同じ条件で動かす」だけで一仕事になります。

このシリーズは Qwen3.5-4B FP8 (RedHatAI/Qwen3.5-4B-FP8-dynamic) を題材に、vLLM と SGLang を 12GB GPU で 同じ土俵に乗せた上で比較した記録です。全 4 回の予定で、今回 ① は土台づくりにあたります。

  • ① 本記事: 環境セットアップ、両エンジンの起動オプション対応表、attention backend の起動可否マトリクス
  • ② 次回: backend 単発比較 — context 長を 3 段 (512 / 2048 / 8192 tokens) に変えて tok/s を測る
  • : 並列負荷比較 — concurrency 1 / 2 / 4 / 8 でのスループットと p95 latency
  • : streaming 計測で TTFT と ITL を分離し、scheduler チューニングの効果を検証

この記事単体でも読めるように: vLLM・SGLang の基本的な違い、共通条件の設計根拠、起動時に出た問題と対処を一通りカバーしています。数字の比較は ② 以降ですが、「なぜその数字が信用できるか」の根拠がここにあります。「いきなり tok/s 表が見たい」方は ② からでも読めますが、実験条件が揃わないと数字は嘘になるので、まずは条件合わせの話を書いておきます。

なぜ二つのエンジンを比較するか

vLLM と SGLang は、表向きはどちらも「OpenAI 互換 API を喋る高速 LLM サーバ」です。が、内部実装は別物です。

ひと言で区別するなら:

  • vLLM: 「KV cache をページ単位で管理する PagedAttention」を発明した UC Berkeley 発のエンジン。エコシステムが最も広く、speculative decoding (投機的デコーディング) の実装が充実している
  • SGLang: 同じ UC Berkeley → LMSYS グループが作った後発エンジン。「RadixAttention」という、共通プレフィックスの KV cache を tree 構造で共有する仕組みが特徴
観点 vLLM SGLang
主な強み PagedAttention、speculative decoding 充実 RadixAttention による prefix 共有、scheduler 効率
既定の attention backend RTX 4070 では FlashAttention を優先選択 FlashInfer / Triton (GPU 世代で fallback)
主要パラメータ名 --gpu-memory-utilization --max-model-len --mem-fraction-static --context-length
API ポート (例) 8000 30000

オプション名すら違うため、「同じ条件」を 1 つずつ写経しても、実は VRAM 割り当てが大きくずれていた、ということが起きます。今回は 同じモデル / 同じ prompt / 同じ生成長 / 同じ context limit / 同じ concurrency に揃え、揃えられない部分は caveat として明示する方針で進めます。

共通条件

GPU             : RTX 4070 12GB (Ada Lovelace, SM 8.9)
OS              : Ubuntu (Xorg + GNOME, GPU 常時消費 ~0.71 GiB)
モデル          : RedHatAI/Qwen3.5-4B-FP8-dynamic
量子化          : FP8 (compressed-tensors, pre-quantized)
context length  : 2048 (Phase 1 の short は 512, long は 8192 まで)
concurrency     : Phase 1 = 1, Phase 2 = 1/2/4/8
temperature     : 0
warmup          : 1 回 (結果破棄)
measured runs   : 5 回、中央値を採用
API             : OpenAI 互換 `/v1/chat/completions`
thinking        : `enable_thinking=false` で無効化

Qwen3.5 は thinking モードがデフォルト有効ですが、ベンチでは出力の構造を揃えるために chat_template_kwargs.enable_thinking=false を毎回指定しています。これを忘れると vLLM 側だけ <think>...</think> 分の出力 tokens が増え、tok/s の見かけが歪みます。

Docker でバージョンを固定する

両エンジンとも nightly 系イメージを使っており、latest タグはじわじわ動きます。再現性のために、本記事で使用したバージョンは次のとおりです。

# vLLM (本記事のバージョン: v0.21.1rc1.dev243)
docker pull vllm/vllm-openai:nightly

# SGLang
docker pull lmsysorg/sglang:latest

コンテナ内で pip show vllm / pip show sglang を叩くと実バージョンが取れます。nightly を pin したい場合は起動直後にこの 2 行を控えておくと再現実行が楽になります。

両エンジンに共通する Docker 上の落とし穴を 2 つ挙げておきます。

  • --shm-size: vLLM は 1g でも動きますが、SGLang は公式 example が 32g を推奨しています。tokenizer worker や IPC で /dev/shm を使うため、絞りすぎると無言で固まります。
  • --ipc=host: SGLang は推奨。これも IPC 経由の worker 起動に効きます。

起動オプション対応表

vLLM と SGLang は「同じ役割の起動オプション」が違う名前になっていることが多く、写経で揃えると意味がズレることがあります。主要なものを並べておきます。

目的 vLLM SGLang
モデル指定 --model RedHatAI/Qwen3.5-4B-FP8-dynamic --model-path RedHatAI/Qwen3.5-4B-FP8-dynamic
最大文脈長 --max-model-len 2048 --context-length 2048
GPU メモリ割り当て --gpu-memory-utilization 0.93 --mem-fraction-static 0.85
バッチトークン上限 --max-num-batched-tokens 4096 --chunked-prefill-size 4096
attention backend --attention-backend flashinfer --attention-backend flashinfer
KV cache dtype --kv-cache-dtype fp8 --kv-cache-dtype fp8_e4m3
CUDA graph 無効化 --enforce-eager --disable-cuda-graph
メトリクス公開 デフォルト有効 (/metrics) --enable-metrics

特に注意したいのが メモリ割り当ての考え方:

  • vLLM の --gpu-memory-utilization は「GPU 全体のうちこの割合まで使ってよい」上限です。0.93 なら 12GB の 93% ≈ 11.16GB が上限。
  • SGLang の --mem-fraction-static は「サーバ起動時に静的に確保するブロックの比率」。0.85 だと、起動時に約 85% を予約し、残りを動的に使う配分です。

数値の絶対比較で「vLLM 0.93 と SGLang 0.85 は揃っている」と言えないため、今回はそれぞれ「OOM せず安定動作する最大値」を選び、起動後の peak VRAM を実測して横並びに見ています。

RTX 4070 (SM 8.9) 固有の制約

両エンジン共通で、RTX 4070 では以下の制約があります。

  • --enforce-eager または --disable-cuda-graph がほぼ必須: 4B クラスを 12GB に乗せる場合、CUDA graph のキャプチャで (batch_size × max_seq_len) のサンプルバッファを取られ、OOM の原因になります。Phase 1/2 では両エンジンとも CUDA graph 無効で計測しています。
  • FA3 (FlashAttention 3) は Hopper 系前提: SGLang の fa3 は SM 9.x (H100 など) 向けで、SM 8.9 では一部だけ動きます。後述する起動マトリクスでは「CUDA graph を切れば smoke は通った」というステータスで記録。
  • flash_attn + 長文 (max_model_len 8192) の不安定: vLLM の flash_attn backend は短い context (2048) では smoke が通るのに、8192 を要求した瞬間に起動失敗するケースがありました。詳細は次節。

Phase 0: 起動可否マトリクス (smoke)

各 backend を一度だけ起動し、/health と 32 tokens の chat completion で素通しを確認した結果です。max_model_len = 2048 で測定。

framework backend status 備考
vLLM auto smoke_ok ログでは FLASH_ATTN が自動選択
vLLM flash_attn smoke_ok RTX 4070 で起動確認
vLLM flashinfer smoke_ok 起動確認、/metrics 取得可
vLLM triton_attn smoke_ok 起動確認、初回 JIT で TTFT スパイク
SGLang auto smoke_ok ログでは flashinfer が選択
SGLang flashinfer smoke_ok 起動確認
SGLang triton smoke_ok 起動確認
SGLang fa3 smoke_ok_with_caveat default CUDA graph では失敗。--disable-cuda-graph で起動

ここまでは全 backend が「とりあえず動く」段階です。問題は次のフェーズで context を伸ばしたときに出ます。

Phase 1 起動時に出た問題: flash_attn × 長文 context の再現性

Phase 1 では同じ条件で max_model_len を 8192 まで伸ばしました。初回の計測ではすべての backend が起動して完走できたのですが、後日 nightly イメージを更新して 再現実行したとき、vllm flash_attn だけが profile_run 段階で 900 秒のタイムアウトに引っかかって起動できなくなりました。

backend 初回計測 (Phase 1) 再現実行 (image 更新後)
vLLM flash_attn ok startup_timeout (900s)
vLLM flashinfer ok ok
vLLM triton_attn ok ok
SGLang flashinfer ok ok
SGLang triton ok ok

max_model_len 8192 × enforce_eager × FP8 weight × 12GB VRAM という条件で、flash_attn だけがバージョンアップ後の profile_run を完走できなくなった、という状況でした。max_model_len を 4096 まで絞れば起動するため、KV cache pool が小さい運用なら現役で使える選択肢ですが、8192 を要求する条件では他 backend より安定性が一段落ちることは記録しておく価値があります。

次回 ② の比較データは初回計測のものを採用しています。flash_attn の数字も「初回は安定して取れた」事実に基づいて掲載します。

Phase 1 で測る対象

Phase 0 を通った backend のうち、Phase 1 の正規比較は次の 5 構成です。

config framework backend max_model_len
vllm_flash_attn vLLM flash_attn 8192
vllm_flashinfer vLLM flashinfer 8192
vllm_triton_attn vLLM triton_attn 8192
sglang_flashinfer SGLang flashinfer 8192
sglang_triton SGLang triton 8192

workload は context 長で 3 段:

workload prompt tokens max_tokens
short_ctx512 ~540 256
mid_ctx2048 ~2,135 512
long_ctx8192 ~7,395 512

prompt は同じ日本語テキストを切り出して使い、tokenizer は両エンジンで RedHatAI/Qwen3.5-4B-FP8-dynamic の付属 tokenizer に揃えています。

ベンチクライアントの作り方

ありがちな失敗が「同じプロンプトを投げたつもりが、tokenizer 違いで実際の入力長がズレている」というものです。今回は次のように対策しました。

  1. 入力 token 数を測定用クライアント側で事前計算し、ベンチ条件として記録
  2. 各リクエストの API レスポンスに含まれる usage.prompt_tokens / usage.completion_tokens を別途取得
  3. tokenizer 違いで 1〜2 tokens ずれることは許容しつつ、completion_tokens が要求値 (256 / 512) と一致していることを毎回確認

中央値だけをサマリーにするのではなく、5 回分の生データ (framework / backend / workload / prompt_tokens / completion_tokens / elapsed_ms / tok_per_s) を保持しておくと、外れ値が混ざったときに後から確認できます。今回の比較は全てこの形で取得した上で集計しています。

計測上の注意点: warmup と Triton JIT

同じ RTX 4070 12GB + Qwen3.5-4B FP8 を題材にした別シリーズ ローカルゲーミングPCでLLM - VRAM 12GB 環境で FP8 KV Cache は実用投入できるか — テキスト性能評価 でも触れたとおり、Triton 系 backend は 初回リクエストでカーネルが JIT コンパイルされます。具体的な数字は次回 ② で出しますが、最初の 1 リクエストだけ TTFT が秒オーダーで突出する現象が出ます。

対策は単純で、各 workload 計測の直前に throwaway warmup を 1 回入れて、その結果は捨てます。

1. throwaway warmup (同 prompt / 同 max_tokens、結果は破棄)
2. 計測 × 5 回 → 中央値を summary に

warmup を入れるかどうかで vllm_triton_attn の short_ctx512 平均 tok/s が 1 割以上動くため、両エンジン両 backend で例外なく入れています。

peak VRAM の取り方

両エンジンとも /metrics を Prometheus 形式で公開していますが、ラベル付けが違うため数値の意味を完全に揃えにくいです。そこで今回は nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits の値を 200ms 間隔でサンプリングし、計測ウィンドウ中の最大値を peak としました。エンジン非依存に揃えるのが目的です。

実測 peak VRAM の詳細は ③ の結果と合わせて報告します。ここでは「vLLM は 10.2〜10.7 GB、SGLang は 10.8〜11.3 GB に収まる」とだけ述べておきます。SGLang が多めなのは --mem-fraction-static 0.85 で起動時に静的に確保するためです。

結論

本記事で揃えた準備の到達点は次のとおりです。

観点 状態
共通条件 モデル / context / temperature / warmup / 計測回数を揃え済み
起動可否 vLLM 3 backend + SGLang 2 backend = 5 構成が正規比較に到達
安定性の注記 vLLM flash_attn × max_model_len 8192 は再現実行で startup_timeout が出る挙動あり
caveat SGLang fa3--disable-cuda-graph 限定で smoke のみ通過
メモリの単位合わせ --gpu-memory-utilization--mem-fraction-static は意味が違うため、peak VRAM 実測で横並びを担保

この準備編から持ち帰れる実用的な知見は以下の通りです。

  • 両エンジンは「同じ役割の起動オプション」が違う名前 で生えている。--gpu-memory-utilization--mem-fraction-static は意味も違う。写経で揃えると条件がずれる
  • RTX 4070 12GB で 4B を動かすには --enforce-eager / --disable-cuda-graph がほぼ必須。CUDA graph キャプチャで OOM するため
  • attention backend は smoke レベルではほぼ全て通るが、max_model_len 8192 のような尖った条件では vLLM の flash_attn が再現性を欠くケースがあった
  • Triton 系 backend は warmup 必須。初回 JIT で TTFT が秒オーダーで突出する
  • peak VRAM は /metrics ではなく nvidia-smi サンプリングで取る。エンジン間のラベル差を回避するため

ここまでで「同じ条件で 5 構成を比較できる土台」が整いました。準備編としてはここで一旦完結します。

次回 ② で何を測るか

次回 ② では、ここで揃えた 5 構成について context 長 512 / 2048 / 8192 の単発リクエスト性能を比較します。具体的には次の問いに答えます。

  • attention backend (flash_attn / flashinfer / triton_attn) を変えると tok/s はどれだけ動くのか
  • フレームワーク (vLLM vs SGLang) を変えるとどれだけ動くのか
  • context 長を伸ばしたとき、両エンジンの優劣はどう変化するのか

「FlashAttention は速い」「Triton は遅い」といった巷の通説が、このスケールでどこまで成り立つのかを実測で確かめます。


実験環境: RTX 4070 12GB / Ubuntu / vLLM nightly (v0.21.1rc1.dev243) / SGLang lmsysorg/sglang:latest / モデル: RedHatAI/Qwen3.5-4B-FP8-dynamic

関連記事 (同じハードウェア・同じモデルでの FP8 KV cache 検証):

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?