Repository navigation
[Feat] [AMD] vision: Support ViT CUDA graph with the aiter_attn backend - #44727
Draft
vorapolsiloai wants to merge 9 commits into
Draft
vorapolsiloai wants to merge 9 commits into
vorapolsiloai wants to merge 9 commits into
Conversation
vorapolsiloai
force-pushed
the
feat/rocm-vit-graph-aiter
branch
from
October 23, 2026 12:54
3543d8a to
9989742
Compare
Contributor
Preview approval requiredThis pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit |
On ROCm with SGLANG_USE_AITER=1 the vision encoder resolves to aiter_attn, which ViTCudaGraphRunner rejected at capture time, so SGLANG_VIT_ENABLE_CUDA_GRAPH=1 could not be used on AMD GPUs. - ViTCudaGraphRunner: warm up and capture aiter_attn with the [cu_seqlens, max_len] layout already used for FA3/FA4. - VisionAiterAttention: take cu_seqlens and max_seqlen from that list as FA4 does, so replay does no host sync (.item()); tensor callers keep the existing eager path.
…ViT graph path forward_with_cuda_graph interpolated the position embeddings with fast_pos_embed_interpolate, which follows enable_precise_embedding_interpolation (half-pixel coordinates by default), while the eager forward always uses the linspace interpolation of fast_pos_embed_interpolate_from_list / _vectorized. Turning SGLANG_VIT_ENABLE_CUDA_GRAPH on therefore changed the image embeddings. Use the same interpolation as eager in both paths.
The ViT graph runner copied the model's rotary cos/sin into workspaces of the ViT dtype. Qwen3-VL computes them in fp32, so in graph mode they were rounded to bf16 before the rotation while eager mode rotates with fp32 values. Allocate the workspace with the dtype of the cos/sin the model passes.
With SGLANG_VIT_ENABLE_CUDA_GRAPH set, VisionAttention rotates q/k with the eager rotary helper while eager mode uses the torch.compile'd one, so graph and eager outputs differ in the last bits. On ROCm the compiled helper captures cleanly after the runner's eager block-0 warmup, so use it there and keep graph and eager rotations identical. Other platforms keep the eager helper that avoids compiling inside the capture.
ViT graphs are captured on the first request of each shape, while the process is already serving. Other threads of the process, such as the c10d NCCL watchdog polling in-flight collectives, keep calling the CUDA/HIP runtime during that capture, and the default global capture mode turns those calls into a capture error. Capture in thread_local mode, as the metadata-glue and DSpark sampling graphs already do.
Each ViT graph was captured into its own private memory pool. Graphs are keyed on the exact image layout, so reserved memory grew with every new image size. Capture all of them into one pool, as the LLM graph runners and the Kimi-K3 ViT graph runner do. Sharing is safe because replays are serialized on one stream and replay() returns a copy of the graph output, so a later replay of another graph cannot overwrite an output a caller still holds. The copy comes from sgl-project#43315, which this change depends on.
ViT graphs are keyed on the exact attention layout (sequence length plus segment boundaries), so every new image size or batch composition captures another graph and allocates its input, workspace and output buffers; the cache had no limit. Add SGLANG_VIT_CUDA_GRAPH_CACHE_CAPACITY. Once that many graphs are captured, new layouts run the same blocks + merger code eagerly, with the arguments a graph would use and without per-layout buffers; captured layouts keep replaying. Unset keeps the current unbounded behavior. The block loop moves into _forward_blocks so capture and the uncaptured path share one implementation.
The ViT graph path now uses the same interpolation as eager, which was its last caller.
Three image sizes with up to 8 images per ViT call need 18 graphs per rank, so 32 leaves headroom while keeping arbitrary image sizes in real traffic from capturing without bound. Each graph adds only its input, output and attention-workspace buffers; activations share one memory pool.
vorapolsiloai
force-pushed
the
feat/rocm-vit-graph-aiter
branch
from
October 26, 2026 23:15
bce80b8 to
882c41f
Compare
This was referenced Oct 31, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
On ROCm with
SGLANG_USE_AITER=1, the vision encoder resolves to theaiter_attnbackend.ViTCudaGraphRunneronly knew thetriton_attn,fa3andfa4argument layouts, andVisionAiterAttentioncould not take the runner's list argument. WithSGLANG_VIT_ENABLE_CUDA_GRAPH=1, the first graph warmup/capture therefore failed. As a result, ViT CUDA graphs could not be used on AMD Instinct GPUs. The target workload is Qwen3-VL-family models (Qwen3.8-27B) on MI355X, where image requests are short-output and spend much of their time in the encoder.Once the graph ran, its output was not identical to eager mode, it could fail to capture while the server was busy, and its memory grew with every new image layout. The follow-up commits fix those so the graph is a drop-in for eager.
Modifications
Rebased onto current
main.aiter_attn support (first commit)
ViTCudaGraphRunner: warm up and captureaiter_attnwith the[cu_seqlens, max_len]layout already used for FA3/FA4.VisionAiterAttention.forward: when the runner passes[cu_seqlens, max_seqlen], use them directly, asVisionFlash4Attentiondoes. Graph replay then does no.item()host sync. Tensor callers, including models without a graph runner while the env var is set, keep the existing eager path unchanged.Graph output == eager output
forward_with_cuda_graphinterpolated withfast_pos_embed_interpolate, which followsenable_precise_embedding_interpolation(half-pixel coordinates by default), while the eager forward always uses the linspace interpolation. The graph path now uses the same interpolation as eager, and the Qwen3-VLfast_pos_embed_interpolateit called, now unused, is removed.VisionAttentionused the eager rotary helper while eager mode uses thetorch.compiled one, so outputs differed in the last bits. On ROCm the compiled helper captures cleanly after the runner's eager block-0 warmup, so it is used in both modes there. Other platforms keep the eager helper from fix: fix vlm cuda graph shape stability #30868 (compiling inside the capture failed there); this PR does not change their behavior.Robustness and memory
capture_error_mode="thread_local".SGLANG_VIT_CUDA_GRAPH_CACHE_CAPACITY(default 32) caps the number of captured graphs; later layouts run the same blocks + merger code eagerly (shared_forward_blockshelper, no per-layout buffers), and captured layouts keep replaying. Sizing: a sweep of three image sizes (512², 768×1024, 2048×1600) with up to 8 images per ViT call needs 18 graphs per rank; each graph adds only its input, output and attention-workspace buffers (about 7 KB per patch for a 1152-wide encoder, about 180 MB for the largest layout), while activations share one pool. The model eager path (SGLANG_VIT_ENABLE_CUDA_GRAPHunset) is unchanged.Tests and docs
aiter_attnin the backend layout list and the cache capacity incuda_graph_for_multi_modal_encoder.mdx; the new env var inenvironment_variables.mdx.ViT CUDA graphs stay opt-in (
SGLANG_VIT_ENABLE_CUDA_GRAPH, default off).Dependency: #43315 (still open). Graph replay returns the graph's static output buffer, which a later replay can overwrite. #43315 (return an owned output from ViT graph replays) fixes this for all backends. This PR does not duplicate it. The shared graph pool depends on it more strongly: with one pool, a replay of a different graph can also reuse that memory, so the pool commit is only safe with #43315 applied. All validation of this PR is done with #43315 applied, and it should merge after #43315.
Accuracy Tests
On an earlier internal MI355X build with older
main+ #43315 + this PR's changes except the cache bound, the ViT graph output was bit-exact vs eager in 148 of 148 checks on Qwen3.8-27B.To be re-run on this head (MI355X): ViT graph vs eager output for several image sizes and batch compositions, plus MMMU with the graph off/on.
Speed Tests and Profiling
On that earlier build, enabling the ViT graph raised image-serving throughput on MI355X full sweeps (Qwen3.8-27B) by about +8% at TP1 and +10.5% at TP2. The TP2 build also carried a separate batched-ViT change that will be its own PR.
MI355X numbers to be re-measured on this head: image serving rows (
bench_serve, random images 512² / 768×1024 / 2048×1600, OSL 64) with the graph off/on, plus capture count and memory at high concurrency with and withoutSGLANG_VIT_CUDA_GRAPH_CACHE_CAPACITY.Checklist
CI States
Latest PR Test (Base): ❌ Run #40418501314
Latest PR Test (Extra): ❌ Run #40418501286
Latest PR Test (AMD ROCm 10): ❌ Run #40418501264