[Attention] Enable masked MHA for GLM-5 head dimensions (#53785)

Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
This commit is contained in:
Matthew Bonanni
2026-08-27 21:17:12 +00:00
committed by GitHub
co-authored by OpenAI Codex
parent 3a9bfc209f
commit de9250ac9e
10 changed files with 527 additions and 74 deletions
+32 -6
View File
@@ -1143,12 +1143,26 @@ def main():
]
else:
variants = available_variants
model_sweep = getattr(args, "model_parameter_sweep", None)
if model_sweep is None:
num_q_heads_values = [args.num_q_heads]
elif model_sweep.param_name == "num_q_heads":
num_q_heads_values = model_sweep.values
else:
raise ValueError(
"mha_vs_mqa mode only supports a num_q_heads model parameter sweep"
)
benchmark_specs = [
(spec, num_q_heads)
for spec in args.batch_specs
for num_q_heads in num_q_heads_values
]
masked_mha_max_seq_len = getattr(
args, "sparse_mla_masked_mha_max_seq_len", None
)
formatter = ResultsFormatter(console)
total = 0
for spec in args.batch_specs:
for spec, _ in benchmark_specs:
q_len = max(request.q_len for request in parse_batch_spec(spec))
for variant_label, _, _ in variants:
if (
@@ -1164,7 +1178,7 @@ def main():
total += len(backends)
with tqdm(total=total, desc="Benchmarking") as pbar:
for spec in args.batch_specs:
for spec, num_q_heads in benchmark_specs:
q_len = max(request.q_len for request in parse_batch_spec(spec))
for backend in backends:
for variant_label, force_mqa, mha_mode in variants:
@@ -1178,12 +1192,17 @@ def main():
and q_len > masked_mha_max_seq_len
):
continue
result_backend = f"{backend}_{variant_label}"
if model_sweep is not None:
result_backend = model_sweep.get_label(
result_backend, num_q_heads
)
config = BenchmarkConfig(
backend=f"{backend}_{variant_label}",
backend=result_backend,
batch_spec=spec,
num_layers=args.num_layers,
head_dim=args.head_dim,
num_q_heads=args.num_q_heads,
num_q_heads=num_q_heads,
num_kv_heads=args.num_kv_heads,
block_size=args.block_size,
device=args.device,
@@ -1211,7 +1230,7 @@ def main():
# run_mla_benchmark needs the real backend name
from mla_runner import run_mla_benchmark as run_mla
run_label = f"{backend}_{variant_label} {spec}"
run_label = f"{backend}_{variant_label} {num_q_heads}h {spec}"
pbar.set_postfix_str(run_label)
try:
@@ -1248,7 +1267,14 @@ def main():
# Display results with variant labels as separate "backends"
console.print("\n[bold green]MHA vs MQA Results:[/]")
variant_backends = [f"{b}_{v}" for b in backends for v, _, _ in variants]
variant_backends = []
for num_q_heads in num_q_heads_values:
for backend in backends:
for variant, _, _ in variants:
label = f"{backend}_{variant}"
if model_sweep is not None:
label = model_sweep.get_label(label, num_q_heads)
variant_backends.append(label)
formatter.print_table(all_results, variant_backends)
# Handle model parameter sweep mode
@@ -0,0 +1,59 @@
mode: mha_vs_mqa
model:
name: "glm-5.x"
num_layers: 78
num_q_heads: 64
num_kv_heads: 1
head_dim: 576
kv_lora_rank: 512
qk_nope_head_dim: 192
qk_rope_head_dim: 64
v_head_dim: 256
block_size: 128
max_model_len: 131072
model_parameter_sweep:
param_name: "num_q_heads"
values: [64, 32, 16, 8]
label_format: "{backend}_{value}h"
batch_specs:
- "q256s2048"
- "q512s2048"
- "q1024s2048"
- "q1536s2048"
- "q2048"
- "q256s4096"
- "q512s4096"
- "q1024s4096"
- "q1536s4096"
- "q2048s4096"
- "q4096"
- "q256s8192"
- "q512s8192"
- "q1024s8192"
- "q1536s8192"
- "q2048s8192"
- "q4096s8192"
- "q8192"
- "q256s16384"
- "q512s16384"
- "q1024s16384"
- "q1536s16384"
- "q2048s16384"
- "q4096s16384"
- "q8192s16384"
- "q16384"
- "q256s32768"
- "q512s32768"
- "q1024s32768"
- "q1536s32768"
- "q2048s32768"
- "q4096s32768"
- "q8192s32768"
- "q16384s32768"
- "q32768"
backends: ["FLASHMLA_SPARSE", "FLASHINFER_MLA_SPARSE"]
device: "cuda:0"
profile_memory: false
sparse_mla_masked_mha_max_seq_len: 131072
sparse_mla_mha_variants: ["masked_mha", "mqa"]
sparse_mla_topk_pattern: "random"
@@ -313,6 +313,9 @@ _BACKEND_PROPERTIES = {
"FLASHMLA_SPARSE": {
"query_format": "concat", # Single concatenated tensor (vs tuple)
},
"FLASHINFER_MLA_SPARSE": {
"query_format": "concat", # Single concatenated tensor (vs tuple)
},
}
@@ -1184,6 +1187,35 @@ def _run_mla_benchmark_batched(
)
# Run each benchmark with the shared impl
for config, threshold, num_splits in configs_with_params:
force_masked_mha = (
is_sparse and getattr(config, "sparse_mla_mha_mode", "auto") == "masked"
)
if force_masked_mha:
impl.masked_mha_available = True
from vllm.model_executor.layers.attention.sparse_mla_attention import (
GLOBAL_TOPK_MASK_MAX_BYTES,
_topk_mask_shape,
)
requests = parse_batch_spec(config.batch_spec)
max_query_len = max(request.q_len for request in requests)
mask_shape = _topk_mask_shape(
len(requests), max_query_len, max_query_len
)
mask_words = max(
GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
mask_shape[0] * mask_shape[1] * mask_shape[2],
)
if (
builder_instance.topk_mask_workspace is None
or builder_instance.topk_mask_workspace.numel() != mask_words
):
builder_instance.topk_mask_workspace = torch.zeros(
mask_words,
dtype=torch.int32,
device=device,
)
# Set threshold for this benchmark (FlashAttn/FlashMLA only)
original_threshold = None
effective_threshold = threshold
@@ -39,7 +39,7 @@ else()
FetchContent_Declare(
vllm-flash-attn
GIT_REPOSITORY https://github.com/vllm-project/flash-attention.git
GIT_TAG 4eb29359a51b4fe67c8c0f0c5e931236971aa492
GIT_TAG 06bdd47c0d0383daf6a2ff0c418faff9c6da16e5
GIT_PROGRESS TRUE
# Don't share the vllm-flash-attn build between build types
BINARY_DIR ${CMAKE_BINARY_DIR}/vllm-flash-attn
@@ -8,6 +8,7 @@ import torch
import vllm.model_executor.layers.sparse_attn_indexer as sparse_indexer
from vllm.config import CUDAGraphMode
from vllm.models.deepseek_v32 import attention as deepseek_v32_attention
from vllm.models.deepseek_v32.attention import DeepseekV32Attention
from vllm.v1.attention.backends.mla.indexer import DeepseekV32IndexerMetadata
@@ -49,7 +50,6 @@ def make_mla_metadata(*, use_dense_mha: bool = True, num_decode_tokens: int = 0)
[
"short",
"threshold_mismatch",
"mqa_only_layer",
"force_mqa",
"mla_decode",
"capture",
@@ -141,11 +141,7 @@ def test_short_prefill_updates_k_cache_before_scoring_decision(
topk_indices = torch.full((7, 2048), 17, dtype=torch.int32)
def run_indexer():
if batch_kind == "mqa_only_layer":
assert not DeepseekV32Attention.supports_dense_mha_prefill
dense_mha_layer = ""
else:
dense_mha_layer = MLA_LAYER
assert DeepseekV32Attention.supports_dense_mha_prefill
return sparse_indexer.sparse_attn_indexer(
hidden_states,
INDEXER_LAYER,
@@ -163,7 +159,7 @@ def test_short_prefill_updates_k_cache_before_scoring_decision(
topk_indices,
False,
False,
dense_mha_layer,
MLA_LAYER,
)
if should_skip:
@@ -222,3 +218,75 @@ def test_skipped_k_cache_insert_accepts_no_k(
assert result is topk_indices
assert torch.all(topk_indices == -1)
@pytest.mark.parametrize("fp8_query", [False, True])
def test_deepseek_v32_dispatches_selected_mha(
monkeypatch: pytest.MonkeyPatch,
fp8_query: bool,
) -> None:
attn_metadata = SimpleNamespace(num_actual_tokens=2)
kv_cache = torch.empty(1)
monkeypatch.setattr(
deepseek_v32_attention,
"get_attention_context",
lambda _: (attn_metadata, None, kv_cache, None),
)
observed = {}
def record_forward_impl(*args):
observed["args"] = args
layer = SimpleNamespace(
indexer=None,
skip_topk=False,
layer_name=MLA_LAYER,
use_pcp=False,
_fp8_query=fp8_query,
_use_sparse_mha=lambda _: True,
rotary_emb=lambda _positions, q: (q + 1, None),
forward_impl=record_forward_impl,
)
q_nope = torch.randn(2, 1, 2)
q_pe = torch.randn(2, 1, 2)
mqa_q = torch.randn(2, 1, 2)
mha_q = torch.cat((q_nope, q_pe + 1 if fp8_query else mqa_q), dim=-1)
kv_c = torch.empty(2, 2)
k_pe = torch.empty(2, 2)
output = torch.empty(2, 2)
DeepseekV32Attention._sparse_indexer_and_attn(
layer,
torch.arange(2),
torch.empty(2, 2),
q_nope,
q_pe,
None,
None,
None,
kv_c,
k_pe,
torch.empty(2, 1, 2),
mqa_q,
output,
)
expected_args = (
mha_q,
kv_c,
kv_cache,
attn_metadata,
output,
)
actual_args = observed["args"]
torch.testing.assert_close(actual_args[0], mha_q)
assert actual_args[2].shape == (2, 1, 2)
assert actual_args[2].data_ptr() == k_pe.data_ptr()
assert all(
actual is expected
for actual, expected in zip(
actual_args[1:2] + actual_args[3:],
expected_args[1:],
)
)
+119
View File
@@ -27,6 +27,7 @@ from vllm.model_executor.layers.attention.mla_attention import (
MLAAttention,
QueryLenSupport,
_DecodeConcatQuantFP8,
_use_masked_mha,
build_mla_chunked_context_metadata,
)
from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
@@ -68,6 +69,124 @@ if current_platform.is_rocm():
DEVICE_TYPE = current_platform.device_type
@pytest.mark.parametrize(
("tensor_parallel_size", "query_len", "expected"),
[
(1, 8192, True),
(1, 9216, False),
(2, 20480, True),
(2, 24576, False),
(4, 48 * 1024, True),
(4, 56 * 1024, False),
(8, 112 * 1024, True),
(8, 128 * 1024, False),
],
)
def test_glm5_masked_mha_pure_prefill_routing(
tensor_parallel_size, query_len, expected
):
assert (
_use_masked_mha(
backend_name="FLASHMLA_SPARSE",
tensor_parallel_size=tensor_parallel_size,
qk_head_dim=256,
v_head_dim=256,
query_len=query_len,
seq_len=query_len,
has_context=False,
)
is expected
)
@pytest.mark.parametrize(
("tensor_parallel_size", "query_len", "seq_len", "expected"),
[
(1, 8192, 8192, False),
(2, 4096, 6144, True),
(2, 4096, 8192, False),
(2, 8192, 10240, True),
(4, 16384, 24576, True),
(4, 16384, 32768, False),
(4, 32768, 34 * 1024, True),
(4, 32768, 36 * 1024, False),
(8, 32768, 49152, True),
(8, 32768, 65536, False),
],
)
def test_glm5_masked_mha_context_routing(
tensor_parallel_size, query_len, seq_len, expected
):
assert (
_use_masked_mha(
backend_name="FLASHMLA_SPARSE",
tensor_parallel_size=tensor_parallel_size,
qk_head_dim=256,
v_head_dim=256,
query_len=query_len,
seq_len=seq_len,
has_context=True,
)
is expected
)
@pytest.mark.parametrize(
("tensor_parallel_size", "query_len", "seq_len", "has_context", "expected"),
[
(4, 36 * 1024, 36 * 1024, False, True),
(4, 40 * 1024, 40 * 1024, False, False),
(4, 4 * 1024, 12 * 1024, True, True),
(4, 4 * 1024, 16 * 1024, True, False),
(4, 24 * 1024, 28 * 1024, True, True),
(4, 32 * 1024, 36 * 1024, True, False),
(8, 64 * 1024, 64 * 1024, False, True),
(8, 68 * 1024, 68 * 1024, False, False),
(8, 4 * 1024, 20 * 1024, True, True),
(8, 16 * 1024, 32 * 1024, True, True),
(8, 48 * 1024, 64 * 1024, True, True),
(8, 56 * 1024, 72 * 1024, True, True),
(8, 63 * 1024, 79 * 1024, True, False),
],
)
def test_glm5_flashinfer_masked_mha_routing(
tensor_parallel_size, query_len, seq_len, has_context, expected
):
assert (
_use_masked_mha(
backend_name="FLASHINFER_MLA_SPARSE",
tensor_parallel_size=tensor_parallel_size,
qk_head_dim=256,
v_head_dim=256,
query_len=query_len,
seq_len=seq_len,
has_context=has_context,
)
is expected
)
def test_masked_mha_routing_is_dimension_specific():
assert _use_masked_mha(
backend_name="FLASHMLA_SPARSE",
tensor_parallel_size=1,
qk_head_dim=192,
v_head_dim=128,
query_len=1536,
seq_len=2048,
has_context=True,
)
assert not _use_masked_mha(
backend_name="FLASHMLA_SPARSE",
tensor_parallel_size=1,
qk_head_dim=128,
v_head_dim=128,
query_len=1536,
seq_len=2048,
has_context=True,
)
@pytest.mark.parametrize(
("cache_dtype", "expected_quant_mode"),
[
+42 -5
View File
@@ -21,6 +21,7 @@ from tests.v1.attention.utils import (
)
from vllm import _custom_ops as ops
from vllm.config import set_current_vllm_config
from vllm.model_executor.layers.attention.mla_attention import _use_masked_mha
from vllm.model_executor.layers.attention.sparse_mla_attention import (
GLOBAL_TOPK_MASK_MAX_BYTES,
_masked_mha_workspace_fits,
@@ -884,6 +885,35 @@ def test_masked_mha_workspace_fits_single_request_boundary(max_query_len, expect
)
@pytest.mark.parametrize(
("backend_name", "tensor_parallel_size", "query_len"),
[
("FLASHMLA_SPARSE", 4, 48 * 1024),
("FLASHMLA_SPARSE", 8, 112 * 1024),
("FLASHINFER_MLA_SPARSE", 4, 36 * 1024),
("FLASHINFER_MLA_SPARSE", 8, 64 * 1024),
],
)
def test_masked_mha_workspace_guards_long_routing_policy(
backend_name, tensor_parallel_size, query_len
):
assert _use_masked_mha(
backend_name=backend_name,
tensor_parallel_size=tensor_parallel_size,
qk_head_dim=256,
v_head_dim=256,
query_len=query_len,
seq_len=query_len,
has_context=False,
)
assert not _masked_mha_workspace_fits(
batch_size=1,
max_query_len=query_len,
max_context_chunk_seq_len=0,
workspace_numel=GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
)
def test_masked_mha_workspace_fits_accounts_for_batch_and_context():
"""Request count and context chunk length are independent multipliers."""
base = dict(batch_size=2, max_query_len=2048, max_context_chunk_seq_len=2048)
@@ -912,14 +942,25 @@ PREFILL_BATCH_SPECS = {
)
@pytest.mark.parametrize("batch_name", list(PREFILL_BATCH_SPECS.keys()))
@pytest.mark.parametrize("kv_cache_dtype", ["auto"])
@pytest.mark.parametrize(
("num_heads", "qk_nope_head_dim", "qk_rope_head_dim", "v_head_dim"),
[
pytest.param(128, 128, 64, 128, id="deepseek_hd192_v128"),
pytest.param(64, 192, 64, 256, id="glm5_hd256_v256"),
],
)
def test_sparse_backend_prefill_correctness(
default_vllm_config,
dist_init,
batch_name,
kv_cache_dtype,
num_heads,
qk_nope_head_dim,
qk_rope_head_dim,
v_head_dim,
workspace_init,
):
"""Test single-pass dense and masked MHA for sparse MLA prefill."""
"""Test dense and masked MHA across supported sparse MLA dimensions."""
backend_cls = FlashMLASparseBackend
batch_spec = PREFILL_BATCH_SPECS[batch_name]
@@ -927,11 +968,7 @@ def test_sparse_backend_prefill_correctness(
dtype = torch.bfloat16
block_size = 64
num_heads = 128
kv_lora_rank = 512
qk_nope_head_dim = 128
qk_rope_head_dim = 64
v_head_dim = 128
head_size = kv_lora_rank + qk_rope_head_dim
masked_mha = batch_name.startswith("masked_mha")
topk_tokens = 200 if masked_mha else 512
@@ -827,25 +827,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
use_mha = True
if self.impl.is_sparse and num_mha_tokens > 0:
prefill = getattr(attn_metadata, "prefill", None)
use_dense_mha = getattr(prefill, "use_dense_mha", False)
prefill_max_seq_len = attn_metadata.prefill_max_seq_len # type: ignore[attr-defined]
use_masked_mha = (
self.prefill_backend is not None
and self.impl.masked_mha_available # type: ignore[attr-defined]
and self.impl.dcp_world_size <= 1
and prefill is not None
and _use_masked_mha(
backend_name=self.attn_backend.get_name(),
tensor_parallel_size=self._vllm_config.parallel_config.tensor_parallel_size,
query_len=prefill.max_query_len,
seq_len=prefill_max_seq_len,
)
and self.impl.masked_mha_workspace_fits(prefill) # type: ignore[attr-defined]
)
use_mha = (use_dense_mha or use_masked_mha) and not (
self._vllm_config.attention_config.sparse_mla_force_mqa
)
use_mha = self._use_sparse_mha(attn_metadata)
if not use_mha:
num_mqa_tokens = q.size(0)
num_mha_tokens = 0
@@ -1063,6 +1045,31 @@ class MLAAttention(nn.Module, AttentionLayerBase):
output_padded[num_actual_toks:].zero_()
return output_padded
def _use_sparse_mha(self, attn_metadata: "MLACommonMetadata") -> bool:
prefill = attn_metadata.prefill
if prefill is None:
return False
use_masked_mha = (
self.prefill_backend is not None
and self.impl.masked_mha_available # type: ignore[attr-defined]
and self.impl.dcp_world_size <= 1
and _use_masked_mha(
backend_name=self.attn_backend.get_name(),
tensor_parallel_size=(
self._vllm_config.parallel_config.tensor_parallel_size
),
qk_head_dim=self.qk_nope_head_dim + self.qk_rope_head_dim,
v_head_dim=self.v_head_dim,
query_len=prefill.max_query_len,
seq_len=attn_metadata.prefill_max_seq_len, # type: ignore[attr-defined]
has_context=prefill.chunked_context is not None,
)
and self.impl.masked_mha_workspace_fits(prefill) # type: ignore[attr-defined]
)
return (prefill.use_dense_mha or use_masked_mha) and not (
self._vllm_config.attention_config.sparse_mla_force_mqa
)
def process_weights_after_loading(self, act_dtype: torch.dtype):
# Let per-backend impls do their own weight packing first (no-op
# unless overridden), mirroring Attention.process_weights_after_loading.
@@ -1619,36 +1626,121 @@ def get_mla_dims(model_config: ModelConfig) -> MLADims:
)
_DSV32_MASKED_MHA_THRESHOLDS: dict[str, dict[int, tuple[int | None, ...]]] = {
"FLASHMLA_SPARSE": {
1: (1536, 4096, None, None, None),
2: (512, 1024, 4096, None, None),
4: (512, 1024, 1536, 8192, None),
8: (512, 512, 1024, 2048, 16384),
_MaskedMHARanges = tuple[tuple[int, int], ...]
_MASKED_MHA_THRESHOLDS: dict[
tuple[int, int],
dict[str, dict[int, tuple[_MaskedMHARanges, _MaskedMHARanges | None]]],
] = {
(192, 128): {
"FLASHMLA_SPARSE": {
1: (((2048, 1536), (4096, 4096)), None),
2: (((2048, 512), (4096, 1024), (8192, 4096)), None),
4: (
((2048, 512), (4096, 1024), (8192, 1536), (16384, 8192)),
None,
),
8: (
(
(2048, 512),
(4096, 512),
(8192, 1024),
(16384, 2048),
(32768, 16384),
),
None,
),
},
"FLASHINFER_MLA_SPARSE": {
8: (
(
(2048, 512),
(4096, 1024),
(8192, 1024),
(16384, 2048),
(32768, 32768),
),
None,
),
},
},
"FLASHINFER_MLA_SPARSE": {
8: (512, 1024, 1024, 2048, 32768),
(256, 256): {
"FLASHMLA_SPARSE": {
1: (((8 * 1024, 0),), ()),
2: (
((20 * 1024, 0),),
((6 * 1024, 4 * 1024), (10 * 1024, 8 * 1024)),
),
4: (
((48 * 1024, 0),),
(
(8 * 1024, 4 * 1024),
(16 * 1024, 8 * 1024),
(24 * 1024, 16 * 1024),
(34 * 1024, 32 * 1024),
),
),
8: (
((112 * 1024, 0),),
(
(8 * 1024, 4 * 1024),
(16 * 1024, 8 * 1024),
(32 * 1024, 16 * 1024),
(48 * 1024, 32 * 1024),
),
),
},
"FLASHINFER_MLA_SPARSE": {
4: (
((36 * 1024, 0),),
(
(12 * 1024, 4 * 1024),
(16 * 1024, 8 * 1024),
(24 * 1024, 16 * 1024),
(28 * 1024, 24 * 1024),
),
),
8: (
((64 * 1024, 0),),
(
(20 * 1024, 4 * 1024),
(24 * 1024, 8 * 1024),
(32 * 1024, 16 * 1024),
(48 * 1024, 32 * 1024),
(56 * 1024, 40 * 1024),
(64 * 1024, 48 * 1024),
(72 * 1024, 56 * 1024),
),
),
},
},
}
_DSV32_SEQ_LEN_BUCKETS = (2048, 4096, 8192, 16384, 32768)
def _use_masked_mha(
*,
backend_name: str,
tensor_parallel_size: int,
qk_head_dim: int,
v_head_dim: int,
query_len: int,
seq_len: int,
has_context: bool,
) -> bool:
thresholds = _DSV32_MASKED_MHA_THRESHOLDS.get(backend_name, {}).get(
tensor_parallel_size
thresholds = (
_MASKED_MHA_THRESHOLDS.get((qk_head_dim, v_head_dim), {})
.get(backend_name, {})
.get(tensor_parallel_size)
)
if thresholds is None:
return False
for bucket_idx, bucket_seq_len in enumerate(_DSV32_SEQ_LEN_BUCKETS):
if seq_len <= bucket_seq_len:
min_query_len = thresholds[bucket_idx]
return min_query_len is not None and query_len >= min_query_len
pure_prefill_ranges, context_ranges = thresholds
ranges = pure_prefill_ranges
if has_context and context_ranges is not None:
ranges = context_ranges
for max_seq_len, min_query_len in ranges:
if seq_len <= max_seq_len:
return query_len >= min_query_len
return False
@@ -88,16 +88,27 @@ def _is_masked_mha_available(
"""Check if masked MHA can ever fire for this model configuration."""
if not current_platform.is_device_capability_family(100):
return False
if (
num_heads_total != 128
or kv_lora_rank != 512
or qk_nope_head_dim != 128
or qk_rope_head_dim != 64
or v_head_dim != 128
model_dims = (
num_heads_total,
kv_lora_rank,
qk_nope_head_dim,
qk_rope_head_dim,
v_head_dim,
)
if model_dims not in (
(128, 512, 128, 64, 128),
(64, 512, 192, 64, 256),
):
return False
qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
fa_version = get_flash_attn_version(head_size=qk_head_dim, head_size_v=v_head_dim)
if qk_head_dim == 256 and v_head_dim == 256:
# This path uses contiguous K/V, so it does not need the paged-KV
# features that keep head-dim 256 disabled in the general FA selector.
fa_version = get_flash_attn_version()
else:
fa_version = get_flash_attn_version(
head_size=qk_head_dim, head_size_v=v_head_dim
)
return fa_version == 4 and not is_quantized_kv_cache(kv_cache_dtype)
+23 -14
View File
@@ -119,8 +119,6 @@ class DeepseekV32Attention(MLAAttention):
indexer: "DeepseekV32Indexer | None"
indexer_cls: "type[DeepseekV32Indexer]" = DeepseekV32Indexer
supports_dense_mha_prefill = False
def __init__(
self,
vllm_config: VllmConfig,
@@ -303,15 +301,6 @@ class DeepseekV32Attention(MLAAttention):
device=hidden_states.device,
)
forward_context = get_forward_context()
attn_metadata_raw = forward_context.attn_metadata
if isinstance(attn_metadata_raw, dict):
attn_metadata = attn_metadata_raw.get(self.layer_name)
elif isinstance(attn_metadata_raw, list):
attn_metadata = attn_metadata_raw[0].get(self.layer_name)
else:
attn_metadata = attn_metadata_raw
attn_metadata = cast("MLACommonMetadata | None", attn_metadata)
slot_mapping = forward_context.slot_mapping
assert isinstance(slot_mapping, dict)
mla_slot = slot_mapping.get(self.layer_name)
@@ -337,7 +326,7 @@ class DeepseekV32Attention(MLAAttention):
indexer_softmax_scale = 0.0
indexer_n_head_scale = 0.0
if attn_metadata is None or self.use_pcp:
if forward_context.attn_metadata is None or self.use_pcp:
mla_kv_cache = None
mla_k_scale = None
indexer_k_cache = None
@@ -346,8 +335,8 @@ class DeepseekV32Attention(MLAAttention):
mla_kv_cache = self.kv_cache
mla_k_scale = self._k_scale
kv_c_out = torch.empty_like(kv_c) if self.use_pcp else None
k_pe_out = torch.empty_like(k_pe) if self.use_pcp else None
kv_c_out = torch.empty_like(kv_c)
k_pe_out = torch.empty_like(k_pe)
q_c = fused_norm_rope(
positions,
q_c,
@@ -403,7 +392,10 @@ class DeepseekV32Attention(MLAAttention):
)
self._sparse_indexer_and_attn(
positions,
q_c,
q_nope,
q_pe,
index_q_fp8,
index_k_out,
index_weights_out,
@@ -418,7 +410,10 @@ class DeepseekV32Attention(MLAAttention):
@eager_break_during_capture
def _sparse_indexer_and_attn(
self,
positions: torch.Tensor,
q_c: torch.Tensor,
q_nope: torch.Tensor,
q_pe: torch.Tensor,
index_q_fp8: torch.Tensor | None,
index_k: torch.Tensor | None,
index_weights_out: torch.Tensor | None,
@@ -498,6 +493,20 @@ class DeepseekV32Attention(MLAAttention):
output.zero_()
return
if self._use_sparse_mha(attn_metadata):
assert kv_c is not None and k_pe is not None
mha_q_pe = self.rotary_emb(positions, q_pe)[0] if self._fp8_query else mqa_q
mha_q = torch.cat((q_nope, mha_q_pe), dim=-1)
self.forward_impl(
mha_q,
kv_c,
k_pe.unsqueeze(1),
kv_cache,
attn_metadata,
output,
)
return
if self._fp8_kv_needs_view:
kv_cache = kv_cache.view(torch.float8_e4m3fn)
if self._fp8_query: