FP8 ViT 인코더 어텐션
FP8 ViT 인코더 어텐션 (FP8 ViT Encoder Attention)
문자가 아닌 이미지를 이해하는 비전(vision) 모델을 돌릴 때, 큰 이미지를 처리하는 ViT(Vision Transformer) 인코더의 어텐션 연산이 어느 순간 병목이 되는 경우가 있어요. 특히 이미지가 QHD·4K처럼 고해상도면서 텍스트 프롬프트와 생성이 짧은 작업, 그리고 텍스트 모델 자체를 양자화(예: NVFP4)한 경우에 더 두드러지죠. vLLM이 이 ViT 인코더 어텐션에 선택적으로 FP8 양자화를 적용하는 방법을 설명드릴게요.
이 기능이 필요한 상황 (Why)
ViT 인코더 어텐션의 연산량은 이미지 해상도에 따라 급격히 늘어나요. 큰 이미지를 여러 장 처리하는 작업에서는 이 인코더가 전체 파이프라인에서 상당한 시간을 차지하죠. vLLM은 NVIDIA GPU에서는 FlashInfer cuDNN, AMD GPU에서는 AITER 백엔드를 통해 이 어텐션을 FP8로 양자화해요. 어텐션을 호출하기 직전에 Q/K/V가 그때그때(on-the-fly) FP8로 변환됩니다.
다만 주의할 점이 있어요. 아직 Qwen3-VL 계열 모델에서만 지원돼요(qwen3_vl, qwen3_vl_moe, qwen3_5, qwen3_5_moe, 그리고 Qwen3 ViT를 쓰는 다른 모델들). 또 동적 스케일링은 ViT 전체 CUDA 그래프와는 함께 쓸 수 없고, 성능 이득은 대부분 QHD/4K 해상도나 멀티 이미지 요청에서만 눈에 띕니다. 작은 이미지는 양자화 오버헤드(양자화 커널 3회 실행 + 언패딩) 때문에 오히려 속도 향상이 없을 수도 있어요.
요구 사항 (Requirements)
- NVIDIA: cuDNN ≥ 9.17.1을 갖춘 FlashInfer cuDNN 백엔드.
- AMD: gfx942(MI300 시리즈) 또는 gfx950(MI350 시리즈)에서
flash_attn_varlen_fp8_pertensor_func를 지원하는 AITER.
사용법 (Usage)
--mm-encoder-attn-dtype fp8 플래그를 넘기고, 현재 플랫폼에 맞는 백엔드를 지정하면 돼요.
vllm serve $MODEL \
--mm-encoder-attn-backend FLASHINFER \
--mm-encoder-attn-dtype fp8
# AMD ROCm
vllm serve $MODEL \
--mm-encoder-attn-backend ROCM_AITER_FA \
--mm-encoder-attn-dtype fp8
스케일 파일을 따로 주지 않으면 기본적으로 동적 스케일링(dynamic scaling) 이 동작해요. 관찰된 Q/K/V의 amax 값을 담는 16개 항목의 순환 버퍼(circular buffer)가 포워드마다 스케일을 갱신하는 방식이죠. 별도 캘리브레이션 없이도 BF16 수준 정확도를 유지하지만, 포워드마다 약간의 오버헤드가 붙습니다.
캘리브레이션 후 재사용 워크플로 (Calibrate-Once, Reuse)
운영 환경이라면 대표 데이터셋으로 정적 스케일을 한 번 캘리브레이션해 두고 재사용하는 걸 권장해요. 그러면 동적 방식의 오버헤드를 피할 수 있죠.
# Step 1: 스케일 캘리브레이션 후 저장 (동적 스케일링으로 16번 실행한 뒤,
# 학습한 스케일을 JSON으로 덤프)
vllm bench mm-processor \
--model $MODEL --mm-encoder-attn-backend $MM_ATTN_BACKEND \
--mm-encoder-attn-dtype fp8 \
--mm-encoder-fp8-scale-save-path /path/to/scales.json \
--dataset-name hf --dataset-path lmarena-ai/VisionArena-Chat \
--num-prompts 100
# Step 2: 정적 스케일로 서빙 (동적 오버헤드 없음)
vllm serve $MODEL \
--mm-encoder-attn-backend $MM_ATTN_BACKEND \
--mm-encoder-attn-dtype fp8 \
--mm-encoder-fp8-scale-path /path/to/scales.json
저장된 스케일은 --mm-encoder-fp8-scale-save-margin(기본값 1.5)만큼 곱해져서, 캘리브레이션 세트에 없던 활성화 이상값(outlier)에 대비한 여유분을 남겨요. 이 기본값은 데이터셋을 넘어 잘 일반화되는 것으로 검증됐다고 하네요(예: VisionArena-Chat 캘리브레이션이 ChartQA에서도 BF16 정확도를 유지).
스케일 파일 형식 (Scale File Format)
{
"visual.blocks.0.attn.attn": {"q": 224.0, "k": 198.0, "v": 210.0},
"visual.blocks.1.attn.attn": {"q": 218.0, "k": 195.0, "v": 207.0}
}
q_scale / k_scale / v_scale 키를 별명(alias)으로 써도 돼요.
성능 (Performance)
직접 성능 수치를 보면 체감이 정확해요. 핵심 cuDNN 어텐션 커널(PyTorch 프로파일러 기준, head_dim=128, seq_len=8192):
| 하드웨어 | BF16 | FP8 | Speedup |
|---|---|---|---|
| GB200 | 350 us | 312 us | 1.12x |
| GB300 | 300 us | 211 us | 1.42x |
엔드투엔드 인코더 포워드 시간(Qwen3-VL-30B-A3B-Instruct, GB200, 요청당 이미지 3장):
| 해상도 | BF16 중앙값 | FP8 중앙값 | Speedup |
|---|---|---|---|
| HD (720x1280) | 31.77 ms | 36.39 ms | 0.87x |
| FullHD (1080x1920) | 57.99 ms | 58.73 ms | ~동일 |
| QHD (1440x2560) | 131.83 ms | 122.30 ms | 1.08x |
| 4K (2160x3840) | 543.44 ms | 460.31 ms | 1.18x |
이미지 3장 기준 FullHD 근처에서 교차점이 생기고, QHD 이상에서는 FP8이 이겨요. FP8 텐서코어의 이득은 GB200보다 GB300에서 더 두드러지게 나타나죠.
정확도 (Accuracy)
ChartQA, Qwen3-VL-8B-Instruct, 500개 샘플 실험에서 FP8 정적 스케일은 VisionArena-Chat으로 캘리브레이션한 값(기본 1.5x 여유)을 썼어요:
| 메트릭 | BF16 | FP8 동적 | FP8 정적 |
|---|---|---|---|
| relaxed_accuracy | 0.780 | 0.776 | 0.780 |
| anywhere_accuracy | 0.806 | 0.816 | 0.814 |
| exact_match | 0.584 | 0.582 | 0.578 |
세 구성 모두 통계적 오차 범위 안에서 일치하는 걸 확인할 수 있어요. 한 데이터셋에서 캘리브레이션한 정적 스케일이 다른 데이터셋으로도 잘 일반화된다는 뜻이죠.