3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルLLM運用時の並列リクエストで「応答までの待ち時間」を短くできるか — 設定チューニング次第で最大速度 8 倍差を実現!

3
Posted at

はじめに

このシリーズは 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 点をまとめます。

  1. 本当の差は TTFT に集中している。aggregate tok/s では 1.68x 差だったものが、TTFT で見ると hom_c8 で 8.3x、prefill_heavy で 9.2x。「速い/遅い」の体感の正体はここにある
  2. ITL は両者ほぼ互角 (vLLM ~20ms / SGLang ~17ms、約 17% 差)。つまり vLLM が遅いのは decode ループではなく prefill が詰まるから
  3. vLLM では Head-of-Line Blocking が起きる。混在ワークロード (short 6 + long 2) で short リクエストが 794ms 待たされる。SGLang は long の影響を受けず 212ms。RAG のような「リクエスト長がまちまち」の運用で直撃する差
  4. 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_seqsmax_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 検証):

シリーズ全4回:

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?