비전 인코더

비전 인코더 (ViT) CUDA 그래프

비전-언어 모델을 서빙하다 보면 이미지 처리의 CUDA 커널 실행 오버헤드가 눈에 띄는 경우가 있어요. 특히 배치·이미지 크기가 작을 때 그렇죠. vLLM은 언어 모델(디코더) forward pass를 CUDA 그래프로 캡처하는 것에 더해, 비전 트랜스포머(인코더) forward pass도 디코더와 독립적으로 CUDA 그래프로 캡처할 수 있어요. 이 문서는 그 방법을 다뤄요.

출처: vLLM Vision Encoder (ViT) CUDA Graphs

CUDA Graphs 인프라는 주로 디코더(언어 모델) forward pass를 겨냥해요. vLLM은 인코더(비전 트랜스포머) forward pass도 디코더와 독립적으로 CUDA 그래프로 캡처를 지원해요. 이는 https://github.com/vllm-project/vllm/pull/35963을 기반으로 해요.

투 타워 비전 인코더(예: DeepSeek-OCR의 SAM + CLIP, 동적 타일링 사용)의 경우 듀얼 패스 그래프 모드가 두 개의 독립적인 CUDA 그래프 집합을 캡처해요 — 전역 이미지 경로용 하나와 로컬 패치 경로용 하나 — 그래서 경로별로 독립적인 예산 선택과 부분 eager 폴백이 가능해져요. 이는 https://github.com/vllm-project/vllm/pull/43586을 기반으로 해요.

!!! note 인코더 CUDA 그래프는 디코더 CUDA 그래프와 직교해요 — 둘 다 동시에 활성화할 수 있어요. 인코더 그래프는 비전 인코더 실행(예: Qwen3-VL의 ViT)을 캡처하고, 디코더 그래프는 CUDA 그래프 설계 문서에서 설명한 것처럼 언어 모델 실행을 캡처해요.

호환성 매트릭스

!!! note 아래 기호들의 의미는 다음과 같아요.

- ✅ = 완전 호환
- 🟠 = 부분 호환
- ❌ = 호환 없음
- ❔ = 알 수 없음 또는 TBD

모델 x 기능

Architecture Models CG for Image CG for Video Multi-Path Graph
DeepseekOCRForCausalLM DeepSeek-OCR ✅︎ ❌︎ ✅︎
Ernie4_5_VLMoeForConditionalGeneration ERNIE-4.5-VL ✅︎ ❌︎ ❌︎
Gemma3ForConditionalGeneration Gemma3 ✅︎ ❌︎ ❌︎
Glm4vForConditionalGeneration GLM-4.1V, GLM-4.6V-Flash ✅︎ ✅︎ ❌︎
Gemma4ForConditionalGeneration Gemma-4 ✅︎ ✅︎ ❌︎
InternVLChatModel InternVL3.5, InternVL3, InternVL2.5, InternVL2 ✅︎ ✅︎ ❌︎
KimiVLForConditionalGeneration Kimi-VL ✅︎ ❌︎ ❌︎
Llama4ForConditionalGeneration Llama 4 ✅︎ ❌︎ ❌︎
Qwen2VLForConditionalGeneration Qwen2-VL ✅︎ ✅︎ ❌︎
Qwen2_5_VLForConditionalGeneration Qwen2.5-VL ✅︎ ✅︎ ❌︎
Qwen3VLForConditionalGeneration Qwen3-VL ✅︎ ✅︎ ❌︎
Qwen3_5ForConditionalGeneration Qwen3.5, Qwen3.6 ✅︎ ✅︎ ❌︎
Qwen3_5MoeForConditionalGeneration Qwen3.5-MoE, Qwen3.6-MoE ✅︎ ✅︎ ❌︎
Step3VLForConditionalGeneration Step3-VL ✅︎ ❌︎ ✅︎
MiniCPMV MiniCPMV2.5 ✅︎ ❌︎ ❌︎
MiniCPMV MiniCPMV2.6, MiniCPMV4.0 ✅︎ ✅︎ ❌︎

모델 x 하드웨어

Architecture NV Blackwell NV Ampere AMD MI300X AMD MI350X / MI355X
DeepseekOCRForCausalLM ✅︎ ✅︎ ✅︎
Ernie4_5_VLMoeForConditionalGeneration ✅︎ ✅︎ ✅︎
Gemma3ForConditionalGeneration ✅︎ ✅︎ ✅︎
Glm4vForConditionalGeneration ✅︎ ✅︎ ✅︎
Gemma4ForConditionalGeneration ✅︎ ✅︎ ✅︎
InternVLChatModel ✅︎ ✅︎ ✅︎
KimiVLForConditionalGeneration ✅︎ ✅︎ ✅︎
Llama4ForConditionalGeneration ✅︎ ✅︎ ✅︎
Qwen2VLForConditionalGeneration ✅︎ ✅︎ ✅︎
Qwen2_5_VLForConditionalGeneration ✅︎ ✅︎ ✅︎
Qwen3VLForConditionalGeneration ✅︎ ✅︎ ✅︎
Qwen3_5ForConditionalGeneration ✅︎ ✅︎ ✅︎
Qwen3_5MoeForConditionalGeneration ✅︎ ✅︎ ✅︎
Step3VLForConditionalGeneration ✅︎ ✅︎ ✅︎
MiniCPMV ✅︎ ✅︎

!!! note 인코더 CUDA 그래프는 현재 Blackwell GPU에서 --mm-encoder-attn-backend=FLASH_ATTN--mm-encoder-attn-backend=FLASHINFER로 테스트됐어요. Qwen2-VL과 Qwen2.5-VL은 FA2와 FA3만 테스트됐어요. 인코더 CUDA 그래프는 AMD MI350X(gfx950)에서도 --mm-encoder-attn-backend=FLASH_ATTN(ROCm 기본값)으로 테스트됐어요.

동기 (Motivation)

비전 인코더 추론은 호스트 쪽에서 CUDA 커널 실행 오버헤드를 발생시켜요. 배치 크기나 이미지 크기가 작을수록 그 오버헤드가 더 두드러져요.

인코더 CUDA 그래프는 모델 초기화 중에 전체 인코더 forward pass를 여러 토큰 예산 레벨로 미리 캡처한 뒤, 런타임에 적절한 그래프를 리플레이해서 이 오버헤드를 제거해요.

DeepSeek-OCR(SAM + CLIP, 동적 타일링) 같은 투 타워 비전 인코더의 경우 전역 이미지 경로와 로컬 패치 경로가 독립적인 토큰 프로파일을 가져요(전역 이미지당 272 토큰 vs 로컬 패치당 100 토큰). 두 경로를 위해 단일 모놀리식 그래프를 캡처하면 패킹 효율이 크게 떨어져요. 듀얼 패스 그래프 모드는 각 경로를 별도의 예산 집합으로 캡처해서, 매니저가 각 경로를 독립적으로 패킹·리플레이할 수 있게 해요.

설계 (Design)

인코더 CUDA 그래프 시스템은 [EncoderCudaGraphManager][vllm.v1.worker.encoder_cudagraph.EncoderCudaGraphManager]가 관리하는 예산 기반 캡처/리플레이 전략을 사용해요. 시스템의 핵심 컴포넌트는 다음과 같아요.

  • [EncoderCudaGraphManager][vllm.v1.worker.encoder_cudagraph.EncoderCudaGraphManager]: 인코더 CUDA 그래프의 캡처·리플레이·greedy 패킹·데이터 병렬 실행을 조율해요.
  • [SupportsEncoderCudaGraph][vllm.model_executor.models.interfaces.SupportsEncoderCudaGraph]: 모델이 인코더 CUDA 그래프에 옵트인하도록 구현하는 런타임 검사 가능 프로토콜이에요.
  • [EncoderItemSpec][vllm.v1.worker.encoder_cudagraph_defs.EncoderItemSpec]: 입력 크기와 출력 토큰 수와 함께 단일 인코더 입력 항목(이미지 또는 비디오)을 설명해요.
  • [BudgetGraphMetadata][vllm.v1.worker.encoder_cudagraph.BudgetGraphMetadata]: 단일 토큰 예산 레벨에 대한 캡처된 CUDA 그래프와 관련 I/O 버퍼를 담아요.

예산 기반 그래프 캡처

여러 CUDA 그래프가 서로 다른 토큰 예산 레벨(예: [2048, 4096, 8192, 13824])로 미리 캡처돼요. 각 예산은 고정 토큰 용량을 정의하고, 모든 예산은 같은 최대 배치 크기(이미지 수)를 공유해요. 각 레벨의 BudgetGraphMetadata는 그래프와 미리 할당된 입력·메타데이터·출력 버퍼를 저장해요.

@dataclass
class BudgetGraphMetadata:
    token_budget: int
    max_batch_size: int
    max_frames_per_batch: int
    graph: torch.cuda.CUDAGraph
    input_buffers: dict[str, torch.Tensor]  # e.g. pixel_values, embeddings, seq metadata
    output_buffer: torch.Tensor      # encoder hidden states

예산은 get_encoder_cudagraph_budget_range()가 모델 제공 범위에서 2의 거듭제곱 레벨로 자동 생성돼요. 최대 예산은 2의 거듭제곱 경계에 떨어지지 않아도 항상 포함돼요. 예산은 CompilationConfigencoder_cudagraph_token_budgets로 사용자가 명시적으로 지정할 수도 있어요.

EncoderCudaGraphConfig.paths의 각 항목은 독립적으로 캡처되는 인코더 경로를 정의해요. 경로는 자신만의 최소 토큰 예산을 제공하고 제로 토큰 배치에 옵트인할 수 있으며, 매니저는 구성된 모든 경로에 대해 별도의 예산 그래프 집합을 생성·저장해요.

런타임에서의 Greedy bin-packing

이미지 배치가 도착하면 매니저는 이미지를 출력 토큰 수로 정렬(가장 작은 것 먼저)하고, 가장 큰 토큰 예산과 최대 배치 크기 안에 최대한 많은 이미지를 각 서브배치에 greedily 패킹해요. 서브배치가 확정되면(다음 이미지가 어느 한 제약을 넘길 때), 매니저는 서브배치의 총 토큰에 맞는 가장 작은 예산을 찾아 해당 CUDA 그래프를 리플레이해요. 이 과정을 배치가 소진될 때까지 반복해요. 모든 예산을 초과하는 이미지는 eager 실행으로 폴백해요.

멀티패스 모델의 경우 같은 greedy 패킹 루프가 구성된 모든 경로를 동시에 제약해요(Multi-Path 그래프 캡처 참고).

각 그래프 리플레이에 대해:

  1. prepare_encoder_cudagraph_replay_buffers()를 호출해 실제 배치 입력에서 버퍼 값(실제 pixel_values와 사전 계산된 메타데이터 포함)을 계산해요.
  2. 미리 할당된 input_buffers를 0으로 만들고, 리플레이 값을 슬라이스 복사해요.
  3. CUDA 그래프를 리플레이해요.
  4. output_buffer에서 출력을 클론해요(버퍼가 리플레이들 사이에 재사용되므로 클론이 필요해요).

Multi-Path 그래프 캡처

EncoderCudaGraphConfig.paths는 경로 이름을 EncoderCudaGraphPathConfig 캡처 정책에 매핑해요. 예를 들어 DeepSeek-OCR은 전역 이미지 경로와 로컬 패치 경로를 구성하고, 이들은 budget_graphs["global"]budget_graphs["local"] 아래에 독립적으로 캡처돼요.

예산 생성. 각 경로는 별도의 예산 목록을 받아요. DeepSeek-OCR의 경우:

  • global 경로 — 전역 경로 최소값에서 시작하는 2의 거듭제곱 예산(예: DeepSeek-OCR의 [272, 544, 1088, 2176, 4352, 8704, 13824]).
  • local 경로 — 로컬 경로 최소값에서 시작하는 2의 거듭제곱 예산(예: DeepSeek-OCR의 [0, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 13824]). allow_zero_tokens=True면 예산 0이 포함되어 로컬 패치가 없는 이미지(≤ 640×640, 전역 피처만 생성)를 처리해요.

두 목록 모두 같은 max_budget에서 제한돼요.

멀티패스 greedy 패킹.EncoderItemSpecpath_output_tokens을 제공해 각 경로의 항목별 기여를 매핑해요. 패킹 알고리즘은 모든 경로를 동시에 제약해요.

  • 이미지를 총 출력 토큰(전역 + 로컬)으로 정렬, 가장 작은 것 먼저.
  • Greedily 패킹: 누적된 모든 경로 토큰 수가 그 경로의 최대 예산 내에 있고, 이미지 수가 max_batch_size 이하일 때만 이미지를 현재 서브배치에 추가해요.
  • 어떤 경로 제약이라도 넘치면 서브배치를 확정하고 각 경로에 대해 독립적으로 가장 작은 맞는 예산을 찾아요.
  • 모든 이미지를 패킹할 때까지 반복해요.

부분 그래프 폴백. 각 비어 있지 않은 경로에 대해 매니저는 가장 작은 맞는 그래프를 리플레이하거나, 맞는 그래프가 없으면 그 경로만 eager로 실행해요. 토큰이 0인 경로는 건너뛰고, 0 예산 그래프는 절대 캡처·리플레이되지 않아요.

경로별 버퍼 키. 전역·로컬 경로는 다른 버퍼 키를 사용해요. DeepSeek-OCR에서 전역 경로는 pixel_values(전체 이미지, 형태 [B, 3, 1280, 1280])를, 로컬 경로는 images_crop(패치, 형태 [P, 3, 1024, 1024])을 사용해요. 매니저는 공유된 buffer_keys 목록 대신 각 캡처된 그래프의 자체 input_buffers.keys()를 반복하므로, 두 경로가 서로 다른 버퍼를 사용할 수 있어요.

후처리. postprocess_encoder_output 메서드는 경로 이름으로 키가 지정된 outputs 딕셔너리를 받아요. 모델은 전역·로컬 피처를 최종 per-image 임베딩으로 조립할 책임이 있어요. DeepSeek-OCR은 전역 출력을 [B, 272, n_embed]로, 로컬 출력을 [P, 100, n_embed]로 리셰이프하고, newline 토큰으로 패치 그리드를 조립한 뒤 각 이미지에 대해 [patches_grid, global, view_separator]를 연결해요.

!!! note 듀얼패스 설계는 부분 CUDA 그래프 커버리지를 가능하게 해요 — 한 경로는 히트하고 다른 경로는 eager로 폴백할 수 있어요. 이는 타일링되지 않은 이미지의 제로 패딩 패치 버퍼에 낭비되는 연산을 피하고, 이미지별 가변 crop_shape로 인한 그래프 무효화를 피해요.

데이터 병렬 지원

mm_encoder_tp_mode="data"일 때 매니저는 get_load_balance_assignment로 로드 밸런싱된 할당을 사용해 TP 랭크에 이미지를 분배하고, 각 랭크에서 로컬 실행한 뒤 tensor_model_parallel_all_gather로 원래 순서대로 결과를 모아요.

비디오 추론 지원

https://github.com/vllm-project/vllm/pull/35963(이미지 추론용 ViT 전체 CUDA 그래프 지원)에 이어, https://github.com/vllm-project/vllm/pull/38061은 Qwen3-VL의 비디오 추론을 지원하도록 인코더 CUDA 그래프 프레임워크를 확장해요. 이전에는 CUDA 그래프 캡처/리플레이 경로가 이미지 입력(pixel_values + image_grid_thw)만 처리했어요. 비디오 입력은 다른 키(pixel_values_videos + video_grid_thw)를 사용하고, 각 비디오 항목이 여러 프레임(T 어텐션 시퀀스)을 기여하므로 더 큰 cu_seqlens 버퍼가 필요해요. 이 PR은 프로토콜과 매니저를 일반화해 단일 공유 그래프 매니저로 두 모달리티를 모두 처리해요.

!!! note 비디오 토큰 프루닝(EVS 또는 VidCom2)이 활성화되면 비디오 CUDA 그래프가 자동으로 비활성화돼요. 프루닝 때문에 토큰 수가 데이터 의존적이 되고 CUDA 그래프 캡처와 호환되지 않기 때문이에요.

프롬프트당 혼합 입력(이미지+비디오)도 이제 지원돼요.

SupportsEncoderCudaGraph로 모델 통합

모델은 [SupportsEncoderCudaGraph][vllm.model_executor.models.interfaces.SupportsEncoderCudaGraph] 프로토콜을 구현해 인코더 CUDA 그래프에 옵트인해요. 이 프로토콜은 모든 모델별 로직을 캡슐화해서 매니저가 모델에 구애받지 않게 해요. 프로토콜이 정의하는 메서드는 다음과 같아요.

  • get_encoder_cudagraph_config() — 정적 설정(지원 모달리티, 버퍼 키, 출력 히든 크기, 패딩 로직, 비디오당 최대 프레임)을 반환해요.
  • get_encoder_cudagraph_budget_range(vllm_config) — 토큰 예산 자동 추론을 위한 (min_budget, max_budget)을 반환해요.
  • get_encoder_cudagraph_item_specs(mm_kwargs) — 입력 크기, 총 출력 토큰 수(output_tokens), 멀티패스 모델의 경로별 토큰 수(path_output_tokens)와 함께 각 항목을 설명하는 list[EncoderItemSpec]을 반환해요.
  • select_encoder_cudagraph_items(mm_kwargs, indices) — greedy 패킹과 DP 샤딩 중에 사용되는 인덱스별 항목 서브배치를 추출해요.
  • prepare_encoder_cudagraph_capture_inputs(..., path="default") — 그래프 캡처용 더미 입력을 만들어요. path 파라미터("global" 또는 "local")가 모델에 어떤 경로용 더미 입력을 생성할지 알려줘요. 그래프에 기록될 모든 버퍼를 담은 단일 values: dict[str, torch.Tensor]가 있는 EncoderCudaGraphCaptureInputs를 반환해요.
  • prepare_encoder_cudagraph_replay_buffers(mm_kwargs, max_batch_size, max_frames_per_batch, path="default") — 실제 배치 입력에서 버퍼 값을 계산해요. path 파라미터가 mm_kwargs에서 추출할 모달리티 키를 선택해요. 키가 캡처된 그래프의 input_buffers.keys()와 일치하는 values 딕셔너리가 있는 EncoderCudaGraphReplayBuffers를 반환해요.
  • encoder_cudagraph_forward(inputs: dict[str, torch.Tensor], path="default") — 고정 형태 입력 텐서(캡처된 values 딕셔너리)만 받는 forward pass예요. 캡처와 리플레이 중에 모두 호출돼요. path 파라미터가 올바른 인코더 서브모듈(예: DeepSeek-OCR의 전역 vs 로컬 경로)로 분기해요.
  • encoder_eager_forward(mm_kwargs, path="default") — 맞는 그래프가 없을 때의 폴백 eager forward예요. path"global" 또는 "local"이면 그래프 캡처 없이 그 인코더 경로만 실행해요.
  • postprocess_encoder_output(outputs, ...) — 경로 이름으로 키가 지정된 인코더 출력을 후처리해 멀티패스 모델이 경로별 피처를 최종 per-item 임베딩으로 조립할 수 있게 해요.

!!! note SupportsEncoderCudaGraph 프로토콜은 모델에 구애받지 않도록 설계됐어요. 새 비전 인코더 모델은 매니저를 수정하지 않고 프로토콜 메서드를 구현해 옵트인할 수 있어요.

설정 (Configuration)

CompilationConfig의 네 필드가 인코더 CUDA 그래프를 제어해요.

  • cudagraph_mm_encoder (bool, 기본 False) — 멀티모달 인코더의 CUDA 그래프 캡처를 활성화해요. 활성화하면 각 토큰 예산 레벨에 대해 전체 인코더 forward를 CUDA 그래프로 캡처해요.
  • encoder_cudagraph_token_budgets (list[int], 기본 []) — 캡처용 토큰 예산 레벨이에요. 비어 있으면(기본) 모델 아키텍처에서 2의 거듭제곱 레벨로 자동 추론돼요. 사용자 제공 값이 자동 추론을 오버라이드해요.
  • encoder_cudagraph_max_vision_items_per_batch (int, 기본 0) — 캡처 중 배치당 최대 이미지/비디오 수예요. 0(기본)이면 max_budget // min_budget으로 자동 추론돼요.
  • encoder_cudagraph_max_frames_per_batch (int, 기본 None) — 캡처 중 배치당 최대 비디오 프레임 수예요. None(기본)이면 encoder_cudagraph_max_vision_items_per_batch * max_frames_per_video로 자동 추론돼요(max_frames_per_video는 모델의 get_max_frames_per_video()로 계산되는 EncoderCudaGraphConfig의 모델별 값). 프롬프트당 비디오 수를 0으로 제한하면 이것도 0으로 설정돼요(즉 이미지 전용 모드로 폴백).

멀티패스 모드는 모델 레벨에서 EncoderCudaGraphConfig.paths로 구성돼요. 각 EncoderCudaGraphPathConfig는 경로별 최소 예산과 제로 토큰 배치 허용 여부를 설정할 수 있어요. 매니저는 단일·멀티패스 모델 모두에 대해 별도 예산 목록을 자동 생성하고 같은 실행 루프를 사용해요.

사용 가이드

이미지 추론

compilation_config로 인코더 CUDA 그래프를 활성화해요.

vllm serve Qwen/Qwen3-VL-32B \
  --compilation-config '{"cudagraph_mm_encoder": true}'

Llama 4(이미지 전용)의 경우:

vllm serve meta-llama/Llama-4-Scout-17B-16E-Instruct \
  --limit-mm-per-prompt '{"image": 1}' \
  --compilation-config '{"cudagraph_mm_encoder": true}'

명시적 예산 사용:

vllm serve Qwen/Qwen3-VL-32B \
  --compilation-config '{"cudagraph_mm_encoder": true, "encoder_cudagraph_token_budgets": [2048, 4096, 8192, 13824], "encoder_cudagraph_max_vision_items_per_batch": 8}'

파이썬 예시:

import vllm

compilation_config = {
    "cudagraph_mm_encoder": True,
    # Optional: override auto-inferred budgets
    # "encoder_cudagraph_token_budgets": [2048, 4096, 8192, 13824],
    # "encoder_cudagraph_max_vision_items_per_batch": 8,
}

model = vllm.LLM(
    model="Qwen/Qwen3-VL-32B",
    compilation_config=compilation_config,
)

매니저는 히트/미스 통계를 추적하고 주기적으로 로깅해요. "히트"는 이미지가 CUDA 그래프 리플레이로 처리됐다는 뜻이고, "미스"는 eager 폴백(이미지가 모든 예산을 초과)을 뜻해요.

비디오 추론

compilation_config로 인코더 CUDA 그래프를 활성화해요.

vllm serve Qwen/Qwen3-VL-32B \
  --compilation-config '{"cudagraph_mm_encoder": true}'

명시적 예산 사용:

vllm serve Qwen/Qwen3-VL-32B \
  --compilation-config '{"cudagraph_mm_encoder": true, "encoder_cudagraph_token_budgets": [2048, 4096, 8192, 13824], "encoder_cudagraph_max_vision_items_per_batch": 8, "encoder_cudagraph_max_frames_per_batch": 64}'

파이썬 예시:

import vllm

compilation_config = {
    "cudagraph_mm_encoder": True,
    # Optional: override auto-inferred budgets
    # "encoder_cudagraph_token_budgets": [2048, 4096, 8192, 13824],
    # "encoder_cudagraph_max_vision_items_per_batch": 8,
    # "encoder_cudagraph_max_frames_per_batch": 64,
}

model = vllm.LLM(
    model="Qwen/Qwen3-VL-32B",
    compilation_config=compilation_config,
)

벤치마크 결과

다음 벤치마크는 vllm bench mm-processor로 Blackwell GPU(GB200)에서 실행됐어요. 전체 세부사항은 #35963를 참고하세요.

단일 GPU (1x GB200)

모델: Qwen/Qwen3-VL-30B-A3B-Instruct, 데이터셋: lmarena-ai/VisionArena-Chat(3000 프롬프트, 300 워밍업), max_model_len=32768.

Backend 평균 지연시간 개선 P99 지연시간 개선
FLASH_ATTN +11.8% (5.13→4.52ms) +31.6% (9.16→6.26ms)
FLASHINFER +19.6% (5.42→4.36ms) +40.3% (10.87→6.49ms)

재현하려면:

vllm bench mm-processor \
  --model Qwen/Qwen3-VL-30B-A3B-Instruct \
  --dataset-name hf --dataset-path lmarena-ai/VisionArena-Chat \
  --num-prompts 3000 --num-warmups 300 \
  --max-model-len 32768 --seed 42 \
  --mm-encoder-attn-backend FLASH_ATTN \
  --compilation-config '{"cudagraph_mm_encoder": true, "encoder_cudagraph_token_budgets": [512, 1024, 1536, 2048, 2560, 3072, 3584, 4096, 4864], "encoder_cudagraph_max_vision_items_per_batch": 8}'

멀티 GPU (4x GB200, TP=4, DP=4)

모델: Qwen/Qwen3-VL-32B-Instruct, 데이터셋: random-mm(1000 프롬프트, 200 워밍업, 요청당 20개 이미지 at 336x336), max_model_len=8192.

Backend 평균 지연시간 개선 P99 지연시간 개선
FLASH_ATTN +18.4% (28.39→23.16ms) +14.0% (238.78→205.28ms)
FLASHINFER +44.4% (23.24→12.91ms) +84.9% (172.41→26.05ms)

재현하려면:

vllm bench mm-processor \
  --model Qwen/Qwen3-VL-32B-Instruct \
  --dataset-name random-mm \
  --random-mm-base-items-per-request 20 \
  --random-mm-num-mm-items-range-ratio 0.0 \
  --random-mm-bucket-config '{"(336,336,1)": 1.0}' \
  --num-prompts 1000 --num-warmups 200 \
  --max-model-len 8192 --seed 42 \
  --mm-encoder-attn-backend FLASHINFER \
  --tensor-parallel-size 4 --mm-encoder-tp-mode data \
  --compilation-config '{"cudagraph_mm_encoder": true, "encoder_cudagraph_token_budgets": [512, 1024, 1536, 2048, 2560, 3072, 3584, 4096, 4864], "encoder_cudagraph_max_vision_items_per_batch": 8}'

!!! note 비디오 추론용 GPU(A100) 벤치마크에 대한 더 자세한 내용은 #38061에서 확인하세요.

더 알아보기 (Learn more)