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
962 lines
35 KiB
Python
962 lines
35 KiB
Python
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,
|
|
)
|