[Kernel] Gemma-4 FA4 FP8 Kernel (#48666)

Signed-off-by: Qixiang Lin <qixiangl@nvidia.com> Signed-off-by: Jhao-Ting Chen <jhaotingc@nvidia.com> Co-authored-by: Qixiang Lin <qixiangl@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Yongye Zhu <zyy1102000@gmail.com>

Jhao-Ting Chen committed Aug 13, 2026 at 21:42 UTC 827a2af806c4e4ea7bcc280f57f793e6a5fcc676
8 files changed +152 -29
cmake/external_projects/vllm_flash_attn.cmake
+1 -1
@@ -39,7 +39,7 @@ else()
39 FetchContent_Declare(
40 vllm-flash-attn
41 GIT_REPOSITORY https://github.com/vllm-project/flash-attention.git
42 - GIT_TAG f3e1a4f74c99145c0717709860bf765de1703779
42 + GIT_TAG 617264c1c7955c9e84817654ebeedff069f3c5f1
43 GIT_PROGRESS TRUE
44 # Don't share the vllm-flash-attn build between build types
45 BINARY_DIR ${CMAKE_BINARY_DIR}/vllm-flash-attn
vllm/model_executor/layers/attention/attention.py
+11 -3
@@ -8,7 +8,10 @@ import torch.nn as nn
8
9 import vllm.envs as envs
10 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
11 -from vllm.config import CacheConfig, get_current_vllm_config
11 +from vllm.config import (
12 + CacheConfig,
13 + get_current_vllm_config,
14 +)
15 from vllm.config.vllm import VllmConfig
16 from vllm.forward_context import ForwardContext, get_forward_context
17 from vllm.logger import init_logger
@@ -94,6 +97,7 @@ def should_load_quant_weights(quant_method: QuantizeMethodBase | None) -> bool:
97
98 def _largest_kernel_block_within(
99 attn_backend: "type[AttentionBackend]",
100 + vllm_config: VllmConfig,
101 per_token_bytes: int,
102 page_budget: int | None,
103 fallback: int,
@@ -108,7 +112,7 @@ def _largest_kernel_block_within(
112 """
113 from vllm.v1.attention.backend import MultipleOf
114
111 - sizes = attn_backend.get_supported_kernel_block_sizes()
115 + sizes = attn_backend.get_supported_kernel_block_sizes_for_config(vllm_config)
116 candidates = [s for s in sizes if isinstance(s, int)]
117 if not candidates:
118 candidates = [s.base for s in sizes if isinstance(s, MultipleOf)]
@@ -629,7 +633,11 @@ class Attention(nn.Module, AttentionLayerBase):
633 sliding_window=self.sliding_window,
634 ).real_page_size_bytes
635 sw_block_size = _largest_kernel_block_within(
632 - self.attn_backend, sw_per_token, shared_page, block_size
636 + self.attn_backend,
637 + vllm_config,
638 + sw_per_token,
639 + shared_page,
640 + block_size,
641 )
642 return SlidingWindowSpec(
643 block_size=sw_block_size,
vllm/platforms/interface.py
+3 -5
@@ -612,7 +612,6 @@ class Platform:
612 For hybrid models, also aligns block_size with mamba page sizes.
613 """
614 from vllm.config.cache import CacheConfig
615 - from vllm.config.vllm import set_current_vllm_config
615
616 cache_config = vllm_config.cache_config
617 model_config = vllm_config.model_config
@@ -627,10 +626,9 @@ class Platform:
626
627 # Phase 1: Pick block size from backend (skip if user set --block-size)
628 if not cache_config.user_specified_block_size:
630 - with set_current_vllm_config(vllm_config):
631 - preferred = backend_cls.get_preferred_block_size(
632 - CacheConfig.DEFAULT_BLOCK_SIZE
633 - )
629 + preferred = backend_cls.get_preferred_block_size_for_config(
630 + CacheConfig.DEFAULT_BLOCK_SIZE, vllm_config
631 + )
632 if preferred != CacheConfig.DEFAULT_BLOCK_SIZE:
633 logger.info(
634 "Setting kv cache block size to %d for %s backend.",
vllm/v1/attention/backend.py
+14
@@ -69,6 +69,13 @@ class AttentionBackend(ABC):
69 def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
70 return [MultipleOf(1)]
71
72 + @classmethod
73 + def get_supported_kernel_block_sizes_for_config(
74 + cls, vllm_config: "VllmConfig"
75 + ) -> list[int | MultipleOf]:
76 + """Return kernel block sizes for a concrete engine configuration."""
77 + return cls.get_supported_kernel_block_sizes()
78 +
79 @staticmethod
80 @abstractmethod
81 def get_name() -> str:
@@ -201,6 +208,13 @@ class AttentionBackend(ABC):
208
209 return min(s.base if isinstance(s, MultipleOf) else s for s in supported_sizes)
210
211 + @classmethod
212 + def get_preferred_block_size_for_config(
213 + cls, default_block_size: int, vllm_config: "VllmConfig"
214 + ) -> int:
215 + """Return the preferred block size for a concrete engine config."""
216 + return cls.get_preferred_block_size(default_block_size)
217 +
218 @classmethod
219 def indexes_kv_by_block_stride(cls) -> bool:
220 """Whether the backend reads KV pages by the runtime block stride.
vllm/v1/attention/backends/fa_utils.py
+3 -3
@@ -241,9 +241,9 @@ def flash_attn_supports_kv_cache_dtype(
241 head_size_v=head_size_v,
242 has_sinks=has_sinks,
243 )
244 - return (fa_version == 3 and current_platform.is_device_capability_family(90)) or (
245 - fa_version == 4 and current_platform.is_device_capability_family(100)
246 - )
244 + return (
245 + fa_version in (3, 4) and current_platform.is_device_capability_family(90)
246 + ) or (fa_version == 4 and current_platform.is_device_capability_family(100))
247
248
249 def flash_attn_supports_quant_query_input() -> bool:
vllm/v1/attention/backends/flash_attn.py
+57 -2
@@ -85,13 +85,58 @@ class FlashAttentionBackend(AttentionBackend):
85 ]
86
87 @staticmethod
88 - def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
88 + def _get_sm90_fa4_fp8_kv_block_size(
89 + vllm_config: VllmConfig | None = None,
90 + ) -> int | None:
91 + if vllm_config is None:
92 + vllm_config = get_current_vllm_config_or_none()
93 + if vllm_config is None or vllm_config.model_config is None:
94 + return None
95 +
96 + head_size = vllm_config.model_config.get_head_size()
97 + if (
98 + current_platform.is_device_capability_family(90)
99 + and vllm_config.cache_config.cache_dtype in ("fp8", "fp8_e4m3")
100 + and head_size == 512
101 + and get_flash_attn_version(head_size=head_size) == 4
102 + ):
103 + # The SM90 FP8-KV-dequant kernel uses a 64-token TMA tile/page.
104 + return 64
105 + return None
106 +
107 + @classmethod
108 + def get_supported_kernel_block_sizes(cls) -> list[int | MultipleOf]:
109 + if block_size := cls._get_sm90_fa4_fp8_kv_block_size():
110 + # Sliding-window cache specs select the smallest advertised size.
111 + # Report the kernel's exact page-size contract instead of the
112 + # generic FlashAttention multiple-of-16 capability.
113 + return [block_size]
114 + return [MultipleOf(16)]
115 +
116 + @classmethod
117 + def get_supported_kernel_block_sizes_for_config(
118 + cls, vllm_config: VllmConfig
119 + ) -> list[int | MultipleOf]:
120 + if block_size := cls._get_sm90_fa4_fp8_kv_block_size(vllm_config):
121 + return [block_size]
122 return [MultipleOf(16)]
123
124 forward_includes_kv_cache_update: bool = False
125
126 @classmethod
127 def get_preferred_block_size(cls, default_block_size: int) -> int:
128 + if block_size := cls._get_sm90_fa4_fp8_kv_block_size():
129 + return max(default_block_size, block_size)
130 + if current_platform.is_xpu():
131 + return max(default_block_size, 64)
132 + return super().get_preferred_block_size(default_block_size)
133 +
134 + @classmethod
135 + def get_preferred_block_size_for_config(
136 + cls, default_block_size: int, vllm_config: VllmConfig
137 + ) -> int:
138 + if block_size := cls._get_sm90_fa4_fp8_kv_block_size(vllm_config):
139 + return max(default_block_size, block_size)
140 if current_platform.is_xpu():
141 return max(default_block_size, 64)
142 return super().get_preferred_block_size(default_block_size)
@@ -895,7 +940,17 @@ class FlashAttentionImpl(AttentionImpl):
940 "heads in the layer"
941 )
942
898 - self.supports_quant_query_input = flash_attn_supports_quant_query_input()
943 + # FA4's SM90 FP8-KV path consumes native FP16/BF16 Q and dequantizes
944 + # FP8 K/V in-kernel. Other FA4 paths (notably SM100) still require Q,
945 + # K, and V to have the same FP8 dtype.
946 + uses_sm90_fa4_fp8_kv_dequant = (
947 + self.vllm_flash_attn_version == 4
948 + and current_platform.is_device_capability_family(90)
949 + and self.kv_cache_dtype in ("fp8", "fp8_e4m3")
950 + )
951 + self.supports_quant_query_input = flash_attn_supports_quant_query_input() and (
952 + not uses_sm90_fa4_fp8_kv_dequant
953 + )
954
955 vllm_config = get_current_vllm_config_or_none()
956 dcp_a2a = (
vllm/v1/worker/gpu/spec_decode/gemma4/speculator.py
+43 -12
@@ -23,6 +23,23 @@ from vllm.v1.worker.gpu.spec_decode.autoregressive.speculator import (
23 logger = init_logger(__name__)
24
25
26 +def _copy_target_kv_scales(attn: nn.Module, target_attn: nn.Module) -> None:
27 + """Copy target KV scales while preserving their tensor representation.
28 +
29 + Default attention scales are scalar buffers, while some quantization
30 + methods replace them with length-one or per-head parameters. Re-register
31 + cloned buffers on the draft layer so the shared KV cache is interpreted
32 + with the target's values and shapes without aliasing target parameters.
33 + """
34 + for scale_name in ("_k_scale", "_v_scale"):
35 + target_scale = getattr(target_attn, scale_name)
36 + attn.register_buffer(scale_name, target_scale.detach().clone())
37 + for scale_name in ("_k_scale_float", "_v_scale_float"):
38 + setattr(attn, scale_name, getattr(target_attn, scale_name))
39 + for scale_name in ("_k_scale_cpu", "_v_scale_cpu"):
40 + getattr(attn, scale_name).copy_(getattr(target_attn, scale_name))
41 +
42 +
43 class Gemma4Speculator(AutoRegressiveSpeculator):
44 @property
45 def advance_draft_positions(self) -> bool:
@@ -76,7 +93,7 @@ class Gemma4Speculator(AutoRegressiveSpeculator):
93 model: nn.Module,
94 target_attn_layer_names: set[str],
95 ) -> None:
79 - """Wire draft layers to share KV with the target model.
96 + """Wire draft layers to share KV and KV scales with the target model.
97
98 Each draft decoder layer is mapped to the last non-KV-shared
99 target layer of the same attention type (sliding or full).
@@ -92,15 +109,19 @@ class Gemma4Speculator(AutoRegressiveSpeculator):
109
110 target_num_kv_shared = getattr(target_text_config, "num_kv_shared_layers", 0)
111 num_non_shared = len(target_layer_types) - target_num_kv_shared
95 - type_to_target_indices: dict[str, list[int]] = defaultdict(list)
96 - for idx, lt in enumerate(target_layer_types[:num_non_shared]):
97 - type_to_target_indices[lt].append(idx)
98 -
99 - target_prefix = "model.layers"
112 + target_names_by_index: dict[int, str] = {}
113 for name in target_attn_layer_names:
101 - if ".layers." in name:
102 - target_prefix = name.split(".layers.")[0] + ".layers"
103 - break
114 + _, separator, layer_suffix = name.partition(".layers.")
115 + if not separator:
116 + continue
117 + layer_index, _, _ = layer_suffix.partition(".")
118 + if layer_index.isdigit():
119 + target_names_by_index[int(layer_index)] = name
120 +
121 + type_to_target_names: dict[str, list[str]] = defaultdict(list)
122 + for idx, lt in enumerate(target_layer_types[:num_non_shared]):
123 + if target_name := target_names_by_index.get(idx):
124 + type_to_target_names[lt].append(target_name)
125
126 draft_layer_types = getattr(draft_text_config, "layer_types", [])
127 for draft_idx, layer in enumerate(model.model.layers):
@@ -115,7 +136,7 @@ class Gemma4Speculator(AutoRegressiveSpeculator):
136 if draft_idx < len(draft_layer_types)
137 else "full_attention"
138 )
118 - candidates = type_to_target_indices.get(draft_layer_type, [])
139 + candidates = type_to_target_names.get(draft_layer_type, [])
140 if not candidates:
141 logger.warning(
142 "No target layer of type '%s' for draft layer %d",
@@ -124,9 +145,19 @@ class Gemma4Speculator(AutoRegressiveSpeculator):
145 )
146 continue
147
127 - target_idx = candidates[-1]
128 - target_layer_name = f"{target_prefix}.{target_idx}.self_attn.attn"
148 + target_layer_name = candidates[-1]
149 attn.kv_sharing_target_layer_name = target_layer_name
150 +
151 + # KV-cache sharing aliases the cache tensor during allocation, but
152 + # the quantization scales live on the Attention modules themselves.
153 + # The BF16 draft model has no quantization config, so its K/V scales
154 + # otherwise remain at the default 1.0 while it reads the target's
155 + # calibrated FP8 cache.
156 + target_attn = self.vllm_config.compilation_config.static_forward_context[
157 + target_layer_name
158 + ]
159 + _copy_target_kv_scales(attn, target_attn)
160 +
161 logger.info(
162 "Gemma4 MTP: draft layer %d (%s) -> %s",
163 draft_idx,
vllm/vllm_flash_attn/flash_attn_interface.py
+20 -3
@@ -404,6 +404,22 @@ def flash_attn_varlen_func(
404
405 from vllm.vllm_flash_attn.cute.interface import _flash_attn_fwd
406
407 + # SM90 FA4 fp8-KV path: fp8 e4m3 paged K/V dequantized and the K/V descale folded
408 + # in-kernel; accepts bf16/fp16 Q and writes O in its native dtype (no Q cast, no
409 + # output copy). Only the (batch, num_kv_heads) f32 K/V descales are forwarded.
410 + fa4_fp8_kv_dequant = (
411 + k.dtype == torch.float8_e4m3fn
412 + and torch.cuda.get_device_capability()[0] == 9
413 + )
414 + if fa4_fp8_kv_dequant:
415 + fa4_q_descale = None
416 + fa4_k_descale = k_descale
417 + fa4_v_descale = v_descale
418 + else:
419 + fa4_q_descale = None
420 + fa4_k_descale = None
421 + fa4_v_descale = None
422 +
423 out, softmax_lse, _, _ = _flash_attn_fwd(
424 q,
425 k,
@@ -428,10 +444,11 @@ def flash_attn_varlen_func(
444 block_sparse_tensors=block_sparse_tensors,
445 aux_tensors=aux_tensors,
446 aux_tensor_leading_dims=aux_tensor_leading_dims,
431 - q_descale=q_descale,
432 - k_descale=k_descale,
433 - v_descale=v_descale,
447 + q_descale=fa4_q_descale,
448 + k_descale=fa4_k_descale,
449 + v_descale=fa4_v_descale,
450 output_scale=output_scale,
451 + fp8_kv_dequant=fa4_fp8_kv_dequant,
452 )
453 else:
454 raise ValueError(f"Unsupported FA version: {fa_version}")