config: business code no longer reads the published ServerArgs (#34081)
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user