Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3982,6 +3982,19 @@ def test_eagle_block_drop_can_be_disabled_without_disabling_eagle(
)


def test_gemma4_mtp_needs_no_eagle_block_drop():
# The Gemma-4 MTP drafter reads the target's KV and writes none of its own.
speculative_config = SpeculativeConfig(method="ngram", num_speculative_tokens=3)
speculative_config.method = "mtp"
speculative_config.draft_model_config = SimpleNamespace(
hf_config=SimpleNamespace(model_type="gemma4_mtp")
)

assert speculative_config.use_eagle()
assert not speculative_config.needs_eagle_block_drop()
assert not speculative_config.use_eagle_block_drop()


def test_draft_sample_method_gumbel_is_rejected():
with pytest.raises(ValidationError):
SpeculativeConfig(
Expand Down
32 changes: 32 additions & 0 deletions tests/v1/core/test_prefix_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -6407,6 +6407,38 @@ def reachable_blocks(
assert reachable_blocks(250, use_eagle=True) == {243, 248, 249}


def test_swa_reachable_block_mask_eagle_periodic_tail_reaches_sibling():
"""Under EAGLE, a sibling that diverges right after a periodic boundary hits
one alignment unit lower; that run must be cached too."""
from vllm.v1.core.single_type_kv_cache_manager import SlidingWindowManager

block_size = 16
spec = SlidingWindowSpec(
block_size=block_size,
num_kv_heads=1,
head_size=1,
dtype=torch.float32,
sliding_window=3 * block_size, # need = 3, or 4 with the EAGLE peek
)

def retained(use_eagle: bool) -> set[int]:
m = SlidingWindowManager.reachable_block_mask(
start_block=0,
end_block=24,
alignment_tokens=2 * block_size,
kv_cache_spec=spec,
use_eagle=use_eagle,
retention_interval=8 * block_size,
)
assert m is not None
return {i for i, v in enumerate(m) if v}

# Boundary at block 8: the writer's run is 5..8 (peek 8). A sibling's hit
# lands at block 6, so it needs 3..6 (peek 6).
assert retained(use_eagle=True) == {*range(3, 9), *range(11, 17), *range(19, 24)}
assert retained(use_eagle=False) == {5, 6, 7, 13, 14, 15, 21, 22, 23}


def test_swa_reachable_block_mask_pins_shared_prefix():
"""SWA analog of the Mamba pin: the shared-prefix junction must keep the
``need``-block sliding-window tail ending on that boundary (not a single
Expand Down
7 changes: 5 additions & 2 deletions vllm/config/speculative.py
Original file line number Diff line number Diff line change
Expand Up @@ -1805,9 +1805,12 @@ def needs_eagle_block_drop(self) -> bool:

EAGLE-style drafters build a position's draft KV from its hidden state
and the next token. DFlash/DSpark build it from the target hidden state
alone, so only EAGLE/EAGLE3/MTP require the trailing-block drop.
alone, and the Gemma-4 MTP drafter writes no KV (it reads the target's),
so only the remaining EAGLE/EAGLE3/MTP drafters require the drop.
"""
return self.use_eagle() and not (self.use_dflash() or self.use_dspark())
return self.use_eagle() and not (
self.use_dflash() or self.use_dspark() or self.use_gemma4_mtp()
)

def use_eagle_block_drop(self) -> bool:
"""Whether the volatile trailing prefix-cache block should be dropped."""
Expand Down
10 changes: 8 additions & 2 deletions vllm/v1/core/single_type_kv_cache_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,11 +1114,17 @@ def reachable_block_mask(
)
if segment_tokens is not None:
per_segment = segment_tokens // block_size
if need >= per_segment:
# Under EAGLE, a sibling that diverges right after a segment boundary
# cannot use the peek block past it, so its hit lands one alignment
# unit lower. Keep that run too, as the shared-prefix junction does.
tail = need
if use_eagle and segment_tokens > alignment_tokens:
tail += alignment_tokens // block_size
if tail >= per_segment:
# Every block is reachable; cache them all.
return None
for i in range(start_block, end_block):
if i >= shift and (i - shift) % per_segment >= per_segment - need:
if i >= shift and (i - shift) % per_segment >= per_segment - tail:
mask[i - start_block] = True

if final_segment_end_block is not None and not use_eagle:
Expand Down
Loading