main
py 159 lines 6.27 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Equivalence tests for the AITER MLA page-index expansion kernel.
4
5 `_expand_page_indices_kernel` turns a block table into the per-token flat page
6 indices the AITER MLA decode kernel consumes (it always runs with page_size=1
7 internally, so `kv_buffer` is flattened and each token needs its own index).
8
9 The kernel was parallelised over token chunks as well as requests; at
10 concurrency 1 the previous `(num_reqs,)` grid put a single workgroup in a serial
11 loop over the whole sequence. These tests pin the result to a straightforward
12 PyTorch expansion so the parallelisation cannot change what is produced.
13
14 Both block-table strides are passed by the caller, so a non-contiguous view is
15 covered explicitly rather than assumed away.
16 """
17
18 import pytest
19 import torch
20
21 from vllm.v1.attention.backends.mla.rocm_aiter_mla import (
22 _expand_page_indices_kernel,
23 )
24
25
26 def _reference(block_table, cu_num_tokens, kernel_block_size, out_len, device):
27 """Per-token flat index, written the obvious way."""
28 out = torch.full((out_len,), -1, dtype=torch.int64, device=device)
29 num_reqs = cu_num_tokens.numel() - 1
30 for r in range(num_reqs):
31 start = int(cu_num_tokens[r])
32 end = int(cu_num_tokens[r + 1])
33 n = end - start
34 if n <= 0:
35 continue
36 tok = torch.arange(n, device=device)
37 blk = tok // kernel_block_size
38 off = tok % kernel_block_size
39 out[start:end] = block_table[r, blk].to(torch.int64) * kernel_block_size + off
40 return out
41
42
43 def _run(block_table, cu_num_tokens, kernel_block_size, out_len, device):
44 out = torch.full((out_len,), -1, dtype=torch.int64, device=device)
45 num_reqs = cu_num_tokens.numel() - 1
46 max_tokens_per_req = block_table.shape[1] * kernel_block_size
47 num_chunks = max(1, -(-max_tokens_per_req // 1024))
48 _expand_page_indices_kernel[(num_reqs, num_chunks)](
49 out,
50 block_table,
51 block_table.stride(0),
52 block_table.stride(1),
53 cu_num_tokens,
54 KERNEL_BLOCK_SIZE=kernel_block_size,
55 BLOCK_SIZE=1024,
56 )
57 return out
58
59
60 def _make(seq_lens, kernel_block_size, device, noncontig=False):
61 num_reqs = len(seq_lens)
62 max_blocks = (
63 max(1, max((s + kernel_block_size - 1) // kernel_block_size for s in seq_lens))
64 + 2
65 )
66 width = max_blocks * 2 if noncontig else max_blocks
67 torch.manual_seed(0)
68 bt = torch.randint(0, 10_000, (num_reqs, width), dtype=torch.int64, device=device)
69 if noncontig:
70 bt = bt[:, ::2]
71 assert not bt.is_contiguous()
72 cu = torch.zeros(num_reqs + 1, dtype=torch.int64, device=device)
73 cu[1:] = torch.tensor(seq_lens, dtype=torch.int64, device=device).cumsum(0)
74 return bt, cu, int(cu[-1])
75
76
77 @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a GPU")
78 @pytest.mark.parametrize(
79 "seq_lens", [[1], [128], [4096], [1, 1], [4096, 7], [37, 4096, 1], [1024] * 8]
80 )
81 @pytest.mark.parametrize("kernel_block_size", [1, 16, 64])
82 def test_matches_reference(seq_lens, kernel_block_size):
83 bt, cu, total = _make(seq_lens, kernel_block_size, "cuda")
84 got = _run(bt, cu, kernel_block_size, total, "cuda")
85 want = _reference(bt, cu, kernel_block_size, total, "cuda")
86 torch.testing.assert_close(got, want, atol=0, rtol=0)
87
88
89 @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a GPU")
90 @pytest.mark.parametrize("kernel_block_size", [1, 16])
91 def test_non_contiguous_block_table(kernel_block_size):
92 """Column stride != 1 must be honoured, not assumed away."""
93 bt, cu, total = _make([4096, 37], kernel_block_size, "cuda", noncontig=True)
94 got = _run(bt, cu, kernel_block_size, total, "cuda")
95 want = _reference(bt, cu, kernel_block_size, total, "cuda")
96 torch.testing.assert_close(got, want, atol=0, rtol=0)
97
98
99 @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a GPU")
100 def test_ragged_batch_leaves_other_rows_untouched():
101 """A short request must not have its tail written by a longer one's chunks."""
102 kbs = 16
103 bt, cu, total = _make([4096, 1, 512], kbs, "cuda")
104 out = torch.full((total + 8,), -999, dtype=torch.int64, device="cuda")
105 num_chunks = max(1, -(-(bt.shape[1] * kbs) // 1024))
106 _expand_page_indices_kernel[(3, num_chunks)](
107 out,
108 bt,
109 bt.stride(0),
110 bt.stride(1),
111 cu,
112 KERNEL_BLOCK_SIZE=kbs,
113 BLOCK_SIZE=1024,
114 )
115 want = _reference(bt, cu, kbs, total, "cuda")
116 torch.testing.assert_close(out[:total], want, atol=0, rtol=0)
117 # The slack past the last request must be untouched.
118 assert torch.all(out[total:] == -999)
119
120
121 @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a GPU")
122 def test_zero_length_request_writes_nothing():
123 """A zero-token request in a padded batch must produce no stores."""
124 kbs = 16
125 bt, cu, total = _make([256, 0, 128], kbs, "cuda")
126 got = _run(bt, cu, kbs, total, "cuda")
127 want = _reference(bt, cu, kbs, total, "cuda")
128 torch.testing.assert_close(got, want, atol=0, rtol=0)
129 assert int(cu[2] - cu[1]) == 0
130
131
132 @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a GPU")
133 def test_over_provisioned_chunks_do_not_change_result():
134 """Extra chunks must be inert.
135
136 Each program covers `chunk_idx * BLOCK_SIZE + [0, BLOCK_SIZE)`, so the grid
137 has to span the request: 8192 tokens need at least ceil(8192/1024) == 8
138 chunks, and fewer would simply leave the tail unwritten. The caller sizes
139 the grid from `block_table.shape[1] * kernel_block_size`, an upper bound
140 over the batch, so in a ragged batch most requests get more chunks than
141 they need. That over-provisioning is the case worth pinning: the surplus
142 programs must return early and change nothing.
143 """
144 kbs = 1
145 bt, cu, total = _make([8192], kbs, "cuda")
146 ref = _reference(bt, cu, kbs, total, "cuda")
147 required = -(-total // 1024)
148 for num_chunks in (required, required * 2, required * 8):
149 out = torch.full((total,), -1, dtype=torch.int64, device="cuda")
150 _expand_page_indices_kernel[(1, num_chunks)](
151 out,
152 bt,
153 bt.stride(0),
154 bt.stride(1),
155 cu,
156 KERNEL_BLOCK_SIZE=kbs,
157 BLOCK_SIZE=1024,
158 )
159 torch.testing.assert_close(out, ref, atol=0, rtol=0)