config: one control-plane log for the process (#35028)
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user