Decouple speculative draft capacity from runtime state (#36897)

Co-authored-by: Ming Yang <minos.future@gmail.com>
This commit is contained in:
Lianmin Zheng
2026-08-30 23:31:25 -07:00
committed by GitHub
co-authored by Ming Yang
parent 2cb3f32b03
commit 3ed3326631
6 changed files with 80 additions and 51 deletions
+14 -14
View File
@@ -1879,22 +1879,22 @@ def max_speculative_num_draft_tokens(server_args: Any) -> Optional[int]:
memo = server_args.__dict__.get("_max_speculative_num_draft_tokens")
if memo is not None:
return memo
if cfg.speculative_num_draft_tokens is None:
result = None
elif not cfg.speculative_adaptive:
result = cfg.speculative_num_draft_tokens
else:
from sglang.srt.speculative.adaptive_spec_params import (
resolve_candidate_steps_from_config,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
candidate_steps = resolve_candidate_steps_from_config(
cfg_path=cfg.speculative_adaptive_config,
result = SpeculativeAlgorithm.from_string(
cfg.speculative_algorithm
).resolve_max_speculative_num_draft_tokens(server_args)
if (
result is not None
and cfg.speculative_num_draft_tokens is not None
and result < cfg.speculative_num_draft_tokens
):
raise ValueError(
"The speculative algorithm declared "
f"max_speculative_num_draft_tokens={result}, below the configured "
"speculative_num_draft_tokens="
f"{cfg.speculative_num_draft_tokens}."
)
# TODO: adaptive spec currently requires topk=1, so each runtime
# state needs steps + 1 draft-token slots. Revisit this if topk>1
# is supported.
result = max(candidate_steps) + 1
if getattr(server_args, "_resolution_finished", False):
server_args._max_speculative_num_draft_tokens = result
return result
+25 -29
View File
@@ -939,14 +939,22 @@ class RuntimeContext:
_resolved_or_field(server_args, "enable_torch_compile", False)
)
self._server_args = server_args
# The adaptive draft-token bound memoizes on the config *path*, so a new
# publication that reuses the path must not inherit the bound computed
# from the file's previous contents.
_adaptive_draft_token_bound.cache_clear()
# Snapshot resolved config into the namespace bags (the single source of
# truth for config reads). Driven by NS(...) metadata; a mock/partial
# config with no NS markers yields an empty tree (no bags projected).
self._config_bags = _build_config_bags(server_args)
spec = self._config_bags.get("spec")
if spec is not None:
from sglang.srt.arg_groups.overrides import (
max_speculative_num_draft_tokens as max_draft_tokens_of,
)
# Keep the launch-time capacity stable while adaptive algorithms
# change the active width in this bag.
spec._set(
"max_speculative_num_draft_tokens",
max_draft_tokens_of(server_args),
)
# Wire the published `parallel` bag onto the live wrapper: it is the slot
# the `config` property reads, which is how config-only leaves like
# pp_max_micro_batch_size are spelled.
@@ -1625,7 +1633,6 @@ def restore_context(state: dict[str, Any]) -> None:
setattr(_CONTEXT, name, value)
for name, value in state["__parallel__"].items():
setattr(_CONTEXT.parallel, name, value)
_adaptive_draft_token_bound.cache_clear()
set_global_dwdp_manager(state["__dwdp__"])
@@ -1639,7 +1646,6 @@ def reset_context() -> None:
"""
_CONTEXT._server_args = None
_CONTEXT._config_bags = None
_adaptive_draft_token_bound.cache_clear()
_CONTEXT._overrides_log = []
_CONTEXT._publish_role = None
_CONTEXT.parallel._config = None
@@ -1917,33 +1923,23 @@ def mamba_track_grid(tree_page: int) -> int:
def max_speculative_num_draft_tokens() -> int | None:
"""The largest draft-token count speculative decoding may use.
All three inputs are ``spec`` leaves, so this derives from the bags and
follows a post-publish override; ``overrides.max_speculative_num_draft_tokens``
is the pre-publish equivalent. Adaptive spec resolves the count from its
candidate-step table instead of the flat field.
Adaptive algorithms may switch to a longer state after the scheduler
reserves KV for the next decode batch, so include the capacity captured
when the resolved configuration was published.
"""
spec = get_spec()
if spec.speculative_num_draft_tokens is None:
return None
if not spec.speculative_adaptive:
return spec.speculative_num_draft_tokens
# The adaptive branch parses a JSON config, and this is called per decode
# batch (`spec_prepare_for_decode`), so memoize on the inputs -- keyed, not
# cached once, so a post-publish override still recomputes.
return _adaptive_draft_token_bound(spec.speculative_adaptive_config)
@functools.lru_cache(maxsize=8)
def _adaptive_draft_token_bound(cfg_path: str | None) -> int:
from sglang.srt.speculative.adaptive_spec_params import (
resolve_candidate_steps_from_config,
return max(
(
bound
for bound in (
spec.speculative_num_draft_tokens,
spec.max_speculative_num_draft_tokens,
)
if bound is not None
),
default=None,
)
candidate_steps = resolve_candidate_steps_from_config(cfg_path=cfg_path)
# Adaptive spec requires topk=1 today, so each runtime state needs
# steps + 1 draft-token slots (mirrors the ServerArgs member).
return max(candidate_steps) + 1
def uses_mla_backend() -> bool:
"""Whether this process's model runs the MLA attention path."""
@@ -226,6 +226,29 @@ class SpeculativeAlgorithm(Enum):
elif self.is_ngram():
_handle_ngram(server_args)
def resolve_max_speculative_num_draft_tokens(
self, server_args: ServerArgs
) -> Optional[int]:
"""Return the largest draft-token width this algorithm may use."""
from sglang.srt.arg_groups.overrides import resolving_view
cfg = resolving_view(server_args)
if cfg.speculative_num_draft_tokens is None:
return None
if not cfg.speculative_adaptive:
return cfg.speculative_num_draft_tokens
from sglang.srt.speculative.adaptive_spec_params import (
resolve_candidate_steps_from_config,
)
candidate_steps = resolve_candidate_steps_from_config(
cfg_path=cfg.speculative_adaptive_config,
)
# Adaptive spec requires topk=1 today, so each runtime state needs
# steps + 1 draft-token slots. Revisit this if topk>1 is supported.
return max(candidate_steps) + 1
def get_num_tokens_per_req_for_target_verify(
self, num_draft_tokens: int, is_draft_worker: bool
) -> int:
@@ -109,6 +109,19 @@ class CustomSpecAlgo:
def handle_server_args(self, server_args: ServerArgs) -> None:
pass
def resolve_max_speculative_num_draft_tokens(
self, server_args: ServerArgs
) -> Optional[int]:
"""Return the largest draft-token width this algorithm may use.
The default covers static algorithms and adaptive algorithms whose
runtime states never exceed their startup width. Overrides must not
return less than ``server_args.speculative_num_draft_tokens``.
"""
from sglang.srt.arg_groups.overrides import resolving_view
return resolving_view(server_args).speculative_num_draft_tokens
def create_worker(self, server_args: ServerArgs) -> Type:
cfg = resolving_view(server_args)