| 1 | # FP8 ViT Encoder Attention |
| 2 | |
| 3 | For visual understanding workloads with large images (e.g. QHD, 4K) and relatively |
| 4 | short text prompts/generation, the ViT encoder attention can become a significant |
| 5 | bottleneck, especially when the text model is quantized (e.g. NVFP4). vLLM |
| 6 | supports optional FP8 quantization for ViT encoder attention via FlashInfer |
| 7 | cuDNN on NVIDIA GPUs and AITER on AMD GPUs. Q/K/V are quantized on-the-fly to |
| 8 | FP8 before the attention call. |
| 9 | |
| 10 | !!! note |
| 11 | - Currently supports Qwen3-VL family models only (`qwen3_vl`, `qwen3_vl_moe`, |
| 12 | `qwen3_5`, `qwen3_5_moe`, and other models using Qwen3 ViT). |
| 13 | - Dynamic scaling is not compatible with ViT full CUDA graphs. |
| 14 | - Performance gains are mostly visible at QHD/4K resolutions or multi-image |
| 15 | requests. Smaller images may see no speedup due to quantization overhead |
| 16 | (3 quantization kernel launches + un-padding). |
| 17 | - FP8 tensor-core speedup is more pronounced on GB300 than GB200. |
| 18 | - On ROCm, packed variable-length image and video batches are supported by |
| 19 | AITER's native varlen FP8 attention. |
| 20 | |
| 21 | ## Requirements |
| 22 | |
| 23 | - NVIDIA: FlashInfer cuDNN backend with cuDNN >= 9.17.1. |
| 24 | - AMD: AITER with `flash_attn_varlen_fp8_pertensor_func` support on gfx942 |
| 25 | (MI300 series) or gfx950 (MI350 series). |
| 26 | |
| 27 | ## Usage |
| 28 | |
| 29 | Enable FP8 ViT attention by passing `--mm-encoder-attn-dtype fp8` and selecting |
| 30 | the backend for the current platform: |
| 31 | |
| 32 | ```bash |
| 33 | vllm serve $MODEL \ |
| 34 | --mm-encoder-attn-backend FLASHINFER \ |
| 35 | --mm-encoder-attn-dtype fp8 |
| 36 | |
| 37 | # AMD ROCm |
| 38 | vllm serve $MODEL \ |
| 39 | --mm-encoder-attn-backend ROCM_AITER_FA \ |
| 40 | --mm-encoder-attn-dtype fp8 |
| 41 | ``` |
| 42 | |
| 43 | By default (no scale file), **dynamic scaling** is used: a 16-entry circular |
| 44 | buffer of observed Q/K/V amax values drives per-forward scale updates. This |
| 45 | matches BF16 accuracy without any calibration but adds a small per-forward |
| 46 | overhead. |
| 47 | |
| 48 | ## Calibrate-Once, Reuse Workflow (Recommended) |
| 49 | |
| 50 | For production, calibrate static scales on a representative dataset once and |
| 51 | reuse them to avoid the dynamic overhead: |
| 52 | |
| 53 | ```bash |
| 54 | # Step 1: calibrate and save scales (runs dynamic scaling for 16 passes, |
| 55 | # then dumps the learned scales to JSON). |
| 56 | vllm bench mm-processor \ |
| 57 | --model $MODEL --mm-encoder-attn-backend $MM_ATTN_BACKEND \ |
| 58 | --mm-encoder-attn-dtype fp8 \ |
| 59 | --mm-encoder-fp8-scale-save-path /path/to/scales.json \ |
| 60 | --dataset-name hf --dataset-path lmarena-ai/VisionArena-Chat \ |
| 61 | --num-prompts 100 |
| 62 | |
| 63 | # Step 2: serve with static scales (no dynamic overhead). |
| 64 | vllm serve $MODEL \ |
| 65 | --mm-encoder-attn-backend $MM_ATTN_BACKEND \ |
| 66 | --mm-encoder-attn-dtype fp8 \ |
| 67 | --mm-encoder-fp8-scale-path /path/to/scales.json |
| 68 | ``` |
| 69 | |
| 70 | Saved scales are multiplied by `--mm-encoder-fp8-scale-save-margin` (default |
| 71 | `1.5`) to leave headroom against activation outliers not present in the |
| 72 | calibration set. The default has been validated to generalize across datasets |
| 73 | (e.g. VisionArena-Chat calibration maintains BF16 accuracy on ChartQA). |
| 74 | |
| 75 | ## Scale File Format |
| 76 | |
| 77 | ```json |
| 78 | { |
| 79 | "visual.blocks.0.attn.attn": {"q": 224.0, "k": 198.0, "v": 210.0}, |
| 80 | "visual.blocks.1.attn.attn": {"q": 218.0, "k": 195.0, "v": 207.0} |
| 81 | } |
| 82 | ``` |
| 83 | |
| 84 | Keys `q_scale` / `k_scale` / `v_scale` are accepted as aliases. |
| 85 | |
| 86 | ## Performance |
| 87 | |
| 88 | **Core cuDNN attention kernel** (PyTorch profiler, `cudnn_generated_fort_native_sdpa_sm100_flash_fprop`, head_dim=128, seq_len=8192): |
| 89 | |
| 90 | | Hardware | BF16 | FP8 | Speedup | |
| 91 | | -------- | ---- | ---- | ------- | |
| 92 | | GB200 | 350 us | 312 us | **1.12x** | |
| 93 | | GB300 | 300 us | 211 us | **1.42x** | |
| 94 | |
| 95 | **End-to-end encoder forward time** (Qwen3-VL-30B-A3B-Instruct on GB200, 3 images/request): |
| 96 | |
| 97 | | Resolution | BF16 median | FP8 median | Speedup | |
| 98 | | ---------- | ----------- | ---------- | ------- | |
| 99 | | HD (720x1280) | 31.77 ms | 36.39 ms | 0.87x | |
| 100 | | FullHD (1080x1920) | 57.99 ms | 58.73 ms | ~same | |
| 101 | | QHD (1440x2560) | 131.83 ms | 122.30 ms | **1.08x** | |
| 102 | | 4K (2160x3840) | 543.44 ms | 460.31 ms | **1.18x** | |
| 103 | |
| 104 | Crossover is around FullHD with 3 images/request. At QHD and above, FP8 wins. |
| 105 | |
| 106 | **Complete AITER attention call on MI300X** (BF16 input, 16 heads, |
| 107 | head_dim=72; FP8 includes Q/K/V quantization): |
| 108 | |
| 109 | | Sequence length | AITER BF16 | AITER FP8 | Speedup | |
| 110 | | --------------- | ---------- | --------- | ------- | |
| 111 | | 2304 | 0.467 ms | 0.337 ms | **1.38x** | |
| 112 | | 4096 | 0.812 ms | 0.764 ms | **1.06x** | |
| 113 | | 8192 | 2.555 ms | 2.364 ms | **1.08x** | |
| 114 | | 16384 | 9.769 ms | 8.655 ms | **1.13x** | |
| 115 | |
| 116 | ## Accuracy |
| 117 | |
| 118 | ChartQA, Qwen3-VL-8B-Instruct, 500 samples. FP8 static uses scales calibrated |
| 119 | on VisionArena-Chat (with default 1.5x margin): |
| 120 | |
| 121 | | Metric | BF16 | FP8 dynamic | FP8 static | |
| 122 | | ------ | ---- | ----------- | ---------- | |
| 123 | | relaxed_accuracy | 0.780 | 0.776 | 0.780 | |
| 124 | | anywhere_accuracy | 0.806 | 0.816 | 0.814 | |
| 125 | | exact_match | 0.584 | 0.582 | 0.578 | |
| 126 | |
| 127 | All three configurations match within statistical noise, confirming that |
| 128 | static scales calibrated on one dataset generalize to another. |