main
py 705 lines 23.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Padded-page handling in create_kv_cache_views.
4
5 Guards that a page_size_padded spec strides the block dimension by the padded page
6 while keeping per-block content compact, so padding bytes at the end of each page are
7 never addressed by the logical view.
8 """
9
10 from types import SimpleNamespace
11 from typing import Any
12
13 import numpy as np
14 import pytest
15 import torch
16
17 import vllm.v1.hisparse.binding as attn_utils_module
18 from tests.v1.attention.utils import dense_kv_cache_views
19 from vllm.config.compilation import CUDAGraphMode
20 from vllm.v1.attention.backend import AttentionBackend, AttentionCGSupport, MultipleOf
21 from vllm.v1.core.kv_cache_utils import KVCacheBlockCopy
22 from vllm.v1.hisparse.binding import allocate_hisparse_kv_caches
23 from vllm.v1.kv_cache_interface import (
24 FullAttentionSpec,
25 HiSparseResidentSpec,
26 KVCacheConfig,
27 KVCacheGroupSpec,
28 KVCacheLayout,
29 KVCacheTensor,
30 MLAAttentionSpec,
31 SparseCacheRole,
32 compute_layout_strides,
33 )
34 from vllm.v1.worker.gpu import attn_utils
35 from vllm.v1.worker.gpu.attn_utils import (
36 FastPrefillHelper,
37 get_attn_cg_support,
38 get_query_lens_mismatch_unsupported_backend,
39 )
40 from vllm.v1.worker.gpu.cudagraph_utils import BatchExecutionDescriptor
41 from vllm.v1.worker.utils import (
42 AttentionGroup,
43 allocate_kv_cache,
44 copy_kv_cache_blocks_inplace,
45 )
46
47
48 @pytest.mark.parametrize(
49 ("enabled", "block_size", "main_sizes", "indexer_sizes", "expected"),
50 [
51 (True, 256, [64], [64], 64),
52 (True, 64, [32, 64], [16, 32], 32),
53 (True, 64, [MultipleOf(16)], [32], 32),
54 (True, 64, [64], [32], None),
55 (False, 256, [64], [64], 256),
56 ],
57 )
58 def test_get_kv_cache_spec_resolves_hisparse_block_size(
59 monkeypatch, enabled, block_size, main_sizes, indexer_sizes, expected
60 ):
61 """Resolve shared MLA geometry before planning; leave other specs alone."""
62 specs = {
63 "main": MLAAttentionSpec(
64 block_size=block_size, num_kv_heads=1, head_size=576, dtype=torch.bfloat16
65 ),
66 "indexer": MLAAttentionSpec(
67 block_size=block_size,
68 num_kv_heads=1,
69 head_size=128,
70 dtype=torch.bfloat16,
71 cache_role=SparseCacheRole.INDEXER,
72 ),
73 "dense": FullAttentionSpec(
74 block_size=block_size, num_kv_heads=1, head_size=128, dtype=torch.bfloat16
75 ),
76 }
77 layers = {}
78 for name, sizes in zip(specs, [main_sizes, indexer_sizes, [block_size]]):
79 backend = SimpleNamespace(
80 get_name=lambda name=name: name,
81 customize_spec=AttentionBackend.customize_spec,
82 get_supported_kernel_block_sizes=lambda sizes=sizes: sizes,
83 )
84 layers[name] = SimpleNamespace(
85 get_kv_cache_spec=lambda _, spec=specs[name]: spec,
86 get_attn_backend=lambda backend=backend: backend,
87 )
88 monkeypatch.setattr(attn_utils, "get_layers_from_vllm_config", lambda *_: layers)
89 monkeypatch.setattr(
90 attn_utils_module, "get_hisparse_kv_cache_groups", lambda *_: []
91 )
92 config = SimpleNamespace(
93 attention_config=SimpleNamespace(hisparse_config=object() if enabled else None)
94 )
95 if expected is None:
96 with pytest.raises(ValueError, match="supported by every sparse"):
97 attn_utils.get_kv_cache_spec(config)
98 return
99
100 resolved = attn_utils.get_kv_cache_spec(config)
101 assert resolved["main"].block_size == resolved["indexer"].block_size == expected
102 assert resolved["dense"] is specs["dense"]
103 assert all(spec.block_size == block_size for spec in specs.values())
104
105
106 class _FakeMetadataBuilder:
107 def __init__(self, support: AttentionCGSupport, varlen_bound: int | None = None):
108 self.support = support
109 self.varlen_bound = varlen_bound
110
111 def get_cudagraph_support(self, *_args):
112 return self.support
113
114 def get_varlen_cudagraph_max_query_len(self, *_args):
115 return self.varlen_bound
116
117
118 class _TargetBackend:
119 @classmethod
120 def supports_device_cpu_query_lens_mismatch(cls) -> bool:
121 return True
122
123
124 class _DraftBackend:
125 @classmethod
126 def supports_device_cpu_query_lens_mismatch(cls) -> bool:
127 return False
128
129
130 def test_attention_checks_preserve_global_and_target_scoped_support():
131 spec = FullAttentionSpec(
132 block_size=16,
133 num_kv_heads=1,
134 head_size=128,
135 dtype=torch.bfloat16,
136 )
137 target_group = AttentionGroup(
138 _TargetBackend,
139 ["target"],
140 spec,
141 0,
142 )
143 target_group.metadata_builders = [
144 _FakeMetadataBuilder(AttentionCGSupport.ALWAYS) # type: ignore[list-item]
145 ]
146 draft_group = AttentionGroup(
147 _DraftBackend,
148 ["draft"],
149 spec,
150 0,
151 )
152 draft_group.metadata_builders = [
153 _FakeMetadataBuilder(AttentionCGSupport.UNIFORM_BATCH) # type: ignore[list-item]
154 ]
155 groups = [[target_group, draft_group]]
156
157 # The runner-wide execution mode must still honor the drafter's limit.
158 unfiltered = get_attn_cg_support(groups, None)
159 assert unfiltered.min_cg_support == AttentionCGSupport.UNIFORM_BATCH
160 assert unfiltered.min_cg_attn_backend == "_DraftBackend"
161
162 # Adaptive verification validates only the target's varlen graphs.
163 target_only = get_attn_cg_support(
164 groups,
165 None,
166 checked_layer_names={"target"},
167 )
168 assert target_only.min_cg_support == AttentionCGSupport.ALWAYS
169 assert target_only.min_cg_attn_backend is None
170 assert (
171 get_query_lens_mismatch_unsupported_backend(
172 groups,
173 checked_layer_names={"target"},
174 )
175 is None
176 )
177
178 # Shared target/draft groups still participate in target-scoped checks.
179 draft_group.layer_names.append("target")
180 target_with_shared_group = get_attn_cg_support(
181 groups,
182 None,
183 checked_layer_names={"target"},
184 )
185 assert target_with_shared_group.min_cg_support == AttentionCGSupport.UNIFORM_BATCH
186 assert (
187 get_query_lens_mismatch_unsupported_backend(
188 groups,
189 checked_layer_names={"target"},
190 )
191 == "_DraftBackend"
192 )
193
194
195 def test_varlen_cudagraph_unsupported_backend_checks_scoped_bounds():
196 """ALWAYS passes without a bound, other builders need one at least as wide as
197 the requested length, and NEVER fails whatever bound a builder reports."""
198 config: Any = SimpleNamespace()
199 spec = FullAttentionSpec(
200 block_size=16,
201 num_kv_heads=1,
202 head_size=128,
203 dtype=torch.bfloat16,
204 )
205
206 def group(
207 backend: Any,
208 layer_name: str,
209 support: AttentionCGSupport,
210 bound: int | None = None,
211 ):
212 builder: Any = _FakeMetadataBuilder(support, bound)
213 attn_group = AttentionGroup(backend, [layer_name], spec, 0)
214 attn_group.metadata_builders = [builder]
215 return attn_group
216
217 target = group(_TargetBackend, "target", AttentionCGSupport.ALWAYS)
218 draft = group(_DraftBackend, "draft", AttentionCGSupport.UNIFORM_BATCH, 8)
219 never = group(_DraftBackend, "never", AttentionCGSupport.NEVER, 8)
220
221 unsupported = attn_utils.get_varlen_cudagraph_unsupported_backend
222 assert unsupported([[target, draft]], config, 8) is None
223 assert unsupported([[target, draft]], config, 9) == ("_DraftBackend", 8)
224 assert (
225 unsupported([[target, draft]], config, 9, checked_layer_names={"target"})
226 is None
227 )
228 assert unsupported([[target, never]], config, 1) == ("_DraftBackend", None)
229
230
231 def test_get_kv_sharing_fast_prefill_eligible_layers(monkeypatch: pytest.MonkeyPatch):
232 """Fast prefill applies to the contiguous suffix of KV-sharing layers.
233
234 Draft-model layers register after the target model's and may share KV, so
235 they must not extend (or break) the target's eligible suffix.
236 """
237
238 def check(
239 layer_names: list[str],
240 shared: dict[str, str],
241 draft_layer_names: set[str] | None = None,
242 ) -> set[str]:
243 monkeypatch.setattr(
244 attn_utils,
245 "get_layers_from_vllm_config",
246 lambda *a, **k: {name: None for name in layer_names},
247 )
248 monkeypatch.setattr(attn_utils, "get_shared_kv_cache_layers", lambda *a: shared)
249 vllm_config = SimpleNamespace(
250 cache_config=SimpleNamespace(kv_sharing_fast_prefill=True)
251 )
252 return attn_utils.get_kv_sharing_fast_prefill_eligible_layers(
253 vllm_config, draft_layer_names
254 )
255
256 # No KV sharing: nothing is eligible.
257 assert check(["t0", "t1"], {}) == set()
258
259 # Trailing run of sharing layers (YOCO-style second half).
260 assert check(["t0", "t1", "t2", "t3"], {"t2": "t1", "t3": "t1"}) == {"t2", "t3"}
261
262 # A non-sharing layer after a sharing one breaks the suffix.
263 assert check(["t0", "t1", "t2", "t3"], {"t1": "t0", "t3": "t0"}) == {"t3"}
264
265 # KV-sharing draft layers at the end are collected without an exclusion...
266 assert check(
267 ["t0", "t1", "t2", "t3", "d0", "d1"],
268 {"t2": "t1", "t3": "t1", "d0": "t1", "d1": "t1"},
269 ) == {"t2", "t3", "d0", "d1"}
270
271 # ...so the runner excludes them: skipped, not collected, and they do not
272 # break the target's trailing run.
273 assert check(
274 ["t0", "t1", "t2", "t3", "d0", "d1"],
275 {"t2": "t1", "t3": "t1", "d0": "t1", "d1": "t1"},
276 draft_layer_names={"d0", "d1"},
277 ) == {"t2", "t3"}
278
279 # Feature flag off: nothing is eligible even with sharing layers.
280 monkeypatch.setattr(
281 attn_utils, "get_layers_from_vllm_config", lambda *a, **k: {"t0": None}
282 )
283 monkeypatch.setattr(
284 attn_utils, "get_shared_kv_cache_layers", lambda *a: {"t0": "t0"}
285 )
286 vllm_config = SimpleNamespace(
287 cache_config=SimpleNamespace(kv_sharing_fast_prefill=False)
288 )
289 assert attn_utils.get_kv_sharing_fast_prefill_eligible_layers(vllm_config) == set()
290
291
292 @pytest.mark.parametrize(
293 "num_tokens", [1, 2, 3, 4, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128]
294 )
295 @pytest.mark.parametrize("num_active_loras", [1, 2, 4])
296 def test_fast_prefill_dispatch_preserves_active_lora_count(
297 num_tokens: int, num_active_loras: int
298 ):
299 """Fast-prefill padding must match the main dispatch's LoRA variant."""
300
301 class FakeCudaGraphManager:
302 device = "cpu"
303
304 def __init__(self):
305 self.dispatch_calls = []
306
307 def dispatch(self, **kwargs):
308 self.dispatch_calls.append(kwargs)
309 # A captured no-LoRA graph is padded to 8 tokens, while the
310 # active-LoRA path stays eager at the unpadded token count.
311 if kwargs["num_active_loras"] == 0:
312 num_tokens = 8
313 mode = CUDAGraphMode.PIECEWISE
314 else:
315 num_tokens = kwargs["num_tokens"]
316 mode = CUDAGraphMode.NONE
317 return BatchExecutionDescriptor(
318 cg_mode=mode,
319 num_tokens=num_tokens,
320 num_reqs=kwargs["num_reqs"],
321 num_active_loras=kwargs["num_active_loras"],
322 num_ubatches=1,
323 )
324
325 manager = FakeCudaGraphManager()
326 helper = FastPrefillHelper(manager, max_num_tokens=max(32, num_tokens))
327 metadata = helper.prepare(
328 torch.arange(num_tokens, dtype=torch.int32),
329 num_reqs=1,
330 cu_num_logits_np=np.array([0, num_tokens], dtype=np.int32),
331 has_prefill=True,
332 batch_desc=BatchExecutionDescriptor(
333 cg_mode=CUDAGraphMode.NONE,
334 num_tokens=num_tokens,
335 num_reqs=1,
336 num_active_loras=num_active_loras,
337 num_ubatches=1,
338 ),
339 num_active_loras=num_active_loras,
340 )
341
342 assert metadata is not None
343 assert metadata.num_logits_indices == num_tokens
344 assert metadata.logits_indices_padded.shape[0] == num_tokens
345 assert metadata.max_logits_per_req == num_tokens
346 assert manager.dispatch_calls[-1]["num_active_loras"] == num_active_loras
347
348
349 class _FakeSharedHostRegion:
350 def __init__(self) -> None:
351 self.cleanup_calls = 0
352 self.base_tensor = torch.empty(1, dtype=torch.int8)
353
354 def cleanup(self) -> None:
355 self.cleanup_calls += 1
356
357
358 def test_profiling_cleanup_releases_tp_shared_region_once(monkeypatch):
359 """TP-shared profiling pools must use region-aware chunk cleanup."""
360 region = _FakeSharedHostRegion()
361 runtime = SimpleNamespace(
362 _host_cache=object(),
363 registered_host_pool=region.base_tensor,
364 hot_backing=object(),
365 shared_host_region=region,
366 )
367 forward_context = {
368 "layer": SimpleNamespace(
369 hisparse_cache=SimpleNamespace(runtime=runtime),
370 )
371 }
372 released = []
373
374 def release_pinned_state(runtimes, pinned_host_pools, shared_host_region):
375 released.append((runtimes, pinned_host_pools, shared_host_region))
376
377 monkeypatch.setattr(
378 attn_utils_module,
379 "release_pinned_state",
380 release_pinned_state,
381 )
382
383 attn_utils_module.release_hisparse_profiling_cache(forward_context)
384
385 assert released == [([runtime], [], region)]
386
387
388 @pytest.mark.parametrize("failure_phase", ["allocation", "binding", "buffers"])
389 def test_init_hisparse_rolls_back_shared_region(monkeypatch, failure_phase):
390 """A failure after mmap allocation must not leak the shared registration."""
391 region = _FakeSharedHostRegion()
392 vllm_config = SimpleNamespace(
393 cache_config=SimpleNamespace(
394 get_resolved_kv_cache_layout=lambda: KVCacheLayout.BLHNC
395 ),
396 scheduler_config=SimpleNamespace(max_num_seqs=1, max_num_batched_tokens=1),
397 )
398
399 def allocate(*args):
400 args[-1].shared_region = region
401 if failure_phase == "allocation":
402 raise RuntimeError("initialization failed")
403 return {}
404
405 def bind(**kwargs):
406 if failure_phase == "binding":
407 raise RuntimeError("initialization failed")
408 return []
409
410 def buffers(*args, **kwargs):
411 raise RuntimeError("initialization failed")
412
413 monkeypatch.setattr(attn_utils_module, "allocate_hisparse_kv_caches", allocate)
414 monkeypatch.setattr(attn_utils_module, "bind_hisparse_kv_caches", bind)
415 monkeypatch.setattr(
416 attn_utils_module, "initialize_hisparse_runtime_buffers", buffers
417 )
418 with pytest.raises(RuntimeError, match="initialization failed"):
419 attn_utils_module.init_hisparse_kv_cache(
420 SimpleNamespace(),
421 torch.device("cpu"),
422 [],
423 vllm_config,
424 {},
425 SimpleNamespace(),
426 )
427 assert region.cleanup_calls == 1
428
429
430 def test_reshape_padded_kv_cache_strides_by_padded_page():
431 num_blocks = 3
432 spec = FullAttentionSpec(
433 block_size=16,
434 num_kv_heads=1,
435 head_size=2,
436 dtype=torch.float32,
437 page_size_padded=384,
438 )
439 assert spec.real_page_size_bytes == 256
440
441 raw = torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
442 (kv_cache,) = dense_kv_cache_views(raw, spec, num_blocks, 1, KVCacheLayout.LBHNC)
443
444 elem_size = 4 # float32
445 # Content dim packs K and V: 2 * head_size.
446 assert kv_cache.shape == (num_blocks, 1, 16, 2 * spec.head_size)
447 assert kv_cache.dtype == spec.dtype
448 assert kv_cache.stride(0) == spec.page_size_padded // elem_size
449 assert kv_cache[1].storage_offset() == spec.page_size_padded // elem_size
450 # Within one block the (unpadded) content stays compact.
451 assert kv_cache[0].is_contiguous()
452
453
454 @pytest.mark.parametrize(
455 (
456 "kernel_block_sizes",
457 "storage_block_size",
458 "expected_num_blocks",
459 "expected_num_states",
460 ),
461 [
462 (None, None, 4, 64),
463 ([256], None, 4, 64),
464 ([64], None, 16, 16),
465 ([64], 256, 4, 64),
466 ],
467 )
468 def test_allocate_compressed_mla_cache(
469 kernel_block_sizes: list[int] | None,
470 storage_block_size: int | None,
471 expected_num_blocks: int,
472 expected_num_states: int,
473 ):
474 spec = MLAAttentionSpec(
475 block_size=256,
476 num_kv_heads=1,
477 head_size=128,
478 dtype=torch.bfloat16,
479 tokens_per_state=4,
480 storage_block_size=storage_block_size,
481 )
482 num_pages = 4
483 config = KVCacheConfig(
484 num_blocks=num_pages,
485 kv_cache_tensors=[
486 KVCacheTensor(
487 size=num_pages * spec.page_size_bytes,
488 layers=["layer.0"],
489 layer_stride=num_pages * spec.page_size_bytes,
490 block_stride=spec.page_size_bytes,
491 )
492 ],
493 kv_cache_groups=[KVCacheGroupSpec(["layer.0"], spec)],
494 )
495
496 caches = allocate_kv_cache(
497 config, torch.device("cpu"), KVCacheLayout.LBHNC, kernel_block_sizes
498 )
499
500 assert caches["layer.0"].shape == (expected_num_blocks, 1, expected_num_states, 128)
501
502
503 @pytest.mark.parametrize("layout", list(KVCacheLayout))
504 def test_copy_kv_cache_blocks_shared_storage(layout: KVCacheLayout):
505 num_blocks = 4
506 num_layers = 2
507 spec = FullAttentionSpec(
508 block_size=2,
509 num_kv_heads=2,
510 head_size=2,
511 dtype=torch.float32,
512 )
513 raw = torch.zeros(num_blocks * num_layers * spec.page_size_bytes, dtype=torch.int8)
514 caches = dense_kv_cache_views(raw, spec, num_blocks, num_layers, layout)
515
516 for layer_idx, cache in enumerate(caches):
517 for block_idx in range(num_blocks):
518 cache[block_idx].fill_(10 * layer_idx + block_idx)
519
520 expected = [[cache[i].clone() for i in range(num_blocks)] for cache in caches]
521 copies = [KVCacheBlockCopy(src_block_id=0, dst_block_id=2)]
522
523 copy_kv_cache_blocks_inplace(caches, num_blocks, copies)
524
525 for layer_idx, cache in enumerate(caches):
526 torch.testing.assert_close(cache[2], expected[layer_idx][0])
527 torch.testing.assert_close(cache[1], expected[layer_idx][1])
528
529
530 def test_fixed_block_stride_propagates_outward_in_lhbnc():
531 num_blocks = 3
532 num_layers = 2
533 spec = FullAttentionSpec(
534 block_size=2,
535 num_kv_heads=2,
536 head_size=2,
537 dtype=torch.float32,
538 )
539 natural = compute_layout_strides(spec, num_blocks, num_layers, KVCacheLayout.LHBNC)
540 block_stride = natural[1] + 8
541
542 strides = compute_layout_strides(
543 spec,
544 num_blocks,
545 num_layers,
546 KVCacheLayout.LHBNC,
547 fixed_strides=(None, block_stride, None, None, None),
548 )
549
550 assert strides[1] == block_stride
551 assert strides[2] == block_stride * num_blocks
552 assert strides[0] == strides[2] * spec.num_heads
553
554
555 def test_copy_kv_cache_blocks_separate_head_groups():
556 # LHBNC stores each head group separately, so a block's bytes are scattered
557 # across L*H regions.
558 layout = KVCacheLayout.LHBNC
559 num_blocks = 4
560 num_layers = 2
561 spec = FullAttentionSpec(
562 block_size=2,
563 num_kv_heads=2,
564 head_size=2,
565 dtype=torch.float32,
566 num_head_slots=2,
567 state_content_bytes=2 * 2 * 4,
568 )
569 raw = torch.zeros(num_blocks * num_layers * spec.page_size_bytes, dtype=torch.int8)
570 caches = dense_kv_cache_views(raw, spec, num_blocks, num_layers, layout)
571
572 for layer_idx, cache in enumerate(caches):
573 for block_idx in range(num_blocks):
574 for head_idx in range(cache.shape[1]):
575 cache[block_idx, head_idx].fill_(
576 100 * layer_idx + 10 * head_idx + block_idx
577 )
578
579 expected = [[cache[i].clone() for i in range(num_blocks)] for cache in caches]
580 copy_kv_cache_blocks_inplace(
581 caches,
582 num_blocks,
583 [KVCacheBlockCopy(src_block_id=0, dst_block_id=2)],
584 )
585
586 for layer_idx, cache in enumerate(caches):
587 torch.testing.assert_close(cache[2], expected[layer_idx][0])
588 torch.testing.assert_close(cache[1], expected[layer_idx][1])
589
590
591 @pytest.mark.parametrize(
592 "layout,num_layers",
593 [
594 (KVCacheLayout.LBHNC, 2),
595 # Splitting needs a manager block to be one dense page, which a
596 # block-outermost layout only gives when the block holds one layer.
597 (KVCacheLayout.BLHNC, 1),
598 ],
599 )
600 def test_copy_kv_cache_blocks_with_virtual_block_splitting(
601 layout: KVCacheLayout, num_layers: int
602 ):
603 num_blocks = 4
604 physical_per_logical = 2
605 spec = FullAttentionSpec(
606 block_size=4,
607 num_kv_heads=1,
608 head_size=2,
609 dtype=torch.float32,
610 )
611 raw = torch.zeros(num_blocks * num_layers * spec.page_size_bytes, dtype=torch.int8)
612 caches = dense_kv_cache_views(
613 raw,
614 spec,
615 num_blocks,
616 num_layers,
617 layout,
618 kernel_block_size=spec.block_size // physical_per_logical,
619 )
620
621 for layer_idx, cache in enumerate(caches):
622 for block_idx in range(cache.shape[0]):
623 cache[block_idx].fill_(100 * layer_idx + block_idx)
624 expected = [[cache[i].clone() for i in range(cache.shape[0])] for cache in caches]
625
626 copy_kv_cache_blocks_inplace(
627 caches,
628 num_blocks,
629 [KVCacheBlockCopy(src_block_id=0, dst_block_id=2)],
630 )
631
632 dst_start = 2 * physical_per_logical
633 for layer_idx, cache in enumerate(caches):
634 for physical_idx in range(physical_per_logical):
635 torch.testing.assert_close(
636 cache[dst_start + physical_idx], expected[layer_idx][physical_idx]
637 )
638
639
640 def test_allocate_hisparse_kv_caches_host_pool_and_view_less_specs():
641 """Host tensors get their own backing; view-less specs keep the raw one."""
642 spec = FullAttentionSpec(
643 block_size=2, num_kv_heads=1, head_size=4, dtype=torch.float32
644 )
645 page = spec.page_size_bytes
646 resident_spec = HiSparseResidentSpec(block_size=2, page_size=page)
647 device_size = 4 * page
648 config = KVCacheConfig(
649 num_blocks=4,
650 hisparse_host_num_blocks=3,
651 kv_cache_tensors=[
652 KVCacheTensor(
653 size=3 * page,
654 layers=["source"],
655 layer_stride=3 * page,
656 block_stride=page,
657 host_resident=True,
658 ),
659 KVCacheTensor(
660 size=device_size,
661 layers=["indexer"],
662 layer_stride=device_size,
663 block_stride=page,
664 ),
665 KVCacheTensor(
666 size=device_size,
667 layers=["resident"],
668 layer_stride=device_size,
669 block_stride=page,
670 ),
671 ],
672 kv_cache_groups=[
673 KVCacheGroupSpec(["source"], spec, host_resident=True),
674 KVCacheGroupSpec(["indexer"], spec),
675 KVCacheGroupSpec(["resident"], resident_spec),
676 ],
677 )
678 host_buffers: list[torch.Tensor] = []
679
680 def host_allocator(size: int) -> torch.Tensor:
681 host_buffers.append(torch.zeros(size, dtype=torch.int8))
682 return host_buffers[-1]
683
684 caches = allocate_hisparse_kv_caches(
685 config,
686 torch.device("cpu"),
687 KVCacheLayout.LBHNC,
688 [2, 2, 2],
689 SimpleNamespace(allocate=host_allocator),
690 )
691 assert len(config.kv_cache_tensors) == 3
692
693 assert [buf.numel() for buf in host_buffers] == [3 * page]
694 assert caches["source"].shape[0] == 3
695 assert (
696 caches["source"].untyped_storage().data_ptr()
697 == host_buffers[0].untyped_storage().data_ptr()
698 )
699 assert caches["indexer"].shape[0] == 4
700 backing = caches["resident"]
701 assert backing.dtype == torch.int8 and backing.numel() >= device_size
702 assert (
703 backing.untyped_storage().data_ptr()
704 == caches["indexer"].untyped_storage().data_ptr()
705 )