Files
vllm-project--vllm/tests/kernels/attention/test_mla_cross_layer_kernel_equivalence.py
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

567 lines
20 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Bit-exact kernel equivalence for MLA decode/write kernels on the
cross-layer (block-major) KV cache layout.
The cross-layer layout carves each layer's per-block page out of a single
unified slot, so the per-layer view has an inflated ``stride(0)`` (the full
unified slot) and a non-zero storage offset. These tests confirm the MLA
kernels behind the backends that opt in to the layout (FlashMLA dense,
FlashInfer MLA dense, FlashMLA fp8 sparse, plus the ``concat_and_cache_mla``
write) honor that strided view bit-identically to a contiguous per-layer
cache, and that writes do not bleed into neighbouring layers' segments.
"""
import pytest
import torch
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available(), reason="MLA cache kernels require CUDA"
)
def test_concat_and_cache_mla_into_unified_slot_view():
"""concat_and_cache_mla must write correctly into a per-layer view whose
block stride is the full unified slot (block-major), with zero bleed into
the other layers' segments of the same slot."""
from vllm import _custom_ops as ops
torch.manual_seed(0)
dev = "cuda"
kv_lora_rank = 512
pe = 64
entry = kv_lora_rank + pe
page = 64
num_blocks = 32
ntok = 200
kv_c = torch.randn(ntok, kv_lora_rank, device=dev, dtype=torch.bfloat16)
k_pe = torch.randn(ntok, pe, device=dev, dtype=torch.bfloat16)
slot = torch.randperm(num_blocks * page, device=dev, dtype=torch.int64)[:ntok]
scale = torch.tensor(1.0, device=dev)
def write(cache):
ops.concat_and_cache_mla(kv_c, k_pe, cache, slot, "auto", scale)
# Contiguous per-layer reference: (num_blocks, page, entry).
ref = torch.zeros(num_blocks, page, entry, device=dev, dtype=torch.bfloat16)
write(ref)
# Unified slot holding three layer pages per block. Carve the middle
# layer's view (non-zero offset, block stride == full unified slot).
layer_page_elems = page * entry
n_layers = 3
unified_slot_elems = n_layers * layer_page_elems
big = torch.zeros(num_blocks, unified_slot_elems, device=dev, dtype=torch.bfloat16)
flat = big.view(-1)
offset = layer_page_elems # middle layer
view = torch.as_strided(
flat,
size=(num_blocks, page, entry),
stride=(unified_slot_elems, entry, 1),
storage_offset=offset,
)
assert not view.is_contiguous()
assert view.stride(0) == unified_slot_elems
write(view)
# Bit-exact equivalence and zero bleed into the neighbour segments.
max_diff = (ref.float() - view.float()).abs().max().item()
assert max_diff == 0.0, f"max|Δ| = {max_diff}"
neighbour_lo = torch.as_strided(
flat, (num_blocks, layer_page_elems), (unified_slot_elems, 1), 0
)
neighbour_hi = torch.as_strided(
flat,
(num_blocks, layer_page_elems),
(unified_slot_elems, 1),
2 * layer_page_elems,
)
assert neighbour_lo.abs().max().item() == 0.0
assert neighbour_hi.abs().max().item() == 0.0
def test_flashmla_dense_decode_unified_slot_view():
"""FlashMLA dense decode (FLASHMLA backend, e.g. Kimi-K2-style dense MLA
on Hopper) must read a unified-slot block-major view bit-identically to a
contiguous per-layer cache."""
import vllm.v1.attention.ops.flashmla as fm
ok, reason = fm.is_flashmla_dense_supported()
if not ok:
pytest.skip(reason)
torch.manual_seed(0)
dev = "cuda"
dt = torch.bfloat16
head_dim = 576
hdv = 512
h_q = 128
page = 64
num_blocks = 64
bs = 4
n_layers = 3
layer = 1
q = torch.randn(bs, 1, h_q, head_dim, device=dev, dtype=dt) * 0.1
kv_data = torch.randn(num_blocks, page, 1, head_dim, device=dev, dtype=dt) * 0.1
# (A) contiguous per-layer reference.
cache_contiguous = kv_data.clone().contiguous()
# (B) unified slot: view one layer -> inflated stride(0), non-zero offset.
unified = (
torch.randn(num_blocks, n_layers, page, 1, head_dim, device=dev, dtype=dt) * 0.1
)
unified[:, layer].copy_(kv_data)
cache_view = unified[:, layer]
assert not cache_view.is_contiguous()
assert cache_view.stride(0) == n_layers * page * 1 * head_dim
max_blk = num_blocks // bs
block_table = torch.arange(num_blocks, device=dev, dtype=torch.int32).view(
bs, max_blk
)
cache_seqlens = torch.full((bs,), max_blk * page, device=dev, dtype=torch.int32)
def run(kc):
meta, num_splits = fm.get_mla_metadata()
out, _ = fm.flash_mla_with_kvcache(
q=q,
k_cache=kc,
block_table=block_table,
cache_seqlens=cache_seqlens,
head_dim_v=hdv,
tile_scheduler_metadata=meta,
num_splits=num_splits,
softmax_scale=head_dim**-0.5,
causal=True,
)
return out.clone().float()
out_ref = run(cache_contiguous)
out_view = run(cache_view)
assert torch.isfinite(out_ref).all()
assert out_ref.abs().max().item() > 0.0
assert (out_ref - out_view).abs().max().item() == 0.0
def test_flashinfer_mla_dense_decode_unified_slot_view():
"""FlashInfer MLA dense decode must read a unified-slot block-major view
(inflated stride(0), non-zero storage offset) bit-identically to a
contiguous per-layer cache."""
try:
from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
except ImportError:
pytest.skip("flashinfer is not available")
from vllm.platforms import current_platform
if not current_platform.is_device_capability_family(100):
pytest.skip("FlashInfer trtllm-gen MLA requires sm100")
torch.manual_seed(0)
dev = "cuda"
dt = torch.bfloat16
kv_lora_rank = 512
qk_rope_head_dim = 64
qk_nope_head_dim = 128
head_dim = kv_lora_rank + qk_rope_head_dim # 576
num_qo_heads = 128
page = 64
num_blocks = 64
bs = 4
n_layers = 3 # >1 so the per-layer view's block stride is inflated.
layer = 1
q = torch.randn(bs, 1, num_qo_heads, head_dim, device=dev, dtype=dt)
kv_data = torch.randn(num_blocks, 1, page, head_dim, device=dev, dtype=dt)
# (A) contiguous per-layer reference.
kv_contiguous = kv_data.clone().contiguous()
# (B) unified slot: block b of every layer packed together; view one layer
# -> stride(0) is n_layers x larger and storage offset is non-zero.
unified = torch.randn(num_blocks, n_layers, 1, page, head_dim, device=dev, dtype=dt)
unified[:, layer].copy_(kv_data)
kv_view = unified[:, layer]
assert not kv_view.is_contiguous()
assert kv_view.stride(0) == n_layers * 1 * page * head_dim
max_blk = num_blocks // bs
block_tables = torch.arange(num_blocks, device=dev, dtype=torch.int32).view(
bs, max_blk
)
seq_lens = torch.full((bs,), max_blk * page, device=dev, dtype=torch.int32)
ws = torch.empty(128 * 1024 * 1024, dtype=torch.int8, device=dev)
scale = head_dim**-0.5
def run(kv):
return trtllm_batch_decode_with_kv_cache_mla(
query=q,
kv_cache=kv,
workspace_buffer=ws,
qk_nope_head_dim=qk_nope_head_dim,
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
block_tables=block_tables,
seq_lens=seq_lens,
max_seq_len=int(seq_lens.max().item()),
bmm1_scale=scale,
bmm2_scale=1.0,
).clone()
out_ref = run(kv_contiguous).float()
out_view = run(kv_view).float()
assert torch.isfinite(out_ref).all()
assert (out_ref - out_view).abs().max().item() == 0.0
def test_flashmla_fp8_sparse_decode_unified_slot_view():
"""FlashMLA fp8 sparse decode (DeepSeek V3.2/V4 DSA path) must read a
unified-slot block-major view bit-identically to a contiguous fp8_ds_mla
cache, with finite nonzero output."""
import vllm.v1.attention.ops.flashmla as fm
ok, reason = fm.is_flashmla_sparse_supported()
if not ok:
pytest.skip(reason)
torch.manual_seed(0)
dev = "cuda"
entry = 656 # fp8_ds_mla bytes per token
page = 64
num_blocks = 32
h_q = 128
head_dim = 576
hdv = 512
batch = 2
topk = 128
n_layers = 3
layer = 1
q = torch.randn(batch, 1, h_q, head_dim, device=dev, dtype=torch.bfloat16) * 0.1
# Structurally valid fp8 ds_mla payload: 512B fp8 + 16B f32 scales + 128B
# bf16 rope (random bytes corrupt the scale region and yield NaNs).
nope = (torch.randn(num_blocks, page, 1, 512, device=dev) * 0.1).to(
torch.float8_e4m3fn
)
scales = torch.ones(num_blocks, page, 1, 4, device=dev, dtype=torch.float32)
rope = (torch.randn(num_blocks, page, 1, 64, device=dev) * 0.1).to(torch.bfloat16)
payload = torch.cat(
[
nope.view(torch.uint8).view(num_blocks, page, 1, 512),
scales.view(torch.uint8).view(num_blocks, page, 1, 16),
rope.view(torch.uint8).view(num_blocks, page, 1, 128),
],
dim=-1,
).contiguous()
assert payload.shape[-1] == entry and payload.dtype == torch.uint8
# (A) contiguous reference.
cache_contiguous = payload.clone().contiguous()
# (B) unified slot: view one layer -> inflated stride(0), non-zero offset.
unified = torch.randint(
0, 256, (num_blocks, n_layers, page, 1, entry), device=dev, dtype=torch.uint8
)
unified[:, layer].copy_(payload)
cache_view = unified[:, layer]
assert not cache_view.is_contiguous()
assert cache_view.stride(0) == n_layers * page * 1 * entry
# Sparse indices: each batch uses its own disjoint blocks.
blocks_per_batch = num_blocks // batch
idx = torch.full((batch, 1, topk), -1, device=dev, dtype=torch.int32)
for b in range(batch):
slots: list[int] = []
for blk in range(b * blocks_per_batch, (b + 1) * blocks_per_batch):
slots.extend(blk * page + off for off in range(page))
slots_t = torch.tensor(slots[:topk], device=dev, dtype=torch.int32)
idx[b, 0, : slots_t.numel()] = slots_t
def run(kc):
meta, num_splits = fm.get_mla_metadata()
out, _ = fm.flash_mla_with_kvcache(
q=q,
k_cache=kc,
block_table=None,
cache_seqlens=None,
head_dim_v=hdv,
tile_scheduler_metadata=meta,
is_fp8_kvcache=True,
indices=idx,
softmax_scale=head_dim**-0.5,
)
return out.clone().float()
out_ref = run(cache_contiguous)
out_view = run(cache_view)
assert torch.isfinite(out_ref).all()
assert out_ref.abs().max().item() > 0.0
assert (out_ref - out_view).abs().max().item() == 0.0
def test_indexer_k_quant_and_cache_into_unified_slot_view():
"""indexer_k_quant_and_cache (DeepSeek V3.2/V4 DSA indexer K write) must
write correctly into a per-layer view whose block stride is the full
unified slot, with zero bleed into the other layers' segments."""
from vllm import _custom_ops as ops
torch.manual_seed(0)
dev = "cuda"
head_dim = 128
quant_block_size = 128
block_size = 64
num_blocks = 16
ntok = 100
# Indexer cache layout per token: head_dim fp8 bytes followed by
# head_dim * 4 / quant_block_size scale bytes.
cache_stride = head_dim + head_dim * 4 // quant_block_size
k = torch.randn(ntok, head_dim, device=dev, dtype=torch.bfloat16)
slot = torch.randperm(num_blocks * block_size, device=dev, dtype=torch.int64)[:ntok]
def write(cache):
ops.indexer_k_quant_and_cache(k, cache, slot, quant_block_size, "ue8m0")
# Contiguous per-layer reference.
ref = torch.zeros(
num_blocks, block_size, cache_stride, device=dev, dtype=torch.uint8
)
write(ref)
# Unified slot holding three layer pages per block; carve the middle one.
n_layers = 3
layer = 1
unified = torch.zeros(
num_blocks, n_layers, block_size, cache_stride, device=dev, dtype=torch.uint8
)
view = unified[:, layer]
assert not view.is_contiguous()
assert view.stride(0) == n_layers * block_size * cache_stride
write(view)
assert torch.equal(ref, view.contiguous())
# Zero bleed into the neighbour layers' segments.
assert unified[:, 0].abs().max().item() == 0
assert unified[:, 2].abs().max().item() == 0
def test_flashattn_mla_dense_decode_unified_slot_view():
"""FA3 decode (FLASH_ATTN_MLA backend) must read a unified-slot
block-major view bit-identically to a contiguous per-layer cache."""
try:
from vllm.vllm_flash_attn import flash_attn_varlen_func
except ImportError:
pytest.skip("vllm_flash_attn is not available")
from vllm.v1.attention.backends.fa_utils import flash_attn_supports_mla
if not flash_attn_supports_mla():
pytest.skip("FA3 MLA requires a Hopper device")
torch.manual_seed(0)
dev = "cuda"
dt = torch.bfloat16
kv_lora_rank = 512
rope_dim = 64
entry = kv_lora_rank + rope_dim # 576
h_q = 16
page = 64
num_blocks = 64
bs = 4
n_layers = 3
layer = 1
q_pe = torch.randn(bs, h_q, rope_dim, device=dev, dtype=dt) * 0.1
q_nope = torch.randn(bs, h_q, kv_lora_rank, device=dev, dtype=dt) * 0.1
kv_data = torch.randn(num_blocks, page, entry, device=dev, dtype=dt) * 0.1
# (A) contiguous per-layer reference.
cache_contiguous = kv_data.clone().contiguous()
# (B) unified slot: view one layer -> inflated stride(0), non-zero offset.
unified = torch.randn(num_blocks, n_layers, page, entry, device=dev, dtype=dt) * 0.1
unified[:, layer].copy_(kv_data)
cache_view = unified[:, layer]
assert not cache_view.is_contiguous()
assert cache_view.stride(0) == n_layers * page * entry
max_blk = num_blocks // bs
block_table = torch.arange(num_blocks, device=dev, dtype=torch.int32).view(
bs, max_blk
)
seq_lens = torch.full((bs,), max_blk * page, device=dev, dtype=torch.int32)
cu_seqlens_q = torch.arange(bs + 1, device=dev, dtype=torch.int32)
def run(cache):
kv_c_cache = cache[..., :kv_lora_rank]
k_pe_cache = cache[..., kv_lora_rank:]
out = flash_attn_varlen_func(
q=q_pe,
k=k_pe_cache.unsqueeze(-2), # Add head dim of 1
v=kv_c_cache.unsqueeze(-2), # Add head dim of 1
q_v=q_nope,
max_seqlen_q=1,
cu_seqlens_q=cu_seqlens_q,
max_seqlen_k=int(seq_lens.max().item()),
seqused_k=seq_lens,
block_table=block_table,
softmax_scale=entry**-0.5,
causal=True,
fa_version=3,
)
return out.clone().float()
out_ref = run(cache_contiguous)
out_view = run(cache_view)
assert torch.isfinite(out_ref).all()
assert out_ref.abs().max().item() > 0.0
assert (out_ref - out_view).abs().max().item() == 0.0
def test_flashmla_dense_fp8_decode_unified_slot_view():
"""FlashMLA dense fp8 decode (FLASHMLA backend with quantized KV cache)
must read a unified-slot block-major view bit-identically to a contiguous
per-layer fp8 cache."""
import vllm.v1.attention.ops.flashmla as fm
ok, reason = fm.is_flashmla_dense_supported()
if not ok:
pytest.skip(reason)
torch.manual_seed(0)
dev = "cuda"
head_dim = 576
hdv = 512
h_q = 128
page = 64
num_blocks = 64
bs = 4
n_layers = 3
layer = 1
q = torch.randn(bs, 1, h_q, head_dim, device=dev, dtype=torch.bfloat16) * 0.1
kv_data = (torch.randn(num_blocks, page, head_dim, device=dev) * 0.1).to(
torch.float8_e4m3fn
)
# (A) contiguous per-layer reference.
cache_contiguous = kv_data.clone().contiguous()
# (B) unified slot: view one layer -> inflated stride(0), non-zero offset.
unified = (torch.randn(num_blocks, n_layers, page, head_dim, device=dev) * 0.1).to(
torch.float8_e4m3fn
)
unified[:, layer].copy_(kv_data)
cache_view = unified[:, layer]
assert not cache_view.is_contiguous()
assert cache_view.stride(0) == n_layers * page * head_dim
max_blk = num_blocks // bs
block_table = torch.arange(num_blocks, device=dev, dtype=torch.int32).view(
bs, max_blk
)
cache_seqlens = torch.full((bs,), max_blk * page, device=dev, dtype=torch.int32)
descale = torch.ones(1, device=dev, dtype=torch.float32)
def run(kc):
tile_md, num_splits = fm.get_mla_metadata_dense_fp8(cache_seqlens, h_q, 1)
out, _ = fm.flash_mla_with_kvcache_fp8(
q=q,
k_cache=kc.unsqueeze(-2), # Add head dim of 1
block_table=block_table,
cache_seqlens=cache_seqlens,
head_dim_v=hdv,
tile_scheduler_metadata=tile_md,
num_splits=num_splits,
softmax_scale=head_dim**-0.5,
causal=True,
descale_q=descale,
descale_k=descale,
)
return out.clone().float()
out_ref = run(cache_contiguous)
out_view = run(cache_view)
assert torch.isfinite(out_ref).all()
assert out_ref.abs().max().item() > 0.0
assert (out_ref - out_view).abs().max().item() == 0.0
def test_flashinfer_mla_dense_fp8_decode_unified_slot_view():
"""FlashInfer MLA dense decode with an fp8 KV cache must read a
unified-slot block-major view bit-identically to a contiguous per-layer
cache."""
try:
from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
except ImportError:
pytest.skip("flashinfer is not available")
from vllm.platforms import current_platform
if not current_platform.is_device_capability_family(100):
pytest.skip("FlashInfer trtllm-gen MLA requires sm100")
torch.manual_seed(0)
dev = "cuda"
kv_lora_rank = 512
qk_rope_head_dim = 64
qk_nope_head_dim = 128
head_dim = kv_lora_rank + qk_rope_head_dim # 576
num_qo_heads = 128
page = 64
num_blocks = 64
bs = 4
n_layers = 3
layer = 1
# With a quantized KV cache the decode query is quantized to fp8 as well
# (trtllm-gen has no bf16-query x fp8-cache decode kernel).
q = (torch.randn(bs, 1, num_qo_heads, head_dim, device=dev) * 0.1).to(
torch.float8_e4m3fn
)
kv_data = (torch.randn(num_blocks, 1, page, head_dim, device=dev) * 0.1).to(
torch.float8_e4m3fn
)
# (A) contiguous per-layer reference.
kv_contiguous = kv_data.clone().contiguous()
# (B) unified slot: view one layer -> inflated stride(0), non-zero offset.
unified = (
torch.randn(num_blocks, n_layers, 1, page, head_dim, device=dev) * 0.1
).to(torch.float8_e4m3fn)
unified[:, layer].copy_(kv_data)
kv_view = unified[:, layer]
assert not kv_view.is_contiguous()
assert kv_view.stride(0) == n_layers * 1 * page * head_dim
max_blk = num_blocks // bs
block_tables = torch.arange(num_blocks, device=dev, dtype=torch.int32).view(
bs, max_blk
)
seq_lens = torch.full((bs,), max_blk * page, device=dev, dtype=torch.int32)
ws = torch.empty(128 * 1024 * 1024, dtype=torch.int8, device=dev)
scale = head_dim**-0.5
def run(kv):
return trtllm_batch_decode_with_kv_cache_mla(
query=q,
kv_cache=kv,
workspace_buffer=ws,
qk_nope_head_dim=qk_nope_head_dim,
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
block_tables=block_tables,
seq_lens=seq_lens,
max_seq_len=int(seq_lens.max().item()),
bmm1_scale=scale,
bmm2_scale=1.0,
).clone()
out_ref = run(kv_contiguous).float()
out_view = run(kv_view).float()
assert torch.isfinite(out_ref).all()
assert (out_ref - out_view).abs().max().item() == 0.0