はじめに
このシリーズは RTX 4070 12GB で LLM サービングエンジン vLLM と SGLang を同条件で比較した記録です。この記事 ④ は streaming 計測・scheduler チューニング編で、シリーズの最終回です。単体でも読めますが、① 準備編 と ② 単発比較 と ③ 並列負荷編を通読すると「なぜこの実験設計か」「スループットの数字がどこから来ているか」がわかります。
シリーズ ③ で「並列 concurrency 8 のスループットが SGLang 1.68x」という数字を出しました。ただ aggregate tok/s は「全リクエストが終わってから計算する合計値」で、ユーザーが感じる体感速度とは別の話です。
今回 ④ は streaming 計測を導入し、TTFT (Time to First Token: 最初の文字が届くまでの時間) と ITL (Inter-Token Latency: トークンとトークンの間隔) を分離して測ります。この 2 つを分けることで、"速い/遅い" の原因がどこにあるかが初めて見えてきます。
streaming を入れて分かったのは、スループット差 (1.68x) よりも TTFT の差が圧倒的に大きいということです。並列 c8 の同質ワークロードで、SGLang の p95 TTFT は vLLM の 8 分の 1 以下でした。
測定設計
streaming endpoint (stream=True) を使い、最初の content chunk が届いた時刻を TTFT、以降の chunk 間隔を ITL として記録します。
4 種類のワークロードで分解します。case_id の接頭辞は内容を表しており、hom = homogeneous (同質、同じ長さのリクエストを c8 並列)、mix = 長短混在、prefill_heavy = 長 prompt 中心、decode_heavy = 短 prompt × 長出力、という意味で付けています。
| case_id | 内容 | 目的 |
|---|---|---|
hom_c8_2k_each |
同質 c8、prompt ~2,189 tokens × 8 | Phase 2 c8 を TTFT / ITL に分解 |
mix_short6_long2_c8 |
short (~512 tokens) × 6 + long (~2,189 tokens) × 2、合計 c8 | 長い prefill が短いリクエストを詰まらせるかを見る |
prefill_heavy_c4_4k_each |
prompt ~4,323 tokens × 4、c4 | 長い prefill のみの負荷 |
decode_heavy_c8_short |
prompt ~512 tokens × 8、max_tokens 多め | decode ループが長い場合の ITL 安定性 |
なお本記事の表で出てくる agg tok/s は aggregate tok/s (バッチ全体の合計スループット、③ で扱った指標と同じ)、p95 req (ms) はリクエスト投入から完了までの 95 パーセンタイル時間です。
比較するのは 2 baseline + 4 tuning = 6 構成です。
| config_id | framework | 変更点 |
|---|---|---|
vllm_flashinfer_base |
vLLM | デフォルト |
vllm_flashinfer_seq8 |
vLLM | --max-num-seqs 8 |
vllm_flashinfer_batch8192 |
vLLM |
--max-num-batched-tokens 8192 (デフォルト 4096) |
sglang_flashinfer_base |
SGLang | デフォルト |
sglang_flashinfer_run8 |
SGLang | --max-running-requests 8 |
sglang_flashinfer_chunk4096 |
SGLang | --chunked-prefill-size 4096 |
Baseline 結果: TTFT の差は 8 倍
同質 c8 (hom_c8_2k_each)
同じ長さのリクエストを 8 本同時に投入し、先頭トークンまでの待ち時間を比較します。
| config | agg tok/s | p95 TTFT (ms) | p95 ITL (ms) | p95 req (ms) |
|---|---|---|---|---|
| vLLM base | 277.66 | 2,295 | 20.40 | 7,363 |
| SGLang base | 465.35 | 277 | 17.04 | 4,512 |
tok/s は 1.68x 差でしたが、TTFT は 8.3x 差。SGLang は 277ms で最初のトークンを返すのに対し、vLLM は 2,295ms かかっています。
より直感的に見るために、40 リクエスト分の TTFT 分布を並べます。
| 統計 | vLLM | SGLang |
|---|---|---|
| min | 520 ms | 38 ms |
| p50 (中央値) | 2,036 ms | 157 ms |
| p95 | 2,295 ms | 278 ms |
| max | 2,302 ms | 278 ms |
vLLM の TTFT は 520ms〜2,302ms という広い分布で、同じバッチ内でも「早く処理されたリクエスト」と「後回しにされたリクエスト」が混在しています。SGLang は 38ms〜278ms で収まっており、バッチ内の不公平が少ない。
ITL は両者ほぼ同等です。vLLM 20.4ms、SGLang 17.0ms と約 17% 差で、decode ループの 1 step のコストはそこまで違いません。つまり 速度差の根本は「最初のトークンを出すまで」に集中している。
ワークロード別比較
4 種類の workload で 2 エンジンを並べます。
| workload | vLLM agg tok/s | SGLang agg tok/s | vLLM p95 TTFT | SGLang p95 TTFT | TTFT 倍率 |
|---|---|---|---|---|---|
| hom_c8_2k_each | 277.66 | 465.35 | 2,295 ms | 277 ms | 8.3x |
| mix_short6_long2_c8 | 344.44 | 465.24 | 1,021 ms | 333 ms | 3.1x |
| prefill_heavy_c4_4k_each | 109.12 | 240.47 | 2,292 ms | 249 ms | 9.2x |
| decode_heavy_c8_short | 401.04 | 478.62 | 637 ms | 249 ms | 2.6x |
tok/s 比では最大 2.2x (prefill_heavy) でしたが、TTFT は最大 9.2x 差。どのワークロードでも、「最初のトークンまでの待ち」が最も大きな差の源泉です。
重要な観察: 短いリクエストが長いリクエストに巻き込まれるか
mix_short6_long2_c8 は c8 のうち 6 本が short (prompt ~512 tokens)、2 本が long (prompt ~2,189 tokens) という混在ワークロードです。「長い prefill の 2 本が、短い 6 本の TTFT を悪化させるか」を見ます。
| エンジン | リクエスト種別 | p50 TTFT | p95 TTFT |
|---|---|---|---|
| vLLM | short (~512 tokens) | 794 ms | 813 ms |
| vLLM | long (~2,189 tokens) | 1,011 ms | 1,025 ms |
| SGLang | short (~512 tokens) | 212 ms | 333 ms |
| SGLang | long (~2,189 tokens) | 212 ms | 332 ms |
vLLM では short リクエストが 794ms 待たされています。long リクエスト (1,011ms) とほぼ同じ待ち時間。short の prefill は長い long の prefill が終わるまでバッチに入れてもらえず、Head-of-Line Blocking が起きています。
SGLang は short も long も同じ 212ms。prefill 長に関係なく、すべてのリクエストがほぼ同じ TTFT で先頭トークンを受け取れています。これは SGLang の scheduler が prefill をインターリーブして処理していることを示しています。
この差は、RAG サービスのような「ドキュメント長がリクエストごとにまちまち」の運用で直撃します。vLLM では長い入力を持つリクエストが混ざるたびに、短い入力のユーザーも同じだけ待たされる。SGLang はそれを分離できています。
ITL の安定性
decode_heavy_c8_short は短い prompt × 長い出力という、decode ループが長く続くワークロードです。この条件での ITL (トークン間隔) を見ます。
| config | agg tok/s | p95 TTFT | p95 ITL | 備考 |
|---|---|---|---|---|
| vLLM base | 401.04 | 637 ms | 19.81 ms | |
| SGLang base | 478.62 | 249 ms | 16.95 ms |
ITL は vLLM 19.8ms、SGLang 17.0ms で、decode 中の token 間隔は 2ms 差。どちらも安定して token を出し続けており、decode ループでの劣化は見られませんでした。
「vLLM が遅い」のは decode が遅いのではなく、prefill が詰まるから。この事実が Phase 3 の最大の収穫です。
Scheduler Tuning 結果
以上の baseline を踏まえた上で、scheduler 系パラメータを 1 軸ずつ変えます。
vLLM — チューニングが効かなかった
| config | hom_c8 tok/s | p95 TTFT | VRAM |
|---|---|---|---|
| base (max_num_batched_tokens=4096) | 277.66 | 2,295 ms | 10,655 MB |
| seq8 (max_num_seqs=8) | 278.31 | 2,283 ms | 10,615 MB |
| batch8192 (max_num_batched_tokens=8192) | 277.67 | 2,333 ms | 11,217 MB |
--max-num-seqs 8 はほぼ無変化。--max-num-batched-tokens 8192 は VRAM が +562MB 増えて TTFT が わずかに悪化。性能を改善するどころか逆方向に動きました。
バッチトークン上限を 2x にしても改善しない理由は、RTX 4070 12GB スケールでの Qwen3.5-4B FP8 の場合、スループットは GPU の forward 計算能力 (GEMM) で決まっており、scheduler がバッチに詰め込む量の限界よりも先にコンピュートが飽和しているからと考えられます。VRAM が増えるだけで GPU の演算リソースは変わらない。
SGLang — run8 は有効、chunk4096 は逆効果
| config | hom_c8 tok/s | p95 TTFT | VRAM |
|---|---|---|---|
| base | 465.35 | 277 ms | 11,285 MB |
| run8 (max_running_requests=8) | 464.29 | 164 ms | 11,285 MB |
| chunk4096 (chunked_prefill_size=4096) | 450.51 | 397 ms (悪化) | 11,629 MB |
--max-running-requests 8 (run8): TTFT が 277ms → 164ms と 40% 改善。スループットはほぼ変わらず (465 → 464)。VRAM も増えない。デフォルトより多くのリクエストを同時に実行可能にすることで、scheduler がリクエストを早く取り出してきて prefill をスタートさせる効果があります。
--chunked-prefill-size 4096 (chunk4096): TTFT が 277ms → 397ms と 43% 悪化。throughput も 465 → 450 と落ち、VRAM は +344MB 増加。三指標すべてで baseline より悪くなりました。
chunked prefill は「長い prefill を小さなチャンクに分割して decode と交互に処理する」手法で、TTFT 改善を目的としています。理論的には正しい設計ですが、今回の条件 (RTX 4070 12GB、concurrency 8、max_model_len 8192) では チャンク化のオーバーヘッドが改善効果を上回りました。
推測される理由は次のとおりです。
- Qwen3.5-4B FP8 は GDN (Gated Delta Net) ハイブリッド構造のため、attention とは別の SSM 系の計算経路が mixed に入る
- prefill を 4096 token チャンクに刻むと、SSM 系の state transfer や context の flush がチャンク境界ごとに発生する可能性がある
- decode も並行させるため VRAM が細かく分散し、HBM アクセスパターンが悪化する
「chunked prefill を入れれば TTFT が改善する」は一般的に正しいですが、モデルアーキテクチャとハードウェアスケールの組み合わせ次第で逆効果になるケースがあるという実証事例になりました。
tuning 適用後の最良構成
| config | hom_c8 tok/s | p95 TTFT | p95 ITL |
|---|---|---|---|
| vLLM base (best) | 277.66 | 2,295 ms | 20.40 ms |
| SGLang base | 465.35 | 277 ms | 17.04 ms |
| SGLang run8 | 464.29 | 164 ms | 17.04 ms |
tuning で最も改善できたのは SGLang に --max-running-requests 8 を追加した構成。VRAM ペナルティなしで TTFT 40% 改善。vLLM 比では TTFT が 14x 短縮になります。
結論
streaming 計測と scheduler チューニングで言える 4 点をまとめます。
- 本当の差は TTFT に集中している。aggregate tok/s では 1.68x 差だったものが、TTFT で見ると hom_c8 で 8.3x、prefill_heavy で 9.2x。「速い/遅い」の体感の正体はここにある
- ITL は両者ほぼ互角 (vLLM ~20ms / SGLang ~17ms、約 17% 差)。つまり vLLM が遅いのは decode ループではなく prefill が詰まるから
- vLLM では Head-of-Line Blocking が起きる。混在ワークロード (short 6 + long 2) で short リクエストが 794ms 待たされる。SGLang は long の影響を受けず 212ms。RAG のような「リクエスト長がまちまち」の運用で直撃する差
-
scheduler tuning は SGLang
--max-running-requests 8が当たり。TTFT 40% 改善 (277 → 164ms)、スループット維持、VRAM ペナルティなし。一方 vLLM 側のチューニングは効かず、SGLang--chunked-prefill-size 4096は逆効果
本記事の整理:
| 観点 | 結論 |
|---|---|
| TTFT の差 | SGLang が圧倒的に短い (hom_c8 で 8.3x、prefill_heavy で 9.2x) |
| ITL の差 | vLLM ~20ms、SGLang ~17ms。約 17% 差で decode 自体は近い |
| Head-of-Line Blocking | vLLM は long prefill が short リクエストを 794ms 待たせる。SGLang は影響なし |
| vLLM scheduler tuning |
max_num_seqs も max_num_batched_tokens も無効。VRAM が増えるだけ |
| SGLang run8 | TTFT 40% 改善、スループット維持、VRAM 変化なし。有効 |
| SGLang chunk4096 | TTFT 43% 悪化、スループット低下、VRAM +344MB。逆効果 |
シリーズ全体の結論
全 4 回を通じて、RTX 4070 12GB + Qwen3.5-4B FP8 という条件で見えた判断軸は次のとおりです。
vLLM を選ぶべきケース
- speculative decoding (MTP / D-Flash / Eagle) を使いたい: vLLM のエコシステムが深く、関連実験で FP8 + D-Flash 2.6x、FP8 + MTP-n1 1.19x を確認済み
- シングルリクエストまたは低 concurrency の用途: TTFT 差が小さい領域 (c1 では 277ms 程度) では、speculative decoding の上乗せで最速構成を作れる
- HuggingFace モデルの幅広い対応を優先する: 量子化フォーマット (AWQ / GPTQ / FP8) やモデルアーキテクチャの対応が現状最も広い
SGLang を選ぶべきケース
- 複数ユーザー・並列リクエストが前提のサーバ: c8 で tok/s 1.68x、TTFT 8x、p95 latency 0.59 倍
- RAG / 長文処理: prefill_heavy で 2.2x tok/s 差、リクエスト長が混在しても TTFT が崩れない
- レスポンスの体感速度を最優先する: TTFT の差は直接ユーザー体感に繋がる
-
tuning は
--max-running-requestsから始める: chunked prefill はモデル依存で逆効果になり得る
全 4 回を通した本質的な学び
-
「FlashAttention/FlashInfer/Triton のどれが速いか」という問いは、このスケールではほぼ無意味だった。backend 差は 1% 以下に収まり、
--attention-backendを最初に詰めても結果はほぼ動かない - 支配的なのは framework そのもの。さらにその中身を分解すると、決定的に効いているのは prefill scheduler の品質 であり、attention カーネルでも KV cache フォーマットでもない
- 同じ aggregate tok/s でも、TTFT は 8 倍違うことがある。「平均速度」だけで運用エンジンを選ぶと、ユーザー体感を大きく損なう可能性がある
- チューニングは効かない方向にも倒れる。chunked prefill は理論的に正しい設計でも、モデルアーキテクチャ (GDN) とハードウェアスケール (12GB) の組み合わせ次第で逆効果になる
ここまでがシリーズ「vLLM × SGLang on RTX 4070 12GB」全 4 回の結論です。同じハードウェア・同じモデル・同じプロンプトで揃えても、エンジンを変えるだけで体感速度が 8 倍動く — そのことが定量的に追跡できた、というのが本シリーズの収穫でした。
実験環境: 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 で処理できるようになるか
シリーズ全4回:
- ① vLLM × SGLang でLLMベンチマーク計測 — 12GB GPU で同じモデルを公平に比較する準備編
- ② LLM 推論の速度はどこで決まるか — vLLM/SGLang × attention backend の実測検証
- ③ ローカルGPU(RTX 4070 12GB)で並列 LLM 推論はどこまでスケールするか — vLLM × SGLang での並列処理実測検証
- ④ 本記事: ローカルLLM運用時の並列リクエストで「応答までの待ち時間」を短くできるか — 設定チューニング次第で最大速度 8 倍差を実現!