main
py 420 lines 11.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Standalone unit tests for trtllm_prefill_attn_kvfp8_dequant.
4
5 Tests both contiguous and non-contiguous (cross-layer unified) KV cache
6 layouts against a pure-PyTorch reference implementation.
7 """
8
9 import pytest
10 import torch
11
12 from vllm.platforms import current_platform
13 from vllm.v1.kv_cache_interface import KVCacheLayout
14
15 if current_platform.is_rocm():
16 pytest.skip(
17 "trtllm kvfp8 dequant is not supported on ROCm.",
18 allow_module_level=True,
19 )
20
21 FP8_DTYPE = current_platform.fp8_dtype()
22
23 NUM_BLOCKS = 128
24
25
26 def to_float8(x, dtype=None):
27 if dtype is None:
28 dtype = FP8_DTYPE
29 finfo = torch.finfo(dtype)
30 min_val, max_val = x.aminmax()
31 amax = torch.maximum(min_val.abs(), max_val.abs()).clamp(min=1e-12)
32 scale = finfo.max / amax * 0.1
33 x_scl_sat = (x * scale).clamp(min=finfo.min, max=finfo.max)
34 return x_scl_sat.to(dtype), scale.float().reciprocal()
35
36
37 def make_random_kv_cache(
38 num_blocks,
39 num_kv_heads,
40 block_size,
41 head_size,
42 layout=KVCacheLayout.LBHNC,
43 num_layers=None,
44 ):
45 """Create a random fp8 KV cache mimicking the production allocation.
46
47 Physical layout: [L, B, H, N, 2 * hs] permuted by ``layout.stride_order``
48 Returned view: (num_blocks, 2, num_kv_heads, block_size, head_size)
49 with non-contiguous strides under cross-layer layouts (they skip over
50 num_layers), matching the actual forward path.
51 """
52 logical_4d = (num_blocks, num_kv_heads, block_size, 2 * head_size)
53 if num_layers is None:
54 num_layers = 1 if layout.is_layer_compact else 2
55 logical_5d = (num_layers, *logical_4d)
56 physical_5d = tuple(logical_5d[i] for i in layout.stride_order)
57 inv_order = [layout.stride_order.index(i) for i in range(5)]
58
59 raw_phys = torch.randn(*physical_5d, dtype=torch.bfloat16, device="cuda")
60 fp8_phys, scale = to_float8(raw_phys)
61 fp8_4d = fp8_phys.permute(*inv_order)[0]
62 kv_5d = fp8_4d.view(
63 num_blocks,
64 num_kv_heads,
65 block_size,
66 2,
67 head_size,
68 ).permute(0, 3, 1, 2, 4)
69 return kv_5d, scale
70
71
72 def ref_dequant(kv_cache, block_tables, k_scale, v_scale, dequant_dtype):
73 """Pure PyTorch reference: gather pages and dequantize fp8 -> dequant_dtype."""
74 batch_size, num_pages_per_seq = block_tables.shape
75 s = kv_cache.shape
76 out = torch.zeros(
77 batch_size * num_pages_per_seq + 1,
78 s[1],
79 s[2],
80 s[3],
81 s[4],
82 dtype=dequant_dtype,
83 device=kv_cache.device,
84 )
85 for b in range(batch_size):
86 for p in range(num_pages_per_seq):
87 page_idx = block_tables[b, p].item()
88 if page_idx <= 0:
89 continue
90 mock_idx = b * num_pages_per_seq + p + 1
91 out[mock_idx, 0] = (kv_cache[page_idx, 0].float() * k_scale.item()).to(
92 dequant_dtype
93 )
94 out[mock_idx, 1] = (kv_cache[page_idx, 1].float() * v_scale.item()).to(
95 dequant_dtype
96 )
97 return out
98
99
100 @pytest.mark.parametrize("num_kv_heads", [1, 8])
101 @pytest.mark.parametrize("head_size", [64, 128])
102 @pytest.mark.parametrize("block_size", [16, 32])
103 @pytest.mark.parametrize("batch_size", [1, 4])
104 @pytest.mark.parametrize("num_pages_per_seq", [3, 8])
105 @pytest.mark.parametrize("layout", list(KVCacheLayout))
106 @torch.inference_mode()
107 def test_trtllm_kvfp8_dequant(
108 num_kv_heads: int,
109 head_size: int,
110 block_size: int,
111 batch_size: int,
112 num_pages_per_seq: int,
113 layout: KVCacheLayout,
114 ):
115 from vllm.v1.attention.backends.flashinfer import (
116 trtllm_prefill_attn_kvfp8_dequant,
117 )
118
119 torch.set_default_device("cuda")
120
121 kv_cache, scale = make_random_kv_cache(
122 NUM_BLOCKS,
123 num_kv_heads,
124 block_size,
125 head_size,
126 layout=layout,
127 )
128
129 k_scale = scale.clone()
130 v_scale = scale.clone()
131
132 block_tables = torch.randint(
133 1,
134 NUM_BLOCKS,
135 (batch_size, num_pages_per_seq),
136 dtype=torch.int32,
137 )
138
139 mock_kv_cache, mock_block_table = trtllm_prefill_attn_kvfp8_dequant(
140 kv_cache,
141 block_tables,
142 k_scale,
143 v_scale,
144 torch.bfloat16,
145 )
146
147 ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
148
149 expected_bt = torch.arange(
150 1,
151 batch_size * num_pages_per_seq + 1,
152 dtype=torch.int32,
153 device="cuda",
154 ).reshape(batch_size, num_pages_per_seq)
155 torch.testing.assert_close(mock_block_table, expected_bt)
156
157 # Page 0 is padding (never written), compare only pages 1+
158 torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)
159
160
161 @torch.inference_mode()
162 def test_block_tables_with_zero_pages():
163 """Pages with index <= 0 must be skipped (early return in kernel)."""
164 from vllm.v1.attention.backends.flashinfer import (
165 trtllm_prefill_attn_kvfp8_dequant,
166 )
167
168 torch.set_default_device("cuda")
169 num_kv_heads, block_size, head_size = 8, 16, 64
170
171 kv_cache, scale = make_random_kv_cache(
172 NUM_BLOCKS,
173 num_kv_heads,
174 block_size,
175 head_size,
176 )
177 k_scale = v_scale = scale.clone()
178
179 # Mix of valid pages and zeros (padding)
180 block_tables = torch.tensor(
181 [[5, 0, 10], [0, 0, 0], [3, 7, 0]],
182 dtype=torch.int32,
183 device="cuda",
184 )
185
186 mock_kv_cache, _ = trtllm_prefill_attn_kvfp8_dequant(
187 kv_cache,
188 block_tables,
189 k_scale,
190 v_scale,
191 torch.bfloat16,
192 )
193 ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
194
195 # Only compare pages that were actually written (non-zero page indices)
196 for b in range(block_tables.shape[0]):
197 for p in range(block_tables.shape[1]):
198 if block_tables[b, p].item() > 0:
199 idx = b * block_tables.shape[1] + p + 1
200 torch.testing.assert_close(
201 mock_kv_cache[idx],
202 ref[idx],
203 atol=1e-3,
204 rtol=1e-3,
205 )
206
207
208 @torch.inference_mode()
209 def test_all_zero_block_tables():
210 """All-zero block_tables: kernel should write nothing."""
211 from vllm.v1.attention.backends.flashinfer import (
212 trtllm_prefill_attn_kvfp8_dequant,
213 )
214
215 torch.set_default_device("cuda")
216 num_kv_heads, block_size, head_size = 4, 16, 64
217
218 kv_cache, scale = make_random_kv_cache(
219 NUM_BLOCKS,
220 num_kv_heads,
221 block_size,
222 head_size,
223 )
224 k_scale = v_scale = scale.clone()
225
226 block_tables = torch.zeros(2, 4, dtype=torch.int32, device="cuda")
227
228 # Should not crash even though no pages are valid
229 mock_kv_cache, mock_block_table = trtllm_prefill_attn_kvfp8_dequant(
230 kv_cache,
231 block_tables,
232 k_scale,
233 v_scale,
234 torch.bfloat16,
235 )
236 assert mock_kv_cache.shape[0] == 2 * 4 + 1
237 assert mock_block_table.shape == (2, 4)
238
239
240 @torch.inference_mode()
241 def test_different_k_v_scales():
242 """Verify K and V are dequantized with independent scales."""
243 from vllm.v1.attention.backends.flashinfer import (
244 trtllm_prefill_attn_kvfp8_dequant,
245 )
246
247 torch.set_default_device("cuda")
248 num_kv_heads, block_size, head_size = 8, 16, 64
249
250 kv_cache, _ = make_random_kv_cache(
251 NUM_BLOCKS,
252 num_kv_heads,
253 block_size,
254 head_size,
255 )
256 k_scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
257 v_scale = torch.tensor([2.0], dtype=torch.float32, device="cuda")
258
259 block_tables = torch.tensor([[1, 2]], dtype=torch.int32, device="cuda")
260
261 mock_kv_cache, _ = trtllm_prefill_attn_kvfp8_dequant(
262 kv_cache,
263 block_tables,
264 k_scale,
265 v_scale,
266 torch.bfloat16,
267 )
268 ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
269
270 torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)
271
272
273 @torch.inference_mode()
274 def test_single_page_per_seq():
275 """Minimum grid dim 1 = 1 page per sequence."""
276 from vllm.v1.attention.backends.flashinfer import (
277 trtllm_prefill_attn_kvfp8_dequant,
278 )
279
280 torch.set_default_device("cuda")
281 num_kv_heads, block_size, head_size = 8, 16, 128
282
283 kv_cache, scale = make_random_kv_cache(
284 NUM_BLOCKS,
285 num_kv_heads,
286 block_size,
287 head_size,
288 )
289 k_scale = v_scale = scale.clone()
290
291 block_tables = torch.tensor([[5], [10], [20]], dtype=torch.int32, device="cuda")
292
293 mock_kv_cache, _ = trtllm_prefill_attn_kvfp8_dequant(
294 kv_cache,
295 block_tables,
296 k_scale,
297 v_scale,
298 torch.bfloat16,
299 )
300 ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
301
302 torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)
303
304
305 @torch.inference_mode()
306 def test_large_page_indices():
307 """Page indices near the top of the buffer stress offset arithmetic."""
308 from vllm.v1.attention.backends.flashinfer import (
309 trtllm_prefill_attn_kvfp8_dequant,
310 )
311
312 torch.set_default_device("cuda")
313 num_kv_heads, block_size, head_size = 8, 16, 128
314 large_num_blocks = 32768
315
316 kv_cache, scale = make_random_kv_cache(
317 large_num_blocks,
318 num_kv_heads,
319 block_size,
320 head_size,
321 )
322 k_scale = v_scale = scale.clone()
323
324 # Use page indices near the top of the buffer
325 block_tables = torch.tensor(
326 [[large_num_blocks - 1, large_num_blocks - 2, 1]],
327 dtype=torch.int32,
328 device="cuda",
329 )
330
331 mock_kv_cache, _ = trtllm_prefill_attn_kvfp8_dequant(
332 kv_cache,
333 block_tables,
334 k_scale,
335 v_scale,
336 torch.bfloat16,
337 )
338 ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
339
340 torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)
341
342
343 @torch.inference_mode()
344 def test_large_block_size():
345 """block_size=64 -> HEAD_STRIDE=8192, large tl.arange per thread block."""
346 from vllm.v1.attention.backends.flashinfer import (
347 trtllm_prefill_attn_kvfp8_dequant,
348 )
349
350 torch.set_default_device("cuda")
351 num_kv_heads, block_size, head_size = 4, 64, 128
352
353 kv_cache, scale = make_random_kv_cache(
354 NUM_BLOCKS,
355 num_kv_heads,
356 block_size,
357 head_size,
358 )
359 k_scale = v_scale = scale.clone()
360
361 block_tables = torch.randint(
362 1,
363 NUM_BLOCKS,
364 (2, 4),
365 dtype=torch.int32,
366 device="cuda",
367 )
368
369 mock_kv_cache, _ = trtllm_prefill_attn_kvfp8_dequant(
370 kv_cache,
371 block_tables,
372 k_scale,
373 v_scale,
374 torch.bfloat16,
375 )
376 ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
377
378 torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)
379
380
381 @torch.inference_mode()
382 def test_cross_layer_many_layers():
383 """Non-contiguous with 36 layers -- matches real gpt-oss-120b.
384 Cross-layer (BLHNC) strides are far from contiguous (factor of 36 in the gaps).
385 """
386 from vllm.v1.attention.backends.flashinfer import (
387 trtllm_prefill_attn_kvfp8_dequant,
388 )
389
390 torch.set_default_device("cuda")
391 num_kv_heads, block_size, head_size = 8, 16, 64
392
393 kv_cache, scale = make_random_kv_cache(
394 NUM_BLOCKS,
395 num_kv_heads,
396 block_size,
397 head_size,
398 layout=KVCacheLayout.BLHNC,
399 num_layers=36,
400 )
401 k_scale = v_scale = scale.clone()
402
403 block_tables = torch.randint(
404 1,
405 NUM_BLOCKS,
406 (4, 6),
407 dtype=torch.int32,
408 device="cuda",
409 )
410
411 mock_kv_cache, _ = trtllm_prefill_attn_kvfp8_dequant(
412 kv_cache,
413 block_tables,
414 k_scale,
415 v_scale,
416 torch.bfloat16,
417 )
418 ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
419
420 torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)