config: retire ServerArgs.derive; per-runner values are constructor arguments (#33887)

This commit is contained in:
Cheng Wan
2026-08-07 22:41:09 -07:00
committed by GitHub
parent 5dffa06fe1
commit a5af27f49e
12 changed files with 132 additions and 186 deletions
+21 -21
View File
@@ -60,17 +60,16 @@ resolved configuration lives in the namespace bags.**
multi-tokenizer workers it is serialized for, the schedulers it forks. Returning a multi-tokenizer workers it is serialized for, the schedulers it forks. Returning a
variant here is a bug: the launcher rebinds its local and everyone else keeps the variant here is a bug: the launcher rebinds its local and everyone else keeps the
unresolved object. unresolved object.
- **A config another runner / worker / process is built from**: `server_args.derive( - **A value another runner / worker owns is a constructor argument, not a config
source, **fields)` returns a variant (an encode worker's `base_gpu_id`/`tp_size`). copy.** The draft worker's `context_length`, load format and attention backend
The receiver — and any bags projected from it — are untouched; resolution does travel as arguments to `TpModelWorker` / `ModelRunner` and live on the runner
**not** re-run, so do not reach for it to "re-resolve" a config. (`ModelRunner.draft_attention_backend`, `kv_cache_dtype_str`, …); the encoder
- **Per-runner values inside one process are constructor arguments, not a variant.** DP worker's device is `MMEncoder(gpu_id=...)`. There is no `ServerArgs.derive`
The draft worker's `context_length`, load format and attention backend travel as any more — a config object is never copied-and-edited; test doubles that need a
arguments to `TpModelWorker` / `ModelRunner` and live on the runner modified copy use `sglang.test.test_utils.server_args_variant`.
(`ModelRunner.draft_attention_backend`, `kv_cache_dtype_str`, …), because target
and draft coexist and the process-wide bags can only describe one of them.
**Why a bag override cannot stand in for the last two.** The bags are projected at **Why a bag override cannot stand in for late resolution or per-runner
construction.** The bags are projected at
publish *from the instance's fields*, so anything the runtime must read has to be on publish *from the instance's fields*, so anything the runtime must read has to be on
the instance before publish — an override afterwards puts instance and bags back out the instance before publish — an override afterwards puts instance and bags back out
of agreement, and whole-object readers (`ModelConfig.from_server_args`, of agreement, and whole-object readers (`ModelConfig.from_server_args`,
@@ -98,10 +97,11 @@ bag to override at all.
- **Per-instance boundaries** — the tokenizer-manager family, everything under - **Per-instance boundaries** — the tokenizer-manager family, everything under
`entrypoints/`, and the tokenizer-process multimodal processors read `entrypoints/`, and the tokenizer-process multimodal processors read
`self.server_args`: several `Engine`s can share one process, and the process-global `self.server_args`: several `Engine`s can share one process, and the process-global
bags are last-publish-wins across engines. `base_gpu_id` also differs per worker bags are last-publish-wins across engines. `base_gpu_id` also differs per engine,
(the encode-server DP workers each specialize their own copy), so no process-global so no process-global value can stand in for it —
value can stand in for it — `BaseMultimodalProcessor._fast_image_processor_device` `BaseMultimodalProcessor._fast_image_processor_device` is the shape to copy. (The
is the shape to copy. encode-server DP workers used to specialize a config copy for the same reason;
their device now travels as `MMEncoder(gpu_id=...)`.)
- **Whole-object passes** (`f(server_args)` handing the instance along) keep the - **Whole-object passes** (`f(server_args)` handing the instance along) keep the
supplied-instance contract; don't rewrite the parameter reads to bag reads unless the supplied-instance contract; don't rewrite the parameter reads to bag reads unless the
field is runtime-mutated (see the elastic-EP `ep_size` case in field is runtime-mutated (see the elastic-EP `ep_size` case in
@@ -240,16 +240,16 @@ ONE thread — do not design for TBO threads that don't exist.
1. **Strict mutation guard** (always on): bare `server_args.x = ...` after resolution 1. **Strict mutation guard** (always on): bare `server_args.x = ...` after resolution
raises unconditionally in `ServerArgs.__setattr__` — this *is* the guarantee that raises unconditionally in `ServerArgs.__setattr__` — this *is* the guarantee that
no writer can desync the bags, so there is no writer ratchet any more. Change no writer can desync the bags, so there is no writer ratchet any more. Change
resolved config with `get_context().override`, build a per-runner config with resolved config with `get_context().override`; hand a per-runner value to its
`server_args.derive`. Projected bags are sealed the same way (leaf assignment runner as a constructor argument. Projected bags are sealed the same way (leaf
raises). assignment raises).
2. **Mutation ratchet** (`test_server_args_mutation_ratchet.py`, exact pin 0 over the whole 2. **Mutation ratchet** (`test_server_args_mutation_ratchet.py`, exact pin 0 over the whole
package minus the pipeline / multimodal_gen): textual scan for assignment forms. Never package minus the pipeline / multimodal_gen): textual scan for assignment forms. Never
raise the baseline. raise the baseline.
3. **Derive contract** (`test_server_args_derive.py`): deriving leaves the receiver 3. **No-copy contract** (`test_server_args_no_instance_mutation_entry.py`): neither
intact, a published config still refuses assignment, and deriving does not publish. `ServerArgs.override` nor `ServerArgs.derive` exists, and nothing in the package
Rerouting a writer to the bags means flipping **all its readers in the same commit** calls either form. Rerouting a writer to the bags means flipping **all its readers
(no transitional dual-write). in the same commit** (no transitional dual-write).
4. **Legacy-accessor ratchet** (`test_legacy_global_ratchet.py`): `get_global_server_args` 4. **Legacy-accessor ratchet** (`test_legacy_global_ratchet.py`): `get_global_server_args`
call sites must not grow — new code uses `runtime_context.get_server_args()` (and call sites must not grow — new code uses `runtime_context.get_server_args()` (and
business decisions should read the bags). business decisions should read the bags).
@@ -288,7 +288,12 @@ class MMEncoder:
schedule_path=None, schedule_path=None,
dist_init_method=None, dist_init_method=None,
rank: int = 0, rank: int = 0,
gpu_id: Optional[int] = None,
): ):
"""``gpu_id`` pins this encoder to a device other than
``base_gpu_id + rank`` — the DP launcher's per-worker placement. It is
this instance's value, not a config change, so it travels as an
argument."""
logger.info(f"init MMEncoder {rank}/{server_args.tp_size}") logger.info(f"init MMEncoder {rank}/{server_args.tp_size}")
self.server_args = server_args self.server_args = server_args
publish(server_args, role="encoder") publish(server_args, role="encoder")
@@ -315,7 +320,7 @@ class MMEncoder:
).lower() ).lower()
self.device = server_args.device self.device = server_args.device
self.gpu_id = server_args.base_gpu_id + rank self.gpu_id = server_args.base_gpu_id + rank if gpu_id is None else gpu_id
self.device_config = DeviceConfig( self.device_config = DeviceConfig(
device=self.device, device=self.device,
@@ -3517,10 +3522,13 @@ async def run_dp_worker(
) )
# gpu_id is the device chosen by maybe_reindex_device_id in the parent: # gpu_id is the device chosen by maybe_reindex_device_id in the parent:
# 0 when CVD is pinned to one GPU, else the absolute id. rank=0, so # 0 when CVD is pinned to one GPU, else the absolute id.
# MMEncoder runs set_device(base_gpu_id). enc = MMEncoder(
args = server_args.derive("encode_server.dp_worker", base_gpu_id=gpu_id, tp_size=1) server_args,
enc = MMEncoder(args, dist_init_method=f"tcp://127.0.0.1:{get_free_port()}", rank=0) dist_init_method=f"tcp://127.0.0.1:{get_free_port()}",
rank=0,
gpu_id=gpu_id,
)
global encoder_metrics_collector global encoder_metrics_collector
if server_args.enable_metrics: if server_args.enable_metrics:
+12 -2
View File
@@ -1027,9 +1027,19 @@ class _ServerArgsOverride:
self._prev_publish_role = ctx._publish_role self._prev_publish_role = ctx._publish_role
self._prev_parallel_config = ctx.parallel._config self._prev_parallel_config = ctx.parallel._config
self._prev_capture = ctx.flags.capture.enable_torch_compile self._prev_capture = ctx.flags.capture.enable_torch_compile
from sglang.srt.arg_groups.overrides import _apply_fields
server_args = ServerArgs(model_path="dummy") server_args = ServerArgs(model_path="dummy")
if self._fields: # Underscore names seed private property caches (the strict guard
server_args = server_args.derive("test-override", **self._fields) # exempts them); everything else must be a real config field.
unknown = {name for name in self._fields if not name.startswith("_")} - set(
type(server_args).__dataclass_fields__
)
if unknown:
raise ValueError(
f"override_server_args: unknown ServerArgs field(s): {sorted(unknown)}"
)
_apply_fields(server_args, self._fields)
# The dummy boundary skips materialization, which would leave the # The dummy boundary skips materialization, which would leave the
# strict mutation guard unarmed on the published object — mark it # strict mutation guard unarmed on the published object — mark it
# materialized so bare post-publish writes raise like they do on a # materialized so bare post-publish writes raise like they do on a
+4 -45
View File
@@ -16,7 +16,6 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import copy
import dataclasses import dataclasses
import glob import glob
import importlib import importlib
@@ -8609,52 +8608,12 @@ class ServerArgs:
declare_late_resolution(self, source, **fields) declare_late_resolution(self, source, **fields)
def derive(self, source: str, **fields) -> ServerArgs:
"""A copy carrying variant values: a draft worker's context length, an
encode worker's device, a launcher's late port pick.
The receiver is untouched, so a config already published from it -- and
the namespace bags projected out of it -- stay true; the variant is a
second config, to be published in its own right or handed to whoever
owns it. Resolution does not re-run: the values being set are decided
*after* it, from inputs resolution never had, and re-resolving a
resolved config re-derives conditional decisions from the wrong ones.
Whitelisted resolvable fields also join the copy's declaration stash so
a later re-resolution keeps them; ``source`` is recorded for provenance.
"""
from sglang.srt.arg_groups.arg_utils import resolvable_fields
variant = copy.deepcopy(self)
whitelist = resolvable_fields(type(variant))
declared = {k: v for k, v in fields.items() if k in whitelist}
rest = {k: v for k, v in fields.items() if k not in whitelist}
if declared:
stash = getattr(variant, "_resolved_overrides", None)
if stash is None:
stash = []
object.__setattr__(variant, "_resolved_overrides", stash)
stash.append((source, dict(declared)))
if rest:
log = getattr(variant, "_runtime_mutations", None)
if log is None:
log = []
object.__setattr__(variant, "_runtime_mutations", log)
log.append((source, dict(rest)))
object.__setattr__(variant, "_internal_write", True)
try:
for field, value in fields.items():
setattr(variant, field, value)
finally:
object.__setattr__(variant, "_internal_write", False)
return variant
def __setattr__(self, name, value): def __setattr__(self, name, value):
# After materialization the fields are the resolved startup # After materialization the fields are the resolved startup
# configuration -- the pristine, READ-ONLY record that the config bags # configuration -- the pristine, READ-ONLY record that the config bags
# were projected from. Resolved config changes go to the bags via # were projected from. Resolved config changes go to the bags via
# get_context().override(source, ...); a config that differs per runner # get_context().override(source, ...); a value one runner or worker
# or per worker is a separate object, built with derive(). # owns travels as a constructor argument to it.
if ( if (
not name.startswith("_") not name.startswith("_")
and getattr(self, "_declarations_materialized", False) and getattr(self, "_declarations_materialized", False)
@@ -8663,8 +8622,8 @@ class ServerArgs:
raise AttributeError( raise AttributeError(
f"server_args.{name} assigned after resolution; server_args is " f"server_args.{name} assigned after resolution; server_args is "
"read-only -- use get_context().override(source, ...) to change " "read-only -- use get_context().override(source, ...) to change "
"resolved config, or server_args.derive(source, ...) to build a " "resolved config; a value one runner owns travels as a "
"variant for one runner." "constructor argument."
) )
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
@@ -326,10 +326,9 @@ def _configure_runner_for_eagle_draft(
"use_mla_backend": runner.use_mla_backend, "use_mla_backend": runner.use_mla_backend,
} }
from sglang.srt.runtime_context import get_context from sglang.srt.runtime_context import get_context
from sglang.test.test_utils import server_args_variant
runner.server_args = runner.server_args.derive( runner.server_args = server_args_variant(runner.server_args, **updates)
"attention-unittest-eagle-draft", **updates
)
get_context().set_server_args(runner.server_args) get_context().set_server_args(runner.server_args)
runner.spec_algorithm = SpeculativeAlgorithm.EAGLE runner.spec_algorithm = SpeculativeAlgorithm.EAGLE
@@ -392,9 +391,10 @@ def _build_frozen_kv_mtp_fixture(
) )
_configure_runner_for_eagle_draft(fixture.runner, case, settings) _configure_runner_for_eagle_draft(fixture.runner, case, settings)
from sglang.srt.runtime_context import get_context from sglang.srt.runtime_context import get_context
from sglang.test.test_utils import server_args_variant
fixture.runner.server_args = fixture.runner.server_args.derive( fixture.runner.server_args = server_args_variant(
"attention_unittest.frozen_kv_draft", speculative_algorithm="FROZEN_KV_MTP" fixture.runner.server_args, speculative_algorithm="FROZEN_KV_MTP"
) )
get_context().set_server_args(fixture.runner.server_args) get_context().set_server_args(fixture.runner.server_args)
fixture.runner.spec_algorithm = SpeculativeAlgorithm.FROZEN_KV_MTP fixture.runner.spec_algorithm = SpeculativeAlgorithm.FROZEN_KV_MTP
+22
View File
@@ -2345,6 +2345,28 @@ def _wait_for_gpu_idle_in_ci(
pass pass
def server_args_variant(server_args, **fields):
"""A modified deep copy of a config, for a test double whose fixture
differs from the (possibly published, read-only) config it starts from.
The receiver is untouched; the copy keeps its read-only guard.
A name may also shadow a method with a fixture value (the runner kits set
``use_mla_backend``, a method ModelRunner itself overwrites at init);
names that exist nowhere on the class fail loudly."""
variant = copy.deepcopy(server_args)
cls = type(variant)
unknown = {
name
for name in fields
if name not in cls.__dataclass_fields__ and not hasattr(cls, name)
}
if unknown:
raise ValueError(f"unknown ServerArgs field(s): {sorted(unknown)}")
for name, value in fields.items():
object.__setattr__(variant, name, value)
return variant
class CustomTestCase(unittest.TestCase): class CustomTestCase(unittest.TestCase):
def __init_subclass__(cls, **kwargs): def __init_subclass__(cls, **kwargs):
@@ -46,9 +46,10 @@ class _ChunkKVMLARunner(MockMLAModelRunner):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
from sglang.srt.runtime_context import get_context from sglang.srt.runtime_context import get_context
from sglang.test.test_utils import server_args_variant
self.server_args = self.server_args.derive( self.server_args = server_args_variant(
"attention-unittest", self.server_args,
disable_chunked_prefix_cache=False, disable_chunked_prefix_cache=False,
flashinfer_mla_disable_ragged=False, flashinfer_mla_disable_ragged=False,
) )
+4 -6
View File
@@ -261,9 +261,9 @@ class TestServerArgsScopedOverride(_IsolatedServerArgs):
# unnamed fields keep their dataclass defaults # unnamed fields keep their dataclass defaults
self.assertEqual(published.tp_size, 1) self.assertEqual(published.tp_size, 1)
def test_fields_carry_provenance(self): def test_unknown_fields_are_rejected(self):
published = get_context().override_server_args(tp_size=4).install() with self.assertRaises(ValueError):
self.assertIn(("test-override", {"tp_size": 4}), published._runtime_mutations) get_context().override_server_args(not_a_config_field=1).install()
def test_restore_reinstates_previous_publish(self): def test_restore_reinstates_previous_publish(self):
previous = object() previous = object()
@@ -299,12 +299,10 @@ class TestServerArgsScopedOverride(_IsolatedServerArgs):
def test_installed_config_arms_the_strict_guard(self): def test_installed_config_arms_the_strict_guard(self):
# The published dummy must behave like a resolved config: bare writes # The published dummy must behave like a resolved config: bare writes
# raise, and a differing value lives on a derived variant. # raise.
published = get_context().override_server_args(tp_size=2).install() published = get_context().override_server_args(tp_size=2).install()
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
published.tp_size = 4 published.tp_size = 4
variant = published.derive("test", tp_size=4)
self.assertEqual(variant.tp_size, 4)
self.assertEqual(published.tp_size, 2) self.assertEqual(published.tp_size, 2)
def test_restore_resets_the_capture_seed(self): def test_restore_resets_the_capture_seed(self):
@@ -118,9 +118,7 @@ class TestContextOverride(CustomTestCase):
rc.get_context().override( rc.get_context().override(
"ModelRunner.configure_kv_cache_dtype", kv_cache_dtype="fp8_e4m3" "ModelRunner.configure_kv_cache_dtype", kv_cache_dtype="fp8_e4m3"
) )
draft = ServerArgs(model_path="dummy").derive( draft = ServerArgs(model_path="dummy", kv_cache_dtype="bf16")
"draft-build", kv_cache_dtype="bf16"
)
with rc.get_context().preserve_config(): with rc.get_context().preserve_config():
rc.get_context().set_server_args(draft) rc.get_context().set_server_args(draft)
# Inside the scope the draft's bags are live... # Inside the scope the draft's bags are live...
@@ -1,79 +0,0 @@
"""``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()
@@ -10,9 +10,9 @@ baseline to lock in the progress.
There is no post-resolution mutation entry point on the instance any more: There is no post-resolution mutation entry point on the instance any more:
resolved config changes go to the context bags via resolved config changes go to the context bags via
``get_context().override(source, **fields)``, and a config that differs for one ``get_context().override(source, **fields)``, and a value that differs for one
runner or worker is a separate object built with ``ServerArgs.derive(source, runner or worker travels as a constructor argument to it. The baseline is
**fields)``. The baseline is therefore zero. ``ServerArgs.__setattr__`` raises therefore zero. ``ServerArgs.__setattr__`` raises
on a bare assignment after resolution; this ratchet catches the sites the tests on a bare assignment after resolution; this ratchet catches the sites the tests
never execute. never execute.
""" """
@@ -70,8 +70,8 @@ class TestServerArgsMutationRatchet(CustomTestCase):
f"{count} > baseline {_BASELINE}. Configuration is resolved in " f"{count} > baseline {_BASELINE}. Configuration is resolved in "
"ServerArgs.__post_init__; declare through the pipeline " "ServerArgs.__post_init__; declare through the pipeline "
"(passes / declare_load_time_override), change resolved config " "(passes / declare_load_time_override), change resolved config "
"with get_context().override(source, ...), or build a variant " "with get_context().override(source, ...), or hand the value "
"with server_args.derive(source, ...) — do not assign fields." "to its runner as a constructor argument — do not assign fields."
) )
if count < _BASELINE: if count < _BASELINE:
self.fail( self.fail(
@@ -1,13 +1,14 @@
"""``ServerArgs`` has no in-place mutation entry, and nothing calls one. """``ServerArgs`` has no in-place mutation entry, and nothing calls one.
``ServerArgs.override(source, **fields)`` used to mutate a resolved instance; ``ServerArgs.override(source, **fields)`` used to mutate a resolved instance,
after resolution the fields are the record the config bags were projected from, and ``ServerArgs.derive(source, **fields)`` used to copy-and-edit one; after
so such a write desyncs every namespace reader. The method is gone and the two resolution the fields are the record the config bags were projected from, so a
sanctioned replacements are ``get_context().override`` (post-publish, writes the write desyncs every namespace reader, and a copy invites publishing stale
bags) and ``ServerArgs.derive`` (a variant for another runner / process). Late variants. Both are gone: post-publish changes go to the bags
launcher-stage resolution writes in place through (``get_context().override``), a value one runner or worker owns travels as a
``arg_groups.overrides.declare_late_resolution``, which refuses the published constructor argument, and late launcher-stage resolution writes in place
instance. through ``arg_groups.overrides.declare_late_resolution``, which refuses the
published instance.
The textual half of this guard matters because the resolution pipeline's own file The textual half of this guard matters because the resolution pipeline's own file
is exempt from the mutation ratchet: a ``self.override(...)`` there — exactly is exempt from the mutation ratchet: a ``self.override(...)`` there — exactly
@@ -41,11 +42,19 @@ _EXCLUDED = ("multimodal_gen",)
class TestNoServerArgsMutationEntry(CustomTestCase): class TestNoServerArgsMutationEntry(CustomTestCase):
def test_the_method_is_gone(self): def test_the_methods_are_gone(self):
self.assertFalse( self.assertFalse(
hasattr(ServerArgs, "override"), hasattr(ServerArgs, "override"),
"ServerArgs.override is back; post-publish changes belong on the bags " "ServerArgs.override is back; post-publish changes belong on the bags "
"(get_context().override) and per-runner values on a derive() variant.", "(get_context().override); a value one runner owns travels as a "
"constructor argument.",
)
self.assertFalse(
hasattr(ServerArgs, "derive"),
"ServerArgs.derive is back; a value one runner or worker owns travels "
"as a constructor argument (draft_attention_backend, MMEncoder "
"gpu_id), and test doubles copy via "
"sglang.test.test_utils.server_args_variant.",
) )
def test_nothing_calls_an_instance_override(self): def test_nothing_calls_an_instance_override(self):
@@ -63,11 +72,31 @@ class TestNoServerArgsMutationEntry(CustomTestCase):
self.fail( self.fail(
"in-place ServerArgs mutation call-sites:\n" "in-place ServerArgs mutation call-sites:\n"
+ "\n".join(offenders) + "\n".join(offenders)
+ "\n\nUse get_context().override(source, ...) for resolved config, " + "\n\nUse get_context().override(source, ...) for resolved config "
"server_args.derive(source, ...) for a per-runner variant, or " "or declare_late_resolution(...) for pre-publish launcher "
"declare_late_resolution(...) for pre-publish launcher resolution." "resolution."
) )
def test_nothing_derives(self):
"""A config is never copied-and-edited in the package: a value one
runner consumes travels as a constructor argument, and test doubles
copy via ``server_args_variant`` (test_utils)."""
derive_pattern = re.compile(r"\.derive\(")
offenders = []
for path in sorted(_SGLANG_ROOT.rglob("*.py")):
rel = path.relative_to(_SGLANG_ROOT).as_posix()
if rel.startswith(_EXCLUDED):
continue
source = path.read_text()
for match in derive_pattern.finditer(source):
line = source.count("\n", 0, match.start()) + 1
offenders.append(f"{rel}:{line}")
self.assertFalse(
offenders,
".derive( call-sites in the package (the method no longer exists):\n"
+ "\n".join(offenders),
)
def test_late_resolution_refuses_the_published_config(self): def test_late_resolution_refuses_the_published_config(self):
from sglang.srt.arg_groups.overrides import declare_late_resolution from sglang.srt.arg_groups.overrides import declare_late_resolution
from sglang.srt.runtime_context import get_context from sglang.srt.runtime_context import get_context