chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,134 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Protocol
from sglang.srt.speculative.adaptive_spec_params import AdaptiveSpeculativeParams
if TYPE_CHECKING:
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
from sglang.srt.model_executor.cpu_graph_runner import CPUGraphRunner
from sglang.srt.model_executor.runner import DecodeCudaGraphRunner
from sglang.srt.speculative.eagle_draft_cuda_graph_runner import (
EAGLEDraftCudaGraphRunner,
)
from sglang.srt.speculative.eagle_draft_extend_cuda_graph_runner import (
EAGLEDraftExtendCudaGraphRunner,
)
@dataclass
class SpecRuntimeState:
"""A complete set of runtime resources bound to a specific speculative
decoding configuration.
Each decode round runs three stages — draft, verify, extend — and every
stage has shape-dependent resources (attention backends and CUDA graphs)
that must match the current configuration. Switching adaptive steps
means swapping the entire state atomically.
"""
# -- Configuration (determines shapes for all stages) --
speculative_num_steps: int
speculative_num_draft_tokens: int
# -- Draft stage: draft model multi-step autoregressive generation --
draft_attn_backend: "AttentionBackend | None"
cuda_graph_runner: "EAGLEDraftCudaGraphRunner | None"
# -- Verify stage: target model one-pass tree verification --
target_attn_backend: "AttentionBackend"
target_graph_runner: "DecodeCudaGraphRunner | CPUGraphRunner | None"
# -- Extend stage: draft model KV cache catch-up after verify --
draft_extend_attn_backend: "AttentionBackend | None"
cuda_graph_runner_for_draft_extend: "EAGLEDraftExtendCudaGraphRunner | None"
class AdaptiveSpecWorker(Protocol):
"""Protocol that a worker must implement to use AdaptiveController."""
speculative_num_steps: int
def build_adaptive_runtime_state(
self,
speculative_num_steps: int,
speculative_num_draft_tokens: int,
cuda_graph_bs: list[int] | None = None,
) -> SpecRuntimeState: ...
def apply_runtime_state(self, state: SpecRuntimeState) -> None: ...
class AdaptiveController:
"""Facade that owns adaptive decision-making and runtime state switching.
Works with any worker that implements AdaptiveSpecWorker protocol:
- build_adaptive_runtime_state(steps, draft_tokens) → runtime state
- apply_runtime_state(state) → apply it to the worker
The worker only needs to:
1. Call register() for the initial state, then init_states()
once during startup.
2. Call on_verify_complete(num_correct_drafts_per_req) after each decode verify.
"""
def __init__(self, worker: AdaptiveSpecWorker, config_path: str | None = None):
self.worker = worker
self.params = AdaptiveSpeculativeParams(
initial_steps=worker.speculative_num_steps,
cfg_path=config_path,
)
self._states: dict[int, SpecRuntimeState] = {}
@property
def candidate_steps(self) -> list[int]:
return self.params.candidate_steps
def register(self, state: SpecRuntimeState, steps: int | None = None) -> None:
"""Register a pre-built runtime state.
*steps* defaults to state.speculative_num_steps when not given.
"""
key = steps if steps is not None else state.speculative_num_steps
self._states[key] = state
def init_states(self, cuda_graph_bs: list[int] | None = None) -> None:
"""Build and register runtime states for all candidate steps."""
self.params.set_cuda_graph_bs(cuda_graph_bs)
for steps in self.candidate_steps:
if steps in self._states:
continue
pruned_bs = self.params.cuda_graph_bs_for_step(steps)
state = self.worker.build_adaptive_runtime_state(
speculative_num_steps=steps,
speculative_num_draft_tokens=steps + 1,
cuda_graph_bs=pruned_bs,
)
self._states[steps] = state
# Start on the initial step.
self._activate(self.worker.speculative_num_steps)
def activate_step_by_batch(self, batch_size: int) -> None:
target = self.params.get_steps_for_batch(batch_size)
if target != self.worker.speculative_num_steps:
self._activate(target)
def on_verify_complete(
self, num_correct_drafts_per_req: list[int], batch_size: int
) -> None:
"""Feed verify results; switch runtime state if EMA warrants it."""
new_step = self.params.on_verify_complete(
num_correct_drafts_per_req, batch_size
)
if new_step is not None:
self._activate(new_step)
def _activate(self, speculative_num_steps: int) -> None:
state = self._states.get(speculative_num_steps)
if state is None:
raise ValueError(
f"Missing adaptive runtime state for steps={speculative_num_steps}"
)
self.worker.apply_runtime_state(state)
@@ -0,0 +1,345 @@
"""Adaptive speculative decoding parameters.
Adjusts speculative_num_steps at runtime based on observed acceptance lengths.
"""
from __future__ import annotations
import bisect
import json
import logging
import math
from functools import cached_property
from typing import TYPE_CHECKING
from sglang.srt.utils import log_info_on_rank0
if TYPE_CHECKING:
from sglang.srt.server_args import ServerArgs
logger = logging.getLogger(__name__)
DEFAULT_ADAPTIVE_CONFIG: dict[str, dict] = {
"1": {
"candidate_steps": [1, 3, 7],
"up_hysteresis": 0.0,
"down_hysteresis": -0.25,
"ceiling_coeff": 0,
},
"8": {
"candidate_steps": [0, 1, 3],
"up_hysteresis": 0.0,
"down_hysteresis": 0.0,
"ceiling_coeff": 0,
},
"32": {
"candidate_steps": [0, 1],
"up_hysteresis": 0.0,
"down_hysteresis": 0.0,
"ceiling_coeff": 0,
},
"64": {
"candidate_steps": [0],
"up_hysteresis": 0.0,
"down_hysteresis": 0.0,
"ceiling_coeff": 0,
},
}
def adaptive_unsupported_reason(server_args: ServerArgs) -> str | None:
"""Return why adaptive spec cannot run under the given server args, or None if supported."""
from sglang.srt.arg_groups.overrides import resolved_view
if server_args.speculative_algorithm not in ("EAGLE", "EAGLE3"):
return (
f"speculative_algorithm={server_args.speculative_algorithm} "
"(only EAGLE/EAGLE3 are supported)"
)
if (
server_args.speculative_eagle_topk is not None
and server_args.speculative_eagle_topk != 1
):
return (
f"speculative_eagle_topk={server_args.speculative_eagle_topk} "
"(only topk=1 is supported)"
)
if resolved_view(server_args).enable_dp_attention:
return (
"enable_dp_attention=True is not supported "
"(adaptive tier decisions are not synchronized across DP ranks)"
)
if resolved_view(server_args).enable_multi_layer_eagle:
return (
"enable_multi_layer_eagle=True is not supported "
"(MultiLayerEagleWorkerV2 does not implement adaptive)"
)
if server_args.enable_two_batch_overlap:
return (
"enable_two_batch_overlap=True is not supported "
"(adaptive state swap would discard the TboAttnBackend wrapper)"
)
if server_args.enable_pdmux:
return (
"enable_pdmux=True is not supported "
"(adaptive state swap does not update decode_attn_backend_group)"
)
return None
def _load_adaptive_config(
cfg_path: str | None,
) -> tuple[dict, dict[int, dict]]:
"""Load and validate adaptive config.
Uses ``DEFAULT_ADAPTIVE_CONFIG`` when *cfg_path* is ``None``.
"""
if cfg_path is not None:
with open(cfg_path) as f:
cfg = json.load(f)
else:
cfg = DEFAULT_ADAPTIVE_CONFIG
bs_entries: dict[int, dict] = {}
for key, entry in cfg.items():
if not key.isdigit():
continue
steps = entry.get("candidate_steps")
if (
not isinstance(steps, list)
or not steps
or not all(isinstance(s, int) and s >= 0 for s in steps)
):
raise ValueError(
f"BS {key}: candidate_steps must be a list of non-negative ints, "
f"got {steps!r}"
)
bs_entries[int(key)] = entry
if not bs_entries:
raise ValueError(
"speculative_adaptive_config must contain at least one integer-string "
'BS key, e.g. {"1": {"candidate_steps": [1,3,7]}}. '
f"Got keys: {list(cfg.keys())}"
)
return cfg, bs_entries
def resolve_candidate_steps_from_config(
cfg_path: str | None = None,
) -> list[int]:
"""Union of every BS slot's candidate steps; sizes the runtime buffers."""
_, bs_entries = _load_adaptive_config(cfg_path)
all_steps: set[int] = set()
for entry in bs_entries.values():
all_steps.update(entry["candidate_steps"])
return sorted(all_steps)
class AdaptiveStepSlot:
"""Tracks acceptance rate via EMA and adapts num_steps accordingly.
The core idea: if drafts are consistently accepted, try more steps;
if drafts are consistently rejected early, reduce steps to avoid waste.
Formula: target_steps = clamp(round(ema_accept_len) + 1, min_steps, max_steps)
- Probes one step beyond observed acceptance
- EMA smoothing prevents oscillation
- Only updates every `update_interval` batches for stability
- num_steps can be selected from different candidate sets on different batch_sizes
"""
def __init__(self, initial_steps: int, cfg: dict):
candidates = sorted(set(cfg["candidate_steps"]))
assert len(candidates) >= 1, "candidate_steps must have at least 1 value"
self.candidate_steps = candidates
self.ema_alpha = cfg.get("ema_alpha", 0.2)
self.update_interval = cfg.get("update_interval", 5)
self.warmup_batches = cfg.get("warmup_batches", 10)
self.down_hysteresis = cfg.get("down_hysteresis", -0.25)
self.up_hysteresis = cfg.get("up_hysteresis", 0.0)
self.ceiling_coeff = cfg.get("ceiling_coeff", 0)
if initial_steps in self.candidate_steps:
self.current_steps = initial_steps
else:
self.current_steps = self.candidate_steps[len(self.candidate_steps) // 2]
# Initialize EMA at current steps - 1 (neutral starting point)
self.ema_accept_len = float(self.current_steps - 1)
self._batch_count = 0
def update(self, num_correct_drafts_per_req: list[int]) -> bool:
"""Update EMA with observed accept lengths. Returns True if params changed.
Args:
num_correct_drafts_per_req: Per-request accepted draft token counts from last verify.
"""
if not num_correct_drafts_per_req:
return False
if self.current_steps > 0:
batch_avg = sum(num_correct_drafts_per_req) / len(
num_correct_drafts_per_req
)
self.ema_accept_len = (
1 - self.ema_alpha
) * self.ema_accept_len + self.ema_alpha * batch_avg
self._batch_count += 1
if self._batch_count <= self.warmup_batches:
return False
if (self._batch_count - self.warmup_batches) % self.update_interval != 0:
return False
return self._recompute_params()
def _recompute_params(self) -> bool:
"""Recompute steps from EMA. Returns True if params changed."""
old_steps = self.current_steps
current_idx = self.candidate_steps.index(old_steps)
old_idx = current_idx
# Probe the smallest positive step after a zero-step nospec interval.
if old_steps == 0:
current_idx = min(current_idx + 1, len(self.candidate_steps) - 1)
target = self.candidate_steps[current_idx]
if target > 0 and self.ema_accept_len < 0:
# A slot initialized at steps=0 has no draft acceptance history;
# start the first positive-step probe from that step's neutral EMA.
self.ema_accept_len = float(target - 1)
return self._apply_target_steps(old_steps, target)
# TODO: Consider limiting step changes to avoid overshooting.
while current_idx > 0:
prev_step = self.candidate_steps[current_idx - 1]
# A zero-step candidate disables drafting. Treat zero accepted drafts
# as low enough to reach it when it is the floor candidate.
drop_threshold = 0.5 if prev_step == 0 else prev_step - 0.5
drop_threshold += self.down_hysteresis
if self.ema_accept_len <= drop_threshold:
current_idx -= 1
else:
break
moved_down = current_idx < old_idx
if not moved_down:
while current_idx < len(self.candidate_steps) - 1:
current_step = self.candidate_steps[current_idx]
rise_threshold = current_step - 0.5 + self.up_hysteresis
if self.ema_accept_len > rise_threshold:
current_idx += 1
else:
break
target = self.candidate_steps[current_idx]
# EMA ceiling: only caps downward — never blocks step-ups, so the
# system can explore higher steps and let the EMA catch up.
if self.ceiling_coeff > 0:
ceiling = max(1, math.ceil(self.ema_accept_len * self.ceiling_coeff))
if target > ceiling and target <= old_steps:
while current_idx > 0 and self.candidate_steps[current_idx] > ceiling:
current_idx -= 1
target = self.candidate_steps[current_idx]
return self._apply_target_steps(old_steps, target)
def _apply_target_steps(self, old_steps: int, target: int) -> bool:
if target != old_steps:
self.current_steps = target
log_info_on_rank0(
logger,
f"Adaptive spec params updated: steps {old_steps} -> {target} "
f"(ema_accept_len={self.ema_accept_len:.2f})",
)
return True
return False
class AdaptiveSpeculativeParams:
"""Routes ``batch_size`` to the correct per-BS slot.
A slot is a per-BS configuration of adaptive step selection.
"""
def __init__(
self,
initial_steps: int,
cfg_path: str | None = None,
):
cfg, bs_entries = _load_adaptive_config(cfg_path)
self._bs_list: list[int] = sorted(bs_entries)
self._slots: dict[int, AdaptiveStepSlot] = {}
self._cuda_graph_bs: list[int] | None = None
for bs, entry in sorted(bs_entries.items()):
self._slots[bs] = AdaptiveStepSlot(
initial_steps=initial_steps,
cfg={**cfg, **entry},
)
first_slot = self._slots[self._bs_list[0]]
log_info_on_rank0(
logger,
f"AdaptiveSpeculativeParams initialized: "
f"steps={first_slot.current_steps}, "
f"candidate_steps={first_slot.candidate_steps}",
)
@cached_property
def candidate_steps(self) -> list[int]:
"""Union of all BS slots' candidate steps."""
return sorted({s for p in self._slots.values() for s in p.candidate_steps})
def set_cuda_graph_bs(self, cuda_graph_bs: list[int] | None) -> None:
self._cuda_graph_bs = sorted(cuda_graph_bs) if cuda_graph_bs else None
def get_steps_for_batch(self, batch_size: int) -> int:
return self._route(batch_size).current_steps
def on_verify_complete(
self, num_correct_drafts_per_req: list[int], batch_size: int
) -> int | None:
"""Feed verify results to the matching BS slot's EMA.
Returns the new step if a switch is warranted, else ``None``.
"""
params = self._route(batch_size)
if params.update(num_correct_drafts_per_req):
return params.current_steps
return None
def cuda_graph_bs_for_step(self, step: int) -> list[int] | None:
"""Return cuda_graph_bs values that can reach *step* at runtime.
Returns ``None`` when CUDA graphs are disabled (``set_cuda_graph_bs``
was never called or was called with ``None``).
"""
if self._cuda_graph_bs is None:
return None
return [
v
for v in self._cuda_graph_bs
if step in self._slots[self._find_closest_bs(v)].candidate_steps
]
def _route(self, batch_size: int) -> AdaptiveStepSlot:
"""Map *batch_size* → pad to CUDA-graph BS → closest slot."""
return self._slots[
self._find_closest_bs(self._pad_to_cuda_graph_bs(batch_size))
]
def _pad_to_cuda_graph_bs(self, batch_size: int) -> int:
if self._cuda_graph_bs is None:
return batch_size
idx = bisect.bisect_left(self._cuda_graph_bs, batch_size)
return (
self._cuda_graph_bs[idx] if idx < len(self._cuda_graph_bs) else batch_size
)
def _find_closest_bs(self, target: int) -> int:
idx = bisect.bisect_right(self._bs_list, target) - 1
return self._bs_list[max(0, idx)]
@@ -0,0 +1,355 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
import torch
from sglang.srt.utils import is_cpu
_is_cpu = is_cpu()
if _is_cpu:
from sgl_kernel import assign_draft_cache_locs_contiguous_cpu
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
from sglang.srt.model_executor.model_runner import ModelRunner
from sglang.srt.speculative.eagle_draft_cuda_graph_runner import (
EAGLEDraftCudaGraphRunner,
)
from sglang.srt.speculative.eagle_info import (
EagleDraftExtendInput,
EagleDraftInput,
)
def duplicate_prefix_tail_to_draft_branches(
token_to_kv_pool,
rows: torch.Tensor,
prefix_base: torch.Tensor,
last_page: torch.Tensor,
num_new_pages: torch.Tensor,
topk: int,
page_size: int,
) -> None:
"""Copy the prefix partial-tail page into each branch's first-page holes (page>1 + topk>1).
The draft-decode expand pass reads each branch's own draft page by block id
(cache_loc // page_size), so branch b>=1's hole slots [0, last_page) must hold the
real prefix tail (branch 0's first page already is it). Mirrors V1 #7725.
"""
if topk <= 1:
return
bs = rows.shape[0]
page_off = torch.arange(page_size, device=rows.device, dtype=torch.int64)
branches = torch.arange(1, topk, device=rows.device, dtype=torch.int64).view(
1, topk - 1, 1
)
# Source: the prefix tail page [prefix_base, prefix_base + page_size), one per branch.
src_pos = (prefix_base.view(bs, 1, 1) + page_off.view(1, 1, page_size)).expand(
bs, topk - 1, page_size
)
# Target: branch b's first page [prefix_base + b*num_new_pages*page, + page_size).
tgt_pos = (
prefix_base.view(bs, 1, 1)
+ branches * (num_new_pages.view(bs, 1, 1) * page_size)
+ page_off.view(1, 1, page_size)
)
# Only [0, last_page) holds real prefix KV; [last_page, page_size) are the branch's
# own draft slots and must not be overwritten.
vmask = (page_off.view(1, 1, page_size) < last_page.view(bs, 1, 1)).expand(
bs, topk - 1, page_size
)
src_slots = torch.gather(rows, 1, src_pos.reshape(bs, -1)).reshape(
bs, topk - 1, page_size
)[vmask]
tgt_slots = torch.gather(rows, 1, tgt_pos.reshape(bs, -1)).reshape(
bs, topk - 1, page_size
)[vmask]
if src_slots.numel() > 0:
token_to_kv_pool.move_kv_cache(tgt_slots, src_slots)
class EagleDraftWorkerBase(ABC):
@abstractmethod
def draft():
pass
@abstractmethod
def draft_extend():
pass
def alloc_memory_pool(self, **kwargs):
pass
def init_attention_backends(self):
"""Subclasses wrap this with their context managers (draft_tp_context,
speculative_moe_backend_context, etc.) rather than reimplementing it."""
self.draft_worker.init_attention_backends()
self.init_attention_backend()
def init_cuda_graphs(self):
"""Capture draft graphs (decode disabled on the draft TpModelWorker)."""
self.draft_worker.init_cuda_graphs(capture_decode_cuda_graph=False)
self._capture_cuda_graphs()
def prepare_for_draft_extend(
self,
draft_extend_input: EagleDraftExtendInput,
batch: ScheduleBatch,
predict: torch.Tensor,
num_draft_tokens: int,
draft_model_runner: Any,
cuda_graph_runner: Any,
):
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.utils.async_probe import maybe_detect_oob
from sglang.srt.utils.common import is_npu
bs = len(batch.seq_lens)
extend_num_tokens = bs * num_draft_tokens
# When seq_lens_cpu is absent, stay on GPU-only path -- no .tolist()/.cpu().
gpu_only = batch.seq_lens_cpu is None
batch.spec_info = draft_extend_input
# Do NOT cast predict dtype here. The caller (e.g., _draft_extend_for_decode)
# may run this under a plan stream; casting inside the plan stream creates a
# cross-stream dependency that can lead to data races and break MTP acceptance.
# The caller should cast to int64 before entering the plan stream context.
batch.input_ids = predict
maybe_detect_oob(
batch.input_ids,
0,
batch.model_config.vocab_size,
"v2 prepare_for_draft_extend input_ids",
)
# init_new requires both list or both Tensor;
# gpu_only emits device tensors to skip H2D.
if gpu_only:
batch.prefix_lens = batch.seq_lens.to(torch.int32)
batch.extend_lens = torch.full(
(bs,), num_draft_tokens, dtype=torch.int32, device=batch.seq_lens.device
)
else:
batch.prefix_lens = batch.seq_lens_cpu.tolist()
batch.extend_lens = [num_draft_tokens] * bs
batch.extend_num_tokens = extend_num_tokens
capture_mode = (
CaptureHiddenMode.NULL
if draft_model_runner.spec_algorithm.is_standalone()
else CaptureHiddenMode.FULL
)
batch.forward_mode = (
ForwardMode.IDLE
if batch.forward_mode.is_idle()
else ForwardMode.DRAFT_EXTEND_V2
)
batch.capture_hidden_mode = capture_mode
forward_batch = ForwardBatch.init_new(batch, draft_model_runner)
# Forward sees post-write length (draft extend writes num_draft_tokens
# slots); mutation stays on forward_batch to preserve SB.seq_lens.
forward_batch.seq_lens = forward_batch.seq_lens + num_draft_tokens
if not gpu_only:
forward_batch.seq_lens_cpu = forward_batch.seq_lens_cpu + num_draft_tokens
forward_batch.seq_lens_sum = int(forward_batch.seq_lens_cpu.sum())
else:
# Supply CPU mirror (extend_seq_lens are all num_draft_tokens) so
# backend max() reads from list without a per-iter D2H sync.
forward_batch.extend_seq_lens_cpu = [num_draft_tokens] * bs
can_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
forward_batch
)
if not batch.forward_mode.is_idle() and not can_cuda_graph:
draft_model_runner.attn_backend.init_forward_metadata(forward_batch)
# Planned pre-pad; do NOT opt into post-pad re-plan. DSA's indexer
# cannot rebuild its deep_gemm schedule_meta on a DP-padded batch
# (the `_batch_size == batch_size` assertion, see #27091); the
# marked pre-pad metadata is used as-is, matching the proven
# skip_attn_backend_init=True behavior.
# On NPU with --disable-cuda-graph, block_table shape won't match
# after prepare_mlp_sync_batch padding; defer re-init to
# forward_extend (post-pad) instead.
if not is_npu() or can_cuda_graph:
forward_batch.mark_forward_metadata_ready()
return forward_batch
def prepare_for_draft(
self,
draft_input: EagleDraftInput,
req_to_token_pool: ReqToTokenPool,
batch: ScheduleBatch,
cuda_graph_runner: EAGLEDraftCudaGraphRunner,
draft_model_runner: ModelRunner,
topk: int,
num_steps: int,
):
from sglang.kernels.ops.speculative.cache_locs import (
assign_draft_cache_locs_contiguous,
)
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
)
if not batch.forward_mode.is_idle():
bs = len(batch.seq_lens)
# Assign cache locations (draft-write targets).
page_size = batch.token_to_kv_pool_allocator.page_size
if page_size == 1 or topk == 1:
batch.out_cache_loc = torch.empty(
(bs * topk * num_steps,),
dtype=torch.int64,
device=batch.device,
)
if _is_cpu:
assign_draft_cache_locs_contiguous_cpu(
batch.req_pool_indices,
req_to_token_pool.req_to_token,
batch.seq_lens,
batch.out_cache_loc,
req_to_token_pool.req_to_token.shape[1],
topk,
num_steps,
)
else:
# FIXME(lsyin): align with the default code path
assign_draft_cache_locs_contiguous[(bs,)](
batch.req_pool_indices,
req_to_token_pool.req_to_token,
batch.seq_lens,
batch.out_cache_loc,
req_to_token_pool.req_to_token.shape[1],
topk,
num_steps,
)
else:
# page_size > 1 + topk > 1: per-branch page-aligned draft pages.
# Reduce out_cache_loc from the page-aligned tree region down to the
# dense draft slots (skip each branch's duplicated prefix-tail slots
# and trailing padding), matching generate_draft_decode_kv_indices'
# paged read formula: prefix_base + t*num_new_pages*page + last_page + s.
# base is batch.seq_lens (== KV-ready committed prefix at draft time;
# the bonus is the tree root written by verify, not part of [0:seq_lens]).
rows = req_to_token_pool.req_to_token[batch.req_pool_indices.long()]
seq_lens = batch.seq_lens.to(torch.int64)
last_page = seq_lens % page_size
prefix_base = seq_lens - last_page
num_new_pages = (last_page + num_steps + page_size - 1) // page_size
topk_ids = torch.arange(
topk, device=rows.device, dtype=torch.int64
).view(1, topk)
starts = (
prefix_base.view(bs, 1)
+ topk_ids * (num_new_pages.view(bs, 1) * page_size)
+ last_page.view(bs, 1)
)
steps = torch.arange(
num_steps, device=rows.device, dtype=torch.int64
).view(1, 1, num_steps)
pos = (starts.view(bs, topk, 1) + steps).reshape(bs, topk * num_steps)
batch.out_cache_loc = (
torch.gather(rows, 1, pos).reshape(-1).contiguous()
)
# Each branch's page-aligned region starts with `last_page` hole slots
# overlapping the prefix tail page; duplicate the real prefix-tail KV
# into them so whole-page reads stay coherent (see helper docstring).
duplicate_prefix_tail_to_draft_branches(
draft_model_runner.token_to_kv_pool,
rows,
prefix_base,
last_page,
num_new_pages,
topk,
page_size,
)
# Get a forward batch
draft_input.num_tokens_per_req = topk
draft_input.num_tokens_for_logprob_per_req = topk
capture_mode = (
CaptureHiddenMode.NULL
if draft_model_runner.spec_algorithm.is_standalone()
else CaptureHiddenMode.LAST
)
draft_input.positions = batch.seq_lens.repeat_interleave(topk, dim=0)
batch.capture_hidden_mode = capture_mode
forward_batch = ForwardBatch.init_new(batch, draft_model_runner)
can_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
forward_batch
)
return forward_batch, can_cuda_graph
class BaseSpecWorker(ABC):
@property
@abstractmethod
def target_worker(self) -> TpModelWorker:
pass
@property
@abstractmethod
def draft_worker(self) -> EagleDraftWorkerBase:
pass
@property
def war_fastpath_runner(self):
# The runner that runs the step's LAST shared-buffer-reading phase --
# it owns the read-done event the scheduler's WAR barrier waits on.
# Default is the target runner; override if the last phase runs
# elsewhere (eagle's draft_extend runs on the draft runner).
return self.target_worker.model_runner
@property
def spec_v2_attn_backends(self) -> tuple:
"""Attn backends touched by spec_v2 forward; OR-ed by decide_needs_cpu_seq_lens.
Default returns target only; subclasses extend with draft backends."""
return (self.target_worker.model_runner.attn_backend,)
@abstractmethod
def clear_cache_pool(self):
# TODO: move this abstract method to BaseTpWorker and call through self.model_runner
pass
def alloc_memory_pool(self, **kwargs):
pass
def init_attention_backends(self):
pass
def init_cuda_graphs(self):
pass
def on_verify_complete_cpu(
self, num_correct_drafts_per_req: list[int], batch_size: int = 0
) -> None:
"""Hook called after verify finishes and accept counts are on CPU.
Default no-op. Adaptive-aware workers override this to feed the
controller without forcing a GPU→CPU sync in the worker hot path.
"""
pass
def note_request_finished(self, *, rid: str, natural_stop: bool) -> None:
"""Hook called by the batch-result processor when a request finishes.
Default no-op. DSpark overrides this to settle / censor its
block-accept estimator state for the finished request.
"""
pass
def activate_step_by_batch(self, batch_size: int) -> None:
"""Activate the optimal adaptive step for the current batch size.
Default no-op. Adaptive-aware workers override this to switch
the runtime state before each draft round.
"""
pass
@@ -0,0 +1,15 @@
BasedOnStyle: Google
IndentWidth: 2
ColumnLimit: 120
AllowShortFunctionsOnASingleLine: Empty
DerivePointerAlignment: false
PointerAlignment: Left
NamespaceIndentation: None
SortIncludes: true
AllowShortLoopsOnASingleLine: false
BinPackParameters: false # Prevents packing parameters in declarations
BinPackArguments: false # Prevents packing arguments in function calls
AlignAfterOpenBracket: AlwaysBreak # Forces a break after the opening parenthesis
AlignOperands: Align # Aligns arguments vertically
PenaltyBreakBeforeFirstCallParameter: 1 # Encourages breaking before the first argument
PenaltyReturnTypeOnItsOwnLine: 100 # Keeps return type with function name
@@ -0,0 +1,62 @@
import json
from collections.abc import Iterator
from pathlib import Path
# Must match SuffixAutomaton::kSeparatorToken in suffix_automaton.h.
SEPARATOR_TOKEN = -(2**31)
# Default chunk size for streaming tokenized documents into the SAM.
DEFAULT_CHUNK_SIZE = 4096
def iter_external_corpus_chunks(
path: str, tokenizer, max_tokens: int, chunk_size: int = DEFAULT_CHUNK_SIZE
) -> Iterator[list[int]]:
"""Chunk documents and yield fixed-size token chunks from a JSONL corpus file."""
corpus_path = Path(path)
if not corpus_path.is_file():
raise ValueError(f"External ngram corpus path does not exist: {path}")
if tokenizer is None:
raise ValueError("A tokenizer is required to load an external ngram corpus.")
if max_tokens <= 0:
raise ValueError("External ngram corpus max tokens must be positive.")
total_tokens = 0
has_previous_doc = False
with corpus_path.open("r", encoding="utf-8") as f:
for line_no, line in enumerate(f, start=1):
if not line.strip():
continue
try:
record = json.loads(line)
except json.JSONDecodeError as e:
raise ValueError(
f"Invalid JSON in external ngram corpus at line {line_no}: {e.msg}"
) from e
if not isinstance(record, str):
raise ValueError(
"Invalid external ngram corpus record at line "
f"{line_no}: expected a JSON string."
)
token_ids = list(tokenizer.encode(record, add_special_tokens=False))
if not token_ids:
continue
separator_cost = 1 if has_previous_doc else 0
next_total_tokens = total_tokens + separator_cost + len(token_ids)
if next_total_tokens > max_tokens:
raise ValueError(
"External ngram corpus exceeds the configured token limit "
f"({max_tokens}) at line {line_no} after loading "
f"{total_tokens} tokens."
)
total_tokens = next_total_tokens
if has_previous_doc:
token_ids = [SEPARATOR_TOKEN] + token_ids
for i in range(0, len(token_ids), chunk_size):
yield token_ids[i : i + chunk_size]
has_previous_doc = True
@@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
import logging
from collections.abc import Iterable, Sequence
from typing import Dict, List, Tuple
import numpy as np
from sglang.jit_kernel.ngram_corpus import get_ngram_corpus_cls
logger = logging.getLogger(__name__)
class NgramCorpus:
def __init__(
self,
max_trie_depth=18,
min_bfs_breadth=1,
max_bfs_breadth=8,
draft_token_num=8,
match_type="BFS",
capacity=1000000,
external_sam_budget=0,
external_corpus_max_tokens=10000000,
) -> None:
cls = get_ngram_corpus_cls()
self._obj = cls(
capacity=capacity,
max_trie_depth=max_trie_depth,
min_bfs_breadth=min_bfs_breadth,
max_bfs_breadth=max_bfs_breadth,
draft_token_num=draft_token_num,
match_type=match_type,
external_sam_budget=external_sam_budget,
external_corpus_max_tokens=external_corpus_max_tokens,
)
self.draft_token_num = draft_token_num
self.external_corpus_max_tokens = external_corpus_max_tokens
self._req_id_to_state_id: Dict[str, int] = {}
self._next_state_id: int = 0
self._corpus_token_counts: Dict[str, int] = {}
self._total_loaded_tokens: int = 0
def _get_state_id(self, req_id: str) -> int:
sid = self._req_id_to_state_id.get(req_id)
if sid is None:
sid = self._next_state_id
self._next_state_id += 1
self._req_id_to_state_id[req_id] = sid
return sid
def batch_put(self, batch_tokens: List[List[int]]):
self._obj.insert(batch_tokens)
def synchronize(self):
self._obj.synchronize() # type: ignore
@property
def remaining_token_budget(self) -> int:
return self.external_corpus_max_tokens - self._total_loaded_tokens
def load_external_corpus_named(
self, corpus_id: str, chunks: Iterable[Sequence[int]]
) -> int:
if corpus_id in self._corpus_token_counts:
raise ValueError(
f"External corpus '{corpus_id}' already exists. Remove it before "
f"adding a new corpus with the same id."
)
# Note(kpham-sgl): remaining_token_budget is stale (e.g if there are removes
# during the load), which makes the budget more conservative than it should be.
# This is acceptable because otherwise load_external_corpus_named would need to check the budget after each chunk,
# which would be inefficient.
_, loaded_token_count = self._obj.load_external_corpus_named(
corpus_id, chunks, self.remaining_token_budget
)
return loaded_token_count
# Commit corpus bookkeeping after successful load. Call only at background thread join.
# (or after synchronous load_external_corpus_named returns)
def commit_external_corpus_load(
self, corpus_id: str, loaded_token_count: int
) -> None:
self._corpus_token_counts[corpus_id] = loaded_token_count
self._total_loaded_tokens += loaded_token_count
def remove_external_corpus(self, corpus_id: str) -> None:
self._obj.remove_corpus(corpus_id)
old_count = self._corpus_token_counts.pop(corpus_id, 0)
self._total_loaded_tokens -= old_count
def list_external_corpora(self) -> Dict[str, int]:
return self._obj.list_corpora()
def reset(self):
self._obj.reset() # type: ignore
self._req_id_to_state_id.clear()
self._next_state_id = 0
def batch_get(
self,
req_ids: List[str],
batch_tokens: List[List[int]],
total_lens: List[int],
) -> Tuple[np.ndarray, np.ndarray]:
state_ids = [self._get_state_id(rid) for rid in req_ids]
return self._obj.match_stateful(state_ids, batch_tokens, total_lens)
def erase_match_state(self, req_ids: List[str]):
state_ids = []
for rid in req_ids:
sid = self._req_id_to_state_id.pop(rid, None)
if sid is not None:
state_ids.append(sid)
if state_ids:
self._obj.erase_states(state_ids)
def leaf_paths_from_mask(
self, tokens: List[int], tree_mask: List[List[int]]
) -> List[List[int]]:
"""
Find all leaf paths according to the binary tree_mask (i.e., paths that are not prefixes of any other path).
Args:
mask : List[List[int]] # nxn binary matrix
tokens : List[int] # token list corresponding to columns
Returns:
List[List[int]] # token lists of only the leaf paths, preserving their order of appearance
"""
row_sets = [
(i, {idx for idx, v in enumerate(row) if v == 1})
for i, row in enumerate(tree_mask)
]
leaf_sets = []
leaf_rows = []
for i, cur_set in reversed(row_sets):
if any(cur_set <= kept for kept in leaf_sets):
continue
leaf_sets.append(cur_set)
leaf_rows.append(i)
leaf_rows.reverse()
result = []
for r in leaf_rows:
path = [tokens[col] for col in range(len(tokens)) if tree_mask[r][col] == 1]
result.append(path)
return result
def debug_result(
self, decoding_ids: np.ndarray, decoding_masks: np.ndarray, tokenizer=None
):
decoding_ids = decoding_ids.reshape(-1, self.draft_token_num)
decoding_masks = decoding_masks.reshape(
-1, self.draft_token_num, self.draft_token_num
)
logger.info(f"\n{decoding_ids=}\n{decoding_masks=}")
for i in range(decoding_ids.shape[0]):
leaf_paths = self.leaf_paths_from_mask(
decoding_ids[i].tolist(), decoding_masks[i].tolist()
)
if tokenizer is None:
logger.info(f"draft path {i}: {leaf_paths}")
else:
logger.info(f"result {i}:")
for leaf_path in leaf_paths:
logger.info(
f"draft path {i}: {leaf_path} -> {tokenizer.decode(leaf_path, ensure_ascii=False)}"
)
# main function
if __name__ == "__main__":
format = f"%(levelname)s %(asctime)s %(filename)s:%(lineno)d] %(message)s"
logging.basicConfig(
level=logging.DEBUG,
format=format,
datefmt="%Y-%m-%d %H:%M:%S",
force=True,
)
token_ids = [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[1, 2, 3, 44, 55, 66, 77, 88, 99, 100],
]
corpus = NgramCorpus(max_trie_depth=12, draft_token_num=8)
corpus.batch_put(token_ids)
corpus.synchronize()
queries = [[1, 2, 3], [3, 44], [3, 6, 999]]
decoding_ids, decoding_masks = corpus.batch_get(
req_ids=[f"query-{i}" for i in range(len(queries))],
batch_tokens=queries,
total_lens=[len(q) for q in queries],
)
corpus.debug_result(decoding_ids, decoding_masks)
@@ -0,0 +1,384 @@
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Optional
class DraftMeshMessageType(str, Enum):
CONTROL_BATCH = "control_batch"
TAIL_STREAM_OUTPUT_BATCH = "tail_stream_output_batch"
@dataclass(frozen=True)
class DraftReqKey:
"""Request identity on the drafter side.
The original request_id is only unique within the verifier that owns it.
src_verifier_rank keeps the drafter-side request table unambiguous when
multiple verifier ranks send work to the same drafter rank.
"""
src_verifier_rank: int
request_id: str
def build_draft_scheduler_rid(draft_key: DraftReqKey) -> str:
return f"draft:{int(draft_key.src_verifier_rank)}:{draft_key.request_id}"
def parse_draft_scheduler_rid(rid: str) -> DraftReqKey:
if rid.startswith("draft:"):
encoded = rid[len("draft:") :]
rank_text, sep, request_id = encoded.partition(":")
if sep and request_id:
return DraftReqKey(
src_verifier_rank=int(rank_text),
request_id=request_id,
)
raise ValueError(f"Invalid decoupled draft scheduler rid: {rid}")
@dataclass
class DraftSync:
"""Open or re-open a drafter request from a verifier-owned prefix.
The verifier is the source of truth for committed tokens. DraftSync gives
the drafter the prompt and already committed output prefix that it must
align to before it can emit draft tail tokens.
"""
request_id: str
src_verifier_rank: int
dst_drafter_rank: int
prompt_token_ids: list[int] = field(default_factory=list)
committed_outputs: list[int] = field(default_factory=list)
@property
def draft_key(self) -> DraftReqKey:
return DraftReqKey(
src_verifier_rank=int(self.src_verifier_rank),
request_id=self.request_id,
)
@dataclass
class VerifyCommit:
"""
Sent from verifier to drafter to commit a portion of the draft outputs.
committed_tokens is the verifier-committed contiguous output segment:
output_ids[
pre_verify_committed_len:
pre_verify_committed_len + len(committed_tokens)
].
Drafter must align its reqs to these committed tokens,
and sometimes needs to truncate tokens / reprefill.
"""
request_id: str
src_verifier_rank: int
dst_drafter_rank: int
pre_verify_committed_len: int
committed_tokens: list[int]
@property
def draft_key(self) -> DraftReqKey:
return DraftReqKey(
src_verifier_rank=int(self.src_verifier_rank),
request_id=self.request_id,
)
def validate_committed_tokens(self) -> None:
if not self.committed_tokens:
raise ValueError(
"VerifyCommit committed_tokens must be non-empty: "
f"request_id={self.request_id} "
f"pre_verify_committed_len={self.pre_verify_committed_len}"
)
if int(self.pre_verify_committed_len) < 0:
raise ValueError(
"VerifyCommit pre_verify_committed_len must be non-negative: "
f"request_id={self.request_id} "
f"pre_verify_committed_len={self.pre_verify_committed_len}"
)
@dataclass
class DraftClose:
request_id: str
src_verifier_rank: int
dst_drafter_rank: int
reason: str
@property
def draft_key(self) -> DraftReqKey:
return DraftReqKey(
src_verifier_rank=int(self.src_verifier_rank),
request_id=self.request_id,
)
@dataclass
class DraftTailStreamOutput:
"""
Drafter sends one output token to the verifier-side DraftTailBuffer.
base_committed_len records the verifier prefix length that the drafter used
as the base when this token was emitted. The verifier compares it with its
stale-base boundary before accepting the token as tail data or as
pending-prefix confirmation.
new_token_pos is the 0-based output token position for new_token. Normal
decode streams send the latest generated token.
"""
src_drafter_rank: int
dst_verifier_rank: int
request_id: str
base_committed_len: int
new_token_pos: int
new_token: int
@dataclass
class DraftTailStreamOutputBatch:
outputs: list[DraftTailStreamOutput] = field(default_factory=list)
@dataclass
class DraftControlBatch:
dst_drafter_rank: int
sync_messages: list[DraftSync] = field(default_factory=list)
verify_commit_messages: list[VerifyCommit] = field(default_factory=list)
close_messages: list[DraftClose] = field(default_factory=list)
@dataclass
class VerifierCommitSegment:
"""Contiguous VerifyCommit messages coalesced for one drafter request.
When receiving contiguous VerifyCommit messages for the same draft req,
the transport thread(TokenSync thread at drafter side) coalesces them into a single VerifierCommitSegment.
VerifierCommitSegment represents a contiguous verifier-committed token segment for drafter,
and drafter scheduler should align with these segments before emitting tail tokens
"""
draft_key: DraftReqKey
dst_drafter_rank: int
pre_verify_committed_len: int
committed_tokens: list[int] = field(default_factory=list)
@property
def end_committed_len(self) -> int:
return int(self.pre_verify_committed_len) + len(self.committed_tokens)
def append_message(self, message: VerifyCommit) -> None:
"""
It runs on TokenSyncThread under _pending_lock. That loop only
catches zmq.error.ContextTerminated, so a raise here escapes _run and
silently kills the drafter control thread. It then stops applying
ALL requests' controls while the verifier keeps pushing.
TODO: 1. peer-data violations (non-contiguous / invalid len)
should quarantine just that request (drop + add to close_keys), not
crash the thread. 2. phase 5.c will handle the drafter failure by
degrading the verifier into normal autoregressive decoding.
"""
if message.draft_key != self.draft_key:
raise RuntimeError(
"Verifier commit segment received a commit for a different "
f"request: segment_key={self.draft_key} message_key={message.draft_key}"
)
if int(message.dst_drafter_rank) != int(self.dst_drafter_rank):
raise RuntimeError(
"Verifier commit segment received a commit for a different "
"drafter rank: "
f"request_id={message.request_id} "
f"segment_drafter_rank={self.dst_drafter_rank} "
f"message_drafter_rank={message.dst_drafter_rank}"
)
message.validate_committed_tokens()
pre_verify_committed_len = int(message.pre_verify_committed_len)
if pre_verify_committed_len != self.end_committed_len:
raise RuntimeError(
"Verifier commit segment requires contiguous VerifyCommit "
"messages: "
f"request_id={message.request_id} "
f"expected_pre_verify_committed_len={self.end_committed_len} "
f"actual_pre_verify_committed_len={pre_verify_committed_len}"
)
token_ids = [int(token_id) for token_id in message.committed_tokens]
self.committed_tokens.extend(token_ids)
def extract_prefix(self, num_tokens: int) -> VerifierCommitSegment:
num_tokens = int(num_tokens)
if num_tokens <= 0:
raise ValueError(
"Verifier commit segment prefix length must be positive: "
f"request_id={self.draft_key.request_id} num_tokens={num_tokens}"
)
if num_tokens > len(self.committed_tokens):
raise ValueError(
"Verifier commit segment prefix length exceeds segment length: "
f"request_id={self.draft_key.request_id} "
f"num_tokens={num_tokens} "
f"segment_len={len(self.committed_tokens)}"
)
prefix_tokens = [
int(token_id) for token_id in self.committed_tokens[:num_tokens]
]
remaining_tokens = [
int(token_id) for token_id in self.committed_tokens[num_tokens:]
]
prefix_segment = VerifierCommitSegment(
draft_key=self.draft_key,
dst_drafter_rank=int(self.dst_drafter_rank),
pre_verify_committed_len=int(self.pre_verify_committed_len),
committed_tokens=prefix_tokens,
)
self.pre_verify_committed_len = int(self.pre_verify_committed_len) + num_tokens
self.committed_tokens = remaining_tokens
return prefix_segment
@dataclass
class DraftControlInbox:
"""Drafter-side inbox for verifier control messages.
The TokenSync thread temporarily stores incoming control messages here.
The drafter scheduler extracts and consumes them each time it finishes a decoding step.
"""
sync_messages: list[DraftSync] = field(default_factory=list)
verifier_commit_segments: dict[DraftReqKey, VerifierCommitSegment] = field(
default_factory=dict
)
close_keys: set[DraftReqKey] = field(default_factory=set)
def is_empty(self) -> bool:
return (
not self.sync_messages
and not self.verifier_commit_segments
and not self.close_keys
)
def pending_control_count(self) -> int:
return (
len(self.sync_messages)
+ len(self.verifier_commit_segments)
+ len(self.close_keys)
)
def add_control_batch_locked(self, batch: DraftControlBatch) -> None:
for message in batch.close_messages:
self.add_close_key_locked(message.draft_key)
for message in batch.sync_messages:
if message.draft_key not in self.close_keys:
self.sync_messages.append(message)
for message in batch.verify_commit_messages:
self.add_verify_commit_locked(message)
def add_close_key_locked(self, draft_key: DraftReqKey) -> None:
self.close_keys.add(draft_key)
self.verifier_commit_segments.pop(draft_key, None)
self.sync_messages = [
message for message in self.sync_messages if message.draft_key != draft_key
]
def add_verify_commit_locked(self, message: VerifyCommit) -> None:
if message.draft_key in self.close_keys:
return
segment = self.verifier_commit_segments.get(message.draft_key)
if segment is None:
segment = VerifierCommitSegment(
draft_key=message.draft_key,
dst_drafter_rank=int(message.dst_drafter_rank),
pre_verify_committed_len=int(message.pre_verify_committed_len),
)
segment.append_message(message)
self.verifier_commit_segments[message.draft_key] = segment
return
segment.append_message(message)
def extract_ready_controls_locked(
self,
consumable_commit_len: Callable[[VerifierCommitSegment], int],
) -> ReadyDraftControls:
ready_controls = ReadyDraftControls()
if self.close_keys:
ready_controls.close_keys = self.close_keys
self.close_keys = set()
if self.sync_messages:
ready_controls.sync_messages = self.sync_messages
self.sync_messages = []
for draft_key, segment in list(self.verifier_commit_segments.items()):
consumable_len = consumable_commit_len(segment)
if consumable_len <= 0:
continue
ready_controls.ready_commit_segments.append(
segment.extract_prefix(consumable_len)
)
if not segment.committed_tokens:
self.verifier_commit_segments.pop(draft_key, None)
return ready_controls
@dataclass
class ReadyDraftControls:
sync_messages: list[DraftSync] = field(default_factory=list)
close_keys: set[DraftReqKey] = field(default_factory=set)
ready_commit_segments: list[VerifierCommitSegment] = field(default_factory=list)
def is_empty(self) -> bool:
return (
not self.sync_messages
and not self.close_keys
and not self.ready_commit_segments
)
def extracted_control_count(self) -> int:
return (
len(self.sync_messages)
+ len(self.close_keys)
+ len(self.ready_commit_segments)
)
@dataclass
class DraftMeshMessage:
message_type: DraftMeshMessageType
control_batch: Optional[DraftControlBatch] = None
tail_stream_output_batch: Optional[DraftTailStreamOutputBatch] = None
@staticmethod
def from_control_batch(message: DraftControlBatch) -> DraftMeshMessage:
return DraftMeshMessage(
message_type=DraftMeshMessageType.CONTROL_BATCH,
control_batch=message,
)
@staticmethod
def from_tail_stream_output_batch(
message: DraftTailStreamOutputBatch,
) -> DraftMeshMessage:
return DraftMeshMessage(
message_type=DraftMeshMessageType.TAIL_STREAM_OUTPUT_BATCH,
tail_stream_output_batch=message,
)
@dataclass(frozen=True)
class DecoupledSpecIpcConfig:
bind_endpoint: str
connect_endpoints: tuple[str, ...]
rank: int
@@ -0,0 +1,161 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Tuple
import torch
from sglang.srt.layers.attention.utils import create_flashinfer_kv_indices_triton
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.speculative.spec_info import SpecInput, SpecInputType
if TYPE_CHECKING:
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.speculative.ragged_verify import RaggedVerifyLayout
@dataclass
class DFlashVerifyInput(SpecInput):
"""Inputs for a target-model verify forward in DFlash.
The verify forward is run with `ForwardMode.TARGET_VERIFY` so that the target
model returns logits for all tokens in the block, enabling accept-length
computation.
"""
draft_token: torch.Tensor
positions: torch.Tensor
draft_token_num: int
# Kept for compatibility with attention backends that gate tree metadata by `topk > 1`.
# DFLASH verify is linear (non-tree), so this is always 1.
topk: int = 1
# Custom attention "allow mask" for TARGET_VERIFY in backends that require it.
# Semantics follow SGLang speculative conventions: True means the (q, k) pair is allowed.
custom_mask: torch.Tensor | None = None
capture_hidden_mode: CaptureHiddenMode = CaptureHiddenMode.FULL
# Shape info for padding (e.g., DP attention / CUDA graph).
num_tokens_per_req: int = -1
ragged_verify_layout: Optional[RaggedVerifyLayout] = None
def __post_init__(self):
super().__init__(spec_input_type=SpecInputType.DFLASH_VERIFY)
if self.num_tokens_per_req == -1:
self.num_tokens_per_req = int(self.draft_token_num)
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
return self.draft_token_num, self.draft_token_num
def prepare_for_verify(
self,
batch: ScheduleBatch,
target_worker: TpModelWorker,
) -> tuple[ForwardBatch, bool]:
"""Prepare a DFLASH verify forward batch for overlap scheduling.
The caller computes and stores `batch.out_cache_loc` before this
method is called. This helper only packages the verify forward and pre-initializes either CUDA-graph replay
metadata or eager attention metadata so the actual forward can run with
`skip_attn_backend_init=True`.
"""
batch.input_ids = self.draft_token
batch.spec_info = self
batch.forward_mode = (
ForwardMode.IDLE
if batch.forward_mode.is_idle()
else ForwardMode.TARGET_VERIFY
)
batch.capture_hidden_mode = self.capture_hidden_mode
verify_forward_batch = ForwardBatch.init_new(batch, target_worker.model_runner)
can_run_cuda_graph = bool(
target_worker.model_runner.decode_cuda_graph_runner
and target_worker.model_runner.decode_cuda_graph_runner.can_run_graph(
verify_forward_batch
)
)
if can_run_cuda_graph:
target_worker.model_runner.decode_cuda_graph_runner.load_batch(
verify_forward_batch
)
elif not batch.forward_mode.is_idle():
target_worker.model_runner.attn_backend.init_forward_metadata(
verify_forward_batch
)
return verify_forward_batch, can_run_cuda_graph
def generate_attn_arg_prefill(
self,
req_pool_indices: torch.Tensor,
paged_kernel_lens: torch.Tensor,
paged_kernel_lens_sum: int,
req_to_token: torch.Tensor,
kv_start_idx: Optional[torch.Tensor] = None,
):
device = req_pool_indices.device
bs = len(req_pool_indices)
layout = self.ragged_verify_layout
if layout is None:
qo_indptr = torch.arange(
0,
(bs + 1) * self.draft_token_num,
step=self.draft_token_num,
dtype=torch.int32,
device=device,
)
verify_lens = self.draft_token_num
kv_indices_extra = self.draft_token_num * bs
else:
qo_indptr = layout.qo_indptr_device
verify_lens = layout.verify_lens
kv_indices_extra = layout.total_verify_tokens
cum_kv_seq_len = torch.zeros((bs + 1,), dtype=torch.int32, device=device)
paged_kernel_lens = paged_kernel_lens + verify_lens
cum_kv_seq_len[1:] = torch.cumsum(paged_kernel_lens, dim=0)
kv_indices = torch.empty(
paged_kernel_lens_sum + kv_indices_extra,
dtype=torch.int32,
device=device,
)
create_flashinfer_kv_indices_triton[(bs,)](
req_to_token,
req_pool_indices,
paged_kernel_lens,
cum_kv_seq_len,
kv_start_idx,
kv_indices,
req_to_token.size(1),
)
mask = self.custom_mask
if mask is not None:
mask_numel = (
paged_kernel_lens_sum * self.draft_token_num
+ (self.draft_token_num**2) * bs
)
if mask.numel() < mask_numel:
# FIXME(attn): temporary fix for custom mask padding with cuda graph
mask = torch.cat(
[
mask,
torch.full(
(mask_numel - mask.numel(),),
True,
dtype=torch.bool,
device=device,
),
],
dim=0,
)
self.custom_mask = mask
return kv_indices, cum_kv_seq_len, qo_indptr, mask
@@ -0,0 +1,288 @@
"""DFLASH spec-v2 overlap scheduling data structures."""
import contextlib
from dataclasses import dataclass
from typing import Optional, Tuple
import torch
from sglang.srt.environ import envs
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.mem_cache.common import (
alloc_paged_token_slots_extend,
alloc_token_slots,
get_last_loc,
)
from sglang.srt.runtime_context import get_server_args
from sglang.srt.speculative.spec_info import SpecInput, SpecInputType
from sglang.srt.speculative.spec_utils import assign_req_to_token_pool_func
from sglang.srt.utils.common import is_pin_memory_available
_OVERLAP_PLAN_STREAMS: dict[str, torch.cuda.Stream] = {}
def _get_overlap_plan_stream(
device: torch.device | str,
) -> tuple[Optional[torch.cuda.Stream], contextlib.AbstractContextManager]:
"""Return an optional plan stream/context for overlap scheduling prep kernels."""
if not envs.SGLANG_ENABLE_OVERLAP_PLAN_STREAM.get():
return None, contextlib.nullcontext()
device_str = str(device)
stream = _OVERLAP_PLAN_STREAMS.get(device_str)
if stream is None:
stream = torch.get_device_module(device_str).Stream()
_OVERLAP_PLAN_STREAMS[device_str] = stream
return stream, torch.get_device_module(device_str).stream(stream)
@dataclass
class DFlashDraftInputV2(SpecInput):
"""Draft-side state carried across overlap iterations (spec-v2)."""
# Legacy Eagle-shaped fields; DFLASH relays via FutureMap so these are unused.
topk_p: torch.Tensor
topk_index: torch.Tensor
bonus_tokens: torch.Tensor
new_seq_lens: torch.Tensor
hidden_states: torch.Tensor
max_top_k: int = 1
uniform_top_k_value: Optional[int] = None
reserved_seq_lens_cpu: Optional[torch.Tensor] = None
reserved_seq_lens_sum: Optional[int] = None
_prepare_batch_seq_lens_cpu_buf: Optional[torch.Tensor] = None
_prepare_cur_kv_lens_cpu_buf: Optional[torch.Tensor] = None
_prepare_nxt_kv_lens_cpu_buf: Optional[torch.Tensor] = None
_prepare_cur_kv_lens_gpu_buf: Optional[torch.Tensor] = None
_prepare_nxt_kv_lens_gpu_buf: Optional[torch.Tensor] = None
# Filled by scheduler after dispatch.
future_indices: Optional[torch.Tensor] = None
verify_token_budget: Optional[int] = None
def __post_init__(self):
super().__init__(spec_input_type=SpecInputType.DFLASH_DRAFT)
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
# Spec v2 draft state itself does not change token accounting.
return (1, 1)
def _ensure_prepare_length_buffers(
self, bs: int, device: torch.device | str
) -> None:
pin_memory = is_pin_memory_available(device)
def needs_cpu_alloc(buf: Optional[torch.Tensor]) -> bool:
return buf is None or buf.numel() < bs
def needs_gpu_alloc(buf: Optional[torch.Tensor]) -> bool:
return buf is None or buf.numel() < bs or str(buf.device) != str(device)
def grown_capacity(buf: Optional[torch.Tensor]) -> int:
current = 0 if buf is None else int(buf.numel())
return max(bs, 32, current * 2 if current > 0 else 0)
# The three CPU scratch buffers grow together; capacity is the only
# invariant (batch is int64 non-pinned, cur/nxt are int32 pinned).
if needs_cpu_alloc(self._prepare_batch_seq_lens_cpu_buf):
capacity = grown_capacity(self._prepare_batch_seq_lens_cpu_buf)
self._prepare_batch_seq_lens_cpu_buf = torch.empty(
(capacity,), dtype=torch.int64, device="cpu"
)
self._prepare_cur_kv_lens_cpu_buf = torch.empty(
(capacity,), dtype=torch.int32, device="cpu", pin_memory=pin_memory
)
self._prepare_nxt_kv_lens_cpu_buf = torch.empty(
(capacity,), dtype=torch.int32, device="cpu", pin_memory=pin_memory
)
if needs_gpu_alloc(self._prepare_cur_kv_lens_gpu_buf):
capacity = grown_capacity(self._prepare_cur_kv_lens_gpu_buf)
self._prepare_cur_kv_lens_gpu_buf = torch.empty(
(capacity,), dtype=torch.int32, device=device
)
self._prepare_nxt_kv_lens_gpu_buf = torch.empty(
(capacity,), dtype=torch.int32, device=device
)
@classmethod
def create_idle_input(cls, device: torch.device) -> "DFlashDraftInputV2":
return cls(
topk_p=torch.empty((0, 0), device=device, dtype=torch.float32),
topk_index=torch.empty((0, 0), device=device, dtype=torch.int64),
bonus_tokens=torch.empty((0,), device=device, dtype=torch.int64),
new_seq_lens=torch.empty((0,), device=device, dtype=torch.int64),
hidden_states=torch.empty((0, 0), device=device, dtype=torch.float16),
)
def prepare_for_decode(self, batch: ScheduleBatch):
"""Allocate headroom in the shared req_to_token pool for the next DFLASH step.
DFLASH spec-v2 uses overlap scheduling's "over-allocation" approach: we reserve
future KV slots ahead of time so the worker can gather `out_cache_loc` directly
from `req_to_token` without allocator backup/restore. CPU metadata intentionally
lags by one iteration; keep it separate from the reserved upper bound that backs
the overallocated mapping.
"""
plan_stream, plan_stream_ctx = _get_overlap_plan_stream(batch.device)
bs = batch.batch_size()
if bs == 0:
return
self._ensure_prepare_length_buffers(bs, batch.device)
assert self._prepare_batch_seq_lens_cpu_buf is not None
assert self._prepare_cur_kv_lens_cpu_buf is not None
assert self._prepare_nxt_kv_lens_cpu_buf is not None
assert self._prepare_cur_kv_lens_gpu_buf is not None
assert self._prepare_nxt_kv_lens_gpu_buf is not None
batch_seq_lens_cpu_t = self._prepare_batch_seq_lens_cpu_buf[:bs]
cur_kv_lens_cpu_t = self._prepare_cur_kv_lens_cpu_buf[:bs]
# For DFLASH, each decode step needs a fixed-size verify block.
block_size = int(get_server_args().speculative_num_draft_tokens)
if block_size <= 0:
raise ValueError(
f"DFLASH invalid speculative_num_draft_tokens={block_size}."
)
page_size = batch.token_to_kv_pool_allocator.page_size
nxt_kv_lens_cpu_t = self._prepare_nxt_kv_lens_cpu_buf[:bs]
committed_seq_lens_sum = 0
reserved_seq_lens_sum = 0
num_needed_tokens = 0
max_top_k = 1
uniform_top_k_value = None
uniform_top_k = True
for i, req in enumerate(batch.reqs):
committed_len = int(req.kv_committed_len)
# Read the allocation watermark from the req object like EAGLE.
cur_alloc_len = int(req.kv_allocated_len)
reserved_len = max(cur_alloc_len, committed_len + 2 * block_size)
top_k = int(req.sampling_params.top_k)
batch_seq_lens_cpu_t[i] = committed_len
cur_kv_lens_cpu_t[i] = cur_alloc_len
nxt_kv_lens_cpu_t[i] = reserved_len
committed_seq_lens_sum += committed_len
reserved_seq_lens_sum += reserved_len
num_needed_tokens += reserved_len - cur_alloc_len
if top_k > max_top_k:
max_top_k = top_k
if i == 0:
uniform_top_k_value = top_k
elif uniform_top_k and top_k != uniform_top_k_value:
uniform_top_k = False
self.max_top_k = max(max_top_k, 1)
self.uniform_top_k_value = uniform_top_k_value if uniform_top_k else None
caller_stream = None
if plan_stream is not None:
caller_stream = torch.get_device_module(batch.device).current_stream()
with plan_stream_ctx:
if plan_stream is not None and caller_stream is not None:
# `batch.seq_lens`, `batch.req_pool_indices`, and related tensors may
# have just been rebuilt on the scheduler stream by filter/merge ops.
# The plan stream must wait for those writes before reading them.
plan_stream.wait_stream(caller_stream)
cur_kv_lens = self._prepare_cur_kv_lens_gpu_buf[:bs]
nxt_kv_lens = self._prepare_nxt_kv_lens_gpu_buf[:bs]
cur_kv_lens.copy_(cur_kv_lens_cpu_t, non_blocking=True)
nxt_kv_lens.copy_(nxt_kv_lens_cpu_t, non_blocking=True)
if num_needed_tokens > 0:
if page_size == 1:
out_cache_loc = alloc_token_slots(
batch.tree_cache, num_needed_tokens
)
else:
last_loc = get_last_loc(
batch.req_to_token_pool.req_to_token,
batch.req_pool_indices,
cur_kv_lens,
)
out_cache_loc = alloc_paged_token_slots_extend(
batch.tree_cache,
cur_kv_lens,
cur_kv_lens_cpu_t,
nxt_kv_lens,
nxt_kv_lens_cpu_t,
last_loc,
num_needed_tokens,
)
# Updating req_to_token is a write to a shared tensor: it must not overlap
# with the previous batch's forward, which also reads req_to_token.
assign_req_to_token_pool_func(
batch.req_pool_indices,
batch.req_to_token_pool.req_to_token,
cur_kv_lens,
nxt_kv_lens,
out_cache_loc,
bs,
)
if caller_stream is not None:
# Enqueue the dependency on the caller's stream, not inside the
# plan-stream context, so forward work cannot observe partially
# prepared req_to_token / KV allocation state.
caller_stream.wait_stream(plan_stream)
# This request-side high-water mark is what release_kv_cache() uses to
# reclaim any DFLASH over-allocation if the request finishes later.
for i, req in enumerate(batch.reqs):
req.kv_allocated_len = max(req.kv_allocated_len, int(nxt_kv_lens_cpu_t[i]))
# Seed committed; overlap's resolve overwrites it with the published value.
batch.seq_lens_cpu = batch_seq_lens_cpu_t
batch.seq_lens_sum = committed_seq_lens_sum
self.reserved_seq_lens_cpu = nxt_kv_lens_cpu_t
self.reserved_seq_lens_sum = reserved_seq_lens_sum
def filter_batch(self, new_indices: torch.Tensor, has_been_filtered: bool = True):
if self.reserved_seq_lens_cpu is not None:
self.reserved_seq_lens_cpu = self.reserved_seq_lens_cpu[new_indices.cpu()]
self.reserved_seq_lens_sum = int(self.reserved_seq_lens_cpu.sum().item())
if self.future_indices is not None:
self.future_indices = self.future_indices[new_indices]
return
self.topk_p = self.topk_p[new_indices]
self.topk_index = self.topk_index[new_indices]
self.bonus_tokens = self.bonus_tokens[new_indices]
self.new_seq_lens = self.new_seq_lens[new_indices]
self.hidden_states = self.hidden_states[new_indices]
def merge_batch(self, spec_info: "DFlashDraftInputV2"):
if self.reserved_seq_lens_cpu is not None:
assert spec_info.reserved_seq_lens_cpu is not None
self.reserved_seq_lens_cpu = torch.cat(
[self.reserved_seq_lens_cpu, spec_info.reserved_seq_lens_cpu]
)
self.reserved_seq_lens_sum = int(self.reserved_seq_lens_cpu.sum().item())
elif spec_info.reserved_seq_lens_cpu is not None:
self.reserved_seq_lens_cpu = spec_info.reserved_seq_lens_cpu
self.reserved_seq_lens_sum = spec_info.reserved_seq_lens_sum
if self.future_indices is not None:
assert spec_info.future_indices is not None
self.future_indices = torch.cat(
[self.future_indices, spec_info.future_indices]
)
return
self.topk_p = torch.cat([self.topk_p, spec_info.topk_p], dim=0)
self.topk_index = torch.cat([self.topk_index, spec_info.topk_index], dim=0)
self.bonus_tokens = torch.cat(
[self.bonus_tokens, spec_info.bonus_tokens], dim=0
)
self.new_seq_lens = torch.cat(
[self.new_seq_lens, spec_info.new_seq_lens], dim=0
)
self.hidden_states = torch.cat(
[self.hidden_states, spec_info.hidden_states], dim=0
)
@@ -0,0 +1,814 @@
from __future__ import annotations
import logging
from collections.abc import Sequence
from dataclasses import dataclass
from numbers import Integral
from typing import Any, List, Optional, Tuple
import torch
import torch.nn.functional as F
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
from sglang.srt.layers.sampler import apply_custom_logit_processor
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.utils import is_cuda, is_musa
DEFAULT_DFLASH_MASK_TOKEN = "<|MASK|>"
logger = logging.getLogger(__name__)
_DFLASH_SAMPLING_VERIFY_AVAILABLE = False
_DFLASH_CHAIN_VERIFY_BUFFERS: dict[tuple[Optional[int], int], dict[str, Any]] = {}
_DFLASH_VERIFY_SKIP_CUSTOM_MASK_BACKENDS = frozenset(
{
"FlashInferAttnBackend",
"FlashInferMLAAttnBackend",
"FlashAttentionBackend",
"TritonAttnBackend",
"TRTLLMHAAttnBackend",
"TRTLLMMLABackend",
}
)
if is_cuda() or is_musa():
try:
from sgl_kernel import (
top_k_renorm_prob,
top_p_renorm_prob,
tree_speculative_sampling_target_only,
)
_DFLASH_SAMPLING_VERIFY_AVAILABLE = True
except Exception:
top_k_renorm_prob = None
top_p_renorm_prob = None
tree_speculative_sampling_target_only = None
else:
top_k_renorm_prob = None
top_p_renorm_prob = None
tree_speculative_sampling_target_only = None
def is_dflash_sampling_verify_available() -> bool:
return _DFLASH_SAMPLING_VERIFY_AVAILABLE
def scale_kv_cell_size_per_token_for_dflash(
*,
target_cell_size_per_token: int,
target_num_layers: int,
draft_num_layers: int,
draft_cell_size_per_token: Optional[int] = None,
) -> int:
"""Compute bytes/token budget for combined target+draft KV pools (DFLASH).
DFLASH runs a separate draft runner with its own KV pool. The target runner's
token capacity must fit both pools in aggregate.
Returns:
Approximate per-token bytes for (target KV + draft KV), expressed as a
scaled version of `target_cell_size_per_token`, unless an explicit
`draft_cell_size_per_token` is provided (in which case we sum them).
"""
if target_cell_size_per_token <= 0:
raise ValueError(
"target_cell_size_per_token must be positive, "
f"got {target_cell_size_per_token}."
)
if draft_cell_size_per_token is not None:
draft_cell_size_per_token = int(draft_cell_size_per_token)
if draft_cell_size_per_token <= 0:
raise ValueError(
"draft_cell_size_per_token must be positive when provided, "
f"got {draft_cell_size_per_token}."
)
return int(target_cell_size_per_token) + int(draft_cell_size_per_token)
if target_num_layers <= 0 or draft_num_layers <= 0:
return int(target_cell_size_per_token)
total_layers = int(target_num_layers) + int(draft_num_layers)
return (
int(target_cell_size_per_token) * int(total_layers) + int(target_num_layers) - 1
) // int(target_num_layers)
def resolve_dflash_verify_mask_policy(attn_backend: Any) -> tuple[str, bool]:
backend = attn_backend
for _ in range(4):
full_backend = getattr(backend, "full_attn_backend", None)
if full_backend is None:
break
backend = full_backend
backend_name = type(backend).__name__
return backend_name, (backend_name not in _DFLASH_VERIFY_SKIP_CUSTOM_MASK_BACKENDS)
def apply_dflash_verify_logits_adjustments(
*,
next_token_logits: torch.Tensor,
sampling_info: Any,
draft_token_num: int,
) -> None:
"""Apply sampling-time logit adjustments for DFlash verify in place.
This keeps v1 and v2 verify semantics aligned while letting overlap scheduling
use the cheaper precomputed `acc_linear_penalties` path instead of allocating a
repeated `[bs * draft_token_num, vocab]` penalty tensor every step.
"""
if sampling_info is None:
return
if next_token_logits.ndim != 2:
raise ValueError(
"next_token_logits must be 2D, "
f"got shape={tuple(next_token_logits.shape)}."
)
if draft_token_num <= 0:
raise ValueError(f"draft_token_num must be positive, got {draft_token_num}.")
bs = len(sampling_info)
if next_token_logits.shape[0] != bs * draft_token_num:
raise ValueError(
"next_token_logits row count mismatch for DFlash verify adjustments. "
f"Expected {bs * draft_token_num}, got {next_token_logits.shape[0]}."
)
if sampling_info.has_custom_logit_processor:
apply_custom_logit_processor(
next_token_logits,
sampling_info,
num_tokens_in_batch=draft_token_num,
)
acc_linear_penalties = getattr(sampling_info, "acc_linear_penalties", None)
penalizer = getattr(sampling_info, "penalizer_orchestrator", None)
vocab_mask = getattr(sampling_info, "vocab_mask", None)
logit_bias = getattr(sampling_info, "logit_bias", None)
logits_3d: Optional[torch.Tensor] = None
def get_logits_3d() -> torch.Tensor:
nonlocal logits_3d
if logits_3d is None:
logits_3d = next_token_logits.reshape(bs, draft_token_num, -1)
return logits_3d
# Dense fallback only when we need live penalizer application or a vocab mask.
# In overlap scheduling the common path is `acc_linear_penalties`, which can be
# broadcast over the verify block without materializing a repeated buffer.
if (
penalizer is not None and penalizer.is_required and acc_linear_penalties is None
) or vocab_mask is not None:
linear_penalty = torch.zeros(
(bs, next_token_logits.shape[1]),
dtype=torch.float32,
device=next_token_logits.device,
)
sampling_info.apply_logits_bias(linear_penalty)
get_logits_3d().add_(
linear_penalty[:, None, :].to(dtype=next_token_logits.dtype)
)
return
if acc_linear_penalties is not None:
if (
acc_linear_penalties.device != next_token_logits.device
or acc_linear_penalties.dtype != next_token_logits.dtype
):
acc_linear_penalties = acc_linear_penalties.to(
device=next_token_logits.device,
dtype=next_token_logits.dtype,
)
get_logits_3d().add_(acc_linear_penalties[:, None, :])
if logit_bias is not None:
if (
logit_bias.device != next_token_logits.device
or logit_bias.dtype != next_token_logits.dtype
):
logit_bias = logit_bias.to(
device=next_token_logits.device,
dtype=next_token_logits.dtype,
)
get_logits_3d().add_(logit_bias[:, None, :])
def _get_or_create_chain_verify_buffers(
*,
bs: int,
draft_token_num: int,
device: torch.device,
) -> tuple[
torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
]:
key = (device.index, int(draft_token_num))
cached = _DFLASH_CHAIN_VERIFY_BUFFERS.get(key)
cap_bs = 0 if cached is None else int(cached["cap_bs"])
if cap_bs < bs:
new_cap = max(int(bs), cap_bs * 2 if cap_bs > 0 else int(bs))
retrieve_index = torch.arange(
new_cap * draft_token_num, dtype=torch.int64, device=device
).view(new_cap, draft_token_num)
row_next = torch.arange(
1, draft_token_num + 1, dtype=torch.int64, device=device
)
row_next[-1] = -1
retrieve_next_token = row_next.unsqueeze(0).expand(new_cap, -1).clone()
retrieve_next_sibling = torch.full(
(new_cap, draft_token_num), -1, dtype=torch.int64, device=device
)
predicts = torch.empty(
(new_cap * draft_token_num,), dtype=torch.int32, device=device
)
accept_index = torch.empty(
(new_cap, draft_token_num), dtype=torch.int32, device=device
)
accept_token_num = torch.empty((new_cap,), dtype=torch.int32, device=device)
cached = {
"cap_bs": int(new_cap),
"retrieve_index": retrieve_index,
"retrieve_next_token": retrieve_next_token,
"retrieve_next_sibling": retrieve_next_sibling,
"predicts": predicts,
"accept_index": accept_index,
"accept_token_num": accept_token_num,
}
_DFLASH_CHAIN_VERIFY_BUFFERS[key] = cached
assert cached is not None
retrieve_index = cached["retrieve_index"][:bs]
retrieve_next_token = cached["retrieve_next_token"][:bs]
retrieve_next_sibling = cached["retrieve_next_sibling"][:bs]
predicts = cached["predicts"][: bs * draft_token_num]
accept_index = cached["accept_index"][:bs]
accept_token_num = cached["accept_token_num"][:bs]
return (
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
predicts,
accept_index,
accept_token_num,
)
def build_target_layer_ids(num_target_layers: int, num_draft_layers: int) -> List[int]:
"""Select target layer indices used to build DFlash context features.
Args:
num_target_layers: Number of transformer layers in the runtime target model.
num_draft_layers: Number of layers in the DFlash draft model.
Returns:
A list of 0-based target layer indices of length `num_draft_layers`.
Notes:
- DFlash uses hidden states after each selected target layer (HF-style).
- SGLang captures "before layer i", so the model hook will typically add +1
when mapping to capture points.
"""
if num_target_layers <= 0:
raise ValueError(
f"num_target_layers must be positive, got {num_target_layers}."
)
if num_draft_layers <= 0:
raise ValueError(f"num_draft_layers must be positive, got {num_draft_layers}.")
if num_draft_layers == 1:
return [num_target_layers // 2]
start = 1
end = num_target_layers - 3
if end < start:
raise ValueError(
"DFlash layer selection requires num_target_layers >= 4. "
f"Got num_target_layers={num_target_layers}."
)
span = end - start
return [
int(round(start + (i * span) / (num_draft_layers - 1)))
for i in range(num_draft_layers)
]
def get_dflash_layer_types(config: Any) -> Optional[Sequence[str]]:
text_config = _get_text_config(config)
layer_types = _cfg_get(text_config, "layer_types", _cfg_get(config, "layer_types"))
if layer_types is None:
return None
if isinstance(layer_types, str) or not isinstance(layer_types, Sequence):
raise ValueError(
"DFLASH config.layer_types must be a sequence of attention type strings."
)
return layer_types
def get_dflash_attention_sliding_window_size(config: Any) -> Optional[int]:
layer_types = get_dflash_layer_types(config)
if layer_types is None or "sliding_attention" not in layer_types:
return None
text_config = _get_text_config(config)
sliding_window = _cfg_get(
text_config, "sliding_window", _cfg_get(config, "sliding_window")
)
if sliding_window is None:
raise ValueError(
"DFLASH sliding_attention layers require config.sliding_window."
)
# HF sliding windows include the current token; SGLang stores window_left.
return int(sliding_window) - 1
def _cfg_get(config: Any, key: str, default: Any = None) -> Any:
if isinstance(config, dict):
return config.get(key, default)
return getattr(config, key, default)
def _get_text_config(config: Any) -> Any:
if config is None:
return None
if isinstance(config, dict):
return config.get("text_config", config)
text_config = getattr(config, "text_config", None)
if text_config is not None:
return text_config
get_text_config = getattr(config, "get_text_config", None)
if callable(get_text_config):
try:
resolved = get_text_config()
if resolved is not None:
return resolved
except TypeError:
pass
return config
def _get_dflash_config(config: Any) -> dict:
if isinstance(config, dict):
cfg = config.get("dflash_config", None)
else:
cfg = getattr(config, "dflash_config", None)
if cfg is None:
return {}
if isinstance(cfg, dict):
return cfg
try:
return dict(cfg)
except Exception:
return {}
def _parse_optional_int(
value: Any,
*,
field_name: str,
min_value: Optional[int] = None,
) -> Optional[int]:
if value is None:
return None
try:
parsed = int(value)
except Exception as e:
raise ValueError(f"Invalid {field_name}={value!r}.") from e
if min_value is not None and parsed < int(min_value):
comparator = "positive" if int(min_value) == 1 else f">= {int(min_value)}"
raise ValueError(f"{field_name} must be {comparator}, got {parsed}.")
return parsed
@dataclass(frozen=True)
class DFlashDraftConfig:
num_hidden_layers: Optional[int]
num_target_layers: Optional[int]
block_size: Optional[int]
target_layer_ids: Optional[List[int]]
mask_token: str
mask_token_id: Optional[int]
def require_num_layers(self) -> int:
if self.num_hidden_layers is None:
raise ValueError(
"DFLASH requires draft num_hidden_layers in config. "
"Got config without num_hidden_layers."
)
return int(self.num_hidden_layers)
def resolve_block_size(self, *, default: Optional[int] = None) -> Optional[int]:
return self.block_size if self.block_size is not None else default
def resolve_target_layer_ids(
self,
*,
target_num_layers: int,
draft_num_layers: Optional[int] = None,
) -> List[int]:
target_num_layers = int(target_num_layers)
if target_num_layers <= 0:
raise ValueError(
f"target_num_layers must be positive, got {target_num_layers}."
)
if self.target_layer_ids is None:
if draft_num_layers is None:
draft_num_layers = self.require_num_layers()
return build_target_layer_ids(target_num_layers, int(draft_num_layers))
resolved = list(self.target_layer_ids)
if len(resolved) <= 0:
raise ValueError(
"DFLASH dflash_config.target_layer_ids must be non-empty. "
f"Got len(target_layer_ids)={len(resolved)}."
)
for idx, val in enumerate(resolved):
if val < 0 or val >= target_num_layers:
raise ValueError(
"DFLASH target_layer_ids contains an out-of-range layer id. "
f"target_layer_ids[{idx}]={val}, target_num_layers={target_num_layers}."
)
return resolved
def parse_dflash_draft_config(*, draft_hf_config: Any) -> DFlashDraftConfig:
"""Parse and validate DFLASH draft config fields from HF config/dict."""
dflash_cfg = _get_dflash_config(draft_hf_config)
draft_text_config = _get_text_config(draft_hf_config)
num_hidden_layers = _parse_optional_int(
_cfg_get(draft_text_config, "num_hidden_layers", None),
field_name="DFLASH draft num_hidden_layers",
min_value=1,
)
raw_num_target_layers = dflash_cfg.get(
"num_target_layers",
_cfg_get(draft_hf_config, "num_target_layers", None),
)
num_target_layers = _parse_optional_int(
raw_num_target_layers,
field_name="DFLASH draft num_target_layers",
min_value=1,
)
# Keep support for current checkpoints where block_size is top-level.
raw_block_size = dflash_cfg.get(
"block_size",
_cfg_get(draft_hf_config, "block_size", None),
)
block_size = _parse_optional_int(
raw_block_size,
field_name="DFLASH block_size",
min_value=1,
)
layer_ids = dflash_cfg.get(
"target_layer_ids",
_cfg_get(draft_hf_config, "target_layer_ids", None),
)
parsed_target_layer_ids: Optional[List[int]]
if layer_ids is None:
parsed_target_layer_ids = None
else:
if not isinstance(layer_ids, (list, tuple)):
raise ValueError(
"DFLASH dflash_config.target_layer_ids must be a list of ints, "
f"got type={type(layer_ids).__name__}."
)
parsed_target_layer_ids = [int(x) for x in layer_ids]
if len(parsed_target_layer_ids) <= 0:
raise ValueError(
"DFLASH dflash_config.target_layer_ids must be non-empty. "
f"Got len(target_layer_ids)={len(parsed_target_layer_ids)}."
)
mask_token = dflash_cfg.get("mask_token", None)
if mask_token is None:
mask_token = DEFAULT_DFLASH_MASK_TOKEN
if not isinstance(mask_token, str) or not mask_token:
raise ValueError(
"DFLASH dflash_config.mask_token must be a non-empty string, "
f"got {mask_token!r}."
)
mask_token_id = dflash_cfg.get("mask_token_id", None)
if mask_token_id is not None:
if not isinstance(mask_token_id, Integral) or isinstance(mask_token_id, bool):
raise ValueError(
"DFLASH dflash_config.mask_token_id must be an integer, "
f"got {mask_token_id!r} (type={type(mask_token_id).__name__})."
)
mask_token_id = int(mask_token_id)
if mask_token_id < 0:
raise ValueError(
"DFLASH dflash_config.mask_token_id must be non-negative, "
f"got {mask_token_id}."
)
return DFlashDraftConfig(
num_hidden_layers=num_hidden_layers,
num_target_layers=num_target_layers,
block_size=block_size,
target_layer_ids=parsed_target_layer_ids,
mask_token=mask_token,
mask_token_id=mask_token_id,
)
def can_dflash_slice_qkv_weight(qkv_proj: Any) -> Tuple[bool, str]:
"""Validate whether DFlash can slice KV weights from a fused QKV linear layer."""
quant_method = getattr(qkv_proj, "quant_method", None)
if not isinstance(quant_method, UnquantizedLinearMethod):
return (
False,
"quantized qkv_proj is not supported for this path "
f"(quant_method={type(quant_method).__name__})",
)
if not hasattr(qkv_proj, "weight"):
return False, "qkv weight tensor is missing"
return True, ""
def can_dflash_use_fused_qkv_proj(qkv_proj: Any) -> Tuple[bool, str]:
"""Validate whether a QKV layer is eligible for DFlash fused KV materialization."""
eligible, reason = can_dflash_slice_qkv_weight(qkv_proj)
if not eligible:
return False, reason
if getattr(qkv_proj, "bias", None) is not None:
return False, "qkv bias is not supported for fused KV path"
return True, ""
def compute_dflash_correct_drafts_and_bonus(
*,
candidates: torch.Tensor,
target_predict: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Compute DFlash accept lengths and bonus tokens (greedy verify rule).
Args:
candidates: Token ids proposed by the DFlash draft, including the current token.
Shape: [bs, block_size]. candidates[:, 0] is the current token.
target_predict: Token ids predicted by the target model for each position in the block.
Shape: [bs, block_size]. target_predict[:, t] corresponds to argmax at position t.
Returns:
correct_len: int32 tensor [bs], number of accepted *draft* tokens (excluding current token and bonus token).
bonus: int64 tensor [bs], the target-predicted token at index correct_len (the "bonus" token to append).
Notes:
Matches the reference implementation rule:
accept while candidates[:, 1:] == target_predict[:, :-1] consecutively.
"""
if candidates.ndim != 2:
raise ValueError(f"candidates must be 2D, got shape={tuple(candidates.shape)}")
if target_predict.shape != candidates.shape:
raise ValueError(
"target_predict must have the same shape as candidates. "
f"candidates.shape={tuple(candidates.shape)}, target_predict.shape={tuple(target_predict.shape)}"
)
bs, block_size = candidates.shape
if bs <= 0:
raise ValueError(f"batch size must be positive, got {bs}.")
if block_size <= 0:
raise ValueError(f"block_size must be positive, got {block_size}.")
matches = candidates[:, 1:] == target_predict[:, :-1]
correct_len = matches.to(torch.int32).cumprod(dim=1).sum(dim=1)
bonus = target_predict[torch.arange(bs, device=target_predict.device), correct_len]
return correct_len, bonus.to(torch.int64)
def compute_dflash_sampling_correct_drafts_and_bonus(
*,
candidates: torch.Tensor,
next_token_logits: torch.Tensor,
sampling_info: Any,
max_top_k: Optional[int] = None,
uniform_top_k_value: Optional[int] = None,
threshold_single: Optional[float] = None,
threshold_acc: Optional[float] = None,
uniform_samples: Optional[torch.Tensor] = None,
uniform_samples_for_final_sampling: Optional[torch.Tensor] = None,
use_sparse_topk: bool = True,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Compute DFlash accept lengths and bonus tokens for non-greedy sampling.
This is a chain-specialized variant of speculative target-only verification:
- DFlash proposals are linear (topk == 1), so each verify level has at most one candidate.
- When a candidate is rejected at a level, the final token is sampled from
`relu(q - p)` where `p` has only the rejected candidate mass.
"""
if not _DFLASH_SAMPLING_VERIFY_AVAILABLE:
raise RuntimeError(
"DFLASH non-greedy verification is unavailable on this build/device."
)
if candidates.ndim != 2:
raise ValueError(f"candidates must be 2D, got shape={tuple(candidates.shape)}")
if next_token_logits.ndim != 2:
raise ValueError(
"next_token_logits must be 2D, "
f"got shape={tuple(next_token_logits.shape)}."
)
bs, draft_token_num = candidates.shape
if bs <= 0:
raise ValueError(f"batch size must be positive, got {bs}.")
if draft_token_num <= 0:
raise ValueError(f"draft_token_num must be positive, got {draft_token_num}.")
if next_token_logits.shape[0] != bs * draft_token_num:
raise ValueError(
"next_token_logits row count mismatch. "
f"Expected {bs * draft_token_num}, got {next_token_logits.shape[0]}."
)
if candidates.device != next_token_logits.device:
raise ValueError(
"candidates and next_token_logits must be on the same device, "
f"got {candidates.device} and {next_token_logits.device}."
)
if threshold_single is None:
from sglang.srt.runtime_context import get_server_args
threshold_single = get_server_args().speculative_accept_threshold_single
if threshold_acc is None:
from sglang.srt.runtime_context import get_server_args
threshold_acc = get_server_args().speculative_accept_threshold_acc
threshold_single = float(threshold_single)
threshold_acc = max(float(threshold_acc), 1e-9)
device = next_token_logits.device
if uniform_samples is None:
uniform_samples = torch.rand(
(bs, draft_token_num), dtype=torch.float32, device=device
)
else:
if uniform_samples.shape != (bs, draft_token_num):
raise ValueError(
"uniform_samples shape mismatch. "
f"Expected {(bs, draft_token_num)}, got {tuple(uniform_samples.shape)}."
)
uniform_samples = uniform_samples.to(device=device, dtype=torch.float32)
if uniform_samples_for_final_sampling is None:
uniform_samples_for_final_sampling = torch.rand(
(bs,), dtype=torch.float32, device=device
)
else:
if uniform_samples_for_final_sampling.shape != (bs,):
raise ValueError(
"uniform_samples_for_final_sampling shape mismatch. "
f"Expected {(bs,)}, got {tuple(uniform_samples_for_final_sampling.shape)}."
)
uniform_samples_for_final_sampling = uniform_samples_for_final_sampling.to(
device=device,
dtype=torch.float32,
)
target_probs = build_dflash_verify_target_probs(
next_token_logits=next_token_logits,
sampling_info=sampling_info,
draft_token_num=draft_token_num,
bs=bs,
max_top_k=max_top_k,
uniform_top_k_value=uniform_top_k_value,
use_sparse_topk=use_sparse_topk,
)
draft_probs = torch.zeros_like(target_probs)
(
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
predicts,
accept_index,
accept_token_num,
) = _get_or_create_chain_verify_buffers(
bs=bs,
draft_token_num=draft_token_num,
device=device,
)
candidates_i64 = (
candidates if candidates.dtype == torch.int64 else candidates.to(torch.int64)
)
tree_speculative_sampling_target_only(
predicts=predicts,
accept_index=accept_index,
accept_token_num=accept_token_num,
candidates=candidates_i64,
retrive_index=retrieve_index,
retrive_next_token=retrieve_next_token,
retrive_next_sibling=retrieve_next_sibling,
uniform_samples=uniform_samples,
uniform_samples_for_final_sampling=uniform_samples_for_final_sampling,
target_probs=target_probs,
draft_probs=draft_probs,
threshold_single=threshold_single,
threshold_acc=threshold_acc,
deterministic=True,
)
correct_len = accept_token_num
row_ids = torch.arange(bs, dtype=torch.long, device=device)
accept_pos = accept_index[row_ids, correct_len.to(torch.long)].to(torch.long)
bonus = predicts[accept_pos].to(torch.int64)
return correct_len, bonus
def build_dflash_verify_target_probs(
*,
next_token_logits: torch.Tensor,
sampling_info: Any,
draft_token_num: int,
bs: int,
max_top_k: Optional[int] = None,
uniform_top_k_value: Optional[int] = None,
use_sparse_topk: bool = True,
) -> torch.Tensor:
device = next_token_logits.device
need_top_k = bool(getattr(sampling_info, "need_top_k_sampling", True))
need_top_p = bool(getattr(sampling_info, "need_top_p_sampling", False))
expanded_temperature = torch.repeat_interleave(
sampling_info.temperatures, draft_token_num, dim=0
)
scaled_logits = next_token_logits / expanded_temperature
sparse_topk_applied = False
if use_sparse_topk and need_top_k:
repeated_top_ks = torch.repeat_interleave(
sampling_info.top_ks, draft_token_num, dim=0
).to(dtype=torch.int64)
vocab_size = int(scaled_logits.shape[-1])
repeated_top_ks.clamp_(min=1, max=vocab_size)
if max_top_k is None:
max_top_k = int(repeated_top_ks.max().item())
else:
max_top_k = int(max_top_k)
if max_top_k < 1:
max_top_k = 1
elif max_top_k > vocab_size:
max_top_k = vocab_size
# Sparse exact path for top-k/top-p (top-k-first semantics), then scatter to dense.
if 0 < max_top_k < vocab_size:
topk_logits, topk_indices = torch.topk(scaled_logits, k=max_top_k, dim=-1)
if uniform_top_k_value is None or int(uniform_top_k_value) != max_top_k:
ranks = torch.arange(max_top_k, device=device, dtype=torch.int64)[
None, :
]
valid = ranks < repeated_top_ks.unsqueeze(1)
topk_logits = topk_logits.masked_fill(~valid, float("-inf"))
topk_probs = F.softmax(topk_logits, dim=-1)
if need_top_p:
repeated_top_ps = torch.repeat_interleave(
sampling_info.top_ps, draft_token_num, dim=0
)
topk_probs = top_p_renorm_prob(topk_probs, repeated_top_ps)
target_probs = torch.zeros_like(scaled_logits, dtype=topk_probs.dtype)
target_probs.scatter_(1, topk_indices, topk_probs)
sparse_topk_applied = True
if not sparse_topk_applied:
target_probs = F.softmax(scaled_logits, dim=-1)
if need_top_k:
target_probs = top_k_renorm_prob(
target_probs,
torch.repeat_interleave(sampling_info.top_ks, draft_token_num, dim=0),
)
if need_top_p:
target_probs = top_p_renorm_prob(
target_probs,
torch.repeat_interleave(sampling_info.top_ps, draft_token_num, dim=0),
)
return target_probs.view(bs, draft_token_num, -1).contiguous()
def validate_dflash_request(req: Req, enable_overlap: bool) -> Optional[str]:
if req.return_logprob:
return "DFLASH speculative decoding does not support return_logprob yet."
if enable_overlap and req.return_hidden_states:
return "DFLASH speculative decoding does not support return_hidden_states yet."
if (
req.sampling_params.json_schema is not None
or req.sampling_params.regex is not None
or req.sampling_params.ebnf is not None
or req.sampling_params.structural_tag is not None
):
return (
"DFLASH speculative decoding does not support "
"grammar-constrained decoding yet."
)
return None
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,394 @@
import logging
from sglang.srt.server_args import ServerArgs
from sglang.srt.utils.common import (
cpu_has_amx_support,
is_blackwell,
is_cpu,
is_hip,
is_musa,
is_npu,
)
logger = logging.getLogger(__name__)
class DraftBackendFactory:
def __init__(
self,
server_args: ServerArgs,
draft_model_runner,
topk: int,
speculative_num_steps: int,
):
self.server_args = server_args
self.draft_model_runner = draft_model_runner
self.topk = topk
self.speculative_num_steps = speculative_num_steps
self.draft_attn_backend = server_args.speculative_draft_attention_backend
def _create_backend(
self, backend_name: str, backend_map: dict, error_template: str
):
backend_type = (
self.draft_attn_backend
if self.draft_attn_backend
else getattr(self.server_args, backend_name)
)
if backend_type is None:
backend_type = self.server_args.attention_backend
if backend_type not in backend_map:
raise ValueError(error_template.format(backend_type=backend_type))
return backend_map[backend_type]()
def create_decode_backend(self):
# No multi-step draft backend for steps=0 (nospec) or steps=1.
if self.speculative_num_steps <= 1:
return None
backend_map = {
"flashinfer": self._create_flashinfer_decode_backend,
"triton": self._create_triton_decode_backend,
"intel_amx": self._create_intel_amx_decode_backend,
"aiter": self._create_aiter_decode_backend,
"fa3": self._create_fa3_decode_backend,
"hybrid_linear_attn": self._create_hybrid_linear_attn_decode_backend,
"flashmla": self._create_flashmla_decode_backend,
"trtllm_mha": self._create_trtllm_mha_decode_backend,
"trtllm_mla": self._create_trtllm_mla_decode_backend,
"cutedsl_mla": self._create_cutedsl_mla_decode_backend,
"tokenspeed_mla": self._create_tokenspeed_mla_decode_backend,
"dsa": self._create_dsa_decode_backend,
"nsa": self._create_dsa_decode_backend, # Deprecated alias for "dsa"
"ascend": self._create_ascend_decode_backend,
"fa4": self._create_fa4_decode_backend,
"dsv4": self._create_dsv4_decode_backend,
}
return self._create_backend(
"decode_attention_backend",
backend_map,
"EAGLE is not supported in decode attention backend {backend_type}",
)
def create_draft_extend_backend(self):
backend_map = {
"flashinfer": self._create_flashinfer_prefill_backend,
"triton": self._create_triton_prefill_backend,
"intel_amx": self._create_intel_amx_prefill_backend,
"aiter": self._create_aiter_prefill_backend,
"fa3": self._create_fa3_prefill_backend,
"hybrid_linear_attn": self._create_hybrid_linear_attn_prefill_backend,
"flashmla": self._create_flashmla_prefill_backend,
"trtllm_mha": self._create_trtllm_mha_prefill_backend,
"trtllm_mla": self._create_trtllm_mla_prefill_backend,
# cute-dsl MLA only supports decode; draft-extend falls back to trtllm-gen.
"cutedsl_mla": self._create_trtllm_mla_prefill_backend,
"tokenspeed_mla": self._create_tokenspeed_mla_prefill_backend,
"dsa": self._create_dsa_prefill_backend,
"nsa": self._create_dsa_prefill_backend, # Deprecated alias for "dsa"
"ascend": self._create_ascend_prefill_backend,
"fa4": self._create_fa4_prefill_backend,
"dsv4": self._create_dsv4_prefill_backend,
}
backend_name = (
"decode_attention_backend"
if self.server_args.speculative_attention_mode == "decode"
else "prefill_attention_backend"
)
return self._create_backend(
backend_name,
backend_map,
"EAGLE is not supported in attention backend {backend_type}",
)
def _create_dsa_decode_backend(self):
from sglang.srt.layers.attention.dsa_backend import (
DeepseekSparseAttnMultiStepBackend,
)
return DeepseekSparseAttnMultiStepBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_dsa_prefill_backend(self):
from sglang.srt.layers.attention.dsa_backend import DeepseekSparseAttnBackend
return DeepseekSparseAttnBackend(self.draft_model_runner, skip_prefill=False)
def _create_flashinfer_decode_backend(self):
if not self.draft_model_runner.use_mla_backend:
from sglang.srt.layers.attention.flashinfer_backend import (
FlashInferMultiStepDraftBackend,
)
return FlashInferMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
else:
from sglang.srt.layers.attention.flashinfer_mla_backend import (
FlashInferMLAMultiStepDraftBackend,
)
return FlashInferMLAMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_triton_decode_backend(self):
from sglang.srt.layers.attention.triton_backend import (
TritonMultiStepDraftBackend,
)
return TritonMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_intel_amx_decode_backend(self):
from sglang.srt.layers.attention.intel_amx_backend import (
IntelAMXMultiStepDraftBackend,
)
return IntelAMXMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_hybrid_linear_attn_decode_backend(self):
if is_cpu() and cpu_has_amx_support():
return self._create_intel_amx_decode_backend()
if is_blackwell():
return self._create_triton_decode_backend()
return self._create_fa3_decode_backend()
def _create_hybrid_linear_attn_prefill_backend(self):
if is_cpu() and cpu_has_amx_support():
return self._create_intel_amx_prefill_backend()
if is_blackwell():
return self._create_triton_prefill_backend()
return self._create_fa3_prefill_backend()
def _create_aiter_decode_backend(self):
from sglang.srt.layers.attention.aiter_backend import AiterMultiStepDraftBackend
return AiterMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_fa_decode_backend(self, fa_impl_ver: int = 3):
if not is_musa():
from sglang.srt.layers.attention.flashattention_backend import (
FlashAttentionMultiStepBackend,
)
else:
from sglang.srt.hardware_backend.musa.attention.flashattention_backend import (
MusaFlashAttentionMultiStepBackend as FlashAttentionMultiStepBackend,
)
return FlashAttentionMultiStepBackend(
self.draft_model_runner,
self.topk,
self.speculative_num_steps,
fa_impl_ver=fa_impl_ver,
)
def _create_fa3_decode_backend(self):
return self._create_fa_decode_backend(fa_impl_ver=3)
def _create_fa4_decode_backend(self):
return self._create_fa_decode_backend(fa_impl_ver=4)
def _create_flashmla_decode_backend(self):
from sglang.srt.layers.attention.flashmla_backend import (
FlashMLAMultiStepDraftBackend,
)
return FlashMLAMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_trtllm_mha_decode_backend(self):
from sglang.srt.layers.attention.trtllm_mha_backend import (
TRTLLMHAAttnMultiStepDraftBackend,
)
return TRTLLMHAAttnMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_trtllm_mla_decode_backend(self, backend: str = "trtllm-gen"):
if not self.draft_model_runner.use_mla_backend:
raise ValueError(
"trtllm_mla backend requires MLA model (use_mla_backend=True)."
)
from sglang.srt.layers.attention.trtllm_mla_backend import (
TRTLLMMLAMultiStepDraftBackend,
)
return TRTLLMMLAMultiStepDraftBackend(
self.draft_model_runner,
self.topk,
self.speculative_num_steps,
backend=backend,
)
def _create_cutedsl_mla_decode_backend(self):
return self._create_trtllm_mla_decode_backend(backend="cute-dsl")
def _create_tokenspeed_mla_decode_backend(self):
if not self.draft_model_runner.use_mla_backend:
raise ValueError(
"tokenspeed_mla backend requires MLA model (use_mla_backend=True)."
)
from sglang.srt.layers.attention.tokenspeed_mla_backend import (
TokenspeedMLAMultiStepDraftBackend,
)
return TokenspeedMLAMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_ascend_decode_backend(self):
from sglang.srt.hardware_backend.npu.attention.ascend_backend import (
AscendAttnMultiStepDraftBackend,
)
return AscendAttnMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_dsv4_decode_backend(self):
# Decode here is the EAGLE multi-step draft decode path.
if is_npu():
from sglang.srt.hardware_backend.npu.attention.ascend_dsv4_backend import (
DeepseekV4AscendMultiStepDraftBackend,
)
return DeepseekV4AscendMultiStepDraftBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
elif is_hip():
from sglang.srt.layers.attention.deepseek_v4_backend_hip_radix import (
DeepseekV4MultiStepBackend,
)
else:
from sglang.srt.layers.attention.deepseek_v4_backend import (
DeepseekV4MultiStepBackend,
)
return DeepseekV4MultiStepBackend(
self.draft_model_runner, self.topk, self.speculative_num_steps
)
def _create_flashinfer_prefill_backend(self):
if not self.draft_model_runner.use_mla_backend:
from sglang.srt.layers.attention.flashinfer_backend import (
FlashInferAttnBackend,
)
return FlashInferAttnBackend(self.draft_model_runner, skip_prefill=False)
else:
from sglang.srt.layers.attention.flashinfer_mla_backend import (
FlashInferMLAAttnBackend,
)
return FlashInferMLAAttnBackend(self.draft_model_runner, skip_prefill=False)
def _create_triton_prefill_backend(self):
from sglang.srt.layers.attention.triton_backend import TritonAttnBackend
return TritonAttnBackend(self.draft_model_runner, skip_prefill=False)
def _create_intel_amx_prefill_backend(self):
from sglang.srt.layers.attention.intel_amx_backend import IntelAMXAttnBackend
return IntelAMXAttnBackend(self.draft_model_runner)
def _create_aiter_prefill_backend(self):
from sglang.srt.layers.attention.aiter_backend import AiterAttnBackend
return AiterAttnBackend(self.draft_model_runner, skip_prefill=False)
def _create_fa_prefill_backend(self, fa_impl_ver: int = 3):
if not is_musa():
from sglang.srt.layers.attention.flashattention_backend import (
FlashAttentionBackend,
)
else:
from sglang.srt.hardware_backend.musa.attention.flashattention_backend import (
MusaFlashAttentionBackend as FlashAttentionBackend,
)
return FlashAttentionBackend(
self.draft_model_runner, skip_prefill=False, fa_impl_ver=fa_impl_ver
)
def _create_fa3_prefill_backend(self):
return self._create_fa_prefill_backend(fa_impl_ver=3)
def _create_fa4_prefill_backend(self):
return self._create_fa_prefill_backend(fa_impl_ver=4)
def _create_trtllm_mha_prefill_backend(self):
from sglang.srt.layers.attention.trtllm_mha_backend import TRTLLMHAAttnBackend
return TRTLLMHAAttnBackend(self.draft_model_runner, skip_prefill=False)
def _create_trtllm_mla_prefill_backend(self):
if not self.draft_model_runner.use_mla_backend:
raise ValueError(
"trtllm_mla backend requires MLA model (use_mla_backend=True)."
)
from sglang.srt.layers.attention.trtllm_mla_backend import TRTLLMMLABackend
return TRTLLMMLABackend(self.draft_model_runner, skip_prefill=False)
def _create_tokenspeed_mla_prefill_backend(self):
if not self.draft_model_runner.use_mla_backend:
raise ValueError(
"tokenspeed_mla backend requires MLA model (use_mla_backend=True)."
)
from sglang.srt.layers.attention.tokenspeed_mla_backend import (
TokenspeedMLABackend,
)
return TokenspeedMLABackend(self.draft_model_runner, skip_prefill=False)
def _create_ascend_prefill_backend(self):
from sglang.srt.hardware_backend.npu.attention.ascend_backend import (
AscendAttnBackend,
)
return AscendAttnBackend(self.draft_model_runner)
def _create_flashmla_prefill_backend(self):
logger.warning(
"flashmla prefill backend is not yet supported for draft extend."
)
return None
def _create_dsv4_prefill_backend(self):
# On NPU the "dsv4" backend resolves to the Ascend V4 subclass; its
# draft-extend path uses the registered DSV4 prefill backend.
if is_npu():
from sglang.srt.layers.attention.attention_registry import (
ATTENTION_BACKENDS,
)
return ATTENTION_BACKENDS["dsv4"](self.draft_model_runner)
elif is_hip():
from sglang.srt.layers.attention.deepseek_v4_backend_hip_radix import (
DeepseekV4HipRadixBackend,
)
return DeepseekV4HipRadixBackend(
self.draft_model_runner, skip_prefill=False
)
from sglang.srt.layers.attention.deepseek_v4_backend import (
DeepseekV4AttnBackend,
)
return DeepseekV4AttnBackend(self.draft_model_runner, skip_prefill=False)
@@ -0,0 +1,168 @@
from __future__ import annotations
import logging
from copy import deepcopy
from typing import TYPE_CHECKING, Optional
import msgspec
import torch
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode
from sglang.srt.runtime_context import get_context, get_server_args
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
if TYPE_CHECKING:
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.model_executor.model_runner import ModelRunner
logger = logging.getLogger(__name__)
_SUPPORTED_DRAFT_BACKENDS = ("flashinfer", "fa3", "fa4", "triton", "ascend")
class DraftWorkerBundle(msgspec.Struct, frozen=True):
draft_worker: TpModelWorker
draft_model_runner: ModelRunner
draft_model: torch.nn.Module
resolved_attention_backend: str
def _resolve_draft_attention_backend_fallback(
*, draft_server_args: ServerArgs, algo_label: str
) -> str:
draft_backend = draft_server_args.speculative_draft_attention_backend
if draft_backend is None:
draft_backend, _ = draft_server_args.get_attention_backends()
if draft_backend is None:
return "triton" if torch.version.hip else "flashinfer"
if draft_backend not in _SUPPORTED_DRAFT_BACKENDS:
fallback = "triton" if torch.version.hip else "flashinfer"
logger.warning(
"%s draft worker only supports attention_backend in %s for now, "
"but got %r. Falling back to '%s'.",
algo_label,
_SUPPORTED_DRAFT_BACKENDS,
draft_backend,
fallback,
)
return fallback
return draft_backend
def build_draft_tp_worker(
*,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_model_config: ModelConfig,
algo_label: str,
attention_backend_override: Optional[str] = None,
) -> DraftWorkerBundle:
draft_server_args = deepcopy(server_args)
# An override names a draft-specific backend the caller has already
# validated (e.g. a self-drafting architecture); it skips the generic
# supported-backend fallback below.
draft_backend = attention_backend_override or (
_resolve_draft_attention_backend_fallback(
draft_server_args=draft_server_args, algo_label=algo_label
)
)
# Post-resolution ServerArgs rejects bare assignment; route the draft-copy
# adjustments through the audited mutation point. Keep the resolved value
# on speculative_draft_attention_backend: downstream draft-worker logic
# keys on that field (backend selection in _get_attention_backend and the
# fa4-draft KV dtype override in configure_kv_cache_dtype), so nulling it
# would silently skip those paths. context_length keeps the draft aligned
# with the target.
draft_server_args.override(
"draft_worker.build",
skip_tokenizer_init=True,
speculative_draft_attention_backend=draft_backend,
prefill_attention_backend=None,
decode_attention_backend=None,
attention_backend=draft_backend,
context_length=target_model_config.context_len,
)
saved_server_args = get_server_args()
try:
draft_worker = TpModelWorker(
server_args=draft_server_args,
gpu_id=gpu_id,
tp_rank=tp_rank,
moe_ep_rank=moe_ep_rank,
pp_rank=0,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
dp_rank=dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
)
finally:
get_context().set_server_args(saved_server_args)
draft_model_runner = draft_worker.model_runner
draft_worker.draft_runner = draft_model_runner
return DraftWorkerBundle(
draft_worker=draft_worker,
draft_model_runner=draft_model_runner,
draft_model=draft_model_runner.model,
resolved_attention_backend=draft_backend,
)
def make_draft_input_v2(
*,
bonus_tokens: torch.Tensor,
new_seq_lens: torch.Tensor,
) -> DFlashDraftInputV2:
bs = int(new_seq_lens.numel())
device = bonus_tokens.device
return DFlashDraftInputV2(
topk_p=torch.empty((bs, 0), device=device, dtype=torch.float32),
topk_index=torch.empty((bs, 0), device=device, dtype=torch.int64),
bonus_tokens=bonus_tokens.to(dtype=torch.int64),
new_seq_lens=new_seq_lens.to(dtype=torch.int64),
hidden_states=torch.empty((bs, 0), device=device, dtype=torch.float16),
)
def make_draft_block_spec_info(
*,
draft_token_num: int,
device: torch.device,
) -> DFlashVerifyInput:
return DFlashVerifyInput(
draft_token=torch.empty((0,), dtype=torch.long, device=device),
positions=torch.empty((0,), dtype=torch.int64, device=device),
draft_token_num=int(draft_token_num),
custom_mask=None,
capture_hidden_mode=CaptureHiddenMode.NULL,
)
def make_draft_sampler_capture_hook(draft_sampler):
def capture_hook(runner, out, forward_batch, num_tokens):
del runner, num_tokens
if not isinstance(out, LogitsProcessorOutput) or out.hidden_states is None:
raise RuntimeError(
"draft sampler set but the draft forward has no "
"hidden_states to capture into the graph."
)
draft_sampler(out.hidden_states, forward_batch.input_ids)
return capture_hook
def build_block_pos_offsets(*, length: int, device: torch.device) -> torch.Tensor:
return torch.arange(int(length), device=device, dtype=torch.int64)
@@ -0,0 +1,835 @@
from __future__ import annotations
import json
import logging
import math
from collections import deque
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union
import msgspec
import torch
from sglang.srt.environ import envs
from sglang.srt.kv_canary.runner.future_tensor import DelayedDeviceHostHandler
from sglang.srt.speculative.ragged_verify import RaggedVerifyLayout
logger = logging.getLogger(__name__)
_GATHER_ROW_CHUNK = 512
_STATE_SWEEP_INTERVAL = 1024
_STATE_EXPIRE_STEPS = 4096
_FLUSH_EVERY_STEPS = 16
_PENDING_BUCKET_MIN = 16
_DEFAULT_ONLINE_WINDOW_STEPS = 256
SKIP_STEP_WARNING = (
"skipping step: {} (pending blocks of affected requests "
"are dropped by the seq-len continuity check)"
)
def block_accept_skip_reason(
*,
logits_adjustments_are_noop: bool,
corrected_logits: Optional[Any],
) -> Optional[str]:
if not logits_adjustments_are_noop:
return (
"non-noop logits adjustments (penalizer/logit_bias/grammar) "
"in batch; cross-step conditioning of the gathered target "
"probabilities would be state-dependent"
)
if corrected_logits is None:
return "corrected_logits unavailable (folded draft path)"
return None
def warn_once(warned_reasons: set, *, reason: str) -> None:
if reason not in warned_reasons:
warned_reasons.add(reason)
logger.warning(
"DSPARK block accept estimate recorder: %s (warned once)", reason
)
def gather_chunked_token_logprobs(
*,
logits,
row_indices,
token_indices,
per_row_temps,
chunk_size: int,
):
"""Chunked per-row token logprob gather: logprob of token_indices[i] under
logits[row_indices[i]] / per_row_temps[i], computed chunk_size rows at a
time to bound the fp32 softmax workspace."""
results = []
for start in range(0, row_indices.shape[0], chunk_size):
end = start + chunk_size
rows = logits[row_indices[start:end]].to(torch.float32)
rows = rows / per_row_temps[start:end, None]
log_norm = torch.logsumexp(rows, dim=-1)
token_logits = rows.gather(dim=1, index=token_indices[start:end, None]).squeeze(
1
)
results.append(token_logits - log_norm)
return torch.cat(results)
def _pending_bucket(count: int) -> int:
if count == 0:
return 0
bucket = _PENDING_BUCKET_MIN
while bucket < count:
bucket *= 2
return bucket
class _CeilingSnapshot(msgspec.Struct):
window_lo: float
window_hi: float
window_blocks: int
window_horizon: int
cumulative_lo: float
cumulative_hi: float
cumulative_blocks: int
class _OnlineCeiling:
def __init__(self, *, log_interval: int, window_steps: int) -> None:
self._log_interval = log_interval
self._window_steps = window_steps
self._steps: deque[Tuple[int, float, float, int]] = deque()
self._win_lo = 0.0
self._win_hi = 0.0
self._win_count = 0
self._cum_lo = 0.0
self._cum_hi = 0.0
self._cum_count = 0
self._max_forward_ct = 0
def add(self, *, forward_ct: int, lo: float, hi: float) -> None:
self._max_forward_ct = max(self._max_forward_ct, forward_ct)
if self._steps and self._steps[-1][0] == forward_ct:
fct, slo, shi, c = self._steps[-1]
self._steps[-1] = (fct, slo + lo, shi + hi, c + 1)
else:
self._steps.append((forward_ct, lo, hi, 1))
self._win_lo += lo
self._win_hi += hi
self._win_count += 1
self._cum_lo += lo
self._cum_hi += hi
self._cum_count += 1
self._evict(forward_ct=self._max_forward_ct)
def _evict(self, *, forward_ct: int) -> None:
cutoff = forward_ct - self._window_steps
while self._steps and self._steps[0][0] <= cutoff:
_, slo, shi, c = self._steps.popleft()
self._win_lo -= slo
self._win_hi -= shi
self._win_count -= c
def estimate(self) -> Optional[_CeilingSnapshot]:
if self._cum_count == 0:
return None
return _CeilingSnapshot(
window_lo=self._win_lo / self._win_count,
window_hi=self._win_hi / self._win_count,
window_blocks=self._win_count,
window_horizon=min(self._window_steps, self._max_forward_ct),
cumulative_lo=self._cum_lo / self._cum_count,
cumulative_hi=self._cum_hi / self._cum_count,
cumulative_blocks=self._cum_count,
)
def maybe_log(self, *, forward_ct: int) -> None:
if self._log_interval <= 0 or forward_ct % self._log_interval != 0:
return
snap = self.estimate()
if snap is None:
return
logger.info(
"DSpark uncapped-acc-len estimate (forward_ct=%d): "
"last %d passes ~%.3f [%.3f, %.3f] w=%.3f (%d blocks) | "
"cumulative ~%.3f [%.3f, %.3f] w=%.3f (%d blocks)",
forward_ct,
snap.window_horizon,
0.5 * (snap.window_lo + snap.window_hi),
snap.window_lo,
snap.window_hi,
snap.window_hi - snap.window_lo,
snap.window_blocks,
0.5 * (snap.cumulative_lo + snap.cumulative_hi),
snap.cumulative_lo,
snap.cumulative_hi,
snap.cumulative_hi - snap.cumulative_lo,
snap.cumulative_blocks,
)
class _PendingBlock(msgspec.Struct):
forward_ct: int
anchor_pos: int
window: int
trimmed_tokens: List[int]
next_offset: int
q_lps: List[float] = []
est_prod: float = 1.0
est_lo_extra: float = 0.0
class _RequestState(msgspec.Struct):
expected_seq_len: int = -1
last_seen_ct: int = 0
pending: List[_PendingBlock] = []
class _PendingPlan(msgspec.Struct):
rows: List[int]
tokens: List[int]
slot_lookup: dict[tuple[int, int, int], int]
class _SettleBatch(msgspec.Struct):
forward_ct: int
rids: List[str]
row_meta: List[List[int]]
drafts: List[List[int]]
q_all: List[List[float]]
target_diag: List[List[float]]
pending_logprobs: List[float]
slot_lookup: dict[tuple[int, int, int], int]
@classmethod
def from_bundle(cls, bundle: dict[str, Any]) -> _SettleBatch:
return cls(
forward_ct=bundle["forward_ct"],
rids=bundle["rids"],
row_meta=bundle["row_meta"].tolist(),
drafts=bundle["draft_tokens"].tolist(),
q_all=bundle["q_all"].tolist(),
target_diag=bundle["target_diag_logprobs"].tolist(),
pending_logprobs=bundle["pending_logprobs"].tolist(),
slot_lookup=bundle["pending_slot_lookup"],
)
class BlockAcceptEstimateRecorder:
def __init__(
self,
*,
path: str,
gamma: int,
device: Union[str, torch.device],
online_log_interval: int = 0,
online_window_steps: int = 0,
) -> None:
self._gamma = gamma
self._last_forward_ct = 0
if path:
self._path: Optional[Path] = Path(path)
self._path.parent.mkdir(parents=True, exist_ok=True)
self._file = self._path.open("w")
else:
self._path = None
self._file = None
self._device = torch.device(device)
self._states: dict[str, _RequestState] = {}
self._steps_since_flush = 0
self._observed_step_ct = 0
self._discontinuity_drop_ct = 0
self._skipped_step_ct = 0
self._warned_skip_reasons: set[str] = set()
self._finish_intents: dict[str, bool] = {}
self._online = _OnlineCeiling(
log_interval=online_log_interval,
window_steps=(
online_window_steps
if online_window_steps > 0
else (
online_log_interval
if online_log_interval > 0
else _DEFAULT_ONLINE_WINDOW_STEPS
)
),
)
self._retained_h2d: List[torch.Tensor] = []
self._delayed: Optional[DelayedDeviceHostHandler] = None
if self._device.type == "cuda":
self._delayed = DelayedDeviceHostHandler(
d2h_stream=torch.cuda.Stream(device=self._device)
)
logger.info(
"DSPARK block accept estimate recorder enabled: path=%s gamma=%d "
"async=%s online_log_interval=%d",
path,
gamma,
self._delayed is not None,
online_log_interval,
)
def observe_verify_step(
self,
*,
forward_ct: int,
rids: List[str],
draft_tokens: torch.Tensor,
corrected_logits: Optional[torch.Tensor],
draft_temperatures: torch.Tensor,
greedy_mask: torch.Tensor,
target_logits: torch.Tensor,
target_temperatures: torch.Tensor,
truncated_sampling_mask: Optional[torch.Tensor],
logits_adjustments_are_noop: bool,
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
bonus: torch.Tensor,
prefix_lens: torch.Tensor,
layout: Optional[RaggedVerifyLayout],
) -> None:
if (
self._delayed is not None
and torch.cuda.is_available()
and torch.cuda.is_current_stream_capturing()
):
return
skip_reason = self._skip_reason(
logits_adjustments_are_noop=logits_adjustments_are_noop,
corrected_logits=corrected_logits,
)
if skip_reason is not None:
self._skip_step(reason=skip_reason)
def compute_on_device() -> Optional[dict[str, Any]]:
if skip_reason is not None:
return None
return self._build_device_bundle(
forward_ct=forward_ct,
rids=rids,
draft_tokens=draft_tokens,
corrected_logits=corrected_logits,
draft_temperatures=draft_temperatures,
greedy_mask=greedy_mask,
target_logits=target_logits,
target_temperatures=target_temperatures,
truncated_sampling_mask=truncated_sampling_mask,
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
bonus=bonus,
prefix_lens=prefix_lens,
layout=layout,
)
if self._delayed is not None:
self._delayed.step(
compute_on_device=compute_on_device,
postprocess_on_host=self._settle_and_write,
)
else:
bundle = compute_on_device()
if bundle is not None:
self._settle_and_write(bundle)
def flush(self) -> None:
if self._delayed is not None:
self._delayed.step(
compute_on_device=lambda: None,
postprocess_on_host=self._settle_and_write,
)
self._apply_all_finish_intents()
if self._file is not None:
self._file.flush()
self._steps_since_flush = 0
def note_request_finished(self, *, rid: str, natural_stop: bool) -> None:
if self._delayed is None:
self._finalize_request(
rid=rid, natural_stop=natural_stop, forward_ct=self._last_forward_ct
)
else:
self._finish_intents[rid] = natural_stop
def _apply_all_finish_intents(self) -> None:
for rid in list(self._finish_intents):
self._finalize_request(
rid=rid,
natural_stop=self._finish_intents.pop(rid),
forward_ct=self._last_forward_ct,
)
def _finalize_request(
self, *, rid: str, natural_stop: bool, forward_ct: int
) -> None:
state = self._states.pop(rid, None)
if state is None:
return
for block in state.pending:
if natural_stop:
self._finalize_eos_online(block, forward_ct=forward_ct)
else:
self._finalize_at_end_online(block, forward_ct=forward_ct)
if natural_stop and state.pending:
self._write_eos_marker(rid=rid, blocks=state.pending)
def _finalize_eos_online(self, block: _PendingBlock, *, forward_ct: int) -> None:
lo = block.window + 1.0 + block.est_lo_extra
self._online.add(forward_ct=forward_ct, lo=lo, hi=lo)
def _write_eos_marker(self, *, rid: str, blocks: List[_PendingBlock]) -> None:
if self._file is None:
return
marker = {"rid": rid, "eos_end": [block.forward_ct for block in blocks]}
self._file.write(json.dumps(marker) + "\n")
def online_estimate(self) -> Optional[_CeilingSnapshot]:
return self._online.estimate()
def estimate_log_suffix(self) -> Optional[str]:
snap = self.online_estimate()
if snap is None:
return None
mid = 0.5 * (snap.cumulative_lo + snap.cumulative_hi)
return (
f"est uncap acc len: {mid:.2f} "
f"[{snap.cumulative_lo:.2f}, {snap.cumulative_hi:.2f}]"
)
def drain_pending_online(self) -> None:
for state in self._states.values():
for block in state.pending:
self._finalize_at_end_online(block, forward_ct=self._last_forward_ct)
state.pending = []
def _finalize_walk_online(
self, block: _PendingBlock, *, diverged: bool, forward_ct: int
) -> None:
base = block.window + 1.0
lo = base + block.est_lo_extra
if diverged:
offset = block.next_offset - 1
tail = (
block.est_prod * (self._gamma - offset) if offset < self._gamma else 0.0
)
else:
tail = 0.0
self._online.add(forward_ct=forward_ct, lo=lo, hi=lo + tail)
def _finalize_at_end_online(self, block: _PendingBlock, *, forward_ct: int) -> None:
base = block.window + 1.0
lo = base + block.est_lo_extra
tail = block.est_prod * (self._gamma - block.next_offset + 1)
self._online.add(forward_ct=forward_ct, lo=lo, hi=lo + tail)
def _build_device_bundle(
self,
*,
forward_ct: int,
rids: List[str],
draft_tokens: torch.Tensor,
corrected_logits: torch.Tensor,
draft_temperatures: torch.Tensor,
greedy_mask: torch.Tensor,
target_logits: torch.Tensor,
target_temperatures: torch.Tensor,
truncated_sampling_mask: Optional[torch.Tensor],
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
bonus: torch.Tensor,
prefix_lens: torch.Tensor,
layout: Optional[RaggedVerifyLayout],
) -> dict[str, Any]:
gamma = self._gamma
rows_per_request = gamma + 1
bs = len(rids)
device = target_logits.device
assert draft_tokens.shape == (bs, gamma)
assert corrected_logits.shape[0] == bs and corrected_logits.shape[1] == gamma
assert target_logits.shape[0] == bs * rows_per_request
if truncated_sampling_mask is not None:
truncated_mask = truncated_sampling_mask
else:
truncated_mask = torch.zeros(bs, dtype=torch.bool, device=device)
if layout is not None:
verify_lens = layout.verify_lens
else:
verify_lens = torch.full(
(bs,), rows_per_request, dtype=torch.int32, device=device
)
draft_temps_full = (
draft_temperatures.reshape(bs).to(torch.float32).repeat_interleave(gamma)
)
target_temps_full = (
target_temperatures.reshape(bs)
.to(torch.float32)
.repeat_interleave(rows_per_request)
)
draft_flat = draft_tokens.reshape(-1)
q_all = self._gather_logprobs(
logits=corrected_logits.reshape(bs * gamma, -1),
row_indices=torch.arange(bs * gamma, device=device),
token_indices=draft_flat,
temps=draft_temps_full,
).reshape(bs, gamma)
target_diag = self._gather_logprobs(
logits=target_logits,
row_indices=self._diag_rows(bs=bs, rows_per_request=rows_per_request),
token_indices=draft_flat,
temps=target_temps_full,
).reshape(bs, gamma)
self._retained_h2d = []
plan = self._plan_pending(bs=bs, rows_per_request=rows_per_request, rids=rids)
pending_logprobs = self._gather_pending(
plan=plan,
target_logits=target_logits,
target_temps_full=target_temps_full,
device=device,
)
return {
"forward_ct": int(forward_ct),
"rids": list(rids),
"row_meta": self._pack_row_meta(
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
bonus=bonus,
prefix_lens=prefix_lens,
greedy_mask=greedy_mask,
truncated_mask=truncated_mask,
verify_lens=verify_lens,
),
"draft_tokens": draft_tokens,
"q_all": q_all,
"target_diag_logprobs": target_diag,
"pending_logprobs": pending_logprobs,
"pending_slot_lookup": plan.slot_lookup,
}
def _diag_rows(self, *, bs: int, rows_per_request: int) -> torch.Tensor:
device = self._device
return (
(torch.arange(bs, device=device) * rows_per_request)[:, None]
+ torch.arange(self._gamma, device=device)[None, :]
).reshape(-1)
def _plan_pending(
self, *, bs: int, rows_per_request: int, rids: List[str]
) -> _PendingPlan:
gamma = self._gamma
rows: List[int] = []
tokens: List[int] = []
slot_lookup: dict[tuple[int, int, int], int] = {}
for b in range(bs):
state = self._states.get(rids[b])
if state is None or not state.pending or state.expected_seq_len < 0:
continue
expected_seq_len = state.expected_seq_len
for block_idx, block in enumerate(state.pending):
offset = block.next_offset
while offset <= gamma:
row = block.anchor_pos + offset - expected_seq_len
if row < 0 or row >= rows_per_request:
break
slot_lookup[(b, block_idx, offset)] = len(rows)
rows.append(b * rows_per_request + row)
tokens.append(block.trimmed_tokens[offset - block.window - 1])
offset += 1
return _PendingPlan(rows=rows, tokens=tokens, slot_lookup=slot_lookup)
def _gather_pending(
self,
*,
plan: _PendingPlan,
target_logits: torch.Tensor,
target_temps_full: torch.Tensor,
device: torch.device,
) -> torch.Tensor:
bucket = _pending_bucket(len(plan.rows))
rows = plan.rows + [0] * (bucket - len(plan.rows))
tokens = plan.tokens + [0] * (bucket - len(plan.tokens))
return self._gather_logprobs(
logits=target_logits,
row_indices=self._host_to_device_async(rows, device=device),
token_indices=self._host_to_device_async(tokens, device=device),
temps=target_temps_full,
)
def _pack_row_meta(
self,
*,
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
bonus: torch.Tensor,
prefix_lens: torch.Tensor,
greedy_mask: torch.Tensor,
truncated_mask: torch.Tensor,
verify_lens: torch.Tensor,
) -> torch.Tensor:
return torch.stack(
[
correct_len.to(torch.int64),
cap_trim_lens.to(torch.int64),
bonus.to(torch.int64),
prefix_lens.to(torch.int64),
greedy_mask.to(torch.int64),
truncated_mask.to(torch.int64),
verify_lens.to(torch.int64),
],
dim=1,
)
def _settle_and_write(self, bundle: dict[str, Any]) -> None:
batch = _SettleBatch.from_bundle(bundle)
self._last_forward_ct = batch.forward_ct
for b in range(len(batch.rids)):
self._settle_row(b=b, batch=batch)
self._finish_step(forward_ct=batch.forward_ct)
self._apply_all_finish_intents()
def _settle_row(self, *, b: int, batch: _SettleBatch) -> None:
forward_ct = batch.forward_ct
rid = batch.rids[b]
state = self._states.setdefault(rid, _RequestState())
state.last_seen_ct = forward_ct
cl, cap_trim, bonus_token, seq_len, is_greedy, is_truncated, verify_len = (
batch.row_meta[b]
)
window = verify_len - 1
assert 0 <= cl <= window <= self._gamma
self._drop_pending_on_discontinuity(
state, seq_len=seq_len, forward_ct=forward_ct
)
state.expected_seq_len = seq_len + cl + 1
if is_greedy or is_truncated:
if is_truncated and not is_greedy:
self._warn_once(
reason="requests with top-k/top-p/min-p sampling are "
"excluded per-row; the estimator only supports "
"pure-temperature sampling (processed target distribution "
"would differ from plain softmax(logits/T))"
)
state.pending = []
return
record: dict[str, Any] = {
"rid": rid,
"fct": forward_ct,
"w": window,
"cl": cl,
"ct": cap_trim,
}
num_old_pending = len(state.pending)
if cl == window and window < self._gamma:
self._open_block(
state,
record,
drafts_row=batch.drafts[b],
q_all_row=batch.q_all[b],
window=window,
seq_len=seq_len,
forward_ct=forward_ct,
)
else:
self._online.add(forward_ct=forward_ct, lo=cl + 1.0, hi=cl + 1.0)
pending_gathers = self._settle_pending(
b=b,
batch=batch,
state=state,
realized=batch.drafts[b][:cl] + [bonus_token],
cl=cl,
seq_len=seq_len,
num_old_pending=num_old_pending,
)
if pending_gathers:
record["pg"] = pending_gathers
if self._file is not None:
self._file.write(json.dumps(record) + "\n")
def _open_block(
self,
state: _RequestState,
record: dict[str, Any],
*,
drafts_row: List[int],
q_all_row: List[float],
window: int,
seq_len: int,
forward_ct: int,
) -> None:
trimmed_tokens = drafts_row[window : self._gamma]
q_lps = q_all_row[window : self._gamma]
state.pending.append(
_PendingBlock(
forward_ct=forward_ct,
anchor_pos=seq_len - 1,
window=window,
trimmed_tokens=trimmed_tokens,
next_offset=window + 1,
q_lps=q_lps,
)
)
record["trimmed_tokens"] = trimmed_tokens
record["q_lp"] = q_lps
def _settle_pending(
self,
*,
b: int,
batch: _SettleBatch,
state: _RequestState,
realized: List[int],
cl: int,
seq_len: int,
num_old_pending: int,
) -> List[list]:
gamma = self._gamma
pending_gathers: List[list] = []
kept_pending: List[_PendingBlock] = []
for block_idx, block in enumerate(state.pending):
diverged = False
while block.next_offset <= gamma:
row = block.anchor_pos + block.next_offset - seq_len
assert row >= 0
if row > cl:
break
token = block.trimmed_tokens[block.next_offset - block.window - 1]
if block_idx < num_old_pending:
p_lp = batch.pending_logprobs[
batch.slot_lookup[(b, block_idx, block.next_offset)]
]
else:
p_lp = batch.target_diag[b][row]
pending_gathers.append(
[block.forward_ct, block.next_offset, p_lp, token, realized[row]]
)
self._accumulate_online(block, p_lp=p_lp)
block.next_offset += 1
if realized[row] != token:
diverged = True
break
if not diverged and block.next_offset <= gamma:
kept_pending.append(block)
else:
self._finalize_walk_online(
block, diverged=diverged, forward_ct=batch.forward_ct
)
state.pending = kept_pending
return pending_gathers
def _accumulate_online(self, block: _PendingBlock, *, p_lp: float) -> None:
a = min(1.0, math.exp(p_lp - block.q_lps[block.next_offset - block.window - 1]))
block.est_prod *= a
block.est_lo_extra += block.est_prod
def _drop_pending_on_discontinuity(
self, state: _RequestState, *, seq_len: int, forward_ct: int
) -> None:
if state.expected_seq_len < 0 or seq_len == state.expected_seq_len:
return
if not state.pending:
return
self._discontinuity_drop_ct += len(state.pending)
for block in state.pending:
self._finalize_at_end_online(block, forward_ct=forward_ct)
state.pending = []
def _finish_step(self, *, forward_ct: int) -> None:
self._observed_step_ct += 1
if self._file is not None:
self._steps_since_flush += 1
if self._steps_since_flush >= _FLUSH_EVERY_STEPS:
self._file.flush()
self._steps_since_flush = 0
if self._observed_step_ct % _STATE_SWEEP_INTERVAL == 0:
self._sweep_states(forward_ct=forward_ct)
self._online.maybe_log(forward_ct=forward_ct)
def _host_to_device_async(
self, values: List[int], *, device: torch.device
) -> torch.Tensor:
host = torch.tensor(values, dtype=torch.long, pin_memory=device.type == "cuda")
self._retained_h2d.append(host)
return host.to(device=device, non_blocking=True)
def _gather_logprobs(
self,
*,
logits: torch.Tensor,
row_indices: torch.Tensor,
token_indices: torch.Tensor,
temps: torch.Tensor,
) -> torch.Tensor:
if row_indices.numel() == 0:
return torch.zeros(0, dtype=torch.float32, device=logits.device)
per_row_temps = temps[row_indices].clamp_min(1e-5)
return gather_chunked_token_logprobs(
logits=logits,
row_indices=row_indices,
token_indices=token_indices,
per_row_temps=per_row_temps,
chunk_size=_GATHER_ROW_CHUNK,
)
def _sweep_states(self, *, forward_ct: int) -> None:
expired = [
rid
for rid, state in self._states.items()
if forward_ct - state.last_seen_ct > _STATE_EXPIRE_STEPS
]
for rid in expired:
for block in self._states[rid].pending:
self._finalize_at_end_online(block, forward_ct=forward_ct)
del self._states[rid]
self._finish_intents.pop(rid, None)
def _skip_reason(
self,
*,
logits_adjustments_are_noop: bool,
corrected_logits: Optional[torch.Tensor],
) -> Optional[str]:
return block_accept_skip_reason(
logits_adjustments_are_noop=logits_adjustments_are_noop,
corrected_logits=corrected_logits,
)
def _skip_step(self, *, reason: str) -> None:
self._skipped_step_ct += 1
self._warn_once(reason=SKIP_STEP_WARNING.format(reason))
def _warn_once(self, *, reason: str) -> None:
warn_once(self._warned_skip_reasons, reason=reason)
def create_block_accept_estimate_recorder(
*, gamma: int, device: Union[str, torch.device], tp_rank: int
) -> Optional[BlockAcceptEstimateRecorder]:
if tp_rank != 0:
return None
path = envs.SGLANG_DSPARK_BLOCK_ACCEPT_ESTIMATE_PATH.get()
online_log_interval = envs.SGLANG_DSPARK_BLOCK_ACCEPT_ONLINE_INTERVAL.get()
if not path and online_log_interval <= 0:
return None
return BlockAcceptEstimateRecorder(
path=path,
gamma=gamma,
device=device,
online_log_interval=online_log_interval,
)
@@ -0,0 +1,295 @@
from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING, Any, List, Optional
import msgspec
from sglang.srt.speculative.dflash_utils import parse_dflash_draft_config
if TYPE_CHECKING:
from sglang.srt.server_args import ServerArgs
logger = logging.getLogger(__name__)
DEFAULT_DSPARK_GAMMA = 7
SUPPORTED_DSPARK_MARKOV_HEAD_TYPES = ("vanilla", "gated", "rnn")
# The dsv4 self-drafting checkpoint runs its draft attention on the dedicated
# DeepSeek-V4 backend instead of the generic draft-backend fallback.
DSV4_DRAFT_ATTENTION_BACKEND = "dsv4"
def draft_is_deepseek_v4(*, server_args: ServerArgs) -> bool:
from sglang.srt.configs.model_config import is_deepseek_v4
from sglang.srt.utils.hf_transformers_utils import get_config
draft_model_path = server_args.speculative_draft_model_path
if not draft_model_path:
return False
draft_hf_config = get_config(
draft_model_path,
trust_remote_code=server_args.trust_remote_code,
revision=server_args.speculative_draft_model_revision,
model_override_args=json.loads(server_args.json_model_override_args),
model_config_parser=server_args.model_config_parser,
)
return draft_hf_config is not None and is_deepseek_v4(draft_hf_config)
def dspark_gamma_from_num_draft_tokens(num_draft_tokens: int) -> int:
gamma = int(num_draft_tokens) - 1
if gamma < 1:
raise ValueError(
"DSpark speculative_num_draft_tokens must be >= 2 (= gamma + 1), "
f"got {num_draft_tokens}."
)
return gamma
class DSparkDraftConfig(msgspec.Struct, frozen=True):
num_hidden_layers: Optional[int]
num_target_layers: Optional[int]
gamma: Optional[int]
target_layer_ids: Optional[List[int]]
mask_token: str
mask_token_id: Optional[int]
markov_rank: int
markov_head_type: Optional[str]
def resolve_gamma(self, *, default: Optional[int] = None) -> Optional[int]:
return self.gamma if self.gamma is not None else default
def require_markov(self) -> bool:
return int(self.markov_rank) > 0
class DSparkRuntimeConfig(msgspec.Struct, frozen=True):
gamma: int
verify_num_draft_tokens: int
mask_token_id: int
def resolve_runtime_config(
*,
draft_hf_config: Any,
speculative_num_draft_tokens: Optional[int],
target_vocab_size: int,
) -> DSparkRuntimeConfig:
"""Resolve and validate the worker-facing DSpark runtime knobs (gamma,
verify window, mask token) from the draft checkpoint config, with
server_args.speculative_num_draft_tokens taking precedence for gamma."""
draft_config = parse_dspark_draft_config(draft_hf_config=draft_hf_config)
if not draft_config.require_markov():
raise ValueError(
"DSpark draft requires markov_rank > 0; got "
f"markov_rank={draft_config.markov_rank}."
)
if speculative_num_draft_tokens is None:
gamma = int(draft_config.resolve_gamma(default=None) or 0)
if gamma < 1:
raise ValueError(
"DSpark could not resolve gamma from the draft config and "
"speculative_num_draft_tokens is unset."
)
else:
gamma = dspark_gamma_from_num_draft_tokens(int(speculative_num_draft_tokens))
config_gamma = draft_config.resolve_gamma(default=None)
if config_gamma is not None and int(config_gamma) != gamma:
logger.warning(
"DSpark gamma mismatch: using gamma=%s (from "
"speculative_num_draft_tokens=%s) but draft config block_size=%s.",
gamma,
speculative_num_draft_tokens,
config_gamma,
)
if draft_config.mask_token_id is None:
raise ValueError(
"DSpark requires mask_token_id to be set in the draft model config."
)
mask_token_id = int(draft_config.mask_token_id)
if mask_token_id >= target_vocab_size:
raise ValueError(
f"DSpark mask_token_id={mask_token_id} is outside the target "
f"vocab size {target_vocab_size}."
)
return DSparkRuntimeConfig(
gamma=gamma,
verify_num_draft_tokens=gamma + 1,
mask_token_id=mask_token_id,
)
def read_draft_checkpoint_gamma(*, server_args: ServerArgs) -> Optional[int]:
"""Load the draft checkpoint's hf config and read its DSpark gamma
(block_size). Raises on config-load failure; callers pick the fallback."""
from sglang.srt.utils.hf_transformers_utils import get_config
draft_hf_config = get_config(
server_args.speculative_draft_model_path,
trust_remote_code=server_args.trust_remote_code,
revision=server_args.speculative_draft_model_revision,
model_override_args=json.loads(server_args.json_model_override_args),
)
return parse_dspark_draft_config(draft_hf_config=draft_hf_config).resolve_gamma(
default=None
)
def checkpoint_bundles_dspark_draft(hf_config: Any) -> bool:
"""The checkpoint carries a bundled DSpark draft head, marked by the
prefixed dspark_* keys on the target hf config. Single source of truth
for the bundling convention (draft-path defaulting, draft-arch remap)."""
return any(
_cfg_get(hf_config, key, None) is not None
for key in (
"dspark_block_size",
"dspark_markov_rank",
"dspark_noise_token_id",
"dspark_target_layer_ids",
)
)
def _cfg_get(config: Any, key: str, default: Any = None) -> Any:
if isinstance(config, dict):
return config.get(key, default)
return getattr(config, key, default)
def _get_text_config(config: Any) -> Any:
if config is None:
return None
if isinstance(config, dict):
return config.get("text_config", config)
text_config = getattr(config, "text_config", None)
if text_config is not None:
return text_config
return config
def _get_dspark_config(config: Any) -> dict:
cfg = _cfg_get(config, "dspark_config", None)
if cfg is None:
return {}
if isinstance(cfg, dict):
return cfg
try:
return dict(cfg)
except Exception:
return {}
def parse_dspark_draft_config(*, draft_hf_config: Any) -> DSparkDraftConfig:
base = parse_dflash_draft_config(draft_hf_config=draft_hf_config)
dspark_cfg = _get_dspark_config(draft_hf_config)
text_config = _get_text_config(draft_hf_config)
prefixed_block_size = _cfg_get(draft_hf_config, "dspark_block_size", None)
prefixed_markov_rank = _cfg_get(draft_hf_config, "dspark_markov_rank", None)
prefixed_markov_head_type = _cfg_get(
draft_hf_config, "dspark_markov_head_type", None
)
prefixed_noise_token_id = _cfg_get(draft_hf_config, "dspark_noise_token_id", None)
prefixed_target_layer_ids = _cfg_get(
draft_hf_config, "dspark_target_layer_ids", None
)
uses_prefixed = any(
value is not None
for value in (
prefixed_block_size,
prefixed_markov_rank,
prefixed_noise_token_id,
prefixed_target_layer_ids,
)
)
raw_markov_rank = (
prefixed_markov_rank
if prefixed_markov_rank is not None
else dspark_cfg.get(
"markov_rank",
_cfg_get(
text_config, "markov_rank", _cfg_get(draft_hf_config, "markov_rank", 0)
),
)
)
markov_rank = int(raw_markov_rank) if raw_markov_rank is not None else 0
if markov_rank < 0:
raise ValueError(f"DSpark markov_rank must be >= 0, got {markov_rank}.")
markov_head_type = (
prefixed_markov_head_type
if prefixed_markov_head_type is not None
else dspark_cfg.get(
"markov_head_type",
_cfg_get(
text_config,
"markov_head_type",
_cfg_get(draft_hf_config, "markov_head_type", None),
),
)
)
if markov_rank > 0 and markov_head_type is None and not uses_prefixed:
raise ValueError(
"DSpark requires markov_head_type when markov_rank > 0, got None."
)
if markov_head_type is not None:
markov_head_type = str(markov_head_type).lower()
if markov_head_type not in SUPPORTED_DSPARK_MARKOV_HEAD_TYPES:
raise ValueError(
f"Unsupported DSpark markov_head_type={markov_head_type!r}. "
f"Supported: {SUPPORTED_DSPARK_MARKOV_HEAD_TYPES}."
)
raw_mask_token_id = (
prefixed_noise_token_id
if prefixed_noise_token_id is not None
else dspark_cfg.get(
"mask_token_id",
_cfg_get(
text_config,
"mask_token_id",
_cfg_get(draft_hf_config, "mask_token_id", base.mask_token_id),
),
)
)
mask_token_id = int(raw_mask_token_id) if raw_mask_token_id is not None else None
if mask_token_id is not None and mask_token_id < 0:
raise ValueError(
f"DSpark mask_token_id must be non-negative, got {mask_token_id}."
)
gamma = (
int(prefixed_block_size) if prefixed_block_size is not None else base.block_size
)
if prefixed_target_layer_ids is not None:
if not isinstance(prefixed_target_layer_ids, (list, tuple)) or not len(
prefixed_target_layer_ids
):
raise ValueError(
"DSpark dspark_target_layer_ids must be a non-empty list of ints, "
f"got {prefixed_target_layer_ids!r}."
)
target_layer_ids: Optional[List[int]] = [
int(x) for x in prefixed_target_layer_ids
]
else:
target_layer_ids = base.target_layer_ids
return DSparkDraftConfig(
num_hidden_layers=base.num_hidden_layers,
num_target_layers=base.num_target_layers,
gamma=gamma,
target_layer_ids=target_layer_ids,
mask_token=base.mask_token,
mask_token_id=mask_token_id,
markov_rank=markov_rank,
markov_head_type=markov_head_type,
)
@@ -0,0 +1,421 @@
from __future__ import annotations
import logging
from contextlib import nullcontext
from typing import Optional
import msgspec
import torch
from sglang.srt.environ import envs
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.runtime_context import get_parallel
from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
from sglang.srt.speculative.draft_worker_common import make_draft_input_v2
from sglang.srt.speculative.dspark_components.dspark_planner import VerifyWindow
from sglang.srt.speculative.dspark_components.kernels.dspark_draft_model import (
SampleStepTokens,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import draft_tp_context
logger = logging.getLogger(__name__)
class DraftBlockResult(msgspec.Struct, frozen=True):
draft_tokens: torch.Tensor
corrected_logits: Optional[torch.Tensor]
greedy_mask: torch.Tensor
temperatures: torch.Tensor
class DraftForwardResult(msgspec.Struct, frozen=True):
draft_block_ids: torch.Tensor
raw_hidden: torch.Tensor
draft_hidden_3d: torch.Tensor
can_run_graph: bool
class DraftProposal(msgspec.Struct, frozen=True):
draft_block_ids: torch.Tensor
draft_block: DraftBlockResult
draft_hidden: Optional[torch.Tensor]
confidence: Optional[torch.Tensor] = None
confidence_tap: Optional[torch.Tensor] = None
folded: bool = False
def greedy_step_sampler(step_logits: torch.Tensor, step_idx: int) -> torch.Tensor:
del step_idx
return torch.argmax(step_logits, dim=-1)
class DsparkDraftSampler:
def __init__(self, *, model, gamma, max_bs, device, confidence_fn=None, out=None):
self.model = model
self.markov_head = model.markov_head
self.gamma = int(gamma)
if out is not None:
assert out.shape == (int(max_bs) * self.gamma,) and out.dtype == torch.int64
self.out = out
else:
self.out = torch.empty(
(int(max_bs) * self.gamma,), dtype=torch.int64, device=device
)
self.confidence_fn = confidence_fn
self.confidence_out = (
torch.empty((int(max_bs), self.gamma), dtype=torch.float32, device=device)
if confidence_fn is not None
else None
)
def __call__(self, hidden_states, input_ids):
bs = hidden_states.shape[0] // self.gamma
base_logits, confidence_tap = self.model.compute_base_logits(hidden_states)
base_logits = base_logits.view(bs, self.gamma, -1)
anchor = input_ids.view(bs, self.gamma)[:, 0]
draft_tokens, _ = self.markov_head.sample_block(
base_logits,
first_prev_tokens=anchor,
hidden_states=hidden_states.view(bs, self.gamma, -1),
sampler=greedy_step_sampler,
)
self.out[: draft_tokens.numel()].copy_(draft_tokens.reshape(-1))
if self.confidence_out is not None:
confidence = self.confidence_fn(
draft_hidden=hidden_states.view(bs, self.gamma, -1),
anchor_tokens=anchor,
draft_tokens=draft_tokens,
confidence_tap=confidence_tap,
)
self.confidence_out[:bs].copy_(confidence)
def maybe_build_draft_sampler(
*,
draft_model,
gamma: int,
max_bs: int,
device,
tp_rank: int,
confidence_fn=None,
out=None,
) -> Optional[DsparkDraftSampler]:
"""Build the graph-folded greedy draft sampler, or return None (with the
reason logged) when the draft model cannot support folding and the
proposal must stay eager."""
def _eager(reason):
if tp_rank == 0:
logger.info("DSpark draft greedy proposal kept eager (reason=%s).", reason)
return None
if gamma <= 0:
return _eager("gamma<=0")
if not hasattr(draft_model, "compute_base_logits"):
return _eager("no compute_base_logits")
if getattr(draft_model, "markov_head", None) is None:
return _eager("no markov head")
if tp_rank == 0:
logger.info("DSpark draft greedy proposal folded into the draft cuda graph.")
return DsparkDraftSampler(
model=draft_model,
gamma=gamma,
max_bs=max_bs,
device=device,
confidence_fn=confidence_fn,
out=out,
)
def make_next_draft_input(
*,
bonus_tokens: torch.Tensor,
new_seq_lens: torch.Tensor,
) -> DFlashDraftInputV2:
return make_draft_input_v2(bonus_tokens=bonus_tokens, new_seq_lens=new_seq_lens)
def resolve_greedy_mask(
*,
bs: int,
sampling_info,
device: torch.device,
) -> torch.Tensor:
if sampling_info is None:
return torch.ones(bs, dtype=torch.bool, device=device)
return (sampling_info.top_ks <= 1).view(-1)
def sample_draft_block(
*,
base_logits: torch.Tensor,
anchor_tokens: torch.Tensor,
draft_hidden: torch.Tensor,
sampling_info,
markov_head,
device: torch.device,
) -> DraftBlockResult:
bs = base_logits.shape[0]
greedy_mask = resolve_greedy_mask(bs=bs, sampling_info=sampling_info, device=device)
any_sampling = sampling_info is not None and not sampling_info.is_all_greedy
fast_sampling = envs.SGLANG_DSPARK_FAST_SAMPLING.get()
if sampling_info is None:
temperatures = torch.ones(bs, dtype=torch.float32, device=device)
else:
temperatures = (
sampling_info.temperatures.view(-1).to(torch.float32).clamp_min(1e-5)
)
if not any_sampling:
def sampler(step_logits: torch.Tensor, step_idx: int) -> torch.Tensor:
return torch.argmax(step_logits, dim=-1)
else:
def sampler(step_logits: torch.Tensor, step_idx: int) -> torch.Tensor:
if fast_sampling:
exp_noise = torch.empty(
step_logits.shape, dtype=torch.float32, device=step_logits.device
).exponential_(1)
return SampleStepTokens.execute(
step_logits=step_logits,
temperatures=temperatures,
greedy_mask=greedy_mask,
exp_noise=exp_noise,
)
else:
probs = torch.softmax(
step_logits.float() / temperatures[:, None], dim=-1
)
argmax_tokens = torch.argmax(step_logits, dim=-1)
sampled_tokens = torch.multinomial(probs, num_samples=1).squeeze(-1)
return torch.where(greedy_mask, argmax_tokens, sampled_tokens)
draft_tokens, corrected_logits = markov_head.sample_block(
base_logits,
first_prev_tokens=anchor_tokens,
hidden_states=draft_hidden,
sampler=sampler,
)
return DraftBlockResult(
draft_tokens=draft_tokens,
corrected_logits=corrected_logits,
greedy_mask=greedy_mask,
temperatures=temperatures,
)
class DraftBlockProposer:
def __init__(
self,
*,
draft_model,
draft_model_runner,
gamma: int,
mask_token_id: int,
draft_block_spec_info,
dp_moe_sync: bool = False,
) -> None:
self.draft_model = draft_model
self.draft_model_runner = draft_model_runner
self.gamma = gamma
self._mask_token_id = mask_token_id
self._draft_block_spec_info = draft_block_spec_info
self._draft_sampler = None
self._dp_moe_sync = dp_moe_sync
def attach_draft_sampler(self, draft_sampler) -> None:
self._draft_sampler = draft_sampler
def _base_logits_context(self):
if self._dp_moe_sync:
return draft_tp_context(get_parallel().attn_tp_group)
return nullcontext()
def propose(
self,
*,
batch: ScheduleBatch,
draft_input: DFlashDraftInputV2,
verify_window: VerifyWindow,
bs: int,
device: str,
target_model,
sampling_info,
) -> DraftProposal:
embed_module = target_model.get_input_embeddings()
fwd = self._run_forward(
batch=batch,
draft_input=draft_input,
verify_window=verify_window,
bs=bs,
device=device,
embed_module=embed_module,
)
draft_block_ids = fwd.draft_block_ids
draft_sampler = self._draft_sampler
all_greedy = sampling_info is None or sampling_info.is_all_greedy
folded_confidence = None
confidence_tap = None
folded = False
if draft_sampler is not None and fwd.can_run_graph and all_greedy:
folded = True
if sampling_info is None:
temperatures = torch.ones(bs, dtype=torch.float32, device=device)
else:
temperatures = (
sampling_info.temperatures.view(-1)
.to(torch.float32)
.clamp_min(1e-5)
)
draft_block = DraftBlockResult(
draft_tokens=draft_sampler.out[: bs * self.gamma].view(bs, self.gamma),
corrected_logits=None,
greedy_mask=resolve_greedy_mask(
bs=bs, sampling_info=sampling_info, device=device
),
temperatures=temperatures,
)
if draft_sampler.confidence_out is not None:
folded_confidence = draft_sampler.confidence_out[:bs]
else:
with self._base_logits_context():
base_logits, confidence_tap = self.draft_model.compute_base_logits(
fwd.raw_hidden
)
base_logits = base_logits.view(bs, self.gamma, -1)
draft_block = sample_draft_block(
base_logits=base_logits,
anchor_tokens=draft_block_ids[:, 0],
draft_hidden=fwd.draft_hidden_3d,
sampling_info=sampling_info,
markov_head=self.draft_model.markov_head,
device=device,
)
return DraftProposal(
draft_block_ids=draft_block_ids,
draft_block=draft_block,
draft_hidden=fwd.draft_hidden_3d,
confidence=folded_confidence,
confidence_tap=confidence_tap,
folded=folded,
)
def run_idle_participation(self, batch: ScheduleBatch) -> None:
if not self._dp_moe_sync or batch.global_num_tokens is None:
return
device = self.draft_model_runner.device
empty_long = torch.empty((0,), dtype=torch.int64, device=device)
idle_batch = ForwardBatch(
forward_mode=ForwardMode.IDLE,
batch_size=0,
input_ids=empty_long,
req_pool_indices=empty_long,
seq_lens=empty_long,
out_cache_loc=empty_long,
seq_lens_sum=0,
seq_lens_cpu=torch.empty((0,), dtype=torch.int64),
positions=empty_long,
spec_algorithm=SpeculativeAlgorithm.DSPARK,
spec_info=self._draft_block_spec_info,
capture_hidden_mode=CaptureHiddenMode.NULL,
)
self._fill_dp_moe_sync_metadata(idle_batch, batch)
with torch.inference_mode():
self.draft_model_runner.forward(idle_batch)
def _run_forward(
self,
*,
batch: ScheduleBatch,
draft_input: DFlashDraftInputV2,
verify_window: VerifyWindow,
bs: int,
device: str,
embed_module,
) -> DraftForwardResult:
gamma = self.gamma
prefix_lens = batch.seq_lens
positions_2d = verify_window.positions_2d
verify_cache_loc_2d = verify_window.verify_cache_loc_2d
draft_block_ids = torch.full(
(bs, gamma), int(self._mask_token_id), dtype=torch.long, device=device
)
draft_block_ids[:, 0].copy_(draft_input.bonus_tokens.view(-1))
draft_positions = positions_2d[:, :gamma].reshape(-1)
draft_cache_loc = verify_cache_loc_2d[:, :gamma].reshape(-1)
draft_owns_embed = hasattr(self.draft_model, "forward_embed")
draft_input_embeds: Optional[torch.Tensor] = None
if not draft_owns_embed:
noise_embedding = embed_module(draft_block_ids)
draft_input_embeds = noise_embedding.view(-1, noise_embedding.shape[-1])
if batch.seq_lens_cpu is not None:
draft_seq_lens_cpu = batch.seq_lens_cpu + gamma
draft_seq_lens_sum = int(draft_seq_lens_cpu.sum())
elif draft_input.reserved_seq_lens_cpu is not None:
draft_seq_lens_cpu = draft_input.reserved_seq_lens_cpu
draft_seq_lens_sum = int(draft_input.reserved_seq_lens_sum)
else:
raise RuntimeError("DSpark decode expected batch.seq_lens_cpu, got None")
draft_forward_batch = ForwardBatch(
forward_mode=ForwardMode.TARGET_VERIFY,
batch_size=bs,
input_ids=draft_block_ids.flatten(),
req_pool_indices=batch.req_pool_indices,
seq_lens=prefix_lens,
out_cache_loc=draft_cache_loc,
seq_lens_sum=draft_seq_lens_sum,
seq_lens_cpu=draft_seq_lens_cpu,
positions=draft_positions,
input_embeds=draft_input_embeds,
spec_algorithm=SpeculativeAlgorithm.DSPARK,
spec_info=self._draft_block_spec_info,
capture_hidden_mode=CaptureHiddenMode.NULL,
)
self._fill_dp_moe_sync_metadata(draft_forward_batch, batch)
with torch.inference_mode():
draft_out = self.draft_model_runner.forward(draft_forward_batch)
logits_output = draft_out.logits_output
raw_hidden = logits_output.hidden_states
if raw_hidden is None:
raise RuntimeError("DSpark draft model returned no hidden states.")
draft_hidden_3d = raw_hidden.view(bs, gamma, -1)
return DraftForwardResult(
draft_block_ids=draft_block_ids,
raw_hidden=raw_hidden,
draft_hidden_3d=draft_hidden_3d,
can_run_graph=draft_out.can_run_graph,
)
def _fill_dp_moe_sync_metadata(
self, forward_batch: ForwardBatch, batch: ScheduleBatch
) -> None:
if not self._dp_moe_sync or batch.global_num_tokens is None:
return
gnt, gnt_logprob = (
self._draft_block_spec_info.get_spec_adjusted_global_num_tokens(batch)
)
device = self.draft_model_runner.device
forward_batch.global_num_tokens_cpu = gnt
forward_batch.global_num_tokens_for_logprob_cpu = gnt_logprob
forward_batch.global_num_tokens_gpu = torch.tensor(gnt, dtype=torch.int64).to(
device, non_blocking=True
)
forward_batch.global_num_tokens_for_logprob_gpu = torch.tensor(
gnt_logprob, dtype=torch.int64
).to(device, non_blocking=True)
forward_batch.can_run_dp_cuda_graph = batch.can_run_dp_cuda_graph
@@ -0,0 +1,157 @@
from typing import Optional
import torch
from sglang.kernels.ops.speculative.cache_locs import assign_extend_cache_locs_func
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.speculative.dspark_components.kernels.dspark_verify_window import (
BuildCommitInjectLayout,
)
from sglang.srt.speculative.ragged_verify import RaggedVerifyLayout
class TargetHiddenKvInjector:
def __init__(
self,
*,
draft_model,
draft_model_runner,
model_runner,
device,
verify_num_draft_tokens: int,
block_pos_offsets: torch.Tensor,
) -> None:
self.draft_model = draft_model
self.draft_model_runner = draft_model_runner
self.model_runner = model_runner
self.device = device
self.verify_num_draft_tokens = verify_num_draft_tokens
self._block_pos_offsets = block_pos_offsets
def inject_target_hidden(
self,
*,
target_hidden: torch.Tensor,
cache_loc: torch.Tensor,
positions: torch.Tensor,
cache_loc_2d: Optional[torch.Tensor] = None,
commit_lens: Optional[torch.Tensor] = None,
) -> None:
if target_hidden is None or target_hidden.numel() == 0:
return
device = self.model_runner.device
cache_loc = cache_loc.to(device=device, dtype=torch.int64, non_blocking=True)
positions = positions.to(device=device, dtype=torch.int64, non_blocking=True)
target_hidden = target_hidden.to(device=device, non_blocking=True)
n_real = positions.shape[0]
if target_hidden.shape[0] > n_real:
target_hidden = target_hidden[:n_real]
if cache_loc_2d is not None:
cache_loc_2d = cache_loc_2d.to(
device=device, dtype=torch.int64, non_blocking=True
)
if commit_lens is not None:
commit_lens = commit_lens.to(
device=device, dtype=torch.int32, non_blocking=True
)
pool = self.draft_model_runner.token_to_kv_pool
if hasattr(pool, "set_swa_key_buffer_radix_fused_norm_rope"):
self._inject_mla(
pool=pool,
target_hidden=target_hidden,
cache_loc=cache_loc,
positions=positions,
cache_loc_2d=cache_loc_2d,
commit_lens=commit_lens,
)
return
with torch.inference_mode():
self.draft_model.write_target_hidden_kv(
target_hidden=target_hidden,
pool=pool,
positions=positions,
cache_loc=cache_loc,
cache_loc_2d=cache_loc_2d,
commit_lens=commit_lens,
)
def _inject_mla(
self,
*,
pool,
target_hidden: torch.Tensor,
cache_loc: torch.Tensor,
positions: torch.Tensor,
cache_loc_2d: Optional[torch.Tensor],
commit_lens: Optional[torch.Tensor],
) -> None:
swa_loc = pool.translate_loc_from_full_to_swa(cache_loc).to(torch.int32)
if commit_lens is not None and cache_loc_2d is not None:
bs, verify_len = cache_loc_2d.shape
col = torch.arange(verify_len, device=cache_loc.device).view(1, -1)
committed_mask = (col < commit_lens.to(torch.long).view(-1, 1)).reshape(-1)
swa_loc = torch.where(committed_mask, swa_loc, torch.full_like(swa_loc, -1))
with torch.inference_mode():
self.draft_model.write_target_hidden_kv(
main_hidden=target_hidden,
swa_loc=swa_loc,
positions=positions,
pool=pool,
)
def inject_ragged(
self,
*,
batch: ScheduleBatch,
layout: RaggedVerifyLayout,
hidden_strided: torch.Tensor,
commit_lens: torch.Tensor,
bs: int,
) -> None:
stride = self.verify_num_draft_tokens
prefix_lens = batch.seq_lens
hidden = hidden_strided.view(bs, stride, -1)
pool = self.draft_model_runner.token_to_kv_pool
if hasattr(pool, "set_swa_key_buffer_radix_fused_norm_rope"):
if hidden_strided.numel() == 0:
return
inject_layout = BuildCommitInjectLayout.execute(
req_pool_indices=batch.req_pool_indices,
req_to_token=self.model_runner.req_to_token_pool.req_to_token,
prefix_lens=prefix_lens,
block_pos_offsets=self._block_pos_offsets[:stride],
full_to_swa_mapping=pool.full_to_swa_index_mapping,
commit_lens=commit_lens,
stride=stride,
)
with torch.inference_mode():
self.draft_model.write_target_hidden_kv(
main_hidden=hidden.reshape(-1, hidden.shape[-1]),
swa_loc=inject_layout.swa_loc,
positions=inject_layout.positions,
pool=pool,
)
return
positions_2d = prefix_lens.unsqueeze(1) + self._block_pos_offsets
verify_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=self.model_runner.req_to_token_pool.req_to_token,
start_offset=prefix_lens,
end_offset=prefix_lens + stride,
batch_size=bs,
draft_token_num=stride,
device=self.device,
)
verify_cache_loc_2d = verify_cache_loc.view(bs, stride)
self.inject_target_hidden(
target_hidden=hidden.reshape(-1, hidden.shape[-1]),
cache_loc=verify_cache_loc,
cache_loc_2d=verify_cache_loc_2d,
positions=positions_2d.reshape(-1),
commit_lens=commit_lens,
)
@@ -0,0 +1,961 @@
from __future__ import annotations
import logging
import math
import statistics
import time
from collections import deque
from contextlib import contextmanager, nullcontext
from enum import Enum
from typing import Callable, ContextManager, Iterator, Optional, Union
import msgspec
import torch
from sglang.srt.environ import envs
from sglang.srt.kv_canary.runner.future_tensor import FutureTensors
from sglang.srt.runtime_context import get_parallel
from sglang.srt.sampling.sampling_params import TOP_K_ALL
from sglang.srt.speculative.dflash_utils import compute_dflash_correct_drafts_and_bonus
from sglang.srt.speculative.dspark_components.dspark_block_accept_estimator import (
create_block_accept_estimate_recorder,
)
from sglang.srt.speculative.dspark_components.dspark_sts import StsDataRecorder
from sglang.srt.speculative.dspark_components.dspark_verify import (
verify_logits_adjustments_are_noop,
)
logger = logging.getLogger(__name__)
_NULL_SEGMENT = nullcontext()
ALL_COMPONENTS_TOKEN = "all"
class InfoComponent(str, Enum):
CORE = "core"
STEP_CPU_TIME = "step_cpu_time"
STEP_GPU_TIME = "step_gpu_time"
DRAFT_GPU_TIME = "draft_gpu_time"
TARGET_VERIFY_GPU_TIME = "target_verify_gpu_time"
REQS = "reqs"
class InfoSegment(str, Enum):
STEP = "step"
DRAFT = "draft"
TARGET_VERIFY = "target_verify"
INFO_DUMP_MAX_RECORDS = 200_000
INFO_DUMP_MAX_STEP_CPU_SECONDS = 1.0
def resolve_enabled_components() -> set[InfoComponent]:
"""Components enabled via env: SGLANG_DSPARK_DEBUG_DUMP tokens, plus the
published SPS-profiling switch SGLANG_DSPARK_ENABLE_SPS_RECORD=1, which is
an alias for the core,step_cpu_time components the SPS table fit needs."""
components = resolve_components(envs.SGLANG_DSPARK_DEBUG_DUMP.get())
if envs.SGLANG_DSPARK_ENABLE_SPS_RECORD.get():
components |= {InfoComponent.CORE, InfoComponent.STEP_CPU_TIME}
return components
def resolve_components(raw: tuple[str, ...]) -> set[InfoComponent]:
tokens = {token.strip() for token in raw if token.strip()}
if not tokens:
return set()
if ALL_COMPONENTS_TOKEN in tokens:
return set(InfoComponent)
try:
return {InfoComponent(token) for token in tokens}
except ValueError as exc:
valid = [component.value for component in InfoComponent]
raise ValueError(
f"Invalid SGLANG_DSPARK_DEBUG_DUMP token in {sorted(tokens)}; "
f"valid: {valid} or '{ALL_COMPONENTS_TOKEN}'."
) from exc
class ReqDetail(msgspec.Struct, omit_defaults=True):
req_pool_index: int
prefix_len: int
verify_len: int
acc_len: int
correct_drafts: int
cap_trim: int
bonus_token: int
draft_tokens: list[int]
rid: Optional[str] = None
confidence: Optional[list[float]] = None
survival: Optional[list[float]] = None
class DecodeStepRecord(msgspec.Struct, omit_defaults=True):
forward_ct: int
bs: int = -1
mode: str = ""
budget: Optional[int] = None
lag_steps: Optional[int] = None
num_running_reqs: int = -1
num_verify_tokens: int = -1
verify_tokens_local: int = -1
verify_tokens_dp_synced: int = -1
verify_tokens_graph_key: int = -1
predicted_step_ms: Optional[float] = None
predicted_theta: Optional[float] = None
step_cpu_ms: Optional[float] = None
step_gpu_ms: Optional[float] = None
draft_gpu_ms: Optional[float] = None
target_verify_gpu_ms: Optional[float] = None
reqs: Optional[list[ReqDetail]] = None
class DecodeStepObservation(msgspec.Struct):
forward_ct: int
bs: int
mode: str
budget: Optional[int]
lag_steps: Optional[int]
num_verify_tokens: int
verify_tokens_local: int
verify_tokens_dp_synced: int
verify_tokens_graph_key: int
predicted_step_ms: Optional[float]
predicted_theta: Optional[float]
verify_lens: Optional[torch.Tensor]
confidence: Optional[torch.Tensor]
req_pool_indices: torch.Tensor
prefix_lens: torch.Tensor
draft_tokens: torch.Tensor
bonus_tokens: torch.Tensor
correct_len: torch.Tensor
cap_trim_lens: torch.Tensor
commit_lens: torch.Tensor
rids: Optional[list[str]]
class _PendingStep(msgspec.Struct):
forward_ct: int
bs: int
mode: str
budget: Optional[int]
lag_steps: Optional[int]
num_verify_tokens: int
verify_tokens_local: int
verify_tokens_dp_synced: int
verify_tokens_graph_key: int
predicted_step_ms: Optional[float]
predicted_theta: Optional[float]
step_cpu_ms: Optional[float]
rids: Optional[list[str]]
future: Optional[FutureTensors]
segment_events: dict[InfoSegment, tuple[torch.cuda.Event, torch.cuda.Event]]
class DsparkInfoDumper:
def __init__(
self,
*,
components: set[Union[InfoComponent, str]],
gamma: int,
verify_num_draft_tokens: int,
attn_tp_rank: int,
device: torch.device,
mode_value: str,
sps_report_interval: int = 0,
max_records: int = INFO_DUMP_MAX_RECORDS,
max_step_cpu_seconds: float = INFO_DUMP_MAX_STEP_CPU_SECONDS,
clock: Callable[[], float] = time.monotonic,
) -> None:
self.gamma = int(gamma)
self.verify_num_draft_tokens = int(verify_num_draft_tokens)
self.attn_tp_rank = int(attn_tp_rank)
self.device = device
self.mode_value = mode_value
self._clock = clock
self._max_step_cpu_seconds = max_step_cpu_seconds
self._components: set[InfoComponent] = {
InfoComponent(component) for component in components
}
self._sps_report_interval = int(sps_report_interval)
if self._sps_report_interval > 0:
self._components.add(InfoComponent.STEP_GPU_TIME)
# Dedup within an attention-TP group only: records describe the
# DP-rank-local batch, so under dp-attention every DP rank must keep
# dumping (the SPS profiler reads one payload per DP rank).
self.enabled = bool(self._components) and self.attn_tp_rank == 0
self._sps_window: list[tuple[float, float]] = []
self._sps_mismatched = 0
self._records: deque[DecodeStepRecord] = deque(maxlen=max_records)
self._pending: Optional[_PendingStep] = None
self._prev_stamp: Optional[float] = None
self._d2h_stream: Optional[torch.cuda.Stream] = None
if self.enabled and InfoComponent.REQS in self._components:
self._d2h_stream = torch.cuda.Stream(device=device)
self._current_segments: dict[
InfoSegment, tuple[torch.cuda.Event, torch.cuda.Event]
] = {}
self._open_segments: dict[InfoSegment, torch.cuda.Event] = {}
def begin_step(self) -> None:
if not self.enabled:
return
self._current_segments = {}
self._open_segments = {}
if InfoComponent.STEP_GPU_TIME in self._components:
self._open_segment(InfoSegment.STEP)
def segment(self, name: Union[InfoSegment, str]) -> ContextManager[None]:
if not self.enabled:
return _NULL_SEGMENT
segment = InfoSegment(name)
if not self._segment_enabled(segment):
return _NULL_SEGMENT
return self._active_segment(segment)
@contextmanager
def _active_segment(self, segment: InfoSegment) -> Iterator[None]:
self._open_segment(segment)
try:
yield
finally:
self._close_segment(segment)
def observe_decode_step(self, obs: DecodeStepObservation) -> None:
if not self.enabled:
return
if InfoComponent.STEP_GPU_TIME in self._components:
self._close_segment(InfoSegment.STEP)
now = self._clock()
step_cpu_ms = self._step_cpu_ms(now=now)
self._drain_pending()
future = (
self._stage_reqs(obs) if InfoComponent.REQS in self._components else None
)
self._pending = _PendingStep(
forward_ct=int(obs.forward_ct),
bs=int(obs.bs),
mode=obs.mode,
budget=None if obs.budget is None else int(obs.budget),
lag_steps=None if obs.lag_steps is None else int(obs.lag_steps),
num_verify_tokens=int(obs.num_verify_tokens),
verify_tokens_local=int(obs.verify_tokens_local),
verify_tokens_dp_synced=int(obs.verify_tokens_dp_synced),
verify_tokens_graph_key=int(obs.verify_tokens_graph_key),
predicted_step_ms=obs.predicted_step_ms,
predicted_theta=obs.predicted_theta,
step_cpu_ms=step_cpu_ms,
rids=obs.rids,
future=future,
segment_events=self._current_segments,
)
self._current_segments = {}
self._prev_stamp = now
def note_non_decode_step(self) -> None:
if not self.enabled:
return
self._drain_pending()
self._prev_stamp = None
self._current_segments = {}
self._open_segments = {}
def flush(self) -> None:
if not self.enabled:
return
self._drain_pending()
def clear(self) -> None:
self._records.clear()
self._pending = None
self._prev_stamp = None
self._current_segments = {}
self._open_segments = {}
self._sps_window = []
self._sps_mismatched = 0
def dump(self) -> Optional[dict]:
if not self.enabled:
return None
self.flush()
return {
"mode": self.mode_value,
"gamma": self.gamma,
"verify_num_draft_tokens": self.verify_num_draft_tokens,
"components": sorted(component.value for component in self._components),
"records": [msgspec.to_builtins(record) for record in self._records],
}
def _segment_enabled(self, segment: InfoSegment) -> bool:
if segment is InfoSegment.STEP:
return InfoComponent.STEP_GPU_TIME in self._components
if segment is InfoSegment.DRAFT:
return InfoComponent.DRAFT_GPU_TIME in self._components
if segment is InfoSegment.TARGET_VERIFY:
return InfoComponent.TARGET_VERIFY_GPU_TIME in self._components
return False
def _open_segment(self, segment: InfoSegment) -> None:
start = torch.cuda.Event(enable_timing=True)
start.record()
self._open_segments[segment] = start
def _close_segment(self, segment: InfoSegment) -> None:
start = self._open_segments.pop(segment, None)
if start is None:
return
end = torch.cuda.Event(enable_timing=True)
end.record()
self._current_segments[segment] = (start, end)
def _stage_reqs(self, obs: DecodeStepObservation) -> Optional[FutureTensors]:
tensors: dict[str, torch.Tensor] = {
"req_pool_indices": obs.req_pool_indices,
"prefix_lens": obs.prefix_lens,
"draft_tokens": obs.draft_tokens,
"bonus_tokens": obs.bonus_tokens,
"correct_len": obs.correct_len,
"cap_trim_lens": obs.cap_trim_lens,
"commit_lens": obs.commit_lens,
}
if obs.verify_lens is not None:
tensors["verify_lens"] = obs.verify_lens
if obs.confidence is not None:
tensors["confidence"] = obs.confidence
return FutureTensors.device_to_host(tensors, d2h_stream=self._d2h_stream)
def _drain_pending(self) -> None:
pending = self._pending
self._pending = None
if pending is None:
return
record = DecodeStepRecord(forward_ct=pending.forward_ct)
if InfoComponent.CORE in self._components:
record.bs = pending.bs
record.mode = pending.mode
record.budget = pending.budget
record.lag_steps = pending.lag_steps
record.num_running_reqs = pending.bs
record.num_verify_tokens = pending.num_verify_tokens
record.verify_tokens_local = pending.verify_tokens_local
record.verify_tokens_dp_synced = pending.verify_tokens_dp_synced
record.verify_tokens_graph_key = pending.verify_tokens_graph_key
record.predicted_step_ms = pending.predicted_step_ms
record.predicted_theta = pending.predicted_theta
if InfoComponent.STEP_CPU_TIME in self._components:
record.step_cpu_ms = pending.step_cpu_ms
if InfoComponent.STEP_GPU_TIME in self._components:
record.step_gpu_ms = self._segment_ms(pending, InfoSegment.STEP)
if InfoComponent.DRAFT_GPU_TIME in self._components:
record.draft_gpu_ms = self._segment_ms(pending, InfoSegment.DRAFT)
if InfoComponent.TARGET_VERIFY_GPU_TIME in self._components:
record.target_verify_gpu_ms = self._segment_ms(
pending, InfoSegment.TARGET_VERIFY
)
if InfoComponent.REQS in self._components and pending.future is not None:
record.reqs = self._build_reqs(
host=pending.future.wait(), bs=pending.bs, rids=pending.rids
)
elif pending.future is not None:
pending.future.wait()
self._records.append(record)
if self._sps_report_interval > 0:
self._report_sps_prediction(pending=pending, step_gpu_ms=record.step_gpu_ms)
def _report_sps_prediction(
self, *, pending: _PendingStep, step_gpu_ms: Optional[float]
) -> None:
predicted = pending.predicted_step_ms
if predicted is None or step_gpu_ms is None:
return
matched = (
pending.budget is not None
and pending.bs + pending.budget == pending.num_verify_tokens
)
if not matched:
self._sps_mismatched += 1
return
self._sps_window.append((predicted, step_gpu_ms))
if len(self._sps_window) < self._sps_report_interval:
return
predictions = [p for p, _ in self._sps_window]
actuals = [a for _, a in self._sps_window]
abs_err = [abs(p - a) for p, a in self._sps_window]
rel_err = [abs(p - a) / a * 100 for p, a in self._sps_window if a > 0]
total = len(self._sps_window) + self._sps_mismatched
logger.info(
"DSpark SPS prediction: n=%d mean predicted=%.3fms mean actual=%.3fms "
"MAE=%.3fms median rel-err=%.1f%% mean bias(pred-actual)=%+.3fms "
"M_mismatch_rate=%.1f%% (%d/%d)",
len(self._sps_window),
statistics.fmean(predictions),
statistics.fmean(actuals),
statistics.fmean(abs_err),
statistics.median(rel_err) if rel_err else float("nan"),
statistics.fmean([p - a for p, a in self._sps_window]),
self._sps_mismatched / total * 100 if total else 0.0,
self._sps_mismatched,
total,
)
self._sps_window = []
self._sps_mismatched = 0
def _step_cpu_ms(self, *, now: float) -> Optional[float]:
prev = self._prev_stamp
if prev is None:
return None
step_cpu = now - prev
if not (0.0 < step_cpu <= self._max_step_cpu_seconds):
return None
return round(step_cpu * 1000.0, 4)
def _segment_ms(
self, pending: _PendingStep, segment: InfoSegment
) -> Optional[float]:
events = pending.segment_events.get(segment)
if events is None:
return None
start, end = events
end.synchronize()
elapsed_ms = start.elapsed_time(end)
if elapsed_ms > self._max_step_cpu_seconds * 1000.0:
return None
return round(elapsed_ms, 4)
def _build_reqs(
self, *, host: dict, bs: int, rids: Optional[list[str]]
) -> list[ReqDetail]:
req_ids = host["req_pool_indices"].tolist()
prefixes = host["prefix_lens"].tolist()
draft_rows = host["draft_tokens"].tolist()
bonus = host["bonus_tokens"].tolist()
correct = host["correct_len"].tolist()
cap_trim = host["cap_trim_lens"].tolist()
commit = host["commit_lens"].tolist()
verify_lens = host["verify_lens"].tolist() if "verify_lens" in host else None
if "confidence" in host:
conf_host = host["confidence"].float()
conf_rows = conf_host.tolist()
survival_rows = torch.cumprod(conf_host, dim=1).tolist()
else:
conf_rows = None
survival_rows = None
reqs: list[ReqDetail] = []
for row in range(bs):
verify_len = (
self.verify_num_draft_tokens
if verify_lens is None
else int(verify_lens[row])
)
reqs.append(
ReqDetail(
rid=None if rids is None else rids[row],
req_pool_index=int(req_ids[row]),
prefix_len=int(prefixes[row]),
verify_len=verify_len,
acc_len=int(commit[row]),
correct_drafts=int(correct[row]),
cap_trim=int(cap_trim[row]),
bonus_token=int(bonus[row]),
draft_tokens=[int(t) for t in draft_rows[row]],
confidence=(
None
if conf_rows is None
else [round(float(p), 4) for p in conf_rows[row]]
),
survival=(
None
if survival_rows is None
else [round(float(p), 4) for p in survival_rows[row]]
),
)
)
return reqs
EPS_PROB = 1e-8
def _format_float(value: float, digits: int = 4) -> str:
value = float(value)
if math.isnan(value):
return "nan"
return f"{value:.{digits}f}"
class PerPositionConfidenceMetrics:
def __init__(
self,
*,
gamma: int,
device: torch.device,
num_coarse_bins: int = 15,
num_fine_bins: int = 1024,
) -> None:
self.gamma = int(gamma)
self.num_coarse_bins = int(num_coarse_bins)
self.num_fine_bins = int(num_fine_bins)
self.coarse_count = torch.zeros(
(self.gamma, self.num_coarse_bins), dtype=torch.float64, device=device
)
self.coarse_pred = torch.zeros_like(self.coarse_count)
self.coarse_target = torch.zeros_like(self.coarse_count)
self.fine_pos = torch.zeros(
(self.gamma, self.num_fine_bins), dtype=torch.float64, device=device
)
self.fine_neg = torch.zeros_like(self.fine_pos)
self.brier_num = torch.zeros(self.gamma, dtype=torch.float64, device=device)
def update(self, *, survival: torch.Tensor, prefix_mask: torch.Tensor) -> None:
assert survival.shape == prefix_mask.shape
assert survival.dim() == 2 and survival.shape[1] == self.gamma
probs = survival.to(torch.float64).clamp(EPS_PROB, 1.0 - EPS_PROB)
targets = prefix_mask.to(torch.float64)
bs = probs.shape[0]
probs_flat = probs.reshape(-1)
targets_flat = targets.reshape(-1)
weights = torch.ones_like(probs_flat)
pos_idx = (
torch.arange(self.gamma, device=probs.device)
.view(1, -1)
.expand(bs, self.gamma)
.reshape(-1)
)
coarse_idx = (
(probs_flat * self.num_coarse_bins)
.long()
.clamp_(0, self.num_coarse_bins - 1)
)
flat_coarse = pos_idx * self.num_coarse_bins + coarse_idx
self.coarse_count.view(-1).scatter_add_(0, flat_coarse, weights)
self.coarse_pred.view(-1).scatter_add_(0, flat_coarse, probs_flat)
self.coarse_target.view(-1).scatter_add_(0, flat_coarse, targets_flat)
fine_idx = (
(probs_flat * self.num_fine_bins).long().clamp_(0, self.num_fine_bins - 1)
)
flat_fine = pos_idx * self.num_fine_bins + fine_idx
self.fine_pos.view(-1).scatter_add_(0, flat_fine, targets_flat)
self.fine_neg.view(-1).scatter_add_(0, flat_fine, 1.0 - targets_flat)
self.brier_num.add_((probs - targets).pow(2).sum(dim=0))
@staticmethod
def _auroc_from_hist(pos_hist: torch.Tensor, neg_hist: torch.Tensor) -> float:
total_pos = float(pos_hist.sum())
total_neg = float(neg_hist.sum())
if total_pos <= 0.0 or total_neg <= 0.0:
return float("nan")
cum_neg = torch.cumsum(neg_hist, dim=0)
cum_neg_before = cum_neg - neg_hist
pair = (pos_hist * cum_neg_before).sum() + 0.5 * (pos_hist * neg_hist).sum()
return float(pair) / (total_pos * total_neg)
def compute(self) -> list[dict]:
coarse_count = self.coarse_count.cpu()
coarse_pred = self.coarse_pred.cpu()
coarse_target = self.coarse_target.cpu()
fine_pos = self.fine_pos.cpu()
fine_neg = self.fine_neg.cpu()
brier_num = self.brier_num.cpu()
out: list[dict] = []
for pos in range(self.gamma):
weights = coarse_count[pos]
total = float(weights.sum())
if total <= 1e-12:
out.append(
{
"position": pos,
"total_weight": 0.0,
"ece": float("nan"),
"auc": float("nan"),
"brier": float("nan"),
"pred_mean": float("nan"),
"target_mean": float("nan"),
"reliability": [],
}
)
continue
denom = weights.clamp_min(1e-12)
avg_pred = coarse_pred[pos] / denom
avg_target = coarse_target[pos] / denom
bin_err = (avg_pred - avg_target).abs()
ece = float((bin_err * weights).sum()) / total
auc = self._auroc_from_hist(fine_pos[pos], fine_neg[pos])
brier = float(brier_num[pos]) / total
reliability = []
for bin_idx in range(self.num_coarse_bins):
weight = float(weights[bin_idx])
if weight <= 0.0:
continue
reliability.append(
{
"bin": bin_idx,
"range": [
bin_idx / self.num_coarse_bins,
(bin_idx + 1) / self.num_coarse_bins,
],
"avg_pred": float(avg_pred[bin_idx]),
"avg_target": float(avg_target[bin_idx]),
"weight": weight,
}
)
out.append(
{
"position": pos,
"total_weight": total,
"ece": ece,
"auc": auc,
"brier": brier,
"pred_mean": float(coarse_pred[pos].sum()) / total,
"target_mean": float(coarse_target[pos].sum()) / total,
"reliability": reliability,
}
)
return out
def format_table(self) -> str:
rows = self.compute()
header = (
f"{'pos':>3} {'count':>12} {'pred':>8} {'target':>8} "
f"{'ece':>8} {'auc':>8} {'brier':>8}"
)
lines = [
"DSpark confidence-head per-position calibration "
"(cumprod survival vs leading-correct-prefix)",
header,
]
for row in rows:
lines.append(
f"{row['position']:>3} {row['total_weight']:>12.0f} "
f"{_format_float(row['pred_mean']):>8} "
f"{_format_float(row['target_mean']):>8} "
f"{_format_float(row['ece']):>8} "
f"{_format_float(row['auc']):>8} "
f"{_format_float(row['brier']):>8}"
)
return "\n".join(lines)
class ConfidenceMetricsProbe:
def __init__(
self,
*,
gamma: int,
verify_num_draft_tokens: int,
tp_rank: int,
print_every: int = 256,
) -> None:
self.gamma = int(gamma)
self.verify_num_draft_tokens = int(verify_num_draft_tokens)
self.tp_rank = int(tp_rank)
self.print_every = int(print_every)
self._metrics: Optional[PerPositionConfidenceMetrics] = None
self._step_ct: int = 0
self._compact_warned: bool = False
def maybe_observe(
self,
*,
carries_confidence: bool,
is_compact_mode: bool,
confidence_raw: Optional[torch.Tensor],
verify_ids_2d: torch.Tensor,
target_logits: torch.Tensor,
bs: int,
) -> None:
if not envs.SGLANG_DSPARK_DEBUG_CONFIDENCE_METRICS.get():
return
if self.tp_rank != 0:
return
if not carries_confidence:
return
if is_compact_mode:
if not self._compact_warned:
logger.warning(
"SGLANG_DSPARK_DEBUG_CONFIDENCE_METRICS is ignored under "
"SGLANG_RAGGED_VERIFY_MODE=compact (padded verify rows corrupt the "
"per-position prefix label); run cap-accept or static to measure it."
)
self._compact_warned = True
return
if confidence_raw is None:
return
target_predict = torch.argmax(target_logits, dim=-1).view(
bs, self.verify_num_draft_tokens
)
num_correct_drafts, _ = compute_dflash_correct_drafts_and_bonus(
candidates=verify_ids_2d,
target_predict=target_predict,
)
positions = torch.arange(self.gamma, device=confidence_raw.device).view(1, -1)
prefix_mask = (positions < num_correct_drafts.view(-1, 1)).to(torch.float32)
survival = torch.cumprod(torch.sigmoid(confidence_raw.float()), dim=1)
if self._metrics is None:
self._metrics = PerPositionConfidenceMetrics(
gamma=self.gamma, device=confidence_raw.device
)
self._metrics.update(survival=survival, prefix_mask=prefix_mask)
self._step_ct += 1
if self._step_ct % self.print_every == 0:
logger.info("%s", self._metrics.format_table())
_STS_COLLECT_FLUSH_EVERY: int = 256
class DsparkStepObservers:
"""Facade over the per-step observability sinks (info dumper, confidence
probe, STS collection, block-accept estimator). The worker's decode path
makes one call per step; all sink gating and field derivation live here
so the hot path stays free of observer plumbing."""
def __init__(
self,
*,
planner,
gamma: int,
verify_num_draft_tokens: int,
tp_rank: int,
device,
simulate_acc_len: float,
) -> None:
self._planner = planner
self._gamma = int(gamma)
self._verify_num_draft_tokens = int(verify_num_draft_tokens)
self._simulate_acc_len = float(simulate_acc_len)
self._confidence_probe = ConfidenceMetricsProbe(
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
tp_rank=tp_rank,
)
self._info_dumper = DsparkInfoDumper(
components=resolve_enabled_components(),
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
attn_tp_rank=get_parallel().attn_tp_rank,
device=device,
mode_value=planner.mode_value,
sps_report_interval=envs.SGLANG_DSPARK_LOG_SPS_PRED_INTERVAL.get(),
)
self._block_accept_recorder = create_block_accept_estimate_recorder(
gamma=gamma, device=device, tp_rank=tp_rank
)
if self._simulate_acc_len > 0 and self._block_accept_recorder is not None:
raise ValueError(
"SGLANG_DSPARK_BLOCK_ACCEPT_ESTIMATE_PATH cannot be combined with "
"SGLANG_SIMULATE_ACC_LEN (simulated correct_len breaks the "
"accept-probability bookkeeping of the estimator)."
)
self._sts_collect_path = envs.SGLANG_DSPARK_STS_COLLECT_PATH.get()
self._sts_recorder: Optional[StsDataRecorder] = None
# --- step lifecycle -------------------------------------------------
def begin_step(self) -> None:
self._info_dumper.begin_step()
def segment(self, name: Union[InfoSegment, str]) -> ContextManager[None]:
return self._info_dumper.segment(name)
def note_prefill_step(self) -> None:
self._info_dumper.note_non_decode_step()
if self._block_accept_recorder is not None:
self._block_accept_recorder.flush()
def note_idle_decode_step(self) -> None:
self._info_dumper.note_non_decode_step()
# --- scheduler-facing hooks ------------------------------------------
def dump_info_records(self) -> Optional[dict]:
dumped = self._info_dumper.dump()
if dumped is None:
return None
dumped["simulate_acc_len"] = (
self._simulate_acc_len if self._simulate_acc_len > 0 else None
)
return dumped
def clear_info_records(self) -> None:
self._info_dumper.clear()
def block_accept_estimate_log_suffix(self) -> Optional[str]:
if self._block_accept_recorder is None:
return None
return self._block_accept_recorder.estimate_log_suffix()
def note_request_finished(self, *, rid: str, natural_stop: bool) -> None:
if self._block_accept_recorder is None:
return
self._block_accept_recorder.note_request_finished(
rid=rid, natural_stop=natural_stop
)
# --- per-step observation --------------------------------------------
def observe_verify_step(
self,
*,
forward_ct: int,
reqs,
bs: int,
proposal_folded: bool,
verify_ids_2d: torch.Tensor,
target_logits: Optional[torch.Tensor],
layout,
confidence: Optional[torch.Tensor],
prefix_lens: torch.Tensor,
draft_tokens: torch.Tensor,
draft_block,
sampling_info,
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
bonus: torch.Tensor,
commit_lens: torch.Tensor,
verify_token_budget: Optional[int],
req_pool_indices: torch.Tensor,
verify_tier_num_tokens: int,
dp_tier_num_tokens: Optional[int],
) -> None:
planner = self._planner
if not proposal_folded:
self._maybe_record_sts_collect(
verify_ids_2d=verify_ids_2d,
target_logits=target_logits,
bs=bs,
)
self._confidence_probe.maybe_observe(
carries_confidence=planner.carries_confidence,
is_compact_mode=planner.is_compact_mode,
confidence_raw=planner.last_confidence_raw,
verify_ids_2d=verify_ids_2d,
target_logits=target_logits,
bs=bs,
)
if self._block_accept_recorder is not None and not proposal_folded:
self._block_accept_recorder.observe_verify_step(
forward_ct=forward_ct,
rids=[req.rid for req in reqs],
draft_tokens=draft_tokens,
corrected_logits=draft_block.corrected_logits,
draft_temperatures=draft_block.temperatures,
greedy_mask=draft_block.greedy_mask,
target_logits=target_logits,
target_temperatures=(
sampling_info.temperatures
if sampling_info is not None
else draft_block.temperatures
),
truncated_sampling_mask=(
(sampling_info.top_ks != TOP_K_ALL)
| (sampling_info.top_ps != 1.0)
| (sampling_info.min_ps > 0)
if sampling_info is not None
else None
),
logits_adjustments_are_noop=verify_logits_adjustments_are_noop(
sampling_info
),
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
bonus=bonus,
prefix_lens=prefix_lens,
layout=layout,
)
if self._info_dumper.enabled:
budget_decision = planner.take_budget_decision()
predicted_step_ms = (
None
if budget_decision is None
or budget_decision.predicted_step_seconds is None
else budget_decision.predicted_step_seconds * 1e3
)
predicted_theta = (
None if budget_decision is None else budget_decision.predicted_theta
)
num_verify_tokens = (
layout.graph_num_tokens
if layout is not None
else int(verify_ids_2d.numel())
)
self._info_dumper.observe_decode_step(
DecodeStepObservation(
forward_ct=forward_ct,
bs=bs,
mode=planner.mode_value,
budget=verify_token_budget,
lag_steps=planner.lag_steps,
num_verify_tokens=num_verify_tokens,
verify_tokens_local=verify_tier_num_tokens,
verify_tokens_dp_synced=(
-1 if dp_tier_num_tokens is None else int(dp_tier_num_tokens)
),
verify_tokens_graph_key=num_verify_tokens,
predicted_step_ms=predicted_step_ms,
predicted_theta=predicted_theta,
verify_lens=layout.verify_lens if layout is not None else None,
confidence=confidence,
req_pool_indices=req_pool_indices,
prefix_lens=prefix_lens,
draft_tokens=draft_tokens,
bonus_tokens=bonus,
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
commit_lens=commit_lens,
rids=[req.rid for req in reqs],
)
)
def _maybe_record_sts_collect(
self,
*,
verify_ids_2d: torch.Tensor,
target_logits: Optional[torch.Tensor],
bs: int,
) -> None:
if not self._sts_collect_path:
return
if not self._planner.carries_confidence:
return
confidence_raw = self._planner.last_confidence_raw
if confidence_raw is None:
return
if self._sts_recorder is None:
self._sts_recorder = StsDataRecorder(
path_stem=self._sts_collect_path,
gamma=self._gamma,
flush_every=_STS_COLLECT_FLUSH_EVERY,
)
target_predict = torch.argmax(target_logits, dim=-1).view(
bs, self._verify_num_draft_tokens
)
num_correct_drafts, _ = compute_dflash_correct_drafts_and_bonus(
candidates=verify_ids_2d,
target_predict=target_predict,
)
self._sts_recorder.record(
confidence_raw=confidence_raw,
num_correct_drafts=num_correct_drafts,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,164 @@
from __future__ import annotations
import bisect
from typing import Optional
import msgspec
def floor_probe_index(edges: list[int], batch_tokens: int) -> int:
idx = bisect.bisect_right(edges, batch_tokens) - 1
return max(0, min(idx, len(edges) - 1))
class SpsCostTable(msgspec.Struct, frozen=True):
sample_batch_tokens: list[int]
sample_steps_per_sec: list[float]
max_batch_tokens: int
def __post_init__(self) -> None:
if not self.sample_batch_tokens:
raise ValueError("SpsCostTable requires at least one probe.")
if self.sample_batch_tokens != sorted(set(self.sample_batch_tokens)):
raise ValueError(
"sample_batch_tokens must be strictly increasing (monotone-sorted "
f"invariant), got {self.sample_batch_tokens}."
)
if len(self.sample_batch_tokens) != len(self.sample_steps_per_sec):
raise ValueError(
"sample_batch_tokens and sample_steps_per_sec must have equal length, "
f"got {len(self.sample_batch_tokens)} vs {len(self.sample_steps_per_sec)}."
)
if self.max_batch_tokens < self.sample_batch_tokens[-1]:
raise ValueError(
"max_batch_tokens must be >= the largest probe, got "
f"{self.max_batch_tokens} < {self.sample_batch_tokens[-1]}."
)
def lookup(self, batch_tokens: int) -> float:
return self.sample_steps_per_sec[
floor_probe_index(self.sample_batch_tokens, batch_tokens)
]
def to_json(self) -> str:
return msgspec.json.encode(self).decode("utf-8")
@classmethod
def from_json(cls, data: str) -> SpsCostTable:
return msgspec.json.decode(data.encode("utf-8"), type=cls)
def _interp_clamped(xs: list[int], ys: list[float], x: float) -> float:
if x <= xs[0]:
return ys[0]
if x >= xs[-1]:
return ys[-1]
hi = bisect.bisect_right(xs, x)
lo = hi - 1
frac = (x - xs[lo]) / (xs[hi] - xs[lo])
return ys[lo] + frac * (ys[hi] - ys[lo])
class SpsAdditiveCostTable(msgspec.Struct, frozen=True):
bias_seconds: float
bs_probes: list[int]
alpha_seconds: list[float]
m_probes: list[int]
theta_seconds: list[float]
def __post_init__(self) -> None:
for name, probes, values in (
("bs", self.bs_probes, self.alpha_seconds),
("m", self.m_probes, self.theta_seconds),
):
if not probes:
raise ValueError(f"SpsAdditiveCostTable requires {name}_probes.")
if probes != sorted(set(probes)):
raise ValueError(
f"{name}_probes must be strictly increasing, got {probes}."
)
if len(probes) != len(values):
raise ValueError(
f"{name}_probes and its values must have equal length, got "
f"{len(probes)} vs {len(values)}."
)
if self.bias_seconds <= 0:
raise ValueError(f"bias_seconds must be > 0, got {self.bias_seconds}.")
def step_time(self, *, num_reqs: int, budget: int) -> float:
return (
self.bias_seconds
+ _interp_clamped(self.bs_probes, self.alpha_seconds, float(num_reqs))
+ _interp_clamped(
self.m_probes, self.theta_seconds, float(num_reqs + budget)
)
)
def to_json(self) -> str:
return msgspec.json.encode(self).decode("utf-8")
@classmethod
def from_json(cls, data: str) -> SpsAdditiveCostTable:
return msgspec.json.decode(data.encode("utf-8"), type=cls)
def profile_sps_table(
*,
probes: list[tuple[int, float]],
max_batch_tokens: Optional[int] = None,
) -> SpsCostTable:
if not probes:
raise ValueError("profile_sps_table requires at least one probe.")
sorted_probes = sorted(probes, key=lambda probe: probe[0])
sample_batch_tokens: list[int] = []
sample_steps_per_sec: list[float] = []
for batch_tokens, steps_per_sec in sorted_probes:
batch_tokens = int(batch_tokens)
if batch_tokens < 1:
raise ValueError(
f"profile_sps_table requires batch_tokens >= 1, got {batch_tokens}."
)
if sample_batch_tokens and batch_tokens == sample_batch_tokens[-1]:
raise ValueError(
"profile_sps_table requires unique batch_tokens per probe; "
f"batch_tokens={batch_tokens} appears more than once. Median the "
"repeated samples per batch_tokens before calling the assembler."
)
sample_batch_tokens.append(batch_tokens)
sample_steps_per_sec.append(float(steps_per_sec))
resolved_max = (
int(max_batch_tokens)
if max_batch_tokens is not None
else sample_batch_tokens[-1]
)
return SpsCostTable(
sample_batch_tokens=sample_batch_tokens,
sample_steps_per_sec=sample_steps_per_sec,
max_batch_tokens=resolved_max,
)
def load_sps_table_from_path(path: str):
with open(path, "r", encoding="utf-8") as f:
data = f.read()
if '"bias_seconds"' in data:
return SpsAdditiveCostTable.from_json(data)
return SpsCostTable.from_json(data)
def build_uninitialized_sps_table(*, max_batch_tokens: int) -> SpsCostTable:
return SpsCostTable(
sample_batch_tokens=[1],
sample_steps_per_sec=[1.0],
max_batch_tokens=max_batch_tokens,
)
def is_uninitialized_sps_table(table: SpsCostTable | SpsAdditiveCostTable) -> bool:
if isinstance(table, SpsAdditiveCostTable):
return False
return len(table.sample_batch_tokens) <= 1
@@ -0,0 +1,76 @@
from __future__ import annotations
from pathlib import Path
import msgspec
import torch
class DSparkStsCalibration(msgspec.Struct, frozen=True, omit_defaults=True):
temperatures: list[float]
dataset: str = ""
num_samples: int = 0
ece_before: list[float] = []
ece_after: list[float] = []
def __post_init__(self) -> None:
if not self.temperatures:
raise ValueError("DSparkStsCalibration requires at least one temperature.")
for temperature in self.temperatures:
if temperature <= 0:
raise ValueError(
"DSparkStsCalibration temperatures must all be > 0, got "
f"{self.temperatures}."
)
def to_json(self) -> str:
return msgspec.json.encode(self).decode("utf-8")
@classmethod
def from_json(cls, data: str) -> DSparkStsCalibration:
return msgspec.json.decode(data.encode("utf-8"), type=cls)
def load_sts_calibration_from_path(path: str) -> DSparkStsCalibration:
with open(path, "r", encoding="utf-8") as f:
return DSparkStsCalibration.from_json(f.read())
class StsDataRecorder:
def __init__(self, *, path_stem: str, gamma: int, flush_every: int) -> None:
self.path_stem = path_stem
self.gamma = int(gamma)
self.flush_every = int(flush_every)
self._logits_buffer: list[torch.Tensor] = []
self._prefix_mask_buffer: list[torch.Tensor] = []
self._shard_ct = 0
def record(
self, *, confidence_raw: torch.Tensor, num_correct_drafts: torch.Tensor
) -> None:
logits = confidence_raw.detach().to(device="cpu", dtype=torch.float32)
positions = torch.arange(self.gamma).view(1, -1)
counts = (
num_correct_drafts.detach().to(device="cpu", dtype=torch.int64).view(-1, 1)
)
prefix_mask = (positions < counts).to(torch.float32)
self._logits_buffer.append(logits)
self._prefix_mask_buffer.append(prefix_mask)
if len(self._logits_buffer) >= self.flush_every:
self.flush()
def flush(self) -> None:
if not self._logits_buffer:
return
shard_path = Path(f"{self.path_stem}.{self._shard_ct}.pt")
shard_path.parent.mkdir(parents=True, exist_ok=True)
torch.save(
{
"logits": torch.cat(self._logits_buffer, dim=0),
"prefix_mask": torch.cat(self._prefix_mask_buffer, dim=0),
},
shard_path,
)
self._logits_buffer.clear()
self._prefix_mask_buffer.clear()
self._shard_ct += 1
@@ -0,0 +1,716 @@
from __future__ import annotations
from typing import Optional
import msgspec
import torch
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode, ForwardMode
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
from sglang.srt.speculative.dflash_utils import apply_dflash_verify_logits_adjustments
from sglang.srt.speculative.dspark_components.dspark_draft import DraftBlockResult
from sglang.srt.speculative.dspark_components.dspark_kv_inject import (
TargetHiddenKvInjector,
)
from sglang.srt.speculative.dspark_components.dspark_planner import (
VerifyWindow,
apply_logits_adjustments_strided,
)
from sglang.srt.speculative.dspark_components.kernels.dspark_accept import (
AcceptGreedy,
AcceptSampling,
FinalizeAcceptLens,
SelectMixedAccept,
SoftmaxTemp,
accept_greedy_triton,
finalize_accept_lens_triton,
)
from sglang.srt.speculative.dspark_components.kernels.dspark_verify_window import (
BuildCommitInjectLayout,
BuildOutTokens,
BuildRaggedVerifyWindow,
RaggedVerifyWindow,
ScatterCompactToStrided,
scatter_compact_to_strided_into,
)
from sglang.srt.speculative.ragged_verify import RaggedVerifyLayout
def verify_logits_adjustments_are_noop(sampling_info) -> bool:
if sampling_info is None:
return True
if sampling_info.has_custom_logit_processor:
return False
if getattr(sampling_info, "acc_linear_penalties", None) is not None:
return False
penalizer = getattr(sampling_info, "penalizer_orchestrator", None)
if penalizer is not None and penalizer.is_required:
return False
if getattr(sampling_info, "vocab_mask", None) is not None:
return False
if getattr(sampling_info, "logit_bias", None) is not None:
return False
return True
class TargetVerifyResult(msgspec.Struct, frozen=True):
logits_output: object
can_run_cuda_graph: bool
class TargetVerifyExecutor:
def __init__(
self,
*,
target_worker,
gamma: int,
verify_num_draft_tokens: int,
model_runner,
kv_injector: TargetHiddenKvInjector,
verify_epilogue=None,
simulate_acc_len: float = 0.0,
) -> None:
self.target_worker = target_worker
self.gamma = int(gamma)
self.verify_num_draft_tokens = verify_num_draft_tokens
self.model_runner = model_runner
self.kv_injector = kv_injector
self.verify_epilogue = verify_epilogue
self._verify_backend_self_adds_seq_lens_cache: Optional[bool] = None
self._simulate_acc_len = float(simulate_acc_len)
self._simulated_correct_drafts_buf: Optional[torch.Tensor] = None
def accept_and_finalize(
self,
*,
folded_accept: bool,
bs: int,
verify_ids_2d: torch.Tensor,
target_logits: Optional[torch.Tensor],
draft_block: DraftBlockResult,
sampling_info,
draft_input: DFlashDraftInputV2,
layout: Optional[RaggedVerifyLayout],
prefix_lens: torch.Tensor,
draft_tokens: torch.Tensor,
) -> AcceptOuts:
"""Produce the per-request accept outcome after target verify.
Folded path: the accept/finalize/out-token kernels already ran inside
the target-verify cuda graph (DsparkVerifyEpilogue); read its buffers.
Eager path: run them here, including the SGLANG_SIMULATE_ACC_LEN
override.
"""
if folded_accept:
return self.verify_epilogue.read_accept(bs)
correct_len, bonus, cap_trim_lens = accept_draft_tokens(
candidates=verify_ids_2d,
target_logits=target_logits,
draft_block=draft_block,
sampling_info=sampling_info,
draft_input=draft_input,
gamma=self.gamma,
verify_num_draft_tokens=self.verify_num_draft_tokens,
cutoff_layout=layout,
)
if self._simulate_acc_len > 0:
correct_len = self._simulated_correct_len(
bs=bs, dtype=correct_len.dtype, device=correct_len.device
)
finalized = FinalizeAcceptLens.execute(
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
prefix_lens=prefix_lens,
)
out_tokens = BuildOutTokens.execute(
draft_tokens=draft_tokens,
correct_len=correct_len,
bonus=bonus,
verify_num_draft_tokens=self.verify_num_draft_tokens,
gamma=self.gamma,
)
return AcceptOuts(
correct_len=correct_len,
bonus=bonus,
cap_trim_lens=finalized.cap_trim_lens,
commit_lens=finalized.commit_lens,
new_seq_lens=finalized.new_seq_lens,
out_tokens=out_tokens,
)
def _simulated_correct_len(
self, *, bs: int, dtype: torch.dtype, device: torch.device
) -> torch.Tensor:
buf = self._simulated_correct_drafts_buf
if buf is None or buf.numel() < bs or buf.dtype != dtype:
correct_target = int(
round(min(max(self._simulate_acc_len - 1.0, 0.0), float(self.gamma)))
)
buf = torch.full(
(max(bs, 512),), correct_target, dtype=dtype, device=device
)
self._simulated_correct_drafts_buf = buf
return buf[:bs]
def run_idle_participation(
self,
*,
batch: ScheduleBatch,
idle_layout: Optional[RaggedVerifyLayout],
) -> None:
"""Run a dummy target-verify forward so an idle DP rank joins the
token-keyed collective ops of the busy ranks' verify step."""
device = self.model_runner.device
if self.verify_epilogue is not None:
self.verify_epilogue.begin_step(None, armed=False)
num_dummy_tokens = (
idle_layout.graph_num_tokens if idle_layout is not None else 0
)
verify_input = DFlashVerifyInput(
draft_token=torch.zeros(
(num_dummy_tokens,), dtype=torch.int64, device=device
),
positions=torch.zeros(
(num_dummy_tokens,), dtype=torch.int64, device=device
),
draft_token_num=self.verify_num_draft_tokens,
custom_mask=None,
capture_hidden_mode=CaptureHiddenMode.FULL,
ragged_verify_layout=idle_layout,
)
batch.out_cache_loc = torch.zeros(
(num_dummy_tokens,), dtype=torch.int64, device=device
)
if idle_layout is not None:
num_dummy_slots = int(idle_layout.verify_lens.numel())
batch.seq_lens = torch.ones(
(num_dummy_slots,), dtype=torch.int64, device=device
)
batch.req_pool_indices = torch.zeros(
(num_dummy_slots,), dtype=torch.int64, device=device
)
batch.seq_lens_cpu = torch.ones((num_dummy_slots,), dtype=torch.int64)
batch.seq_lens_sum = num_dummy_slots
batch.forward_mode = ForwardMode.TARGET_VERIFY
verify_forward_batch, _ = verify_input.prepare_for_verify(
batch, self.target_worker
)
self.target_worker.forward_batch_generation(
batch=None,
forward_batch=verify_forward_batch,
is_verify=True,
skip_attn_backend_init=True,
)
def run_non_compact(
self,
*,
batch: ScheduleBatch,
draft_input: DFlashDraftInputV2,
verify_ids_2d: torch.Tensor,
verify_window: VerifyWindow,
sampling_info,
) -> TargetVerifyResult:
verify_w = self.verify_num_draft_tokens
positions_2d = verify_window.positions_2d
verify_cache_loc = verify_window.verify_cache_loc
verify_input = DFlashVerifyInput(
draft_token=verify_ids_2d.reshape(-1),
positions=positions_2d.reshape(-1),
draft_token_num=verify_w,
custom_mask=None,
capture_hidden_mode=CaptureHiddenMode.FULL,
)
batch.out_cache_loc = verify_cache_loc
seq_lens_cpu_backup = batch.seq_lens_cpu
seq_lens_sum_backup = batch.seq_lens_sum
if not self._verify_backend_self_adds_seq_lens():
if seq_lens_cpu_backup is not None:
batch.seq_lens_cpu = seq_lens_cpu_backup + verify_w
batch.seq_lens_sum = int(batch.seq_lens_cpu.sum())
elif draft_input.reserved_seq_lens_cpu is not None:
batch.seq_lens_cpu = draft_input.reserved_seq_lens_cpu
batch.seq_lens_sum = int(draft_input.reserved_seq_lens_sum)
result = self._forward_prepared_verify(
batch=batch,
verify_input=verify_input,
seq_lens_cpu_backup=seq_lens_cpu_backup,
seq_lens_sum_backup=seq_lens_sum_backup,
)
if sampling_info is not None:
apply_dflash_verify_logits_adjustments(
next_token_logits=result.logits_output.next_token_logits,
sampling_info=sampling_info,
draft_token_num=verify_w,
)
return result
def _forward_prepared_verify(
self,
*,
batch: ScheduleBatch,
verify_input: DFlashVerifyInput,
seq_lens_cpu_backup,
seq_lens_sum_backup,
) -> TargetVerifyResult:
verify_forward_batch, _ = verify_input.prepare_for_verify(
batch, self.target_worker
)
batch.seq_lens_cpu = seq_lens_cpu_backup
batch.seq_lens_sum = seq_lens_sum_backup
target_out = self.target_worker.forward_batch_generation(
batch=None,
forward_batch=verify_forward_batch,
is_verify=True,
skip_attn_backend_init=True,
)
return TargetVerifyResult(
logits_output=target_out.logits_output,
can_run_cuda_graph=target_out.can_run_cuda_graph,
)
def commit_hidden(
self,
*,
batch: ScheduleBatch,
layout: Optional[RaggedVerifyLayout],
hidden_strided: Optional[torch.Tensor],
verify_window: VerifyWindow,
logits_output,
commit_lens: torch.Tensor,
bs: int,
run_compact: bool,
) -> None:
if run_compact:
self.kv_injector.inject_ragged(
batch=batch,
layout=layout,
hidden_strided=hidden_strided,
commit_lens=commit_lens,
bs=bs,
)
return
hidden = logits_output.hidden_states
if hidden is None:
raise RuntimeError("DSpark verify requires target hidden states, got None.")
hidden = hidden.view(bs, self.verify_num_draft_tokens, -1)
self.kv_injector.inject_target_hidden(
target_hidden=hidden.reshape(-1, hidden.shape[-1]),
cache_loc=verify_window.verify_cache_loc,
cache_loc_2d=verify_window.verify_cache_loc_2d,
positions=verify_window.positions_2d.reshape(-1),
commit_lens=commit_lens,
)
def _run_ragged(
self,
*,
batch: ScheduleBatch,
layout: RaggedVerifyLayout,
ragged_window: RaggedVerifyWindow,
sampling_info,
) -> TargetVerifyResult:
verify_input = DFlashVerifyInput(
draft_token=ragged_window.verify_ids,
positions=ragged_window.positions,
draft_token_num=self.verify_num_draft_tokens,
custom_mask=None,
capture_hidden_mode=CaptureHiddenMode.FULL,
ragged_verify_layout=layout,
)
batch.out_cache_loc = ragged_window.verify_cache_loc
seq_lens_cpu_backup = batch.seq_lens_cpu
seq_lens_sum_backup = batch.seq_lens_sum
if seq_lens_cpu_backup is not None:
verify_lens_cpu = (
layout.verify_lens_cpu
if layout.verify_lens_cpu is not None
else layout.verify_lens.cpu().tolist()
)
batch.seq_lens_cpu = seq_lens_cpu_backup + torch.tensor(
verify_lens_cpu, dtype=seq_lens_cpu_backup.dtype
)
batch.seq_lens_sum = int(batch.seq_lens_cpu.sum())
return self._forward_prepared_verify(
batch=batch,
verify_input=verify_input,
seq_lens_cpu_backup=seq_lens_cpu_backup,
seq_lens_sum_backup=seq_lens_sum_backup,
)
def run_compact(
self,
*,
batch: ScheduleBatch,
layout: RaggedVerifyLayout,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
bs: int,
device: str,
sampling_info,
inject_gate: bool = False,
) -> tuple[TargetVerifyResult, torch.Tensor]:
ragged_window = BuildRaggedVerifyWindow.execute(
batch=batch,
layout=layout,
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
bs=bs,
device=device,
verify_num_draft_tokens=self.verify_num_draft_tokens,
model_runner=self.model_runner,
)
if self.verify_epilogue is not None:
self.verify_epilogue.begin_step(layout.verify_lens, armed=inject_gate)
target_verify = self._run_ragged(
batch=batch,
layout=layout,
ragged_window=ragged_window,
sampling_info=sampling_info,
)
logits_output = target_verify.logits_output
stride = self.verify_num_draft_tokens
if self.verify_epilogue is not None and target_verify.can_run_cuda_graph:
strided_logits = self.verify_epilogue.strided_logits
hidden_strided = self.verify_epilogue.strided_hidden
assert strided_logits is not None and hidden_strided is not None, (
"verify epilogue buffers unwritten after a graph replay -- the "
"replayed graph was captured without the epilogue"
)
strided_logits = strided_logits[: bs * stride]
hidden_strided = hidden_strided[: bs * stride]
else:
compact_logits = logits_output.next_token_logits
strided_logits = ScatterCompactToStrided.execute(
compact=compact_logits,
layout=layout,
fill_value=0.0,
verify_num_draft_tokens=stride,
)
compact_hidden = logits_output.hidden_states
if compact_hidden is None:
raise RuntimeError(
"DSpark verify requires target hidden states, got None."
)
hidden_strided = ScatterCompactToStrided.execute(
compact=compact_hidden,
layout=layout,
fill_value=0.0,
verify_num_draft_tokens=stride,
)
apply_logits_adjustments_strided(
next_token_logits=strided_logits,
sampling_info=sampling_info,
verify_num_draft_tokens=stride,
)
logits_output.next_token_logits = strided_logits
logits_output.hidden_states = hidden_strided
return target_verify, hidden_strided
def _verify_backend_self_adds_seq_lens(self) -> bool:
if self._verify_backend_self_adds_seq_lens_cache is None:
backend = self.target_worker.model_runner.attn_backend
self._verify_backend_self_adds_seq_lens_cache = hasattr(
backend, "make_forward_metadata_from_raw_verify"
)
return self._verify_backend_self_adds_seq_lens_cache
class CommitInjectCtx(msgspec.Struct):
draft_model: object
block_pos_offsets: torch.Tensor
resolve_pool: object
resolve_req_to_token: object
class AcceptOuts(msgspec.Struct):
correct_len: torch.Tensor
bonus: torch.Tensor
cap_trim_lens: torch.Tensor
commit_lens: torch.Tensor
new_seq_lens: torch.Tensor
out_tokens: torch.Tensor
class DsparkVerifyEpilogue:
def __init__(
self,
*,
max_bs: int,
verify_num_draft_tokens: int,
device,
commit_ctx: Optional[CommitInjectCtx] = None,
) -> None:
self.max_bs = int(max_bs)
self.stride = int(verify_num_draft_tokens)
self.gamma = self.stride - 1
self.commit_ctx = commit_ctx
self.inject_gate_buf = torch.zeros((1,), dtype=torch.int32, device=device)
self.verify_lens_buf = torch.zeros(
(self.max_bs,), dtype=torch.int64, device=device
)
self.draft_tokens_buf = torch.zeros(
(self.max_bs * self.gamma,), dtype=torch.int64, device=device
)
self.correct_len_buf = torch.zeros(
(self.max_bs,), dtype=torch.int64, device=device
)
self.bonus_buf = torch.zeros((self.max_bs,), dtype=torch.int64, device=device)
self.cap_trim_lens_buf = torch.zeros(
(self.max_bs,), dtype=torch.int32, device=device
)
self.commit_lens_buf = torch.zeros(
(self.max_bs,), dtype=torch.int32, device=device
)
self.new_seq_lens_buf = torch.zeros(
(self.max_bs,), dtype=torch.int64, device=device
)
self.out_tokens_buf = torch.zeros(
(self.max_bs, self.stride), dtype=torch.int64, device=device
)
self.strided_logits: Optional[torch.Tensor] = None
self.strided_hidden: Optional[torch.Tensor] = None
def capture_hook(self, runner, out, forward_batch, num_tokens) -> None:
if runner.model_runner.is_draft_worker or not runner.ragged_verify_mode:
return
if (
not isinstance(out, LogitsProcessorOutput)
or out.next_token_logits is None
or out.hidden_states is None
):
return
self(
compact_logits=out.next_token_logits,
compact_hidden=out.hidden_states,
input_ids=forward_batch.input_ids,
seq_lens=forward_batch.seq_lens,
req_pool_indices=forward_batch.req_pool_indices,
bs=forward_batch.batch_size,
)
def begin_step(self, verify_lens, armed: bool) -> None:
if verify_lens is None:
self.verify_lens_buf.zero_()
else:
bs = verify_lens.shape[0]
self.verify_lens_buf[:bs].copy_(verify_lens)
if bs < self.max_bs:
self.verify_lens_buf[bs:].zero_()
self.inject_gate_buf.fill_(1 if armed else 0)
def read_accept(self, bs: int) -> AcceptOuts:
return AcceptOuts(
correct_len=self.correct_len_buf[:bs],
bonus=self.bonus_buf[:bs],
cap_trim_lens=self.cap_trim_lens_buf[:bs],
commit_lens=self.commit_lens_buf[:bs],
new_seq_lens=self.new_seq_lens_buf[:bs],
out_tokens=self.out_tokens_buf[:bs],
)
@property
def folds_commit(self) -> bool:
if self.commit_ctx is None:
return False
pool = self.commit_ctx.resolve_pool()
return hasattr(pool, "set_swa_key_buffer_radix_fused_norm_rope")
def _ensure_out(
self, buf: Optional[torch.Tensor], compact: torch.Tensor
) -> torch.Tensor:
if (
buf is not None
and buf.dtype == compact.dtype
and buf.shape[1] == compact.shape[1]
):
return buf
assert not torch.cuda.is_current_stream_capturing(), (
"DsparkVerifyEpilogue output buffers must be allocated during "
"warmup, not inside graph capture (pool memory is unreadable "
"post-replay)."
)
return torch.empty(
(self.max_bs * self.stride, compact.shape[1]),
dtype=compact.dtype,
device=compact.device,
)
def __call__(
self,
*,
compact_logits: torch.Tensor,
compact_hidden: torch.Tensor,
input_ids: torch.Tensor,
seq_lens: torch.Tensor,
req_pool_indices: torch.Tensor,
bs: int,
) -> None:
self.strided_logits = self._ensure_out(self.strided_logits, compact_logits)
self.strided_hidden = self._ensure_out(self.strided_hidden, compact_hidden)
verify_lens = self.verify_lens_buf[:bs]
self._scatter(compact_logits, compact_hidden, verify_lens, bs)
commit_lens = self._accept(input_ids, seq_lens, verify_lens, bs)
if self.folds_commit:
self._commit_inject(
commit_lens, verify_lens, seq_lens, req_pool_indices, bs
)
def _scatter(self, compact_logits, compact_hidden, verify_lens, bs: int) -> None:
scatter_compact_to_strided_into(
compact=compact_logits,
verify_lens=verify_lens,
out=self.strided_logits[: bs * self.stride],
stride=self.stride,
fill_value=0.0,
)
scatter_compact_to_strided_into(
compact=compact_hidden,
verify_lens=verify_lens,
out=self.strided_hidden[: bs * self.stride],
stride=self.stride,
fill_value=0.0,
)
def _accept(self, input_ids, seq_lens, verify_lens, bs: int) -> torch.Tensor:
candidates = torch.zeros(
(bs * self.stride, 1), dtype=input_ids.dtype, device=input_ids.device
)
scatter_compact_to_strided_into(
compact=input_ids.view(-1, 1),
verify_lens=verify_lens,
out=candidates,
stride=self.stride,
fill_value=0,
)
correct_len, bonus, cap_trim_lens = accept_greedy_triton(
candidates=candidates.view(bs, self.stride),
target_logits=self.strided_logits[: bs * self.stride],
verify_num_draft_tokens=self.stride,
cutoff_verify_lens=verify_lens,
)
finalized = finalize_accept_lens_triton(
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
prefix_lens=seq_lens[:bs],
)
out_tokens = BuildOutTokens.execute(
draft_tokens=self.draft_tokens_buf[: bs * self.gamma].view(bs, self.gamma),
correct_len=correct_len,
bonus=bonus,
verify_num_draft_tokens=self.stride,
gamma=self.gamma,
)
self.correct_len_buf[:bs].copy_(correct_len)
self.bonus_buf[:bs].copy_(bonus)
self.cap_trim_lens_buf[:bs].copy_(cap_trim_lens.to(torch.int32))
self.commit_lens_buf[:bs].copy_(finalized.commit_lens)
self.new_seq_lens_buf[:bs].copy_(finalized.new_seq_lens)
self.out_tokens_buf[:bs].copy_(out_tokens.view(bs, self.stride))
return finalized.commit_lens
def _commit_inject(
self, commit_lens, verify_lens, seq_lens, req_pool_indices, bs: int
) -> None:
ctx = self.commit_ctx
pool = ctx.resolve_pool()
gated_commit_lens = (
torch.minimum(commit_lens, verify_lens.to(torch.int32))
* self.inject_gate_buf
)
inject_layout = BuildCommitInjectLayout.execute(
req_pool_indices=req_pool_indices,
req_to_token=ctx.resolve_req_to_token(),
prefix_lens=seq_lens[:bs],
block_pos_offsets=ctx.block_pos_offsets[: self.stride],
full_to_swa_mapping=pool.full_to_swa_index_mapping,
commit_lens=gated_commit_lens,
stride=self.stride,
)
with torch.inference_mode():
ctx.draft_model.write_target_hidden_kv(
main_hidden=self.strided_hidden[: bs * self.stride],
swa_loc=inject_layout.swa_loc,
positions=inject_layout.positions,
pool=pool,
)
def accept_draft_tokens(
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
draft_block: DraftBlockResult,
sampling_info,
draft_input: DFlashDraftInputV2,
gamma: int,
verify_num_draft_tokens: int,
cutoff_layout: Optional[RaggedVerifyLayout] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
greedy_mask = draft_block.greedy_mask
cutoff_verify_lens = None if cutoff_layout is None else cutoff_layout.verify_lens
all_greedy = sampling_info is None or sampling_info.is_all_greedy
if all_greedy:
return AcceptGreedy.execute(
candidates=candidates,
target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
bs, gamma_rows, vocab = draft_block.corrected_logits.shape
draft_probs = SoftmaxTemp.execute(
logits=draft_block.corrected_logits.reshape(bs * gamma_rows, vocab),
temperatures=draft_block.temperatures,
rows_per_request=gamma_rows,
).view(bs, gamma_rows, vocab)
if not sampling_info.is_any_greedy:
return AcceptSampling.execute(
candidates=candidates,
target_logits=target_logits,
draft_probs=draft_probs,
sampling_info=sampling_info,
draft_input=draft_input,
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
greedy_len, greedy_bonus, greedy_trim = AcceptGreedy.execute(
candidates=candidates,
target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
sampling_len, sampling_bonus, sampling_trim = AcceptSampling.execute(
candidates=candidates,
target_logits=target_logits,
draft_probs=draft_probs,
sampling_info=sampling_info,
draft_input=draft_input,
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
selected = SelectMixedAccept.execute(
greedy_mask=greedy_mask,
greedy_len=greedy_len,
greedy_bonus=greedy_bonus,
greedy_trim=greedy_trim,
sampling_len=sampling_len,
sampling_bonus=sampling_bonus,
sampling_trim=sampling_trim,
)
return selected.correct_len, selected.bonus, selected.cap_trim_lens
@@ -0,0 +1,693 @@
import logging
from contextlib import nullcontext
from typing import Optional
import torch
from sglang.srt.environ import envs
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.scheduler import GenerationBatchResult
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
compute_position,
)
from sglang.srt.runtime_context import get_parallel
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker
from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
from sglang.srt.speculative.draft_worker_common import (
build_block_pos_offsets,
build_draft_tp_worker,
make_draft_block_spec_info,
make_draft_sampler_capture_hook,
)
from sglang.srt.speculative.dspark_components.dspark_config import (
DSV4_DRAFT_ATTENTION_BACKEND,
draft_is_deepseek_v4,
resolve_runtime_config,
)
from sglang.srt.speculative.dspark_components.dspark_draft import (
DraftBlockProposer,
make_next_draft_input,
maybe_build_draft_sampler,
)
from sglang.srt.speculative.dspark_components.dspark_kv_inject import (
TargetHiddenKvInjector,
)
from sglang.srt.speculative.dspark_components.dspark_observability import (
DsparkStepObservers,
InfoSegment,
)
from sglang.srt.speculative.dspark_components.dspark_planner import (
DSparkVerifyPlanner,
alloc_verify_window,
dp_global_verify_tier_num_tokens,
idle_ragged_layout,
)
from sglang.srt.speculative.dspark_components.dspark_verify import (
CommitInjectCtx,
DsparkVerifyEpilogue,
TargetVerifyExecutor,
verify_logits_adjustments_are_noop,
)
from sglang.srt.speculative.spec_utils import draft_tp_context
from sglang.srt.utils import get_available_gpu_memory, is_cuda
logger = logging.getLogger(__name__)
class DSparkWorkerV2(BaseSpecWorker):
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
self.server_args = server_args
self.gpu_id = gpu_id
self.tp_rank = tp_rank
self.dp_rank = dp_rank
self.moe_ep_rank = moe_ep_rank
self.attn_cp_rank = attn_cp_rank
self.moe_dp_rank = moe_dp_rank
self.nccl_port = nccl_port
self._target_worker = target_worker
self.model_runner = target_worker.model_runner
self.page_size = server_args.page_size
self.device = target_worker.device
self._draft_is_moe = draft_is_deepseek_v4(server_args=server_args)
self._draft_dp_context_enabled = (
server_args.enable_dp_attention and not self._draft_is_moe
)
attn_tp_size = server_args.tp_size // max(server_args.dp_size, 1)
if server_args.enable_dp_attention and self._draft_is_moe and attn_tp_size > 1:
raise ValueError(
"DSpark + dp attention with a DeepSeek-V4 (MoE) draft requires "
"attn_tp == 1 (set --dp-size == --tp). attn_tp > 1 corrupts the "
"MoE-under-DP all-reduce."
)
with self._draft_context():
bundle = build_draft_tp_worker(
server_args=server_args,
gpu_id=gpu_id,
tp_rank=tp_rank,
dp_rank=dp_rank,
moe_ep_rank=moe_ep_rank,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
target_model_config=target_worker.model_runner.model_config,
algo_label="DSPARK",
attention_backend_override=(
DSV4_DRAFT_ATTENTION_BACKEND if self._draft_is_moe else None
),
)
self._draft_worker = bundle.draft_worker
self.draft_model_runner = bundle.draft_model_runner
self.draft_model = bundle.draft_model
self._draft_sampler = None
runtime_config = resolve_runtime_config(
draft_hf_config=self.draft_model_runner.model_config.hf_config,
speculative_num_draft_tokens=server_args.speculative_num_draft_tokens,
target_vocab_size=int(
self.target_worker.model_runner.model_config.vocab_size
),
)
self.gamma = runtime_config.gamma
self.verify_num_draft_tokens = runtime_config.verify_num_draft_tokens
self.speculative_num_draft_tokens = self.verify_num_draft_tokens
self._mask_token_id = runtime_config.mask_token_id
if self.tp_rank == 0:
logger.info(
"Initialized DSpark draft runner. attention_backend=%s, model=%s, "
"gamma=%s, verify_num_draft_tokens=%s, mask_token_id=%s, "
"markov_head=%s",
bundle.resolved_attention_backend,
self.draft_model.__class__.__name__,
self.gamma,
self.verify_num_draft_tokens,
self._mask_token_id,
type(self.draft_model.markov_head).__name__,
)
self._block_pos_offsets = build_block_pos_offsets(
length=self.verify_num_draft_tokens, device=self.device
)
self._draft_block_spec_info = make_draft_block_spec_info(
draft_token_num=int(self.gamma), device=self.device
)
target_model = self.target_worker.model_runner.model
lm_head = getattr(target_model, "lm_head", None)
if lm_head is None or not hasattr(lm_head, "weight"):
raise RuntimeError(
"DSpark requires the target model to expose `lm_head` with `weight`."
)
self.draft_model.attach_shared_modules(
embed_tokens=self._resolve_target_embed_tokens(target_model),
lm_head=lm_head,
)
self._verify_planner = DSparkVerifyPlanner(
draft_model=self.draft_model,
gamma=self.gamma,
model_runner=self.model_runner,
device=self.device,
tp_rank=self.tp_rank,
server_args=self.server_args,
verify_num_draft_tokens=self.verify_num_draft_tokens,
)
if (
server_args.enable_dp_attention
and not self._draft_is_moe
and self._verify_planner.is_compact_mode
and not server_args.disable_cuda_graph
):
raise ValueError(
"DSpark dense-draft compact verify under --enable-dp-attention does not "
"yet support cuda graph (idle DP groups cannot join the token-keyed "
"compact graph). Re-run with --disable-cuda-graph (eager is lossless), "
"or use SGLANG_RAGGED_VERIFY_MODE=static. The dsv4 (MoE) draft supports "
"cuda graph under DP."
)
self._kv_injector = TargetHiddenKvInjector(
draft_model=self.draft_model,
draft_model_runner=self.draft_model_runner,
model_runner=self.model_runner,
device=self.device,
verify_num_draft_tokens=self.verify_num_draft_tokens,
block_pos_offsets=self._block_pos_offsets,
)
self._proposer = DraftBlockProposer(
draft_model=self.draft_model,
draft_model_runner=self.draft_model_runner,
gamma=self.gamma,
mask_token_id=self._mask_token_id,
draft_block_spec_info=self._draft_block_spec_info,
dp_moe_sync=self._draft_is_moe and server_args.enable_dp_attention,
)
self._verify_epilogue = None
if (
self._verify_planner.is_compact_mode
and not server_args.disable_cuda_graph
and is_cuda()
):
self._verify_epilogue = DsparkVerifyEpilogue(
max_bs=max(server_args.cuda_graph_config.decode.bs),
verify_num_draft_tokens=self.verify_num_draft_tokens,
device=self.device,
commit_ctx=CommitInjectCtx(
draft_model=self.draft_model,
block_pos_offsets=self._block_pos_offsets,
resolve_pool=lambda: self.draft_model_runner.token_to_kv_pool,
resolve_req_to_token=lambda: (
self.model_runner.req_to_token_pool.req_to_token
),
),
)
self.model_runner.capture_tail_hooks.append(
self._verify_epilogue.capture_hook
)
self._simulate_acc_len = float(envs.SGLANG_SIMULATE_ACC_LEN.get())
if (
self._simulate_acc_len > 0
and self._simulate_acc_len != 1.0
and not self._verify_planner.is_verify_all
):
raise ValueError(
"SGLANG_SIMULATE_ACC_LEN>1.0 with DSpark requires a verify-all "
"schedule (SGLANG_RAGGED_VERIFY_MODE=static, or =compact with the "
"uninitialized/flat SPS table): a constant simulated correct_len>0 "
"can exceed a trimmed request's verify budget (cap-accept, or "
"compact with a profiled SPS table) and break the cutoff/cap "
"accounting. SGLANG_SIMULATE_ACC_LEN=1.0 yields correct_len=0 "
"(commit is the bonus token only), which stays within every verify "
"budget and is safe in any mode. Got mode="
f"{self._verify_planner.mode_value!r}, simulate_acc_len="
f"{self._simulate_acc_len}."
)
self._verify_executor = TargetVerifyExecutor(
target_worker=self.target_worker,
gamma=self.gamma,
verify_num_draft_tokens=self.verify_num_draft_tokens,
model_runner=self.model_runner,
kv_injector=self._kv_injector,
verify_epilogue=self._verify_epilogue,
simulate_acc_len=self._simulate_acc_len,
)
self._forced_budget_frac: Optional[float] = None
self._observers = DsparkStepObservers(
planner=self._verify_planner,
gamma=self.gamma,
verify_num_draft_tokens=self.verify_num_draft_tokens,
tp_rank=self.tp_rank,
device=self.device,
simulate_acc_len=self._simulate_acc_len,
)
def _resolve_target_embed_tokens(self, target_model):
if hasattr(target_model, "get_input_embeddings"):
return target_model.get_input_embeddings()
return target_model.model.get_input_embeddings()
@property
def carries_confidence(self) -> bool:
return self._verify_planner.carries_confidence
@property
def target_worker(self) -> TpModelWorker:
return self._target_worker
@property
def draft_worker(self):
return self._draft_worker
@property
def spec_v2_attn_backends(self) -> tuple:
return (
self._target_worker.model_runner.attn_backend,
self.draft_model_runner.attn_backend,
)
def __getattr__(self, name):
if name == "_target_worker":
raise AttributeError(name)
return getattr(self.target_worker, name)
def _draft_context(self):
if self._draft_dp_context_enabled:
return draft_tp_context(get_parallel().attn_tp_group)
return nullcontext()
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
self._draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
def init_attention_backends(self):
with self._draft_context():
self._draft_worker.init_attention_backends()
def init_cuda_graphs(self):
capture_decode_cuda_graph = not self.server_args.disable_cuda_graph
if is_cuda() and capture_decode_cuda_graph:
available_mem = get_available_gpu_memory(self.device, self.gpu_id)
if available_mem < 1.0:
capture_decode_cuda_graph = False
logger.warning(
"Disable DSpark draft cuda graph because only %.2f GB GPU "
"memory is available after target backend initialization.",
available_mem,
)
with self._draft_context():
if capture_decode_cuda_graph:
self._draft_sampler = self._maybe_build_draft_sampler()
if self._draft_sampler is not None:
self.draft_model_runner.capture_tail_hooks.append(
make_draft_sampler_capture_hook(self._draft_sampler)
)
self._proposer.attach_draft_sampler(self._draft_sampler)
self._draft_worker.init_cuda_graphs(
capture_decode_cuda_graph=capture_decode_cuda_graph
)
def _maybe_build_draft_sampler(self):
return maybe_build_draft_sampler(
draft_model=self.draft_model,
gamma=self.gamma,
max_bs=max(self.server_args.cuda_graph_config.decode.bs),
device=self.device,
tp_rank=self.tp_rank,
confidence_fn=(
self._verify_planner.compute_confidence_tensor
if self._verify_planner.carries_confidence
else None
),
out=(
self._verify_epilogue.draft_tokens_buf
if self._verify_epilogue is not None
else None
),
)
def clear_cache_pool(self):
pass
def set_dspark_forced_budget_frac(self, frac: Optional[float]) -> None:
self._forced_budget_frac = frac
self._verify_planner.set_forced_budget_frac(frac)
def dump_info_records(self) -> Optional[dict]:
return self._observers.dump_info_records()
def clear_info_records(self) -> None:
self._observers.clear_info_records()
def block_accept_estimate_log_suffix(self) -> Optional[str]:
return self._observers.block_accept_estimate_log_suffix()
def note_request_finished(self, *, rid: str, natural_stop: bool) -> None:
self._observers.note_request_finished(rid=rid, natural_stop=natural_stop)
def forward_batch_generation(
self,
batch: ScheduleBatch,
on_publish=None,
) -> GenerationBatchResult:
if getattr(batch, "return_logprob", False):
raise ValueError(
"DSpark speculative decoding does not support return_logprob yet."
)
if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
self._verify_planner.note_non_decode_step()
self._observers.note_prefill_step()
return self._forward_prefill(batch, on_publish)
return self._forward_decode(batch, on_publish)
def _forward_prefill(
self, batch: ScheduleBatch, on_publish
) -> GenerationBatchResult:
if batch.forward_mode.is_idle():
if self.server_args.enable_dp_attention:
batch.capture_hidden_mode = CaptureHiddenMode.FULL
self.target_worker.forward_batch_generation(batch)
return self._decode_idle_result(on_publish=on_publish)
batch.capture_hidden_mode = CaptureHiddenMode.FULL
batch_output = self.target_worker.forward_batch_generation(batch)
logits_output = batch_output.logits_output
next_token_ids = batch_output.next_token_ids
batch_output.new_seq_lens = batch.seq_lens
if on_publish is not None:
on_publish(batch_output.new_seq_lens)
if logits_output.hidden_states is None:
raise RuntimeError(
"DSpark requires target aux hidden capture for prefill, but got None. "
"Make sure the target model has DFlash layers-to-capture configured."
)
if batch.extend_lens is None or batch.prefix_lens is None:
raise RuntimeError(
"DSpark expected extend_lens / prefix_lens in extend mode, got None."
)
if batch.out_cache_loc is None:
raise RuntimeError("DSpark prefill expected out_cache_loc, but got None.")
device = next_token_ids.device
ctx_lens = torch.tensor(batch.extend_lens, dtype=torch.int32, device=device)
draft_seq_lens = torch.tensor(
batch.prefix_lens, dtype=torch.int32, device=device
)
positions, _ = compute_position(
self.model_runner.server_args.attention_backend,
draft_seq_lens,
ctx_lens,
int(sum(batch.extend_lens)),
)
self._kv_injector.inject_target_hidden(
target_hidden=logits_output.hidden_states,
cache_loc=batch.out_cache_loc,
positions=positions,
)
logits_output.hidden_states = None
batch_output.next_draft_input = make_next_draft_input(
bonus_tokens=next_token_ids,
new_seq_lens=batch.seq_lens,
)
return batch_output
def _idle_verify_ragged_layout(self, batch: ScheduleBatch):
if batch.global_num_tokens is None or not self._verify_planner.is_compact_mode:
return None
global_bs = max(batch.global_num_tokens)
if global_bs <= 0:
return None
return idle_ragged_layout(
tier_num_reqs=global_bs,
dp_tier_num_tokens=self._dp_verify_tier_num_tokens(batch),
device=self.device,
verify_num_draft_tokens=self.verify_num_draft_tokens,
model_runner=self.model_runner,
)
def _dp_verify_tier_num_tokens(self, batch: ScheduleBatch) -> Optional[int]:
if not (
self._draft_is_moe
and self.server_args.enable_dp_attention
and batch.global_num_tokens is not None
and self._verify_planner.is_compact_mode
):
return None
return dp_global_verify_tier_num_tokens(
global_tier_num_tokens=batch.global_spec_verify_tier_num_tokens
)
def _decode_idle_result(
self,
*,
on_publish,
) -> GenerationBatchResult:
next_draft_input = make_next_draft_input(
bonus_tokens=torch.empty((0,), device=self.device, dtype=torch.int64),
new_seq_lens=torch.empty((0,), device=self.device, dtype=torch.int64),
)
if on_publish is not None:
on_publish(next_draft_input.new_seq_lens)
return GenerationBatchResult(
logits_output=None,
next_token_ids=torch.empty((0,), dtype=torch.int64, device=self.device),
accept_lens=torch.empty((0,), dtype=torch.int32, device=self.device),
block_accept_lens=torch.empty((0,), dtype=torch.int32, device=self.device),
next_draft_input=next_draft_input,
can_run_cuda_graph=False,
speculative_num_draft_tokens=int(self.verify_num_draft_tokens),
new_seq_lens=next_draft_input.new_seq_lens,
)
def _forward_decode(
self, batch: ScheduleBatch, on_publish
) -> GenerationBatchResult:
if batch.spec_info is None:
batch.spec_info = DFlashDraftInputV2.create_idle_input(device=self.device)
draft_input = batch.spec_info
if not isinstance(draft_input, DFlashDraftInputV2):
raise RuntimeError(
"DSpark spec-v2 expected DFlashDraftInputV2 state on the running batch."
)
if batch.forward_mode.is_idle():
self._observers.note_idle_decode_step()
if self.server_args.enable_dp_attention:
if self._draft_is_moe:
self._proposer.run_idle_participation(batch)
self._verify_executor.run_idle_participation(
batch=batch, idle_layout=self._idle_verify_ragged_layout(batch)
)
return self._decode_idle_result(on_publish=on_publish)
batch.seq_lens.record_stream(
torch.get_device_module(self.device).current_stream()
)
bs = len(batch.seq_lens)
device = self.device
prefix_lens = batch.seq_lens
self._observers.begin_step()
target_model = self.target_worker.model_runner.model
verify_window = alloc_verify_window(
batch=batch,
bs=bs,
device=device,
verify_num_draft_tokens=self.verify_num_draft_tokens,
block_pos_offsets=self._block_pos_offsets,
model_runner=self.model_runner,
)
sampling_info = batch.sampling_info
with self._draft_context(), self._observers.segment(InfoSegment.DRAFT):
proposal = self._proposer.propose(
batch=batch,
draft_input=draft_input,
verify_window=verify_window,
bs=bs,
device=device,
target_model=target_model,
sampling_info=sampling_info,
)
draft_block_ids = proposal.draft_block_ids
draft_block = proposal.draft_block
draft_tokens = draft_block.draft_tokens
confidence = proposal.confidence
if confidence is None:
confidence = self._verify_planner.compute_confidence_tensor(
draft_hidden=proposal.draft_hidden,
anchor_tokens=draft_block_ids[:, 0],
draft_tokens=draft_tokens,
confidence_tap=proposal.confidence_tap,
)
verify_token_budget = self._verify_planner.resolve_verify_token_budget(
draft_input=draft_input,
confidence=confidence,
prefix_lens=prefix_lens,
req_pool_indices=batch.req_pool_indices,
)
global_num_reqs = (
max(batch.global_num_tokens)
if self._draft_is_moe
and self.server_args.enable_dp_attention
and batch.global_num_tokens is not None
else None
)
layout = self._verify_planner.schedule_layout(
req_pool_indices=batch.req_pool_indices,
prefix_lens=prefix_lens,
device=device,
confidence=confidence,
budget=verify_token_budget,
global_num_reqs=global_num_reqs,
dp_tier_num_tokens=self._dp_verify_tier_num_tokens(batch),
)
run_compact = self._verify_planner.should_run_compact(layout=layout)
verify_ids_2d = torch.cat(
[draft_block_ids[:, :1], draft_tokens], dim=1
).contiguous()
fold_eligible = (
self._verify_executor.verify_epilogue is not None
and proposal.folded
and verify_logits_adjustments_are_noop(sampling_info)
and self._simulate_acc_len <= 0
)
with self._observers.segment(InfoSegment.TARGET_VERIFY):
if run_compact:
target_verify, hidden_strided = self._verify_executor.run_compact(
batch=batch,
layout=layout,
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
bs=bs,
device=device,
sampling_info=sampling_info,
inject_gate=fold_eligible,
)
else:
target_verify = self._verify_executor.run_non_compact(
batch=batch,
draft_input=draft_input,
verify_ids_2d=verify_ids_2d,
verify_window=verify_window,
sampling_info=sampling_info,
)
hidden_strided = None
logits_output = target_verify.logits_output
can_run_cuda_graph = target_verify.can_run_cuda_graph
epilogue = self._verify_executor.verify_epilogue
folded_accept = fold_eligible and run_compact and can_run_cuda_graph
accept = self._verify_executor.accept_and_finalize(
folded_accept=folded_accept,
bs=bs,
verify_ids_2d=verify_ids_2d,
target_logits=logits_output.next_token_logits,
draft_block=draft_block,
sampling_info=sampling_info,
draft_input=draft_input,
layout=layout,
prefix_lens=prefix_lens,
draft_tokens=draft_tokens,
)
if on_publish is not None:
if confidence is not None:
on_publish(accept.new_seq_lens, confidence=confidence)
else:
on_publish(accept.new_seq_lens)
folded_commit = folded_accept and epilogue.folds_commit
if not folded_commit:
self._verify_executor.commit_hidden(
batch=batch,
layout=layout,
hidden_strided=hidden_strided,
verify_window=verify_window,
logits_output=logits_output,
commit_lens=accept.commit_lens,
bs=bs,
run_compact=run_compact,
)
logits_output.hidden_states = None
self._observers.observe_verify_step(
forward_ct=int(batch.forward_iter),
reqs=batch.reqs,
bs=bs,
proposal_folded=proposal.folded,
verify_ids_2d=verify_ids_2d,
target_logits=logits_output.next_token_logits,
layout=layout,
confidence=confidence,
prefix_lens=prefix_lens,
draft_tokens=draft_tokens,
draft_block=draft_block,
sampling_info=sampling_info,
correct_len=accept.correct_len,
cap_trim_lens=accept.cap_trim_lens,
bonus=accept.bonus,
commit_lens=accept.commit_lens,
verify_token_budget=verify_token_budget,
req_pool_indices=batch.req_pool_indices,
verify_tier_num_tokens=int(batch.spec_verify_tier_num_tokens),
dp_tier_num_tokens=self._dp_verify_tier_num_tokens(batch),
)
next_draft_input = make_next_draft_input(
bonus_tokens=accept.bonus,
new_seq_lens=accept.new_seq_lens,
)
return GenerationBatchResult(
logits_output=logits_output,
next_token_ids=accept.out_tokens.reshape(-1),
accept_lens=accept.commit_lens,
block_accept_lens=accept.commit_lens + accept.cap_trim_lens,
cap_lens=(
layout.verify_lens.to(torch.int32) if layout is not None else None
),
can_run_cuda_graph=can_run_cuda_graph,
next_draft_input=next_draft_input,
speculative_num_draft_tokens=int(self.verify_num_draft_tokens),
new_seq_lens=accept.new_seq_lens,
)
def get_confidence_budget_prepare(self):
return self._verify_planner.confidence_budget_prepare()
@@ -0,0 +1,14 @@
from __future__ import annotations
import torch
def inputs_on_cuda(*args, **kwargs) -> bool:
"""Route kernel dispatch by input placement: the first tensor argument
decides. CUDA inputs take the fused triton kernel; CPU inputs take the
torch reference implementation (triton is CUDA-only, and CPU-side callers
such as unit tests exercise the reference path)."""
for value in (*args, *kwargs.values()):
if isinstance(value, torch.Tensor):
return value.is_cuda
raise AssertionError("kernel dispatch requires at least one tensor argument")
@@ -0,0 +1,862 @@
from __future__ import annotations
from typing import Optional
import msgspec
import torch
import triton
import triton.language as tl
from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
from sglang.srt.speculative.dflash_utils import (
_get_or_create_chain_verify_buffers,
build_dflash_verify_target_probs,
compute_dflash_correct_drafts_and_bonus,
)
from sglang.srt.speculative.dspark_components.kernels.dispatch import inputs_on_cuda
from sglang.srt.speculative.reject_sampling import chain_speculative_sampling_triton
class AcceptSampling:
@classmethod
def execute(
cls, *args, **kwargs
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
draft_probs: torch.Tensor,
sampling_info,
draft_input: DFlashDraftInputV2,
gamma: int,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return accept_sampling(
candidates=candidates,
target_logits=target_logits,
draft_probs=draft_probs,
sampling_info=sampling_info,
draft_input=draft_input,
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
@classmethod
def triton(
cls,
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
draft_probs: torch.Tensor,
sampling_info,
draft_input: DFlashDraftInputV2,
gamma: int,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return accept_sampling_triton(
candidates=candidates,
target_logits=target_logits,
draft_probs=draft_probs,
sampling_info=sampling_info,
draft_input=draft_input,
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
def _accept_sampling_core(
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
draft_probs: torch.Tensor,
sampling_info,
draft_input: DFlashDraftInputV2,
gamma: int,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor],
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
bs = candidates.shape[0]
device = candidates.device
if not sampling_info.need_top_k_sampling and not sampling_info.need_top_p_sampling:
target_probs = SoftmaxTemp.execute(
logits=target_logits,
temperatures=sampling_info.temperatures,
rows_per_request=verify_num_draft_tokens,
).view(bs, verify_num_draft_tokens, -1)
else:
target_probs = build_dflash_verify_target_probs(
next_token_logits=target_logits,
sampling_info=sampling_info,
draft_token_num=verify_num_draft_tokens,
bs=bs,
max_top_k=draft_input.max_top_k,
uniform_top_k_value=draft_input.uniform_top_k_value,
)
(
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
predicts,
accept_index,
accept_token_num,
) = _get_or_create_chain_verify_buffers(
bs=bs,
draft_token_num=verify_num_draft_tokens,
device=device,
)
uniform_samples = torch.rand((bs, gamma), dtype=torch.float32, device=device)
uniform_samples_final = torch.rand((bs,), dtype=torch.float32, device=device)
chain_speculative_sampling_triton(
predicts=predicts,
accept_index=accept_index,
accept_token_num=accept_token_num,
candidates=candidates,
retrive_index=retrieve_index,
retrive_next_token=retrieve_next_token,
retrive_next_sibling=retrieve_next_sibling,
uniform_samples=uniform_samples,
uniform_samples_for_final_sampling=uniform_samples_final,
target_probs=target_probs,
draft_probs=draft_probs,
threshold_single=1.0,
threshold_acc=1.0,
deterministic=True,
)
correct_len = accept_token_num
if cutoff_verify_lens is not None:
correct_len, cap_trim_lens = CapCorrectLen.execute(
correct_len=correct_len, verify_lens=cutoff_verify_lens
)
else:
cap_trim_lens = torch.zeros_like(correct_len)
return correct_len, cap_trim_lens, accept_index, predicts
def accept_sampling(
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
draft_probs: torch.Tensor,
sampling_info,
draft_input: DFlashDraftInputV2,
gamma: int,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
bs = candidates.shape[0]
device = candidates.device
correct_len, cap_trim_lens, accept_index, predicts = _accept_sampling_core(
candidates=candidates,
target_logits=target_logits,
draft_probs=draft_probs,
sampling_info=sampling_info,
draft_input=draft_input,
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
row_ids = torch.arange(bs, dtype=torch.long, device=device)
accept_pos = accept_index[row_ids, correct_len.to(torch.long)].to(torch.long)
bonus = predicts[accept_pos].to(torch.int64)
return correct_len, bonus, cap_trim_lens
@triton.jit
def _gather_two_level_bonus_kernel(
accept_index_ptr,
predicts_ptr,
correct_len_ptr,
out_ptr,
cols,
n,
BLOCK: tl.constexpr,
):
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
cl = tl.load(correct_len_ptr + offs, mask=mask, other=0).to(tl.int64)
accept_pos = tl.load(accept_index_ptr + offs * cols + cl, mask=mask, other=0).to(
tl.int64
)
bonus = tl.load(predicts_ptr + accept_pos, mask=mask, other=0)
tl.store(out_ptr + offs, bonus.to(tl.int64), mask=mask)
def gather_two_level_bonus_triton(
*,
accept_index: torch.Tensor,
predicts: torch.Tensor,
correct_len: torch.Tensor,
) -> torch.Tensor:
bs, cols = accept_index.shape
accept_index = accept_index.contiguous()
predicts = predicts.contiguous()
correct_len = correct_len.contiguous()
out = torch.empty(bs, dtype=torch.int64, device=accept_index.device)
BLOCK = 256
grid = (triton.cdiv(bs, BLOCK),)
_gather_two_level_bonus_kernel[grid](
accept_index, predicts, correct_len, out, cols, bs, BLOCK=BLOCK
)
return out
def accept_sampling_triton(
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
draft_probs: torch.Tensor,
sampling_info,
draft_input: DFlashDraftInputV2,
gamma: int,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
correct_len, cap_trim_lens, accept_index, predicts = _accept_sampling_core(
candidates=candidates,
target_logits=target_logits,
draft_probs=draft_probs,
sampling_info=sampling_info,
draft_input=draft_input,
gamma=gamma,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
bonus = gather_two_level_bonus_triton(
accept_index=accept_index, predicts=predicts, correct_len=correct_len
)
return correct_len, bonus, cap_trim_lens
try:
from flashinfer.sampling import softmax as _flashinfer_softmax
except ImportError:
_flashinfer_softmax = None
class SoftmaxTemp:
@classmethod
def execute(cls, *args, **kwargs) -> torch.Tensor:
if not inputs_on_cuda(*args, **kwargs):
return cls.torch(*args, **kwargs)
if _flashinfer_softmax is not None:
return cls.flashinfer(*args, **kwargs)
return cls.triton(*args, **kwargs)
@classmethod
def torch(
cls,
*,
logits: torch.Tensor,
temperatures: torch.Tensor,
rows_per_request: int,
) -> torch.Tensor:
return softmax_temp(
logits=logits,
temperatures=temperatures,
rows_per_request=rows_per_request,
)
@classmethod
def triton(
cls,
*,
logits: torch.Tensor,
temperatures: torch.Tensor,
rows_per_request: int,
) -> torch.Tensor:
return softmax_temp_triton(
logits=logits,
temperatures=temperatures,
rows_per_request=rows_per_request,
)
@classmethod
def flashinfer(
cls,
*,
logits: torch.Tensor,
temperatures: torch.Tensor,
rows_per_request: int,
) -> torch.Tensor:
return softmax_temp_flashinfer(
logits=logits,
temperatures=temperatures,
rows_per_request=rows_per_request,
)
def softmax_temp(
*,
logits: torch.Tensor,
temperatures: torch.Tensor,
rows_per_request: int,
) -> torch.Tensor:
num_rows = logits.shape[0]
bs = num_rows // rows_per_request
assert (
bs * rows_per_request == num_rows
), f"num_rows {num_rows} not divisible by rows_per_request {rows_per_request}"
temp_per_row = torch.repeat_interleave(
temperatures.reshape(bs).to(torch.float32), rows_per_request, dim=0
)
scaled = logits.to(torch.float32) / temp_per_row[:, None]
return torch.softmax(scaled, dim=-1)
@triton.jit
def _softmax_temp_kernel(
logits_ptr,
temp_ptr,
out_ptr,
vocab,
rows_per_request,
logits_row_stride,
BLOCK_V: tl.constexpr,
):
row = tl.program_id(0)
temp = tl.load(temp_ptr + row // rows_per_request).to(tl.float32)
base = logits_ptr + row.to(tl.int64) * logits_row_stride
out_base = out_ptr + row.to(tl.int64) * vocab
row_max = -float("inf")
for v0 in range(0, vocab, BLOCK_V):
offs = v0 + tl.arange(0, BLOCK_V)
vmask = offs < vocab
x = tl.load(base + offs, mask=vmask, other=-float("inf")).to(tl.float32)
x = x / temp
row_max = tl.maximum(row_max, tl.max(x, axis=0))
sum_exp = 0.0
for v0 in range(0, vocab, BLOCK_V):
offs = v0 + tl.arange(0, BLOCK_V)
vmask = offs < vocab
x = tl.load(base + offs, mask=vmask, other=-float("inf")).to(tl.float32)
x = x / temp
e = tl.exp(x - row_max)
e = tl.where(vmask, e, 0.0)
sum_exp += tl.sum(e, axis=0)
for v0 in range(0, vocab, BLOCK_V):
offs = v0 + tl.arange(0, BLOCK_V)
vmask = offs < vocab
x = tl.load(base + offs, mask=vmask, other=-float("inf")).to(tl.float32)
x = x / temp
e = tl.exp(x - row_max)
tl.store(out_base + offs, e / sum_exp, mask=vmask)
def softmax_temp_triton(
*,
logits: torch.Tensor,
temperatures: torch.Tensor,
rows_per_request: int,
) -> torch.Tensor:
num_rows, vocab = logits.shape[0], logits.shape[-1]
bs = num_rows // rows_per_request
assert (
bs * rows_per_request == num_rows
), f"num_rows {num_rows} not divisible by rows_per_request {rows_per_request}"
temperatures = temperatures.reshape(bs).to(torch.float32).contiguous()
out = torch.empty((num_rows, vocab), dtype=torch.float32, device=logits.device)
BLOCK_V = 4096
_softmax_temp_kernel[(num_rows,)](
logits,
temperatures,
out,
vocab,
rows_per_request,
logits.stride(0),
BLOCK_V=BLOCK_V,
)
return out
def softmax_temp_flashinfer(
*,
logits: torch.Tensor,
temperatures: torch.Tensor,
rows_per_request: int,
) -> torch.Tensor:
if _flashinfer_softmax is None:
raise RuntimeError(
"softmax_temp_flashinfer requires flashinfer.sampling.softmax, "
"which is unavailable in this environment"
)
num_rows, vocab = logits.shape[0], logits.shape[-1]
bs = num_rows // rows_per_request
assert (
bs * rows_per_request == num_rows
), f"num_rows {num_rows} not divisible by rows_per_request {rows_per_request}"
temp_per_row = torch.repeat_interleave(
temperatures.reshape(bs).to(torch.float32), rows_per_request, dim=0
).contiguous()
logits_2d = logits.to(torch.float32).contiguous()
return _flashinfer_softmax(logits=logits_2d, temperature=temp_per_row)
class MixedAcceptSelectResult(msgspec.Struct):
correct_len: torch.Tensor
bonus: torch.Tensor
cap_trim_lens: torch.Tensor
class SelectMixedAccept:
@classmethod
def execute(cls, *args, **kwargs) -> MixedAcceptSelectResult:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
greedy_mask: torch.Tensor,
greedy_len: torch.Tensor,
greedy_bonus: torch.Tensor,
greedy_trim: torch.Tensor,
sampling_len: torch.Tensor,
sampling_bonus: torch.Tensor,
sampling_trim: torch.Tensor,
) -> MixedAcceptSelectResult:
return select_mixed_accept(
greedy_mask=greedy_mask,
greedy_len=greedy_len,
greedy_bonus=greedy_bonus,
greedy_trim=greedy_trim,
sampling_len=sampling_len,
sampling_bonus=sampling_bonus,
sampling_trim=sampling_trim,
)
@classmethod
def triton(
cls,
*,
greedy_mask: torch.Tensor,
greedy_len: torch.Tensor,
greedy_bonus: torch.Tensor,
greedy_trim: torch.Tensor,
sampling_len: torch.Tensor,
sampling_bonus: torch.Tensor,
sampling_trim: torch.Tensor,
) -> MixedAcceptSelectResult:
return select_mixed_accept_triton(
greedy_mask=greedy_mask,
greedy_len=greedy_len,
greedy_bonus=greedy_bonus,
greedy_trim=greedy_trim,
sampling_len=sampling_len,
sampling_bonus=sampling_bonus,
sampling_trim=sampling_trim,
)
def select_mixed_accept(
*,
greedy_mask: torch.Tensor,
greedy_len: torch.Tensor,
greedy_bonus: torch.Tensor,
greedy_trim: torch.Tensor,
sampling_len: torch.Tensor,
sampling_bonus: torch.Tensor,
sampling_trim: torch.Tensor,
) -> MixedAcceptSelectResult:
correct_len = torch.where(
greedy_mask, greedy_len.to(sampling_len.dtype), sampling_len
)
bonus = torch.where(greedy_mask, greedy_bonus, sampling_bonus)
cap_trim_lens = torch.where(
greedy_mask, greedy_trim.to(sampling_trim.dtype), sampling_trim
)
return MixedAcceptSelectResult(
correct_len=correct_len, bonus=bonus, cap_trim_lens=cap_trim_lens
)
@triton.jit
def _mixed_accept_select_kernel(
greedy_mask_ptr,
greedy_len_ptr,
greedy_bonus_ptr,
greedy_trim_ptr,
sampling_len_ptr,
sampling_bonus_ptr,
sampling_trim_ptr,
correct_len_ptr,
bonus_ptr,
cap_trim_ptr,
bs,
BLOCK: tl.constexpr,
):
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
mask = offs < bs
is_greedy = tl.load(greedy_mask_ptr + offs, mask=mask, other=0) != 0
g_len = tl.load(greedy_len_ptr + offs, mask=mask, other=0)
s_len = tl.load(sampling_len_ptr + offs, mask=mask, other=0)
tl.store(correct_len_ptr + offs, tl.where(is_greedy, g_len, s_len), mask=mask)
g_bonus = tl.load(greedy_bonus_ptr + offs, mask=mask, other=0)
s_bonus = tl.load(sampling_bonus_ptr + offs, mask=mask, other=0)
tl.store(bonus_ptr + offs, tl.where(is_greedy, g_bonus, s_bonus), mask=mask)
g_trim = tl.load(greedy_trim_ptr + offs, mask=mask, other=0)
s_trim = tl.load(sampling_trim_ptr + offs, mask=mask, other=0)
tl.store(cap_trim_ptr + offs, tl.where(is_greedy, g_trim, s_trim), mask=mask)
def select_mixed_accept_triton(
*,
greedy_mask: torch.Tensor,
greedy_len: torch.Tensor,
greedy_bonus: torch.Tensor,
greedy_trim: torch.Tensor,
sampling_len: torch.Tensor,
sampling_bonus: torch.Tensor,
sampling_trim: torch.Tensor,
) -> MixedAcceptSelectResult:
bs = greedy_mask.shape[0]
device = greedy_mask.device
correct_len = torch.empty(bs, dtype=sampling_len.dtype, device=device)
bonus = torch.empty(bs, dtype=sampling_bonus.dtype, device=device)
cap_trim_lens = torch.empty(bs, dtype=sampling_trim.dtype, device=device)
BLOCK = 256
_mixed_accept_select_kernel[(triton.cdiv(bs, BLOCK),)](
greedy_mask,
greedy_len,
greedy_bonus,
greedy_trim,
sampling_len,
sampling_bonus,
sampling_trim,
correct_len,
bonus,
cap_trim_lens,
bs,
BLOCK=BLOCK,
)
return MixedAcceptSelectResult(
correct_len=correct_len, bonus=bonus, cap_trim_lens=cap_trim_lens
)
class AcceptGreedy:
@classmethod
def execute(
cls, *args, **kwargs
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return accept_greedy(
candidates=candidates,
target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
@classmethod
def triton(
cls,
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return accept_greedy_triton(
candidates=candidates,
target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens,
)
def accept_greedy(
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
bs = candidates.shape[0]
target_predict = torch.argmax(target_logits, dim=-1).view(
bs, verify_num_draft_tokens
)
correct_len, bonus = compute_dflash_correct_drafts_and_bonus(
candidates=candidates,
target_predict=target_predict,
)
cap_trim_lens = torch.zeros_like(correct_len)
if cutoff_verify_lens is not None:
correct_len, cap_trim_lens = CapCorrectLen.execute(
correct_len=correct_len, verify_lens=cutoff_verify_lens
)
row_ids = torch.arange(bs, device=target_predict.device)
bonus = target_predict[row_ids, correct_len.to(torch.long)].to(torch.int64)
return correct_len, bonus, cap_trim_lens
@triton.jit
def _gather_row_bonus_kernel(
table_ptr,
idx_ptr,
out_ptr,
cols,
n,
BLOCK: tl.constexpr,
):
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
idx = tl.load(idx_ptr + offs, mask=mask, other=0).to(tl.int64)
val = tl.load(table_ptr + offs * cols + idx, mask=mask, other=0)
tl.store(out_ptr + offs, val.to(tl.int64), mask=mask)
def gather_row_bonus_triton(*, table: torch.Tensor, idx: torch.Tensor) -> torch.Tensor:
bs, cols = table.shape
table = table.contiguous()
idx = idx.contiguous()
out = torch.empty(bs, dtype=torch.int64, device=table.device)
BLOCK = 256
grid = (triton.cdiv(bs, BLOCK),)
_gather_row_bonus_kernel[grid](table, idx, out, cols, bs, BLOCK=BLOCK)
return out
def accept_greedy_triton(
*,
candidates: torch.Tensor,
target_logits: torch.Tensor,
verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
bs = candidates.shape[0]
target_predict = torch.argmax(target_logits, dim=-1).view(
bs, verify_num_draft_tokens
)
correct_len, bonus = compute_dflash_correct_drafts_and_bonus(
candidates=candidates,
target_predict=target_predict,
)
cap_trim_lens = torch.zeros_like(correct_len)
if cutoff_verify_lens is not None:
correct_len, cap_trim_lens = CapCorrectLen.execute(
correct_len=correct_len, verify_lens=cutoff_verify_lens
)
bonus = gather_row_bonus_triton(table=target_predict, idx=correct_len)
return correct_len, bonus, cap_trim_lens
class FinalizeAcceptLensResult(msgspec.Struct):
commit_lens: torch.Tensor
new_seq_lens: torch.Tensor
cap_trim_lens: torch.Tensor
class FinalizeAcceptLens:
@classmethod
def execute(cls, *args, **kwargs) -> FinalizeAcceptLensResult:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
prefix_lens: torch.Tensor,
) -> FinalizeAcceptLensResult:
return finalize_accept_lens(
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
prefix_lens=prefix_lens,
)
@classmethod
def triton(
cls,
*,
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
prefix_lens: torch.Tensor,
) -> FinalizeAcceptLensResult:
return finalize_accept_lens_triton(
correct_len=correct_len,
cap_trim_lens=cap_trim_lens,
prefix_lens=prefix_lens,
)
def finalize_accept_lens(
*,
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
prefix_lens: torch.Tensor,
) -> FinalizeAcceptLensResult:
commit_lens = correct_len.to(torch.int32) + 1
new_seq_lens = prefix_lens + commit_lens.to(prefix_lens.dtype)
return FinalizeAcceptLensResult(
commit_lens=commit_lens,
new_seq_lens=new_seq_lens,
cap_trim_lens=cap_trim_lens.to(torch.int32),
)
@triton.jit
def _finalize_accept_lens_kernel(
correct_len_ptr,
cap_trim_ptr,
prefix_lens_ptr,
commit_lens_ptr,
new_seq_lens_ptr,
cap_trim_out_ptr,
bs,
BLOCK: tl.constexpr,
):
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
mask = offs < bs
commit = tl.load(correct_len_ptr + offs, mask=mask, other=0).to(tl.int32) + 1
prefix = tl.load(prefix_lens_ptr + offs, mask=mask, other=0)
trim = tl.load(cap_trim_ptr + offs, mask=mask, other=0).to(tl.int32)
tl.store(commit_lens_ptr + offs, commit, mask=mask)
tl.store(new_seq_lens_ptr + offs, prefix + commit, mask=mask)
tl.store(cap_trim_out_ptr + offs, trim, mask=mask)
def finalize_accept_lens_triton(
*,
correct_len: torch.Tensor,
cap_trim_lens: torch.Tensor,
prefix_lens: torch.Tensor,
) -> FinalizeAcceptLensResult:
bs = correct_len.shape[0]
device = correct_len.device
commit_lens = torch.empty(bs, dtype=torch.int32, device=device)
new_seq_lens = torch.empty(bs, dtype=prefix_lens.dtype, device=device)
cap_trim_out = torch.empty(bs, dtype=torch.int32, device=device)
BLOCK = 256
_finalize_accept_lens_kernel[(triton.cdiv(bs, BLOCK),)](
correct_len,
cap_trim_lens,
prefix_lens,
commit_lens,
new_seq_lens,
cap_trim_out,
bs,
BLOCK=BLOCK,
)
return FinalizeAcceptLensResult(
commit_lens=commit_lens,
new_seq_lens=new_seq_lens,
cap_trim_lens=cap_trim_out,
)
class CapCorrectLen:
@classmethod
def execute(cls, *args, **kwargs) -> tuple[torch.Tensor, torch.Tensor]:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
correct_len: torch.Tensor,
verify_lens: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
return cap_correct_len(
correct_len=correct_len,
verify_lens=verify_lens,
)
@classmethod
def triton(
cls,
*,
correct_len: torch.Tensor,
verify_lens: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
return cap_correct_len_triton(
correct_len=correct_len,
verify_lens=verify_lens,
)
def cap_correct_len(
*,
correct_len: torch.Tensor,
verify_lens: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
ell_r = (verify_lens.to(device=correct_len.device) - 1).to(correct_len.dtype)
capped = torch.minimum(correct_len, ell_r)
cap_trim_lens = correct_len - capped
return capped, cap_trim_lens
@triton.jit
def _cap_correct_len_kernel(
correct_len_ptr,
verify_lens_ptr,
capped_ptr,
trim_ptr,
n,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
cl = tl.load(correct_len_ptr + offs, mask=mask, other=0).to(tl.int64)
vl = tl.load(verify_lens_ptr + offs, mask=mask, other=0).to(tl.int64)
ell = vl - 1
capped = tl.minimum(cl, ell)
trim = cl - capped
tl.store(capped_ptr + offs, capped, mask=mask)
tl.store(trim_ptr + offs, trim, mask=mask)
def cap_correct_len_triton(
*,
correct_len: torch.Tensor,
verify_lens: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
device = correct_len.device
correct_len = correct_len.contiguous()
verify_lens = verify_lens.to(device=device).contiguous()
n = correct_len.shape[0]
capped = torch.empty_like(correct_len)
trim = torch.empty_like(correct_len)
BLOCK = 1024
grid = (triton.cdiv(n, BLOCK),)
_cap_correct_len_kernel[grid](
correct_len, verify_lens, capped, trim, n, BLOCK=BLOCK
)
return capped, trim
@@ -0,0 +1,491 @@
from __future__ import annotations
from typing import Tuple
import msgspec
import torch
import triton
import triton.language as tl
from sglang.srt.speculative.dspark_components.kernels.dispatch import inputs_on_cuda
from sglang.srt.utils import ceil_align
class DsparkWindowGather(msgspec.Struct, frozen=True):
num_q: int
bs: int
context_lens: torch.Tensor
req_pool_indices_per_request: torch.Tensor
offsets: torch.Tensor
invalid: torch.Tensor
class ComputeDsparkWindowGather:
@classmethod
def execute(cls, *args, **kwargs) -> DsparkWindowGather:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
seq_lens_casual: torch.Tensor,
req_pool_indices_repeated: torch.Tensor,
block_size: int,
swa_window: int,
) -> DsparkWindowGather:
return compute_dspark_window_gather(
seq_lens_casual=seq_lens_casual,
req_pool_indices_repeated=req_pool_indices_repeated,
block_size=block_size,
swa_window=swa_window,
)
@classmethod
def triton(
cls,
*,
seq_lens_casual: torch.Tensor,
req_pool_indices_repeated: torch.Tensor,
block_size: int,
swa_window: int,
) -> DsparkWindowGather:
return compute_dspark_window_gather_triton(
seq_lens_casual=seq_lens_casual,
req_pool_indices_repeated=req_pool_indices_repeated,
block_size=block_size,
swa_window=swa_window,
)
class BuildDsparkSwaPageIndices:
@classmethod
def execute(cls, *args, **kwargs) -> Tuple[torch.Tensor, torch.Tensor]:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
req_to_token: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
req_pool_indices_per_request: torch.Tensor,
offsets: torch.Tensor,
invalid: torch.Tensor,
out_loc: torch.Tensor,
context_lens: torch.Tensor,
block_size: int,
swa_window: int,
page_index_aligned_size: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
return build_dspark_swa_page_indices(
req_to_token=req_to_token,
full_to_swa_mapping=full_to_swa_mapping,
req_pool_indices_per_request=req_pool_indices_per_request,
offsets=offsets,
invalid=invalid,
out_loc=out_loc,
context_lens=context_lens,
block_size=block_size,
swa_window=swa_window,
page_index_aligned_size=page_index_aligned_size,
)
@classmethod
def triton(
cls,
*,
req_to_token: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
req_pool_indices_per_request: torch.Tensor,
offsets: torch.Tensor,
invalid: torch.Tensor,
out_loc: torch.Tensor,
context_lens: torch.Tensor,
block_size: int,
swa_window: int,
page_index_aligned_size: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
return build_dspark_swa_page_indices_triton(
req_to_token=req_to_token,
full_to_swa_mapping=full_to_swa_mapping,
req_pool_indices_per_request=req_pool_indices_per_request,
offsets=offsets,
out_loc=out_loc,
context_lens=context_lens,
block_size=block_size,
swa_window=swa_window,
page_index_aligned_size=page_index_aligned_size,
)
def compute_dspark_window_gather(
*,
seq_lens_casual: torch.Tensor,
req_pool_indices_repeated: torch.Tensor,
block_size: int,
swa_window: int,
) -> DsparkWindowGather:
seq_lens_casual = seq_lens_casual.to(torch.int32)
num_q = seq_lens_casual.size(0)
assert num_q % block_size == 0, (
f"DSpark draft block forward must be uniform-gamma: num_q={num_q} not "
f"divisible by block_size={block_size}."
)
bs = num_q // block_size
device = seq_lens_casual.device
first_token = torch.arange(bs, device=device, dtype=torch.int64) * block_size
prefix_lens = (seq_lens_casual[first_token] - 1).to(torch.int32)
context_lens = torch.clamp(prefix_lens, max=swa_window).to(torch.int32)
req_pool_indices_per_request = req_pool_indices_repeated[first_token]
offsets = (
prefix_lens.to(torch.int64).unsqueeze(1)
- swa_window
+ torch.arange(swa_window, device=device, dtype=torch.int64).unsqueeze(0)
)
invalid = offsets < 0
offsets = offsets.clamp(min=0)
return DsparkWindowGather(
num_q=num_q,
bs=bs,
context_lens=context_lens,
req_pool_indices_per_request=req_pool_indices_per_request,
offsets=offsets,
invalid=invalid,
)
@triton.jit
def _window_gather_kernel(
seq_lens_casual_ptr,
req_pool_rep_ptr,
context_lens_ptr,
req_pool_out_ptr,
offsets_ptr,
invalid_ptr,
block_size,
swa_window,
W_BLOCK: tl.constexpr,
):
i = tl.program_id(0)
ft = i * block_size
prefix = tl.load(seq_lens_casual_ptr + ft).to(tl.int64) - 1
tl.store(context_lens_ptr + i, tl.minimum(prefix, swa_window).to(tl.int32))
tl.store(req_pool_out_ptr + i, tl.load(req_pool_rep_ptr + ft))
col = tl.arange(0, W_BLOCK)
cmask = col < swa_window
off = prefix - swa_window + col
tl.store(invalid_ptr + i * swa_window + col, off < 0, mask=cmask)
tl.store(offsets_ptr + i * swa_window + col, tl.maximum(off, 0), mask=cmask)
def compute_dspark_window_gather_triton(
*,
seq_lens_casual: torch.Tensor,
req_pool_indices_repeated: torch.Tensor,
block_size: int,
swa_window: int,
) -> DsparkWindowGather:
seq_lens_casual = seq_lens_casual.to(torch.int32).contiguous()
num_q = seq_lens_casual.size(0)
assert num_q % block_size == 0, (
f"DSpark draft block forward must be uniform-gamma: num_q={num_q} not "
f"divisible by block_size={block_size}."
)
bs = num_q // block_size
device = seq_lens_casual.device
req_pool_indices_repeated = req_pool_indices_repeated.to(device=device).contiguous()
context_lens = torch.empty(bs, dtype=torch.int32, device=device)
req_pool_out = torch.empty(bs, dtype=req_pool_indices_repeated.dtype, device=device)
offsets = torch.empty((bs, swa_window), dtype=torch.int64, device=device)
invalid = torch.empty((bs, swa_window), dtype=torch.bool, device=device)
W_BLOCK = triton.next_power_of_2(swa_window)
_window_gather_kernel[(bs,)](
seq_lens_casual,
req_pool_indices_repeated,
context_lens,
req_pool_out,
offsets,
invalid,
block_size,
swa_window,
W_BLOCK=W_BLOCK,
)
return DsparkWindowGather(
num_q=num_q,
bs=bs,
context_lens=context_lens,
req_pool_indices_per_request=req_pool_out,
offsets=offsets,
invalid=invalid,
)
def build_dspark_swa_page_indices(
*,
req_to_token: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
req_pool_indices_per_request: torch.Tensor,
offsets: torch.Tensor,
invalid: torch.Tensor,
out_loc: torch.Tensor,
context_lens: torch.Tensor,
block_size: int,
swa_window: int,
page_index_aligned_size: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
if offsets.ndim != 2 or offsets.shape[1] != swa_window:
raise ValueError(
"offsets must be [bs, swa_window]; "
f"got shape={tuple(offsets.shape)} (swa_window={swa_window})."
)
bs = offsets.shape[0]
device = offsets.device
context_lens = context_lens.to(device=device, dtype=torch.int32)
window_full_locs = req_to_token[
req_pool_indices_per_request[:, None].to(torch.int64), offsets
]
window_full_locs = window_full_locs.masked_fill(invalid, 0)
window_swa_locs = full_to_swa_mapping[window_full_locs].to(torch.int32)
window_swa_locs = window_swa_locs.masked_fill(invalid, -1)
block_full_locs = out_loc[: bs * block_size].view(bs, block_size)
block_swa_locs = full_to_swa_mapping[block_full_locs].to(torch.int32)
target_width = ceil_align(swa_window + block_size, page_index_aligned_size)
swa_page_indices = _compact_dspark_window_then_block(
window_swa_locs=window_swa_locs,
block_swa_locs=block_swa_locs,
context_lens=context_lens,
target_width=target_width,
block_size=block_size,
swa_window=swa_window,
)
swa_page_indices = (
swa_page_indices.view(bs, 1, target_width)
.expand(bs, block_size, target_width)
.reshape(bs * block_size, target_width)
.contiguous()
)
swa_topk_lengths = (
(context_lens + block_size)
.view(bs, 1)
.expand(bs, block_size)
.reshape(bs * block_size)
.contiguous()
.to(torch.int32)
)
return swa_page_indices, swa_topk_lengths
def _compact_dspark_window_then_block(
*,
window_swa_locs: torch.Tensor,
block_swa_locs: torch.Tensor,
context_lens: torch.Tensor,
target_width: int,
block_size: int,
swa_window: int,
) -> torch.Tensor:
bs = window_swa_locs.shape[0]
device = window_swa_locs.device
out = torch.full((bs, target_width), -1, dtype=torch.int32, device=device)
j = torch.arange(swa_window, device=device, dtype=torch.int32).view(1, -1)
shift = (swa_window - context_lens.view(-1, 1)).to(torch.int32)
src_col = (shift + j).clamp_(min=0, max=swa_window - 1).to(torch.int64)
gathered = torch.gather(window_swa_locs, dim=1, index=src_col)
valid = j < context_lens.view(-1, 1)
out[:, :swa_window] = torch.where(valid, gathered, -1)
block_col = context_lens.view(-1, 1) + torch.arange(
block_size, device=device, dtype=torch.int32
).view(1, -1)
block_rows = torch.arange(bs, device=device).view(-1, 1).expand(-1, block_size)
out[block_rows, block_col] = block_swa_locs
return out
@triton.jit
def _swa_page_indices_kernel(
req_to_token_ptr,
full_to_swa_ptr,
req_pool_ptr,
offsets_ptr,
out_loc_ptr,
context_lens_ptr,
out_ptr,
topk_ptr,
rt_stride,
swa_window,
block_size,
target_width,
TW_BLOCK: tl.constexpr,
):
q = tl.program_id(0)
i = q // block_size
cl = tl.load(context_lens_ptr + i)
rp = tl.load(req_pool_ptr + i).to(tl.int64)
k = tl.arange(0, TW_BLOCK)
kmask = k < target_width
in_window = k < cl
src_col = tl.minimum(tl.maximum((swa_window - cl) + k, 0), swa_window - 1)
wmask = kmask & in_window
off = tl.load(offsets_ptr + i * swa_window + src_col, mask=wmask, other=0).to(
tl.int64
)
win_full = tl.load(req_to_token_ptr + rp * rt_stride + off, mask=wmask, other=0).to(
tl.int64
)
win_swa = tl.load(full_to_swa_ptr + win_full, mask=wmask, other=-1).to(tl.int32)
in_block = (k >= cl) & (k < cl + block_size)
bmask = kmask & in_block
bcol = tl.maximum(k - cl, 0)
blk_full = tl.load(out_loc_ptr + i * block_size + bcol, mask=bmask, other=0).to(
tl.int64
)
blk_swa = tl.load(full_to_swa_ptr + blk_full, mask=bmask, other=-1).to(tl.int32)
val = tl.where(in_window, win_swa, tl.where(in_block, blk_swa, -1))
tl.store(out_ptr + q * target_width + k, val.to(tl.int32), mask=kmask)
tl.store(topk_ptr + q, (cl + block_size).to(tl.int32))
def build_dspark_swa_page_indices_triton(
*,
req_to_token: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
req_pool_indices_per_request: torch.Tensor,
offsets: torch.Tensor,
out_loc: torch.Tensor,
context_lens: torch.Tensor,
block_size: int,
swa_window: int,
page_index_aligned_size: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
if offsets.ndim != 2 or offsets.shape[1] != swa_window:
raise ValueError(
"offsets must be [bs, swa_window]; "
f"got shape={tuple(offsets.shape)} (swa_window={swa_window})."
)
bs = offsets.shape[0]
device = offsets.device
req_pool = req_pool_indices_per_request.to(device=device).contiguous()
offsets = offsets.to(torch.int64).contiguous()
out_loc = out_loc[: bs * block_size].contiguous()
context_lens = context_lens.to(device=device, dtype=torch.int32).contiguous()
rt_stride = req_to_token.stride(0)
target_width = ceil_align(swa_window + block_size, page_index_aligned_size)
n_q = bs * block_size
swa_page_indices = torch.empty(
(n_q, target_width), dtype=torch.int32, device=device
)
swa_topk_lengths = torch.empty(n_q, dtype=torch.int32, device=device)
TW_BLOCK = triton.next_power_of_2(target_width)
_swa_page_indices_kernel[(n_q,)](
req_to_token,
full_to_swa_mapping,
req_pool,
offsets,
out_loc,
context_lens,
swa_page_indices,
swa_topk_lengths,
rt_stride,
swa_window,
block_size,
target_width,
TW_BLOCK=TW_BLOCK,
)
return swa_page_indices, swa_topk_lengths
class BuildBlockSeqLensCausal:
@classmethod
def execute(cls, *args, **kwargs) -> torch.Tensor:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
seq_lens: torch.Tensor,
block_size: int,
device: torch.device,
) -> torch.Tensor:
return build_block_seq_lens_causal(
seq_lens=seq_lens,
block_size=block_size,
device=device,
)
@classmethod
def triton(
cls,
*,
seq_lens: torch.Tensor,
block_size: int,
device: torch.device,
) -> torch.Tensor:
return build_block_seq_lens_causal_triton(
seq_lens=seq_lens,
block_size=block_size,
device=device,
)
def build_block_seq_lens_causal(
*,
seq_lens: torch.Tensor,
block_size: int,
device: torch.device,
) -> torch.Tensor:
prefix = seq_lens.to(torch.int32)
steps = torch.arange(1, block_size + 1, device=device, dtype=torch.int32)
return (prefix[:, None] + steps[None, :]).reshape(-1)
@triton.jit
def _block_seq_lens_casual_kernel(
seq_lens_ptr,
out_ptr,
block_size,
n_out,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n_out
row = offs // block_size
col = offs % block_size
prefix = tl.load(seq_lens_ptr + row, mask=mask, other=0)
tl.store(out_ptr + offs, (prefix + col + 1).to(tl.int32), mask=mask)
def build_block_seq_lens_causal_triton(
*,
seq_lens: torch.Tensor,
block_size: int,
device: torch.device,
) -> torch.Tensor:
seq_lens = seq_lens.to(device=device, dtype=torch.int64).contiguous()
n_rows = seq_lens.shape[0]
n_out = n_rows * block_size
out = torch.empty(n_out, dtype=torch.int32, device=device)
BLOCK = 256
grid = (triton.cdiv(n_out, BLOCK),)
_block_seq_lens_casual_kernel[grid](seq_lens, out, block_size, n_out, BLOCK=BLOCK)
return out
@@ -0,0 +1,443 @@
from __future__ import annotations
from typing import Optional
import msgspec
import torch
import torch.nn.functional as F
import triton
import triton.language as tl
from sglang.srt.speculative.dspark_components.kernels.dispatch import inputs_on_cuda
_BLOCK_V = 1024
_IDX_SENTINEL = tl.constexpr(2147483647)
class SampleStepTokens:
@classmethod
def execute(
cls,
*,
step_logits: torch.Tensor,
temperatures: torch.Tensor,
greedy_mask: torch.Tensor,
exp_noise: torch.Tensor,
) -> torch.Tensor:
if step_logits.is_cuda:
return cls.triton(
step_logits=step_logits,
temperatures=temperatures,
greedy_mask=greedy_mask,
exp_noise=exp_noise,
)
return cls.torch(
step_logits=step_logits,
temperatures=temperatures,
greedy_mask=greedy_mask,
exp_noise=exp_noise,
)
@classmethod
def torch(
cls,
*,
step_logits: torch.Tensor,
temperatures: torch.Tensor,
greedy_mask: torch.Tensor,
exp_noise: torch.Tensor,
) -> torch.Tensor:
return sample_step_tokens(
step_logits=step_logits,
temperatures=temperatures,
greedy_mask=greedy_mask,
exp_noise=exp_noise,
)
@classmethod
def triton(
cls,
*,
step_logits: torch.Tensor,
temperatures: torch.Tensor,
greedy_mask: torch.Tensor,
exp_noise: torch.Tensor,
) -> torch.Tensor:
return sample_step_tokens_triton(
step_logits=step_logits,
temperatures=temperatures,
greedy_mask=greedy_mask,
exp_noise=exp_noise,
)
def sample_step_tokens(
*,
step_logits: torch.Tensor,
temperatures: torch.Tensor,
greedy_mask: torch.Tensor,
exp_noise: torch.Tensor,
) -> torch.Tensor:
probs = torch.softmax(step_logits.float() / temperatures[:, None], dim=-1)
noise = torch.where(greedy_mask[:, None], 1.0, exp_noise)
return probs.div_(noise).argmax(dim=-1)
@triton.jit
def _online_partial_kernel(
logits_ptr,
temperatures_ptr,
greedy_mask_ptr,
exp_noise_ptr,
tile_max_ptr,
partial_key_ptr,
partial_idx_ptr,
V,
stride_row,
n_tiles,
BLOCK_V: tl.constexpr,
):
row = tl.program_id(0)
tile = tl.program_id(1)
offs = tile * BLOCK_V + tl.arange(0, BLOCK_V)
mask = offs < V
logits = tl.load(
logits_ptr + row * stride_row + offs, mask=mask, other=float("-inf")
).to(tl.float32)
temperature = tl.load(temperatures_ptr + row)
s = logits / temperature
tile_max = tl.max(s, axis=0)
greedy = tl.load(greedy_mask_ptr + row) != 0
noise = tl.load(exp_noise_ptr + row * V + offs, mask=mask, other=1.0)
denom = tl.where(greedy, 1.0, noise)
key = tl.exp(s - tile_max) / denom
key = tl.where(mask, key, -1.0)
tile_best = tl.max(key, axis=0)
idx = tl.where(key == tile_best, offs, _IDX_SENTINEL)
tl.store(tile_max_ptr + row * n_tiles + tile, tile_max)
tl.store(partial_key_ptr + row * n_tiles + tile, tile_best)
tl.store(partial_idx_ptr + row * n_tiles + tile, tl.min(idx, axis=0))
@triton.jit
def _online_combine_kernel(
tile_max_ptr,
partial_key_ptr,
partial_idx_ptr,
next_tokens_ptr,
n_tiles,
BLOCK_TILES: tl.constexpr,
):
row = tl.program_id(0)
offs = tl.arange(0, BLOCK_TILES)
mask = offs < n_tiles
tile_max = tl.load(
tile_max_ptr + row * n_tiles + offs, mask=mask, other=float("-inf")
)
keys = tl.load(partial_key_ptr + row * n_tiles + offs, mask=mask, other=-1.0)
idxs = tl.load(
partial_idx_ptr + row * n_tiles + offs, mask=mask, other=_IDX_SENTINEL
)
global_max = tl.max(tile_max, axis=0)
rescaled = keys * tl.exp(tile_max - global_max)
rescaled = tl.where(mask, rescaled, -1.0)
best = tl.max(rescaled, axis=0)
cand = tl.where(rescaled == best, idxs, _IDX_SENTINEL)
tl.store(next_tokens_ptr + row, tl.min(cand, axis=0).to(tl.int64))
def sample_step_tokens_triton(
*,
step_logits: torch.Tensor,
temperatures: torch.Tensor,
greedy_mask: torch.Tensor,
exp_noise: torch.Tensor,
) -> torch.Tensor:
bs, V = step_logits.shape
device = step_logits.device
assert step_logits.stride(1) == 1, "step_logits rows must be contiguous"
stride_row = step_logits.stride(0)
temperatures = temperatures.to(torch.float32).contiguous()
greedy_mask = greedy_mask.to(torch.int32).contiguous()
exp_noise = exp_noise.to(torch.float32).contiguous()
n_tiles = triton.cdiv(V, _BLOCK_V)
block_tiles = triton.next_power_of_2(n_tiles)
tile_max = torch.empty((bs, n_tiles), dtype=torch.float32, device=device)
partial_key = torch.empty((bs, n_tiles), dtype=torch.float32, device=device)
partial_idx = torch.empty((bs, n_tiles), dtype=torch.int32, device=device)
next_tokens = torch.empty((bs,), dtype=torch.int64, device=device)
tile_grid = (bs, n_tiles)
row_grid = (bs,)
_online_partial_kernel[tile_grid](
step_logits,
temperatures,
greedy_mask,
exp_noise,
tile_max,
partial_key,
partial_idx,
V,
stride_row,
n_tiles,
BLOCK_V=_BLOCK_V,
)
_online_combine_kernel[row_grid](
tile_max,
partial_key,
partial_idx,
next_tokens,
n_tiles,
BLOCK_TILES=block_tiles,
)
return next_tokens
_STACKED_WEIGHT_CACHE: dict[int, _StackedWkvWeight] = {}
class CommitKvProj:
@classmethod
def execute(
cls,
*,
main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module],
) -> list[torch.Tensor]:
if main_x.is_cuda and _fused_commit_kv_proj_supported(wkv_linears=wkv_linears):
return cls.triton(main_x=main_x, wkv_linears=wkv_linears)
return cls.torch(main_x=main_x, wkv_linears=wkv_linears)
@classmethod
def torch(
cls,
*,
main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module],
) -> list[torch.Tensor]:
return commit_kv_proj(main_x=main_x, wkv_linears=wkv_linears)
@classmethod
def triton(
cls,
*,
main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module],
) -> list[torch.Tensor]:
return commit_kv_proj_fused(main_x=main_x, wkv_linears=wkv_linears)
def commit_kv_proj(
*,
main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module],
) -> list[torch.Tensor]:
return [linear(main_x)[0] for linear in wkv_linears]
def commit_kv_proj_fused(
*,
main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module],
) -> list[torch.Tensor]:
num_stages = len(wkv_linears)
stacked = _stacked_wkv_weight(wkv_linears=wkv_linears)
if stacked.fp8_scale is not None:
quant_method = wkv_linears[0].quant_method
kv_all = quant_method.w8a8_block_fp8_linear(
input=main_x,
weight=stacked.weight,
block_size=quant_method.quant_config.weight_block_size,
weight_scale=stacked.fp8_scale,
input_scale=None,
bias=None,
)
else:
kv_all = torch.nn.functional.linear(main_x, stacked.weight)
head_dim = kv_all.shape[-1] // num_stages
return [
kv_all[:, i * head_dim : (i + 1) * head_dim].contiguous()
for i in range(num_stages)
]
class _StackedWkvWeight(msgspec.Struct):
weight: torch.Tensor
fp8_scale: Optional[torch.Tensor]
def _stacked_wkv_weight(*, wkv_linears: list[torch.nn.Module]) -> _StackedWkvWeight:
key = id(wkv_linears[0])
cached = _STACKED_WEIGHT_CACHE.get(key)
if cached is None:
cached = _build_stacked_wkv_weight(wkv_linears=wkv_linears)
_STACKED_WEIGHT_CACHE[key] = cached
return cached
def _block_quant_stack_applies(*, wkv_linears: list[torch.nn.Module]) -> bool:
quant_method = wkv_linears[0].quant_method
block_quant = hasattr(quant_method, "block_quant") and quant_method.block_quant
if not (block_quant and hasattr(quant_method, "w8a8_block_fp8_linear")):
return False
block_out = quant_method.quant_config.weight_block_size[0]
return all(
linear.weight.dtype == torch.float8_e4m3fn
and linear.weight.shape[0] % block_out == 0
for linear in wkv_linears
)
def _dequant_supported(linear: torch.nn.Module) -> bool:
"""Mirrors the preconditions asserted in _dequant_linear_weight."""
weight = linear.weight
if weight.dtype in (torch.bfloat16, torch.float16, torch.float32):
return True
if weight.dtype != torch.float8_e4m3fn:
return False
block = 128
out_dim, in_dim = weight.shape
expected_scale_shape = (
(out_dim + block - 1) // block,
(in_dim + block - 1) // block,
)
return tuple(linear.weight_scale_inv.shape) == expected_scale_shape
def _fused_commit_kv_proj_supported(*, wkv_linears: list[torch.nn.Module]) -> bool:
"""Whether _build_stacked_wkv_weight can handle these weights; unsupported
quant schemes fall back to the per-linear torch path in execute()."""
if _block_quant_stack_applies(wkv_linears=wkv_linears):
return True
return all(_dequant_supported(linear) for linear in wkv_linears)
def _build_stacked_wkv_weight(
*, wkv_linears: list[torch.nn.Module]
) -> _StackedWkvWeight:
if _block_quant_stack_applies(wkv_linears=wkv_linears):
weight = torch.cat([linear.weight for linear in wkv_linears], dim=0)
if wkv_linears[0].weight_scale_inv.dtype == torch.int32:
from sglang.srt.layers.quantization.fp8_utils import (
inverse_transform_scale_ue8m0,
transform_scale_ue8m0,
)
sf_fp32 = torch.cat(
[
inverse_transform_scale_ue8m0(
linear.weight_scale_inv, mn=linear.weight.shape[0]
)
for linear in wkv_linears
],
dim=0,
)
scale = transform_scale_ue8m0(sf_fp32, mn=weight.shape[0])
return _StackedWkvWeight(weight=weight, fp8_scale=scale)
scale = torch.cat([linear.weight_scale_inv for linear in wkv_linears], dim=0)
if scale.dim() >= 2 and scale.stride(-2) != 1:
scale = scale.transpose(-2, -1).contiguous().transpose(-2, -1)
return _StackedWkvWeight(weight=weight, fp8_scale=scale)
weight = torch.cat(
[_dequant_linear_weight(linear) for linear in wkv_linears], dim=0
)
return _StackedWkvWeight(weight=weight, fp8_scale=None)
def _dequant_linear_weight(linear: torch.nn.Module) -> torch.Tensor:
weight = linear.weight
if weight.dtype in (torch.bfloat16, torch.float16, torch.float32):
return weight.to(torch.bfloat16)
assert weight.dtype == torch.float8_e4m3fn, (
f"unsupported wkv weight dtype {weight.dtype} for the fused commit kv proj; "
f"execute() should have routed this to the torch path "
f"(_fused_commit_kv_proj_supported)"
)
block = 128
scale = linear.weight_scale_inv
out_dim, in_dim = weight.shape
expected_scale_shape = (
(out_dim + block - 1) // block,
(in_dim + block - 1) // block,
)
assert tuple(scale.shape) == expected_scale_shape, (
f"wkv weight_scale_inv shape {tuple(scale.shape)} does not match the "
f"128x128 block grid {expected_scale_shape} for weight {tuple(weight.shape)}; "
f"execute() should have routed this to the torch path "
f"(_fused_commit_kv_proj_supported)"
)
scale_full = scale.repeat_interleave(block, dim=0)[:out_dim]
scale_full = scale_full.repeat_interleave(block, dim=1)[:, :in_dim]
return (weight.to(torch.float32) * scale_full.to(torch.float32)).to(torch.bfloat16)
_BLOCK = 1024
class BuildStepLocal:
@classmethod
def execute(cls, *args, **kwargs) -> torch.Tensor:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(cls, *, bias: torch.Tensor, base_local: torch.Tensor) -> torch.Tensor:
return build_step_local(bias=bias, base_local=base_local)
@classmethod
def triton(cls, *, bias: torch.Tensor, base_local: torch.Tensor) -> torch.Tensor:
return build_step_local_triton(bias=bias, base_local=base_local)
def build_step_local(*, bias: torch.Tensor, base_local: torch.Tensor) -> torch.Tensor:
per_partition = base_local.shape[-1]
pad = per_partition - bias.shape[-1]
padded = (
F.pad(bias.to(torch.float32), (0, pad)) if pad > 0 else bias.to(torch.float32)
)
return base_local + padded
@triton.jit
def _build_step_local_kernel(
bias_ptr,
base_ptr,
out_ptr,
org_width,
per_partition,
BLOCK: tl.constexpr,
):
row = tl.program_id(0)
tile = tl.program_id(1)
offs = tile * BLOCK + tl.arange(0, BLOCK)
mask = offs < per_partition
base = tl.load(base_ptr + row * per_partition + offs, mask=mask, other=0.0).to(
tl.float32
)
bias = tl.load(
bias_ptr + row * org_width + offs, mask=offs < org_width, other=0.0
).to(tl.float32)
tl.store(out_ptr + row * per_partition + offs, base + bias, mask=mask)
def build_step_local_triton(
*, bias: torch.Tensor, base_local: torch.Tensor
) -> torch.Tensor:
bs, per_partition = base_local.shape
org_width = bias.shape[-1]
base_local = base_local.contiguous()
bias = bias.contiguous()
out = torch.empty(
(bs, per_partition), dtype=torch.float32, device=base_local.device
)
grid = (bs, triton.cdiv(per_partition, _BLOCK))
_build_step_local_kernel[grid](
bias, base_local, out, org_width, per_partition, BLOCK=_BLOCK
)
return out
@@ -0,0 +1,260 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
import triton
import triton.language as tl
from sglang.srt.speculative.dspark_components.kernels.dispatch import (
inputs_on_cuda,
)
if TYPE_CHECKING:
from sglang.srt.speculative.dspark_components.dspark_planner import (
DSparkScheduleConfig,
)
class ScheduleVerifyLensTopk:
@classmethod
def execute(cls, *args, **kwargs) -> torch.Tensor:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
confidence: torch.Tensor,
budget: int,
cfg: DSparkScheduleConfig,
) -> torch.Tensor:
return schedule_verify_lens_topk(confidence=confidence, budget=budget, cfg=cfg)
@classmethod
def triton(
cls,
*,
confidence: torch.Tensor,
budget: int,
cfg: DSparkScheduleConfig,
) -> torch.Tensor:
return schedule_verify_lens_topk_triton(
confidence=confidence, budget=budget, cfg=cfg
)
def compute_sort_survival(confidence: torch.Tensor) -> torch.Tensor:
return torch.cumprod(confidence.to(torch.float32), dim=1)
def schedule_verify_lens_topk(
*,
confidence: torch.Tensor,
budget: int,
cfg: DSparkScheduleConfig,
) -> torch.Tensor:
return schedule_verify_lens_topk_from_survival(
survival_probs=compute_sort_survival(confidence), budget=budget, cfg=cfg
)
def schedule_verify_lens_topk_from_survival(
*,
survival_probs: torch.Tensor,
budget: int,
cfg: DSparkScheduleConfig,
) -> torch.Tensor:
num_requests, _gamma = survival_probs.shape
max_len = cfg.resolved_max_verify_len()
device = survival_probs.device
selected_extra = torch.zeros(num_requests, dtype=torch.int64, device=device)
if budget > 0:
candidate_window = survival_probs[:, :max_len]
num_candidates = candidate_window.numel()
if num_candidates > 0:
request_index = (
torch.arange(num_requests, device=device)
.view(num_requests, 1)
.expand_as(candidate_window)
)
position_index = (
torch.arange(candidate_window.shape[1], device=device)
.view(1, candidate_window.shape[1])
.expand_as(candidate_window)
)
valid = candidate_window >= cfg.survival_eps
flat_prob = candidate_window.reshape(-1).to(torch.float64)
flat_request = request_index.reshape(-1)
flat_position = position_index.reshape(-1)
flat_valid = valid.reshape(-1)
order = _value_independent_descending_order(
probs=flat_prob,
positions=flat_position,
requests=flat_request,
valid=flat_valid,
)
take = min(int(budget), num_candidates)
chosen = order[:take]
chosen_requests = flat_request[chosen]
chosen_valid = flat_valid[chosen].to(torch.int64)
selected_extra.scatter_add_(0, chosen_requests, chosen_valid)
min_len = torch.full(
(num_requests,), cfg.min_verify_len, dtype=torch.int64, device=device
)
verify_lens = min_len + selected_extra
lower_bound = max(cfg.min_verify_len, 1)
verify_lens = torch.clamp(verify_lens, min=lower_bound, max=max_len)
return verify_lens.to(torch.int32)
def _value_independent_descending_order(
*,
probs: torch.Tensor,
positions: torch.Tensor,
requests: torch.Tensor,
valid: torch.Tensor,
) -> torch.Tensor:
masked_prob = torch.where(valid, probs, torch.full_like(probs, float("-inf")))
num_candidates = masked_prob.numel()
order = torch.arange(num_candidates, device=probs.device)
order = order[torch.argsort(requests[order], stable=True)]
order = order[torch.argsort(positions[order], stable=True)]
order = order[torch.argsort(-masked_prob[order], stable=True)]
return order
@triton.jit
def _schedule_topk_prep_kernel(
confidence_ptr,
survival_ptr,
selected_extra_ptr,
gamma,
cols,
G_P2: tl.constexpr,
):
row = tl.program_id(0)
g = tl.arange(0, G_P2)
conf = tl.load(
confidence_ptr + row.to(tl.int64) * gamma + g, mask=g < gamma, other=1.0
).to(tl.float32)
surv = tl.cumprod(conf, axis=0)
tl.store(survival_ptr + row.to(tl.int64) * cols + g, surv, mask=g < cols)
tl.store(selected_extra_ptr + row, 0)
@triton.jit
def _schedule_topk_finalize_kernel(
selected_extra_ptr,
out_ptr,
min_verify_len,
lower_bound,
max_len,
bs,
BLOCK: tl.constexpr,
):
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
mask = offs < bs
extra = tl.load(selected_extra_ptr + offs, mask=mask, other=0).to(tl.int32)
lens = min_verify_len + extra
lens = tl.maximum(lens, lower_bound)
lens = tl.minimum(lens, max_len)
tl.store(out_ptr + offs, lens, mask=mask)
@triton.jit
def _schedule_topk_selected_extra_kernel(
survival_ptr,
selected_extra_ptr,
budget,
cols,
n,
survival_eps,
BLOCK_C: tl.constexpr,
BLOCK_CP: tl.constexpr,
):
pid = tl.program_id(0)
c = pid * BLOCK_C + tl.arange(0, BLOCK_C)
cmask = c < n
r = c // cols
p = c % cols
sp = tl.load(survival_ptr + c, mask=cmask, other=0.0)
valid_c = sp >= survival_eps
mp = tl.where(valid_c, sp, float("-inf"))
rank = tl.zeros([BLOCK_C], dtype=tl.int32)
for cp0 in range(0, n, BLOCK_CP):
cp = cp0 + tl.arange(0, BLOCK_CP)
cpmask = cp < n
rp = cp // cols
pp = cp % cols
spp = tl.load(survival_ptr + cp, mask=cpmask, other=0.0)
validp = spp >= survival_eps
mpp = tl.where(validp, spp, float("-inf"))
gt = mpp[None, :] > mp[:, None]
eq = mpp[None, :] == mp[:, None]
pos_lt = pp[None, :] < p[:, None]
pos_eq = pp[None, :] == p[:, None]
req_lt = rp[None, :] < r[:, None]
before = gt | (eq & (pos_lt | (pos_eq & req_lt)))
before = before & cpmask[None, :]
rank += tl.sum(before.to(tl.int32), axis=1)
selected = valid_c & (rank < budget)
tl.atomic_add(selected_extra_ptr + r, selected.to(tl.int32), mask=cmask)
def schedule_verify_lens_topk_triton(
*,
confidence: torch.Tensor,
budget: int,
cfg: DSparkScheduleConfig,
) -> torch.Tensor:
num_requests, gamma = confidence.shape
max_len = cfg.resolved_max_verify_len()
device = confidence.device
cols = min(max_len, gamma)
n = num_requests * cols
selected_extra = torch.empty(num_requests, dtype=torch.int32, device=device)
survival = torch.empty((num_requests, cols), dtype=torch.float32, device=device)
_schedule_topk_prep_kernel[(num_requests,)](
confidence.contiguous(),
survival,
selected_extra,
gamma,
cols,
G_P2=triton.next_power_of_2(max(gamma, 1)),
)
if budget > 0 and n > 0:
BLOCK_C = 64
BLOCK_CP = 256
grid = (triton.cdiv(n, BLOCK_C),)
_schedule_topk_selected_extra_kernel[grid](
survival,
selected_extra,
int(budget),
cols,
n,
float(cfg.survival_eps),
BLOCK_C=BLOCK_C,
BLOCK_CP=BLOCK_CP,
)
verify_lens = torch.empty(num_requests, dtype=torch.int32, device=device)
BLOCK = 256
_schedule_topk_finalize_kernel[(triton.cdiv(num_requests, BLOCK),)](
selected_extra,
verify_lens,
int(cfg.min_verify_len),
max(cfg.min_verify_len, 1),
int(max_len),
num_requests,
BLOCK=BLOCK,
)
return verify_lens
@@ -0,0 +1,871 @@
from __future__ import annotations
import msgspec
import torch
import triton
import triton.language as tl
from sglang.kernels.ops.speculative.cache_locs import assign_extend_cache_locs_func
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.speculative.dspark_components.kernels.dispatch import inputs_on_cuda
from sglang.srt.speculative.ragged_verify import RaggedVerifyLayout
class RaggedVerifyWindow(msgspec.Struct, frozen=True):
positions: torch.Tensor
verify_cache_loc: torch.Tensor
verify_ids: torch.Tensor
class BuildRaggedVerifyWindow:
@classmethod
def execute(cls, *args, **kwargs) -> RaggedVerifyWindow:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
batch: ScheduleBatch,
layout: RaggedVerifyLayout,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
bs: int,
device: str,
verify_num_draft_tokens: int,
model_runner,
) -> RaggedVerifyWindow:
return build_ragged_verify_window(
batch=batch,
layout=layout,
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
bs=bs,
device=device,
verify_num_draft_tokens=verify_num_draft_tokens,
model_runner=model_runner,
)
@classmethod
def triton(
cls,
*,
batch: ScheduleBatch,
layout: RaggedVerifyLayout,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
bs: int,
device: str,
verify_num_draft_tokens: int,
model_runner,
) -> RaggedVerifyWindow:
return build_ragged_verify_window_triton(
batch=batch,
layout=layout,
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
bs=bs,
device=device,
verify_num_draft_tokens=verify_num_draft_tokens,
model_runner=model_runner,
)
def build_ragged_verify_window(
*,
batch: ScheduleBatch,
layout: RaggedVerifyLayout,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
bs: int,
device: str,
verify_num_draft_tokens: int,
model_runner,
) -> RaggedVerifyWindow:
prefix_lens = batch.seq_lens
verify_lens = layout.verify_lens.to(device=device, dtype=torch.int32)
padded_total = layout.graph_num_tokens
req_id, within, valid = compact_row_index(
verify_lens=verify_lens, padded_total=padded_total, device=device
)
safe_req = req_id.clamp(max=bs - 1)
positions = torch.where(
valid,
prefix_lens.to(torch.int64)[safe_req] + within,
torch.zeros_like(within),
)
real_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=model_runner.req_to_token_pool.req_to_token,
start_offset=prefix_lens,
end_offset=prefix_lens + verify_lens.to(prefix_lens.dtype),
batch_size=bs,
draft_token_num=verify_num_draft_tokens,
device=device,
)
verify_cache_loc = torch.nn.functional.pad(
real_cache_loc, (0, padded_total - real_cache_loc.shape[0])
)
verify_cache_loc = torch.where(
valid, verify_cache_loc, torch.zeros_like(verify_cache_loc)
)
verify_ids = compact_verify_ids(
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
layout=layout,
device=device,
)
return RaggedVerifyWindow(
positions=positions,
verify_cache_loc=verify_cache_loc,
verify_ids=verify_ids,
)
@triton.jit
def _ragged_finalize_kernel(
req_ptr,
within_ptr,
prefix_ptr,
cache_ptr,
pos_out_ptr,
cache_out_ptr,
bs,
n,
real_len,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
req = tl.load(req_ptr + offs, mask=mask, other=0)
within = tl.load(within_ptr + offs, mask=mask, other=0)
valid = req < bs
safe_req = tl.minimum(req, bs - 1)
prefix = tl.load(prefix_ptr + safe_req, mask=mask, other=0)
pos = tl.where(valid, prefix + within, 0)
lmask = mask & (offs < real_len)
cl = tl.load(cache_ptr + offs, mask=lmask, other=0)
cl = tl.where(valid, cl, 0)
tl.store(pos_out_ptr + offs, pos, mask=mask)
tl.store(cache_out_ptr + offs, cl, mask=mask)
def build_ragged_verify_window_triton(
*,
batch: ScheduleBatch,
layout: RaggedVerifyLayout,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
bs: int,
device: str,
verify_num_draft_tokens: int,
model_runner,
) -> RaggedVerifyWindow:
prefix_lens = batch.seq_lens
verify_lens = layout.verify_lens.to(device=device, dtype=torch.int32)
padded_total = layout.graph_num_tokens
req_id, within, _valid = compact_row_index_triton(
verify_lens=verify_lens, padded_total=padded_total, device=device
)
real_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=model_runner.req_to_token_pool.req_to_token,
start_offset=prefix_lens,
end_offset=prefix_lens + verify_lens.to(prefix_lens.dtype),
batch_size=bs,
draft_token_num=verify_num_draft_tokens,
device=device,
)
prefix_i64 = prefix_lens.to(device=device, dtype=torch.int64).contiguous()
positions = torch.empty(padded_total, dtype=torch.int64, device=device)
verify_cache_loc = torch.empty(
padded_total, dtype=real_cache_loc.dtype, device=device
)
BLOCK = 256
grid = (triton.cdiv(padded_total, BLOCK),)
_ragged_finalize_kernel[grid](
req_id,
within,
prefix_i64,
real_cache_loc,
positions,
verify_cache_loc,
bs,
padded_total,
real_cache_loc.shape[0],
BLOCK=BLOCK,
)
verify_ids = compact_verify_ids_triton(
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
layout=layout,
device=device,
)
return RaggedVerifyWindow(
positions=positions,
verify_cache_loc=verify_cache_loc,
verify_ids=verify_ids,
)
_SEARCH_NBITS = 11
class CompactRowIndex:
@classmethod
def execute(
cls, *args, **kwargs
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
verify_lens: torch.Tensor,
padded_total: int,
device,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return compact_row_index(
verify_lens=verify_lens,
padded_total=padded_total,
device=device,
)
@classmethod
def triton(
cls,
*,
verify_lens: torch.Tensor,
padded_total: int,
device,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return compact_row_index_triton(
verify_lens=verify_lens,
padded_total=padded_total,
device=device,
)
class CompactVerifyIds:
@classmethod
def execute(cls, *args, **kwargs) -> torch.Tensor:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
layout: RaggedVerifyLayout,
device: str,
) -> torch.Tensor:
return compact_verify_ids(
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
layout=layout,
device=device,
)
@classmethod
def triton(
cls,
*,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
layout: RaggedVerifyLayout,
device: str,
) -> torch.Tensor:
return compact_verify_ids_triton(
draft_block_ids=draft_block_ids,
draft_tokens=draft_tokens,
layout=layout,
device=device,
)
def compact_verify_ids(
*,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
layout: RaggedVerifyLayout,
device: str,
) -> torch.Tensor:
req_id, within, valid = compact_row_index(
verify_lens=layout.verify_lens,
padded_total=layout.graph_num_tokens,
device=device,
)
bs = layout.verify_lens.shape[0]
safe_req = req_id.clamp(max=bs - 1)
anchors = draft_block_ids[:, 0]
drafts = draft_tokens[safe_req, (within - 1).clamp_min(0)]
verify_ids = torch.where(within == 0, anchors[safe_req], drafts)
verify_ids = torch.where(valid, verify_ids, torch.zeros_like(verify_ids))
return verify_ids.to(torch.int64)
def compact_row_index(
*,
verify_lens: torch.Tensor,
padded_total: int,
device,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
verify_lens = verify_lens.to(device=device, dtype=torch.int64)
bs = int(verify_lens.numel())
incl = torch.cumsum(verify_lens, dim=0)
start = incl - verify_lens
real_total = incl[-1]
row = torch.arange(padded_total, device=device, dtype=torch.int64)
valid = row < real_total
req_id = torch.searchsorted(incl, row, right=True)
req_id = torch.where(valid, req_id, torch.full_like(req_id, bs))
within = torch.where(
valid, row - start[req_id.clamp(max=bs - 1)], torch.zeros_like(row)
)
return req_id, within, valid
@triton.jit
def _compact_row_index_kernel(
incl_ptr,
req_out_ptr,
within_out_ptr,
valid_out_ptr,
bs,
n,
BLOCK: tl.constexpr,
NBITS: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
row = offs.to(tl.int64)
real_total = tl.load(incl_ptr + (bs - 1))
lo = tl.zeros([BLOCK], dtype=tl.int32)
hi = tl.full([BLOCK], bs, dtype=tl.int32)
for _ in range(NBITS):
mid = (lo + hi) // 2
active = lo < hi
val = tl.load(incl_ptr + tl.minimum(mid, bs - 1), mask=mask, other=0)
go_right = val <= row
lo = tl.where(active & go_right, mid + 1, lo)
hi = tl.where(active & (~go_right), mid, hi)
req = lo
gidx = tl.maximum(req - 1, 0)
start = tl.load(incl_ptr + gidx, mask=mask, other=0)
start = tl.where(req > 0, start, 0)
valid = row < real_total
within = tl.where(valid, row - start, 0)
req_final = tl.where(valid, req.to(tl.int64), bs)
tl.store(req_out_ptr + offs, req_final, mask=mask)
tl.store(within_out_ptr + offs, within, mask=mask)
tl.store(valid_out_ptr + offs, valid, mask=mask)
def compact_row_index_triton(
*,
verify_lens: torch.Tensor,
padded_total: int,
device,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
verify_lens = verify_lens.to(device=device, dtype=torch.int64).contiguous()
bs = verify_lens.shape[0]
incl = torch.cumsum(verify_lens, dim=0).contiguous()
req = torch.empty(padded_total, dtype=torch.int64, device=device)
within = torch.empty(padded_total, dtype=torch.int64, device=device)
valid = torch.empty(padded_total, dtype=torch.bool, device=device)
BLOCK = 256
grid = (triton.cdiv(padded_total, BLOCK),)
_compact_row_index_kernel[grid](
incl, req, within, valid, bs, padded_total, BLOCK=BLOCK, NBITS=_SEARCH_NBITS
)
return req, within, valid
@triton.jit
def _compact_verify_ids_gather_kernel(
req_ptr,
within_ptr,
draft_block_ids_ptr,
draft_tokens_ptr,
out_ptr,
bs,
gamma,
n,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
req = tl.load(req_ptr + offs, mask=mask, other=0)
within = tl.load(within_ptr + offs, mask=mask, other=0)
valid = req < bs
safe_req = tl.minimum(req, bs - 1)
anchor = tl.load(draft_block_ids_ptr + safe_req * gamma, mask=mask, other=0)
wcol = tl.maximum(within - 1, 0)
draft = tl.load(draft_tokens_ptr + safe_req * gamma + wcol, mask=mask, other=0)
v = tl.where(within == 0, anchor, draft)
v = tl.where(valid, v, 0)
tl.store(out_ptr + offs, v.to(tl.int64), mask=mask)
def compact_verify_ids_triton(
*,
draft_block_ids: torch.Tensor,
draft_tokens: torch.Tensor,
layout: RaggedVerifyLayout,
device: str,
) -> torch.Tensor:
req, within, _valid = compact_row_index_triton(
verify_lens=layout.verify_lens,
padded_total=layout.graph_num_tokens,
device=device,
)
bs = layout.verify_lens.shape[0]
gamma = draft_tokens.shape[1]
draft_block_ids = draft_block_ids.to(device=device, dtype=torch.int64).contiguous()
draft_tokens = draft_tokens.to(device=device, dtype=torch.int64).contiguous()
n = layout.graph_num_tokens
out = torch.empty(n, dtype=torch.int64, device=device)
BLOCK = 256
grid = (triton.cdiv(n, BLOCK),)
_compact_verify_ids_gather_kernel[grid](
req, within, draft_block_ids, draft_tokens, out, bs, gamma, n, BLOCK=BLOCK
)
return out
class ScatterCompactToStrided:
@classmethod
def execute(cls, *args, **kwargs) -> torch.Tensor:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
compact: torch.Tensor,
layout: RaggedVerifyLayout,
fill_value: float,
verify_num_draft_tokens: int,
) -> torch.Tensor:
return scatter_compact_to_strided(
compact=compact,
layout=layout,
fill_value=fill_value,
verify_num_draft_tokens=verify_num_draft_tokens,
)
@classmethod
def triton(
cls,
*,
compact: torch.Tensor,
layout: RaggedVerifyLayout,
fill_value: float,
verify_num_draft_tokens: int,
) -> torch.Tensor:
return scatter_compact_to_strided_triton(
compact=compact,
layout=layout,
fill_value=fill_value,
verify_num_draft_tokens=verify_num_draft_tokens,
)
def scatter_compact_to_strided(
*,
compact: torch.Tensor,
layout: RaggedVerifyLayout,
fill_value: float,
verify_num_draft_tokens: int,
) -> torch.Tensor:
stride = verify_num_draft_tokens
bs = layout.verify_lens.shape[0]
dim = compact.shape[1]
device = compact.device
compact = compact[: layout.graph_num_tokens]
strided = torch.full(
(bs * stride + 1, dim), fill_value, dtype=compact.dtype, device=device
)
req_id, within, valid = compact_row_index(
verify_lens=layout.verify_lens,
padded_total=layout.graph_num_tokens,
device=device,
)
sink = bs * stride
strided_pos = torch.where(
valid,
req_id.clamp(max=bs - 1) * stride + within,
torch.full_like(within, sink),
)
strided.index_copy_(0, strided_pos, compact)
return strided[: bs * stride]
@triton.jit
def _scatter_compact_to_strided_kernel(
compact_ptr,
verify_lens_ptr,
start_ptr,
out_ptr,
stride,
dim,
fill_value,
BLOCK_D: tl.constexpr,
):
o = tl.program_id(0).to(tl.int64)
dblk = tl.program_id(1)
i = o // stride
w = o % stride
vl_i = tl.load(verify_lens_ptr + i)
start_i = tl.load(start_ptr + i)
d = dblk * BLOCK_D + tl.arange(0, BLOCK_D)
dmask = d < dim
in_range = w < vl_i
src = tl.where(in_range, start_i + w, 0)
val = tl.load(compact_ptr + src * dim + d, mask=dmask & in_range, other=0)
val = tl.where(in_range, val, fill_value)
tl.store(out_ptr + o * dim + d, val, mask=dmask)
def scatter_compact_to_strided_into(
*,
compact: torch.Tensor,
verify_lens: torch.Tensor,
out: torch.Tensor,
stride: int,
fill_value: float,
) -> torch.Tensor:
dim = compact.shape[1]
fill_value = float(fill_value) if out.dtype.is_floating_point else int(fill_value)
compact = compact.contiguous()
verify_lens = verify_lens.to(dtype=torch.int64).contiguous()
start = (torch.cumsum(verify_lens, dim=0) - verify_lens).contiguous()
n_out = out.shape[0]
BLOCK_D = 1024
grid = (n_out, triton.cdiv(dim, BLOCK_D))
_scatter_compact_to_strided_kernel[grid](
compact,
verify_lens,
start,
out,
stride,
dim,
fill_value,
BLOCK_D=BLOCK_D,
)
return out
def scatter_compact_to_strided_triton(
*,
compact: torch.Tensor,
layout: RaggedVerifyLayout,
fill_value: float,
verify_num_draft_tokens: int,
) -> torch.Tensor:
stride = verify_num_draft_tokens
bs = layout.verify_lens.shape[0]
dim = compact.shape[1]
device = compact.device
out = torch.empty((bs * stride, dim), dtype=compact.dtype, device=device)
return scatter_compact_to_strided_into(
compact=compact,
verify_lens=layout.verify_lens.to(device=device),
out=out,
stride=stride,
fill_value=fill_value,
)
class CommitInjectLayoutResult(msgspec.Struct):
swa_loc: torch.Tensor
positions: torch.Tensor
class BuildCommitInjectLayout:
@classmethod
def execute(cls, *args, **kwargs) -> CommitInjectLayoutResult:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
req_pool_indices: torch.Tensor,
req_to_token: torch.Tensor,
prefix_lens: torch.Tensor,
block_pos_offsets: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
commit_lens: torch.Tensor,
stride: int,
) -> CommitInjectLayoutResult:
return build_commit_inject_layout(
req_pool_indices=req_pool_indices,
req_to_token=req_to_token,
prefix_lens=prefix_lens,
block_pos_offsets=block_pos_offsets,
full_to_swa_mapping=full_to_swa_mapping,
commit_lens=commit_lens,
stride=stride,
)
@classmethod
def triton(
cls,
*,
req_pool_indices: torch.Tensor,
req_to_token: torch.Tensor,
prefix_lens: torch.Tensor,
block_pos_offsets: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
commit_lens: torch.Tensor,
stride: int,
) -> CommitInjectLayoutResult:
return build_commit_inject_layout_triton(
req_pool_indices=req_pool_indices,
req_to_token=req_to_token,
prefix_lens=prefix_lens,
block_pos_offsets=block_pos_offsets,
full_to_swa_mapping=full_to_swa_mapping,
commit_lens=commit_lens,
stride=stride,
)
def build_commit_inject_layout(
*,
req_pool_indices: torch.Tensor,
req_to_token: torch.Tensor,
prefix_lens: torch.Tensor,
block_pos_offsets: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
commit_lens: torch.Tensor,
stride: int,
) -> CommitInjectLayoutResult:
from sglang.kernels.ops.speculative.cache_locs import (
assign_extend_cache_locs_func,
)
bs = req_pool_indices.shape[0]
device = req_pool_indices.device
positions_2d = prefix_lens.unsqueeze(1) + block_pos_offsets[:stride]
positions = positions_2d.reshape(-1).to(dtype=torch.int64)
cache_loc = assign_extend_cache_locs_func(
req_pool_indices=req_pool_indices,
req_to_token=req_to_token,
start_offset=prefix_lens,
end_offset=prefix_lens + stride,
batch_size=bs,
draft_token_num=stride,
device=device,
).to(dtype=torch.int64)
swa_loc = full_to_swa_mapping[cache_loc].to(torch.int32)
col = torch.arange(stride, device=device).view(1, -1)
committed = (col < commit_lens.to(torch.long).view(-1, 1)).reshape(-1)
swa_loc = torch.where(committed, swa_loc, torch.full_like(swa_loc, -1))
return CommitInjectLayoutResult(swa_loc=swa_loc, positions=positions)
@triton.jit
def _commit_inject_layout_kernel(
req_pool_ptr,
req_to_token_ptr,
prefix_lens_ptr,
block_pos_offsets_ptr,
full_to_swa_ptr,
commit_lens_ptr,
swa_loc_ptr,
positions_ptr,
rt_stride,
stride,
n,
BLOCK: tl.constexpr,
):
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
r = offs // stride
c = offs % stride
prefix = tl.load(prefix_lens_ptr + r, mask=mask, other=0).to(tl.int64)
pos_off = tl.load(block_pos_offsets_ptr + c, mask=mask, other=0).to(tl.int64)
rp = tl.load(req_pool_ptr + r, mask=mask, other=0).to(tl.int64)
full_loc = tl.load(
req_to_token_ptr + rp * rt_stride + prefix + pos_off, mask=mask, other=0
).to(tl.int64)
swa = tl.load(full_to_swa_ptr + full_loc, mask=mask, other=-1).to(tl.int32)
commit_len = tl.load(commit_lens_ptr + r, mask=mask, other=0).to(tl.int64)
swa = tl.where(c.to(tl.int64) < commit_len, swa, -1)
tl.store(swa_loc_ptr + offs, swa, mask=mask)
tl.store(positions_ptr + offs, prefix + pos_off, mask=mask)
def build_commit_inject_layout_triton(
*,
req_pool_indices: torch.Tensor,
req_to_token: torch.Tensor,
prefix_lens: torch.Tensor,
block_pos_offsets: torch.Tensor,
full_to_swa_mapping: torch.Tensor,
commit_lens: torch.Tensor,
stride: int,
) -> CommitInjectLayoutResult:
bs = req_pool_indices.shape[0]
n = bs * stride
device = req_pool_indices.device
swa_loc = torch.empty(n, dtype=torch.int32, device=device)
positions = torch.empty(n, dtype=torch.int64, device=device)
BLOCK = 256
_commit_inject_layout_kernel[(triton.cdiv(n, BLOCK),)](
req_pool_indices,
req_to_token,
prefix_lens,
block_pos_offsets,
full_to_swa_mapping,
commit_lens,
swa_loc,
positions,
req_to_token.stride(0),
stride,
n,
BLOCK=BLOCK,
)
return CommitInjectLayoutResult(swa_loc=swa_loc, positions=positions)
class BuildOutTokens:
@classmethod
def execute(cls, *args, **kwargs) -> torch.Tensor:
if inputs_on_cuda(*args, **kwargs):
return cls.triton(*args, **kwargs)
return cls.torch(*args, **kwargs)
@classmethod
def torch(
cls,
*,
draft_tokens: torch.Tensor,
correct_len: torch.Tensor,
bonus: torch.Tensor,
verify_num_draft_tokens: int,
gamma: int,
) -> torch.Tensor:
return build_out_tokens(
draft_tokens=draft_tokens,
correct_len=correct_len,
bonus=bonus,
verify_num_draft_tokens=verify_num_draft_tokens,
gamma=gamma,
)
@classmethod
def triton(
cls,
*,
draft_tokens: torch.Tensor,
correct_len: torch.Tensor,
bonus: torch.Tensor,
verify_num_draft_tokens: int,
gamma: int,
) -> torch.Tensor:
return build_out_tokens_triton(
draft_tokens=draft_tokens,
correct_len=correct_len,
bonus=bonus,
verify_num_draft_tokens=verify_num_draft_tokens,
gamma=gamma,
)
def build_out_tokens(
*,
draft_tokens: torch.Tensor,
correct_len: torch.Tensor,
bonus: torch.Tensor,
verify_num_draft_tokens: int,
gamma: int,
) -> torch.Tensor:
bs = draft_tokens.shape[0]
out_tokens = torch.empty(
(bs, verify_num_draft_tokens),
dtype=torch.int64,
device=draft_tokens.device,
)
out_tokens[:, :gamma].copy_(draft_tokens)
out_tokens[:, gamma].fill_(0)
out_tokens.scatter_(1, correct_len.to(torch.int64)[:, None], bonus[:, None])
return out_tokens
@triton.jit
def _build_out_tokens_kernel(
draft_tokens_ptr,
correct_len_ptr,
bonus_ptr,
out_ptr,
gamma,
T,
n_out,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n_out
b = offs // T
k = offs % T
cl = tl.load(correct_len_ptr + b, mask=mask, other=0).to(tl.int32)
bonus = tl.load(bonus_ptr + b, mask=mask, other=0)
draft_mask = mask & (k < gamma)
draft = tl.load(draft_tokens_ptr + b * gamma + k, mask=draft_mask, other=0)
val = tl.where(k == cl, bonus, tl.where(k < gamma, draft, 0))
tl.store(out_ptr + offs, val.to(tl.int64), mask=mask)
def build_out_tokens_triton(
*,
draft_tokens: torch.Tensor,
correct_len: torch.Tensor,
bonus: torch.Tensor,
verify_num_draft_tokens: int,
gamma: int,
) -> torch.Tensor:
bs = draft_tokens.shape[0]
T = verify_num_draft_tokens
device = draft_tokens.device
draft_tokens = draft_tokens.to(torch.int64).contiguous()
correct_len_i = correct_len.to(torch.int64).contiguous()
bonus_i = bonus.to(torch.int64).contiguous()
out = torch.empty((bs, T), dtype=torch.int64, device=device)
n_out = bs * T
BLOCK = 256
grid = (triton.cdiv(n_out, BLOCK),)
_build_out_tokens_kernel[grid](
draft_tokens, correct_len_i, bonus_i, out, gamma, T, n_out, BLOCK=BLOCK
)
return out
@@ -0,0 +1,71 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.srt.managers.overlap_utils import RelayPayload
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode
from sglang.srt.speculative.eagle_info import EagleDraftInput
if TYPE_CHECKING:
from sglang.srt.managers.overlap_utils import FutureMap
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.server_args import ServerArgs
def build_eagle_disagg_draft_input(
batch: ScheduleBatch,
server_args: ServerArgs,
last_tokens_tensor: torch.Tensor,
future_map: FutureMap,
) -> EagleDraftInput:
num_states = server_args.speculative_eagle_topk
if server_args.enable_multi_layer_eagle:
num_states *= server_args.speculative_num_steps
topk_p = torch.stack(
[
torch.as_tensor(
req.output_topk_p[:num_states],
device=batch.device,
dtype=torch.float32,
)
for req in batch.reqs
],
dim=0,
)
topk_index = torch.stack(
[
torch.as_tensor(
req.output_topk_index[:num_states],
device=batch.device,
dtype=torch.int64,
)
for req in batch.reqs
],
dim=0,
)
hidden_states = torch.stack(
[req.hidden_states_tensor for req in batch.reqs], dim=0
).to(batch.device)
spec_info = EagleDraftInput(
topk_p=topk_p,
topk_index=topk_index,
hidden_states=hidden_states,
bonus_tokens=last_tokens_tensor,
)
spec_info.capture_hidden_mode = CaptureHiddenMode.LAST
if batch.enable_overlap:
spec_info.future_indices = batch.req_pool_indices
# Seed the relay buf with the known seq_lens; publish's chained record
# keeps the in-flight forward's fence intact (see FutureMap.publish).
future_map.publish(spec_info.future_indices, batch.seq_lens)
future_map.stash(
spec_info.future_indices, RelayPayload.from_draft_input(spec_info)
)
return spec_info
@@ -0,0 +1,668 @@
from __future__ import annotations
import contextlib
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Optional
import torch
from sglang.srt.compilation.torch_compile_decoration import set_torch_compile_config
from sglang.srt.environ import envs
from sglang.srt.layers.dp_attention import (
DpPaddingMode,
set_dp_buffer_len,
set_is_extend_in_batch,
)
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
from sglang.srt.model_executor.input_buffers import ForwardInputBuffers
from sglang.srt.model_executor.runner import (
DecodeCudaGraphRunner,
DeepEPCudaGraphRunnerAdapter,
ShapeKey,
_grouped_foreach_copy_,
get_batch_sizes_to_capture,
model_capture_mode,
)
from sglang.srt.model_executor.runner.flashinfer_autotune import (
maybe_flashinfer_autotune_speculative_draft,
)
from sglang.srt.model_executor.runner_backend.utils import resolve_decode_backend
from sglang.srt.model_executor.runner_backend_utils import (
CUDA_GRAPH_CAPTURE_FAILED_MSG,
)
from sglang.srt.runtime_context import get_flags
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
from sglang.srt.speculative.eagle_info import EagleDraftInput
from sglang.srt.speculative.eagle_utils import get_draft_recurrent_hidden_state_spec
from sglang.srt.utils import (
require_attn_tp_gather,
require_gathered_buffer,
require_mlp_sync,
require_mlp_tp_gather,
)
from sglang.srt.utils.async_probe import maybe_detect_nan, maybe_detect_oob
if TYPE_CHECKING:
from sglang.srt.speculative.eagle_worker_v2 import EagleDraftWorker
@dataclass
class EagleDraftInputBuffers(ForwardInputBuffers):
input_ids: torch.Tensor
req_pool_indices: torch.Tensor
out_cache_loc: torch.Tensor
positions: torch.Tensor
mrope_positions: torch.Tensor
rids_int: Optional[torch.Tensor]
bootstrap_room_ids_int: Optional[torch.Tensor]
seq_lens: torch.Tensor
seq_lens_cpu: torch.Tensor
extend_seq_lens: torch.Tensor
topk_p: torch.Tensor
topk_index: torch.Tensor
draft_probs: Optional[torch.Tensor]
hidden_states: Optional[torch.Tensor]
global_num_tokens_gpu: Optional[torch.Tensor]
global_num_tokens_for_logprob_gpu: Optional[torch.Tensor]
dsa_seed_topk: Optional[torch.Tensor] = None
class EAGLEDraftCudaGraphRunner(DecodeCudaGraphRunner):
"""EAGLE draft cuda-graph runner.
Subclasses DecodeCudaGraphRunner to inherit the outer capture
loop (capture()), bucket-padding helper (_pad_to_bucket),
and the backend-driven capture/replay scaffolding. EAGLE-specific
bits — buffer dataclass, dummy ForwardBatch construction in
capture_one_shape, replay output unwrap, and can_run_graph — are
overridden.
EAGLE does not call DecodeCudaGraphRunner.__init__ (that init
sets up many decode-only fields like SWA/encoder-decoder/MLA-aware
state). Instead it sets up its own state directly while making sure
the parent's capture() / backend contract is satisfied.
"""
def __init__(
self,
eagle_worker: EagleDraftWorker,
*,
draft_attn_backend=None,
speculative_num_steps: Optional[int] = None,
):
# Parse args
self.eagle_worker = eagle_worker
if not hasattr(eagle_worker, "model_runner"):
# V2: EagleDraftWorker
self.model_runner = model_runner = eagle_worker.draft_runner
else:
self.model_runner = model_runner = eagle_worker.model_runner
# Fields the parent's capture() reads:
self.device = model_runner.device
self.device_module = torch.get_device_module(self.device)
self.tp_size = model_runner.tp_size
self.dp_size = model_runner.dp_size
self.pp_size = model_runner.server_args.pp_size
self.enable_torch_compile = get_flags().capture.enable_torch_compile
self.disable_padding = model_runner.server_args.disable_cuda_graph_padding
self.require_gathered_buffer = require_gathered_buffer(model_runner.server_args)
self.require_mlp_tp_gather = require_mlp_tp_gather(model_runner.server_args)
self.require_mlp_sync = require_mlp_sync(model_runner.server_args)
self.require_attn_tp_gather = require_attn_tp_gather(model_runner.server_args)
self.enable_profile_cuda_graph = (
model_runner.server_args.enable_profile_cuda_graph
)
self.speculative_num_steps = (
model_runner.server_args.speculative_num_steps
if speculative_num_steps is None
else speculative_num_steps
)
self.topk = model_runner.server_args.speculative_eagle_topk
self.draft_attn_backend = draft_attn_backend or model_runner.draft_attn_backend
# Patch_model in parent's capture() needs an attn_backend reference.
# EAGLE doesn't use it (capture_one_shape calls draft_forward instead),
# but the field must exist.
self.attn_backend = self.draft_attn_backend
# Disable parent paths that don't apply to EAGLE.
self.compile_bs = [] # disables patch_model torch.compile wrapping
self.enable_pdmux = False
self.record_nolora_graph = False
self.is_dllm = False
self.deepep_adapter = DeepEPCudaGraphRunnerAdapter()
# Capture-time globals required by parent's capture_one_shape signature.
self.capture_forward_mode = ForwardMode.DECODE
self.capture_hidden_mode = CaptureHiddenMode.LAST
# Bucket sizes
self.capture_bs, _ = get_batch_sizes_to_capture(model_runner)
self.num_tokens_per_bs = self.topk
self.max_bs = max(self.capture_bs)
self.max_num_token = self.max_bs * self.num_tokens_per_bs
# Attention backend init
self.draft_attn_backend.init_cuda_graph_state(self.max_bs, self.max_num_token)
self.seq_len_fill_value = self.draft_attn_backend.attn_backends[
0
].get_cuda_graph_seq_len_fill_value()
self.extend_seq_lens_cpu = [self.seq_len_fill_value] * self.max_bs
if self.enable_torch_compile:
set_torch_compile_config()
# Static buffers
with torch.device(model_runner.device):
input_ids = torch.zeros((self.max_num_token,), dtype=torch.int64)
req_pool_indices = torch.zeros((self.max_bs,), dtype=torch.int64)
out_cache_loc = torch.zeros(
(self.max_num_token * self.speculative_num_steps,),
dtype=self._cache_loc_dtype(),
)
positions = torch.zeros((self.max_num_token,), dtype=torch.int64)
mrope_positions = torch.zeros((3, self.max_num_token), dtype=torch.int64)
rids_int = (
torch.zeros((self.max_bs,), dtype=torch.int64)
if envs.SGLANG_KV_CANARY_ENABLE_TOKEN_ORACLE.get()
else None
)
bootstrap_room_ids_int = (
torch.full((self.max_bs,), -1, dtype=torch.int64)
if envs.SGLANG_KV_CANARY_ENABLE_TOKEN_ORACLE.get()
else None
)
seq_lens = torch.full(
(self.max_bs,), self.seq_len_fill_value, dtype=torch.int64
)
extend_seq_lens = torch.ones((self.max_bs,), dtype=torch.int32)
topk_p = torch.zeros((self.max_bs, self.topk), dtype=torch.float32)
topk_index = torch.zeros((self.max_bs, self.topk), dtype=torch.int64)
draft_probs = (
torch.zeros(
(self.max_bs, self.model_runner.model_config.vocab_size),
dtype=torch.float32,
)
if self.model_runner.server_args.speculative_use_rejection_sampling
else None
)
_hidden_size, _hidden_dtype = get_draft_recurrent_hidden_state_spec(
model_runner
)
hidden_states = (
torch.zeros(
(self.max_bs, _hidden_size),
dtype=_hidden_dtype,
)
if _hidden_size is not None
else None
)
self.temperatures = torch.ones((self.max_bs, 1), dtype=torch.float)
if self.require_gathered_buffer:
if self.require_mlp_tp_gather:
global_num_tokens_gpu = torch.zeros(
(self.dp_size,), dtype=torch.int32
)
global_num_tokens_for_logprob_gpu = torch.zeros(
(self.dp_size,), dtype=torch.int32
)
else:
assert self.require_attn_tp_gather
global_num_tokens_gpu = torch.zeros((1,), dtype=torch.int32)
global_num_tokens_for_logprob_gpu = torch.zeros(
(1,), dtype=torch.int32
)
else:
global_num_tokens_gpu = None
global_num_tokens_for_logprob_gpu = None
seq_lens_cpu = torch.full(
(self.max_bs,), self.seq_len_fill_value, dtype=torch.int64, device="cpu"
)
dsa_seed_topk = (
torch.zeros(
(self.max_bs, self.eagle_worker.dsa_index_topk),
dtype=torch.int32,
device=model_runner.device,
)
if self.eagle_worker.seed_dsa_topk_from_draft_extend
else None
)
self.buffers = EagleDraftInputBuffers(
input_ids=input_ids,
req_pool_indices=req_pool_indices,
out_cache_loc=out_cache_loc,
positions=positions,
mrope_positions=mrope_positions,
rids_int=rids_int,
bootstrap_room_ids_int=bootstrap_room_ids_int,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
extend_seq_lens=extend_seq_lens,
topk_p=topk_p,
topk_index=topk_index,
draft_probs=draft_probs,
hidden_states=hidden_states,
global_num_tokens_gpu=global_num_tokens_gpu,
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
dsa_seed_topk=dsa_seed_topk,
)
self.buffers.share_buffers()
self.backend = resolve_decode_backend(self)
# Capture
try:
with model_capture_mode():
self.capture()
except RuntimeError as e:
raise Exception(
f"Capture cuda graph failed: {e}\n{CUDA_GRAPH_CAPTURE_FAILED_MSG}"
)
def _replay_graph(self, shape_key, forward_batch):
return self.backend.replay(shape_key, forward_batch)
# -----------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------
def _cache_loc_dtype(self):
return torch.int64
def _make_graph_key(self, bs, stream_idx=None, variant_label=None):
# EAGLE doesn't use stream_idx / lora variants.
return ShapeKey(size=bs)
# -----------------------------------------------------------------
# can_run_graph
# -----------------------------------------------------------------
def can_run_graph(self, forward_batch: ForwardBatch):
if self.require_mlp_tp_gather:
cuda_graph_bs = (
max(forward_batch.global_num_tokens_cpu) // self.num_tokens_per_bs
if self.model_runner.spec_algorithm.is_eagle()
or self.model_runner.spec_algorithm.is_standalone()
else max(forward_batch.global_num_tokens_cpu)
)
else:
cuda_graph_bs = forward_batch.batch_size
is_bs_supported = (
self.backend.can_run(forward_batch, cuda_graph_bs)
if self.disable_padding
else cuda_graph_bs <= self.max_bs
)
if self.require_mlp_sync:
is_bs_supported = is_bs_supported and forward_batch.can_run_dp_cuda_graph
return is_bs_supported
# -----------------------------------------------------------------
# Capture (per-shape)
# -----------------------------------------------------------------
def capture_one_shape(
self,
size: int,
forward: Callable,
stream_idx: Optional[int] = None,
variant_label: Optional[str] = None,
):
num_seqs = size # EAGLE legacy name
buffers = self.buffers
num_tokens = num_seqs * self.num_tokens_per_bs
# Graph inputs
req_pool_indices = buffers.req_pool_indices[:num_seqs]
seq_lens = buffers.seq_lens[:num_seqs]
seq_lens_cpu = buffers.seq_lens_cpu[:num_seqs]
extend_seq_lens = buffers.extend_seq_lens[:num_seqs]
extend_seq_lens_cpu = self.extend_seq_lens_cpu[:num_seqs]
out_cache_loc = buffers.out_cache_loc[: num_tokens * self.speculative_num_steps]
positions = buffers.positions[:num_tokens]
mrope_positions = buffers.mrope_positions[:, :num_tokens]
rids_int = buffers.rids_int[:num_seqs] if buffers.rids_int is not None else None
bootstrap_room_ids_int = (
buffers.bootstrap_room_ids_int[:num_seqs]
if buffers.bootstrap_room_ids_int is not None
else None
)
hidden_states = (
buffers.hidden_states[:num_seqs]
if buffers.hidden_states is not None
else None
)
topk_p = buffers.topk_p[:num_seqs]
topk_index = buffers.topk_index[:num_seqs]
draft_probs = (
buffers.draft_probs[:num_seqs] if buffers.draft_probs is not None else None
)
if self.require_mlp_tp_gather:
global_num_tokens_cpu = [num_tokens] * self.dp_size
elif self.require_attn_tp_gather:
global_num_tokens_cpu = [num_tokens]
else:
global_num_tokens_cpu = None
if global_num_tokens_cpu is not None:
global_dp_buffer_len = sum(global_num_tokens_cpu)
num_tokens_tensor = torch.tensor(
global_num_tokens_cpu,
dtype=torch.int32,
device=buffers.input_ids.device,
)
buffers.global_num_tokens_gpu.copy_(num_tokens_tensor)
buffers.global_num_tokens_for_logprob_gpu.copy_(num_tokens_tensor)
global_num_tokens = buffers.global_num_tokens_gpu
global_num_tokens_for_logprob = buffers.global_num_tokens_for_logprob_gpu
else:
global_dp_buffer_len = None
global_num_tokens = None
global_num_tokens_for_logprob = None
capture_mode = (
CaptureHiddenMode.NULL
if self.model_runner.spec_algorithm.is_standalone()
else CaptureHiddenMode.LAST
)
spec_info = EagleDraftInput(
topk_p=topk_p,
topk_index=topk_index,
draft_probs=draft_probs,
hidden_states=hidden_states,
capture_hidden_mode=capture_mode,
)
if self.buffers.dsa_seed_topk is not None:
spec_info.dsa_topk_indices = self.buffers.dsa_seed_topk[:num_seqs]
sampling_info = SamplingBatchInfo(
temperatures=self.temperatures[:num_seqs],
top_ps=torch.ones((num_seqs,), dtype=torch.float),
top_ks=torch.full((num_seqs,), -1, dtype=torch.int32),
min_ps=torch.zeros((num_seqs,), dtype=torch.float),
is_all_greedy=False,
is_any_greedy=False,
need_top_p_sampling=False,
need_top_k_sampling=False,
need_min_p_sampling=False,
vocab_size=self.model_runner.model_config.vocab_size,
)
forward_batch = ForwardBatch(
forward_mode=ForwardMode.DECODE,
batch_size=num_seqs,
input_ids=None,
req_pool_indices=req_pool_indices,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
extend_seq_lens=extend_seq_lens,
extend_seq_lens_cpu=extend_seq_lens_cpu,
out_cache_loc=out_cache_loc,
seq_lens_sum=seq_lens.sum().item(),
return_logprob=False,
positions=positions,
mrope_positions=mrope_positions,
global_num_tokens_gpu=global_num_tokens,
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob,
dp_padding_mode=DpPaddingMode.get_default_mode_in_cuda_graph(),
global_dp_buffer_len=global_dp_buffer_len,
spec_algorithm=self.model_runner.spec_algorithm,
spec_info=spec_info,
sampling_info=sampling_info,
rids_int=rids_int,
bootstrap_room_ids_int=bootstrap_room_ids_int,
capture_hidden_mode=(
spec_info.capture_hidden_mode if spec_info else CaptureHiddenMode.NULL
),
)
def run_once():
self.draft_attn_backend.init_forward_metadata_in_graph(forward_batch)
forward_batch.dp_local_start_pos = forward_batch.dp_local_num_tokens = None
set_dp_buffer_len(
global_dp_buffer_len,
num_tokens,
forward_batch.dp_padding_mode.is_max_len(),
global_num_tokens_cpu,
)
set_is_extend_in_batch(False)
output_cache_loc_backup = forward_batch.out_cache_loc
hidden_states_backup = forward_batch.spec_info.hidden_states
ret = self.eagle_worker.draft_forward(forward_batch)
forward_batch.out_cache_loc = output_cache_loc_backup
forward_batch.spec_info.hidden_states = hidden_states_backup
forward_batch.positions.sub_(self.eagle_worker.speculative_num_steps - 1)
return ret
with forward_context(ForwardContext(attn_backend=self.draft_attn_backend)):
self.draft_attn_backend.init_forward_metadata_out_graph(
forward_batch, in_capture=True
)
# The capture batch is planned here (out-of-forward), so the
# per-step forwards inside draft_forward must not re-plan.
forward_batch.mark_forward_metadata_ready()
self.deepep_adapter.capture(is_extend_in_batch=False)
shape_key = self._make_graph_key(num_seqs)
post_warmup_hook = getattr(
self.draft_attn_backend, "on_after_cuda_graph_warmup", None
)
maybe_flashinfer_autotune_speculative_draft(
self,
run_once,
post_warmup_hook=post_warmup_hook,
skip_logits=False,
)
self.backend.capture_one(
shape_key,
run_once,
dummies=None,
post_warmup_hook=post_warmup_hook,
)
def _postprocess_output_to_raw_bs(self, out, raw_bs):
parent_list, top_scores_index, draft_tokens, draft_probs = (
t[:raw_bs] if t is not None else None for t in out
)
return parent_list, top_scores_index, draft_tokens, draft_probs
# -----------------------------------------------------------------
# Replay
# -----------------------------------------------------------------
def execute(self, forward_batch: ForwardBatch):
assert forward_batch.out_cache_loc is not None
self.deepep_adapter.replay()
buffers = self.buffers
raw_bs = forward_batch.batch_size
raw_num_token = raw_bs * self.num_tokens_per_bs
# Pad to nearest captured shape
if self.require_mlp_tp_gather:
max_num_tokens = max(forward_batch.global_num_tokens_cpu)
max_batch_size = (
max_num_tokens // self.num_tokens_per_bs
if self.model_runner.spec_algorithm.is_eagle()
or self.model_runner.spec_algorithm.is_standalone()
else max_num_tokens
)
bs = self._pad_to_bucket(int(max_batch_size), self.capture_bs)
else:
bs = self._pad_to_bucket(raw_bs, self.capture_bs)
if bs != raw_bs:
buffers.seq_lens.fill_(self.seq_len_fill_value)
buffers.out_cache_loc.zero_()
buffers.positions.zero_()
if buffers.rids_int is not None:
buffers.rids_int.zero_()
if buffers.bootstrap_room_ids_int is not None:
buffers.bootstrap_room_ids_int.fill_(-1)
buffers.topk_p.zero_()
buffers.topk_index.zero_()
if buffers.draft_probs is not None:
buffers.draft_probs.zero_()
if buffers.hidden_states is not None:
buffers.hidden_states.zero_()
if buffers.dsa_seed_topk is not None:
buffers.dsa_seed_topk.zero_()
buffers.req_pool_indices.zero_()
num_tokens = bs * self.num_tokens_per_bs
maybe_detect_nan(
forward_batch.spec_info.topk_p,
"EagleDraftCudaGraphRunner.replay: topk_p",
)
maybe_detect_oob(
forward_batch.spec_info.topk_index,
0,
self.model_runner.model_config.vocab_size,
"EagleDraftCudaGraphRunner.replay: topk_index vs vocab_size="
f"{self.model_runner.model_config.vocab_size}",
)
# Common inputs — batch the small per-field device copies into a grouped
# foreach copy (one foreach call per dtype pair) to cut launch overhead.
# hidden_states is handled separately below (see note), and seq_lens_cpu
# is handled further down since it lives on host.
copy_dsts = [
buffers.seq_lens[:raw_bs],
buffers.out_cache_loc[: raw_num_token * self.speculative_num_steps],
buffers.positions[:raw_num_token],
buffers.topk_p[:raw_bs],
buffers.topk_index[:raw_bs],
buffers.req_pool_indices[:raw_bs],
]
copy_srcs = [
forward_batch.seq_lens,
forward_batch.out_cache_loc,
forward_batch.positions,
forward_batch.spec_info.topk_p,
forward_batch.spec_info.topk_index,
forward_batch.req_pool_indices,
]
if buffers.rids_int is not None and forward_batch.rids_int is not None:
copy_dsts.append(buffers.rids_int[:raw_bs])
copy_srcs.append(forward_batch.rids_int)
if (
buffers.bootstrap_room_ids_int is not None
and forward_batch.bootstrap_room_ids_int is not None
):
copy_dsts.append(buffers.bootstrap_room_ids_int[:raw_bs])
copy_srcs.append(forward_batch.bootstrap_room_ids_int)
_grouped_foreach_copy_(copy_dsts, copy_srcs)
# hidden_states is large + contiguous: copy_() uses the cudaMemcpyAsync
# DMA engine; foreach would force the ~3x slower compute-kernel copy.
if (
buffers.draft_probs is not None
and forward_batch.spec_info.draft_probs is not None
):
buffers.draft_probs[:raw_bs].copy_(forward_batch.spec_info.draft_probs)
if (
buffers.hidden_states is not None
and forward_batch.spec_info.hidden_states is not None
):
buffers.hidden_states[:raw_bs].copy_(forward_batch.spec_info.hidden_states)
if buffers.dsa_seed_topk is not None:
seed = forward_batch.spec_info.dsa_topk_indices
if seed is not None:
buffers.dsa_seed_topk[:raw_bs].copy_(seed)
else:
buffers.dsa_seed_topk[:raw_bs].zero_()
# Only rejection sampling reads temperatures (renorm_draft_probs); skip
# the copy otherwise to keep the non-RS path free of extra work.
if (
self.model_runner.server_args.speculative_use_rejection_sampling
and forward_batch.sampling_info is not None
):
self.temperatures[:raw_bs].copy_(
forward_batch.sampling_info.temperatures[:raw_bs]
)
# TODO(ch-wan): support num_token_non_padded
if self.require_gathered_buffer:
buffers.global_num_tokens_gpu.fill_(bs * self.num_tokens_per_bs)
buffers.global_num_tokens_for_logprob_gpu.fill_(bs * self.num_tokens_per_bs)
# Save the raw seq_lens_sum; it is restored after replay. While the graph
# runs it must reflect the padded fake rows (set below), since draft decode
# backends read seq_lens_sum to size/slice kv_indices.
raw_seq_lens_sum = forward_batch.seq_lens_sum
if bs != raw_bs:
forward_batch.batch_size = bs
forward_batch.seq_lens = buffers.seq_lens[:bs]
forward_batch.req_pool_indices = buffers.req_pool_indices[:bs]
forward_batch.positions = buffers.positions[:num_tokens]
if raw_seq_lens_sum is not None:
forward_batch.seq_lens_sum = (
raw_seq_lens_sum + (bs - raw_bs) * self.seq_len_fill_value
)
if buffers.rids_int is not None and forward_batch.rids_int is not None:
forward_batch.rids_int = buffers.rids_int[:bs]
if (
buffers.bootstrap_room_ids_int is not None
and forward_batch.bootstrap_room_ids_int is not None
):
forward_batch.bootstrap_room_ids_int = buffers.bootstrap_room_ids_int[
:bs
]
if forward_batch.seq_lens_cpu is not None:
if bs != raw_bs:
buffers.seq_lens_cpu.fill_(self.seq_len_fill_value)
buffers.seq_lens_cpu[:raw_bs].copy_(forward_batch.seq_lens_cpu)
forward_batch.seq_lens_cpu = buffers.seq_lens_cpu[:bs]
# forward_batch.batch_size was overwritten to bs above when padding.
self.draft_attn_backend.init_forward_metadata_out_graph(forward_batch)
self.raw_bs = raw_bs
self.bs = bs
# Replay via backend
shape_key = self._make_graph_key(bs)
timer_ctx = (
self.model_runner.device_timer.wrap(metadata={"category": "eagle_draft"})
if self.model_runner.device_timer
else contextlib.nullcontext()
)
with timer_ctx:
out = self._replay_graph(shape_key, forward_batch)
if bs != raw_bs:
out = self._postprocess_output_to_raw_bs(out, raw_bs)
forward_batch.batch_size = raw_bs
forward_batch.positions = buffers.positions[:raw_num_token]
forward_batch.seq_lens = buffers.seq_lens[:raw_bs]
forward_batch.req_pool_indices = buffers.req_pool_indices[:raw_bs]
if buffers.rids_int is not None and forward_batch.rids_int is not None:
forward_batch.rids_int = buffers.rids_int[:raw_bs]
if (
buffers.bootstrap_room_ids_int is not None
and forward_batch.bootstrap_room_ids_int is not None
):
forward_batch.bootstrap_room_ids_int = buffers.bootstrap_room_ids_int[
:raw_bs
]
if forward_batch.seq_lens_cpu is not None:
forward_batch.seq_lens_cpu = buffers.seq_lens_cpu[:raw_bs]
forward_batch.seq_lens_sum = raw_seq_lens_sum
return out
@@ -0,0 +1,617 @@
from __future__ import annotations
import contextlib
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Optional
import torch
from sglang.srt.compilation.torch_compile_decoration import set_torch_compile_config
from sglang.srt.layers.dp_attention import (
DpPaddingMode,
set_dp_buffer_len,
set_is_extend_in_batch,
)
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
from sglang.srt.model_executor.input_buffers import ForwardInputBuffers
from sglang.srt.model_executor.runner import (
DecodeCudaGraphRunner,
DeepEPCudaGraphRunnerAdapter,
ShapeKey,
_grouped_foreach_copy_,
get_batch_sizes_to_capture,
model_capture_mode,
)
from sglang.srt.model_executor.runner.flashinfer_autotune import (
maybe_flashinfer_autotune_speculative_draft,
)
from sglang.srt.model_executor.runner_backend.utils import resolve_decode_backend
from sglang.srt.model_executor.runner_backend_utils import (
CUDA_GRAPH_CAPTURE_FAILED_MSG,
)
from sglang.srt.runtime_context import get_flags
from sglang.srt.speculative.eagle_info import EagleDraftExtendInput
from sglang.srt.speculative.eagle_utils import get_draft_input_from_target_hidden_dim
from sglang.srt.speculative.spec_utils import fast_topk
from sglang.srt.utils import (
is_hip,
require_attn_tp_gather,
require_gathered_buffer,
require_mlp_sync,
require_mlp_tp_gather,
)
_is_hip = is_hip()
if TYPE_CHECKING:
from sglang.srt.speculative.eagle_worker_v2 import EagleDraftWorker
@dataclass
class EagleDraftExtendInputBuffers(ForwardInputBuffers):
input_ids: torch.Tensor
req_pool_indices: torch.Tensor
out_cache_loc: torch.Tensor
positions: torch.Tensor
mrope_positions: torch.Tensor
hidden_states: Optional[torch.Tensor]
seq_lens: torch.Tensor
seq_lens_cpu: torch.Tensor
extend_seq_lens: torch.Tensor
num_correct_drafts: torch.Tensor
num_accept_tokens: torch.Tensor
next_token_logits_buffer: torch.Tensor
global_num_tokens_gpu: Optional[torch.Tensor]
global_num_tokens_for_logprob_gpu: Optional[torch.Tensor]
dsa_seed_topk_capture: Optional[torch.Tensor] = None
class EAGLEDraftExtendCudaGraphRunner(DecodeCudaGraphRunner):
"""EAGLE draft-extend cuda-graph runner.
Subclasses DecodeCudaGraphRunner to inherit the outer capture
loop + backend scaffolding. Overrides capture_one_shape,
replay, can_run_graph for EAGLE-specific draft-extend semantics.
"""
def __init__(
self,
eagle_worker: EagleDraftWorker,
*,
draft_extend_attn_backend=None,
speculative_num_steps: Optional[int] = None,
):
# Parse args
self.eagle_worker = eagle_worker
self.model_runner = model_runner = eagle_worker.draft_runner
self.forward_mode = ForwardMode.DRAFT_EXTEND_V2
# Fields the parent's capture() reads:
self.device = model_runner.device
self.device_module = torch.get_device_module(self.device)
self.tp_size = model_runner.tp_size
self.dp_size = model_runner.dp_size
self.pp_size = model_runner.server_args.pp_size
self.enable_torch_compile = get_flags().capture.enable_torch_compile
self.disable_padding = model_runner.server_args.disable_cuda_graph_padding
self.require_gathered_buffer = require_gathered_buffer(model_runner.server_args)
self.require_mlp_tp_gather = require_mlp_tp_gather(model_runner.server_args)
self.require_mlp_sync = require_mlp_sync(model_runner.server_args)
self.require_attn_tp_gather = require_attn_tp_gather(model_runner.server_args)
self.enable_profile_cuda_graph = (
model_runner.server_args.enable_profile_cuda_graph
)
self.speculative_num_steps = (
model_runner.server_args.speculative_num_steps
if speculative_num_steps is None
else speculative_num_steps
)
self.topk = model_runner.server_args.speculative_eagle_topk
self.draft_extend_attn_backend = (
draft_extend_attn_backend or eagle_worker.draft_extend_attn_backend
)
self.attn_backend = self.draft_extend_attn_backend
# Disable parent paths that don't apply.
self.compile_bs = []
self.enable_pdmux = False
self.record_nolora_graph = False
self.is_dllm = False
self.deepep_adapter = DeepEPCudaGraphRunnerAdapter()
self.capture_forward_mode = self.forward_mode
self.capture_hidden_mode = CaptureHiddenMode.LAST
self.capture_bs, _ = get_batch_sizes_to_capture(model_runner)
self.padded_static_len = -1
# Size cuda-graph buffers by num_draft_tokens (full tree width), not
# num_steps + 1, or topk > 1 draft-extend overflows them.
self.num_tokens_per_bs = model_runner.server_args.speculative_num_draft_tokens
self.max_bs = max(self.capture_bs)
self.max_num_token = self.max_bs * self.num_tokens_per_bs
self.draft_extend_attn_backend.init_cuda_graph_state(
self.max_bs, self.max_num_token
)
self.seq_len_fill_value = (
self.draft_extend_attn_backend.get_cuda_graph_seq_len_fill_value()
)
self.extend_seq_lens_cpu = [self.num_tokens_per_bs] * self.max_bs
if self.enable_torch_compile:
set_torch_compile_config()
with torch.device(model_runner.device):
input_ids = torch.zeros((self.max_num_token,), dtype=torch.int64)
req_pool_indices = torch.zeros((self.max_bs,), dtype=torch.int64)
out_cache_loc = torch.ones(
(self.max_num_token,), dtype=self._cache_loc_dtype()
)
positions = torch.zeros((self.max_num_token,), dtype=torch.int64)
mrope_positions = torch.zeros((3, self.max_num_token), dtype=torch.int64)
# Width and dtype both come from the draft `model_runner` so the
# source stays consistent (the draft dtype matches the target dtype
# that produced these hidden states).
_hidden_dtype = model_runner.model_config.dtype
_hidden_size = (
None
if self.eagle_worker.speculative_algorithm.is_standalone()
else get_draft_input_from_target_hidden_dim(model_runner)
)
hidden_states = (
torch.zeros(
(self.max_num_token, _hidden_size),
dtype=_hidden_dtype,
)
if _hidden_size is not None
else None
)
self.seq_len_fill_value = (
self.draft_extend_attn_backend.get_cuda_graph_seq_len_fill_value()
)
seq_lens = torch.full(
(self.max_bs,), self.seq_len_fill_value, dtype=torch.int64
)
extend_seq_lens = torch.full(
(self.max_bs,), self.num_tokens_per_bs, dtype=torch.int32
)
num_correct_drafts = torch.full(
(self.max_bs,), self.num_tokens_per_bs, dtype=torch.int32
)
num_accept_tokens = torch.full(
(self.max_bs,), self.num_tokens_per_bs, dtype=torch.int32
)
if self.require_gathered_buffer:
if self.require_mlp_tp_gather:
global_num_tokens_gpu = torch.zeros(
(self.dp_size,), dtype=torch.int32
)
global_num_tokens_for_logprob_gpu = torch.zeros(
(self.dp_size,), dtype=torch.int32
)
else:
assert self.require_attn_tp_gather
global_num_tokens_gpu = torch.zeros((1,), dtype=torch.int32)
global_num_tokens_for_logprob_gpu = torch.zeros(
(1,), dtype=torch.int32
)
else:
global_num_tokens_gpu = None
global_num_tokens_for_logprob_gpu = None
hot_token_id = getattr(self.eagle_worker, "hot_token_id", None)
if hasattr(
self.model_runner.model_config.hf_config, "draft_vocab_size"
): # llama_eagle
vocab_size = self.model_runner.model_config.hf_config.draft_vocab_size
elif hasattr(
self.model_runner.model_config.hf_config, "hot_vocab_size"
): # llama_eagle3
vocab_size = self.model_runner.model_config.hf_config.hot_vocab_size
elif hot_token_id is not None:
# FR-Spec: reduced vocab is injected via a late
# json_model_override_args, so hf_config lacks it; size from the head.
vocab_size = len(hot_token_id)
else:
vocab_size = self.model_runner.model_config.vocab_size
next_token_logits_buffer = (
self.model_runner.graph_shared_output.get_logits_buffer(
vocab_size, rows=self.max_bs * self.num_tokens_per_bs
)
)
seq_lens_cpu = torch.full(
(self.max_bs,), self.seq_len_fill_value, dtype=torch.int64, device="cpu"
)
dsa_seed_topk_capture = (
torch.full(
(self.max_num_token, self.eagle_worker.dsa_index_topk),
-1,
dtype=torch.int32,
device=model_runner.device,
)
if self.eagle_worker.seed_dsa_topk_from_draft_extend
else None
)
self.buffers = EagleDraftExtendInputBuffers(
input_ids=input_ids,
req_pool_indices=req_pool_indices,
out_cache_loc=out_cache_loc,
positions=positions,
mrope_positions=mrope_positions,
hidden_states=hidden_states,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
extend_seq_lens=extend_seq_lens,
num_correct_drafts=num_correct_drafts,
num_accept_tokens=num_accept_tokens,
next_token_logits_buffer=next_token_logits_buffer,
global_num_tokens_gpu=global_num_tokens_gpu,
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
dsa_seed_topk_capture=dsa_seed_topk_capture,
)
self.buffers.share_buffers()
self.backend = resolve_decode_backend(self)
try:
with model_capture_mode():
self.capture()
except RuntimeError as e:
raise Exception(
f"Capture cuda graph failed: {e}\n{CUDA_GRAPH_CAPTURE_FAILED_MSG}"
)
def _replay_graph(self, shape_key, forward_batch):
return self.backend.replay(shape_key, forward_batch)
def _cache_loc_dtype(self):
return torch.int64
def _make_graph_key(self, bs, stream_idx=None, variant_label=None):
return ShapeKey(size=bs)
def can_run_graph(self, forward_batch: ForwardBatch):
if self.require_mlp_tp_gather:
cuda_graph_bs = (
max(forward_batch.global_num_tokens_cpu) // self.num_tokens_per_bs
if self.model_runner.spec_algorithm.is_eagle()
or self.model_runner.spec_algorithm.is_standalone()
else max(forward_batch.global_num_tokens_cpu)
)
else:
cuda_graph_bs = forward_batch.seq_lens.numel()
is_bs_supported = (
self.backend.can_run(forward_batch, cuda_graph_bs)
if self.disable_padding
else cuda_graph_bs <= self.max_bs
)
if self.require_mlp_sync:
is_bs_supported = is_bs_supported and forward_batch.can_run_dp_cuda_graph
return is_bs_supported
def capture_one_shape(
self,
size: int,
forward: Callable,
stream_idx: Optional[int] = None,
variant_label: Optional[str] = None,
):
bs = size
buffers = self.buffers
num_tokens = bs * self.num_tokens_per_bs
# Graph inputs
input_ids = buffers.input_ids[:num_tokens]
req_pool_indices = buffers.req_pool_indices[:bs]
seq_lens = buffers.seq_lens[:bs]
seq_lens_cpu = buffers.seq_lens_cpu[:bs]
extend_seq_lens = buffers.extend_seq_lens[:bs]
extend_seq_lens_cpu = self.extend_seq_lens_cpu[:bs]
out_cache_loc = buffers.out_cache_loc[:num_tokens]
positions = buffers.positions[:num_tokens]
mrope_positions = buffers.mrope_positions[:, :num_tokens]
hidden_states = (
buffers.hidden_states[:num_tokens]
if buffers.hidden_states is not None
else None
)
num_correct_drafts = buffers.num_correct_drafts[:bs]
num_accept_tokens = buffers.num_accept_tokens[:bs]
next_token_logits_buffer = buffers.next_token_logits_buffer[:num_tokens]
# pruned_states = num_tokens (all tokens)
num_tokens_for_logprob = num_tokens
if self.require_mlp_tp_gather:
global_num_tokens_cpu = [num_tokens] * self.dp_size
elif self.require_attn_tp_gather:
global_num_tokens_cpu = [num_tokens]
else:
global_num_tokens_cpu = None
if global_num_tokens_cpu is not None:
global_dp_buffer_len = sum(global_num_tokens_cpu)
buffers.global_num_tokens_gpu.copy_(
torch.tensor(
global_num_tokens_cpu,
dtype=torch.int32,
device=buffers.input_ids.device,
)
)
buffers.global_num_tokens_for_logprob_gpu.copy_(
torch.tensor(
[num_tokens_for_logprob] * len(global_num_tokens_cpu),
dtype=torch.int32,
device=buffers.input_ids.device,
)
)
else:
global_dp_buffer_len = None
spec_info = EagleDraftExtendInput(
hidden_states=hidden_states,
num_correct_drafts=num_correct_drafts,
num_accept_tokens=num_accept_tokens,
# Padded tree width per req; drives the constant qo layout.
num_tokens_per_req=self.num_tokens_per_bs,
)
forward_batch = ForwardBatch(
forward_mode=self.forward_mode,
batch_size=bs,
input_ids=input_ids,
req_pool_indices=req_pool_indices,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
next_token_logits_buffer=next_token_logits_buffer,
extend_seq_lens=extend_seq_lens,
extend_seq_lens_cpu=extend_seq_lens_cpu,
out_cache_loc=out_cache_loc,
seq_lens_sum=seq_lens.sum().item(),
return_logprob=False,
positions=positions,
mrope_positions=mrope_positions,
global_num_tokens_gpu=buffers.global_num_tokens_gpu,
global_num_tokens_for_logprob_gpu=buffers.global_num_tokens_for_logprob_gpu,
dp_padding_mode=DpPaddingMode.get_default_mode_in_cuda_graph(),
global_dp_buffer_len=global_dp_buffer_len,
spec_algorithm=self.model_runner.spec_algorithm,
spec_info=spec_info,
capture_hidden_mode=CaptureHiddenMode.LAST,
padded_static_len=self.padded_static_len,
)
if self.buffers.dsa_seed_topk_capture is not None:
spec_info.dsa_seed_topk_capture = self.buffers.dsa_seed_topk_capture[
:num_tokens
]
def run_once():
self.draft_extend_attn_backend.init_forward_metadata_in_graph(forward_batch)
# Clean intermediate result cache for DP attention
forward_batch.dp_local_start_pos = forward_batch.dp_local_num_tokens = None
set_dp_buffer_len(
global_dp_buffer_len,
num_tokens,
forward_batch.dp_padding_mode.is_max_len(),
global_num_tokens_cpu,
)
set_is_extend_in_batch(False)
output_cache_loc_backup = forward_batch.out_cache_loc
hidden_states_backup = forward_batch.spec_info.hidden_states
ret = self.model_runner.model.forward(
forward_batch.input_ids,
forward_batch.positions,
forward_batch,
)
# ROCm's argmax tie-breaks differently from CUDA's softmax+max
# path on FP8 logits, which corrupts MTP draft selection on AMD.
# Keep the fastpath CUDA-only.
if self.topk == 1 and not _is_hip:
ret.topk_index = torch.argmax(
ret.next_token_logits, dim=-1, keepdim=True
)
ret.topk_p = torch.ones_like(ret.topk_index, dtype=torch.float32)
else:
probs = torch.softmax(ret.next_token_logits, dim=-1)
ret.topk_p, ret.topk_index = fast_topk(probs, self.topk, dim=-1)
forward_batch.out_cache_loc = output_cache_loc_backup
forward_batch.spec_info.hidden_states = hidden_states_backup
return ret
with forward_context(
ForwardContext(attn_backend=self.draft_extend_attn_backend)
):
self.draft_extend_attn_backend.init_forward_metadata_out_graph(
forward_batch, in_capture=True
)
self.deepep_adapter.capture(is_extend_in_batch=True)
canary_ctx = (
c.with_active_single_forward_manager(0)
if (c := self.model_runner.canary_manager) is not None
else contextlib.nullcontext()
)
with canary_ctx:
shape_key = self._make_graph_key(bs)
post_warmup_hook = getattr(
self.draft_extend_attn_backend,
"on_after_cuda_graph_warmup",
None,
)
maybe_flashinfer_autotune_speculative_draft(
self,
run_once,
post_warmup_hook=post_warmup_hook,
skip_logits=False,
)
self.backend.capture_one(
shape_key,
run_once,
dummies=None,
post_warmup_hook=post_warmup_hook,
)
def execute(self, forward_batch: ForwardBatch):
assert forward_batch.out_cache_loc is not None
self.deepep_adapter.replay()
buffers = self.buffers
raw_bs = forward_batch.batch_size
num_tokens = forward_batch.input_ids.shape[0]
if self.require_mlp_tp_gather:
max_num_tokens = max(forward_batch.global_num_tokens_cpu)
max_batch_size = (
max_num_tokens // self.num_tokens_per_bs
if self.model_runner.spec_algorithm.is_eagle()
else max_num_tokens
)
bs = self._pad_to_bucket(int(max_batch_size), self.capture_bs)
else:
bs = self._pad_to_bucket(raw_bs, self.capture_bs)
if bs * self.num_tokens_per_bs != num_tokens:
buffers.seq_lens.fill_(self.seq_len_fill_value)
buffers.out_cache_loc.zero_()
buffers.positions.zero_()
# Pair with seq_lens fill: padded rows must point at reserved
# req_pool slot 0 (req_to_token[0, :] is all zeros from init).
buffers.req_pool_indices.zero_()
buffers.num_correct_drafts.fill_(self.num_tokens_per_bs)
buffers.num_accept_tokens.fill_(self.num_tokens_per_bs)
buffers.extend_seq_lens.fill_(self.num_tokens_per_bs)
# Batch the small per-field device copies into a grouped foreach copy
# (one foreach call per dtype pair) to cut launch overhead. hidden_states
# is handled separately below (see note), and seq_lens_cpu is handled
# further down since it lives on host.
copy_dsts = [
buffers.input_ids[:num_tokens],
buffers.seq_lens[:raw_bs],
buffers.out_cache_loc[:num_tokens],
buffers.positions[:num_tokens],
buffers.req_pool_indices[:raw_bs],
]
copy_srcs = [
forward_batch.input_ids,
forward_batch.seq_lens,
forward_batch.out_cache_loc,
forward_batch.positions,
forward_batch.req_pool_indices,
]
if forward_batch.extend_seq_lens is not None:
copy_dsts.append(buffers.extend_seq_lens[:raw_bs])
copy_srcs.append(forward_batch.extend_seq_lens)
else:
buffers.extend_seq_lens[:raw_bs].fill_(self.num_tokens_per_bs)
if forward_batch.spec_info.num_correct_drafts is not None:
copy_dsts.append(buffers.num_correct_drafts[:raw_bs])
copy_srcs.append(forward_batch.spec_info.num_correct_drafts)
copy_dsts.append(buffers.num_accept_tokens[:raw_bs])
copy_srcs.append(forward_batch.spec_info.num_accept_tokens)
_grouped_foreach_copy_(copy_dsts, copy_srcs)
# hidden_states is large + contiguous: copy_() uses the cudaMemcpyAsync
# DMA engine; foreach would force the ~3x slower compute-kernel copy.
if (
buffers.hidden_states is not None
and forward_batch.spec_info.hidden_states is not None
and forward_batch.spec_info.hidden_states.shape[1]
== buffers.hidden_states.shape[1]
):
buffers.hidden_states[:num_tokens].copy_(
forward_batch.spec_info.hidden_states
)
# TODO(ch-wan): support num_token_non_padded
if self.require_gathered_buffer:
buffers.global_num_tokens_gpu.fill_(bs * self.num_tokens_per_bs)
buffers.global_num_tokens_for_logprob_gpu.fill_(bs * self.num_tokens_per_bs)
if forward_batch.seq_lens_cpu is not None:
if bs != raw_bs:
buffers.seq_lens_cpu.fill_(self.seq_len_fill_value)
buffers.seq_lens_cpu[:raw_bs].copy_(forward_batch.seq_lens_cpu)
if forward_batch.extend_seq_lens_cpu is not None:
self.extend_seq_lens_cpu[:raw_bs] = forward_batch.extend_seq_lens_cpu
else:
self.extend_seq_lens_cpu[:raw_bs] = [self.num_tokens_per_bs] * raw_bs
if bs > raw_bs:
self.extend_seq_lens_cpu[raw_bs:bs] = [self.num_tokens_per_bs] * (
bs - raw_bs
)
forward_batch.spec_info.extend_seq_lens_cpu = list(
self.extend_seq_lens_cpu[:bs]
)
forward_batch.spec_info.extend_seq_lens_tensor = buffers.extend_seq_lens[:bs]
if bs != raw_bs:
forward_batch.spec_info.positions = buffers.positions[:num_tokens]
forward_batch.spec_info.num_correct_drafts = buffers.num_correct_drafts[:bs]
forward_batch.spec_info.num_accept_tokens = buffers.num_accept_tokens[:bs]
from types import SimpleNamespace
seq_lens_sum = forward_batch.seq_lens_sum
if seq_lens_sum is not None:
seq_lens_sum = seq_lens_sum + (bs - raw_bs) * self.seq_len_fill_value
fb_view = SimpleNamespace(
batch_size=bs,
forward_mode=self.forward_mode,
input_ids=getattr(forward_batch, "input_ids", None),
req_pool_indices=buffers.req_pool_indices,
seq_lens=buffers.seq_lens,
seq_lens_sum=seq_lens_sum,
seq_lens_cpu=buffers.seq_lens_cpu,
encoder_lens=None,
out_cache_loc=buffers.out_cache_loc[:num_tokens],
out_cache_loc_dsv4=getattr(forward_batch, "out_cache_loc_dsv4", None),
spec_info=forward_batch.spec_info,
)
self.draft_extend_attn_backend.init_forward_metadata_out_graph(fb_view)
# Snapshot built -- the forward is done reading the shared pool. Publish
# a read-done event the scheduler's WAR barrier waits on.
read_done = self.device_module.Event()
read_done.record()
self.model_runner.war_fastpath_read_done_event = read_done
self.raw_bs = raw_bs
self.bs = bs
shape_key = self._make_graph_key(bs)
timer_ctx = (
self.model_runner.device_timer.wrap(
metadata={"category": "eagle_draft_extend"}
)
if self.model_runner.device_timer
else contextlib.nullcontext()
)
with timer_ctx:
out = self._replay_graph(shape_key, forward_batch)
out = LogitsProcessorOutput(
next_token_logits=out.next_token_logits[:num_tokens],
hidden_states=out.hidden_states[:num_tokens],
)
return out
+406
View File
@@ -0,0 +1,406 @@
import logging
from dataclasses import dataclass
from typing import List, Optional, Tuple
import torch
from sglang.srt.constrained.base_grammar_backend import BaseGrammarObject
from sglang.srt.environ import envs
from sglang.srt.layers.attention.utils import create_flashinfer_kv_indices_triton
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode
from sglang.srt.runtime_context import get_server_args
from sglang.srt.speculative.spec_info import SpecInput, SpecInputType
logger = logging.getLogger(__name__)
@dataclass
class EagleVerifyInput(SpecInput):
draft_token: torch.Tensor
custom_mask: torch.Tensor
positions: torch.Tensor
retrieve_index: torch.Tensor
retrieve_next_token: torch.Tensor
retrieve_next_sibling: torch.Tensor
retrieve_cum_len: torch.Tensor
spec_steps: int
topk: int
draft_token_num: int
capture_hidden_mode: CaptureHiddenMode
seq_lens_sum: int
seq_lens_cpu: torch.Tensor
grammar: BaseGrammarObject = None
# Stacked per-step draft proposal distribution q, shape (bs, num_steps,
# vocab); only set under rejection sampling. Consumed by the verify kernel.
draft_probs: torch.Tensor = None
# Shape info for padding
num_tokens_per_req: int = -1 # -1 auto-fills from draft_token_num.
def __post_init__(self):
super().__init__(SpecInputType.EAGLE_VERIFY)
if self.num_tokens_per_req < 0:
self.num_tokens_per_req = self.draft_token_num
@property
def max_tree_depth(self) -> int:
"""Longest root-to-leaf chain of the verify tree, incl. the root;
bounds the accept_index row width. EAGLE trees are depth-bounded by
the draft loop. Algorithms with other tree shapes override this."""
return self.spec_steps + 1
@property
def tree_topk(self) -> int:
"""Branching factor passed to the tree-verify kernels; -1 means an
irregular tree (no fixed per-level branching)."""
return self.topk
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
return self.draft_token_num, self.draft_token_num
@classmethod
def create_idle_input(
cls, topk: int, spec_steps: int, num_verify_tokens: int, device: str
):
return cls(
draft_token=torch.empty((0,), dtype=torch.long, device=device),
custom_mask=torch.full((0,), True, dtype=torch.bool, device=device),
positions=torch.empty((0,), dtype=torch.int64, device=device),
retrieve_index=torch.full(
(0, num_verify_tokens), -1, dtype=torch.long, device=device
),
retrieve_next_token=torch.full(
(0, num_verify_tokens), -1, dtype=torch.long, device=device
),
retrieve_next_sibling=torch.full(
(0, num_verify_tokens), -1, dtype=torch.long, device=device
),
retrieve_cum_len=None,
topk=topk,
draft_token_num=num_verify_tokens,
spec_steps=spec_steps,
capture_hidden_mode=CaptureHiddenMode.FULL,
seq_lens_sum=0,
seq_lens_cpu=torch.empty((0,), dtype=torch.int64),
)
def generate_attn_arg_prefill(
self,
req_pool_indices: torch.Tensor,
paged_kernel_lens: torch.Tensor,
paged_kernel_lens_sum: int,
req_to_token: torch.Tensor,
):
device = req_pool_indices.device
batch_size = len(req_pool_indices)
qo_indptr = torch.arange(
0,
(1 + batch_size) * self.draft_token_num,
step=self.draft_token_num,
dtype=torch.int32,
device=device,
)
cum_kv_seq_len = torch.zeros(
(batch_size + 1,), dtype=torch.int32, device=device
)
paged_kernel_lens = paged_kernel_lens + self.draft_token_num
cum_kv_seq_len[1:] = torch.cumsum(paged_kernel_lens, dim=0)
kv_indices = torch.empty(
paged_kernel_lens_sum + self.draft_token_num * batch_size,
dtype=torch.int32,
device=device,
)
create_flashinfer_kv_indices_triton[(batch_size,)](
req_to_token,
req_pool_indices,
paged_kernel_lens,
cum_kv_seq_len,
None,
kv_indices,
req_to_token.size(1),
)
mask_numel = (
paged_kernel_lens_sum * self.draft_token_num
+ (self.draft_token_num**2) * batch_size
)
if self.custom_mask.numel() < mask_numel:
# FIXME(attn): temporary fix for custom mask padding with cuda graph
self.custom_mask = torch.cat(
[
self.custom_mask,
torch.full(
(mask_numel - self.custom_mask.numel(),),
True,
dtype=torch.bool,
device=device,
),
],
dim=0,
)
return kv_indices, cum_kv_seq_len, qo_indptr, self.custom_mask
@dataclass
class EagleDraftInput(SpecInput):
# For idle stubs use `create_idle_input`, not the bare ctor: `filter_batch`
# / `merge_batch` slice / cat `topk_p` / `topk_index` / `hidden_states` /
# `bonus_tokens` unconditionally.
# shape: (b, topk)
topk_p: torch.Tensor = None
topk_index: torch.Tensor = None
# Draft proposal q from draft-extend, only set under rejection sampling:
# (b, vocab) single-layer; (b, num_steps, vocab) multi-layer chain.
draft_probs: torch.Tensor = None
# shape: (b, hidden_size) - one hidden per req, consumed by `draft` forward.
# None when the spec algorithm's draft doesn't read hidden_states
# (e.g., STANDALONE — vanilla LLM draft).
hidden_states: Optional[torch.Tensor] = None
capture_hidden_mode: CaptureHiddenMode = CaptureHiddenMode.FULL
# Survives across draft steps: spec_info is shared by reference across the
# per-step forwards (each runs on a copied ForwardBatch, dropping writebacks).
dsa_topk_indices: Optional[torch.Tensor] = None
# Per-req bonus token (the "+1" target prediction at end of each accept
# chain); the worker copies it here post-extend for next iter's draft.
bonus_tokens: torch.Tensor = None
# shape: (b + 1,)
kv_indptr: torch.Tensor = None
kv_indices: torch.Tensor = None
num_tokens_per_req: int = -1
num_tokens_for_logprob_per_req: int = -1
# V2 overlap worker only: req_pool_indices used as buf slot keys.
future_indices: Optional[torch.Tensor] = None
def __post_init__(self):
super().__init__(SpecInputType.EAGLE_DRAFT)
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
return self.num_tokens_per_req, self.num_tokens_for_logprob_per_req
@classmethod
def create_idle_input(
cls,
device: torch.device,
hidden_size: Optional[int],
dtype: Optional[torch.dtype],
topk: int,
capture_hidden_mode: CaptureHiddenMode,
vocab_size: int = 0,
):
return cls(
bonus_tokens=torch.empty((0,), device=device, dtype=torch.int32),
hidden_states=(
torch.empty((0, hidden_size), device=device, dtype=dtype)
if hidden_size is not None
else None
),
topk_p=torch.empty((0, topk), device=device, dtype=torch.float32),
topk_index=torch.empty((0, topk), device=device, dtype=torch.int64),
draft_probs=(
torch.empty((0, vocab_size), device=device, dtype=torch.float32)
if get_server_args().speculative_use_rejection_sampling
else None
),
capture_hidden_mode=capture_hidden_mode,
)
def filter_batch(self, new_indices: torch.Tensor, has_been_filtered: bool = True):
if self.future_indices is not None:
self.future_indices = self.future_indices[new_indices]
return
strict_check = envs.SGLANG_SPEC_ENABLE_STRICT_FILTER_CHECK.get()
if has_been_filtered:
# in eagle_utils.py:verify, we have already filtered the batch by `unfinished_index`
# therefore, we don't need to filter the batch again in scheduler
error_msg = f"length of new_indices: {len(new_indices)} != length of topk_p: {len(self.topk_p)}, this should not happen"
if len(new_indices) != len(self.topk_p):
if strict_check:
raise ValueError(error_msg)
else:
logger.warning(error_msg)
self.topk_p = self.topk_p[: len(new_indices)]
self.topk_index = self.topk_index[: len(new_indices)]
if self.draft_probs is not None:
self.draft_probs = self.draft_probs[: len(new_indices)]
if self.hidden_states is not None:
self.hidden_states = self.hidden_states[: len(new_indices)]
self.bonus_tokens = self.bonus_tokens[: len(new_indices)]
if self.dsa_topk_indices is not None:
self.dsa_topk_indices = self.dsa_topk_indices[: len(new_indices)]
else:
# in some cases(e.g draft_extend), we have not filtered the batch by `unfinished_index`
self.topk_p = self.topk_p[new_indices]
self.topk_index = self.topk_index[new_indices]
if self.draft_probs is not None:
self.draft_probs = self.draft_probs[new_indices]
if self.hidden_states is not None:
self.hidden_states = self.hidden_states[new_indices]
self.bonus_tokens = self.bonus_tokens[new_indices]
if self.dsa_topk_indices is not None:
self.dsa_topk_indices = self.dsa_topk_indices[new_indices]
def merge_batch(self, spec_info: "EagleDraftInput"):
if self.future_indices is not None:
assert spec_info.future_indices is not None
self.future_indices = torch.cat(
[self.future_indices, spec_info.future_indices]
)
return
# Detect idle stub by `topk_index` length (idle inputs have
# shape[0] == 0 across all fields). Don't use `hidden_states is None`:
# for STANDALONE all non-idle inputs also have None hidden_states.
if len(self.topk_index) == 0:
self.hidden_states = spec_info.hidden_states
self.bonus_tokens = spec_info.bonus_tokens
self.topk_p = spec_info.topk_p
self.topk_index = spec_info.topk_index
self.draft_probs = spec_info.draft_probs
self.dsa_topk_indices = spec_info.dsa_topk_indices
return
if len(spec_info.topk_index) == 0:
return
if self.hidden_states is not None and spec_info.hidden_states is not None:
self.hidden_states = torch.cat(
[self.hidden_states, spec_info.hidden_states], axis=0
)
self.bonus_tokens = torch.cat(
[self.bonus_tokens, spec_info.bonus_tokens], axis=0
)
self.topk_p = torch.cat([self.topk_p, spec_info.topk_p])
self.topk_index = torch.cat([self.topk_index, spec_info.topk_index])
if self.dsa_topk_indices is not None and spec_info.dsa_topk_indices is not None:
self.dsa_topk_indices = torch.cat(
[self.dsa_topk_indices, spec_info.dsa_topk_indices]
)
else:
self.dsa_topk_indices = None
if self.draft_probs is not None and spec_info.draft_probs is not None:
self.draft_probs = torch.cat([self.draft_probs, spec_info.draft_probs])
@dataclass
class EagleDraftExtendInput(SpecInput):
"""Inputs to the draft-extend forward (the fill-draft-kvcache pass after
target prefill / verify).
Installed on `batch.spec_info` by the worker's `_draft_extend_for_*`
(and synthetically by draft-extend cuda-graph capture), then replaced
with a fresh `EagleDraftInput` for the next iter's draft.
"""
# Target-model hidden states for the draft-extend forward; None when the
# draft doesn't read hidden_states (e.g., STANDALONE). Shape: decode
# (bs * num_draft_tokens, hidden), prefill (extend_num_tokens, hidden).
hidden_states: Optional[torch.Tensor] = None
# Per-req accept counts. `num_accept_tokens = num_correct_drafts + 1`.
# Both kept for cuda-graph buffer indexing.
num_correct_drafts: torch.Tensor = None
num_accept_tokens: torch.Tensor = None
# CPU view, read by attention backends during the extend forward.
num_accept_tokens_cpu: List[int] = None
# Per-req batch-state slices for the draft-extend forward:
# - input_ids: accept tokens flat over surviving reqs
# - seq_lens / _cpu: per-req sequence length (post-accept)
# - req_pool_indices: per-req kv-pool slot
input_ids: torch.Tensor = None
seq_lens: torch.Tensor = None
seq_lens_cpu: torch.Tensor = None
req_pool_indices: torch.Tensor = None
# - positions: shape `[total_accepted]`.
# - bonus_tokens: shape `[bs]`; read post-extend to populate next iter's
# `EagleDraftInput.bonus_tokens`.
positions: Optional[torch.Tensor] = None
bonus_tokens: Optional[torch.Tensor] = None
capture_hidden_mode: CaptureHiddenMode = CaptureHiddenMode.LAST
num_tokens_per_req: int = -1
num_tokens_for_logprob_per_req: int = 1
dsa_seed_topk_capture: Optional[torch.Tensor] = None
dsa_seed_topk_select: Optional[torch.Tensor] = None
# None for draft-extend's idle batch; attention backends fall back to
# rebuilding plain metadata from seq_lens when this is None.
kv_indptr: torch.Tensor = None
def __post_init__(self):
super().__init__(SpecInputType.EAGLE_DRAFT_EXTEND)
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
return self.num_tokens_per_req, self.num_tokens_for_logprob_per_req
@classmethod
def create_idle_input(
cls,
device: torch.device,
hidden_size: Optional[int],
dtype: Optional[torch.dtype],
capture_hidden_mode: CaptureHiddenMode = CaptureHiddenMode.LAST,
) -> "EagleDraftExtendInput":
return cls(
hidden_states=(
torch.empty((0, hidden_size), device=device, dtype=dtype)
if hidden_size is not None
else None
),
num_correct_drafts=torch.empty((0,), device=device, dtype=torch.int32),
num_accept_tokens=torch.empty((0,), device=device, dtype=torch.int32),
num_accept_tokens_cpu=[],
input_ids=torch.empty((0,), device=device, dtype=torch.long),
seq_lens=torch.empty((0,), device=device, dtype=torch.int64),
seq_lens_cpu=torch.empty((0,), dtype=torch.int64),
req_pool_indices=torch.empty((0,), device=device, dtype=torch.int64),
capture_hidden_mode=capture_hidden_mode,
)
def generate_attn_arg_prefill(
self,
req_pool_indices: torch.Tensor,
paged_kernel_lens: torch.Tensor,
paged_kernel_lens_sum: Optional[int],
req_to_token: torch.Tensor,
):
device = req_pool_indices.device
bs = self.num_correct_drafts.numel()
# Constant num_tokens_per_req qo layout (required for cuda-graph capture).
qo_indptr = torch.arange(
0,
(bs + 1) * self.num_tokens_per_req,
step=self.num_tokens_per_req,
dtype=torch.int32,
device=device,
)
cum_kv_seq_len = torch.zeros((bs + 1,), dtype=torch.int32, device=device)
cum_kv_seq_len[1:] = torch.cumsum(paged_kernel_lens, dim=0)
if paged_kernel_lens_sum is None:
paged_kernel_lens_sum = cum_kv_seq_len[-1]
kv_indices = torch.empty(
paged_kernel_lens_sum, dtype=torch.int32, device=device
)
create_flashinfer_kv_indices_triton[(bs,)](
req_to_token,
req_pool_indices,
paged_kernel_lens,
cum_kv_seq_len,
None,
kv_indices,
req_to_token.size(1),
)
return kv_indices, cum_kv_seq_len, qo_indptr, None
@@ -0,0 +1,892 @@
from __future__ import annotations
import logging
import math
from collections import defaultdict
from enum import IntEnum
from typing import TYPE_CHECKING, List, Optional
import torch
from sglang.kernels.ops.speculative.spec_tree import (
sgl_build_tree_kernel_efficient_triton,
verify_tree_greedy_kernel_triton,
)
from sglang.srt.hardware_backend.npu.dsv4.dsv4_allocator import (
alloc_paged_token_slots_extend_npu,
)
from sglang.srt.hardware_backend.npu.dsv4.dsv4_common_hooks import (
maybe_build_dsv4_verify_bundle,
)
from sglang.srt.mem_cache.common import (
alloc_paged_token_slots_extend,
alloc_token_slots,
get_alloc_reserve_per_decode,
get_last_loc,
)
from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils import (
is_cpu,
is_cuda,
is_hip,
is_musa,
is_npu,
is_xpu,
)
from sglang.srt.utils.async_probe import maybe_detect_oob
if TYPE_CHECKING:
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
from sglang.srt.model_executor.model_runner import ModelRunner
from sglang.srt.speculative.eagle_info import EagleVerifyInput
_is_cuda = is_cuda()
_is_hip = is_hip()
_is_npu = is_npu()
_is_musa = is_musa()
_is_xpu = is_xpu()
_is_cpu = is_cpu()
logger = logging.getLogger(__name__)
if _is_cuda or _is_hip or _is_musa:
from sgl_kernel import (
build_tree_kernel_efficient as sgl_build_tree_kernel_efficient,
)
elif _is_cpu:
from sgl_kernel import (
build_tree_kernel_efficient_cpu as sgl_build_tree_kernel_efficient_cpu,
)
from sgl_kernel import verify_tree_greedy_cpu as sgl_verify_tree_greedy_cpu
ALLOC_EXTEND_FUNCS = defaultdict(
lambda: alloc_paged_token_slots_extend,
{
"npu": alloc_paged_token_slots_extend_npu,
},
)
def per_step_draft_out_cache_loc(
out_cache_loc: torch.Tensor,
batch_size: int,
topk: int,
num_steps: int,
) -> torch.Tensor:
"""Per-step slice of the multi-step EAGLE draft out_cache_loc buffer.
Single source of truth for the layout shared by EagleWorkerV2.draft_forward
(per-step write target) and DeepseekV4AttnBackend (per-step compression
write target baked into metadata).
"""
expected = batch_size * topk * num_steps
assert out_cache_loc.shape[0] == expected, (
f"out_cache_loc.shape[0]={out_cache_loc.shape[0]} != "
f"batch_size * topk * num_steps = {batch_size}*{topk}*{num_steps}={expected}"
)
return (
out_cache_loc.view(batch_size, topk, num_steps)
.permute(2, 0, 1)
.reshape(num_steps, -1)
)
def _eagle_prefill_tail_tokens(
batch: ScheduleBatch, next_token_ids: torch.Tensor
) -> torch.Tensor:
"""Per-seq tail token for EAGLE prefill rotation; uses next prompt token for
non-final chunks (chunked-prefill chain consistency, see PR #26329)."""
tail_tokens = next_token_ids.to(batch.input_ids.dtype)
next_prompt_token = batch.chunked_req_next_prompt_token
if next_prompt_token is not None:
for i, r in enumerate(batch.reqs):
if r is batch.chunked_req:
tail_tokens = tail_tokens.clone()
tail_tokens[i] = next_prompt_token
break
return tail_tokens
def organize_draft_results(
score_list: List[torch.Tensor],
token_list: List[torch.Tensor],
parents_list: List[torch.Tensor],
num_draft_token: int,
):
score_list = torch.cat(score_list, dim=1).flatten(1)
ss_token_list = torch.cat(token_list, dim=1)
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, dtype=torch.long, device=parents_list[0].device
)
return parent_list, top_scores_index, draft_tokens
class TreeMaskMode(IntEnum):
FULL_MASK = 0
QLEN_ONLY = 1
QLEN_ONLY_BITPACKING = 2
def default_tree_mask_mode() -> TreeMaskMode:
# The CPU verify attention kernel (intel_amx) consumes the qlen x qlen
# QLEN_ONLY tree mask directly; FULL_MASK is for the GPU kernels.
return TreeMaskMode.QLEN_ONLY if _is_cpu else TreeMaskMode.FULL_MASK
def build_tree_kernel_efficient(
bonus_tokens: torch.Tensor,
parent_list: List[torch.Tensor],
top_scores_index: torch.Tensor,
draft_tokens: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_sum: int,
topk: int,
spec_steps: int,
num_verify_tokens: int,
tree_mask_mode: TreeMaskMode = TreeMaskMode.FULL_MASK,
tree_mask_buf: Optional[torch.Tensor] = None,
position_buf: Optional[torch.Tensor] = None,
):
draft_tokens = torch.cat((bonus_tokens.unsqueeze(1), draft_tokens), dim=1).flatten()
# seq_lens_sum == sum(seq_lens); seq_lens: sequence length without draft tokens
bs = seq_lens.numel()
device = seq_lens.device
# e.g. for bs=1, tree_mask: num_draft_token, seq_lens_sum + num_draft_token (flattened)
# where each row indicates the attending pattern of each draft token
# if use_partial_packed_tree_mask is True, tree_mask: num_draft_token (flattened, packed)
if tree_mask_buf is not None:
tree_mask = tree_mask_buf
if tree_mask_mode == TreeMaskMode.QLEN_ONLY:
tree_mask.fill_(True)
elif tree_mask_mode == TreeMaskMode.QLEN_ONLY_BITPACKING:
tree_mask.fill_(0)
elif tree_mask_mode == TreeMaskMode.FULL_MASK:
tree_mask.fill_(True)
else:
raise NotImplementedError(f"Invalid tree mask: {tree_mask_mode=}")
elif tree_mask_mode == TreeMaskMode.QLEN_ONLY:
tree_mask = torch.full(
(num_verify_tokens * bs * num_verify_tokens,),
True,
dtype=torch.bool,
device=device,
)
elif tree_mask_mode == TreeMaskMode.QLEN_ONLY_BITPACKING:
packed_dtypes = [torch.uint8, torch.uint16, torch.uint32]
packed_dtype_idx = int(math.ceil(math.log2((num_verify_tokens + 7) // 8)))
tree_mask = torch.zeros(
(num_verify_tokens * bs,),
dtype=packed_dtypes[packed_dtype_idx],
device=device,
)
elif tree_mask_mode == TreeMaskMode.FULL_MASK:
tree_mask = torch.full(
(
seq_lens_sum * num_verify_tokens
+ num_verify_tokens * num_verify_tokens * bs,
),
True,
device=device,
)
else:
raise NotImplementedError(f"Invalid tree mask: {tree_mask_mode=}")
# TODO: make them torch.empty and fuse them into `sgl_build_tree_kernel`
retrieve_buf = torch.full(
(3, bs, num_verify_tokens), -1, device=device, dtype=torch.long
)
retrieve_index, retrieve_next_token, retrieve_next_sibling = retrieve_buf
# position: where each token belongs to
# e.g. if depth of each draft token is [0, 1, 1, 2] and the prompt length is 7
# then, positions = [7, 8, 8, 9]
if position_buf is not None:
positions = position_buf
else:
positions = torch.empty(
(bs * num_verify_tokens,), device=device, dtype=torch.long
)
if _is_npu:
torch.ops.npu.build_tree_kernel_efficient(
parent_list.to(dtype=torch.int64),
top_scores_index,
seq_lens,
tree_mask,
positions,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
topk,
spec_steps,
num_verify_tokens,
tree_mask_mode,
)
elif _is_xpu:
sgl_build_tree_kernel_triton(
parent_list,
top_scores_index,
seq_lens,
tree_mask,
positions,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
topk,
spec_steps,
num_verify_tokens,
tree_mask_mode,
)
elif _is_cpu:
sgl_build_tree_kernel_efficient_cpu(
parent_list,
top_scores_index,
seq_lens,
tree_mask,
positions,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
topk,
spec_steps,
num_verify_tokens,
tree_mask_mode,
)
else:
sgl_build_tree_kernel_efficient(
parent_list,
top_scores_index,
seq_lens,
tree_mask,
positions,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
topk,
spec_steps,
num_verify_tokens,
tree_mask_mode,
)
return (
tree_mask,
positions,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
draft_tokens,
)
def sgl_build_tree_kernel_triton(
parent_list: torch.Tensor,
selected_index: torch.Tensor,
verified_seq_len: torch.Tensor,
tree_mask: torch.Tensor,
positions: torch.Tensor,
retrieve_index: torch.Tensor,
retrieve_next_token: torch.Tensor,
retrieve_next_sibling: torch.Tensor,
topk: int,
depth: int,
draft_token_num: int,
tree_mask_mode: TreeMaskMode = TreeMaskMode.FULL_MASK,
):
"""Triton-based implementation."""
# TODO: Add support for QLEN_ONLY_BITPACKING mode
if tree_mask_mode == TreeMaskMode.QLEN_ONLY_BITPACKING:
raise NotImplementedError(
"QLEN_ONLY_BITPACKING is not supported in Triton implementation"
)
batch_size = verified_seq_len.shape[0]
seq_len_prefix_sum = torch.cumsum(verified_seq_len, dim=0) - verified_seq_len
# Launch kernel with one program per batch item
grid = (batch_size,)
sgl_build_tree_kernel_efficient_triton[grid](
parent_list,
selected_index,
verified_seq_len,
seq_len_prefix_sum,
tree_mask,
positions,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
topk=topk,
depth=depth,
draft_token_num=draft_token_num,
tree_mask_mode=int(tree_mask_mode),
batch_size=batch_size,
parent_list_stride=(
parent_list.stride(0) if parent_list.dim() > 1 else parent_list.shape[0]
),
selected_index_stride=selected_index.stride(0),
)
def verify_tree_greedy_triton(
predicts: torch.Tensor,
accept_index: torch.Tensor,
accept_token_num: torch.Tensor,
candidates: torch.Tensor,
retrieve_index: torch.Tensor,
retrieve_next_token: torch.Tensor,
retrieve_next_sibling: torch.Tensor,
target_predict: torch.Tensor,
):
"""Triton-based implementation."""
batch_size = candidates.shape[0]
num_speculative_tokens = accept_index.shape[1]
num_draft_tokens = candidates.shape[1]
# Launch kernel with one program per batch item
grid = (batch_size,)
verify_tree_greedy_kernel_triton[grid](
predicts,
accept_index,
accept_token_num,
candidates,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
target_predict,
batch_size=batch_size,
num_speculative_tokens=num_speculative_tokens,
num_draft_tokens=num_draft_tokens,
)
def verify_tree_greedy_func(
predicts: torch.Tensor,
accept_index: torch.Tensor,
accept_token_num: torch.Tensor,
candidates: torch.Tensor,
retrieve_index: torch.Tensor,
retrieve_next_token: torch.Tensor,
retrieve_next_sibling: torch.Tensor,
target_predict: torch.Tensor,
topk: int = -1,
):
if _is_cuda or _is_hip or _is_musa:
from sgl_kernel import verify_tree_greedy
verify_tree_greedy(
predicts=predicts, # mutable
accept_index=accept_index, # mutable
accept_token_num=accept_token_num, # mutable
candidates=candidates,
# kwarg LHS retained as `retrive_*` to match sgl_kernel op schema.
retrive_index=retrieve_index,
retrive_next_token=retrieve_next_token,
retrive_next_sibling=retrieve_next_sibling,
target_predict=target_predict,
)
elif _is_cpu:
sgl_verify_tree_greedy_cpu(
predicts=predicts, # mutable
accept_index=accept_index, # mutable
accept_token_num=accept_token_num, # mutable
candidates=candidates,
# kwarg LHS retained as `retrive_*` to match the CUDA op schema, so
# the CPU/CUDA call sites stay grep-symmetric.
retrive_index=retrieve_index,
retrive_next_token=retrieve_next_token,
retrive_next_sibling=retrieve_next_sibling,
target_predict=target_predict,
)
elif _is_npu:
from sgl_kernel_npu.sample.verify_tree_greedy import verify_tree_greedy
verify_tree_greedy(
predicts=predicts,
accept_index=accept_index,
accept_token_num=accept_token_num,
candidates=candidates,
# kwarg LHS retained as `retrive_*` to match sgl_kernel op schema.
retrive_index=retrieve_index,
retrive_next_token=retrieve_next_token,
retrive_next_sibling=retrieve_next_sibling,
target_predict=target_predict,
)
elif _is_xpu:
verify_tree_greedy_triton(
predicts=predicts,
accept_index=accept_index,
accept_token_num=accept_token_num,
candidates=candidates,
retrieve_index=retrieve_index,
retrieve_next_token=retrieve_next_token,
retrieve_next_sibling=retrieve_next_sibling,
target_predict=target_predict,
)
return predicts, accept_index, accept_token_num
def get_draft_input_from_target_hidden_dim(model_runner: ModelRunner) -> int:
"""Width of the target hidden states fed into the draft model.
This is the single source of truth and is derived entirely from config: for
EAGLE3 aux mode the draft consumes `num_aux` concatenated target layers
(each `target_hidden_size` wide); every other arch consumes the per-layer
`spec_hidden_size`.
Do NOT read this off a draft projection's `in_features` (e.g. an `fc`
layer): that width is arch-specific.
Note: read entirely from the *draft* `model_runner`'s config. The non-aux
branch assumes the draft's `spec_hidden_size` equals the target hidden width
fed to the draft (true for standard EAGLE, where the draft mirrors the
target hidden size); aux mode reads the explicit `target_hidden_size`.
"""
model_config = model_runner.model_config
hf_config = model_config.hf_config
eagle_config = getattr(hf_config, "eagle_config", None) or {}
get_eagle_config = (
eagle_config.get
if isinstance(eagle_config, dict)
else lambda key, default=None: getattr(eagle_config, key, default)
)
use_aux = get_eagle_config("use_aux_hidden_state", True)
spec_algorithm = model_runner.spec_algorithm
if not (spec_algorithm is not None and spec_algorithm.is_eagle3() and use_aux):
return model_config.spec_hidden_size
target_hidden = getattr(hf_config, "target_hidden_size", None)
if target_hidden is None:
target_hidden = model_config.hidden_size
num_aux = getattr(hf_config, "num_aux_hidden_states", None)
if num_aux is None:
layer_ids = get_eagle_config("eagle_aux_hidden_state_layer_ids", None)
if layer_ids is None:
layer_ids = getattr(hf_config, "eagle_aux_hidden_state_layer_ids", None)
num_aux = len(layer_ids) if layer_ids else 3
return target_hidden * num_aux
def get_draft_recurrent_hidden_state_spec(
model_runner: ModelRunner,
) -> tuple[Optional[int], Optional[torch.dtype]]:
"""Return hidden_states width/dtype carried between draft decode steps."""
if model_runner.spec_algorithm.is_standalone():
return None, None
return model_runner.model_config.spec_hidden_size, model_runner.model_config.dtype
def eagle_prepare_for_verify(
verify_input: EagleVerifyInput,
req_to_token_pool: ReqToTokenPool,
batch: ScheduleBatch,
target_worker: TpModelWorker,
):
from sglang.kernels.ops.speculative.cache_locs import (
assign_extend_cache_locs_func,
)
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.speculative.spec_utils import prepare_mamba_track_for_verify
if not batch.forward_mode.is_idle():
# Assign cache locations
bs = len(batch.req_pool_indices)
batch.input_ids = verify_input.draft_token
maybe_detect_oob(
batch.input_ids,
0,
batch.model_config.vocab_size,
"v2 prepare_for_verify input_ids",
)
device = batch.device
batch.out_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=req_to_token_pool.req_to_token,
start_offset=batch.seq_lens,
end_offset=batch.seq_lens + verify_input.draft_token_num,
batch_size=bs,
draft_token_num=verify_input.draft_token_num,
device=device,
)
batch.out_cache_loc_dsv4 = maybe_build_dsv4_verify_bundle(
batch, verify_input.draft_token_num
)
prepare_mamba_track_for_verify(batch)
# TBO's split_spec_info reads these; no-verify-sync leaves both None.
verify_input.seq_lens_cpu = batch.seq_lens_cpu
verify_input.seq_lens_sum = (
int(batch.seq_lens_cpu.sum()) if batch.seq_lens_cpu is not None else None
)
# Get a forward batch
batch.forward_mode = (
ForwardMode.IDLE if batch.forward_mode.is_idle() else ForwardMode.TARGET_VERIFY
)
capture_mode = (
CaptureHiddenMode.NULL
if target_worker.model_runner.spec_algorithm.is_standalone()
else CaptureHiddenMode.FULL
)
batch.capture_hidden_mode = capture_mode
verify_forward_batch = ForwardBatch.init_new(batch, target_worker.model_runner)
# Run attention backend plan and cuda graph preparation
can_run_cuda_graph = bool(
target_worker.model_runner.decode_cuda_graph_runner
and target_worker.model_runner.decode_cuda_graph_runner.can_run_graph(
verify_forward_batch
)
)
if can_run_cuda_graph:
target_worker.model_runner.decode_cuda_graph_runner.load_batch(
verify_forward_batch
)
verify_forward_batch.mark_forward_metadata_ready()
# Non-cuda-graph: defer init to forward_extend, which runs after
# `_forward_raw -> prepare_mlp_sync_batch` pads the batch. Initing
# here would use pre-pad shapes and trip DSv4 indexer shape match.
return verify_forward_batch, can_run_cuda_graph
def eagle_sample(
verify_input: EagleVerifyInput,
batch: ScheduleBatch,
logits_output: LogitsProcessorOutput,
vocab_mask: torch.Tensor = None,
):
"""
Verify and find accepted tokens based on logits output and batch
(which contains spec decoding information).
"""
import torch.nn.functional as F
from sglang.srt.distributed import get_tp_group
from sglang.srt.layers.dp_attention import (
is_dp_attention_enabled,
)
from sglang.srt.runtime_context import get_server_args
from sglang.srt.sampling.penaltylib.repetition_penalty import (
apply_scaling_penalties,
)
from sglang.srt.speculative.spec_utils import (
SIMULATE_ACC_LEN,
SIMULATE_ACC_TOKEN_MODE,
generate_simulated_accept_index,
)
from sglang.srt.utils.async_probe import maybe_detect_nan, sanitize_nan_logits
device = batch.device
if batch.forward_mode.is_idle():
predict = torch.empty(0, dtype=torch.int32, device=device)
num_correct_drafts = torch.empty(0, dtype=torch.int32, device=device)
accept_index = torch.empty(0, dtype=torch.int32, device=device)
return predict, num_correct_drafts, accept_index
bs = len(batch.seq_lens)
sampling_info = batch.sampling_info
next_token_logits = logits_output.next_token_logits
sanitize_nan_logits(next_token_logits, "verify: target model logits")
# Apply penalty
# This is a relaxed version of penalties for speculative decoding.
if sampling_info.acc_additive_penalties is not None:
next_token_logits.add_(
torch.repeat_interleave(
sampling_info.acc_additive_penalties,
verify_input.draft_token_num,
dim=0,
)
)
if sampling_info.acc_scaling_penalties is not None:
apply_scaling_penalties(
next_token_logits,
torch.repeat_interleave(
sampling_info.acc_scaling_penalties, verify_input.draft_token_num, dim=0
),
)
if sampling_info.logit_bias is not None:
next_token_logits.add_(
torch.repeat_interleave(
sampling_info.logit_bias, verify_input.draft_token_num, dim=0
)
)
# Apply grammar mask if provided
if vocab_mask is not None:
assert verify_input.grammar is not None
verify_input.grammar.apply_vocab_mask(
logits=next_token_logits, vocab_mask=vocab_mask
)
candidates = verify_input.draft_token.reshape(bs, verify_input.draft_token_num)
predict_shape = list(next_token_logits.shape)[:-1]
predict = torch.zeros(predict_shape, dtype=torch.int32, device=device).flatten()
accept_index = torch.full(
(bs, verify_input.max_tree_depth), -1, dtype=torch.int32, device=device
)
num_correct_drafts = torch.empty((bs,), dtype=torch.int32, device=device)
# Sample tokens
target_predict = None
if sampling_info.is_all_greedy or _is_cpu or _is_npu or _is_hip or _is_xpu:
target_predict = torch.argmax(next_token_logits, dim=-1)
target_predict = target_predict.reshape(bs, verify_input.draft_token_num)
predict, accept_index, num_correct_drafts = verify_tree_greedy_func(
predicts=predict, # mutable
accept_index=accept_index, # mutable
accept_token_num=num_correct_drafts, # mutable
candidates=candidates,
retrieve_index=verify_input.retrieve_index,
retrieve_next_token=verify_input.retrieve_next_token,
retrieve_next_sibling=verify_input.retrieve_next_sibling,
target_predict=target_predict,
topk=verify_input.tree_topk,
)
else:
from sgl_kernel import (
top_k_renorm_prob,
top_p_renorm_prob,
tree_speculative_sampling_target_only,
)
from sglang.srt.speculative.reject_sampling import (
chain_speculative_sampling_triton,
)
use_rejection_sampling = get_server_args().speculative_use_rejection_sampling
# Apply temperature and get target probs
expanded_temperature = torch.repeat_interleave(
sampling_info.temperatures, verify_input.draft_token_num, dim=0
) # (bs * num_draft_tokens, 1)
target_probs = F.softmax(
next_token_logits / expanded_temperature, dim=-1
) # (bs * num_draft_tokens, vocab_size)
maybe_detect_nan(target_probs, "v2 verify: target_probs after softmax")
target_probs = top_k_renorm_prob(
target_probs,
torch.repeat_interleave(
sampling_info.top_ks, verify_input.draft_token_num, dim=0
),
) # (bs * num_draft_tokens, vocab_size)
maybe_detect_nan(target_probs, "v2 verify: target_probs after top_k_renorm")
target_probs = top_p_renorm_prob(
target_probs,
torch.repeat_interleave(
sampling_info.top_ps, verify_input.draft_token_num, dim=0
),
)
maybe_detect_nan(target_probs, "v2 verify: target_probs after top_p_renorm")
target_probs = target_probs.reshape(bs, verify_input.draft_token_num, -1)
draft_probs = (
verify_input.draft_probs
if use_rejection_sampling
else torch.zeros_like(target_probs)
)
# Defense-in-depth behind the spec_hook startup allowlist: validate the
# actual kernel inputs (catches draft_probs plumbing regressions or a
# startup guard bypassed by a worker subclass) before the Triton kernel.
if use_rejection_sampling and (
draft_probs is None or draft_probs.shape[-1] != target_probs.shape[-1]
):
raise ValueError(
"Rejection sampling requires a target-vocab draft proposal "
"distribution; the current speculative algorithm/draft worker "
"does not produce one (draft_probs missing or vocab-mismatched)."
)
# coins for rejection sampling
coins = torch.rand_like(candidates, dtype=torch.float32, device=device)
# coins for final sampling
coins_for_final_sampling = torch.rand((bs,), dtype=torch.float32, device=device)
sampling_fn = (
chain_speculative_sampling_triton
if use_rejection_sampling
else tree_speculative_sampling_target_only
)
sampling_fn(
predicts=predict, # mutable
accept_index=accept_index, # mutable
accept_token_num=num_correct_drafts, # mutable
candidates=candidates,
# kwarg LHS retained as `retrive_*` to match sgl_kernel op schema.
retrive_index=verify_input.retrieve_index,
retrive_next_token=verify_input.retrieve_next_token,
retrive_next_sibling=verify_input.retrieve_next_sibling,
uniform_samples=coins,
uniform_samples_for_final_sampling=coins_for_final_sampling,
target_probs=target_probs,
draft_probs=draft_probs,
threshold_single=get_server_args().speculative_accept_threshold_single,
threshold_acc=get_server_args().speculative_accept_threshold_acc,
deterministic=True,
)
# Sync sampling results across TP ranks: different GPUs may
# produce slightly different target_probs due to floating-point
# non-determinism in softmax/top_k/top_p, causing different
# sampled tokens. Broadcast from rank 0 to ensure consistency.
tp_group = (
get_parallel().attn_tp_group
if is_dp_attention_enabled()
else get_tp_group()
)
if tp_group.world_size > 1:
tp_group.broadcast(predict, src=0)
tp_group.broadcast(accept_index, src=0)
tp_group.broadcast(num_correct_drafts, src=0)
if SIMULATE_ACC_LEN > 0:
# Do simulation. The helper builds (and returns) a replacement
# accept_index of width spec_steps + 1, so pass max_tree_depth - 1
# to keep the simulated width identical to the real one.
if SIMULATE_ACC_TOKEN_MODE not in ("fixed", "real-draft-token"):
raise ValueError(
"Invalid SGLANG_SIMULATE_ACC_TOKEN_MODE "
f"{SIMULATE_ACC_TOKEN_MODE!r}; expected 'fixed' or "
"'real-draft-token'."
)
if SIMULATE_ACC_TOKEN_MODE == "real-draft-token":
if verify_input.tree_topk != 1:
raise ValueError(
"SGLANG_SIMULATE_ACC_LEN with real draft tokens currently "
"requires speculative_eagle_topk=1."
)
# Use target argmax as the synthetic bonus for non-greedy requests.
if target_predict is None:
target_predict = torch.argmax(next_token_logits, dim=-1).reshape(
bs, verify_input.draft_token_num
)
accept_index = generate_simulated_accept_index(
accept_index=accept_index,
predict=predict, # mutable
num_correct_drafts=num_correct_drafts, # mutable
candidates=candidates,
target_predict=target_predict,
simulate_acc_len=SIMULATE_ACC_LEN,
simulate_acc_token_mode=SIMULATE_ACC_TOKEN_MODE,
bs=bs,
spec_steps=verify_input.max_tree_depth - 1,
)
# `num_correct_drafts` stays drafts-only inside this function; the returned
# tensor includes the trailing/bonus token via out-of-place +1 so the
# name no longer flips semantics mid-function (naming doc C2).
return predict, num_correct_drafts + 1, accept_index
def eagle_prepare_for_decode(batch: ScheduleBatch):
batch.maybe_evict_swa()
from sglang.srt.speculative.spec_utils import assign_req_to_token_pool_func
bs = batch.batch_size()
# Accumulate penalty
# This is a relaxed version of penalties for speculative decoding.
if batch.sampling_info.penalizer_orchestrator.is_required:
batch.cumulate_penalty_output_tokens()
page_size = batch.token_to_kv_pool_allocator.page_size
double_alloc = get_alloc_reserve_per_decode()
cur_kv_lens = [0] * bs
nxt_kv_lens = [0] * bs
num_needed_tokens = 0
for i, r in enumerate(batch.reqs):
cur = r.kv_allocated_len
# max(cur, ...) clamps so adaptive downswitch cannot make nxt < cur.
# kv_committed_len is honest (bonus committed in resolve, not here),
# so it lags batch.seq_lens by ~1 verify in overlap; 2*alloc absorbs.
nxt = max(cur, r.kv_committed_len + double_alloc)
cur_kv_lens[i] = cur
nxt_kv_lens[i] = nxt
num_needed_tokens += nxt - cur
r.kv_allocated_len = nxt
r.decode_batch_idx += 1
cur_kv_lens_cpu = torch.tensor(cur_kv_lens, dtype=torch.int32, device="cpu")
nxt_kv_lens_cpu = torch.tensor(nxt_kv_lens, dtype=torch.int32, device="cpu")
# Fail fast if the page>1 + topk>1 draft over-allocation
# (get_alloc_reserve_per_decode) outgrows the req_to_token row: the write below
# would OOB and free would leak KV. The row is widened to hold it in _init_pools
# (PR #26972); fail here with a clear error, not on a later cryptic CUDA assert.
from sglang.srt.runtime_context import get_server_args
if page_size > 1 and (get_server_args().speculative_eagle_topk or 1) > 1:
max_alloc_len = int(nxt_kv_lens_cpu.max())
row_width = batch.req_to_token_pool.req_to_token.shape[1]
assert max_alloc_len <= row_width, (
f"spec v2 page>1 topk>1 draft over-allocation ({max_alloc_len}) exceeds "
f"req_to_token row width ({row_width}); page_size={page_size}. Widen the "
f"row to hold committed + get_alloc_reserve_per_decode (PR #26972)."
)
# non_blocking H2D: a blocking .to() syncs the schedule stream, which the WAR
# barrier has chained to the prev forward -> host stalls a full forward.
cur_kv_lens_device = cur_kv_lens_cpu.to(device=batch.device, non_blocking=True)
nxt_kv_lens_device = nxt_kv_lens_cpu.to(device=batch.device, non_blocking=True)
if page_size == 1:
out_cache_loc = alloc_token_slots(batch.tree_cache, num_needed_tokens)
else:
last_loc = get_last_loc(
batch.req_to_token_pool.req_to_token,
batch.req_pool_indices,
cur_kv_lens_device,
)
device_type = getattr(batch.device, "type", str(batch.device).split(":", 1)[0])
out_cache_loc = ALLOC_EXTEND_FUNCS[device_type](
batch.tree_cache,
cur_kv_lens_device,
cur_kv_lens_cpu,
nxt_kv_lens_device,
nxt_kv_lens_cpu,
last_loc,
num_needed_tokens,
req_pool_indices=batch.req_pool_indices,
batch=batch,
)
assign_req_to_token_pool_func(
batch.req_pool_indices,
batch.req_to_token_pool.req_to_token,
cur_kv_lens_device,
nxt_kv_lens_device,
out_cache_loc,
bs,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,110 @@
"""Manages external SAM corpora for ngram speculative decoding.
Handles add/remove/list operations and async background loading.
Used by the Scheduler — not a mixin, a standalone manager object.
"""
import logging
import threading
from typing import Callable, Optional, Tuple
from sglang.srt.managers.io_struct import (
AddExternalCorpusReqInput,
AddExternalCorpusReqOutput,
ListExternalCorporaReqInput,
ListExternalCorporaReqOutput,
RemoveExternalCorpusReqInput,
RemoveExternalCorpusReqOutput,
)
logger = logging.getLogger(__name__)
class ExternalCorpusManager:
"""Manages external SAM corpus lifecycle for a single scheduler.
Args:
draft_worker: the NGRAMWorker instance (must have add_external_corpus,
remove_external_corpus, list_external_corpora methods).
send_response: callable(output, recv_req) to send deferred responses
back to the tokenizer manager.
"""
def __init__(self, draft_worker, send_response: Callable):
self._worker = draft_worker
self._send_response = send_response
self._pending_load: Optional[
Tuple[AddExternalCorpusReqInput, threading.Thread]
] = None
self._load_result: Optional[AddExternalCorpusReqOutput] = None
def check_pending_load(self):
"""Poll from the scheduler event loop. Sends response when done."""
if self._pending_load is None:
return
recv_req, thread = self._pending_load
if thread.is_alive():
return
self._pending_load = None
thread.join() # formal happens-before for _load_result visibility
result = self._load_result
self._load_result = None
if result.success:
self._worker.commit_corpus_load(result.corpus_id, result.loaded_token_count)
self._send_response(result, recv_req)
def add(
self, recv_req: AddExternalCorpusReqInput
) -> Optional[AddExternalCorpusReqOutput]:
if self._pending_load is not None:
return AddExternalCorpusReqOutput(
success=False,
message="Another corpus load is already in progress.",
)
def _build():
try:
loaded = self._worker.add_external_corpus(
recv_req.corpus_id, recv_req.token_chunks
)
self._load_result = AddExternalCorpusReqOutput(
success=True,
corpus_id=recv_req.corpus_id,
message=f"Loaded corpus '{recv_req.corpus_id}' with {loaded} tokens.",
loaded_token_count=loaded,
)
except Exception as e:
self._load_result = AddExternalCorpusReqOutput(
success=False, message=str(e)
)
thread = threading.Thread(target=_build, daemon=True)
self._pending_load = (recv_req, thread)
thread.start()
return None # response sent later by check_pending_load
# FIXME(kpham-sgl): remove a corpus during a pending load is an undefined behaviour
# and should be explicitly prevented.
def remove(
self, recv_req: RemoveExternalCorpusReqInput
) -> RemoveExternalCorpusReqOutput:
try:
self._worker.remove_external_corpus(recv_req.corpus_id)
return RemoveExternalCorpusReqOutput(
success=True,
message=f"Removed corpus '{recv_req.corpus_id}'.",
)
except Exception as e:
return RemoveExternalCorpusReqOutput(success=False, message=str(e))
def list(
self, recv_req: ListExternalCorporaReqInput
) -> ListExternalCorporaReqOutput:
try:
token_counts = self._worker.list_external_corpora()
return ListExternalCorporaReqOutput(
success=True,
corpus_token_counts=token_counts,
)
except Exception as e:
return ListExternalCorporaReqOutput(success=False, message=str(e))
@@ -0,0 +1,449 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Optional
import torch
from sglang.srt.compilation.torch_compile_decoration import set_torch_compile_config
from sglang.srt.layers.dp_attention import (
DpPaddingMode,
set_dp_buffer_len,
set_is_extend_in_batch,
)
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
from sglang.srt.model_executor.input_buffers import ForwardInputBuffers
from sglang.srt.model_executor.runner import (
DecodeCudaGraphRunner,
DeepEPCudaGraphRunnerAdapter,
ShapeKey,
get_batch_sizes_to_capture,
model_capture_mode,
)
from sglang.srt.model_executor.runner.flashinfer_autotune import (
maybe_flashinfer_autotune_speculative_draft,
)
from sglang.srt.model_executor.runner_backend.utils import resolve_decode_backend
from sglang.srt.model_executor.runner_backend_utils import (
CUDA_GRAPH_CAPTURE_FAILED_MSG,
)
from sglang.srt.runtime_context import get_flags
from sglang.srt.speculative.frozen_kv_mtp_info import FrozenKVMTPDraftInput
from sglang.srt.utils import (
require_attn_tp_gather,
require_gathered_buffer,
require_mlp_sync,
require_mlp_tp_gather,
)
if TYPE_CHECKING:
from sglang.srt.speculative.frozen_kv_mtp_worker_v2 import FrozenKVMTPDraftWorker
@dataclass
class FrozenKVMTPInputBuffers(ForwardInputBuffers):
req_pool_indices: torch.Tensor
positions: torch.Tensor
mrope_positions: torch.Tensor
seq_lens: torch.Tensor
seq_lens_cpu: torch.Tensor
topk_p: torch.Tensor
topk_index: torch.Tensor
hidden_states: torch.Tensor
# Consumed by the captured seed iter; see `FrozenKVMTPDraftWorker.draft_forward`.
bonus_tokens: torch.Tensor
global_num_tokens_gpu: Optional[torch.Tensor]
global_num_tokens_for_logprob_gpu: Optional[torch.Tensor]
class FrozenKVMTPCudaGraphRunner(DecodeCudaGraphRunner):
"""CUDA graph runner for the Frozen-KV MTP recurrent draft-loop step.
Subclasses DecodeCudaGraphRunner to inherit the outer capture loop
(capture() / _capture_one_stream()), the bucket-padding helper
(_pad_to_bucket), and the backend-driven capture/replay scaffolding.
Frozen-KV-MTP-specific bits — the buffer dataclass, the dummy
ForwardBatch + FrozenKVMTPDraftInput built in capture_one_shape, the
target-KV-pool swap during capture, the worker's frozen-KV metadata
helpers, the topk*topk bucket math, the expanded-bs bookkeeping, and
the 3-tuple replay output — are overridden.
Like the EAGLE draft runner, it does NOT call
DecodeCudaGraphRunner.__init__ (that init sets up decode-only state);
it sets up its own fields directly while satisfying the parent's
capture() / backend contract.
"""
def __init__(self, frozen_kv_mtp_worker: FrozenKVMTPDraftWorker):
self.frozen_kv_mtp_worker = frozen_kv_mtp_worker
self.model_runner = model_runner = frozen_kv_mtp_worker.draft_model_runner
self.device = model_runner.device
self.device_module = torch.get_device_module(self.device)
self.enable_torch_compile = get_flags().capture.enable_torch_compile
self.disable_padding = model_runner.server_args.disable_cuda_graph_padding
self.require_gathered_buffer = require_gathered_buffer(model_runner.server_args)
self.require_mlp_tp_gather = require_mlp_tp_gather(model_runner.server_args)
self.require_mlp_sync = require_mlp_sync(model_runner.server_args)
self.require_attn_tp_gather = require_attn_tp_gather(model_runner.server_args)
self.tp_size = self.model_runner.tp_size
self.dp_size = self.model_runner.dp_size
self.pp_size = model_runner.server_args.pp_size
self.speculative_num_steps = model_runner.server_args.speculative_num_steps
self.topk = model_runner.server_args.speculative_eagle_topk
self.draft_attn_backend = frozen_kv_mtp_worker.draft_attn_backend
self.enable_profile_cuda_graph = (
model_runner.server_args.enable_profile_cuda_graph
)
self.attn_backend = self.draft_attn_backend
self.compile_bs = []
self.enable_pdmux = False
self.record_nolora_graph = False
self.is_dllm = False
self.deepep_adapter = DeepEPCudaGraphRunnerAdapter()
self.capture_forward_mode = ForwardMode.DECODE
self.capture_hidden_mode = CaptureHiddenMode.LAST
self.num_tokens_per_bs = self.topk
self.capture_bs, _ = get_batch_sizes_to_capture(
model_runner, self.num_tokens_per_bs
)
self.max_bs = max(self.capture_bs)
self.max_num_token = self.max_bs * self.num_tokens_per_bs
self.draft_attn_backend.init_cuda_graph_state(self.max_bs, self.max_num_token)
self.seq_len_fill_value = (
self.draft_attn_backend.get_cuda_graph_seq_len_fill_value()
)
seq_lens_cpu = torch.full(
(self.max_num_token,), self.seq_len_fill_value, dtype=torch.int64
)
if self.enable_torch_compile:
set_torch_compile_config()
with torch.device(model_runner.device):
req_pool_indices = torch.zeros((self.max_num_token,), dtype=torch.int64)
positions = torch.zeros((self.max_num_token,), dtype=torch.int64)
mrope_positions = torch.zeros((3, self.max_num_token), dtype=torch.int64)
seq_lens = torch.full(
(self.max_num_token,), self.seq_len_fill_value, dtype=torch.int64
)
topk_p = torch.zeros((self.max_bs, self.topk), dtype=torch.float32)
topk_index = torch.zeros((self.max_bs, self.topk), dtype=torch.int64)
hidden_states = torch.zeros(
(self.max_bs, frozen_kv_mtp_worker._recurrent_hidden_size),
dtype=self.model_runner.dtype,
)
bonus_tokens = torch.zeros((self.max_bs,), dtype=torch.int64)
if self.require_gathered_buffer:
if self.require_mlp_tp_gather:
global_num_tokens_gpu = torch.zeros(
(self.dp_size,), dtype=torch.int32
)
global_num_tokens_for_logprob_gpu = torch.zeros(
(self.dp_size,), dtype=torch.int32
)
else:
assert self.require_attn_tp_gather
global_num_tokens_gpu = torch.zeros((1,), dtype=torch.int32)
global_num_tokens_for_logprob_gpu = torch.zeros(
(1,), dtype=torch.int32
)
else:
global_num_tokens_gpu = None
global_num_tokens_for_logprob_gpu = None
self.buffers = FrozenKVMTPInputBuffers(
req_pool_indices=req_pool_indices,
positions=positions,
mrope_positions=mrope_positions,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
topk_p=topk_p,
topk_index=topk_index,
hidden_states=hidden_states,
bonus_tokens=bonus_tokens,
global_num_tokens_gpu=global_num_tokens_gpu,
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
)
self.buffers.share_buffers()
self.backend = resolve_decode_backend(self)
try:
with model_capture_mode():
self.capture()
except RuntimeError as e:
raise Exception(
f"Capture frozen-KV MTP cuda graph failed: {e}\n"
f"{CUDA_GRAPH_CAPTURE_FAILED_MSG}"
)
def _make_graph_key(self, bs, stream_idx=None, variant_label=None):
return ShapeKey(size=bs)
def _replay_graph(self, shape_key, forward_batch):
return self.backend.replay(shape_key, forward_batch)
def can_run_graph(self, forward_batch: ForwardBatch):
if self.require_mlp_tp_gather:
cuda_graph_bs = max(forward_batch.global_num_tokens_cpu) // (
self.topk * self.topk
)
else:
cuda_graph_bs = (
forward_batch.batch_size // self.topk
if self.topk > 1
else forward_batch.batch_size
)
is_bs_supported = (
self.backend.can_run(forward_batch, self._make_graph_key(cuda_graph_bs))
if self.disable_padding
else cuda_graph_bs <= self.max_bs
)
if self.require_mlp_sync:
is_bs_supported = is_bs_supported and forward_batch.can_run_dp_cuda_graph
return is_bs_supported
def capture_one_shape(
self,
size: int,
forward: Callable,
stream_idx: Optional[int] = None,
variant_label: Optional[str] = None,
):
del forward, stream_idx, variant_label
buffers = self.buffers
request_bs = size
expanded_bs = request_bs * self.num_tokens_per_bs
req_pool_indices = buffers.req_pool_indices[:expanded_bs]
positions = buffers.positions[:expanded_bs]
mrope_positions = buffers.mrope_positions[:, :expanded_bs]
seq_lens = buffers.seq_lens[:expanded_bs]
seq_lens_cpu = buffers.seq_lens_cpu[:expanded_bs]
topk_p = buffers.topk_p[:request_bs]
topk_index = buffers.topk_index[:request_bs]
hidden_states = buffers.hidden_states[:request_bs]
bonus_tokens = buffers.bonus_tokens[:request_bs]
if self.require_mlp_tp_gather:
global_num_tokens_cpu = [expanded_bs] * self.dp_size
elif self.require_attn_tp_gather:
global_num_tokens_cpu = [expanded_bs]
else:
global_num_tokens_cpu = None
if global_num_tokens_cpu is not None:
global_dp_buffer_len = sum(global_num_tokens_cpu)
num_tokens_tensor = torch.tensor(
global_num_tokens_cpu,
dtype=torch.int32,
device=buffers.positions.device,
)
buffers.global_num_tokens_gpu.copy_(num_tokens_tensor)
buffers.global_num_tokens_for_logprob_gpu.copy_(num_tokens_tensor)
global_num_tokens = buffers.global_num_tokens_gpu
global_num_tokens_for_logprob = buffers.global_num_tokens_for_logprob_gpu
else:
global_dp_buffer_len = None
global_num_tokens = None
global_num_tokens_for_logprob = None
spec_info = FrozenKVMTPDraftInput(
topk_p=topk_p,
topk_index=topk_index,
hidden_states=hidden_states,
bonus_tokens=bonus_tokens,
capture_hidden_mode=CaptureHiddenMode.LAST,
)
spec_info.num_tokens_per_req = self.topk
spec_info.num_tokens_for_logprob_per_req = self.topk
spec_info.positions = positions
forward_batch = ForwardBatch(
forward_mode=ForwardMode.DECODE,
batch_size=expanded_bs,
input_ids=None,
req_pool_indices=req_pool_indices,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
out_cache_loc=None,
seq_lens_sum=seq_lens.sum().item(),
return_logprob=False,
positions=positions,
mrope_positions=mrope_positions,
global_num_tokens_gpu=global_num_tokens,
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob,
dp_padding_mode=DpPaddingMode.get_default_mode_in_cuda_graph(),
global_dp_buffer_len=global_dp_buffer_len,
spec_algorithm=self.model_runner.spec_algorithm,
spec_info=spec_info,
capture_hidden_mode=CaptureHiddenMode.LAST,
)
def run_once():
# Record the metadata rebuild against the committed target-prefix
# geometry (spec_info nulled → plain target-length decode), matching
# every other frozen-KV metadata init. Without the view, backends
# that key seqlen offsets off spec_info (trtllm_mha's draft-decode
# branch adds speculative_step_id + 1) bake a +1 offset into the
# captured graph and replay reads one extra, never-written KV slot.
with self.frozen_kv_mtp_worker._frozen_kv_target_view(forward_batch):
self.draft_attn_backend.init_forward_metadata_in_graph(forward_batch)
forward_batch.dp_local_start_pos = forward_batch.dp_local_num_tokens = None
set_dp_buffer_len(
global_dp_buffer_len,
expanded_bs,
forward_batch.dp_padding_mode.is_max_len(),
global_num_tokens_cpu,
)
set_is_extend_in_batch(False)
hidden_states_backup = forward_batch.spec_info.hidden_states
# The capture batch is marked by the capture metadata helper
# below, so draft_forward skips its eager plan.
ret = self.frozen_kv_mtp_worker.draft_forward(forward_batch)
forward_batch.spec_info.hidden_states = hidden_states_backup
return ret
# Swap the draft backend's token_to_kv_pool to the frozen target pool
# for the capture; the single backend-attr swap is seen by both
# get_token_to_kv_pool() (via get_attn_backend()) and the
# backend's own reads.
target_pool = self.frozen_kv_mtp_worker.kv_context.target_token_to_kv_pool
saved_backend_pool = self.draft_attn_backend.token_to_kv_pool
self.draft_attn_backend.token_to_kv_pool = target_pool
try:
with forward_context(ForwardContext(attn_backend=self.draft_attn_backend)):
self.frozen_kv_mtp_worker._init_frozen_kv_metadata_capture_cuda_graph(
forward_batch
)
self.deepep_adapter.capture(is_extend_in_batch=False)
shape_key = self._make_graph_key(request_bs)
post_warmup_hook = getattr(
self.draft_attn_backend, "on_after_cuda_graph_warmup", None
)
maybe_flashinfer_autotune_speculative_draft(
self,
run_once,
post_warmup_hook=post_warmup_hook,
skip_logits=False,
)
self.backend.capture_one(
shape_key,
run_once,
dummies=None,
post_warmup_hook=post_warmup_hook,
)
finally:
self.draft_attn_backend.token_to_kv_pool = saved_backend_pool
def _postprocess_output_to_raw_bs(self, out, raw_bs):
parent_list, top_scores_index, draft_tokens = (t[:raw_bs] for t in out)
return parent_list, top_scores_index, draft_tokens
def execute(self, forward_batch: ForwardBatch):
self.deepep_adapter.replay()
buffers = self.buffers
raw_expanded_bs = forward_batch.batch_size
raw_bs = (
raw_expanded_bs // self.num_tokens_per_bs
if self.topk > 1
else raw_expanded_bs
)
raw_num_token = raw_expanded_bs
if self.require_mlp_tp_gather:
max_num_tokens = max(forward_batch.global_num_tokens_cpu)
max_batch_size = max_num_tokens // (
self.num_tokens_per_bs * self.num_tokens_per_bs
)
bs = self._pad_to_bucket(int(max_batch_size), self.capture_bs)
else:
bs = self._pad_to_bucket(raw_bs, self.capture_bs)
expanded_bs = bs * self.num_tokens_per_bs
if bs != raw_bs:
buffers.seq_lens.fill_(self.seq_len_fill_value)
buffers.positions.zero_()
# Pair with seq_lens fill: padded rows must point at reserved
# req_pool slot 0 (req_to_token[0, :] is all zeros from init).
buffers.req_pool_indices.zero_()
num_tokens = expanded_bs
buffers.seq_lens[:raw_expanded_bs].copy_(forward_batch.seq_lens)
buffers.positions[:raw_num_token].copy_(forward_batch.positions)
if forward_batch.mrope_positions is not None:
buffers.mrope_positions[:, :raw_num_token].copy_(
forward_batch.mrope_positions
)
# `topk_p`/`topk_index` are produced by the captured seed iter.
buffers.bonus_tokens[:raw_bs].copy_(forward_batch.spec_info.bonus_tokens)
buffers.hidden_states[:raw_bs].copy_(forward_batch.spec_info.hidden_states)
buffers.req_pool_indices[:raw_expanded_bs].copy_(forward_batch.req_pool_indices)
if self.require_gathered_buffer:
buffers.global_num_tokens_gpu.fill_(expanded_bs)
buffers.global_num_tokens_for_logprob_gpu.fill_(expanded_bs)
if bs != raw_bs:
forward_batch.batch_size = expanded_bs
forward_batch.seq_lens = buffers.seq_lens[:expanded_bs]
forward_batch.req_pool_indices = buffers.req_pool_indices[:expanded_bs]
forward_batch.positions = buffers.positions[:num_tokens]
if forward_batch.mrope_positions is not None:
forward_batch.mrope_positions = buffers.mrope_positions[:, :num_tokens]
if forward_batch.seq_lens_cpu is not None:
if bs != raw_bs:
buffers.seq_lens_cpu.fill_(self.seq_len_fill_value)
buffers.seq_lens_cpu[:raw_expanded_bs].copy_(forward_batch.seq_lens_cpu)
forward_batch.seq_lens_cpu = buffers.seq_lens_cpu[:expanded_bs]
self.frozen_kv_mtp_worker._init_frozen_kv_metadata_replay_cuda_graph(
forward_batch,
expanded_bs,
forward_batch.seq_lens_sum
+ (expanded_bs - raw_expanded_bs) * self.seq_len_fill_value,
)
self.raw_bs = raw_bs
self.bs = bs
shape_key = self._make_graph_key(bs)
# NVTX span: the graph bypasses `model_runner.forward`'s record_function.
span_name = f"step[DRAFT_LOOP raw_bs={raw_bs} bs={bs} topk={self.topk}]"
if torch.autograd._profiler_enabled():
with torch.profiler.record_function(span_name):
out = self._replay_graph(shape_key, forward_batch)
else:
out = self._replay_graph(shape_key, forward_batch)
if bs != raw_bs:
out = self._postprocess_output_to_raw_bs(out, raw_bs)
forward_batch.batch_size = raw_expanded_bs
forward_batch.positions = buffers.positions[:raw_num_token]
forward_batch.seq_lens = buffers.seq_lens[:raw_expanded_bs]
forward_batch.req_pool_indices = buffers.req_pool_indices[:raw_expanded_bs]
if forward_batch.mrope_positions is not None:
forward_batch.mrope_positions = buffers.mrope_positions[
:, :raw_num_token
]
if forward_batch.seq_lens_cpu is not None:
forward_batch.seq_lens_cpu = buffers.seq_lens_cpu[:raw_expanded_bs]
return out
@@ -0,0 +1,60 @@
# Copyright 2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict
from sglang.srt.mem_cache.memory_pool import KVCache
from sglang.srt.speculative.eagle_info import (
EagleDraftInput,
EagleVerifyInput,
)
from sglang.srt.speculative.spec_info import SpecInput, SpecInputType
@dataclass(frozen=True)
class FrozenKVMTPContext:
"""Target KV pool + assistant-logical -> target-physical layer map."""
target_token_to_kv_pool: KVCache
physical_layer_ids: Dict[int, int]
def get_physical_layer_id(self, idx: int) -> int:
if idx not in self.physical_layer_ids:
raise KeyError(
f"FrozenKVMTPContext has no physical layer id for assistant "
f"logical index {idx}; available: {sorted(self.physical_layer_ids)}"
)
return self.physical_layer_ids[idx]
@dataclass
class FrozenKVMTPDraftInput(EagleDraftInput):
"""Draft input for Frozen-KV MTP.
Frozen-KV MTP currently reuses the EAGLE scheduler/attention contract, but
has a dedicated type so algorithm-specific behavior can move here over time.
"""
def __post_init__(self):
SpecInput.__init__(self, SpecInputType.FROZEN_KV_MTP_DRAFT)
@dataclass
class FrozenKVMTPVerifyInput(EagleVerifyInput):
"""Verify input for Frozen-KV MTP."""
def __post_init__(self):
SpecInput.__init__(self, SpecInputType.FROZEN_KV_MTP_VERIFY)
@@ -0,0 +1,155 @@
# Copyright 2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING
import torch
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.speculative.frozen_kv_mtp_info import FrozenKVMTPContext
if TYPE_CHECKING:
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
@contextmanager
def frozen_kv_target_view(
forward_batch: ForwardBatch,
kv_context: FrozenKVMTPContext,
draft_attn_backend: AttentionBackend,
):
"""Build attention metadata against committed target-prefix geometry.
Swaps ``draft_attn_backend.token_to_kv_pool`` to the frozen target pool
so any helper that reads ``get_token_to_kv_pool()`` during metadata init
sees the frozen target pool. Pool refs are derived from
``get_attn_backend().token_to_kv_pool`` — the single backend-attribute
swap is seen by both readers (``get_token_to_kv_pool()`` and the
backend's own ``self.token_to_kv_pool``).
"""
if kv_context is None:
raise RuntimeError(
"Frozen-KV MTP target view called before the model was bound; "
"bind the frozen KV context first."
)
saved_spec_info = forward_batch.spec_info
forward_batch.spec_info = None
saved_backend_pool = draft_attn_backend.token_to_kv_pool
draft_attn_backend.token_to_kv_pool = kv_context.target_token_to_kv_pool
try:
yield
finally:
forward_batch.spec_info = saved_spec_info
draft_attn_backend.token_to_kv_pool = saved_backend_pool
@contextmanager
def target_kv_pool_view(
forward_batch: ForwardBatch,
kv_context: FrozenKVMTPContext,
draft_attn_backend: AttentionBackend,
):
"""Run the draft model's forward with the target's frozen KV pool.
Swaps ``draft_attn_backend.token_to_kv_pool`` to the frozen target pool.
The single backend-attribute swap is seen by both readers —
``get_token_to_kv_pool()`` (because it resolves through
``get_attn_backend()``) and the backend's own ``self.token_to_kv_pool``
reads (because ``self is draft_attn_backend``).
"""
if kv_context is None:
raise RuntimeError(
"Frozen-KV MTP target KV pool view called before the model was bound; "
"bind the frozen KV context first."
)
saved_backend_pool = draft_attn_backend.token_to_kv_pool
draft_attn_backend.token_to_kv_pool = kv_context.target_token_to_kv_pool
try:
yield
finally:
draft_attn_backend.token_to_kv_pool = saved_backend_pool
def set_frozen_kv_positions(forward_batch: ForwardBatch, topk: int) -> None:
"""Rope phase = last written target slot, not advanced per draft step."""
seq_lens = forward_batch.seq_lens
positions = torch.clamp(seq_lens - 1, min=0).to(torch.int64)
if (
topk > 1
and forward_batch.positions is not None
and forward_batch.positions.numel() == positions.numel() * topk
):
positions = positions.repeat_interleave(topk, dim=0)
if forward_batch.positions is None:
forward_batch.positions = positions
else:
if forward_batch.positions.shape == positions.shape:
forward_batch.positions.copy_(positions)
else:
forward_batch.positions = positions
def expand_for_topk_draft(forward_batch: ForwardBatch, topk: int) -> None:
"""Repeat committed-prefix metadata for the active ``B * topk`` frontier."""
if topk == 1 or forward_batch.batch_size == 0:
return
if forward_batch.batch_size != forward_batch.seq_lens.shape[0]:
raise RuntimeError(
"Frozen-KV MTP topk expansion expects an unexpanded forward "
"batch where batch_size == len(seq_lens)."
)
forward_batch.batch_size *= topk
forward_batch.req_pool_indices = forward_batch.req_pool_indices.repeat_interleave(
topk, dim=0
)
forward_batch.seq_lens = forward_batch.seq_lens.repeat_interleave(topk, dim=0)
if forward_batch.seq_lens_cpu is not None:
forward_batch.seq_lens_cpu = forward_batch.seq_lens_cpu.repeat_interleave(
topk, dim=0
)
forward_batch.seq_lens_sum = forward_batch.seq_lens_cpu.sum().item()
else:
forward_batch.seq_lens_sum = torch.sum(forward_batch.seq_lens).item()
positions = torch.clamp(forward_batch.seq_lens - 1, min=0).to(torch.int64)
forward_batch.positions = positions
forward_batch.num_token_non_padded_cpu = positions.numel()
if forward_batch.num_token_non_padded is not None:
forward_batch.num_token_non_padded.fill_(positions.numel())
if (
forward_batch.mrope_positions is not None
and forward_batch.mrope_positions.shape[-1] * topk == positions.numel()
):
forward_batch.mrope_positions = forward_batch.mrope_positions.repeat_interleave(
topk, dim=-1
)
def position_for_batch(batch: ScheduleBatch) -> torch.Tensor:
return torch.clamp(batch.seq_lens - 1, min=0).to(torch.int64)
def select_last_extend_hidden(
batch: ScheduleBatch, hidden_states: torch.Tensor
) -> torch.Tensor:
if hidden_states.shape[0] == batch.batch_size():
return hidden_states
lens = torch.tensor(batch.extend_lens, device=hidden_states.device)
last_indices = torch.cumsum(lens, dim=0) - 1
return hidden_states[last_indices.to(torch.long)]
@@ -0,0 +1,781 @@
# Copyright 2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Spec-v2 worker for Frozen-KV MTP (two layers, like ``eagle_worker_v2``).
The frozen draft reads the target KV cache read-only and owns no KV pool, so
its "draft extend" is not a model forward: it selects the last accepted token +
target hidden state as the next-iter seed, and the seed forward runs at the
start of the next draft.
"""
from __future__ import annotations
import logging
from typing import Optional
import torch
from sglang.srt.layers.moe.utils import (
speculative_moe_a2a_backend_context,
speculative_moe_backend_context,
)
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.cuda_graph_config import cuda_graph_fully_disabled
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
from sglang.srt.model_executor.pool_configurator import MemoryPoolConfig
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.base_spec_worker import EagleDraftWorkerBase
from sglang.srt.speculative.eagle_utils import (
build_tree_kernel_efficient,
organize_draft_results,
)
from sglang.srt.speculative.eagle_worker_v2 import EAGLEWorkerV2, _get_plan_stream
from sglang.srt.speculative.frozen_kv_mtp_info import (
FrozenKVMTPContext,
FrozenKVMTPDraftInput,
FrozenKVMTPVerifyInput,
)
from sglang.srt.speculative.frozen_kv_mtp_utils import (
expand_for_topk_draft,
frozen_kv_target_view,
position_for_batch,
select_last_extend_hidden,
set_frozen_kv_positions,
target_kv_pool_view,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import (
draft_tp_context,
fast_topk,
select_top_k_tokens,
spec_stage_span,
)
from sglang.srt.utils import empty_context
from sglang.srt.utils.async_probe import (
maybe_detect_inf,
maybe_detect_nan,
maybe_detect_oob,
)
logger = logging.getLogger(__name__)
class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
"""Frozen-KV MTP draft worker.
The assistant reads target KV only. It reuses EAGLE's verify input/output
contract, but owns the seed and recurrent draft loop because there is no
assistant-side KV extension.
"""
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
self.server_args = server_args
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.gpu_id = gpu_id
self.device = server_args.device
self.target_worker = target_worker
self.page_size = server_args.page_size
self.speculative_algorithm = SpeculativeAlgorithm.from_string(
server_args.speculative_algorithm
)
assert self.speculative_algorithm.is_frozen_kv_mtp(), (
"FrozenKVMTPDraftWorker should only be instantiated for "
"SpeculativeAlgorithm.FROZEN_KV_MTP, got "
f"{self.speculative_algorithm.name}."
)
# Target pools (read-only) are bound in alloc_memory_pool(), not here, so
# the worker can be built before the target pool exists (see #29021).
self.req_to_token_pool = None
self.token_to_kv_pool_allocator = None
self.draft_pool_config: Optional[MemoryPoolConfig] = None
self.hot_token_id = None
with (
empty_context()
), speculative_moe_backend_context(), speculative_moe_a2a_backend_context():
# NOTE: call TpModelWorker.__init__ explicitly -- EagleDraftWorkerBase is
# an ABC with no __init__, so cooperative super() would be ambiguous.
TpModelWorker.__init__(
self,
server_args=server_args,
gpu_id=gpu_id,
tp_rank=tp_rank,
pp_rank=0,
dp_rank=dp_rank,
moe_ep_rank=moe_ep_rank,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
)
embed, head = self.target_worker.model_runner.model.get_embed_and_head()
if hasattr(self.draft_model_runner.model, "set_embed_and_head"):
self.draft_model_runner.model.set_embed_and_head(embed, head)
else:
logger.debug(
"Draft model %s does not implement set_embed_and_head; "
"skipping target-embedding bind in Frozen-KV MTP skeleton.",
type(self.draft_model_runner.model).__name__,
)
self.kv_context: Optional[FrozenKVMTPContext] = None
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
self.draft_attn_backend = None
self.cuda_graph_runner = None
# Frozen draft has no draft-extend forward (seed-select only); keep these
# None so inherited probes (spec_v2_attn_backends, adaptive) stay typed.
self.draft_extend_attn_backend = None
self.cuda_graph_runner_for_draft_extend = None
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.draft_pool_config = MemoryPoolConfig(
max_total_num_tokens=64, # Dummy value
max_running_requests=memory_pool_config.max_running_requests,
)
# NOTE: call TpModelWorker explicitly -- EagleDraftWorkerBase precedes it in
# the MRO and its alloc_memory_pool is a no-op stub.
TpModelWorker.alloc_memory_pool(
self,
memory_pool_config=self.draft_pool_config,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
if hasattr(self.draft_model_runner.model, "bind_frozen_kv_context"):
self._bind_kv_context()
def init_attention_backends(self):
with (
self.draft_tp_context(self.draft_model_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
):
TpModelWorker.init_attention_backends(self)
self.draft_attn_backend = self._init_draft_attn_backend()
self.draft_model_runner.draft_attn_backend = self.draft_attn_backend
def init_cuda_graphs(self):
with (
self.draft_tp_context(self.draft_model_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
):
TpModelWorker.init_cuda_graphs(self, capture_decode_cuda_graph=False)
self._capture_cuda_graphs()
@property
def draft_model_runner(self):
return self.model_runner
@property
def draft_runner(self):
# Alias for the inherited EAGLEWorkerV2 forward/verify skeleton, which
# reads `draft_worker.draft_runner`.
return self.model_runner
def get_attn_backend(self): # pragma: no cover - exposed for adaptive
return self.draft_attn_backend
def clear_cache_pool(self):
pass
def _resolve_draft_backend_type(self) -> str:
return (
self.server_args.speculative_draft_attention_backend
or self.server_args.decode_attention_backend
or self.server_args.attention_backend
)
def _init_draft_attn_backend(self):
if self.topk == 1:
return self.draft_model_runner.attn_backend
backend_type = self._resolve_draft_backend_type()
if backend_type != "triton":
raise ValueError(
"Frozen-KV MTP topk > 1 currently supports only the triton "
f"attention backend, got {backend_type}."
)
return self._init_triton_draft_attn_backend()
def _init_triton_draft_attn_backend(self):
from sglang.srt.layers.attention.triton_backend import TritonAttnBackend
max_bs = self.req_to_token_pool.size * self.topk
kv_indptr_buf = torch.zeros(
(max_bs + 1,), dtype=torch.int32, device=self.draft_model_runner.device
)
return TritonAttnBackend(
self.draft_model_runner,
skip_prefill=True,
kv_indptr_buf=kv_indptr_buf,
)
def _bind_kv_context(self) -> None:
draft_model = self.draft_model_runner.model
if not hasattr(draft_model, "build_frozen_kv_mtp_context") or not hasattr(
draft_model, "bind_frozen_kv_context"
):
logger.debug(
"Draft model %s does not implement Frozen-KV MTP context hooks; "
"skipping frozen-kv bind.",
type(draft_model).__name__,
)
return
ctx = draft_model.build_frozen_kv_mtp_context(
target_model=self.target_worker.model_runner.model,
target_token_to_kv_pool=self.target_worker.model_runner.token_to_kv_pool,
)
draft_model.bind_frozen_kv_context(ctx)
self.kv_context = ctx
def _frozen_kv_target_view(self, forward_batch: ForwardBatch):
return frozen_kv_target_view(
forward_batch, self.kv_context, self.draft_attn_backend
)
def _target_kv_pool_view(self, forward_batch: ForwardBatch):
return target_kv_pool_view(
forward_batch, self.kv_context, self.draft_attn_backend
)
def _set_positions(self, forward_batch: ForwardBatch) -> None:
set_frozen_kv_positions(forward_batch, self.topk)
def _expand_for_topk_draft(self, forward_batch: ForwardBatch) -> None:
expand_for_topk_draft(forward_batch, self.topk)
def _position_for_batch(self, batch: ScheduleBatch) -> torch.Tensor:
return position_for_batch(batch)
@property
def _recurrent_hidden_size(self) -> int:
return int(self.draft_model_runner.model.backbone_hidden_size)
def _init_frozen_kv_metadata(self, forward_batch: ForwardBatch) -> None:
if forward_batch.forward_mode.is_idle():
return
if forward_batch.seq_lens_cpu is not None:
forward_batch.seq_lens_sum = forward_batch.seq_lens_cpu.sum().item()
else:
forward_batch.seq_lens_sum = torch.sum(forward_batch.seq_lens).item()
with self._frozen_kv_target_view(forward_batch):
self.draft_attn_backend.init_forward_metadata(forward_batch)
forward_batch.mark_forward_metadata_ready()
def _init_frozen_kv_metadata_capture_cuda_graph(
self, forward_batch: ForwardBatch
) -> None:
with self._frozen_kv_target_view(forward_batch):
self.draft_attn_backend.init_forward_metadata_out_graph(
forward_batch, in_capture=True
)
forward_batch.mark_forward_metadata_ready()
def _init_frozen_kv_metadata_replay_cuda_graph(
self, forward_batch: ForwardBatch, bs: int, seq_lens_sum: int
) -> None:
from types import SimpleNamespace
fb_view = SimpleNamespace(
batch_size=bs,
forward_mode=ForwardMode.DECODE,
input_ids=getattr(forward_batch, "input_ids", None),
req_pool_indices=forward_batch.req_pool_indices[:bs],
seq_lens=forward_batch.seq_lens[:bs],
seq_lens_sum=seq_lens_sum,
seq_lens_cpu=(
forward_batch.seq_lens_cpu[:bs]
if forward_batch.seq_lens_cpu is not None
else None
),
encoder_lens=None,
out_cache_loc=getattr(forward_batch, "out_cache_loc", None),
spec_info=None,
)
with self._frozen_kv_target_view(forward_batch):
self.draft_attn_backend.init_forward_metadata_out_graph(fb_view)
def _capture_cuda_graphs(self) -> None:
if cuda_graph_fully_disabled() or self.speculative_num_steps <= 1:
return
if self.target_worker.device != "cuda":
logger.info(
"Frozen-KV MTP draft CUDA graph is only supported on CUDA; "
"running the draft loop eagerly on %s.",
self.target_worker.device,
)
return
from sglang.srt.speculative.frozen_kv_mtp_cuda_graph_runner import (
FrozenKVMTPCudaGraphRunner,
)
logger.info("Capture Frozen-KV MTP draft cuda graph begin.")
self.cuda_graph_runner = FrozenKVMTPCudaGraphRunner(self)
logger.info("Capture Frozen-KV MTP draft cuda graph end.")
def _select_last_extend_hidden(
self, batch: ScheduleBatch, hidden_states: torch.Tensor
) -> torch.Tensor:
return select_last_extend_hidden(batch, hidden_states)
def _idle_seed(self) -> FrozenKVMTPDraftInput:
return FrozenKVMTPDraftInput.create_idle_input(
device=self.device,
hidden_size=self._recurrent_hidden_size,
dtype=self.model_config.dtype,
topk=self.topk,
capture_hidden_mode=CaptureHiddenMode.LAST,
)
def _build_seed_draft_input(
self,
last_token_ids: torch.Tensor,
last_hidden_states: torch.Tensor,
) -> FrozenKVMTPDraftInput:
"""Build the next-iter seed ``FrozenKVMTPDraftInput`` from (bonus token,
target hidden). No forward here -- the seed forward runs inside the
captured draft graph (see ``draft_forward``'s seed iter)."""
if last_token_ids.numel() == 0:
return self._idle_seed()
stashed = FrozenKVMTPDraftInput()
stashed.bonus_tokens = last_token_ids.to(torch.int64)
stashed.hidden_states = last_hidden_states
# Real-shaped zeros so inherited `filter_batch`/`merge_batch` can slice
# them between iters; overwritten by the captured seed iter.
bs = last_token_ids.shape[0]
device = last_token_ids.device
stashed.topk_p = torch.zeros(
(bs, self.topk), device=device, dtype=torch.float32
)
stashed.topk_index = torch.zeros(
(bs, self.topk), device=device, dtype=torch.int64
)
stashed.capture_hidden_mode = CaptureHiddenMode.LAST
stashed.num_tokens_per_req = 1
stashed.num_tokens_for_logprob_per_req = 1
return stashed
def draft(self, batch: ScheduleBatch):
if batch.forward_mode.is_idle():
return FrozenKVMTPVerifyInput.create_idle_input(
self.topk,
self.speculative_num_steps,
self.speculative_num_draft_tokens,
)
spec_info = batch.spec_info
assert isinstance(spec_info, FrozenKVMTPDraftInput)
# NOTE: per-iter bookkeeping (penalty cumulation, maybe_evict_swa,
# decode_batch_idx tick) is done by the scheduler-driven
# eagle_utils.eagle_prepare_for_decode (see
# ScheduleBatch.prepare_for_decode), not here -- matching EAGLE v2.
# Repeating evict/tick here would double-run them: the idx clock
# gates SWA eviction timing and the SWA prefix-lock release.
spec_info.capture_hidden_mode = CaptureHiddenMode.LAST
spec_info.num_tokens_per_req = self.topk
spec_info.num_tokens_for_logprob_per_req = self.topk
spec_info.positions = self._position_for_batch(batch)
batch.seq_lens_sum = torch.sum(batch.seq_lens).item()
batch.return_hidden_states = False
forward_batch = ForwardBatch.init_new(batch, self.draft_model_runner)
assert forward_batch.capture_hidden_mode == CaptureHiddenMode.LAST
self._set_positions(forward_batch)
self._expand_for_topk_draft(forward_batch)
# Frozen draft never writes KV; None signals fill_from to skip the slot.
forward_batch.out_cache_loc = None
can_run_cuda_graph = (
self.cuda_graph_runner
and self.cuda_graph_runner.can_run_graph(forward_batch)
)
if can_run_cuda_graph:
parent_list, top_scores_index, draft_tokens = (
self.cuda_graph_runner.execute(forward_batch)
)
else:
forward_batch.can_run_dp_cuda_graph = False
parent_list, top_scores_index, draft_tokens = self.draft_forward(
forward_batch
)
(
tree_mask,
position,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
draft_tokens,
) = build_tree_kernel_efficient(
spec_info.bonus_tokens,
parent_list,
top_scores_index,
draft_tokens,
batch.seq_lens,
batch.seq_lens_sum,
self.topk,
self.speculative_num_steps,
self.speculative_num_draft_tokens,
)
return FrozenKVMTPVerifyInput(
draft_token=draft_tokens,
custom_mask=tree_mask,
positions=position,
retrieve_index=retrieve_index,
retrieve_next_token=retrieve_next_token,
retrieve_next_sibling=retrieve_next_sibling,
retrieve_cum_len=None,
spec_steps=self.speculative_num_steps,
topk=self.topk,
draft_token_num=self.speculative_num_draft_tokens,
capture_hidden_mode=CaptureHiddenMode.FULL,
seq_lens_sum=batch.seq_lens_sum,
seq_lens_cpu=batch.seq_lens_cpu,
)
def draft_forward(self, forward_batch: ForwardBatch):
spec_info = forward_batch.spec_info
assert isinstance(spec_info, FrozenKVMTPDraftInput)
score_list: list[torch.Tensor] = []
token_list: list[torch.Tensor] = []
parents_list: list[torch.Tensor] = []
# Seed + recurrent iters share the same `seq_lens - 1` rope position,
# so one init covers the loop. Must run even at num_steps == 1.
if forward_batch.needs_forward_metadata_init():
self._init_frozen_kv_metadata(forward_batch)
# Seed iter: assistant forward on (bonus_token, target_h) to produce
# iter-0 `(topk_p, topk_index, hidden_states)`. For topk>1, replicate
# to `bs*topk` to match kernel shapes, then slice back per-req.
bonus_tokens = spec_info.bonus_tokens
target_hidden = spec_info.hidden_states
if self.topk > 1:
seed_input_ids = bonus_tokens.repeat_interleave(self.topk, dim=0)
seed_prev_hidden = target_hidden.repeat_interleave(self.topk, dim=0)
else:
seed_input_ids = bonus_tokens
seed_prev_hidden = target_hidden
forward_batch.input_ids = seed_input_ids
forward_batch.spec_info.hidden_states = seed_prev_hidden
self._set_positions(forward_batch)
with (
self._target_kv_pool_view(forward_batch),
forward_context(ForwardContext(attn_backend=self.draft_attn_backend)),
):
seed_output = self.draft_model_runner.forward(forward_batch).logits_output
maybe_detect_nan(
seed_output.next_token_logits, "frozen_kv_mtp_draft: seed iter"
)
if self.topk > 1:
seed_next_logits = seed_output.next_token_logits[:: self.topk]
seed_hidden_per_req = seed_output.hidden_states[:: self.topk]
else:
seed_next_logits = seed_output.next_token_logits
seed_hidden_per_req = seed_output.hidden_states
probs = torch.softmax(seed_next_logits, dim=-1)
topk_p, topk_index = fast_topk(probs, self.topk, dim=-1)
maybe_detect_oob(
topk_index,
0,
seed_next_logits.shape[-1],
"frozen_kv_mtp_draft: seed topk_index OOB",
)
hidden_states = seed_hidden_per_req
scores = None
for i in range(self.speculative_num_steps):
input_ids, hidden_states, scores, tree_info = select_top_k_tokens(
i, topk_p, topk_index, hidden_states, scores, self.topk
)
score_list.append(tree_info[0])
token_list.append(tree_info[1])
parents_list.append(tree_info[2])
if i == self.speculative_num_steps - 1:
break
forward_batch.input_ids = input_ids
forward_batch.spec_info.hidden_states = hidden_states
self._set_positions(forward_batch)
with (
self._target_kv_pool_view(forward_batch),
forward_context(ForwardContext(attn_backend=self.draft_attn_backend)),
):
logits_output = self.draft_model_runner.forward(
forward_batch
).logits_output
maybe_detect_nan(
logits_output.next_token_logits, f"frozen_kv_mtp_draft step {i}"
)
maybe_detect_inf(
logits_output.next_token_logits, f"frozen_kv_mtp_draft step {i}"
)
probs = torch.softmax(logits_output.next_token_logits, dim=-1)
topk_p, topk_index = fast_topk(probs, self.topk, dim=-1)
maybe_detect_oob(
topk_index,
0,
logits_output.next_token_logits.shape[-1],
"frozen_kv_mtp_draft: topk_index OOB",
)
hidden_states = logits_output.hidden_states
return organize_draft_results(
score_list, token_list, parents_list, self.speculative_num_draft_tokens
)
def draft_extend(self):
# EagleDraftWorkerBase contract. Frozen has no draft-KV extend forward; the
# orchestrator calls `_draft_extend_for_{prefill,decode}` directly.
pass
def _draft_extend_for_prefill(
self,
batch: ScheduleBatch,
target_hidden_states: torch.Tensor,
next_token_ids: torch.Tensor,
mm_input_embeds: Optional[torch.Tensor] = None,
) -> FrozenKVMTPDraftInput:
"""Seed for the first decode iter after prefill. Frozen draft writes no
KV (reads target KV), so unlike EAGLE there is no draft-extend forward:
just select the last prompt hidden + bonus token and stash the seed."""
del mm_input_embeds # frozen seed needs no input embeds
if batch.forward_mode.is_idle():
return self._idle_seed()
last_hidden = self._select_last_extend_hidden(batch, target_hidden_states)
return self._build_seed_draft_input(next_token_ids, last_hidden)
def _draft_extend_for_decode(self, batch: ScheduleBatch, batch_result) -> None:
"""Frozen 'draft extend': no forward. Pull the last accepted token's
target hidden from the verify output and stash it as the next-iter seed.
Replaces verify's `EagleDraftInput` with a `FrozenKVMTPDraftInput` so the
next draft passes the FROZEN_KV_MTP attn-backend assertions.
"""
if batch.forward_mode.is_idle():
batch_result.next_draft_input = self._idle_seed()
return
bs = len(batch.seq_lens)
# Same per-req select_index EAGLE uses on its draft-extend output: the
# last accepted node (accept_lens - 1) in each per-req block of width
# num_draft_tokens. Verify already compacted the accepted path to the
# front (topk > 1) / it is the front chain (topk == 1).
select_index = (
torch.arange(bs, device=self.device) * self.speculative_num_draft_tokens
+ batch_result.accept_lens
- 1
)
last_hidden = batch_result.logits_output.hidden_states[select_index]
bonus_tokens = batch_result.next_draft_input.bonus_tokens
batch_result.next_draft_input = self._build_seed_draft_input(
bonus_tokens, last_hidden
)
class FrozenKVMTPWorkerV2(EAGLEWorkerV2):
"""Spec-v2 (overlap) orchestrator for Frozen-KV MTP.
Reuses ``EAGLEWorkerV2``'s verify / ``move_accept_tokens`` / forward
skeleton verbatim; only the draft worker and the seed-based draft-extend
are frozen-specific.
"""
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
# NOTE: intentionally does NOT call EAGLEWorkerV2.__init__ -- that builds
# an EagleDraftWorker (with its own draft KV pool). The frozen draft owns
# no KV, so we mirror the relevant setup and build a FrozenKVMTPDraftWorker.
self.server_args = server_args
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.tp_rank = tp_rank
self.gpu_id = gpu_id
self.device = server_args.device
self._target_worker = target_worker
self.page_size = server_args.page_size
self.speculative_algorithm = SpeculativeAlgorithm.from_string(
server_args.speculative_algorithm
)
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
# Match the draft context length to the target (assistant reads target KV).
server_args.override(
"spec_worker.match_target_context_length",
context_length=target_worker.model_runner.model_config.context_len,
)
self._draft_worker = FrozenKVMTPDraftWorker(
server_args,
gpu_id,
tp_rank,
dp_rank,
moe_ep_rank,
attn_cp_rank,
moe_dp_rank,
nccl_port,
target_worker,
)
# Frozen MTP does not wire the adaptive controller yet.
assert (
not server_args.speculative_adaptive
), "Frozen-KV MTP does not support adaptive speculative decoding yet."
self.adaptive_controller = None
# Some dummy tensors (parity with EAGLEWorkerV2 init).
self.num_new_pages_per_topk = torch.empty(
(), dtype=torch.int64, device=self.device
)
self.extend_lens = torch.empty((), dtype=torch.int64, device=self.device)
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
@property
def spec_v2_attn_backends(self) -> tuple:
# Frozen draft touches no draft-extend backend; only target + draft.
return (
self._target_worker.model_runner.attn_backend,
self._draft_worker.draft_attn_backend,
)
def forward_batch_generation(self, batch: ScheduleBatch, on_publish=None):
# Mirrors EAGLEWorkerV2.forward_batch_generation; the only frozen-specific
# change is the idle draft-input (FrozenKVMTPDraftInput + recurrent hidden
# size). The draft / seed-based draft-extend hooks are FrozenKVMTPDraftWorker's.
if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
# Target prefill (frozen is never standalone -> capture FULL hidden).
batch.capture_hidden_mode = CaptureHiddenMode.FULL
batch_output = self.target_worker.forward_batch_generation(batch)
# Spec_v2 convention: batch.seq_lens = length BEFORE this iter's tokens.
batch_output.new_seq_lens = batch.seq_lens
# Publish before draft-extend so the fence is at target-end.
if on_publish is not None:
on_publish(batch_output.new_seq_lens)
# Draft prefill seed (no forward).
with (
self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group
),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
spec_stage_span("draft_extend"),
):
batch_output.next_draft_input = (
self.draft_worker._draft_extend_for_prefill(
batch,
batch_output.logits_output.hidden_states,
batch_output.next_token_ids,
batch_output.logits_output.mm_input_embeds,
)
)
return batch_output
else:
self.activate_step_by_batch(batch.seq_lens.shape[0])
if batch.spec_info is None:
batch.spec_info = self.draft_worker._idle_seed()
with (
self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group
),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
spec_stage_span("draft"),
):
verify_input = self.draft_worker.draft(batch)
assert verify_input.is_verify_input()
batch.spec_info = verify_input
batch_output = self.verify(batch)
# Publish before draft-extend so the fence is at verify-end.
if on_publish is not None:
on_publish(batch_output.new_seq_lens)
with (
self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group
),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
spec_stage_span("draft_extend"),
):
self.draft_worker._draft_extend_for_decode(batch, batch_output)
return batch_output
@@ -0,0 +1,718 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
from __future__ import annotations
import contextlib
import logging
import time
from dataclasses import dataclass
from types import SimpleNamespace
from typing import TYPE_CHECKING, Callable, List, Optional
import torch
from sglang.srt.compilation.torch_compile_decoration import set_torch_compile_config
from sglang.srt.layers.dp_attention import (
DpPaddingMode,
set_dp_buffer_len,
set_is_extend_in_batch,
)
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.model_executor.cuda_graph_config import (
Backend,
Phase,
check_cuda_graph_backend,
)
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.model_executor.forward_context import (
ForwardContext,
forward_context,
)
from sglang.srt.model_executor.input_buffers import ForwardInputBuffers
from sglang.srt.model_executor.runner import (
DecodeCudaGraphRunner,
DeepEPCudaGraphRunnerAdapter,
ShapeKey,
get_batch_sizes_to_capture,
model_capture_mode,
)
from sglang.srt.model_executor.runner.flashinfer_autotune import (
maybe_flashinfer_autotune_speculative_draft,
)
from sglang.srt.model_executor.runner_backend.utils import resolve_decode_backend
from sglang.srt.model_executor.runner_backend_utils import (
CUDA_GRAPH_CAPTURE_FAILED_MSG,
)
from sglang.srt.runtime_context import get_flags
from sglang.srt.speculative.eagle_info import EagleDraftExtendInput
from sglang.srt.speculative.eagle_utils import get_draft_input_from_target_hidden_dim
from sglang.srt.speculative.spec_utils import fast_topk
from sglang.srt.utils import (
get_available_gpu_memory,
require_attn_tp_gather,
require_gathered_buffer,
require_mlp_sync,
require_mlp_tp_gather,
)
if TYPE_CHECKING:
from sglang.srt.speculative.multi_layer_eagle_worker_v2 import (
MultiLayerEagleDraftWorker,
)
logger = logging.getLogger(__name__)
@dataclass
class MultiLayerEagleDraftExtendInputBuffers(ForwardInputBuffers):
"""A single persistent buffer set shared by every MTP draft step."""
input_ids: torch.Tensor
out_cache_loc: torch.Tensor
positions: torch.Tensor
seq_lens: torch.Tensor
seq_lens_cpu: torch.Tensor
req_pool_indices: torch.Tensor
num_correct_drafts: torch.Tensor
num_accept_tokens: torch.Tensor
extend_seq_lens: torch.Tensor
extend_start_loc: torch.Tensor
# Flat index (into the token dimension) of each request's last accepted
# token. Used both by the in-graph top-k gather and by the worker's
# per-step input_ids rotation.
select_index: torch.Tensor
mrope_positions: torch.Tensor
hidden_states: torch.Tensor
next_token_logits_buffer: torch.Tensor
global_num_tokens_gpu: Optional[torch.Tensor]
global_num_tokens_for_logprob_gpu: Optional[torch.Tensor]
class MultiLayerEagleDraftExtendCudaGraphRunner(DecodeCudaGraphRunner):
"""Per-step multi-layer EAGLE draft-extend runner.
Subclasses DecodeCudaGraphRunner. All steps share a single buffer set
owned by the composite MultiLayerEagleMultiStepDraftExtendCudaGraphRunner,
so initialization is split: __init__ does basic field setup, and
init_buffers_and_capture (called by the composite once the shared buffers
exist) attaches them and runs capture.
"""
def __init__(self, eagle_worker: MultiLayerEagleDraftWorker, step: int):
# Parse args
self.step = step
self.eagle_worker = eagle_worker
self.model_runner = model_runner = eagle_worker.mtp_model_runner(self.step)
self.forward_mode = ForwardMode.DRAFT_EXTEND_V2
# Fields the parent's capture() reads:
self.device = model_runner.device
self.device_module = torch.get_device_module(self.device)
self.tp_size = model_runner.tp_size
self.dp_size = model_runner.server_args.dp_size
self.pp_size = model_runner.server_args.pp_size
self.enable_torch_compile = get_flags().capture.enable_torch_compile
self.disable_padding = model_runner.server_args.disable_cuda_graph_padding
self.require_gathered_buffer = require_gathered_buffer(model_runner.server_args)
self.require_mlp_tp_gather = require_mlp_tp_gather(model_runner.server_args)
self.require_mlp_sync = require_mlp_sync(model_runner.server_args)
self.require_attn_tp_gather = require_attn_tp_gather(model_runner.server_args)
self.enable_pdmux = model_runner.server_args.enable_pdmux
self.speculative_num_steps = model_runner.server_args.speculative_num_steps
self.speculative_num_draft_tokens = (
model_runner.server_args.speculative_num_draft_tokens
)
self.topk = model_runner.server_args.speculative_eagle_topk
self.enable_profile_cuda_graph = (
model_runner.server_args.enable_profile_cuda_graph
)
self.attn_backend = self.eagle_worker.draft_extend_attn_backend_list[self.step]
# Disable parent paths that don't apply.
self.compile_bs = []
self.record_nolora_graph = False
self.is_dllm = False
self.deepep_adapter = DeepEPCudaGraphRunnerAdapter()
self.capture_forward_mode = self.forward_mode
self.capture_hidden_mode = CaptureHiddenMode.FULL
self.capture_bs, _ = get_batch_sizes_to_capture(model_runner)
self.padded_static_len = -1
# Fixed window: every step extends each request by the same number of
# tokens, which lets all steps share one buffer set.
self.num_tokens_per_bs = self.speculative_num_draft_tokens
self.max_bs = max(self.capture_bs)
self.max_num_token = self.max_bs * self.num_tokens_per_bs
self.extend_seq_lens_cpu = [self.num_tokens_per_bs] * self.max_bs
self.eagle_worker.draft_extend_attn_backend_list[
self.step
].init_cuda_graph_state(self.max_bs, self.max_num_token)
self.seq_len_fill_value = self.eagle_worker.draft_extend_attn_backend_list[
self.step
].get_cuda_graph_seq_len_fill_value()
def init_buffers_and_capture(self, buffers: MultiLayerEagleDraftExtendInputBuffers):
"""Attach the shared buffer set and capture this step's graphs."""
self.buffers = buffers
if self.enable_torch_compile:
set_torch_compile_config()
self.backend = resolve_decode_backend(self)
try:
with model_capture_mode():
self.capture()
except RuntimeError as e:
raise Exception(
f"Capture cuda graph failed: {e}\n{CUDA_GRAPH_CAPTURE_FAILED_MSG}"
)
def _replay_graph(self, shape_key, forward_batch):
return self.backend.replay(shape_key, forward_batch)
def _make_graph_key(self, bs, stream_idx=None, variant_label=None):
return ShapeKey(size=bs)
def can_run_graph(self, forward_batch: ForwardBatch):
if self.require_mlp_tp_gather:
cuda_graph_bs = (
max(forward_batch.global_num_tokens_cpu) // self.num_tokens_per_bs
if self.model_runner.spec_algorithm.is_eagle()
else max(forward_batch.global_num_tokens_cpu)
)
else:
cuda_graph_bs = forward_batch.seq_lens.numel()
is_bs_supported = (
self.backend.can_run(forward_batch, cuda_graph_bs)
if self.disable_padding
else cuda_graph_bs <= self.max_bs
)
if self.require_mlp_sync:
is_bs_supported = is_bs_supported and forward_batch.can_run_dp_cuda_graph
return is_bs_supported
def get_forward_batch(self, bs: int) -> ForwardBatch:
buffers = self.buffers
num_tokens = bs * self.num_tokens_per_bs
input_ids = buffers.input_ids[:num_tokens]
req_pool_indices = buffers.req_pool_indices[:bs]
seq_lens = buffers.seq_lens[:bs]
seq_lens_cpu = buffers.seq_lens_cpu[:bs]
extend_seq_lens = buffers.extend_seq_lens[:bs]
extend_seq_lens_cpu = self.extend_seq_lens_cpu[:bs]
extend_start_loc = buffers.extend_start_loc[:bs]
num_correct_drafts = buffers.num_correct_drafts[:bs]
num_accept_tokens = buffers.num_accept_tokens[:bs]
out_cache_loc = buffers.out_cache_loc[:num_tokens]
positions = buffers.positions[:num_tokens]
mrope_positions = buffers.mrope_positions[:, :num_tokens]
hidden_states = buffers.hidden_states[:num_tokens]
next_token_logits_buffer = buffers.next_token_logits_buffer[:num_tokens]
if self.require_mlp_tp_gather:
global_num_tokens_cpu = [num_tokens] * self.dp_size
global_num_tokens_for_logprob_cpu = [num_tokens] * self.dp_size
elif self.require_attn_tp_gather:
global_num_tokens_cpu = [num_tokens]
# DRAFT_EXTEND_V2 produces logits for all tokens, not bs (see mlp branch above)
global_num_tokens_for_logprob_cpu = [num_tokens]
else:
global_num_tokens_cpu = None
if global_num_tokens_cpu is not None:
global_dp_buffer_len = sum(global_num_tokens_cpu)
buffers.global_num_tokens_gpu.copy_(
torch.tensor(
global_num_tokens_cpu,
dtype=torch.int32,
device=buffers.input_ids.device,
)
)
buffers.global_num_tokens_for_logprob_gpu.copy_(
torch.tensor(
global_num_tokens_for_logprob_cpu,
dtype=torch.int32,
device=buffers.input_ids.device,
)
)
else:
global_dp_buffer_len = None
spec_info = EagleDraftExtendInput(
hidden_states=hidden_states,
num_correct_drafts=num_correct_drafts,
num_accept_tokens=num_accept_tokens,
)
spec_info.positions = None
capture_mode = (
CaptureHiddenMode.NULL
if self.model_runner.spec_algorithm.is_standalone()
else CaptureHiddenMode.FULL
)
# Forward batch
forward_batch = ForwardBatch(
forward_mode=self.forward_mode,
batch_size=bs,
input_ids=input_ids,
req_pool_indices=req_pool_indices,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
next_token_logits_buffer=next_token_logits_buffer,
out_cache_loc=out_cache_loc,
seq_lens_sum=seq_lens.sum().item(),
return_logprob=False,
positions=positions,
mrope_positions=mrope_positions,
global_num_tokens_gpu=buffers.global_num_tokens_gpu,
global_num_tokens_for_logprob_gpu=buffers.global_num_tokens_for_logprob_gpu,
dp_padding_mode=DpPaddingMode.get_default_mode_in_cuda_graph(),
global_dp_buffer_len=global_dp_buffer_len,
global_num_tokens_cpu=global_num_tokens_cpu,
spec_algorithm=self.model_runner.spec_algorithm,
spec_info=spec_info,
capture_hidden_mode=capture_mode,
extend_seq_lens=extend_seq_lens,
extend_seq_lens_cpu=extend_seq_lens_cpu,
padded_static_len=self.padded_static_len,
extend_start_loc=extend_start_loc,
extend_num_tokens=self.num_tokens_per_bs * bs,
num_token_non_padded_cpu=self.num_tokens_per_bs * bs,
return_hidden_states_before_norm=True,
)
return forward_batch
def _postprocess_forward_batch(self, forward_batch: ForwardBatch, bs: int):
"""Hook for subclasses to mutate the captured forward batch."""
return forward_batch
def _compute_topk(self, ret, bs: int):
"""Compute top-k on the last accepted token's logits and attach it to
``ret``. The gather index lives in a persistent buffer, so the captured
graph reads the right rows on each replay. Overridable so distributed
(vocab-sharded) builds can plug in an all-reduce-aware sampler."""
buffers = self.buffers
probs = torch.softmax(ret.next_token_logits[buffers.select_index[:bs]], dim=-1)
ret.topk_p, ret.topk_index = fast_topk(probs, self.topk, dim=-1)
def capture_one_shape(
self,
size: int,
forward: Callable,
stream_idx: Optional[int] = None,
variant_label: Optional[str] = None,
):
bs = size
buffers = self.buffers
num_tokens = bs * self.num_tokens_per_bs
forward_batch = self.get_forward_batch(bs)
forward_batch = self._postprocess_forward_batch(forward_batch, bs)
attn_backend = self.eagle_worker.draft_extend_attn_backend_list[self.step]
def run_once():
attn_backend.init_forward_metadata_in_graph(forward_batch)
# Clean intermediate result cache for DP attention
forward_batch.dp_local_start_pos = forward_batch.dp_local_num_tokens = None
set_dp_buffer_len(
forward_batch.global_dp_buffer_len,
num_tokens,
forward_batch.dp_padding_mode.is_max_len(),
forward_batch.global_num_tokens_cpu,
)
set_is_extend_in_batch(False)
output_cache_loc_backup = forward_batch.out_cache_loc
hidden_states_backup = forward_batch.spec_info.hidden_states
ret = self.model_runner.model.forward(
forward_batch.input_ids,
forward_batch.positions,
forward_batch,
)
if (
self.eagle_worker.chain_mtp_hidden_states
and ret.hidden_states is not None
):
buffers.hidden_states[:num_tokens].copy_(ret.hidden_states[:num_tokens])
self._compute_topk(ret, bs)
forward_batch.out_cache_loc = output_cache_loc_backup
forward_batch.spec_info.hidden_states = hidden_states_backup
return ret
with forward_context(ForwardContext(attn_backend=attn_backend)):
attn_backend.init_forward_metadata_out_graph(forward_batch, in_capture=True)
self.deepep_adapter.capture(is_extend_in_batch=True)
shape_key = self._make_graph_key(bs)
post_warmup_hook = getattr(
self.attn_backend, "on_after_cuda_graph_warmup", None
)
maybe_flashinfer_autotune_speculative_draft(
self,
run_once,
post_warmup_hook=post_warmup_hook,
skip_logits=False,
)
self.backend.capture_one(
shape_key,
run_once,
dummies=None,
post_warmup_hook=post_warmup_hook,
)
def replay(self, bs: int, seq_lens_sum: int, spec_info: EagleDraftExtendInput):
"""Init this step's attention metadata for the prepared bucket and
replay its graph. Buffers must already be populated by the composite
runner's ``prepare`` (step 0) or by the previous step's in-graph chain
write + worker-side rotation (steps > 0)."""
self.deepep_adapter.replay()
buffers = self.buffers
num_tokens = bs * self.num_tokens_per_bs
if self.require_gathered_buffer:
buffers.global_num_tokens_gpu.fill_(num_tokens)
buffers.global_num_tokens_for_logprob_gpu.fill_(num_tokens)
fb_view = SimpleNamespace(
batch_size=bs,
forward_mode=self.forward_mode,
input_ids=buffers.input_ids[:num_tokens],
req_pool_indices=buffers.req_pool_indices,
seq_lens=buffers.seq_lens,
seq_lens_sum=seq_lens_sum,
seq_lens_cpu=buffers.seq_lens_cpu,
encoder_lens=None,
# per-step write target; out_cache_loc is frozen at prepare() time.
out_cache_loc=buffers.out_cache_loc[:num_tokens],
spec_info=spec_info,
)
self.eagle_worker.draft_extend_attn_backend_list[
self.step
].init_forward_metadata_out_graph(fb_view)
self.bs = bs
shape_key = self._make_graph_key(bs)
return self._replay_graph(shape_key, fb_view)
class MultiLayerEagleMultiStepDraftExtendCudaGraphRunner:
"""Owns one shared buffer set and the per-step runners.
Usage from the worker::
runner.prepare(forward_batch)
for step in range(num_steps):
_, topk_p, topk_index = runner.replay(step)
if step < num_steps - 1:
rotate_input_ids(...) # advance the draft chain
Not itself a DecodeCudaGraphRunner -- it only routes work to the per-step
runners.
"""
def __init__(self, eagle_worker: MultiLayerEagleDraftWorker):
self.eagle_worker = eagle_worker
self.device = eagle_worker.device
self.gpu_id = eagle_worker.gpu_id
self.speculative_num_steps = eagle_worker.speculative_num_steps
self.draft_extend_attn_backend_list = (
eagle_worker.draft_extend_attn_backend_list
)
self.runners: List[Optional[MultiLayerEagleDraftExtendCudaGraphRunner]] = []
self.seq_len_fill_value = 1
self.max_bs = 1
self.num_tokens_per_bs = 1
self._init_and_capture()
def _create_runner(self, step: int) -> MultiLayerEagleDraftExtendCudaGraphRunner:
return MultiLayerEagleDraftExtendCudaGraphRunner(self.eagle_worker, step)
def _capture_context(self, step: int):
"""Context manager active while capturing ``step``'s graphs. Subclasses
can use it e.g. to temporarily expose a sharded local vocab size."""
return contextlib.nullcontext()
def _on_runners_created(self):
"""Hook called after all per-step runners exist but before buffers are
allocated/captured (e.g. to allocate shared sconv buffers)."""
def _cuda_graph_disabled(self) -> bool:
return check_cuda_graph_backend(Phase.DECODE, Backend.DISABLED)
def _init_and_capture(self):
if self._cuda_graph_disabled():
self.runners = [None] * self.speculative_num_steps
return
self.runners = []
# 1. Construct per-step runners (each initializes its own attn cuda
# graph state). They share the same fixed window size.
for step in range(self.speculative_num_steps):
if self.draft_extend_attn_backend_list[step]:
runner = self._create_runner(step)
self.runners.append(runner)
self.seq_len_fill_value = runner.seq_len_fill_value
self.max_bs = runner.max_bs
self.num_tokens_per_bs = runner.num_tokens_per_bs
self.capture_bs = runner.capture_bs
self.require_gathered_buffer = runner.require_gathered_buffer
self.require_mlp_tp_gather = runner.require_mlp_tp_gather
self.require_mlp_sync = runner.require_mlp_sync
self.disable_padding = runner.disable_padding
else:
self.runners.append(None)
self._on_runners_created()
# 2. Allocate the single shared buffer set and capture each step in
# reverse order.
self.buffers = self._allocate_buffers()
for step in range(self.speculative_num_steps - 1, -1, -1):
if self.runners[step] is not None:
tic = time.perf_counter()
before_mem = get_available_gpu_memory(self.device, self.gpu_id)
logger.info(
f"Capture draft extend CUDA graph begin. step={step}, "
f"avail mem={before_mem:.2f} GB"
)
with self._capture_context(step):
self.runners[step].init_buffers_and_capture(self.buffers)
after_mem = get_available_gpu_memory(self.device, self.gpu_id)
logger.info(
"Capture draft extend CUDA graph end. "
f"step={step}, elapsed={time.perf_counter() - tic:.2f} s, "
f"mem usage={(before_mem - after_mem):.2f} GB, "
f"avail mem={after_mem:.2f} GB."
)
def _vocab_size(self) -> int:
model_runner = self.eagle_worker.mtp_model_runner(0)
if hasattr(model_runner.model_config.hf_config, "draft_vocab_size"):
return model_runner.model_config.hf_config.draft_vocab_size
if hasattr(model_runner.model_config.hf_config, "hot_vocab_size"):
return model_runner.model_config.hf_config.hot_vocab_size
return model_runner.model_config.vocab_size
def _allocate_buffers(self) -> MultiLayerEagleDraftExtendInputBuffers:
runner = next(r for r in self.runners if r is not None)
model_runner = runner.model_runner
max_bs = self.max_bs
num_tokens_per_bs = self.num_tokens_per_bs
max_num_token = max_bs * num_tokens_per_bs
hidden_size = get_draft_input_from_target_hidden_dim(model_runner)
dtype = model_runner.model_config.dtype
vocab_size = self._vocab_size()
seq_lens_cpu = torch.full((max_bs,), self.seq_len_fill_value, dtype=torch.int32)
with torch.device(self.device):
input_ids = torch.zeros((max_num_token,), dtype=torch.int64)
out_cache_loc = torch.ones((max_num_token,), dtype=torch.int64)
positions = torch.zeros((max_num_token,), dtype=torch.int64)
mrope_positions = torch.zeros((3, max_num_token), dtype=torch.int64)
hidden_states = torch.zeros((max_num_token, hidden_size), dtype=dtype)
seq_lens = torch.full((max_bs,), self.seq_len_fill_value, dtype=torch.int32)
req_pool_indices = torch.zeros((max_bs,), dtype=torch.int64)
num_correct_drafts = torch.full((max_bs,), 1, dtype=torch.int32)
num_accept_tokens = torch.full((max_bs,), 1, dtype=torch.int32)
# Fixed window: every request extends by exactly num_tokens_per_bs
# tokens, and start locs are a constant arange.
extend_seq_lens = torch.full(
(max_bs,), num_tokens_per_bs, dtype=torch.int32
)
extend_start_loc = torch.arange(
0, max_num_token, step=num_tokens_per_bs, dtype=torch.int32
)
select_index = torch.zeros((max_bs,), dtype=torch.int64)
next_token_logits_buffer = torch.zeros(
(max_num_token, vocab_size), dtype=torch.float
)
if self.require_gathered_buffer:
if self.require_mlp_tp_gather:
dp_size = runner.dp_size
global_num_tokens_gpu = torch.zeros((dp_size,), dtype=torch.int32)
global_num_tokens_for_logprob_gpu = torch.zeros(
(dp_size,), dtype=torch.int32
)
else:
global_num_tokens_gpu = torch.zeros((1,), dtype=torch.int32)
global_num_tokens_for_logprob_gpu = torch.zeros(
(1,), dtype=torch.int32
)
else:
global_num_tokens_gpu = None
global_num_tokens_for_logprob_gpu = None
return MultiLayerEagleDraftExtendInputBuffers(
input_ids=input_ids,
out_cache_loc=out_cache_loc,
positions=positions,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
req_pool_indices=req_pool_indices,
num_correct_drafts=num_correct_drafts,
num_accept_tokens=num_accept_tokens,
extend_seq_lens=extend_seq_lens,
extend_start_loc=extend_start_loc,
select_index=select_index,
mrope_positions=mrope_positions,
hidden_states=hidden_states,
next_token_logits_buffer=next_token_logits_buffer,
global_num_tokens_gpu=global_num_tokens_gpu,
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
)
def _prepare_extra(self, forward_batch: ForwardBatch) -> None:
"""Hook for subclasses to populate extra per-call buffers (e.g. sconv)."""
def prepare(self, forward_batch: ForwardBatch):
"""Populate the shared buffers once from ``forward_batch`` and bucketize
the batch size. Subsequent ``replay(step)`` calls reuse this state."""
buffers = self.buffers
raw_bs = forward_batch.batch_size
num_tokens = raw_bs * self.num_tokens_per_bs
# Bucketize to a captured batch size (padding the tail).
if self.require_mlp_tp_gather:
max_batch_size = max(forward_batch.original_global_num_tokens_cpu)
bs = self.get_runner(0)._pad_to_bucket(int(max_batch_size), self.capture_bs)
else:
bs = self.get_runner(0)._pad_to_bucket(raw_bs, self.capture_bs)
# Reset padded slots, then copy the real values in.
buffers.input_ids.zero_()
buffers.out_cache_loc.zero_()
buffers.positions.zero_()
buffers.seq_lens.fill_(self.seq_len_fill_value)
buffers.input_ids[:num_tokens].copy_(forward_batch.input_ids)
buffers.positions[:num_tokens].copy_(forward_batch.positions)
buffers.out_cache_loc[:num_tokens].copy_(forward_batch.out_cache_loc)
buffers.seq_lens[:raw_bs].copy_(forward_batch.seq_lens)
buffers.req_pool_indices[:raw_bs].copy_(forward_batch.req_pool_indices)
if (
forward_batch.spec_info.hidden_states.shape[1]
== buffers.hidden_states.shape[1]
):
buffers.hidden_states[:num_tokens].copy_(
forward_batch.spec_info.hidden_states
)
buffers.num_correct_drafts[:raw_bs].copy_(
forward_batch.spec_info.num_correct_drafts
)
buffers.num_accept_tokens[:raw_bs].copy_(
forward_batch.spec_info.num_accept_tokens
)
if forward_batch.seq_lens_cpu is not None:
if bs != raw_bs:
buffers.seq_lens_cpu.fill_(self.seq_len_fill_value)
buffers.seq_lens_cpu[:raw_bs].copy_(forward_batch.seq_lens_cpu)
# select_index[i] = i * window + num_correct_drafts[i]: the flat index
# of request i's last accepted token. Used by the in-graph top-k gather
# and by the worker's rotation.
arange = torch.arange(bs, device=self.device, dtype=torch.int64)
buffers.select_index[:bs].copy_(
arange * self.num_tokens_per_bs + buffers.num_correct_drafts[:bs]
)
if self.require_gathered_buffer:
buffers.global_num_tokens_gpu.fill_(bs * self.num_tokens_per_bs)
buffers.global_num_tokens_for_logprob_gpu.fill_(bs * self.num_tokens_per_bs)
# Reusable spec_info for per-step attention metadata.
padded_num_tokens = bs * self.num_tokens_per_bs
spec_info = EagleDraftExtendInput(
hidden_states=buffers.hidden_states[:padded_num_tokens],
num_correct_drafts=buffers.num_correct_drafts[:bs],
num_accept_tokens=buffers.num_accept_tokens[:bs],
)
spec_info.num_tokens_per_req = self.num_tokens_per_bs
spec_info.num_tokens_for_logprob_per_req = 1
spec_info.positions = buffers.positions[:padded_num_tokens]
spec_info.extend_seq_lens_tensor = buffers.extend_seq_lens[:bs]
self._replay_spec_info = spec_info
self.raw_bs = raw_bs
self.bs = bs
self.raw_num_tokens = num_tokens
self.seq_lens_sum = (
forward_batch.seq_lens_sum + (bs - raw_bs) * self.seq_len_fill_value
)
self._prepare_extra(forward_batch)
def replay(self, step: int):
"""Replay ``step``'s graph at the prepared bucket. Returns
``(LogitsProcessorOutput, topk_p, topk_index)`` sliced to the real
batch size."""
runner = self.runners[step]
runner.raw_bs = self.raw_bs
out = runner.replay(self.bs, self.seq_lens_sum, self._replay_spec_info)
raw_bs = self.raw_bs
raw_num_tokens = self.raw_num_tokens
logits_output = LogitsProcessorOutput(
next_token_logits=out.next_token_logits[:raw_num_tokens],
hidden_states=(
out.hidden_states[:raw_num_tokens]
if out.hidden_states is not None
else None
),
)
return (
logits_output,
out.topk_p[:raw_bs],
out.topk_index[:raw_bs],
)
def get_runner(self, step):
return self.runners[step]
def get_last_runner(self):
return self.runners[-1] if self.runners else None
def can_run_graph(self, forward_batch):
return self.runners[0].can_run_graph(forward_batch)
@@ -0,0 +1,23 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
from sglang.kernels.ops.speculative.multi_layer_eagle import (
rotate_input_ids,
rotate_input_ids_kernel,
)
__all__ = [
"rotate_input_ids",
"rotate_input_ids_kernel",
]
@@ -0,0 +1,930 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
import contextlib
import logging
from typing import TYPE_CHECKING, List, Optional, Tuple
import torch
from sglang.kernels.ops.speculative.eagle import fill_bonus_tokens_func
from sglang.srt.environ import envs
from sglang.srt.hardware_backend.npu.graph_runner.multi_layer_eagle_draft_extend_npu_graph_runner import (
MultiLayerEagleMultiStepDraftExtendNpuGraphRunner,
)
from sglang.srt.layers.moe.utils import speculative_moe_backend_context
from sglang.srt.layers.utils.logprob import compute_spec_v2_logprobs
from sglang.srt.managers.io_struct import (
UpdateWeightFromDiskReqInput,
UpdateWeightsFromIPCReqInput,
)
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.scheduler import GenerationBatchResult
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.cuda_graph_config import (
Backend,
Phase,
check_cuda_graph_backend,
)
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
)
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker, EagleDraftWorkerBase
from sglang.srt.speculative.draft_utils import DraftBackendFactory
from sglang.srt.speculative.eagle_info import (
EagleDraftExtendInput,
EagleDraftInput,
EagleVerifyInput,
)
from sglang.srt.speculative.eagle_utils import (
build_tree_kernel_efficient,
default_tree_mask_mode,
eagle_prepare_for_verify,
eagle_sample,
get_draft_recurrent_hidden_state_spec,
)
from sglang.srt.speculative.multi_layer_eagle_draft_extend_cuda_graph_runner import (
MultiLayerEagleMultiStepDraftExtendCudaGraphRunner,
)
from sglang.srt.speculative.multi_layer_eagle_utils import rotate_input_ids
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import (
draft_tp_context,
record_stream_each,
record_stream_for_v2_verify,
sample_draft_proposal,
select_top_k_tokens,
)
from sglang.srt.utils import is_cpu, is_npu
from sglang.srt.utils.async_probe import (
maybe_detect_inf,
maybe_detect_nan,
maybe_detect_oob,
)
from sglang.srt.utils.common import empty_context, fast_topk
_is_npu = is_npu()
_is_cpu = is_cpu()
if TYPE_CHECKING:
from sglang.srt.model_executor.model_runner import ModelRunner, ModelRunnerOutput
logger = logging.getLogger(__name__)
def _get_plan_stream(
device: str,
) -> Tuple[any, contextlib.AbstractContextManager]:
if envs.SGLANG_ENABLE_OVERLAP_PLAN_STREAM.get():
plan_stream = torch.get_device_module(device).Stream()
plan_stream_ctx = torch.get_device_module(device).stream(plan_stream)
return plan_stream, plan_stream_ctx
else:
return None, contextlib.nullcontext()
class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: int,
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
# copy args
self.server_args = server_args
self.gpu_id = gpu_id
self.tp_rank = tp_rank
self.dp_rank = dp_rank
self.moe_ep_rank = moe_ep_rank
self.nccl_port = nccl_port
self.target_worker = target_worker
self.draft_extend_attn_backend_list = []
self.model_config = target_worker.model_config
# Args for easy access
self.device = server_args.device
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.use_rejection_sampling = server_args.speculative_use_rejection_sampling
assert self.speculative_num_draft_tokens == self.speculative_num_steps + 1, (
"multi-layer EAGLE requires speculative_num_draft_tokens == "
"speculative_num_steps + 1, "
f"got {self.speculative_num_draft_tokens} and {self.speculative_num_steps}"
)
self.speculative_algorithm = SpeculativeAlgorithm.from_string(
server_args.speculative_algorithm
)
# Set constant
EagleDraftInput.ALLOC_LEN_PER_DECODE = max(
self.speculative_num_steps * self.topk, self.speculative_num_draft_tokens
)
# Load draft model weights only.
with empty_context(), speculative_moe_backend_context():
self.draft_worker = TpModelWorker(
server_args=server_args,
gpu_id=gpu_id,
tp_rank=tp_rank,
pp_rank=0, # spec workers don't support pipeline parallelism
dp_rank=dp_rank,
moe_ep_rank=moe_ep_rank,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
is_multi_layer_eagle=True,
)
# Alias for better readability
self.draft_runner_list: List[ModelRunner] = self.draft_worker.model_runner_list
# Match `EagleDraftWorker.draft_runner` for generic draft-runner access.
self.draft_runner: ModelRunner = self.draft_runner_list[0]
# Chain-style MTP: each step propagates its own output hidden states to the
# next step. Non-chain: each step uses the target model's hidden states.
draft_arch = self.draft_worker.model_config.hf_config.architectures[0]
self.chain_mtp_hidden_states = draft_arch in ["Step3p5MTP"]
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
self.tree_mask_mode = default_tree_mask_mode()
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
"""Allocate draft KV cache pools (called by scheduler)."""
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
self.init_lm_head()
def init_attention_backends(self):
with (
self.draft_tp_context(self.draft_runner_list[0].tp_group),
speculative_moe_backend_context(),
):
super().init_attention_backends()
def init_cuda_graphs(self):
with (
self.draft_tp_context(self.draft_runner_list[0].tp_group),
speculative_moe_backend_context(),
):
super().init_cuda_graphs()
def mtp_model_runner(self, step: int):
return self.draft_runner_list[step]
def init_lm_head(self):
embed, head = self.target_worker.model_runner.model.get_embed_and_head()
# Share the embedding and lm_head
for i in range(self.speculative_num_steps):
self.draft_runner_list[i].model.set_embed_and_head(embed, head)
def init_attention_backend(self):
# Create attn backends
self.draft_extend_attn_backend_list = []
for step in range(self.speculative_num_steps):
draft_backend_factory = DraftBackendFactory(
self.server_args,
self.draft_runner_list[step],
self.topk,
self.speculative_num_steps,
)
self.draft_extend_attn_backend_list.append(
draft_backend_factory.create_draft_extend_backend()
)
if self.draft_extend_attn_backend_list[-1] is not None:
self.draft_runner_list[step].attn_backend = (
self.draft_extend_attn_backend_list[-1]
)
def _capture_cuda_graphs(self):
self.cuda_graph_runner = None
self.cuda_graph_runner_for_draft_extend = None
if _is_cpu or check_cuda_graph_backend(Phase.DECODE, Backend.DISABLED):
return
if envs.SGLANG_DISABLE_DRAFT_EXTEND_CUDA_GRAPH.get():
return
if not _is_npu:
self.cuda_graph_runner_for_draft_extend = (
MultiLayerEagleMultiStepDraftExtendCudaGraphRunner(self)
)
else:
self.cuda_graph_runner_for_draft_extend = (
MultiLayerEagleMultiStepDraftExtendNpuGraphRunner(self)
)
def draft(self, batch: ScheduleBatch):
draft_input: EagleDraftInput = batch.spec_info
forward_batch, can_cuda_graph = self.prepare_for_draft(
draft_input,
self.req_to_token_pool,
batch,
self.cuda_graph_runner,
self.draft_runner_list[0],
self.topk,
self.speculative_num_steps,
)
# Run draft
parent_list, top_scores_index, draft_tokens = self.draft_forward(forward_batch)
if batch.forward_mode.is_idle():
return EagleVerifyInput.create_idle_input(
self.topk,
self.speculative_num_steps,
self.speculative_num_draft_tokens,
self.device,
)
# Build tree mask
# Directly write to cuda graph buffers for verify attn
tree_mask_buf, position_buf = (
self.target_worker.model_runner.attn_backend.get_verify_buffers_to_fill_after_draft()
)
(
tree_mask,
position,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
draft_tokens,
) = build_tree_kernel_efficient(
draft_input.bonus_tokens,
parent_list,
top_scores_index,
draft_tokens,
batch.seq_lens,
batch.seq_lens_sum,
self.topk,
self.speculative_num_steps,
self.speculative_num_draft_tokens,
self.tree_mask_mode,
tree_mask_buf,
position_buf,
)
return EagleVerifyInput(
draft_token=draft_tokens,
custom_mask=tree_mask,
positions=position,
retrieve_index=retrieve_index,
retrieve_next_token=retrieve_next_token,
retrieve_next_sibling=retrieve_next_sibling,
retrieve_cum_len=None,
spec_steps=self.speculative_num_steps,
topk=self.topk,
draft_token_num=self.speculative_num_draft_tokens,
capture_hidden_mode=None,
seq_lens_sum=None,
seq_lens_cpu=None,
draft_probs=draft_input.draft_probs,
)
def draft_forward(self, forward_batch: ForwardBatch):
# Parse args
spec_info: EagleDraftInput = forward_batch.spec_info
topk_p, topk_index, hidden_states = (
spec_info.topk_p,
spec_info.topk_index,
spec_info.hidden_states,
)
maybe_detect_nan(topk_p, "draft_forward: NaN in initial topk_p from spec_info")
# Return values
score_list: List[torch.Tensor] = []
token_list: List[torch.Tensor] = []
parents_list: List[torch.Tensor] = []
# Forward multiple steps
scores = None
_, hidden_states, scores, tree_info = select_top_k_tokens(
0, topk_p, topk_index, hidden_states, scores, self.topk
)
if self.speculative_num_steps == 1:
score_list.append(tree_info[0])
token_list.append(tree_info[1])
parents_list.append(tree_info[2])
else:
for i in range(self.speculative_num_steps):
score_list.append(tree_info[0][:, :, i].unsqueeze(-1))
token_index = tree_info[1][:, i].unsqueeze(-1)
token_list.append(token_index)
if i == 0:
parents_list.append(tree_info[2])
else:
parents_list.append(
torch.full(
(tree_info[2].size(0), 1),
i,
dtype=torch.long,
device=tree_info[2].device,
)
)
# 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 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
def draft_extend(self):
pass
def _draft_extend_for_prefill(
self,
batch: ScheduleBatch,
target_hidden_states: torch.Tensor,
next_token_ids: torch.Tensor,
):
"""
Run draft model extend to correctly fill the KV cache.
Args:
batch: The batch to run.
target_hidden_states: Hidden states from the target model forward
next_token_ids: Next token ids generated from the target forward.
"""
# The draft embed clamps unconditionally (to tolerate multimodal pad
# sentinels), so probe next_token_ids here first -- otherwise a corrupted id
# would be clamped away instead of surfacing.
maybe_detect_oob(
next_token_ids,
0,
self.model_config.vocab_size,
"draft_extend_for_prefill: next_token_ids before draft embed",
)
# Draft-extend spec_info for the extend forward; carries only
# hidden_states + shape info.
extend_input = EagleDraftExtendInput(
hidden_states=target_hidden_states,
# draft mode is same with decode mode, only 1 token per req
num_tokens_per_req=1,
num_tokens_for_logprob_per_req=1,
)
batch.spec_info = extend_input
# Chain-style MTP needs FULL to get all-token hidden states;
# non-chain only needs LAST (the target model's hidden states).
# STANDALONE skips hidden states end-to-end.
if self.speculative_algorithm.is_standalone():
draft_capture_hidden_mode = CaptureHiddenMode.NULL
elif self.chain_mtp_hidden_states:
draft_capture_hidden_mode = CaptureHiddenMode.FULL
else:
draft_capture_hidden_mode = CaptureHiddenMode.LAST
# Run forward
batch.capture_hidden_mode = draft_capture_hidden_mode
batch.return_hidden_states_before_norm = True
forward_batch = ForwardBatch.init_new(batch, self.draft_runner_list[0])
# Construct input_ids
# TODO: same chunked-prefill chain divergence as PR #26329.
if not batch.forward_mode.is_idle():
rotate_input_ids(
forward_batch.input_ids,
forward_batch.extend_start_loc,
forward_batch.extend_seq_lens,
next_token_ids,
)
topk_p_list = []
topk_index_list = []
draft_probs_list = []
for step in range(self.speculative_num_steps):
output: ModelRunnerOutput = self.draft_runner_list[step].forward(
forward_batch
)
maybe_detect_nan(
output.logits_output.next_token_logits,
f"draft_extend_for_prefill step {step}",
)
maybe_detect_inf(
output.logits_output.next_token_logits,
f"draft_extend_for_prefill step {step}",
)
if self.use_rejection_sampling and self.topk == 1:
# Sample X ~ q and stash q for the first verify's Leviathan step.
probs, topk_p, topk_index = sample_draft_proposal(
output.logits_output.next_token_logits,
forward_batch.sampling_info.temperatures,
)
draft_probs_list.append(probs)
else:
probs = torch.softmax(output.logits_output.next_token_logits, dim=-1)
topk_p, topk_index = fast_topk(probs, self.topk, dim=-1)
topk_p_list.append(topk_p)
topk_index_list.append(topk_index)
# Chain-style: use this step's output hidden_states as next step's input
if (
self.chain_mtp_hidden_states
and step < self.speculative_num_steps - 1
and output.logits_output.hidden_states is not None
):
forward_batch.spec_info.hidden_states = (
output.logits_output.hidden_states
)
if forward_batch.extend_seq_lens is not None:
rotate_input_ids(
forward_batch.input_ids,
forward_batch.extend_start_loc,
forward_batch.extend_seq_lens,
topk_index,
)
next_draft_input = EagleDraftInput(
topk_p=torch.cat(topk_p_list, dim=1),
topk_index=torch.cat(topk_index_list, dim=1),
# Chain-style left the last step's hidden_states on the extend
# input; non-chain keeps the target hidden states.
hidden_states=extend_input.hidden_states,
bonus_tokens=next_token_ids,
num_tokens_per_req=1,
num_tokens_for_logprob_per_req=1,
)
# q [bs, num_steps, vocab] for the first verify's Leviathan step (RS only).
next_draft_input.draft_probs = (
torch.stack(draft_probs_list, dim=1)
if self.use_rejection_sampling and draft_probs_list
else None
)
return next_draft_input
def _draft_extend_for_decode(
self, batch: ScheduleBatch, batch_result: GenerationBatchResult
):
# Batch 2: Draft extend
draft_extend_input = EagleDraftExtendInput(
hidden_states=batch_result.logits_output.hidden_states,
num_tokens_per_req=self.speculative_num_steps + 1,
num_tokens_for_logprob_per_req=1,
)
# Prepare for draft extend in a separate stream
# Notice that here we use batch_result.next_token_ids as the input ids
with self.plan_stream_ctx:
forward_batch = self.prepare_for_draft_extend(
draft_extend_input,
batch,
batch_result.next_token_ids,
self.speculative_num_draft_tokens,
self.draft_runner_list[0],
self.cuda_graph_runner_for_draft_extend,
)
forward_batch.return_hidden_states_before_norm = True
if self.plan_stream:
torch.get_device_module(self.device).current_stream().wait_stream(
self.plan_stream
)
# `batch_result.accept_lens` includes the bonus token, so drafts-only
# is accept_lens - 1. Stash on spec_info for the cuda-graph prepare().
forward_batch.spec_info.num_correct_drafts = batch_result.accept_lens - 1
forward_batch.spec_info.num_accept_tokens = batch_result.accept_lens
# Run draft extend batch in the main compute stream
can_cuda_graph = (
self.cuda_graph_runner_for_draft_extend
and self.cuda_graph_runner_for_draft_extend.can_run_graph(forward_batch)
)
ret_topk_p_list = []
ret_topk_index_list = []
ret_draft_probs_list = []
next_token_ids_backup = batch_result.next_token_ids.clone()
if can_cuda_graph:
cgr = self.cuda_graph_runner_for_draft_extend
# Populate the single shared buffer set once; each step replays
# against it and the chain is advanced in place between steps.
cgr.prepare(forward_batch)
for step in range(self.speculative_num_steps):
_out, ret_topk_p, ret_topk_index = cgr.replay(step)
if self.use_rejection_sampling and self.topk == 1:
# Re-pick X ~ q worker-side so the chain rotation carries it
# to step N+1 (per-step graph does not sample in-graph).
sel = cgr.buffers.select_index[: cgr.raw_bs]
probs, ret_topk_p, ret_topk_index = sample_draft_proposal(
_out.next_token_logits[sel],
forward_batch.sampling_info.temperatures,
)
ret_draft_probs_list.append(probs)
ret_topk_p_list.append(ret_topk_p.clone())
ret_topk_index_list.append(ret_topk_index.clone())
# Advance the draft chain by rotating the shared input_ids window
# in place; step N+1's graph then reads the rotated values.
if step < self.speculative_num_steps - 1:
rotate_input_ids(
cgr.buffers.input_ids[: cgr.raw_num_tokens],
cgr.buffers.extend_start_loc[: cgr.raw_bs],
cgr.buffers.extend_seq_lens[: cgr.raw_bs],
ret_topk_index,
cgr.buffers.select_index[: cgr.raw_bs],
)
else:
logger.warning_once(
"can't use cuda graph for draft extend! may have correctness issue!"
)
select_index = (
torch.arange(len(batch.seq_lens), device=self.device)
* self.speculative_num_draft_tokens
+ batch_result.accept_lens
- 1
)
# NOTE: this non-graph path runs the per-step forwards without any
# pre-plan (see warning above). Mark the batch so the forward path
# keeps skipping metadata init — preserves the pre-existing
# behavior; the latent issue is tracked by the warning.
# On NPU with --disable-cuda-graph, leave each draft runner to init
# its own metadata in forward_extend (post-pad), otherwise
# per-runner attn_backend.forward_metadata is never initialized for
# draft_runner_list[1+].
if not _is_npu:
forward_batch.mark_forward_metadata_ready()
for step in range(self.speculative_num_steps):
draft_logits_output = self.draft_runner_list[step].forward(
forward_batch
)
logits_sel = draft_logits_output.logits_output.next_token_logits[
select_index
]
if self.use_rejection_sampling and self.topk == 1:
probs, ret_topk_p, ret_topk_index = sample_draft_proposal(
logits_sel, forward_batch.sampling_info.temperatures
)
ret_draft_probs_list.append(probs)
else:
probs = torch.softmax(logits_sel, dim=-1)
ret_topk_p, ret_topk_index = fast_topk(probs, self.topk, dim=-1)
# Chain-style: use this step's output hidden_states as next step's input
if (
self.chain_mtp_hidden_states
and step < self.speculative_num_steps - 1
and draft_logits_output.logits_output.hidden_states is not None
):
forward_batch.spec_info.hidden_states = (
draft_logits_output.logits_output.hidden_states
)
if forward_batch.extend_seq_lens is not None:
rotate_input_ids(
forward_batch.input_ids,
forward_batch.extend_start_loc,
forward_batch.extend_seq_lens,
ret_topk_index,
select_index,
)
ret_topk_p_list.append(ret_topk_p)
ret_topk_index_list.append(ret_topk_index)
batch_result.next_token_ids = next_token_ids_backup
# Construct the return values
next_draft_input = batch_result.next_draft_input
(
next_draft_input.topk_p,
next_draft_input.topk_index,
next_draft_input.hidden_states,
) = (
torch.cat(ret_topk_p_list, dim=1).clone(),
torch.cat(ret_topk_index_list, dim=1).clone(),
None,
)
# q [bs, num_steps, vocab] carries the per-chain-step draft distributions
# to the next verify's Leviathan step (accept iff coin*q < p). None
# otherwise (default target-only tree sampling).
next_draft_input.draft_probs = (
torch.stack(ret_draft_probs_list, dim=1) if ret_draft_probs_list else None
)
class MultiLayerEagleWorkerV2(BaseSpecWorker):
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
# Parse arguments
self.server_args = server_args
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.gpu_id = gpu_id
self.device = server_args.device
self._target_worker = target_worker
self.page_size = server_args.page_size
self.speculative_algorithm = SpeculativeAlgorithm.from_string(
server_args.speculative_algorithm
)
# Override the context length of the draft model to be the same as the target model.
server_args.override(
"spec_worker.match_target_context_length",
context_length=target_worker.model_runner.model_config.context_len,
)
self._draft_worker = MultiLayerEagleDraftWorker(
server_args,
gpu_id,
tp_rank,
dp_rank,
moe_ep_rank,
attn_cp_rank,
moe_dp_rank,
nccl_port,
target_worker,
)
# Some dummy tensors
self.num_new_pages_per_topk = torch.empty(
(), dtype=torch.int64, device=self.device
)
self.extend_lens = torch.empty((), dtype=torch.int64, device=self.device)
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
self._draft_worker.alloc_memory_pool(
memory_pool_config, req_to_token_pool, token_to_kv_pool_allocator
)
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
def init_attention_backends(self):
self._draft_worker.init_attention_backends()
def init_cuda_graphs(self):
self._draft_worker.init_cuda_graphs()
@property
def target_worker(self):
return self._target_worker
@property
def draft_worker(self):
return self._draft_worker
@property
def spec_v2_attn_backends(self) -> tuple:
return (
self._target_worker.model_runner.attn_backend,
*(
backend or runner.attn_backend
for backend, runner in zip(
self._draft_worker.draft_extend_attn_backend_list,
self._draft_worker.draft_runner_list,
)
),
)
def clear_cache_pool(self):
# allocator and kv cache pool are shared with target worker, which are cleared in scheduler
pass
def forward_batch_generation(self, batch: ScheduleBatch, on_publish=None):
if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
# Target prefill
target_capture_mode = (
CaptureHiddenMode.NULL
if self.speculative_algorithm.is_standalone()
else CaptureHiddenMode.FULL
)
batch.capture_hidden_mode = target_capture_mode
batch_output = self.target_worker.forward_batch_generation(batch)
# Spec_v2 convention: batch.seq_lens = length BEFORE this iter's tokens.
# Extend processed L prompt tokens; next verify iter expects same L.
batch_output.new_seq_lens = batch.seq_lens
# Publish before draft_extend so the fence is at target-end.
if on_publish is not None:
on_publish(batch_output.new_seq_lens)
# Chain-style MTP needs FULL to get all-token hidden states;
# non-chain only needs LAST (the target model's hidden states).
batch_output.next_draft_input = self.draft_worker._draft_extend_for_prefill(
batch,
batch_output.logits_output.hidden_states,
batch_output.next_token_ids,
)
return batch_output
else:
if batch.spec_info is None:
capture_mode = (
CaptureHiddenMode.NULL
if self.speculative_algorithm.is_standalone()
else CaptureHiddenMode.LAST
)
hidden_size, hidden_dtype = get_draft_recurrent_hidden_state_spec(
self.draft_worker.draft_runner
)
batch.spec_info = EagleDraftInput.create_idle_input(
device=self.device,
hidden_size=hidden_size,
dtype=hidden_dtype,
topk=self.topk * self.speculative_num_steps,
capture_hidden_mode=capture_mode,
)
verify_input: EagleVerifyInput = self.draft_worker.draft(batch)
assert verify_input.is_verify_input()
batch.spec_info = verify_input
batch_output = self.verify(batch)
# Publish before draft_extend so the fence is at verify-end.
if on_publish is not None:
on_publish(batch_output.new_seq_lens)
self.draft_worker._draft_extend_for_decode(batch, batch_output)
return batch_output
def verify(
self,
batch: ScheduleBatch,
):
fwd_stream = torch.get_device_module(self.device).current_stream()
verify_input: EagleVerifyInput = batch.spec_info
record_stream_for_v2_verify(batch, verify_input, fwd_stream)
bs = len(batch.seq_lens)
# Batch 1: Target verify
# Prepare for target verify in a separate stream
with self.plan_stream_ctx:
verify_forward_batch, can_run_cuda_graph = eagle_prepare_for_verify(
verify_input,
self.req_to_token_pool,
batch,
self.target_worker,
)
# Cover post-prepare rebinds: draft_token, plan_stream-allocated out_cache_loc.
record_stream_each((batch.input_ids, batch.out_cache_loc), fwd_stream)
# Correct some buffers due to the overlap plan
if self.plan_stream:
torch.get_device_module(self.device).current_stream().wait_stream(
self.plan_stream
)
# Some values such as custom_mask and position depend on the output of draft,
# so the previous plan step used the wrong values. Here, we need to run the related
# computation again to update them to the correct values.
self.target_worker.model_runner.attn_backend.update_verify_buffers_to_fill_after_draft(
verify_input,
(
self.target_worker.model_runner.decode_cuda_graph_runner.bs
if can_run_cuda_graph
else None
),
)
# NOTE: metadata init is skipped here unconditionally, although
# eagle_prepare_for_verify only plans when cuda-graph load_batch ran.
# eagle_worker_v2 re-inits the non-graph path instead (post-pad); this
# worker has not adopted that fix, so preserve its behavior verbatim.
# On NPU with --disable-cuda-graph, non-graph verify needs metadata init
# in forward_extend (post-pad); only mark ready for the cuda-graph path.
if not _is_npu or can_run_cuda_graph:
verify_forward_batch.mark_forward_metadata_ready()
# Run target verify batch in the main compute stream
forward_batch_output = self.target_worker.forward_batch_generation(
batch=None,
forward_batch=verify_forward_batch,
is_verify=True,
)
logits_output = forward_batch_output.logits_output
# Sample
maybe_detect_nan(logits_output.next_token_logits, "verify: target model logits")
maybe_detect_inf(logits_output.next_token_logits, "verify: target model logits")
(
predict,
accept_lens,
accept_index,
) = eagle_sample(verify_input, batch, logits_output)
new_seq_lens = batch.seq_lens + accept_lens
if not batch.forward_mode.is_idle():
accept_tokens = predict[accept_index]
bonus_tokens = torch.empty_like(accept_lens, dtype=torch.int32)
# stride = accept_tokens per-req width = accept_index.shape[1].
fill_bonus_tokens_func(
accept_tokens,
accept_lens,
bonus_tokens,
accept_index.shape[1],
bs,
)
else:
bonus_tokens = torch.empty((0,), device=self.device, dtype=torch.int32)
if batch.return_logprob and not batch.forward_mode.is_idle():
compute_spec_v2_logprobs(
batch, logits_output, predict, accept_index, self.speculative_num_steps
)
next_draft_input = EagleDraftInput(bonus_tokens=bonus_tokens)
# verify_forward_batch transitively holds verify-time GPU tensors that
# must outlive the imminent batch.input_ids rebind; scheduler pins it
# in batch_record_buf via extra_keep_alive_refs. See EAGLEWorkerV2.verify.
return GenerationBatchResult(
logits_output=logits_output,
next_token_ids=predict,
can_run_cuda_graph=can_run_cuda_graph,
speculative_num_draft_tokens=self.speculative_num_draft_tokens,
next_draft_input=next_draft_input,
accept_lens=accept_lens,
new_seq_lens=new_seq_lens,
routed_experts_output=forward_batch_output.routed_experts_output,
indexer_topk_output=forward_batch_output.indexer_topk_output,
extra_keep_alive_refs=[verify_forward_batch],
)
def update_weights_from_disk(self, recv_req: UpdateWeightFromDiskReqInput):
for i in range(self.speculative_num_steps):
success, message = self._draft_worker.draft_runner_list[
i
].update_weights_from_disk(
recv_req.model_path,
recv_req.load_format,
recapture_cuda_graph=recv_req.recapture_cuda_graph,
)
if not success:
return success, message
return True, "Succeeded to update model weights."
def update_weights_from_ipc(self, recv_req: UpdateWeightsFromIPCReqInput):
for i in range(self.speculative_num_steps):
success, message = self._draft_worker.draft_runner_list[
i
].update_weights_from_ipc(recv_req)
if not success:
return success, message
return True, "Succeeded to update model weights."
+145
View File
@@ -0,0 +1,145 @@
from __future__ import annotations
from typing import Optional, Tuple
import torch
from sglang.srt.constrained.base_grammar_backend import BaseGrammarObject
from sglang.srt.layers.attention.utils import create_flashinfer_kv_indices_triton
from sglang.srt.speculative.spec_info import SpecInput, SpecInputType
class NgramVerifyInput(SpecInput):
def __init__(
self,
draft_token: torch.Tensor = None,
custom_mask: torch.Tensor = None,
positions: torch.Tensor = None,
retrieve_index: torch.Tensor = None,
retrieve_next_token: torch.Tensor = None,
retrieve_next_sibling: torch.Tensor = None,
draft_token_num: int = None,
grammar: BaseGrammarObject = None,
future_indices: Optional[torch.Tensor] = None,
new_seq_lens: Optional[torch.Tensor] = None,
accept_tokens: Optional[torch.Tensor] = None,
accept_lens: Optional[torch.Tensor] = None,
):
super().__init__(SpecInputType.NGRAM_VERIFY)
self.draft_token = draft_token
self.custom_mask = custom_mask
self.positions = positions
self.retrieve_index = retrieve_index
self.retrieve_next_token = retrieve_next_token
self.retrieve_next_sibling = retrieve_next_sibling
self.draft_token_num = draft_token_num
self.grammar = grammar
# Inputs for V2 overlap worker
self.future_indices = future_indices
self.new_seq_lens = new_seq_lens
self.accept_tokens = accept_tokens
self.accept_lens = accept_lens
self.device = (
custom_mask.device if custom_mask is not None else new_seq_lens.device
)
@property
def max_tree_depth(self) -> int:
# NGRAM trees are node-budgeted with no depth cap: the corpus BFS only
# stops on the node budget, so a single long match can chain all
# draft_token_num nodes (spec_steps is meaningless for this tree).
return self.draft_token_num
@property
def tree_topk(self) -> int:
# Irregular tree: per-level branching follows the corpus matches.
return -1
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
return self.draft_token_num, self.draft_token_num
def generate_attn_arg_prefill(
self,
req_pool_indices: torch.Tensor,
paged_kernel_lens: torch.Tensor,
paged_kernel_lens_sum: int,
req_to_token: torch.Tensor,
):
bs = len(req_pool_indices)
cum_kv_seq_len = torch.zeros((bs + 1,), dtype=torch.int32, device=self.device)
paged_kernel_lens = paged_kernel_lens + self.draft_token_num
cum_kv_seq_len[1:] = torch.cumsum(paged_kernel_lens, dim=0)
self.qo_indptr = (
torch.arange(0, bs + 1, dtype=torch.int32, device=self.device)
* self.draft_token_num
)
kv_indices = torch.empty(
paged_kernel_lens_sum + self.draft_token_num * bs,
dtype=torch.int32,
device=self.device,
)
create_flashinfer_kv_indices_triton[(bs,)](
req_to_token,
req_pool_indices,
paged_kernel_lens,
cum_kv_seq_len,
None,
kv_indices,
req_to_token.size(1),
)
# Pad custom_mask when CUDA graph pads batch size beyond the actual number of requests.
mask_numel = (
paged_kernel_lens_sum * self.draft_token_num
+ (self.draft_token_num**2) * bs
)
custom_mask = self.custom_mask
if custom_mask.numel() < mask_numel:
custom_mask = torch.cat(
[
custom_mask,
torch.full(
(mask_numel - custom_mask.numel(),),
True,
dtype=torch.bool,
device=self.device,
),
],
dim=0,
)
return kv_indices, cum_kv_seq_len, self.qo_indptr, custom_mask
def filter_batch(self, new_indices: torch.Tensor, has_been_filtered: bool = True):
if self.future_indices is not None:
self.future_indices = self.future_indices[new_indices]
if self.new_seq_lens is not None:
self.new_seq_lens = self.new_seq_lens[new_indices]
self.accept_tokens = self.accept_tokens.reshape(-1, self.draft_token_num)[
new_indices, :
]
self.accept_tokens = self.accept_tokens.flatten()
self.accept_lens = self.accept_lens[new_indices]
def merge_batch(self, spec_info: NgramVerifyInput):
if self.future_indices is not None:
assert spec_info.future_indices is not None
self.future_indices = torch.cat(
(self.future_indices, spec_info.future_indices), dim=0
)
if self.new_seq_lens is not None:
assert spec_info.new_seq_lens is not None
self.new_seq_lens = torch.cat(
(self.new_seq_lens, spec_info.new_seq_lens), dim=0
)
self.accept_tokens = torch.cat(
(self.accept_tokens, spec_info.accept_tokens), dim=0
)
self.accept_lens = torch.cat((self.accept_lens, spec_info.accept_lens), dim=0)
@@ -0,0 +1,524 @@
import logging
from typing import List, Optional
import numpy as np
import torch
from sgl_kernel.speculative import reconstruct_indices_from_tree_mask
from sglang.kernels.ops.speculative.cache_locs import (
assign_extend_cache_locs_func as assign_extend_cache_locs_func,
)
from sglang.srt.layers.utils.logprob import compute_spec_v2_logprobs
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.scheduler import GenerationBatchResult
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.forward_batch_info import ForwardMode
from sglang.srt.observability.req_time_stats import set_time_batch
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker, EagleDraftWorkerBase
from sglang.srt.speculative.cpp_ngram.ngram_corpus import NgramCorpus
from sglang.srt.speculative.eagle_utils import eagle_sample
from sglang.srt.speculative.ngram_info import NgramVerifyInput
from sglang.srt.speculative.spec_utils import (
commit_mamba_states_after_verify,
generate_token_bitmask,
move_accept_tokens_to_target_kvcache,
prepare_mamba_track_for_verify,
record_stream_for_v2_verify,
)
from sglang.srt.utils import is_cpu
from sglang.srt.utils.async_probe import maybe_detect_inf, maybe_detect_nan
_is_cpu = is_cpu()
logger = logging.getLogger(__name__)
USE_FULL_MASK = True
class NGRAMWorker(BaseSpecWorker):
def alloc_memory_pool(self, **kwargs):
# The target memory pool does not exist yet when __init__ runs.
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
self._target_worker.get_memory_pool()
)
self.max_batch_size = self.model_runner.max_running_requests
self._init_preallocated_tensors()
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
self.server_args = server_args
self.enable_overlap = not server_args.disable_overlap_schedule
self._target_worker = target_worker
self.model_runner = target_worker.model_runner
self.tp_rank = tp_rank
self.page_size = server_args.page_size
self.draft_token_num: int = server_args.speculative_num_draft_tokens
self.max_trie_depth: int = server_args.speculative_ngram_max_trie_depth
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
# req_to_token_pool / token_to_kv_pool_allocator are set in
# alloc_memory_pool(), after the target pools are allocated.
self.device = server_args.device
self.adaptive_controller = None
# rids of the last decode batch; used to erase corpus match state for
# requests that left the batch (see forward_batch_generation).
self._prev_decode_rids: set = set()
self.ngram_corpus = NgramCorpus(
min_bfs_breadth=server_args.speculative_ngram_min_bfs_breadth,
max_bfs_breadth=server_args.speculative_ngram_max_bfs_breadth,
match_type=server_args.speculative_ngram_match_type,
capacity=server_args.speculative_ngram_capacity,
max_trie_depth=server_args.speculative_ngram_max_trie_depth,
draft_token_num=server_args.speculative_num_draft_tokens,
external_sam_budget=server_args.speculative_ngram_external_sam_budget,
external_corpus_max_tokens=server_args.speculative_ngram_external_corpus_max_tokens,
)
if server_args.speculative_ngram_external_corpus_path is not None:
from sglang.srt.speculative.cpp_ngram.external_corpus import (
iter_external_corpus_chunks,
)
corpus_path = server_args.speculative_ngram_external_corpus_path
chunks = list(
iter_external_corpus_chunks(
corpus_path,
target_worker.tokenizer,
server_args.speculative_ngram_external_corpus_max_tokens,
)
)
loaded = self.add_external_corpus(corpus_path, chunks)
self.commit_corpus_load(corpus_path, loaded)
logger.info(
"Loaded external ngram corpus '%s' (%d tokens).",
corpus_path,
loaded,
)
@property
def target_worker(self) -> TpModelWorker:
return self._target_worker
@property
def draft_worker(self) -> Optional[EagleDraftWorkerBase]:
# NGRAM has no draft model; drafts come from the CPU-side corpus.
return None
def clear_cache_pool(self):
self.ngram_corpus.reset()
self._prev_decode_rids = set()
def update_weights_from_tensor(self, recv_req):
# NGRAM has no draft weights of its own — the n-gram corpus is a CPU
# lookup structure built from request token streams — and its
# `model_runner` is shared with the target worker. The scheduler
# mixin dispatches via `self.draft_worker or self.tp_worker`, so
# without this method any caller of `update_weights_from_tensor`
# under `--speculative-algorithm NGRAM` raises AttributeError.
return self.target_worker.update_weights_from_tensor(recv_req)
def add_external_corpus(self, corpus_id: str, token_chunks: list[list[int]]) -> int:
return self.ngram_corpus.load_external_corpus_named(corpus_id, token_chunks)
def commit_corpus_load(self, corpus_id: str, loaded_token_count: int) -> None:
self.ngram_corpus.commit_external_corpus_load(corpus_id, loaded_token_count)
def remove_external_corpus(self, corpus_id: str) -> None:
self.ngram_corpus.remove_external_corpus(corpus_id)
def list_external_corpora(self) -> dict[str, int]:
return self.ngram_corpus.list_external_corpora()
def _efficient_concat_last_n(self, seq1: List[int], seq2: List[int], n: int):
seq2_len = len(seq2)
if seq2_len >= n:
return seq2[-n:]
need_from_seq1 = n - seq2_len
return seq1[-need_from_seq1:] + seq2
def _init_preallocated_tensors(self):
max_total_drafts = self.max_batch_size * self.draft_token_num
max_total_mask_size = (
self.max_batch_size * self.draft_token_num * self.draft_token_num
)
self.draft_tokens = torch.empty(
(max_total_drafts,), dtype=torch.int64, device=self.device
)
self.retrieve_indexes = torch.empty(
(self.max_batch_size, self.draft_token_num),
dtype=torch.int64,
device=self.device,
)
self.retrieve_next_token = torch.empty(
(self.max_batch_size, self.draft_token_num),
dtype=torch.int64,
device=self.device,
)
self.retrieve_next_sibling = torch.empty(
(self.max_batch_size, self.draft_token_num),
dtype=torch.int64,
device=self.device,
)
self.positions = torch.empty(
(max_total_drafts,), dtype=torch.int64, device=self.device
)
self.tree_mask = torch.empty(
(max_total_mask_size,), dtype=torch.bool, device=self.device
)
self.draft_tokens_batch = []
self.tree_mask_batch = []
self.retrieve_indexes_batch = []
self.retrieve_next_token_batch = []
self.retrieve_next_sibling_batch = []
self.positions_batch = []
for bs in range(0, self.max_batch_size + 1):
self.retrieve_indexes_batch.append(self.retrieve_indexes[:bs, :])
self.retrieve_next_token_batch.append(self.retrieve_next_token[:bs, :])
self.retrieve_next_sibling_batch.append(self.retrieve_next_sibling[:bs, :])
self.positions_batch.append(self.positions[: bs * self.draft_token_num])
self.draft_tokens_batch.append(
self.draft_tokens[: bs * self.draft_token_num]
)
self.tree_mask_batch.append(
self.tree_mask[: bs * self.draft_token_num * self.draft_token_num]
)
def on_verify_complete_cpu(
self, num_correct_drafts_per_req: list[int], batch_size: int = 0
) -> None:
# Signature must match BaseSpecWorker.on_verify_complete_cpu; the
# result processor calls it with batch_size as a keyword argument.
if self.adaptive_controller is not None:
self.adaptive_controller.on_verify_complete(num_correct_drafts_per_req)
def _prepare_draft_tokens(
self, batch: ScheduleBatch
) -> tuple[np.ndarray, np.ndarray]:
bs = len(batch.reqs)
stride = self.draft_token_num
prev_token_ids, prev_accept_lens = (
batch.spec_info.accept_tokens,
batch.spec_info.accept_lens,
)
if not prev_token_ids.is_cpu:
prev_token_ids = prev_token_ids.cpu()
prev_accept_lens = prev_accept_lens.cpu()
# Worker-level staging: written here at draft prep, consumed by
# _update_ngram_corpus after verify within the same forward call.
self.prev_token_ids = prev_token_ids.tolist()
self.prev_accept_lens = prev_accept_lens.tolist()
self.ngram_corpus.synchronize()
req_ids = []
batch_tokens = []
total_lens = []
assert len(batch.reqs) == len(self.prev_accept_lens)
# Overlap mode processes results one iteration behind, so the last
# round's accepted tokens are not yet in req.output_ids and must be
# spliced in from spec_info. Sync mode and grammar batches process
# results before the next draft prep, so output_ids is already
# complete and splicing would duplicate the tail.
use_prev_tokens = self.enable_overlap and not batch.has_grammar
i = 0
for req in batch.reqs:
prev_tokens = (
self.prev_token_ids[i * stride : i * stride + self.prev_accept_lens[i]]
if use_prev_tokens
else []
)
check_token = self._efficient_concat_last_n(
list(req.origin_input_ids),
list(req.output_ids[-self.max_trie_depth :]) + prev_tokens,
self.max_trie_depth,
)
req_ids.append(req.rid)
batch_tokens.append(check_token)
i += 1
total_lens.append(
len(req.origin_input_ids) + len(req.output_ids) + len(prev_tokens)
)
req_drafts, mask = self.ngram_corpus.batch_get(
req_ids, batch_tokens, total_lens
)
total_draft_token_num = len(req_drafts)
# Check if speculative decoding is needed; here we always enforce it
assert (
total_draft_token_num == bs * self.draft_token_num
), f"{total_draft_token_num=}, {bs=}, {self.draft_token_num=}"
return req_drafts, mask
def _prepare_for_speculative_decoding(self, batch: ScheduleBatch):
# Decode-only: extend goes through the plain target forward, and an
# IDLE batch must keep its forward_mode instead of being rewritten to
# TARGET_VERIFY below (relevant once DP attention support lands).
if not batch.forward_mode.is_decode():
return
bs = len(batch.reqs)
retrieve_index = self.retrieve_indexes_batch[bs]
retrieve_next_token = self.retrieve_next_token_batch[bs]
retrieve_next_sibling = self.retrieve_next_sibling_batch[bs]
positions = self.positions_batch[bs]
tree_mask = self.tree_mask_batch[bs]
draft_tokens = self.draft_tokens_batch[bs]
req_drafts, mask = self._prepare_draft_tokens(batch)
tree_mask.copy_(torch.from_numpy(mask), non_blocking=True)
draft_tokens.copy_(torch.from_numpy(req_drafts), non_blocking=True)
# generate positions and some indices using tree_mask
reconstruct_indices_from_tree_mask(
tree_mask,
batch.seq_lens,
positions, # mutable
retrieve_index, # mutable
retrieve_next_token, # mutable
retrieve_next_sibling, # mutable
bs,
self.draft_token_num,
)
# NOTE: QLEN_MASK is faster than FULL_MASK, but requires corresponding changes in flashinfer.
# Testing shows about 8% performance improvement (the effect is roughly proportional to batch size).
if USE_FULL_MASK and not _is_cpu:
tree_mask = []
mask = mask.reshape(bs, self.draft_token_num, self.draft_token_num)
# TODO(siyuan): the for loop here leads to significant overhead in large batch size. Can be written into a kernel.
for i in range(bs):
seq_len = batch.seq_lens_cpu[i]
req_mask = torch.ones(
(self.draft_token_num, seq_len), device=self.device
)
req_mask = torch.cat(
(
req_mask,
torch.from_numpy(mask[i]).to(
device=self.device, non_blocking=True
),
),
dim=1,
).to(torch.bool)
tree_mask.append(req_mask.flatten())
tree_mask = torch.cat(tree_mask, dim=0)
batch.forward_mode = ForwardMode.TARGET_VERIFY
batch.input_ids = draft_tokens
batch.out_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=batch.req_to_token_pool.req_to_token,
start_offset=batch.seq_lens,
end_offset=batch.seq_lens + self.draft_token_num,
batch_size=bs,
draft_token_num=self.draft_token_num,
device=self.device,
)
prepare_mamba_track_for_verify(batch)
batch.spec_info = NgramVerifyInput(
draft_token=draft_tokens,
custom_mask=tree_mask,
positions=positions,
retrieve_index=retrieve_index,
retrieve_next_token=retrieve_next_token,
retrieve_next_sibling=retrieve_next_sibling,
draft_token_num=self.draft_token_num,
)
def _update_ngram_corpus(self, batch: ScheduleBatch):
batch_tokens = []
i, stride = 0, self.draft_token_num
# Same splice condition as _prepare_draft_tokens: only overlap mode
# has accepted tokens missing from req.output_ids.
use_prev_tokens = self.enable_overlap and not batch.has_grammar
for req in batch.reqs:
# FIXME: Whether to insert 'extend' into the cache or not, after testing,
# there is not much difference, so we will not insert it for now.
# if batch.forward_mode.is_extend():
# put_ids = req.origin_input_ids + req.output_ids
# else:
prev_tokens = (
self.prev_token_ids[i * stride : i * stride + self.prev_accept_lens[i]]
if use_prev_tokens
else []
)
put_ids = self._efficient_concat_last_n(
list(req.origin_input_ids),
list(req.output_ids[-self.max_trie_depth :]) + prev_tokens,
self.max_trie_depth,
)
batch_tokens.append(put_ids)
i += 1
self.ngram_corpus.batch_put(batch_tokens)
def forward_batch_generation(
self, batch: ScheduleBatch, on_publish=None
) -> GenerationBatchResult:
fwd_stream = torch.get_device_module(self.device).current_stream()
record_stream_for_v2_verify(batch, None, fwd_stream)
bs = len(batch.reqs)
set_time_batch(batch.reqs, "set_spec_draft_start_time", trace_only=True)
self._prepare_for_speculative_decoding(batch)
set_time_batch(batch.reqs, "set_spec_draft_end_time", trace_only=True)
verify_input: NgramVerifyInput = batch.spec_info
accept_lens = torch.ones(bs, dtype=torch.int32, device=self.device)
if batch.forward_mode.is_target_verify():
# Prepare grammar data on CPU if needed
if batch.has_grammar:
retrieve_next_token_cpu = verify_input.retrieve_next_token.cpu()
retrieve_next_sibling_cpu = verify_input.retrieve_next_sibling.cpu()
draft_tokens_cpu = verify_input.draft_token.view(
verify_input.retrieve_next_token.shape
).cpu()
batch_result = self.target_worker.forward_batch_generation(
batch, is_verify=True
)
logits_output, can_run_cuda_graph = (
batch_result.logits_output,
batch_result.can_run_cuda_graph,
)
verify_input: NgramVerifyInput = batch.spec_info
vocab_mask = None
if batch.has_grammar:
# Generate the logit mask for structured output.
# Overlap the CPU operations for bitmask generation with the forward pass.
vocab_mask = generate_token_bitmask(
batch.reqs,
verify_input,
retrieve_next_token_cpu,
retrieve_next_sibling_cpu,
draft_tokens_cpu,
batch.sampling_info.vocab_size,
)
if vocab_mask is not None:
assert verify_input.grammar is not None
vocab_mask = vocab_mask.to(verify_input.retrieve_next_token.device)
# NOTE (sk): otherwise, this vocab mask will be the one from the previous extend stage
# and will be applied to produce wrong results
batch.sampling_info.vocab_mask = None
# Sample
maybe_detect_nan(
logits_output.next_token_logits, "verify: target model logits"
)
maybe_detect_inf(
logits_output.next_token_logits, "verify: target model logits"
)
(
predict,
accept_lens,
accept_index,
) = eagle_sample(verify_input, batch, logits_output, vocab_mask)
new_seq_lens = batch.seq_lens + accept_lens
commit_mamba_states_after_verify(
self.target_worker,
batch,
accept_lens,
accept_index,
self.draft_token_num,
)
accept_tokens = predict[accept_index].flatten()
next_token_ids = accept_tokens
# The KV mover expects drafts-only counts. NGRAM's
# accept_lens includes the bonus token, matching scheduler output.
num_correct_drafts_per_req = accept_lens - 1
move_accept_tokens_to_target_kvcache(
batch,
accept_index,
num_correct_drafts_per_req,
self.token_to_kv_pool_allocator,
)
if batch.return_logprob:
# The last arg is the accept_index row width minus 1. NGRAM's
# accept_index is (bs, draft_token_num) -- the tree depth is not
# bounded by spec_steps like EAGLE's (bs, spec_steps + 1).
compute_spec_v2_logprobs(
batch,
logits_output,
predict,
accept_index,
self.draft_token_num - 1,
)
if on_publish is not None:
on_publish(new_seq_lens)
self._update_ngram_corpus(batch)
# Erase match state of requests that left the decode batch.
# req.finished() is unusable here: under overlap it flips at result
# processing, one iteration after the request left the batch.
# The last batch's entries persist while idle (bounded, small).
cur_rids = {req.rid for req in batch.reqs}
departed_rids = self._prev_decode_rids - cur_rids
if departed_rids:
self.ngram_corpus.erase_match_state(list(departed_rids))
self._prev_decode_rids = cur_rids
batch.forward_mode = ForwardMode.DECODE
else:
batch_result = self.target_worker.forward_batch_generation(batch)
logits_output, predict, can_run_cuda_graph = (
batch_result.logits_output,
batch_result.next_token_ids,
batch_result.can_run_cuda_graph,
)
new_seq_lens = batch.seq_lens.clone()
accept_tokens = torch.zeros(
bs, self.draft_token_num, dtype=torch.int32, device=self.device
)
accept_tokens[:, 0] = predict
accept_tokens = accept_tokens.flatten()
next_token_ids = predict
if on_publish is not None:
on_publish(new_seq_lens)
# Construct the next draft input
next_draft_input = NgramVerifyInput(
draft_token_num=self.draft_token_num,
new_seq_lens=new_seq_lens,
accept_tokens=accept_tokens,
accept_lens=accept_lens,
)
return GenerationBatchResult(
logits_output=logits_output,
next_token_ids=next_token_ids,
can_run_cuda_graph=can_run_cuda_graph,
accept_lens=accept_lens,
# Consumed by the non-overlap V2 scheduler branch to advance
# batch.seq_lens after the isolation restore; overlap mode relays
# it via on_publish instead.
new_seq_lens=new_seq_lens,
next_draft_input=next_draft_input,
speculative_num_draft_tokens=self.speculative_num_draft_tokens,
)
@@ -0,0 +1,304 @@
from __future__ import annotations
import bisect
from enum import Enum
from typing import List, Optional, Sequence, Tuple
import msgspec
import torch
from sglang.srt.environ import envs
class RaggedVerifyMode(str, Enum):
STATIC = "static"
CAP_ACCEPT = "cap-accept"
COMPACT = "compact"
def read_ragged_verify_mode() -> RaggedVerifyMode:
value = envs.SGLANG_RAGGED_VERIFY_MODE.get()
for mode in RaggedVerifyMode:
if value == mode.value:
return mode
raise ValueError(
f"invalid SGLANG_RAGGED_VERIFY_MODE={value!r}; expected one of "
f"{', '.join(repr(m.value) for m in RaggedVerifyMode)}"
)
def ragged_verify_compact_enabled() -> bool:
return read_ragged_verify_mode() == RaggedVerifyMode.COMPACT
def round_up_grid(total: int, grid: Sequence[int]) -> int:
if not grid:
raise ValueError("round_up_grid requires a non-empty grid")
if total > grid[-1]:
raise ValueError(
f"total {total} exceeds max grid tier {grid[-1]}; "
"the caller must reject this batch before selecting a graph tier"
)
index = bisect.bisect_left(grid, total)
return grid[index]
class RaggedVerifyLayout(msgspec.Struct, frozen=True):
verify_lens: torch.Tensor
graph_num_tokens: int
extend_start_loc: torch.Tensor
qo_indptr_device: torch.Tensor
verify_lens_cpu: Optional[list[int]] = None
total_verify_tokens: Optional[int] = None
qo_indptr_host: Optional[torch.Tensor] = None
kv_indptr_host: Optional[torch.Tensor] = None
kv_lens_host: Optional[torch.Tensor] = None
max_q_len: Optional[int] = None
max_kv_len: Optional[int] = None
def __post_init__(self) -> None:
if self.verify_lens_cpu is None:
return
if not self.verify_lens_cpu:
raise ValueError("RaggedVerifyLayout requires at least one request")
if min(self.verify_lens_cpu) < 1:
raise ValueError(
f"every request must verify the anchor (verify_len >= 1), got "
f"{self.verify_lens_cpu}"
)
if self.total_verify_tokens != sum(self.verify_lens_cpu):
raise ValueError(
f"total_verify_tokens {self.total_verify_tokens} != "
f"sum(verify_lens_cpu) {sum(self.verify_lens_cpu)}"
)
if not (self.total_verify_tokens <= self.graph_num_tokens):
raise ValueError(
f"total_verify_tokens {self.total_verify_tokens} exceeds "
f"graph_num_tokens {self.graph_num_tokens}"
)
@property
def bs(self) -> int:
return int(self.verify_lens.shape[0])
@classmethod
def _assemble_device(
cls,
*,
verify_lens: torch.Tensor,
graph_num_tokens: int,
verify_lens_cpu: Optional[list[int]] = None,
total_verify_tokens: Optional[int] = None,
) -> RaggedVerifyLayout:
from sglang.srt.speculative.ragged_verify_kernels import (
BuildQoIndptr,
)
verify_lens = verify_lens.to(torch.int32)
indptr = BuildQoIndptr.execute(verify_lens=verify_lens)
return cls(
verify_lens=verify_lens,
graph_num_tokens=graph_num_tokens,
extend_start_loc=indptr.extend_start_loc,
qo_indptr_device=indptr.qo_indptr,
verify_lens_cpu=verify_lens_cpu,
total_verify_tokens=total_verify_tokens,
)
@classmethod
def _assemble(
cls,
*,
verify_lens_cpu: list[int],
total_verify_tokens: int,
graph_num_tokens: int,
device: torch.device,
) -> RaggedVerifyLayout:
verify_lens = torch.tensor(verify_lens_cpu, dtype=torch.int32, device=device)
return cls._assemble_device(
verify_lens=verify_lens,
graph_num_tokens=graph_num_tokens,
verify_lens_cpu=verify_lens_cpu,
total_verify_tokens=total_verify_tokens,
)
@classmethod
def from_verify_lens_device(
cls,
*,
verify_lens: torch.Tensor,
graph_num_tokens: int,
) -> RaggedVerifyLayout:
return cls._assemble_device(
verify_lens=verify_lens, graph_num_tokens=graph_num_tokens
)
@classmethod
def from_verify_lens(
cls,
*,
verify_lens_cpu: Sequence[int],
device: torch.device,
grid: Sequence[int],
graph_num_tokens_floor: int = 0,
) -> RaggedVerifyLayout:
verify_lens_list = [int(v) for v in verify_lens_cpu]
total_verify_tokens = sum(verify_lens_list)
bucket_input = max(total_verify_tokens, graph_num_tokens_floor)
graph_num_tokens = round_up_grid(total=bucket_input, grid=grid)
return cls._assemble(
verify_lens_cpu=verify_lens_list,
total_verify_tokens=total_verify_tokens,
graph_num_tokens=graph_num_tokens,
device=device,
)
def padded_to_bucket(self, *, padded_bs: int) -> RaggedVerifyLayout:
from sglang.srt.speculative.ragged_verify_kernels import (
PaddedToBucket,
)
padded = PaddedToBucket.execute(
verify_lens=self.verify_lens,
graph_num_tokens=self.graph_num_tokens,
bs=self.bs,
padded_bs=padded_bs,
)
return RaggedVerifyLayout._assemble_device(
verify_lens=padded,
graph_num_tokens=self.graph_num_tokens,
total_verify_tokens=self.graph_num_tokens,
)
def build_capture_verify_lens(
*,
num_tokens: int,
num_slots: int,
num_draft_tokens: int,
) -> list[int]:
if num_slots < 1 or num_tokens < num_slots:
raise ValueError(
f"capture layout needs 1 <= num_slots <= num_tokens, got "
f"num_slots={num_slots}, num_tokens={num_tokens}"
)
if num_tokens > num_slots * num_draft_tokens:
raise ValueError(
f"capture layout cannot pack num_tokens={num_tokens} into "
f"{num_slots} rows of at most {num_draft_tokens} tokens"
)
base = num_tokens // num_slots
rem = num_tokens - base * num_slots
return [base + 1] * rem + [base] * (num_slots - rem)
def resolve_ragged_verify_layout(forward_batch) -> Optional[RaggedVerifyLayout]:
"""Layout riding the batch's spec input, or None. Tolerates the runner's
ad-hoc replay batch views, which may not carry spec_info at all."""
spec_info = getattr(forward_batch, "spec_info", None)
if spec_info is None:
return None
return spec_info.ragged_verify_layout
class RaggedTargetVerifyGeometry(msgspec.Struct):
cache_seqlens_int32: torch.Tensor
cu_seqlens_q: torch.Tensor
cu_seqlens_k: torch.Tensor
max_seq_len_q: Optional[int]
def build_ragged_target_verify_geometry(
*,
seq_lens: torch.Tensor,
layout: RaggedVerifyLayout,
) -> RaggedTargetVerifyGeometry:
cache_seqlens_int32 = (seq_lens + layout.verify_lens).to(torch.int32)
cu_seqlens_q = layout.qo_indptr_device.to(torch.int32)
cu_seqlens_k = torch.nn.functional.pad(
torch.cumsum(cache_seqlens_int32, dim=0, dtype=torch.int32), (1, 0)
)
max_seq_len_q = (
max(layout.verify_lens_cpu) if layout.verify_lens_cpu is not None else None
)
return RaggedTargetVerifyGeometry(
cache_seqlens_int32=cache_seqlens_int32,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seq_len_q=max_seq_len_q,
)
def compute_target_verify_graph_key(
*,
bs: int,
num_draft_tokens: int,
ragged_layout: Optional[RaggedVerifyLayout],
) -> Tuple[int, int]:
num_tokens_full_block = num_draft_tokens * bs
if ragged_layout is None:
return bs, num_tokens_full_block
graph_num_tokens = ragged_layout.graph_num_tokens
assert graph_num_tokens <= num_tokens_full_block, (
f"ragged verify graph_num_tokens={graph_num_tokens} exceeds full block "
f"num_draft*bs={num_tokens_full_block}"
)
total_verify_tokens = ragged_layout.total_verify_tokens
if total_verify_tokens is not None:
assert total_verify_tokens <= graph_num_tokens, (
f"ragged verify total_verify_tokens={total_verify_tokens} exceeds the "
f"round-up bucket graph_num_tokens={graph_num_tokens}"
)
return graph_num_tokens, graph_num_tokens
class VerifyExtendLengths(msgspec.Struct, frozen=True):
seq_lens_extended: torch.Tensor
seq_lens_cpu_extended: List[int]
extend_seq_lens_cpu: List[int]
num_tokens: int
extend_start_loc: Optional[torch.Tensor]
def compute_uniform_extend_lengths(
*,
seq_lens: torch.Tensor,
seq_lens_cpu: List[int],
extend_len: int,
) -> VerifyExtendLengths:
batch_size = len(seq_lens_cpu)
seq_lens_extended = seq_lens + extend_len
seq_lens_cpu_extended = [x + extend_len for x in seq_lens_cpu]
extend_seq_lens_cpu = [extend_len] * batch_size
num_tokens = extend_len * batch_size
return VerifyExtendLengths(
seq_lens_extended=seq_lens_extended,
seq_lens_cpu_extended=seq_lens_cpu_extended,
extend_seq_lens_cpu=extend_seq_lens_cpu,
num_tokens=num_tokens,
extend_start_loc=None,
)
def compute_ragged_extend_lengths(
*,
seq_lens: torch.Tensor,
seq_lens_cpu: List[int],
ragged_layout: RaggedVerifyLayout,
) -> VerifyExtendLengths:
extend_seq_lens_cpu = list(ragged_layout.verify_lens_cpu)
seq_lens_extended = seq_lens + ragged_layout.verify_lens
seq_lens_cpu_extended = [
raw + length for raw, length in zip(seq_lens_cpu, extend_seq_lens_cpu)
]
num_tokens = ragged_layout.total_verify_tokens
extend_start_loc = ragged_layout.extend_start_loc
return VerifyExtendLengths(
seq_lens_extended=seq_lens_extended,
seq_lens_cpu_extended=seq_lens_cpu_extended,
extend_seq_lens_cpu=extend_seq_lens_cpu,
num_tokens=num_tokens,
extend_start_loc=extend_start_loc,
)
@@ -0,0 +1,199 @@
from __future__ import annotations
import msgspec
import torch
import triton
import triton.language as tl
class PaddedToBucket:
@classmethod
def execute(
cls,
*,
verify_lens: torch.Tensor,
graph_num_tokens: int,
bs: int,
padded_bs: int,
) -> torch.Tensor:
impl = cls.triton if verify_lens.is_cuda else cls.torch
return impl(
verify_lens=verify_lens,
graph_num_tokens=graph_num_tokens,
bs=bs,
padded_bs=padded_bs,
)
@classmethod
def torch(
cls,
*,
verify_lens: torch.Tensor,
graph_num_tokens: int,
bs: int,
padded_bs: int,
) -> torch.Tensor:
return pad_verify_lens_to_bucket(
verify_lens=verify_lens,
graph_num_tokens=graph_num_tokens,
bs=bs,
padded_bs=padded_bs,
)
@classmethod
def triton(
cls,
*,
verify_lens: torch.Tensor,
graph_num_tokens: int,
bs: int,
padded_bs: int,
) -> torch.Tensor:
return pad_verify_lens_to_bucket_triton(
verify_lens=verify_lens,
graph_num_tokens=graph_num_tokens,
bs=bs,
padded_bs=padded_bs,
)
def pad_verify_lens_to_bucket(
*,
verify_lens: torch.Tensor,
graph_num_tokens: int,
bs: int,
padded_bs: int,
) -> torch.Tensor:
assert padded_bs >= bs, (
f"padded_bs {padded_bs} < bs {bs}: the captured tier cannot hold this "
"batch's requests"
)
device = verify_lens.device
num_pad_reqs = padded_bs - bs
padded = verify_lens.to(torch.int32)
leftover = graph_num_tokens - padded.to(torch.int64).sum()
if num_pad_reqs > 0:
base = leftover // num_pad_reqs
rem = leftover - base * num_pad_reqs
pad_block = base + (
torch.arange(num_pad_reqs, device=device, dtype=torch.int64) < rem
)
padded = torch.cat([padded, pad_block.to(torch.int32)])
else:
padded = padded.clone()
padded[-1] = (padded[-1].to(torch.int64) + leftover).to(torch.int32)
return padded
@triton.jit
def _padded_to_bucket_kernel(
verify_lens_ptr,
out_ptr,
bs,
padded_bs,
graph_num_tokens,
BLOCK: tl.constexpr,
):
idx = tl.arange(0, BLOCK)
valid = idx < padded_bs
is_real = idx < bs
vl = tl.load(verify_lens_ptr + idx, mask=is_real, other=0).to(tl.int64)
leftover = graph_num_tokens - tl.sum(vl)
num_pad = padded_bs - bs
num_pad_safe = tl.maximum(num_pad, 1)
base = leftover // num_pad_safe
rem = leftover - base * num_pad_safe
pad_len = base + tl.where((idx - bs) < rem, 1, 0)
final = tl.where(is_real, vl, pad_len)
final = final + tl.where((num_pad == 0) & (idx == bs - 1), leftover, 0)
tl.store(out_ptr + idx, final.to(tl.int32), mask=valid)
def pad_verify_lens_to_bucket_triton(
*,
verify_lens: torch.Tensor,
graph_num_tokens: int,
bs: int,
padded_bs: int,
) -> torch.Tensor:
assert padded_bs >= bs, (
f"padded_bs {padded_bs} < bs {bs}: the captured tier cannot hold this "
"batch's requests"
)
device = verify_lens.device
verify_lens = verify_lens.to(torch.int32).contiguous()
out = torch.empty(padded_bs, dtype=torch.int32, device=device)
BLOCK = triton.next_power_of_2(max(padded_bs, 1))
_padded_to_bucket_kernel[(1,)](
verify_lens,
out,
bs,
padded_bs,
graph_num_tokens,
BLOCK=BLOCK,
)
return out
class QoIndptrResult(msgspec.Struct):
qo_indptr: torch.Tensor
extend_start_loc: torch.Tensor
class BuildQoIndptr:
@classmethod
def execute(cls, *, verify_lens: torch.Tensor) -> QoIndptrResult:
impl = cls.triton if verify_lens.is_cuda else cls.torch
return impl(verify_lens=verify_lens)
@classmethod
def torch(cls, *, verify_lens: torch.Tensor) -> QoIndptrResult:
return build_qo_indptr(verify_lens=verify_lens)
@classmethod
def triton(cls, *, verify_lens: torch.Tensor) -> QoIndptrResult:
return build_qo_indptr_triton(verify_lens=verify_lens)
def build_qo_indptr(*, verify_lens: torch.Tensor) -> QoIndptrResult:
verify_lens = verify_lens.to(torch.int32)
cumsum = torch.cumsum(verify_lens, dim=0).to(torch.int32)
zero = torch.zeros(1, dtype=torch.int32, device=verify_lens.device)
qo_indptr = torch.cat([zero, cumsum])
extend_start_loc = qo_indptr[:-1].clone()
return QoIndptrResult(qo_indptr=qo_indptr, extend_start_loc=extend_start_loc)
@triton.jit
def _qo_indptr_kernel(
verify_lens_ptr,
qo_indptr_ptr,
extend_start_loc_ptr,
bs,
BLOCK: tl.constexpr,
):
idx = tl.arange(0, BLOCK)
valid = idx < bs
vl = tl.load(verify_lens_ptr + idx, mask=valid, other=0).to(tl.int32)
incl = tl.cumsum(vl, axis=0)
excl = incl - vl
tl.store(qo_indptr_ptr, 0)
tl.store(qo_indptr_ptr + 1 + idx, incl, mask=valid)
tl.store(extend_start_loc_ptr + idx, excl, mask=valid)
def build_qo_indptr_triton(*, verify_lens: torch.Tensor) -> QoIndptrResult:
bs = verify_lens.shape[0]
device = verify_lens.device
verify_lens = verify_lens.contiguous()
qo_indptr = torch.empty(bs + 1, dtype=torch.int32, device=device)
extend_start_loc = torch.empty(bs, dtype=torch.int32, device=device)
BLOCK = triton.next_power_of_2(max(bs, 1))
_qo_indptr_kernel[(1,)](
verify_lens,
qo_indptr,
extend_start_loc,
bs,
BLOCK=BLOCK,
)
return QoIndptrResult(qo_indptr=qo_indptr, extend_start_loc=extend_start_loc)
@@ -0,0 +1,204 @@
import triton
import triton.language as tl
@triton.jit
def speculative_sampling_classic_kernel(
# Pointers
Predicts,
AcceptIndex,
AcceptTokenNum,
Candidates,
RetriveIndex,
UniformSamples,
UniformSamplesFinal,
TargetProbs,
DraftProbs,
# Strides
stride_cand_b,
stride_cand_s,
stride_idx_b,
stride_idx_s,
stride_uni_b,
stride_uni_s,
stride_tp_b,
stride_tp_s,
stride_tp_v,
stride_dp_b,
stride_dp_s,
stride_dp_v,
# Constants
NUM_SLOTS: tl.constexpr,
VOCAB_SIZE: tl.constexpr,
BLOCK_V: tl.constexpr,
):
pid = tl.program_id(0)
cur_prob_row = 0
cand_ptr_base = Candidates + pid * stride_cand_b
idx_ptr_base = RetriveIndex + pid * stride_idx_b
uni_ptr_base = UniformSamples + pid * stride_uni_b
root_global_idx = tl.load(idx_ptr_base + 0 * stride_idx_s)
tl.store(AcceptIndex + pid * stride_idx_b + 0 * stride_idx_s, root_global_idx)
last_accepted_global_idx = root_global_idx
num_accept = 0
# Verification Loop
step = 1
continue_verifying = 1
while (step < NUM_SLOTS) and (continue_verifying == 1):
draft_token = tl.load(cand_ptr_base + step * stride_cand_s)
offset_prob = (
(pid * stride_tp_b)
+ (cur_prob_row * stride_tp_s)
+ (draft_token * stride_tp_v)
)
offset_draft = (
(pid * stride_dp_b)
+ (cur_prob_row * stride_dp_s)
+ (draft_token * stride_dp_v)
)
p = tl.load(TargetProbs + offset_prob)
q = tl.load(DraftProbs + offset_draft)
coin = tl.load(uni_ptr_base + (step - 1) * stride_uni_s)
if coin * q < p:
num_accept += 1
cur_prob_row = step
tl.store(Predicts + last_accepted_global_idx, draft_token)
curr_global_idx = tl.load(idx_ptr_base + step * stride_idx_s)
tl.store(
AcceptIndex + pid * stride_idx_b + num_accept * stride_idx_s,
curr_global_idx,
)
last_accepted_global_idx = curr_global_idx
step += 1
else:
continue_verifying = 0
tl.store(AcceptTokenNum + pid, num_accept)
# Final Sampling
all_drafts_accepted = continue_verifying
coin_final = tl.load(UniformSamplesFinal + pid)
norm_sum = 0.0
tp_base_ptr = TargetProbs + (pid * stride_tp_b) + (cur_prob_row * stride_tp_s)
# DraftProbs has only num_steps rows (TargetProbs has num_steps + 1). When
# all drafts are accepted cur_prob_row == num_steps is out of bounds for
# DraftProbs, but the all-accepted branch samples pure target p and never
# dereferences this pointer; on rejection cur_prob_row <= num_steps - 1.
dp_base_ptr_safe = DraftProbs + (pid * stride_dp_b) + (cur_prob_row * stride_dp_s)
# Pass 1: Sum
for v_start in range(0, VOCAB_SIZE, BLOCK_V):
v_offsets = v_start + tl.arange(0, BLOCK_V)
mask = v_offsets < VOCAB_SIZE
p_ptr = tp_base_ptr + v_offsets * stride_tp_v
p_val = tl.load(p_ptr, mask=mask, other=0.0)
if all_drafts_accepted:
val = p_val
else:
q_ptr = dp_base_ptr_safe + v_offsets * stride_dp_v
q_val = tl.load(q_ptr, mask=mask, other=0.0)
diff = p_val - q_val
val = tl.where(diff > 0.0, diff, 0.0)
norm_sum += tl.sum(val)
# Pass 2: CDF. Degenerate residual (norm_sum == 0, i.e. p == q everywhere on
# rejection) leaves the cumsum at 0 <= target_u, so final_token falls back to
# VOCAB_SIZE - 1; acceptable since this case is numerically near-impossible.
target_u = coin_final * norm_sum
cum_sum = 0.0
final_token = VOCAB_SIZE - 1
found = 0
for v_start in range(0, VOCAB_SIZE, BLOCK_V):
if found == 0:
v_offsets = v_start + tl.arange(0, BLOCK_V)
mask = v_offsets < VOCAB_SIZE
p_ptr = tp_base_ptr + v_offsets * stride_tp_v
p_val = tl.load(p_ptr, mask=mask, other=0.0)
if all_drafts_accepted:
val = p_val
else:
q_ptr = dp_base_ptr_safe + v_offsets * stride_dp_v
q_val = tl.load(q_ptr, mask=mask, other=0.0)
diff = p_val - q_val
val = tl.where(diff > 0.0, diff, 0.0)
block_cumsum = tl.cumsum(val, axis=0)
total_cumsum = cum_sum + block_cumsum
candidates_mask = total_cumsum > target_u
has_match = tl.max(candidates_mask, axis=0)
if has_match:
match_idx = tl.argmax(candidates_mask.to(tl.int32), axis=0)
final_token = v_start + match_idx
found = 1
cum_sum += tl.sum(val)
tl.store(Predicts + last_accepted_global_idx, final_token)
def chain_speculative_sampling_triton(
predicts,
accept_index,
accept_token_num,
candidates,
retrive_index,
retrive_next_token,
retrive_next_sibling, # not used in chain verification
uniform_samples,
uniform_samples_for_final_sampling,
target_probs,
draft_probs,
threshold_single,
threshold_acc,
deterministic, # not used
):
batch_size, num_slots = candidates.shape
vocab_size = target_probs.shape[-1]
grid = (batch_size,)
speculative_sampling_classic_kernel[grid](
predicts,
accept_index,
accept_token_num,
candidates,
retrive_index,
uniform_samples,
uniform_samples_for_final_sampling,
target_probs,
draft_probs,
candidates.stride(0),
candidates.stride(1),
retrive_index.stride(0),
retrive_index.stride(1),
uniform_samples.stride(0),
uniform_samples.stride(1),
target_probs.stride(0),
target_probs.stride(1),
target_probs.stride(2),
draft_probs.stride(0),
draft_probs.stride(1),
draft_probs.stride(2),
NUM_SLOTS=num_slots,
VOCAB_SIZE=vocab_size,
BLOCK_V=4096,
)
+402
View File
@@ -0,0 +1,402 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum, IntEnum, auto
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Type, Union
import torch
from sglang.srt.speculative.spec_registry import (
CustomSpecAlgo,
ServerArgsValidator,
WorkerFactory,
)
from sglang.srt.speculative.spec_registry import get_spec as _get_registered_spec
from sglang.srt.speculative.spec_registry import (
register_algorithm as _register_algorithm,
)
if TYPE_CHECKING:
from sglang.srt.managers.overlap_utils import FutureMap
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker
from sglang.srt.speculative.ngram_worker import NGRAMWorker
from sglang.srt.speculative.ragged_verify import RaggedVerifyLayout
class SpeculativeAlgorithm(Enum):
"""Builtin speculative decoding algorithms. Plugin-registered ones are
``CustomSpecAlgo`` instances; ``from_string`` returns either type, and
both expose the same ``is_*()`` / ``create_worker`` interface so callers
dispatch uniformly without isinstance checks.
"""
DFLASH = auto()
DSPARK = auto()
EAGLE = auto()
EAGLE3 = auto()
FROZEN_KV_MTP = auto()
STANDALONE = auto()
NGRAM = auto()
NONE = auto()
@classmethod
def from_string(
cls, name: Optional[str]
) -> Union[SpeculativeAlgorithm, CustomSpecAlgo]:
if name is None:
return cls.NONE
upper = name.upper()
try:
return cls[upper]
except KeyError:
pass
spec = _get_registered_spec(upper)
if spec is not None:
return spec
raise ValueError(f"Unknown speculative algorithm name: {name}")
@classmethod
def register(
cls,
name: str,
*,
supports_overlap: bool = False,
validate_server_args: Optional[ServerArgsValidator] = None,
spec_class: Type[CustomSpecAlgo] = CustomSpecAlgo,
) -> Callable[[WorkerFactory], WorkerFactory]:
"""Decorator to register a plugin speculative algorithm. The factory
takes ``server_args`` and returns the worker class. Pass a
``CustomSpecAlgo`` subclass via ``spec_class`` to override any
``is_*()`` / ``create_worker`` method.
Example:
@SpeculativeAlgorithm.register("MY_SPEC", supports_overlap=False)
def _factory(server_args):
return MySpecWorker
"""
return _register_algorithm(
name,
supports_overlap=supports_overlap,
validate_server_args=validate_server_args,
spec_class=spec_class,
)
def is_some(self) -> bool:
return self != SpeculativeAlgorithm.NONE
def is_none(self) -> bool:
return self == SpeculativeAlgorithm.NONE
def is_speculative(self) -> bool:
return self != SpeculativeAlgorithm.NONE
def is_eagle(self) -> bool:
# FIXME(kpham_sgl): Remove FROZEN_KV_MTP here once we
# have established support for it in the scheduler.
return self in (
SpeculativeAlgorithm.EAGLE,
SpeculativeAlgorithm.EAGLE3,
SpeculativeAlgorithm.FROZEN_KV_MTP,
)
def is_eagle3(self) -> bool:
return self == SpeculativeAlgorithm.EAGLE3
def is_frozen_kv_mtp(self) -> bool:
return self == SpeculativeAlgorithm.FROZEN_KV_MTP
def is_dflash(self) -> bool:
return self == SpeculativeAlgorithm.DFLASH
def is_dspark(self) -> bool:
return self == SpeculativeAlgorithm.DSPARK
def is_dflash_family(self) -> bool:
return self.is_dflash() or self.is_dspark()
def is_standalone(self) -> bool:
return self == SpeculativeAlgorithm.STANDALONE
def is_ngram(self) -> bool:
return self == SpeculativeAlgorithm.NGRAM
def supports_target_verify_for_draft(self) -> bool:
return self.is_dflash_family()
def supports_ragged_verify(self) -> bool:
"""Whether this algorithm's verify step may carry a RaggedVerifyLayout
(per-request verify lengths); gates the token-bucket-keyed verify
graphs in the decode cuda graph runner."""
return self.is_dspark()
def has_draft_kv(self) -> bool:
"""Whether the draft phase writes KV chains. NGRAM does not (its tree
lives only in the verify mask), so per-decode KV sizing needs no
per-topk page rounding; see get_alloc_len_per_decode."""
return not self.is_ngram()
def carries_draft_hidden_states(self) -> bool:
"""Whether the disagg prefill->decode transfer carries draft hidden
states (EAGLE-family only; STANDALONE's vanilla draft ignores them)."""
return self.is_eagle()
def create_future_map(
self,
device: torch.device,
req_to_token_pool,
needs_cpu_seq_lens: bool = True,
needs_confidence_relay: bool = False,
) -> FutureMap:
from sglang.srt.managers.overlap_utils import FutureMap
return FutureMap(
device,
self,
req_to_token_pool,
needs_cpu_seq_lens,
needs_confidence_relay,
)
def build_disagg_draft_input(
self,
batch: ScheduleBatch,
server_args: ServerArgs,
last_tokens_tensor: torch.Tensor,
future_map: FutureMap,
) -> Optional[SpecInput]:
if self.is_eagle():
from sglang.srt.speculative.eagle_disaggregation import (
build_eagle_disagg_draft_input,
)
return build_eagle_disagg_draft_input(
batch, server_args, last_tokens_tensor, future_map
)
return None
def need_topk(self) -> bool:
return self.is_eagle() or self.is_standalone()
def handle_server_args(self, server_args: ServerArgs) -> None:
"""Hook for per-algorithm server args mutation.
In-place updated.
"""
from sglang.srt.arg_groups.speculative_hook import (
_handle_dflash,
_handle_dspark,
_handle_eagle_family,
_handle_frozen_kv_mtp,
_handle_ngram,
)
# Validate for every algorithm at startup: the metrics paths read the
# ragged-verify mode env and must not be where a typo'd value raises.
from sglang.srt.speculative.ragged_verify import read_ragged_verify_mode
read_ragged_verify_mode()
if self.is_dflash():
_handle_dflash(server_args)
elif self.is_dspark():
_handle_dspark(server_args)
elif self.is_frozen_kv_mtp():
_handle_frozen_kv_mtp(server_args)
elif self.is_eagle() or self.is_standalone():
_handle_eagle_family(server_args)
elif self.is_ngram():
_handle_ngram(server_args)
def get_num_tokens_per_bs_for_target_verify(
self, num_draft_tokens: int, is_draft_worker: bool
) -> int:
# FIXME: Remove this after the forward mode refactor. Target verify is
# essentially a fixed sequence length prefill/extend with full cuda
# graph support. We can use it for target verify, or we can use it for
# other cases which is not target verify but fixed length prefill.
# Here, we expose this interface to allow the other use cases.
if self.is_dspark() and is_draft_worker:
return num_draft_tokens - 1
return num_draft_tokens
def create_worker(
self, server_args: ServerArgs
) -> Optional[Union[Type[BaseSpecWorker], Type[TpModelWorker], Type[NGRAMWorker]]]:
assert (
not self.is_none()
), "Cannot create worker for NONE speculative algorithm."
if self.is_dflash():
# V2 worker drives both overlap and non-overlap (scheduler runs it
# synchronously when overlap is disabled), same as EAGLE.
from sglang.srt.speculative.dflash_worker_v2 import DFlashWorkerV2
return DFlashWorkerV2
if self.is_dspark():
from sglang.srt.speculative.dspark_components.dspark_worker_v2 import (
DSparkWorkerV2,
)
return DSparkWorkerV2
if self.is_frozen_kv_mtp():
# V2 worker drives both overlap and non-overlap (scheduler runs it
# synchronously when overlap is disabled), same as EAGLE.
from sglang.srt.speculative.frozen_kv_mtp_worker_v2 import (
FrozenKVMTPWorkerV2,
)
return FrozenKVMTPWorkerV2
# EAGLE / EAGLE3 / STANDALONE / MULTI_LAYER always use the V2 worker,
# even with overlap disabled (scheduler drives it synchronously).
if self.is_eagle() and server_args.enable_multi_layer_eagle:
from sglang.srt.speculative.multi_layer_eagle_worker_v2 import (
MultiLayerEagleWorkerV2,
)
return MultiLayerEagleWorkerV2
elif self.is_eagle():
from sglang.srt.speculative.eagle_worker_v2 import EAGLEWorkerV2
return EAGLEWorkerV2
elif self.is_standalone():
from sglang.srt.speculative.standalone_worker_v2 import (
StandaloneWorkerV2,
)
return StandaloneWorkerV2
elif self.is_ngram():
from sglang.srt.speculative.ngram_worker import NGRAMWorker
return NGRAMWorker
raise ValueError("Unreachable code path in create_worker.")
class SpecInputType(IntEnum):
# NOTE: introduce this to distinguish the SpecInput types of multiple algorithms when asserting in attention backends.
# If all algorithms can share the same datastrucutre of draft_input and verify_input, consider simplify it
EAGLE_DRAFT = auto()
EAGLE_DRAFT_EXTEND = auto()
EAGLE_VERIFY = auto()
FROZEN_KV_MTP_DRAFT = auto()
FROZEN_KV_MTP_VERIFY = auto()
DFLASH_DRAFT = auto()
DFLASH_VERIFY = auto()
NGRAM_VERIFY = auto()
class SpecInput(ABC):
# Per-request verify lengths for the ragged-verify graphs (see
# sglang.srt.speculative.ragged_verify); verify inputs of algorithms with
# supports_ragged_verify() override it per step. Must stay a class-level
# default, not an __init__ assignment: dataclass subclasses declare it as
# a field and run __post_init__ -> super().__init__ *after* field
# assignment, so an init-time default would clobber the passed layout.
ragged_verify_layout: Optional[RaggedVerifyLayout] = None
def __init__(self, spec_input_type: SpecInputType):
self.spec_input_type = spec_input_type
# Cross-algorithm phase guards. Used by attention backends and
# ForwardBatch padding logic to dispatch on phase without hardcoding the
# specific algo class (EAGLE / FROZEN_KV_MTP / DFLASH / NGRAM each have
# their own draft / verify SpecInput subclasses).
def is_draft_input(self) -> bool:
return self.spec_input_type in {
SpecInputType.EAGLE_DRAFT,
SpecInputType.EAGLE_DRAFT_EXTEND,
SpecInputType.FROZEN_KV_MTP_DRAFT,
SpecInputType.DFLASH_DRAFT,
}
def is_verify_input(self) -> bool:
return self.spec_input_type in {
SpecInputType.EAGLE_VERIFY,
SpecInputType.FROZEN_KV_MTP_VERIFY,
SpecInputType.DFLASH_VERIFY,
SpecInputType.NGRAM_VERIFY,
}
@abstractmethod
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
pass
def get_spec_adjusted_global_num_tokens(
self, batch: ScheduleBatch
) -> Tuple[List[int], List[int]]:
c1, c2 = self.get_spec_adjust_token_coefficient()
global_num_tokens = [x * c1 for x in batch.global_num_tokens]
global_num_tokens_for_logprob = [
x * c2 for x in batch.global_num_tokens_for_logprob
]
return global_num_tokens, global_num_tokens_for_logprob
def create_dummy_verify_input(
spec_algorithm: SpeculativeAlgorithm,
server_args: ServerArgs,
custom_mask: torch.Tensor,
num_tokens_per_bs: int,
is_draft_worker: bool,
) -> Optional[SpecInput]:
"""Dummy verify ``SpecInput`` for CUDA-graph capture (per-algorithm dispatch)."""
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode
spec_info = None
if spec_algorithm.is_eagle() or spec_algorithm.is_standalone():
from sglang.srt.speculative.eagle_info import EagleVerifyInput
if is_draft_worker:
raise RuntimeError("This should not happen.")
else:
spec_info = EagleVerifyInput(
draft_token=None,
custom_mask=custom_mask,
positions=None,
retrieve_index=None,
retrieve_next_token=None,
retrieve_next_sibling=None,
retrieve_cum_len=None,
spec_steps=server_args.speculative_num_steps,
topk=server_args.speculative_eagle_topk,
draft_token_num=server_args.speculative_num_draft_tokens,
capture_hidden_mode=CaptureHiddenMode.FULL,
seq_lens_sum=None,
seq_lens_cpu=None,
)
elif spec_algorithm.is_dflash_family():
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
# Dummy warmup only needs shape metadata; avoid forcing custom-mask mode.
spec_info = DFlashVerifyInput(
draft_token=None,
positions=None,
draft_token_num=server_args.speculative_num_draft_tokens,
custom_mask=None,
capture_hidden_mode=(
CaptureHiddenMode.NULL if is_draft_worker else CaptureHiddenMode.FULL
),
)
elif spec_algorithm.is_ngram():
from sglang.srt.speculative.ngram_info import NgramVerifyInput
spec_info = NgramVerifyInput(
draft_token=None,
custom_mask=custom_mask,
positions=None,
retrieve_index=None,
retrieve_next_token=None,
retrieve_next_sibling=None,
draft_token_num=num_tokens_per_bs,
)
spec_info.capture_hidden_mode = CaptureHiddenMode.NULL
return spec_info
@@ -0,0 +1,236 @@
"""Internal storage backing ``SpeculativeAlgorithm.register``. Plugins
should use that classmethod API; do not import from this module directly.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Callable, Dict, Optional, Type
import torch
if TYPE_CHECKING:
from sglang.srt.managers.overlap_utils import FutureMap
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.spec_info import SpecInput
WorkerFactory = Callable[["ServerArgs"], Type]
ServerArgsValidator = Callable[["ServerArgs"], None]
logger = logging.getLogger(__name__)
class CustomSpecAlgo:
"""A plugin-registered speculative algorithm. Duck-types
``SpeculativeAlgorithm`` enum values (same ``is_*()`` / ``create_worker``
interface).
Plugins may subclass this to override any ``is_*()`` / ``supports_*()`` /
``create_worker`` method (e.g. to integrate with builtin-specific
branches like ``if spec_algorithm.is_eagle():`` in scheduler /
model_runner). Pass the subclass via ``spec_class=...`` at registration.
Defaults: all ``is_*()`` return ``False`` except ``is_speculative``.
``supports_overlap=False`` is deprecated: the spec V1 worker path has been
removed, so such algorithms run on the V2 scheduler schema with overlap
disabled (synchronous). Migrate plugin workers to the V2 schema and
overlap scheduling.
"""
def __init__(
self,
name: str,
factory: WorkerFactory,
*,
supports_overlap: bool = False,
validate_server_args: Optional[ServerArgsValidator] = None,
):
self.name = name
self.factory = factory
self.supports_overlap = supports_overlap
self.validate_server_args = validate_server_args
def __repr__(self) -> str:
return f"CustomSpecAlgo({self.name!r})"
def is_some(self) -> bool:
return True
def is_none(self) -> bool:
return False
def is_speculative(self) -> bool:
return True
def is_eagle(self) -> bool:
return False
def is_eagle3(self) -> bool:
return False
def is_frozen_kv_mtp(self) -> bool:
return False
def is_dflash(self) -> bool:
return False
def is_dspark(self) -> bool:
return False
def is_dflash_family(self) -> bool:
return False
def is_standalone(self) -> bool:
return False
def is_ngram(self) -> bool:
return False
def supports_target_verify_for_draft(self) -> bool:
return False
def supports_ragged_verify(self) -> bool:
return False
def has_draft_kv(self) -> bool:
# Conservative default: the larger KV reserve.
return True
def handle_server_args(self, server_args: ServerArgs) -> None:
pass
def create_worker(self, server_args: ServerArgs) -> Type:
if not server_args.disable_overlap_schedule and not self.supports_overlap:
raise ValueError(
f"Speculative algorithm {self.name} does not support overlap scheduling."
)
if not self.supports_overlap:
# Reached only when overlap is disabled, so the algorithm really
# does run synchronously on the V2 schema below.
logger.warning(
"Speculative algorithm %s is registered with "
"supports_overlap=False, which is deprecated: the spec V1 "
"worker path has been removed, and the algorithm now runs on "
"the V2 scheduler schema with overlap disabled (synchronous). "
"Migrate the plugin worker to support overlap scheduling.",
self.name,
)
return self.factory(server_args)
def get_num_tokens_per_bs_for_target_verify(
self, num_draft_tokens: int, is_draft_worker: bool
) -> int:
# FIXME: Remove this after the forward mode refactor. Target verify is
# essentially a fixed sequence length prefill/extend with full cuda
# graph support. We can use it for target verify, or we can use it for
# other cases which is not target verify but fixed length prefill.
# Here, we expose this interface to allow the other use cases.
return num_draft_tokens
def build_disagg_draft_input(
self,
batch: ScheduleBatch,
server_args: ServerArgs,
last_tokens_tensor: torch.Tensor,
future_map: FutureMap,
) -> Optional[SpecInput]:
return None
_REGISTRY: Dict[str, CustomSpecAlgo] = {}
# CLI spellings that are not ``SpeculativeAlgorithm`` members but still resolve
# to a builtin (e.g. NEXTN -> EAGLE). Reserved alongside the enum members so
# plugins cannot shadow them.
_RESERVED_ALIASES = frozenset({"NEXTN"})
def _reserved_names() -> frozenset:
"""Names plugins cannot register under: every ``SpeculativeAlgorithm``
member plus ``_RESERVED_ALIASES``.
Derived from the enum (lazily, to avoid a circular import — ``spec_info``
imports this module) so any new builtin is reserved automatically without
editing a second list.
"""
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
return frozenset(algo.name for algo in SpeculativeAlgorithm) | _RESERVED_ALIASES
def _assert_custom_spec_algo_conforms(spec_class: Type[CustomSpecAlgo]) -> None:
"""Fail fast if ``spec_class`` drifts from the ``SpeculativeAlgorithm``
duck-typing contract.
``from_string`` returns either type and callers dispatch on the shared
``is_*()`` / ``supports_*()`` interface without isinstance checks, so every
such method on the enum must also exist on the registered spec class —
otherwise a plugin-registered algo hits ``AttributeError`` at a call site
(this is how ``is_some`` / ``is_frozen_kv_mtp`` silently went missing). New
predicates are covered automatically; no second list to maintain.
Called from ``register_algorithm`` rather than at import time because
``spec_info`` imports this module, so ``SpeculativeAlgorithm`` does not yet
exist while this module is loading; at registration time it is fully
defined.
"""
# NOTE: use ``vars()`` not ``dir()`` for the enum — ``EnumMeta.__dir__``
# hides instance methods, so ``dir(SpeculativeAlgorithm)`` would yield an
# empty interface and turn this guard into a silent no-op.
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
interface = {
name
for name in vars(SpeculativeAlgorithm)
if name.startswith(("is_", "supports_"))
}
missing = sorted(interface - set(dir(spec_class)))
if missing:
raise TypeError(
f"{spec_class.__name__} is missing duck-typed methods from "
f"SpeculativeAlgorithm: {missing}. Add them to {spec_class.__name__} "
"so plugin-registered algorithms stay dispatchable."
)
def register_algorithm(
name: str,
*,
supports_overlap: bool = False,
validate_server_args: Optional[ServerArgsValidator] = None,
spec_class: Type[CustomSpecAlgo] = CustomSpecAlgo,
) -> Callable[[WorkerFactory], WorkerFactory]:
"""Return a decorator that registers a plugin algorithm under ``name``.
Pass a ``spec_class`` subclass of ``CustomSpecAlgo`` to override any
``is_*()`` / ``supports_*()`` / ``create_worker`` method.
"""
upper = name.upper()
if upper in _reserved_names():
raise ValueError(
f"'{upper}' is a reserved speculative algorithm name; cannot be re-registered."
)
if upper in _REGISTRY:
raise ValueError(f"Speculative algorithm '{upper}' already registered.")
_assert_custom_spec_algo_conforms(spec_class)
def decorator(factory: WorkerFactory) -> WorkerFactory:
_REGISTRY[upper] = spec_class(
name=upper,
factory=factory,
supports_overlap=supports_overlap,
validate_server_args=validate_server_args,
)
return factory
return decorator
def get_spec(name: Optional[str]) -> Optional[CustomSpecAlgo]:
"""Return the registered spec for ``name``, or ``None`` for builtin /
unknown names."""
if name is None:
return None
return _REGISTRY.get(name.upper())
+719
View File
@@ -0,0 +1,719 @@
from __future__ import annotations
import logging
import os
import time
from contextlib import contextmanager
from typing import TYPE_CHECKING, List, Optional
import torch
from huggingface_hub import snapshot_download
from sglang.kernels.ops.speculative.cache_locs import (
align_evict_mask_to_page_size as align_evict_mask_to_page_size,
)
from sglang.kernels.ops.speculative.cache_locs import (
assign_extend_cache_locs as assign_extend_cache_locs,
)
from sglang.kernels.ops.speculative.cache_locs import (
assign_req_to_token_pool as assign_req_to_token_pool,
)
from sglang.kernels.ops.speculative.cache_locs import (
assign_req_to_token_pool_func as assign_req_to_token_pool_func,
)
from sglang.kernels.ops.speculative.cache_locs import (
filter_finished_cache_loc_kernel as filter_finished_cache_loc_kernel,
)
from sglang.kernels.ops.speculative.cache_locs import (
generate_draft_decode_kv_indices as generate_draft_decode_kv_indices,
)
from sglang.kernels.ops.speculative.cache_locs import (
get_src_tgt_cache_loc as get_src_tgt_cache_loc,
)
from sglang.kernels.ops.speculative.cache_locs import (
get_target_cache_loc as get_target_cache_loc,
)
from sglang.kernels.ops.speculative.eagle import (
fill_accept_out_cache_loc_func as fill_accept_out_cache_loc_func,
)
from sglang.srt.distributed.parallel_state import (
GroupCoordinator,
patch_tensor_parallel_group,
)
from sglang.srt.environ import envs
from sglang.srt.managers.schedule_batch import set_mamba_track_indices_from_reqs
from sglang.srt.runtime_context import get_server_args
from sglang.srt.utils import (
is_cpu,
is_cuda,
is_hip,
is_musa,
is_npu,
is_xpu,
next_power_of_2,
)
from sglang.srt.utils.async_probe import maybe_detect_oob
from sglang.srt.utils.nvtx_utils import profile_range
_is_cuda = is_cuda()
_is_hip = is_hip()
_is_npu = is_npu()
_is_musa = is_musa()
_is_xpu = is_xpu()
_is_cpu = is_cpu()
if TYPE_CHECKING:
from sglang.srt.constrained.base_grammar_backend import BaseGrammarObject
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.eagle_info import EagleVerifyInput
if _is_cuda:
from sgl_kernel import fast_topk
elif _is_hip:
from sgl_kernel import fast_topk
else:
from sglang.srt.utils.common import fast_topk
if _is_cpu:
from sgl_kernel import assign_extend_cache_locs_cpu
logger = logging.getLogger(__name__)
def fast_sample(probs: torch.Tensor, num_samples: int = 1):
sample_index = torch.multinomial(probs, num_samples=num_samples)
sample_p = probs.gather(1, sample_index)
return sample_p, sample_index
def renorm_draft_probs(
next_token_logits: torch.Tensor,
sampling_info,
use_rejection_sampling: bool,
) -> torch.Tensor:
"""Draft-side next-token distribution.
Plain softmax, except under rejection sampling where logits are
temperature-scaled so the draft proposal q tracks the target sampling
temperature (higher acceptance; correctness holds for any q).
"""
if not use_rejection_sampling or not next_token_logits.size(0):
return torch.softmax(next_token_logits, dim=-1)
return torch.softmax(next_token_logits / sampling_info.temperatures, dim=-1)
def sample_draft_proposal(next_token_logits: torch.Tensor, temperatures: torch.Tensor):
"""Leviathan draft proposal: q = softmax(logits / T), X ~ q.
Returns (q, q(X), X). The verify's accept test coin*q(X) < p(X) is unbiased
only if q is exactly the distribution X was drawn from, so callers must hand
the returned q (not a recomputed one) to the verify.
"""
probs = torch.softmax(next_token_logits / temperatures, dim=-1)
topk_p, topk_index = fast_sample(probs, num_samples=1)
return probs, topk_p, topk_index
# Simulate acceptance length for benchmarking purposes
SIMULATE_ACC_LEN = envs.SGLANG_SIMULATE_ACC_LEN.get() # turn off if < 0
SIMULATE_ACC_METHOD = envs.SGLANG_SIMULATE_ACC_METHOD.get()
SIMULATE_ACC_TOKEN_MODE = envs.SGLANG_SIMULATE_ACC_TOKEN_MODE.get()
TREE_TRAVERSE_TIME_THRESHOLD = 1 # TODO: set this properly
TREE_SPEC_KERNEL_AVAILABLE = (
_is_cuda or _is_musa
) # This kernel is only available for CUDA and MUSA now
def draft_kv_indices_buffer_width(
num_seqs: int, topk: int, max_context_len: int
) -> int:
"""Per-step row width of the EAGLE draft-decode kv_indices buffer.
num_seqs * topk branches each attend up to max_context_len KV slots; the topk
factor is mandatory -- dropping it under-allocates and overflows the row (#27338, #27460).
"""
assert (
num_seqs * topk * max_context_len < 2**31
), "kv_indices flat offset would overflow int32; reduce batch/topk/context"
return num_seqs * topk * max_context_len
def draft_kv_indices_used_len(
seq_lens_sum: int, topk: int, bs: int, num_steps: int
) -> int:
"""kv_indices length used through num_steps draft-decode steps.
bs = topk * num_seqs branches, one index appended per branch per step. Called with
num_steps = i + 1 (per-step slice) and speculative_num_steps (capacity assert).
"""
return seq_lens_sum * topk + bs * num_steps
def record_stream_each(tensors, stream):
"""Call record_stream(stream) on each cuda tensor in `tensors`, skipping
non-tensor / non-cuda entries. Tells the caching allocator that the
tensors are also used on `stream`, so memory is not recycled while
queued work is still in flight after Python refs drop.
"""
for t in tensors:
if isinstance(t, torch.Tensor) and t.is_cuda:
t.record_stream(stream)
def record_stream_for_v2_verify(batch, verify_input, fwd_stream):
"""Mark pre-prepare SB / verify_input GPU tensors as used on `fwd_stream`.
Spec V2 mutates SB mid-forward (`prepare_for_verify` rebinds
`batch.input_ids` / `out_cache_loc`; `_draft_extend_for_decode` later
replaces `batch.input_ids` again). Each rebind drops the only SB Python
ref to the old tensor while the verify forward kernel may still be
reading its memory on `fwd_stream`; `record_stream` tells the caching
allocator to wait for `fwd_stream` before recycling the block.
Covers pre-prepare tensors only; caller must also `record_stream_each`
the post-prepare rebinds (new `batch.input_ids` / `out_cache_loc`).
"""
candidates = [
batch.seq_lens,
batch.req_pool_indices,
batch.input_ids,
batch.out_cache_loc,
]
if verify_input is not None:
candidates.extend(
[
getattr(verify_input, attr, None)
for attr in (
"draft_token",
"custom_mask",
"positions",
"retrieve_index",
"retrieve_next_token",
"retrieve_next_sibling",
)
]
)
record_stream_each(candidates, fwd_stream)
def spec_need_hidden_states(server_args: Optional[ServerArgs] = None) -> bool:
if server_args is None:
server_args = get_server_args()
# STANDALONE drafts don't consume `spec_info.hidden_states` (vanilla LLM).
# multi_layer_eagle, DFLASH, and DSPARK don't relay hidden_states through FutureMap.
# TODO(lsyin): also skip when step == 1.
if server_args.speculative_algorithm in ("STANDALONE", "DFLASH", "DSPARK"):
return False
return not server_args.enable_multi_layer_eagle
@torch.compile(dynamic=True, disable=_is_npu or _is_xpu)
def create_num_accept_tokens_filter(
num_correct_drafts: torch.Tensor,
unfinished_index_device: torch.Tensor,
seq_lens: torch.Tensor,
):
num_accept_tokens_filter = torch.zeros_like(num_correct_drafts)
num_accept_tokens_filter[unfinished_index_device] = (
num_correct_drafts[unfinished_index_device] + 1
)
seq_lens.add_(num_correct_drafts + 1)
return num_accept_tokens_filter
def _select_top_k_tokens_first(
topk_p: torch.Tensor,
topk_index: torch.Tensor,
hidden_states: Optional[torch.Tensor],
topk: int,
):
input_ids = topk_index.flatten()
if hidden_states is not None:
hidden_states = hidden_states.repeat_interleave(topk, dim=0)
tree_info = (
topk_p.unsqueeze(1), # (b, 1, topk)
topk_index, # (b, topk)
torch.arange(-1, topk, dtype=torch.long, device=input_ids.device).expand(
topk_p.shape[0], -1
), # (b, topk + 1) — expand avoids the allocation of repeat
)
return input_ids, hidden_states, topk_p, tree_info
@torch.compile(dynamic=True, disable=_is_npu or _is_xpu)
def _select_top_k_tokens_later(
i: int,
topk_p: torch.Tensor,
topk_index: torch.Tensor,
hidden_states: torch.Tensor,
scores: torch.Tensor,
topk: int,
):
topk_sq = topk * topk
expand_scores = scores.unsqueeze(2) * topk_p.view(-1, topk, topk)
# (b, topk, 1) * (b, topk, topk) -> (b, topk, topk)
topk_cs_p, topk_cs_index = fast_topk(
expand_scores.flatten(start_dim=1), topk, dim=-1
) # (b, topk)
topk_index = topk_index.view(-1, topk_sq)
input_ids = torch.gather(topk_index, 1, topk_cs_index).flatten()
if hidden_states is not None and hidden_states.shape[0] > 0:
flat_cs = topk_cs_index.flatten()
batch_offsets = torch.arange(
0, hidden_states.shape[0], step=topk, device=flat_cs.device
)
selected_input_index = flat_cs // topk + batch_offsets.repeat_interleave(topk)
hidden_states = hidden_states[selected_input_index]
tree_info = (
expand_scores, # (b, topk, topk)
topk_index, # (b, topk * topk)
topk_cs_index + (topk_sq * (i - 1) + topk), # (b, topk)
)
return input_ids, hidden_states, topk_cs_p, tree_info
def select_top_k_tokens(
i: int,
topk_p: torch.Tensor,
topk_index: torch.Tensor,
hidden_states: torch.Tensor,
scores: torch.Tensor,
topk: int,
):
if i == 0:
return _select_top_k_tokens_first(topk_p, topk_index, hidden_states, topk)
return _select_top_k_tokens_later(
i, topk_p, topk_index, hidden_states, scores, topk
)
def _sample_simulated_acc_len(
simulate_acc_len: float,
simulate_acc_method: str,
max_len: int,
) -> int:
"""Sample a simulated acceptance length in [1, max_len]."""
if simulate_acc_method == "multinomial":
simulated_values = torch.normal(
mean=simulate_acc_len,
std=1.0,
size=(1,),
device="cpu",
)
# clamp simulated values to be between 1 and max_len
simulated_values = torch.clamp(simulated_values, min=1.0, max=max_len)
simulate_acc_len = int(simulated_values.round().item())
elif simulate_acc_method == "match-expected":
# multinomial sampling does not match the expected length
# we keep it for the sake of compatibility of existing tests
# but it's better to use "match-expected" for the cases that need to
# match the expected length, One caveat is that this will only sample
# either round down or round up of the expected length
simulate_acc_len = max(1.0, min(max_len, simulate_acc_len))
lower = int(simulate_acc_len // 1)
upper = lower + 1 if lower < max_len else lower
if lower == upper:
simulate_acc_len = lower
else:
weight_upper = simulate_acc_len - lower
weight_lower = 1.0 - weight_upper
probs = torch.tensor([weight_lower, weight_upper], device="cpu")
sampled_index = torch.multinomial(probs, num_samples=1)
simulate_acc_len = lower if sampled_index == 0 else upper
else:
raise ValueError(f"Invalid simulate_acc_method: {simulate_acc_method}")
return int(simulate_acc_len)
def generate_simulated_accept_index(
accept_index,
predict,
num_correct_drafts,
candidates,
target_predict,
bs,
spec_steps,
simulate_acc_len: float = SIMULATE_ACC_LEN,
simulate_acc_method: str = SIMULATE_ACC_METHOD,
simulate_acc_token_mode: str = SIMULATE_ACC_TOKEN_MODE,
):
use_real_draft_tokens = simulate_acc_token_mode == "real-draft-token"
assert simulate_acc_len > 0.0
simulate_acc_len = _sample_simulated_acc_len(
simulate_acc_len, simulate_acc_method, spec_steps + 1
)
accept_indx_first_col = accept_index[:, 0].view(-1, 1)
sim_accept_index = torch.full(
(bs, spec_steps + 1), -1, dtype=torch.int32, device=accept_index.device
)
sim_accept_index[:, :simulate_acc_len] = accept_indx_first_col + torch.arange(
simulate_acc_len, device=accept_index.device
)
num_correct_drafts.fill_(simulate_acc_len - 1)
if not use_real_draft_tokens:
predict.fill_(100) # some legit token id
return sim_accept_index
# Use the topk=1 draft chain for forced acceptance, then a target-derived bonus.
if simulate_acc_len > 1:
draft_node_indices = sim_accept_index[:, : simulate_acc_len - 1].long()
predict[draft_node_indices] = candidates[:, 1:simulate_acc_len].to(
dtype=predict.dtype
)
bonus_node_indices = sim_accept_index[:, simulate_acc_len - 1].long()
predict[bonus_node_indices] = target_predict[:, simulate_acc_len - 1].to(
dtype=predict.dtype
)
return sim_accept_index
def traverse_tree(
retrieve_next_token: torch.Tensor,
retrieve_next_sibling: torch.Tensor,
draft_tokens: torch.Tensor,
grammar: BaseGrammarObject,
allocate_token_bitmask: torch.Tensor,
vocab_size: Optional[int] = None,
):
"""
Traverse the tree constructed by the draft model to generate the logits mask.
"""
assert (
retrieve_next_token.shape == retrieve_next_sibling.shape == draft_tokens.shape
)
def dfs(
curr: int,
retrieve_next_token: torch.Tensor,
retrieve_next_sibling: torch.Tensor,
parent_pos: int,
):
if curr == 0:
# the first token generated by the target model, and thus it is always
# accepted from the previous iteration
is_accepted = True
else:
parent_bitmask = allocate_token_bitmask[parent_pos]
current_token = draft_tokens[curr]
if vocab_size and current_token >= vocab_size:
is_accepted = False
else:
# 32 boolean bitmask values are packed into 32-bit integers
is_accepted = (
parent_bitmask[current_token // 32] & (1 << (current_token % 32))
) != 0
if is_accepted:
if curr != 0:
# Accept the current token
grammar.accept_token(int(draft_tokens[curr]))
if not grammar.is_terminated():
# Generate the bitmask for the current token
grammar.fill_vocab_mask(allocate_token_bitmask, curr)
if retrieve_next_token[curr] != -1:
# Visit the child node
dfs(
int(retrieve_next_token[curr]),
retrieve_next_token,
retrieve_next_sibling,
curr,
)
if curr != 0:
# Rollback the current token
grammar.rollback(1)
if retrieve_next_sibling[curr] != -1:
# Visit the sibling node
dfs(
int(retrieve_next_sibling[curr]),
retrieve_next_token,
retrieve_next_sibling,
parent_pos,
)
dfs(0, retrieve_next_token, retrieve_next_sibling, -1)
def generate_token_bitmask(
reqs: List[Req],
verify_input: EagleVerifyInput,
retrieve_next_token_cpu: torch.Tensor,
retrieve_next_sibling_cpu: torch.Tensor,
draft_tokens_cpu: torch.Tensor,
vocab_size: int,
):
"""
Generate the logit mask for structured output.
Draft model's token can be either valid or invalid with respect to the grammar.
We need to perform DFS to
1. figure out which tokens are accepted by the grammar.
2. if so, what is the corresponding logit mask.
"""
num_draft_tokens = draft_tokens_cpu.shape[-1]
allocate_token_bitmask = None
assert len(reqs) == retrieve_next_token_cpu.shape[0]
grammar = None
for i, req in enumerate(reqs):
if req.grammar is not None:
if allocate_token_bitmask is None:
allocate_token_bitmask = req.grammar.allocate_vocab_mask(
vocab_size=vocab_size,
batch_size=draft_tokens_cpu.numel(),
device="cpu",
)
grammar = req.grammar
s = time.perf_counter()
traverse_tree(
retrieve_next_token_cpu[i],
retrieve_next_sibling_cpu[i],
draft_tokens_cpu[i],
req.grammar,
allocate_token_bitmask[
i * num_draft_tokens : (i + 1) * num_draft_tokens
],
vocab_size=vocab_size,
)
tree_traverse_time = time.perf_counter() - s
if tree_traverse_time > TREE_TRAVERSE_TIME_THRESHOLD:
logger.warning(
f"Bit mask generation took {tree_traverse_time} seconds with "
f"grammar: {req.grammar}"
)
verify_input.grammar = grammar
return allocate_token_bitmask
def load_token_map(token_map_path: str) -> List[int]:
if not os.path.exists(token_map_path):
repo_id = os.path.dirname(token_map_path)
file_name = os.path.basename(token_map_path)
cache_dir = None
if envs.SGLANG_USE_MODELSCOPE.get():
from modelscope.utils.file_utils import get_model_cache_root
cached_repo_path = os.path.join(get_model_cache_root(), repo_id)
if os.path.exists(cached_repo_path):
cache_dir = cached_repo_path
if cache_dir is None:
if envs.SGLANG_USE_MODELSCOPE.get():
from modelscope.hub.snapshot_download import (
snapshot_download as download_func,
)
else:
download_func = snapshot_download
cache_dir = download_func(
repo_id,
ignore_patterns=["*.bin", "*.safetensors"],
)
token_map_path = os.path.join(cache_dir, file_name)
hot_token_id = torch.load(token_map_path, weights_only=True)
return torch.tensor(hot_token_id, dtype=torch.int64)
@contextmanager
def draft_tp_context(tp_group: GroupCoordinator):
# Draft model doesn't use dp and has its own tp group.
# We disable mscclpp now because it doesn't support 2 comm groups.
with patch_tensor_parallel_group(tp_group):
yield
def spec_stage_span(name: str):
"""Profiler span for a coarse speculative-decoding stage (``draft`` /
``draft_extend`` / ``verify``).
"""
return profile_range(name)
def move_accept_tokens_to_target_kvcache(
batch: ScheduleBatch,
accept_index: torch.Tensor,
num_correct_drafts: torch.Tensor,
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator,
):
"""
Move accepted tokens (drafts + bonus) to the target KV cache.
Args:
batch: The batch to run.
accept_index: The index of the accepted tokens (incl. bonus).
num_correct_drafts: Per-req count of correct drafts (excludes bonus);
seq_lens is advanced by ``num_correct_drafts + 1`` to cover the bonus slot.
"""
bs = len(batch.seq_lens)
device = batch.seq_lens.device
# accept_index element count, NOT bs * num_draft_tokens: for topk > 1 the
# tree exceeds the accepted chain, over-reading accept_index (illegal memory).
size = bs * accept_index.shape[1]
# fill_accept_out_cache_loc reads out_cache_loc[accept_index]; -1 sentinel ok.
maybe_detect_oob(
accept_index,
-1,
batch.out_cache_loc.size(0),
"spec v2 move_accept_tokens accept_index",
)
tgt_cache_loc = torch.zeros(
size,
dtype=torch.int64,
device=device,
)
accept_out_cache_loc = torch.zeros(size, dtype=torch.int64, device=device)
if _is_cpu:
assign_extend_cache_locs_cpu(
batch.req_pool_indices,
batch.req_to_token_pool.req_to_token,
batch.seq_lens,
batch.seq_lens + num_correct_drafts + 1,
tgt_cache_loc,
batch.req_to_token_pool.req_to_token.shape[1],
)
else:
assign_extend_cache_locs[(bs,)](
batch.req_pool_indices,
batch.req_to_token_pool.req_to_token,
batch.seq_lens,
batch.seq_lens + num_correct_drafts + 1,
tgt_cache_loc,
batch.req_to_token_pool.req_to_token.shape[1],
next_power_of_2(bs),
)
fill_accept_out_cache_loc_func(
accept_index,
batch.out_cache_loc,
accept_out_cache_loc,
size,
)
token_to_kv_pool_allocator.get_kvcache().move_kv_cache(
tgt_cache_loc, accept_out_cache_loc
)
def prepare_mamba_track_for_verify(batch: ScheduleBatch) -> None:
"""Rebuild mamba track indices from reqs before a TARGET_VERIFY forward.
Spec batches skip the refresh in prepare_for_decode, and filter/merge
null these fields, so they must be rebuilt right before verify. Clearing
the mask also keeps a stale extend-time mask from triggering in-forward
tracking during TARGET_VERIFY; tracking is done in
commit_mamba_states_after_verify instead.
"""
if not get_server_args().enable_mamba_extra_buffer():
return
set_mamba_track_indices_from_reqs(batch)
batch.mamba_track_mask = None
batch.mamba_track_seqlens = None
def commit_mamba_states_after_verify(
target_worker: TpModelWorker,
batch: ScheduleBatch,
accept_lens: torch.Tensor,
accept_index: torch.Tensor,
draft_token_num: int,
) -> None:
"""Commit accepted per-step mamba states into the persistent caches.
During TARGET_VERIFY, hybrid linear attention backends keep per-step
states in intermediate caches instead of advancing the persistent
conv/ssm caches. After acceptance, the state of each request's last
accepted step is committed back, plus the interval-crossing state used
for prefix-cache tracking (mamba extra_buffer mode).
No-op for models without mamba-style state or backends without the
commit hook.
"""
model_runner = target_worker.model_runner
if model_runner.mambaish_config is None:
return
attn_backend = model_runner.attn_backend
if not hasattr(attn_backend, "update_mamba_state_after_mtp_verify"):
return
bs = accept_lens.shape[0]
# `accept_lens` already includes the bonus token (drafts + 1 per req).
if not batch.forward_mode.is_idle() and accept_index.numel() > 0:
accept_indices_offset = torch.arange(
0,
bs * draft_token_num,
step=draft_token_num,
dtype=accept_lens.dtype,
device=accept_lens.device,
)
req_idx = torch.arange(bs, dtype=torch.int64, device=accept_lens.device)
# Per-req tree step of the last accepted node, i.e. the step whose
# mamba state to commit; reduces to accept_lens - 1 for topk == 1.
last_correct_step_indices = (
accept_index[req_idx, (accept_lens - 1).to(torch.int64)]
- accept_indices_offset
)
if batch.mamba_track_indices is not None:
# If after verify, the request's seq_lens has crossed a mamba track interval,
# we need to update the mamba state for the request at the crossing point.
seq_lens_pre_verify = batch.seq_lens
seq_lens_post_verify = batch.seq_lens + accept_lens
mamba_track_interval = get_server_args().mamba_track_interval
to_track_mask = (
seq_lens_pre_verify // mamba_track_interval
!= seq_lens_post_verify // mamba_track_interval
)
tracking_point = (
seq_lens_post_verify // mamba_track_interval * mamba_track_interval
)
to_track_ith = torch.clamp(
tracking_point - seq_lens_pre_verify - 1, min=0
).to(torch.int64)
candidate_track_steps = (
accept_index[req_idx, to_track_ith] - accept_indices_offset
)
mamba_steps_to_track = torch.where(
to_track_mask,
candidate_track_steps,
torch.full_like(candidate_track_steps, -1),
)
else:
mamba_steps_to_track = None
attn_backend.update_mamba_state_after_mtp_verify(
last_correct_step_indices=last_correct_step_indices,
mamba_track_indices=batch.mamba_track_indices,
mamba_steps_to_track=mamba_steps_to_track,
model=model_runner.model,
)
def spec_prepare_for_decode(batch: ScheduleBatch) -> None:
"""eagle/ngram share a stateless free function; dflash keeps stateful
prep on its draft input -- the dispatcher routes.
"""
if batch.spec_algorithm.is_dflash_family():
batch.spec_info.prepare_for_decode(batch)
else:
from sglang.srt.speculative.eagle_utils import eagle_prepare_for_decode
eagle_prepare_for_decode(batch)
@@ -0,0 +1,248 @@
import contextlib
import logging
from typing import Optional, Tuple
import torch
from sglang.srt.environ import envs
from sglang.srt.layers.moe.utils import speculative_moe_backend_context
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.adaptive_runtime_state import (
AdaptiveController,
)
from sglang.srt.speculative.eagle_utils import default_tree_mask_mode
from sglang.srt.speculative.eagle_worker_v2 import EagleDraftWorker, EAGLEWorkerV2
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import draft_tp_context
from sglang.srt.utils import empty_context, get_bool_env_var, is_cuda
if is_cuda():
from sgl_kernel import segment_packbits # noqa: F401
logger = logging.getLogger(__name__)
SGLANG_RETURN_ORIGINAL_LOGPROB = get_bool_env_var("SGLANG_RETURN_ORIGINAL_LOGPROB")
def _get_plan_stream(
device: str,
) -> Tuple[any, contextlib.AbstractContextManager]:
if envs.SGLANG_ENABLE_OVERLAP_PLAN_STREAM.get():
plan_stream = torch.get_device_module(device).Stream()
plan_stream_ctx = torch.get_device_module(device).stream(plan_stream)
return plan_stream, plan_stream_ctx
else:
return None, contextlib.nullcontext()
class StandaloneDraftWorker(EagleDraftWorker):
"""Custom EagleDraftWorker that doesn't share embeddings/lm_head with target model."""
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: int,
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
# copy args
self.server_args = server_args
self.gpu_id = gpu_id
self.tp_rank = tp_rank
self.dp_rank = dp_rank
self.moe_ep_rank = moe_ep_rank
self.nccl_port = nccl_port
self.target_worker = target_worker
self.attn_cp_rank = attn_cp_rank
self.moe_dp_rank = moe_dp_rank
# Args for easy access
self.device = server_args.device
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.speculative_algorithm = SpeculativeAlgorithm.from_string(
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
EagleDraftInput.ALLOC_LEN_PER_DECODE = max(
self.speculative_num_steps * self.topk, self.speculative_num_draft_tokens
)
# Load draft model weights only.
with empty_context():
self.draft_worker = TpModelWorker(
server_args=server_args,
gpu_id=gpu_id,
tp_rank=tp_rank,
pp_rank=0, # spec workers don't support pipeline parallelism
dp_rank=dp_rank,
moe_ep_rank=moe_ep_rank,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
)
# Alias for better readability
self.draft_runner = self.draft_worker.model_runner
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
self.tree_mask_mode = default_tree_mask_mode()
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
# draft_forward reads this (set in EagleDraftWorker.__init__, skipped here).
self.index_share_for_mtp_iteration = (
getattr(
self.draft_runner.model_config.hf_config,
"index_share_for_mtp_iteration",
False,
)
and self.topk == 1
)
self.dsa_index_topk = None
self.seed_dsa_topk_from_draft_extend = False
self.dsa_extend_topk_buf = None
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
"""Standalone: allocate pools without sharing embeddings."""
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
self.init_token_map()
self.init_lm_head()
def init_attention_backends(self):
with self.draft_tp_context(
self.draft_runner.tp_group
), speculative_moe_backend_context():
super().init_attention_backends()
def init_cuda_graphs(self):
with self.draft_tp_context(
self.draft_runner.tp_group
), speculative_moe_backend_context():
super().init_cuda_graphs()
def init_lm_head(self):
"""Override to prevent sharing embeddings and lm_head with target model."""
# For standalone worker, we don't share embeddings and lm_head
# The draft model uses its own embeddings and lm_head
pass
class StandaloneWorkerV2(EAGLEWorkerV2):
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
# Parse arguments
self.server_args = server_args
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.gpu_id = gpu_id
self.device = server_args.device
self._target_worker = target_worker
self.page_size = server_args.page_size
self.speculative_algorithm = SpeculativeAlgorithm.from_string(
server_args.speculative_algorithm
)
# Override the context length of the draft model to be the same as the target model.
server_args.override(
"spec_worker.match_target_context_length",
context_length=target_worker.model_runner.model_config.context_len,
)
# Create our custom draft worker that doesn't share embeddings/lm_head
self._draft_worker = StandaloneDraftWorker(
server_args,
gpu_id,
tp_rank,
dp_rank,
moe_ep_rank,
attn_cp_rank,
moe_dp_rank,
nccl_port,
target_worker,
)
self._validate_vocab_compatibility(
target_vocab_size=target_worker.model_runner.model_config.vocab_size,
target_tokenizer=target_worker.tokenizer,
)
# Some dummy tensors
self.num_new_pages_per_topk = torch.empty(
(), dtype=torch.int64, device=self.device
)
self.extend_lens = torch.empty((), dtype=torch.int64, device=self.device)
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
# TODO: Adaptive speculative
self.adaptive_controller: Optional[AdaptiveController] = None
def _validate_vocab_compatibility(
self,
target_vocab_size: int,
target_tokenizer,
) -> None:
"""Raise ValueError if the draft and target vocabularies are incompatible."""
draft_vocab_size = self._draft_worker.draft_runner.model_config.vocab_size
draft_tokenizer = self._draft_worker.draft_worker.tokenizer
if target_vocab_size != draft_vocab_size:
raise ValueError(
f"STANDALONE speculative decoding requires the draft model to share the "
f"same vocabulary as the target model, but got "
f"target vocab_size={target_vocab_size} and "
f"draft vocab_size={draft_vocab_size}. "
f"Use a draft model with a matching vocabulary, or a speculative "
f"algorithm that supports heterogeneous vocabularies."
)
if (
target_tokenizer is not None
and draft_tokenizer is not None
and hasattr(target_tokenizer, "get_vocab")
and hasattr(draft_tokenizer, "get_vocab")
and target_tokenizer.get_vocab() != draft_tokenizer.get_vocab()
):
raise ValueError(
"STANDALONE speculative decoding requires the draft model to share the "
"same vocabulary as the target model, but the two tokenizers have "
"different token-to-id mappings even though their vocab sizes match. "
"Use a draft model with a matching vocabulary, or a speculative "
"algorithm that supports heterogeneous vocabularies."
)