runtime_context: add resolved-config namespace bags and accessors (#31810)

This commit is contained in:
Cheng Wan
2026-07-22 01:16:24 -07:00
committed by GitHub
parent 1a19f2b50f
commit 09688d58bc
2 changed files with 332 additions and 15 deletions
+201 -15
View File
@@ -21,20 +21,27 @@ wrapper, not a cache. It gives call-sites one import and one naming scheme in
place of a dozen free functions, plus a test-only ``override()`` hook to force a
topology without monkeypatching the underlying getters.
``get_server_args()`` returns the process-wide ``ServerArgs`` (the config
tier). The context owns the storage: publishing goes through
``RuntimeContext.set_server_args`` (the legacy
``set_global_server_args_for_scheduler`` / ``get_global_server_args`` in
``server_args.py`` are thin shims over this slot), and the object is returned
by reference — the same live instance everywhere, never a copy.
``get_server_args()`` returns the process-wide ``ServerArgs``. This is the pristine / resolved-at-startup **read-only** record kept
for debug and reproduction; business code reads resolved config from the
namespace bags below, not from this object. The context owns the storage:
publishing goes through ``RuntimeContext.set_server_args`` (the legacy
``set_global_server_args_for_scheduler`` / ``get_global_server_args`` are thin
shims over this slot).
``get_flags()`` returns the runtime-flags tier. Resolved configuration lives
on ``server_args`` fields (declarations materialize at the end of
``__post_init__``), so this tier only carries genuine runtime state that is
not a function of the configuration alone — today the capture lifecycle
(``flags.capture``). Flags live in typed dataclass groups; reads and writes
are plain attribute access, and each group offers a transactional, test-only
``override(**kw)``.
``get_exec()`` / ``get_memory()`` / ``get_schedule()`` / ``get_device()`` /
``get_model()`` / ``get_spec()`` / ``get_lora()`` / ``get_mm()`` /
``get_disagg()`` / ``get_serving()`` / ``get_observability()`` return the
resolved **config namespace bags** — the single source of truth for config,
snapshotted from ``server_args`` at publish and driven by the ``NS(...)``
metadata on each field (multi-level under ``exec.*``). Reads are attribute
chains (``get_exec().moe.moe_runner_backend``); bags are read-only by bare
assignment (written via ``override``).
``get_flags()`` returns the runtime-flags tier: state that is **not** a pure
function of config (the capture lifecycle, ACTIVE MoE backend, DP runtime) —
never a mirror of config. Flags live in typed dataclass groups; reads and
writes are plain attribute access, and each group offers a transactional,
test-only ``override(**kw)``.
"""
from __future__ import annotations
@@ -533,15 +540,130 @@ class ForwardFlags:
self._plain[name] = value
class _ConfigBag:
"""A resolved-config namespace bag.
Values are snapshotted from ``server_args`` at ``publish`` and this bag is
the **single source of truth** for its fields thereafter. Read is plain
attribute access; the bag is read-only by bare assignment. The sanctioned
writers are ``get_context().override(source, ...)`` (permanent) and
the scoped ``.override(**kw)`` context manager (tests). Sub-namespaces
(e.g. ``exec.moe``) are nested ``_ConfigBag`` instances reached by attribute.
"""
__slots__ = ("_path", "_fields", "_subs")
def __init__(self, path: str):
object.__setattr__(self, "_path", path)
object.__setattr__(self, "_fields", {}) # {leaf: value}
object.__setattr__(self, "_subs", {}) # {subname: _ConfigBag}
def __getattr__(self, name: str) -> Any:
# Reached only when ``name`` is not a real attribute (slot).
fields = object.__getattribute__(self, "_fields")
if name in fields:
return fields[name]
subs = object.__getattribute__(self, "_subs")
if name in subs:
return subs[name]
path = object.__getattribute__(self, "_path")
raise AttributeError(f"config namespace {path!r} has no leaf/subgroup {name!r}")
def __setattr__(self, name: str, value: Any) -> None:
raise AttributeError(
f"config namespace {self._path!r} is read-only; write via "
"get_context().override(source, ...) or the scoped .override(**kw)"
)
def _set(self, name: str, value: Any) -> None:
"""Internal write (publish + override) that bypasses the read-only guard."""
object.__getattribute__(self, "_fields")[name] = value
def __contains__(self, name: str) -> bool:
return name in object.__getattribute__(self, "_fields")
@contextmanager
def override(self, **kwargs):
"""Scoped, transactional test-only override of this bag's own leaves
(keys validated before any write; restored on exit)."""
fields = object.__getattribute__(self, "_fields")
unknown = set(kwargs) - set(fields)
if unknown:
path = object.__getattribute__(self, "_path")
raise ValueError(f"unknown config leaf for {path!r}: {sorted(unknown)}")
saved = {name: fields[name] for name in kwargs}
fields.update(kwargs)
try:
yield self
finally:
fields.update(saved)
def _build_config_bags(server_args: Any) -> dict:
"""Snapshot resolved ``server_args`` into the namespace bag tree, driven by
the ``NS(...)`` metadata on the dataclass fields. Returns
``{top_level_name: _ConfigBag}``, arbitrarily nested (``exec.moe.eplb.…``).
Only dataclass fields carry ``NS`` markers, so derived properties/methods are
naturally excluded (they stay on the bag). A name used as both a leaf and a
subgroup at the same level is a hard error — no silent shadowing."""
from sglang.srt.arg_groups.arg_utils import namespace_of
_MISSING = object()
tops: dict = {}
for field, path in namespace_of(type(server_args)).items():
value = getattr(server_args, field, _MISSING)
if value is _MISSING:
# Every NS-declared field is a dataclass field, so a resolved config
# always carries it; a miss means a malformed/partial config object
# was published. Fail loud here rather than silently omitting the
# leaf (which surfaces later as a confusing "not a published leaf").
raise AttributeError(
f"config field {field!r} is declared NS({path!r}) but absent from "
f"the published {type(server_args).__name__}; cannot project its bag leaf"
)
parts = path.split(".")
bag = tops.get(parts[0])
if bag is None:
bag = tops[parts[0]] = _ConfigBag(parts[0])
for depth in range(1, len(parts)):
name = parts[depth]
if name in object.__getattribute__(bag, "_fields"):
raise ValueError(
f"config namespace collision: {'.'.join(parts[: depth + 1])!r} "
"is declared as both a leaf and a subgroup"
)
subs = object.__getattribute__(bag, "_subs")
child = subs.get(name)
if child is None:
child = subs[name] = _ConfigBag(".".join(parts[: depth + 1]))
bag = child
if field in object.__getattribute__(bag, "_subs"):
raise ValueError(
f"config namespace collision: leaf {field!r} under {path!r} "
"clashes with a subgroup of the same name"
)
bag._set(field, value)
return tops
class RuntimeContext:
"""Container for the structured runtime accessors; exposes ``parallel``,
``server_args``, ``flags``, ``resources``, and ``forward``."""
``server_args``, the resolved config namespace bags, ``flags``,
``resources``, and ``forward``."""
__slots__ = ("parallel", "_server_args", "flags", "resources", "forward")
__slots__ = (
"parallel",
"_server_args",
"_config_bags",
"flags",
"resources",
"forward",
)
def __init__(self, parallel: ParallelContext):
self.parallel = parallel
self._server_args: ServerArgs | None = None
self._config_bags: dict | None = None
self.flags = Flags()
self.resources = Resources()
self.forward = ForwardFlags()
@@ -599,6 +721,20 @@ class RuntimeContext:
server_args, "enable_torch_compile", False
)
self._server_args = server_args
# 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)
def config_bag(self, name: str) -> _ConfigBag:
"""Return the top-level config namespace bag (``device`` / ``model`` /
``exec`` / ``schedule`` / ``memory`` / ``spec`` / ``lora`` / ``mm`` /
``disagg`` / ``serving`` / ``observability``). Fails closed until
``publish`` / ``set_server_args`` has projected it."""
bags = self._config_bags
if not bags or name not in bags:
raise ValueError(f"config namespace {name!r} not published")
return bags[name]
def override_server_args(self, **fields) -> _ServerArgsOverride:
"""Test-only scoped override for the config tier — the sibling of
@@ -710,6 +846,55 @@ def get_forward() -> ForwardFlags:
return _CONTEXT.forward
# --- Resolved config namespaces -------------------------
# Each returns the top-level snapshot bag; reads are `get_exec().moe.field` etc.
# All fail with ValueError("... not published") until publish has projected them.
# ``parallel`` config leaves are served by ``get_parallel()`` (live wrapper);
# their config-bag wiring is a scoped follow-up.
def get_device() -> _ConfigBag:
return _CONTEXT.config_bag("device")
def get_model() -> _ConfigBag:
return _CONTEXT.config_bag("model")
def get_exec() -> _ConfigBag:
return _CONTEXT.config_bag("exec")
def get_schedule() -> _ConfigBag:
return _CONTEXT.config_bag("schedule")
def get_memory() -> _ConfigBag:
return _CONTEXT.config_bag("memory")
def get_spec() -> _ConfigBag:
return _CONTEXT.config_bag("spec")
def get_lora() -> _ConfigBag:
return _CONTEXT.config_bag("lora")
def get_mm() -> _ConfigBag:
return _CONTEXT.config_bag("mm")
def get_disagg() -> _ConfigBag:
return _CONTEXT.config_bag("disagg")
def get_serving() -> _ConfigBag:
return _CONTEXT.config_bag("serving")
def get_observability() -> _ConfigBag:
return _CONTEXT.config_bag("observability")
def get_stream(name: str) -> Any:
return _CONTEXT.get_stream(name)
@@ -741,6 +926,7 @@ def reset_context() -> None:
Wrapper subsystems (``parallel``) hold no state and are unaffected.
"""
_CONTEXT._server_args = None
_CONTEXT._config_bags = None
_CONTEXT.flags = Flags()
_CONTEXT.resources = Resources()
_CONTEXT.forward = ForwardFlags()