Skip to content

[MM][CG] Support ViT full CUDA graph for Ernie-4.5-VL image inference - #45254

Merged
Isotr0py merged 16 commits into
vllm-project:mainfrom
qyYue1389:ernie45vl-encoder-cudagraph
Aug 6, 2026
Merged

[MM][CG] Support ViT full CUDA graph for Ernie-4.5-VL image inference#45254
Isotr0py merged 16 commits into
vllm-project:mainfrom
qyYue1389:ernie45vl-encoder-cudagraph

Conversation

@qyYue1389

@qyYue1389 qyYue1389 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Purpose

Adds encoder (ViT) CUDA graph support for Ernie4_5_VLMoeForConditionalGeneration (image inputs), under the ViT Full CUDA Graph tracker #38175. Follows the Qwen3-VL SupportsEncoderCudaGraph reference.

Key Changes

  • Splits the inline rotary / cu_seqlens / max_seqlen computation out of Ernie4_5_VisionTransformer.forward into prepare_encoder_metadata(), so it can be precomputed on the host and fed into the captured graph via fixed buffers. Ernie consumes a single rotary freqs tensor, so the buffer set is 4 keys (pixel_values, rotary_pos_emb, cu_seqlens, max_seqlen)

  • Capture boundary (ViT only) Ernie's spatial-merge & projection live in a separate VariableResolutionResamplerModel whose temporal path does host-side work (grid_thw.cpu().numpy() & dynamic index_select) that can't be captured. The resampler therefore runs eagerly in postprocess_encoder_output (graph path) and encoder_eager_forward (eager fallback)

  • Image only Video has use_temporal_conv in the resampler that changes the output token count (code), which needs separate handling. A separate PR is needed for video and will be raised after this one is merged.

Test

1. Functional

  • Added ernie45_vl (image, load_format=dummy) to tests/models/multimodal/generation/test_vit_cudagraph.py. pytest tests/models/multimodal/generation/test_vit_cudagraph.py::test_vit_cudagraph_image[ernie45_vl] - E2E encoder CG capture & replay & eager-resampler postprocess
# pytest tests/models/multimodal/generation/test_vit_cudagraph.py::test_vit_cudagraph_image[ernie45_v
l]
============================================================ test session starts ============================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /workspace/vllm
configfile: pyproject.toml
plugins: anyio-4.13.0, timeout-2.4.0, asyncio-1.4.0
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 1 item                                                                                                                            

tests/models/multimodal/generation/test_vit_cudagraph.py .                                                                            [100%]

============================================================= warnings summary ==============================================================
...
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================================= 1 passed, 17 warnings in 78.43s (0:01:18) =================================================

2. Benchmark (H100 80GB)

# CUDA graph
vllm bench mm-processor \
  --model baidu/ERNIE-4.5-VL-28B-A3B-PT \
  --trust-remote-code \
  --max-model-len 4096 \
  --dataset-name random-mm \
  --random-mm-base-items-per-request 1 \
  --random-mm-num-mm-items-range-ratio 0.0 \
  --random-mm-bucket-config '{(224, 224, 1): 1.0}' \
  --random-mm-limit-mm-per-prompt '{"image": 1, "video": 0}' \
  --num-prompts 1000 --seed 42 \
  --compilation-config '{"cudagraph_mm_encoder": true, "encoder_cudagraph_token_budgets": [128, 256, 512, 1024, 1536, 2048], "encoder_cudagraph_max_vision_items_per_batch": 4}'

# eager baseline
vllm bench mm-processor \
  --model baidu/ERNIE-4.5-VL-28B-A3B-PT \
  --trust-remote-code \
  --max-model-len 4096 \
  --dataset-name random-mm \
  --random-mm-base-items-per-request 1 \
  --random-mm-num-mm-items-range-ratio 0.0 \
  --random-mm-bucket-config '{(224, 224, 1): 1.0}' \
  --random-mm-limit-mm-per-prompt '{"image": 1, "video": 0}' \
  --num-prompts 1000 --seed 42 \
encoder_forward_ms Mean Median Std P99
eager 20.19 19.89 1.69 30.88
CUDA graph 5.33 5.31 0.22 5.77
speedup 3.79× 3.74× 5.35×

3. Accuracy (CG on vs off)

CG toggled via compilation_config={"cudagraph_mm_encoder": ...} on the same PR branch with lmms-eval and greedy decoding

benchmark CG ON CG OFF Δ
AI2D 0.6548 ± 0.0086 0.6564 ± 0.0085 ≈0.2σ
MMStar 0.1241 ± 0.0088 0.1284 ± 0.0089 ≈0.5σ

Both within stderr and show enabling the ViT CUDA graph does not change accuracy.

Output parity (comparing raw outputs directly): greedy-decoded 50 images with the encoder CUDA graph ON vs OFF: 47/50 (94%) byte-identical outputs. The 3 diffs are single-letter flips on near-tie questions, expected since the graph replays from padded fixed buffers (numerically close, not bit-identical). The ViT CUDA graph does not change the model's outputs.

(MMStar's absolute is low for both arms, but that's the benchmark, not a vision/setup issue. The same model scores AI2D 0.65 in this setup, so vision is fine. MMStar's fine-grained multiple-choice is hard for Ernie here. It affects eager and CG identically, so it doesn't affect the accuracy comparison.)


@mergify

mergify Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--45254.org.readthedocs.build/en/45254/

@mergify mergify Bot added documentation Improvements or additions to documentation multi-modality Related to multi-modality (#4194) nvidia labels Jun 11, 2026
@qyYue1389
qyYue1389 marked this pull request as ready for review June 11, 2026 09:04
@qyYue1389

Copy link
Copy Markdown
Contributor Author

Hi @shen-shanshan, could you also review when getting a chance? Thanks!

@Isotr0py

Copy link
Copy Markdown
Member

Can you verify the multimodal accuracy with ViT CG as well?

@qyYue1389

qyYue1389 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @Isotr0py!

Finished verification on ERNIE-4.5-VL-28B-A3B on H100 with benchmarks AI2D and MMStar, toggling the ViT CUDA graph via compilation_config={"cudagraph_mm_encoder": ...}

Both within stderr and show enabling the ViT CUDA graph does not change accuracy
Updated details in the PR description

@qyYue1389

Copy link
Copy Markdown
Contributor Author

Hi @shen-shanshan @Isotr0py, could you review the PR when getting a chance? Thanks!

@mergify

mergify Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @qyYue1389.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jun 16, 2026
@qyYue1389
qyYue1389 force-pushed the ernie45vl-encoder-cudagraph branch from 59d5051 to 2f0cc33 Compare June 17, 2026 06:11
@mergify mergify Bot removed the needs-rebase label Jun 17, 2026
@qyYue1389
qyYue1389 force-pushed the ernie45vl-encoder-cudagraph branch 2 times, most recently from c44a606 to 54f466a Compare June 17, 2026 06:16

@shen-shanshan shen-shanshan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments and questions.

| Architecture | Models | CG for Image | CG for Video | Dual-Path Graph |
| ------------ | ------ | ------------ | ------------ | --------------- |
| `DeepseekOCRForCausalLM` | `DeepSeek-OCR` | ✅︎ | ❌︎ | ✅︎ |
| `Ernie4_5_VLMoeForConditionalGeneration` | `ERNIE-4.5-VL` | ✅︎ | ❌︎ | ❌︎ |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, this model supports video inference, does this PR support?

@qyYue1389 qyYue1389 Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it supports video inference, but this PR supports image only for now

ERNIE's video path applies a use_temporal_conv in the resampler that changes the output token count (code), which needs separate handling. Thus I plan to work on it as a follow-up PR

This PR only adds the image encoder CUDA graph, and Eager video inference is unaffected. Thus ❌ in the Video column for now.

Does the follow-up PR plan sound good to you or any suggestions? Thanks!

Comment on lines +1606 to +1609
def get_input_modality(self, mm_kwargs: dict[str, Any]) -> str:
if "image_grid_thw" in mm_kwargs:
return "image"
raise AssertionError("This line should be unreachable.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it only supports image modality, this method is not needed, FYI, #44484.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing out! Removed

Comment on lines +1621 to +1628
def _get_pixel_values_by_modality(self, mm_kwargs: dict[str, Any]) -> torch.Tensor:
return mm_kwargs["pixel_values"]

def _get_grid_thw_by_modality(self, mm_kwargs: dict[str, Any]) -> list[list[int]]:
grid_thw = mm_kwargs["image_grid_thw"]
if not isinstance(grid_thw, list):
grid_thw = grid_thw.tolist()
return grid_thw

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing out!

grid_thw_list = self._get_grid_thw_by_modality(batch_mm_kwargs)
num_valid = sum(t * h * w for t, h, w in grid_thw_list)
grid_thw = torch.tensor(grid_thw_list, device=output.device)
image_embeds = self.resampler_model(output[:num_valid], grid_thw)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In VariableResolutionResamplerModel, if use_temporal_conv=False, I suppose it's CUDA graph compatible? Since it only contains linear layer and activation layer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. If use_temporal_conv=False the resampler is just linear and activation and would be CUDA graph compatible.

However ERNIE-4.5-VL-28B-A3B ships with use_temporal_conv=True from Ernie4_5_VLMoEConfig.__init__ in the model's bundled configuration_ernie4_5_vl.py
With that flag, VariableResolutionResamplerModel.forward always takes the fwd_placeholder path. Hence only ViT is put into the CUDA graph in this PR and the resampler stays on the eager path

Including the resampler in the CUDA graph for use_temporal_conv=False could be done in the follow-up PR

Does it sound good or any suggestions?

@qyYue1389

Copy link
Copy Markdown
Contributor Author

Hi @shen-shanshan, thank you for your feedback. I finished adding changes and reply. Could you review again when getting a chance? Thank you!

@mergify

mergify Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @qyYue1389.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jun 29, 2026
Signed-off-by: Qiuyang Yue <yueqiuyang1389@gmail.com>
Signed-off-by: Qiuyang Yue <yueqiuyang1389@gmail.com>
Signed-off-by: Qiuyang Yue <yueqiuyang1389@gmail.com>
…iuyang1389@gmail.com>

Signed-off-by: Qiuyang Yue <yueqiuyang1389@gmail.com>
Signed-off-by: Qiuyang Yue <yueqiuyang1389@gmail.com>
@qyYue1389
qyYue1389 force-pushed the ernie45vl-encoder-cudagraph branch from fd2210f to 7a82531 Compare June 29, 2026 18:32
@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Jul 13, 2026
Signed-off-by: Qiuyang Yue <yueqiuyang1389@gmail.com>
Signed-off-by: qyYue1389 <yueqiuyang1389@gmail.com>
Signed-off-by: qyYue1389 <yueqiuyang1389@gmail.com>
Signed-off-by: qyYue1389 <yueqiuyang1389@gmail.com>
Signed-off-by: Qiuyang Yue <yueqiuyang1389@gmail.com>
@qyYue1389

qyYue1389 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the approval @Isotr0py!
The remaining failing CI is due to infra, and I can't seem to clear it by pushing from my fork - builds failed immediately at bootstrap with:
ValueError: Invalid branch name: qyYue1389:ernie45vl-encoder-cudagraph. Contains disallowed characters.

It looks like the fork PR's owner:branch ref (the :) trips the bootstrap branch-name check, so retriggering by pushing to my fork just re-hits this.
Could you help kick off a run from Buildkite (or maybe send it through the merge queue) when getting a chance? Thanks a lot!

@qyYue1389

Copy link
Copy Markdown
Contributor Author

Hi @Isotr0py, thanks for re-triggering the CI. Looks both CI failures are in test_can_initialize_large_subset on GLM-4.7-Flash / Glm4MoeLite models, which should be unrelated to this PR changes

Could you please rerun the two failing initialization jobs or maybe merge when getting a chance? Thanks a lot!

@qyYue1389

qyYue1389 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Isotr0py, sorry looks the CI failed again. Both failures are in Mamba and sampling test areas and involve pfnet/plamo-2-1b / facebook/opt-125m, none of which is related to this PR. Probably they are flaky tests?

Could you please retry the two failing jobs (or re-run after a merge with main)?

Pls let me know if we need to rebase this PR from main

Thank you so much!

@qyYue1389

Copy link
Copy Markdown
Contributor Author

Hi @shen-shanshan, sorry the CI failed again. Both failures are in Mamba and sampling test areas and involve pfnet/plamo-2-1b / facebook/opt-125m, looks none of which is related to this PR. Probably they are flaky tests?

Could you please retry the two failing jobs and help merge (or re-run after a merge with main)?

Pls let me know if we need to rebase this PR from main

Thank you so much!

@Isotr0py

Isotr0py commented Aug 5, 2026

Copy link
Copy Markdown
Member

/ci retry

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

✅ No earlier CI build exists for this PR. Use /ci run first.

@qyYue1389

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #82516 for commit 729d0ba7a726.

@qyYue1389

Copy link
Copy Markdown
Contributor Author

/ci retry

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

✅ Queued 2 failed job(s) for retry in Buildkite CI #82516.

@qyYue1389

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #82578 for commit 1404260e950e.

@qyYue1389

Copy link
Copy Markdown
Contributor Author

/ci retry

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

✅ Queued 4 failed job(s) for retry in Buildkite CI #82578.

@Isotr0py
Isotr0py merged commit 777b01d into vllm-project:main Aug 6, 2026
98 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in NVIDIA Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation multi-modality Related to multi-modality (#4194) nvidia ready ONLY add when PR is ready to merge/full CI is needed

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants