config: publishing is the process entry's job (#36251)
This commit is contained in:
@@ -19,6 +19,7 @@ from sglang.srt.distributed.parallel_state_wrapper import ParallelState
|
||||
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
from sglang.srt.runtime_context import publish
|
||||
from sglang.srt.sampling.sampling_params import SamplingParams
|
||||
from sglang.srt.server_args import PortArgs, ServerArgs
|
||||
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
|
||||
@@ -52,6 +53,8 @@ class TestForwardSplitPrefill(CustomTestCase):
|
||||
|
||||
cls.port_args = PortArgs.init_new(cls.server_args)
|
||||
|
||||
publish(cls.server_args, role="scheduler")
|
||||
|
||||
# Load model and tokenizer
|
||||
cls.model_config = ModelConfig.from_server_args(cls.server_args)
|
||||
cls.model_runner = ModelRunner(
|
||||
|
||||
@@ -15,6 +15,7 @@ from unittest.mock import Mock, patch
|
||||
|
||||
from sglang.srt.managers.io_struct import GenerateReqInput
|
||||
from sglang.srt.managers.tokenizer_manager import TokenizerManager
|
||||
from sglang.srt.runtime_context import publish
|
||||
from sglang.srt.server_args import PortArgs, ServerArgs
|
||||
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
||||
|
||||
@@ -39,6 +40,7 @@ class TestTokenizerBatchEncode(unittest.TestCase):
|
||||
):
|
||||
|
||||
mock_tokenizer.return_value = Mock(vocab_size=32000)
|
||||
publish(self.server_args, role="tokenizer")
|
||||
self.tokenizer_manager = TokenizerManager(self.server_args, self.port_args)
|
||||
|
||||
def test_batch_encode_enabled(self):
|
||||
|
||||
@@ -24,6 +24,7 @@ from sglang.srt.managers.tokenizer_manager import (
|
||||
TokenizerManager,
|
||||
)
|
||||
from sglang.srt.observability.req_time_stats import APIServerReqTimeStats
|
||||
from sglang.srt.runtime_context import publish
|
||||
from sglang.srt.server_args import PortArgs, ServerArgs
|
||||
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
||||
|
||||
@@ -45,6 +46,7 @@ class TestInputFormatDetection(unittest.TestCase):
|
||||
) as mock_tokenizer,
|
||||
):
|
||||
mock_tokenizer.return_value = Mock(vocab_size=32000)
|
||||
publish(self.server_args, role="tokenizer")
|
||||
self.tokenizer_manager = TokenizerManager(self.server_args, self.port_args)
|
||||
|
||||
def test_detect_single_string(self):
|
||||
@@ -143,6 +145,7 @@ class TestTokenizerInputPreparation(unittest.TestCase):
|
||||
) as mock_tokenizer,
|
||||
):
|
||||
mock_tokenizer.return_value = Mock(vocab_size=32000)
|
||||
publish(self.server_args, role="tokenizer")
|
||||
self.tokenizer_manager = TokenizerManager(self.server_args, self.port_args)
|
||||
|
||||
def test_prepare_single_string_input(self):
|
||||
@@ -203,6 +206,7 @@ class TestTokenizerResultExtraction(unittest.TestCase):
|
||||
) as mock_tokenizer,
|
||||
):
|
||||
mock_tokenizer.return_value = Mock(vocab_size=32000)
|
||||
publish(self.server_args, role="tokenizer")
|
||||
self.tokenizer_manager = TokenizerManager(self.server_args, self.port_args)
|
||||
|
||||
def test_extract_single_string_results(self):
|
||||
@@ -327,6 +331,7 @@ class TestTokenizerManagerIntegration(unittest.TestCase):
|
||||
) as mock_tokenizer,
|
||||
):
|
||||
mock_tokenizer.return_value = Mock(vocab_size=32000)
|
||||
publish(self.server_args, role="tokenizer")
|
||||
self.tokenizer_manager = TokenizerManager(self.server_args, self.port_args)
|
||||
|
||||
def test_full_workflow_single_string(self):
|
||||
|
||||
@@ -20,6 +20,7 @@ from sglang.srt.managers.schedule_batch import (
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
from sglang.srt.multimodal.processors.base_processor import BaseMultimodalProcessor
|
||||
from sglang.srt.parser.conversation import generate_chat_conv
|
||||
from sglang.srt.runtime_context import publish
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.test.test_utils import download_image_with_retry
|
||||
|
||||
@@ -141,16 +142,18 @@ class VisionLLMLogitsBase(unittest.IsolatedAsyncioTestCase):
|
||||
return inputs
|
||||
|
||||
def get_sglang_model(self):
|
||||
server_args = ServerArgs(
|
||||
model_path=self.model_path,
|
||||
disable_cuda_graph=True,
|
||||
)
|
||||
publish(server_args, role="scheduler")
|
||||
self.model_runner = ModelRunner(
|
||||
model_config=ModelConfig(self.model_path, model_override_args="{}"),
|
||||
mem_fraction_static=0.8,
|
||||
gpu_id=0,
|
||||
ps=ParallelState.trivial(),
|
||||
nccl_port=12435,
|
||||
server_args=ServerArgs(
|
||||
model_path=self.model_path,
|
||||
disable_cuda_graph=True,
|
||||
),
|
||||
server_args=server_args,
|
||||
)
|
||||
return self.model_runner.model
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ class TestServerInfoControlPlaneUpdates(CustomTestCase):
|
||||
tokenizer_manager.record_config_updates("test", weight_version="v2")
|
||||
self.assertEqual(tokenizer_manager.config_value("weight_version"), "v2")
|
||||
overlaid = tokenizer_manager.resolved_config_dict(
|
||||
dataclasses.asdict(server_args)
|
||||
server_args.resolved_dict()
|
||||
)
|
||||
self.assertEqual(overlaid["weight_version"], "v2")
|
||||
finally:
|
||||
@@ -313,9 +313,9 @@ class TestServerInfoExistingFieldsPreserved(CustomTestCase):
|
||||
"""
|
||||
|
||||
def test_every_server_args_field_appears_in_response(self):
|
||||
# `dataclasses.asdict(server_args)` is spread into the response;
|
||||
# asserting every dataclass field surfaces is the strongest
|
||||
# backward-compat guarantee that's still implementation-agnostic.
|
||||
# `server_args.resolved_dict()` is spread into the response; asserting
|
||||
# every dataclass field surfaces is the strongest backward-compat
|
||||
# guarantee that's still implementation-agnostic.
|
||||
args = ServerArgs(model_path="dummy")
|
||||
|
||||
info = _call_server_info_with(args)
|
||||
|
||||
@@ -376,6 +376,32 @@ class TestResolutionDeclarations(CustomTestCase):
|
||||
+ "\n ".join(differences),
|
||||
)
|
||||
|
||||
def test_the_whole_object_readback_carries_only_fields(self):
|
||||
"""`/server_info` and its gRPC and in-process twins report
|
||||
`ServerArgs.resolved_dict()`.
|
||||
|
||||
The dump is exactly the field names, carrying the resolution result
|
||||
for each. It holds none of the resolution bookkeeping (`_raw_input`, the
|
||||
declaration stash, the finished flag) and no `ModelConfig` memo: none of
|
||||
that is configuration, and all of it would cross IPC with the
|
||||
readback.
|
||||
"""
|
||||
server_args = self._resolve({"tp_size": 2})
|
||||
dump = server_args.resolved_dict()
|
||||
self.assertEqual(
|
||||
sorted(dump),
|
||||
sorted(field.name for field in dataclasses.fields(server_args)),
|
||||
"the readback dump is no longer exactly the fields",
|
||||
)
|
||||
leaked = sorted(
|
||||
name
|
||||
for name in vars(server_args)
|
||||
if name not in dump and not name.startswith("__")
|
||||
)
|
||||
self.assertNotEqual(
|
||||
leaked, [], "nothing to leak any more -- this check is now vacuous"
|
||||
)
|
||||
|
||||
def test_every_published_leaf_is_what_resolution_decided(self):
|
||||
"""One hop further than the check above: the leaf a reader reads.
|
||||
|
||||
|
||||
@@ -78,15 +78,22 @@ _KNOWN_ENTRIES = frozenset(
|
||||
"run_data_parallel_controller_process",
|
||||
),
|
||||
("srt/ray/scheduler_actor.py", "__init__"),
|
||||
("srt/disaggregation/encoder/server.py", "__init__"),
|
||||
("srt/disaggregation/encoder/http_server.py", "launch_server"),
|
||||
("srt/managers/tokenizer_manager.py", "__init__"),
|
||||
("srt/entrypoints/engine.py", "_launch_subprocesses"),
|
||||
(
|
||||
"srt/elastic_ep/expert_backup_manager.py",
|
||||
"run_expert_backup_manager_process",
|
||||
),
|
||||
("srt/weight_cache/daemon.py", "load"),
|
||||
# The multi-tokenizer worker, the benchmark work functions (run
|
||||
# inline or spawned per rank), and the encoder's gRPC / spawned-TP /
|
||||
# spawned-DP entries.
|
||||
("srt/entrypoints/http_server.py", "init_multi_tokenizer"),
|
||||
("benchmark/one_batch.py", "latency_test"),
|
||||
("benchmark/one_batch.py", "correctness_test"),
|
||||
("srt/disaggregation/encoder/grpc_server.py", "serve_grpc_encoder"),
|
||||
("srt/disaggregation/encoder/server.py", "launch_encoder"),
|
||||
("srt/disaggregation/encoder/runtime.py", "launch_dp_worker"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from sglang.srt.runtime_context import (
|
||||
ParallelContext,
|
||||
RuntimeContext,
|
||||
_FlagGroupBase,
|
||||
ensure_published,
|
||||
assert_published,
|
||||
get_context,
|
||||
get_exec,
|
||||
get_flags,
|
||||
@@ -255,35 +255,30 @@ class TestServerArgsOwnership(_IsolatedServerArgs):
|
||||
get_server_args()
|
||||
|
||||
|
||||
class TestEnsurePublished(_IsolatedServerArgs):
|
||||
"""A defensive publish must not re-project over a live process.
|
||||
class TestAssertPublished(_IsolatedServerArgs):
|
||||
"""Publishing is the process entry's job; the constructors only check.
|
||||
|
||||
Three constructors publish because each can be built with nothing published
|
||||
first: `ModelRunner`, `TokenizerManager`, `MMEncoder`. Inside a process that
|
||||
already published the same record, publishing again re-projects the bags --
|
||||
discarding every `override()` taken since, and the provenance log with it.
|
||||
|
||||
No current override sits in one of those windows, so what these assertions
|
||||
protect is the mechanism, not a reproduction: the drop is silent and depends
|
||||
on where a constructor happens to sit relative to the overrides around it.
|
||||
`ModelRunner`, `TokenizerManager` and `MMEncoder` assert. A publish inside
|
||||
a process that has already published re-projects the bags, discarding every
|
||||
`override()` taken since and the provenance log with it, so a constructor
|
||||
that finds nothing published fails loud.
|
||||
"""
|
||||
|
||||
def _record(self, **fields):
|
||||
return ServerArgs(model_path="dummy", **fields)
|
||||
|
||||
def test_a_second_publish_of_the_same_record_keeps_the_overrides(self):
|
||||
def test_the_check_leaves_a_live_process_alone(self):
|
||||
record = self._record(grammar_backend="xgrammar")
|
||||
publish(record, role="scheduler")
|
||||
get_context().override("grammar.import_fallback", grammar_backend="none")
|
||||
|
||||
ensure_published(record, role="scheduler")
|
||||
assert_published(record, role="scheduler")
|
||||
|
||||
self.assertEqual(
|
||||
get_exec().kernel.grammar_backend,
|
||||
"none",
|
||||
"the constructor's publish re-projected the bags, so the import "
|
||||
"fallback was discarded and the process reports a backend it is "
|
||||
"not using",
|
||||
"the check re-projected the bags, so the import fallback was "
|
||||
"discarded and the process reports a backend it is not using",
|
||||
)
|
||||
self.assertEqual(
|
||||
len(get_context().overrides_log()),
|
||||
@@ -291,45 +286,49 @@ class TestEnsurePublished(_IsolatedServerArgs):
|
||||
"the provenance of the override went with it",
|
||||
)
|
||||
|
||||
def test_a_different_record_is_published(self):
|
||||
def test_a_different_record_fails(self):
|
||||
first = self._record(grammar_backend="xgrammar")
|
||||
publish(first, role="scheduler")
|
||||
second = self._record(grammar_backend="llguidance")
|
||||
|
||||
ensure_published(second, role="scheduler")
|
||||
with self.assertRaisesRegex(RuntimeError, "a different record is published"):
|
||||
assert_published(second, role="scheduler")
|
||||
|
||||
self.assertIs(get_server_args(), second)
|
||||
self.assertEqual(get_exec().kernel.grammar_backend, "llguidance")
|
||||
self.assertIs(
|
||||
get_server_args(),
|
||||
first,
|
||||
"the failing check published anyway",
|
||||
)
|
||||
|
||||
def test_an_empty_slot_is_published(self):
|
||||
"""The standalone case the defensive publish exists for."""
|
||||
def test_an_empty_slot_fails(self):
|
||||
"""An empty slot fails."""
|
||||
reset_context()
|
||||
record = self._record(grammar_backend="xgrammar")
|
||||
|
||||
ensure_published(record, role="scheduler")
|
||||
with self.assertRaisesRegex(
|
||||
RuntimeError, "nothing is published in this process"
|
||||
):
|
||||
assert_published(record, role="scheduler")
|
||||
|
||||
self.assertIs(get_server_args(), record)
|
||||
self.assertEqual(publish_role(), "scheduler")
|
||||
|
||||
def test_the_same_record_under_a_different_role_is_republished(self):
|
||||
def test_the_same_record_under_a_different_role_fails(self):
|
||||
"""The role decides which namespaces this process may read."""
|
||||
record = self._record()
|
||||
publish(record, role="tokenizer")
|
||||
|
||||
ensure_published(record, role="scheduler")
|
||||
with self.assertRaisesRegex(RuntimeError, "published under role 'tokenizer'"):
|
||||
assert_published(record, role="scheduler")
|
||||
|
||||
self.assertEqual(publish_role(), "scheduler")
|
||||
self.assertEqual(publish_role(), "tokenizer")
|
||||
|
||||
def test_every_constructor_that_publishes_is_classified(self):
|
||||
"""A new constructor publish has to say which of the two it is.
|
||||
def test_no_constructor_publishes_outside_the_two_entries(self):
|
||||
"""Publishing from an `__init__` is an entry's job or a bug.
|
||||
|
||||
Publishing in a constructor is right when the constructor *is* the
|
||||
entry -- a spawned worker, the Ray actor that stands in for
|
||||
`run_scheduler_process`, an `Engine` being (re)built, where resetting
|
||||
the bags is the point -- and wrong when the process is already live
|
||||
with the same record, where it silently drops overrides. The
|
||||
difference is not visible in the syntax, so the census is pinned:
|
||||
adding one fails here until it is classified.
|
||||
It is right when the constructor *is* the entry -- an `Engine` being
|
||||
(re)built, the Ray actor that stands in for `run_scheduler_process`,
|
||||
where resetting the bags is the point. It is wrong anywhere else,
|
||||
because the process is already live with a record and re-projecting
|
||||
drops its overrides. The census is pinned, so a new constructor publish
|
||||
fails here until it is one of the two.
|
||||
|
||||
Both the publisher set and "which `__init__` reaches one" come from
|
||||
`sglang.test.config_publishers`, which derives them from the code --
|
||||
@@ -347,24 +346,11 @@ class TestEnsurePublished(_IsolatedServerArgs):
|
||||
self.assertEqual(
|
||||
constructor_publishers(srt),
|
||||
{
|
||||
# Entries: nothing published yet, or a rebuild that must not
|
||||
# inherit the previous engine's runtime overrides.
|
||||
("entrypoints/engine.py", "Engine", "publish"),
|
||||
("ray/scheduler_actor.py", "SchedulerActor", "publish"),
|
||||
# Defensive: the process is usually already live with this
|
||||
# record, and `launch_server` publishes before building the
|
||||
# in-process encoder.
|
||||
("disaggregation/encoder/server.py", "MMEncoder", "ensure_published"),
|
||||
(
|
||||
"managers/tokenizer_manager.py",
|
||||
"TokenizerManager",
|
||||
"ensure_published",
|
||||
),
|
||||
("model_executor/model_runner.py", "ModelRunner", "ensure_published"),
|
||||
},
|
||||
"a constructor publishes and this census does not know which kind "
|
||||
"it is; an entry uses publish(), one that may run inside a live "
|
||||
"process with the same record uses ensure_published()",
|
||||
"a constructor publishes and it is not one of the two entries; "
|
||||
"publish at the process entry and let the constructor assert",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user