Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions tests/v1/attention/test_attention_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,79 @@ def causal_mask_mod(
)


@pytest.mark.skipif(
not current_platform.is_device_capability_family(100),
reason="FA4 takes FP8 Q/K/V with descales on Blackwell.",
)
def test_flash_attn_fa4_unit_fp8_descale_is_exact(default_vllm_config):
"""FlashAttentionImpl passes FA4 no descale for a per-tensor scale of 1.0,
which FA4 reads as exactly 1.0; calibrated scales keep their descale."""
from vllm.v1.attention.backends.fa_utils import (
flash_attn_varlen_func,
is_fa_version_supported,
)
from vllm.v1.attention.backends.flash_attn import FlashAttentionImpl

if not is_fa_version_supported(4):
pytest.skip("FA4 is not supported.")
set_random_seed(0)
device = torch.device(f"{DEVICE_TYPE}:0")
fp8 = current_platform.fp8_dtype()
seq_lens, q_len = [1328, 523, 2011], 3
num_heads, num_kv_heads, head_size = 64, 4, 128
impl = FlashAttentionImpl(
num_heads=num_heads,
head_size=head_size,
scale=head_size**-0.5,
num_kv_heads=num_kv_heads,
alibi_slopes=None,
sliding_window=None,
kv_cache_dtype="fp8",
)
assert impl.vllm_flash_attn_version == 4

q = torch.randn(len(seq_lens) * q_len, num_heads, head_size, device=device)
k_cache, v_cache = (
torch.randn(64, 128, num_kv_heads, head_size, device=device).to(fp8)
for _ in range(2)
)
block_table = torch.randperm(64, dtype=torch.int32, device=device)[:48]

def run(descale: torch.Tensor | None) -> torch.Tensor:
return flash_attn_varlen_func(
q=q.to(fp8),
k=k_cache,
v=v_cache,
cu_seqlens_q=torch.arange(
0, q.shape[0] + 1, q_len, dtype=torch.int32, device=device
),
seqused_k=torch.tensor(seq_lens, dtype=torch.int32, device=device),
max_seqlen_q=q_len,
max_seqlen_k=max(seq_lens),
softmax_scale=head_size**-0.5,
causal=True,
block_table=block_table.view(len(seq_lens), -1),
fa_version=4,
q_descale=descale,
k_descale=descale,
v_descale=descale,
)

shape = (len(seq_lens), num_kv_heads)
unit = torch.ones(1, device=device)
assert impl._descale(unit, 1.0, shape) is None
expanded = unit.expand(shape) # What FA4 got for a unit scale before.
output = run(impl._descale(unit, 1.0, shape))
torch.testing.assert_close(output, run(expanded), atol=0, rtol=0)
# Descales are applied when given, so the comparison above is not vacuous.
assert not torch.equal(output, run(2 * expanded))
# Calibrated and per-head scales (whose float holds the max) keep theirs.
calibrated = impl._descale(0.5 * unit, 0.5, shape)
assert calibrated is not None and torch.equal(calibrated, 0.5 * expanded)
per_head = torch.tensor([1.0, 0.5, 1.0, 0.25], device=device)
assert impl._descale(per_head, 1.0, shape) is not None


@pytest.mark.skipif(
AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST,
reason="FlashInfer is not available.",
Expand Down
26 changes: 22 additions & 4 deletions vllm/v1/attention/backends/flash_attn.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,25 @@ def __init__(
vllm_config.scheduler_config.max_num_batched_tokens
)

def _descale(
self, scale: torch.Tensor, scale_float: float, shape: tuple[int, int]
) -> torch.Tensor | None:
# FA4 reads a missing descale as 1.0, and it copies every expanded view
# into a contiguous tensor: three copy kernels per layer and step. The
# scales are fixed once weights are loaded, so a per-tensor scale of 1.0
# (FP8 KV cache without calibrated scales) needs none. Per-head scales
# keep their tensor: their float mirror holds only the maximum.
if (
self.vllm_flash_attn_version == 4
and scale.numel() == 1
and scale_float == 1.0
):
return None
# A (1, 1) expanded view (one sequence, one KV head per rank) counts
# as contiguous, so FA4 skips its copy and its CuTe layout check then
# rejects the zero stride.
return canonicalize_singleton_dim_strides(scale.expand(shape))

def forward(
self,
layer: torch.nn.Module,
Expand Down Expand Up @@ -1286,14 +1305,13 @@ def forward(
scheduler_metadata = attn_metadata.scheduler_metadata

descale_shape = (cu_seqlens_q.shape[0] - 1, self.num_kv_heads)

q_descale = (
layer._q_scale.expand(descale_shape)
self._descale(layer._q_scale, layer._q_scale_float, descale_shape)
if self.supports_quant_query_input
else None
)
k_descale = layer._k_scale.expand(descale_shape)
v_descale = layer._v_scale.expand(descale_shape)
k_descale = self._descale(layer._k_scale, layer._k_scale_float, descale_shape)
v_descale = self._descale(layer._v_scale, layer._v_scale_float, descale_shape)

if self.dcp_world_size > 1:
self._forward_with_dcp(
Expand Down
Loading