mirror of
https://github.com/vllm-project/vllm.git
synced 2026-08-09 23:38:13 +00:00
[Bugfix][Spec Decode] Wire draft_probs into probabilistic draft_model rejection (#40269)
This commit is contained in:
@@ -1286,6 +1286,24 @@ def test_eagle_draft_model_config():
|
||||
assert draft_model_config.architecture == "EagleLlamaForCausalLM"
|
||||
|
||||
|
||||
def test_draft_sample_method_probabilistic_is_accepted():
|
||||
speculative_config = SpeculativeConfig(
|
||||
method="ngram",
|
||||
num_speculative_tokens=1,
|
||||
draft_sample_method="probabilistic",
|
||||
)
|
||||
assert speculative_config.draft_sample_method == "probabilistic"
|
||||
|
||||
|
||||
def test_draft_sample_method_gumbel_is_rejected():
|
||||
with pytest.raises(ValidationError):
|
||||
SpeculativeConfig(
|
||||
method="ngram",
|
||||
num_speculative_tokens=1,
|
||||
draft_sample_method="gumbel",
|
||||
)
|
||||
|
||||
|
||||
def test_ir_op_priority_default():
|
||||
"""Test that IR op priority defaults are set correctly."""
|
||||
from vllm.config.kernel import IrOpPriorityConfig
|
||||
|
||||
@@ -50,6 +50,8 @@ def _create_proposer(
|
||||
num_speculative_tokens: int,
|
||||
attention_backend: str | None = None,
|
||||
parallel_drafting: bool = False,
|
||||
rejection_sample_method: str = "standard",
|
||||
draft_sample_method: str = "greedy",
|
||||
) -> EagleProposer:
|
||||
# Method-dependent setup
|
||||
if method == "eagle":
|
||||
@@ -81,6 +83,8 @@ def _create_proposer(
|
||||
method=method,
|
||||
num_speculative_tokens=num_speculative_tokens,
|
||||
parallel_drafting=parallel_drafting,
|
||||
rejection_sample_method=rejection_sample_method,
|
||||
draft_sample_method=draft_sample_method,
|
||||
)
|
||||
if parallel_drafting:
|
||||
# Overwrite pard_token to avoid crash during init
|
||||
@@ -997,6 +1001,103 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
|
||||
assert torch.equal(result, expected_tokens)
|
||||
|
||||
|
||||
def test_propose_stores_probabilistic_draft_probs(monkeypatch):
|
||||
device = torch.device(DEVICE_TYPE)
|
||||
batch_size = 2
|
||||
seq_lens = [5, 3]
|
||||
total_tokens = sum(seq_lens)
|
||||
num_speculative_tokens = 3
|
||||
vocab_size = 8
|
||||
|
||||
proposer = _create_proposer(
|
||||
"draft_model",
|
||||
num_speculative_tokens,
|
||||
rejection_sample_method="standard",
|
||||
draft_sample_method="probabilistic",
|
||||
)
|
||||
hidden_size = proposer.hidden_size
|
||||
expanded_total_tokens = total_tokens + batch_size
|
||||
|
||||
model_mock = mock.MagicMock()
|
||||
forward_returns = []
|
||||
logits_returns = []
|
||||
for step in range(num_speculative_tokens):
|
||||
token_count = expanded_total_tokens if step == 0 else batch_size
|
||||
forward_returns.append(torch.zeros(token_count, hidden_size, device=device))
|
||||
logits = torch.full((batch_size, vocab_size), -10.0, device=device)
|
||||
logits[0, step + 1] = 5.0
|
||||
logits[1, step + 3] = 4.0
|
||||
logits_returns.append(logits)
|
||||
|
||||
model_mock.side_effect = forward_returns
|
||||
model_mock.compute_logits.side_effect = logits_returns
|
||||
proposer.model = model_mock
|
||||
proposer._draft_attn_layer_names = {"layer.0"}
|
||||
|
||||
def fake_compute_probs(logits, sampling_metadata):
|
||||
probs = torch.softmax(logits, dim=-1)
|
||||
return probs.argmax(dim=-1), probs
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.v1.spec_decode.llm_base_proposer.compute_probs_and_sample_next_token",
|
||||
fake_compute_probs,
|
||||
)
|
||||
|
||||
batch_spec = BatchSpec(seq_lens=seq_lens, query_lens=seq_lens)
|
||||
common_attn_metadata = create_common_attn_metadata(
|
||||
batch_spec,
|
||||
block_size=BLOCK_SIZE,
|
||||
device=device,
|
||||
)
|
||||
|
||||
attn_metadata_builder_cls, _ = try_get_attention_backend(
|
||||
AttentionBackendEnum.FLASH_ATTN
|
||||
)
|
||||
attn_metadata_builder = attn_metadata_builder_cls(
|
||||
kv_cache_spec=create_standard_kv_cache_spec(proposer.vllm_config),
|
||||
layer_names=proposer._draft_attn_layer_names,
|
||||
vllm_config=proposer.vllm_config,
|
||||
device=device,
|
||||
)
|
||||
proposer.runner = mock.MagicMock()
|
||||
mock_attn_group = mock.MagicMock()
|
||||
mock_attn_group.get_metadata_builder.return_value = attn_metadata_builder
|
||||
mock_attn_group.layer_names = list(proposer._draft_attn_layer_names)
|
||||
mock_attn_group.kv_cache_spec = attn_metadata_builder.kv_cache_spec
|
||||
proposer.draft_attn_groups = [mock_attn_group]
|
||||
|
||||
sampling_metadata = mock.MagicMock()
|
||||
sampling_metadata.all_greedy = False
|
||||
|
||||
result = proposer.propose(
|
||||
target_token_ids=torch.randint(0, vocab_size, (total_tokens,), device=device),
|
||||
target_positions=torch.cat(
|
||||
[
|
||||
torch.arange(seq_lens[0], device=device),
|
||||
torch.arange(seq_lens[1], device=device),
|
||||
]
|
||||
),
|
||||
target_hidden_states=torch.randn(total_tokens, hidden_size, device=device),
|
||||
next_token_ids=torch.randint(
|
||||
0, vocab_size, (batch_size,), dtype=torch.int32, device=device
|
||||
),
|
||||
token_indices_to_sample=None,
|
||||
common_attn_metadata=common_attn_metadata,
|
||||
sampling_metadata=sampling_metadata,
|
||||
)
|
||||
|
||||
assert result.shape == (batch_size, num_speculative_tokens)
|
||||
|
||||
draft_probs = proposer.take_last_draft_probs()
|
||||
assert draft_probs is not None
|
||||
assert draft_probs.shape == (batch_size, num_speculative_tokens, vocab_size)
|
||||
for step, expected_logits in enumerate(logits_returns):
|
||||
assert torch.allclose(
|
||||
draft_probs[:, step, :],
|
||||
torch.softmax(expected_logits, dim=-1),
|
||||
)
|
||||
|
||||
|
||||
def test_set_inputs_first_pass_dflash():
|
||||
"""
|
||||
Test for DFlash set_inputs_first_pass.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -40,6 +41,7 @@ from vllm.v1.kv_cache_interface import (
|
||||
KVCacheTensor,
|
||||
)
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
|
||||
from vllm.v1.worker.gpu_input_batch import InputBatch
|
||||
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
|
||||
from vllm.v1.worker.utils import AttentionGroup, select_common_block_size
|
||||
@@ -747,6 +749,39 @@ def test_reload_weights_before_load_model(model_runner):
|
||||
model_runner.reload_weights()
|
||||
|
||||
|
||||
def test_sample_passes_reordered_draft_probs_to_rejection_sampler():
|
||||
runner = object.__new__(GPUModelRunner)
|
||||
runner.use_async_scheduling = False
|
||||
runner.input_batch = SimpleNamespace(
|
||||
sampling_metadata=Mock(spec=SamplingMetadata),
|
||||
update_async_output_token_ids=Mock(),
|
||||
req_ids=["req_a", "req_b", "req_c"],
|
||||
)
|
||||
runner.rejection_sampler = Mock(return_value="sampler_output")
|
||||
runner.sampler = Mock()
|
||||
runner._draft_prob_req_ids = ["req_c", "req_a", "req_b"]
|
||||
runner._draft_probs = torch.arange(3 * 3 * 4, dtype=torch.float32).reshape(3, 3, 4)
|
||||
|
||||
spec_decode_metadata = SpecDecodeMetadata.make_dummy(
|
||||
[[1, 2], [], [3]],
|
||||
device=torch.device("cpu"),
|
||||
)
|
||||
logits = torch.randn(6, 4)
|
||||
|
||||
output = GPUModelRunner._sample(runner, logits, spec_decode_metadata)
|
||||
|
||||
assert output == "sampler_output"
|
||||
passed_draft_probs = runner.rejection_sampler.call_args.args[1]
|
||||
expected_draft_probs = torch.cat(
|
||||
[
|
||||
runner._draft_probs[1, :2],
|
||||
runner._draft_probs[0, :1],
|
||||
],
|
||||
dim=0,
|
||||
)
|
||||
assert torch.equal(passed_draft_probs, expected_draft_probs)
|
||||
|
||||
|
||||
def test_init_kv_cache_with_kv_sharing_invalid_target_layer_order(default_vllm_config):
|
||||
torch.set_default_dtype(torch.float16)
|
||||
layer_0 = "model.layers.0.self_attn.attn"
|
||||
|
||||
@@ -67,7 +67,7 @@ SpeculativeMethod = Literal[
|
||||
NgramGPUTypes,
|
||||
]
|
||||
RejectionSampleMethod = Literal["standard", "synthetic"]
|
||||
DraftSampleMethod = Literal["greedy", "gumbel"]
|
||||
DraftSampleMethod = Literal["greedy", "probabilistic"]
|
||||
|
||||
|
||||
@config
|
||||
@@ -256,10 +256,10 @@ class SpeculativeConfig:
|
||||
draft_sample_method: DraftSampleMethod = "greedy"
|
||||
"""How the draft model samples tokens. 'greedy' always picks the argmax
|
||||
token, and the draft probabilities are treated as one-hot during rejection
|
||||
sampling. 'gumbel' adds Gumbel noise for stochastic sampling, and the full
|
||||
draft logits are used for the probability ratio test during rejection
|
||||
sampling. This comes at the cost of additional GPU memory usage. This
|
||||
parameter currently only applies to Model Runner V2."""
|
||||
sampling. 'probabilistic' samples stochastically from the draft
|
||||
distribution and uses the full draft logits for the probability ratio test
|
||||
during rejection sampling. This comes at the cost of additional GPU memory
|
||||
usage."""
|
||||
|
||||
def compute_hash(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -225,6 +225,11 @@ class SpecDecodeBaseProposer:
|
||||
device=device,
|
||||
with_numpy=True,
|
||||
)
|
||||
self._enable_probabilistic_draft_probs = (
|
||||
self.speculative_config.rejection_sample_method == "standard"
|
||||
and self.speculative_config.draft_sample_method == "probabilistic"
|
||||
)
|
||||
self._last_draft_probs: torch.Tensor | None = None
|
||||
|
||||
self._slot_mapping_buffer = torch.zeros(
|
||||
self.max_positions,
|
||||
@@ -389,6 +394,30 @@ class SpecDecodeBaseProposer:
|
||||
return self.model.get_top_tokens(hidden_states)
|
||||
return self.model.compute_logits(hidden_states).argmax(dim=-1)
|
||||
|
||||
def _sample_from_logits(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
sampling_metadata: SamplingMetadata,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
if not self._enable_probabilistic_draft_probs:
|
||||
return logits.argmax(dim=-1), None
|
||||
if sampling_metadata.all_greedy:
|
||||
return logits.argmax(dim=-1), None
|
||||
return compute_probs_and_sample_next_token(logits, sampling_metadata)
|
||||
|
||||
def _sample_draft_tokens(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
sampling_metadata: SamplingMetadata,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
if not self._enable_probabilistic_draft_probs or sampling_metadata.all_greedy:
|
||||
return self._greedy_sample(hidden_states), None
|
||||
logits = self.model.compute_logits(hidden_states)
|
||||
return self._sample_from_logits(logits, sampling_metadata)
|
||||
|
||||
def take_last_draft_probs(self) -> torch.Tensor | None:
|
||||
return self._last_draft_probs
|
||||
|
||||
def propose(
|
||||
self,
|
||||
# [num_tokens]
|
||||
@@ -408,6 +437,7 @@ class SpecDecodeBaseProposer:
|
||||
| list[dict[str, torch.Tensor]]
|
||||
| None = None,
|
||||
) -> torch.Tensor:
|
||||
self._last_draft_probs = None
|
||||
batch_size = common_attn_metadata.batch_size()
|
||||
|
||||
if self.method in ("eagle3", "dflash"):
|
||||
@@ -469,7 +499,13 @@ class SpecDecodeBaseProposer:
|
||||
|
||||
# Early exit if there is only one draft token to be generated.
|
||||
if self.num_speculative_tokens == 1 or self.parallel_drafting:
|
||||
draft_token_ids = self._greedy_sample(sample_hidden_states)
|
||||
draft_token_ids, draft_probs = self._sample_draft_tokens(
|
||||
sample_hidden_states, sampling_metadata
|
||||
)
|
||||
if draft_probs is not None:
|
||||
self._last_draft_probs = draft_probs.view(
|
||||
-1, self.num_speculative_tokens, draft_probs.shape[-1]
|
||||
).contiguous()
|
||||
return draft_token_ids.view(-1, self.num_speculative_tokens)
|
||||
|
||||
if self.uses_mrope:
|
||||
@@ -484,7 +520,10 @@ class SpecDecodeBaseProposer:
|
||||
# (which read via _get_positions) use the correct values.
|
||||
self.positions[:batch_size] = positions
|
||||
|
||||
draft_token_ids = self._greedy_sample(sample_hidden_states)
|
||||
draft_token_ids, draft_probs = self._sample_draft_tokens(
|
||||
sample_hidden_states, sampling_metadata
|
||||
)
|
||||
draft_probs_list = None if draft_probs is None else [draft_probs]
|
||||
|
||||
if self.allowed_attn_types is not None:
|
||||
for group_md in per_group_attn_metadata:
|
||||
@@ -584,11 +623,18 @@ class SpecDecodeBaseProposer:
|
||||
last_hidden_states, hidden_states = ret_hidden_states
|
||||
|
||||
hidden_states = hidden_states[:batch_size]
|
||||
draft_token_ids = self._greedy_sample(last_hidden_states[:batch_size])
|
||||
draft_token_ids, draft_probs = self._sample_draft_tokens(
|
||||
last_hidden_states[:batch_size], sampling_metadata
|
||||
)
|
||||
if draft_probs is not None:
|
||||
assert draft_probs_list is not None
|
||||
draft_probs_list.append(draft_probs)
|
||||
draft_token_ids_list.append(draft_token_ids)
|
||||
|
||||
# [batch_size, num_speculative_tokens]
|
||||
draft_token_ids = torch.stack(draft_token_ids_list, dim=1)
|
||||
if draft_probs_list is not None:
|
||||
self._last_draft_probs = torch.stack(draft_probs_list, dim=1).contiguous()
|
||||
return draft_token_ids
|
||||
|
||||
def _update_positions_dependent_metadata(
|
||||
|
||||
@@ -106,7 +106,7 @@ class EagleSpeculator:
|
||||
)
|
||||
|
||||
self.draft_logits: torch.Tensor | None = None
|
||||
if self.speculative_config.draft_sample_method == "gumbel":
|
||||
if self.speculative_config.draft_sample_method == "probabilistic":
|
||||
self.draft_logits = torch.zeros(
|
||||
self.max_num_reqs,
|
||||
self.num_speculative_steps,
|
||||
|
||||
@@ -806,6 +806,8 @@ class GPUModelRunner(
|
||||
|
||||
# Cached outputs.
|
||||
self._draft_token_ids: list[list[int]] | torch.Tensor | None = None
|
||||
self._draft_probs: torch.Tensor | None = None
|
||||
self._draft_prob_req_ids: list[str] | None = None
|
||||
# N-gram GPU path: async D2H buffer/event for per-request valid draft counts.
|
||||
self._num_valid_draft_tokens: torch.Tensor | None = None
|
||||
self._num_valid_draft_tokens_cpu: torch.Tensor | None = None
|
||||
@@ -3433,9 +3435,10 @@ class GPUModelRunner(
|
||||
draft_token_ids_cpu, _ = self._get_draft_token_ids_cpu()
|
||||
self.input_batch.update_async_spec_token_ids(draft_token_ids_cpu)
|
||||
|
||||
draft_probs = self._get_spec_decode_draft_probs(spec_decode_metadata)
|
||||
sampler_output = self.rejection_sampler(
|
||||
spec_decode_metadata,
|
||||
None, # draft_probs
|
||||
draft_probs,
|
||||
logits,
|
||||
sampling_metadata,
|
||||
)
|
||||
@@ -4280,6 +4283,8 @@ class GPUModelRunner(
|
||||
)
|
||||
|
||||
self._draft_token_ids = None
|
||||
self._draft_probs = None
|
||||
self._draft_prob_req_ids = None
|
||||
self._draft_token_req_ids = None
|
||||
self.valid_sampled_token_count_gpu = None
|
||||
self.input_batch.prev_sampled_token_ids = None
|
||||
@@ -4374,6 +4379,8 @@ class GPUModelRunner(
|
||||
self._draft_token_ids = torch.zeros(
|
||||
1, device=self.device, dtype=torch.int32
|
||||
).expand(len(self.input_batch.req_ids), self.num_spec_tokens)
|
||||
self._draft_probs = None
|
||||
self._draft_prob_req_ids = None
|
||||
self._copy_draft_token_ids_to_cpu(scheduler_output, zeros_only=True)
|
||||
|
||||
with record_function_or_nullcontext("gpu_model_runner: bookkeep"):
|
||||
@@ -4599,6 +4606,35 @@ class GPUModelRunner(
|
||||
sampled_count_event.synchronize()
|
||||
return counts_cpu[: prev_sampled_token_ids.shape[0]].tolist()
|
||||
|
||||
def _get_spec_decode_draft_probs(
|
||||
self, spec_decode_metadata: SpecDecodeMetadata
|
||||
) -> torch.Tensor | None:
|
||||
if self._draft_probs is None or self._draft_prob_req_ids is None:
|
||||
return None
|
||||
|
||||
row_by_req_id = {
|
||||
req_id: idx for idx, req_id in enumerate(self._draft_prob_req_ids)
|
||||
}
|
||||
draft_probs_rows: list[torch.Tensor] = []
|
||||
for req_id, num_draft in zip(
|
||||
self.input_batch.req_ids, spec_decode_metadata.num_draft_tokens
|
||||
):
|
||||
if num_draft == 0:
|
||||
continue
|
||||
row_idx = row_by_req_id.get(req_id)
|
||||
if row_idx is None:
|
||||
logger.warning(
|
||||
"Missing cached draft probabilities for request %s; "
|
||||
"falling back to legacy speculative rejection behavior.",
|
||||
req_id,
|
||||
)
|
||||
return None
|
||||
draft_probs_rows.append(self._draft_probs[row_idx, :num_draft])
|
||||
|
||||
if not draft_probs_rows:
|
||||
return None
|
||||
return torch.cat(draft_probs_rows, dim=0).contiguous()
|
||||
|
||||
def propose_draft_token_ids(
|
||||
self,
|
||||
scheduler_output: "SchedulerOutput",
|
||||
@@ -4614,6 +4650,8 @@ class GPUModelRunner(
|
||||
num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
|
||||
spec_config = self.speculative_config
|
||||
assert spec_config is not None
|
||||
self._draft_probs = None
|
||||
self._draft_prob_req_ids = None
|
||||
if spec_config.method == "ngram":
|
||||
from vllm.v1.spec_decode.ngram_proposer import NgramProposer
|
||||
|
||||
@@ -4859,6 +4897,11 @@ class GPUModelRunner(
|
||||
num_rejected_tokens_gpu=num_rejected_tokens_gpu,
|
||||
slot_mappings=slot_mappings,
|
||||
)
|
||||
if hasattr(self.drafter, "take_last_draft_probs"):
|
||||
draft_probs = self.drafter.take_last_draft_probs()
|
||||
if draft_probs is not None:
|
||||
self._draft_probs = draft_probs
|
||||
self._draft_prob_req_ids = self.input_batch.req_ids.copy()
|
||||
|
||||
return draft_token_ids
|
||||
|
||||
@@ -5806,10 +5849,18 @@ class GPUModelRunner(
|
||||
)
|
||||
|
||||
num_tokens = sum(len(ids) for ids in draft_token_ids)
|
||||
# draft_probs = torch.randn(
|
||||
# num_tokens, logits.shape[-1], device=self.device,
|
||||
# dtype=logits.dtype)
|
||||
draft_probs = None
|
||||
if (
|
||||
self.speculative_config.rejection_sample_method == "standard"
|
||||
and self.speculative_config.draft_sample_method == "probabilistic"
|
||||
):
|
||||
draft_probs = torch.rand(
|
||||
num_tokens,
|
||||
logits.shape[-1],
|
||||
device=self.device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
draft_probs = torch.softmax(draft_probs, dim=-1)
|
||||
logits = torch.randn(
|
||||
num_tokens + num_reqs,
|
||||
logits.shape[-1],
|
||||
|
||||
Reference in New Issue
Block a user