A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 12, 13:30 UTC
[Hybrid Allocator] Support KV cache groups with different block_size (#29143)
Signed-off-by: Yifan Qiao <yifanqiao@berkeley.edu> Co-authored-by: Chen Zhang <zhangch99@outlook.com>
Yifan Qiao committed
Nov 25, 2025 at 07:30 UTC
48ddb02b79d7e22e2eefbf5294bf70de50afd1b2
11 files changed
+472
-87
tests/v1/core/test_kv_cache_utils.py
+45
-4
@@ -1248,7 +1248,9 @@ def test_allocate_with_lookahead():
1248
)
1249
1250
# Test case 1: Requires additional lookahead tokens
1251
- kv_cache_manager = KVCacheManager(kv_cache_config=config, max_model_len=100)
1251
+ kv_cache_manager = KVCacheManager(
1252
+ kv_cache_config=config, max_model_len=100, hash_block_size=block_size
1253
+ )
1254
blocks = kv_cache_manager.allocate_slots(
1255
request,
1256
num_new_tokens=3,
@@ -1257,7 +1259,9 @@ def test_allocate_with_lookahead():
1259
assert len(blocks.get_block_ids()[0]) == 2 # ceil(5/4)=2 blocks
1260
1261
# Test case 2: With precomputed blocks
1260
- kv_cache_manager = KVCacheManager(kv_cache_config=config, max_model_len=100)
1262
+ kv_cache_manager = KVCacheManager(
1263
+ kv_cache_config=config, max_model_len=100, hash_block_size=block_size
1264
+ )
1265
# required_blocks = ceil((3 + 2) /4) = 2
1266
blocks = kv_cache_manager.allocate_slots(
1267
request,
@@ -1268,7 +1272,9 @@ def test_allocate_with_lookahead():
1272
1273
# Test case 3: With precomputed blocks
1274
# required_blocks = ceil((3 + 4) / 4) = 2
1271
- kv_cache_manager = KVCacheManager(kv_cache_config=config, max_model_len=100)
1275
+ kv_cache_manager = KVCacheManager(
1276
+ kv_cache_config=config, max_model_len=100, hash_block_size=block_size
1277
+ )
1278
blocks = kv_cache_manager.allocate_slots(
1279
request,
1280
num_new_tokens=3,
@@ -1495,7 +1501,8 @@ def test_get_kv_cache_config_one_worker():
1501
),
1502
],
1503
)
1498
- # different hidden size
1504
+
1505
+ # different hidden size but same type, use UniformTypeKVCacheSpecs
1506
kv_cache_specs_hybrid = {
1507
"layer_1": new_kv_cache_spec(head_size=128),
1508
"layer_2": new_kv_cache_spec(head_size=64),
@@ -1519,6 +1526,40 @@ def test_get_kv_cache_config_one_worker():
1526
],
1527
)
1528
1529
+ # Different hidden size and different type, align by different block size
1530
+ kv_cache_specs_hybrid = {
1531
+ "layer_1": new_kv_cache_spec(head_size=64),
1532
+ "layer_2": new_sliding_window_spec(head_size=32),
1533
+ }
1534
+ kv_cache_config_hybrid = get_kv_cache_configs(
1535
+ vllm_config, [kv_cache_specs_hybrid], [mem_per_block_per_layer * 32]
1536
+ )[0]
1537
+ assert kv_cache_config_hybrid == KVCacheConfig(
1538
+ num_blocks=32,
1539
+ kv_cache_tensors=[
1540
+ KVCacheTensor(
1541
+ size=mem_per_block_per_layer * 32, shared_by=["layer_1", "layer_2"]
1542
+ ),
1543
+ ],
1544
+ kv_cache_groups=[
1545
+ KVCacheGroupSpec(["layer_1"], new_kv_cache_spec(head_size=64)),
1546
+ KVCacheGroupSpec(
1547
+ ["layer_2"], new_sliding_window_spec(head_size=32, block_size=32)
1548
+ ),
1549
+ ],
1550
+ )
1551
+
1552
+ # different hidden size that cannot be aligned by using different block size
1553
+ kv_cache_specs_hybrid = {
1554
+ "layer_1": new_kv_cache_spec(head_size=64),
1555
+ "layer_2": new_sliding_window_spec(head_size=96),
1556
+ }
1557
+
1558
+ with pytest.raises(NotImplementedError):
1559
+ get_kv_cache_configs(
1560
+ vllm_config, [kv_cache_specs_hybrid], [mem_per_block_per_layer * 2 * 32]
1561
+ )[0]
1562
+
1563
# Test num_gpu_blocks_override
1564
vllm_config.cache_config.num_gpu_blocks_override = 16
1565
kv_cache_config_override_blocks = get_kv_cache_configs(
tests/v1/core/test_prefix_caching.py
+93
-2
@@ -134,6 +134,7 @@ def test_prefill(hash_fn):
134
make_kv_cache_config(block_size, 11),
135
max_model_len=8192,
136
enable_caching=True,
137
+ hash_block_size=block_size,
138
)
139
140
# Complete 3 blocks (48 tokens)
@@ -256,6 +257,7 @@ def test_prefill_hybrid_model():
257
make_kv_cache_config_hybrid_model(block_size, 21),
258
max_model_len=8192,
259
enable_caching=True,
260
+ hash_block_size=block_size,
261
)
262
263
hash_fn = sha256
@@ -416,6 +418,7 @@ def test_prefill_plp():
418
make_kv_cache_config(block_size, 11),
419
max_model_len=8192,
420
enable_caching=True,
421
+ hash_block_size=block_size,
422
)
423
# the default hash function is sha256
424
hash_fn = sha256
@@ -523,6 +526,7 @@ def test_decode():
526
make_kv_cache_config(block_size, 11),
527
max_model_len=8192,
528
enable_caching=True,
529
+ hash_block_size=block_size,
530
)
531
532
# Complete 3 blocks (48 tokens)
@@ -585,6 +589,7 @@ def test_evict():
589
make_kv_cache_config(block_size, 11),
590
max_model_len=8192,
591
enable_caching=True,
592
+ hash_block_size=block_size,
593
)
594
595
last_token_id = 5 * 16 + 7
@@ -643,6 +648,7 @@ def test_hash_block_correct_reuse():
648
make_kv_cache_config(16, 2),
649
max_model_len=8192,
650
enable_caching=True,
651
+ hash_block_size=block_size,
652
)
653
654
# Allocate 1 block and cache it.
@@ -683,6 +689,7 @@ def test_computed_blocks_not_evicted():
689
make_kv_cache_config(block_size, 3),
690
max_model_len=8192,
691
enable_caching=True,
692
+ hash_block_size=block_size,
693
)
694
695
# Allocate a block and cache it.
@@ -741,6 +748,7 @@ def test_basic_prefix_caching_disabled():
748
make_kv_cache_config(block_size, 5),
749
max_model_len=8192,
750
enable_caching=False,
751
+ hash_block_size=block_size,
752
)
753
754
req1 = make_request(
@@ -790,6 +798,7 @@ def test_cache_blocks(hash_fn):
798
block_pool = BlockPool(
799
num_gpu_blocks=5,
800
enable_caching=True,
801
+ hash_block_size=block_size,
802
)
803
# Req:
804
# Block 0: [0, 1, 2, 3]
@@ -833,7 +842,9 @@ def test_cache_blocks_multi_group():
842
This tests that blocks are cached correctly for different kv cache groups.
843
"""
844
block_size = 4
836
- block_pool = BlockPool(num_gpu_blocks=10, enable_caching=True)
845
+ block_pool = BlockPool(
846
+ num_gpu_blocks=10, enable_caching=True, hash_block_size=block_size
847
+ )
848
849
# Req:
850
# Block 0/4: [0, 1, 2, 3]
@@ -921,6 +932,7 @@ def test_mm_prefix_caching():
932
make_kv_cache_config(block_size, 11),
933
max_model_len=8192,
934
enable_caching=True,
935
+ hash_block_size=block_size,
936
)
937
938
# Common prompt tokens (T is text tokens and P is image placeholder tokens)
@@ -1020,6 +1032,7 @@ def test_cache_key_salting():
1032
make_kv_cache_config(block_size, 11),
1033
max_model_len=8192,
1034
enable_caching=True,
1035
+ hash_block_size=block_size,
1036
)
1037
1038
# 3 complete blocks and an incomplete block with 11 tokens.
@@ -1101,6 +1114,7 @@ def test_prefill_not_enough_free_blocks_with_computed_blocks():
1114
make_kv_cache_config(block_size, 11),
1115
max_model_len=8192,
1116
enable_caching=True,
1117
+ hash_block_size=block_size,
1118
)
1119
# Complete 3 blocks (48 tokens)
1120
# | Common-0 | Common-1 | Common-2 | ... |
@@ -1173,6 +1187,7 @@ def test_reset_prefix_cache():
1187
make_kv_cache_config(block_size, 11),
1188
max_model_len=8192,
1189
enable_caching=True,
1190
+ hash_block_size=block_size,
1191
)
1192
1193
full_block_token_ids = [i for i in range(3) for _ in range(16)]
@@ -1213,6 +1228,7 @@ def test_prefix_cache_stats_disabled():
1228
make_kv_cache_config(block_size, 11),
1229
max_model_len=8192,
1230
enable_caching=True,
1231
+ hash_block_size=block_size,
1232
log_stats=False, # Disable logging stats
1233
)
1234
assert manager.prefix_cache_stats is None
@@ -1232,7 +1248,7 @@ def test_prefix_cache_stats_disabled():
1248
1249
1250
def test_maybe_evict_cached_block():
1235
- pool = BlockPool(num_gpu_blocks=4, enable_caching=True)
1251
+ pool = BlockPool(num_gpu_blocks=4, enable_caching=True, hash_block_size=16)
1252
block_hash0 = make_block_hash_with_group_id(BlockHash(b"10"), 1000)
1253
block_hash1 = make_block_hash_with_group_id(BlockHash(b"20"), 2000)
1254
block_hash2 = make_block_hash_with_group_id(BlockHash(b"30"), 3000)
@@ -1293,6 +1309,7 @@ def test_kv_cache_events(blocks_to_cache: int):
1309
max_model_len=8192,
1310
enable_caching=True,
1311
enable_kv_cache_events=True,
1312
+ hash_block_size=block_size,
1313
)
1314
1315
num_tokens = block_size * blocks_to_cache
@@ -1351,6 +1368,7 @@ def test_kv_cache_events_with_lora(blocks_to_cache: int):
1368
max_model_len=8192,
1369
enable_caching=True,
1370
enable_kv_cache_events=True,
1371
+ hash_block_size=block_size,
1372
)
1373
1374
# Test with LoRA request
@@ -1405,6 +1423,7 @@ def test_eagle_enabled_removes_last_block():
1423
max_model_len=8192,
1424
enable_caching=True,
1425
use_eagle=True,
1426
+ hash_block_size=block_size,
1427
)
1428
1429
# Request with 3 full blocks (48 tokens)
@@ -1437,6 +1456,7 @@ def test_eagle_with_partial_blocks():
1456
max_model_len=8192,
1457
enable_caching=True,
1458
use_eagle=True,
1459
+ hash_block_size=block_size,
1460
)
1461
# 2 full blocks + 5 tokens (non-divisible length)
1462
token_ids = [0] * (2 * block_size + 5)
@@ -1476,6 +1496,7 @@ def test_eagle_with_sliding_window():
1496
max_model_len=8192,
1497
enable_caching=True,
1498
use_eagle=True,
1499
+ hash_block_size=block_size,
1500
)
1501
1502
# 2 full blocks + 5 tokens (non-divisible length)
@@ -1522,6 +1543,76 @@ def test_eagle_with_sliding_window():
1543
assert num_tokens == 0
1544
1545
1546
+def test_different_block_size():
1547
+ block_size = 16
1548
+ # full attention and sliding window attention layers have the same page size:
1549
+ # (32 tokens/block * float16 token, vs. 16 tokens/block * float32 token)
1550
+ kv_cache_config = KVCacheConfig(
1551
+ num_blocks=100,
1552
+ kv_cache_tensors=[],
1553
+ kv_cache_groups=[
1554
+ KVCacheGroupSpec(
1555
+ ["layer1"],
1556
+ FullAttentionSpec(block_size * 2, 1, 1, torch.float16),
1557
+ ),
1558
+ KVCacheGroupSpec(
1559
+ ["layer2"],
1560
+ SlidingWindowSpec(
1561
+ block_size,
1562
+ 1,
1563
+ 1,
1564
+ torch.float32,
1565
+ sliding_window=2 * block_size,
1566
+ ),
1567
+ ),
1568
+ ],
1569
+ )
1570
+ manager = KVCacheManager(
1571
+ kv_cache_config=kv_cache_config,
1572
+ max_model_len=8192,
1573
+ enable_caching=True,
1574
+ hash_block_size=block_size,
1575
+ )
1576
+
1577
+ # 10 blocks of 16 tokens each. Token ids are not strictly aligned for each block.
1578
+ common_token_ids = [i for i in range(10) for _ in range(block_size)]
1579
+
1580
+ req0 = make_request("0", common_token_ids, block_size, sha256)
1581
+ computed_blocks, num_computed_tokens = manager.get_computed_blocks(req0)
1582
+ assert not computed_blocks.blocks[0]
1583
+ assert not computed_blocks.blocks[1]
1584
+ assert num_computed_tokens == 0
1585
+ blocks = manager.allocate_slots(
1586
+ req0, 7 * block_size, len(computed_blocks.blocks[0]) * 16, computed_blocks
1587
+ )
1588
+ assert blocks.get_block_ids() == ([1, 2, 3, 4], [5, 6, 7, 8, 9, 10, 11])
1589
+ req1 = make_request("1", common_token_ids[: 7 * block_size + 1], block_size, sha256)
1590
+ computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
1591
+ assert len(computed_blocks.blocks[0]) == 3
1592
+ assert len(computed_blocks.blocks[1]) == 6
1593
+ assert num_computed_tokens == 6 * 16
1594
+
1595
+ req2 = make_request("2", common_token_ids[: 6 * block_size + 1], block_size, sha256)
1596
+ computed_blocks, num_computed_tokens = manager.get_computed_blocks(req2)
1597
+ assert len(computed_blocks.blocks[0]) == 3
1598
+ assert len(computed_blocks.blocks[1]) == 6
1599
+ assert num_computed_tokens == 6 * 16
1600
+
1601
+ # Evict some blocks to make sliding window cache hit length 5*16
1602
+ # But should return 4 * 16 because full attention cache hit length must be
1603
+ # a multiple of 32
1604
+ manager.block_pool.cached_block_hash_to_block.pop(
1605
+ make_block_hash_with_group_id(req1.block_hashes[6], 1), 11
1606
+ )
1607
+ manager.block_pool.cached_block_hash_to_block.pop(
1608
+ make_block_hash_with_group_id(req1.block_hashes[5], 1), 10
1609
+ )
1610
+ computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
1611
+ assert len(computed_blocks.blocks[0]) == 2
1612
+ assert len(computed_blocks.blocks[1]) == 4
1613
+ assert num_computed_tokens == 4 * 16
1614
+
1615
+
1616
def test_block_lookup_cache_single_block_per_key():
1617
cache = BlockHashToBlockMap()
1618
key0 = BlockHashWithGroupId(b"hash0")
tests/v1/core/test_single_type_kv_cache_manager.py
+16
-6
@@ -41,7 +41,9 @@ def test_chunked_local_attention_possible_cached_prefix():
41
attention_chunk_size=4,
42
)
43
44
- block_pool = BlockPool(num_gpu_blocks=100, enable_caching=True)
44
+ block_pool = BlockPool(
45
+ num_gpu_blocks=100, enable_caching=True, hash_block_size=block_size
46
+ )
47
manager = get_chunked_local_attention_manager(
48
chunked_local_attention_spec, block_pool
49
)
@@ -70,6 +72,7 @@ def test_chunked_local_attention_possible_cached_prefix():
72
block_pool=block_pool,
73
kv_cache_spec=chunked_local_attention_spec,
74
use_eagle=False,
75
+ alignment_tokens=block_size,
76
)[0]
77
assert len(computed_blocks) == expect_length
78
@@ -111,7 +114,9 @@ def test_sliding_window_possible_cached_prefix():
114
sliding_window=4,
115
)
116
114
- block_pool = BlockPool(num_gpu_blocks=100, enable_caching=True)
117
+ block_pool = BlockPool(
118
+ num_gpu_blocks=100, enable_caching=True, hash_block_size=block_size
119
+ )
120
manager = get_sliding_window_manager(sliding_window_spec, block_pool)
121
122
def run_one_case(block_is_cached, expect_length):
@@ -138,6 +143,7 @@ def test_sliding_window_possible_cached_prefix():
143
block_pool=block_pool,
144
kv_cache_spec=sliding_window_spec,
145
use_eagle=False,
146
+ alignment_tokens=block_size,
147
)[0]
148
assert len(computed_blocks) == expect_length
149
@@ -178,7 +184,7 @@ def test_chunked_local_attention_remove_skipped_blocks():
184
attention_chunk_size=4,
185
)
186
181
- block_pool = BlockPool(num_gpu_blocks=2000, enable_caching=True)
187
+ block_pool = BlockPool(num_gpu_blocks=2000, enable_caching=True, hash_block_size=2)
188
189
manager = get_chunked_local_attention_manager(attention_spec, block_pool)
190
@@ -239,7 +245,7 @@ def test_sliding_window_remove_skipped_blocks():
245
sliding_window=4,
246
)
247
242
- block_pool = BlockPool(num_gpu_blocks=2000, enable_caching=True)
248
+ block_pool = BlockPool(num_gpu_blocks=2000, enable_caching=True, hash_block_size=2)
249
250
manager = get_sliding_window_manager(sliding_window_spec, block_pool)
251
@@ -316,7 +322,9 @@ def test_get_num_blocks_to_allocate():
322
sliding_window=4, # Placeholder value, not related to test result
323
)
324
319
- block_pool = BlockPool(num_gpu_blocks=100, enable_caching=True)
325
+ block_pool = BlockPool(
326
+ num_gpu_blocks=100, enable_caching=True, hash_block_size=block_size
327
+ )
328
manager = get_sliding_window_manager(sliding_window_spec, block_pool)
329
cached_blocks_1 = [KVCacheBlock(i + 1) for i in range(10)]
330
cached_blocks_2 = [block_pool.null_block for _ in range(5)] + [
@@ -341,7 +349,9 @@ def test_chunked_local_attention_get_num_blocks_to_allocate():
349
attention_chunk_size=4, # Placeholder value, not related to test result
350
)
351
344
- block_pool = BlockPool(num_gpu_blocks=100, enable_caching=True)
352
+ block_pool = BlockPool(
353
+ num_gpu_blocks=100, enable_caching=True, hash_block_size=block_size
354
+ )
355
manager = get_chunked_local_attention_manager(attention_spec, block_pool)
356
cached_blocks_1 = [KVCacheBlock(i + 1) for i in range(10)]
357
cached_blocks_2 = [block_pool.null_block for _ in range(5)] + [
vllm/engine/arg_utils.py
+5
-3
@@ -1816,9 +1816,11 @@ class EngineArgs:
1816
if model_config.runner_type != "pooling":
1817
default_chunked_prefill = True
1818
1819
- # Disable prefix caching default for hybrid models
1820
- # since the feature is still experimental.
1821
- default_prefix_caching = not model_config.is_hybrid
1819
+ # Disable prefix caching default for hybrid models and mamba-only
1820
+ # models since the feature is still experimental.
1821
+ default_prefix_caching = not (
1822
+ model_config.is_hybrid or model_config.is_attention_free
1823
+ )
1824
else:
1825
assert model_config.pooler_config is not None
1826
vllm/model_executor/models/config.py
+8
-3
@@ -289,9 +289,6 @@ class MambaModelConfig(VerifyAndUpdateConfig):
289
model_config = vllm_config.model_config
290
cache_config = vllm_config.cache_config
291
292
- if cache_config.mamba_block_size is None:
293
- cache_config.mamba_block_size = model_config.max_model_len
294
-
292
if cache_config.enable_prefix_caching:
293
if model_config.supports_mamba_prefix_caching:
294
logger.info(
@@ -299,6 +296,11 @@ class MambaModelConfig(VerifyAndUpdateConfig):
296
"Its support for Mamba layers is experimental. "
297
"Please report any issues you may observe."
298
)
299
+ # By default, mamba block size will be set to max_model_len (see
300
+ # below). When enabling prefix caching, we align mamba block size
301
+ # to the block size as the basic granularity for prefix caching.
302
+ if cache_config.mamba_block_size is None:
303
+ cache_config.mamba_block_size = cache_config.block_size
304
else:
305
logger.info(
306
"Hybrid or mamba-based model detected without "
@@ -306,6 +308,9 @@ class MambaModelConfig(VerifyAndUpdateConfig):
308
)
309
cache_config.enable_prefix_caching = False
310
311
+ if cache_config.mamba_block_size is None:
312
+ cache_config.mamba_block_size = model_config.max_model_len
313
+
314
# TODO(tdoublep): remove once cascade attention is supported
315
logger.info(
316
"Disabling cascade attention since it is not supported for hybrid models."
vllm/v1/core/block_pool.py
+21
-1
@@ -13,6 +13,8 @@ from vllm.distributed.kv_events import (
13
from vllm.logger import init_logger
14
from vllm.v1.core.kv_cache_utils import (
15
BlockHash,
16
+ BlockHashList,
17
+ BlockHashListWithBlockSize,
18
BlockHashWithGroupId,
19
ExternalBlockHash,
20
FreeKVCacheBlockQueue,
@@ -133,6 +135,10 @@ class BlockPool:
135
Args:
136
num_gpu_blocks: The number of blocks in the pool.
137
enable_caching: Whether to enable prefix caching.
138
+ hash_block_size: The block size of which the block hashes are computed.
139
+ The actual block size usually equals hash_block_size, but in cases
140
+ where different KV cache groups have different block sizes, the
141
+ actual block size can be a multiple of hash_block_size.
142
enable_kv_cache_events: Whether to enable kv cache events.
143
"""
144
@@ -140,11 +146,13 @@ class BlockPool:
146
self,
147
num_gpu_blocks: int,
148
enable_caching: bool,
149
+ hash_block_size: int,
150
enable_kv_cache_events: bool = False,
151
):
152
assert isinstance(num_gpu_blocks, int) and num_gpu_blocks > 0
153
self.num_gpu_blocks = num_gpu_blocks
154
self.enable_caching = enable_caching
155
+ self.hash_block_size = hash_block_size
156
# All kv-cache blocks.
157
self.blocks: list[KVCacheBlock] = [
158
KVCacheBlock(idx) for idx in range(num_gpu_blocks)
@@ -223,8 +231,20 @@ class BlockPool:
231
return
232
new_full_blocks = blocks[num_cached_blocks:num_full_blocks]
233
assert len(request.block_hashes) >= num_full_blocks
226
- new_block_hashes = request.block_hashes[num_cached_blocks:]
234
+ if block_size == self.hash_block_size:
235
+ # Common case.
236
+ block_hashes: BlockHashList = request.block_hashes
237
+ else:
238
+ # block_size is a multiple of hash_block_size. This happens when
239
+ # different KV cache groups have different block sizes.
240
+ assert block_size % self.hash_block_size == 0
241
+ # Recalculate block_hashes at the granularity of block_size, using
242
+ # the original block_hashes (at the granularity of hash_block_size).
243
+ block_hashes = BlockHashListWithBlockSize(
244
+ request.block_hashes, self.hash_block_size, block_size
245
+ )
246
247
+ new_block_hashes = block_hashes[num_cached_blocks:]
248
new_hashes: list[ExternalBlockHash] | None = (
249
[] if self.enable_kv_cache_events else None
250
)
vllm/v1/core/kv_cache_coordinator.py
+79
-19
@@ -2,15 +2,25 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
from abc import ABC, abstractmethod
4
from collections.abc import Sequence
5
+from math import lcm
6
7
from vllm.v1.core.block_pool import BlockPool
7
-from vllm.v1.core.kv_cache_utils import BlockHash, KVCacheBlock
8
+from vllm.v1.core.kv_cache_utils import (
9
+ BlockHash,
10
+ BlockHashList,
11
+ BlockHashListWithBlockSize,
12
+ KVCacheBlock,
13
+)
14
from vllm.v1.core.single_type_kv_cache_manager import (
15
CrossAttentionManager,
16
FullAttentionManager,
17
get_manager_for_kv_cache_spec,
18
)
13
-from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheSpec
19
+from vllm.v1.kv_cache_interface import (
20
+ FullAttentionSpec,
21
+ KVCacheConfig,
22
+ KVCacheSpec,
23
+)
24
from vllm.v1.request import Request
25
26
@@ -28,13 +38,17 @@ class KVCacheCoordinator(ABC):
38
enable_kv_cache_events: bool,
39
dcp_world_size: int,
40
pcp_world_size: int,
41
+ hash_block_size: int,
42
):
43
self.kv_cache_config = kv_cache_config
44
self.max_model_len = max_model_len
45
self.enable_caching = enable_caching
46
47
self.block_pool = BlockPool(
37
- kv_cache_config.num_blocks, enable_caching, enable_kv_cache_events
48
+ kv_cache_config.num_blocks,
49
+ enable_caching,
50
+ hash_block_size,
51
+ enable_kv_cache_events,
52
)
53
54
# Needs special handling for find_longest_cache_hit if eagle is enabled
@@ -213,6 +227,7 @@ class KVCacheCoordinatorNoPrefixCache(KVCacheCoordinator):
227
enable_kv_cache_events: bool,
228
dcp_world_size: int,
229
pcp_world_size: int,
230
+ hash_block_size: int,
231
):
232
super().__init__(
233
kv_cache_config,
@@ -222,6 +237,7 @@ class KVCacheCoordinatorNoPrefixCache(KVCacheCoordinator):
237
enable_kv_cache_events,
238
dcp_world_size=dcp_world_size,
239
pcp_world_size=pcp_world_size,
240
+ hash_block_size=hash_block_size,
241
)
242
self.num_single_type_manager = len(self.single_type_managers)
243
@@ -255,6 +271,7 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
271
enable_kv_cache_events: bool,
272
dcp_world_size: int,
273
pcp_world_size: int,
274
+ hash_block_size: int,
275
):
276
super().__init__(
277
kv_cache_config,
@@ -264,6 +281,7 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
281
enable_kv_cache_events,
282
dcp_world_size=dcp_world_size,
283
pcp_world_size=pcp_world_size,
284
+ hash_block_size=hash_block_size,
285
)
286
self.kv_cache_spec = self.kv_cache_config.kv_cache_groups[0].kv_cache_spec
287
self.block_size = self.kv_cache_spec.block_size
@@ -273,6 +291,11 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
291
self.block_size *= dcp_world_size
292
if pcp_world_size > 1:
293
self.block_size *= pcp_world_size
294
+ # For models using only Mamba, block_size is set to max_model_len when
295
+ # prefix caching is disabled, and hash_block_size validation is skipped.
296
+ assert not enable_caching or (hash_block_size == self.block_size), (
297
+ "UnitaryKVCacheCoordinator assumes hash_block_size == block_size"
298
+ )
299
assert len(self.kv_cache_config.kv_cache_groups) == 1, (
300
"UnitaryKVCacheCoordinator assumes only one kv cache group"
301
)
@@ -289,6 +312,7 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
312
block_pool=self.block_pool,
313
kv_cache_spec=self.kv_cache_spec,
314
use_eagle=self.use_eagle,
315
+ alignment_tokens=self.block_size,
316
dcp_world_size=self.dcp_world_size,
317
pcp_world_size=self.pcp_world_size,
318
)
@@ -313,6 +337,7 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
337
enable_kv_cache_events: bool,
338
dcp_world_size: int,
339
pcp_world_size: int,
340
+ hash_block_size: int,
341
):
342
super().__init__(
343
kv_cache_config,
@@ -322,7 +347,17 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
347
enable_kv_cache_events,
348
dcp_world_size=dcp_world_size,
349
pcp_world_size=pcp_world_size,
350
+ hash_block_size=hash_block_size,
351
)
352
+ # hash_block_size: the block size used to compute block hashes.
353
+ # The actual block size usually equals hash_block_size, but in cases where
354
+ # different KV cache groups have different block sizes, the actual block size
355
+ # can be a multiple of hash_block_size.
356
+ self.hash_block_size = hash_block_size
357
+ assert all(
358
+ g.kv_cache_spec.block_size % hash_block_size == 0
359
+ for g in kv_cache_config.kv_cache_groups
360
+ ), "block_size must be divisible by hash_block_size"
361
assert dcp_world_size == 1, "DCP not support hybrid attn now."
362
assert pcp_world_size == 1, "PCP not support hybrid attn now."
363
self.verify_and_split_kv_cache_groups()
@@ -373,14 +408,12 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
408
self.other_spec = other_spec
409
self.full_attention_block_size = self.full_attention_spec.block_size
410
self.other_block_size = self.other_spec.block_size
376
-
377
- if self.enable_caching:
378
- # this requirement is only needed for the prefix caching logic
379
- divisible = self.other_block_size % self.full_attention_block_size
380
- assert divisible == 0, (
381
- "KVCacheCoordinator assumes the block_size of full "
382
- "attention layers is divisible by other layers now."
383
- )
411
+ # The LCM of the block sizes of full attention and other attention.
412
+ # The cache hit length must be a multiple of the LCM of the block sizes
413
+ # to make sure the cache hit length is a multiple of the block size of
414
+ # each attention type. Requiring this because we don't support partial
415
+ # block cache hit yet.
416
+ self.lcm_block_size = lcm(self.full_attention_block_size, self.other_block_size)
417
418
if max(self.full_attention_group_ids) < min(self.other_group_ids):
419
self.full_attn_first = True
@@ -414,25 +447,48 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
447
- The number of tokens of the longest cache hit.
448
"""
449
# First, find the longest cache hit for full attention.
450
+ if self.full_attention_spec.block_size == self.hash_block_size:
451
+ # Common case.
452
+ full_attention_block_hashes: BlockHashList = block_hashes
453
+ else:
454
+ # block_size is a multiple of hash_block_size. This happens when different
455
+ # KV cache groups have different block sizes. In this case, we need to
456
+ # recalculate block_hashes at the granularity of block_size, using the
457
+ # original block_hashes (at the granularity of hash_block_size).
458
+ full_attention_block_hashes = BlockHashListWithBlockSize(
459
+ block_hashes, self.hash_block_size, self.full_attention_spec.block_size
460
+ )
461
hit_blocks_full_attn = self.full_attention_manager_cls.find_longest_cache_hit(
418
- block_hashes=block_hashes,
462
+ block_hashes=full_attention_block_hashes,
463
max_length=max_cache_hit_length,
464
kv_cache_group_ids=self.full_attention_group_ids,
465
block_pool=self.block_pool,
466
kv_cache_spec=self.full_attention_spec,
467
use_eagle=self.use_eagle,
468
+ alignment_tokens=self.lcm_block_size,
469
)
470
hit_length = len(hit_blocks_full_attn[0]) * self.full_attention_block_size
471
472
# Next, find the cache hit for the other attention WITHIN
473
# the cache hit of full attention.
474
+ if self.other_spec.block_size == self.hash_block_size:
475
+ # Common case.
476
+ other_block_hashes: BlockHashList = block_hashes
477
+ else:
478
+ # Similar to the full attention case, here we need to recalculate
479
+ # block_hashes at the granularity of block_size, using the original
480
+ # block_hashes (at the granularity of hash_block_size).
481
+ other_block_hashes = BlockHashListWithBlockSize(
482
+ block_hashes, self.hash_block_size, self.other_spec.block_size
483
+ )
484
hit_blocks_other_attn = self.other_attention_cls.find_longest_cache_hit(
430
- block_hashes=block_hashes,
485
+ block_hashes=other_block_hashes,
486
max_length=hit_length,
487
kv_cache_group_ids=self.other_group_ids,
488
block_pool=self.block_pool,
489
kv_cache_spec=self.other_spec,
490
use_eagle=self.use_eagle,
491
+ alignment_tokens=self.lcm_block_size,
492
)
493
hit_length = len(hit_blocks_other_attn[0]) * self.other_block_size
494
@@ -466,6 +522,7 @@ def get_kv_cache_coordinator(
522
enable_kv_cache_events: bool,
523
dcp_world_size: int,
524
pcp_world_size: int,
525
+ hash_block_size: int,
526
) -> KVCacheCoordinator:
527
if not enable_caching:
528
return KVCacheCoordinatorNoPrefixCache(
@@ -473,8 +530,9 @@ def get_kv_cache_coordinator(
530
max_model_len,
531
use_eagle,
532
enable_kv_cache_events,
476
- dcp_world_size=dcp_world_size,
477
- pcp_world_size=pcp_world_size,
533
+ dcp_world_size,
534
+ pcp_world_size,
535
+ hash_block_size,
536
)
537
if len(kv_cache_config.kv_cache_groups) == 1:
538
return UnitaryKVCacheCoordinator(
@@ -483,8 +541,9 @@ def get_kv_cache_coordinator(
541
use_eagle,
542
enable_caching,
543
enable_kv_cache_events,
486
- dcp_world_size=dcp_world_size,
487
- pcp_world_size=pcp_world_size,
544
+ dcp_world_size,
545
+ pcp_world_size,
546
+ hash_block_size,
547
)
548
return HybridKVCacheCoordinator(
549
kv_cache_config,
@@ -492,6 +551,7 @@ def get_kv_cache_coordinator(
551
use_eagle,
552
enable_caching,
553
enable_kv_cache_events,
495
- dcp_world_size=dcp_world_size,
496
- pcp_world_size=pcp_world_size,
554
+ dcp_world_size,
555
+ pcp_world_size,
556
+ hash_block_size,
557
)
vllm/v1/core/kv_cache_manager.py
+5
-20
@@ -95,6 +95,7 @@ class KVCacheManager:
95
self,
96
kv_cache_config: KVCacheConfig,
97
max_model_len: int,
98
+ hash_block_size: int,
99
enable_caching: bool = True,
100
use_eagle: bool = False,
101
log_stats: bool = False,
@@ -107,28 +108,11 @@ class KVCacheManager:
108
self.enable_caching = enable_caching
109
self.use_eagle = use_eagle
110
self.log_stats = log_stats
110
- # FIXME: make prefix cache stats conditional on log_stats
111
+ # FIXME: make prefix cache stats conditional on log_stats. We still need
112
+ # this comment because when the log stats is enabled there are still
113
+ # potential configs we could expose in the future.
114
self.prefix_cache_stats = PrefixCacheStats() if log_stats else None
115
113
- self.block_size: int | None = None
114
- if self.enable_caching:
115
- assert (
116
- len(
117
- set(
118
- g.kv_cache_spec.block_size
119
- for g in kv_cache_config.kv_cache_groups
120
- )
121
- )
122
- == 1
123
- ), "Only one block size is supported for now"
124
- self.block_size = kv_cache_config.kv_cache_groups[
125
- 0
126
- ].kv_cache_spec.block_size
127
-
128
- if dcp_world_size * pcp_world_size > 1:
129
- assert len(kv_cache_config.kv_cache_groups) == 1
130
- self.block_size *= dcp_world_size * pcp_world_size
131
-
116
self.coordinator = get_kv_cache_coordinator(
117
kv_cache_config=kv_cache_config,
118
max_model_len=self.max_model_len,
@@ -137,6 +121,7 @@ class KVCacheManager:
121
enable_kv_cache_events=enable_kv_cache_events,
122
dcp_world_size=dcp_world_size,
123
pcp_world_size=pcp_world_size,
124
+ hash_block_size=hash_block_size,
125
)
126
self.num_kv_cache_groups = len(kv_cache_config.kv_cache_groups)
127
self.block_pool = self.coordinator.block_pool
vllm/v1/core/kv_cache_utils.py
+136
-20
@@ -5,9 +5,9 @@
5
import copy
6
import os
7
from collections import defaultdict
8
-from collections.abc import Callable, Iterable, Sequence
9
-from dataclasses import dataclass
10
-from typing import Any, NewType, TypeAlias
8
+from collections.abc import Callable, Iterable, Iterator, Sequence
9
+from dataclasses import dataclass, replace
10
+from typing import Any, NewType, TypeAlias, overload
11
12
from vllm import envs
13
from vllm.config import VllmConfig
@@ -825,11 +825,11 @@ def get_num_blocks(
825
return num_blocks
826
827
828
-def get_uniform_page_size(kv_cache_spec: dict[str, KVCacheSpec]) -> int:
828
+def get_uniform_page_size(kv_cache_specs: Iterable[KVCacheSpec]) -> int:
829
"""
830
Get the page size of the KV cache.
831
"""
832
- page_sizes = set(layer.page_size_bytes for layer in kv_cache_spec.values())
832
+ page_sizes = {layer.page_size_bytes for layer in kv_cache_specs}
833
assert len(page_sizes) == 1
834
return page_sizes.pop()
835
@@ -882,6 +882,46 @@ def is_kv_cache_page_size_uniform(kv_cache_spec: dict[str, KVCacheSpec]) -> bool
882
return len(page_sizes) == 1
883
884
885
+def unify_kv_cache_spec_page_size(
886
+ kv_cache_spec: dict[str, KVCacheSpec],
887
+) -> dict[str, KVCacheSpec]:
888
+ """
889
+ Unify the page size of the given KVCacheSpec. If the page size of all layers
890
+ are the same, return the original KVCacheSpec. If not same, unify the page
891
+ size by increasing the block size of layers with smaller page size. Raise
892
+ NotImplementedError if failed to unify the page size.
893
+
894
+ Args:
895
+ kv_cache_spec: The KVCacheSpec of each attention layer in the model
896
+
897
+ Returns:
898
+ The updated KVCacheSpec with the same page_size_bytes.
899
+ """
900
+ page_sizes = {layer.page_size_bytes for layer in kv_cache_spec.values()}
901
+ if len(page_sizes) <= 1:
902
+ # All layers have the same page size, no need to unify.
903
+ return kv_cache_spec
904
+
905
+ max_page_size = max(page_sizes)
906
+ new_kv_cache_spec = {}
907
+ for layer_name, layer_spec in kv_cache_spec.items():
908
+ if layer_spec.page_size_bytes == max_page_size:
909
+ new_kv_cache_spec[layer_name] = layer_spec
910
+ else:
911
+ layer_page_size = layer_spec.page_size_bytes
912
+ if max_page_size % layer_page_size != 0:
913
+ raise NotImplementedError(
914
+ "The page size of the layer is not divisible by the "
915
+ "maximum page size. Cannot unify by adjusting block_size."
916
+ )
917
+ ratio = max_page_size // layer_page_size
918
+ new_block_size = layer_spec.block_size * ratio
919
+ new_spec = replace(layer_spec, block_size=new_block_size)
920
+ assert new_spec.page_size_bytes == max_page_size
921
+ new_kv_cache_spec[layer_name] = new_spec
922
+ return new_kv_cache_spec
923
+
924
+
925
def is_kv_cache_type_attention_free(kv_cache_spec: dict[str, KVCacheSpec]) -> bool:
926
# kv_cache_spec is an empty dict for attention free models
927
return not kv_cache_spec
@@ -1010,7 +1050,6 @@ def _get_kv_cache_groups_uniform_page_size(
1050
def get_kv_cache_config_from_groups(
1051
vllm_config: VllmConfig,
1052
kv_cache_groups: list[KVCacheGroupSpec],
1013
- kv_cache_specs: dict[str, KVCacheSpec],
1053
available_memory: int,
1054
) -> KVCacheConfig:
1055
"""
@@ -1020,7 +1059,6 @@ def get_kv_cache_config_from_groups(
1059
Args:
1060
vllm_config: The global VllmConfig
1061
kv_cache_groups: The KV cache groups
1023
- kv_cache_specs: The KV cache spec of each attention layer in the model
1062
available_memory: Memory available for KV cache in bytes
1063
Returns:
1064
The generated KVCacheConfig
@@ -1064,7 +1102,9 @@ def get_kv_cache_config_from_groups(
1102
# full.1, sw.2: share another Tensor with size=available_memory//2
1103
group_size = max(len(group.layer_names) for group in kv_cache_groups)
1104
1067
- page_size = get_uniform_page_size(kv_cache_specs)
1105
+ page_size = get_uniform_page_size(
1106
+ [group.kv_cache_spec for group in kv_cache_groups]
1107
+ )
1108
assert group_size > 0, "group_size must be greater than 0"
1109
num_blocks = get_num_blocks(
1110
vllm_config, group_size, available_memory, page_size
@@ -1166,7 +1206,8 @@ def get_kv_cache_groups(
1206
# This returns an empty list to allow for the KVCacheManager to handle
1207
# attention free models.
1208
return []
1169
- elif is_kv_cache_spec_uniform(kv_cache_spec):
1209
+
1210
+ if is_kv_cache_spec_uniform(kv_cache_spec):
1211
# KV cache of all layers are the same, which is true for
1212
# most models. Allocate the same amount of memory for
1213
# each layer.
@@ -1176,14 +1217,16 @@ def get_kv_cache_groups(
1217
# full attention, or all layers are sliding window attention with the
1218
# same window size). Put all layers into one group.
1219
return _get_kv_cache_groups_uniform_type(uniform_spec)
1179
- elif is_kv_cache_page_size_uniform(kv_cache_spec):
1180
- # Model contains multiple attention types, but KV cache of all layers
1181
- # have the same physical memory per block per layer. Split the layers
1182
- # into groups with the same number of layers, and thus same total page
1183
- # size.
1184
- return _get_kv_cache_groups_uniform_page_size(kv_cache_spec)
1220
1186
- raise NotImplementedError
1221
+ # As KVCacheManager can only allocate memory of one size, we need to unify
1222
+ # the page size of the layers. For cases cannot be unified, this function
1223
+ # will raise an error.
1224
+ kv_cache_spec = unify_kv_cache_spec_page_size(kv_cache_spec)
1225
+ # Model contains multiple attention types, but KV cache of all layers
1226
+ # have the same physical memory per block per layer. Split the layers
1227
+ # into groups with the same number of layers, and thus same total page
1228
+ # size.
1229
+ return _get_kv_cache_groups_uniform_page_size(kv_cache_spec)
1230
1231
1232
def generate_scheduler_kv_cache_config(
@@ -1327,10 +1370,7 @@ def get_kv_cache_configs(
1370
) == len(kv_cache_spec_one_worker), "Some layers are not assigned to any group."
1371
kv_cache_configs.append(
1372
get_kv_cache_config_from_groups(
1330
- vllm_config,
1331
- kv_cache_groups_one_worker,
1332
- kv_cache_spec_one_worker,
1333
- available_memory_one_worker,
1373
+ vllm_config, kv_cache_groups_one_worker, available_memory_one_worker
1374
)
1375
)
1376
@@ -1353,3 +1393,79 @@ def get_kv_cache_configs(
1393
_report_kv_cache_config(vllm_config, kv_cache_config)
1394
1395
return kv_cache_configs
1396
+
1397
+
1398
+class BlockHashListWithBlockSize:
1399
+ """
1400
+ Convert block-hash granularity from `hash_block_size` to `target_block_size`.
1401
+ Used when KV cache groups have different block sizes: `hash_block_size`
1402
+ is the size used to compute the original `block_hashes`; `target_block_size`
1403
+ is the group's actual block size.
1404
+
1405
+ Currently, only scaling up by an integer factor is supported (i.e.,
1406
+ `target_block_size` is a multiple of `hash_block_size`). Conversion is
1407
+ performed lazily on access for efficiency, by concatenating consecutive
1408
+ hashes at `hash_block_size` to form each hash at `target_block_size`.
1409
+
1410
+ Example (`hash_block_size` = 16, `target_block_size` = 32):
1411
+ concatenating two 16-size hashes yields one 32-size hash:
1412
+
1413
+ Block hashes with block_size 16:
1414
+ | Token Range | 0-15 | 16-31 | 32-47 | 48-63 |
1415
+ |-------------|------|-------|-------|-------|
1416
+ | Hash | A | B | C | D |
1417
+
1418
+ Block hashes with block_size 32:
1419
+ | Token Range | 0-31 | 32-63 |
1420
+ |-------------|------|-------|
1421
+ | Hash | AB | CD |
1422
+
1423
+ Args:
1424
+ block_hashes: Block hashes to convert, computed at `hash_block_size`.
1425
+ hash_block_size: Block size at which `block_hashes` were computed.
1426
+ target_block_size: Desired block size; must be a multiple of `hash_block_size`.
1427
+ """
1428
+
1429
+ def __init__(
1430
+ self,
1431
+ block_hashes: list[BlockHash],
1432
+ hash_block_size: int,
1433
+ target_block_size: int,
1434
+ ):
1435
+ self.block_hashes = block_hashes
1436
+ assert target_block_size % hash_block_size == 0
1437
+ self.scale_factor = target_block_size // hash_block_size
1438
+
1439
+ def __len__(self) -> int:
1440
+ return len(self.block_hashes) // self.scale_factor
1441
+
1442
+ @overload
1443
+ def __getitem__(self, idx: int) -> BlockHash: ...
1444
+
1445
+ @overload
1446
+ def __getitem__(self, idx: slice) -> list[BlockHash]: ...
1447
+
1448
+ def __getitem__(self, idx):
1449
+ if isinstance(idx, int):
1450
+ return self._get_value_at(idx)
1451
+
1452
+ if isinstance(idx, slice):
1453
+ start, stop, step = idx.indices(len(self))
1454
+ return [self._get_value_at(i) for i in range(start, stop, step)]
1455
+
1456
+ raise TypeError(f"Invalid index type: {type(idx)!r}")
1457
+
1458
+ def __iter__(self) -> Iterator[BlockHash]:
1459
+ for i in range(len(self)):
1460
+ yield self._get_value_at(i)
1461
+
1462
+ def _get_value_at(self, idx: int) -> BlockHash:
1463
+ base = idx * self.scale_factor
1464
+ end = base + self.scale_factor
1465
+ merged_hash: bytes = self.block_hashes[base]
1466
+ for i in range(base + 1, end):
1467
+ merged_hash += self.block_hashes[i]
1468
+ return BlockHash(merged_hash)
1469
+
1470
+
1471
+BlockHashList = list[BlockHash] | BlockHashListWithBlockSize
vllm/v1/core/sched/scheduler.py
+1
@@ -186,6 +186,7 @@ class Scheduler(SchedulerInterface):
186
enable_kv_cache_events=self.enable_kv_cache_events,
187
dcp_world_size=self.dcp_world_size,
188
pcp_world_size=self.pcp_world_size,
189
+ hash_block_size=self.block_size,
190
)
191
self.use_pp = self.parallel_config.pipeline_parallel_size > 1
192
self.use_v2_model_runner = envs.VLLM_USE_V2_MODEL_RUNNER
vllm/v1/core/single_type_kv_cache_manager.py
+63
-9
@@ -7,7 +7,7 @@ from collections.abc import Sequence
7
8
from vllm.utils.math_utils import cdiv
9
from vllm.v1.core.block_pool import BlockPool
10
-from vllm.v1.core.kv_cache_utils import BlockHash, KVCacheBlock
10
+from vllm.v1.core.kv_cache_utils import BlockHashList, KVCacheBlock
11
from vllm.v1.kv_cache_interface import (
12
ChunkedLocalAttentionSpec,
13
CrossAttentionSpec,
@@ -207,12 +207,13 @@ class SingleTypeKVCacheManager(ABC):
207
@abstractmethod
208
def find_longest_cache_hit(
209
cls,
210
- block_hashes: list[BlockHash],
210
+ block_hashes: BlockHashList,
211
max_length: int,
212
kv_cache_group_ids: list[int],
213
block_pool: BlockPool,
214
kv_cache_spec: KVCacheSpec,
215
use_eagle: bool,
216
+ alignment_tokens: int,
217
dcp_world_size: int = 1,
218
pcp_world_size: int = 1,
219
) -> tuple[list[KVCacheBlock], ...]:
@@ -232,6 +233,11 @@ class SingleTypeKVCacheManager(ABC):
233
block_pool: The block pool.
234
kv_cache_spec: The kv cache spec.
235
use_eagle: Whether to use eagle.
236
+ alignment_tokens: The returned cache hit length (in tokens) should
237
+ be a multiple of this value (in tokens). By default, it should
238
+ be set to the block_size.
239
+ dcp_world_size: The world size of decode context parallelism.
240
+ pcp_world_size: The world size of prefill context parallelism.
241
242
Returns:
243
A list of cached blocks with skipped blocks replaced by null block
@@ -299,17 +305,18 @@ class FullAttentionManager(SingleTypeKVCacheManager):
305
@classmethod
306
def find_longest_cache_hit(
307
cls,
302
- block_hashes: list[BlockHash],
308
+ block_hashes: BlockHashList,
309
max_length: int,
310
kv_cache_group_ids: list[int],
311
block_pool: BlockPool,
312
kv_cache_spec: KVCacheSpec,
313
use_eagle: bool,
314
+ alignment_tokens: int,
315
dcp_world_size: int = 1,
316
pcp_world_size: int = 1,
317
) -> tuple[list[KVCacheBlock], ...]:
318
assert isinstance(
312
- kv_cache_spec, (FullAttentionSpec, ChunkedLocalAttentionSpec)
319
+ kv_cache_spec, FullAttentionSpec | ChunkedLocalAttentionSpec
320
), (
321
"FullAttentionManager can only be used for full attention "
322
"and chunked local attention groups"
@@ -333,6 +340,13 @@ class FullAttentionManager(SingleTypeKVCacheManager):
340
else:
341
break
342
if use_eagle and computed_blocks[0]:
343
+ # Need to drop the last matched block if eagle is enabled.
344
+ for computed in computed_blocks:
345
+ computed.pop()
346
+ while (
347
+ block_size != alignment_tokens # Faster for common case.
348
+ and len(computed_blocks[0]) * block_size % alignment_tokens != 0
349
+ ):
350
for computed in computed_blocks:
351
computed.pop()
352
return computed_blocks
@@ -359,12 +373,13 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
373
@classmethod
374
def find_longest_cache_hit(
375
cls,
362
- block_hashes: list[BlockHash],
376
+ block_hashes: BlockHashList,
377
max_length: int,
378
kv_cache_group_ids: list[int],
379
block_pool: BlockPool,
380
kv_cache_spec: KVCacheSpec,
381
use_eagle: bool,
382
+ alignment_tokens: int,
383
dcp_world_size: int = 1,
384
pcp_world_size: int = 1,
385
) -> tuple[list[KVCacheBlock], ...]:
@@ -396,6 +411,7 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
411
[block_pool.null_block] * max_num_blocks
412
for _ in range(len(kv_cache_group_ids))
413
)
414
+ block_size = kv_cache_spec.block_size
415
num_contiguous_blocks = 0
416
match_found = False
417
# Search from right to left and early stop when a match is found.
@@ -403,6 +419,15 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
419
if cached_block := block_pool.get_cached_block(
420
block_hashes[i], kv_cache_group_ids
421
):
422
+ # Skip prefix matching check if the block is not aligned with
423
+ # `alignment_tokens`.
424
+ if (
425
+ num_contiguous_blocks == 0
426
+ and block_size != alignment_tokens # Faster for common case.
427
+ and (i + 1) * block_size % alignment_tokens != 0
428
+ ):
429
+ continue
430
+ # Add the cached block to the computed blocks.
431
for computed, cached in zip(computed_blocks, cached_block):
432
computed[i] = cached
433
num_contiguous_blocks += 1
@@ -421,7 +446,16 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
446
# `num_contiguous_blocks < sliding_window_contiguous_blocks`.
447
for computed in computed_blocks:
448
del computed[num_contiguous_blocks:]
449
+ while (
450
+ block_size != alignment_tokens # Faster for common case.
451
+ and len(computed_blocks[0]) * block_size % alignment_tokens != 0
452
+ ):
453
+ for computed in computed_blocks:
454
+ computed.pop()
455
if use_eagle and computed_blocks[0]:
456
+ assert kv_cache_spec.block_size == alignment_tokens, (
457
+ "aligned_length is not compatible with eagle now"
458
+ )
459
for computed in computed_blocks:
460
computed.pop()
461
return computed_blocks
@@ -475,12 +509,13 @@ class ChunkedLocalAttentionManager(SingleTypeKVCacheManager):
509
@classmethod
510
def find_longest_cache_hit(
511
cls,
478
- block_hashes: list[BlockHash],
512
+ block_hashes: BlockHashList,
513
max_length: int,
514
kv_cache_group_ids: list[int],
515
block_pool: BlockPool,
516
kv_cache_spec: KVCacheSpec,
517
use_eagle: bool,
518
+ alignment_tokens: int,
519
dcp_world_size: int = 1,
520
pcp_world_size: int = 1,
521
) -> tuple[list[KVCacheBlock], ...]:
@@ -511,6 +546,10 @@ class ChunkedLocalAttentionManager(SingleTypeKVCacheManager):
546
block_pool: The block pool.
547
kv_cache_spec: The kv cache spec.
548
use_eagle: Whether to use eagle.
549
+ dcp_world_size: The world size of decode context parallelism.
550
+ pcp_world_size: The world size of prefill context parallelism.
551
+ alignment_tokens: The returned cache hit length (in tokens) should
552
+ be a multiple of this value (in tokens).
553
554
Returns:
555
A list of cached blocks
@@ -524,6 +563,10 @@ class ChunkedLocalAttentionManager(SingleTypeKVCacheManager):
563
)
564
assert dcp_world_size == 1, "DCP not support chunked local attn now."
565
assert pcp_world_size == 1, "PCP not support chunked local attn now."
566
+ assert kv_cache_spec.block_size == alignment_tokens, (
567
+ "KV cache groups with different block sizes are not compatible with "
568
+ "chunked local attention now"
569
+ )
570
max_num_blocks = max_length // kv_cache_spec.block_size
571
if max_length > 0:
572
local_attention_start_idx = (
@@ -612,12 +655,13 @@ class MambaManager(SingleTypeKVCacheManager):
655
@classmethod
656
def find_longest_cache_hit(
657
cls,
615
- block_hashes: list[BlockHash],
658
+ block_hashes: BlockHashList,
659
max_length: int,
660
kv_cache_group_ids: list[int],
661
block_pool: BlockPool,
662
kv_cache_spec: KVCacheSpec,
663
use_eagle: bool,
664
+ alignment_tokens: int,
665
dcp_world_size: int = 1,
666
pcp_world_size: int = 1,
667
) -> tuple[list[KVCacheBlock], ...]:
@@ -630,12 +674,21 @@ class MambaManager(SingleTypeKVCacheManager):
674
[] for _ in range(len(kv_cache_group_ids))
675
)
676
633
- max_num_blocks = max_length // kv_cache_spec.block_size
677
+ block_size = kv_cache_spec.block_size
678
+ max_num_blocks = max_length // block_size
679
# Search from right to left and early stop when a match is found.
680
for i in range(max_num_blocks - 1, -1, -1):
681
if cached_block := block_pool.get_cached_block(
682
block_hashes[i], kv_cache_group_ids
683
):
684
+ # When enable Mamba prefix caching, `block_size` will be aligned
685
+ # across full attention layers and Mamba layers to ensure the
686
+ # prefix hit length aligned at block
687
+ if (
688
+ block_size != alignment_tokens # Faster for common case.
689
+ and (i + 1) * block_size % alignment_tokens != 0
690
+ ):
691
+ continue
692
for computed, cached in zip(computed_blocks, cached_block):
693
# the hit length logic later assumes:
694
# hit_length = len(hit_blocks_other_attn[0])
@@ -708,12 +761,13 @@ class CrossAttentionManager(SingleTypeKVCacheManager):
761
@classmethod
762
def find_longest_cache_hit(
763
cls,
711
- block_hashes: list[BlockHash],
764
+ block_hashes: BlockHashList,
765
max_length: int,
766
kv_cache_group_ids: list[int],
767
block_pool: BlockPool,
768
kv_cache_spec: KVCacheSpec,
769
use_eagle: bool,
770
+ alignment_tokens: int,
771
dcp_world_size: int = 1,
772
pcp_world_size: int = 1,
773
) -> tuple[list[KVCacheBlock], ...]: