[Spec] Add DSpark: confidence-scheduled speculative decoding (#30261)

Co-authored-by: sglang-bot <232288953+sglang-bot@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Liangsheng Yin <lsyincs@gmail.com>
Co-authored-by: Liangsheng Yin <hnyls2002@gmail.com>
This commit is contained in:
sglang-bot
2026-07-12 17:25:26 -05:00
committed by GitHub
co-authored by sglang-bot Claude Code Codex Liangsheng Yin Liangsheng Yin
parent 24d59d8d74
commit 6cc9352dfe
84 changed files with 17700 additions and 287 deletions
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
from __future__ import annotations
import argparse
import glob
import logging
import math
from pathlib import Path
from typing import Optional
import torch
from sglang.srt.speculative.dspark_components.dspark_sts import (
DSparkStsCalibration,
)
logger = logging.getLogger(__name__)
_EPS_PROB = 1e-8
def default_temperature_grid() -> torch.Tensor:
return torch.logspace(math.log10(0.1), math.log10(10.0), steps=41)
def expected_calibration_error(
*,
probs: torch.Tensor,
targets: torch.Tensor,
num_bins: int,
) -> float:
probs = probs.reshape(-1).to(torch.float64).clamp(_EPS_PROB, 1.0 - _EPS_PROB)
targets = targets.reshape(-1).to(torch.float64)
total = probs.numel()
if total == 0:
return float("nan")
bin_index = (probs * num_bins).long().clamp_(0, num_bins - 1)
count = torch.zeros(num_bins, dtype=torch.float64)
pred_sum = torch.zeros(num_bins, dtype=torch.float64)
target_sum = torch.zeros(num_bins, dtype=torch.float64)
count.scatter_add_(0, bin_index, torch.ones_like(probs))
pred_sum.scatter_add_(0, bin_index, probs)
target_sum.scatter_add_(0, bin_index, targets)
denom = count.clamp_min(1.0)
bin_error = (pred_sum / denom - target_sum / denom).abs()
return float((bin_error * count).sum().item() / total)
def fit_sts_temperatures(
*,
logits: torch.Tensor,
prefix_mask: torch.Tensor,
grid: torch.Tensor,
num_bins: int = 15,
) -> dict[str, list[float]]:
logits = logits.to(torch.float64)
prefix_mask = prefix_mask.to(torch.float64)
num_samples, gamma = logits.shape
if num_samples == 0:
raise ValueError("fit_sts_temperatures requires at least one sample.")
grid_values = grid.to(torch.float64).tolist()
temperatures: list[float] = []
ece_before: list[float] = []
ece_after: list[float] = []
survival_at_one = torch.ones(num_samples, dtype=torch.float64)
survival_fitted = torch.ones(num_samples, dtype=torch.float64)
for position in range(gamma):
position_logits = logits[:, position]
position_target = prefix_mask[:, position]
survival_at_one = survival_at_one * torch.sigmoid(position_logits)
ece_before.append(
expected_calibration_error(
probs=survival_at_one,
targets=position_target,
num_bins=num_bins,
)
)
best_temperature = grid_values[0]
best_survival = survival_fitted * torch.sigmoid(
position_logits / best_temperature
)
best_ece = expected_calibration_error(
probs=best_survival, targets=position_target, num_bins=num_bins
)
for temperature in grid_values[1:]:
candidate_survival = survival_fitted * torch.sigmoid(
position_logits / temperature
)
candidate_ece = expected_calibration_error(
probs=candidate_survival,
targets=position_target,
num_bins=num_bins,
)
if candidate_ece < best_ece:
best_ece = candidate_ece
best_temperature = temperature
best_survival = candidate_survival
temperatures.append(float(best_temperature))
ece_after.append(float(best_ece))
survival_fitted = best_survival
return {
"temperatures": temperatures,
"ece_before": ece_before,
"ece_after": ece_after,
}
def load_collected_shards(*, data_glob: str) -> tuple[torch.Tensor, torch.Tensor]:
shard_paths = sorted(glob.glob(data_glob))
if not shard_paths:
raise ValueError(f"No STS data shards matched {data_glob!r}.")
logits_shards: list[torch.Tensor] = []
prefix_mask_shards: list[torch.Tensor] = []
shard_gamma: Optional[int] = None
for shard_path in shard_paths:
shard = torch.load(shard_path, map_location="cpu")
shard_logits = shard["logits"]
shard_prefix_mask = shard["prefix_mask"]
if shard_logits.shape != shard_prefix_mask.shape:
raise ValueError(
f"Shard {shard_path!r} logits / prefix_mask shape mismatch: "
f"{tuple(shard_logits.shape)} vs {tuple(shard_prefix_mask.shape)}."
)
if shard_gamma is None:
shard_gamma = int(shard_logits.shape[1])
elif int(shard_logits.shape[1]) != shard_gamma:
raise ValueError(
f"Shard {shard_path!r} gamma {int(shard_logits.shape[1])} disagrees "
f"with earlier shards' gamma {shard_gamma}."
)
logits_shards.append(shard_logits)
prefix_mask_shards.append(shard_prefix_mask)
return torch.cat(logits_shards, dim=0), torch.cat(prefix_mask_shards, dim=0)
def fit(
*,
data_glob: str,
out: Path,
num_bins: int = 15,
gamma: Optional[int] = None,
) -> None:
logits, prefix_mask = load_collected_shards(data_glob=data_glob)
resolved_gamma = int(logits.shape[1])
if gamma is not None and gamma != resolved_gamma:
raise ValueError(
f"Collected shards have gamma={resolved_gamma} but --gamma={gamma}."
)
num_samples = int(logits.shape[0])
result = fit_sts_temperatures(
logits=logits,
prefix_mask=prefix_mask,
grid=default_temperature_grid(),
num_bins=num_bins,
)
calibration = DSparkStsCalibration(
temperatures=result["temperatures"],
dataset=data_glob,
num_samples=num_samples,
ece_before=result["ece_before"],
ece_after=result["ece_after"],
)
out.write_text(calibration.to_json(), encoding="utf-8")
print(
f"Fit STS temperatures over {num_samples} samples (gamma={resolved_gamma}) "
f"-> {out}"
)
print("pos temperature ece_before ece_after")
for position in range(resolved_gamma):
print(
f"{position:>3} {result['temperatures'][position]:>11.4f} "
f"{result['ece_before'][position]:>10.4f} "
f"{result['ece_after'][position]:>9.4f}"
)
def main() -> None:
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser(
description="Fit DSpark Sequential Temperature Scaling (STS) calibration "
"temperatures from collected confidence shards."
)
parser.add_argument(
"--data-glob",
required=True,
help="Glob of collected .pt shards, each a dict with [n, gamma] "
"'logits' and 'prefix_mask' tensors.",
)
parser.add_argument(
"--out",
required=True,
type=Path,
help="Output STS calibration JSON path.",
)
parser.add_argument(
"--num-bins",
type=int,
default=15,
help="Number of equal-width ECE bins.",
)
parser.add_argument(
"--gamma",
type=int,
default=None,
help="Optional gamma override to validate the shards against.",
)
args = parser.parse_args()
fit(
data_glob=args.data_glob,
out=args.out,
num_bins=args.num_bins,
gamma=args.gamma,
)
if __name__ == "__main__":
main()
+12
View File
@@ -109,6 +109,9 @@ class RequestFuncOutput:
cached_tokens: int = 0
cached_tokens_details: Optional[Dict[str, Any]] = None
spec_accept_length: float = 0.0
spec_cap_length: float = 0.0
spec_block_accept_length: float = 0.0
spec_cap_lens_histogram: List[int] = field(default_factory=list)
@staticmethod
def init_new(request_func_input: RequestFuncInput):
@@ -483,6 +486,15 @@ async def async_request_openai_chat_completions(
output.spec_accept_length = (
_meta_info.get("spec_accept_length", 0.0) or 0.0
)
output.spec_cap_length = (
_meta_info.get("spec_cap_length", 0.0) or 0.0
)
output.spec_block_accept_length = (
_meta_info.get("spec_block_accept_length", 0.0) or 0.0
)
output.spec_cap_lens_histogram = (
_meta_info.get("spec_cap_lens_histogram", []) or []
)
if getattr(args, "cache_report", False):
_extract_cache_from_sglext(response_json, output)
else: