Files
sglang/test/registered/unit/test_server_args_derive.py
T
Cheng Wan 99cfc90658 config: retire ServerArgs.override in favour of derive()
`ServerArgs.override(source, **fields)` was the last way to change a resolved
`ServerArgs` in place. Every remaining call-site was one of two things, and
neither wanted an in-place write:

- **A config for someone else.** A draft worker's context length, an encode
  worker's device, the compile script's watchdog, the client's port pick, a test
  fixture's backends. These already deepcopied first — the write was on the copy.
- **A launcher-stage resolution.** `resolve_auto_parsers` detected the chat
  template's parsers and wrote them back, to be inherited by the schedulers it
  spawns.

Both are "one config becomes another", so `derive(source, **fields)` returns the
variant and leaves the receiver — and any bags projected from it — untouched. It
deliberately is not `dataclasses.replace`: resolution does not re-run, because
the values being set are decided after it, from inputs it never had. Provenance
and the resolvable-field stash work as before, on the copy.

`resolve_auto_parsers` now computes the parsers and returns the config to launch
with; the detection helpers stop taking a config to mutate. `HiMambaRadixCache`
re-applied a HiCache layout normalization `__post_init__` already performs (the
same duplicate removed from `UnifiedRadixCache` in ebb1c88d23) and just goes.

With no in-place mutation left, `ServerArgs.__setattr__` raising after
resolution *is* the guarantee, so the textual writer ratchet retires and
`test_server_args_derive.py` pins the contract instead: the receiver survives
deriving, the published instance still refuses assignment, and deriving does not
publish. `SGLANG_STRICT_CONFIG_MUTATION` was already unused — the guard has been
unconditional since the mutation sweep — and goes with it.

The detection tests drop their `SimpleNamespace` stand-in for a real
`ServerArgs`; the test kit and the MLA chunk-metadata fixture publish a derived
variant instead of writing the runner's published config.
2026-08-05 19:30:53 -07:00

80 lines
3.0 KiB
Python

"""``ServerArgs.derive`` is the only way one config becomes another.
After resolution the instance is the process's read-only startup record and the
object the config bags were projected from, so it cannot be mutated: a change to
resolved config goes to the bags (``get_context().override``), and a config that
differs for one runner or worker — a draft's context length, an encode worker's
device — is a second object.
"""
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
import unittest
from sglang.srt.runtime_context import get_context, reset_context
from sglang.srt.server_args import ServerArgs
from sglang.test.test_utils import CustomTestCase
class TestServerArgsDerive(CustomTestCase):
def tearDown(self):
reset_context()
def _resolved(self) -> ServerArgs:
"""A config in the post-resolution state, without a real model."""
server_args = ServerArgs(model_path="dummy")
object.__setattr__(server_args, "_declarations_materialized", True)
return server_args
def test_the_receiver_is_untouched(self):
server_args = self._resolved()
variant = server_args.derive("draft_worker.copy", context_length=1024)
self.assertEqual(variant.context_length, 1024)
self.assertIsNot(variant, server_args)
self.assertIsNone(server_args.context_length)
def test_a_published_config_rejects_assignment_but_still_derives(self):
override = get_context().override_server_args(tp_size=2)
published = override.install()
self.addCleanup(override.restore)
with self.assertRaises(AttributeError):
published.tp_size = 4
self.assertEqual(published.derive("worker", tp_size=4).tp_size, 4)
self.assertEqual(published.tp_size, 2)
def test_the_variant_is_not_published_by_deriving(self):
override = get_context().override_server_args(tp_size=2)
published = override.install()
self.addCleanup(override.restore)
published.derive("worker", tp_size=4)
self.assertIs(get_context().server_args, published)
def test_the_variant_is_frozen_too(self):
variant = self._resolved().derive("worker", context_length=1024)
with self.assertRaises(AttributeError):
variant.context_length = 2048
def test_provenance_is_recorded(self):
variant = self._resolved().derive("draft_worker.copy", watchdog_timeout=1.0)
self.assertIn(
("draft_worker.copy", {"watchdog_timeout": 1.0}),
getattr(variant, "_runtime_mutations", []),
)
def test_a_resolvable_field_joins_the_declaration_stash(self):
"""So a re-resolution of the variant keeps the derived value."""
variant = self._resolved().derive("draft_worker.build", kv_cache_dtype="bf16")
stash = getattr(variant, "_resolved_overrides", [])
self.assertIn(("draft_worker.build", {"kv_cache_dtype": "bf16"}), stash)
if __name__ == "__main__":
unittest.main()