Compare commits

...
Author SHA1 Message Date
166a8e954b [Model] Add Inkling multi-depth MTP support [5/N]
Extend the merged Inkling MTP=1 implementation to multiple checkpoint depths with multi-step speculative decoding and KV-cache plumbing.

Co-authored-by: Bugen Zhao <i@bugenzhao.com>
Co-authored-by: Giancarlo Delfin <32987265+TheEpicDolphin@users.noreply.github.com>
Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: Isotr0py <mozf@inferact.ai>
Co-authored-by: Jee Jee Li <jeejeelee@inferact.ai>
Co-authored-by: Roger Wang <hey@rogerw.io>
Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-07-16 21:17:29 +00:00
17 changed files with 672 additions and 120 deletions
+6 -1
View File
@@ -173,7 +173,12 @@ impl ResolvedPlaceholder {
let marker_token_id = context.tokenizer().token_to_id(&token).ok_or_else(|| { let marker_token_id = context.tokenizer().token_to_id(&token).ok_or_else(|| {
multimodal!("placeholder token `{token}` is not in the tokenizer vocabulary") multimodal!("placeholder token `{token}` is not in the tokenizer vocabulary")
})?; })?;
let embed_token_id = raw.placeholder_token_id_for(&metadata, modality)? as u32; let embed_token_id = raw.placeholder_token_id_for(&metadata, modality)?;
let embed_token_id = u32::try_from(embed_token_id).map_err(|_| {
multimodal!(
"placeholder token id `{embed_token_id}` for `{modality}` must be non-negative"
)
})?;
Ok(Self { Ok(Self {
token, token,
@@ -86,7 +86,7 @@ def test_arch_mapping_applies_before_callable_override():
@pytest.mark.cpu_test @pytest.mark.cpu_test
def test_inkling_override_exposes_only_first_mtp_depth(): def test_inkling_override_exposes_checkpoint_mtp_depths():
text_config = _make_hf_config( text_config = _make_hf_config(
architectures=["InklingForCausalLM"], architectures=["InklingForCausalLM"],
model_type="inkling_model", model_type="inkling_model",
@@ -107,7 +107,7 @@ def test_inkling_override_exposes_only_first_mtp_depth():
assert out is text_config assert out is text_config
assert out.model_type == "inkling_mtp" assert out.model_type == "inkling_mtp"
assert out.architectures == ["InklingMTPModel"] assert out.architectures == ["InklingMTPModel"]
assert out.n_predict == 1 assert out.n_predict == 8
assert out.num_nextn_predict_layers == 8 assert out.num_nextn_predict_layers == 8
assert out.chain_hidden_post_norm is False assert out.chain_hidden_post_norm is False
assert out.local_layer_ids == [0, 2, 4] assert out.local_layer_ids == [0, 2, 4]
+8 -9
View File
@@ -1422,16 +1422,15 @@ class CompilationConfig:
"and make sure compilation mode is VLLM_COMPILE" "and make sure compilation mode is VLLM_COMPILE"
) )
# MRV1 adjusts cudagraph sizes to be a multiple of uniform_decode_query_len # Round cudagraph sizes to a multiple of uniform_decode_query_len so a
# to avoid: https://github.com/vllm-project/vllm/issues/28207 and temp-fix: # uniform decode batch cannot dispatch to a "mixed" descriptor whose token
# https://github.com/vllm-project/vllm/issues/28207#issuecomment-3504004536 # count is not a multiple of the decode span, which corrupts per-request
# Will be removed in the near future when we have separate cudagraph capture # conv/attention state (https://github.com/vllm-project/vllm/issues/28207).
# sizes for decode and mixed prefill-decode. # Enabled for MRV2 as well: its cudagraph_utils.py sizing does not on its
# MRV2 handles cudagraph capture sizing in cudagraph_utils.py # own keep uniform batches off mixed descriptors for multi-module MTP.
# and doesn't need below: https://github.com/vllm-project/vllm/pull/45953 # Temporary until decode and mixed prefill-decode have separate sizes.
if ( if (
not use_v2_model_runner cudagraph_mode.decode_mode() == CUDAGraphMode.FULL
and cudagraph_mode.decode_mode() == CUDAGraphMode.FULL
and uniform_decode_query_len > 1 and uniform_decode_query_len > 1
): ):
self.adjust_cudagraph_sizes_for_spec_decode( self.adjust_cudagraph_sizes_for_spec_decode(
+14 -10
View File
@@ -555,6 +555,8 @@ class SpeculativeConfig:
) )
if hf_config.model_type in ("inkling_mm_model", "inkling_model"): if hf_config.model_type in ("inkling_mm_model", "inkling_model"):
# MTP weights live on the text backbone. Promote text_config and
# expose the nested MTP fields on the flat draft config.
mtp_config = getattr(hf_config, "mtp_config", None) or {} mtp_config = getattr(hf_config, "mtp_config", None) or {}
hf_config = getattr(hf_config, "text_config", hf_config) hf_config = getattr(hf_config, "text_config", hf_config)
checkpoint_depths = mtp_config.get("num_nextn_predict_layers", 0) checkpoint_depths = mtp_config.get("num_nextn_predict_layers", 0)
@@ -563,12 +565,13 @@ class SpeculativeConfig:
hf_config.model_type = "inkling_mtp" hf_config.model_type = "inkling_mtp"
hf_config.update( hf_config.update(
{ {
# Inkling currently exposes only the first checkpoint depth. "n_predict": checkpoint_depths,
"n_predict": 1,
"num_nextn_predict_layers": checkpoint_depths, "num_nextn_predict_layers": checkpoint_depths,
"chain_hidden_post_norm": mtp_config.get( "chain_hidden_post_norm": mtp_config.get(
"chain_hidden_post_norm", False "chain_hidden_post_norm", False
), ),
# The MTP depth blocks carry their own sliding-window
# pattern, which differs from the backbone's layer pattern.
"local_layer_ids": mtp_config.get("local_layer_ids", []), "local_layer_ids": mtp_config.get("local_layer_ids", []),
"architectures": ["InklingMTPModel"], "architectures": ["InklingMTPModel"],
} }
@@ -974,14 +977,6 @@ class SpeculativeConfig:
"`num_speculative_tokens` was not provided" "`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.method == "dspark": if self.method == "dspark":
# DSpark is a semi-autoregressive *block* drafter. A # DSpark is a semi-autoregressive *block* drafter. A
# speculative length smaller than the checkpoint's block # speculative length smaller than the checkpoint's block
@@ -1308,6 +1303,15 @@ class SpeculativeConfig:
# TODO(ben): Refactor this so the naming is clearer # TODO(ben): Refactor this so the naming is clearer
return self.method in ("eagle", "eagle3", "mtp", "dflash", "dspark") return self.method in ("eagle", "eagle3", "mtp", "dflash", "dspark")
def num_speculative_prefill_steps(self) -> int:
if self.method == "mtp" and self.draft_model_config is not None:
n_predict = getattr(
self.draft_model_config.hf_config, "num_nextn_predict_layers", None
)
if n_predict:
return min(int(n_predict), self.num_speculative_tokens)
return 1
def use_dflash(self) -> bool: def use_dflash(self) -> bool:
return self.method == "dflash" return self.method == "dflash"
+9 -7
View File
@@ -227,6 +227,7 @@ class InklingDecoderLayer(nn.Module):
# Caller folds mlp_output (pre-reduce, pre-sconv) into the next # Caller folds mlp_output (pre-reduce, pre-sconv) into the next
# fused sconv+add+rmsnorm. # fused sconv+add+rmsnorm.
return hidden_states, (mlp_output, self.mlp_sconv) return hidden_states, (mlp_output, self.mlp_sconv)
# Standalone (MTP) tail: finish the sublayer without a norm.
return _sconv_add_norm( return _sconv_add_norm(
mlp_output, hidden_states, self.mlp_sconv, None, positions mlp_output, hidden_states, self.mlp_sconv, None, positions
)[1] )[1]
@@ -236,10 +237,12 @@ class InklingReplicatedEmbedding(nn.Module):
"""Full-vocab embedding table replicated on every TP rank. """Full-vocab embedding table replicated on every TP rank.
Trades the full table per rank (~2.3 GiB at V=201k / H=6144 bf16, vs a Trades the full table per rank (~2.3 GiB at V=201k / H=6144 bf16, vs a
1/tp shard) for no masked lookup or per-lookup TP all-reduce, and keeps the 1/tp shard) for no masked lookup and no per-lookup TP all-reduce — one
full table on-rank for the fused gather+norm kernel. Bit-exact vs all-reduce per MTP draft step plus one per verify pass — and keeps the
vocab-parallel: the all-reduce there only ever summed one real row against full table on-rank for the fused gather+norm kernels (``embed_rmsnorm``,
exact zeros. The LM head stays vocab-sharded. ``embed_dual_rmsnorm_cat``). Bit-exact vs vocab-parallel: the all-reduce
there only ever summed one real row against exact zeros. The LM head
stays vocab-sharded.
""" """
def __init__(self, num_embeddings: int, embedding_dim: int) -> None: def __init__(self, num_embeddings: int, embedding_dim: int) -> None:
@@ -611,7 +614,8 @@ class InklingForConditionalGeneration(_TmlForCausalLMBase, SupportsMultiModal):
def get_language_model(self) -> nn.Module: def get_language_model(self) -> nn.Module:
# This class IS the causal LM (the towers are side branches), so the # This class IS the causal LM (the towers are side branches), so the
# language model is self — callers expect a module exposing ``.model`` # language model is self — callers expect a module exposing ``.model``
# and ``.lm_head``. # / ``.lm_head`` (e.g. the MTP/eagle loader shares embeddings via
# ``get_language_model().model.embed_tokens``).
return self return self
@@ -672,8 +676,6 @@ def _load_inkling_weights(
yield name, weight yield name, weight
# The release checkpoint also carries auxiliary prediction-head weights;
# they are not part of the causal LM served by this implementation.
loader = AutoWeightsLoader(module, skip_prefixes=["model.mtp."]) loader = AutoWeightsLoader(module, skip_prefixes=["model.mtp."])
loaded |= loader.load_weights(_iter_loadable_weights()) loaded |= loader.load_weights(_iter_loadable_weights())
+3 -3
View File
@@ -475,9 +475,9 @@ class InklingMoE(nn.Module):
lambda: self.sink_experts(x, gammas), lambda: self.sink_experts(x, gammas),
self._sink_events[0], self._sink_events[0],
self._sink_events[1], self._sink_events[1],
self._sink_stream self._sink_stream,
if num_tokens <= envs.VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD enable_parallel=num_tokens
else None, <= envs.VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD,
) )
self._routed_sel = None self._routed_sel = None
+42 -13
View File
@@ -2,9 +2,13 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Inkling MTP (Multi-Token Prediction) draft model (NVIDIA). """Inkling MTP (Multi-Token Prediction) draft model (NVIDIA).
Implements the first MTP depth from the reference ``mtp_model.py`` shipped with Mirrors the reference ``mtp_model.py`` shipped with the checkpoint: each MTP
the checkpoint. It owns ``hidden_norm`` / ``embed_norm`` RMSNorms, a ``2H -> H`` depth ``i`` owns ``hidden_norm`` / ``embed_norm`` RMSNorms, an ``input_proj``
input projection, and a full Inkling transformer block with a dense bf16 MLP. (``2H -> H``) and a full Inkling transformer block (dense bf16 MLP, with the
same short convolutions as the backbone; its attention is full or sliding-window
per depth, selected by ``mtp_config.local_layer_ids``). When enabled, a shared
``chain_norm`` is applied after every depth; its output is both the logits input
and the previous hidden state fed to the next depth.
The draft shares the target's token embedding table and LM head The draft shares the target's token embedding table and LM head
(``load_eagle_model`` wires those references) and applies the backbone (``load_eagle_model`` wires those references) and applies the backbone
@@ -50,6 +54,16 @@ def _mtp_depth_from_name(name: str) -> int | None:
return int(m.group(1)) if m else None return int(m.group(1)) if m else None
def _select_mtp_depth_count(n_predict: int, num_spec: int | None) -> int:
num_layers = min(n_predict, num_spec) if num_spec else n_predict
if num_layers <= 0:
raise ValueError(
"Inkling MTP requires num_nextn_predict_layers and "
"num_speculative_tokens to select at least one depth layer."
)
return num_layers
class InklingMTPDepthLayer(nn.Module): class InklingMTPDepthLayer(nn.Module):
"""One MTP depth: norm both inputs, fuse (2H->H), run a Inkling block.""" """One MTP depth: norm both inputs, fuse (2H->H), run a Inkling block."""
@@ -94,14 +108,30 @@ class InklingMultiTokenPredictor(nn.Module):
vllm_config.speculative_config.draft_model_config.hf_config vllm_config.speculative_config.draft_model_config.hf_config
) )
self.config = config self.config = config
if vllm_config.speculative_config.num_speculative_tokens != 1: # The checkpoint ships num_nextn_predict_layers depth blocks, but only
raise ValueError( # the first ``num_speculative_tokens`` are exercised (step i uses depth
"Inkling MTP currently supports exactly one speculative token" # i). Build only those to save memory — each depth is a full Inkling block
) # with its own (large) full-history sconv caches and KV cache.
n_predict = config.num_nextn_predict_layers
num_spec = vllm_config.speculative_config.num_speculative_tokens
self.num_mtp_layers = _select_mtp_depth_count(n_predict, num_spec)
self.chain_hidden_post_norm = config.chain_hidden_post_norm self.chain_hidden_post_norm = config.chain_hidden_post_norm
# Depth blocks whose attention is sliding-window (swa_* head config)
# rather than full; keyed by MTP depth via the checkpoint's
# mtp_config.local_layer_ids (promoted onto the draft config). Mirrors
# InklingModel's local_ids split, but over MTP depths, not backbone
# layers.
local_ids = set(config.local_layer_ids) local_ids = set(config.local_layer_ids)
# Keyed by depth index (str) to mirror the checkpoint layout.
self.layers = nn.ModuleDict( self.layers = nn.ModuleDict(
{"0": InklingMTPDepthLayer(config, f"{prefix}.layers.0", 0 in local_ids)} {
str(idx): InklingMTPDepthLayer(
config, f"{prefix}.layers.{idx}", idx in local_ids
)
for idx in range(self.num_mtp_layers)
}
) )
self.chain_norm = ( self.chain_norm = (
InklingRMSNorm(config.hidden_size, eps=config.rms_norm_eps) InklingRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
@@ -199,9 +229,8 @@ class InklingMultiTokenPredictor(nn.Module):
# auto-enumerated as a draft attention layer); its per-token metadata is # auto-enumerated as a draft attention layer); its per-token metadata is
# built by the speculator's build_attn_metadata and read from the # built by the speculator's build_attn_metadata and read from the
# forward context, so nothing extra is threaded here. # forward context, so nothing extra is threaded here.
if spec_step_idx != 0: depth = spec_step_idx % self.num_mtp_layers
raise ValueError("Inkling MTP only supports spec_step_idx=0") layer = self.layers[str(depth)]
layer = self.layers["0"]
combined = self.fused_input_cat( combined = self.fused_input_cat(
layer, previous_hidden_states, input_ids, inputs_embeds layer, previous_hidden_states, input_ids, inputs_embeds
) )
@@ -353,8 +382,8 @@ def _load_inkling_mtp_weights(
# Only consume the MTP weights; everything else belongs to the target. # Only consume the MTP weights; everything else belongs to the target.
if ".mtp." not in name: if ".mtp." not in name:
continue continue
# Only the first checkpoint depth is used for MTP=1. # Skip depth blocks beyond the ones we built (num_speculative_tokens).
if depth is not None and depth != 0: if depth is not None and depth >= module.model.num_mtp_layers:
continue continue
# model.mtp.chain_norm.weight -> model.chain_norm.weight # model.mtp.chain_norm.weight -> model.chain_norm.weight
# model.mtp.layers.{i}.X -> model.layers.{i}.X # model.mtp.layers.{i}.X -> model.layers.{i}.X
+3 -1
View File
@@ -23,6 +23,7 @@ def maybe_execute_in_parallel(
event0: torch.cuda.Event, event0: torch.cuda.Event,
event1: torch.cuda.Event, event1: torch.cuda.Event,
aux_stream: torch.cuda.Stream | None = None, aux_stream: torch.cuda.Stream | None = None,
enable_parallel: bool = True,
) -> tuple[Any, Any]: ) -> tuple[Any, Any]:
"""Run two functions potentially in parallel on separate CUDA streams. """Run two functions potentially in parallel on separate CUDA streams.
@@ -40,11 +41,12 @@ def maybe_execute_in_parallel(
event1: CUDA event recorded after fn1 so default stream can wait. event1: CUDA event recorded after fn1 so default stream can wait.
aux_stream: The second CUDA stream for fn1. aux_stream: The second CUDA stream for fn1.
Multi-stream is disabled when aux_stream is None. Multi-stream is disabled when aux_stream is None.
enable_parallel: Opt-in switch for the multi-stream path. Defaults to True.
Returns: Returns:
Tuple of (fn0_result, fn1_result). Tuple of (fn0_result, fn1_result).
""" """
if aux_stream is not None: if aux_stream is not None and enable_parallel:
event0.record() event0.record()
result0 = fn0() result0 = fn0()
with torch.cuda.stream(aux_stream): with torch.cuda.stream(aux_stream):
+20 -3
View File
@@ -86,6 +86,7 @@ class KVCacheCoordinator(ABC):
for g in kv_cache_config.kv_cache_groups for g in kv_cache_config.kv_cache_groups
) )
self.scheduler_block_size = scheduler_block_size self.scheduler_block_size = scheduler_block_size
self.num_spec_prefill_steps = 1
self.block_pool = BlockPool( self.block_pool = BlockPool(
num_gpu_blocks=kv_cache_config.num_blocks, num_gpu_blocks=kv_cache_config.num_blocks,
@@ -281,9 +282,14 @@ class KVCacheCoordinator(ABC):
(including tokens that are already cached). (including tokens that are already cached).
""" """
for manager in self.single_type_managers: for manager in self.single_type_managers:
# Only cache tokens with finalized KV. The last num_spec_prefill_steps - 1
# tokens can be re-prefilled by speculative modules.
num_tokens_to_cache = max(
0, num_computed_tokens - (self.num_spec_prefill_steps - 1)
)
manager.cache_blocks( manager.cache_blocks(
request, request,
num_computed_tokens, num_tokens_to_cache,
retention_interval=self.retention_interval, retention_interval=self.retention_interval,
) )
@@ -661,9 +667,20 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
# EAGLE groups match one block past each aligned boundary and drop # EAGLE groups match one block past each aligned boundary and drop
# it, so make that lookahead block eligible to be cached. # it, so make that lookahead block eligible to be cached.
if manager.use_eagle and aligned_num_computed_tokens > 0: if manager.use_eagle and aligned_num_computed_tokens > 0:
# Only cache tokens with finalized KV. The last
# num_spec_prefill_steps - 1 tokens can be re-prefilled by
# speculative modules.
num_finalized_computed_tokens = max(
0, num_computed_tokens - (self.num_spec_prefill_steps - 1)
)
aligned_num_finalized_computed_tokens = (
num_finalized_computed_tokens
// self.scheduler_block_size
* self.scheduler_block_size
)
num_tokens_to_cache = min( num_tokens_to_cache = min(
num_computed_tokens, num_finalized_computed_tokens,
aligned_num_computed_tokens + manager.block_size, aligned_num_finalized_computed_tokens + manager.block_size,
) )
# The manager already knows the fine hit granularity # The manager already knows the fine hit granularity
# (``scheduler_block_size``); retention is passed separately so it # (``scheduler_block_size``); retention is passed separately so it
+2
View File
@@ -121,6 +121,7 @@ class KVCacheManager:
max_in_flight_tokens: int | None = None, max_in_flight_tokens: int | None = None,
enable_caching: bool = True, enable_caching: bool = True,
use_eagle: bool = False, use_eagle: bool = False,
num_spec_prefill_steps: int = 1,
log_stats: bool = False, log_stats: bool = False,
enable_kv_cache_events: bool = False, enable_kv_cache_events: bool = False,
dcp_world_size: int = 1, dcp_world_size: int = 1,
@@ -158,6 +159,7 @@ class KVCacheManager:
hash_block_size=hash_block_size, hash_block_size=hash_block_size,
metrics_collector=self.metrics_collector, metrics_collector=self.metrics_collector,
) )
self.coordinator.num_spec_prefill_steps = num_spec_prefill_steps
self.num_kv_cache_groups = len(kv_cache_config.kv_cache_groups) self.num_kv_cache_groups = len(kv_cache_config.kv_cache_groups)
self.block_pool = self.coordinator.block_pool self.block_pool = self.coordinator.block_pool
self.kv_cache_config = kv_cache_config self.kv_cache_config = kv_cache_config
+20
View File
@@ -2086,6 +2086,26 @@ def get_kv_cache_configs(
# Check if the KV cache specs are registered correctly. # Check if the KV cache specs are registered correctly.
# This is to prevent that some layers are initialized with unregistered specs. # This is to prevent that some layers are initialized with unregistered specs.
KVCacheSpecRegistry.check_kv_cache_spec_registry(merged_kv_cache_specs) KVCacheSpecRegistry.check_kv_cache_spec_registry(merged_kv_cache_specs)
# Under multi-module MTP with prefix caching, tag every sliding-window spec
# with the store-side retention lag so pool sizing, the runtime admission
# cap, and block eviction all agree that these groups retain a few extra
# trailing blocks (single source of truth: the spec field). No-op (lag 0)
# otherwise, so non-MTP models are unchanged. Done before grouping so the lag
# flows through spec unification (which preserves it via ``replace``).
# 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_prefill_steps() - 1
if vllm_config.speculative_config is not None
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 # Get global KV cache groups. This also handles spec unification for
# hybrid models when disable_hybrid_kv_cache_manager is enabled. # hybrid models when disable_hybrid_kv_cache_manager is enabled.
# After this call, merged_kv_cache_specs may be modified in-place. # After this call, merged_kv_cache_specs may be modified in-place.
+5
View File
@@ -233,6 +233,7 @@ class Scheduler(SchedulerInterface):
speculative_config = vllm_config.speculative_config speculative_config = vllm_config.speculative_config
self.use_eagle = False self.use_eagle = False
self.num_spec_tokens = vllm_config.num_speculative_tokens self.num_spec_tokens = vllm_config.num_speculative_tokens
self.num_spec_prefill_steps = 1
self.num_lookahead_tokens = 0 self.num_lookahead_tokens = 0
self.dynamic_sd_lookup: list[int] | None = None self.dynamic_sd_lookup: list[int] | None = None
if speculative_config is not None: if speculative_config is not None:
@@ -245,6 +246,9 @@ class Scheduler(SchedulerInterface):
if speculative_config.use_eagle(): if speculative_config.use_eagle():
self.use_eagle = True self.use_eagle = True
self.num_lookahead_tokens = self.num_spec_tokens self.num_lookahead_tokens = self.num_spec_tokens
self.num_spec_prefill_steps = (
speculative_config.num_speculative_prefill_steps()
)
if speculative_config.uses_draft_model(): if speculative_config.uses_draft_model():
self.num_lookahead_tokens = self.num_spec_tokens self.num_lookahead_tokens = self.num_spec_tokens
if speculative_config.use_dflash(): if speculative_config.use_dflash():
@@ -268,6 +272,7 @@ class Scheduler(SchedulerInterface):
max_in_flight_tokens=vllm_config.max_in_flight_tokens, max_in_flight_tokens=vllm_config.max_in_flight_tokens,
enable_caching=self.cache_config.enable_prefix_caching, enable_caching=self.cache_config.enable_prefix_caching,
use_eagle=self.use_eagle, use_eagle=self.use_eagle,
num_spec_prefill_steps=self.num_spec_prefill_steps,
log_stats=self.log_stats, log_stats=self.log_stats,
enable_kv_cache_events=self.enable_kv_cache_events, enable_kv_cache_events=self.enable_kv_cache_events,
dcp_world_size=self.dcp_world_size, dcp_world_size=self.dcp_world_size,
+14 -1
View File
@@ -853,6 +853,10 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
def __init__(self, kv_cache_spec: SlidingWindowSpec, **kwargs) -> None: def __init__(self, kv_cache_spec: SlidingWindowSpec, **kwargs) -> None:
super().__init__(kv_cache_spec, **kwargs) super().__init__(kv_cache_spec, **kwargs)
self.sliding_window = kv_cache_spec.sliding_window self.sliding_window = kv_cache_spec.sliding_window
# Extra trailing tokens to retain below the window (never attended) so a
# multi-module MTP store-side lag can still reconstruct the window from
# cached blocks.
self.extra_retained_tokens = kv_cache_spec.extra_retained_tokens
@classmethod @classmethod
def _contiguous_blocks_for_hit( def _contiguous_blocks_for_hit(
@@ -1048,13 +1052,22 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
attention computation since they are outside the sliding window. attention computation since they are outside the sliding window.
Thus, get_num_skipped_tokens(7) == 4. Thus, get_num_skipped_tokens(7) == 4.
The trailing edge of the window is extended by ``extra_retained_tokens``
so that those extra trailing tokens' blocks are retained (but not
attended). This is needed for multi-module spec decoding which can
re-prefill the last num_spec_prefill_tokens - 1 tokens from the end
of the sequence, and thus needs to delay freeing/caching of blocks.
Args: Args:
num_computed_tokens: The number of tokens that have been computed. num_computed_tokens: The number of tokens that have been computed.
Returns: Returns:
The number of tokens that will be skipped for attention computation. The number of tokens that will be skipped for attention computation.
""" """
return max(0, num_computed_tokens - self.sliding_window + 1) return max(
0,
num_computed_tokens - self.sliding_window + 1 - self.extra_retained_tokens,
)
def get_num_common_prefix_blocks(self, running_request_id: str) -> int: def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
""" """
+13 -2
View File
@@ -547,6 +547,12 @@ class ChunkedLocalAttentionSpec(AttentionSpec):
class SlidingWindowSpec(AttentionSpec): class SlidingWindowSpec(AttentionSpec):
sliding_window: int sliding_window: int
head_size_v: int = None # type: ignore[assignment] head_size_v: int = None # type: ignore[assignment]
# The trailing edge of the window is extended by ``extra_retained_tokens``
# so that those extra trailing tokens' blocks are retained (but not
# attended). This is needed for multi-module spec decoding which can
# re-prefill the last num_spec_prefill_tokens - 1 tokens from the end
# of the sequence, and thus needs to delay freeing/caching of blocks.
extra_retained_tokens: int = 0
def __post_init__(self): def __post_init__(self):
if self.head_size_v is None: if self.head_size_v is None:
@@ -588,8 +594,13 @@ class SlidingWindowSpec(AttentionSpec):
""" """
# During chunked prefill, we hold KV for the last `sliding_window-1` # During chunked prefill, we hold KV for the last `sliding_window-1`
# computed tokens plus the in-flight tokens (frees happen on the # computed tokens plus the in-flight tokens (frees happen on the
# processed-token basis); never more than `max_model_len`. # processed-token basis); never more than `max_model_len`. An additional
num_tokens = min(self.sliding_window - 1 + max_in_flight_tokens, max_model_len) # `extra_retained_tokens` trailing tokens are kept alive below the
# window for multi-module spec decoding, and must be accounted here too.
num_tokens = min(
self.sliding_window - 1 + self.extra_retained_tokens + max_in_flight_tokens,
max_model_len,
)
# +1 because the sliding window may not start from the beginning of # +1 because the sliding window may not start from the beginning of
# the block. E.g. block size 4 and num_token 4 needs two blocks # the block. E.g. block size 4 and num_token 4 needs two blocks
# [XXCD][EF] to store the 6-token window [CDEF]. # [XXCD][EF] to store the 6-token window [CDEF].
@@ -10,6 +10,7 @@ from vllm.forward_context import BatchDescriptor, set_forward_context
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.multimodal import MULTIMODAL_REGISTRY from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.triton_utils import tl, triton from vllm.triton_utils import tl, triton
from vllm.v1.attention.backends.utils import PAD_SLOT_ID
from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
from vllm.v1.worker.gpu.cudagraph_utils import ( from vllm.v1.worker.gpu.cudagraph_utils import (
BatchExecutionDescriptor, BatchExecutionDescriptor,
@@ -40,6 +41,17 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
self.supports_mm_inputs = MULTIMODAL_REGISTRY.supports_multimodal_inputs( self.supports_mm_inputs = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
self.draft_model_config self.draft_model_config
) )
# HACK: the Inkling MTP draft has no MM processor of its own (its draft
# config is flattened text-only), but it consumes the target's merged
# embeddings at draft prefill — treat it as MM-capable whenever the
# target is.
if (
not self.supports_mm_inputs
and self.draft_model_config.hf_config.model_type == "inkling_mtp"
):
self.supports_mm_inputs = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
vllm_config.model_config
)
if self.supports_mm_inputs: if self.supports_mm_inputs:
self.inputs_embeds = torch.zeros( self.inputs_embeds = torch.zeros(
self.max_num_tokens, self.hidden_size, dtype=self.dtype, device=device self.max_num_tokens, self.hidden_size, dtype=self.dtype, device=device
@@ -48,6 +60,23 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
self.prefill_cudagraph_manager: SpeculatorCudaGraphManager | None = None self.prefill_cudagraph_manager: SpeculatorCudaGraphManager | None = None
self.decode_cudagraph_manager: SpeculatorCudaGraphManager | None = None self.decode_cudagraph_manager: SpeculatorCudaGraphManager | None = None
self.num_spec_prefill_steps = (
self.speculative_config.num_speculative_prefill_steps()
)
self.cached_draft_input_ids = torch.zeros(
self.max_num_reqs,
self.num_spec_prefill_steps - 1,
dtype=torch.int64,
device=self.device,
)
self.cached_target_hidden_states = torch.zeros(
self.max_num_reqs,
self.num_spec_prefill_steps - 1,
self.hidden_size,
dtype=self.dtype,
device=self.device,
)
@property @property
def advance_draft_positions(self) -> bool: def advance_draft_positions(self) -> bool:
""" """
@@ -92,23 +121,29 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
# For FULL graphs, the entire routine is recorded as one graph. # For FULL graphs, the entire routine is recorded as one graph.
# For PIECEWISE, only the model's compiled regions are captured # For PIECEWISE, only the model's compiled regions are captured
# and the rest (compute_logits, gumbel_sample) runs eagerly. # and the rest (compute_logits, gumbel_sample) runs eagerly.
# Draft prefill reuses the target model's attention metadata at # When num_spec_prefill_steps > 1 (e.g. multi-module MTP), the
# runtime, so capture builds its dummy metadata through the target # speculator builds its attention metadata using its own builders
# model runner's builders and buffers. # and buffers. Otherwise, the target model's attention metadata
# can be reused, so capture builds its dummy metadata through the
# target model runner's builders and buffers.
assert self.prefill_cudagraph_manager is not None assert self.prefill_cudagraph_manager is not None
if self.prefill_cudagraph_manager.use_breakable_cg: if self.prefill_cudagraph_manager.use_breakable_cg:
self.prefill_cudagraph_manager.init_breakable_cg_runner(self.model) self.prefill_cudagraph_manager.init_breakable_cg_runner(self.model)
self.prefill_cudagraph_manager.capture( self.prefill_cudagraph_manager.capture(
self._prefill, self._prefill,
self.model_state, self.model_state,
self.target_input_buffers, self.input_buffers
if self.num_spec_prefill_steps > 1
else self.target_input_buffers,
self.block_tables, self.block_tables,
self.target_attn_groups, self.attn_groups
if self.num_spec_prefill_steps > 1
else self.target_attn_groups,
self.kv_cache_config, self.kv_cache_config,
progress_bar_desc="Capturing prefill CUDA graphs", progress_bar_desc="Capturing prefill CUDA graphs",
) )
if self.num_speculative_steps == 1: if self.num_speculative_steps <= self.num_spec_prefill_steps:
return return
# Capture the decode draft generation routine (model forward + # Capture the decode draft generation routine (model forward +
@@ -116,7 +151,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
# step. # step.
assert self.decode_cudagraph_manager is not None assert self.decode_cudagraph_manager is not None
self.decode_cudagraph_manager.capture( self.decode_cudagraph_manager.capture(
self._generate_draft, self._decode,
self.model_state, self.model_state,
self.input_buffers, self.input_buffers,
self.block_tables, self.block_tables,
@@ -160,6 +195,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
self.draft_max_seq_len = min( self.draft_max_seq_len = min(
max_seq_len + self.num_speculative_steps, self.max_model_len max_seq_len + self.num_speculative_steps, self.max_model_len
) )
skip_attn = dummy_run and skip_attn_for_dummy_run
# NOTE(woosuk): To avoid CPU-GPU synchronization without CPU knowing the # NOTE(woosuk): To avoid CPU-GPU synchronization without CPU knowing the
# number of rejected tokens, we maintain the size of input_ids and # number of rejected tokens, we maintain the size of input_ids and
@@ -174,7 +210,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
) )
else: else:
hidden_states = last_hidden_states hidden_states = last_hidden_states
self.hidden_states[:num_tokens].copy_(hidden_states)
self._copy_request_inputs( self._copy_request_inputs(
num_reqs, num_reqs,
@@ -183,16 +218,21 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
seeds, seeds,
) )
# Get the input ids and last token indices for the speculator. # Get the input ids, last token indices, and input hidden states for
# the speculator.
prepare_prefill_inputs( prepare_prefill_inputs(
self.last_token_indices, self.last_token_indices,
self.current_draft_step, self.hidden_states,
self.input_buffers, self.input_buffers,
hidden_states,
self.cached_target_hidden_states,
self.cached_draft_input_ids,
input_batch, input_batch,
num_sampled, num_sampled,
num_rejected, num_rejected,
last_sampled, last_sampled,
next_prefill_tokens, next_prefill_tokens,
self.num_spec_prefill_steps,
self.max_num_reqs, self.max_num_reqs,
) )
@@ -215,6 +255,38 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
need_eager=is_profile, need_eager=is_profile,
) )
if not skip_attn and self.num_spec_prefill_steps > 1:
# Re-prefill shifts the draft positions/seq_lens, so the target's
# slot mappings and attention metadata can no longer be reused.
# Rebuild both from the draft buffers written by
# prepare_prefill_inputs, keeping the target's (non-uniform) query
# layout since the draft query_start_loc mirrors the target's.
index_mapping = self.idx_mapping[:num_reqs]
last_token_indices = self.last_token_indices[:num_reqs]
slot_mappings_tensor = self.block_tables.compute_slot_mappings(
index_mapping,
self.input_buffers.query_start_loc,
self.input_buffers.positions,
prefill_batch_desc.num_tokens,
)
# Apply padding values to slots not corresponding to real draft
# tokens to prevent stale value writes.
pad_trailing_draft_slots(
slot_mappings_tensor,
self.input_buffers.query_start_loc,
last_token_indices,
num_reqs,
)
slot_mappings = build_slot_mappings_by_layer(
slot_mappings_tensor, self.kv_cache_config
)
attn_metadata = self._build_draft_attn_metadata(
num_reqs=num_reqs,
num_reqs_padded=prefill_batch_desc.num_reqs or num_reqs,
num_tokens_padded=prefill_batch_desc.num_tokens,
query_start_loc_np=input_batch.query_start_loc_np,
)
self._prepare_eplb_forward(input_batch.num_tokens) self._prepare_eplb_forward(input_batch.num_tokens)
if prefill_batch_desc.cg_mode == CUDAGraphMode.FULL: if prefill_batch_desc.cg_mode == CUDAGraphMode.FULL:
@@ -235,9 +307,9 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
mm_inputs=mm_inputs, mm_inputs=mm_inputs,
) )
if self.num_speculative_steps == 1: if self.num_speculative_steps <= self.num_spec_prefill_steps:
# Early exit. # Early exit.
return self.draft_tokens[:num_reqs, :1] return self.draft_tokens[:num_reqs, : self.num_speculative_steps]
# Prepare the inputs for the decode steps. # Prepare the inputs for the decode steps.
prepare_decode_inputs( prepare_decode_inputs(
@@ -265,7 +337,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
# Generate the remaining num_speculative_steps - 1 draft tokens. # Generate the remaining num_speculative_steps - 1 draft tokens.
self._multi_step_decode( self._multi_step_decode(
num_reqs, num_reqs,
dummy_run and skip_attn_for_dummy_run, skip_attn,
decode_batch_desc, decode_batch_desc,
num_tokens_across_dp, num_tokens_across_dp,
) )
@@ -281,6 +353,8 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
num_tokens_across_dp: torch.Tensor | None, num_tokens_across_dp: torch.Tensor | None,
cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE, cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None, mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
embed_mm: bool = False,
spec_module_idx: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor]:
batch_descriptor = BatchDescriptor(num_tokens=num_tokens) batch_descriptor = BatchDescriptor(num_tokens=num_tokens)
with set_forward_context( with set_forward_context(
@@ -293,7 +367,11 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
batch_descriptor=batch_descriptor, batch_descriptor=batch_descriptor,
): ):
inputs_embeds = None inputs_embeds = None
if self.supports_mm_inputs: # Only draft prefill consumes (target-merged) MM embeddings; the
# decode steps always draft text tokens and keep the raw-ids path
# (per-callsite, not data-dependent, so each cudagraph family
# captures the branch it will replay).
if self.supports_mm_inputs and embed_mm:
# Merge multimodal embeddings with input ids. # Merge multimodal embeddings with input ids.
mm_embeds, is_mm_embed = mm_inputs or (None, None) mm_embeds, is_mm_embed = mm_inputs or (None, None)
num_input_tokens = ( num_input_tokens = (
@@ -312,6 +390,10 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
hidden_states=self.hidden_states[:num_tokens], hidden_states=self.hidden_states[:num_tokens],
inputs_embeds=inputs_embeds, inputs_embeds=inputs_embeds,
) )
if spec_module_idx > 0:
# Pass the speculative module index to the model to indicate which
# module to use for the forward pass.
model_inputs["spec_step_idx"] = spec_module_idx
if cudagraph_runtime_mode == CUDAGraphMode.PIECEWISE: if cudagraph_runtime_mode == CUDAGraphMode.PIECEWISE:
# Draft prefill with PIECEWISE cudagraph (compiled PW or breakable), # Draft prefill with PIECEWISE cudagraph (compiled PW or breakable),
# chosen inside run_pw_graph. # chosen inside run_pw_graph.
@@ -342,33 +424,67 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None, mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
) -> None: ) -> None:
last_token_indices = self.last_token_indices[:num_reqs] last_token_indices = self.last_token_indices[:num_reqs]
positions = self.input_buffers.positions[last_token_indices] sample_positions = self.input_buffers.positions[last_token_indices]
idx_mapping = self.idx_mapping[:num_reqs] idx_mapping = self.idx_mapping[:num_reqs]
last_hidden_states, hidden_states = self._run_model( if self.num_spec_prefill_steps > 1:
num_tokens, # Snapshot the trailing draft tokens/hidden states (for the next
attn_metadata, # prefill's re-prefill gap) before the step loop overwrites them.
slot_mappings, cache_prefill_state(
num_tokens_across_dp=num_tokens_across_dp, self.input_buffers,
cudagraph_runtime_mode=cudagraph_runtime_mode, self.hidden_states,
mm_inputs=mm_inputs, self.cached_draft_input_ids,
) self.cached_target_hidden_states,
sample_hidden_states = last_hidden_states[last_token_indices] last_token_indices,
idx_mapping,
num_reqs,
self.num_spec_prefill_steps,
)
for step in range(self.num_spec_prefill_steps):
# Update the current draft step.
self.current_draft_step.fill_(step)
# Run the model forward pass.
last_hidden_states, hidden_states = self._run_model(
num_tokens,
attn_metadata,
slot_mappings,
num_tokens_across_dp=num_tokens_across_dp,
cudagraph_runtime_mode=cudagraph_runtime_mode,
mm_inputs=mm_inputs,
embed_mm=True,
spec_module_idx=step,
)
# Sample draft tokens for the current step.
sample_hidden_states = last_hidden_states[last_token_indices]
draft_tokens = self.sample_draft(
sample_hidden_states,
sample_positions,
idx_mapping,
self.temperature,
self.seeds,
self.current_draft_step,
self.draft_logits,
)
self.draft_tokens[:num_reqs, step] = draft_tokens
if step < self.num_spec_prefill_steps - 1:
self.hidden_states[:num_tokens] = hidden_states
update_prefill_inputs(
draft_tokens,
self.input_buffers,
last_token_indices,
num_reqs,
)
sample_positions += 1
self.draft_tokens[:num_reqs, 0] = self.sample_draft(
sample_hidden_states,
positions,
idx_mapping,
self.temperature,
self.seeds,
self.current_draft_step,
self.draft_logits,
)
if last_hidden_states is hidden_states: if last_hidden_states is hidden_states:
self.hidden_states[:num_reqs] = sample_hidden_states self.hidden_states[:num_reqs] = sample_hidden_states
else: else:
self.hidden_states[:num_reqs] = hidden_states[last_token_indices] self.hidden_states[:num_reqs] = hidden_states[last_token_indices]
self.input_buffers.positions[:num_reqs] = positions self.input_buffers.positions[:num_reqs] = sample_positions
def _multi_step_decode( def _multi_step_decode(
self, self,
@@ -383,7 +499,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
attn_metadata = None attn_metadata = None
slot_mappings_by_layer = None slot_mappings_by_layer = None
for step in range(1, self.num_speculative_steps): for step in range(self.num_spec_prefill_steps, self.num_speculative_steps):
# Rebuild every step when positions advance, or just once # Rebuild every step when positions advance, or just once
# on the first step when positions are constant (Gemma4 MTP). # on the first step when positions are constant (Gemma4 MTP).
if not skip_attn and (self.advance_draft_positions or step == 1): if not skip_attn and (self.advance_draft_positions or step == 1):
@@ -396,7 +512,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
slot_mappings_by_layer = build_slot_mappings_by_layer( slot_mappings_by_layer = build_slot_mappings_by_layer(
slot_mappings, self.kv_cache_config slot_mappings, self.kv_cache_config
) )
attn_metadata = self._build_draft_attn_metadata( attn_metadata = self._build_uniform_draft_attn_metadata(
num_reqs=num_reqs, num_reqs=num_reqs,
num_reqs_padded=batch_desc.num_reqs or num_reqs, num_reqs_padded=batch_desc.num_reqs or num_reqs,
num_tokens_padded=batch_desc.num_tokens, num_tokens_padded=batch_desc.num_tokens,
@@ -410,7 +526,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
assert self.decode_cudagraph_manager is not None assert self.decode_cudagraph_manager is not None
self.decode_cudagraph_manager.run_fullgraph(batch_desc) self.decode_cudagraph_manager.run_fullgraph(batch_desc)
else: else:
self._generate_draft( self._decode(
num_reqs, num_reqs,
batch_desc.num_tokens, batch_desc.num_tokens,
attn_metadata, attn_metadata,
@@ -419,7 +535,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
cudagraph_runtime_mode=batch_desc.cg_mode, cudagraph_runtime_mode=batch_desc.cg_mode,
) )
def _generate_draft( def _decode(
self, self,
num_reqs: int, num_reqs: int,
num_tokens_padded: int, num_tokens_padded: int,
@@ -471,13 +587,14 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
@triton.jit @triton.jit
def _prepare_prefill_inputs_kernel( def _prepare_prefill_inputs_kernel(
last_token_indices_ptr, last_token_indices_ptr,
draft_current_step_ptr,
draft_input_ids_ptr, draft_input_ids_ptr,
draft_positions_ptr, draft_positions_ptr,
draft_query_start_loc_ptr, draft_query_start_loc_ptr,
draft_seq_lens_ptr, draft_seq_lens_ptr,
target_input_ids_ptr, target_input_ids_ptr,
target_positions_ptr, target_positions_ptr,
cached_draft_input_ids_ptr,
cached_draft_input_ids_stride0,
idx_mapping_ptr, idx_mapping_ptr,
last_sampled_ptr, last_sampled_ptr,
next_prefill_tokens_ptr, next_prefill_tokens_ptr,
@@ -486,6 +603,7 @@ def _prepare_prefill_inputs_kernel(
query_start_loc_ptr, query_start_loc_ptr,
seq_lens_ptr, seq_lens_ptr,
max_num_reqs, max_num_reqs,
num_spec_prefill_steps,
BLOCK_SIZE: tl.constexpr, BLOCK_SIZE: tl.constexpr,
): ):
req_idx = tl.program_id(0) req_idx = tl.program_id(0)
@@ -500,6 +618,7 @@ def _prepare_prefill_inputs_kernel(
# Get the true query length and next token after accounting for rejected tokens. # Get the true query length and next token after accounting for rejected tokens.
num_rejected = tl.load(num_rejected_ptr + req_idx) num_rejected = tl.load(num_rejected_ptr + req_idx)
query_len -= num_rejected query_len -= num_rejected
num_reprefills = 0 if num_spec_prefill_steps == 1 else max(0, num_rejected - 1)
num_sampled = tl.load(num_sampled_ptr + req_idx) num_sampled = tl.load(num_sampled_ptr + req_idx)
if num_sampled > 0: if num_sampled > 0:
@@ -509,31 +628,57 @@ def _prepare_prefill_inputs_kernel(
# Get the next prefill token. # Get the next prefill token.
next_token = tl.load(next_prefill_tokens_ptr + req_state_idx) next_token = tl.load(next_prefill_tokens_ptr + req_state_idx)
# Shift target_input_ids by one. # Shift target_input_ids by one + the number of tokens to be re-prefilled.
for i in range(1, query_len, BLOCK_SIZE): for i in range(1, query_len, BLOCK_SIZE):
block = i + tl.arange(0, BLOCK_SIZE) block = i + tl.arange(0, BLOCK_SIZE)
mask = block < query_len mask = block < query_len
input_ids = tl.load(target_input_ids_ptr + query_start + block, mask=mask) input_ids = tl.load(target_input_ids_ptr + query_start + block, mask=mask)
tl.store(draft_input_ids_ptr + query_start + block - 1, input_ids, mask=mask) tl.store(
draft_input_ids_ptr + query_start + block - 1 + num_reprefills,
input_ids,
mask=mask,
)
last_token_index = query_start + query_len - 1 last_token_index = query_start + query_len - 1 + num_reprefills
tl.store(last_token_indices_ptr + req_idx, last_token_index) tl.store(last_token_indices_ptr + req_idx, last_token_index)
tl.store(draft_input_ids_ptr + last_token_index, next_token) tl.store(draft_input_ids_ptr + last_token_index, next_token)
# Copy positions. # Copy positions, shifted over by the number of tokens to be re-prefilled.
for i in range(0, query_len, BLOCK_SIZE): for i in range(0, query_len, BLOCK_SIZE):
block = i + tl.arange(0, BLOCK_SIZE) block = i + tl.arange(0, BLOCK_SIZE)
mask = block < query_len mask = block < query_len
target_pos = tl.load(target_positions_ptr + query_start + block, mask=mask) target_pos = tl.load(target_positions_ptr + query_start + block, mask=mask)
tl.store(draft_positions_ptr + query_start + block, target_pos, mask=mask) tl.store(
draft_positions_ptr + query_start + block + num_reprefills,
target_pos,
mask=mask,
)
# Fill the re-prefill gap with the cached token ids from the previous
# decode step. These tokens sit immediately before the query's first
# token, so their positions are contiguous and derived here.
first_position = tl.load(target_positions_ptr + query_start)
for i in range(num_reprefills):
cache_read_slot = num_spec_prefill_steps - 1 - num_reprefills + i
cached_token_id = tl.load(
cached_draft_input_ids_ptr
+ req_state_idx * cached_draft_input_ids_stride0
+ cache_read_slot
)
tl.store(draft_input_ids_ptr + query_start + i, cached_token_id)
tl.store(
draft_positions_ptr + query_start + i,
first_position - num_reprefills + i,
)
# Copy query start locations. # Copy query start locations.
tl.store(draft_query_start_loc_ptr + req_idx, query_start) tl.store(draft_query_start_loc_ptr + req_idx, query_start)
# Copy sequence lengths. # Copy sequence lengths. Re-prefilled tokens are packed into the query
tl.store(draft_seq_lens_ptr + req_idx, seq_len) # window without widening it, so the effective KV length shrinks by
# num_reprefills to keep RoPE positions aligned with the attention's
# implicit (seq_len - query_len) causal positions.
tl.store(draft_seq_lens_ptr + req_idx, seq_len - num_reprefills)
if req_idx == (num_reqs - 1): if req_idx == (num_reqs - 1):
# Reset the current draft step to 0.
tl.store(draft_current_step_ptr, 0)
# Pad query_start_loc for CUDA graphs. # Pad query_start_loc for CUDA graphs.
for i in range(num_reqs, max_num_reqs + 1, BLOCK_SIZE): for i in range(num_reqs, max_num_reqs + 1, BLOCK_SIZE):
block = i + tl.arange(0, BLOCK_SIZE) block = i + tl.arange(0, BLOCK_SIZE)
@@ -551,11 +696,126 @@ def _prepare_prefill_inputs_kernel(
tl.store(last_token_indices_ptr + block, 0, mask=mask) tl.store(last_token_indices_ptr + block, 0, mask=mask)
@triton.jit
def _prepare_prefill_hidden_states_kernel(
draft_input_hidden_states_ptr,
draft_input_hidden_states_stride0,
target_hidden_states_ptr,
target_hidden_states_stride0,
cached_target_hidden_states_ptr,
cached_target_hidden_states_stride0,
cached_target_hidden_states_stride1,
idx_mapping_ptr,
num_rejected_ptr,
query_start_loc_ptr,
num_spec_prefill_steps,
hidden_size,
BLOCK_SIZE: tl.constexpr,
):
req_idx = tl.program_id(0)
block_idx = tl.program_id(1)
block = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = block < hidden_size
req_state_idx = tl.load(idx_mapping_ptr + req_idx)
query_start = tl.load(query_start_loc_ptr + req_idx)
query_end = tl.load(query_start_loc_ptr + req_idx + 1)
query_len = query_end - query_start
num_rejected = tl.load(num_rejected_ptr + req_idx)
query_len -= num_rejected
num_reprefills = 0 if num_spec_prefill_steps == 1 else max(0, num_rejected - 1)
# Fill the re-prefill gap with the cached hidden states from the previous
# decode step, mirroring the token ids and positions inserted by
# _prepare_prefill_inputs_kernel.
for i in range(num_reprefills):
cache_read_slot = num_spec_prefill_steps - 1 - num_reprefills + i
cached_hidden_state = tl.load(
cached_target_hidden_states_ptr
+ req_state_idx * cached_target_hidden_states_stride0
+ cache_read_slot * cached_target_hidden_states_stride1
+ block,
mask=mask,
)
tl.store(
draft_input_hidden_states_ptr
+ (query_start + i) * draft_input_hidden_states_stride0
+ block,
cached_hidden_state,
mask=mask,
)
for i in range(query_len):
hidden_state = tl.load(
target_hidden_states_ptr
+ (query_start + i) * target_hidden_states_stride0
+ block,
mask=mask,
)
tl.store(
draft_input_hidden_states_ptr
+ (query_start + num_reprefills + i) * draft_input_hidden_states_stride0
+ block,
hidden_state,
mask=mask,
)
@triton.jit
def _pad_trailing_draft_slots_kernel(
slot_mappings_ptr,
slot_mappings_stride0,
query_start_loc_ptr,
last_token_indices_ptr,
PAD_ID,
BLOCK_SIZE: tl.constexpr,
):
group_idx = tl.program_id(0)
req_idx = tl.program_id(1)
# Slots computed from stale token positions in the range
# [last_token_index + 1, query_end) can result in writes to blocks.
# Pad these slot values so that attention kernels ignore them.
start = tl.load(last_token_indices_ptr + req_idx) + 1
end = tl.load(query_start_loc_ptr + req_idx + 1)
base = slot_mappings_ptr + group_idx * slot_mappings_stride0
for i in range(start, end, BLOCK_SIZE):
offs = i + tl.arange(0, BLOCK_SIZE)
mask = offs < end
tl.store(base + offs, PAD_ID, mask=mask)
def pad_trailing_draft_slots(
# [num_groups, num_tokens_padded]
slot_mappings: torch.Tensor,
# [num_reqs + 1]
query_start_loc: torch.Tensor,
# [num_reqs]
last_token_indices: torch.Tensor,
num_reqs: int,
) -> None:
num_groups = slot_mappings.shape[0]
_pad_trailing_draft_slots_kernel[(num_groups, num_reqs)](
slot_mappings,
slot_mappings.stride(0),
query_start_loc,
last_token_indices,
PAD_SLOT_ID,
BLOCK_SIZE=256,
)
def prepare_prefill_inputs( def prepare_prefill_inputs(
# [num_reqs] # [num_reqs]
last_token_indices: torch.Tensor, last_token_indices: torch.Tensor,
current_draft_step: torch.Tensor, draft_input_hidden_states: torch.Tensor,
input_buffers: InputBuffers, input_buffers: InputBuffers,
target_hidden_states: torch.Tensor,
# [max_num_reqs, num_spec_prefill_steps - 1, hidden_size]
cached_target_hidden_states: torch.Tensor | None,
# [max_num_reqs, num_spec_prefill_steps - 1]
cached_draft_input_ids: torch.Tensor | None,
input_batch: InputBatch, input_batch: InputBatch,
# [num_reqs] # [num_reqs]
num_sampled: torch.Tensor, num_sampled: torch.Tensor,
@@ -565,18 +825,21 @@ def prepare_prefill_inputs(
last_sampled: torch.Tensor, last_sampled: torch.Tensor,
# [max_num_reqs] # [max_num_reqs]
next_prefill_tokens: torch.Tensor, next_prefill_tokens: torch.Tensor,
num_spec_prefill_steps,
max_num_reqs, max_num_reqs,
) -> torch.Tensor: ) -> torch.Tensor:
num_reqs = input_batch.num_reqs num_reqs = input_batch.num_reqs
hidden_size = target_hidden_states.shape[-1]
_prepare_prefill_inputs_kernel[(num_reqs,)]( _prepare_prefill_inputs_kernel[(num_reqs,)](
last_token_indices, last_token_indices,
current_draft_step,
input_buffers.input_ids, input_buffers.input_ids,
input_buffers.positions, input_buffers.positions,
input_buffers.query_start_loc, input_buffers.query_start_loc,
input_buffers.seq_lens, input_buffers.seq_lens,
input_batch.input_ids, input_batch.input_ids,
input_batch.positions, input_batch.positions,
cached_draft_input_ids,
cached_draft_input_ids.stride(0) if cached_draft_input_ids is not None else 0,
input_batch.idx_mapping, input_batch.idx_mapping,
last_sampled, last_sampled,
next_prefill_tokens, next_prefill_tokens,
@@ -585,11 +848,176 @@ def prepare_prefill_inputs(
input_batch.query_start_loc, input_batch.query_start_loc,
input_batch.seq_lens, input_batch.seq_lens,
max_num_reqs, max_num_reqs,
num_spec_prefill_steps,
BLOCK_SIZE=1024, BLOCK_SIZE=1024,
) )
if num_spec_prefill_steps > 1:
hidden_block_size = 1024
num_dim_blocks = triton.cdiv(hidden_size, hidden_block_size)
_prepare_prefill_hidden_states_kernel[(num_reqs, num_dim_blocks)](
draft_input_hidden_states,
draft_input_hidden_states.stride(0),
target_hidden_states,
target_hidden_states.stride(0),
cached_target_hidden_states,
cached_target_hidden_states.stride(0)
if cached_target_hidden_states is not None
else 0,
cached_target_hidden_states.stride(1)
if cached_target_hidden_states is not None
else 0,
input_batch.idx_mapping,
num_rejected,
input_batch.query_start_loc,
num_spec_prefill_steps,
hidden_size,
BLOCK_SIZE=hidden_block_size,
)
else:
num_tokens = input_batch.num_tokens
draft_input_hidden_states[:num_tokens].copy_(target_hidden_states[:num_tokens])
return last_token_indices return last_token_indices
@triton.jit
def _cache_prefill_state_kernel(
draft_input_ids_ptr,
draft_input_hidden_states_ptr,
draft_input_hidden_states_stride0,
cached_draft_input_ids_ptr,
cached_draft_input_ids_stride0,
cached_target_hidden_states_ptr,
cached_target_hidden_states_stride0,
cached_target_hidden_states_stride1,
idx_mapping_ptr,
last_token_indices_ptr,
query_start_loc_ptr,
num_spec_prefill_steps,
hidden_size,
BLOCK_SIZE: tl.constexpr,
):
req_idx = tl.program_id(0)
block_idx = tl.program_id(1)
block = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = block < hidden_size
req_state_idx = tl.load(idx_mapping_ptr + req_idx)
if req_state_idx < 0:
# Skip cudagraph padded requests.
return
query_start = tl.load(query_start_loc_ptr + req_idx)
last_token_index = tl.load(last_token_indices_ptr + req_idx)
# Snapshot the last num_spec_prefill_steps - 1 input draft tokens/hidden
# states, indexed by request state, so the next prefill can re-prefill
# rejected positions.
cache_window_size = num_spec_prefill_steps - 1
window_start = last_token_index - cache_window_size + 1
for i in range(max(window_start, query_start), last_token_index + 1):
cache_write_slot = i - window_start
if block_idx == 0:
input_id = tl.load(draft_input_ids_ptr + i)
tl.store(
cached_draft_input_ids_ptr
+ req_state_idx * cached_draft_input_ids_stride0
+ cache_write_slot,
input_id,
)
hidden_state = tl.load(
draft_input_hidden_states_ptr
+ i * draft_input_hidden_states_stride0
+ block,
mask=mask,
)
tl.store(
cached_target_hidden_states_ptr
+ req_state_idx * cached_target_hidden_states_stride0
+ cache_write_slot * cached_target_hidden_states_stride1
+ block,
hidden_state,
mask=mask,
)
def cache_prefill_state(
input_buffers: InputBuffers,
# [num_tokens, hidden_size]
draft_input_hidden_states: torch.Tensor,
# [max_num_reqs, num_spec_prefill_steps - 1]
cached_draft_input_ids: torch.Tensor,
# [max_num_reqs, num_spec_prefill_steps - 1, hidden_size]
cached_target_hidden_states: torch.Tensor,
# [num_reqs]
last_token_indices: torch.Tensor,
# [num_reqs]
idx_mapping: torch.Tensor,
num_reqs: int,
num_spec_prefill_steps: int,
) -> None:
hidden_size = draft_input_hidden_states.shape[-1]
hidden_block_size = 1024
_cache_prefill_state_kernel[
(num_reqs, triton.cdiv(hidden_size, hidden_block_size))
](
input_buffers.input_ids,
draft_input_hidden_states,
draft_input_hidden_states.stride(0),
cached_draft_input_ids,
cached_draft_input_ids.stride(0),
cached_target_hidden_states,
cached_target_hidden_states.stride(0),
cached_target_hidden_states.stride(1),
idx_mapping,
last_token_indices,
input_buffers.query_start_loc,
num_spec_prefill_steps,
hidden_size,
BLOCK_SIZE=hidden_block_size,
)
@triton.jit
def _update_prefill_inputs_kernel(
input_ids_ptr,
query_start_loc_ptr,
last_token_indices_ptr,
draft_tokens_ptr,
BLOCK_SIZE: tl.constexpr,
):
req_idx = tl.program_id(0)
query_start = tl.load(query_start_loc_ptr + req_idx)
# Use the post-rejection last token index so the shift and insertion align
# with the position the draft token was sampled from.
last_token_index = tl.load(last_token_indices_ptr + req_idx)
query_len = last_token_index - query_start + 1
# Shift input token ids to the left by one position and
# insert the last sampled draft token.
for i in range(1, query_len, BLOCK_SIZE):
block = i + tl.arange(0, BLOCK_SIZE)
mask = block < query_len
input_ids = tl.load(input_ids_ptr + query_start + block, mask=mask)
tl.store(input_ids_ptr + query_start + block - 1, input_ids, mask=mask)
draft_token = tl.load(draft_tokens_ptr + req_idx)
tl.store(input_ids_ptr + last_token_index, draft_token)
def update_prefill_inputs(
draft_tokens: torch.Tensor,
input_buffers: InputBuffers,
last_token_indices: torch.Tensor,
num_reqs: int,
) -> None:
_update_prefill_inputs_kernel[(num_reqs,)](
input_buffers.input_ids,
input_buffers.query_start_loc,
last_token_indices,
draft_tokens,
BLOCK_SIZE=1024,
)
@triton.jit @triton.jit
def _prepare_decode_inputs_kernel( def _prepare_decode_inputs_kernel(
draft_tokens_ptr, draft_tokens_ptr,
@@ -264,7 +264,7 @@ class DFlashSpeculator(DraftModelSpeculator):
num_reqs, self.num_speculative_steps num_reqs, self.num_speculative_steps
) )
def _build_draft_attn_metadata( def _build_uniform_draft_attn_metadata(
self, self,
num_reqs: int, num_reqs: int,
num_reqs_padded: int, num_reqs_padded: int,
@@ -275,7 +275,7 @@ class DFlashSpeculator(DraftModelSpeculator):
if not self.draft_attn_layer_names: if not self.draft_attn_layer_names:
return None return None
assert num_query_per_req is None # Omitted for DFlash, read from self instead assert num_query_per_req is None # Omitted for DFlash, read from self instead
return super()._build_draft_attn_metadata( return super()._build_uniform_draft_attn_metadata(
num_reqs, num_reqs,
num_reqs_padded, num_reqs_padded,
num_tokens_padded, num_tokens_padded,
@@ -423,7 +423,7 @@ class DFlashSpeculator(DraftModelSpeculator):
# Rebuild the draft attention metadata even when replaying the FULL # Rebuild the draft attention metadata even when replaying the FULL
# graph so that any attention metadata builder state is updated. # graph so that any attention metadata builder state is updated.
draft_attn_metadata = self._build_draft_attn_metadata( draft_attn_metadata = self._build_uniform_draft_attn_metadata(
num_reqs=num_reqs, num_reqs=num_reqs,
num_reqs_padded=num_reqs_padded, num_reqs_padded=num_reqs_padded,
num_tokens_padded=num_tokens_padded, num_tokens_padded=num_tokens_padded,
+30 -15
View File
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any from typing import Any
import numpy as np
import torch import torch
import torch.nn as nn import torch.nn as nn
@@ -122,9 +123,7 @@ class DraftModelSpeculator(BaseSpeculator):
dtype=torch.int64, dtype=torch.int64,
device=device, device=device,
) )
self.arange = torch.arange( self.np_arange = np.arange(self.max_num_reqs + 1, dtype=np.int32)
self.max_num_reqs + 1, dtype=torch.int32, device="cpu"
)
self.draft_logits: torch.Tensor | None = None self.draft_logits: torch.Tensor | None = None
if self.speculative_config.draft_sample_method == "probabilistic": if self.speculative_config.draft_sample_method == "probabilistic":
@@ -205,7 +204,7 @@ class DraftModelSpeculator(BaseSpeculator):
self.target_input_buffers = target_input_buffers self.target_input_buffers = target_input_buffers
self.target_attn_groups = target_attn_groups self.target_attn_groups = target_attn_groups
def _build_draft_attn_metadata( def _build_uniform_draft_attn_metadata(
self, self,
num_reqs: int, num_reqs: int,
num_reqs_padded: int, num_reqs_padded: int,
@@ -213,13 +212,30 @@ class DraftModelSpeculator(BaseSpeculator):
num_query_per_req: int = 1, num_query_per_req: int = 1,
causal: bool | Mapping[int, bool] = True, causal: bool | Mapping[int, bool] = True,
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
# Uniform query: query_start_loc[i] = min(i, num_reqs) * num_query_per_req. return self._build_draft_attn_metadata(
# Clamp keeps the series non-decreasing past num_reqs, which some num_reqs,
# attention backends require. num_reqs_padded,
query_start_loc_cpu = ( num_tokens_padded,
torch.clamp(self.arange[: num_reqs_padded + 1], max=num_reqs) self.np_arange[: num_reqs_padded + 1] * num_query_per_req,
* num_query_per_req causal=causal,
) )
def _build_draft_attn_metadata(
self,
num_reqs: int,
num_reqs_padded: int,
num_tokens_padded: int,
query_start_loc_np: np.ndarray,
causal: bool | Mapping[int, bool] = True,
) -> dict[str, Any]:
# Fresh tensor: no aliasing with the caller's array, sized to the padded
# request count, with the tail clamped to the last real cumulative value.
query_start_loc_cpu = torch.empty(num_reqs_padded + 1, dtype=torch.int32)
query_start_loc_cpu[: num_reqs + 1] = torch.from_numpy(
query_start_loc_np[: num_reqs + 1]
)
query_start_loc_cpu[num_reqs:] = query_start_loc_cpu[num_reqs]
max_query_len = int((query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]).max())
block_tables = [ block_tables = [
x[:num_reqs_padded] for x in self.block_tables.input_block_tables x[:num_reqs_padded] for x in self.block_tables.input_block_tables
] ]
@@ -232,7 +248,7 @@ class DraftModelSpeculator(BaseSpeculator):
: num_reqs_padded + 1 : num_reqs_padded + 1
], ],
query_start_loc_cpu=query_start_loc_cpu, query_start_loc_cpu=query_start_loc_cpu,
max_query_len=num_query_per_req, max_query_len=max_query_len,
seq_lens=self.input_buffers.seq_lens[:num_reqs_padded], seq_lens=self.input_buffers.seq_lens[:num_reqs_padded],
max_seq_len=self.draft_max_seq_len, max_seq_len=self.draft_max_seq_len,
block_tables=block_tables, block_tables=block_tables,
@@ -313,7 +329,6 @@ class DraftModelSpeculator(BaseSpeculator):
self.temperature.copy_(temperature) self.temperature.copy_(temperature)
self.seeds.copy_(seeds) self.seeds.copy_(seeds)
self.idx_mapping[:num_reqs].copy_(idx_mapping) self.idx_mapping[:num_reqs].copy_(idx_mapping)
if self.draft_logits is not None: # idx_mapping for CG padded requests points to -1, which is ignored
# idx_mapping for CG padded requests points to -1, which is ignored # during sampling to prevent writing stale values to draft logits.
# during sampling to prevent writing stale values to draft logits. self.idx_mapping[num_reqs:].fill_(-1)
self.idx_mapping[num_reqs:].fill_(-1)