config: one control-plane log for the process (#35028)

This commit is contained in:
Cheng Wan
2026-08-17 16:19:00 -07:00
committed by GitHub
parent c70c7d72a8
commit b3c8f0d923
8 changed files with 245 additions and 95 deletions
+15 -29
View File
@@ -122,6 +122,7 @@ from sglang.srt.observability.request_metrics_exporter import (
)
from sglang.srt.observability.trace import SpanAttributes, extract_trace_headers
from sglang.srt.runtime_context import (
get_context,
get_device,
get_disagg,
get_exec,
@@ -379,8 +380,6 @@ class InputFormat(Enum):
CROSS_ENCODER_PAIRS = 3 # Cross-encoder pairs like [["query", "document"]]
_SERVER_ARGS_FIELDS = frozenset(f.name for f in dataclasses.fields(ServerArgs))
_MANAGER_OWNED_FIELDS = ("model_path", "served_model_name")
@@ -410,7 +409,6 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
# the in-process path re-projects the object the launcher published.
set_global_server_args_for_tokenizer(server_args)
self.startup_time: Optional[Dict[str, Any]] = None
self._config_updates: List[Tuple[str, Dict[str, Any]]] = []
self.elastic_worker_count = get_parallel().dp_size
self.elastic_pending_ep_size = None
self.elastic_scale_phase = "idle"
@@ -2065,31 +2063,19 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
return success, message, num_paused_requests
def record_config_updates(self, source: str, **fields) -> None:
"""Record a control-plane config change for this engine.
"""Record a control-plane config change: a weight update, a parser
resolved from the chat template, a HiCache mirror attach.
These are post-startup facts the config bags do not model (weight
version, model path, the tokenizer's HiCache mirror); the readback
endpoints overlay them onto the startup config. The process-global
sibling is ``RuntimeContext.override`` / ``resolved_server_args_dict``,
which writes the config bags.
These land in the config bags like every other post-publish change, so
one log carries the provenance for the whole process.
"""
unknown = sorted(f for f in fields if f not in _SERVER_ARGS_FIELDS)
if unknown:
raise ValueError(
f"{unknown} are not ServerArgs fields; the readback endpoints "
"overlay these onto a serialized ServerArgs, so an unknown key "
"would surface as a phantom config entry."
)
self._config_updates.append((source, dict(fields)))
get_context().override(source, **fields)
def config_value(self, name: str):
"""The value in effect for one config field, control-plane updates first."""
"""The value in effect for one config field."""
if name in _MANAGER_OWNED_FIELDS:
return getattr(self, name)
for _source, fields in reversed(self._config_updates):
if name in fields:
return fields[name]
return getattr(self.server_args, name)
return get_context().config_leaf(name)
def _dump_config_snapshot(self) -> Optional[Dict[str, Any]]:
"""The config in effect, or None when it cannot be serialized.
@@ -2104,18 +2090,18 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
return None
def resolved_config_dict(self, base: Dict[str, Any]) -> Dict[str, Any]:
"""``base`` (a serialized ``ServerArgs``) with the control-plane updates on top."""
resolved = dict(base)
for _source, fields in self._config_updates:
resolved.update(fields)
"""``base`` (a serialized ``ServerArgs``) with the control-plane changes on top."""
resolved = get_context().resolved_server_args_dict(base)
for name in _MANAGER_OWNED_FIELDS:
resolved[name] = getattr(self, name)
return resolved
def _update_model_path_info(self, model_path: str, load_format: str):
# These two stay on the manager: the readback reads them from here,
# and a bag write would not reach the other processes anyway.
self.served_model_name = model_path
self.record_config_updates("tokenizer.update_weights", load_format=load_format)
self.model_path = model_path
self.record_config_updates("tokenizer.update_weights", load_format=load_format)
async def _wait_for_model_update_from_disk(
self, obj: UpdateWeightFromDiskReqInput
@@ -2986,7 +2972,7 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
logger.info(log_message)
to_dump_with_server_args = {
"server_args": self.server_args,
"config_updates": list(self._config_updates),
"config_updates": get_context().overrides_log(),
"resolved_config": self._dump_config_snapshot(),
"requests": data_list.copy(),
}
@@ -3071,7 +3057,7 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
# Write the data to the file
data_to_dump_with_server_args = {
"server_args": self.server_args,
"config_updates": list(self._config_updates),
"config_updates": get_context().overrides_log(),
"resolved_config": self._dump_config_snapshot(),
"requests": data_to_dump,
"launch_command": " ".join(sys.argv),
+31 -6
View File
@@ -903,6 +903,29 @@ class RuntimeContext:
bag._set(name, value)
self._overrides_log.append((source, dict(fields)))
def config_leaf(self, name: str):
"""One resolved config leaf by field name — the read side of ``override``.
Callers that hold a field name rather than a namespace (a readback
endpoint, a control-plane handler) would otherwise have to know which
bag it lives in.
"""
bags = self._config_bags
if bags is None:
raise ValueError("config not published; cannot read a config leaf")
from sglang.srt.arg_groups.arg_utils import namespace_of
path = namespace_of(type(self._server_args)).get(name)
if path is None:
raise ValueError(f"{name!r} is not a config leaf (no NS namespace)")
parts = path.split(".")
bag = self.config_bag(parts[0])
for seg in parts[1:]:
bag = object.__getattribute__(bag, "_subs").get(seg)
if bag is None:
raise ValueError(f"subgroup {seg!r} missing under {path!r}")
return getattr(bag, name)
def overrides_log(self) -> list:
"""Provenance of post-publish ``override`` calls: ``[(source, {field: value})]``.
@@ -925,12 +948,14 @@ class RuntimeContext:
``ServerArgs`` field names, so overlaying them onto the top level of
either base is exact.
This covers the process-global bags only. Control-plane facts the bags
do not model (weight version, model path, the tokenizer's HiCache
mirror) live on the tokenizer manager, and
``TokenizerManager.resolved_config_dict`` overlays those for the
top-level ``/server_info`` body. The two are separate logs, not one
merged dict.
The log is per process: it carries what *this* process overrode. A
weight reload records ``model_path`` and ``load_format`` from the
scheduler process (``ModelRunner.update_model_fields``); the tokenizer
process records only ``load_format`` and keeps ``model_path`` /
``served_model_name`` as ``TokenizerManager`` attributes, which
``TokenizerManager.resolved_config_dict`` overlays on top of this dump.
The top-level ``/server_info`` fields are the startup record, not this
dump.
"""
d = dict(vars(self.server_args)) if base is None else dict(base)
for _source, fields in self._overrides_log: