[SRT] Clean up no-op compiler pass, dead helpers, and migration tests (#39295)

This commit is contained in:
Xiaoyu Zhang
2026-09-16 12:04:21 +08:00
committed by GitHub
parent 6331e43081
commit 537ac52c5b
17 changed files with 36 additions and 3159 deletions
@@ -1,595 +0,0 @@
"""Resolution reads its own decisions, not the record's fields.
`declare_resolution` records a decision in the declaration stash and writes
nothing. The fields keep what the caller passed, so a resolver that reads a
field another resolver may have decided reads the raw input -- silently, and
only on the configurations where that other resolver fires. The whole pipeline
therefore reads through `resolving_view` (or `resolved_view`, which is
the same view after resolution has finished), and this pins that there is
nothing left reading a field directly.
Subjects: every function in `arg_groups/` that takes a config, every
`ServerArgs` handler the dispatcher reaches, and every member of `ServerArgs` /
`PortArgs` -- the members are reached from the hooks and from business code,
which the handler walk cannot see, and a member that recomputes from a raw field
decides from what was typed. All three
are derived -- a new hook file, a new handler or a new member is covered the
moment it is written. Readers *outside* those
two -- the platform defaults, `ModelConfig`, the spec-algo hook -- are reached by
resolution too and have moved to the view as well, but enumerating them needs
the call-graph derivation `test_resolution_reads_no_bag` owns; this file pins
the two scopes it can derive exactly.
"""
import ast
import pathlib
import re
import msgspec
import msgspec.structs
import sglang
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=45, suite="base-a-test-cpu")
_SRT = pathlib.Path(sglang.__file__).resolve().parent / "srt"
_FIELDS = frozenset(field.name for field in msgspec.structs.fields(ServerArgs))
# Names a config travels under. `args` is included because the platform hooks
# use it; a false positive would be a function taking an argparse Namespace and
# reading an attribute that happens to be a ServerArgs field name, which the
# allowlist below would then have to carry.
_HOLDER_NAMES = frozenset({"server_args", "sa", "args"})
def _holders(fn):
names = {
arg.arg
for arg in list(fn.args.posonlyargs)
+ list(fn.args.args)
+ list(fn.args.kwonlyargs)
if arg.arg in _HOLDER_NAMES
}
for arg in (
list(fn.args.posonlyargs) + list(fn.args.args) + list(fn.args.kwonlyargs)
):
annotation = arg.annotation
text = (
annotation.value
if isinstance(annotation, ast.Constant)
else (
annotation.id
if isinstance(annotation, ast.Name)
else annotation.attr
if isinstance(annotation, ast.Attribute)
else None
)
)
if text == "ServerArgs":
names.add(arg.arg)
return names
def _field_reads(fn, holders):
for node in ast.walk(fn):
if (
isinstance(node, ast.Attribute)
and node.attr in _FIELDS
and isinstance(node.value, ast.Name)
and node.value.id in holders
and isinstance(node.ctx, ast.Load)
):
yield node.lineno, node.attr
_DECLARERS = frozenset(
{
"declare_resolution",
"record_foreign_defaults",
}
)
def _declared_fields():
"""The fields resolution decides, read off every shape that reaches the stash.
A keyword on a `declare_*` call is only one shape: the model-override and
post-process passes build a mapping instead (`MODEL_OVERRIDES` literals,
`overrides["dtype"] = ...`, a returned dict), and late resolution splats a
variable-keyed one. Deriving from keywords alone leaves nineteen fields
outside the subject set, `dtype` and `reasoning_parser` among them.
"""
fields = set()
# The declaration calls live wherever a resolver does; the mapping channels
# only exist where the override providers and post-process passes are.
keyword_sources = [_SRT / "server_args.py"]
for sub in ("arg_groups", "hardware_backend", "parser"):
keyword_sources += sorted((_SRT / sub).rglob("*.py"))
mapping_sources = {_SRT / "server_args.py", *(_SRT / "arg_groups").rglob("*.py")}
for path in keyword_sources:
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
for node in ast.walk(tree):
# 1. `declare_resolution(sa, src, page_size=64)` and its siblings
if isinstance(node, ast.Call):
name = (
node.func.id
if isinstance(node.func, ast.Name)
else getattr(node.func, "attr", None)
)
if name in _DECLARERS:
for keyword in node.keywords:
if keyword.arg:
fields.add(keyword.arg)
elif isinstance(keyword.value, ast.Dict):
fields.update(_string_keys(keyword.value))
# 2. every mapping literal in the files that declare through one:
# the MODEL_OVERRIDES tables, the dicts the override providers
# return, the ones the post-process passes build. Scanning
# unrelated files here would collect a plain kwarg dict
# (`tokenizer_config={"trust_remote_code": ...}`) and turn a
# passthrough read into a violation.
if isinstance(node, ast.Dict) and path in mapping_sources:
fields.update(_string_keys(node))
# 3. `overrides["field"] = ...`
if (
path in mapping_sources
and isinstance(node, ast.Assign)
and isinstance(node.targets[0], ast.Subscript)
and isinstance(node.targets[0].slice, ast.Constant)
and isinstance(node.targets[0].slice.value, str)
):
fields.add(node.targets[0].slice.value)
return frozenset(fields & _FIELDS)
def _string_keys(node: ast.Dict) -> set:
return {
key.value
for key in node.keys
if isinstance(key, ast.Constant) and isinstance(key.value, str)
}
def _record_members():
"""Every member of `ServerArgs` / `PortArgs`, by class and name."""
tree = ast.parse((_SRT / "server_args.py").read_text(encoding="utf-8-sig"))
members = {}
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name in ("ServerArgs", "PortArgs"):
for member in node.body:
if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)):
members[f"{node.name}.{member.name}"] = member
return members
def _config_reading_helpers():
"""Module functions that load a decided field off the config they are handed.
A member that hands them `self`, or a call site that hands them a record,
reads the raw input through the callee -- the shape neither an attribute
scan nor a `getattr` scan can see, because the field name is spelled in the
helper and the record is spelled at the call site.
"""
decided = _declared_fields()
helpers = {}
sources = [_SRT / "server_args.py"] + sorted((_SRT / "arg_groups").rglob("*.py"))
for path in sources:
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
for fn in ast.walk(tree):
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
params = {
arg.arg
for arg in list(fn.args.posonlyargs)
+ list(fn.args.args)
+ list(fn.args.kwonlyargs)
} - {"self", "cls"}
if not params:
continue
reads = {
node.attr
for node in ast.walk(fn)
if isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Name)
and node.value.id in params
and isinstance(node.ctx, ast.Load)
and node.attr in decided
}
if reads:
helpers[fn.name] = sorted(reads)
return helpers
# The accessors that hand back the process-global record itself. A helper that
# is handed one of these reads the raw input exactly as a bare `self` would.
_RECORD_ACCESSORS = frozenset({"get_server_args", "global_server_args"})
# `self._server_args` is the same record under a private name; the scan has to
# see it or a reader inside the context object escapes every shape above.
_RECORD_ATTR = re.compile(r"^_*(server_args|sa)$")
def _record_arguments(node, aliases=frozenset()):
"""The bare-record arguments of a call.
Four spellings reach a helper with a record: the bare name (`self`, `sa`),
an attribute (`runner.server_args`), the process-global accessor called
inline (`get_server_args()`), and a local bound to either of the last two
earlier in the same function.
"""
out = []
for arg in node.args:
if isinstance(arg, ast.Name) and arg.id in ("self", "server_args", "sa"):
out.append(arg.id)
elif isinstance(arg, ast.Attribute) and _RECORD_ATTR.match(arg.attr or ""):
out.append(ast.unparse(arg))
elif (
isinstance(arg, ast.Call)
and isinstance(arg.func, ast.Name)
and arg.func.id in _RECORD_ACCESSORS
):
out.append(ast.unparse(arg))
elif isinstance(arg, ast.Name) and arg.id in aliases:
out.append(arg.id)
return out
def _record_aliases(function):
"""Locals bound to the record under a name of their own.
`_sa = getattr(runner, "server_args", None)`, `cfg = get_server_args()` and
`engine_args = ServerArgs.from_cli_args(args)` all put the record behind a
name the argument scan does not recognise, so a later
`getattr(_sa, "<decided leaf>")` reads what the operator typed.
"""
aliases = set()
for node in ast.walk(function):
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
continue
target = node.targets[0]
if not isinstance(target, ast.Name):
continue
value = node.value
if isinstance(value, ast.Attribute):
if _RECORD_ATTR.match(value.attr or ""):
aliases.add(target.id)
continue
if not isinstance(value, ast.Call):
continue
func = value.func
if isinstance(func, ast.Name):
if func.id in _RECORD_ACCESSORS or func.id == "ServerArgs":
aliases.add(target.id)
elif (
func.id == "getattr"
and len(value.args) >= 2
and isinstance(value.args[1], ast.Constant)
and _RECORD_ATTR.match(str(value.args[1].value))
):
aliases.add(target.id)
elif (
isinstance(func, ast.Attribute)
and func.attr == "from_cli_args"
and isinstance(func.value, ast.Name)
and func.value.id == "ServerArgs"
):
aliases.add(target.id)
return aliases
# The one reader for which the raw field is the right answer. The gateway sizes
# its worker pool from the operator's requested replica count; `--dwdp-size`
# makes resolution declare a `dp_size` describing one multi-rank server's
# internal topology, so reading the decision there would spawn dp_size
# single-rank children and ask for dp_size^2 GPUs. A new entry here needs that
# kind of reason next to it.
_NO_RESOLVED_SURFACE = frozenset(
{
(
"sgl-model-gateway/bindings/python/src/sglang_router/launch_server.py",
"server_args.dp_size",
),
}
)
def _is_record_base(node, aliases):
"""Is this expression the record itself?
A local bound to one, a parameter that carries one (`server_args`, `sa`,
`engine_args`), or an attribute holding one (`self._server_args`).
"""
if isinstance(node, ast.Name):
return node.id in aliases
if isinstance(node, ast.Attribute):
return bool(_RECORD_ATTR.match(node.attr or ""))
return False
def _record_handoff_offenders(rel, tree, helpers, decided, is_record=False):
"""Every way a decided leaf is reached through a record in one module.
Two shapes, both scanned under the record aliases the function binds:
handing the record to a helper that loads a decided field, and loading one
off the alias directly (`alias.<leaf>` or `getattr(alias, "<leaf>")`). The
second is what the MiniMax backend spelled, and an argument scan cannot see
it -- the leaf never appears at a call site.
"""
offenders, seen = [], set()
def record(lineno, text):
if (lineno, text) in seen:
return
seen.add((lineno, text))
offenders.append(f"{rel}:{lineno} {text}")
scopes = [(tree, frozenset())] + [
(fn, _record_aliases(fn))
for fn in ast.walk(tree)
if isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef))
]
for scope, aliases in scopes:
for node in ast.walk(scope):
if isinstance(node, ast.Call):
name = (
node.func.id
if isinstance(node.func, ast.Name)
else getattr(node.func, "attr", None)
)
if name in helpers:
for arg in _record_arguments(node, aliases):
if arg == "self" and not is_record:
continue
record(
node.lineno,
f"{name}({arg}) reads {', '.join(helpers[name])}",
)
if (
isinstance(node.func, ast.Name)
and node.func.id == "getattr"
and len(node.args) >= 2
and isinstance(node.args[0], ast.Name)
and node.args[0].id in aliases
and isinstance(node.args[1], ast.Constant)
and node.args[1].value in decided
):
record(
node.lineno,
f'getattr({node.args[0].id}, "{node.args[1].value}")',
)
elif (
isinstance(node, ast.Attribute)
and isinstance(node.ctx, ast.Load)
and node.attr in decided
and _is_record_base(node.value, aliases)
):
record(node.lineno, f"{ast.unparse(node.value)}.{node.attr}")
return offenders
# Source the scanner must read the same way whether or not the tree happens to
# contain these shapes today. The first four are the spellings that reached
# production and were converted; the last two are the legal forms next to them,
# which have to stay quiet or the guard is unusable.
_SPELLINGS = """
def hands_the_alias_to_a_helper(runner):
_sa = getattr(runner, "server_args", None)
return m3_fp8_attn_gemm_enabled(_sa)
def loads_a_leaf_off_the_alias(runner):
_sa = getattr(runner, "server_args", None)
return getattr(_sa, "speculative_num_draft_tokens", None)
def reads_a_leaf_through_the_alias(runner):
sa_local = runner.server_args
return sa_local.attention_backend
def hands_the_accessor_to_a_helper():
return attention_backends_of(get_server_args())
def reads_the_view(runner):
cfg = resolving_view(runner.server_args)
return cfg.attention_backend
def reads_an_undecided_leaf(runner):
_sa = runner.server_args
return _sa.tp_size
def reads_a_leaf_off_a_private_attribute(self):
return self._server_args.attention_backend
def reads_a_leaf_off_a_constructed_record(cli):
engine_args = ServerArgs.from_cli_args(cli)
engine_args.resolve_once()
return engine_args.attention_backend
"""
class TestResolutionReadsTheDeclarations(CustomTestCase):
def test_no_hook_reads_a_field_off_the_record(self):
offenders = []
files = sorted((_SRT / "arg_groups").glob("*.py"))
self.assertGreater(len(files), 5, "the hook scan found almost nothing")
for path in files:
rel = f"arg_groups/{path.name}"
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
for fn in ast.walk(tree):
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
holders = _holders(fn)
if not holders:
continue
for lineno, field in _field_reads(fn, holders):
offenders.append(f"{rel}:{lineno} {fn.name} reads .{field}")
self.assertEqual(
offenders,
[],
"a resolution hook reads a field off the record; the field holds the "
"raw input, so this decides from what was typed rather than from "
"what resolution decided. Read `resolving_view(server_args)`:\n "
+ "\n ".join(offenders),
)
def test_the_record_hosts_no_resolution_handler(self):
"""The pipeline and every step it runs live under `arg_groups/`.
While a step was a method, it could read a raw field off `self` and
`test_no_handler_reads_a_field_off_self` had to say it could not. There
is no such method left, so the invariant is now the stronger one: the
record hosts none of them. What the steps read is checked on the
package side, by `test_no_hook_reads_a_field_off_the_record`.
"""
handlers = sorted(
name
for name, node in _record_members().items()
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and (
name.split(".")[-1].startswith(("_handle_", "_validate_"))
or "resolution_pipeline" in name
)
)
self.assertEqual(
handlers,
[],
"a resolution handler is back on the record; it belongs in an "
"`arg_groups` family, where the package-side guards can see it:\n "
+ "\n ".join(handlers),
)
def test_no_member_recomputes_from_a_raw_field(self):
decided = _declared_fields()
self.assertGreater(
len(decided), 100, f"the declaration set derived only {len(decided)} fields"
)
members = _record_members()
# The floor is here to catch the scan collapsing, not to pin the
# class's size -- it drops as derived members move to their namespaces
# and become declarations rather than methods on the record.
self.assertGreater(len(members), 10, f"only {len(members)} members were found")
offenders = []
for name, fn in sorted(members.items()):
holders = _holders(fn) | {"self"}
for lineno, field in _field_reads(fn, holders):
if field in decided:
offenders.append(f"server_args.py:{lineno} {name} reads .{field}")
self.assertEqual(
offenders,
[],
"a record member recomputes from a field resolution decides; the "
"field holds the raw input, so the member answers for what was "
"typed. Bind `cfg = resolving_view(self)` and read that:\n "
+ "\n ".join(offenders),
)
def test_no_reader_hands_the_record_to_a_config_helper(self):
helpers = _config_reading_helpers()
self.assertGreater(
len(helpers), 5, f"the helper derivation found only {len(helpers)}"
)
decided = _declared_fields()
offenders = []
# `scripts/`, `examples/` and the gateway binding are outside the
# package but hold records they resolve themselves, and every reader
# this scan found in them was reading a field resolution fills in.
_REPO = _SRT.parent.parent.parent
roots = (
[_SRT]
+ [_SRT.parent / d for d in ("benchmark", "lang")]
+ [
_REPO / d
for d in (
"scripts",
"examples",
"sgl-model-gateway/bindings/python/src",
)
]
)
for root in roots:
if not root.exists():
continue
for path in sorted(root.rglob("*.py")):
# Package files keep their `srt/...` spelling (the skips below
# key on it); the repo-level roots are named from the repo.
try:
rel = path.relative_to(_SRT.parent).as_posix()
except ValueError:
rel = path.relative_to(_REPO).as_posix()
if rel.startswith(("srt/arg_groups/", "multimodal_gen/")):
continue
try:
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
except SyntaxError:
continue
offenders += _record_handoff_offenders(
rel, tree, helpers, decided, is_record=rel == "srt/server_args.py"
)
offenders = [
line
for line in offenders
if (line.split(":", 1)[0], line.split(" ", 1)[1])
not in _NO_RESOLVED_SURFACE
]
self.assertEqual(
offenders,
[],
"a caller hands the record to a helper that loads a field "
"resolution decides; the helper then reads the raw input. Hand it "
"`resolving_view(record)` (or the published bag):\n "
+ "\n ".join(offenders),
)
def test_the_scan_sees_every_spelling_that_reached_production(self):
"""Every spelling that reached production, pinned next to the scanner.
A shape the scan stops seeing is a silent hole, so each one is listed
here with the legal forms beside it and the flagged set compared
exactly.
What it does not reach: a record that arrives as a *parameter* and was
resolved by the caller (`scripts/playground/bench_speculative.py` hands
`main(args, server_args)` one). Binding that would need the call graph,
and naming a parameter `server_args` is also how the resolution-time
readers spell a view.
"""
helpers = _config_reading_helpers()
decided = _declared_fields()
for name in ("m3_fp8_attn_gemm_enabled", "attention_backends_of"):
self.assertIn(name, helpers, f"the helper derivation lost {name}")
for field in ("speculative_num_draft_tokens", "attention_backend"):
self.assertIn(field, decided, f"the declared set lost {field}")
offenders = _record_handoff_offenders(
"sample.py", ast.parse(_SPELLINGS), helpers, decided
)
flagged = {line.split(" ", 1)[1] for line in offenders}
self.assertEqual(
flagged,
{
"m3_fp8_attn_gemm_enabled(_sa)"
" reads " + ", ".join(helpers["m3_fp8_attn_gemm_enabled"]),
'getattr(_sa, "speculative_num_draft_tokens")',
"sa_local.attention_backend",
"self._server_args.attention_backend",
"engine_args.attention_backend",
"attention_backends_of(get_server_args())"
" reads " + ", ".join(helpers["attention_backends_of"]),
},
"the scan lost a spelling, or started flagging a legal one:\n "
+ "\n ".join(sorted(flagged)),
)
if __name__ == "__main__":
import unittest
unittest.main()
@@ -1,583 +0,0 @@
"""Nobody reaches the startup record through another object for a resolved value.
The supplied-instance census counts three spellings, all of which start from a
`server_args` parameter -- the caller chose the object, which is the contract
that makes those reads defensible. This pins the fourth: `model_runner.
server_args.field`, `self.scheduler.server_args.field`, `tokenizer_manager.
server_args.field`. A reference lifted off whatever object happened to hold the
record carries no contract at all, and it was invisible to every census, which
is how it grew to 105 reads across 36 files unnoticed.
They are gone, and this is what keeps them gone. Only fields resolution writes
are pinned: reading `model_runner.server_args.host` off the record answers with
what the caller asked for, which is what the record is for. The written set is
derived from the declaration sites rather than listed, so a field that stops
being resolution-written drops out on its own -- and a field that stops being
*declared* cannot slip out that way, because bare assignment during resolution
is refused by `server_args/test_resolution_declarations.py`.
"""
import ast
import pathlib
import unittest
import sglang
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=28, suite="base-a-test-cpu")
_PACKAGE = pathlib.Path(sglang.__file__).resolve().parent
_SRT = _PACKAGE / "srt"
# The pipeline and its extension points: reading the in-flight record is their
# job, and they run before anything is published.
_OWNERS = ("server_args.py", "runtime_context.py", "arg_groups/")
# Where the writers are. Resolution lives in `srt` -- nothing outside it
# declares -- so the written-field derivations scan `srt` while the *reads* are
# counted across the whole shipped package: a borrowed read answers with the
# startup default wherever it is written, and `benchmark/` ships too.
_READS_SCANNED = _PACKAGE
_DECLARERS = ("declare_resolution",)
def _declared_by_keyword():
"""Fields named as a keyword at a declaration site."""
written = set()
for path in sorted(_SRT.rglob("*.py")):
source = path.read_text(encoding="utf-8-sig")
if not any(declarer in source for declarer in _DECLARERS):
continue
try:
tree = ast.parse(source)
except SyntaxError:
raise AssertionError(f"unparsable module in the census: {path}")
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
if isinstance(node.func, ast.Attribute):
name = node.func.attr
elif isinstance(node.func, ast.Name):
name = node.func.id
else:
continue
if name in _DECLARERS:
written |= {kw.arg for kw in node.keywords if kw.arg}
return written
def _returned_field_names(function):
"""Field names a provider/pass writes: the keys of the mapping it returns.
Only the returned mapping counts -- walking every `ast.Dict` in the body
also collects a dict-valued field's *nested* keys and any unrelated local
mapping, and those stray names would reject valid borrowed reads of fields
resolution never writes. The mapping is traced through four spellings: a
returned literal, assignments (annotated or not) to a returned name, a
literal-key subscript write on it, and `.update(field=...)` on it. A
spelling this cannot see raises instead of skipping.
``overrides[name]`` is also accepted when ``name`` comes from
``for name in ("a", "b", ...)`` -- the keys stay statically enumerable.
"""
names = set()
returned = set()
# for x in ("a", "b"): ... -> {"x": {"a", "b"}}
loop_keys = {
node.target.id: {elt.value for elt in node.iter.elts}
for node in ast.walk(function)
if isinstance(node, ast.For)
and isinstance(node.target, ast.Name)
and isinstance(node.iter, (ast.Tuple, ast.List))
and node.iter.elts
and all(
isinstance(elt, ast.Constant) and isinstance(elt.value, str)
for elt in node.iter.elts
)
}
def top_level_keys(mapping):
for key in mapping.keys:
if not (isinstance(key, ast.Constant) and isinstance(key.value, str)):
raise AssertionError(f"non-literal key in {function.name}")
names.add(key.value)
def add_subscript_key(key):
if isinstance(key, ast.Constant):
names.add(key.value)
elif isinstance(key, ast.Name) and key.id in loop_keys:
names.update(loop_keys[key.id])
else:
raise AssertionError(f"non-literal key in {function.name}")
for node in ast.walk(function):
if isinstance(node, ast.Return) and node.value is not None:
value = node.value
if isinstance(value, ast.Dict):
top_level_keys(value)
elif isinstance(value, ast.Name):
returned.add(value.id)
elif isinstance(value, ast.Constant) and value.value is None:
pass
else:
raise AssertionError(
f"opaque return in {function.name}: {ast.unparse(value)}"
)
for node in ast.walk(function):
if isinstance(node, (ast.Assign, ast.AnnAssign)):
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
for target in targets:
if (
isinstance(target, ast.Name)
and target.id in returned
and isinstance(node.value, ast.Dict)
):
top_level_keys(node.value)
if isinstance(target, ast.Subscript) and (
isinstance(target.value, ast.Name) and target.value.id in returned
):
add_subscript_key(target.slice)
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "update"
and isinstance(node.func.value, ast.Name)
and node.func.value.id in returned
):
names |= {kw.arg for kw in node.keywords if kw.arg}
# A positional dict literal has to be read here. The Dict walk above
# only reaches literals that are *returned* or assigned to a returned
# name, so `d.update({"field": value})` was being type-checked and
# then dropped -- silently, under a comment claiming otherwise.
for arg in node.args:
if isinstance(arg, ast.Dict):
top_level_keys(arg)
else:
raise AssertionError(f"opaque update() argument in {function.name}")
if any(kw.arg is None for kw in node.keywords):
raise AssertionError(f"**kwargs update() in {function.name}")
return names
def _declared_by_registry_and_passes():
"""Fields the model-override registry and the post-process passes write.
These field names are *data* -- dict keys, not keywords -- so a keyword
scan misses every one of them. The callables are collected from the live
registries rather than by matching decorator names: 26 of the 27 providers
register through a `_register_for(...)` helper, so a scan for
`@register_model_override*` sees exactly one of them and reports a healthy
census over a channel it cannot see.
"""
import sys
from sglang.srt.arg_groups import overrides
# Resolve each callable's body in the file it actually lives in. The
# declarations are spread over `arg_groups/model_overrides/`, one module per
# model family, and a scan hard-coded to `overrides.py` would find none of
# them -- and, worse, would keep reporting a healthy census while doing it.
bodies_by_module = {}
def _bodies(module_name):
if module_name not in bodies_by_module:
path = getattr(sys.modules[module_name], "__file__", None)
assert path, f"{module_name} has no source file"
module_tree = ast.parse(pathlib.Path(path).read_text(encoding="utf-8-sig"))
bodies_by_module[module_name] = {
node.name: node
for node in ast.walk(module_tree)
if isinstance(node, ast.FunctionDef)
}
return bodies_by_module[module_name]
callables = {fn for fns in overrides._MODEL_OVERRIDE_FNS.values() for fn in fns}
callables |= {
fn for _predicate, fn in getattr(overrides, "_PREDICATE_OVERRIDE_FNS", ())
}
callables |= set(overrides.POST_PROCESS_PASSES)
fields = set()
for fn in callables:
name = getattr(fn, "__name__", "")
body = _bodies(fn.__module__).get(name)
# Loud, not silent: a body this scan cannot find is a field census it
# is not taking, and a narrower census makes every check downstream of
# it quietly vacuous.
assert body is not None, f"{fn.__module__}.{name} has no body to scan"
fields |= _returned_field_names(body)
# The literal arch -> {field: value} table, which has no callable at all.
# It lives with the rest of the registry, in `model_override_base`.
from sglang.srt.arg_groups import model_override_base
table_tree = ast.parse(
pathlib.Path(model_override_base.__file__).read_text(encoding="utf-8-sig")
)
seen_table = False
for node in table_tree.body:
target = None
if isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name):
target = node.targets[0].id
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
target = node.target.id
if target != "MODEL_OVERRIDES" or node.value is None:
continue
for inner in ast.walk(node.value):
if not isinstance(inner, ast.Dict):
continue
for key, value in zip(inner.keys, inner.values):
if isinstance(value, ast.Dict):
continue
if not isinstance(key, ast.Constant):
raise AssertionError("non-literal override key")
fields.add(key.value)
seen_table = True
assert seen_table, "MODEL_OVERRIDES is not where this scan looks for it"
return fields
def _written_after_publish():
"""Fields the runtime overrides once the bags exist.
Imported from the supplied-instance ratchet rather than re-derived: it
already enumerates `get_context().override(...)` and its named wrapper, and
a second derivation of the same channel is what drifts narrower. A borrowed
read of one of these answers with the startup value the same way a
resolution-written one does -- the write just lands later.
"""
import importlib.util
companion = (
pathlib.Path(__file__).resolve().parent
/ "test_supplied_instance_exposure_ratchet.py"
)
spec = importlib.util.spec_from_file_location("_exposure_for_ratchet", companion)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return set(module.TestSuppliedInstanceExposure._override_written_fields())
def _resolution_written():
"""Every field the startup record answers wrong once it stays raw."""
return (
_declared_by_keyword()
| _declared_by_registry_and_passes()
| _written_after_publish()
)
def _borrowed_parking_spans(tree):
"""[(span, parked attribute names)] for classes that park a borrowed record.
The supplied-instance census counts `self.server_args.<field>` only where
the class was handed the record as a parameter -- `self.server_args =
server_args` inside a method that takes one. A class that borrows it off
another object instead (`self.server_args = scheduler.server_args`) is
covered by neither census, and a read through that attribute is exactly the
borrowed-record chain read this file is about. The parked name is whatever
the class chose -- `self.args = scheduler.server_args` hides the same read,
so the assignment target is recorded, not assumed.
"""
spans = []
for cls in (n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)):
parked_names = set()
for node in ast.walk(cls):
# Both assignment spellings -- `self.args = x.server_args` and the
# annotated `self.args: ServerArgs = x.server_args`.
if isinstance(node, ast.Assign):
targets = node.targets
elif isinstance(node, ast.AnnAssign) and node.value is not None:
targets = [node.target]
else:
continue
# A bare name on the right is the parameter the companion census
# follows; an attribute chain ending in `.server_args` is a record
# taken off another object.
if not (
isinstance(node.value, ast.Attribute)
and node.value.attr == "server_args"
):
continue
for target in targets:
if (
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == "self"
):
parked_names.add(target.attr)
if parked_names:
spans.append(((cls.lineno, cls.end_lineno), parked_names))
return spans
def _subtrees_with_their_own_record():
"""Top-level package directories that define a second `ServerArgs`.
`multimodal_gen` ships one, so `x.server_args.<field>` inside it names a
field of *that* class -- and `model_path` is a field of both. Keying on the
spelling alone once put a resolution call into a diffusion entry point.
Derived from the class definitions rather than named here, so a third
record would be excluded the same way instead of silently counting.
"""
roots = set()
for path in _PACKAGE.rglob("*.py"):
rel = path.relative_to(_PACKAGE).as_posix()
if rel.startswith("srt/") or "/" not in rel:
continue
source = path.read_text(encoding="utf-8-sig")
if "ServerArgs" not in source:
continue
try:
tree = ast.parse(source)
except SyntaxError:
continue
if any(
isinstance(node, ast.ClassDef) and node.name == "ServerArgs"
for node in ast.walk(tree)
):
roots.add(rel.split("/")[0])
return roots
def _reads_the_startup_record(tree):
"""True when this module's `server_args` is the one `srt` resolves.
Inside a subtree that owns another record, only a module that imports the
startup record is talking about it.
"""
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
if node.module.startswith("sglang.srt"):
# The *original* names: `ServerArgs as SrtServerArgs` is still
# the startup record, whatever this module calls it.
names = {alias.name for alias in node.names}
if names & {"ServerArgs", "server_args", "prepare_server_args"}:
return True
return False
def _chain_reads(written):
"""`<expression>.server_args.<field>` where the field is resolution-written."""
found = []
other_records = _subtrees_with_their_own_record()
for path in sorted(_READS_SCANNED.rglob("*.py")):
rel = path.relative_to(_READS_SCANNED).as_posix()
in_srt = rel.startswith("srt/")
if in_srt:
under_srt = rel[len("srt/") :]
if path.name in _OWNERS or under_srt.startswith(_OWNERS[-1]):
continue
source = path.read_text(encoding="utf-8-sig")
if "server_args" not in source:
continue
try:
tree = ast.parse(source)
except SyntaxError:
raise AssertionError(f"unparsable module in the census: {rel}")
if (
not in_srt
and rel.split("/")[0] in other_records
and not _reads_the_startup_record(tree)
):
continue
parked = _borrowed_parking_spans(tree)
def parked_alias(lineno, name):
return any(
start <= lineno <= end and name in names
for (start, end), names in parked
)
for node in ast.walk(tree):
if not (
isinstance(node, ast.Attribute)
and isinstance(node.ctx, ast.Load)
and node.attr in written
):
continue
base = node.value
if not isinstance(base, ast.Attribute):
continue
through_self = isinstance(base.value, ast.Name) and base.value.id == "self"
if base.attr == "server_args":
# `self.server_args.field` is the parked spelling the
# supplied-instance census counts -- but only where the record
# arrived as a parameter.
if through_self and not parked_alias(node.lineno, base.attr):
continue
elif not (through_self and parked_alias(node.lineno, base.attr)):
# Any other attribute counts only as a recorded parked alias
# (`self.args = scheduler.server_args` and later `self.args.x`).
continue
suffix = " (parked borrowed record)" if through_self else ""
found.append(f"{rel}:{node.lineno} {ast.unparse(base)}.{node.attr}{suffix}")
# `getattr(model_runner.server_args, "field", default)` reads the same
# borrowed record through a `Call`, with the same stale default.
for node in ast.walk(tree):
if not (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "getattr"
and len(node.args) >= 2
and isinstance(node.args[1], ast.Constant)
and node.args[1].value in written
):
continue
base = node.args[0]
if not isinstance(base, ast.Attribute):
continue
through_self = isinstance(base.value, ast.Name) and base.value.id == "self"
if base.attr == "server_args":
if through_self and not parked_alias(node.lineno, base.attr):
continue
elif not (through_self and parked_alias(node.lineno, base.attr)):
continue
found.append(
f"{rel}:{node.lineno} getattr({ast.unparse(base)}, "
f"{node.args[1].value!r})"
)
return sorted(found)
def _passes_named_at_call_sites() -> set:
"""Names passed to ``run_post_process_pass(sa, fn)`` anywhere in the tree.
A call whose pass is not a bare name is a hard failure, not a skip: this
scan is the ground truth every registry-driven check below is derived from,
so `run_post_process_pass(self, overrides._new_pass)` (an `ast.Attribute`)
or `run_post_process_pass(self, fn=_new_pass)` (a keyword) would otherwise
walk past all of them silently. Keeping the call shape uniform is the
price of the scan being complete.
"""
names = set()
for path in sorted(pathlib.Path(next(iter(sglang.__path__))).rglob("*.py")):
source = path.read_text(encoding="utf-8-sig")
if "run_post_process_pass" not in source:
continue
try:
tree = ast.parse(source)
except SyntaxError:
continue
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
if isinstance(func, ast.Name):
called = func.id
elif isinstance(func, ast.Attribute):
called = func.attr
else:
called = None
if called != "run_post_process_pass":
continue
if (
len(node.args) != 2
or node.keywords
or not isinstance(node.args[1], ast.Name)
):
raise AssertionError(
f"{path}:{node.lineno}: run_post_process_pass takes the pass "
"as a bare name in its second positional argument; "
f"{ast.unparse(node)!r} is invisible to this scan and to "
"every registry-driven check derived from it"
)
names.add(node.args[1].id)
return names
class TestEveryInvokedPassIsRegistered(CustomTestCase):
"""The registry is what the scans above enumerate, so a pass missing from it
is a pass nothing checks.
Being invoked and being registered are two edits, and `_a2a_fusion_adjustments`
shipped with only the first: it ran in production while the registry-driven
scans walked past it. The call sites are the ground truth here -- the registry
is derived from a decorator someone has to remember.
"""
def test_the_registry_covers_every_call_site(self):
from sglang.srt.arg_groups import overrides
invoked = _passes_named_at_call_sites()
self.assertGreater(
len(invoked),
20,
f"only {len(invoked)} call sites found; the scan is broken, not the tree",
)
registered = {fn.__name__ for fn in overrides.POST_PROCESS_PASSES}
self.assertEqual(
set(),
invoked - registered,
"these passes are invoked but carry no @register_post_process, so "
"every check that walks POST_PROCESS_PASSES skips them",
)
self.assertEqual(
set(),
registered - invoked,
"these passes carry @register_post_process but no slot invokes "
"them; deleting a call site and leaving the decorator behind "
"leaves a pass that only the scans can see",
)
class TestNoChainReadsOfResolvedConfig(CustomTestCase):
def test_the_census_has_something_to_count(self):
"""A written set that collapsed would make the pin vacuous.
Each mechanism is checked on its own, because they fail
independently: the keyword scan cannot see a field name that is data,
and a scan for `@register_model_override*` sees one provider out of
twenty-seven because the rest register through a helper. A hand-written
expectation of the resulting field names is what hid that -- it stayed
green while a whole channel went unscanned -- so each mechanism is
pinned by a floor derived from the live registry instead.
"""
from sglang.srt.arg_groups import overrides
by_keyword = _declared_by_keyword()
by_data = _declared_by_registry_and_passes()
self.assertGreater(
len(by_keyword),
100,
f"only {len(by_keyword)} fields are declared by keyword; the scan broke",
)
providers = {fn for fns in overrides._MODEL_OVERRIDE_FNS.values() for fn in fns}
providers |= {
fn for _predicate, fn in getattr(overrides, "_PREDICATE_OVERRIDE_FNS", ())
}
self.assertGreater(
len(providers) + len(overrides.POST_PROCESS_PASSES),
50,
"the registry and pass tables collapsed; the data-channel scan is "
"reading an empty registry",
)
self.assertGreater(
len(by_data),
25,
f"only {len(by_data)} fields come from the registry and the passes, "
f"across {len(providers)} providers and "
f"{len(overrides.POST_PROCESS_PASSES)} passes; the scan of the "
"dict-key channel broke",
)
# The data channel is not the keyword scan's subset: if it became one,
# that scan would be doing all the work and a regression here would be
# invisible.
self.assertTrue(by_data - by_keyword, "the data channel adds nothing")
def test_nothing_reads_a_resolved_field_off_a_borrowed_record(self):
found = _chain_reads(_resolution_written())
self.assertEqual(
found,
[],
"these reach the startup record through another object for a value "
"resolution decides, so they answer with the CLI default once the "
"record stays raw; read the config bag instead:\n " + "\n ".join(found),
)
if __name__ == "__main__":
unittest.main()
@@ -1,874 +0,0 @@
"""The supplied-instance surface is measured on two axes, and may only shrink.
A callee that takes ``server_args`` keeps the supplied-instance contract: the
caller chose the object, so no global-read ratchet counts it. Step 12 changes
what that object *carries* — the instance stays at the user's raw input — so a
callee reading a field **resolution fills in** would start seeing the CLI default
instead of the effective value.
This pins that intersection. Each entry is one (file, field) pair where a
parameter named ``server_args`` is read for a field resolution writes; the plan
doc carries the proposed disposition per field
(``global_context/12-raw-input-config.md``, "the supplied-instance conversion
list"). New pairs fail: a new one is new step-12 work, and the moment to decide
where the value should come from is when the read is written, not during the
flip. Pairs that disappear also fail, with the entry to delete — the list is the
measurement, not a memory of one.
The written-field set is derived here rather than hardcoded: the
representative configs in ``_MATRIX`` (one per resolution family it exercises)
are resolved and compared against the dataclass defaults, the same matrix the
context repo's audit tool uses. Ambient environment is normalized per entry --
resolution branches on CI detection and leaves sticky process state, so each
entry resolves from the pristine snapshot, and the CI shape is an explicit
entry rather than an accident of the runner. The read scan mirrors that
tool's three shapes — a parameter attribute, ``getattr(server_args, "literal")``,
and the parameter parked on ``self`` — because two implementations of one census
that disagree are worse than either alone.
The second axis is **already wrong today**, not after a flip. Some config is
decided *after* publish and recorded with ``get_context().override(...)`` —
elastic-EP resizing `ep_size`, a weight update rewriting `model_path` /
`load_format`, HiCache attach naming a storage backend, adaptive speculative
decoding moving `speculative_num_steps`. That write reaches the bags and never
the record, so a supplied-instance read of one of those fields answers with the
startup value from the moment the override lands. Whether that is a defect
depends on ordering — a value copied at construction, before any override, is
fine — so this axis is pinned as a measurement with the same growth guard rather
than as a list of bugs. One of them *was* a defect and is fixed at the base of
this stack: the linear-attn dispatch table rebuilt itself from the record after
the SM100 GDN prefill decision had been recorded in the bag, so a second runner's
rebuild dropped it. That choice is a per-runner stamp now and is not recorded
process-wide at all, so neither the read nor the field is on this axis.
"""
import ast
import json
import os
import shutil
import tempfile
import unittest
from pathlib import Path
import msgspec
import msgspec.structs
import sglang
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci, register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=23, suite="base-a-test-cpu")
# Also on a CUDA runner: the written set is derived by resolving on the running
# host, and `is_cuda()` / capability gates only open on real hardware. The pin
# is split by host so both registrations stay exact: `_EXPOSED` is asserted
# everywhere, and a pair whose write only happens on CUDA belongs in
# `_EXPOSED_CUDA_ONLY` -- pinned on the CUDA runner, invisible to the CPU
# assertion. Without the split, one shared exact list could not hold such a
# pair at all: pinning it fails the CPU run as "gone", omitting it fails the
# CUDA run as "new". (No AMD registration: an `is_hip()`-gated write would
# shift the exact sets in ways none of the pinning hosts can verify; the ROCm
# resolution surface is covered by `test_resolution_is_reproducible.py`
# instead, whose assertion is device-agnostic.)
register_cuda_ci(est_time=16, stage="base-b", runner_config="1-gpu-small")
_PACKAGE_ROOT = Path(next(iter(sglang.__path__))) / "srt"
# The config the resolution pipeline owns; reading the in-flight record is their
# job, not a supplied-instance read.
_OWNERS = ("server_args.py", "runtime_context.py", "arg_groups/")
_MINI_CONFIG = {
"architectures": ["LlamaForCausalLM"],
"model_type": "llama",
"hidden_size": 16,
"intermediate_size": 32,
"num_attention_heads": 2,
"num_key_value_heads": 2,
"num_hidden_layers": 2,
"vocab_size": 128,
"max_position_embeddings": 2048,
}
# One config resolves only its own decisions, so the written set is a union.
_MATRIX = (
{},
{
"speculative_algorithm": "EAGLE",
"speculative_num_steps": 3,
"speculative_eagle_topk": 1,
"speculative_num_draft_tokens": 4,
},
{"dp_size": 2, "tp_size": 2, "enable_dp_attention": True},
# DWDP resolves dp_size and enable_dp_attention *itself* -- the plain DP
# entry above passes them in, and passed-in fields are excluded from the
# written set, so without this entry the dp_size readers would never pin.
{"tp_size": 2, "dwdp_size": 2},
{"enable_hierarchical_cache": True, "hicache_ratio": 2.0},
{"disaggregation_mode": "prefill"},
{"tp_size": 2, "attn_cp_size": 2},
{"enable_lora": True, "max_lora_rank": 16},
{"kv_cache_dtype": "fp8_e4m3", "page_size": 64},
# MIS resolves disable_radix_cache (and friends) itself; the backend is
# passed in because the handler asserts flashinfer rather than switching.
{"enable_mis": True, "attention_backend": "flashinfer"},
)
# `declare_resolution` call sites whose keyword expansion is built
# dynamically; the written fields are spelled out here and drift-guarded.
_LATE_RESOLUTION_DYNAMIC_SITES = {
"parser/template_detection.py": frozenset({"reasoning_parser", "tool_call_parser"}),
}
# `get_context().override(...)` declares through the same seam, but the fields
# are the caller's -- a test names them one call at a time. There is no static
# set to collect, and nothing resolution decides: whatever a caller overrides
# there is exposure only through that caller's own reads.
_CALLER_SUPPLIED_LATE_SITES = frozenset({"runtime_context.py"})
# Resolution also branches on ambient environment; those shapes are explicit
# entries so the written set is the same on every host. `SGLANG_IS_IN_CI`
# makes resolution fill `soft_watchdog_timeout`.
_ENV_MATRIX = (({}, {"SGLANG_IS_IN_CI": "true"}),)
# Only true constructor inputs: `tokenizer_path` / `served_model_name` are
# resolution-written (filled from `model_path` when unset), so their readers
# are step-12 exposure like any other pair.
_PASSED = frozenset({"model_path", "device", "random_seed"})
# Empty. A pair belongs here when a reader has no bag to read -- it runs before
# its process publishes -- and cannot use `resolving_view` either. The launcher's
# pre-publish reads (`_set_envs_and_config`, the auto-parser gate) and the
# late-resolution detection it calls all read the declarations now, so nothing
# qualifies. A new entry needs that kind of reason next to it.
_EXPOSED: frozenset = frozenset()
# Pairs whose resolution write only happens on a CUDA host (capability or
# `is_cuda()` gated): asserted on the CUDA registration, invisible to the CPU
# one. Empty today -- the current written sets coincide across the two hosts --
# but this is where a GPU-only write's readers get pinned without breaking the
# CPU-exact assertion.
_EXPOSED_CUDA_ONLY: frozenset = frozenset()
# Axis two: (file, field) pairs where a supplied-instance read names a field that
# some code overrides post-publish. Each needs an ordering judgment, not a blanket
# conversion; the list exists so a new one is a decision made when it is written.
_OVERRIDDEN_AND_READ: frozenset = frozenset()
def _expanded_override_keys(rel, tree, call, kw) -> set:
"""The statically visible keys behind an ``override(..., **expr)``.
Handles a dict literal, a conditional between dict literals, and a name
bound to a dict literal in the enclosing function (plus constant-subscript
stores onto it -- the HiCache attach shape). One expansion is unresolvable
by design and exempted by name: ``update_server_args`` forwards
operator-chosen fields, so its key set is the API's, not this file's.
Anything else unresolvable fails -- a silently skipped expansion would
shrink the written set.
"""
for a in call.args:
if isinstance(a, ast.Constant) and a.value == "update_server_args":
return set()
for k in call.keywords:
if (
k.arg == "source"
and isinstance(k.value, ast.Constant)
and k.value.value == "update_server_args"
):
return set()
def loop_variable_values(name: str) -> set:
"""The values a `for name, ... in (<literal tuples>)` loop binds.
A handler that records one field per loop iteration spells the field
names in the loop's own literal, so they are still static.
"""
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) -> set:
assert isinstance(node, ast.Dict), (
f"non-literal dict in override expansion at {rel}:{call.lineno}"
)
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 override expansion at {rel}:{call.lineno}"
)
bound = loop_variable_values(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)
if isinstance(kw.value, ast.IfExp):
keys = set()
for branch in (kw.value.body, kw.value.orelse):
if isinstance(branch, ast.Dict) and branch.keys:
keys |= dict_keys(branch)
elif isinstance(branch, ast.Dict):
pass
else:
raise AssertionError(
f"unresolvable override expansion at {rel}:{call.lineno}"
)
return keys
assert isinstance(kw.value, ast.Name), (
f"unresolvable override expansion at {rel}:{call.lineno}"
)
name = kw.value.id
enclosing = None
for fn in ast.walk(tree):
if isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
if (
fn.lineno
<= call.lineno
<= max(getattr(fn, "end_lineno", fn.lineno), fn.lineno)
):
if enclosing is None or fn.lineno > enclosing.lineno:
enclosing = fn
assert enclosing is not None, (
f"override expansion outside any function at {rel}:{call.lineno}"
)
keys = set()
found = False
for node in ast.walk(enclosing):
if (
isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Name)
and node.targets[0].id == name
and isinstance(node.value, ast.Dict)
):
found = True
keys |= dict_keys(node.value)
elif (
isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Subscript)
and isinstance(node.targets[0].value, ast.Name)
and node.targets[0].value.id == name
and isinstance(node.targets[0].slice, ast.Constant)
):
keys.add(node.targets[0].slice.value)
assert found, (
f"override expansion '{name}' at {rel}:{call.lineno} has no "
"dict-literal assignment in its function; extend the resolver"
)
return keys
class TestSuppliedInstanceExposure(CustomTestCase):
def _callTestMethod(self, method):
# No CI retry: a failed first attempt has already resolved the matrix
# and mutated process state; a retry against that contamination could
# pass on a drifted written set or mask a real drift.
return unittest.TestCase._callTestMethod(self, method)
def setUp(self):
# Resolving the matrix writes process state on the way through (the
# multimodal transport handler sets SGLANG_USE_CUDA_IPC_TRANSPORT, and
# `EnvField.set()` flips a descriptor flag `os.environ` does not carry).
# Leaking it makes *later* files in the same worker fail, which is how
# this was found -- so the case restores what it touched.
super().setUp()
state = (dict(os.environ), self._env_field_flags())
self.addCleanup(self._restore_process_state, state)
@staticmethod
def _env_field_flags() -> dict:
from sglang.srt.environ import EnvField, envs
flags = {}
for klass in reversed(type(envs).__mro__):
for name, field in vars(klass).items():
if isinstance(field, EnvField):
flags[name] = field._set_to_none
return flags
@staticmethod
def _restore_process_state(state) -> None:
from sglang.srt.environ import envs
saved_environ, saved_flags = state
os.environ.clear()
os.environ.update(saved_environ)
for name, was_none in saved_flags.items():
getattr(type(envs), name)._set_to_none = was_none
def _config_dir(self) -> str:
config_dir = tempfile.mkdtemp(prefix="supplied_instance_")
self.addCleanup(shutil.rmtree, config_dir, ignore_errors=True)
with open(os.path.join(config_dir, "config.json"), "w") as handle:
json.dump(_MINI_CONFIG, handle)
return config_dir
def _resolution_written_fields(self) -> set:
"""The union of what resolution fills in across the matrix.
Every entry must resolve. A silently skipped one would shrink this set,
which makes pinned pairs look like they disappeared -- the list would
then drift by environment rather than by code, and the failure would
point at the wrong thing. Each entry resolves from the pristine
process snapshot (resolution writes env and EnvField flags on the way
through, and DWDP flips `SGLANG_SCHEDULER_SKIP_ALL_GATHER`), so the
union does not depend on matrix order; and the ambient CI marker is
cleared, so a runner's identity cannot leak into the measurement --
the CI-conditioned writes come from `_ENV_MATRIX`'s explicit entry.
Declarers outside `arg_groups/` count too: the parser auto-detection
runs at launcher stage and the NPU helper is called by the pipeline, so
their target fields are collected statically from the call sites --
resolution writes by definition, just not reached by the matrix.
"""
pristine = (dict(os.environ), self._env_field_flags())
written = set()
def resolve_one(extra, env):
self._restore_process_state(pristine)
os.environ.pop("SGLANG_IS_IN_CI", None)
os.environ.update(env)
model_path = self._config_dir()
try:
resolved = ServerArgs(
model_path=model_path, device="cuda", random_seed=42, **extra
)
resolved.resolve_once()
except Exception as exc:
self.fail(
f"the matrix entry {extra} (env={env}) did not resolve in "
f"this environment ({type(exc).__name__}: {exc}); the "
"written-field union would be short and the pinned list "
"would drift"
)
defaults = {}
for field in msgspec.structs.fields(resolved):
if field.default is not msgspec.NODEFAULT:
defaults[field.name] = field.default
elif field.default_factory is not msgspec.NODEFAULT:
defaults[field.name] = field.default_factory()
for field_name, default in defaults.items():
if field_name in _PASSED or field_name in extra:
continue
if getattr(resolved, field_name) != default:
written.add(field_name)
for extra in _MATRIX:
resolve_one(extra, {})
for extra, env in _ENV_MATRIX:
resolve_one(extra, env)
self._restore_process_state(pristine)
written |= self._declared_outside_the_pipeline()
written |= self._hook_assignment_targets()
written |= self._record_method_assignment_targets()
written |= self._declarative_override_fields()
return written
def _hook_assignment_targets(self) -> set:
"""Fields any resolution hook can write, collected statically.
The matrix can only enumerate families someone thought to add -- the
DFLASH hole (its hook is the sole writer of
`speculative_draft_attention_backend`, and no entry ran it) showed
that a family nobody listed leaves its readers unpinned. The hook
modules under `arg_groups/` are the resolution pipeline's extension
points -- along with the NPU default helper, which the pipeline calls
the same way -- and their write surface is the may-write set,
family-blind by
construction. A hook writes two ways: `server_args.field = ...`, and
`declare_resolution(server_args, source, field=...)`, which records
the write in the declaration stash on its way to the field. Counting
only the assignment would read a hook's conversion to a declaration as
the field having stopped being written. Collected like the
late-resolution keywords: statically, failing loudly on an
unparsable module. Underscore-prefixed targets are pipeline
bookkeeping, not config leaves.
"""
targets = set()
modules = sorted((_PACKAGE_ROOT / "arg_groups").glob("*.py"))
modules.append(_PACKAGE_ROOT / "hardware_backend/npu/utils.py")
for path in modules:
try:
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
except SyntaxError:
self.fail(f"unparsable hook module in the census: {path.name}")
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
tgts = node.targets
elif isinstance(node, (ast.AnnAssign, ast.AugAssign)):
tgts = [node.target]
elif (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "declare_resolution"
):
targets |= {
kw.arg
for kw in node.keywords
if kw.arg and not kw.arg.startswith("_")
}
continue
else:
continue
for tgt in tgts:
if (
isinstance(tgt, ast.Attribute)
and isinstance(tgt.value, ast.Name)
and tgt.value.id == "server_args"
and not tgt.attr.startswith("_")
):
targets.add(tgt.attr)
return targets
def _record_method_assignment_targets(self) -> set:
"""Fields ``ServerArgs``'s own methods can write, collected statically.
The record's handlers are as family-conditional as the hooks -- the
mooncake/layer_first layout rewrite, the deepseek-EP mode defaults,
the seed fill that only runs when the caller did *not* supply one (so
construct-and-diff can never see it: measuring requires supplying).
A write site that can never fire is a dead branch to delete upstream,
not a census exemption. Only names that are declared dataclass fields
count; underscore bookkeeping does not.
Two spellings write: an assignment, and ``self._declare(source,
field=value)``, which records the write in the declaration stash on
its way to the field. Counting only assignments would read a handler's
conversion to a declaration as the field having stopped being written,
which would quietly retire every pinned pair that reads it.
"""
tree = ast.parse(
(_PACKAGE_ROOT / "server_args.py").read_text(encoding="utf-8-sig")
)
sa_class = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "ServerArgs"
)
declared = {
node.target.id
for node in sa_class.body
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)
}
targets = set()
for node in ast.walk(sa_class):
if isinstance(node, ast.Assign):
tgts = node.targets
elif isinstance(node, (ast.AnnAssign, ast.AugAssign)):
tgts = [node.target]
elif (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "declare_resolution"
):
targets |= {
kw.arg
for kw in node.keywords
if kw.arg in declared and not kw.arg.startswith("_")
}
continue
else:
continue
for tgt in tgts:
if (
isinstance(tgt, ast.Attribute)
and isinstance(tgt.value, ast.Name)
and tgt.value.id == "self"
and tgt.attr in declared
and not tgt.attr.startswith("_")
):
targets.add(tgt.attr)
# The deprecated-alias normalization declares through `**renamed`, so
# the keyword scan sees no names; its field set is pinned here.
alias_fields = {
"attention_backend",
"decode_attention_backend",
"prefill_attention_backend",
"speculative_draft_attention_backend",
}
# The handler lives in `arg_groups/serving_hook.py`, reached either as a
# record method or as a bare-name call, so look the loop up by both.
def _deprecated_alias_handler():
for node in ast.walk(sa_class):
if (
isinstance(node, ast.FunctionDef)
and node.name == "_handle_deprecated_args"
and any(isinstance(n, ast.For) for n in ast.walk(node))
):
return node
for path in sorted((_PACKAGE_ROOT / "arg_groups").glob("*.py")):
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
for node in tree.body:
if (
isinstance(node, ast.FunctionDef)
and node.name == "handle_deprecated_args"
):
return node
raise AssertionError("the deprecated-alias handler was not found")
deprecated = _deprecated_alias_handler()
found_tuples = [
{elt.value for elt in node.iter.elts if isinstance(elt, ast.Constant)}
for node in ast.walk(deprecated)
if isinstance(node, ast.For) and isinstance(node.iter, ast.Tuple)
]
self.assertIn(
alias_fields,
found_tuples,
"the deprecated-alias normalization loop moved or changed its "
"field tuple; update alias_fields to match",
)
return targets | alias_fields
def _declarative_override_fields(self) -> set:
"""Fields the declarative override registry can write.
``MODEL_OVERRIDES`` maps arch -> {field: value}, and the
``@register_model_override``(-``_predicate``) providers return (or
build by subscript) {field: value} dicts, which go straight into the
declaration stash, so no assignment scan sees these writes and a
llama-only matrix never triggers them. Keys must be
string literals; anything else fails loudly.
"""
tree = ast.parse(
(_PACKAGE_ROOT / "arg_groups" / "overrides.py").read_text(
encoding="utf-8-sig"
)
)
fields = set()
for node in tree.body:
target = None
if isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name):
target = node.targets[0].id
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
target = node.target.id
if target != "MODEL_OVERRIDES" or node.value is None:
continue
for inner in ast.walk(node.value):
if not isinstance(inner, ast.Dict):
continue
for key, value in zip(inner.keys, inner.values):
if isinstance(value, ast.Dict):
continue # arch -> {…} outer layer
self.assertIsInstance(key, ast.Constant, "non-literal override key")
fields.add(key.value)
for node in ast.walk(tree):
if not isinstance(node, ast.FunctionDef):
continue
if not any(
isinstance(dec, ast.Call)
and isinstance(dec.func, ast.Name)
and dec.func.id.startswith("register_model_override")
for dec in node.decorator_list
):
continue
for inner in ast.walk(node):
if isinstance(inner, ast.Assign) and isinstance(
inner.targets[0], ast.Subscript
):
key = inner.targets[0].slice
self.assertIsInstance(
key, ast.Constant, f"non-literal override key in {node.name}"
)
fields.add(key.value)
if isinstance(inner, ast.Dict):
for key in inner.keys:
self.assertIsInstance(
key,
ast.Constant,
f"non-literal override key in {node.name}",
)
fields.add(key.value)
return fields
def _declared_outside_the_pipeline(self) -> set:
"""Fields declared by a `declare_resolution` caller outside
`arg_groups/`, collected statically.
Resolution's launcher-stage writes live here -- the auto-detected
parsers need a tokenizer or chat-template load, so they cannot run in
`__post_init__` -- alongside the NPU default helper and the expert-pack
loader, which the pipeline calls the same way. The construct-and-diff
pass above never sees any of them.
`arg_groups/` is deliberately excluded: `_hook_assignment_targets`
covers it exactly, and it resolves the pipeline's own computed
expansions (`record_foreign_defaults` declares a `**` dict this
collector's resolver cannot read). The keywords at the call sites are
the written fields; an expansion this cannot resolve fails loudly like
the override collector's, except the named dynamic sites below, whose
field sets are spelled out and drift-guarded (each name must still
appear as a constant in the file)."""
written = set()
root = _PACKAGE_ROOT
for path in sorted(root.rglob("*.py")):
rel = path.relative_to(root).as_posix()
if rel.startswith("arg_groups/"):
continue
source = path.read_text(encoding="utf-8-sig")
if "declare_resolution" not in source:
continue
try:
tree = ast.parse(source)
except SyntaxError:
self.fail(f"unparsable module in the census: {rel}")
for node in ast.walk(tree):
if not (
isinstance(node, ast.Call)
and (
(
isinstance(node.func, ast.Name)
and node.func.id == "declare_resolution"
)
or (
isinstance(node.func, ast.Attribute)
and node.func.attr
in ("declare_resolution", "_declare_resolution")
)
)
):
continue
if all(kw.arg is None for kw in node.keywords) and any(
isinstance(kw.value, ast.Name) and kw.value.id == "fields"
for kw in node.keywords
):
# The forwarding shim (`ServerArgs._late_resolution` /
# the helper's own body) re-expands its caller's kwargs;
# the write sites are the callers.
continue
for kw in node.keywords:
if kw.arg and kw.arg != "source":
written.add(kw.arg)
elif kw.arg is None:
if rel in _CALLER_SUPPLIED_LATE_SITES:
continue
dynamic = _LATE_RESOLUTION_DYNAMIC_SITES.get(rel)
if dynamic is not None:
constants = {
c.value
for c in ast.walk(tree)
if isinstance(c, ast.Constant)
}
missing = dynamic - constants
self.assertFalse(
missing,
f"{rel}: the declared dynamic field set drifted "
f"from the file ({sorted(missing)} not found)",
)
written |= dynamic
else:
written |= _expanded_override_keys(rel, tree, node, kw)
return written
_READS_CACHE = None
def _supplied_instance_reads(self) -> set:
"""Three spellings of the same read: ``server_args.field`` off the
parameter, ``getattr(server_args, "field", default)`` with a literal
name, and the *parked* form -- ``self.x = server_args`` in a method
that takes the parameter, read as ``self.x.field`` anywhere in the
class. Parking under a different object, a container, or a computed
name stays invisible, like in every census of this family. The
loudest boundary *was* the *chain* spelling,
``model_runner.server_args.field`` off some other object, which this
census still does not count -- but those reads are gone for every
resolution-written field and ``test_chain_read_ratchet.py`` holds them
at zero, so the gap is no longer where the risk is."""
if TestSuppliedInstanceExposure._READS_CACHE is not None:
return TestSuppliedInstanceExposure._READS_CACHE
pairs = set()
for path in sorted(_PACKAGE_ROOT.rglob("*.py")):
rel = path.relative_to(_PACKAGE_ROOT).as_posix()
if rel.startswith(_OWNERS):
continue
source = path.read_text(encoding="utf-8-sig")
if "server_args" not in source:
continue
try:
tree = ast.parse(source)
except SyntaxError:
# A silently dropped module shrinks `found` and reads as
# intentional surface shrinkage under the bidirectional pin.
self.fail(f"unparsable module in the census: {rel}")
for fn in ast.walk(tree):
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
params = {a.arg for a in list(fn.args.args) + list(fn.args.kwonlyargs)}
if "server_args" not in params:
continue
for node in ast.walk(fn):
if (
isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Name)
and node.value.id == "server_args"
and isinstance(node.ctx, ast.Load)
):
pairs.add((rel, node.attr))
elif (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "getattr"
and len(node.args) >= 2
and isinstance(node.args[0], ast.Name)
and node.args[0].id == "server_args"
and isinstance(node.args[1], ast.Constant)
and isinstance(node.args[1].value, str)
):
# The same read in optional clothing. Only a literal
# name is censusable; a computed one is not.
pairs.add((rel, node.args[1].value))
for cls in ast.walk(tree):
if not isinstance(cls, ast.ClassDef):
continue
# Parked: `self.x = server_args` in a method that takes the
# parameter, read as `self.x.field` anywhere in the class.
parked = set()
for fn in cls.body:
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if "server_args" not in {
a.arg for a in list(fn.args.args) + list(fn.args.kwonlyargs)
}:
continue
for node in ast.walk(fn):
if (
isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Attribute)
and isinstance(node.targets[0].value, ast.Name)
and node.targets[0].value.id == "self"
and isinstance(node.value, ast.Name)
and node.value.id == "server_args"
):
parked.add(node.targets[0].attr)
for node in ast.walk(cls):
if (
isinstance(node, ast.Attribute)
and isinstance(node.ctx, ast.Load)
and isinstance(node.value, ast.Attribute)
and node.value.attr in parked
and isinstance(node.value.value, ast.Name)
and node.value.value.id == "self"
):
pairs.add((rel, node.attr))
TestSuppliedInstanceExposure._READS_CACHE = pairs
return pairs
@staticmethod
def _override_written_fields() -> set:
"""Fields written post-publish through ``get_context().override(...)``."""
written = set()
for path in sorted(_PACKAGE_ROOT.rglob("*.py")):
rel = path.relative_to(_PACKAGE_ROOT).as_posix()
source = path.read_text(encoding="utf-8-sig")
if "record_config_updates" not in source and not (
"get_context" in source and "override" in source
):
continue
try:
tree = ast.parse(source)
except SyntaxError:
raise AssertionError(f"unparsable module in the census: {rel}")
for node in ast.walk(tree):
if not (
isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
):
continue
base = node.func.value
is_override = node.func.attr == "override" and (
isinstance(base, ast.Call)
and isinstance(base.func, ast.Name)
and base.func.id == "get_context"
)
# `record_config_updates` is a named wrapper over override, so
# its call sites are override sites. Its body forwards **kwargs
# and names no field, so skip the forwarding call itself.
is_wrapper = node.func.attr == "record_config_updates"
if not (is_override or is_wrapper):
continue
inside_wrapper = any(
isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef))
and fn.name == "record_config_updates"
and fn.lineno <= node.lineno <= (fn.end_lineno or fn.lineno)
for fn in ast.walk(tree)
)
if inside_wrapper:
continue
for kw in node.keywords:
if kw.arg == "source":
# Override metadata, not a config field.
continue
if kw.arg:
written.add(kw.arg)
else:
written |= _expanded_override_keys(rel, tree, node, kw)
return written
def test_the_post_publish_override_surface_matches_the_pinned_list(self):
written = self._override_written_fields()
self.assertGreater(
len(written), 5, "found almost no override targets; the scan broke"
)
found = {pair for pair in self._supplied_instance_reads() if pair[1] in written}
new = sorted(found - _OVERRIDDEN_AND_READ)
gone = sorted(_OVERRIDDEN_AND_READ - found)
self.assertEqual(
([], []),
(new, gone),
"the post-publish override surface drifted. A read here answers with "
"the startup value once the override lands, so a new pair needs an "
"ordering judgment: copied before any override (fine), or read after "
"one (then it must come from the bags).\n"
f" new: {new}\n"
f" gone (delete from _OVERRIDDEN_AND_READ): {gone}",
)
def test_the_exposed_set_matches_the_pinned_list(self):
import torch
written = self._resolution_written_fields()
found = {pair for pair in self._supplied_instance_reads() if pair[1] in written}
expected = set(_EXPOSED)
if torch.cuda.is_available():
expected |= _EXPOSED_CUDA_ONLY
new = sorted(found - expected)
gone = sorted(expected - found)
self.assertEqual(
([], []),
(new, gone),
"the supplied-instance step-12 surface drifted.\n"
f" new (decide where the resolved value comes from): {new}\n"
f" gone (delete from _EXPOSED / _EXPOSED_CUDA_ONLY): {gone}",
)
if __name__ == "__main__":
unittest.main()