config: business code no longer reads the published ServerArgs (#34081)

This commit is contained in:
Cheng Wan
2026-08-09 14:44:08 -07:00
committed by GitHub
parent 110bf7e6a8
commit 63833f8034
35 changed files with 1101 additions and 213 deletions
@@ -5,7 +5,10 @@ from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
import dataclasses
import json
import os
import shutil
import tempfile
import unittest
from unittest.mock import patch
@@ -20,8 +23,10 @@ from sglang.srt.runtime_context import (
get_flags,
get_parallel,
get_server_args,
max_speculative_num_draft_tokens,
reset_context,
)
from sglang.srt.server_args import ServerArgs
from sglang.test.test_utils import CustomTestCase
_PS = "sglang.srt.distributed.parallel_state"
@@ -378,6 +383,14 @@ class _FakeResolvedArgs:
sampling_backend: A[
str | None, Arg(help="s", resolvable=True), NS("exec.kernel")
] = None
attention_backend: A[str | None, Arg(help="ab"), NS("exec.kernel")] = None
prefill_attention_backend: A[str | None, Arg(help="pab"), NS("exec.kernel")] = None
decode_attention_backend: A[str | None, Arg(help="dab"), NS("exec.kernel")] = None
disable_radix_cache: A[bool, Arg(help="drc"), NS("memory")] = False
mamba_radix_cache_strategy: A[str, Arg(help="mrcs"), NS("exec.mamba")] = "auto"
speculative_num_draft_tokens: A[int | None, Arg(help="d"), NS("spec")] = None
speculative_adaptive: A[bool, Arg(help="a"), NS("spec")] = False
speculative_adaptive_config: A[str | None, Arg(help="c"), NS("spec")] = None
_resolved_overrides: list = dataclasses.field(default_factory=list)
@@ -965,5 +978,180 @@ class TestPublishLifecycle(_IsolatedServerArgs):
self.assertFalse(get_flags().capture.enable_torch_compile)
class TestDerivedPredicatesAgreeAcrossTiers(_IsolatedServerArgs):
"""One definition per predicate, checked rather than asserted in prose.
Each of these exists twice by construction -- once over a config-shaped
object (the resolution pipeline's `*_of` helper, which `ServerArgs`
delegates to) and once over the published bags. The pair must agree on
every input, or a decision made before publish differs from the same
decision made after it.
"""
_STRATEGIES = ("auto", "no_buffer", "extra_buffer", "extra_buffer_lazy")
def test_mamba_extra_buffer_matches_the_member(self):
from sglang.srt.runtime_context import (
mamba_extra_buffer_enabled,
mamba_extra_buffer_lazy_enabled,
)
for disable_radix_cache in (False, True):
for strategy in self._STRATEGIES:
with self.subTest(radix=disable_radix_cache, strategy=strategy):
args = _FakeResolvedArgs(
disable_radix_cache=disable_radix_cache,
mamba_radix_cache_strategy=strategy,
)
get_context().set_server_args(args)
self.assertEqual(
ServerArgs.enable_mamba_extra_buffer(args),
mamba_extra_buffer_enabled(),
)
self.assertEqual(
ServerArgs.enable_mamba_extra_buffer_lazy(args),
mamba_extra_buffer_lazy_enabled(),
)
def test_attention_backends_match_the_member(self):
from sglang.srt.runtime_context import attention_backends
backends = (None, "fa3", "triton")
for base in backends:
for prefill in backends:
for decode in backends:
with self.subTest(base=base, prefill=prefill, decode=decode):
args = _FakeResolvedArgs(
attention_backend=base,
prefill_attention_backend=prefill,
decode_attention_backend=decode,
)
get_context().set_server_args(args)
self.assertEqual(
ServerArgs.get_attention_backends(args),
attention_backends(),
)
class TestAdaptiveDraftBoundLifecycle(_IsolatedServerArgs):
"""The adaptive draft-token bound is memoized on the config path, so the
memo has to end with the publication it was computed under.
Without that, a process that republishes with the same adaptive-config path
-- the file having been rewritten in between -- keeps the previous bound and
under-allocates the draft-token buffers sized from it.
"""
def _write_config(self, steps):
path = os.path.join(tempfile.mkdtemp(prefix="adaptive_cfg_"), "adaptive.json")
self.addCleanup(shutil.rmtree, os.path.dirname(path), ignore_errors=True)
with open(path, "w") as handle:
json.dump({"1": {"candidate_steps": steps}}, handle)
return path
def test_republishing_recomputes_the_bound(self):
path = self._write_config([2])
get_context().set_server_args(
_FakeResolvedArgs(
speculative_num_draft_tokens=3,
speculative_adaptive=True,
speculative_adaptive_config=path,
)
)
self.assertEqual(max_speculative_num_draft_tokens(), 3)
with open(path, "w") as handle:
json.dump({"1": {"candidate_steps": [4]}}, handle)
# Same path, new contents: the memo must not survive the republish.
get_context().set_server_args(
_FakeResolvedArgs(
speculative_num_draft_tokens=3,
speculative_adaptive=True,
speculative_adaptive_config=path,
)
)
self.assertEqual(max_speculative_num_draft_tokens(), 5)
def test_reset_clears_the_bound(self):
path = self._write_config([2])
get_context().set_server_args(
_FakeResolvedArgs(
speculative_num_draft_tokens=3,
speculative_adaptive=True,
speculative_adaptive_config=path,
)
)
self.assertEqual(max_speculative_num_draft_tokens(), 3)
reset_context()
with open(path, "w") as handle:
json.dump({"1": {"candidate_steps": [6]}}, handle)
get_context().set_server_args(
_FakeResolvedArgs(
speculative_num_draft_tokens=3,
speculative_adaptive=True,
speculative_adaptive_config=path,
)
)
self.assertEqual(max_speculative_num_draft_tokens(), 7)
class TestNamedAccessorsCallWhatTheyWrap(CustomTestCase):
"""A named accessor must *call* a member that is a method.
`return get_server_args().x` hands back a bound method when `x` is defined
with `def`; the failure then lands far away, in whatever arithmetic the
caller does with it. Checked statically so accessors that need a real model
config are covered too.
"""
def test_accessors_that_wrap_methods_call_them(self):
import ast
import functools
import inspect
import sglang.srt.runtime_context as rc
from sglang.srt.server_args import ServerArgs
tree = ast.parse(inspect.getsource(rc))
wrong = []
for node in tree.body:
if not isinstance(node, ast.FunctionDef):
continue
for inner in ast.walk(node):
if not (isinstance(inner, ast.Return) and inner.value is not None):
continue
value = inner.value
called = isinstance(value, ast.Call)
target = value.func if called else value
if not (
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Call)
and isinstance(target.value.func, ast.Name)
and target.value.func.id == "get_server_args"
):
continue
member = getattr(ServerArgs, target.attr, None)
# A `property` / `functools.cached_property` member is already
# evaluated by the attribute access, so it is named here to keep
# the failure message from calling it "not a method" -- the fix
# for those is the opposite one.
kind = (
"a property"
if isinstance(member, (property, functools.cached_property))
else "not a method"
)
if inspect.isfunction(member) and not called:
wrong.append(
f"{node.name}(): returns ServerArgs.{target.attr} without "
"calling it, so callers get a bound method"
)
if not inspect.isfunction(member) and called:
wrong.append(
f"{node.name}(): calls ServerArgs.{target.attr}, which is "
f"{kind} -- the attribute access already produced the value"
)
self.assertEqual([], wrong, "\n".join(wrong))
if __name__ == "__main__":
unittest.main()