[Perf][Spec Decoding] Skip cat/topk/sort/gather in draft_forward for topk=1 (#26424)

This commit is contained in:
Qiaolin Yu
2026-06-01 15:37:47 -07:00
committed by GitHub
parent 1d4ee060c2
commit 4151a04d1a
4 changed files with 164 additions and 26 deletions
+10 -1
View File
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, List, Optional
import torch
from sglang.srt.utils import is_cuda, is_hip, is_musa, is_npu
from sglang.srt.utils.async_probe import maybe_detect_oob
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import ScheduleBatch
@@ -98,13 +99,21 @@ def organize_draft_results(
top_scores = torch.topk(score_list, num_draft_token - 1, dim=-1)
top_scores_index = top_scores.indices
top_scores_index = torch.sort(top_scores_index).values
maybe_detect_oob(
top_scores_index,
0,
ss_token_list.shape[1],
"organize_draft_results: top_scores_index OOB for gather on ss_token_list",
)
draft_tokens = torch.gather(ss_token_list, index=top_scores_index, dim=1)
if len(parents_list) > 1:
parent_list = torch.cat(parents_list[:-1], dim=1)
else:
batch_size = parents_list[0].shape[0]
parent_list = torch.empty(batch_size, 0, device=parents_list[0].device)
parent_list = torch.empty(
batch_size, 0, dtype=torch.long, device=parents_list[0].device
)
return parent_list, top_scores_index, draft_tokens
@@ -59,6 +59,7 @@ from sglang.srt.speculative.eagle_utils import (
TreeMaskMode,
_eagle_prefill_tail_tokens,
build_tree_kernel_efficient,
organize_draft_results,
per_step_draft_out_cache_loc,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
@@ -141,6 +142,11 @@ class EagleDraftWorker(BaseDraftWorker):
server_args.speculative_algorithm
)
# Pre-allocated constants for the topk=1 chain fast path in draft_forward.
self._topk1_parents_prealloc = None
self._topk1_score_indices_prealloc = None
self._rebuild_topk1_chain_buffers()
# Do not capture cuda graph in `TpModelWorker` init,
# will capture later with init_cuda_graphs()
backup_disable_cuda_graph = server_args.disable_cuda_graph
@@ -214,6 +220,35 @@ class EagleDraftWorker(BaseDraftWorker):
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def _rebuild_topk1_chain_buffers(self) -> None:
# For topk=1 the draft tree degenerates to a chain, so parent_list and
# top_scores_index are runtime-invariant. Must be rebuilt after any
# change to speculative_num_steps / speculative_num_draft_tokens.
if self.topk != 1:
return
# _override_worker_state can set both directly, bypassing the hook that
# pins this relation; the fast path is only valid when it holds.
assert self.speculative_num_draft_tokens == self.speculative_num_steps + 1, (
"topk=1 requires speculative_num_draft_tokens == speculative_num_steps + 1, "
f"got {self.speculative_num_draft_tokens} and {self.speculative_num_steps}"
)
num_steps = self.speculative_num_steps
sa = self.server_args
max_bs = max(
sa.cuda_graph_max_bs or 0,
sa.max_running_requests or 0,
1,
)
# A single-step chain has no parent entries (slow path drops the last
# step). repeat (not expand): the kernel reads these as contiguous.
parent_width = num_steps if num_steps > 1 else 0
self._topk1_parents_prealloc = torch.arange(
-1, parent_width - 1, dtype=torch.long, device=self.device
).repeat(max_bs, 1)
self._topk1_score_indices_prealloc = torch.arange(
num_steps, dtype=torch.long, device=self.device
).repeat(max_bs, 1)
def init_token_map(self):
# Load hot token ids
if self.speculative_algorithm.is_eagle3():
@@ -554,32 +589,24 @@ class EagleDraftWorker(BaseDraftWorker):
forward_batch.positions.add_(1)
# Organize the results
score_list = torch.cat(score_list, dim=1).flatten(
1
) # b, n, topk; n= 1 + (num_steps-1) * self.topk
ss_token_list = torch.cat(
token_list, dim=1
) # b, (self.topk + (num_steps-1) * self.topk)
top_scores = torch.topk(
score_list, self.speculative_num_draft_tokens - 1, dim=-1
)
top_scores_index = top_scores.indices
top_scores_index = torch.sort(top_scores_index).values
maybe_detect_oob(
top_scores_index,
0,
ss_token_list.shape[1],
"draft_forward: top_scores_index OOB for gather on ss_token_list",
)
draft_tokens = torch.gather(ss_token_list, index=top_scores_index, dim=1)
if (
self.topk == 1
and token_list[0].shape[0] <= self._topk1_parents_prealloc.shape[0]
):
# Chain topology: draft_tokens = concat of per-step tokens; the
# full-length topk/sort/gather over score_list collapses to an
# identity. parent_list and top_scores_index are runtime-invariant
# constants pre-allocated on the worker. Oversized batches (rare,
# would silently truncate the slice) fall through to the slow path.
bs = token_list[0].shape[0]
draft_tokens = torch.cat(token_list, dim=1)
top_scores_index = self._topk1_score_indices_prealloc[:bs]
parent_list = self._topk1_parents_prealloc[:bs]
return parent_list, top_scores_index, draft_tokens
if len(parents_list) > 1:
parent_list = torch.cat(parents_list[:-1], dim=1)
else:
batch_size = parents_list[0].shape[0]
parent_list = torch.empty(batch_size, 0, device=parents_list[0].device)
return parent_list, top_scores_index, draft_tokens
return organize_draft_results(
score_list, token_list, parents_list, self.speculative_num_draft_tokens
)
def draft_extend(self):
pass
@@ -1032,6 +1059,7 @@ class EAGLEWorkerV2(BaseSpecWorker):
dw.cuda_graph_runner = state.cuda_graph_runner
dw.draft_extend_attn_backend = state.draft_extend_attn_backend
dw.cuda_graph_runner_for_draft_extend = state.cuda_graph_runner_for_draft_extend
dw._rebuild_topk1_chain_buffers()
# Target side
self._target_worker.model_runner.attn_backend = state.target_attn_backend
@@ -1070,6 +1098,7 @@ class EAGLEWorkerV2(BaseSpecWorker):
dw.speculative_num_draft_tokens = speculative_num_draft_tokens
sa.speculative_num_steps = speculative_num_steps
sa.speculative_num_draft_tokens = speculative_num_draft_tokens
dw._rebuild_topk1_chain_buffers()
try:
yield
@@ -1087,6 +1116,7 @@ class EAGLEWorkerV2(BaseSpecWorker):
sa.speculative_num_steps,
sa.speculative_num_draft_tokens,
) = backup
dw._rebuild_topk1_chain_buffers()
def verify(self, batch: ScheduleBatch):
fwd_stream = torch.get_device_module(self.device).current_stream()
@@ -70,6 +70,11 @@ class StandaloneDraftWorker(EagleDraftWorker):
server_args.speculative_algorithm
)
# Pre-allocated constants for the topk=1 chain fast path in draft_forward.
self._topk1_parents_prealloc = None
self._topk1_score_indices_prealloc = None
self._rebuild_topk1_chain_buffers()
# Set constant
from sglang.srt.speculative.eagle_info import EagleDraftInput