main
py 225 lines 7.79 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import sys
5 from types import ModuleType, SimpleNamespace
6 from unittest.mock import Mock
7
8 import pytest
9 import torch
10
11 from vllm.platforms import current_platform
12
13 if not current_platform.is_rocm():
14 pytest.skip(
15 "ROCm AITER sparse MLA metadata sync test requires ROCm.",
16 allow_module_level=True,
17 )
18
19 from vllm._aiter_ops import is_aiter_found_and_supported
20
21 if not is_aiter_found_and_supported():
22 pytest.skip(
23 "ROCm AITER sparse MLA metadata sync test requires a supported AITER "
24 "installation.",
25 allow_module_level=True,
26 )
27
28 from vllm.v1.attention.backend import CommonAttentionMetadata
29 from vllm.v1.attention.backends.mla import rocm_aiter_mla_sparse as sparse_mod
30
31
32 class _FakeAiter(ModuleType):
33 get_mla_metadata_v1: Mock
34
35
36 def _make_builder():
37 builder = object.__new__(sparse_mod.ROCMAiterMLASparseMetadataBuilder)
38 max_num_batched_tokens = 8
39 topk_tokens = 4
40
41 builder.device = torch.device("cpu")
42 builder.kv_cache_spec = SimpleNamespace(block_size=1)
43 builder.model_dtype = torch.bfloat16
44 builder.kv_cache_dtype = "fp8"
45 builder.mla_dims = SimpleNamespace(kv_lora_rank=512, qk_rope_head_dim=64)
46 builder.topk_tokens = topk_tokens
47 builder.req_id_per_token_buffer = torch.zeros(
48 max_num_batched_tokens, dtype=torch.int32, device="cpu"
49 )
50 builder.qo_indptr = torch.arange(
51 max_num_batched_tokens + 1, dtype=torch.int32, device="cpu"
52 )
53 builder.paged_kv_last_page_len = torch.ones(
54 max_num_batched_tokens, dtype=torch.int32, device="cpu"
55 )
56 builder.paged_kv_indices = torch.zeros(
57 max_num_batched_tokens * topk_tokens, dtype=torch.int32, device="cpu"
58 )
59 builder.paged_kv_indptr = torch.zeros(
60 max_num_batched_tokens + 1, dtype=torch.int32, device="cpu"
61 )
62 builder._use_persistent_metadata = True
63 builder._num_attention_heads = 16
64 builder._num_compute_units = current_platform.num_compute_units()
65 builder._mla_work_meta_data = torch.empty(1, dtype=torch.int32, device="cpu")
66 builder._mla_work_indptr = torch.empty(1, dtype=torch.int32, device="cpu")
67 builder._mla_work_info_set = torch.empty(1, dtype=torch.int32, device="cpu")
68 builder._mla_reduce_indptr = torch.empty(1, dtype=torch.int32, device="cpu")
69 builder._mla_reduce_final_map = torch.empty(1, dtype=torch.int32, device="cpu")
70 builder._mla_reduce_partial_map = torch.empty(1, dtype=torch.int32, device="cpu")
71 builder._prev_req_extent = 0
72 builder._prev_indices_extent = 0
73 builder._prev_metadata_key = None
74 return builder
75
76
77 def _make_common_metadata():
78 query_start_loc = torch.tensor([0, 1, 2], dtype=torch.int32, device="cpu")
79 seq_lens = torch.tensor([16, 8], dtype=torch.int32, device="cpu")
80 return CommonAttentionMetadata(
81 query_start_loc=query_start_loc,
82 query_start_loc_cpu=query_start_loc,
83 seq_lens=seq_lens,
84 seq_lens_cpu_upper_bound=seq_lens,
85 num_reqs=2,
86 num_actual_tokens=2,
87 max_query_len=1,
88 max_seq_len=16,
89 block_table_tensor=torch.arange(16, dtype=torch.int32, device="cpu").view(2, 8),
90 slot_mapping=torch.arange(2, dtype=torch.int64, device="cpu"),
91 )
92
93
94 def _make_mixed_common_metadata():
95 # req0: query_len 1 (decode), req1: query_len 4 (prefill) -> decode-first
96 query_start_loc = torch.tensor([0, 1, 5], dtype=torch.int32, device="cpu")
97 seq_lens = torch.tensor([16, 10], dtype=torch.int32, device="cpu")
98 return CommonAttentionMetadata(
99 query_start_loc=query_start_loc,
100 query_start_loc_cpu=query_start_loc,
101 seq_lens=seq_lens,
102 seq_lens_cpu_upper_bound=seq_lens,
103 num_reqs=2,
104 num_actual_tokens=5,
105 max_query_len=4,
106 max_seq_len=16,
107 block_table_tensor=torch.arange(16, dtype=torch.int32, device="cpu").view(2, 8),
108 slot_mapping=torch.arange(5, dtype=torch.int64, device="cpu"),
109 )
110
111
112 def _patch_build_deps(monkeypatch, events=None):
113 """Stub the aiter kernel, triton helper and CUDA sync so ``build()`` runs
114 on CPU."""
115
116 def fake_generate_sparse_seqlen_triton(
117 query_lens, seq_lens, cu_query_lens, topk_token, num_tokens, max_query_len
118 ):
119 return torch.zeros(num_tokens, dtype=torch.int32, device="cpu")
120
121 fake_aiter = _FakeAiter("aiter")
122 fake_aiter.get_mla_metadata_v1 = Mock(side_effect=lambda *a, **k: None)
123 monkeypatch.setitem(sys.modules, "aiter", fake_aiter)
124 monkeypatch.setattr(
125 sparse_mod, "generate_sparse_seqlen_triton", fake_generate_sparse_seqlen_triton
126 )
127 monkeypatch.setattr(
128 sparse_mod.torch.cuda,
129 "current_stream",
130 lambda device=None: SimpleNamespace(
131 synchronize=lambda: events.append("sync") if events is not None else None
132 ),
133 )
134 return fake_aiter
135
136
137 def test_build_populates_decode_only_split_fields(monkeypatch):
138 """Decode-only batch: all reqs count as decodes, prefill fields default."""
139 builder = _make_builder()
140 _patch_build_deps(monkeypatch)
141
142 md = builder.build(
143 common_prefix_len=0, common_attn_metadata=_make_common_metadata()
144 )
145
146 assert md.num_decodes == 2
147 assert md.num_prefills == 0
148 assert md.num_decode_tokens == 2
149 assert md.prefill_max_seq_len == 0
150 assert md.prefill is None
151
152
153 def test_build_populates_mixed_split_fields(monkeypatch):
154 """Mixed decode+prefill batch: split is reported, prefill fields stay
155 default because this impl always runs the MQA path."""
156 builder = _make_builder()
157 _patch_build_deps(monkeypatch)
158
159 md = builder.build(
160 common_prefix_len=0, common_attn_metadata=_make_mixed_common_metadata()
161 )
162
163 assert md.num_decodes == 1
164 assert md.num_prefills == 1
165 assert md.num_decode_tokens == 1
166 assert md.prefill_max_seq_len == 0
167 assert md.prefill is None
168
169
170 def test_sink_build_skips_persistent_metadata(monkeypatch):
171 builder = _make_builder()
172 builder._use_persistent_metadata = False
173 fake_aiter = _patch_build_deps(monkeypatch)
174
175 md = builder.build(
176 common_prefix_len=0, common_attn_metadata=_make_common_metadata()
177 )
178
179 fake_aiter.get_mla_metadata_v1.assert_not_called()
180 assert md.work_meta_data is None
181 assert md.work_indptr is None
182 assert md.work_info_set is None
183 assert md.reduce_indptr is None
184 assert md.reduce_final_map is None
185 assert md.reduce_partial_map is None
186 assert builder._prev_metadata_key is None
187
188
189 def test_sparse_persistent_metadata_syncs_only_after_recompute(monkeypatch):
190 builder = _make_builder()
191 common_metadata = _make_common_metadata()
192 events: list[str] = []
193
194 def fake_generate_sparse_seqlen_triton(*args, **kwargs):
195 return torch.tensor([1, 2], dtype=torch.int32, device="cpu")
196
197 fake_aiter = _FakeAiter("aiter")
198
199 def fake_get_mla_metadata_v1(*args, **kwargs):
200 events.append("metadata")
201
202 fake_get_mla_metadata_v1_mock = Mock(side_effect=fake_get_mla_metadata_v1)
203 fake_aiter.get_mla_metadata_v1 = fake_get_mla_metadata_v1_mock
204 monkeypatch.setitem(sys.modules, "aiter", fake_aiter)
205 monkeypatch.setattr(
206 sparse_mod, "generate_sparse_seqlen_triton", fake_generate_sparse_seqlen_triton
207 )
208 monkeypatch.setattr(
209 sparse_mod.torch.cuda,
210 "current_stream",
211 lambda device=None: SimpleNamespace(synchronize=lambda: events.append("sync")),
212 )
213
214 builder.build(common_prefix_len=0, common_attn_metadata=common_metadata)
215
216 assert events == ["metadata", "sync"]
217 assert fake_get_mla_metadata_v1_mock.call_count == 1
218 assert fake_get_mla_metadata_v1_mock.call_args.kwargs["max_split_per_batch"] == 1
219
220 events.clear()
221
222 builder.build(common_prefix_len=0, common_attn_metadata=common_metadata)
223
224 assert events == []
225 assert fake_get_mla_metadata_v1_mock.call_count == 1