はじめに
このシリーズは RTX 4070 12GB (consumer GPU) の上で、LLM サービングエンジン vLLM と SGLang を同じモデル・同じ条件で比較した記録です。① 準備編 では環境構築と起動条件の揃え方を整理しました。この記事 ② は単体でも読めますが、① を先に読むと「なぜこの比較が公平か」の根拠がわかります。
今回 ② の本題は、attention backend を変えると単発リクエストの decode 速度がどれだけ変わるかです。
巷の比較記事では「FlashAttention は速い」「FlashInfer は速い」「Triton は遅い」のような断片的な評価をよく見ますが、実際に同じモデル・同じプロンプトで全部回すと印象が変わります。今回測った範囲では、vLLM 内の backend 差は計測誤差レベル、SGLang 内の backend 差も計測誤差レベル、しかし vLLM と SGLang の間には明確な差が出ました。
検証対象は次の 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 |
モデルは RedHatAI/Qwen3.5-4B-FP8-dynamic (compressed-tensors FP8) で固定。concurrency 1、temperature 0、warmup 1 回、計測 5 回の中央値です。
attention backend の役割
念のため整理します。Transformer の attention 計算は「QK 内積 → softmax → V との重み付き和」ですが、これを愚直に書くと中間テンソル (seq_len, seq_len) を materialize して GPU メモリを食い潰します。実装上は次の最適化が入ります。
- FlashAttention: tile を SRAM に載せて on-line softmax を回す。HBM I/O を大幅に減らす
- FlashInfer: PagedAttention 構造とブロック疎な KV cache に最適化。長い context や複雑な batching に強い
- Triton attention: Triton DSL で書かれた汎用カーネル。新しい GPU や experimental shape に対応しやすい
- FA3 (FlashAttention 3): Hopper (H100) 向けに非同期コピーと WGMMA を活用した版
vLLM では --attention-backend で指定でき、未指定なら GPU 世代に応じて自動選択されます。SGLang も同様に --attention-backend を持ちます。同じ「flashinfer」という名前でも、フレームワーク間で呼ばれる実装は微妙に違うため、横並び比較するときは framework × backend のセル単位で考えるのが安全です。
ワークロード
短中長の 3 段で context 長を振ります。プロンプトは日本語 (長文ブログのドラフト) を切り出して使用。
| workload | prompt tokens | max_tokens | 想定シナリオ |
|---|---|---|---|
| short_ctx512 | 538 | 256 | チャット 1 ターン |
| mid_ctx2048 | 2,135 | 512 | 中量 RAG / 要約 |
| long_ctx8192 | 7,395 | 512 | 長文 RAG / 議事録要約 |
prompt token 数は両エンジンの usage.prompt_tokens から取得した実測値です。long_ctx8192 は両エンジンとも max_model_len = 8192 ぎりぎりまで詰めるシナリオ。
計測結果: 全 5 構成 × 3 workload
中央値 (tok/s) を並べます。
| framework | backend | short_ctx512 | mid_ctx2048 | long_ctx8192 |
|---|---|---|---|---|
| vLLM | flash_attn | 59.47 | 58.11 | 52.51 |
| vLLM | flashinfer | 59.51 | 58.13 | 52.64 |
| vLLM | triton_attn | 59.62 | 58.26 | 52.34 |
| SGLang | flashinfer | 70.27 | 70.47 | 67.47 |
| SGLang | triton | 70.52 | 70.60 | 66.92 |
vLLM 内: backend 差は誤差レベル
vLLM 側の 3 backend を横で見ると、全 workload で 0.15〜0.30 tok/s 差しかありません。short_ctx512 でいうと 59.47 〜 59.62、長文 long_ctx8192 でも 52.34 〜 52.64。
- short_ctx512: 0.25% の差
- mid_ctx2048: 0.26% の差
- long_ctx8192: 0.57% の差
5 回計測の中央値で 0.6% 差は、warmup 後でも残るマイクロ揺らぎの範囲です。vLLM の中では --attention-backend を選び直しても decode 速度は実質的に動かない、というのが今回の結論です。
これは「FlashAttention や FlashInfer が無意味」という意味ではありません。Qwen3.5-4B クラス + concurrency 1 + FP8 weight という条件では、attention カーネルの差よりも モデル forward 全体 (FP8 GEMM + RMSNorm + GDN ハイブリッド attention など) のコストが支配的で、attention 単体の最適化差が見えなくなっている、という解釈が自然です。GDN (Gated Delta Net) は Qwen3.5 固有のハイブリッド attention 構造で、通常の Transformer attention に加え SSM (State Space Model) 的な計算が混在しており、全体コストに占める attention の比率が下がっています。
SGLang 内: こちらも backend 差は小さい
SGLang 側も flashinfer と triton の差は最大 0.55 tok/s (long で約 0.8%)。vLLM 同様、SGLang 内でも --attention-backend の選択は decode 速度にほぼ影響しません。
vLLM vs SGLang: 一段違う
ところがフレームワークをまたぐと差がはっきり出ます。同じ workload で「vLLM 側の最速」と「SGLang 側の最速」を並べると、
| workload | vLLM 最速 | SGLang 最速 | SGLang / vLLM |
|---|---|---|---|
| short_ctx512 | 59.62 | 70.52 | 1.18x |
| mid_ctx2048 | 58.26 | 70.60 | 1.21x |
| long_ctx8192 | 52.64 | 67.47 | 1.28x |
short_ctx512 で +18%、long_ctx8192 で +28%。context が長くなるほど SGLang の優位が広がるのがポイントです。
これは attention 単体ではなく、prefill の chunking 戦略と scheduler の実装、KV cache 管理を含めたパイプライン全体の差と読むのが妥当です。長い prompt を処理する prefill 段階で、SGLang の方が GPU を遊ばせていない、という挙動が tok/s に反映されています。
なお、SGLang の代表的な機能である RadixAttention は 複数リクエストで共通の prefix を持つ KV cache を tree 構造で共有するものですが、今回は異なる日本語テキストを切り出した別々のプロンプトを投げているため、prefix 共有の恩恵は影響が小さいと考えられます。それでも差が出ている以上、原因は prefix 共有以外の scheduler / prefill 実装の差にあると見るのが安全です。
なぜ context が伸びると SGLang の差が広がるのか
詳細を全部追うとそれだけで 1 本書けるので、今回はざっくり言える範囲だけ。
decode 速度 (tok/s) は次の 2 つで決まります。
- prefill 時間: 最初の forward で全 prompt token の KV を作る
- decode ループ: 1 トークンずつ生成する。ここが「素の」 tok/s に近い
long_ctx8192 では prompt 7,395 tokens を 1 回の prefill で処理しないといけません。max_tokens=512 なので、生成中の全 elapsed_ms に占める prefill 比率は無視できません。SGLang は chunked prefill と radix cache 周辺の実装で、この長い prefill を vLLM より早く吐き出している可能性が高いです。
逆に short_ctx512 (prompt 538 tokens, gen 256 tokens) は prefill 比率が小さく、両者の差は 「素の decode ループ」の差 に近くなります。それでも +18% 出ているので、decode ループ自体も SGLang の方が軽い、という見立てができます。
観測されたバラつき
中央値だけだと「数字が綺麗すぎないか」と疑問が湧くと思うので、5 回計測の生データから幅を抜粋しておきます。
vllm_flashinfer の例:
| workload | run1 | run2 | run3 | run4 | run5 | median |
|---|---|---|---|---|---|---|
| short_ctx512 | 59.53 | 59.49 | 59.49 | 59.51 | 59.49 | 59.49 |
| mid_ctx2048 | 58.02 | 58.12 | 58.12 | 58.12 | 58.13 | 58.12 |
| long_ctx8192 | 52.59 | 52.59 | 52.59 | 52.60 | 52.60 | 52.59 |
temperature 0 + 同じ prompt なので、出力 token も decode 経路もほぼ同じになるはずで、実測でも幅は 0.04〜0.11 tok/s に収まりました。Triton JIT を warmup 1 回で剥がした効果も含めて、reproducibility はかなり高い領域です。
SGLang 側も同様で、5 run 中での幅は 0.1 tok/s 以下に収まりました。今回の結論「フレームワーク差は 18%」「backend 差は 1% 未満」は、この再現性を踏まえた上での発言です。
起動時間と peak VRAM
純粋な tok/s だけだと運用判断材料として薄いので、副次指標も並べておきます。
| framework | backend | 起動秒数 | peak VRAM (MB, long_ctx8192 計測中) |
|---|---|---|---|
| vLLM | flash_attn | 130 | ~10,200 |
| vLLM | flashinfer | 135 | ~10,600 |
| vLLM | triton_attn | 135 | ~10,250 |
| SGLang | flashinfer | 75 | ~11,290 |
| SGLang | triton | 80 | ~10,820 |
-
起動時間: SGLang は 75〜80 秒、vLLM は 130〜140 秒。コンテナ立ち上げ後、
/healthが通るまでの実測。SGLang の方が「上げ下げ」が軽いので、開発時の反復速度に効きます -
peak VRAM: SGLang のほうが 200〜600 MB 多めに使う傾向。
--mem-fraction-static 0.85で静的に確保するため
12GB VRAM のうち、両エンジンとも 1 GB 以上の余白を残せています。FP8 KV cache まで使えば余白をさらに作れて、長文も入りやすくなりますが、今回はそこに手を出さず kv_cache_dtype=auto (= 既定の BF16 相当) で揃えています。FP8 KV cache 単体の品質・速度の検証と、それで context をどこまで伸ばせるかは別シリーズ ローカルゲーミングPCでLLM - VRAM 12GB 環境で FP8 KV Cache は実用投入できるか と ローカルゲーミングPCでLLM - 長文運用の限界。FP8 KV Cache でより長い長文を LLM で処理できるようになるか でまとめています。
出力品質の簡易チェック
temperature=0 で同じプロンプトを投げた以上、出力テキストはエンジン間で一致するべきですが、tokenizer と chat template の扱いがエンジン間で完全に同じとは限らないので、long_ctx8192 の出力先頭を実際に並べておきます。
vllm_flashinfer long_ctx8192 先頭:
以下に、本記事で取り上げた技術的観察を整理して簡潔にまとめます。
1. attention backend の選択は decode 速度に対して
想像より影響が小さい場合がある...
sglang_flashinfer long_ctx8192 先頭:
以下に、本記事で取り上げた技術的観察を整理して簡潔にまとめます。
1. attention backend の選択は decode 速度に対して
想像より影響が小さい場合がある...
冒頭は完全に一致しています。temperature=0 下で、Qwen3.5-4B FP8 の出力分布はエンジンを超えて再現できている、と読めます。途中で 1〜2 token ずれることはありますが (chat template の細部差から来る) 、明らかな品質崩れは見られませんでした。
tokenizer の差が起こす落とし穴
シリーズ ① でも触れた話の追加観測です。最初は long_ctx8192 用に作った ctx8192.txt (人間目線で約 8000 token 相当のテキスト) を投げたところ、両エンジンとも request_failed になりました。
- vLLM 側のエラー:
prompt contains at least 7681 input tokens, plus 512 generation = 8193 > max_model_len 8192 - SGLang 側のエラー:
The input (8284 tokens) is longer than the model's context length (8192 tokens)
同じテキスト・同じモデルなのに input tokens の数字が 7,681 vs 8,284 でズレています。差は 600 token 強。
原因はおそらく chat template と内部の prompt 加工パスの違いです。vLLM は OpenAI 互換変換時に <|im_start|> 系のラッパーを 1 セット入れる、SGLang は何らかの理由でもう少しオーバーヘッドが乗る、というような細かな差で 600 tokens 動いていると考えられます。
実用的な教訓は次のとおりです。
- 「8K context モデルだから 8K プロンプトを入れていい」とは限らない。chat template と system prompt と (生成上限 max_tokens) の合計が context window を食う
- ベンチでは prompt を 両エンジンで安全に通る長さ (今回は 7,395 tokens) にして公平に比較する
- 実運用では、エンジン非依存に tokenizer を呼んで実 token 数を計算する層を 1 段挟むのが安全
ここまでの整理
| 観点 | 結果 |
|---|---|
| vLLM 内 backend 差 | flash_attn / flashinfer / triton_attn の median tok/s 差は 0.6% 以内 |
| SGLang 内 backend 差 | flashinfer / triton の差は 1% 以内 |
| vLLM vs SGLang | short で +18%、mid で +21%、long で +28% SGLang 有利 |
| context 長依存 | context が伸びるほど SGLang の優位が拡大 (prefill 戦略の差) |
| 起動時間 | SGLang ~80s、vLLM ~135s |
| peak VRAM | SGLang +200〜600 MB |
| 出力品質 |
temperature=0 で冒頭一致。明らかな品質差なし |
結論
今回の実験で言える 3 点をまとめます。
-
「attention backend を変えれば速くなる」は今回の条件では成り立たない。RTX 4070 12GB + Qwen3.5-4B FP8 + concurrency 1 では、
--attention-backendの選択は vLLM 内で 0.6%、SGLang 内で 1% 未満の差しか生まず、これは計測誤差レベル。FlashAttention / FlashInfer / Triton のどれを選んでも tok/s はほぼ同じです。 - 支配的なのはフレームワーク選定。同じ条件で vLLM vs SGLang の差は short で +18%、long で +28%。backend よりフレームワークそのものを選ぶ方が、tok/s に直接効きます。
- context が長くなるほど SGLang 優位が拡大。short +18% → long +28%。長い prompt の処理 (prefill) を含むワークロード (RAG、長文要約) では、その差はさらに広がる可能性が高いと考えられます。
実用上の指針としては、まずフレームワーク (vLLM か SGLang か) を決め、attention backend は default のまま測り、必要なら最後に微調整する という順序が現実的です。--attention-backend を最初に詰めても 1% 以内の差しか取れません。
まだ残る疑問
ここまでは concurrency 1 の単発リクエストの話でした。一方で実運用は通常 複数リクエストを同時に捌くサーバとして動かします。その場合に出てくる疑問は次のとおりです。
- 並列度を 2 / 4 / 8 と上げたとき、tok/s はどう伸びるのか? SGLang の +28% という差はそのまま広がるのか、それとも縮むのか
- p95 latency (95% のリクエストが完了するまでの時間) で見るとどちらが優位か
- 12GB VRAM の余白は並列時にどこまで耐えるか
これらは次回 ③ で別途調査しました。本記事 ② の単発比較としてはここで完結します。
実験環境: RTX 4070 12GB / Ubuntu / vLLM nightly (v0.21.1rc1.dev243) / SGLang lmsysorg/sglang:latest / モデル: RedHatAI/Qwen3.5-4B-FP8-dynamic
シリーズ過去記事:
関連記事 (同じハードウェア・同じモデルでの FP8 KV cache 検証):