Skip to content
Merged
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
6 changes: 4 additions & 2 deletions tests/config/test_speculative_draft_hf_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def record(hf_config: PretrainedConfig) -> PretrainedConfig:


@pytest.mark.cpu_test
def test_inkling_override_exposes_only_first_mtp_depth():
def test_inkling_override_exposes_all_mtp_depths():
text_config = _make_hf_config(
architectures=["InklingForCausalLM"],
model_type="inkling_model",
Expand All @@ -107,7 +107,9 @@ def test_inkling_override_exposes_only_first_mtp_depth():
assert out is text_config
assert out.model_type == "inkling_mtp"
assert out.architectures == ["InklingMTPModel"]
assert out.n_predict == 1
# Multi-module MTP: every checkpoint depth is exposed (module i drafts
# speculative token i), no longer clamped to the first depth.
assert out.n_predict == 8
assert out.num_nextn_predict_layers == 8
assert out.chain_hidden_post_norm is False
assert out.local_layer_ids == [0, 2, 4]
Expand Down
4 changes: 3 additions & 1 deletion tests/v1/core/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3701,6 +3701,7 @@ def test_mamba_align_eagle_schedules_encoder_at_boundary():
)
scheduler.need_mamba_block_aligned_split = True
scheduler.use_eagle = True
scheduler.num_prefill_lookahead = 1
scheduler.max_num_encoder_input_tokens = 2048
scheduler.encoder_cache_manager = EncoderCacheManager(cache_size=2048)

Expand Down Expand Up @@ -5341,8 +5342,9 @@ def test_free_encoder_inputs_defers_for_eagle_lookahead():
worker-side token-embedding fallback is only a backstop."""
scheduler = create_scheduler(model="llava-hf/llava-1.5-7b-hf")
# create_scheduler only builds ngram spec configs; force the eagle path that
# _free_encoder_inputs keys off (self.use_eagle).
# _free_encoder_inputs keys off (its read-ahead deferral).
scheduler.use_eagle = True
scheduler.num_prefill_lookahead = 1
mm_positions = [[PlaceholderRange(offset=50, length=100)]]
request = create_requests(
num_requests=1,
Expand Down
11 changes: 1 addition & 10 deletions vllm/config/speculative.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,8 +630,7 @@ def hf_config_override(hf_config: PretrainedConfig) -> PretrainedConfig:
hf_config.model_type = "inkling_mtp"
hf_config.update(
{
# Inkling currently exposes only the first checkpoint depth.
"n_predict": 1,
"n_predict": checkpoint_depths,
"num_nextn_predict_layers": checkpoint_depths,
"chain_hidden_post_norm": mtp_config.get(
"chain_hidden_post_norm", False
Expand Down Expand Up @@ -1083,14 +1082,6 @@ def __post_init__(self):
"`num_speculative_tokens` was not provided"
)

if (
self.draft_model_config.hf_config.model_type == "inkling_mtp"
and self.num_speculative_tokens != 1
):
raise ValueError(
"Inkling MTP currently supports exactly one speculative token"
)

if self.dspark_draft_topk is not None and self.method != "dspark":
raise ValueError("dspark_draft_topk is only supported by DSpark")

Expand Down
56 changes: 53 additions & 3 deletions vllm/v1/core/kv_cache_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def __init__(
scheduler_block_size: int,
hash_block_size: int,
metrics_collector: KVCacheMetricsCollector | None = None,
num_prefill_lookahead: int = 0,
):
self.kv_cache_config = kv_cache_config
self.max_model_len = max_model_len
Expand All @@ -91,6 +92,7 @@ def __init__(
for g in kv_cache_config.kv_cache_groups
)
self.scheduler_block_size = scheduler_block_size
self.num_reprefillable_tokens = max(0, num_prefill_lookahead - 1)

self.block_pool = BlockPool(
num_gpu_blocks=kv_cache_config.num_blocks,
Expand All @@ -108,6 +110,28 @@ def __init__(
if use_eagle and not self.eagle_group_ids:
self.eagle_group_ids = set(range(len(kv_cache_config.kv_cache_groups)))

# During chunked prefill with EAGLE, the single next prefill lookahead
# token past the chunk boundary is combined with the final hidden state
# and written to the KV cache. Therefore, the final chunk token must be
# excluded from prefix cache hits to prevent requests from acquiring the
# KV cache slot polluted with the next prefill token, which may or may not
# be present after the matching prefix. The last-block drop handles this
# edge case. During multi-module MTP, the issue generalizes to a prefill
# lookahead of num_speculative_tokens, so the dropped tail must be large
# enough to contain them. Hits land on scheduler-block boundaries (see
# `_cache_hit_alignment_tokens`), so the excluded tail is
# scheduler_block_size, not the group's own block size.
if (
enable_caching
and self.eagle_group_ids
and scheduler_block_size < num_prefill_lookahead
):
raise ValueError(
f"Multi-module MTP with prefix caching requires scheduler_block_size"
f" (={scheduler_block_size}) >= num_speculative_tokens"
f" (={num_prefill_lookahead})."
)

self.single_type_managers = tuple(
get_manager_for_kv_cache_spec(
kv_cache_spec=kv_cache_group.kv_cache_spec,
Expand Down Expand Up @@ -286,9 +310,14 @@ def cache_blocks(self, request: Request, num_computed_tokens: int) -> None:
(including tokens that are already cached).
"""
for manager in self.single_type_managers:
# Only cache tokens with finalized KV. The last num_reprefillable_tokens
# tokens can be re-prefilled during multi-module MTP.
num_tokens_to_cache = max(
0, num_computed_tokens - self.num_reprefillable_tokens
)
manager.cache_blocks(
request,
num_computed_tokens,
num_tokens_to_cache,
retention_interval=self.retention_interval,
)

Expand Down Expand Up @@ -407,6 +436,7 @@ def __init__(
scheduler_block_size: int,
hash_block_size: int,
metrics_collector: KVCacheMetricsCollector | None = None,
num_prefill_lookahead: int = 0,
):
super().__init__(
kv_cache_config,
Expand All @@ -420,6 +450,7 @@ def __init__(
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
metrics_collector=metrics_collector,
num_prefill_lookahead=num_prefill_lookahead,
)
self.num_single_type_manager = len(self.single_type_managers)

Expand Down Expand Up @@ -457,6 +488,7 @@ def __init__(
scheduler_block_size: int,
hash_block_size: int,
metrics_collector: KVCacheMetricsCollector | None = None,
num_prefill_lookahead: int = 0,
):
super().__init__(
kv_cache_config,
Expand All @@ -470,6 +502,7 @@ def __init__(
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
metrics_collector=metrics_collector,
num_prefill_lookahead=num_prefill_lookahead,
)
self.kv_cache_spec = self.kv_cache_config.kv_cache_groups[0].kv_cache_spec
self.block_size = self.kv_cache_spec.block_size
Expand Down Expand Up @@ -542,6 +575,7 @@ def __init__(
scheduler_block_size: int,
hash_block_size: int,
metrics_collector: KVCacheMetricsCollector | None = None,
num_prefill_lookahead: int = 0,
):
super().__init__(
kv_cache_config,
Expand All @@ -555,6 +589,7 @@ def __init__(
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
metrics_collector=metrics_collector,
num_prefill_lookahead=num_prefill_lookahead,
)
# hash_block_size: the block size used to compute block hashes.
# The actual block size usually equals hash_block_size, but in cases where
Expand Down Expand Up @@ -688,9 +723,20 @@ def cache_blocks(self, request: Request, num_computed_tokens: int) -> None:
# EAGLE groups match one block past each aligned boundary and drop
# it, so make that lookahead block eligible to be cached.
if manager.use_eagle and aligned_num_computed_tokens > 0:
# Only cache tokens with finalized KV. The last
# num_reprefillable_tokens tokens can be re-prefilled during
# multi-module MTP.
num_finalized_computed_tokens = max(
0, num_computed_tokens - self.num_reprefillable_tokens
)
aligned_num_finalized_computed_tokens = (
num_finalized_computed_tokens
// self.scheduler_block_size
* self.scheduler_block_size
)
num_tokens_to_cache = min(
num_computed_tokens,
aligned_num_computed_tokens + manager.block_size,
num_finalized_computed_tokens,
aligned_num_finalized_computed_tokens + manager.block_size,
)
# The manager already knows the fine hit granularity
# (``scheduler_block_size``); retention is passed separately so it
Expand Down Expand Up @@ -880,6 +926,7 @@ def get_kv_cache_coordinator(
scheduler_block_size: int,
hash_block_size: int,
metrics_collector: KVCacheMetricsCollector | None = None,
num_prefill_lookahead: int = 0,
) -> KVCacheCoordinator:
if not enable_caching:
return KVCacheCoordinatorNoPrefixCache(
Expand All @@ -893,6 +940,7 @@ def get_kv_cache_coordinator(
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
metrics_collector=metrics_collector,
num_prefill_lookahead=num_prefill_lookahead,
)
if len(kv_cache_config.kv_cache_groups) == 1:
return UnitaryKVCacheCoordinator(
Expand All @@ -907,6 +955,7 @@ def get_kv_cache_coordinator(
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
metrics_collector=metrics_collector,
num_prefill_lookahead=num_prefill_lookahead,
)
return HybridKVCacheCoordinator(
kv_cache_config,
Expand All @@ -920,4 +969,5 @@ def get_kv_cache_coordinator(
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
metrics_collector=metrics_collector,
num_prefill_lookahead=num_prefill_lookahead,
)
2 changes: 2 additions & 0 deletions vllm/v1/core/kv_cache_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def __init__(
max_in_flight_tokens: int | None = None,
enable_caching: bool = True,
use_eagle: bool = False,
num_prefill_lookahead: int = 0,
log_stats: bool = False,
enable_kv_cache_events: bool = False,
dcp_world_size: int = 1,
Expand Down Expand Up @@ -161,6 +162,7 @@ def __init__(
scheduler_block_size=scheduler_block_size,
hash_block_size=hash_block_size,
metrics_collector=self.metrics_collector,
num_prefill_lookahead=num_prefill_lookahead,
)
self.num_kv_cache_groups = len(kv_cache_config.kv_cache_groups)
self.block_pool = self.coordinator.block_pool
Expand Down
18 changes: 18 additions & 0 deletions vllm/v1/core/kv_cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,9 @@ def promoted_page_size_padded(spec: AttentionSpec, block_size: int) -> int | Non
promoted_specs[layer_name] = replace_as(
spec,
target_cls,
# Promoted specs allocate blocks for all tokens and never free
# below the window, so the trailing-edge extension is moot.
drop=("extra_retained_tokens",),
block_size=block_size,
page_size_padded=promoted_page_size_padded(spec, block_size),
)
Expand Down Expand Up @@ -2128,6 +2131,21 @@ def get_kv_cache_configs(
# Check if the KV cache specs are registered correctly.
# This is to prevent that some layers are initialized with unregistered specs.
KVCacheSpecRegistry.check_kv_cache_spec_registry(merged_kv_cache_specs)

# When speculating with more than 1 speculative module (e.g. multi-layered MTP)
# tag every SlidingWindowSpec with how many extra tokens to retain in the window.
extra_retained_tokens = (
vllm_config.speculative_config.num_speculative_tokens - 1
if vllm_config.speculative_config is not None
and vllm_config.speculative_config.use_multi_module_mtp()
else 0
)
for layer_name, layer_spec in merged_kv_cache_specs.items():
if isinstance(layer_spec, SlidingWindowSpec):
merged_kv_cache_specs[layer_name] = replace(
layer_spec, extra_retained_tokens=extra_retained_tokens
)

# Get global KV cache groups. This also handles spec unification for
# hybrid models when disable_hybrid_kv_cache_manager is enabled.
# After this call, merged_kv_cache_specs may be modified in-place.
Expand Down
66 changes: 58 additions & 8 deletions vllm/v1/core/sched/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ def __init__(
self.use_eagle = False
self.num_spec_tokens = vllm_config.num_speculative_tokens
self.num_lookahead_tokens = vllm_config.num_lookahead_tokens
# Positions past the computed tokens that the drafter reads mid-prefill.
# Eagle-family drafters read 1 ahead, but multi-module MTP reads
# num_spec_tokens ahead at chunked-prefill boundaries. Determines the
# encoder scheduling shift, the deferred encoder free, the KV cache
# manager's re-prefillable window (this minus 1), and how many tokens to
# reserve between a chunk boundary and the prefill end.
self.num_prefill_lookahead = 0
self.dynamic_sd_lookup: list[int] | None = None
if speculative_config is not None:
if speculative_config.num_speculative_tokens_per_batch_size:
Expand All @@ -256,6 +263,12 @@ def __init__(
vllm_num_speculative_tokens=self.num_spec_tokens,
)
self.use_eagle = speculative_config.use_eagle()
if self.use_eagle:
self.num_prefill_lookahead = (
self.num_spec_tokens
if speculative_config.use_multi_module_mtp()
else 1
)

# Create the KV cache manager.
if hash_block_size is None:
Expand All @@ -267,6 +280,7 @@ def __init__(
max_in_flight_tokens=vllm_config.max_in_flight_tokens,
enable_caching=self.cache_config.enable_prefix_caching,
use_eagle=self.use_eagle,
num_prefill_lookahead=self.num_prefill_lookahead,
log_stats=self.log_stats,
enable_kv_cache_events=self.enable_kv_cache_events,
dcp_world_size=self.dcp_world_size,
Expand Down Expand Up @@ -438,6 +452,27 @@ def _get_local_prefix_cache_hit(
)
return blocks, num_local, shared_prefix_boundary, False

def _reserve_prefill_lookahead(
self,
request: Request,
num_computed_tokens: int,
num_new_tokens: int,
) -> int:
"""Never end a prefill chunk within num_prefill_lookahead of the
prefill end.

At a chunked-prefill boundary, the multi-module MTP drafter consumes
the next num_prefill_lookahead known prefill tokens as draft inputs. A
boundary closer to the end than that would make it fall back to
sampled drafts, permanently polluting the trailing modules' KV caches.
Either finish the prefill or leave at least num_prefill_lookahead for
the next chunk. No-op for eagle-family drafters (lookahead 1).
"""
remaining = request.num_tokens - num_computed_tokens - num_new_tokens
if 0 < remaining < self.num_prefill_lookahead:
num_new_tokens -= self.num_prefill_lookahead - remaining
return max(num_new_tokens, 0)

def schedule(self, throttle_prefills: bool = False) -> SchedulerOutput:
self.current_step += 1
# NOTE(woosuk) on the scheduling algorithm:
Expand Down Expand Up @@ -561,9 +596,15 @@ def schedule(self, throttle_prefills: bool = False) -> SchedulerOutput:
request.num_computed_tokens,
num_new_tokens,
encoder_compute_budget,
shift_computed_tokens=1 if self.use_eagle else 0,
shift_computed_tokens=self.num_prefill_lookahead,
)

# Multi-module MTP: avoid ending a prefill chunk within
# num_prefill_lookahead of the prefill end.
num_new_tokens = self._reserve_prefill_lookahead(
request, request.num_computed_tokens, num_new_tokens
)

if num_new_tokens == 0:
# The request cannot be scheduled because one of the following
# reasons:
Expand All @@ -576,6 +617,8 @@ def schedule(self, throttle_prefills: bool = False) -> SchedulerOutput:
# 3. The encoder cache is exhausted.
# 4. Insufficient budget for a block-aligned chunk in hybrid
# models with mamba cache mode \"align\".
# 5. Insufficient budget to keep a multi-module MTP prefill
# chunk out of the prefill-lookahead window.
# NOTE(woosuk): Here, by doing `continue` instead of `break`,
# we do not strictly follow the FCFS scheduling policy and
# allow the lower-priority requests to be scheduled.
Expand Down Expand Up @@ -946,11 +989,18 @@ def schedule(self, throttle_prefills: bool = False) -> SchedulerOutput:
num_computed_tokens,
num_new_tokens,
encoder_compute_budget,
shift_computed_tokens=1 if self.use_eagle else 0,
shift_computed_tokens=self.num_prefill_lookahead,
)
if num_new_tokens == 0:
# The request cannot be scheduled.
break

# Multi-module MTP: avoid ending a prefill chunk within
# num_prefill_lookahead of the prefill end.
num_new_tokens = self._reserve_prefill_lookahead(
request, num_computed_tokens, num_new_tokens
)

if num_new_tokens == 0:
# The request cannot be scheduled.
break

# During async KV load, no forward pass is run yet.
# Allocate speculative lookahead slots later to avoid
Expand Down Expand Up @@ -2157,9 +2207,9 @@ def _free_encoder_inputs(self, request: Request) -> None:
return

# Defer the free by the drafter's look-ahead so an entry stays
# referenced until the drafter's +1 read has also passed it, mirroring
# the shift the encoder scheduling path applies.
spec_lookahead = 1 if self.use_eagle else 0
# referenced until the drafter's read-ahead has also passed it,
# mirroring the shift the encoder scheduling path applies.
spec_lookahead = self.num_prefill_lookahead

# Here, we use list(set) to avoid modifying the set while iterating
# over it.
Expand Down
Loading
Loading