Fix ut module importing (#22176)

This commit is contained in:
Ke Bao
2026-04-06 11:53:58 +08:00
committed by GitHub
parent e835601fb7
commit c4240218cb
3 changed files with 81 additions and 898 deletions
@@ -1,837 +0,0 @@
"""Unit tests for req_time_stats.py — no server, no model loading."""
# ── Lightweight stubs for heavy transitive deps (torch, distributed, etc.) ──
# req_time_stats.py imports from disaggregation, model_executor, and other
# modules that transitively pull in torch/CUDA. We pre-populate sys.modules
# with minimal stubs so the module loads in a CPU-only environment.
import os
import sys
import types
from dataclasses import dataclass
from enum import Enum, IntEnum, auto
def _ensure_module(name):
if name not in sys.modules:
sys.modules[name] = types.ModuleType(name)
return sys.modules[name]
# Mirrors sglang.srt.disaggregation.utils.DisaggregationMode (stubbed to avoid torch dep).
class _DisaggregationMode(Enum):
NULL = "null"
PREFILL = "prefill"
DECODE = "decode"
def _kv_to_page_num(n, page_size):
return (n + page_size - 1) // page_size
_ensure_module("sglang.srt.disaggregation")
_du = _ensure_module("sglang.srt.disaggregation.utils")
_du.DisaggregationMode = _DisaggregationMode
_du.kv_to_page_num = _kv_to_page_num
# Mirrors sglang.srt.model_executor.forward_batch_info.ForwardMode (stubbed to avoid torch dep).
class _ForwardMode(IntEnum):
EXTEND = auto()
DECODE = auto()
MIXED = auto()
IDLE = auto()
TARGET_VERIFY = auto()
DRAFT_EXTEND = auto()
DRAFT_EXTEND_V2 = auto()
PREBUILT = auto()
SPLIT_PREFILL = auto()
DLLM_EXTEND = auto()
def is_decode(self):
return self == _ForwardMode.DECODE
def is_prefill(self):
return self in (
_ForwardMode.EXTEND,
_ForwardMode.MIXED,
_ForwardMode.DRAFT_EXTEND,
_ForwardMode.TARGET_VERIFY,
_ForwardMode.SPLIT_PREFILL,
)
def is_prebuilt(self):
return self == _ForwardMode.PREBUILT
_ensure_module("sglang.srt.model_executor")
_fbi = _ensure_module("sglang.srt.model_executor.forward_batch_info")
_fbi.ForwardMode = _ForwardMode
# -- sglang.srt.observability.metrics_collector --
_mc = _ensure_module("sglang.srt.observability.metrics_collector")
_mc.SchedulerMetricsCollector = type("SchedulerMetricsCollector", (), {})
_mc.TokenizerMetricsCollector = type("TokenizerMetricsCollector", (), {})
# -- sglang.srt.observability.trace --
@dataclass
class _TraceNullContext:
tracing_enable: bool = False
def __getattr__(self, name):
return self
def __call__(self, *args, **kwargs):
return self
# -- sglang.srt.utils --
# Stub both get_bool_env_var (for req_time_stats) and get_int_env_var (for trace.py)
# so the real trace module can load without torch.
def _get_bool_env_var(name, default="false"):
return os.getenv(name, default).lower() in ("true", "1")
def _get_int_env_var(name, default=0):
return int(os.getenv(name, str(default)))
_su = _ensure_module("sglang.srt.utils")
_su.get_bool_env_var = _get_bool_env_var
if not hasattr(_su, "get_int_env_var"):
_su.get_int_env_var = _get_int_env_var
_ensure_module("sglang.srt.utils.common")
# ── End stubs ────────────────────────────────────────────────────────
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="stage-a-test-cpu")
import unittest
from unittest.mock import MagicMock
import sglang.srt.observability.req_time_stats as rts_module
import sglang.srt.observability.trace as trace_module
from sglang.srt.observability.req_time_stats import (
APIServerReqTimeStats,
DPControllerReqTimeStats,
ReqTimeStatsBase,
RequestStage,
RequestStageConfig,
SchedulerReqTimeStats,
calibrate_time_diff,
convert_time_cross_thread,
convert_time_to_realtime,
convert_time_to_realtime_ns,
monotonic_time,
real_time,
set_schedule_time_batch,
set_time_batch,
)
from sglang.srt.observability.trace import SpanAttributes
DisaggregationMode = _DisaggregationMode
ForwardMode = _ForwardMode
class TestUtilityFunctions(unittest.TestCase):
def test_real_time_and_monotonic_time(self):
self.assertGreater(real_time(), 0)
self.assertGreater(monotonic_time(), 0)
def test_convert_time_to_realtime(self):
result = convert_time_to_realtime(100.0)
self.assertIsInstance(result, float)
def test_convert_time_to_realtime_ns(self):
result = convert_time_to_realtime_ns(100.0)
self.assertIsInstance(result, int)
self.assertGreater(result, 0)
def test_convert_time_cross_thread(self):
self.assertAlmostEqual(
convert_time_cross_thread(10.0, old_diff=5.0, new_diff=3.0), 12.0
)
def test_calibrate_time_diff(self):
calibrate_time_diff() # should not raise
class TestRequestStage(unittest.TestCase):
def test_stage_config_defaults(self):
cfg = RequestStageConfig("test_stage")
self.assertEqual(cfg.stage_name, "test_stage")
self.assertEqual(cfg.level, 0)
self.assertFalse(cfg.metrics_is_observed)
def test_predefined_stages(self):
self.assertEqual(RequestStage.TOKENIZE.stage_name, "tokenize")
self.assertTrue(RequestStage.PREFILL_FORWARD.metrics_is_observed)
self.assertFalse(RequestStage.PREFILL_WAITING.metrics_is_observed)
self.assertEqual(RequestStage.ANONYMOUS.stage_name, "")
class TestReqTimeStatsBase(unittest.TestCase):
def test_disagg_mode_str(self):
base = ReqTimeStatsBase()
base.disagg_mode = DisaggregationMode.NULL
self.assertEqual(base.disagg_mode_str(), "unified")
base.disagg_mode = DisaggregationMode.DECODE
self.assertEqual(base.disagg_mode_str(), "decode")
base.disagg_mode = DisaggregationMode.PREFILL
self.assertEqual(base.disagg_mode_str(), "prefill")
def test_set_metrics_collector(self):
base = ReqTimeStatsBase()
collector = MagicMock()
base.set_metrics_collector(collector)
self.assertTrue(base.enable_metrics)
self.assertIs(base.metrics_collector, collector)
def test_set_metrics_collector_falsy(self):
base = ReqTimeStatsBase()
base.set_metrics_collector(None)
self.assertFalse(base.enable_metrics)
def test_observe_per_stage_req_latency(self):
base = ReqTimeStatsBase()
collector = MagicMock()
base.set_metrics_collector(collector)
stage = RequestStageConfig("test", metrics_is_observed=True)
base.observe_per_stage_req_latency(stage, 1.5)
collector.observe_per_stage_req_latency.assert_called_once_with("test", 1.5)
def test_observe_per_stage_not_observed(self):
base = ReqTimeStatsBase()
collector = MagicMock()
base.set_metrics_collector(collector)
stage = RequestStageConfig("test", metrics_is_observed=False)
base.observe_per_stage_req_latency(stage, 1.0)
collector.observe_per_stage_req_latency.assert_not_called()
def test_init_trace_ctx(self):
base = ReqTimeStatsBase()
base.init_trace_ctx("rid-1", bootstrap_room=None)
# TraceReqContext stub has tracing_enable=False → replaced by TraceNullContext
self.assertFalse(base.trace_ctx.tracing_enable)
def test_trace_slice_noop_when_tracing_disabled(self):
base = ReqTimeStatsBase()
base.trace_slice(RequestStage.TOKENIZE, 0.0, 1.0)
def test_trace_slice_when_tracing_enabled(self):
base = ReqTimeStatsBase()
base.trace_ctx = MagicMock()
base.trace_ctx.tracing_enable = True
base.trace_slice(RequestStage.TOKENIZE, 0.0, 1.0, {"key": "val"})
base.trace_ctx.trace_slice.assert_called_once()
def test_new_from_obj_none(self):
obj = ReqTimeStatsBase.new_from_obj(None)
self.assertIsInstance(obj, ReqTimeStatsBase)
def test_new_from_obj_copy(self):
src = ReqTimeStatsBase()
src.disagg_mode = DisaggregationMode.PREFILL
src.enable_metrics = True
dst = ReqTimeStatsBase.new_from_obj(src)
self.assertEqual(dst.disagg_mode, DisaggregationMode.PREFILL)
self.assertTrue(dst.enable_metrics)
def test_getstate(self):
base = ReqTimeStatsBase()
base.disagg_mode = DisaggregationMode.DECODE
state = base.__getstate__()
self.assertEqual(state["disagg_mode"], DisaggregationMode.DECODE)
self.assertFalse(state["enable_metrics"])
def test_setstate_converts_time_fields(self):
base = ReqTimeStatsBase()
state = {
"created_time": 100.0,
"diff_realtime_monotonic": 50.0,
"disagg_mode": DisaggregationMode.NULL,
}
base.__setstate__(state)
self.assertEqual(base.disagg_mode, DisaggregationMode.NULL)
# created_time ends with "time" → converted via convert_time_cross_thread
expected = convert_time_cross_thread(
100.0, 50.0, rts_module.global_diff_realtime_monotonic
)
self.assertAlmostEqual(base.created_time, expected)
class TestAPIServerReqTimeStats(unittest.TestCase):
def test_setters_auto_timestamp(self):
"""All setters work with default ts=None (auto perf_counter)."""
s = APIServerReqTimeStats()
s.set_created_time()
s.set_tokenize_finish_time()
s.set_api_server_dispatch_time()
s.set_api_server_dispatch_finish_time()
s.set_first_token_time()
s.set_last_time()
s.set_response_sent_to_client_time()
s.set_finished_time()
self.assertGreater(s.created_time, 0)
self.assertGreater(s.finished_time, 0)
def test_getters(self):
s = APIServerReqTimeStats()
s.set_created_time(1.0)
s.set_first_token_time(3.0)
s.set_finished_time(5.0)
self.assertAlmostEqual(s.get_first_token_latency(), 2.0)
self.assertAlmostEqual(s.get_e2e_latency(), 4.0)
self.assertAlmostEqual(s.get_decode_latency(), 2.0)
def test_get_interval(self):
s = APIServerReqTimeStats()
s.set_first_token_time(monotonic_time())
self.assertGreaterEqual(s.get_interval(), 0.0)
def test_getstate(self):
s = APIServerReqTimeStats(disagg_mode=DisaggregationMode.NULL)
state = s.__getstate__()
self.assertIn("disagg_mode", state)
self.assertFalse(state["enable_metrics"])
def test_get_response_sent_to_client_realtime(self):
s = APIServerReqTimeStats()
s.set_response_sent_to_client_time(100.0)
result = s.get_response_sent_to_client_realtime()
self.assertIsInstance(result, float)
def test_convert_to_output_meta_info(self):
s = APIServerReqTimeStats()
s.set_created_time(1.0)
s.set_api_server_dispatch_finish_time(2.0)
s.set_first_token_time(3.0)
s.set_response_sent_to_client_time(4.0)
s.set_finished_time(5.0)
meta = s.convert_to_output_meta_info(completion_tokens=10)
self.assertIn("request_received_ts", meta)
self.assertIn("api_server_dispatch_finish_ts", meta)
self.assertIn("response_sent_to_client_ts", meta)
self.assertIn("request_finished_ts", meta)
self.assertIn("decode_throughput", meta)
def test_convert_to_output_meta_info_with_scheduler_stats(self):
s = APIServerReqTimeStats()
s.set_created_time(1.0)
s.set_first_token_time(3.0)
s.set_finished_time(5.0)
sched = MagicMock()
sched.forward_entry_time = 2.0
meta = s.convert_to_output_meta_info(
scheduler_time_stats=sched, completion_tokens=10
)
self.assertIn("inference_time", meta)
self.assertAlmostEqual(meta["inference_time"], 3.0)
def test_convert_to_output_meta_info_empty(self):
"""No timestamps set → minimal meta_info."""
s = APIServerReqTimeStats()
meta = s.convert_to_output_meta_info()
self.assertNotIn("request_received_ts", meta)
self.assertNotIn("decode_throughput", meta)
def test_convert_to_gen_ai_span_attrs(self):
s = APIServerReqTimeStats()
s.set_created_time(1.0)
s.set_first_token_time(3.0)
s.set_api_server_dispatch_finish_time(2.0)
s.set_finished_time(5.0)
attrs = s.convert_to_gen_ai_span_attrs()
self.assertAlmostEqual(
attrs[SpanAttributes.GEN_AI_LATENCY_TIME_TO_FIRST_TOKEN], 2.0
)
self.assertAlmostEqual(attrs[SpanAttributes.GEN_AI_LATENCY_E2E], 4.0)
self.assertAlmostEqual(
attrs[SpanAttributes.GEN_AI_LATENCY_TIME_IN_MODEL_DECODE], 2.0
)
self.assertAlmostEqual(
attrs[SpanAttributes.GEN_AI_LATENCY_TIME_IN_MODEL_INFERENCE], 3.0
)
self.assertAlmostEqual(
attrs[SpanAttributes.GEN_AI_LATENCY_TIME_IN_MODEL_PREFILL], 1.0
)
def test_convert_to_gen_ai_span_attrs_empty(self):
s = APIServerReqTimeStats()
attrs = s.convert_to_gen_ai_span_attrs()
self.assertEqual(len(attrs), 0)
class TestDPControllerReqTimeStats(unittest.TestCase):
def test_setters(self):
s = DPControllerReqTimeStats()
s.set_dp_dispatch_time()
s.set_dp_dispatch_finish_time()
self.assertGreater(s.dc_dispatch_time, 0)
self.assertGreater(s.dc_dispatch_finish_time, 0)
def test_setters_explicit(self):
s = DPControllerReqTimeStats()
s.set_dp_dispatch_time(10.0)
s.set_dp_dispatch_finish_time(20.0)
self.assertEqual(s.dc_dispatch_time, 10.0)
self.assertEqual(s.dc_dispatch_finish_time, 20.0)
def test_getstate(self):
s = DPControllerReqTimeStats(disagg_mode=DisaggregationMode.NULL)
state = s.__getstate__()
self.assertIn("disagg_mode", state)
class TestSchedulerReqTimeStats(unittest.TestCase):
def _make_stats(self, **kwargs):
defaults = dict(disagg_mode=DisaggregationMode.NULL)
defaults.update(kwargs)
return SchedulerReqTimeStats(**defaults)
def _make_enabled_stats(self, **kwargs):
s = self._make_stats(**kwargs)
s.set_metrics_collector(MagicMock())
return s
def test_getstate_metrics_disabled(self):
s = self._make_stats()
self.assertEqual(s.__getstate__(), {})
def test_getstate_metrics_enabled(self):
s = self._make_enabled_stats()
s.wait_queue_entry_time = 1.0
s.forward_entry_time = 2.0
state = s.__getstate__()
self.assertIn("wait_queue_entry_time", state)
self.assertIn("forward_entry_time", state)
def test_set_scheduler_recv_time(self):
s = self._make_stats()
s.set_scheduler_recv_time()
self.assertGreater(s.scheduler_recv_time, 0)
def test_set_prefill_run_batch_times(self):
s = self._make_stats()
s.set_prefill_run_batch_start_time(1.0)
s.set_prefill_run_batch_end_time(2.0)
self.assertEqual(s.prefill_run_batch_start_time, 1.0)
self.assertEqual(s.prefill_run_batch_end_time, 2.0)
def test_set_quick_finish_time(self):
s = self._make_stats()
s.set_quick_finish_time(5.0)
self.assertEqual(s.completion_time, 5.0)
self.assertEqual(s.forward_entry_time, 5.0)
def test_set_bootstrap_done_time(self):
s = self._make_stats()
s.set_bootstrap_done_time(1.0)
self.assertEqual(s.bootstrap_done_time, 1.0)
# Second call does not overwrite
s.set_bootstrap_done_time(2.0)
self.assertEqual(s.bootstrap_done_time, 1.0)
def test_set_completion_time(self):
s = self._make_stats()
s.set_completion_time(10.0)
self.assertEqual(s.completion_time, 10.0)
def test_set_prefill_transfer_queue_entry_time(self):
s = self._make_stats()
s.set_prefill_transfer_queue_entry_time(1.0)
self.assertEqual(s.prefill_transfer_queue_entry_time, 1.0)
def test_set_prefill_kv_transfer_finish_time(self):
s = self._make_stats()
s.prefill_transfer_queue_entry_time = 1.0
s.set_prefill_kv_transfer_finish_time(3.0)
self.assertEqual(s.prefill_kv_transfer_finish_time, 3.0)
def test_set_decode_prealloc_queue_entry_time(self):
s = self._make_stats()
s.scheduler_recv_time = 1.0
s.set_decode_prealloc_queue_entry_time(2.0)
self.assertEqual(s.decode_prealloc_queue_entry_time, 2.0)
def test_set_decode_transfer_queue_entry_time(self):
s = self._make_stats()
s.decode_prealloc_queue_entry_time = 1.0
s.set_decode_transfer_queue_entry_time(2.0)
self.assertEqual(s.decode_transfer_queue_entry_time, 2.0)
def test_set_decode_prebuilt_finish_time(self):
s = self._make_stats()
s.last_forward_entry_time = 1.0
s.set_decode_prebuilt_finish_time(3.0)
self.assertEqual(s.decode_prebuilt_finish_time, 3.0)
def test_set_prefill_bootstrap_queue_entry_time(self):
s = self._make_stats()
s.scheduler_recv_time = 1.0
s.set_prefill_bootstrap_queue_entry_time(2.0)
self.assertEqual(s.prefill_bootstrap_queue_entry_time, 2.0)
def test_auto_timestamp_setters(self):
"""All setters default to perf_counter() when called without args."""
s = self._make_stats()
s.set_scheduler_recv_time()
s.set_prefill_run_batch_start_time()
s.set_prefill_run_batch_end_time()
s.set_prefill_transfer_queue_entry_time()
s.set_prefill_kv_transfer_finish_time()
s.set_decode_prealloc_queue_entry_time()
s.set_decode_transfer_queue_entry_time()
s.set_bootstrap_done_time()
s.set_decode_prebuilt_finish_time()
s.set_completion_time()
s.set_quick_finish_time()
s.set_retract_time()
s.set_wait_queue_entry_time()
s.set_forward_entry_time()
s.set_last_chunked_prefill_finish_time()
s.set_prefill_finished_time()
s.set_last_decode_finish_time()
s.set_prefill_bootstrap_queue_entry_time()
s.set_last_scheduled_time(ForwardMode.DECODE)
self.assertGreater(s.scheduler_recv_time, 0)
self.assertGreater(s.completion_time, 0)
def test_set_retract_time(self):
s = self._make_stats()
s.last_forward_entry_time = 1.0
s.last_prefill_finished_time = 2.0
s.set_retract_time(5.0)
self.assertEqual(s.last_forward_entry_time, 0.0)
self.assertEqual(s.last_prefill_finished_time, 0.0)
def test_set_wait_queue_entry_time_first_call_null(self):
s = self._make_enabled_stats()
s.scheduler_recv_time = 1.0
s.set_wait_queue_entry_time(3.0)
self.assertEqual(s.wait_queue_entry_time, 3.0)
s.metrics_collector.observe_per_stage_req_latency.assert_called()
def test_set_wait_queue_entry_time_first_call_prefill(self):
s = self._make_enabled_stats(disagg_mode=DisaggregationMode.PREFILL)
s.prefill_bootstrap_queue_entry_time = 1.0
s.set_wait_queue_entry_time(3.0)
self.assertEqual(s.wait_queue_entry_time, 3.0)
def test_set_wait_queue_entry_time_first_call_decode(self):
s = self._make_enabled_stats(disagg_mode=DisaggregationMode.DECODE)
s.decode_transfer_queue_entry_time = 1.0
s.set_wait_queue_entry_time(3.0)
self.assertEqual(s.wait_queue_entry_time, 3.0)
def test_set_wait_queue_entry_time_retract(self):
s = self._make_stats()
s.wait_queue_entry_time = 1.0 # already set
s.set_wait_queue_entry_time(5.0)
self.assertEqual(s.wait_queue_entry_time, 5.0)
# retract resets these
self.assertEqual(s.last_forward_entry_time, 0.0)
def test_set_forward_entry_time_first_call(self):
s = self._make_enabled_stats()
s.wait_queue_entry_time = 1.0
s.set_forward_entry_time(3.0)
self.assertEqual(s.forward_entry_time, 3.0)
self.assertEqual(s.last_forward_entry_time, 3.0)
s.metrics_collector.observe_queue_time.assert_called_once()
def test_set_forward_entry_time_first_call_decode(self):
s = self._make_enabled_stats(disagg_mode=DisaggregationMode.DECODE)
s.wait_queue_entry_time = 1.0
s.set_forward_entry_time(3.0)
self.assertEqual(s.forward_entry_time, 3.0)
def test_set_forward_entry_time_retract(self):
s = self._make_stats()
s.forward_entry_time = 1.0 # already set
s.last_forward_entry_time = 0.0 # reset by retract
s.set_forward_entry_time(5.0)
self.assertEqual(s.last_forward_entry_time, 5.0)
def test_set_last_chunked_prefill_finish_time_first(self):
s = self._make_stats()
s.last_forward_entry_time = 1.0
s.set_last_chunked_prefill_finish_time(3.0)
self.assertEqual(s.last_chunked_prefill_finish_time, 3.0)
def test_set_last_chunked_prefill_finish_time_subsequent(self):
s = self._make_stats()
s.last_forward_entry_time = 1.0
s.last_chunked_prefill_finish_time = 2.0
s.set_last_chunked_prefill_finish_time(4.0)
self.assertEqual(s.last_chunked_prefill_finish_time, 4.0)
def test_set_prefill_finished_time_first_call(self):
s = self._make_enabled_stats()
s.last_forward_entry_time = 1.0
s.set_prefill_finished_time(3.0)
self.assertEqual(s.prefill_finished_time, 3.0)
self.assertEqual(s.last_prefill_finished_time, 3.0)
def test_set_prefill_finished_time_retract(self):
s = self._make_stats()
s.prefill_finished_time = 1.0 # already set
s.last_prefill_finished_time = 0.0 # reset by retract
s.last_forward_entry_time = 0.5
s.set_prefill_finished_time(5.0)
self.assertEqual(s.last_prefill_finished_time, 5.0)
def test_set_prefill_finished_time_retract_with_chunked(self):
s = self._make_stats()
s.prefill_finished_time = 1.0
s.last_prefill_finished_time = 0.0
s.last_chunked_prefill_finish_time = 2.0
s.set_prefill_finished_time(5.0)
self.assertEqual(s.last_prefill_finished_time, 5.0)
def test_set_prefill_finished_time_tracing_enabled(self):
s = self._make_enabled_stats()
s.trace_ctx = MagicMock()
s.trace_ctx.tracing_enable = True
s.last_forward_entry_time = 1.0
s.last_chunked_prefill_finish_time = 2.0
s.last_decode_scheduled_time = 1.5
s.set_prefill_finished_time(3.0)
self.assertEqual(s.prefill_finished_time, 3.0)
s.trace_ctx.trace_slice_end.assert_called_once()
# NULL mode + last_decode_scheduled_time > 0 → trace_slice_start for decode
s.trace_ctx.trace_slice_start.assert_called_once()
def test_set_last_decode_finish_time_first(self):
s = self._make_enabled_stats()
s.last_prefill_finished_time = 1.0
s.last_decode_scheduled_time = 0.5
s.set_last_decode_finish_time(3.0)
self.assertEqual(s.last_decode_finish_time, 3.0)
self.assertEqual(s.decode_ct, 1)
def test_set_last_decode_finish_time_first_decode_mode(self):
s = self._make_enabled_stats(disagg_mode=DisaggregationMode.DECODE)
s.decode_prebuilt_finish_time = 1.0
s.set_last_decode_finish_time(3.0)
self.assertEqual(s.decode_ct, 1)
def test_set_last_decode_finish_time_first_with_scheduled(self):
s = self._make_enabled_stats()
s.last_prefill_finished_time = 1.0
s.last_decode_scheduled_time = 2.0
s.set_last_decode_finish_time(3.0)
self.assertEqual(s.decode_ct, 1)
def test_set_last_decode_finish_time_subsequent(self):
s = self._make_enabled_stats()
s.last_decode_finish_time = 1.0
s.set_last_decode_finish_time(3.0)
self.assertEqual(s.decode_ct, 1)
def test_set_last_scheduled_time_decode(self):
s = self._make_stats()
s.set_last_scheduled_time(ForwardMode.DECODE, 5.0)
self.assertEqual(s.last_decode_scheduled_time, 5.0)
def test_set_last_scheduled_time_extend(self):
s = self._make_stats()
s.set_last_scheduled_time(ForwardMode.EXTEND, 5.0)
self.assertEqual(s.last_decode_scheduled_time, 0.0) # not decode
def test_set_last_scheduled_time_tracing_enabled(self):
s = self._make_stats()
s.trace_ctx = MagicMock()
s.trace_ctx.tracing_enable = True
s.last_prefill_finished_time = 1.0
s.set_last_scheduled_time(ForwardMode.DECODE, 5.0)
s.trace_ctx.trace_event.assert_called_once()
# NULL mode + first decode + last_prefill_finished_time > 0 → trace decode waiting
self.assertEqual(s.last_decode_scheduled_time, 5.0)
def test_get_queueing_time(self):
s = self._make_stats()
s.forward_entry_time = 5.0
s.wait_queue_entry_time = 2.0
self.assertAlmostEqual(s.get_queueing_time(), 3.0)
def test_get_prefill_waiting_latency(self):
s = self._make_stats()
self.assertIsNone(s.get_prefill_waiting_latency())
s.prefill_run_batch_start_time = 3.0
s.forward_entry_time = 1.0
self.assertAlmostEqual(s.get_prefill_waiting_latency(), 2.0)
def test_get_prefill_launch_latency(self):
s = self._make_stats()
self.assertIsNone(s.get_prefill_launch_latency())
s.prefill_run_batch_start_time = 1.0
s.prefill_run_batch_end_time = 3.0
self.assertAlmostEqual(s.get_prefill_launch_latency(), 2.0)
def test_format_duration(self):
s = self._make_stats()
self.assertEqual(s.format_duration(0.001), "1.00ms")
def test_convert_to_duration_null(self):
s = self._make_stats(
wait_queue_entry_time=1.0,
forward_entry_time=2.0,
completion_time=5.0,
)
result = s.convert_to_duration()
self.assertIn("queue_duration=", result)
self.assertIn("forward_duration=", result)
def test_convert_to_duration_prefill_no_bootstrap(self):
s = self._make_stats(
disagg_mode=DisaggregationMode.PREFILL,
prefill_bootstrap_queue_entry_time=1.0,
wait_queue_entry_time=2.0,
forward_entry_time=3.0,
completion_time=5.0,
)
result = s.convert_to_duration()
self.assertIn("bootstrap_queue_duration", result)
self.assertNotIn("alloc_wait", result)
def test_convert_to_duration_prefill_with_bootstrap(self):
s = self._make_stats(
disagg_mode=DisaggregationMode.PREFILL,
prefill_bootstrap_queue_entry_time=1.0,
bootstrap_done_time=1.5,
wait_queue_entry_time=2.0,
forward_entry_time=3.0,
completion_time=5.0,
)
result = s.convert_to_duration()
self.assertIn("bootstrap(", result)
self.assertIn("alloc_wait(", result)
def test_convert_to_duration_decode_no_bootstrap(self):
s = self._make_stats(
disagg_mode=DisaggregationMode.DECODE,
decode_prealloc_queue_entry_time=1.0,
decode_transfer_queue_entry_time=2.0,
wait_queue_entry_time=3.0,
forward_entry_time=4.0,
completion_time=6.0,
)
result = s.convert_to_duration()
self.assertIn("prealloc_queue_duration", result)
self.assertIn("transfer_duration=", result)
self.assertNotIn("alloc_wait", result)
def test_convert_to_duration_decode_with_bootstrap(self):
s = self._make_stats(
disagg_mode=DisaggregationMode.DECODE,
decode_prealloc_queue_entry_time=1.0,
bootstrap_done_time=1.5,
decode_transfer_queue_entry_time=2.0,
wait_queue_entry_time=3.0,
forward_entry_time=4.0,
completion_time=6.0,
)
result = s.convert_to_duration()
self.assertIn("bootstrap(", result)
self.assertIn("alloc_wait(", result)
def test_convert_to_duration_unknown_mode(self):
s = self._make_stats()
# Force an invalid disagg_mode to hit the else branch
s.disagg_mode = "invalid"
self.assertEqual(s.convert_to_duration(), "Unknown Time Stats")
def test_convert_to_output_meta_info(self):
s = self._make_stats(
forward_entry_time=2.0,
prefill_finished_time=3.0,
wait_queue_entry_time=1.0,
prefill_run_batch_start_time=2.5,
prefill_run_batch_end_time=2.8,
)
meta = s.convert_to_output_meta_info()
self.assertIn("forward_entry_time", meta)
self.assertIn("prefill_finished_time", meta)
self.assertIn("queue_time", meta)
self.assertIn("prefill_waiting_latency", meta)
self.assertIn("prefill_launch_latency", meta)
def test_compute_kv_transfer_metrics(self):
s = self._make_enabled_stats()
s.prefill_transfer_queue_entry_time = 1.0
s.completion_time = 2.0
result = s.compute_and_observe_kv_transfer_metrics(
num_tokens=10, page_size=4, bytes_per_page_all_layers=1024
)
self.assertIn("latency_ms", result)
self.assertIn("total_mb", result)
self.assertIn("speed_gb_s", result)
s.metrics_collector.observe_kv_transfer_metrics.assert_called_once()
def test_compute_kv_transfer_with_bootstrap(self):
s = self._make_enabled_stats()
s.prefill_transfer_queue_entry_time = 1.0
s.completion_time = 2.0
s.prefill_bootstrap_queue_entry_time = 0.5
s.bootstrap_done_time = 0.8
s.wait_queue_entry_time = 1.0
result = s.compute_and_observe_kv_transfer_metrics(
num_tokens=10, page_size=4, bytes_per_page_all_layers=1024
)
self.assertIn("bootstrap_ms", result)
self.assertIn("alloc_ms", result)
s.metrics_collector.observe_kv_transfer_bootstrap.assert_called_once()
def test_compute_kv_transfer_metrics_none(self):
s = self._make_stats()
result = s.compute_and_observe_kv_transfer_metrics(
num_tokens=10, page_size=4, bytes_per_page_all_layers=1024
)
self.assertIsNone(result)
class TestBatchFunctions(unittest.TestCase):
def test_set_time_batch_empty(self):
set_time_batch(None, "set_completion_time")
set_time_batch([], "set_completion_time")
def test_set_time_batch(self):
req = MagicMock()
set_time_batch([req], "set_completion_time")
req.time_stats.set_completion_time.assert_called_once()
def test_set_schedule_time_batch_tracing_disabled(self):
batch = MagicMock()
set_schedule_time_batch(batch)
# Tracing is disabled in our stub → returns early
def test_set_schedule_time_batch_tracing_enabled(self):
orig = trace_module.get_global_tracing_enabled
trace_module.get_global_tracing_enabled = lambda: True
rts_module.get_global_tracing_enabled = lambda: True
try:
req = MagicMock()
batch = MagicMock()
batch.reqs = [req]
batch.forward_mode = ForwardMode.DECODE
batch.forward_mode.is_decode = lambda: True
batch.forward_mode.is_prefill = lambda: False
batch.forward_mode.is_prebuilt = lambda: False
set_schedule_time_batch(batch)
req.time_stats.set_last_scheduled_time.assert_called_once()
finally:
trace_module.get_global_tracing_enabled = orig
rts_module.get_global_tracing_enabled = orig
if __name__ == "__main__":
unittest.main()
@@ -1,23 +1,31 @@
"""Unit tests for request_metrics_exporter.py — no server, no model loading.""" """Unit tests for request_metrics_exporter.py — no server, no model loading."""
# ── Lightweight stubs for heavy transitive deps ──
import sys
import types
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sglang.test.ci.ci_register import register_cpu_ci
def _ensure_module(name): register_cpu_ci(est_time=5, suite="stage-a-test-cpu")
if name not in sys.modules:
sys.modules[name] = types.ModuleType(name) import asyncio
return sys.modules[name] import json
import os
import shutil
import tempfile
import types
import unittest
from unittest.mock import MagicMock, patch
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
# ── Test helper classes (local only, never injected into sys.modules) ──
@dataclass @dataclass
class _GenerateReqInput: class _GenerateReqInput:
rid: Optional[str] = None rid: Optional[str] = None
text: Optional[str] = None text: Optional[str] = None
image_data: Optional[Any] = None # in ALWAYS_EXCLUDE_FIELDS image_data: Optional[Any] = None
sampling_params: Optional[Dict] = None sampling_params: Optional[Dict] = None
@@ -35,36 +43,74 @@ class _ServerArgs:
setattr(self, k, v) setattr(self, k, v)
# Pre-populate modules before importing the module under test. # ── Deferred import of the module-under-test ──
_ensure_module("sglang.srt.managers") # request_metrics_exporter.py imports io_struct and server_args at module level.
_io = _ensure_module("sglang.srt.managers.io_struct") # We use patch.dict to temporarily provide lightweight stubs so the import
_io.GenerateReqInput = _GenerateReqInput # succeeds without pulling in heavy transitive deps (torch, triton, …).
_io.EmbeddingReqInput = _EmbeddingReqInput # The patch is started in setUpModule and stopped in tearDownModule,
# so sys.modules is never modified during pytest collection.
_sa = _ensure_module("sglang.srt.server_args") _patcher = None
_sa.ServerArgs = _ServerArgs
# ── End stubs ── # Module-under-test symbols, populated by setUpModule
FileRequestMetricsExporter = None
RequestMetricsExporter = None
RequestMetricsExporterManager = None
create_request_metrics_exporters = None
_ConcreteExporter = None
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="stage-a-test-cpu") def setUpModule():
global _patcher
global FileRequestMetricsExporter, RequestMetricsExporter
global RequestMetricsExporterManager, create_request_metrics_exporters
global _ConcreteExporter
import asyncio stub_modules = {}
import json for name in (
import os "sglang.srt.managers",
import shutil "sglang.srt.managers.io_struct",
import tempfile "sglang.srt.server_args",
import unittest ):
from unittest.mock import MagicMock, patch if name not in __import__("sys").modules:
stub_modules[name] = types.ModuleType(name)
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX if stub_modules:
from sglang.srt.observability.request_metrics_exporter import ( if "sglang.srt.managers.io_struct" in stub_modules:
FileRequestMetricsExporter, stub_modules["sglang.srt.managers.io_struct"].GenerateReqInput = (
RequestMetricsExporter, _GenerateReqInput
RequestMetricsExporterManager, )
create_request_metrics_exporters, stub_modules["sglang.srt.managers.io_struct"].EmbeddingReqInput = (
) _EmbeddingReqInput
)
if "sglang.srt.server_args" in stub_modules:
stub_modules["sglang.srt.server_args"].ServerArgs = _ServerArgs
_patcher = patch.dict("sys.modules", stub_modules)
_patcher.start()
import sglang.srt.observability.request_metrics_exporter as _mod
FileRequestMetricsExporter = _mod.FileRequestMetricsExporter
RequestMetricsExporter = _mod.RequestMetricsExporter
RequestMetricsExporterManager = _mod.RequestMetricsExporterManager
create_request_metrics_exporters = _mod.create_request_metrics_exporters
class ConcreteExporter(RequestMetricsExporter):
"""Minimal concrete subclass for testing base class methods."""
async def write_record(self, obj, out_dict):
pass
_ConcreteExporter = ConcreteExporter
def tearDownModule():
if _patcher is not None:
_patcher.stop()
# ── Helpers ──
def _make_server_args(tmp_dir, enabled=True): def _make_server_args(tmp_dir, enabled=True):
@@ -74,13 +120,6 @@ def _make_server_args(tmp_dir, enabled=True):
) )
class _ConcreteExporter(RequestMetricsExporter):
"""Minimal concrete subclass for testing base class methods."""
async def write_record(self, obj, out_dict):
pass
class TestFormatOutputData(unittest.TestCase): class TestFormatOutputData(unittest.TestCase):
def test_basic_formatting(self): def test_basic_formatting(self):
server_args = _make_server_args("/tmp/unused") server_args = _make_server_args("/tmp/unused")
@@ -1,27 +1,6 @@
"""Unit tests for trace.py — no server, no model loading.""" """Unit tests for trace.py — no server, no model loading."""
# ── Stubs for heavy transitive deps ──
import os import os
import sys
import types
def _ensure_module(name):
if name not in sys.modules:
sys.modules[name] = types.ModuleType(name)
return sys.modules[name]
_su = _ensure_module("sglang.srt.utils")
if not hasattr(_su, "get_int_env_var"):
_su.get_int_env_var = lambda name, default=0: int(os.getenv(name, str(default)))
_ensure_module("sglang.srt.utils.common")
_ensure_module("sglang.srt.managers")
_sb = _ensure_module("sglang.srt.managers.schedule_batch")
_sb.BaseFinishReason = type("BaseFinishReason", (), {"to_json": lambda self: {}})
# ── End stubs ──
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
@@ -468,7 +447,9 @@ class TestTraceReqContextEnabled(unittest.TestCase):
def test_abort_with_base_finish_reason(self): def test_abort_with_base_finish_reason(self):
ctx = TraceReqContext(rid="req-1") ctx = TraceReqContext(rid="req-1")
ctx.trace_req_start(ts=1000) ctx.trace_req_start(ts=1000)
abort_obj = _sb.BaseFinishReason() from sglang.srt.managers.schedule_batch import FINISH_LENGTH
abort_obj = FINISH_LENGTH(length=10)
ctx.abort(ts=2000, abort_info=abort_obj) ctx.abort(ts=2000, abort_info=abort_obj)
self.assertIsNone(ctx.thread_context) self.assertIsNone(ctx.thread_context)