[misc] Trim redundant variants from the 8-gpu-h20 disaggregation test suite (#39544)

This commit is contained in:
Liangsheng Yin
2026-09-15 01:24:46 -07:00
committed by GitHub
parent 4f52c9948b
commit 8565b11003
5 changed files with 91 additions and 149 deletions
@@ -26,15 +26,7 @@ from sglang.test.test_utils import (
try_cached_model,
)
register_cuda_ci(est_time=509, stage="base-c", runner_config="8-gpu-h20")
def _has_nixl():
try:
import nixl._api # noqa: F401
except ImportError:
return False
return True
register_cuda_ci(est_time=170, stage="base-c", runner_config="8-gpu-h20")
def _has_mooncake():
@@ -134,16 +126,6 @@ class DisaggregationDecodeRadixCacheTestMixin:
)
@unittest.skipUnless(
is_in_ci() or _has_nixl(),
"NIXL is required for decode radix cache disaggregation coverage.",
)
class TestDisaggregationDecodeRadixCacheNixl(
DisaggregationDecodeRadixCacheTestMixin, PDDisaggregationServerBase
):
transfer_backend_name = "nixl"
@unittest.skipUnless(
is_in_ci() or _has_mooncake(),
"Mooncake is required for decode radix cache disaggregation coverage.",
@@ -1,7 +1,6 @@
import unittest
from types import SimpleNamespace
from sglang.benchmark.serving import run_benchmark
from sglang.srt.environ import envs
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.run_eval import run_eval
@@ -11,18 +10,15 @@ from sglang.test.server_fixtures.disaggregation_fixture import (
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST_MLA,
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
get_benchmark_args,
popen_launch_pd_server,
try_cached_model,
)
register_cuda_ci(est_time=191, stage="base-c", runner_config="8-gpu-h20")
register_cuda_ci(est_time=140, stage="base-c", runner_config="8-gpu-h20")
class TestDisaggregationDPAttention(PDDisaggregationServerBase):
"""PD-disagg + DP-attention e2e on `total_tokens` LB — the most complex
dispatch (token accounting + tie-break + estimated_tokens). Simpler
algorithms are unit-tested in
"""The dispatch algorithm itself is covered in
test/registered/unit/managers/test_data_parallel_controller.py.
"""
@@ -113,22 +109,6 @@ class TestDisaggregationDPAttention(PDDisaggregationServerBase):
self.assertGreater(metrics["score"], 0.60)
def test_bench_serving(self):
args = get_benchmark_args(
base_url=f"http://{self.base_host}:{self.lb_port}",
dataset_name="random",
tokenizer=self.model,
num_prompts=1000,
random_input_len=4096,
random_output_len=1024,
request_rate=float("inf"),
max_concurrency=256,
)
result = run_benchmark(args)
self.assertLess(result["mean_tpot_ms"], 20)
self.assertEqual(result["completed"], 1000)
if __name__ == "__main__":
unittest.main()
@@ -22,7 +22,7 @@ from sglang.test.test_utils import (
try_cached_model,
)
register_cuda_ci(est_time=506, stage="base-c", runner_config="8-gpu-h20")
register_cuda_ci(est_time=350, stage="base-c", runner_config="8-gpu-h20")
# base-c 8-GPU runner is required for TP4 prefill + TP4 decode.
NIXL_PREFILL_TP_SIZE = 4
@@ -198,12 +198,8 @@ class NixlPDDisaggregationServerBase(PDDisaggregationServerBase):
"NIXL with the configured backend is required for this test.",
)
class TestDisaggregationNixlBasic(NixlPDDisaggregationServerBase):
"""Small NIXL PD E2E coverage.
Mooncake already owns the broad disaggregation functional matrix in
test_disaggregation_basic.py. This class intentionally mirrors only the
subset that proves NIXL can launch, transfer KV, serve a request, return
logprobs, and keep all workers alive.
"""Mooncake owns the broad disaggregation functional matrix in
test_disaggregation_basic.py; this is the NIXL-only subset.
"""
@classmethod
@@ -256,21 +252,6 @@ class TestDisaggregationNixlBasic(NixlPDDisaggregationServerBase):
self.assertEqual(len(output_logprobs), completion_tokens)
self.assertGreater(len(input_logprobs), 0)
@unittest.skipUnless(
_HAS_CONFIGURED_NIXL_BACKEND,
"NIXL with the configured backend is required for this test.",
)
class TestDisaggregationNixlAccuracy(NixlPDDisaggregationServerBase):
@classmethod
def setUpClass(cls):
_require_configured_nixl_backend()
_clear_disagg_failure_env()
super().setUpClass()
cls.model = try_cached_model(DEFAULT_MODEL_NAME_FOR_TEST)
configure_nixl_pd_backend(cls)
cls.launch_all()
def test_gsm8k_accuracy(self):
args = SimpleNamespace(
base_url=f"http://{self.base_host}:{self.lb_port}",
@@ -305,7 +286,10 @@ class TestDisaggregationNixlFailure(NixlPDDisaggregationServerBase):
def setUpClass(cls):
_require_configured_nixl_backend()
super().setUpClass()
os.environ["SGLANG_TEST_DISAGG_FAILURE_PROB"] = "0.05"
# 0.2, not the mooncake twin's 0.05: this test never inspects the eval
# result, so the rate alone decides how much of the failure path runs;
# over 50 requests 0.05 would leave an 8% chance of exercising none.
os.environ["SGLANG_TEST_DISAGG_FAILURE_PROB"] = "0.2"
cls.model = try_cached_model(DEFAULT_MODEL_NAME_FOR_TEST)
configure_nixl_pd_backend(cls)
cls.launch_all()
@@ -321,12 +305,12 @@ class TestDisaggregationNixlFailure(NixlPDDisaggregationServerBase):
eval_name="gsm8k",
api="completion",
max_tokens=512,
num_examples=200,
num_examples=50,
num_threads=128,
)
# Match TestDisaggregationMooncakeFailure: inject many transfer failures
# and tolerate eval/request errors as long as workers remain healthy.
# Tolerate eval/request errors; the gate is that workers stay healthy
# after the injected transfer failures, not the score itself.
try:
metrics = run_eval(args)
print(f"Evaluation metrics: {metrics}")
@@ -14,84 +14,7 @@ from sglang.test.test_utils import (
try_cached_model,
)
register_cuda_ci(est_time=357, stage="base-c", runner_config="8-gpu-h20")
class TestDisaggregationPrefillPPAccuracy(PDDisaggregationServerBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.model = try_cached_model(DEFAULT_MODEL_NAME_FOR_TEST)
# Non blocking start servers
cls.start_prefill()
cls.start_decode()
# Block until both
cls.wait_server_ready(cls.prefill_url + "/health", process=cls.process_prefill)
cls.wait_server_ready(cls.decode_url + "/health", process=cls.process_decode)
cls.launch_lb()
@classmethod
def start_prefill(cls):
prefill_args = [
"--trust-remote-code",
"--disaggregation-mode",
"prefill",
"--disaggregation-bootstrap-port",
cls.bootstrap_port,
"--tp-size",
"2",
"--pp-size",
"2",
"--disable-overlap-schedule",
]
prefill_args += cls.transfer_backend + cls.rdma_devices
cls.process_prefill = popen_launch_pd_server(
cls.model,
cls.prefill_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=prefill_args,
)
@classmethod
def start_decode(cls):
decode_args = [
"--trust-remote-code",
"--disaggregation-mode",
"decode",
"--disaggregation-bootstrap-port",
cls.bootstrap_port,
"--tp-size",
"2",
"--base-gpu-id",
"4",
]
decode_args += cls.transfer_backend + cls.rdma_devices
cls.process_decode = popen_launch_pd_server(
cls.model,
cls.decode_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=decode_args,
)
def test_gsm8k(self):
args = SimpleNamespace(
base_url=self.base_url,
model=self.model,
eval_name="gsm8k",
api="completion",
max_tokens=512,
num_examples=200,
num_threads=128,
)
metrics = run_eval(args)
print(f"{metrics=}")
self.assertGreater(metrics["score"], 0.24)
# Wait a little bit so that the memory check happens.
time.sleep(5)
register_cuda_ci(est_time=250, stage="base-c", runner_config="8-gpu-h20")
class TestDisaggregationPrefillPPDynamicChunkAccuracy(PDDisaggregationServerBase):
@@ -1,16 +1,17 @@
"""DPBudget + DataParallelController dispatch tests.
`total_tokens` (the most complex algorithm) is exercised end-to-end in
test/registered/disaggregation/test_disaggregation_dp_attention.py; its
tie-break on `total_requests` transitively covers that state.
The e2e counterpart, over the real scheduler load-report path, is
test/registered/disaggregation/test_disaggregation_dp_attention.py.
Fragility: scheduler tests bypass `DataParallelController.__init__` via
`__new__` and inject only the attrs the schedulers read (`workers`, `status`,
`_active_workers`, `round_robin_counter`, `dp_budget`). Update `_make_controller`
if a scheduler starts reading another attr. `maybe_external_dp_rank_routing`
is exercised as the real method, no mock.
is exercised as the real method, no mock. The refresh-throttle tests inject
`load_snapshot_reader` and `_last_refresh_time` on top of those.
"""
import time
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock
@@ -279,5 +280,77 @@ class TestStatusAwarenessInconsistency(CustomTestCase):
ctl.workers[2].send_pyobj.assert_called_once()
class TestRefreshLoadBudgetThrottle(CustomTestCase):
@staticmethod
def _controller_with_reader(dp_size, snapshots):
ctl = _make_controller(dp_size)
ctl.load_snapshot_reader = MagicMock()
ctl.load_snapshot_reader.read_all.return_value = snapshots
return ctl
def test_throttled_refresh_spreads_a_burst_across_ranks(self):
idle = [_load(dp_rank=i, timestamp=1.0, num_total_tokens=0) for i in range(4)]
ctl = self._controller_with_reader(dp_size=4, snapshots=idle)
# A refresh stamp in the future keeps every call inside the window, so
# the burst runs entirely on speculative counters.
ctl._last_refresh_time = time.perf_counter() + 3600.0
for _ in range(8):
ctl.refresh_load_budget()
ctl.total_tokens_scheduler(_req(input_ids=[0] * 100))
ctl.load_snapshot_reader.read_all.assert_not_called()
self.assertEqual(
ctl.dp_budget.total_tokens,
[200, 200, 200, 200],
"speculative increments should spread the burst evenly",
)
for i, worker in enumerate(ctl.workers):
self.assertEqual(
worker.send_pyobj.call_count, 2, f"worker {i} should get 2 of 8 reqs"
)
def test_refresh_outside_window_overwrites_speculative_increments(self):
reported = [
_load(dp_rank=0, timestamp=2.0, num_total_tokens=10),
_load(dp_rank=1, timestamp=2.0, num_total_tokens=20),
]
ctl = self._controller_with_reader(dp_size=2, snapshots=reported)
ctl._last_refresh_time = 0.0 # window has long passed
ctl.dp_budget.total_tokens = [999, 999]
ctl.refresh_load_budget()
ctl.load_snapshot_reader.read_all.assert_called_once()
self.assertEqual(
ctl.dp_budget.total_tokens,
[10, 20],
"a fresh snapshot must replace the speculative state",
)
self.assertGreater(ctl._last_refresh_time, 0.0)
def test_unchanged_snapshot_does_not_reset_the_burst(self):
frozen = [_load(dp_rank=i, timestamp=1.0, num_total_tokens=0) for i in range(2)]
ctl = self._controller_with_reader(dp_size=2, snapshots=frozen)
ctl._last_refresh_time = 0.0
ctl.refresh_load_budget() # adopts timestamp 1.0
for _ in range(4):
ctl.total_tokens_scheduler(_req(input_ids=[0] * 50))
after_burst = list(ctl.dp_budget.total_tokens)
ctl._last_refresh_time = 0.0 # let the next refresh through the throttle
ctl.refresh_load_budget() # same timestamp -> update_budget skips it
self.assertEqual(
after_burst, [100, 100], "burst should have spread over both ranks"
)
self.assertEqual(
ctl.dp_budget.total_tokens,
after_burst,
"a stale-timestamp snapshot must not wipe the speculative state",
)
if __name__ == "__main__":
unittest.main()