config: business code no longer reads the published ServerArgs (#34081)

This commit is contained in:
Cheng Wan
2026-08-09 14:44:08 -07:00
committed by GitHub
parent 110bf7e6a8
commit 63833f8034
35 changed files with 1101 additions and 213 deletions
+147
View File
@@ -47,6 +47,7 @@ test-only ``override(**kw)``.
from __future__ import annotations
import dataclasses
import functools
import os
import sys
from contextlib import contextmanager
@@ -827,6 +828,10 @@ class RuntimeContext:
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).
@@ -1376,6 +1381,7 @@ 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
@@ -1406,3 +1412,144 @@ def mamba_extra_buffer_lazy_enabled() -> bool:
get_memory().disable_radix_cache is False
and get_exec().mamba.mamba_radix_cache_strategy == "extra_buffer_lazy"
)
# --- Derived config accessors ------------------------------------------------
#
# A few values are computed from several config fields plus the HF config, so
# they are ``ServerArgs`` members rather than namespace leaves. Business code
# must not reach for the startup record to get them: these accessors are the
# named home, and this module — which owns the slot — is the only place that
# reads it. Each one keeps the member's exact semantics, including which model
# config it derives from (always the process's, i.e. the target's).
def mamba_cache_chunk_size() -> int:
"""The caching point granularity for mamba state: ``max(the model's mamba
chunk size, page_size)``. Cached on the config after the first call."""
return get_server_args().mamba_cache_chunk_size
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; ``ServerArgs.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.
"""
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,
)
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."""
return get_server_args().use_mla_backend()
def attention_backends() -> tuple:
"""The configured ``(prefill, decode)`` backend pair, split fields falling
back to ``attention_backend``.
All three inputs are ``exec.kernel`` leaves, so this derives from the bags
and follows a post-publish override; ``ServerArgs.get_attention_backends``
is the pre-publish equivalent the resolution pipeline uses. A built runner
stamps its own resolved pair (``ModelRunner.prefill_attention_backend_str``);
read that when there is a runner in hand.
"""
from sglang.srt.arg_groups.overrides import attention_backends_of
# All three leaves live in the same bag, so the resolution pipeline's own
# helper applies directly -- one definition of the fallback rule.
return attention_backends_of(get_exec().kernel)
def process_model_config():
"""The process's ``ModelConfig`` (built once from the published config)."""
return get_server_args().get_model_config()
def cutedsl_moe_max_num_tokens() -> int:
"""The CuteDSL A2A per-rank token budget.
Every input is a published leaf (``spec``, ``schedule``, ``exec.graph``), so
this derives from the bags and follows a post-publish override;
``ServerArgs.cutedsl_moe_max_num_tokens`` is the pre-publish equivalent the
resolution pipeline uses. Max over the prefill bound, the piecewise-prefill
capture, and the decode/verify bound.
"""
from sglang.srt.model_executor.cuda_graph_config import Backend
spec = get_spec()
num_tokens_per_req = (
(spec.speculative_num_draft_tokens or 1) if spec.speculative_algorithm else 1
)
prefill_tokens = get_schedule().max_prefill_tokens
cg_config = get_exec().graph.cuda_graph_config
if cg_config is not None and cg_config.prefill.backend == Backend.TC_PIECEWISE:
prefill_tokens = max(prefill_tokens, cg_config.prefill.max_bs or 0)
decode_max_bs = (cg_config.decode.max_bs if cg_config is not None else 0) or 0
return max(prefill_tokens, decode_max_bs * num_tokens_per_req)
# --- Configured (not live) parallel sizes ------------------------------------
#
# ``get_parallel()`` shadows these names with the LIVE topology, which is the
# right answer almost everywhere. A handful of call sites need what was
# *configured* instead — before the groups exist, in a process that has none,
# or where the live value is deliberately aliased to another dimension. Each
# accessor below names that intent so no business call site has to reach for
# the startup record; the per-site reasons live in the read ratchet.
#
# They read the published leaf rather than the record: the bag is what
# ``override`` writes, and once the instance holds only the user's raw input
# the record would answer with what was *typed* instead of what resolution
# produced. Going through the bag directly is what gets past the live property
# that shadows these four names on ``get_parallel()``.
def _configured_parallel(name: str):
# The bag itself, not ParallelContext, whose live property shadows these
# four names. Read through the parallel slot the way the leaf accessor
# does — ``parallel`` is deliberately outside the per-role namespace table
# (every process reads topology config), so this must not route through
# ``config_bag()``'s role check, which would record or reject the read.
config = _CONTEXT.parallel._config
if config is None:
raise ValueError("config namespace 'parallel' not published")
return getattr(config, name)
def configured_tp_size() -> int:
return _configured_parallel("tp_size")
def configured_pp_size() -> int:
return _configured_parallel("pp_size")
def configured_moe_dp_size() -> int:
return _configured_parallel("moe_dp_size")
def configured_attn_cp_size() -> int:
return _configured_parallel("attn_cp_size")