@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / f51f6844f9
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 3, 17:45 UTC

[Bugfix][Spec Decode] Wire draft_probs into probabilistic draft_model rejection (#40269)

Siddharth Bedekar committed May 13, 2026 at 18:04 UTC f51f6844f99aa38547f1fcae6516da31997bda50
7 files changed +264 -13
tests/test_config.py
+18
@@ -1286,6 +1286,24 @@ def test_eagle_draft_model_config():
1286 assert draft_model_config.architecture == "EagleLlamaForCausalLM"
1287
1288
1289 +def test_draft_sample_method_probabilistic_is_accepted():
1290 + speculative_config = SpeculativeConfig(
1291 + method="ngram",
1292 + num_speculative_tokens=1,
1293 + draft_sample_method="probabilistic",
1294 + )
1295 + assert speculative_config.draft_sample_method == "probabilistic"
1296 +
1297 +
1298 +def test_draft_sample_method_gumbel_is_rejected():
1299 + with pytest.raises(ValidationError):
1300 + SpeculativeConfig(
1301 + method="ngram",
1302 + num_speculative_tokens=1,
1303 + draft_sample_method="gumbel",
1304 + )
1305 +
1306 +
1307 def test_ir_op_priority_default():
1308 """Test that IR op priority defaults are set correctly."""
1309 from vllm.config.kernel import IrOpPriorityConfig
tests/v1/spec_decode/test_eagle.py
+101
@@ -50,6 +50,8 @@ def _create_proposer(
50 num_speculative_tokens: int,
51 attention_backend: str | None = None,
52 parallel_drafting: bool = False,
53 + rejection_sample_method: str = "standard",
54 + draft_sample_method: str = "greedy",
55 ) -> EagleProposer:
56 # Method-dependent setup
57 if method == "eagle":
@@ -81,6 +83,8 @@ def _create_proposer(
83 method=method,
84 num_speculative_tokens=num_speculative_tokens,
85 parallel_drafting=parallel_drafting,
86 + rejection_sample_method=rejection_sample_method,
87 + draft_sample_method=draft_sample_method,
88 )
89 if parallel_drafting:
90 # Overwrite pard_token to avoid crash during init
@@ -997,6 +1001,103 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
1001 assert torch.equal(result, expected_tokens)
1002
1003
1004 +def test_propose_stores_probabilistic_draft_probs(monkeypatch):
1005 + device = torch.device(DEVICE_TYPE)
1006 + batch_size = 2
1007 + seq_lens = [5, 3]
1008 + total_tokens = sum(seq_lens)
1009 + num_speculative_tokens = 3
1010 + vocab_size = 8
1011 +
1012 + proposer = _create_proposer(
1013 + "draft_model",
1014 + num_speculative_tokens,
1015 + rejection_sample_method="standard",
1016 + draft_sample_method="probabilistic",
1017 + )
1018 + hidden_size = proposer.hidden_size
1019 + expanded_total_tokens = total_tokens + batch_size
1020 +
1021 + model_mock = mock.MagicMock()
1022 + forward_returns = []
1023 + logits_returns = []
1024 + for step in range(num_speculative_tokens):
1025 + token_count = expanded_total_tokens if step == 0 else batch_size
1026 + forward_returns.append(torch.zeros(token_count, hidden_size, device=device))
1027 + logits = torch.full((batch_size, vocab_size), -10.0, device=device)
1028 + logits[0, step + 1] = 5.0
1029 + logits[1, step + 3] = 4.0
1030 + logits_returns.append(logits)
1031 +
1032 + model_mock.side_effect = forward_returns
1033 + model_mock.compute_logits.side_effect = logits_returns
1034 + proposer.model = model_mock
1035 + proposer._draft_attn_layer_names = {"layer.0"}
1036 +
1037 + def fake_compute_probs(logits, sampling_metadata):
1038 + probs = torch.softmax(logits, dim=-1)
1039 + return probs.argmax(dim=-1), probs
1040 +
1041 + monkeypatch.setattr(
1042 + "vllm.v1.spec_decode.llm_base_proposer.compute_probs_and_sample_next_token",
1043 + fake_compute_probs,
1044 + )
1045 +
1046 + batch_spec = BatchSpec(seq_lens=seq_lens, query_lens=seq_lens)
1047 + common_attn_metadata = create_common_attn_metadata(
1048 + batch_spec,
1049 + block_size=BLOCK_SIZE,
1050 + device=device,
1051 + )
1052 +
1053 + attn_metadata_builder_cls, _ = try_get_attention_backend(
1054 + AttentionBackendEnum.FLASH_ATTN
1055 + )
1056 + attn_metadata_builder = attn_metadata_builder_cls(
1057 + kv_cache_spec=create_standard_kv_cache_spec(proposer.vllm_config),
1058 + layer_names=proposer._draft_attn_layer_names,
1059 + vllm_config=proposer.vllm_config,
1060 + device=device,
1061 + )
1062 + proposer.runner = mock.MagicMock()
1063 + mock_attn_group = mock.MagicMock()
1064 + mock_attn_group.get_metadata_builder.return_value = attn_metadata_builder
1065 + mock_attn_group.layer_names = list(proposer._draft_attn_layer_names)
1066 + mock_attn_group.kv_cache_spec = attn_metadata_builder.kv_cache_spec
1067 + proposer.draft_attn_groups = [mock_attn_group]
1068 +
1069 + sampling_metadata = mock.MagicMock()
1070 + sampling_metadata.all_greedy = False
1071 +
1072 + result = proposer.propose(
1073 + target_token_ids=torch.randint(0, vocab_size, (total_tokens,), device=device),
1074 + target_positions=torch.cat(
1075 + [
1076 + torch.arange(seq_lens[0], device=device),
1077 + torch.arange(seq_lens[1], device=device),
1078 + ]
1079 + ),
1080 + target_hidden_states=torch.randn(total_tokens, hidden_size, device=device),
1081 + next_token_ids=torch.randint(
1082 + 0, vocab_size, (batch_size,), dtype=torch.int32, device=device
1083 + ),
1084 + token_indices_to_sample=None,
1085 + common_attn_metadata=common_attn_metadata,
1086 + sampling_metadata=sampling_metadata,
1087 + )
1088 +
1089 + assert result.shape == (batch_size, num_speculative_tokens)
1090 +
1091 + draft_probs = proposer.take_last_draft_probs()
1092 + assert draft_probs is not None
1093 + assert draft_probs.shape == (batch_size, num_speculative_tokens, vocab_size)
1094 + for step, expected_logits in enumerate(logits_returns):
1095 + assert torch.allclose(
1096 + draft_probs[:, step, :],
1097 + torch.softmax(expected_logits, dim=-1),
1098 + )
1099 +
1100 +
1101 def test_set_inputs_first_pass_dflash():
1102 """
1103 Test for DFlash set_inputs_first_pass.
tests/v1/worker/test_gpu_model_runner.py
+35
@@ -2,6 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from types import SimpleNamespace
5 +from unittest.mock import Mock
6
7 import numpy as np
8 import pytest
@@ -40,6 +41,7 @@ from vllm.v1.kv_cache_interface import (
41 KVCacheTensor,
42 )
43 from vllm.v1.sample.metadata import SamplingMetadata
44 +from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
45 from vllm.v1.worker.gpu_input_batch import InputBatch
46 from vllm.v1.worker.gpu_model_runner import GPUModelRunner
47 from vllm.v1.worker.utils import AttentionGroup, select_common_block_size
@@ -747,6 +749,39 @@ def test_reload_weights_before_load_model(model_runner):
749 model_runner.reload_weights()
750
751
752 +def test_sample_passes_reordered_draft_probs_to_rejection_sampler():
753 + runner = object.__new__(GPUModelRunner)
754 + runner.use_async_scheduling = False
755 + runner.input_batch = SimpleNamespace(
756 + sampling_metadata=Mock(spec=SamplingMetadata),
757 + update_async_output_token_ids=Mock(),
758 + req_ids=["req_a", "req_b", "req_c"],
759 + )
760 + runner.rejection_sampler = Mock(return_value="sampler_output")
761 + runner.sampler = Mock()
762 + runner._draft_prob_req_ids = ["req_c", "req_a", "req_b"]
763 + runner._draft_probs = torch.arange(3 * 3 * 4, dtype=torch.float32).reshape(3, 3, 4)
764 +
765 + spec_decode_metadata = SpecDecodeMetadata.make_dummy(
766 + [[1, 2], [], [3]],
767 + device=torch.device("cpu"),
768 + )
769 + logits = torch.randn(6, 4)
770 +
771 + output = GPUModelRunner._sample(runner, logits, spec_decode_metadata)
772 +
773 + assert output == "sampler_output"
774 + passed_draft_probs = runner.rejection_sampler.call_args.args[1]
775 + expected_draft_probs = torch.cat(
776 + [
777 + runner._draft_probs[1, :2],
778 + runner._draft_probs[0, :1],
779 + ],
780 + dim=0,
781 + )
782 + assert torch.equal(passed_draft_probs, expected_draft_probs)
783 +
784 +
785 def test_init_kv_cache_with_kv_sharing_invalid_target_layer_order(default_vllm_config):
786 torch.set_default_dtype(torch.float16)
787 layer_0 = "model.layers.0.self_attn.attn"
vllm/config/speculative.py
+5 -5
@@ -67,7 +67,7 @@ SpeculativeMethod = Literal[
67 NgramGPUTypes,
68 ]
69 RejectionSampleMethod = Literal["standard", "synthetic"]
70 -DraftSampleMethod = Literal["greedy", "gumbel"]
70 +DraftSampleMethod = Literal["greedy", "probabilistic"]
71
72
73 @config
@@ -256,10 +256,10 @@ class SpeculativeConfig:
256 draft_sample_method: DraftSampleMethod = "greedy"
257 """How the draft model samples tokens. 'greedy' always picks the argmax
258 token, and the draft probabilities are treated as one-hot during rejection
259 - sampling. 'gumbel' adds Gumbel noise for stochastic sampling, and the full
260 - draft logits are used for the probability ratio test during rejection
261 - sampling. This comes at the cost of additional GPU memory usage. This
262 - parameter currently only applies to Model Runner V2."""
259 + sampling. 'probabilistic' samples stochastically from the draft
260 + distribution and uses the full draft logits for the probability ratio test
261 + during rejection sampling. This comes at the cost of additional GPU memory
262 + usage."""
263
264 def compute_hash(self) -> str:
265 """
vllm/v1/spec_decode/llm_base_proposer.py
+49 -3
@@ -225,6 +225,11 @@ class SpecDecodeBaseProposer:
225 device=device,
226 with_numpy=True,
227 )
228 + self._enable_probabilistic_draft_probs = (
229 + self.speculative_config.rejection_sample_method == "standard"
230 + and self.speculative_config.draft_sample_method == "probabilistic"
231 + )
232 + self._last_draft_probs: torch.Tensor | None = None
233
234 self._slot_mapping_buffer = torch.zeros(
235 self.max_positions,
@@ -389,6 +394,30 @@ class SpecDecodeBaseProposer:
394 return self.model.get_top_tokens(hidden_states)
395 return self.model.compute_logits(hidden_states).argmax(dim=-1)
396
397 + def _sample_from_logits(
398 + self,
399 + logits: torch.Tensor,
400 + sampling_metadata: SamplingMetadata,
401 + ) -> tuple[torch.Tensor, torch.Tensor | None]:
402 + if not self._enable_probabilistic_draft_probs:
403 + return logits.argmax(dim=-1), None
404 + if sampling_metadata.all_greedy:
405 + return logits.argmax(dim=-1), None
406 + return compute_probs_and_sample_next_token(logits, sampling_metadata)
407 +
408 + def _sample_draft_tokens(
409 + self,
410 + hidden_states: torch.Tensor,
411 + sampling_metadata: SamplingMetadata,
412 + ) -> tuple[torch.Tensor, torch.Tensor | None]:
413 + if not self._enable_probabilistic_draft_probs or sampling_metadata.all_greedy:
414 + return self._greedy_sample(hidden_states), None
415 + logits = self.model.compute_logits(hidden_states)
416 + return self._sample_from_logits(logits, sampling_metadata)
417 +
418 + def take_last_draft_probs(self) -> torch.Tensor | None:
419 + return self._last_draft_probs
420 +
421 def propose(
422 self,
423 # [num_tokens]
@@ -408,6 +437,7 @@ class SpecDecodeBaseProposer:
437 | list[dict[str, torch.Tensor]]
438 | None = None,
439 ) -> torch.Tensor:
440 + self._last_draft_probs = None
441 batch_size = common_attn_metadata.batch_size()
442
443 if self.method in ("eagle3", "dflash"):
@@ -469,7 +499,13 @@ class SpecDecodeBaseProposer:
499
500 # Early exit if there is only one draft token to be generated.
501 if self.num_speculative_tokens == 1 or self.parallel_drafting:
472 - draft_token_ids = self._greedy_sample(sample_hidden_states)
502 + draft_token_ids, draft_probs = self._sample_draft_tokens(
503 + sample_hidden_states, sampling_metadata
504 + )
505 + if draft_probs is not None:
506 + self._last_draft_probs = draft_probs.view(
507 + -1, self.num_speculative_tokens, draft_probs.shape[-1]
508 + ).contiguous()
509 return draft_token_ids.view(-1, self.num_speculative_tokens)
510
511 if self.uses_mrope:
@@ -484,7 +520,10 @@ class SpecDecodeBaseProposer:
520 # (which read via _get_positions) use the correct values.
521 self.positions[:batch_size] = positions
522
487 - draft_token_ids = self._greedy_sample(sample_hidden_states)
523 + draft_token_ids, draft_probs = self._sample_draft_tokens(
524 + sample_hidden_states, sampling_metadata
525 + )
526 + draft_probs_list = None if draft_probs is None else [draft_probs]
527
528 if self.allowed_attn_types is not None:
529 for group_md in per_group_attn_metadata:
@@ -584,11 +623,18 @@ class SpecDecodeBaseProposer:
623 last_hidden_states, hidden_states = ret_hidden_states
624
625 hidden_states = hidden_states[:batch_size]
587 - draft_token_ids = self._greedy_sample(last_hidden_states[:batch_size])
626 + draft_token_ids, draft_probs = self._sample_draft_tokens(
627 + last_hidden_states[:batch_size], sampling_metadata
628 + )
629 + if draft_probs is not None:
630 + assert draft_probs_list is not None
631 + draft_probs_list.append(draft_probs)
632 draft_token_ids_list.append(draft_token_ids)
633
634 # [batch_size, num_speculative_tokens]
635 draft_token_ids = torch.stack(draft_token_ids_list, dim=1)
636 + if draft_probs_list is not None:
637 + self._last_draft_probs = torch.stack(draft_probs_list, dim=1).contiguous()
638 return draft_token_ids
639
640 def _update_positions_dependent_metadata(
vllm/v1/worker/gpu/spec_decode/eagle/speculator.py
+1 -1
@@ -106,7 +106,7 @@ class EagleSpeculator:
106 )
107
108 self.draft_logits: torch.Tensor | None = None
109 - if self.speculative_config.draft_sample_method == "gumbel":
109 + if self.speculative_config.draft_sample_method == "probabilistic":
110 self.draft_logits = torch.zeros(
111 self.max_num_reqs,
112 self.num_speculative_steps,
vllm/v1/worker/gpu_model_runner.py
+55 -4
@@ -806,6 +806,8 @@ class GPUModelRunner(
806
807 # Cached outputs.
808 self._draft_token_ids: list[list[int]] | torch.Tensor | None = None
809 + self._draft_probs: torch.Tensor | None = None
810 + self._draft_prob_req_ids: list[str] | None = None
811 # N-gram GPU path: async D2H buffer/event for per-request valid draft counts.
812 self._num_valid_draft_tokens: torch.Tensor | None = None
813 self._num_valid_draft_tokens_cpu: torch.Tensor | None = None
@@ -3433,9 +3435,10 @@ class GPUModelRunner(
3435 draft_token_ids_cpu, _ = self._get_draft_token_ids_cpu()
3436 self.input_batch.update_async_spec_token_ids(draft_token_ids_cpu)
3437
3438 + draft_probs = self._get_spec_decode_draft_probs(spec_decode_metadata)
3439 sampler_output = self.rejection_sampler(
3440 spec_decode_metadata,
3438 - None, # draft_probs
3441 + draft_probs,
3442 logits,
3443 sampling_metadata,
3444 )
@@ -4280,6 +4283,8 @@ class GPUModelRunner(
4283 )
4284
4285 self._draft_token_ids = None
4286 + self._draft_probs = None
4287 + self._draft_prob_req_ids = None
4288 self._draft_token_req_ids = None
4289 self.valid_sampled_token_count_gpu = None
4290 self.input_batch.prev_sampled_token_ids = None
@@ -4374,6 +4379,8 @@ class GPUModelRunner(
4379 self._draft_token_ids = torch.zeros(
4380 1, device=self.device, dtype=torch.int32
4381 ).expand(len(self.input_batch.req_ids), self.num_spec_tokens)
4382 + self._draft_probs = None
4383 + self._draft_prob_req_ids = None
4384 self._copy_draft_token_ids_to_cpu(scheduler_output, zeros_only=True)
4385
4386 with record_function_or_nullcontext("gpu_model_runner: bookkeep"):
@@ -4599,6 +4606,35 @@ class GPUModelRunner(
4606 sampled_count_event.synchronize()
4607 return counts_cpu[: prev_sampled_token_ids.shape[0]].tolist()
4608
4609 + def _get_spec_decode_draft_probs(
4610 + self, spec_decode_metadata: SpecDecodeMetadata
4611 + ) -> torch.Tensor | None:
4612 + if self._draft_probs is None or self._draft_prob_req_ids is None:
4613 + return None
4614 +
4615 + row_by_req_id = {
4616 + req_id: idx for idx, req_id in enumerate(self._draft_prob_req_ids)
4617 + }
4618 + draft_probs_rows: list[torch.Tensor] = []
4619 + for req_id, num_draft in zip(
4620 + self.input_batch.req_ids, spec_decode_metadata.num_draft_tokens
4621 + ):
4622 + if num_draft == 0:
4623 + continue
4624 + row_idx = row_by_req_id.get(req_id)
4625 + if row_idx is None:
4626 + logger.warning(
4627 + "Missing cached draft probabilities for request %s; "
4628 + "falling back to legacy speculative rejection behavior.",
4629 + req_id,
4630 + )
4631 + return None
4632 + draft_probs_rows.append(self._draft_probs[row_idx, :num_draft])
4633 +
4634 + if not draft_probs_rows:
4635 + return None
4636 + return torch.cat(draft_probs_rows, dim=0).contiguous()
4637 +
4638 def propose_draft_token_ids(
4639 self,
4640 scheduler_output: "SchedulerOutput",
@@ -4614,6 +4650,8 @@ class GPUModelRunner(
4650 num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
4651 spec_config = self.speculative_config
4652 assert spec_config is not None
4653 + self._draft_probs = None
4654 + self._draft_prob_req_ids = None
4655 if spec_config.method == "ngram":
4656 from vllm.v1.spec_decode.ngram_proposer import NgramProposer
4657
@@ -4859,6 +4897,11 @@ class GPUModelRunner(
4897 num_rejected_tokens_gpu=num_rejected_tokens_gpu,
4898 slot_mappings=slot_mappings,
4899 )
4900 + if hasattr(self.drafter, "take_last_draft_probs"):
4901 + draft_probs = self.drafter.take_last_draft_probs()
4902 + if draft_probs is not None:
4903 + self._draft_probs = draft_probs
4904 + self._draft_prob_req_ids = self.input_batch.req_ids.copy()
4905
4906 return draft_token_ids
4907
@@ -5806,10 +5849,18 @@ class GPUModelRunner(
5849 )
5850
5851 num_tokens = sum(len(ids) for ids in draft_token_ids)
5809 - # draft_probs = torch.randn(
5810 - # num_tokens, logits.shape[-1], device=self.device,
5811 - # dtype=logits.dtype)
5852 draft_probs = None
5853 + if (
5854 + self.speculative_config.rejection_sample_method == "standard"
5855 + and self.speculative_config.draft_sample_method == "probabilistic"
5856 + ):
5857 + draft_probs = torch.rand(
5858 + num_tokens,
5859 + logits.shape[-1],
5860 + device=self.device,
5861 + dtype=torch.float32,
5862 + )
5863 + draft_probs = torch.softmax(draft_probs, dim=-1)
5864 logits = torch.randn(
5865 num_tokens + num_reqs,
5866 logits.shape[-1],