A runner and the objects it builds freeze the placement they describe (#40341)

This commit is contained in:
Cheng Wan
2026-09-21 12:24:17 -07:00
committed by GitHub
parent 2d0e94e3a3
commit 65be3fa71a
13 changed files with 289 additions and 110 deletions
@@ -129,8 +129,27 @@ def mixer2_gated_norm_tensor_parallel(
)
mixer.weight.weight_loader(mixer.weight, weight)
# m2 reads tp via get_parallel().tp_size/rank — force it through the context.
with get_parallel().override(tp_size=1, tp_rank=0):
# m2 reads tp via get_parallel().tp_size/rank — state a single-rank topology
# through the context. Every width that follows from `tp_size` is named:
# narrowing one leaf and leaving the quotients behind describes no layout.
with get_parallel().override(
tp_size=1,
tp_rank=0,
tp_group=None,
attn_tp_size=1,
attn_tp_rank=0,
attn_tp_group=None,
attn_dp_size=1,
attn_dp_rank=0,
attn_cp_size=1,
attn_cp_rank=0,
attn_cp_group=None,
moe_ep_size=1,
moe_ep_rank=0,
moe_ep_group=None,
moe_dp_size=1,
moe_tp_size=1,
):
# create gated-norm without TP to compute reference
mixer_single_gpu = m2.Mixer2RMSNormGated(
full_hidden_size=hidden_size,
@@ -84,7 +84,7 @@ def _stub(
stub._max_mamba_cache_size = max_mamba_cache_size
stub._disable_radix_cache = disable_radix_cache
stub.max_total_num_tokens = max_total_num_tokens
stub.ps = SimpleNamespace(attn_dp_size=dp_size)
stub.attn_dp_size = dp_size
return stub
@@ -94,7 +94,7 @@ def _hybrid_stub_for_initialize(
"""A stub carrying what the real initialize() reads (hybrid path)."""
stub = MlxModelRunnerStub.__new__(MlxModelRunnerStub)
stub._mlx_pool_size = pool
stub.ps = SimpleNamespace(attn_dp_size=1)
stub.attn_dp_size = 1
stub.device = "cpu" # read by init_ngram_embedding_manager
# Evaluated as a call argument in init_ngram_embedding_manager before
# the use_ngram_embedding short-circuit; never read.
@@ -91,7 +91,8 @@ def _make_prefill_aware_swa_runner(
kv_cache_dtype=torch.float16,
kv_cache_dtype_str="auto",
page_size=1,
ps=SimpleNamespace(attn_cp_size=1, tp_size=1),
attn_cp_size=1,
tp_size=1,
is_draft_worker=False,
server_args=server_args,
attention_chunk_size=None,
@@ -671,7 +671,7 @@ class TestModelRunnerStartupWeightLoadOwnership(CustomTestCase):
elastic_ep_backend=None,
is_ep_joiner=False,
)
runner.ps = SimpleNamespace(tp_rank=0)
runner.tp_rank = 0
return runner
def test_start_delegates_to_the_manager(self):
@@ -248,7 +248,7 @@ def test_worker_folds_a_gate_admitted_quantized_selector_head(monkeypatch):
worker = SimpleNamespace(
block_size=8,
selector=object(),
ps=SimpleNamespace(tp_rank=0),
model_runner=SimpleNamespace(tp_rank=0),
draft_model=SimpleNamespace(lm_head=None),
device="cpu",
_selector_sampling_enabled=True,
@@ -282,7 +282,7 @@ def test_worker_warns_once_when_selector_sampling_is_disabled(monkeypatch):
selector=object(),
_selector_sampling_enabled=False,
_warned_sampling_fallback=False,
ps=SimpleNamespace(tp_rank=0),
model_runner=SimpleNamespace(tp_rank=0),
)
batch = SimpleNamespace(sampling_info=SimpleNamespace(is_all_greedy=False))
+142 -5
View File
@@ -2781,17 +2781,154 @@ class TestWhoAnswersDuringADraftScope(CustomTestCase):
def test_a_report_built_for_a_runner_follows_that_runner(self):
"""A weight check is an on-demand request served from the scheduler
loop, so it runs outside the scope that describes a draft runner. Its
report has to name the runner it was built for, which is why it holds
a record instead of asking the context."""
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
report has to name the runner it was built for, which is why it reads
the placement once, where it is built, instead of asking again when the
request arrives."""
from sglang.srt.distributed import parallel_state
from sglang.srt.utils.weight_checker import WeightChecker
draft = ParallelState.trivial(pp_rank=0, pp_size=1)
checker = WeightChecker(get_model=lambda: None, ps=draft)
self._two_stage_pipeline()
self.assertEqual(get_parallel().pp_size, 2)
group = self._single_member_group()
with patch.object(parallel_state, "_PP", group):
with parallel_state.patch_pipeline_parallel_group(group):
checker = WeightChecker(get_model=lambda: None)
# The scope has closed and the context answers the target's shape again.
self.assertEqual(get_parallel().pp_size, 2)
info = checker._parallelism_info()
self.assertEqual((info.pp_rank, info.pp_size), (0, 1))
class TestNothingReadsThePlacementBeforeItIsFrozen(CustomTestCase):
"""`ModelRunner.__init__` freezes its placement partway through.
A method called before that point reads an attribute that does not exist
yet, and only on the configuration that reaches it -- a remote weight
transporter, a NUMA binding -- so the suites say nothing and a GPU job is
where it surfaces. The order is what makes it wrong, so the order is what
is checked.
"""
def _model_runner(self):
import ast as _ast
source = (_SRT / "model_executor" / "model_runner.py").read_text()
for node in _ast.parse(source).body:
if isinstance(node, _ast.ClassDef) and node.name == "ModelRunner":
return {m.name: m for m in node.body if isinstance(m, _ast.FunctionDef)}
raise AssertionError("ModelRunner not found")
def _frozen_names(self, methods):
import ast as _ast
return {
target.attr
for node in _ast.walk(methods["init_torch_distributed"])
if isinstance(node, _ast.Assign)
for target in node.targets
if isinstance(target, _ast.Attribute)
and isinstance(target.value, _ast.Name)
and target.value.id == "self"
}
def _reads(self, methods, fn, frozen, depth=0):
import ast as _ast
found = set()
for node in _ast.walk(fn):
if (
isinstance(node, _ast.Attribute)
and isinstance(node.value, _ast.Name)
and node.value.id == "self"
and isinstance(node.ctx, _ast.Load)
and node.attr in frozen
):
found.add(node.attr)
if (
depth < 2
and isinstance(node, _ast.Call)
and isinstance(node.func, _ast.Attribute)
and isinstance(node.func.value, _ast.Name)
and node.func.value.id == "self"
and node.func.attr in methods
and node.func.attr != "init_torch_distributed"
):
found |= self._reads(
methods, methods[node.func.attr], frozen, depth + 1
)
return found
def _calls_before_the_freeze(self, methods):
import ast as _ast
calls = []
for statement in methods["__init__"].body:
for node in _ast.walk(statement):
if (
isinstance(node, _ast.Call)
and isinstance(node.func, _ast.Attribute)
and isinstance(node.func.value, _ast.Name)
and node.func.value.id == "self"
):
calls.append((node.lineno, node.func.attr))
calls.sort()
names = [name for _, name in calls]
self.assertIn(
"init_torch_distributed",
names,
"the freeze moved; this census is keyed on where it happens",
)
return calls[: names.index("init_torch_distributed")]
def test_no_method_called_before_the_freeze_reads_what_it_freezes(self):
methods = self._model_runner()
frozen = self._frozen_names(methods)
self.assertGreater(len(frozen), 5, "found no frozen names; census is broken")
offenders = []
for lineno, name in self._calls_before_the_freeze(methods):
fn = methods.get(name)
if fn is None:
continue
read = self._reads(methods, fn, frozen)
if read:
offenders.append(
f"__init__:{lineno} self.{name}() reads {sorted(read)}"
)
self.assertEqual(
offenders,
[],
"these run before init_torch_distributed and read what it sets; "
"ask get_parallel() there, or move the call after the freeze:\n "
+ "\n ".join(offenders),
)
def test_the_census_would_notice_one(self):
"""The positive control: the walk has to find a read that is there."""
import ast as _ast
import textwrap
methods = {
m.name: m
for m in _ast.parse(
textwrap.dedent(
"""
class R:
def init_torch_distributed(self):
self.tp_rank = 0
def early(self):
return self.tp_rank
"""
)
)
.body[0]
.body
}
frozen = self._frozen_names(methods)
self.assertEqual(frozen, {"tp_rank"})
self.assertEqual(self._reads(methods, methods["early"], frozen), {"tp_rank"})
if __name__ == "__main__":
unittest.main()
@@ -44,7 +44,7 @@ from sglang.srt.utils.weight_checker_comparator import (
RawComparable,
)
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import CustomTestCase
from sglang.test.test_utils import CustomTestCase, enter_scope, published_topology
register_amd_ci(est_time=30, suite="stage-b-test-1-gpu-small-amd")
register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-small")
@@ -551,7 +551,8 @@ class _WeightCheckerTestBase(CustomTestCase):
torch.manual_seed(0)
self.model = _TinyModel().cuda()
runner = _FakeModelRunner(self.model)
self.checker = WeightChecker(get_model=lambda: runner.model, ps=runner.ps)
enter_scope(self, published_topology())
self.checker = WeightChecker(get_model=lambda: runner.model)
class TestSnapshot(_WeightCheckerTestBase):
@@ -760,9 +761,16 @@ class _ChecksumTestBase(CustomTestCase):
pp_rank=0,
pp_size=1,
)
self.checker = WeightChecker(
get_model=lambda: self.runner.model, ps=self.runner.ps
enter_scope(
self,
published_topology(
tp_size=4,
dp_size=2,
enable_dp_attention=True,
ranks={"world_rank": 2, "dp_rank": 1},
),
)
self.checker = WeightChecker(get_model=lambda: self.runner.model)
class TestComputeChecksum(_ChecksumTestBase):