Skip to content

Commit 6593754

Browse files
authored
[Bugfix][Spec Decode] Keep EAGLE cache registration on the partial-hash-hit path (#52419)
1 parent edd4c81 commit 6593754

2 files changed

Lines changed: 92 additions & 20 deletions

File tree

tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,80 @@ def test_hybrid_mamba_align_partial_hash_hit():
261261
assert manager.get_blocks("1").blocks[1][1].block_hash_num_tokens == 8
262262

263263

264+
def test_eagle_group_registers_unaligned_tail_under_partial_hash_hits():
265+
"""An EAGLE group must not re-floor what partial hash hits leaves un-floored.
266+
267+
``cache_blocks`` decides once how far a request may be registered, and with
268+
fine-grained partial hash hits that bound is the raw token count. The EAGLE
269+
branch then re-derives its own bound for the lookahead block; if it rounds
270+
down to ``scheduler_block_size`` again, everything between the last aligned
271+
boundary and the tail stops being registered -- ``(n % scheduler_block_size)
272+
- manager.block_size`` tokens per call, which is most of a segment whenever
273+
the group's own block is much smaller than the scheduler block.
274+
"""
275+
hash_block_size = 2
276+
mamba_block_size = 4 * hash_block_size
277+
kv_cache_config = KVCacheConfig(
278+
num_blocks=40,
279+
kv_cache_tensors=[],
280+
kv_cache_groups=[
281+
KVCacheGroupSpec(
282+
["full"],
283+
FullAttentionSpec(
284+
block_size=hash_block_size,
285+
num_kv_heads=1,
286+
head_size=1,
287+
dtype=torch.float32,
288+
),
289+
),
290+
KVCacheGroupSpec(
291+
["mamba"],
292+
MambaSpec(
293+
block_size=mamba_block_size,
294+
shapes=(1, 1),
295+
dtypes=(torch.float32,),
296+
mamba_cache_mode="align",
297+
),
298+
),
299+
],
300+
)
301+
manager = make_kv_cache_manager(
302+
kv_cache_config=kv_cache_config,
303+
max_model_len=8192,
304+
enable_caching=True,
305+
hash_block_size=hash_block_size,
306+
)
307+
coordinator = manager.coordinator
308+
assert coordinator.enable_partial_hash_hits
309+
# The full-attention group is the EAGLE one, and its block is smaller than
310+
# the scheduler block -- the geometry where re-flooring loses tokens.
311+
eagle_manager = coordinator.single_type_managers[0]
312+
eagle_manager.use_eagle = True
313+
assert eagle_manager.block_size < coordinator.scheduler_block_size
314+
315+
# Deliberately not a multiple of the scheduler block, so the two bounds
316+
# differ: floor(22/8)*8 + 2 = 18 against 22.
317+
num_tokens = coordinator.scheduler_block_size * 2 + hash_block_size * 3
318+
req = make_request("0", list(range(num_tokens)), hash_block_size, sha256)
319+
320+
recorded: list[int] = []
321+
for single_type_manager in coordinator.single_type_managers:
322+
original = single_type_manager.cache_blocks
323+
324+
def spy(request, num_tokens_to_cache, *args, _orig=original, **kwargs):
325+
recorded.append(num_tokens_to_cache)
326+
return _orig(request, num_tokens_to_cache, *args, **kwargs)
327+
328+
single_type_manager.cache_blocks = spy
329+
330+
# allocate_slots caches on the way out, so this exercises the real path.
331+
computed_blocks, num_computed, _ = manager.get_computed_blocks(req)
332+
assert manager.allocate_slots(req, num_tokens, num_computed, computed_blocks)
333+
334+
# Every group, EAGLE or not, may register the whole unaligned tail.
335+
assert recorded == [num_tokens] * len(coordinator.single_type_managers)
336+
337+
264338
def test_hybrid_mamba_partial_tail_owner_uses_cow_on_continue():
265339
hash_block_size = 2
266340
block_size = 2 * hash_block_size

vllm/v1/core/kv_cache_coordinator.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from vllm import envs
88
from vllm.logger import init_logger
9-
from vllm.utils.math_utils import cdiv
9+
from vllm.utils.math_utils import cdiv, round_down
1010
from vllm.v1.core.block_pool import BlockPool
1111
from vllm.v1.core.kv_cache_metrics import KVCacheMetricsCollector
1212
from vllm.v1.core.kv_cache_utils import (
@@ -704,39 +704,37 @@ def verify_and_split_kv_cache_groups(self) -> None:
704704
for gid in group.group_ids:
705705
self.single_type_managers[gid].use_eagle = True
706706

707-
def cache_blocks(self, request: Request, num_computed_tokens: int) -> None:
707+
def _align_cacheable(self, num_tokens: int) -> int:
708+
"""Largest prefix of ``num_tokens`` a future cache hit could match.
709+
710+
Hits are ``scheduler_block_size``-aligned (see
711+
``find_longest_cache_hit``) unless fine-grained partial hash hits are
712+
enabled, in which case no rounding applies -- rounding even to
713+
``hash_block_size`` would re-register a privatized Mamba tail.
714+
"""
708715
if self.enable_partial_hash_hits:
709-
aligned_num_computed_tokens = num_computed_tokens
710-
else:
711-
# Cache hits in this coordinator are always a multiple of
712-
# ``scheduler_block_size`` tokens (see ``find_longest_cache_hit``).
713-
# Within an aligned region, SWA groups may only consult a subset of
714-
# blocks per ``scheduler_block_size``-segment so the unused blocks
715-
# also stay out of the prefix-cache hash map.
716-
aligned_num_computed_tokens = (
717-
num_computed_tokens
718-
// self.scheduler_block_size
719-
* self.scheduler_block_size
720-
)
716+
return num_tokens
717+
return round_down(num_tokens, self.scheduler_block_size)
718+
719+
def cache_blocks(self, request: Request, num_computed_tokens: int) -> None:
720+
cached_num_computed_tokens = self._align_cacheable(num_computed_tokens)
721721
for manager in self.single_type_managers:
722-
num_tokens_to_cache = aligned_num_computed_tokens
722+
num_tokens_to_cache = cached_num_computed_tokens
723723
# EAGLE groups match one block past each aligned boundary and drop
724724
# it, so make that lookahead block eligible to be cached.
725-
if manager.use_eagle and aligned_num_computed_tokens > 0:
725+
if manager.use_eagle and cached_num_computed_tokens > 0:
726726
# Only cache tokens with finalized KV. The last
727727
# num_reprefillable_tokens tokens can be re-prefilled during
728728
# multi-module MTP.
729729
num_finalized_computed_tokens = max(
730730
0, num_computed_tokens - self.num_reprefillable_tokens
731731
)
732-
aligned_num_finalized_computed_tokens = (
732+
cached_num_finalized_computed_tokens = self._align_cacheable(
733733
num_finalized_computed_tokens
734-
// self.scheduler_block_size
735-
* self.scheduler_block_size
736734
)
737735
num_tokens_to_cache = min(
738736
num_finalized_computed_tokens,
739-
aligned_num_finalized_computed_tokens + manager.block_size,
737+
cached_num_finalized_computed_tokens + manager.block_size,
740738
)
741739
# The manager already knows the fine hit granularity
742740
# (``scheduler_block_size``); retention is passed separately so it

0 commit comments

Comments
 (0)