config: the readback and the resolving view say what they are (#35027)

This commit is contained in:
Cheng Wan
2026-08-17 16:18:19 -07:00
committed by GitHub
parent cba3c5d5ac
commit c70c7d72a8
12 changed files with 775 additions and 97 deletions
@@ -0,0 +1,401 @@
"""Every serving surface can answer what is running, not only what was asked.
`/server_info` and its gRPC and in-process twins report the startup record,
parsers included -- the launcher resolves `auto` into the record before
publishing. What changes after publication -- the model a weight update
swapped in, its load format, an operator-set weight version -- is reported by
the model-info surface, and there is one per entry point: HTTP, gRPC and
`Engine`. Adding a field to one and forgetting the others leaves that entry
point's users with no way to see it, which no test notices because each
surface passes its own tests.
The required set has two halves. The derived half comes from the control-plane
writers: whatever a process writes with `override` after publication is exactly
what can differ from the record, and both the keyword and the `**`-expansion
shapes resolve statically here. The second half is a policy, not a derivation --
what any one surface reports effectively, all of them owe their users -- so a
field every surface drops at once leaves the set with it. Each surface must both
carry the key and take its value from the effective config: reading
`server_args.<field>` under the right key reports the startup value with a
straight face.
"""
import ast
import inspect
import pathlib
import re
import unittest
import sglang
from sglang.srt.entrypoints.engine import Engine
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=3, suite="base-a-test-cpu")
_PACKAGE_ROOT = pathlib.Path(sglang.__file__).resolve().parent
_RUST_MODEL_INFO = (
pathlib.Path(__file__).resolve().parents[4]
/ "rust/sglang-server/src/api_server/common.rs"
)
# The tokenizer process is the one whose control-plane writes a served request
# can observe; a field it overrides there is a post-launch fact.
_TOKENIZER_WRITERS = (
"srt/managers/tokenizer_manager.py",
"srt/managers/tokenizer_control_mixin.py",
"srt/entrypoints/http_server.py",
"srt/entrypoints/engine.py",
)
# The model path and the served name stay manager attributes: a weight update
# moves them on the manager rather than through `override`, so the writer
# derivation cannot see them. They are subtracted from the derived half and
# asserted directly instead -- an exemption whose premise ("every surface
# reports them") is checked, not assumed. It was not true when it was written:
# only `Engine` carried `served_model_name`, and the router had to read the
# launch record off `/server_info` to learn a name a weight update had moved.
_MANAGER_ATTRIBUTES = {"model_path", "served_model_name"}
def _hicache_status_fields() -> set:
"""The fields `GET /hicache/storage-backend` answers with.
The HiCache mirror is written post-publish and reported by its own
endpoint, so the model-info surfaces do not owe it. Taking the set from
that handler is what keeps the exemption honest: a field it stops
reporting falls back to them.
"""
tree = ast.parse((_PACKAGE_ROOT / "srt/entrypoints/http_server.py").read_text())
for fn in ast.walk(tree):
if (
not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef))
or fn.name != "hicache_storage_backend_status"
):
continue
fields = {
elt.value
for comp in ast.walk(fn)
if isinstance(comp, ast.DictComp)
for gen in comp.generators
for elt in getattr(gen.iter, "elts", [])
if isinstance(elt, ast.Constant) and isinstance(elt.value, str)
}
assert fields, "the HiCache status handler names no field"
return fields
raise AssertionError(
"no `hicache_storage_backend_status` handler: the HiCache fields have "
"no endpoint of their own and fall to the model-info surfaces"
)
def _expanded_write_keys(rel: str, tree: ast.AST, call: ast.Call, kw: ast.keyword):
"""The field names behind a `**` at a control-plane writer call.
Resolves a dict literal -- constant keys, or a key bound by an enclosing
literal `for` -- and a name bound to a dict literal in the enclosing
function, including constant-subscript stores onto it. A `**` that
forwards its own function's `**kwargs` names no field: its callers do.
Anything else raises, because a skipped expansion shrinks the required
set instead of failing.
"""
enclosing = None
for fn in ast.walk(tree):
if isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)) and (
fn.lineno <= call.lineno <= (fn.end_lineno or fn.lineno)
):
if enclosing is None or fn.lineno > enclosing.lineno:
enclosing = fn
def loop_bound(name: str) -> set:
"""The values a `for name, ... in (<literal tuples>)` around the call binds."""
values = set()
for node in ast.walk(tree):
if not isinstance(node, ast.For):
continue
target = node.target
names = (
[target]
if isinstance(target, ast.Name)
else list(getattr(target, "elts", []))
)
if not names or not isinstance(names[0], ast.Name) or names[0].id != name:
continue
if not (node.lineno <= call.lineno <= (node.end_lineno or node.lineno)):
continue
for item in getattr(node.iter, "elts", []):
first = (
item.elts[0] if isinstance(item, ast.Tuple) and item.elts else item
)
if isinstance(first, ast.Constant) and isinstance(first.value, str):
values.add(first.value)
return values
def dict_keys(node: ast.Dict) -> set:
keys = set()
for key in node.keys:
if isinstance(key, ast.Constant):
keys.add(key.value)
continue
assert isinstance(
key, ast.Name
), f"non-literal dict key in a writer expansion at {rel}:{call.lineno}"
bound = loop_bound(key.id)
assert bound, (
f"dict key {key.id!r} at {rel}:{call.lineno} is not bound by a "
"literal loop; extend the resolver"
)
keys |= bound
return keys
if isinstance(kw.value, ast.Dict):
return dict_keys(kw.value)
assert isinstance(
kw.value, ast.Name
), f"unresolvable writer expansion at {rel}:{call.lineno}"
assert (
enclosing is not None
), f"writer expansion outside any function at {rel}:{call.lineno}"
name = kw.value.id
if enclosing.args.kwarg is not None and enclosing.args.kwarg.arg == name:
return set()
keys = set()
found = False
for node in ast.walk(enclosing):
if not (isinstance(node, ast.Assign) and len(node.targets) == 1):
continue
target = node.targets[0]
if isinstance(target, ast.Name) and target.id == name:
assert isinstance(node.value, ast.Dict), (
f"writer expansion {name!r} at {rel}:{call.lineno} is assigned "
"something other than a dict literal; extend the resolver"
)
found = True
keys |= dict_keys(node.value)
elif (
isinstance(target, ast.Subscript)
and isinstance(target.value, ast.Name)
and target.value.id == name
and isinstance(target.slice, ast.Constant)
):
keys.add(target.slice.value)
assert found, (
f"writer expansion {name!r} at {rel}:{call.lineno} has no dict-literal "
"assignment in its function; extend the resolver"
)
return keys
def _overridden_fields() -> set:
"""Fields the tokenizer process writes after publication."""
fields = set()
for rel in _TOKENIZER_WRITERS:
tree = ast.parse((_PACKAGE_ROOT / rel).read_text())
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
name = (
node.func.attr
if isinstance(node.func, ast.Attribute)
else getattr(node.func, "id", "")
)
if name not in ("override", "record_config_updates"):
continue
for kw in node.keywords:
if kw.arg == "source":
# The provenance label, not a config field.
continue
if kw.arg:
fields.add(kw.arg)
else:
fields |= _expanded_write_keys(rel, tree, node, kw)
# Only the mirror's own fields earn the endpoint exemption: adding an
# unrelated field to that handler must not buy it a pass here.
hicache = {f for f in _hicache_status_fields() if f.startswith("hicache_")}
return fields - _MANAGER_ATTRIBUTES - hicache
def _effective_reads_in(source: str, func_name: str) -> set:
"""Fields the function reports *and* reads through the effective config.
A key whose value comes off the `ServerArgs` record does not count: that is
the startup value under a name that promises the running one.
"""
tree = ast.parse(source)
for fn in ast.walk(tree):
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if fn.name != func_name:
continue
reported = set()
for node in ast.walk(fn):
if not isinstance(node, ast.Dict):
continue
for key, value in zip(node.keys, node.values):
if not (isinstance(key, ast.Constant) and isinstance(key.value, str)):
continue
for inner in ast.walk(value):
if (
isinstance(inner, ast.Call)
and isinstance(inner.func, ast.Attribute)
and inner.func.attr in ("config_value", "config_leaf")
and inner.args
and isinstance(inner.args[0], ast.Constant)
and inner.args[0].value == key.value
):
reported.add(key.value)
return reported
return set()
def _rust_model_info_keys() -> set:
"""The keys the rust server's `/model_info` handler answers with.
Scanned as text, from the handler's signature to the next item in the
file, with each line cut at its first `//`. A key is always left of its
value, so cutting inside a string can only drop keys, never invent one.
The signature must appear exactly once: a rust handler that moved or was
renamed would otherwise contribute an empty set and let the parity check
pass on nothing.
"""
source = _RUST_MODEL_INFO.read_text()
marker = "async fn model_info("
found = source.count(marker)
assert found == 1, (
f"{_RUST_MODEL_INFO.name} declares `{marker}` {found} times; the rust "
"/model_info surface is the one users reach under SGLANG_RUST_SERVER=1 "
"and is no longer being read"
)
body = source[source.index(marker) + len(marker) :]
following = re.search(r"^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s", body, re.M)
if following is not None:
body = body[: following.start()]
code = "\n".join(line.split("//")[0] for line in body.splitlines())
keys = set(re.findall(r'"([A-Za-z_][A-Za-z0-9_]*)"\s*:', code))
assert keys, "the rust /model_info handler names no field"
return keys
def _reported_keys_in(source: str, func_name: str) -> set:
"""Every string key the function's response dicts carry, whatever the value.
The manager-owned attributes are read off the manager, not through
`config_value`, so `_effective_reads_in` does not see them; this is how they
are checked.
"""
tree = ast.parse(source)
for fn in ast.walk(tree):
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if fn.name != func_name:
continue
return {
key.value
for node in ast.walk(fn)
if isinstance(node, ast.Dict)
for key in node.keys
if isinstance(key, ast.Constant) and isinstance(key.value, str)
}
return set()
def _model_info_sources() -> dict:
"""`(source, function name)` per Python model-info surface."""
return {
"http /model_info": (
(_PACKAGE_ROOT / "srt/entrypoints/http_server.py").read_text(),
"model_info",
),
"grpc get_model_info": (
(_PACKAGE_ROOT / "srt/entrypoints/grpc_bridge.py").read_text(),
"get_model_info",
),
"Engine.get_model_info": (
inspect.getsource(Engine.get_model_info).lstrip(),
"get_model_info",
),
}
def _model_info_surfaces() -> dict:
"""Each Python entry point's model-info surface, by what it reports
effectively."""
return {
name: _effective_reads_in(source, func_name)
for name, (source, func_name) in _model_info_sources().items()
}
class TestEffectiveStateSurfaces(CustomTestCase):
def test_each_entry_point_reports_the_post_launch_facts(self):
surfaces = _model_info_surfaces()
written = _overridden_fields()
# Both writer shapes are in reach of the scan: `load_format` is a
# literal keyword, the parsers arrive as `**{attr: ...}` under a key
# the enclosing loop binds.
self.assertLessEqual(
{"load_format", "reasoning_parser", "tool_call_parser"},
written,
"the derivation stopped finding the control-plane writers",
)
# What any surface reports effectively, all of them owe their users;
# what the control plane overrides, every surface owes regardless.
required = set().union(*surfaces.values()) | written
missing = {
name: sorted(required - reported)
for name, reported in surfaces.items()
if required - reported
}
self.assertEqual(
missing,
{},
"a serving surface cannot report what it is running: " f"{missing}",
)
def test_every_surface_reports_the_manager_owned_identity(self):
"""The identity a weight update moves is answered where it is read.
`model_path` and `served_model_name` live on the tokenizer manager, so
the writer derivation cannot reach them and they are subtracted from the
required set. This is the assertion that pays for that subtraction. A
surface that drops one sends its clients back to the launch record --
which is what the rust router had to read, under a name that had since
moved.
"""
surfaces = {
name: _reported_keys_in(source, func_name)
for name, (source, func_name) in _model_info_sources().items()
}
surfaces["rust /model_info"] = _rust_model_info_keys()
missing = {
name: sorted(_MANAGER_ATTRIBUTES - reported)
for name, reported in surfaces.items()
if _MANAGER_ATTRIBUTES - reported
}
self.assertEqual(
missing,
{},
f"a model-info surface does not say which model it serves: {missing}",
)
def test_the_rust_model_info_answers_the_same_keys(self):
"""`SGLANG_RUST_SERVER=1` swaps the whole HTTP server, not one handler.
The keys are owed there too, or the endpoint's contract depends on
which server the operator launched. The values are the launch record:
that process parses `server_args` once and mounts no route that can
change weights or parsers, so this is a key-set check and the handler
states which it reports.
"""
required = set().union(*_model_info_surfaces().values()) | _overridden_fields()
missing = sorted(required - _rust_model_info_keys())
self.assertEqual(
missing,
[],
"the rust /model_info answers a different contract than the Python "
f"one it replaces: {missing}",
)
if __name__ == "__main__":
unittest.main()
@@ -29,6 +29,7 @@ from types import SimpleNamespace
from sglang.srt.entrypoints import http_server
from sglang.srt.lora.lora_registry import LoRARef
from sglang.srt.managers.tokenizer_manager import TokenizerManager
from sglang.srt.runtime_context import publish, reset_context
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -36,6 +37,25 @@ from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
def _stub_tokenizer_manager(
server_args: ServerArgs, get_internal_state=None
) -> TokenizerManager:
"""A manager carrying the state `/server_info` and its writers read.
`__new__` skips `__init__`, which would open the ZMQ sockets and start
the handle loop; `_config_updates` is the log `record_config_updates`
appends to.
"""
tokenizer_manager = TokenizerManager.__new__(TokenizerManager)
tokenizer_manager.server_args = server_args
tokenizer_manager.model_path = server_args.model_path
tokenizer_manager.served_model_name = server_args.served_model_name
tokenizer_manager.startup_time = None
tokenizer_manager._config_updates = []
tokenizer_manager.get_internal_state = get_internal_state
return tokenizer_manager
def _call_server_info_with(
server_args: ServerArgs,
internal_states: list[dict] | None = None,
@@ -48,31 +68,37 @@ def _call_server_info_with(
`SimpleNamespace` stub via `set_global_state` and awaiting the
coroutine directly is enough to exercise the handler logic without
booting a model server.
`config_updates` are applied the way production applies them -- through
`record_config_updates`, in a process that has published -- rather than
planted on the stub. The endpoint answers the record and does not read that
log, so what the real writer buys is that an assertion on the record is not
satisfied for free by an update that never landed.
"""
async def _fake_internal_state():
return internal_states or [{"max_req_input_len": 1024}]
tokenizer_manager = TokenizerManager.__new__(TokenizerManager)
tokenizer_manager.server_args = server_args
tokenizer_manager.model_path = server_args.model_path
tokenizer_manager.served_model_name = server_args.served_model_name
tokenizer_manager.startup_time = None
tokenizer_manager._config_updates = (
[("test", dict(config_updates))] if config_updates else []
)
tokenizer_manager.get_internal_state = _fake_internal_state
tokenizer_manager = _stub_tokenizer_manager(server_args, _fake_internal_state)
stub_state = SimpleNamespace(
tokenizer_manager=tokenizer_manager,
scheduler_info={"max_req_input_len": 1024},
)
prior_state = http_server.get_global_state()
http_server.set_global_state(stub_state)
published = False
if config_updates:
# The writer runs in a published process, as it does in production.
publish(server_args, role="tokenizer")
published = True
tokenizer_manager.record_config_updates("test", **config_updates)
try:
return asyncio.run(http_server.server_info())
finally:
# Restore so a later test in the same process isn't surprised.
http_server._global_state = prior_state
if published:
reset_context()
class TestServerInfoKvEventsField(CustomTestCase):
@@ -243,16 +269,37 @@ class TestServerInfoKvEventsField(CustomTestCase):
class TestServerInfoControlPlaneUpdates(CustomTestCase):
"""Runtime control-plane updates live on the manager, not on ServerArgs."""
"""/server_info answers what was asked for, not what is in effect."""
def test_recorded_updates_win_over_the_startup_config(self):
def test_the_readback_reports_the_record_not_the_control_plane(self):
# A runtime weight-version change is reported by /model_info and its
# gRPC / Engine twins; this endpoint reports what the operator supplied.
server_args = ServerArgs(model_path="dummy", weight_version="v1")
payload = _call_server_info_with(
server_args, config_updates={"weight_version": "v2"}
)
self.assertEqual(payload["weight_version"], "v2")
self.assertEqual(payload["weight_version"], "v1")
self.assertEqual(server_args.weight_version, "v1")
def test_the_recorded_update_reaches_the_readback_overlay(self):
"""The update the case above records is one `/server_info` could see.
`resolved_config_dict` is the overlay the endpoint dropped; an update
that never reached it would satisfy the assertion above for free.
"""
server_args = ServerArgs(model_path="dummy", weight_version="v1")
tokenizer_manager = _stub_tokenizer_manager(server_args)
publish(server_args, role="tokenizer")
try:
tokenizer_manager.record_config_updates("test", weight_version="v2")
self.assertEqual(tokenizer_manager.config_value("weight_version"), "v2")
overlaid = tokenizer_manager.resolved_config_dict(
dataclasses.asdict(server_args)
)
self.assertEqual(overlaid["weight_version"], "v2")
finally:
reset_context()
class TestServerInfoExistingFieldsPreserved(CustomTestCase):
"""Regression guard: the new `kv_events` field is additive — none of
@@ -112,6 +112,12 @@ _CONFIGURED_SIZE_CALL_SITES = {
"the encode server's launch entry sizes its workers before it has "
"spawned any of them"
),
("srt/utils/common.py", "configured_tp_size"): (
"the require_*_tp_gather predicates compared the configured tp_size "
"when they read the record; the live property answers a different "
"question wherever the groups alias, so the configured accessor is the "
"mechanical substitution and the live one would be a semantic change"
),
("srt/model_loader/loader.py", "configured_moe_dp_size"): (
"the same dict already carries the live moe_dp_size under 'dp'; this entry "
"is the configured intent"
@@ -349,8 +349,6 @@ _EXPOSED = {
("speculative/standalone_worker_v2.py", "speculative_eagle_topk"),
("speculative/standalone_worker_v2.py", "speculative_num_draft_tokens"),
("speculative/standalone_worker_v2.py", "speculative_num_steps"),
("utils/common.py", "page_size"),
("utils/common.py", "speculative_eagle_topk"),
("utils/common.py", "speculative_num_draft_tokens"),
("utils/common.py", "speculative_num_steps"),
("utils/cuda_vmm_transport_utils.py", "mm_feature_transport"),