[Config] msgspec.Struct for the config tier (#38753)

This commit is contained in:
Cheng Wan
2026-09-09 19:41:19 -07:00
committed by GitHub
parent 106cc561f9
commit f1a512c51c
44 changed files with 328 additions and 210 deletions
@@ -17,7 +17,7 @@ register_cpu_ci(est_time=10, suite="base-b-test-cpu-arm64")
class TestServerArgsCPUBackend(CustomTestCase):
def _make_server_args(self, attention_backend=None):
server_args = ServerArgs.__new__(ServerArgs)
server_args = ServerArgs(model_path="dummy")
server_args.device = "cpu"
server_args.attention_backend = attention_backend
server_args.sampling_backend = None
@@ -21,6 +21,7 @@ import torch
from transformers import AutoConfig, AutoTokenizer
from sglang.srt.entrypoints.engine import Engine
from sglang.srt.server_args import _declared_default
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
@@ -56,9 +57,8 @@ class TestMISServerArgsValidation(unittest.TestCase):
def test_enable_mis_default(self):
"""Test that enable_mis defaults to False."""
from sglang.srt.server_args import ServerArgs
self.assertEqual(ServerArgs.enable_mis, False)
self.assertEqual(_declared_default("enable_mis"), False)
class TestMultiItemScoringOptimization(CustomTestCase):
@@ -21,11 +21,13 @@ Current coverage:
"""
import asyncio
import dataclasses
import json
import unittest
from types import SimpleNamespace
import msgspec
import msgspec.structs
from sglang.srt.arg_groups.validation_hook import check_load_publish_args
from sglang.srt.entrypoints import http_server
from sglang.srt.lora.lora_registry import LoRARef
@@ -457,7 +459,7 @@ class TestServerInfoExistingFieldsPreserved(CustomTestCase):
info = _call_server_info_with(args)
for field in dataclasses.fields(ServerArgs):
for field in msgspec.structs.fields(ServerArgs):
self.assertIn(
field.name,
info,
@@ -10,6 +10,9 @@ import re
import unittest
from pathlib import Path
import msgspec
import msgspec.structs
import sglang
from sglang.srt.managers.tokenizer_manager import TokenizerManager
from sglang.srt.runtime_context import get_context, publish, reset_context
@@ -85,14 +88,15 @@ class TestTokenizerConfigUpdates(CustomTestCase):
)
def test_the_dump_snapshot_identifies_the_running_checkpoint(self):
import dataclasses
manager = _manager(self, load_format="auto")
manager.model_path = "at-startup"
manager.served_model_name = "at-startup"
manager._update_model_path_info("after-reload", "dummy")
snapshot = manager.resolved_config_dict(dataclasses.asdict(manager.server_args))
snapshot = manager.resolved_config_dict(
msgspec.structs.asdict(manager.server_args)
)
self.assertEqual(snapshot["model_path"], "after-reload")
self.assertEqual(snapshot["served_model_name"], "after-reload")
self.assertEqual(snapshot["load_format"], "dummy")
@@ -116,7 +120,6 @@ class TestTokenizerConfigUpdates(CustomTestCase):
self.assertIsNone(manager._dump_config_snapshot())
def test_an_unpickleable_field_does_not_lose_the_dump(self):
import dataclasses
import pickle
# What --custom-sigquit-handler leaves on a real ServerArgs.
@@ -128,7 +131,7 @@ class TestTokenizerConfigUpdates(CustomTestCase):
"server_args": manager.server_args,
"config_updates": get_context().overrides_log(),
"resolved_config": manager.resolved_config_dict(
dataclasses.asdict(manager.server_args)
msgspec.structs.asdict(manager.server_args)
),
"requests": [],
}
@@ -4,6 +4,8 @@ import unittest
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
import msgspec
from sglang.srt.parser.template_detection import (
REASONING_PARSER_RULES,
TOOL_CALL_PARSER_RULES,
@@ -892,7 +894,9 @@ class TestResolveAutoParsers(unittest.TestCase):
reasoning_parser="auto",
tool_call_parser="auto",
)
object.__setattr__(args, "model_path", "nonexistent/model-does-not-exist-xyz")
msgspec.Struct.__setattr__(
args, "model_path", "nonexistent/model-does-not-exist-xyz"
)
with _patch_hf_transformers_utils(
Mock(side_effect=RuntimeError("tokenizer unavailable")),
Mock(side_effect=RuntimeError("config unavailable")),
@@ -31,6 +31,8 @@ the unified pool today.
import unittest
from types import SimpleNamespace
import msgspec
from sglang.srt.arg_groups.kv_cache_hook import handle_page_major_kv_layout
from sglang.srt.configs.model_config import AttentionArch
from sglang.srt.server_args import ServerArgs
@@ -50,7 +52,7 @@ def _accepts(
) -> bool:
"""Run just `handle_page_major_kv_layout` against a minimal stand-in, since
ServerArgs' real constructor pulls in a model config."""
sa = ServerArgs.__new__(ServerArgs)
sa = ServerArgs(model_path="dummy")
for name, value in {
"enable_unified_memory": unified,
# The unified pool sets this itself; without it the flag must be explicit
@@ -64,8 +66,8 @@ def _accepts(
"linear_attn_prefill_backend": linear_prefill,
"mamba_backend": "triton",
}.items():
object.__setattr__(sa, name, value)
object.__setattr__(
msgspec.Struct.__setattr__(sa, name, value)
setattr(
sa,
"_model_config",
SimpleNamespace(
@@ -8,13 +8,15 @@ value the caller still holds and the snapshot cannot see it.
"""
import copy
import dataclasses
import json
import os
import shutil
import tempfile
import unittest
import msgspec
import msgspec.structs
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -108,7 +110,7 @@ class TestRecordHoldsTheRawInput(CustomTestCase):
moved = {
field.name: (raw[field.name], getattr(server_args, field.name))
for field in dataclasses.fields(server_args)
for field in msgspec.structs.fields(server_args)
if _moved(getattr(server_args, field.name), raw[field.name])
}
self.assertEqual(
@@ -9,7 +9,6 @@ or is projected into the wrong namespace therefore fails on observed state.
import ast
import copy
import dataclasses
import json
import os
import pathlib
@@ -18,6 +17,9 @@ import tempfile
import unittest
import unittest.mock
import msgspec
import msgspec.structs
import sglang
from sglang.srt.arg_groups.overrides import resolution_result
from sglang.srt.server_args import ServerArgs
@@ -30,7 +32,7 @@ _SRT = pathlib.Path(sglang.__file__).resolve().parent / "srt"
# Every field of the record: resolution has no bare-assignment writer left, so
# the scan states that as a whole rather than a converted-so-far list.
_RESOLVED_FIELDS = frozenset(field.name for field in dataclasses.fields(ServerArgs))
_RESOLVED_FIELDS = frozenset(field.name for field in msgspec.structs.fields(ServerArgs))
# Shapes the agreement check runs on. Each needs a real config.json:
# `model_path="dummy"` takes the pipeline's early return.
@@ -201,14 +203,14 @@ class TestResolutionDeclarations(CustomTestCase):
supplied = {"random_seed": 42, **shape}
server_args = self._resolve(shape)
overlay = _stash_overlay(server_args)
for field in dataclasses.fields(server_args):
for field in msgspec.structs.fields(server_args):
if field.name in ("model_path", "device") or field.name in overlay:
continue
if field.name in supplied:
before = supplied[field.name]
elif field.default is not dataclasses.MISSING:
elif field.default is not msgspec.NODEFAULT:
before = field.default
elif field.default_factory is not dataclasses.MISSING:
elif field.default_factory is not msgspec.NODEFAULT:
before = field.default_factory()
else:
continue
@@ -278,7 +280,7 @@ class TestResolutionDeclarations(CustomTestCase):
dump = server_args.resolved_dict()
self.assertEqual(
sorted(dump),
sorted(field.name for field in dataclasses.fields(server_args)),
sorted(field.name for field in msgspec.structs.fields(server_args)),
"the readback dump is no longer exactly the fields",
)
leaked = sorted(
@@ -532,7 +534,7 @@ class TestResolutionDeclarations(CustomTestCase):
overlay = _stash_overlay(server_args)
raw_input = getattr(server_args, "_raw_input", None)
self.assertTrue(raw_input, f"{shape}: the record kept no raw snapshot")
for field in dataclasses.fields(server_args):
for field in msgspec.structs.fields(server_args):
name = field.name
if name in overlay or name not in raw_input:
continue
@@ -33,6 +33,8 @@ import tempfile
import unittest
import unittest.mock
import msgspec
import msgspec.structs
import torch
from sglang.srt.arg_groups.overrides import (
@@ -270,7 +272,7 @@ class TestResolutionIsReproducible(_RestoresProcessState, CustomTestCase):
the one of those that a shared mutable could corrupt.
"""
out = {}
for field in dataclasses.fields(server_args):
for field in msgspec.structs.fields(server_args):
if field.name in _NOT_COMPARABLE:
continue
# The resolution result, not the field: a declaration-only resolver
@@ -517,7 +519,7 @@ class TestALateDeclarationKeepsTheResolution(_RestoresProcessState, CustomTestCa
record arrives resolved, and a copy would throw that away.
"""
parent = self._resolved()
bare = dataclasses.replace(parent, dist_init_addr="1.2.3.4:5000")
bare = msgspec.structs.replace(parent, dist_init_addr="1.2.3.4:5000")
self.assertFalse(
getattr(bare, "_resolution_finished", False),
"a bare replace carried the flag; then this test proves nothing",
@@ -528,7 +530,7 @@ class TestALateDeclarationKeepsTheResolution(_RestoresProcessState, CustomTestCa
resolution_result(parent, field.name),
resolution_result(bare, field.name),
)
for field in dataclasses.fields(parent)
for field in msgspec.structs.fields(parent)
if field.name not in ("dist_init_addr", "random_seed")
and repr(resolution_result(parent, field.name))
!= repr(resolution_result(bare, field.name))
@@ -22,10 +22,12 @@ the two scopes it can derive exactly.
"""
import ast
import dataclasses
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
@@ -34,7 +36,7 @@ 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 dataclasses.fields(ServerArgs))
_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
@@ -1,5 +1,4 @@
import argparse
import dataclasses
import json
import os
import pickle
@@ -10,6 +9,9 @@ import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import msgspec
import msgspec.structs
import sglang.srt.server_args as server_args_module
from sglang.srt.arg_groups import parallel_hook, pd_disaggregation_hook, serving_hook
from sglang.srt.arg_groups.attention_hook import (
@@ -267,7 +269,7 @@ class TestPrepareServerArgs(CustomTestCase):
# And across the hop that matters: the scheduler and the draft worker
# rebuild the record from its fields and resolve again, so the bit has
# to survive `asdict` and come back the same the second time.
reconstructed = ServerArgs(**dataclasses.asdict(inherited))
reconstructed = ServerArgs(**msgspec.structs.asdict(inherited))
handle_missing_default_values(reconstructed)
self.assertFalse(
@@ -1046,7 +1048,9 @@ class TestContextParallelServerArgs(CustomTestCase):
ServerArgs.add_cli_args(self.parser)
def _new_cp_args(self, **overrides):
server_args = object.__new__(ServerArgs)
# Constructed, not conjured: a Struct has no uninitialized form, and
# every field this case does not name wants its declared default
# anyway.
defaults = dict(
enable_prefill_cp=False,
cp_strategy=None,
@@ -1061,9 +1065,7 @@ class TestContextParallelServerArgs(CustomTestCase):
enable_aiter_allreduce_fusion=False,
)
defaults.update(overrides)
for key, value in defaults.items():
setattr(server_args, key, value)
return server_args
return ServerArgs(**defaults)
def test_canonical_prefill_cp_requires_strategy(self):
args = self.parser.parse_args(["--model", "dummy", "--enable-prefill-cp"])
@@ -2170,7 +2172,7 @@ class TestDeepEPv2Args(CustomTestCase):
prefill=PhaseConfig(backend=Backend.FULL, max_bs=512),
)
server_args._resolved_overrides = []
valid = {f.name for f in dataclasses.fields(ServerArgs)}
valid = {f.name for f in msgspec.structs.fields(ServerArgs)}
for key, value in overrides.items():
# Reject stale field names before setattr silently accepts them.
assert key in valid, f"{key} is not a ServerArgs field"
@@ -2489,7 +2491,7 @@ class TestHandleCrashDumpEnv(CustomTestCase):
)
def _run_handler(self, crash_dump_folder, preset_env=None):
server_args = ServerArgs.__new__(ServerArgs)
server_args = ServerArgs(model_path="dummy")
server_args.crash_dump_folder = crash_dump_folder
with patch.dict(os.environ, preset_env or {}):
for key in self._COREDUMP_ENV_KEYS:
@@ -2868,7 +2870,7 @@ class TestTheInputIsSealedDuringResolution(CustomTestCase):
server_args = ServerArgs(model_path="dummy", device="cuda")
# The seal is what the pipeline runs under; drive it directly rather
# than injecting a violation into a real handler.
object.__setattr__(server_args, "_input_frozen", True)
msgspec.Struct.__setattr__(server_args, "_input_frozen", True)
with self.assertRaisesRegex(AttributeError, "during resolution"):
server_args.tp_size = 4
# and the message says what to do instead
@@ -2901,7 +2903,7 @@ class TestTheInputIsSealedDuringResolution(CustomTestCase):
)
server_args = ServerArgs(model_path="dummy", device="cuda")
object.__setattr__(server_args, "_input_frozen", True)
msgspec.Struct.__setattr__(server_args, "_input_frozen", True)
def foreign(config):
# What a plugin does: read what is decided, assign a default.
@@ -2961,7 +2963,7 @@ class TestLaunchCommand(CustomTestCase):
"""It describes how the configuration was asked for, so it is not part
of the configuration: no CLI flag, no namespace, not in the bags."""
self.assertNotIn(
"launch_command", {f.name for f in dataclasses.fields(ServerArgs)}
"launch_command", {f.name for f in msgspec.structs.fields(ServerArgs)}
)
self.assertNotIn(
"launch_command", ServerArgs(model_path="/tmp/x").resolved_dict()
@@ -32,6 +32,8 @@ user's stated intent), and decode capture is untouched either way.
import unittest
from types import SimpleNamespace
import msgspec
from sglang.srt.arg_groups.kv_cache_hook import handle_unified_memory_pool
from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.server_args import ServerArgs
@@ -42,7 +44,7 @@ register_cpu_ci(est_time=8, suite="base-a-test-cpu")
def _run_handler(*, prefill_backend, explicit):
"""Run just `handle_unified_memory_pool` over a minimal stand-in."""
sa = ServerArgs.__new__(ServerArgs)
sa = ServerArgs(model_path="dummy")
cg = SimpleNamespace(
prefill=SimpleNamespace(backend=prefill_backend),
decode=SimpleNamespace(backend=Backend.FULL),
@@ -59,7 +61,7 @@ def _run_handler(*, prefill_backend, explicit):
"cuda_graph_config": cg,
"cuda_graph_backend_prefill": prefill_backend if explicit else None,
}.items():
object.__setattr__(sa, name, value)
msgspec.Struct.__setattr__(sa, name, value)
handle_unified_memory_pool(sa)
return cg
@@ -23,6 +23,8 @@ pair, so without this gate a running server crashes mid-serving.
import unittest
from types import SimpleNamespace
import msgspec
from sglang.srt.arg_groups.kv_cache_hook import handle_unified_memory_pool
from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.server_args import ServerArgs
@@ -33,7 +35,7 @@ register_cpu_ci(est_time=5, suite="base-a-test-cpu")
def _run_handler(*, unified, tbo):
"""Run just `handle_unified_memory_pool` over a minimal stand-in."""
sa = ServerArgs.__new__(ServerArgs)
sa = ServerArgs(model_path="dummy")
for name, value in {
"enable_unified_memory": unified,
"enable_two_batch_overlap": tbo,
@@ -49,7 +51,7 @@ def _run_handler(*, unified, tbo):
),
"cuda_graph_backend_prefill": Backend.DISABLED,
}.items():
object.__setattr__(sa, name, value)
msgspec.Struct.__setattr__(sa, name, value)
handle_unified_memory_pool(sa)
+2 -2
View File
@@ -36,6 +36,7 @@ from sglang.srt.runtime_context import (
override_platform,
reset_context,
)
from sglang.srt.server_args import _declared_default
from sglang.test.test_utils import CustomTestCase
@@ -1613,14 +1614,13 @@ class TestGoldenModelOverrides(_IsolatedPublish):
from sglang.srt.arg_groups.model_overrides.deepseek_v4 import (
_deepseek_v4_overrides,
)
from sglang.srt.server_args import ServerArgs
hf = SimpleNamespace(architectures=["DeepseekV4ForCausalLM"])
def _args(**kw):
defaults = dict(
device="cuda",
swa_full_tokens_ratio=ServerArgs.swa_full_tokens_ratio,
swa_full_tokens_ratio=_declared_default("swa_full_tokens_ratio"),
moe_a2a_backend="none",
moe_runner_backend="auto",
_model_config=SimpleNamespace(is_fp4_experts=True, nvfp4_moe_meta=None),
+5 -4
View File
@@ -14,6 +14,9 @@ import unittest
import warnings
from unittest.mock import patch
import msgspec
import msgspec.structs
import sglang as _sglang
import sglang.srt.server_args as server_args_module
from sglang.srt.arg_groups.arg_utils import NS, A, Arg
@@ -438,7 +441,7 @@ class TestServerArgsScopedOverride(_IsolatedServerArgs):
from sglang.srt.runtime_context import get_spec
name = "_speculative_draft_quantization_explicitly_set"
self.assertIn(name, ServerArgs.__dataclass_fields__)
self.assertIn(name, ServerArgs.__struct_fields__)
published = get_context().override_server_args(**{name: True}).install()
# The record keeps the operator's input, as it does for every other
@@ -473,7 +476,6 @@ class TestServerArgsScopedOverride(_IsolatedServerArgs):
override.install()
@dataclasses.dataclass
class _FakeCaptureGroup(_FlagGroupBase):
gamma: int = 0
@@ -1639,13 +1641,12 @@ class TestTheDerivedHalfIsDeclared(CustomTestCase):
def test_a_declared_quotient_is_not_a_record_field(self):
"""It has no operator input to preserve, and the record is what crosses
a process boundary."""
import dataclasses
from sglang.srt.arg_groups.arg_utils import Derived
from sglang.srt.arg_groups.fields.parallel import Parallel
from sglang.srt.server_args import ServerArgs
fields = {f.name for f in dataclasses.fields(ServerArgs)}
fields = {f.name for f in msgspec.structs.fields(ServerArgs)}
for name, value in vars(Parallel).items():
if isinstance(value, Derived):
self.assertNotIn(name, fields)
@@ -9,6 +9,9 @@ import dataclasses
import unittest
from unittest import mock
import msgspec
import msgspec.structs
from sglang.srt import runtime_context as rc
from sglang.srt.arg_groups.arg_utils import NS, A
from sglang.srt.arg_groups.overrides import resolution_result
@@ -83,7 +86,7 @@ class TestConfigBags(CustomTestCase):
import dataclasses
sa, reference = self._resolve_published_and_sibling()
defaults = {f.name: f.default for f in dataclasses.fields(ServerArgs)}
defaults = {f.name: f.default for f in msgspec.structs.fields(ServerArgs)}
# Leaves resolution writes on this input on both CI device shapes
# (CUDA host and CPU-only runner): each starts at a None default.
sampled = (
@@ -8,6 +8,8 @@ field aborts before any write; provenance is recorded.
import unittest
import msgspec
from sglang.srt import runtime_context as rc
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
@@ -110,7 +112,7 @@ class TestContextOverride(CustomTestCase):
# server_args is read-only after resolution: resolved config changes go
# to the bags, a per-runner config to a derived variant.
sa = ServerArgs(model_path="dummy")
object.__setattr__(sa, "_resolution_finished", True)
msgspec.Struct.__setattr__(sa, "_resolution_finished", True)
with self.assertRaises(AttributeError):
sa.page_size = 999
@@ -3,7 +3,7 @@
import argparse
import unittest
from sglang.srt.server_args import ServerArgs
from sglang.srt.server_args import ServerArgs, _declared_default
from sglang.srt.utils.common import human_readable_int
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -23,7 +23,9 @@ class TestServerArgsMigratedCliMetadata(CustomTestCase):
}
def test_argparse_shape_is_preserved_for_representative_migrated_options(self):
self.assertEqual(self.actions_by_option["--dtype"].default, ServerArgs.dtype)
self.assertEqual(
self.actions_by_option["--dtype"].default, _declared_default("dtype")
)
self.assertEqual(
self.actions_by_option["--dtype"].choices,
["auto", "half", "float16", "bfloat16", "float", "float32"],
@@ -11,9 +11,11 @@ This is the guardrail that fails when an upstream PR adds a field to a namespace
class that has no ``_NS_PATH``, or adds one outside the taxonomy below.
"""
import dataclasses
import unittest
import msgspec
import msgspec.structs
from sglang.srt.arg_groups.arg_utils import namespace_of
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
@@ -48,7 +50,7 @@ VALID_NAMESPACES = {
def _field_names():
return {f.name for f in dataclasses.fields(ServerArgs)}
return {f.name for f in msgspec.structs.fields(ServerArgs)}
class TestServerArgsNamespaces(CustomTestCase):
@@ -153,13 +153,13 @@ class TestSplitBackendsReachTheDecisions(CustomTestCase):
def test_the_flashinfer_version_guard_sees_a_split_launch(self):
# The launcher runs before any publish, so it asks the record; the
# member and the accessor answer the same pair.
args = ServerArgs.__new__(ServerArgs)
args = ServerArgs(model_path="dummy")
for name, value in (
("attention_backend", None),
("prefill_attention_backend", None),
("decode_attention_backend", "flashinfer"),
):
object.__setattr__(args, name, value)
setattr(args, name, value)
self.assertIn("flashinfer", attention_backends_of(resolved_view(args)))
def test_support_triton_is_the_regression_being_guarded(self):
@@ -43,7 +43,6 @@ process-wide at all, so neither the read nor the field is on this axis.
"""
import ast
import dataclasses
import json
import os
import shutil
@@ -51,6 +50,9 @@ 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
@@ -367,10 +369,10 @@ class TestSuppliedInstanceExposure(CustomTestCase):
"would drift"
)
defaults = {}
for field in dataclasses.fields(resolved):
if field.default is not dataclasses.MISSING:
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 dataclasses.MISSING:
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: