FP8 ViT 인코더 어텐션

FP8 ViT 인코더 어텐션 (FP8 ViT Encoder Attention)

문자가 아닌 이미지를 이해하는 비전(vision) 모델을 돌릴 때, 큰 이미지를 처리하는 ViT(Vision Transformer) 인코더의 어텐션 연산이 어느 순간 병목이 되는 경우가 있어요. 특히 이미지가 QHD·4K처럼 고해상도면서 텍스트 프롬프트와 생성이 짧은 작업, 그리고 텍스트 모델 자체를 양자화(예: NVFP4)한 경우에 더 두드러지죠. vLLM이 이 ViT 인코더 어텐션에 선택적으로 FP8 양자화를 적용하는 방법을 설명드릴게요.

출처: vLLM 공식 문서 — FP8 ViT Encoder Attention

이 기능이 필요한 상황 (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

세 구성 모두 통계적 오차 범위 안에서 일치하는 걸 확인할 수 있어요. 한 데이터셋에서 캘리브레이션한 정적 스케일이 다른 데이터셋으로도 잘 일반화된다는 뜻이죠.

더 알아보기 (Learn more)