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 2, 03:30 UTC
[Bugfix] Cache the EAGLE/MTP lookahead block in the SWA prefix-cache mask (#44082)
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
Yifan Qiao committed
Jun 2, 2026 at 12:21 UTC
e9e08c49b966a7d36bd37745590f658e76a2082d
5 files changed
+338
-82
tests/v1/core/test_prefix_caching.py
+196
-1
@@ -2466,6 +2466,201 @@ def test_eagle_with_sliding_window():
2466
assert num_tokens == 0
2467
2468
2469
+def test_eagle_swa_alignment_caches_extra_block():
2470
+ """Regression: SWA + EAGLE with `sliding_window <= alignment_tokens`.
2471
+
2472
+ When the cache-hit alignment (lcm of per-group block sizes) is larger than
2473
+ the SWA window, the SWA mask only kept the last block of each aligned
2474
+ segment. EAGLE/MTP lookup needs ``tail + 1`` contiguous cached blocks and
2475
+ that +1 block lives at the next segment's first position, which was left
2476
+ uncached. The fix caches that extra block when ``use_eagle=True``.
2477
+ """
2478
+ block_size = 8
2479
+ # Full group uses 4 * block_size, so lcm/alignment is 4 * block_size.
2480
+ # SWA group has sliding_window = block_size (i.e., tail = 1 block).
2481
+ # Without the fix, the second cached block needed for the EAGLE 2-block
2482
+ # match never exists -> EAGLE cache hit fails entirely.
2483
+ kv_cache_config = KVCacheConfig(
2484
+ num_blocks=100,
2485
+ kv_cache_tensors=[],
2486
+ kv_cache_groups=[
2487
+ KVCacheGroupSpec(
2488
+ ["full"],
2489
+ FullAttentionSpec(
2490
+ block_size=4 * block_size,
2491
+ num_kv_heads=1,
2492
+ head_size=1,
2493
+ dtype=torch.float16,
2494
+ ),
2495
+ ),
2496
+ KVCacheGroupSpec(
2497
+ ["swa_mtp"],
2498
+ SlidingWindowSpec(
2499
+ block_size=block_size,
2500
+ num_kv_heads=1,
2501
+ head_size=1,
2502
+ dtype=torch.float32,
2503
+ sliding_window=block_size,
2504
+ ),
2505
+ is_eagle_group=True,
2506
+ ),
2507
+ ],
2508
+ )
2509
+ manager = make_kv_cache_manager(
2510
+ kv_cache_config=kv_cache_config,
2511
+ max_model_len=8192,
2512
+ enable_caching=True,
2513
+ hash_block_size=block_size,
2514
+ use_eagle=True,
2515
+ )
2516
+
2517
+ # Prime the cache with a long prompt (16 swa blocks = 4 aligned segments).
2518
+ token_ids = [i for i in range(16) for _ in range(block_size)]
2519
+ req0 = make_request("0", token_ids, block_size, sha256)
2520
+ computed_blocks, _ = manager.get_computed_blocks(req0)
2521
+ blocks = manager.allocate_slots(
2522
+ req0,
2523
+ len(token_ids),
2524
+ len(computed_blocks.blocks[0]) * block_size,
2525
+ computed_blocks,
2526
+ )
2527
+ assert blocks is not None
2528
+ manager.free(req0)
2529
+
2530
+ # Second request with identical prompt should find an EAGLE cache hit.
2531
+ # Without the fix, ``num_computed_tokens`` is 0; with the fix, it lands at
2532
+ # an alignment boundary (multiple of 32 tokens, minus the EAGLE drop).
2533
+ req1 = make_request("1", token_ids, block_size, sha256)
2534
+ _, num_computed_tokens = manager.get_computed_blocks(req1)
2535
+ assert num_computed_tokens > 0, (
2536
+ "EAGLE + SWA with sliding_window <= alignment failed to find any "
2537
+ "cache hit; the +1 block past each segment boundary must be cached."
2538
+ )
2539
+ # Each aligned segment contributes 4 * block_size = 32 tokens; EAGLE drops
2540
+ # the last block (block_size tokens) from the hit.
2541
+ assert num_computed_tokens % (4 * block_size) == 0
2542
+
2543
+
2544
+def test_eagle_swa_boundary_caches_post_boundary_block():
2545
+ """EAGLE + SWA must cache the first block after an alignment boundary.
2546
+
2547
+ A 40-token computed prefix with 8-token SWA blocks and 32-token hybrid
2548
+ alignment needs SWA blocks 3 and 4 cached to reuse a 32-token prefix:
2549
+ block 3 is the segment tail, and block 4 is the EAGLE lookahead block
2550
+ that gets dropped after lookup.
2551
+ """
2552
+ block_size = 8
2553
+ kv_cache_config = KVCacheConfig(
2554
+ num_blocks=100,
2555
+ kv_cache_tensors=[],
2556
+ kv_cache_groups=[
2557
+ KVCacheGroupSpec(
2558
+ ["full"],
2559
+ FullAttentionSpec(
2560
+ block_size=4 * block_size,
2561
+ num_kv_heads=1,
2562
+ head_size=1,
2563
+ dtype=torch.float16,
2564
+ ),
2565
+ ),
2566
+ KVCacheGroupSpec(
2567
+ ["swa_mtp"],
2568
+ SlidingWindowSpec(
2569
+ block_size=block_size,
2570
+ num_kv_heads=1,
2571
+ head_size=1,
2572
+ dtype=torch.float32,
2573
+ sliding_window=block_size,
2574
+ ),
2575
+ is_eagle_group=True,
2576
+ ),
2577
+ ],
2578
+ )
2579
+ manager = make_kv_cache_manager(
2580
+ kv_cache_config=kv_cache_config,
2581
+ max_model_len=8192,
2582
+ enable_caching=True,
2583
+ hash_block_size=block_size,
2584
+ use_eagle=True,
2585
+ )
2586
+
2587
+ token_ids = [i for i in range(5) for _ in range(block_size)]
2588
+ req0 = make_request("0", token_ids, block_size, sha256)
2589
+ computed_blocks, _ = manager.get_computed_blocks(req0)
2590
+ blocks = manager.allocate_slots(
2591
+ req0,
2592
+ len(token_ids),
2593
+ len(computed_blocks.blocks[0]) * block_size,
2594
+ computed_blocks,
2595
+ )
2596
+ assert blocks is not None
2597
+
2598
+ pool = manager.block_pool
2599
+ assert pool.get_cached_block(req0.block_hashes[3], kv_cache_group_ids=[1])
2600
+ assert pool.get_cached_block(req0.block_hashes[4], kv_cache_group_ids=[1])
2601
+ manager.free(req0)
2602
+
2603
+ req1 = make_request("1", token_ids + [999], block_size, sha256)
2604
+ _, num_computed_tokens = manager.get_computed_blocks(req1)
2605
+ assert num_computed_tokens == 4 * block_size
2606
+
2607
+
2608
+def test_eagle_grouped_swa_siblings_use_same_cache_mask():
2609
+ """Grouped SWA siblings must cache the EAGLE lookahead block together."""
2610
+ block_size = 8
2611
+ swa_spec = SlidingWindowSpec(
2612
+ block_size=block_size,
2613
+ num_kv_heads=1,
2614
+ head_size=1,
2615
+ dtype=torch.float32,
2616
+ sliding_window=block_size,
2617
+ )
2618
+ kv_cache_config = KVCacheConfig(
2619
+ num_blocks=100,
2620
+ kv_cache_tensors=[],
2621
+ kv_cache_groups=[
2622
+ KVCacheGroupSpec(
2623
+ ["full"],
2624
+ FullAttentionSpec(
2625
+ block_size=4 * block_size,
2626
+ num_kv_heads=1,
2627
+ head_size=1,
2628
+ dtype=torch.float16,
2629
+ ),
2630
+ ),
2631
+ KVCacheGroupSpec(["swa_main"], swa_spec),
2632
+ KVCacheGroupSpec(["swa_mtp"], swa_spec, is_eagle_group=True),
2633
+ ],
2634
+ )
2635
+ manager = make_kv_cache_manager(
2636
+ kv_cache_config=kv_cache_config,
2637
+ max_model_len=8192,
2638
+ enable_caching=True,
2639
+ hash_block_size=block_size,
2640
+ use_eagle=True,
2641
+ )
2642
+
2643
+ token_ids = [i for i in range(9) for _ in range(block_size)]
2644
+ req0 = make_request("0", token_ids, block_size, sha256)
2645
+ computed_blocks, _ = manager.get_computed_blocks(req0)
2646
+ blocks = manager.allocate_slots(
2647
+ req0,
2648
+ len(token_ids),
2649
+ len(computed_blocks.blocks[0]) * block_size,
2650
+ computed_blocks,
2651
+ )
2652
+ assert blocks is not None
2653
+
2654
+ pool = manager.block_pool
2655
+ assert pool.get_cached_block(req0.block_hashes[4], kv_cache_group_ids=[1, 2])
2656
+ assert pool.get_cached_block(req0.block_hashes[8], kv_cache_group_ids=[1, 2])
2657
+ manager.free(req0)
2658
+
2659
+ req1 = make_request("1", token_ids + [999], block_size, sha256)
2660
+ _, num_computed_tokens = manager.get_computed_blocks(req1)
2661
+ assert num_computed_tokens == 8 * block_size
2662
+
2663
+
2664
def test_different_block_size():
2665
block_size = 16
2666
# full attention and sliding window attention layers have the same page size:
@@ -2614,7 +2809,7 @@ def test_hybrid_cache_blocks_swa_tail_window_only():
2809
2810
2811
def test_hybrid_cache_blocks_clamped_to_lcm():
2617
- """HybridKVCacheCoordinator.cache_blocks() clamps to lcm_block_size.
2812
+ """HybridKVCacheCoordinator.cache_blocks() clamps to scheduler_block_size.
2813
Chunks past the last lcm-aligned boundary can never participate in a
2814
cache hit (find_longest_cache_hit always returns lcm-aligned hits), so
2815
caching them only pollutes the prefix-cache hash map and keeps blocks
tests/v1/core/test_single_type_kv_cache_manager.py
+2
-2
@@ -86,7 +86,7 @@ def test_chunked_local_attention_possible_cached_prefix():
86
kv_cache_group_ids=[0],
87
block_pool=block_pool,
88
kv_cache_spec=chunked_local_attention_spec,
89
- use_eagle=False,
89
+ drop_eagle_block=False,
90
alignment_tokens=block_size,
91
)[0]
92
assert len(computed_blocks) == expect_length
@@ -157,7 +157,7 @@ def test_sliding_window_possible_cached_prefix():
157
kv_cache_group_ids=[0],
158
block_pool=block_pool,
159
kv_cache_spec=sliding_window_spec,
160
- use_eagle=False,
160
+ drop_eagle_block=False,
161
alignment_tokens=block_size,
162
)[0]
163
assert len(computed_blocks) == expect_length
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/coordinator.py
+5
-5
@@ -233,7 +233,7 @@ class MooncakeStoreCoordinator:
233
kv_cache_group_ids=group_ids,
234
block_pool=cast(BlockPool, cached_block_pool),
235
kv_cache_spec=spec,
236
- use_eagle=(0 in eagle_indices),
236
+ drop_eagle_block=(0 in eagle_indices),
237
alignment_tokens=spec.block_size,
238
)
239
num_groups = len(self.kv_cache_groups)
@@ -262,9 +262,9 @@ class MooncakeStoreCoordinator:
262
)
263
continue
264
265
- use_eagle = idx in eagle_indices and idx not in eagle_verified
265
+ drop_eagle_block = idx in eagle_indices and idx not in eagle_verified
266
_max_length = curr_hit_length
267
- if use_eagle:
267
+ if drop_eagle_block:
268
_max_length = min(curr_hit_length + spec.block_size, max_length)
269
hashes = self.block_hashes_for_spec(block_hashes, spec)
270
hit_blocks = manager_cls.find_longest_cache_hit(
@@ -273,11 +273,11 @@ class MooncakeStoreCoordinator:
273
kv_cache_group_ids=group_ids,
274
block_pool=cast(BlockPool, cached_block_pool),
275
kv_cache_spec=spec,
276
- use_eagle=use_eagle,
276
+ drop_eagle_block=drop_eagle_block,
277
alignment_tokens=self.lcm_block_size,
278
)
279
_new_hit_length = len(hit_blocks[0]) * spec.block_size
280
- if use_eagle:
280
+ if drop_eagle_block:
281
eagle_verified.add(idx)
282
elif _new_hit_length < curr_hit_length:
283
eagle_verified.clear()
vllm/v1/core/kv_cache_coordinator.py
+61
-38
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
from abc import ABC, abstractmethod
4
from collections.abc import Sequence
5
+from typing import NamedTuple
6
7
from vllm.v1.core.block_pool import BlockPool
8
from vllm.v1.core.kv_cache_metrics import KVCacheMetricsCollector
@@ -381,6 +382,8 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
382
assert len(self.kv_cache_config.kv_cache_groups) == 1, (
383
"UnitaryKVCacheCoordinator assumes only one kv cache group"
384
)
385
+ # Single group; useless but just set ``use_eagle`` for consistency regardless.
386
+ self.single_type_managers[0].use_eagle = 0 in self.eagle_group_ids
387
388
def find_longest_cache_hit(
389
self,
@@ -393,7 +396,7 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
396
kv_cache_group_ids=[0],
397
block_pool=self.block_pool,
398
kv_cache_spec=self.kv_cache_spec,
396
- use_eagle=0 in self.eagle_group_ids,
399
+ drop_eagle_block=0 in self.eagle_group_ids,
400
alignment_tokens=self.block_size,
401
dcp_world_size=self.dcp_world_size,
402
pcp_world_size=self.pcp_world_size,
@@ -401,6 +404,21 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
404
return hit_blocks, len(hit_blocks[0]) * self.block_size
405
406
407
+class SpecGroup(NamedTuple):
408
+ """KV cache groups that share one spec, batched together for a single
409
+ cache-hit lookup.
410
+
411
+ ``use_eagle`` is True iff any member group is an EAGLE/MTP group. Members
412
+ sharing a spec are cached and looked up jointly, so the EAGLE last-block drop
413
+ is necessarily decided for the whole spec group.
414
+ """
415
+
416
+ spec: KVCacheSpec
417
+ group_ids: list[int]
418
+ manager_cls: type[SingleTypeKVCacheManager]
419
+ use_eagle: bool
420
+
421
+
422
class HybridKVCacheCoordinator(KVCacheCoordinator):
423
"""
424
KV cache coordinator for hybrid models with multiple KV cache types, and
@@ -452,58 +470,63 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
470
Groups KV cache groups by their spec type for efficient batch processing
471
during cache hit lookup.
472
"""
455
- attention_groups: list[
456
- tuple[KVCacheSpec, list[int], type[SingleTypeKVCacheManager]]
457
- ] = []
458
-
473
+ self.attention_groups: list[SpecGroup] = []
474
for i, g in enumerate(self.kv_cache_config.kv_cache_groups):
475
manager_cls = self.single_type_managers[i].__class__
476
spec = g.kv_cache_spec
477
+ use_eagle = i in self.eagle_group_ids
478
479
# Try to find an existing group with the same spec
464
- for existing_spec, group_ids, existing_cls in attention_groups:
465
- if existing_spec == spec:
466
- assert manager_cls is existing_cls, (
480
+ for idx, group in enumerate(self.attention_groups):
481
+ if group.spec == spec:
482
+ assert manager_cls is group.manager_cls, (
483
"Expected same manager class for identical KV cache specs."
484
)
469
- group_ids.append(i)
485
+ group.group_ids.append(i)
486
+ if use_eagle and not group.use_eagle:
487
+ self.attention_groups[idx] = group._replace(use_eagle=True)
488
break
489
else:
472
- attention_groups.append((spec, [i], manager_cls))
490
+ self.attention_groups.append(
491
+ SpecGroup(spec, [i], manager_cls, use_eagle)
492
+ )
493
474
- assert len(attention_groups) > 1, (
494
+ assert len(self.attention_groups) > 1, (
495
"HybridKVCacheCoordinator requires at least two attention groups."
496
)
497
498
# Put full attention first: its efficient left-to-right scan provides
499
# a tighter initial bound, reducing work for subsequent groups.
480
- self.attention_groups = sorted(
481
- attention_groups,
482
- key=lambda x: not isinstance(x[0], FullAttentionSpec),
500
+ self.attention_groups.sort(
501
+ key=lambda g: not isinstance(g.spec, FullAttentionSpec)
502
)
503
485
- # Attention-group indices (into ``self.attention_groups``) that
486
- # contain at least one EAGLE/MTP KV cache group.
487
- self.eagle_attn_group_indices: set[int] = {
488
- i
489
- for i, (_, group_ids, _) in enumerate(self.attention_groups)
490
- if any(gid in self.eagle_group_ids for gid in group_ids)
491
- }
504
+ # Propagate the eagle bit to each manager (default to ``use_eagle=False``).
505
+ for group in self.attention_groups:
506
+ if group.use_eagle:
507
+ for gid in group.group_ids:
508
+ self.single_type_managers[gid].use_eagle = True
509
510
def cache_blocks(self, request: Request, num_computed_tokens: int) -> None:
511
# Cache hits in this coordinator are always a multiple of
512
# ``scheduler_block_size`` tokens (see ``find_longest_cache_hit``).
496
- # Within an aligned region, SWA groups only consult a subset of blocks
513
+ # Within an aligned region, SWA groups may only consult a subset of blocks
514
# per ``scheduler_block_size``-segment so the unused blocks also stay
515
# out of the prefix-cache hash map.
499
- num_computed_tokens = (
516
+ aligned_num_computed_tokens = (
517
num_computed_tokens // self.scheduler_block_size * self.scheduler_block_size
518
)
519
for manager in self.single_type_managers:
520
+ num_tokens_to_cache = aligned_num_computed_tokens
521
+ # EAGLE groups match one block past each aligned boundary and drop
522
+ # it, so make that lookahead block eligible to be cached.
523
+ if manager.use_eagle and aligned_num_computed_tokens > 0:
524
+ num_tokens_to_cache = min(
525
+ num_computed_tokens,
526
+ aligned_num_computed_tokens + manager.block_size,
527
+ )
528
manager.cache_blocks(
504
- request,
505
- num_computed_tokens,
506
- alignment_tokens=self.scheduler_block_size,
529
+ request, num_tokens_to_cache, alignment_tokens=self.scheduler_block_size
530
)
531
532
def find_longest_cache_hit(
@@ -543,7 +566,7 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
566
# Simple hybrid (1 full attn + 1 other): one iteration suffices.
567
# Full attn is always first if it exists.
568
is_simple_hybrid = len(self.attention_groups) == 2 and isinstance(
546
- self.attention_groups[0][0], FullAttentionSpec
569
+ self.attention_groups[0].spec, FullAttentionSpec
570
)
571
572
# Attention-group indices whose EAGLE drop is verified at the current
@@ -554,7 +577,9 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
577
while True:
578
curr_hit_length = hit_length
579
557
- for idx, (spec, group_ids, manager_cls) in enumerate(self.attention_groups):
580
+ for idx, (spec, group_ids, manager_cls, use_eagle) in enumerate(
581
+ self.attention_groups
582
+ ):
583
cached_blocks = hit_blocks_by_group[group_ids[0]]
584
if isinstance(spec, FullAttentionSpec) and cached_blocks is not None:
585
# Full attention is downward-closed: we only need to look
@@ -565,12 +590,10 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
590
)
591
continue
592
568
- use_eagle = (
569
- idx in self.eagle_attn_group_indices and idx not in eagle_verified
570
- )
593
+ drop_eagle_block = use_eagle and idx not in eagle_verified
594
595
_max_length = curr_hit_length
573
- if use_eagle:
596
+ if drop_eagle_block:
597
# Eagle needs to match one more block and then pop the last.
598
_max_length = min(
599
curr_hit_length + spec.block_size, max_cache_hit_length
@@ -581,11 +604,11 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
604
kv_cache_group_ids=group_ids,
605
block_pool=self.block_pool,
606
kv_cache_spec=spec,
584
- use_eagle=use_eagle,
607
+ drop_eagle_block=drop_eagle_block,
608
alignment_tokens=self.scheduler_block_size,
609
)
610
_new_hit_length = len(hit_blocks[0]) * spec.block_size
588
- if use_eagle:
611
+ if drop_eagle_block:
612
eagle_verified.add(idx)
613
elif _new_hit_length < curr_hit_length:
614
# length shrunk; invalidate previous eagle verifications
@@ -601,10 +624,10 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
624
break
625
626
# Truncate full attention blocks to final hit_length (if present)
604
- spec, group_ids, _ = self.attention_groups[0]
605
- if isinstance(spec, FullAttentionSpec):
606
- num_blocks = hit_length // spec.block_size
607
- for group_id in group_ids:
627
+ first_group = self.attention_groups[0]
628
+ if isinstance(first_group.spec, FullAttentionSpec):
629
+ num_blocks = hit_length // first_group.spec.block_size
630
+ for group_id in first_group.group_ids:
631
if (blks := hit_blocks_by_group[group_id]) is not None:
632
del blks[num_blocks:]
633
vllm/v1/core/single_type_kv_cache_manager.py
+74
-36
@@ -86,6 +86,12 @@ class SingleTypeKVCacheManager(ABC):
86
self.kv_cache_group_id = kv_cache_group_id
87
self._null_block = block_pool.null_block
88
89
+ # Whether this group's prefix-cache hits drop the EAGLE/MTP lookahead
90
+ # block. Only consulted by managers whose hit logic is sparse within an
91
+ # aligned segment (SWA). Initialized lazily by the coordinator after
92
+ # determining the attention groups.
93
+ self.use_eagle = False
94
+
95
@classmethod
96
def _get_num_evictable_blocks(cls, blocks: Sequence[KVCacheBlock]):
97
return sum(blk.ref_cnt == 0 and not blk.is_null for blk in blocks)
@@ -317,8 +323,12 @@ class SingleTypeKVCacheManager(ABC):
323
if alignment_tokens is None or alignment_tokens <= self.block_size:
324
block_mask = None
325
else:
320
- block_mask = self._cache_block_mask(
321
- num_cached_blocks, num_full_blocks, alignment_tokens
326
+ block_mask = self.reachable_block_mask(
327
+ num_cached_blocks,
328
+ num_full_blocks,
329
+ alignment_tokens,
330
+ self.kv_cache_spec,
331
+ self.use_eagle,
332
)
333
self.block_pool.cache_full_blocks(
334
request=request,
@@ -332,11 +342,14 @@ class SingleTypeKVCacheManager(ABC):
342
343
self.num_cached_block[request.request_id] = num_full_blocks
344
335
- def _cache_block_mask(
336
- self,
337
- num_cached_blocks: int,
338
- num_full_blocks: int,
345
+ @classmethod
346
+ def reachable_block_mask(
347
+ cls,
348
+ start_block: int,
349
+ num_blocks: int,
350
alignment_tokens: int,
351
+ kv_cache_spec: KVCacheSpec,
352
+ use_eagle: bool,
353
) -> list[bool] | None:
354
"""Per-block mask for ``cache_full_blocks``. ``None`` means cache
355
every (non-null) block — the default for full attention.
@@ -389,7 +402,7 @@ class SingleTypeKVCacheManager(ABC):
402
kv_cache_group_ids: list[int],
403
block_pool: BlockPool,
404
kv_cache_spec: KVCacheSpec,
392
- use_eagle: bool,
405
+ drop_eagle_block: bool,
406
alignment_tokens: int,
407
dcp_world_size: int = 1,
408
pcp_world_size: int = 1,
@@ -409,7 +422,10 @@ class SingleTypeKVCacheManager(ABC):
422
kv_cache_group_ids: The ids of the kv cache groups.
423
block_pool: The block pool.
424
kv_cache_spec: The kv cache spec.
412
- use_eagle: Whether to use eagle.
425
+ drop_eagle_block: Whether to drop the last matched block for EAGLE/MTP.
426
+ Always False for non-EAGLE/MTP groups, but can be False for EAGLE/MTP
427
+ groups too if the last block is already dropped (e.g., in a
428
+ convergence loop in `find_longest_cache_hit`).
429
alignment_tokens: The returned cache hit length (in tokens) should
430
be a multiple of this value (in tokens). By default, it should
431
be set to the block_size.
@@ -499,7 +515,7 @@ class FullAttentionManager(SingleTypeKVCacheManager):
515
kv_cache_group_ids: list[int],
516
block_pool: BlockPool,
517
kv_cache_spec: KVCacheSpec,
502
- use_eagle: bool,
518
+ drop_eagle_block: bool,
519
alignment_tokens: int,
520
dcp_world_size: int = 1,
521
pcp_world_size: int = 1,
@@ -528,7 +544,7 @@ class FullAttentionManager(SingleTypeKVCacheManager):
544
computed.append(cached)
545
else:
546
break
531
- if use_eagle and computed_blocks[0]:
547
+ if drop_eagle_block and computed_blocks[0]:
548
# Need to drop the last matched block if eagle is enabled.
549
for computed in computed_blocks:
550
computed.pop()
@@ -556,6 +572,19 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
572
super().__init__(kv_cache_spec, **kwargs)
573
self.sliding_window = kv_cache_spec.sliding_window
574
575
+ @classmethod
576
+ def _contiguous_blocks_for_hit(
577
+ cls, window_size: int, block_size: int, use_eagle: bool
578
+ ) -> int:
579
+ blocks = cdiv(window_size - 1, block_size)
580
+ if use_eagle:
581
+ # Need to drop the last matched block if eagle is enabled. For
582
+ # sliding window layer, we achieve this by increasing the number of
583
+ # contiguous blocks needed for prefix cache hit by one and dropping
584
+ # the last matched block.
585
+ blocks += 1
586
+ return blocks
587
+
588
@classmethod
589
def find_longest_cache_hit(
590
cls,
@@ -564,7 +593,7 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
593
kv_cache_group_ids: list[int],
594
block_pool: BlockPool,
595
kv_cache_spec: KVCacheSpec,
567
- use_eagle: bool,
596
+ drop_eagle_block: bool,
597
alignment_tokens: int,
598
dcp_world_size: int = 1,
599
pcp_world_size: int = 1,
@@ -575,17 +604,10 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
604
assert dcp_world_size == 1, "DCP not support sliding window attn now."
605
assert pcp_world_size == 1, "PCP not support sliding window attn now."
606
578
- # The number of contiguous blocks needed for prefix cache hit.
579
- # -1 since the input token itself is also included in the window
580
- sliding_window_contiguous_blocks = cdiv(
581
- kv_cache_spec.sliding_window - 1, kv_cache_spec.block_size
607
+ # The number of contiguous blocks needed for a prefix cache hit.
608
+ sliding_window_contiguous_blocks = cls._contiguous_blocks_for_hit(
609
+ kv_cache_spec.sliding_window, kv_cache_spec.block_size, drop_eagle_block
610
)
583
- if use_eagle:
584
- # Need to drop the last matched block if eagle is enabled. For
585
- # sliding window layer, we achieve this by increasing the number of
586
- # contiguous blocks needed for prefix cache hit by one and dropping
587
- # the last matched block.
588
- sliding_window_contiguous_blocks += 1
611
612
# TODO: reduce i by sliding_window_contiguous_blocks when cache miss, to
613
# optimize the time complexity from O(max_num_blocks) to
@@ -608,7 +630,7 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
630
# Skip prefix matching check if the block is not aligned with
631
# `alignment_tokens`.
632
if num_contiguous_blocks == 0 and block_size != alignment_tokens:
611
- post_pop_blocks = i if use_eagle else i + 1
633
+ post_pop_blocks = i if drop_eagle_block else i + 1
634
if (post_pop_blocks * block_size) % alignment_tokens != 0:
635
continue
636
# Add the cached block to the computed blocks.
@@ -636,7 +658,7 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
658
):
659
for computed in computed_blocks:
660
computed.pop()
639
- if use_eagle and computed_blocks[0]:
661
+ if drop_eagle_block and computed_blocks[0]:
662
for computed in computed_blocks:
663
computed.pop()
664
# Re-align after eagle pop: the pop may break the alignment
@@ -650,17 +672,33 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
672
computed.pop()
673
return computed_blocks
674
653
- def _cache_block_mask(
654
- self, num_cached_blocks: int, num_full_blocks: int, alignment_tokens: int
675
+ @classmethod
676
+ def reachable_block_mask(
677
+ cls,
678
+ start_block: int,
679
+ num_blocks: int,
680
+ alignment_tokens: int,
681
+ kv_cache_spec: KVCacheSpec,
682
+ use_eagle: bool,
683
) -> list[bool] | None:
656
- assert alignment_tokens > self.block_size
657
- per_segment = alignment_tokens // self.block_size
658
- tail = cdiv(self.sliding_window - 1, self.block_size)
659
- if tail >= per_segment:
684
+ assert alignment_tokens > kv_cache_spec.block_size
685
+ assert isinstance(kv_cache_spec, SlidingWindowSpec)
686
+ per_segment = alignment_tokens // kv_cache_spec.block_size
687
+ need = cls._contiguous_blocks_for_hit(
688
+ window_size=kv_cache_spec.sliding_window,
689
+ block_size=kv_cache_spec.block_size,
690
+ use_eagle=use_eagle,
691
+ )
692
+ if need >= per_segment:
693
return None
661
- skip = per_segment - tail
694
+ # The matched run's right edge sits on the aligned boundary block when
695
+ # EAGLE peeks one block past it (shift=1), otherwise on the last block
696
+ # before the boundary (shift=0). A block is reachable iff it falls in
697
+ # the ``need``-wide run ending at some boundary's right edge.
698
+ shift = 1 if use_eagle else 0
699
return [
663
- i % per_segment >= skip for i in range(num_cached_blocks, num_full_blocks)
700
+ i >= shift and (i - shift) % per_segment >= per_segment - need
701
+ for i in range(start_block, num_blocks)
702
]
703
704
def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
@@ -714,7 +752,7 @@ class ChunkedLocalAttentionManager(SingleTypeKVCacheManager):
752
kv_cache_group_ids: list[int],
753
block_pool: BlockPool,
754
kv_cache_spec: KVCacheSpec,
717
- use_eagle: bool,
755
+ drop_eagle_block: bool,
756
alignment_tokens: int,
757
dcp_world_size: int = 1,
758
pcp_world_size: int = 1,
@@ -745,7 +783,7 @@ class ChunkedLocalAttentionManager(SingleTypeKVCacheManager):
783
kv_cache_group_ids: The ids of the kv cache groups.
784
block_pool: The block pool.
785
kv_cache_spec: The kv cache spec.
748
- use_eagle: Whether to use eagle.
786
+ drop_eagle_block: Whether to drop the last matched block for EAGLE/MTP.
787
dcp_world_size: The world size of decode context parallelism.
788
pcp_world_size: The world size of prefill context parallelism.
789
alignment_tokens: The returned cache hit length (in tokens) should
@@ -758,7 +796,7 @@ class ChunkedLocalAttentionManager(SingleTypeKVCacheManager):
796
"ChunkedLocalAttentionManager can only be used for "
797
"chunked local attention groups"
798
)
761
- assert use_eagle is False, (
799
+ assert drop_eagle_block is False, (
800
"Hybrid KV cache is not supported for " + "eagle + chunked local attention."
801
)
802
assert dcp_world_size == 1, "DCP not support chunked local attn now."
@@ -874,7 +912,7 @@ class MambaManager(SingleTypeKVCacheManager):
912
kv_cache_group_ids: list[int],
913
block_pool: BlockPool,
914
kv_cache_spec: KVCacheSpec,
877
- use_eagle: bool,
915
+ drop_eagle_block: bool,
916
alignment_tokens: int,
917
dcp_world_size: int = 1,
918
pcp_world_size: int = 1,
@@ -1168,7 +1206,7 @@ class CrossAttentionManager(SingleTypeKVCacheManager):
1206
kv_cache_group_ids: list[int],
1207
block_pool: BlockPool,
1208
kv_cache_spec: KVCacheSpec,
1171
- use_eagle: bool,
1209
+ drop_eagle_block: bool,
1210
alignment_tokens: int,
1211
dcp_world_size: int = 1,
1212
pcp_world_size: int = 1,