@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 28ef9ba399
Fork 0

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 7, 09:00 UTC

[BugFix] Add support for MTP num_speculative_tokens > 1 with sparse MLA (#34552)

Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Co-authored-by: Matthew Bonanni <mbonanni@redhat.com>

Lucas Wilkinson committed Mar 3, 2026 at 10:21 UTC 28ef9ba399340ea7013df8cd1c359b07acc0a302
7 files changed +258 -195
tests/v1/spec_decode/test_eagle.py
+24 -29
@@ -476,12 +476,12 @@ def test_set_inputs_first_pass_draft_model():
476 proposer.max_num_tokens, dtype=torch.bool, device=device
477 )
478
479 - # Mock the attn_metadata_builder to avoid needing the full model setup
479 + # Mock draft_attn_groups to avoid needing the full model setup
480 mock_kv_cache_spec = mock.MagicMock()
481 mock_kv_cache_spec.block_size = block_size
482 - mock_builder = mock.MagicMock()
483 - mock_builder.kv_cache_spec = mock_kv_cache_spec
484 - proposer.attn_metadata_builder = mock_builder
482 + mock_attn_group = mock.MagicMock()
483 + mock_attn_group.kv_cache_spec = mock_kv_cache_spec
484 + proposer.draft_attn_groups = [mock_attn_group]
485
486 # Request 0: query_len=3 (but 1 rejected), Request 1: query_len=2
487 batch_spec = BatchSpec(
@@ -616,12 +616,12 @@ def test_set_inputs_first_pass_parallel_drafting():
616 proposer.max_num_tokens, dtype=torch.bool, device=device
617 )
618
619 - # Mock the attn_metadata_builder
619 + # Mock draft_attn_groups
620 mock_kv_cache_spec = mock.MagicMock()
621 mock_kv_cache_spec.block_size = block_size
622 - mock_builder = mock.MagicMock()
623 - mock_builder.kv_cache_spec = mock_kv_cache_spec
624 - proposer.attn_metadata_builder = mock_builder
622 + mock_attn_group = mock.MagicMock()
623 + mock_attn_group.kv_cache_spec = mock_kv_cache_spec
624 + proposer.draft_attn_groups = [mock_attn_group]
625
626 # Request 0: query_len=4 (1 rejected), Request 1: query_len=4 (all valid)
627 batch_spec = BatchSpec(
@@ -916,7 +916,7 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
916 proposer.model = model_mock
917
918 # Assign draft attn_layer_names since load_model is not invoked
919 - proposer.attn_layer_names = ["layer.0"]
919 + proposer._draft_attn_layer_names = {"layer.0"}
920
921 # Create input tensors
922 batch_spec = BatchSpec(
@@ -961,20 +961,18 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
961
962 attn_metadata_builder = attn_metadata_builder_cls(
963 kv_cache_spec=create_standard_kv_cache_spec(proposer.vllm_config),
964 - layer_names=proposer.attn_layer_names,
964 + layer_names=proposer._draft_attn_layer_names,
965 vllm_config=proposer.vllm_config,
966 device=device,
967 )
968
969 - # Mock runner for attention metadata building
969 + # Mock runner and draft_attn_groups for attention metadata building
970 proposer.runner = mock.MagicMock()
971 - proposer.runner.attn_groups.append([mock.MagicMock()])
972 - proposer.runner.attn_groups[0][
973 - 0
974 - ].get_metadata_builder.return_value = attn_metadata_builder
975 - proposer._get_attention_metadata_builder = mock.MagicMock(
976 - return_value=attn_metadata_builder
977 - )
971 + mock_attn_group = mock.MagicMock()
972 + mock_attn_group.get_metadata_builder.return_value = attn_metadata_builder
973 + mock_attn_group.layer_names = list(proposer._draft_attn_layer_names)
974 + mock_attn_group.kv_cache_spec = attn_metadata_builder.kv_cache_spec
975 + proposer.draft_attn_groups = [mock_attn_group]
976
977 result = proposer.propose(
978 target_token_ids=target_token_ids,
@@ -1089,7 +1087,7 @@ def test_propose_tree(spec_token_tree):
1087 proposer.model = model_mock
1088
1089 # Assign draft attn_layer_names since load_model is not invoked
1092 - proposer.attn_layer_names = ["layer.0"]
1090 + proposer._draft_attn_layer_names = {"layer.0"}
1091
1092 # Get the tree attention metadata builder.
1093 attn_metadata_builder_cls, _ = try_get_attention_backend(
@@ -1097,21 +1095,18 @@ def test_propose_tree(spec_token_tree):
1095 )
1096 attn_metadata_builder = attn_metadata_builder_cls(
1097 kv_cache_spec=create_standard_kv_cache_spec(proposer.vllm_config),
1100 - layer_names=proposer.attn_layer_names,
1098 + layer_names=proposer._draft_attn_layer_names,
1099 vllm_config=proposer.vllm_config,
1100 device=device,
1101 )
1102
1105 - # Mock runner for attention metadata building.
1103 + # Mock runner and draft_attn_groups for attention metadata building.
1104 proposer.runner = mock.MagicMock()
1107 - proposer.runner.attn_groups.append([mock.MagicMock()])
1108 - proposer.runner.attn_groups[0][0].metadata_builders = [attn_metadata_builder]
1109 - proposer.runner.attn_groups[0][
1110 - 0
1111 - ].get_metadata_builder.return_value = attn_metadata_builder
1112 - proposer._get_attention_metadata_builder = mock.MagicMock(
1113 - return_value=attn_metadata_builder
1114 - )
1105 + mock_attn_group = mock.MagicMock()
1106 + mock_attn_group.get_metadata_builder.return_value = attn_metadata_builder
1107 + mock_attn_group.layer_names = list(proposer._draft_attn_layer_names)
1108 + mock_attn_group.kv_cache_spec = attn_metadata_builder.kv_cache_spec
1109 + proposer.draft_attn_groups = [mock_attn_group]
1110
1111 # Setup inputs for the proposer.
1112 target_token_ids = torch.randint(0, vocab_size, (total_tokens,), device=device)
tests/v1/spec_decode/test_mtp.py
+7 -3
@@ -162,7 +162,7 @@ def test_mtp_propose(num_speculative_tokens, monkeypatch):
162 model_mock.compute_logits.side_effect = logits_returns
163
164 proposer.model = model_mock
165 - proposer.attn_layer_names = ["layer.0"]
165 + proposer._draft_attn_layer_names = {"layer.0"}
166
167 # Prepare inputs
168 batch_spec = BatchSpec(seq_lens=seq_lens, query_lens=seq_lens)
@@ -190,13 +190,17 @@ def test_mtp_propose(num_speculative_tokens, monkeypatch):
190
191 attn_metadata_builder = attn_metadata_builder_cls(
192 kv_cache_spec=create_standard_kv_cache_spec(proposer.vllm_config),
193 - layer_names=proposer.attn_layer_names,
193 + layer_names=list(proposer._draft_attn_layer_names),
194 vllm_config=proposer.vllm_config,
195 device=device,
196 )
197
198 proposer.runner = mock.MagicMock()
199 - proposer.attn_metadata_builder = attn_metadata_builder
199 + mock_attn_group = mock.MagicMock()
200 + mock_attn_group.get_metadata_builder.return_value = attn_metadata_builder
201 + mock_attn_group.layer_names = list(proposer._draft_attn_layer_names)
202 + mock_attn_group.kv_cache_spec = attn_metadata_builder.kv_cache_spec
203 + proposer.draft_attn_groups = [mock_attn_group]
204
205 # Run propose
206 result = proposer.propose(
vllm/model_executor/layers/sparse_attn_indexer.py
+6
@@ -79,6 +79,12 @@ def sparse_attn_indexer(
79 has_prefill = attn_metadata.num_prefills > 0
80 num_decode_tokens = attn_metadata.num_decode_tokens
81
82 + # During speculative decoding, k may be padded to the CUDA graph batch
83 + # size while slot_mapping only covers actual tokens. Truncate k to avoid
84 + # out-of-bounds reads in the kernel.
85 + num_tokens = slot_mapping.shape[0]
86 + k = k[:num_tokens]
87 +
88 ops.indexer_k_quant_and_cache(
89 k,
90 kv_cache,
vllm/v1/attention/backends/mla/indexer.py
+108 -32
@@ -12,6 +12,7 @@ from vllm.utils.deep_gemm import (
12 get_paged_mqa_logits_metadata,
13 is_deep_gemm_supported,
14 )
15 +from vllm.utils.math_utils import cdiv
16 from vllm.utils.platform_utils import num_compute_units
17 from vllm.v1.attention.backend import (
18 AttentionBackend,
@@ -24,6 +25,7 @@ from vllm.v1.attention.backends.utils import (
25 split_decodes_and_prefills,
26 split_prefill_chunks,
27 )
28 +from vllm.v1.worker.cp_utils import get_total_cp_world_size
29
30 logger = init_logger(__name__)
31
@@ -214,20 +216,39 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
216 if self.vllm_config.speculative_config
217 else 0
218 )
217 - if self.num_speculative_tokens > 1:
218 - raise ValueError(
219 - "Sparse MLA only supports "
220 - "num_speculative_tokens <= 1 because the DeepGEMM "
221 - "fp8_paged_mqa_logits kernel does not support next_n > 2. "
222 - f"Got num_speculative_tokens={self.num_speculative_tokens}."
223 - )
219 self.reorder_batch_threshold += self.num_speculative_tokens
220
221 sm_count = num_compute_units(self.device.index)
222 self.num_sms = sm_count
223
224 self.decode_lens_buffer = torch.empty(
230 - (scheduler_config.max_num_seqs,), dtype=torch.int32, device=self.device
225 + (scheduler_config.max_num_batched_tokens,),
226 + dtype=torch.int32,
227 + device=self.device,
228 + )
229 +
230 + # Pre-allocated buffers for flattening (spec decode).
231 + self.arange_buffer = torch.arange(
232 + scheduler_config.max_num_seqs * (1 + self.num_speculative_tokens),
233 + dtype=torch.int32,
234 + device=self.device,
235 + )
236 + self.expanded_seq_lens_buffer = torch.zeros(
237 + (scheduler_config.max_num_batched_tokens,),
238 + dtype=torch.int32,
239 + device=self.device,
240 + )
241 + max_num_blocks_per_req = cdiv(
242 + self.vllm_config.model_config.max_model_len,
243 + self.kv_cache_spec.block_size * get_total_cp_world_size(),
244 + )
245 + self.expanded_block_table_buffer = torch.zeros(
246 + (
247 + scheduler_config.max_num_batched_tokens,
248 + max_num_blocks_per_req,
249 + ),
250 + dtype=torch.int32,
251 + device=self.device,
252 )
253
254 # See: DeepGMM/csrc/apis/attention.hpp
@@ -326,42 +347,97 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
347 common_attn_metadata.query_start_loc_cpu[: num_decodes + 1]
348 )
349
329 - # Use CPU to avoid GPU sync; breaking async scheduling
330 - requires_padding = (decode_lens_cpu.max() > decode_lens_cpu.min()).item()
350 + seq_lens = common_attn_metadata.seq_lens[:num_decodes]
351 + block_table = common_attn_metadata.block_table_tensor[:num_decodes, ...]
352
332 - # Decide which top-k kernel to use based on batch size and sequence length
333 - batch_size = num_decodes
334 - _is_large_context = common_attn_metadata.max_seq_len > 8192
353 + # Padded CUDA graph requests have block_table entries of -1.
354 + # Clamp to 0 to prevent OOB access in the DeepGEMM kernel.
355 + # This is safe because padded requests have seq_lens=0, so the
356 + # kernel produces no meaningful output for those rows.
357 + block_table.clamp_(min=0)
358
336 - # Decision logic based on micro-benchmark results:
337 - # - large_context_topk wins for batch <= 128 and seq_len > 8K
338 - # - top_k_per_row_decode wins for batch > 128 or seq_len <= 8K
339 - use_large_context_topk = batch_size <= 128 and _is_large_context
359 + max_decode_len = int(decode_lens_cpu.max().item())
360 + if max_decode_len > 1:
361 + # Flatten multi-token decode requests into single-token
362 + # batch entries, expanding seq_lens and block tables so
363 + # the kernel always sees next_n=1.
364
341 - next_n = 1 + self.num_speculative_tokens
342 - if next_n > 1:
343 - offsets = torch.arange(next_n, device=self.device, dtype=torch.int32)
344 - else:
345 - offsets = None
365 + # Assume 4 requests with seq_lens [10, 7, 12, 0] (the final req is
366 + # padding) and decode_lens [3, 1, 4, 0] in the below example comments.
367 + # The context lengths are therefore
368 + # [10-3, 7-1, 12-4, 0-0] = [7, 6, 8, 0].
369
347 - seq_lens = common_attn_metadata.seq_lens[:num_decodes]
370 + # 3 + 1 + 4 + 0 = 8
371 + actual_expanded = int(decode_lens_cpu.sum().item())
372 +
373 + # [7, 6, 8, 0] -> [7, 7, 7, 6, 8, 8, 8, 8]
374 + expanded_base = torch.repeat_interleave(
375 + seq_lens - decode_lens, decode_lens
376 + )
377 +
378 + # [0, 3, 4, 8] -> [0, 0, 0, 3, 4, 4, 4, 4]
379 + expanded_starts = torch.repeat_interleave(
380 + common_attn_metadata.query_start_loc[:num_decodes], decode_lens
381 + )
382 +
383 + # [0, 1, 2, 0, 0, 1, 2, 3]
384 + positions_within = (
385 + self.arange_buffer[:actual_expanded] - expanded_starts
386 + )
387 +
388 + # [8, 9, 10, 7, 9, 10, 11, 12, ...] where ... is unused buffer space
389 + self.expanded_seq_lens_buffer[:actual_expanded] = (
390 + expanded_base + positions_within + 1
391 + )
392 + self.expanded_seq_lens_buffer[actual_expanded:] = 0
393 + seq_lens = self.expanded_seq_lens_buffer[:num_decode_tokens]
394 +
395 + # Give each of the flattened entries the same block table row as the
396 + # original request.
397 + self.expanded_block_table_buffer[:actual_expanded] = (
398 + torch.repeat_interleave(block_table, decode_lens, dim=0)
399 + )
400 + if actual_expanded < num_decode_tokens:
401 + self.expanded_block_table_buffer[
402 + actual_expanded:num_decode_tokens, 0
403 + ] = 0
404 + block_table = self.expanded_block_table_buffer[:num_decode_tokens]
405 +
406 + # All reqs now have decode_len=1
407 + self.decode_lens_buffer[:num_decode_tokens] = 1
408 + decode_lens = self.decode_lens_buffer[:num_decode_tokens]
409 + offsets = None
410 + batch_size = num_decode_tokens
411 + else:
412 + next_n = 1 + self.num_speculative_tokens
413 + if next_n > 1:
414 + offsets = torch.arange(
415 + next_n, device=self.device, dtype=torch.int32
416 + )
417 + else:
418 + offsets = None
419 + batch_size = num_decodes
420
421 # DeepGEMM is required for the paged MQA logits on CUDA devices
422 if current_platform.is_cuda() and is_deep_gemm_supported():
423 self.scheduler_metadata_buffer[:] = get_paged_mqa_logits_metadata(
352 - seq_lens, self.kv_cache_spec.block_size, self.num_sms
424 + seq_lens,
425 + self.kv_cache_spec.block_size,
426 + self.num_sms,
427 )
354 - block_table = common_attn_metadata.block_table_tensor[:num_decodes, ...]
355 - # Padded CUDA graph requests have block_table entries of -1.
356 - # Clamp to 0 to prevent OOB access in the DeepGEMM kernel.
357 - # This is safe because padded requests have seq_lens=0, so the
358 - # kernel produces no meaningful output for those rows.
359 - block_table.clamp_(min=0)
428 +
429 + # Decide which top-k kernel to use based on batch size and sequence length
430 + # Decision logic based on micro-benchmark results:
431 + # - large_context_topk wins for batch <= 128 and seq_len > 8K
432 + # - top_k_per_row_decode wins for batch > 128 or seq_len <= 8K
433 + _is_large_context = common_attn_metadata.max_seq_len > 8192
434 + use_large_context_topk = batch_size <= 128 and _is_large_context
435 +
436 decode_metadata = DeepSeekV32IndexerDecodeMetadata(
437 block_table=block_table,
362 - seq_lens=common_attn_metadata.seq_lens[:num_decodes],
438 + seq_lens=seq_lens,
439 decode_lens=decode_lens,
364 - requires_padding=requires_padding,
440 + requires_padding=False,
441 schedule_metadata=self.scheduler_metadata_buffer,
442 use_large_context_topk=use_large_context_topk,
443 offsets=offsets,
vllm/v1/spec_decode/eagle.py
+99 -121
@@ -20,17 +20,13 @@ from vllm.logger import init_logger
20 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
21 from vllm.model_executor.model_loader import get_model
22 from vllm.model_executor.models import supports_multimodal
23 -from vllm.model_executor.models.deepseek_v2 import DeepseekV32IndexerCache
23 from vllm.model_executor.models.interfaces import SupportsMultiModal
24 from vllm.model_executor.models.llama_eagle3 import Eagle3LlamaForCausalLM
25 from vllm.multimodal import MULTIMODAL_REGISTRY
26 from vllm.platforms import current_platform
27 from vllm.triton_utils import triton
28 from vllm.utils.platform_utils import is_pin_memory_available
30 -from vllm.v1.attention.backend import (
31 - AttentionMetadataBuilder,
32 - CommonAttentionMetadata,
33 -)
29 +from vllm.v1.attention.backend import CommonAttentionMetadata
30 from vllm.v1.attention.backends.registry import AttentionBackendEnum
31 from vllm.v1.attention.backends.tree_attn import (
32 TreeAttentionMetadata,
@@ -38,7 +34,7 @@ from vllm.v1.attention.backends.tree_attn import (
34 )
35 from vllm.v1.attention.backends.triton_attn import TritonAttentionMetadata
36 from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher
41 -from vllm.v1.kv_cache_interface import KVCacheConfig
37 +from vllm.v1.kv_cache_interface import KVCacheConfig, UniformTypeKVCacheSpecs
38 from vllm.v1.sample.metadata import SamplingMetadata
39 from vllm.v1.sample.sampler import _SAMPLING_EPS
40 from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
@@ -53,6 +49,7 @@ from vllm.v1.spec_decode.utils import (
49 from vllm.v1.utils import CpuGpuBuffer
50 from vllm.v1.worker.dp_utils import coordinate_batch_across_dp
51 from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
52 +from vllm.v1.worker.utils import AttentionGroup
53
54 logger = init_logger(__name__)
55
@@ -113,10 +110,8 @@ class SpecDecodeBaseProposer:
110 vllm_config.model_config
111 )
112
116 - self.attn_metadata_builder: AttentionMetadataBuilder | None = None
117 - self.draft_indexer_metadata_builder: AttentionMetadataBuilder | None = None
118 - self.attn_layer_names: list[str] = []
119 - self.indexer_layer_names: list[str] = []
113 + self.draft_attn_groups: list[AttentionGroup] = []
114 + self.kv_cache_gid: int = -1
115 self.eagle3_use_aux_hidden_state: bool = (
116 self._get_eagle3_use_aux_hidden_state_from_config()
117 )
@@ -353,7 +348,7 @@ class SpecDecodeBaseProposer:
348 self._slot_mapping_buffer[num_actual:num_tokens].fill_(PADDING_SLOT_ID)
349
350 view = self._slot_mapping_buffer[:num_tokens]
356 - return {name: view for name in self.attn_layer_names + self.indexer_layer_names}
351 + return {name: view for name in self._draft_attn_layer_names}
352
353 def initialize_cudagraph_keys(self, cudagraph_mode: CUDAGraphMode) -> None:
354 """Initialize cudagraph dispatcher keys for eagle.
@@ -420,33 +415,13 @@ class SpecDecodeBaseProposer:
415
416 assert self.runner is not None
417
423 - if self.attn_metadata_builder is None:
424 - attn_metadata_builder = self._get_attention_metadata_builder()
425 - else:
426 - attn_metadata_builder = self.attn_metadata_builder
427 -
428 - attn_metadata = attn_metadata_builder.build_for_drafting(
429 - common_attn_metadata=common_attn_metadata, draft_index=0
430 - )
431 - # FIXME: support hybrid kv for draft model (remove separate indexer)
432 - if self.draft_indexer_metadata_builder:
433 - draft_indexer_metadata = (
434 - self.draft_indexer_metadata_builder.build_for_drafting(
435 - common_attn_metadata=common_attn_metadata,
436 - draft_index=0,
437 - )
418 + per_layer_attn_metadata: dict[str, object] = {}
419 + for attn_group in self.draft_attn_groups:
420 + attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
421 + common_attn_metadata=common_attn_metadata, draft_index=0
422 )
439 - else:
440 - draft_indexer_metadata = None
441 - # At this moment, we assume all eagle layers belong to the same KV
442 - # cache group, thus using the same attention metadata.
443 - per_layer_attn_metadata = {}
444 - for layer_name in self.attn_layer_names:
445 - per_layer_attn_metadata[layer_name] = attn_metadata
446 -
447 - for layer_name in self.indexer_layer_names:
448 - assert draft_indexer_metadata is not None
449 - per_layer_attn_metadata[layer_name] = draft_indexer_metadata
423 + for layer_name in attn_group.layer_names:
424 + per_layer_attn_metadata[layer_name] = attn_metadata
425
426 cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
427 self._determine_batch_execution_and_padding(num_tokens)
@@ -503,12 +478,7 @@ class SpecDecodeBaseProposer:
478 positions = self.mrope_positions[:, token_indices_to_sample]
479 else:
480 positions = self.positions[token_indices_to_sample]
506 - if self.method in (
507 - "deepseek_mtp",
508 - "ernie_mtp",
509 - "longcat_flash_mtp",
510 - "pangu_ultra_moe_mtp",
511 - ):
481 + if self.method == "mtp":
482 hidden_states = self.hidden_states[token_indices_to_sample]
483 else:
484 hidden_states = hidden_states[token_indices_to_sample]
@@ -613,7 +583,8 @@ class SpecDecodeBaseProposer:
583 common_attn_metadata._num_computed_tokens_cpu += 1
584
585 # Compute the slot mapping.
616 - block_size = attn_metadata_builder.kv_cache_spec.block_size
586 + # Use the first draft attention group's kv_cache_spec for block_size
587 + block_size = self.draft_attn_groups[0].kv_cache_spec.block_size
588 if self.uses_mrope:
589 # all dimensions of positions are the same
590 block_numbers = clamped_positions[0] // block_size
@@ -639,11 +610,13 @@ class SpecDecodeBaseProposer:
610 )
611
612 # Rebuild attention metadata
642 - attn_metadata = attn_metadata_builder.build_for_drafting( # type: ignore
643 - common_attn_metadata=common_attn_metadata, draft_index=token_index + 1
644 - )
645 - for layer_name in self.attn_layer_names:
646 - per_layer_attn_metadata[layer_name] = attn_metadata
613 + for attn_group in self.draft_attn_groups:
614 + attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
615 + common_attn_metadata=common_attn_metadata,
616 + draft_index=token_index + 1,
617 + )
618 + for layer_name in attn_group.layer_names:
619 + per_layer_attn_metadata[layer_name] = attn_metadata
620
621 # copy inputs to buffer for cudagraph
622 self.input_ids[:batch_size] = input_ids
@@ -805,18 +778,17 @@ class SpecDecodeBaseProposer:
778 # 2.
779 # Recompute the slot mapping based on the new positions and
780 # rejection mask.
808 - builder = (
809 - self._get_attention_metadata_builder()
810 - if self.attn_metadata_builder is None
811 - else self.attn_metadata_builder
812 - )
781 + # Use the first draft attention group's kv_cache_spec for block_size
782 + # (all draft layers share the same kv-cache group)
783 + assert len(self.draft_attn_groups) > 0
784 + block_size = self.draft_attn_groups[0].kv_cache_spec.block_size
785 new_slot_mapping = compute_new_slot_mapping(
786 cad=cad,
787 new_positions=self.positions[:total_num_output_tokens],
788 is_rejected_token_mask=self.is_rejected_token_mask[
789 :total_num_output_tokens
790 ],
819 - block_size=builder.kv_cache_spec.block_size,
791 + block_size=block_size,
792 num_new_tokens=self.net_num_new_slots_per_request,
793 max_model_len=self.max_model_len,
794 )
@@ -1000,9 +972,7 @@ class SpecDecodeBaseProposer:
972 | list[dict[str, torch.Tensor]]
973 | None = None,
974 ) -> list[torch.Tensor]:
1003 - tree_attn_metadata_builder = self.runner.attn_groups[0][
1004 - 0
1005 - ].get_metadata_builder()
975 + tree_attn_metadata_builder = self.draft_attn_groups[0].get_metadata_builder()
976 assert isinstance(tree_attn_metadata_builder, TreeAttentionMetadataBuilder)
977
978 total_num_drafts = self.cu_drafts_per_level[0]
@@ -1078,10 +1048,11 @@ class SpecDecodeBaseProposer:
1048 common_attn_metadata=common_attn_metadata, draft_index=level + 1
1049 )
1050
1081 - # Apply new attention metadata to all layers.
1051 + # Apply new attention metadata to all draft layers.
1052 per_layer_attn_metadata = {}
1083 - for layer_name in self.attn_layer_names:
1084 - per_layer_attn_metadata[layer_name] = attn_metadata
1053 + for attn_group in self.draft_attn_groups:
1054 + for layer_name in attn_group.layer_names:
1055 + per_layer_attn_metadata[layer_name] = attn_metadata
1056
1057 # Consider max model length.
1058 attn_metadata.max_seq_len = min(
@@ -1288,43 +1259,17 @@ class SpecDecodeBaseProposer:
1259 AttentionLayerBase, # type: ignore[type-abstract]
1260 ).keys()
1261 )
1291 - # FIXME: support hybrid kv for draft model
1292 - target_indexer_layer_names = set(
1293 - get_layers_from_vllm_config(
1294 - self.vllm_config, DeepseekV32IndexerCache
1295 - ).keys()
1296 - )
1262
1263 self.model = self._get_model()
1264
1300 - draft_attn_layer_names = (
1301 - get_layers_from_vllm_config(
1302 - self.vllm_config,
1303 - AttentionLayerBase, # type: ignore[type-abstract]
1304 - ).keys()
1305 - - target_attn_layer_names
1265 + # Find draft layers (attention layers added by draft model)
1266 + all_attn_layers = get_layers_from_vllm_config(
1267 + self.vllm_config,
1268 + AttentionLayerBase, # type: ignore[type-abstract]
1269 )
1307 - indexer_layers = get_layers_from_vllm_config(
1308 - self.vllm_config, DeepseekV32IndexerCache
1270 + self._draft_attn_layer_names = (
1271 + set(all_attn_layers.keys()) - target_attn_layer_names
1272 )
1310 - draft_indexer_layer_names = indexer_layers.keys() - target_indexer_layer_names
1311 - self.attn_layer_names = list(draft_attn_layer_names - draft_indexer_layer_names)
1312 - self.indexer_layer_names = list(draft_indexer_layer_names)
1313 -
1314 - if self.indexer_layer_names:
1315 - first_layer = self.indexer_layer_names[0]
1316 - self.draft_indexer_metadata_builder = (
1317 - indexer_layers[first_layer]
1318 - .get_attn_backend()
1319 - .get_builder_cls()(
1320 - indexer_layers[first_layer].get_kv_cache_spec(self.vllm_config),
1321 - self.indexer_layer_names,
1322 - self.vllm_config,
1323 - self.device,
1324 - )
1325 - )
1326 - else:
1327 - self.draft_indexer_metadata_builder = None
1273
1274 if self.supports_mm_inputs:
1275 # Even if the target model is multimodal, we can also use
@@ -1562,9 +1507,9 @@ class SpecDecodeBaseProposer:
1507
1508 # Make sure to use EAGLE's own buffer during cudagraph capture.
1509 if (
1565 - self.attn_layer_names
1510 + self._draft_attn_layer_names
1511 and slot_mappings is not None
1567 - and self.attn_layer_names[0] in slot_mappings
1512 + and next(iter(self._draft_attn_layer_names)) in slot_mappings
1513 ):
1514 slot_mapping_dict = self._get_slot_mapping(num_input_tokens)
1515 else:
@@ -1594,31 +1539,6 @@ class SpecDecodeBaseProposer:
1539 kwargs["hidden_states"] = self.hidden_states[:num_input_tokens]
1540 self.model(**kwargs)
1541
1597 - def _get_attention_metadata_builder(self) -> AttentionMetadataBuilder:
1598 - """Find and return the attention metadata builders for EAGLE layers.
1599 -
1600 - Returns:
1601 - The metadata builders for EAGLE layers.
1602 -
1603 - Raises:
1604 - AssertionError: If no metadata builders are found for EAGLE layers.
1605 - """
1606 - builder = None
1607 - chosen_layer = self.attn_layer_names[0]
1608 -
1609 - for kv_cache_group in self.runner.attn_groups:
1610 - for attn_group in kv_cache_group:
1611 - if chosen_layer in attn_group.layer_names:
1612 - builder = attn_group.get_metadata_builder()
1613 - break
1614 - if builder is not None:
1615 - break
1616 -
1617 - assert builder is not None, (
1618 - "Failed to find attention metadata builder for EAGLE layers."
1619 - )
1620 - return builder
1621 -
1542 def _get_eagle3_use_aux_hidden_state_from_config(self) -> bool:
1543 """
1544 Some eagle3 heads (e.g., nvidia/gpt-oss-120b-Eagle3-v2) do not use auxiliary
@@ -1651,13 +1571,71 @@ class SpecDecodeBaseProposer:
1571 set(
1572 [
1573 kv_cache_groups[layer_name]
1654 - for layer_name in self.attn_layer_names
1574 + for layer_name in self._draft_attn_layer_names
1575 ]
1576 )
1577 )
1578 == 1
1579 ), "All drafting layers should belong to the same kv cache group"
1580
1581 + def initialize_attn_backend(
1582 + self,
1583 + kv_cache_config: KVCacheConfig,
1584 + kernel_block_sizes: list[int] | None = None,
1585 + ) -> None:
1586 + """
1587 + Initialize AttentionGroups for draft layers using kv_cache_config.
1588 + Called from the model runner's initialize_metadata_builders.
1589 + """
1590 + all_attn_layers = get_layers_from_vllm_config(
1591 + self.vllm_config,
1592 + AttentionLayerBase, # type: ignore[type-abstract]
1593 + )
1594 +
1595 + # Find which kv_cache_group the draft layers belong to
1596 + self.validate_same_kv_cache_group(kv_cache_config)
1597 + kv_cache_spec = None
1598 + for gid, group in enumerate(kv_cache_config.kv_cache_groups):
1599 + if self._draft_attn_layer_names & set(group.layer_names):
1600 + self.kv_cache_gid = gid
1601 + kv_cache_spec = group.kv_cache_spec
1602 + break
1603 +
1604 + attention_groups: dict[tuple[str, str], AttentionGroup] = {}
1605 + if kv_cache_spec is not None:
1606 + for layer_name in self._draft_attn_layer_names:
1607 + attn_backend = all_attn_layers[layer_name].get_attn_backend()
1608 + backend_key = attn_backend.full_cls_name()
1609 + if backend_key not in attention_groups:
1610 + layer_kv_cache_spec = kv_cache_spec
1611 + if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs):
1612 + layer_kv_cache_spec = layer_kv_cache_spec.kv_cache_specs[
1613 + layer_name
1614 + ]
1615 +
1616 + kernel_block_size = (
1617 + kernel_block_sizes[self.kv_cache_gid]
1618 + if kernel_block_sizes is not None
1619 + and self.kv_cache_gid < len(kernel_block_sizes)
1620 + else None
1621 + )
1622 + attn_group = AttentionGroup(
1623 + backend=attn_backend,
1624 + layer_names=[layer_name],
1625 + kv_cache_spec=layer_kv_cache_spec,
1626 + kv_cache_group_id=self.kv_cache_gid,
1627 + )
1628 + attn_group.create_metadata_builders(
1629 + self.vllm_config,
1630 + self.device,
1631 + kernel_block_size=kernel_block_size,
1632 + )
1633 + attention_groups[backend_key] = attn_group
1634 + else:
1635 + attention_groups[backend_key].layer_names.append(layer_name)
1636 +
1637 + self.draft_attn_groups = list(attention_groups.values())
1638 +
1639 def _determine_batch_execution_and_padding(
1640 self,
1641 num_tokens: int,
vllm/v1/worker/gpu_model_runner.py
+13 -9
@@ -1936,7 +1936,7 @@ class GPUModelRunner(
1936
1937 if self.speculative_config and spec_decode_common_attn_metadata is None:
1938 if isinstance(self.drafter, EagleProposer):
1939 - if self.drafter.attn_layer_names[0] in kv_cache_group.layer_names:
1939 + if self.drafter.kv_cache_gid == kv_cache_gid:
1940 spec_decode_common_attn_metadata = cm
1941 else:
1942 spec_decode_common_attn_metadata = cm
@@ -5559,6 +5559,14 @@ class GPUModelRunner(
5559 # because some of them change the threshold at init time.
5560 self.calculate_reorder_batch_threshold()
5561
5562 + # Initialize drafter attention backend
5563 + if self.speculative_config and (
5564 + self.speculative_config.use_eagle()
5565 + or self.speculative_config.uses_draft_model()
5566 + ):
5567 + assert isinstance(self.drafter, EagleProposer | DraftModelProposer)
5568 + self.drafter.initialize_attn_backend(kv_cache_config, kernel_block_sizes)
5569 +
5570 def _check_and_update_cudagraph_mode(
5571 self,
5572 attention_backends: list[set[type[AttentionBackend]]],
@@ -6079,15 +6087,11 @@ class GPUModelRunner(
6087 kv_cache_config, kernel_block_sizes
6088 )
6089
6082 - if self.speculative_config and (
6083 - self.speculative_config.use_eagle()
6084 - or self.speculative_config.uses_draft_model()
6085 - or self.speculative_config.uses_extract_hidden_states()
6090 + if (
6091 + self.speculative_config
6092 + and self.speculative_config.uses_extract_hidden_states()
6093 ):
6087 - assert isinstance(
6088 - self.drafter,
6089 - EagleProposer | DraftModelProposer | ExtractHiddenStatesProposer,
6090 - )
6094 + assert isinstance(self.drafter, ExtractHiddenStatesProposer)
6095 # validate all draft model layers belong to the same kv cache
6096 # group
6097 self.drafter.validate_same_kv_cache_group(kv_cache_config)
vllm/v1/worker/utils.py
+1 -1
@@ -48,7 +48,7 @@ class AttentionGroup:
48 self,
49 vllm_config,
50 device,
51 - kernel_block_size: int | None,
51 + kernel_block_size: int | None = None,
52 num_metadata_builders: int = 1,
53 ):
54 kv_cache_spec_builder = (