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

346 lines
12 KiB
Python

"""Adaptive speculative decoding parameters.
Adjusts speculative_num_steps at runtime based on observed acceptance lengths.
"""
from __future__ import annotations
import bisect
import json
import logging
import math
from functools import cached_property
from typing import TYPE_CHECKING
from sglang.srt.utils import log_info_on_rank0
if TYPE_CHECKING:
from sglang.srt.server_args import ServerArgs
logger = logging.getLogger(__name__)
DEFAULT_ADAPTIVE_CONFIG: dict[str, dict] = {
"1": {
"candidate_steps": [1, 3, 7],
"up_hysteresis": 0.0,
"down_hysteresis": -0.25,
"ceiling_coeff": 0,
},
"8": {
"candidate_steps": [0, 1, 3],
"up_hysteresis": 0.0,
"down_hysteresis": 0.0,
"ceiling_coeff": 0,
},
"32": {
"candidate_steps": [0, 1],
"up_hysteresis": 0.0,
"down_hysteresis": 0.0,
"ceiling_coeff": 0,
},
"64": {
"candidate_steps": [0],
"up_hysteresis": 0.0,
"down_hysteresis": 0.0,
"ceiling_coeff": 0,
},
}
def adaptive_unsupported_reason(server_args: ServerArgs) -> str | None:
"""Return why adaptive spec cannot run under the given server args, or None if supported."""
from sglang.srt.arg_groups.overrides import resolved_view
if server_args.speculative_algorithm not in ("EAGLE", "EAGLE3"):
return (
f"speculative_algorithm={server_args.speculative_algorithm} "
"(only EAGLE/EAGLE3 are supported)"
)
if (
server_args.speculative_eagle_topk is not None
and server_args.speculative_eagle_topk != 1
):
return (
f"speculative_eagle_topk={server_args.speculative_eagle_topk} "
"(only topk=1 is supported)"
)
if resolved_view(server_args).enable_dp_attention:
return (
"enable_dp_attention=True is not supported "
"(adaptive tier decisions are not synchronized across DP ranks)"
)
if resolved_view(server_args).enable_multi_layer_eagle:
return (
"enable_multi_layer_eagle=True is not supported "
"(MultiLayerEagleWorkerV2 does not implement adaptive)"
)
if server_args.enable_two_batch_overlap:
return (
"enable_two_batch_overlap=True is not supported "
"(adaptive state swap would discard the TboAttnBackend wrapper)"
)
if server_args.enable_pdmux:
return (
"enable_pdmux=True is not supported "
"(adaptive state swap does not update decode_attn_backend_group)"
)
return None
def _load_adaptive_config(
cfg_path: str | None,
) -> tuple[dict, dict[int, dict]]:
"""Load and validate adaptive config.
Uses ``DEFAULT_ADAPTIVE_CONFIG`` when *cfg_path* is ``None``.
"""
if cfg_path is not None:
with open(cfg_path) as f:
cfg = json.load(f)
else:
cfg = DEFAULT_ADAPTIVE_CONFIG
bs_entries: dict[int, dict] = {}
for key, entry in cfg.items():
if not key.isdigit():
continue
steps = entry.get("candidate_steps")
if (
not isinstance(steps, list)
or not steps
or not all(isinstance(s, int) and s >= 0 for s in steps)
):
raise ValueError(
f"BS {key}: candidate_steps must be a list of non-negative ints, "
f"got {steps!r}"
)
bs_entries[int(key)] = entry
if not bs_entries:
raise ValueError(
"speculative_adaptive_config must contain at least one integer-string "
'BS key, e.g. {"1": {"candidate_steps": [1,3,7]}}. '
f"Got keys: {list(cfg.keys())}"
)
return cfg, bs_entries
def resolve_candidate_steps_from_config(
cfg_path: str | None = None,
) -> list[int]:
"""Union of every BS slot's candidate steps; sizes the runtime buffers."""
_, bs_entries = _load_adaptive_config(cfg_path)
all_steps: set[int] = set()
for entry in bs_entries.values():
all_steps.update(entry["candidate_steps"])
return sorted(all_steps)
class AdaptiveStepSlot:
"""Tracks acceptance rate via EMA and adapts num_steps accordingly.
The core idea: if drafts are consistently accepted, try more steps;
if drafts are consistently rejected early, reduce steps to avoid waste.
Formula: target_steps = clamp(round(ema_accept_len) + 1, min_steps, max_steps)
- Probes one step beyond observed acceptance
- EMA smoothing prevents oscillation
- Only updates every `update_interval` batches for stability
- num_steps can be selected from different candidate sets on different batch_sizes
"""
def __init__(self, initial_steps: int, cfg: dict):
candidates = sorted(set(cfg["candidate_steps"]))
assert len(candidates) >= 1, "candidate_steps must have at least 1 value"
self.candidate_steps = candidates
self.ema_alpha = cfg.get("ema_alpha", 0.2)
self.update_interval = cfg.get("update_interval", 5)
self.warmup_batches = cfg.get("warmup_batches", 10)
self.down_hysteresis = cfg.get("down_hysteresis", -0.25)
self.up_hysteresis = cfg.get("up_hysteresis", 0.0)
self.ceiling_coeff = cfg.get("ceiling_coeff", 0)
if initial_steps in self.candidate_steps:
self.current_steps = initial_steps
else:
self.current_steps = self.candidate_steps[len(self.candidate_steps) // 2]
# Initialize EMA at current steps - 1 (neutral starting point)
self.ema_accept_len = float(self.current_steps - 1)
self._batch_count = 0
def update(self, num_correct_drafts_per_req: list[int]) -> bool:
"""Update EMA with observed accept lengths. Returns True if params changed.
Args:
num_correct_drafts_per_req: Per-request accepted draft token counts from last verify.
"""
if not num_correct_drafts_per_req:
return False
if self.current_steps > 0:
batch_avg = sum(num_correct_drafts_per_req) / len(
num_correct_drafts_per_req
)
self.ema_accept_len = (
1 - self.ema_alpha
) * self.ema_accept_len + self.ema_alpha * batch_avg
self._batch_count += 1
if self._batch_count <= self.warmup_batches:
return False
if (self._batch_count - self.warmup_batches) % self.update_interval != 0:
return False
return self._recompute_params()
def _recompute_params(self) -> bool:
"""Recompute steps from EMA. Returns True if params changed."""
old_steps = self.current_steps
current_idx = self.candidate_steps.index(old_steps)
old_idx = current_idx
# Probe the smallest positive step after a zero-step nospec interval.
if old_steps == 0:
current_idx = min(current_idx + 1, len(self.candidate_steps) - 1)
target = self.candidate_steps[current_idx]
if target > 0 and self.ema_accept_len < 0:
# A slot initialized at steps=0 has no draft acceptance history;
# start the first positive-step probe from that step's neutral EMA.
self.ema_accept_len = float(target - 1)
return self._apply_target_steps(old_steps, target)
# TODO: Consider limiting step changes to avoid overshooting.
while current_idx > 0:
prev_step = self.candidate_steps[current_idx - 1]
# A zero-step candidate disables drafting. Treat zero accepted drafts
# as low enough to reach it when it is the floor candidate.
drop_threshold = 0.5 if prev_step == 0 else prev_step - 0.5
drop_threshold += self.down_hysteresis
if self.ema_accept_len <= drop_threshold:
current_idx -= 1
else:
break
moved_down = current_idx < old_idx
if not moved_down:
while current_idx < len(self.candidate_steps) - 1:
current_step = self.candidate_steps[current_idx]
rise_threshold = current_step - 0.5 + self.up_hysteresis
if self.ema_accept_len > rise_threshold:
current_idx += 1
else:
break
target = self.candidate_steps[current_idx]
# EMA ceiling: only caps downward — never blocks step-ups, so the
# system can explore higher steps and let the EMA catch up.
if self.ceiling_coeff > 0:
ceiling = max(1, math.ceil(self.ema_accept_len * self.ceiling_coeff))
if target > ceiling and target <= old_steps:
while current_idx > 0 and self.candidate_steps[current_idx] > ceiling:
current_idx -= 1
target = self.candidate_steps[current_idx]
return self._apply_target_steps(old_steps, target)
def _apply_target_steps(self, old_steps: int, target: int) -> bool:
if target != old_steps:
self.current_steps = target
log_info_on_rank0(
logger,
f"Adaptive spec params updated: steps {old_steps} -> {target} "
f"(ema_accept_len={self.ema_accept_len:.2f})",
)
return True
return False
class AdaptiveSpeculativeParams:
"""Routes ``batch_size`` to the correct per-BS slot.
A slot is a per-BS configuration of adaptive step selection.
"""
def __init__(
self,
initial_steps: int,
cfg_path: str | None = None,
):
cfg, bs_entries = _load_adaptive_config(cfg_path)
self._bs_list: list[int] = sorted(bs_entries)
self._slots: dict[int, AdaptiveStepSlot] = {}
self._cuda_graph_bs: list[int] | None = None
for bs, entry in sorted(bs_entries.items()):
self._slots[bs] = AdaptiveStepSlot(
initial_steps=initial_steps,
cfg={**cfg, **entry},
)
first_slot = self._slots[self._bs_list[0]]
log_info_on_rank0(
logger,
f"AdaptiveSpeculativeParams initialized: "
f"steps={first_slot.current_steps}, "
f"candidate_steps={first_slot.candidate_steps}",
)
@cached_property
def candidate_steps(self) -> list[int]:
"""Union of all BS slots' candidate steps."""
return sorted({s for p in self._slots.values() for s in p.candidate_steps})
def set_cuda_graph_bs(self, cuda_graph_bs: list[int] | None) -> None:
self._cuda_graph_bs = sorted(cuda_graph_bs) if cuda_graph_bs else None
def get_steps_for_batch(self, batch_size: int) -> int:
return self._route(batch_size).current_steps
def on_verify_complete(
self, num_correct_drafts_per_req: list[int], batch_size: int
) -> int | None:
"""Feed verify results to the matching BS slot's EMA.
Returns the new step if a switch is warranted, else ``None``.
"""
params = self._route(batch_size)
if params.update(num_correct_drafts_per_req):
return params.current_steps
return None
def cuda_graph_bs_for_step(self, step: int) -> list[int] | None:
"""Return cuda_graph_bs values that can reach *step* at runtime.
Returns ``None`` when CUDA graphs are disabled (``set_cuda_graph_bs``
was never called or was called with ``None``).
"""
if self._cuda_graph_bs is None:
return None
return [
v
for v in self._cuda_graph_bs
if step in self._slots[self._find_closest_bs(v)].candidate_steps
]
def _route(self, batch_size: int) -> AdaptiveStepSlot:
"""Map *batch_size* → pad to CUDA-graph BS → closest slot."""
return self._slots[
self._find_closest_bs(self._pad_to_cuda_graph_bs(batch_size))
]
def _pad_to_cuda_graph_bs(self, batch_size: int) -> int:
if self._cuda_graph_bs is None:
return batch_size
idx = bisect.bisect_left(self._cuda_graph_bs, batch_size)
return (
self._cuda_graph_bs[idx] if idx < len(self._cuda_graph_bs) else batch_size
)
def _find_closest_bs(self, target: int) -> int:
idx = bisect.bisect_right(self._bs_list, target) - 1
return self._bs_list[max(0, idx)]