[ROCm][Spec Decode] Add Aiter MLA decode support non-causal draft block (#55966)
Signed-off-by: Poovaiah Palangappa <poovaiah.palangappa@amd.com> Co-authored-by: Poovaiah Palangappa <poovaiah.palangappa@amd.com> Co-authored-by: John Qin <zhengsheng1997@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Hongxia Yang <62075498+hongxiayang@users.noreply.github.com>
ppalanga committed
Sep 15, 2026 at 10:18 UTC
35dc273072ff133743bc5fcfa306bf030fa8730b
5 files changed
+390
-5
tests/kernels/attention/test_rocm_aiter_mla_fp8_support.py
+116
-1
@@ -30,7 +30,10 @@ def reset_aiter_mla_support_cache(monkeypatch: pytest.MonkeyPatch) -> None:
30
31
32
def _install_fake_aiter_modules(
33
- monkeypatch: pytest.MonkeyPatch, *, supports_fp8: bool
33
+ monkeypatch: pytest.MonkeyPatch,
34
+ *,
35
+ supports_fp8: bool,
36
+ supports_causal: bool = False,
37
) -> None:
38
aiter_mod: Any = types.ModuleType("aiter")
39
mla_mod: Any = types.ModuleType("aiter.mla")
@@ -65,6 +68,14 @@ def _install_fake_aiter_modules(
68
69
mla_decode_fwd = mla_decode_fwd_without_fp8
70
71
+ if supports_causal:
72
+ inner = mla_decode_fwd
73
+
74
+ def mla_decode_fwd_with_causal(*args, causal=True, **kwargs):
75
+ return inner(*args, **kwargs)
76
+
77
+ mla_decode_fwd = mla_decode_fwd_with_causal
78
+
79
mla_mod.mla_decode_fwd = mla_decode_fwd
80
aiter_mod.mla = mla_mod
81
@@ -135,3 +146,107 @@ def test_aiter_mla_fp8_support_result_is_cached(monkeypatch):
146
assert _check_aiter_mla_fp8_support() is True
147
assert _check_aiter_mla_fp8_support() is True
148
assert signature_mock.call_count == 1
149
+
150
+
151
+def test_missing_causal_arg_fails_closed(monkeypatch):
152
+ """Old aiter wheels have no causal=; do not report that as a valid draft path."""
153
+ from vllm._aiter_ops import rocm_aiter_ops
154
+ from vllm.v1.attention.backends.mla.rocm_aiter_mla import AiterMLABackend
155
+
156
+ _install_fake_aiter_modules(monkeypatch, supports_fp8=True, supports_causal=False)
157
+ with pytest.raises(RuntimeError, match="causal-only"):
158
+ rocm_aiter_ops.mla_decode_supports_non_causal()
159
+ with pytest.raises(RuntimeError, match="causal-only"):
160
+ AiterMLABackend.supports_non_causal()
161
+
162
+
163
+@pytest.mark.parametrize("has_kernels", [False, True])
164
+def test_supports_non_causal_requires_arch_kernels(monkeypatch, has_kernels):
165
+ """The Python argument alone does not guarantee an arch has the kernel."""
166
+ from vllm._aiter_ops import rocm_aiter_ops
167
+ from vllm.v1.attention.backends.mla import rocm_aiter_mla
168
+ from vllm.v1.attention.backends.mla.rocm_aiter_mla import AiterMLABackend
169
+
170
+ _install_fake_aiter_modules(monkeypatch, supports_fp8=True, supports_causal=True)
171
+ monkeypatch.setattr(
172
+ rocm_aiter_mla,
173
+ "_aiter_mla_non_causal_asm_kernels",
174
+ lambda: has_kernels,
175
+ )
176
+ assert rocm_aiter_ops.mla_decode_supports_non_causal() is True
177
+ assert AiterMLABackend.supports_non_causal() is has_kernels
178
+
179
+
180
+@pytest.mark.parametrize("causal", [True, False])
181
+def test_decode_fwd_forwards_causal_to_aiter(monkeypatch, causal):
182
+ """The decode op must pass the block mask through; omitting it is causal-only."""
183
+ seen: list = []
184
+
185
+ def mla_decode_fwd(*args, causal=None, **kwargs):
186
+ seen.append(causal)
187
+
188
+ mla_mod: Any = types.ModuleType("aiter.mla")
189
+ mla_mod.mla_decode_fwd = mla_decode_fwd
190
+ aiter_mod: Any = types.ModuleType("aiter")
191
+ aiter_mod.mla = mla_mod
192
+ monkeypatch.setitem(sys.modules, "aiter", aiter_mod)
193
+ monkeypatch.setitem(sys.modules, "aiter.mla", mla_mod)
194
+
195
+ import torch
196
+
197
+ import vllm._aiter_ops as aiter_ops
198
+
199
+ aiter_ops._rocm_aiter_mla_decode_fwd_impl(
200
+ torch.zeros(2, 1, 8),
201
+ torch.zeros(2, 1, 1, 8),
202
+ torch.zeros(2, 1, 8),
203
+ torch.zeros(3, dtype=torch.int32),
204
+ 1,
205
+ causal=causal,
206
+ )
207
+ assert seen == [causal]
208
+
209
+
210
+def _aiter_mla_validate(
211
+ monkeypatch,
212
+ *,
213
+ dtype,
214
+ kv_cache_dtype,
215
+ use_non_causal,
216
+):
217
+ from vllm.platforms.interface import DeviceCapability
218
+ from vllm.v1.attention.backend import AttentionType
219
+ from vllm.v1.attention.backends.mla import rocm_aiter_mla
220
+ from vllm.v1.attention.backends.mla.rocm_aiter_mla import AiterMLABackend
221
+
222
+ _install_fake_aiter_modules(monkeypatch, supports_fp8=True, supports_causal=True)
223
+ monkeypatch.setattr(
224
+ rocm_aiter_mla, "_aiter_mla_non_causal_asm_kernels", lambda: True
225
+ )
226
+ return AiterMLABackend.validate_configuration(
227
+ head_size=576,
228
+ dtype=dtype,
229
+ kv_cache_dtype=kv_cache_dtype,
230
+ block_size=1,
231
+ use_mla=True,
232
+ has_sink=False,
233
+ use_sparse=False,
234
+ use_mm_prefix=False,
235
+ use_per_head_quant_scales=False,
236
+ device_capability=DeviceCapability(9, 5),
237
+ attn_type=AttentionType.DECODER,
238
+ use_non_causal=use_non_causal,
239
+ )
240
+
241
+
242
+def test_non_causal_fp16_auto_cache_is_rejected(monkeypatch):
243
+ """Pinned AITER aborts fp16 Q; auto-select must fall through to Triton."""
244
+ import torch
245
+
246
+ reasons = _aiter_mla_validate(
247
+ monkeypatch,
248
+ dtype=torch.float16,
249
+ kv_cache_dtype="auto",
250
+ use_non_causal=True,
251
+ )
252
+ assert any("non-causal fp16" in reason for reason in reasons)
tests/v1/attention/test_rocm_aiter_mla_fp8_decode_routing.py
+5
@@ -176,3 +176,8 @@ def test_pad_unpad_round_trip_preserves_head_order(num_heads):
176
177
assert unpadded.shape == q.shape
178
torch.testing.assert_close(unpadded, q)
179
+
180
+
181
+def test_a_non_causal_block_never_routes_to_gluon(gluon_available):
182
+ """A small-head block that normally uses Gluon must use ASM when non-causal."""
183
+ assert not AiterMLAHelper.use_gluon_verify(12, 8, "auto", causal=False)
tests/v1/attention/test_rocm_aiter_mla_mtp_split.py
+98
@@ -130,6 +130,7 @@ def _builder(
130
_mla_q_dtype=torch.bfloat16,
131
_mla_kv_dtype=torch.bfloat16,
132
decode_attn_out_dtype=torch.bfloat16,
133
+ _decode_causal=True,
134
)
135
# Bound to the stub rather than faked: the verify flatten's per-row view is
136
# part of what _build_decode is being tested for.
@@ -155,6 +156,25 @@ def test_backend_declares_uniform_batch_support():
156
)
157
158
159
+@pytest.mark.parametrize("causal", [True, False])
160
+def test_build_hands_common_causal_flag_to_aiter_builder(monkeypatch, causal):
161
+ seen = []
162
+
163
+ def fake_common_build(self, *args):
164
+ seen.append(self._decode_causal)
165
+ return SimpleNamespace(decode=None, prefill=None)
166
+
167
+ monkeypatch.setattr(
168
+ rocm_aiter_mla.MLACommonMetadataBuilder, "build", fake_common_build
169
+ )
170
+ builder = object.__new__(AiterMLAMetadataBuilder)
171
+ builder._fp8_prefill_enabled = False
172
+
173
+ builder.build(0, SimpleNamespace(causal=causal))
174
+
175
+ assert seen == [causal]
176
+
177
+
178
def test_dcp_verify_row_view_is_causal_per_row():
179
"""Row t must cover this rank's share of positions [0, seq_len - qlen + t].
180
@@ -949,3 +969,81 @@ def test_persistent_metadata_gate_without_gluon_build(
969
970
assert metadata.has_persistent_metadata is expect_persistent
971
assert get_mla_metadata_v1.called is expect_persistent
972
+
973
+
974
+def _build_non_causal(monkeypatch, *, num_heads, kv_cache_dtype, qlen, mtp_qlen):
975
+ """Drive _build_decode with causal=False and hand back the aiter mock."""
976
+ get_mla_metadata_v1 = mock.MagicMock()
977
+ monkeypatch.setitem(
978
+ sys.modules,
979
+ "aiter",
980
+ SimpleNamespace(get_mla_metadata_v1=get_mla_metadata_v1),
981
+ )
982
+ monkeypatch.setattr(
983
+ rocm_aiter_mla, "_expand_page_indices_kernel", _NoOpTritonKernel()
984
+ )
985
+ builder = _builder(
986
+ mtp_decode_qlen=mtp_qlen,
987
+ kv_cache_dtype=kv_cache_dtype,
988
+ num_heads=num_heads,
989
+ )
990
+ builder._decode_causal = False
991
+ metadata = AiterMLAMetadataBuilder._build_decode(
992
+ builder,
993
+ block_table_tensor=torch.arange(16, dtype=torch.int32).view(2, 8),
994
+ seq_lens_device=torch.tensor([7, 5], dtype=torch.int32),
995
+ max_seq_len=7,
996
+ query_start_loc_cpu=torch.tensor([0, qlen, 2 * qlen], dtype=torch.int32),
997
+ query_start_loc_device=torch.tensor([0, qlen, 2 * qlen], dtype=torch.int32),
998
+ num_decode_tokens=2 * qlen,
999
+ dcp_tot_seq_lens_device=None,
1000
+ )
1001
+ return metadata, get_mla_metadata_v1
1002
+
1003
+
1004
+def test_non_causal_build_hands_the_mask_to_aiter(monkeypatch):
1005
+ """The schedule carries the mask, so is_causal must follow the block."""
1006
+ metadata, get_mla_metadata_v1 = _build_non_causal(
1007
+ monkeypatch, num_heads=12, kv_cache_dtype="auto", qlen=8, mtp_qlen=8
1008
+ )
1009
+ assert metadata.has_persistent_metadata
1010
+ # 6th positional arg of get_mla_metadata_v1 is is_causal.
1011
+ assert get_mla_metadata_v1.call_args.args[5] is False
1012
+
1013
+
1014
+@pytest.mark.parametrize("num_heads", [12, 16, 48])
1015
+def test_a_two_token_fp8_block_is_refused_without_a_fold(monkeypatch, num_heads):
1016
+ """16-head (and padded-to-16) qlen-2 fp8 has no non-causal kernel.
1017
+
1018
+ 48/80/112 (and 33 padded to 48) fold to H16 while keeping Q2, so they
1019
+ must be refused the same way as native 16-head qlen 2.
1020
+ """
1021
+ with pytest.raises(ValueError, match="2-token"):
1022
+ _build_non_causal(
1023
+ monkeypatch,
1024
+ num_heads=num_heads,
1025
+ kv_cache_dtype="fp8",
1026
+ qlen=2,
1027
+ mtp_qlen=8,
1028
+ )
1029
+
1030
+
1031
+def test_a_two_token_fp8_block_is_allowed_when_folded(monkeypatch):
1032
+ """32 heads at qlen 2 folds onto the 16-head / 4-token kernel."""
1033
+ metadata, get_mla_metadata_v1 = _build_non_causal(
1034
+ monkeypatch,
1035
+ num_heads=32,
1036
+ kv_cache_dtype="fp8",
1037
+ qlen=2,
1038
+ mtp_qlen=8,
1039
+ )
1040
+ assert metadata.has_persistent_metadata
1041
+ assert get_mla_metadata_v1.called
1042
+
1043
+
1044
+def test_a_two_token_bf16_block_is_allowed(monkeypatch):
1045
+ """The bf16 fold has no such hole, so the guard must not fire there."""
1046
+ metadata, _ = _build_non_causal(
1047
+ monkeypatch, num_heads=12, kv_cache_dtype="auto", qlen=2, mtp_qlen=8
1048
+ )
1049
+ assert metadata.has_persistent_metadata
vllm/_aiter_ops.py
+26
-1
@@ -583,10 +583,11 @@ def _rocm_aiter_mla_decode_fwd_impl(
583
reduce_indptr: torch.Tensor | None = None,
584
reduce_final_map: torch.Tensor | None = None,
585
reduce_partial_map: torch.Tensor | None = None,
586
+ causal: bool = True,
587
) -> None:
588
from aiter.mla import mla_decode_fwd
589
589
- kwargs: dict[str, float | torch.Tensor | None] = {
590
+ kwargs: dict[str, float | torch.Tensor | None | bool] = {
591
"sm_scale": sm_scale,
592
"logit_cap": logit_cap,
593
}
@@ -596,6 +597,8 @@ def _rocm_aiter_mla_decode_fwd_impl(
597
kwargs["q_scale"] = q_scale
598
kwargs["kv_scale"] = kv_scale
599
600
+ kwargs["causal"] = causal
601
+
602
if work_meta_data is not None:
603
assert work_indptr is not None, (
604
"work_indptr must be provided with work_meta_data"
@@ -2036,6 +2039,26 @@ class rocm_aiter_ops:
2039
def is_mla_enabled(cls) -> bool:
2040
return cls._AITER_ENABLED and cls._MLA_ENABLED
2041
2042
+ @classmethod
2043
+ @if_aiter_supported
2044
+ def mla_decode_supports_non_causal(cls) -> bool:
2045
+ """Whether installed aiter.mla.mla_decode_fwd accepts `causal`.
2046
+
2047
+ Added in aiter v0.1.20. A missing argument is a hard error rather
2048
+ than a boolean the caller can use to pick another MLA backend.
2049
+ """
2050
+ import inspect
2051
+
2052
+ from aiter.mla import mla_decode_fwd
2053
+
2054
+ if "causal" not in inspect.signature(mla_decode_fwd).parameters:
2055
+ raise RuntimeError(
2056
+ "ROCM_AITER_MLA requires aiter.mla.mla_decode_fwd(..., causal=). "
2057
+ "The installed aiter is causal-only; upgrade aiter rather than "
2058
+ "falling back to another MLA backend."
2059
+ )
2060
+ return True
2061
+
2062
@classmethod
2063
@if_aiter_supported
2064
def is_mha_enabled(cls) -> bool:
@@ -2858,6 +2881,7 @@ class rocm_aiter_ops:
2881
reduce_indptr: torch.Tensor | None = None,
2882
reduce_final_map: torch.Tensor | None = None,
2883
reduce_partial_map: torch.Tensor | None = None,
2884
+ causal: bool = True,
2885
):
2886
torch.ops.vllm.rocm_aiter_mla_decode_fwd(
2887
q,
@@ -2878,6 +2902,7 @@ class rocm_aiter_ops:
2902
reduce_indptr=reduce_indptr,
2903
reduce_final_map=reduce_final_map,
2904
reduce_partial_map=reduce_partial_map,
2905
+ causal=causal,
2906
)
2907
2908
@staticmethod
vllm/v1/attention/backends/mla/rocm_aiter_mla.py
+145
-3
@@ -4,7 +4,7 @@
4
import functools
5
from dataclasses import dataclass
6
from pathlib import Path
7
-from typing import ClassVar, Final
7
+from typing import TYPE_CHECKING, ClassVar, Final
8
9
import torch
10
@@ -38,6 +38,9 @@ from vllm.v1.attention.ops.rocm_aiter_mla_merge import (
38
)
39
from vllm.v1.kv_cache_interface import AttentionSpec, is_quantized_kv_cache
40
41
+if TYPE_CHECKING:
42
+ from vllm.platforms.interface import DeviceCapability
43
+
44
logger = init_logger(__name__)
45
46
@@ -144,6 +147,20 @@ def _aiter_mla_native_h24_supported() -> bool:
147
)
148
149
150
+def _aiter_mla_non_causal_asm_kernels() -> bool:
151
+ """Whether this arch ships non-causal MLA decode ASM kernels.
152
+
153
+ The Python `causal=` probe cannot see the per-arch manifest. gfx950 has
154
+ the kernels; gfx942 and everything else must fall through to another
155
+ backend.
156
+ """
157
+ try:
158
+ from vllm.platforms.rocm import on_gfx950
159
+ except Exception: # noqa: BLE001
160
+ return False
161
+ return bool(on_gfx950())
162
+
163
+
164
@functools.lru_cache(maxsize=1)
165
def _gluon_mla_decode_supported() -> bool:
166
"""The small-head Gluon MLA decode kernel only has a gfx950 (CDNA4) build.
@@ -252,6 +269,72 @@ class AiterMLABackend(MLACommonBackend):
269
def get_name() -> str:
270
return "ROCM_AITER_MLA"
271
272
+ @classmethod
273
+ def supports_non_causal(cls) -> bool:
274
+ # causal= on mla_decode_fwd is necessary but not sufficient: pinned
275
+ # AITER v0.1.21.post2 still exports that argument on gfx942, whose ASM
276
+ # manifest has no non-causal decode kernels. Selecting this backend
277
+ # there reaches `cannot get heuristic kernel!` instead of TRITON_MLA.
278
+ return bool(
279
+ rocm_aiter_ops.mla_decode_supports_non_causal()
280
+ and _aiter_mla_non_causal_asm_kernels()
281
+ )
282
+
283
+ @classmethod
284
+ def validate_configuration(
285
+ cls,
286
+ head_size: int,
287
+ dtype: torch.dtype,
288
+ kv_cache_dtype: CacheDType | None,
289
+ block_size: int | None,
290
+ use_mla: bool,
291
+ has_sink: bool,
292
+ use_sparse: bool,
293
+ use_mm_prefix: bool,
294
+ use_per_head_quant_scales: bool,
295
+ device_capability: "DeviceCapability",
296
+ attn_type: str,
297
+ has_sliding_window: bool = False,
298
+ use_non_causal: bool = False,
299
+ use_batch_invariant: bool = False,
300
+ use_kv_connector: bool = False,
301
+ use_pcp: bool = False,
302
+ use_adaptive_verification: bool = False,
303
+ use_dcp: bool = False,
304
+ use_rswa: bool = False,
305
+ ) -> list[str]:
306
+ invalid_reasons = super().validate_configuration(
307
+ head_size=head_size,
308
+ dtype=dtype,
309
+ kv_cache_dtype=kv_cache_dtype,
310
+ block_size=block_size,
311
+ use_mla=use_mla,
312
+ has_sink=has_sink,
313
+ use_sparse=use_sparse,
314
+ use_mm_prefix=use_mm_prefix,
315
+ use_per_head_quant_scales=use_per_head_quant_scales,
316
+ device_capability=device_capability,
317
+ attn_type=attn_type,
318
+ has_sliding_window=has_sliding_window,
319
+ use_non_causal=use_non_causal,
320
+ use_batch_invariant=use_batch_invariant,
321
+ use_kv_connector=use_kv_connector,
322
+ use_pcp=use_pcp,
323
+ use_adaptive_verification=use_adaptive_verification,
324
+ use_dcp=use_dcp,
325
+ use_rswa=use_rswa,
326
+ )
327
+ # Arch/signature cannot express this: the backend still advertises
328
+ # fp16, so a DSpark draft with kv_cache_dtype="auto" would select
329
+ # AITER and abort on fp16 query. Causal fp16 is fine.
330
+ if (
331
+ use_non_causal
332
+ and dtype == torch.float16
333
+ and kv_cache_dtype in (None, "auto", "float16")
334
+ ):
335
+ invalid_reasons.append("non-causal fp16 MLA decode not supported")
336
+ return invalid_reasons
337
+
338
@staticmethod
339
def get_impl_cls() -> type["AiterMLAImpl"]:
340
return AiterMLAImpl
@@ -347,6 +430,12 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
430
# https://github.com/vllm-project/vllm/issues/22945
431
_cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
432
query_len_support: ClassVar[QueryLenSupport] = QueryLenSupport.UNIFORM
433
+ # Served by passing the mask to the kernel; _build_decode turns away the
434
+ # shapes AITER has no non-causal kernel for.
435
+ supports_non_causal_multi_token_decode: ClassVar[bool] = True
436
+ # Set from the common metadata every build; a batch is causal unless the
437
+ # drafter says otherwise.
438
+ _decode_causal: bool = True
439
440
@staticmethod
441
def _uniform_padded_mtp_qo_len(
@@ -919,6 +1008,7 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
1008
num_decode_tokens: int,
1009
dcp_tot_seq_lens_device: torch.Tensor | None,
1010
) -> AiterMLADecodeMetadata:
1011
+ causal = self._decode_causal
1012
device = self.device
1013
num_reqs = seq_lens_device.size(0)
1014
qo_len = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
@@ -970,6 +1060,7 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
1060
int(max_qo_len),
1061
self._kv_cache_dtype_str,
1062
self.dcp_world_size,
1063
+ causal,
1064
)
1065
use_segmented_dcp_verify = (
1066
self._supports_segmented_dcp_verify and max_qo_len > 1
@@ -1047,14 +1138,36 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
1138
# A padded rank has no bf16 persistent kernel past qlen 4 where the
1139
# gfx950 fold is absent; the non-persistent entry covers it. fp8
1140
# keeps the schedule -- its fold rejects non-persistent outright.
1141
+ # A non-causal block keeps it too: what the fold drops past qlen 4
1142
+ # is the block's causal staircase, which a non-causal block does
1143
+ # not have, and the schedule is the only thing carrying its mask.
1144
and (
1051
- self._decode_num_heads >= AiterMLAHelper._AITER_MIN_MLA_HEADS
1145
+ not causal
1146
+ or self._decode_num_heads >= AiterMLAHelper._AITER_MIN_MLA_HEADS
1147
or max_qo_len <= AiterMLAHelper._ASM_PADDED_MAX_PS_QLEN
1148
or is_quantized_kv_cache(self._kv_cache_dtype_str)
1149
)
1150
and max_qo_len >= 1
1151
and max_qo_len <= self._mtp_decode_qlen
1152
)
1153
+ if (
1154
+ not causal
1155
+ and max_qo_len == 2
1156
+ and is_quantized_kv_cache(self._kv_cache_dtype_str)
1157
+ and not AiterMLAHelper.has_fp8_non_causal_qlen2_kernel(
1158
+ self._decode_num_heads
1159
+ )
1160
+ ):
1161
+ # AITER's fp8 dispatch folds (gqa 16, qlen 3 or 4) onto the
1162
+ # qseqlen-4 kernel but never lists 2. Only 32/64/96/128 heads
1163
+ # at qlen 2 fold onto that 16-head / 4-token non-causal kernel;
1164
+ # 16-head qlen 2, and padded 48/80/112, keep Q2 and abort the
1165
+ # process rather than raising. The bf16 fold has no such hole.
1166
+ raise ValueError(
1167
+ "AITER has no non-causal fp8 MLA kernel for this 2-token "
1168
+ "query block. Pin the draft to TRITON_MLA for this "
1169
+ "speculative config."
1170
+ )
1171
if use_persistent_metadata:
1172
from aiter import get_mla_metadata_v1
1173
@@ -1067,7 +1180,7 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
1180
paged_kv_last_page_len,
1181
self._num_attention_heads,
1182
1,
1070
- True,
1183
+ causal,
1184
self._mla_work_meta_data,
1185
self._mla_work_info_set,
1186
self._mla_work_indptr,
@@ -1137,6 +1250,11 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
1250
common_attn_metadata: CommonAttentionMetadata,
1251
fast_build: bool = False,
1252
) -> AiterMLAMetadata:
1253
+ # The common MLA builder owns the causal flag but its _build_decode
1254
+ # contract is shared with CUDA backends. Keep this ROCm-specific state
1255
+ # on the per-ubatch Aiter builder instead of changing that shared API;
1256
+ # super().build invokes _build_decode synchronously.
1257
+ self._decode_causal = common_attn_metadata.causal
1258
attn_metadata = super().build(
1259
common_prefix_len, common_attn_metadata, fast_build
1260
)
@@ -1226,6 +1344,10 @@ class AiterMLAHelper:
1344
# fold that reaches a persistent one is gfx950-only.
1345
_ASM_PADDED_MAX_PS_QLEN: Final = 4
1346
_AITER_UNSUPPORTED_HEADS: ClassVar[tuple[int, ...]] = ()
1347
+ # Pinned AITER v0.1.21.post2 folds these fp8 qlen-2 counts onto the
1348
+ # non-causal 16-head / 4-token kernel. Other multiples of 16 (48/80/112)
1349
+ # fold to H16 while keeping Q2, which has no non-causal fp8 entry.
1350
+ _AITER_FP8_NON_CAUSAL_QLEN2_HEADS: ClassVar[tuple[int, ...]] = (32, 64, 96, 128)
1351
1352
@staticmethod
1353
def qo_indptr_for_uniform_qlen(
@@ -1263,6 +1385,18 @@ class AiterMLAHelper:
1385
)
1386
)
1387
1388
+ @staticmethod
1389
+ def has_fp8_non_causal_qlen2_kernel(num_heads: int) -> bool:
1390
+ """Whether fp8 (num_heads, qlen=2) folds onto a non-causal ASM kernel.
1391
+
1392
+ Pinned AITER v0.1.21.post2 folds 32/64/96/128 heads at qlen 2 onto the
1393
+ non-causal 16-head / 4-token kernel. A 16-head (or padded-to-16) qlen-2
1394
+ block has no matching entry, and padded 48/80/112 keep Q2 after the
1395
+ H16 fold.
1396
+ """
1397
+ kernel_heads = AiterMLAHelper.get_actual_mla_num_heads(num_heads)
1398
+ return kernel_heads in AiterMLAHelper._AITER_FP8_NON_CAUSAL_QLEN2_HEADS
1399
+
1400
@staticmethod
1401
def get_actual_mla_num_heads(num_heads: int) -> int:
1402
if num_heads == 24 and _aiter_mla_native_h24_supported():
@@ -1364,6 +1498,7 @@ class AiterMLAHelper:
1498
max_qo_len: int,
1499
kv_cache_dtype: str,
1500
dcp_world_size: int = 1,
1501
+ causal: bool = True,
1502
) -> bool:
1503
"""Whether a small-head multi-token verify uses native Gluon MTP.
1504
@@ -1376,7 +1511,13 @@ class AiterMLAHelper:
1511
1512
DCP verify is excluded: its per-row causal windows are served by the
1513
segmented path, which Gluon's MTP entry cannot express.
1514
+
1515
+ Gluon masks the block causally with no way to turn it off, so a
1516
+ non-causal block takes the padded asm decode whatever the head count
1517
+ -- padding is what gives it a kernel there.
1518
"""
1519
+ if not causal:
1520
+ return False
1521
if max_qo_len <= 1 or dcp_world_size > 1:
1522
return False
1523
if is_quantized_kv_cache(kv_cache_dtype):
@@ -1974,6 +2115,7 @@ class AiterMLAImpl(MLACommonImpl[AiterMLAMetadata]):
2115
decode.paged_kv_indptr,
2116
decode.paged_kv_indices,
2117
decode.paged_kv_last_page_len,
2118
+ causal=attn_metadata.causal,
2119
**mla_kwargs,
2120
)
2121