SGLang、推論向け CUDA Graph の高度化技術と Breakable CUDA Graph
本文の状態
日本語全文を表示中
詳細モードで約28分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
LMSYS Blog
SGLang が CUDA Graph の実用化に向けた「Breakable CUDA Graph」やフルグラフ対応を公開し、プリフィル処理の速度を最大1.93倍に向上させる技術的進展である。
AI深層分析を開く2026年8月18日 03:04
AI深層分析
キーポイント
共通ランナー・バックエンドインターフェースの導入
SGLang は CUDA Graph サポートを共通のランナー/バックエンドインターフェースを中心に再構築し、異なるキャプチャ戦略を実行パス間で再利用可能にした。
Breakable CUDA Graph の実装とデフォルト化
複雑なプリフィル処理向けに開発された Breakable CUDA Graph が SGLang のデフォルトとなり、トランスパイラ不要でグラフ構築が 3.8〜5.2 倍高速化した。
フル CUDA Graph とメモリ管理の高度化
リクエストパディングを用いたプリフィル全体のキャプチャ(フル CUDA Graph)や、形状間でのメモリ再利用を含む高度なメモリ管理技術が導入された。
Runner と Backend の責務分離
リファクタリングにより、キャプチャと再生に必要な状態管理を担当する Runner と、実行方法を決定する Backend が分離された。これにより各実行パスが独自の戦略を独立して選択できるようになった。
柔軟な実行パスの組み合わせ
Prefill や Decode だけでなく、EAGLE ドラフトやターゲット検証など多様なモードごとに専用の Runner が構築される。共通の Backend インターフェースを通じてこれらの異なる戦略を統合している。
重要な引用
In SGLang, we refactored CUDA Graph support around a common runner/backend interface, making different capture strategies reusable across execution paths.
Breakable CUDA Graph is now SGLang's default. It reaches the same segmented execution as the torch.compile-based piecewise backend in roughly a quarter of the code
Measured on prefill alone, BCG is 1.70× faster than eager execution and full capture reaches 1.93×.
"A runner manages the execution-specific state needed for capture and replay... A backend determines how that execution is captured, whether as one full graph, a sequence of breakable segments, or compiler-generated pieces."
編集コメントを表示
編集コメント
SGLang が CUDA Graph の実装課題を解決し、コンパイルオーバーヘッドなしで高速化を実現した点は、推論エンジンの設計において重要な転換点となる。特にプリフィル処理の速度向上は、大規模モデルのリアルタイム応答性を高める上で即座に活用可能な成果である。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
TL;DR
CUDA Graph はカーネル起動のオーバーヘッドを削減する可能性がありますが、実際の推論エンジンでその恩恵を最大化するには、互換性、起動時間、メモリ使用量に悪影響を与えずに、可能な限り多くの処理をグラフ化する必要があります。
SGLang では、共通の実行器/バックエンドインターフェースを中心に CUDA Graph サポートを再構築し、異なるキャプチャ戦略を実行パス間で再利用可能にしました。特に複雑なプレフィルパスにおいては、コミュニティが「Breakable CUDA Graph」を導入し、FA4 や FlashInfer といったアテンションバックエンドとのフル CUDA Graph サポートを実現する先駆者となりました。これらの技術はすべて、SGLang でオープンソースのサービング手法として最初に開発されました。さらに、形状やグラフセグメント間でのメモリ再利用を含む、CUDA Graph のメモリ管理についても深く掘り下げています。これは SGLang の全体メモリ管理において、ますます重要な役割を果たしています。
プレフィルにおいては、Breakable CUDA Graph が現在 SGLang のデフォルトとなっています。これは torch.compile ベースのピースワイズバックエンドと同等の分割実行を実現しながら、コード量は約 4 分の 1(521 行対 1,771 行)に抑えられています。また、コンパイルを不要とするため、プレフィルグラフの構築速度は 3.8〜5.2 倍高速化され、複雑な機能に対するカバレッジも自然と広がります。さらに進んだ「フル CUDA Graph」では、リクエストのパディングを活用して動的なプレフィルワークロードであっても前方伝播全体をキャプチャします。
プレフィル単体の測定結果では、Breakable CUDA Graph はイーガー実行より 1.70 倍高速で、フルキャプチャは 1.93 倍の速度向上を実現しています。
推論ステップは単一のカーネルではなく、多数の GPU 操作が連続するものです。現代の LLM サービングエンジンでは、CPU からこれらの操作を繰り返し起動すると、特にレイテンシに敏感なワークロードにおいて顕著なオーバーヘッドが生じます。CUDA Graph は、GPU の作業を一度記録し、起動時のオーバーヘッドを大幅に抑えながら再生することで、この問題を解決します。
しかし、現代の推論エンジンで CUDA Graph を効果的に活用するのは容易ではありません。グラフ設計は異なる実行フェーズに適応し、複雑なカーネルやランタイム依存の挙動との互換性を保ちつつ、グラフ自体が導入するキャプチャ時のオーバーヘッドとメモリ使用量を適切に制御する必要があります。推論スタックが複雑化するにつれ、CUDA Graph の適切な統合はますます重要になっています。
本記事では、SGLang における CUDA Graph サポートの構築方法と、私たちが行った変更について解説します。
SGLang における CUDA Graph: Runner/Backend の分離と柔軟な組み合わせ
このリファクタリング以前、CUDA Graph のサポートは個々の実行パスごとに散在していました。デコード、プリフィル、推測的デコーディングそれぞれが独自の CUDA Graph ランナーを持っており、キャプチャ形状や静的バッファ、再生、グラフ設定に関するロジックが重複していました。より多くの実行モードとキャプチャ戦略が追加されるにつれ、この重複によりインフラの再利用が難しくなり、CUDA Graph 関連のサーバー引数も次第に曖昧なものになっていました。
今回のリファクタリング [#23906] では、これらの責任を 2 つの層に分離しました。ランナーは、キャプチャと再生に必要な実行固有の状態(キャプチャされた形状、静的入力バッファ、アテンションメタデータ、およびライブバッチをキャプチャ形状にパディングする処理)を管理します。一方、バックエンドは、その実行が 1 つの完全なグラフとして、あるいは分割可能なセグメントの連続、またはコンパイラ生成されたピースとしてどのようにキャプチャされるかを決定します。
ランナーが共通のバックエンドインターフェースにのみ依存するため、各実行パスは独自のキャプチャ戦略を自由に選択できます。プリフィルとデコードにはそれぞれ専用のランナーがあり、推測的デコーディングではさらに多くのランナーが追加されます。EAGLE ドラフト、ドラフト拡張、凍結 KV の MTP ドラフトステップには、デコードランナーを基盤とした独自のランナーが用意され、ターゲット検証はデコードランナーそのものであり、1 つのリクエストに対して複数のトークンをキャプチャします。
ランナーは実行パスをキャプチャと再生のために準備し、バックエンドはフォワード処理をどのように再生可能なグラフに変換するかを決定します。具体的には、1 つの完全なグラフとして、キャプチャ中に分割されたもの、あるいはキャプチャ前にトレースして分割したもののいずれかになります。
Full CUDA Graph
フルバックエンドでは、選択された形状ごとに 1 つの torch.cuda.CUDAGraph をキャプチャし、イージア領域を設けず、3 つのバックエンドの中で最も少ない再生時の起動回数为済ませます。これはデコード処理に自然と適合します。各リクエストは 1 トークンを生成するため、主要な形状変数はバッチサイズとなり、キャプチャ済みバッチサイズの buckets でカバーできます。一方、プリフィルはより多くの次元に沿って変動するため難易度が高く、これについては後述のセクションで詳しく解説します。
Breakable CUDA Graph
Breakable CUDA Graph(BCG)は、グラフ安全な領域をキャプチャしつつ、選択された演算がグラフセグメント間でイージアとして実行されることを可能にします。互換性のない演算には @eager_on_graph アノテーションを付与し、キャプチャは指定された関数の直前で停止し、その後に再開することで、イージア領域によって区切られた CUDA Graph のシーケンスを生成します。
コンパイラに基づくピースワイズキャプチャとは異なり、これらのブレイクポイントはモデル全体をトレースして発見するのではなく、キャプチャ中に直接挿入されます。この仕組みと、SGLang がなぜこの設計を採用したかについては、次のセクションで詳述します。
TC piecewise CUDA Graph
3 つ目のバックエンドは、コンパイラを通じて同様のセグメンテーションを実現します。torch.compile は fullgraph=True でフォワードパスをトレースし、登録された分割点で FX グラフを分割します。その後、各ピースが個別にコンパイルされ、キャプチャされます。これは SGLang が部分的な CUDA Graph キャプチャに対する最初の回答として提供したものであり、まだ「破断可能なキャプチャ」の検証が行われていないプラットフォーム向けにも引き続き採用されています。
コンパイラ不要の Breakable CUDA Graph:Eager Breaks の活用
従来の CUDA Graph では、キャプチャ対象領域が完全にグラフ互換である必要があります。しかし実際には、現代の推論ワークロードには直接キャプチャできない演算が含まれています。例えば Prefill 時のアテンションは典型的な例で、一部のバックエンドではランタイムメタデータやホスト側の事前準備に依存します。互換性のない演算が一つでも含まれると、CUDA Graph がフォワードパスのより広い範囲をカバーできなくなってしまうのです。
そこで私たちは、キャプチャの柔軟性を高める Breakable CUDA Graph (BCG) を導入しました。この仕組みと @eager_on_graph デコレータは、まず [#19102] で CUDA Graph のデバッグモードの一部として実装され、その後 [#22218] で Prefill 用の破断可能なピースワイズバックエンドに組み込まれました。フォワードパス全体をグラフ互換にする必要はなく、BCG では特定の演算を eager モードで実行しつつ、その周囲のグラフ互換領域をキャプチャします。高レベルでは、フォワードパスは明示的な eager ブレークで接続された CUDA Graph のセグメント列として実現されます。
設計と仕組み
CUDA Graph は、ホストの介入なしに GPU の操作が固定された順序で再生される場合に最も効果を発揮します。しかし、実際の推論処理では、このモデルに自然に収まらない操作が存在します。例えば、アテンションバックエンドは実行中のシーケンス長に基づいて計画を立てる必要があり、集合演算にはランタイムでの調整が必要となる場合があり、サービング機能も状態を動的に更新する必要があります。
このような不適合な操作が一つでも現れるたびに CUDA Graph を諦めてしまうと、前向き処理の大部分がキャプチャされなくなってしまいます。BCG では、開発者が @eager_on_graph アノテーションを使って互換性のない領域を明示的にマークできます。キャプチャ中、実行がこのマークされた関数に到達すると現在のグラフセグメントが閉じられ、その関数が即座に実行されます。その後、新しいセグメントとしてキャプチャが再開されます。
再生時には、記録されたグラフセグメントと即座に実行される関数が同じ順序で動作します。即座に実行される箇所で跨ぐテンソルは、直前のキャプチャ済みセグメントによって生成され、永続的な境界バッファとして登録されます。これにより、デバイスのアドレスが固定されたまま保たれます。次のキャプチャ済みセグメントは、この同じアドレスを対象としてキャプチャされます。再生時には、即座に実行される関数は計算結果を新しいテンソルとして返すのではなく、この境界バッファへ書き戻します。これにより、次のセグメントは、キャプチャ時に使用された元のアドレスから更新された値を読み取ることができます。BCG は即座に実行される領域内の操作を検査したりトレースしたりしません。重要なのは、それらが正しく実行されることだけです。
機能面から見ると、BCG と従来の torch-compile ベースのピースワイズバックエンドは、同じような再生可能な構造を生成します。具体的には、CUDA Graph のセグメントが eager 領域によって区切られた形です。決定的な違いは、この構造をどのように構築するかという点にあります。
TC(torch-compile)ベースのピースワイズでは、まずコンパイラに順伝播全体を理解させた上で、生成されたグラフを分割します。一方、BCG はキャプチャ中に直接分割点を設定します。
メリット
起動時間の短縮。コンパイラーベースのピースワイズグラフの場合、セットアップ時間を支配するのはキャプチャではなくコンパイルです。torch.compile がプリフェッチグラフの準備に要する時間の 78〜86% を占めており、モデルが複雑になるほどこの割合は増加します。例えば、235B の MoE モデルでは 90 秒、GLM-5.2 では 158 秒にも達しました。BCG はこの工程を完全に排除し、キャプチャパスを単一で通すだけでセグメント化された実行を実現します。
プリフェッチ CUDA Graph の構築にかかる時間。42 種類の形状をキャプチャし、TP4 で 4×GB300 を使用した環境での結果です。
このコンパイルのオーバーヘッドは、日常の開発でも顕著でした。当時の CI 設定では、テスト実行ごとにコンパイルが繰り返されるため、CUDA Graph のテストが明らかに遅くなっていました。キャッシュを改善すればある程度緩和できますが、キャプチャパスからコンパイラーを外すことで、開発ループにおけるこの余計な複雑さの要因も同時に排除できました。
より広い互換性。 SGLang は、ネイティブな PyTorch オペレーターではないカスタム CUDA、Triton、JIT コンパイル済みカーネルに大きく依存しています。これらのカーネルを torch.compile に対して認識可能にするため、多くの場合 torch.library を通じてラップし、トレース用のダミー実装を提供する必要がありました。これにより、カーネルスタック全体にコンパイラー固有の足場が組み込まれることになりました。
さらに重要な点は、コンパイラーがグラフ境界をどこに設定できるかを制約したことです。登録されたオペレーター境界をまたぐ入出力は、コンパイラーで表現可能でなければなりません。自然な境界がより専門的なランタイム状態や返却型を含む場合、コンパイラーが処理できるインターフェースを露出させるために、別の切断点を探したり、単純にイーグル領域を広げたりする必要がありました。サービングスタックが大きくなるにつれ、コンパイラーの境界は、コンパイルとは無関係なコード構造にも次第に影響を与えるようになっていきました。
BCG は、イーグル(Eager)実行のブレークポイントにおいてこの制約を解除します。グラフシステムは、マークされた関数の実装内容を理解する必要もなければ、その内部をトレースする必要もありません。これにより、グラフの境界線はコンパイラのトレースや型要件に縛られるのではなく、サービングロジックに沿って設定できるようになります。
CUDA Graph は、DP アテンション、MoE の all-to-all バックエンド、LoRA、PD 分離、階層キャッシュ、決定論的推論など、急速に進化する機能と共存する必要がありました。その結果、CUDA Graph を動作させることは次第に「torch.compile の統合プロジェクト」のような作業のように感じられるようになりました。新しいカーネルが登場すればカスタムオペレーションの登録や疑似実装が必要になり、新機能が追加されればコンパイラ要件を満たすためにグラフ境界線を移動させざるを得ない状況も生まれました。
BCG を採用することで、互換性のない領域は通常のイーグル実行として維持できます。これにより、コンパイラ固有のエンジニアリング負荷を大幅に削減することが可能です。
構築段階からデバッグ可能。 捕捉された CUDA Graph は不透明なユニットとして再生されます。内部では通常の Python コードが実行されないため、print 文やアサーション、ステップごとの検査が困難になります。BCG では、自然な形でイーグル領域を残すため、再生のたびに通常の Python が動作する領域を確保できます。
SGLang は、このアイデアを拡張し、--debug-cuda-graph オプション(#19102)を提供しています。これは、前方伝播全体を eager 実行のブレークポイントとしてラップする機能です。これにより、モデルは eager モードで実行されつつも、CUDA Graph ランナー、静的バッファ、再生パス、メタデータ準備といった処理フローを経由します。
この仕組みはデバッグに非常に有用な境界線となります。もし問題が解消されない場合は、原因はモデル自体かランナー側のパスにあります。逆に問題が消えた場合、キャプチャ処理そのものが主な疑わしい箇所となります。
Diffusion における BCG
BCG は SGLang の拡散モデルスタックでも採用されています(#27436)。Diffusion モデルはノイズ除去の過程で同じ DiT 前方伝播を繰り返し実行するため、多数の小さなカーネルが起動バウンドを起こす場合、CUDA Graph は特に有効です。
- 実際のサービング形状をキャプチャする。 解像度、動画フレーム数、プロンプト条件付けの長さ、CFG モード、選択されたトランスフォーマーなどはすべて、キャプチャのシグネチャーに影響を与えます。実際に提供される形状でウォームアップを行い、未知のシグネチャーに対しては eager 実行へフォールバックします。
- 動的演算の周囲で処理を分断する。 動的アテンションやランタイム依存のメタデータ準備などの演算は eager モードのまま維持し、BCG はそれらの周辺にある安定した計算部分をキャプチャします。これにより、
torch.compileが DiT の前方伝播全体を理解する必要はありません。
反復される拡散構造を活用する
Diffusion モデルは、ノイズ除去ステップ全体を通じて同じ DiT 構造を繰り返し実行します。BCG は安定した領域を一度キャプチャしてノイズ除去ループ内で再生し、動的な領域については eager モードのまま維持します。
これは、処理が起動オーバーヘッドに制限されている場合に特に効果的です。例えば、ウォームアップ後、単一の B200 GPU 上で Qwen-Image を 512×512 で実行すると、エンドツーエンドのレイテンシは 6.48 秒から 2.45 秒へ短縮され、Z-Image も 1.231 秒から 0.662 秒へと改善されます。
ウォームアップ後のエンドツーエンドレイテンシ。各棒グラフのペアは、同じモデル負荷と乱数シードを使用しています。
重要な教訓として、BCG は起動オーバーヘッドを削減するものであり、モデルの FLOPs を減らすわけでも、計算集約型カーネルのコストを下げるものでもないことがわかります。その真価が発揮されるのは、露出した起動ギャップが実行時間の有意な割合を占めている場合です。
プリフィルのための完全 CUDA Graph
デコードにおいては、各リクエストが 1 トークンを生成するため、バッチサイズという主要な変動次元のみが存在するため、完全な CUDA Graph の適用は straightforward です。一方、プリフィルはより困難です。なぜなら、バッチ内ではトークン総数と、それらが属するリクエスト数が同時に変化する 2 つの次元で変動が生じるためです。しかし、キャプチャされたグラフではこれら両方が固定されている必要があります。さらに、ランタイムメタデータに依存するアテンションバックエンドが存在することも相まって、プリフィルへの完全な CUDA Graph の適用は困難でした。これが、私たちがプリフィルにおいて Breakable CUDA Graph を採用した主な理由の一つです。
最近、リクエストスロットやアテンションメタデータの表現方法を見直すことで、プリフィル実行を十分に静的な状態に固定し、フル CUDA Graph の利用を実現する手法を見つけました。これにより、対応するアテンションバックエンドもグラフ外で動作させる必要がなくなりました。
プリフィルの静的化について
SGLang ではトークンバケットを用いてトークン次元を固定します。実行中のバッチは、デコード時と同様に、キャプチャされたトークン数に最も近い値までパディングされます。
リクエスト次元については別の仕組みで処理されます。各キャプチャ済みグラフは、固定数のリクエストスロットを確保しています。実行中のリクエストはこのスロットの先頭から占有され、使用されていないスロットはゼロ長のシンボルとして書き換えられます。これにより、シーケンス長や拡張長、オフセットが実トークンの後に配置されます。もしバッチ内のリクエスト数がグラフのスロット数を超えた場合、そのバッチはイーグル実行にフォールバックします。
再生時には、トークンはキャプチャされたバケットに合わせてパディングされ、使用されていないリクエストスロットにはゼロ長のシンボルが埋め込まれます。
シンボルのメタデータは、グラフがリクエストテーブル全体を読み続けるため、毎回の再生時に書き換えが必要です。同様に、アテンションメタデータも再生前にパディングされたバッチに対してグラフ外で再構築されます。現在、フルプリフィルのキャプチャには、FlashAttention や FlashInfer など、この種のメタデータ準備をサポートするアテンションバックエンドが求められます。
パディングのコストは?
パディングには 2 つの形態があり、それぞれのコストは大きく異なります。
トークンのパディングは実際の計算コストを伴います。これらはキャプチャされたバッチ内の実在する行となり、GEMM の一部として密な射影処理を通過します。SGLang では真のトークン数を別途管理しているため、MoE ルーティングやアテンション、線形アテンションカーネルはパディング領域の大部分をスキップできますが、密な計算においてはこれらの余分な行に対するコストが発生し続けます。
一方、リクエストスロットのパディング(空のスロット)ははるかに安価です。FlashAttention の可変長スケジューラでは、各リクエストスロットに固定量の計算を割り当てるのではなく、シーケンスの実質的な長さに基づいて処理量が決定されます。したがって、長さがゼロのリクエストはアテンション計算への寄与を実質的に持ちません。主な影響はメタデータの追加と、わずかなスケジューリングオーバーヘッドの発生です。
この非対称性は重要です。トークンのパディングが高コストな要因である一方、リクエストスロットのパディングは比較的低コストで済みます。
フルプリフェッチキャプチャはまだ実験的な機能です。明示的に有効化する必要があり、エンジンからは full が実験的であることを警告し、本番環境のワークロードには breakable や tc_piecewise を使用することを推奨しています。現在、この機能が主に動作するのは、キャプチャパスが必要とする拡張モードのメタデータを構築する FlashAttention (fa4) と FlashInfer のバックエンドだけです。他のバックエンドへのサポート拡大や、バケットとスロットの選択に関するチューニングは今後の課題となっています。
Prefill benchmark
プリフィル処理をキャプチャする方法が 3 つあり、さらに eager ベースラインも存在します。残る疑問は、これら各手法の再生時のコストです。
gpt-oss-120b(TP4、4×GB300)環境で、全 4 パスが動作する条件下でプリフィル処理のみを測定しました。条件は固定された入力長と単一の出力トークン、リクエストは逐次実行、各試行でデコードグラフを無効化です。その結果、フルキャプチャは eager よりも 1.93 倍高速、BCG は 1.70 倍、TC piecewise は 1.45 倍でした。つまり BCG はビルド時だけでなく、再生時においてもコンパイラベースのバックエンドよりも 17% 高速です。
GLM-5.2 の場合、BCG だけがキャプチャ可能です。TC piecewise は順伝播をトレースできず、フルキャプチャもスパースアテンションに対応するパスがないためです。このモデルでは eager よりも 1.60 倍高速でした。
すべての曲線がプロンプト長の 32 倍の範囲にわたって平坦であることは、計算負荷ではなく起動オーバーヘッドによる特徴であることを示しています。
gpt-oss-120b におけるプリフィル専用レイテンシ。全 4 つのバックエンドが動作する環境での結果です。
CUDA Graph のメモリフットプリント
メモリには 2 つの課題があります。まず、セグメント化されたキャプチャによって残留メモリの増加を防ぐこと、そして十分な範囲をキャプチャして、残留グラフメモリが最悪の eager アクティベーションピークを実際に上回るようにすることです。
セグメント内での再利用
セグメント化されたバックエンドでは、グラフメモリが増殖するリスクがあります。各キャプチャ形状には複数のグラフセグメントが含まれ、それぞれのセグメントには再生時に有効なまま保持しなければならない中間値が存在するためです。BCG は 3 つの種類の再利用を通じて、この増殖を防ぎます。
セグメント全体で共有されるメモリプールを使用します。キャプチャされた形状に対応するすべてのセグメントが同じ CUDA Graph プールを利用するため、中間データを各セグメントごとに個別に確保(ピン留め)する必要がなく、再利用が可能になります。
イーグル実行の中断ポイントでは、弱参照を活用します。グラフプールがすでにテンソルのストレージを所有している場合、中断ポイントに渡されるテンソルは弱参照として保持されます。これにより、テンソルの寿命を不必要に延ばす Python 側の参照を防ぎます。この弱参照の手法は vLLM [#9724] から導入されたもので、キャプチャされたグラフ同士が出力バッファを共有し、それぞれが独自のバッファをピン留めする必要がないようにするためのものです。
キャプチャサイズ全体で一つの出力バッファを使用します。各形状ごとに個別に出力バッファを確保するのではなく、最大サイズの単一の出力バッファを準備し、必要な行数分だけスライスして利用します。
ただし、イーグル実行の中断ポイント間でデータを伝達するテンソルだけは例外です。このテンソルのアドレスに対して次のグラフセグメントがキャプチャされるため、そのバッファは常に生存状態を保ち、リプレイごとにインプレースで更新され続ける必要があります。
これらの再利用メカニズムにより、大規模なキャプチャテーブルであってもメモリ使用量は抑えられます。例えば、78 レイヤーの MoE 構造を持つ GLM-5.2 モデルでは、42 の形状に対応するグラフメモリの合計はわずか 2.4 GB です。
チャンク化プリフィルサイズのキャプチャ
CUDA Graph は、プリフィル時のメモリ使用パターンを変化させます。グラフで確保されたメモリは「定在メモリ(residents)」として扱われ、キャプチャ時に割り当てられてからサーバーの稼働期間中ずっと保持されます。一方、イーグル実行による活性化データは「一時的」であり、各プリフィル処理で作業用メモリを都度割り当てます。この場合、ピークメモリ量はサポートされる最大のプリフィルサイズによって決定されます。
プリフィル形状をグラフとしてキャプチャすることで、その一時的な作業セットの多くをグラフの定在メモリプールへ移動させることができます。ただし、これは実際にグラフを再生する形状に対してのみ有効です。もしキャプチャ上限(capture ceiling)が最大プリフィルサイズに達していない場合、最大のプリフィル処理は依然としてイーグル実行にフォールバックし、元の活性化ピークを引き続き保持することになります。その上、サーバー側ではそれより下のすべてのグラフ用メモリも同時に確保され続けるため、無駄なコストが発生します。
この理由から、キャプチャ上限の高さが、単にキャプチャされた形状の数を増やすことよりも重要になります。chunked_prefill_size が最大の単一プリフィルフォワードを制限している場合、そのサイズまでグラフをキャプチャすることで、最悪のイーグル活性化ピークを排除できます。
「グラフなし定在ベースライン」を上回るプリフィルメモリ量。これは、ちょうど chunked_prefill_size のサイズで一度だけプリフィルを実行した直後に測定された値です。
チャンクサイズ未満の上限値では、グラフなしベースラインよりもわずかに高いメモリ使用量を示します。これは、定常的なグラフが追加される一方で、アクティベーションのピークは依然として同じ位置にあるためです。しかし、上限値がチャンクサイズに達すると、最大のプリフィル処理で初めてグラフが再生され、そのピークは劇的に低下します。具体的には、gpt-oss-120b では 0.56 GB から 0.001 GB にまで減少し、GLM-5.2 でも 1.55 GB から 0.35 GB へと削減されます。なお GLM-5.2 は、スパースアテンションインデクサーが依然としてブレイクポイントで待機状態(eager)であるため、この効果はより顕著です。
チャンクサイズまでのプリフィル処理をキャプチャすることで、以下の 2 つのメリットが得られます。
- 総メモリ使用量の削減。アクティベーションのピークがリクエストごとに支払われる必要がなくなり、全体としてグラフなしベースラインを下回ります。gpt-oss-120b では 0.51 GB、GLM-5.2 では 1.10 GB の削減効果があります。数百ギガバイト規模のメモリフットプリントに対しては限定的な改善に見えますが、これはコストではなく明確な節約です。
- 予測可能なメモリ使用量。ワークロードに依存して変動するアクティベーションのスパイクを、キャプチャ時点で固定された割り当てに変換できます。エンジン側では、大規模なプリフィル処理時のみ一時的に発生するピークのための予備領域を確保するのではなく、事前に必要なメモリ量を正確に見積もって確保することが可能になります。
謝辞
本研究成果は、SGLang チームと Meta チームの共同作業によって実現されました。
SGLang チーム: Yuwei An*, Cheng Wan, Xiaoyu Zhang, Mick Qian, Baizhou Zhang, Yusheng Su, Ke Bao
Meta チーム: Shiyang Chen*, Lianmin Zheng
また、本研究の進展に貢献いただいた NVIDIA 社、AMD 社、Thinking Machines Lab、および Meta PyTorch チームの皆様にも深く感謝いたします。
(* 同等の貢献者)
原文を表示
TL;DR
CUDA Graphs promise to remove kernel-launch overhead, but getting close to that benefit in a real inference engine requires graphing as much of the workload as possible without sacrificing compatibility, startup time, or memory.
In SGLang, we refactored CUDA Graph support around a common runner/backend interface, making different capture strategies reusable across execution paths. For the more complex prefill path, the SGLang community introduced Breakable CUDA Graph and pioneered full CUDA Graph support with the FA4 and FlashInfer attention backends. Both techniques were first developed in SGLang as open-source serving techniques. We also dive deeper into CUDA Graph memory management, including memory reuse across shapes and graph segments, which is becoming an increasingly important part of SGLang’s overall memory management.
For prefill, Breakable CUDA Graph is now SGLang's default. It reaches the same segmented execution as the torch.compile-based piecewise backend in roughly a quarter of the code (521 versus 1,771 lines), builds prefill graphs 3.8–5.2× faster because no compilation is involved, and has broader coverage for complex functionality naturally. Full CUDA Graph for prefill goes further, using request padding to capture the whole forward even for dynamic prefill workloads. Measured on prefill alone, BCG is 1.70× faster than eager execution and full capture reaches 1.93×.
背景
An inference step is not a single kernel but a sequence of many GPU operations. In modern LLM serving engines, repeatedly launching these operations from the CPU can introduce noticeable overhead, especially for latency-sensitive workloads. CUDA Graph reduces this overhead by recording the GPU work once and replaying it with much lower launch overhead.
But applying CUDA Graphs effectively in a modern inference engine is not straightforward. The graph design must fit different execution phases, remain compatible with complex kernels and runtime-dependent behavior, and control the capture-time and memory overhead introduced by the graphs themselves. As inference stacks become more complex, proper CUDA Graph integration becomes increasingly important.
This post walks through how CUDA Graph support is built in SGLang and what we changed:
- CUDA Graph in SGLang: the Runner/Backend Split and Flexible Combinations
- Breakable CUDA Graph: Eager Breaks without a Compiler
- Full CUDA Graph for Prefill
- Memory Footprint of CUDA Graphs
CUDA Graph in SGLang: the Runner/Backend Split and Flexible Combinations
Before this refactor, CUDA Graph support had grown around individual execution paths. Decode, prefill, and speculative decoding each had their own CUDA Graph runners, with overlapping logic for capture shapes, static buffers, replay, and graph configuration. As more execution modes and capture strategies were added, this duplication made it harder to reuse infrastructure and made CUDA Graph-related server arguments increasingly ambiguous.
The refactor [#23906] separates these responsibilities into two layers. A runner manages the execution-specific state needed for capture and replay: captured shapes, static input buffers, attention metadata, and the padding of live batches into captured shapes. A backend determines how that execution is captured, whether as one full graph, a sequence of breakable segments, or compiler-generated pieces.
Because runners depend only on a common backend interface, each execution path can choose its capture strategy independently. Prefill and decode have separate runners, and speculative decoding adds more: the EAGLE draft, draft-extend and frozen-KV MTP draft steps each get their own runner built on the decode runner, while target verify is the decode runner itself, capturing more than one token per request.
The runner prepares each execution path for capture and replay, while the backend determines how the forward is turned into replayable graphs: as one full graph, segmented during capture, or traced and split before capture.
Full CUDA Graph
The full backend captures one torch.cuda.CUDAGraph for each selected shape, with no eager regions and the fewest replay-time launches of the three backends. This works naturally for decode: each request contributes one token, so the primary shape variable is batch size, which can be covered by a set of captured batch-size buckets. Prefill varies along more dimensions and is therefore harder; we discuss it in its own section.
Breakable CUDA Graph
Breakable CUDA Graph (BCG) captures graph-safe regions while allowing selected operations to run eagerly between graph segments. An incompatible operation can be marked with @eager_on_graph; capture stops before the marked function and resumes afterward, producing a sequence of CUDA Graph segments separated by eager regions.
Unlike compiler-based piecewise capture, these breaks are inserted directly during capture rather than discovered by tracing the full model first. We discuss the mechanism and why SGLang moved to this design in the next section.
TC piecewise CUDA Graph
The third backend reaches similar segmentation through a compiler. torch.compile traces the forward with fullgraph=True, the resulting FX graph is split at registered split points, and each piece is compiled and captured on its own. It was SGLang's first answer to partial CUDA Graph capture and still ships for platforms where breakable capture has not been validated.
Breakable CUDA Graph: Eager Breaks without a Compiler
CUDA Graph traditionally requires the captured region to be fully graph-compatible. In practice, modern inference workloads contain operations that cannot be captured directly. Prefill attention is a common example: some attention backends depend on runtime metadata and host-side preparation. A single incompatible operation can therefore prevent CUDA Graph from covering a much larger part of the forward.
We introduced Breakable CUDA Graph (BCG) to make capture more flexible. The mechanism and the @eager_on_graph decorator landed first as part of CUDA Graph debug mode in [#19102], and were then built into a breakable piecewise backend for prefill in [#22218]. Instead of requiring the entire forward to be graph-compatible, BCG allows selected operations to run eagerly while capturing the graph-compatible regions around them. At a high level, the forward becomes a sequence of CUDA Graph segments connected by explicit eager breaks.
Design and Mechanism
CUDA Graph works best when replay follows a fixed sequence of GPU operations without host participation. Real inference forwards, however, contain operations that do not fit naturally inside that model: attention backends may plan from live sequence lengths, collectives may involve runtime coordination, and serving features may update state dynamically.
Giving up on CUDA Graph whenever one such operation appears would leave much of the forward uncaptured. BCG instead lets developers mark the incompatible region directly with @eager_on_graph. During capture, the current graph segment is closed when execution reaches the marked function, the function runs eagerly, and capture resumes afterward in a new segment.
At replay time, the recorded graph segments and eager functions run in the same order. The tensor crossing an eager break is created by the preceding captured segment and registered as a persistent boundary buffer, so its device address remains fixed. The following captured segment is captured against that same address. During replay, the eager function therefore writes its newly computed result back into this boundary buffer rather than returning a newly allocated tensor, allowing the next segment to read the updated value from the address it was originally captured with. BCG never inspects or traces the operations inside the eager region: they only need to execute correctly.
From a functionality perspective, BCG and the earlier torch-compile-based piecewise backend produce the same kind of replayable structure: CUDA Graph segments separated by eager regions. The key difference is how that structure is constructed. TC piecewise first asks the compiler to understand the full forward and then splits the resulting graph. BCG places the splits directly while capture is happening.
Benefits
Faster startup. For compiler-based piecewise graphs, compilation — not capture — dominates setup: torch.compile accounts for 78–86% of the time spent preparing prefill graphs, and it grows with model complexity, reaching 90 seconds on a 235B MoE and 158 seconds on GLM-5.2. BCG removes that phase entirely, reaching segmented execution in a single capture pass.
Time to build the prefill CUDA Graphs, 42 captured shapes, TP4 on 4×GB300.
The compilation overhead was also visible in day-to-day development. In our CI setup at the time, compilation was often repeated across test runs, making CUDA Graph tests noticeably slower. Better caching could mitigate this, but removing the compiler from the capture path also removed this extra source of complexity from the development loop.
Broader compatibility. SGLang relies heavily on custom CUDA, Triton, and JIT-compiled kernels that are not native PyTorch operators. To make these kernels visible to torch.compile, we often had to wrap them through torch.library and provide fake implementations for tracing. This introduced compiler-specific scaffolding throughout the kernel stack.
More importantly, the compiler also constrained where graph boundaries could be placed. Inputs and outputs crossing a registered operator boundary had to be representable by the compiler. When the natural boundary involved more specialized runtime state or return types, we sometimes had to search for a different cutting point or enlarge the eager region simply to expose an interface the compiler could handle. As the serving stack grew, the compiler boundary increasingly influenced the structure of code that was otherwise unrelated to compilation.
BCG removes this constraint at eager breaks: the graph system does not need to understand how the marked function is implemented or trace through its internals, allowing graph boundaries to follow serving logic rather than compiler tracing and type requirements. As CUDA Graph had to coexist with DP attention, MoE all-to-all backends, LoRA, PD disaggregation, hierarchical cache, deterministic inference, and other rapidly evolving features, making CUDA Graph work increasingly started to feel like a torch.compile integration project. New kernels often meant custom-op registrations and fake implementations, while new features could force us to move graph boundaries simply to satisfy the compiler. With BCG, incompatible regions can remain ordinary eager execution, substantially reducing this compiler-specific engineering overhead.
Debuggable by construction. A captured CUDA Graph replays as an opaque unit: ordinary Python does not execute inside it, which makes prints, assertions, and step-by-step inspection difficult. BCG naturally leaves eager regions where normal Python still runs on every replay.
SGLang extends this idea with --debug-cuda-graph [#19102], which effectively wraps the whole forward in an eager break. The model then executes eagerly while still going through the CUDA Graph runner, static buffers, replay path, and metadata preparation. This provides a useful debugging boundary: if the problem remains, it is likely in the model or runner path; if it disappears, capture itself becomes the primary suspect.
BCG in Diffusion
BCG has also been adopted by SGLang’s diffusion stack [#27436]. Diffusion repeatedly executes the same DiT forward during denoising, making CUDA Graph especially useful when those forwards contain many small, launch-bound kernels.
- Capture the real serving shapes. Resolution, video frame count, prompt-conditioning length, CFG mode, and the selected transformer can all affect the capture signature. We warm up the shapes that are actually served and fall back to eager execution for unseen signatures.
- Break around dynamic operations. Operations such as dynamic attention and runtime-dependent metadata preparation remain eager, while BCG captures the stable computation around them without requiring torch.compile to understand the full DiT forward.
- Exploit repeated denoising structure. Diffusion repeatedly executes the same DiT structure across denoising steps. BCG captures the stable regions once and replays them throughout the denoising loop, while dynamic regions remain eager.
This is particularly effective when execution is launch-bound. For example, after warmup, Qwen-Image at 512×512 on a single B200 improves from 6.48 s to 2.45 s end-to-end latency, and Z-Image improves from 1.231 s to 0.662 s.
End-to-end latency after warmup. Each bar pair uses the same model workload and seed.
The broader lesson is that BCG removes launch overhead; it does not reduce model FLOPs or make compute-bound kernels cheaper. Its advantage is largest when exposed launch gaps are a meaningful fraction of execution time.
Full CUDA Graph for Prefill
Full CUDA Graph is straightforward for decode because each request contributes one token: the main varying dimension is batch size. Prefill is harder because a batch varies in two dimensions at once — the total number of tokens and the number of requests those tokens belong to — while a captured graph requires both to remain fixed. Together with attention backends that depend on runtime metadata, this made full CUDA Graph difficult to apply to prefill and was one of the main reasons we adopted Breakable CUDA Graph there.
More recently, we found ways to make prefill execution sufficiently static for full CUDA Graph [#27988], including restructuring how request slots and attention metadata are represented so that supported attention backends no longer have to remain outside the graph.
Making prefill static
SGLang fixes the token dimension with token buckets. A live batch is padded to the nearest captured token count, much like decode pads batch size to a captured bucket.
The request dimension is handled separately. Each captured graph reserves a fixed number of request slots. Live requests occupy the first slots; unused ones are rewritten as zero-length sentinels, with zero sequence and extend lengths and offsets parked after the real tokens. If a batch contains more requests than the graph has slots, it falls back to eager execution.
At replay, tokens are padded to the captured bucket while unused request slots are filled with zero-length sentinels.
The sentinel metadata must be rewritten on every replay because the captured graph still reads the entire request table. Attention metadata is likewise rebuilt outside the graph for the padded batch before replay. Today, full prefill capture therefore requires attention backends that support this style of metadata preparation, including FlashAttention and FlashInfer.
What does the padding cost?
The two forms of padding have very different costs.
Padded tokens are real work. They become actual rows in the captured batch and therefore pass through dense projections as part of the same GEMMs. SGLang carries the true token count separately, allowing MoE routing, attention, and linear-attention kernels to skip much of the padded region, but dense computation still pays for those extra rows.
Empty request slots are much cheaper. In FlashAttention's variable-length scheduler, work is derived from each sequence's actual length rather than assigning a fixed amount of computation to every request slot. A zero-length request therefore contributes essentially no attention work; it mainly adds metadata and a small amount of scheduling overhead.
This asymmetry is important: token padding is the expensive dimension, while request-slot padding is comparatively cheap.
Full prefill capture is still an experimental feature. It has to be enabled explicitly — the engine warns that full is experimental and points to breakable or tc_piecewise for production workloads — and it currently works mainly on the FlashAttention (fa4) and FlashInfer backends, which are the ones that build extend-mode metadata the way the captured path needs. Broadening backend support and tuning the bucket and slot choices is still ahead of us.
Prefill benchmark
With three ways to capture prefill and an eager baseline, the remaining question is what each one costs at replay. Measuring prefill on its own — a fixed input length with a single output token, one request at a time, decode graphs disabled in every arm — on gpt-oss-120b (TP4, 4×GB300), where all four paths run: full capture is 1.93× faster than eager, BCG 1.70×, and TC piecewise 1.45×, so BCG is also 17% faster than the compiler-based backend at replay, not only at build time. On GLM-5.2 only BCG can capture at all — TC piecewise cannot trace the forward and full capture has no path for its sparse attention — and it is 1.60× over eager there. Every curve is flat across a 32× range in prompt length, which is the signature of launch overhead rather than compute.
Prefill-only latency on gpt-oss-120b, where all four backends run.
Memory Footprint of CUDA Graphs
Memory poses two separate challenges: keeping a segmented capture from multiplying resident memory, and capturing far enough that resident graph memory actually replaces the worst eager activation peak.
Reuse inside a segmented capture
A segmented backend could easily multiply graph memory: every captured shape contains multiple graph segments, and each segment has intermediates that must remain valid for replay. BCG avoids that multiplication through three forms of reuse.
- One shared memory pool across segments. Every segment for a captured shape uses the same CUDA Graph pool, allowing intermediate storage to be reused rather than pinned separately for each segment.
- Weak references at eager breaks. Tensors passed into a break are held weakly when the graph pool already owns their storage, avoiding unnecessary Python references that would extend tensor lifetimes. The tensor weak-reference technique comes from vLLM [#9724], which introduced it so that captured graphs could share output buffers instead of each pinning its own.
- One output buffer across capture sizes. Capture sizes share a single maximum-sized output buffer, sliced to the rows needed by each shape, instead of allocating one output buffer per shape.
One value cannot be treated this way: the tensor that carries data across an eager break. The next graph segment is captured against its address, so that buffer must stay alive and be updated in place on every replay.
With these reuse mechanisms, even a large capture table remains modest: 42 shapes across a 78-layer MoE add 2.4 GB of graph memory on GLM-5.2.
Capture through the chunked-prefill size
CUDA Graphs change the shape of prefill memory usage. Graph memory is resident: it is allocated during capture and remains for the lifetime of the server. Eager activations are transient: each prefill allocates working memory, and the largest supported prefill determines the peak.
Capturing a prefill shape moves much of that transient working set into the graph's resident memory pool. But this only helps for shapes that actually replay a graph. If the capture ladder stops below the maximum prefill size, the largest prefill still falls back to eager execution and retains the original activation peak — while the server also pays for all of the resident graphs below it.
This makes the capture ceiling more important than the number of captured shapes. Since chunked_prefill_size bounds the largest single prefill forward, capturing through that size removes the worst eager activation peak.
Prefill memory above the no-graph resident baseline, measured after one prefill at exactly the chunked-prefill size.
Ceilings below the chunk size sit slightly *above* the no-graph baseline: they add resident graphs while the activation peak stays exactly where it was. Once the ceiling reaches the chunk size, the largest prefill finally replays a graph and that peak collapses — to essentially nothing on gpt-oss-120b (0.56 GB to 0.001 GB), and from 1.55 GB to 0.35 GB on GLM-5.2, whose sparse-attention indexer still runs eagerly at a break.
Capturing through the chunked-prefill size buys two things:
- Lower total memory. The activation peak stops being paid per request, and the total lands below the no-graph baseline — 0.51 GB lower on gpt-oss-120b, 1.10 GB on GLM-5.2. Modest against a footprint of a few hundred gigabytes, but a saving rather than a cost.
- Predictable memory usage. A workload-dependent activation spike becomes a fixed allocation established at capture time. The engine can account for that memory up front instead of reserving headroom for a transient peak that appears only during large prefills.
Acknowledgments
This work was a collaboration between the SGLang team and the Meta team.
SGLang: Yuwei An*, Cheng Wan, Xiaoyu Zhang, Mick Qian, Baizhou Zhang, Yusheng Su, Ke Bao
Meta: Shiyang Chen*, Lianmin Zheng
We also thank the NVIDIA, AMD, Thinking Machines Lab, and Meta PyTorch teams for their help along the way.
(* Equal contribution)
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み