はじめに
このシリーズは RTX 4070 12GB で vLLM と SGLang を同条件で比較した記録です。この記事 ③ は 並列リクエスト編です。単体でも読めますが、① 準備編 と ② 単発比較 で「なぜこの比較が公平か」「単発ではどんな差があるか」を扱っているので、通しで読むと数字の文脈がつかみやすくなります。
シリーズ ② で concurrency 1 の単発リクエストを比較した結果、SGLang が vLLM より +18〜28% 速いことがわかりました。ローカル LLM サーバを「単発リクエストでしか使わない」シーンは少なく、実用では複数リクエストを並列に投げます。
今回 ③ では concurrency を 1 / 2 / 4 / 8 と上げたときの挙動を見ます。RTX 4070 12GB の上で RedHatAI/Qwen3.5-4B-FP8-dynamic を回し、aggregate tok/s、per-request tok/s、p95 latency、peak VRAM を測りました。
結果だけ先取りすると、concurrency 8 でフレームワーク間の差はこうなります。
| backend | aggregate tok/s | p95 latency |
|---|---|---|
| vLLM flashinfer | 278.45 | 7,355 ms |
| SGLang flashinfer | 468.01 | 4,375 ms |
スループットで 1.68x、p95 で 1.68x 短縮。① でも ② でも 1.3x 前後だった差が、並列負荷で大きく広がります。
計測設計
5 構成 × concurrency 4 段 = 20 ケース。各ケースで warmup 1 バッチ + 計測 5 バッチ。バッチごとに concurrency 分のリクエストを同時投入し、最後のリクエストが返るまでの elapsed と aggregate 出力 token 数から aggregate tok/s を求めます。
ケース定義は次のとおりです。
| case_id | concurrency | per-request prompt | per-request max_tokens | active token budget |
|---|---|---|---|---|
| c1_limit_8k | 1 | ~7,395 | 512 | ~7,907 |
| c2_limit_8k_each | 2 | ~7,395 × 2 | 512 | ~15,814 |
| c4_4k_each | 4 | ~4,323 × 4 | 512 | ~19,340 |
| c8_2k_each | 8 | ~2,189 × 8 | 256 | ~19,560 |
concurrency が上がるごとに per-request prompt を短くしているのは、active token budget (同時にエンジン内に存在する KV を持つ token 数の合計) を 1〜2 万 tokens 帯に収めて、フェアな並列負荷を作るためです。c8 だけ max_tokens=256 にしているのは、c8 × 512 tokens 生成だと KV pool が 8K context 上限を超える可能性があるためで、その代わり concurrency を 8 まで上げて pipeline 飽和を見ています。
結果: aggregate tok/s
5 構成 × 4 concurrency の matrix です。
| framework | backend | c1 | c2 | c4 | c8 |
|---|---|---|---|---|---|
| vLLM | flash_attn | 52.57 | 90.01 | 170.00 | 277.36 |
| vLLM | flashinfer | 52.73 | 90.25 | 170.54 | 278.45 |
| vLLM | triton_attn | 52.44 | 89.24 | 169.30 | 276.46 |
| SGLang | flashinfer | 67.43 | 127.63 | 253.59 | 468.01 |
| SGLang | triton | 66.74 | 126.31 | 251.23 | 466.83 |
vLLM 内の backend 差は引き続き誤差
② で見たのと同じで、vLLM 側の 3 backend は全 concurrency で 2 tok/s 以内に収まります。--attention-backend を変えても並列負荷でもほぼ動きません。
SGLang 内も backend 差は小さい
SGLang の flashinfer と triton の差も最大 1.3 tok/s (c2 で 127.63 vs 126.31)。フレームワーク内では backend の選択肢にあまり意味がなく、フレームワーク選定が支配的という ② の結論はそのまま強化されます。
vLLM vs SGLang: 差が広がる
両エンジンの最速 backend を取って並べると次のとおりです。
| concurrency | vLLM 最速 | SGLang 最速 | SGLang / vLLM |
|---|---|---|---|
| 1 | 52.73 | 67.43 | 1.28x |
| 2 | 90.25 | 127.63 | 1.41x |
| 4 | 170.54 | 253.59 | 1.49x |
| 8 | 278.45 | 468.01 | 1.68x |
concurrency が上がるほど SGLang の優位が拡大します。c1 で 1.28x だった差が、c8 では 1.68x。スループット指向で複数リクエストを捌くサーバなら、フレームワーク選定だけで 1.6 倍以上の差が出てくる、という結果です。
スケーリングカーブ
各エンジンを 自分の c1 で正規化 した倍率と、vLLM c1 (52.73 tok/s) を共通基準 1.00x に置いて SGLang を重ねた倍率を、一つの表にまとめます。
| concurrency | vLLM flashinfer (vLLM c1 基準) | SGLang flashinfer (SGLang c1 基準) | SGLang flashinfer (vLLM c1 基準) |
|---|---|---|---|
| 1 | 1.00 | 1.00 | 1.28 |
| 2 | 1.71 | 1.89 | 2.42 |
| 4 | 3.23 | 3.76 | 4.81 |
| 8 | 5.28 | 6.94 | 8.88 |
まず自分の c1 基準で見ると、理想的には c8 でスループット 8x ですが両者とも下回ります。とはいえ SGLang は c8 で 6.94x、vLLM は 5.28x。SGLang のスケーリング効率は vLLM の 1.31 倍です。
そのうえで vLLM c1 を共通基準にして並べ直すと、SGLang c8 は 8.88x に到達。これは理論最大の 8x を 超えています。仕組みとしては、c1 時点でのフレームワーク差 (1.28x) と SGLang 自身のスケーリング (6.94x) が乗算で効くため。「リクエスト 1 本あたりの単速」と「並列で詰める効率」の両方で勝てば、理論限界を上抜けする という現象が起きます。
逆方向に読むと、vLLM で c8 まで並列度を上げても、SGLang の c4 (4.81x) に届かない。「vLLM を 8 並列で回すよりも、SGLang を 4 並列で回す方が速い」というのが今回の構成での結論です。
per-request の tok/s も並べると、SGLang のスケジューラ品質の差がさらにわかりやすくなります。
| concurrency | vLLM flashinfer per-req tok/s | SGLang flashinfer per-req tok/s |
|---|---|---|
| 1 | 52.73 | 67.44 |
| 2 | 45.19 | 63.82 |
| 4 | 42.70 | 63.41 |
| 8 | 34.96 | 58.55 |
vLLM は c1 → c8 で per-request が 34% 落ちる (52.73 → 34.96)。SGLang は同じ条件で 13% 落ちるだけ (67.44 → 58.55)。「並列度を上げても 1 リクエストあたりの体感速度が落ちにくい」のが SGLang の強さです。
p95 リクエスト遅延
スループットだけだと運用判断には足りないので、p95 リクエスト遅延も並べます。各リクエストが投入から完了までに要した時間の 95 パーセンタイルです。
| concurrency | vLLM flashinfer (ms) | SGLang flashinfer (ms) | SGLang / vLLM |
|---|---|---|---|
| 1 | 9,710 | 7,597 | 0.78 |
| 2 | 11,346 | 8,028 | 0.71 |
| 4 | 12,010 | 8,077 | 0.67 |
| 8 | 7,355 | 4,375 | 0.59 |
c8 で SGLang は 4.4 秒、vLLM は 7.4 秒。max_tokens=256 生成での p95 で 3 秒差。同じハードウェア、同じモデル、同じプロンプト、同じ並列度で、エンジンを変えるだけでユーザー体感の応答時間がここまで動きます。
c4 で vLLM が 12,010 ms と最も長くなっているのは、max_tokens=512 で 4 リクエスト並列が KV pool と batching scheduler の両方を最も埋める領域だからと考えられます。SGLang は同じ条件で 8,077 ms。
peak VRAM
並列度を変えても peak VRAM はそれほど動かないというのが今回の観察です。各 backend の c1〜c8 中の最大値を載せます。
| framework | backend | peak VRAM (MB) | 12GB に対する余白 |
|---|---|---|---|
| vLLM | flash_attn | 10,255 | ~1,925 MB |
| vLLM | flashinfer | 10,655 | ~1,525 MB |
| vLLM | triton_attn | 10,255 | ~1,925 MB |
| SGLang | flashinfer | 11,285 | ~895 MB |
| SGLang | triton | 10,821 | ~1,359 MB |
SGLang flashinfer が 11.3 GB と最も VRAM を使っています。スループットが 1.68x になっている代償と考えれば妥当で、「性能のために VRAM を積極的に使う」設計判断が透けて見えます。逆に言うと、メモリ余裕が欲しい場合は SGLang の --mem-fraction-static を 0.85 から 0.80 / 0.75 に下げる選択肢があります (起動オプションだけで触れる)。あるいは KV cache を BF16 → FP8 に切り替えれば KV pool を約 2x に伸ばせるため、長文要件がある場合はその選択肢が現実的です (別シリーズ FP8 KV Cache は実用投入できるか / 長文運用の限界 で同じハードウェアで検証済み)。
vLLM 側は c1〜c8 全体でほぼ peak が動きません。これは --gpu-memory-utilization 0.93 の制約下で起動時にほぼ上限まで埋まっており、リクエスト並列度を上げても新規確保はほぼないからです。
成功率
全 20 ケースで OOM ゼロ、リクエスト失敗ゼロ。
| framework | backend | total | success | fail |
|---|---|---|---|---|
| vLLM | flash_attn | 75 | 75 | 0 |
| vLLM | flashinfer | 75 | 75 | 0 |
| vLLM | triton_attn | 75 | 75 | 0 |
| SGLang | flashinfer | 75 | 75 | 0 |
| SGLang | triton | 75 | 75 | 0 |
ここはどちらも合格です。Phase 1 の長文 prefill で発生した「prompt + max_tokens 合計が context window を超える」現象を回避するため、Phase 2 では case_id ごとに prompt 長と max_tokens を調整してあります。
なぜ並列負荷で SGLang が伸びるのか
短く言える範囲だけ整理しておきます。
-
継続バッチング (continuous batching) と prefill chunking の差
両エンジンとも実装しているが、SGLang は radix cache を起点としたスケジューリングで「次の step で何を実行するか」の判断が軽量で、GPU の forward 占有率が高い時間が伸びる傾向にあります。 -
prefix sharing の効果は今回は影響が小さい
今回のプロンプトは異なるテキストを切り出して使っているため、SGLang の代表機能である radix attention の prefix 共有メリットは影響が小さいと考えられます。にもかかわらず差が出ているということは、scheduling overhead と batching 戦略だけでもこれだけ違う、ということです。 -
CUDA graph 無効下での Python オーバーヘッド
両エンジンとも--enforce-eager/--disable-cuda-graph指定です。CUDA graph が使えない環境では、step ごとの Python/CPU 側オーバーヘッドが相対的に大きくなり、ここでの実装差がそのまま tok/s に乗ります。 -
per-request tok/s が落ちにくい設計
per-request tok/s の劣化幅 (34% vs 13%) は、batch size が増えたときの KV-cache アクセスパターンとスケジューラの効率に直結します。SGLang の per-step latency 安定性が、p95 latency の差にも繋がっています。
これらの定量的内訳は別記事で追跡する価値があると思いますが、今回は 「現状の構成では SGLang を選ぶと並列負荷で大きく差がつく」 という観測事実だけ確定として残します。
結論
並列負荷比較で言える 3 点をまとめます。
- concurrency が上がるほど SGLang の優位が拡大。c1 で 1.28x だった差が、c8 では 1.68x に。スループット指向で複数リクエストを捌くなら、フレームワーク選定だけで 1.6 倍以上の差が出る
- per-request tok/s の劣化幅は vLLM −34% / SGLang −13%。SGLang は並列度を上げても 1 リクエストあたりの体感速度が落ちにくい
- p95 latency (c8) は SGLang 4.4s / vLLM 7.4s。3 秒差はそのままユーザー体感の差になる
並列負荷比較としての整理は以下のとおりです。
| 観点 | 結果 |
|---|---|
| vLLM 内 attention backend 差 | 計測誤差 (2 tok/s 以内) |
| SGLang 内 attention backend 差 | 計測誤差 (1.3 tok/s 以内) |
| vLLM vs SGLang (c1) | SGLang 1.28x |
| vLLM vs SGLang (c8) | SGLang 1.68x |
| スケーリング (c1→c8) | vLLM 5.28x / SGLang 6.94x |
| per-request 劣化 (c1→c8) | vLLM −34% / SGLang −13% |
| p95 latency (c8) | vLLM 7,355ms / SGLang 4,375ms |
| peak VRAM (12GB) | vLLM 10.3〜10.7 GB / SGLang 10.8〜11.3 GB |
| 全 75 ケース | 失敗ゼロ |
スループットと p95 latency だけで判断するなら、暫定的な棲み分けは次のとおりです。
| 用途 | 推奨 | 理由 |
|---|---|---|
| 自宅・低 concurrency チャット | vLLM でも SGLang でも OK | c1 差は +28%。vLLM なら speculative decoding (+20〜30%) も狙える |
| 複数ユーザー・並列前提サーバ | SGLang | c8 で 1.68x、p95 latency 0.59 倍 |
| RAG / 長文要約 | SGLang | context が長いほど差が広がる (② で +28%) |
| Speculative decoding 重視 | vLLM | MTP / D-Flash のエコシステムが充実。4B FP8 + D-Flash で 2.6x 確認済み |
この棲み分けは 並列スループットと p95 latency という指標で見た場合の判断であり、本記事 ③ としてはこの結論で完結します。
まだ残る疑問
ただし、ここで使った指標には抜けがあります。実運用での「体感速度」を決める要素として、次の 2 つが未測定のままです。
- TTFT (Time to First Token): ストリーミング応答の最初のトークンが届くまでの時間。チャット UI で「入力して送信してから最初の文字が見えるまで」の体感を直接決める
- ITL (Inter-Token Latency): トークンとトークンの間隔。「タイピングしているように見えるか、引っかかって見えるか」を決める
aggregate tok/s や p95 latency は「完了するまでの時間」で、TTFT と ITL を内包しています。両者を分離しないと、どちらのフェーズ (prefill か decode か) が遅延の原因か が分かりません。
加えて、scheduler 系パラメータ (--max-running-requests、--chunked-prefill-size など) を 1 軸ずつ振ると、改善できるものと逆効果になるものに分かれることが想定されます。
これらは次回 ④ で別途調査しました。本記事 ③ の並列負荷比較としてはここで完結します。
実験環境: RTX 4070 12GB / Ubuntu / vLLM nightly (v0.21.1rc1.dev243) / SGLang lmsysorg/sglang:latest / モデル: RedHatAI/Qwen3.5-4B-FP8-dynamic
関連記事 (同じハードウェア・同じモデルでの FP8 KV cache 検証):
- ローカルゲーミングPCでLLM - VRAM 12GB 環境で FP8 KV Cache は実用投入できるか — テキスト性能評価
- ローカルゲーミングPCでLLM - 長文運用の限界。FP8 KV Cache でより長い長文を LLM で処理できるようになるか
シリーズ過去記事: