Files
wehub-resource-sync 94057c3d3e
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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1118 lines
41 KiB
Python

from __future__ import annotations
import logging
from typing import Optional, Union
import msgspec
import torch
from sglang.kernels.ops.speculative.cache_locs import assign_extend_cache_locs_func
from sglang.srt.distributed import get_tp_group
from sglang.srt.environ import envs
from sglang.srt.layers.dp_attention import is_dp_attention_enabled
from sglang.srt.managers.overlap_utils import (
CONFIDENCE_RELAY_RING_LAG,
FutureMap,
ResolvedConfidence,
)
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.runtime_context import get_parallel
from sglang.srt.server_args import ServerArgs
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_sps import (
SpsAdditiveCostTable,
SpsCostTable,
_interp_clamped,
build_uninitialized_sps_table,
is_uninitialized_sps_table,
load_sps_table_from_path,
)
from sglang.srt.speculative.dspark_components.dspark_sts import (
load_sts_calibration_from_path,
)
from sglang.srt.speculative.dspark_components.kernels.dspark_schedule import (
ScheduleVerifyLensTopk,
compute_sort_survival,
)
from sglang.srt.speculative.ragged_verify import (
RaggedVerifyLayout,
RaggedVerifyMode,
read_ragged_verify_mode,
round_up_grid,
)
from sglang.srt.utils.async_probe import (
maybe_assert_async,
maybe_detect_in_closed_range,
)
from sglang.srt.utils.common import require_mlp_tp_gather
logger = logging.getLogger(__name__)
class VerifyWindow(msgspec.Struct, frozen=True):
positions_2d: torch.Tensor
verify_cache_loc: torch.Tensor
verify_cache_loc_2d: torch.Tensor
class DSparkVerifyPlanner:
def __init__(
self,
*,
draft_model,
gamma: int,
model_runner,
device,
tp_rank: int,
server_args: ServerArgs,
verify_num_draft_tokens: int,
) -> None:
self.draft_model = draft_model
self.gamma = gamma
self.model_runner = model_runner
self.device = device
self.server_args = server_args
self.verify_num_draft_tokens = verify_num_draft_tokens
self._align_verify_tokens_to_graph_tier = (
server_args.speculative_dspark_align_verify_tokens_to_graph_tier
)
self._confidence_head = getattr(self.draft_model, "confidence_head", None)
sts_path = server_args.speculative_dspark_confidence_sts_path
if sts_path and self._confidence_head is not None:
calibration = load_sts_calibration_from_path(sts_path)
sts_temperatures = torch.tensor(
calibration.temperatures, dtype=torch.float32, device=device
)
if envs.SGLANG_DSPARK_STS_COLLECT_PATH.get() and not bool(
torch.all(sts_temperatures == 1.0)
):
raise ValueError(
"DSpark STS data collection (SGLANG_DSPARK_STS_COLLECT_PATH) "
"requires identity temperatures, but a non-identity calibration "
f"was loaded from {sts_path}. Collect pre-calibration logits with "
"no table (omit --speculative-dspark-confidence-sts-path)."
)
if sts_temperatures.numel() != self.gamma:
raise ValueError(
"DSpark STS calibration was fit for gamma="
f"{sts_temperatures.numel()} but the runtime gamma is "
f"{self.gamma}; refit the table for gamma={self.gamma} or omit "
"--speculative-dspark-confidence-sts-path."
)
self._confidence_head.sts_temperatures = sts_temperatures
if tp_rank == 0:
logger.info(
"DSpark STS calibration loaded from %s (gamma=%d); per-position "
"temperatures applied to confidence-head survival.",
sts_path,
self.gamma,
)
elif sts_path and self._confidence_head is None:
if tp_rank == 0:
logger.warning(
"DSpark STS calibration path given but no confidence head present "
"(static mode / head-less checkpoint); ignoring %s.",
sts_path,
)
self._ragged_verify_mode = read_ragged_verify_mode()
self._schedule_cfg = DSparkScheduleConfig(gamma=self.gamma)
self._budget_planner: Optional[HostConfidenceBudgetPlanner] = None
self._dynamic_graph_tier = False
self._dp_tier_gather_enabled = False
self._is_verify_all = True
if self._ragged_verify_mode is not RaggedVerifyMode.STATIC:
if self._confidence_head is None:
raise ValueError(
f"DSpark ragged-verify mode {self._ragged_verify_mode.value!r} "
f"schedules per-request verify lengths from the draft confidence "
f"head, but this DSpark draft checkpoint has no confidence head -- "
f"the checkpoint is wrong/incomplete (it ships no "
f"enable_confidence_head + trained confidence_head weights). Use a "
f"draft checkpoint that includes the confidence head, or run "
f"SGLANG_RAGGED_VERIFY_MODE=static."
)
self._require_prep_in_cuda_graph()
sps_table = build_sps_cost_table(
server_args=self.server_args,
verify_num_draft_tokens=self.verify_num_draft_tokens,
)
self._is_verify_all = (
self._ragged_verify_mode is RaggedVerifyMode.COMPACT
and is_uninitialized_sps_table(sps_table)
)
relay_lag_steps = (
0
if self.server_args.disable_overlap_schedule
else CONFIDENCE_RELAY_RING_LAG
)
self._budget_planner = HostConfidenceBudgetPlanner(
sps_table=sps_table,
cfg=self._schedule_cfg,
model_runner=self.model_runner,
relay_lag_steps=relay_lag_steps,
)
self._dynamic_graph_tier = not is_dp_attention_enabled()
self._dp_tier_gather_enabled = (
self._ragged_verify_mode is RaggedVerifyMode.COMPACT
and is_dp_attention_enabled()
and get_parallel().attn_tp_size == 1
and get_parallel().attn_cp_size == 1
and require_mlp_tp_gather(self.server_args)
and not self.server_args.disable_overlap_schedule
and not self.server_args.speculative_skip_dp_mlp_sync
and self.server_args.disaggregation_mode == "null"
and self.server_args.pp_size == 1
and not envs.SGLANG_SCHEDULER_SKIP_ALL_GATHER.get()
)
if tp_rank == 0:
sps_table_source = (
self.server_args.speculative_dspark_sps_table_path
or "uninitialized"
)
logger.info(
"DSpark ragged-verify scheduler enabled (mode=%s, lag=%d, "
"relay_lag=%d, sps_table=%s, graph_tier=%s).",
self._ragged_verify_mode.value,
self._budget_planner.lag_steps,
relay_lag_steps,
sps_table_source,
(
"dynamic"
if self._dynamic_graph_tier
else (
"dp-gathered" if self._dp_tier_gather_enabled else "pinned"
)
),
)
if isinstance(sps_table, SpsCostTable) and is_uninitialized_sps_table(
sps_table
):
logger.warning(
"DSpark SPS table is uninitialized (flat): the verify "
"budget degenerates to verify-all (zero scheduling gain). "
"Pass a profiled --speculative-dspark-sps-table-path."
)
def _require_prep_in_cuda_graph(self) -> None:
if not envs.SGLANG_PREP_IN_CUDA_GRAPH.get():
raise ValueError(
f"DSpark ragged-verify mode {self._ragged_verify_mode.value!r} "
f"requires SGLANG_PREP_IN_CUDA_GRAPH=1 (the captured-graph prepare "
f"path). It is currently disabled, which would put per-step "
f"verify_lens_cpu host reads on the critical path. Set "
f"SGLANG_PREP_IN_CUDA_GRAPH=1 or run SGLANG_RAGGED_VERIFY_MODE=static."
)
@property
def carries_confidence(self) -> bool:
return self._confidence_head is not None
@property
def last_confidence_raw(self) -> Optional[torch.Tensor]:
if self._confidence_head is None:
return None
return self._confidence_head._last_confidence_raw
@property
def schedules_verify_budget(self) -> bool:
return self._budget_planner is not None
@property
def is_compact_mode(self) -> bool:
return self._ragged_verify_mode is RaggedVerifyMode.COMPACT
@property
def is_verify_all(self) -> bool:
return self._is_verify_all
@property
def mode_value(self) -> str:
return self._ragged_verify_mode.value
@property
def lag_steps(self) -> Optional[int]:
if self._budget_planner is None:
return None
return self._budget_planner.lag_steps
def take_budget_decision(self) -> Optional[VerifyBudgetDecision]:
if self._budget_planner is None:
return None
return self._budget_planner.take_last_decision()
def should_run_compact(self, *, layout: Optional[RaggedVerifyLayout]) -> bool:
return (
self._ragged_verify_mode is RaggedVerifyMode.COMPACT and layout is not None
)
def compute_confidence_tensor(
self,
*,
draft_hidden: Optional[torch.Tensor],
anchor_tokens: torch.Tensor,
draft_tokens: torch.Tensor,
confidence_tap: Optional[torch.Tensor] = None,
) -> Optional[torch.Tensor]:
if self._confidence_head is None:
return None
compute_confidence_hook = getattr(self.draft_model, "compute_confidence", None)
if compute_confidence_hook is not None:
assert (
confidence_tap is not None
), "dsv4 compute_confidence needs the compute_base_logits tap"
with torch.inference_mode():
return compute_confidence_hook(
anchor_tokens=anchor_tokens,
sampled_tokens=draft_tokens,
x_post_hc=confidence_tap,
)
assert draft_hidden is not None
return compute_confidence(
draft_hidden=draft_hidden,
anchor_tokens=anchor_tokens,
draft_tokens=draft_tokens,
confidence_head=self._confidence_head,
markov_head=self.draft_model.markov_head,
gamma=self.gamma,
)
def prepare_verify_budget(
self, batch: ScheduleBatch, future_map: FutureMap
) -> None:
draft_input = batch.spec_info
if self._budget_planner is None:
return
if draft_input is None:
local_tier_num_tokens = 0 if batch.batch_size() == 0 else -1
self._maybe_gather_dp_verify_tier(
batch=batch, local_tier_num_tokens=local_tier_num_tokens
)
return
if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
self._budget_planner.note_non_decode_step()
self._maybe_gather_dp_verify_tier(batch=batch, local_tier_num_tokens=0)
return
resolved = future_map.resolve_confidence_cpu(batch)
draft_input.verify_token_budget = self._budget_from_resolved(
resolved=resolved, req_pool_indices_cpu=batch.req_pool_indices_cpu
)
batch.spec_verify_tier_num_tokens = local_verify_tier_num_tokens(
bs=batch.batch_size(),
verify_token_budget=draft_input.verify_token_budget,
verify_num_draft_tokens=self.verify_num_draft_tokens,
min_verify_len=self._schedule_cfg.min_verify_len,
)
self._maybe_gather_dp_verify_tier(
batch=batch, local_tier_num_tokens=batch.spec_verify_tier_num_tokens
)
def _maybe_gather_dp_verify_tier(
self, *, batch: ScheduleBatch, local_tier_num_tokens: int
) -> None:
if not self._dp_tier_gather_enabled:
return
if batch.is_extend_in_batch:
batch.global_spec_verify_tier_num_tokens = None
return
cpu_group = get_tp_group().cpu_group
local_tensor = torch.tensor([local_tier_num_tokens], dtype=torch.int64)
gathered = torch.empty(
(torch.distributed.get_world_size(group=cpu_group),), dtype=torch.int64
)
torch.distributed.all_gather_into_tensor(
gathered, local_tensor, group=cpu_group
)
batch.global_spec_verify_tier_num_tokens = gathered.tolist()
def note_non_decode_step(self) -> None:
if self._budget_planner is not None:
self._budget_planner.note_non_decode_step()
def set_forced_budget_frac(self, frac) -> None:
if self._budget_planner is not None:
self._budget_planner.forced_budget_frac = frac
def compute_budget_sync(
self,
*,
confidence: torch.Tensor,
prefix_lens: torch.Tensor,
req_pool_indices: torch.Tensor,
) -> Optional[int]:
del prefix_lens
if self._budget_planner is None:
return None
req_pool_indices_cpu = req_pool_indices.to("cpu").to(torch.int64)
generation = self.model_runner.req_to_token_pool.req_generation[
req_pool_indices_cpu
].clone()
resolved = ResolvedConfidence(
confidence=confidence.to("cpu"),
generation=generation,
)
return self._budget_from_resolved(
resolved=resolved, req_pool_indices_cpu=req_pool_indices_cpu
)
def resolve_verify_token_budget(
self,
*,
draft_input: DFlashDraftInputV2,
confidence: Optional[torch.Tensor],
prefix_lens: torch.Tensor,
req_pool_indices: torch.Tensor,
) -> Optional[int]:
"""Per-step verify-token budget: under overlap it was precomputed into
the draft input by prepare_verify_budget; otherwise compute it now."""
if not self.schedules_verify_budget or confidence is None:
return None
if not self.server_args.disable_overlap_schedule:
return draft_input.verify_token_budget
return self.compute_budget_sync(
confidence=confidence,
prefix_lens=prefix_lens,
req_pool_indices=req_pool_indices,
)
def confidence_budget_prepare(self):
if not self.schedules_verify_budget:
return None
return self.prepare_verify_budget
def _budget_from_resolved(
self,
*,
resolved: Optional[ResolvedConfidence],
req_pool_indices_cpu: torch.Tensor,
) -> Optional[int]:
if resolved is None:
self._budget_planner.note_non_decode_step()
return None
current_generation = self.model_runner.req_to_token_pool.req_generation[
req_pool_indices_cpu.to(torch.int64)
]
return int(
self._budget_planner.compute_budget(
confidence=resolved.confidence,
generation=resolved.generation,
current_generation=current_generation,
req_pool_indices_cpu=req_pool_indices_cpu,
)
)
def schedule_layout(
self,
*,
req_pool_indices: torch.Tensor,
prefix_lens: torch.Tensor,
device: torch.device,
confidence: Optional[torch.Tensor],
budget: Optional[int],
global_num_reqs: Optional[int] = None,
dp_tier_num_tokens: Optional[int] = None,
) -> Optional[RaggedVerifyLayout]:
if self._ragged_verify_mode is RaggedVerifyMode.STATIC:
return None
verify_lens = self._schedule_verify_lens(
req_pool_indices=req_pool_indices,
prefix_lens=prefix_lens,
device=device,
confidence=confidence,
budget=self._budget_aligned_to_graph_tier(
req_pool_indices=req_pool_indices,
budget=budget,
global_num_reqs=global_num_reqs,
dp_tier_num_tokens=dp_tier_num_tokens,
),
)
if verify_lens is None:
assert dp_tier_num_tokens is None, (
"dp tier agreement present but local verify lens are None; "
"the gathered hint and the local budget diverged"
)
if self._ragged_verify_mode is RaggedVerifyMode.COMPACT:
return uniform_ragged_layout(
bs=len(req_pool_indices),
device=device,
verify_num_draft_tokens=self.verify_num_draft_tokens,
ragged_verify_mode=self._ragged_verify_mode,
model_runner=self.model_runner,
tier_num_reqs=global_num_reqs,
)
return None
bs = int(verify_lens.shape[0])
tier_num_reqs = bs if global_num_reqs is None else global_num_reqs
if dp_tier_num_tokens is not None:
assert global_num_reqs is not None, (
"dp tier agreement requires the dp-global request count; "
"keying the tier off the local bs diverges across ranks"
)
tier_num_tokens = dp_tier_num_tokens
elif self._dynamic_graph_tier and budget is not None:
tier_num_tokens = local_verify_tier_num_tokens(
bs=tier_num_reqs,
verify_token_budget=budget,
verify_num_draft_tokens=self.verify_num_draft_tokens,
min_verify_len=self._schedule_cfg.min_verify_len,
)
else:
tier_num_tokens = None
if ragged_layout_exceeds_captured_grid(
num_reqs=tier_num_reqs,
verify_num_draft_tokens=self.verify_num_draft_tokens,
model_runner=self.model_runner,
tier_tokens_hint=tier_num_tokens,
):
return None
graph_num_tokens_floor = verify_layout_graph_num_tokens_floor(
num_reqs=tier_num_reqs,
ragged_verify_mode=self._ragged_verify_mode,
verify_num_draft_tokens=self.verify_num_draft_tokens,
model_runner=self.model_runner,
tier_num_tokens=tier_num_tokens,
)
capture_num_tokens = ragged_capture_num_tokens(model_runner=self.model_runner)
if graph_num_tokens_floor > 0 and capture_num_tokens is not None:
graph_num_tokens = round_up_grid(graph_num_tokens_floor, capture_num_tokens)
return RaggedVerifyLayout.from_verify_lens_device(
verify_lens=verify_lens, graph_num_tokens=graph_num_tokens
)
verify_lens_cpu = verify_lens.to("cpu").tolist()
grid = verify_layout_grid(
verify_lens_cpu=verify_lens_cpu,
ragged_verify_mode=self._ragged_verify_mode,
model_runner=self.model_runner,
)
return RaggedVerifyLayout.from_verify_lens(
verify_lens_cpu=verify_lens_cpu,
device=device,
grid=grid,
graph_num_tokens_floor=graph_num_tokens_floor,
)
def _budget_aligned_to_graph_tier(
self,
*,
req_pool_indices: torch.Tensor,
budget: Optional[int],
global_num_reqs: Optional[int],
dp_tier_num_tokens: Optional[int],
) -> Optional[int]:
# Flag off (default): returns budget unchanged, so the schedule below is
# byte-for-byte the original. On: ceils role 1's verify-token total up to the
# padded graph tier graph_num_tokens = round_up(dp-max tier, captured token
# bucket), which folds in the cuda-graph bucket round-up (H1) and the dp
# cross-rank max (H2); role 2 (the single top-k) then admits that many real
# draft tokens. graph_num_tokens is derived from the same (request count,
# gathered dp tier, original budget) inputs the layout below uses, so the two
# agree by construction -- this only feeds the larger budget into the top-k,
# it does not touch the layout's own tier computation.
if not self._align_verify_tokens_to_graph_tier or budget is None:
return budget
tier_num_reqs = (
int(req_pool_indices.shape[0])
if global_num_reqs is None
else global_num_reqs
)
if dp_tier_num_tokens is not None:
tier_num_tokens = dp_tier_num_tokens
elif self._dynamic_graph_tier:
tier_num_tokens = local_verify_tier_num_tokens(
bs=tier_num_reqs,
verify_token_budget=budget,
verify_num_draft_tokens=self.verify_num_draft_tokens,
min_verify_len=self._schedule_cfg.min_verify_len,
)
else:
tier_num_tokens = None
graph_num_tokens_floor = verify_layout_graph_num_tokens_floor(
num_reqs=tier_num_reqs,
ragged_verify_mode=self._ragged_verify_mode,
verify_num_draft_tokens=self.verify_num_draft_tokens,
model_runner=self.model_runner,
tier_num_tokens=tier_num_tokens,
)
capture_num_tokens = ragged_capture_num_tokens(model_runner=self.model_runner)
if graph_num_tokens_floor <= 0 or capture_num_tokens is None:
return budget
graph_num_tokens = round_up_grid(graph_num_tokens_floor, capture_num_tokens)
return graph_tier_fill_budget(
graph_num_tokens=graph_num_tokens,
bs=int(req_pool_indices.shape[0]),
verify_num_draft_tokens=self.verify_num_draft_tokens,
min_verify_len=self._schedule_cfg.min_verify_len,
)
def _schedule_verify_lens(
self,
*,
req_pool_indices: torch.Tensor,
prefix_lens: torch.Tensor,
device: torch.device,
confidence: Optional[torch.Tensor],
budget: Optional[int],
) -> Optional[torch.Tensor]:
if self._budget_planner is None or confidence is None or budget is None:
return None
verify_lens = ScheduleVerifyLensTopk.execute(
confidence=confidence,
budget=budget,
cfg=self._schedule_cfg,
).to(device=device, dtype=torch.int32)
if envs.SGLANG_ENABLE_ASYNC_ASSERT.get():
verify_lens_64 = verify_lens.to(torch.int64)
effective_floor = max(self._schedule_cfg.min_verify_len, 1)
maybe_assert_async(
(verify_lens_64 - effective_floor).sum() <= budget,
f"DSpark verify-len budget violated (budget={budget})",
)
if envs.SGLANG_DSPARK_DEBUG_CONFIDENCE_PREFIX_SCHEDULER.get():
self._log_verify_lens_decision(
req_pool_indices=req_pool_indices,
prefix_lens=prefix_lens,
budget=budget,
sort_survival=compute_sort_survival(confidence),
verify_lens=verify_lens,
)
broadcast_group, group_size = verify_lens_broadcast_group(
tp_size=self.server_args.tp_size
)
if group_size > 1:
broadcast_group.broadcast(verify_lens, src=0)
return verify_lens
def _log_verify_lens_decision(
self,
*,
req_pool_indices: torch.Tensor,
prefix_lens: torch.Tensor,
budget: int,
sort_survival: torch.Tensor,
verify_lens: torch.Tensor,
) -> None:
cfg = self._schedule_cfg
max_len = cfg.resolved_max_verify_len()
req_ids = req_pool_indices.tolist()
prefixes = prefix_lens.tolist()
lens = verify_lens.tolist()
sort_rows = sort_survival.to(torch.float32).tolist()
logger.info(
"[DSPARK-CPS] num_reqs=%d budget=%d gamma=%d verify_len_range=[%d,%d]",
len(req_ids),
budget,
cfg.gamma,
cfg.min_verify_len,
max_len,
)
for row in range(len(req_ids)):
survival_str = "[" + ", ".join(f"{p:.3f}" for p in sort_rows[row]) + "]"
logger.info(
"[DSPARK-CPS] req=%d prefix=%d verify_len=%d sort_survival=%s",
int(req_ids[row]),
int(prefixes[row]),
int(lens[row]),
survival_str,
)
def local_verify_tier_num_tokens(
*,
bs: int,
verify_token_budget: Optional[int],
verify_num_draft_tokens: int,
min_verify_len: int,
) -> int:
if verify_token_budget is None:
return -1
floor_tokens = bs * max(min_verify_len, 1)
return min(floor_tokens + verify_token_budget, bs * verify_num_draft_tokens)
def graph_tier_fill_budget(
*,
graph_num_tokens: int,
bs: int,
verify_num_draft_tokens: int,
min_verify_len: int,
) -> int:
# top-k budget (tokens above the per-request floor) that makes the scheduled
# total reach the padded graph tier, capped at bs * verify_num_draft_tokens
# since a request cannot verify more than its proposed drafts. Inverse of
# local_verify_tier_num_tokens: total = floor_tokens + budget.
fill_total = min(graph_num_tokens, bs * verify_num_draft_tokens)
floor_tokens = bs * max(min_verify_len, 1)
return max(0, fill_total - floor_tokens)
def dp_global_verify_tier_num_tokens(
*,
global_tier_num_tokens: Optional[list[int]],
) -> Optional[int]:
if global_tier_num_tokens is None:
return None
if any(tier_num_tokens < 0 for tier_num_tokens in global_tier_num_tokens):
return None
max_tier_num_tokens = max(global_tier_num_tokens, default=0)
return max_tier_num_tokens if max_tier_num_tokens > 0 else None
def idle_ragged_layout(
*,
tier_num_reqs: int,
dp_tier_num_tokens: Optional[int],
device: torch.device,
verify_num_draft_tokens: int,
model_runner,
) -> Optional[RaggedVerifyLayout]:
if ragged_capture_num_tokens(model_runner=model_runner) is None:
dp_tier_num_tokens = None
if dp_tier_num_tokens is None:
return uniform_ragged_layout(
bs=tier_num_reqs,
device=device,
verify_num_draft_tokens=verify_num_draft_tokens,
ragged_verify_mode=RaggedVerifyMode.COMPACT,
model_runner=model_runner,
)
if ragged_layout_exceeds_captured_grid(
num_reqs=tier_num_reqs,
verify_num_draft_tokens=verify_num_draft_tokens,
model_runner=model_runner,
tier_tokens_hint=dp_tier_num_tokens,
):
return None
verify_lens_cpu = [1] * tier_num_reqs
grid = verify_layout_grid(
verify_lens_cpu=verify_lens_cpu,
ragged_verify_mode=RaggedVerifyMode.COMPACT,
model_runner=model_runner,
)
return RaggedVerifyLayout.from_verify_lens(
verify_lens_cpu=verify_lens_cpu,
device=device,
grid=grid,
graph_num_tokens_floor=dp_tier_num_tokens,
)
def uniform_ragged_layout(
*,
bs: int,
device: torch.device,
verify_num_draft_tokens: int,
ragged_verify_mode: RaggedVerifyMode,
model_runner,
tier_num_reqs: Optional[int] = None,
) -> Optional[RaggedVerifyLayout]:
tier_num_reqs = bs if tier_num_reqs is None else tier_num_reqs
if ragged_layout_exceeds_captured_grid(
num_reqs=tier_num_reqs,
verify_num_draft_tokens=verify_num_draft_tokens,
model_runner=model_runner,
):
return None
verify_lens_cpu = [verify_num_draft_tokens] * bs
grid = verify_layout_grid(
verify_lens_cpu=verify_lens_cpu,
ragged_verify_mode=ragged_verify_mode,
model_runner=model_runner,
)
graph_num_tokens_floor = verify_layout_graph_num_tokens_floor(
num_reqs=tier_num_reqs,
ragged_verify_mode=ragged_verify_mode,
verify_num_draft_tokens=verify_num_draft_tokens,
model_runner=model_runner,
)
return RaggedVerifyLayout.from_verify_lens(
verify_lens_cpu=verify_lens_cpu,
device=device,
grid=grid,
graph_num_tokens_floor=graph_num_tokens_floor,
)
def verify_lens_broadcast_group(*, tp_size: int) -> tuple:
if is_dp_attention_enabled():
return get_parallel().attn_tp_group, get_parallel().attn_tp_size
return get_tp_group(), tp_size
def verify_layout_grid(
*,
verify_lens_cpu: list[int],
ragged_verify_mode: RaggedVerifyMode,
model_runner,
) -> list[int]:
total = sum(verify_lens_cpu)
if ragged_verify_mode is not RaggedVerifyMode.COMPACT:
return [total]
capture_num_tokens = ragged_capture_num_tokens(model_runner=model_runner)
if capture_num_tokens is None:
return [total]
return capture_num_tokens
def verify_layout_graph_num_tokens_floor(
*,
num_reqs: int,
ragged_verify_mode: RaggedVerifyMode,
verify_num_draft_tokens: int,
model_runner,
tier_num_tokens: Optional[int] = None,
) -> int:
if (
ragged_verify_mode is not RaggedVerifyMode.COMPACT
or ragged_capture_num_tokens(model_runner=model_runner) is None
):
return 0
if tier_num_tokens is not None:
return min(tier_num_tokens, num_reqs * verify_num_draft_tokens)
return num_reqs * verify_num_draft_tokens
def ragged_capture_num_tokens(*, model_runner) -> Optional[list[int]]:
runner = model_runner.decode_cuda_graph_runner
if runner is None or not runner.ragged_verify_mode:
return None
return runner.capture_num_tokens
def ragged_capture_max_slots(*, model_runner) -> Optional[int]:
runner = model_runner.decode_cuda_graph_runner
if runner is None or not runner.ragged_verify_mode:
return None
return runner.max_bs
def ragged_layout_exceeds_captured_grid(
*,
num_reqs: int,
verify_num_draft_tokens: int,
model_runner,
tier_tokens_hint: Optional[int] = None,
) -> bool:
capture_num_tokens = ragged_capture_num_tokens(model_runner=model_runner)
if capture_num_tokens is None:
return False
max_slots = ragged_capture_max_slots(model_runner=model_runner)
if max_slots is not None and num_reqs > max_slots:
return True
tier_tokens = (
tier_tokens_hint
if tier_tokens_hint is not None
else num_reqs * verify_num_draft_tokens
)
return tier_tokens > capture_num_tokens[-1]
def alloc_verify_window(
*,
batch: ScheduleBatch,
bs: int,
device: str,
verify_num_draft_tokens: int,
block_pos_offsets: torch.Tensor,
model_runner,
) -> VerifyWindow:
prefix_lens = batch.seq_lens
verify_w = verify_num_draft_tokens
positions_2d = prefix_lens.unsqueeze(1) + block_pos_offsets
verify_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_w,
batch_size=bs,
draft_token_num=verify_w,
device=device,
)
verify_cache_loc_2d = verify_cache_loc.view(bs, verify_w)
return VerifyWindow(
positions_2d=positions_2d,
verify_cache_loc=verify_cache_loc,
verify_cache_loc_2d=verify_cache_loc_2d,
)
def apply_logits_adjustments_strided(
*,
next_token_logits: torch.Tensor,
sampling_info,
verify_num_draft_tokens: int,
) -> None:
if sampling_info is None:
return
apply_dflash_verify_logits_adjustments(
next_token_logits=next_token_logits,
sampling_info=sampling_info,
draft_token_num=verify_num_draft_tokens,
)
def build_markov_embed_stack(
*,
anchor_tokens: torch.Tensor,
draft_tokens: torch.Tensor,
markov_head,
gamma: int,
) -> torch.Tensor:
prev_seq = torch.cat(
[anchor_tokens.view(-1, 1), draft_tokens[:, : gamma - 1]], dim=1
)
return markov_head.get_prev_embeddings(prev_seq)
def compute_confidence(
*,
draft_hidden: torch.Tensor,
anchor_tokens: torch.Tensor,
draft_tokens: torch.Tensor,
confidence_head,
markov_head,
gamma: int,
) -> torch.Tensor:
assert confidence_head is not None
if confidence_head.with_markov:
markov_embed_stack = build_markov_embed_stack(
anchor_tokens=anchor_tokens,
draft_tokens=draft_tokens,
markov_head=markov_head,
gamma=gamma,
)
else:
markov_embed_stack = None
confidence_raw = confidence_head(draft_hidden, markov_embed_stack)
confidence = confidence_head.apply_sts(confidence_raw)
maybe_detect_in_closed_range(confidence, 0.0, 1.0, "DSpark confidence")
return confidence
class DSparkScheduleConfig(msgspec.Struct):
gamma: int
min_verify_len: int = 1
max_verify_len: int = 0
survival_eps: float = 1e-6
def resolved_max_verify_len(self) -> int:
return self.max_verify_len or (self.gamma + 1)
def validate(self) -> None:
max_len = self.resolved_max_verify_len()
if self.gamma < 1:
raise ValueError(f"DSpark gamma must be >= 1, got {self.gamma}.")
if not (0 <= self.min_verify_len <= max_len <= self.gamma + 1):
raise ValueError(
"DSpark verify-len config must satisfy 0 <= min <= max <= gamma+1, "
f"got min={self.min_verify_len}, max={max_len}, gamma={self.gamma}."
)
if self.survival_eps < 0:
raise ValueError(f"survival_eps must be >= 0, got {self.survival_eps}.")
class VerifyBudgetDecision(msgspec.Struct):
budget: int
predicted_step_seconds: Optional[float] = None
predicted_theta: Optional[float] = None
def compute_verify_token_budget(
*,
history_survival_probs: torch.Tensor,
sps_table: Union[SpsCostTable, SpsAdditiveCostTable],
cfg: DSparkScheduleConfig,
) -> VerifyBudgetDecision:
num_requests = history_survival_probs.shape[0]
max_len = cfg.resolved_max_verify_len()
candidates = history_survival_probs[:, :max_len].flatten()
candidates = candidates[candidates >= cfg.survival_eps].to(torch.float64)
candidates_sorted = torch.sort(candidates, descending=True).values
prefix_sum = torch.cumsum(candidates_sorted, dim=0)
tau_star = num_requests + torch.cat(
[torch.zeros(1, dtype=torch.float64), prefix_sum]
)
if isinstance(sps_table, SpsAdditiveCostTable):
step_time = _additive_step_time_tensor(
table=sps_table,
num_requests=int(num_requests),
num_budgets=int(tau_star.numel()),
)
theta = tau_star / step_time
idx = int(torch.argmax(theta))
predicted_step_seconds = float(step_time[idx])
else:
batch_tokens = num_requests + torch.arange(tau_star.numel(), dtype=torch.int64)
sps = _lookup_sps_tensor(sps_table=sps_table, batch_tokens=batch_tokens)
theta = tau_star * sps
idx = int(torch.argmax(theta))
sps_at_idx = float(sps[idx])
predicted_step_seconds = 1.0 / sps_at_idx if sps_at_idx > 0 else None
return VerifyBudgetDecision(
budget=idx,
predicted_step_seconds=predicted_step_seconds,
predicted_theta=float(theta[idx]),
)
def _lookup_sps_tensor(
*, sps_table: SpsCostTable, batch_tokens: torch.Tensor
) -> torch.Tensor:
probes = torch.tensor(sps_table.sample_batch_tokens, dtype=torch.int64)
sps = torch.tensor(sps_table.sample_steps_per_sec, dtype=torch.float64)
idx = torch.bucketize(batch_tokens, probes, right=True) - 1
idx = idx.clamp_(0, probes.numel() - 1)
return sps[idx]
def _additive_step_time_tensor(
*, table: SpsAdditiveCostTable, num_requests: int, num_budgets: int
) -> torch.Tensor:
floor = table.bias_seconds + _interp_clamped(
table.bs_probes, table.alpha_seconds, float(num_requests)
)
m_probes = torch.tensor(table.m_probes, dtype=torch.float64)
theta_vals = torch.tensor(table.theta_seconds, dtype=torch.float64)
m = (num_requests + torch.arange(num_budgets, dtype=torch.float64)).clamp_(
min=float(table.m_probes[0]), max=float(table.m_probes[-1])
)
hi = torch.bucketize(m, m_probes, right=True).clamp_(1, m_probes.numel() - 1)
lo = hi - 1
span = (m_probes[hi] - m_probes[lo]).clamp_(min=1e-9)
frac = (m - m_probes[lo]) / span
theta_at_m = theta_vals[lo] + frac * (theta_vals[hi] - theta_vals[lo])
return floor + theta_at_m
class HostConfidenceBudgetPlanner:
def __init__(
self,
*,
sps_table: SpsCostTable,
cfg: DSparkScheduleConfig,
model_runner,
relay_lag_steps: int = 1,
) -> None:
cfg.validate()
self.sps_table = sps_table
self.cfg = cfg
self._model_runner = model_runner
self.forced_budget_frac: Optional[float] = None
self.last_decision: Optional[VerifyBudgetDecision] = None
self.lag_steps = max(
int(envs.SGLANG_DSPARK_CONFIDENCE_RELAY_LAG_STEPS.get()), 1
)
self.carry_steps = max(self.lag_steps - int(relay_lag_steps), 0)
self._carry_confidence: Optional[torch.Tensor] = None
self._carry_generation: Optional[torch.Tensor] = None
self._carry_pos = 0
def compute_budget(
self,
*,
confidence: torch.Tensor,
generation: torch.Tensor,
current_generation: torch.Tensor,
req_pool_indices_cpu: torch.Tensor,
) -> int:
lagged_confidence, lagged_generation = self._shift_to_lag(
confidence=confidence,
generation=generation,
req_pool_indices_cpu=req_pool_indices_cpu,
)
survival = self._two_steps_prior_survival(
lagged_confidence=lagged_confidence,
lagged_generation=lagged_generation,
current_generation=current_generation,
)
forced_frac = self.forced_budget_frac
if forced_frac is not None:
full_budget = int(survival[:, : self.cfg.resolved_max_verify_len()].numel())
forced_budget = max(0, int(float(forced_frac) * full_budget))
self.last_decision = VerifyBudgetDecision(budget=forced_budget)
return forced_budget
decision = compute_verify_token_budget(
history_survival_probs=survival,
sps_table=self.sps_table,
cfg=self.cfg,
)
self.last_decision = decision
return decision.budget
def take_last_decision(self) -> Optional[VerifyBudgetDecision]:
decision = self.last_decision
self.last_decision = None
return decision
def note_non_decode_step(self) -> None:
self.last_decision = None
def _shift_to_lag(
self,
*,
confidence: torch.Tensor,
generation: torch.Tensor,
req_pool_indices_cpu: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
if self.carry_steps == 0:
return confidence, generation
self._ensure_carry(gamma=confidence.shape[-1])
slot = self._carry_pos % self.carry_steps
rows = req_pool_indices_cpu.to(torch.int64)
lagged_confidence = self._carry_confidence[slot, rows].clone()
lagged_generation = self._carry_generation[slot, rows].clone()
self._carry_confidence[slot, rows] = confidence.to(torch.float32)
self._carry_generation[slot, rows] = generation.to(torch.int64)
self._carry_pos += 1
return lagged_confidence, lagged_generation
def _two_steps_prior_survival(
self,
*,
lagged_confidence: torch.Tensor,
lagged_generation: torch.Tensor,
current_generation: torch.Tensor,
) -> torch.Tensor:
k_survival = torch.cumprod(lagged_confidence.to(torch.float32), dim=1)
current_gen = current_generation.to(torch.int64)
fresh = (
(current_gen >= 1) & (lagged_generation.to(torch.int64) == current_gen)
).view(-1, 1)
return torch.where(fresh, k_survival, torch.ones_like(k_survival))
def _ensure_carry(self, *, gamma: int) -> None:
if self._carry_confidence is not None:
return
req_pool_size = int(self._model_runner.req_to_token_pool.req_to_token.shape[0])
self._carry_confidence = torch.zeros(
(self.carry_steps, req_pool_size, gamma), dtype=torch.float32
)
self._carry_generation = torch.zeros(
(self.carry_steps, req_pool_size),
dtype=torch.int64,
)
def build_sps_cost_table(
*,
server_args: ServerArgs,
verify_num_draft_tokens: int,
) -> Union[SpsCostTable, SpsAdditiveCostTable]:
sps_table_path = server_args.speculative_dspark_sps_table_path
if sps_table_path:
return load_sps_table_from_path(sps_table_path)
max_batch_tokens = max(
1,
int(server_args.max_running_requests or 1) * verify_num_draft_tokens,
)
return build_uninitialized_sps_table(max_batch_tokens=max_batch_tokens)