Files
sglang/test/registered/unit/server_args/test_declared_fallbacks.py
T

115 lines
5.2 KiB
Python

"""A field can declare what it means when nobody said anything.
`Arg(fallback=...)` is the value a field takes when the operator did not type
it and resolution did not decide it. It is not the dataclass default: that
stays `None`, because `None` is how the record spells "not typed" and the
record is what crosses a process boundary.
The point of the tests below is that the three surfaces keep disagreeing, on
purpose:
* the **record** still holds `None` -- so a model family asking "did anyone set
this?" still gets an answer, and the wire format is unchanged;
* `resolving_view` -- the view a resolution pass decides on -- still answers
`None`, so a family's `if cfg.x is None` fires and its declaration lands.
This is the half that carries the design: `model_overrides/inkling.py` and
`deepseek_v4.py` are the only `is None` readers of either declared field, and
both read this view. Pinned by the family-shaped test below rather than by
asserting the view directly, because the shape is what has to keep working.
`resolved_view` is a separate class and is not pinned here -- nothing reads a
declared field through it, and `with_fallback` is called from exactly one
place (`resolution_result`), which is what makes both views answer `None`
without either of them knowing about fallbacks;
* the **effective** surface -- `resolution_result`, the projection, and the
config bags every runtime reader goes through -- answers with the fallback.
That last split is the whole design. Putting the fallback in the views instead
would make `if cfg.swa_full_tokens_ratio is None` in `model_overrides/inkling.py`
never fire, and the family's 0.1 would be silently replaced by the generic 0.8.
"""
import unittest
from sglang.srt.arg_groups.overrides import (
declare_resolution,
resolution_result,
resolving_view,
)
from sglang.srt.runtime_context import get_schedule, publish, reset_context
from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=30, suite="base-a-test-cpu")
def _resolved(**kwargs) -> ServerArgs:
server_args = ServerArgs(model_path="dummy", **kwargs)
server_args.resolve_once()
return server_args
class TestAFallbackIsWhatNobodySaid(CustomTestCase):
def test_the_effective_value_is_the_declared_fallback(self):
server_args = _resolved()
self.assertEqual(resolution_result(server_args, "swa_full_tokens_ratio"), 0.8)
self.assertEqual(resolution_result(server_args, "mamba_full_memory_ratio"), 0.9)
def test_the_record_still_says_the_operator_typed_nothing(self):
# The fallback is not the dataclass default. A child process that
# unpickles this record has to be able to tell "unset" from "set to
# the value resolution would have picked anyway".
server_args = _resolved()
self.assertIsNone(server_args.swa_full_tokens_ratio)
self.assertIsNone(server_args.mamba_full_memory_ratio)
def test_what_the_operator_typed_wins(self):
server_args = _resolved(swa_full_tokens_ratio=0.25)
self.assertEqual(resolution_result(server_args, "swa_full_tokens_ratio"), 0.25)
def test_a_decision_wins(self):
server_args = _resolved()
declare_resolution(server_args, "a_model_family", swa_full_tokens_ratio=0.1)
self.assertEqual(resolution_result(server_args, "swa_full_tokens_ratio"), 0.1)
def test_the_published_bag_answers_with_the_fallback(self):
# Every runtime reader goes through the bags, so this is the surface
# that decides how the pools are sized.
reset_context()
self.addCleanup(reset_context)
publish(ServerArgs(model_path="dummy"), role="test")
self.assertEqual(get_schedule().swa_full_tokens_ratio, 0.8)
self.assertEqual(get_schedule().mamba_full_memory_ratio, 0.9)
def test_the_dummy_short_circuit_leaves_no_bag_holding_none(self):
# The dummy path returns before most of the pipeline. It used to have
# to call the ratio pass by hand on the way out; a declared fallback
# needs no slot, so there is nothing left to forget.
reset_context()
self.addCleanup(reset_context)
publish(ServerArgs(model_path="none"), role="test")
self.assertIsNotNone(get_schedule().swa_full_tokens_ratio)
self.assertIsNotNone(get_schedule().mamba_full_memory_ratio)
class TestAPassDecidingStillSeesUnset(CustomTestCase):
"""The views are the Decision-over-Input surface, not the Effect surface.
`model_overrides/inkling.py` and `model_overrides/deepseek_v4.py` both ask
`if cfg.swa_full_tokens_ratio is None` and declare 0.1 when it is. If a
fallback answered here, that branch would be dead and the family's value
would never be declared.
"""
def test_a_family_that_tests_is_none_still_fires(self):
server_args = _resolved()
cfg = resolving_view(server_args)
declared = {}
if cfg.swa_full_tokens_ratio is None: # the family's exact shape
declared["swa_full_tokens_ratio"] = 0.1
self.assertEqual(declared, {"swa_full_tokens_ratio": 0.1})
if __name__ == "__main__":
unittest.main()