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,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