[Benchmark] Include speculative drafting in bench forward (#63622)

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Woosuk KwonandCodex authored and GitHub committed 2026-10-25 20:19:50 -07:00
1 parent 93dc587bd9
commit 39e6a1aafc
9 files changed
+318 -28

No files matched your search

+3 -2
View File
@@ -7,8 +7,9 @@ description: Time a vLLM model's forward pass on exact batch shapes (decode, pre
Builds the batch without the scheduler, runs it through MRV2's real Builds the batch without the scheduler, runs it through MRV2's real
`execute_model`, and times the forward as a one-shot CUDA graph (prefill `execute_model`, and times the forward as a one-shot CUDA graph (prefill
included). Sampling, the speculator, and CPU step overhead are excluded, so included). The configured drafter is included by default; use
expect results somewhat below a serving step. `--exclude-drafter` for target-only timing. All draft tokens are accepted.
Target sampling, rejection sampling, and CPU step overhead are excluded.
## Usage ## Usage
+16 -1
View File
@@ -136,7 +136,7 @@ vllm bench {forward, latency, serve, throughput}
### forward ### forward
Benchmark the model forward of exact batches: each case lists its requests as `[N*]Q[@K][c][:D]` (query length Q, KV length K including the query, D draft tokens). A prefill's prefix is a prefix-cache hit unless `c` marks a chunked prefill; they differ only on models with SWA bounded replay, which recompute the last window of a hit. Each request's prefix length varies by ±50% by default (`--kv-len-jitter`), identically on every DP rank. Each forward is captured as a one-shot CUDA graph; sampling and CPU overhead aren't measured. Use `torchrun`, one process per GPU, for DP/TP/EP/PCP/PP. CUDA only for now. Add `--profile-dir DIR` to write a Perfetto trace and a per-kernel GPU time summary of each case; `python -m vllm.benchmarks.forward.profile A.kernels.json [B.kernels.json]` prints a summary or compares two. Under Nsight Systems, `nsys profile --capture-range=cudaProfilerApi --capture-range-end=repeat --cuda-graph-trace=node` records just those replays, one report per case. Benchmark the model forward of exact batches: each case lists its requests as `[N*]Q[@K][c][:D]` (query length Q, KV length K including the query, D draft tokens). A prefill's prefix is a prefix-cache hit unless `c` marks a chunked prefill; they differ only on models with SWA bounded replay, which recompute the last window of a hit. Each request's prefix length varies by ±50% by default (`--kv-len-jitter`), identically on every DP rank. Each forward is captured as a one-shot CUDA graph; target sampling, rejection sampling, and CPU overhead aren't measured. Use `torchrun`, one process per GPU, for DP/TP/EP/PCP/PP. CUDA only for now. Add `--profile-dir DIR` to write a Perfetto trace and a per-kernel GPU time summary of each case; `python -m vllm.benchmarks.forward.profile A.kernels.json [B.kernels.json]` prints a summary or compares two. Under Nsight Systems, `nsys profile --capture-range=cudaProfilerApi --capture-range-end=repeat --cuda-graph-trace=node` records just those replays, one report per case.
```bash ```bash
vllm bench forward \ vllm bench forward \
@@ -171,6 +171,21 @@ torchrun --nproc-per-node=4 -m vllm.entrypoints.cli.main bench forward \
--case prefill8x1k=8*1024 --case prefill1x8k=8192 --case prefill8x1k=8*1024 --case prefill1x8k=8192
``` ```
With `--speculative-config`, timing includes the target forward and the MTP,
EAGLE, DSpark, or DFlash proposal, including the drafter's token sampling.
Use `--exclude-drafter` to measure only the target forward. Verification assumes
all draft tokens are accepted and uses a fixed bonus token; target sampling and
rejection sampling are excluded. Both paths use CUDA graphs and restore the
batch's state before each replay.
Balanced MoE routing fixtures apply to the target model; the drafter uses its
model routing, so use real weights when measuring a MoE drafter.
```bash
vllm bench forward --model Qwen/Qwen3-1.7B \
--speculative-config '{"model":"AngelSlim/Qwen3-1.7B_eagle3","method":"eagle3","num_speculative_tokens":3}' \
--case verify=16*4@4096:3
```
See [vllm bench forward](./bench/forward.md) for the full reference of all available arguments. See [vllm bench forward](./bench/forward.md) for the full reference of all available arguments.
### latency ### latency
+124
View File
@@ -22,6 +22,119 @@ from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
MODEL_NAME = "meta-llama/Llama-3.2-1B-Instruct" MODEL_NAME = "meta-llama/Llama-3.2-1B-Instruct"
@pytest.mark.benchmark
@pytest.mark.skipif(
not current_platform.is_cuda(), reason="Forward graph timing requires CUDA."
)
@pytest.mark.parametrize("method", ["mtp", "eagle3", "dspark", "dflash"])
def test_bench_forward_drafter(tmp_path, method):
"""The CLI captures real proposals, with full acceptance, unless excluded."""
config = dict(
architectures=["Qwen3ForCausalLM"],
model_type="qwen3",
hidden_size=256,
intermediate_size=512,
num_hidden_layers=4,
num_attention_heads=4,
num_key_value_heads=2,
head_dim=64,
vocab_size=1024,
max_position_embeddings=2048,
rms_norm_eps=1e-6,
rope_theta=10000,
hidden_act="silu",
tie_word_embeddings=False,
dtype="bfloat16",
)
if method == "mtp":
config.update(
architectures=["MiMoForCausalLM"],
model_type="qwen2",
num_nextn_predict_layers=1,
attention_bias=True,
)
target = tmp_path / "target"
target.mkdir()
(target / "config.json").write_text(json.dumps(config))
spec = dict(method=method, num_speculative_tokens=3)
if method != "mtp":
draft_config = dict(config, num_hidden_layers=1)
if method == "eagle3":
draft_config.update(
architectures=["LlamaForCausalLMEagle3"],
model_type="llama",
draft_vocab_size=1024,
eagle_aux_hidden_state_layer_ids=[1, 2, 3],
)
else:
draft_config.update(
architectures=[
"Qwen3DSparkModel" if method == "dspark" else "DFlashDraftModel"
],
block_size=4,
num_target_layers=4,
dflash_config=dict(mask_token_id=0, target_layer_ids=[0, 1, 2]),
markov_rank=32,
markov_head_type="vanilla",
sample_from_anchor=True,
)
draft = tmp_path / "draft"
draft.mkdir()
(draft / "config.json").write_text(json.dumps(draft_config))
spec["model"] = str(draft)
command = [
sys.executable,
"-m",
"vllm.entrypoints.cli.main",
"bench",
"forward",
"--model",
str(target),
"--skip-tokenizer-init",
"--speculative-config",
json.dumps(spec),
"--max-model-len",
"2048",
"--max-num-batched-tokens",
"256",
"--gpu-memory-utilization",
"0.2",
"--kv-len-jitter",
"0",
"--case",
"verify=4*4@128:3",
"--case",
"prefill=128",
"--case",
"mixed=32,2*4@128:3",
"--iters",
"3",
"--samples",
"2",
]
for exclude in (False, True):
output = tmp_path / f"{exclude}.json"
args = command + ["--output-json", str(output)]
if exclude:
args.append("--exclude-drafter")
result = subprocess.run(args, capture_output=True, text=True, timeout=600)
assert result.returncode == 0, result.stdout + result.stderr
for row in json.loads(output.read_text()):
assert row["median_us"] > 0
(rank,) = row["ranks"]
assert rank["finite"] and rank["graph_deterministic"]
assert rank["graph_vs_eager"]["relative_rms"] == 0
if exclude:
assert rank["timing_scope"] == "target_only"
assert "drafter" not in rank
else:
assert rank["timing_scope"] == "target_and_drafter"
assert rank["drafter"]["acceptance"] == "all"
assert rank["drafter"]["num_draft_tokens"] == 3
assert rank["drafter"]["graph_matches_eager"]
assert rank["drafter"]["valid_token_ids"]
@pytest.mark.skipif( @pytest.mark.skipif(
not current_platform.is_cuda() or torch.accelerator.device_count() < 2, not current_platform.is_cuda() or torch.accelerator.device_count() < 2,
reason="The pipeline benchmark needs two CUDA devices.", reason="The pipeline benchmark needs two CUDA devices.",
@@ -148,6 +261,17 @@ def test_build_batch_layout():
parse_case("c=1@640"), groups, 1000, 1000, np.random.default_rng(0), 4 parse_case("c=1@640"), groups, 1000, 1000, np.random.default_rng(0), 4
) )
assert batch.group_block_ranges == [[(1, 11)], [(11, 51)]] assert batch.group_block_ranges == [[(1, 11)], [(11, 51)]]
with_drafts = build_batch(
parse_case("c=1@640"),
groups,
1000,
1000,
np.random.default_rng(0),
dcp_size=4,
lookahead=4,
)
assert with_drafts.group_block_ranges == [[(1, 12)], [(12, 53)]]
assert with_drafts.expected == batch.expected
def test_jitter_kv_lens(): def test_jitter_kv_lens():
+3 -2
View File
@@ -74,11 +74,12 @@ def build_batch(
vocab_size: int, vocab_size: int,
rng: np.random.Generator, rng: np.random.Generator,
dcp_size: int = 1, dcp_size: int = 1,
lookahead: int = 0,
) -> BenchBatch: ) -> BenchBatch:
"""Allocate disjoint blocks and build `NewRequestData` for every request. """Allocate disjoint blocks and build `NewRequestData` for every request.
Groups share one block pool, so block IDs are unique across groups. Block Groups share one block pool, so block IDs are unique across groups. Block
0 is the null block. 0 is the null block. Lookahead reserves space for the drafter's KV writes.
With prefix P = K - Q (KV length K, query length Q): With prefix P = K - Q (KV length K, query length Q):
@@ -110,7 +111,7 @@ def build_batch(
block_ids = [] block_ids = []
for group, ranges in zip(kv_cache_groups, group_block_ranges): for group, ranges in zip(kv_cache_groups, group_block_ranges):
skipped, live = _num_blocks( skipped, live = _num_blocks(
group.kv_cache_spec, computed, req.kv_len, dcp_size group.kv_cache_spec, computed, req.kv_len + lookahead, dcp_size
) )
block_ids.append([0] * skipped + list(range(cursor, cursor + live))) block_ids.append([0] * skipped + list(range(cursor, cursor + live)))
ranges.append((cursor, cursor + live)) ranges.append((cursor, cursor + live))
+42 -7
View File
@@ -11,7 +11,7 @@ forward mutates in place (e.g. compressor state) is the same on every replay.
""" """
import time import time
from typing import Any from typing import TYPE_CHECKING, Any
import numpy as np import numpy as np
import torch import torch
@@ -19,10 +19,13 @@ import torch
from vllm.benchmarks.forward.batch import BenchBatch from vllm.benchmarks.forward.batch import BenchBatch
from vllm.config.compilation import CUDAGraphMode from vllm.config.compilation import CUDAGraphMode
from vllm.distributed.parallel_state import get_world_group, graph_capture from vllm.distributed.parallel_state import get_world_group, graph_capture
from vllm.forward_context import get_forward_context from vllm.forward_context import get_forward_context, override_forward_context
from vllm.platforms import current_platform from vllm.platforms import current_platform
from vllm.v1.worker.gpu.input_batch import InputBatch from vllm.v1.worker.gpu.input_batch import InputBatch
if TYPE_CHECKING:
from vllm.benchmarks.forward.speculator import DraftBench
def _first_tensor(output: Any) -> torch.Tensor: def _first_tensor(output: Any) -> torch.Tensor:
if isinstance(output, tuple): if isinstance(output, tuple):
@@ -77,6 +80,8 @@ class ForwardBench:
state_views: list[torch.Tensor], state_views: list[torch.Tensor],
name: str, name: str,
profile_prefix: str | None = None, profile_prefix: str | None = None,
*,
include_drafter: bool = True,
): ):
self.runner = runner self.runner = runner
self.batch = batch self.batch = batch
@@ -88,6 +93,8 @@ class ForwardBench:
self.profile_prefix = profile_prefix self.profile_prefix = profile_prefix
self.phase = "warmup" self.phase = "warmup"
self.result: dict[str, Any] = {} self.result: dict[str, Any] = {}
self.include_drafter = include_drafter
self.drafter: DraftBench | None = None
def run(self) -> dict[str, Any]: def run(self) -> dict[str, Any]:
runner, model = self.runner, self.runner.model runner, model = self.runner, self.runner.model
@@ -122,21 +129,46 @@ class ForwardBench:
def _forward(self, **inputs) -> Any: def _forward(self, **inputs) -> Any:
write_input_ids(self.batch, self._input_batch, inputs["input_ids"]) write_input_ids(self.batch, self._input_batch, inputs["input_ids"])
self._run_model = self._make_forward(inputs)
if self.phase == "warmup":
return self._run_model()
return self._capture_and_time()
def _make_forward(self, inputs):
# Models may update the forward context in place (e.g. sequence # Models may update the forward context in place (e.g. sequence
# parallelism shards is_padding); every call must start from the # parallelism shards is_padding); every call must start from the
# context the runner built. # context the runner built.
context = get_forward_context() context = get_forward_context()
saved = dict(vars(context)) saved = dict(vars(context))
if self.include_drafter and self.runner.speculator is not None:
from vllm.benchmarks.forward.speculator import DraftBench
if self.drafter is None:
self.drafter = DraftBench(self.runner)
# Drafting reuses the target's slot mappings and PCP block tables.
self.state_views.extend(
[self.runner.block_tables.slot_mappings]
+ self.runner.block_tables.input_block_tables
)
self.drafter.prepare(self._input_batch, context)
self.result["timing_scope"] = (
"target_and_drafter" if self.drafter is not None else "target_only"
)
def run_model() -> Any: def run_model() -> Any:
for name, value in saved.items(): for name, value in saved.items():
setattr(context, name, value) setattr(context, name, value)
return self._model_forward(**inputs) with override_forward_context(context):
output = self._model_forward(**inputs)
if not self.runner.is_last_pp_rank:
output = self.runner.pp_handler.relay_aux_hidden_states(
inputs["intermediate_tensors"], output
)
if self.drafter is not None:
self.drafter.propose(output)
return output
self._run_model = run_model return run_model
if self.phase == "warmup":
return run_model()
return self._capture_and_time()
def _check_batch(self, input_batch: InputBatch) -> None: def _check_batch(self, input_batch: InputBatch) -> None:
"""Fail unless the runner built exactly the requested batch.""" """Fail unless the runner built exactly the requested batch."""
@@ -190,6 +222,7 @@ class ForwardBench:
eager = _first_tensor(self._run_model()).clone() eager = _first_tensor(self._run_model()).clone()
restore() restore()
reference = _first_tensor(self._run_model()).clone() reference = _first_tensor(self._run_model()).clone()
draft_reference = self.drafter.tokens.clone() if self.drafter else None
stream.synchronize() stream.synchronize()
graph = torch.cuda.CUDAGraph() graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, stream=stream): with torch.cuda.graph(graph, stream=stream):
@@ -216,6 +249,8 @@ class ForwardBench:
v.numel() * v.element_size() for v in self.state_views v.numel() * v.element_size() for v in self.state_views
), ),
) )
if self.drafter is not None:
self.result["drafter"] = self.drafter.check(draft_reference)
self.result["latency_us"] = self._time(graph) self.result["latency_us"] = self._time(graph)
from vllm.benchmarks.forward.profile import profile_graph from vllm.benchmarks.forward.profile import profile_graph
+4 -1
View File
@@ -51,10 +51,13 @@ def touched_kv_views(
batch: BenchBatch, batch: BenchBatch,
num_blocks: int, num_blocks: int,
dcp_size: int, dcp_size: int,
lookahead: int = 0,
) -> list[torch.Tensor]: ) -> list[torch.Tensor]:
"""Views of the KV blocks a step may write: each request's last """Views of the KV blocks a step may write: each request's last
ceil(scheduled / block_size) + 2 blocks, in every group.""" ceil(scheduled / block_size) + 2 blocks, in every group."""
scheduled_tokens = [batch.expected[req_id][0] for req_id in batch.req_ids] scheduled_tokens = [
batch.expected[req_id][0] + lookahead for req_id in batch.req_ids
]
views = [] views = []
for group, ranges in zip(kv_cache_groups, batch.group_block_ranges): for group, ranges in zip(kv_cache_groups, batch.group_block_ranges):
block_size = kv_block_size(group.kv_cache_spec, dcp_size) block_size = kv_block_size(group.kv_cache_spec, dcp_size)
+27 -3
View File
@@ -5,8 +5,8 @@
Each case is a list of requests `(query_len, kv_len, num_draft_tokens)` Each case is a list of requests `(query_len, kv_len, num_draft_tokens)`
per DP rank. The bench builds the batch without the scheduler, runs it through per DP rank. The bench builds the batch without the scheduler, runs it through
the real `execute_model` (input prep, attention metadata, DP sync), and times the real `execute_model` (input prep, attention metadata, DP sync), and times
the target-model forward captured as a CUDA graph. Sampling, the speculator the target forward and configured drafter captured as a CUDA graph. Target
and CPU step overhead aren't measured. sampling, rejection sampling and CPU step overhead aren't measured.
Single GPU: `vllm bench forward --model M --case decode=32*1@8192`. Single GPU: `vllm bench forward --model M --case decode=32*1@8192`.
Multi-GPU: `torchrun --nproc-per-node=N -m vllm.entrypoints.cli.main bench Multi-GPU: `torchrun --nproc-per-node=N -m vllm.entrypoints.cli.main bench
@@ -71,6 +71,13 @@ def add_cli_args(parser: FlexibleArgumentParser) -> None:
help="MoE routing. balanced: fixed routing with equal load per EP rank. " help="MoE routing. balanced: fixed routing with equal load per EP rank. "
"auto: balanced with dummy weights, else real.", "auto: balanced with dummy weights, else real.",
) )
parser.add_argument(
"--exclude-drafter",
action="store_true",
help="Time only the target model. By default, a configured MTP, EAGLE, "
"DSpark, or DFlash drafter is included, assuming all draft tokens "
"are accepted. Target sampling and rejection sampling are excluded.",
)
parser.add_argument("--iters", type=int, default=20, help="Replays per sample.") parser.add_argument("--iters", type=int, default=20, help="Replays per sample.")
parser.add_argument("--samples", type=int, default=5) parser.add_argument("--samples", type=int, default=5)
parser.add_argument("--output-json", help="Write results to this file.") parser.add_argument("--output-json", help="Write results to this file.")
@@ -149,6 +156,16 @@ def run_case(runner, case: BenchCase, args: argparse.Namespace) -> dict[str, Any
kv_config = runner.kv_cache_config kv_config = runner.kv_cache_config
dcp_size = runner.parallel_config.decode_context_parallel_size dcp_size = runner.parallel_config.decode_context_parallel_size
rng = np.random.default_rng([args.seed, runner.dp_rank]) rng = np.random.default_rng([args.seed, runner.dp_rank])
include_drafter = not args.exclude_drafter
lookahead = (
runner.num_speculative_steps + 1
if include_drafter and runner.vllm_config.speculative_config is not None
else 0
)
if lookahead and any(
r.kv_len + lookahead > runner.model_config.max_model_len for r in case.requests
):
raise ValueError("Increase --max-model-len to leave room for draft lookahead.")
batch = build_batch( batch = build_batch(
case, case,
kv_config.kv_cache_groups, kv_config.kv_cache_groups,
@@ -156,6 +173,7 @@ def run_case(runner, case: BenchCase, args: argparse.Namespace) -> dict[str, Any
runner.model_config.get_vocab_size(), runner.model_config.get_vocab_size(),
rng, rng,
dcp_size, dcp_size,
lookahead,
) )
zero_kv_blocks(context, kv_config.kv_cache_groups, batch, kv_config.num_blocks) zero_kv_blocks(context, kv_config.kv_cache_groups, batch, kv_config.num_blocks)
profile_prefix = None profile_prefix = None
@@ -176,10 +194,16 @@ def run_case(runner, case: BenchCase, args: argparse.Namespace) -> dict[str, Any
args.iters, args.iters,
args.samples, args.samples,
touched_kv_views( touched_kv_views(
context, kv_config.kv_cache_groups, batch, kv_config.num_blocks, dcp_size context,
kv_config.kv_cache_groups,
batch,
kv_config.num_blocks,
dcp_size,
lookahead,
), ),
case.name, case.name,
profile_prefix, profile_prefix,
include_drafter=include_drafter,
) )
routing, balanced = install_routing( routing, balanced = install_routing(
runner, args.routing, args.load_format == "dummy", batch.num_tokens, args.seed runner, args.routing, args.load_format == "dummy", batch.num_tokens, args.seed
+5 -12
View File
@@ -19,7 +19,6 @@ from vllm.distributed.parallel_state import (
get_world_group, get_world_group,
graph_capture, graph_capture,
) )
from vllm.forward_context import get_forward_context, override_forward_context
from vllm.sequence import IntermediateTensors from vllm.sequence import IntermediateTensors
from vllm.v1.executor.uniproc_executor import ExecutorWithExternalLauncher from vllm.v1.executor.uniproc_executor import ExecutorWithExternalLauncher
@@ -144,23 +143,15 @@ class PipelineBench(ForwardBench):
assert received is not None assert received is not None
for key, tensor in self.input_tensors.tensors.items(): for key, tensor in self.input_tensors.tensors.items():
tensor.copy_(received[key]) tensor.copy_(received[key])
context = get_forward_context() self._run_model = self._make_forward(inputs)
saved = dict(vars(context))
def forward():
for name, value in saved.items():
setattr(context, name, value)
with override_forward_context(context):
return self._model_forward(**inputs)
self._run_model = forward
self.snapshots = [(view, view.clone()) for view in self.state_views] self.snapshots = [(view, view.clone()) for view in self.state_views]
self.input_snapshots = ( self.input_snapshots = (
{k: v.clone() for k, v in self.input_tensors.tensors.items()} {k: v.clone() for k, v in self.input_tensors.tensors.items()}
if self.input_tensors is not None if self.input_tensors is not None
else {} else {}
) )
output = forward() output = self._run_model()
self.draft_reference = self.drafter.tokens.clone() if self.drafter else None
self.reference = {k: v.clone() for k, v in _tensors(output).items()} self.reference = {k: v.clone() for k, v in _tensors(output).items()}
if not pp.is_last_rank: if not pp.is_last_rank:
pp.send_tensor_dict(_tensors(output), all_gather_group=get_tp_group()) pp.send_tensor_dict(_tensors(output), all_gather_group=get_tp_group())
@@ -201,6 +192,8 @@ class PipelineBench(ForwardBench):
v.numel() * v.element_size() for v in self.state_views v.numel() * v.element_size() for v in self.state_views
), ),
) )
if self.drafter is not None:
self.result["drafter"] = self.drafter.check(self.draft_reference)
def _stream(self, profile: bool = False): def _stream(self, profile: bool = False):
pp, world = get_pp_group(), get_world_group() pp, world = get_pp_group(), get_world_group()
+94
View File
@@ -0,0 +1,94 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Run the configured drafter with fixed, fully accepted target results."""
import numpy as np
import torch
from vllm.v1.worker.gpu.dp_utils import DPSyncState
from vllm.v1.worker.gpu.spec_decode.utils import get_drafter_hidden_states
from vllm.v1.worker.workspace import use_workspace_lane
class DraftBench:
def __init__(self, runner):
self.runner = runner
self.speculator = runner.speculator
self.method = runner.vllm_config.speculative_config.method
if self.method not in ("mtp", "eagle", "eagle3", "dspark", "dflash"):
raise ValueError(
f"Drafter timing does not support {self.method!r}; "
"use --exclude-drafter for target-only timing."
)
def prepare(self, input_batch, context):
runner = self.runner
self.batch = (
runner.pcp_manager.global_batch if runner.pcp_manager else input_batch
)
self.context = context
drafts = self.batch.num_draft_tokens_per_req
accepted = (
np.ones(self.batch.num_reqs, dtype=np.int32)
if drafts is None
else drafts.astype(np.int32) + 1
)
self.num_sampled = torch.tensor(accepted, device=runner.device)
self.num_rejected = torch.zeros_like(self.num_sampled)
# Fixed valid bonus token: target logits and rejection sampling are not timed.
self.last_sampled = torch.zeros_like(runner.req_states.last_sampled_tokens)
self.dp_sync_state = (
DPSyncState(
num_tokens_across_dp=torch.full(
(runner.dp_size,),
input_batch.num_tokens_after_padding,
dtype=torch.int32,
),
uniform_token_count=None,
eager=True,
num_reqs=self.batch.num_reqs,
)
if runner.dp_size > 1
else None
)
def propose(self, output):
runner = self.runner
if runner.use_aux_hidden_state_outputs:
hidden, aux = output
else:
hidden, aux = output, None
draft_hidden = get_drafter_hidden_states(runner.model, hidden)
if runner.pcp_manager is not None and aux is not None:
aux = [runner.pcp_manager.restore_hidden_states(states) for states in aux]
with use_workspace_lane(runner._draft_workspace_lane):
self.tokens = self.speculator.propose(
self.batch,
self.context.attn_metadata,
self.context.slot_mapping,
draft_hidden,
aux,
self.num_sampled,
self.num_rejected,
self.last_sampled,
runner.req_states.next_prefill_tokens,
runner.sampler.sampling_states.temperature.gpu,
runner.sampler.sampling_states.seeds.gpu,
dp_sync_state=self.dp_sync_state,
num_speculative_tokens=runner.num_speculative_steps,
)
def check(self, reference):
return {
"method": self.method,
"acceptance": "all",
"num_draft_tokens": self.runner.num_speculative_steps,
"output_shape": list(self.tokens.shape),
"graph_matches_eager": bool(torch.equal(self.tokens, reference)),
"valid_token_ids": bool(
(
(self.tokens >= 0)
& (self.tokens < self.runner.model_config.get_vocab_size())
).all()
),
}