Refactor device timer, clean up metrics collector, and add fwd occupancy metric (#24197)

This commit is contained in:
Lianmin Zheng
2026-05-01 10:25:25 -07:00
committed by GitHub
parent 4a50cd781e
commit ece8a1a788
16 changed files with 711 additions and 574 deletions
-1
View File
@@ -12,7 +12,6 @@ SGLang supports various environment variables that can be used to configure its
| `SGLANG_HOST_IP` | Host IP address for the server | `0.0.0.0` | | `SGLANG_HOST_IP` | Host IP address for the server | `0.0.0.0` |
| `SGLANG_PORT` | Port for the server | auto-detected | | `SGLANG_PORT` | Port for the server | auto-detected |
| `SGLANG_LOGGING_CONFIG_PATH` | Custom logging configuration path | Not set | | `SGLANG_LOGGING_CONFIG_PATH` | Custom logging configuration path | Not set |
| `SGLANG_DISABLE_REQUEST_LOGGING` | Disable request logging | `false` |
| `SGLANG_LOG_REQUEST_HEADERS` | Comma-separated list of additional HTTP headers to log when `--log-requests` is enabled. Appends to the default `x-smg-routing-key`. | Not set | | `SGLANG_LOG_REQUEST_HEADERS` | Comma-separated list of additional HTTP headers to log when `--log-requests` is enabled. Appends to the default `x-smg-routing-key`. | Not set |
| `SGLANG_HEALTH_CHECK_TIMEOUT` | Timeout for health check in seconds | `20` | | `SGLANG_HEALTH_CHECK_TIMEOUT` | Timeout for health check in seconds | `20` |
| `SGLANG_EPLB_HEATMAP_COLLECTION_INTERVAL` | The interval of passes to collect the metric of selected count of physical experts on each layer and GPU rank. 0 means disabled. | `0` | | `SGLANG_EPLB_HEATMAP_COLLECTION_INTERVAL` | The interval of passes to collect the metric of selected count of physical experts on each layer and GPU rank. 0 means disabled. | `0` |
+1 -5
View File
@@ -169,7 +169,6 @@ class Envs:
SGLANG_LOG_GC = EnvBool(False) SGLANG_LOG_GC = EnvBool(False)
SGLANG_LOG_FORWARD_ITERS = EnvBool(False) SGLANG_LOG_FORWARD_ITERS = EnvBool(False)
SGLANG_LOG_MS = EnvBool(False) SGLANG_LOG_MS = EnvBool(False)
SGLANG_DISABLE_REQUEST_LOGGING = EnvBool(False)
SGLANG_LOG_REQUEST_EXCEEDED_MS = EnvInt(-1) SGLANG_LOG_REQUEST_EXCEEDED_MS = EnvInt(-1)
SGLANG_LOG_REQUEST_HEADERS = EnvTuple(tuple()) SGLANG_LOG_REQUEST_HEADERS = EnvTuple(tuple())
SGLANG_LOG_SCHEDULER_STATUS_TARGET = EnvStr("") SGLANG_LOG_SCHEDULER_STATUS_TARGET = EnvStr("")
@@ -187,7 +186,6 @@ class Envs:
SGLANG_GRAMMAR_MAX_POLL_ITERATIONS = EnvInt(10000) SGLANG_GRAMMAR_MAX_POLL_ITERATIONS = EnvInt(10000)
SGLANG_DISABLE_OUTLINES_DISK_CACHE = EnvBool(False) SGLANG_DISABLE_OUTLINES_DISK_CACHE = EnvBool(False)
# Test & Debug # Test & Debug
SGLANG_DETECT_SLOW_RANK = EnvBool(False) SGLANG_DETECT_SLOW_RANK = EnvBool(False)
SGLANG_TEST_STUCK_DETOKENIZER = EnvFloat(0) SGLANG_TEST_STUCK_DETOKENIZER = EnvFloat(0)
@@ -425,9 +423,7 @@ class Envs:
# Flash Attention # Flash Attention
SGLANG_USE_SGL_FA3_KERNEL = EnvBool(True) SGLANG_USE_SGL_FA3_KERNEL = EnvBool(True)
# vLLM dependencies (TODO: they have been deprecated, we can remove them safely) # Kernels
USE_VLLM_CUTLASS_W8A8_FP8_KERNEL = EnvBool(False)
USE_TRITON_W8A8_FP8_KERNEL = EnvBool(False) USE_TRITON_W8A8_FP8_KERNEL = EnvBool(False)
SGLANG_RETURN_ORIGINAL_LOGPROB = EnvBool(False) SGLANG_RETURN_ORIGINAL_LOGPROB = EnvBool(False)
SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN = EnvBool(False) SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN = EnvBool(False)
@@ -119,7 +119,6 @@ if _is_cuda:
return mat_a.new_empty((M, N), dtype=out_dtype) return mat_a.new_empty((M, N), dtype=out_dtype)
use_vllm_cutlass_w8a8_fp8_kernel = get_bool_env_var("USE_VLLM_CUTLASS_W8A8_FP8_KERNEL")
use_triton_w8a8_fp8_kernel = get_bool_env_var("USE_TRITON_W8A8_FP8_KERNEL") use_triton_w8a8_fp8_kernel = get_bool_env_var("USE_TRITON_W8A8_FP8_KERNEL")
# Input scaling factors are no longer optional in _scaled_mm starting # Input scaling factors are no longer optional in _scaled_mm starting
+36 -23
View File
@@ -699,6 +699,18 @@ class Scheduler(
else: else:
self.model_worker = self.draft_worker self.model_worker = self.draft_worker
# Install device timer on model runners for fwd occupancy tracking
if hasattr(self, "forward_pass_device_timer"):
timer = self.forward_pass_device_timer
self.tp_worker.model_runner.device_timer = timer
if self.draft_worker is not None:
dw = getattr(self.draft_worker, "draft_worker", None)
if dw is not None:
if hasattr(dw, "draft_runner"):
dw.draft_runner.device_timer = timer
for r in getattr(dw, "draft_runner_list", []):
r.device_timer = timer
# Get token and memory info from the model worker # Get token and memory info from the model worker
( (
self.max_total_num_tokens, self.max_total_num_tokens,
@@ -744,10 +756,10 @@ class Scheduler(
set_random_seed(self.random_seed) set_random_seed(self.random_seed)
# Print debug info # Print debug info
avail_mem = get_available_gpu_memory(
self.device, self.gpu_id, empty_cache=False
)
if self.tp_rank == 0: if self.tp_rank == 0:
avail_mem = get_available_gpu_memory(
self.device, self.gpu_id, empty_cache=False
)
logger.info( logger.info(
f"max_total_num_tokens={self.max_total_num_tokens}, " f"max_total_num_tokens={self.max_total_num_tokens}, "
f"chunked_prefill_size={self.server_args.chunked_prefill_size}, " f"chunked_prefill_size={self.server_args.chunked_prefill_size}, "
@@ -758,8 +770,17 @@ class Scheduler(
) )
if self.enable_metrics and hasattr(self, "metrics_collector"): if self.enable_metrics and hasattr(self, "metrics_collector"):
self.metrics_collector.emit_cache_config_info( self.metrics_collector.emit_constants(
self.page_size, self.max_total_num_tokens // self.page_size max_total_num_tokens=self.max_total_num_tokens,
max_running_requests_under_SLO=getattr(
self, "max_running_requests_under_SLO", None
),
engine_startup_time=0.0,
engine_load_weights_time=0.0,
page_size=self.page_size,
num_pages=self.max_total_num_tokens // self.page_size,
context_len=self.model_config.context_len,
startup_available_gpu_memory_gb=avail_mem,
) )
def init_cache_with_memory_pool(self): def init_cache_with_memory_pool(self):
@@ -1491,7 +1512,6 @@ class Scheduler(
recv_reqs = self.recv_requests() recv_reqs = self.recv_requests()
self.process_input_requests(recv_reqs) self.process_input_requests(recv_reqs)
if self._engine_paused: if self._engine_paused:
self.cancel_bubble_timer()
continue continue
# Get the next batch to run # Get the next batch to run
@@ -1546,7 +1566,6 @@ class Scheduler(
self.result_queue.append((batch.copy(), batch_result)) self.result_queue.append((batch.copy(), batch_result))
else: else:
batch_result = None batch_result = None
self.cancel_bubble_timer()
# Process the last batch # Process the last batch
if self.last_batch: if self.last_batch:
@@ -2922,14 +2941,13 @@ class Scheduler(
bs = len(model_worker_batch.seq_lens) bs = len(model_worker_batch.seq_lens)
future_indices = self.future_map.alloc_future_indices(bs) future_indices = self.future_map.alloc_future_indices(bs)
with self.forward_stream_ctx, self.record_bubble_metrics(batch): with self.forward_stream_ctx:
self.forward_stream.wait_stream(self.schedule_stream) self.forward_stream.wait_stream(self.schedule_stream)
self.future_map.resolve_future(model_worker_batch) self.future_map.resolve_future(model_worker_batch)
with self.record_forward_metrics(batch): batch_result = self.model_worker.forward_batch_generation(
batch_result = self.model_worker.forward_batch_generation( model_worker_batch
model_worker_batch # here pp is not compatible with overlap
# here pp is not compatible with overlap )
)
# FIXME(lsyin): maybe move this to forward_batch_generation # FIXME(lsyin): maybe move this to forward_batch_generation
batch_result.copy_done = self.device_module.Event() batch_result.copy_done = self.device_module.Event()
if batch_result.delay_sample_func is None: if batch_result.delay_sample_func is None:
@@ -2948,11 +2966,6 @@ class Scheduler(
batch.spec_info = batch_result.next_draft_input batch.spec_info = batch_result.next_draft_input
batch.spec_info.future_indices = future_indices batch.spec_info.future_indices = future_indices
# batch.spec_info = EagleDraftInput(
# future_indices=future_indices,
# verify_done=batch_result.next_draft_input.verify_done,
# )
# The future value, usually for next batch preparation # The future value, usually for next batch preparation
# Current implementation strictly synchronizes the seq_lens # Current implementation strictly synchronizes the seq_lens
batch.seq_lens = batch_result.next_draft_input.new_seq_lens batch.seq_lens = batch_result.next_draft_input.new_seq_lens
@@ -2965,10 +2978,9 @@ class Scheduler(
if self.spec_algorithm.is_none() if self.spec_algorithm.is_none()
else {} else {}
) )
with self.record_forward_metrics(batch): batch_result = self.model_worker.forward_batch_generation(
batch_result = self.model_worker.forward_batch_generation( worker_batch_or_batch, **kwargs
worker_batch_or_batch, **kwargs )
)
future_indices_or_next_token_ids = batch_result.next_token_ids future_indices_or_next_token_ids = batch_result.next_token_ids
self.update_cache_from_scheduler(batch, batch_result) self.update_cache_from_scheduler(batch, batch_result)
@@ -2998,7 +3010,7 @@ class Scheduler(
if self.enable_overlap: if self.enable_overlap:
self.record_batch_in_overlap(model_worker_batch) self.record_batch_in_overlap(model_worker_batch)
with self.forward_stream_ctx, self.record_bubble_metrics(batch): with self.forward_stream_ctx:
self.forward_stream.wait_stream(self.schedule_stream) self.forward_stream.wait_stream(self.schedule_stream)
pooler_output = self.tp_worker.forward_batch_embedding( pooler_output = self.tp_worker.forward_batch_embedding(
model_worker_batch model_worker_batch
@@ -3083,6 +3095,7 @@ class Scheduler(
self.log_batch_result_stats(batch, result) self.log_batch_result_stats(batch, result)
self._maybe_clear_mm_inputs(batch) self._maybe_clear_mm_inputs(batch)
self.maybe_send_health_check_signal() self.maybe_send_health_check_signal()
self.update_device_timer()
def maybe_send_health_check_signal(self): def maybe_send_health_check_signal(self):
if self.return_health_check_ipcs: if self.return_health_check_ipcs:
@@ -10,7 +10,6 @@ from sglang.srt.disaggregation.utils import DisaggregationMode
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.observability.metrics_collector import QueueCount from sglang.srt.observability.metrics_collector import QueueCount
from sglang.srt.utils.common import ceil_align, raise_error_or_warn from sglang.srt.utils.common import ceil_align, raise_error_or_warn
from sglang.srt.utils.request_logger import disable_request_logging
from sglang.srt.utils.watchdog import WatchdogRaw from sglang.srt.utils.watchdog import WatchdogRaw
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -556,6 +555,9 @@ class SchedulerRuntimeCheckerMixin:
# reset token ratio # reset token ratio
self.new_token_ratio = self.init_new_token_ratio self.new_token_ratio = self.init_new_token_ratio
# reset device timer window so idle time isn't counted
self.reset_device_timer_window()
# sleep until next event # sleep until next event
self.maybe_sleep_on_idle() self.maybe_sleep_on_idle()
@@ -564,7 +566,7 @@ def create_scheduler_watchdog(
scheduler: Scheduler, watchdog_timeout: float, soft: bool = False scheduler: Scheduler, watchdog_timeout: float, soft: bool = False
) -> WatchdogRaw: ) -> WatchdogRaw:
def dump_info() -> str: def dump_info() -> str:
if scheduler.is_initializing or disable_request_logging(): if scheduler.is_initializing:
return "" return ""
_, messages = scheduler._check_all_pools(scheduler.get_pool_stats()) _, messages = scheduler._check_all_pools(scheduler.get_pool_stats())
return ( return (
@@ -2161,13 +2161,6 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
state.last_completion_tokens = completion_tokens state.last_completion_tokens = completion_tokens
if state.finished: if state.finished:
retraction_count = (
recv_obj.retraction_counts[i]
if getattr(recv_obj, "retraction_counts", None)
and i < len(recv_obj.retraction_counts)
else 0
)
# Get detailed cache breakdown if available # Get detailed cache breakdown if available
cached_tokens_details = None cached_tokens_details = None
if ( if (
@@ -2183,7 +2176,6 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
recv_obj.cached_tokens[i], recv_obj.cached_tokens[i],
state.time_stats.get_e2e_latency(), state.time_stats.get_e2e_latency(),
self._request_has_grammar(state.obj), self._request_has_grammar(state.obj),
retraction_count,
cached_tokens_details, cached_tokens_details,
) )
@@ -16,6 +16,7 @@
from __future__ import annotations from __future__ import annotations
import bisect import bisect
import contextlib
import gc import gc
import inspect import inspect
import logging import logging
@@ -896,9 +897,10 @@ class CudaGraphRunner:
else: else:
set_pdmux_status(False) set_pdmux_status(False)
for i, sg in enumerate(self.stream_groups): for i, sg in enumerate(self.stream_groups):
with graph_capture( with (
stream=sg[1] graph_capture(stream=sg[1]) as graph_capture_context,
) as graph_capture_context, profile_context as prof: profile_context as prof,
):
self.stream = graph_capture_context.stream self.stream = graph_capture_context.stream
_capture_one_stream(i) _capture_one_stream(i)
@@ -1313,7 +1315,17 @@ class CudaGraphRunner:
variant_label = self._resolve_lora_variant(forward_batch) variant_label = self._resolve_lora_variant(forward_batch)
stream_idx = get_current_stream_idx() if self.enable_pdmux else None stream_idx = get_current_stream_idx() if self.enable_pdmux else None
graph_key = self._make_graph_key(self.bs, stream_idx, variant_label) graph_key = self._make_graph_key(self.bs, stream_idx, variant_label)
self.graphs[graph_key].replay() ctx = (
self.model_runner.device_timer.wrap(
metadata={
"category": forward_batch.forward_mode.name.lower(),
}
)
if self.model_runner.device_timer
else contextlib.nullcontext()
)
with ctx:
self.graphs[graph_key].replay()
output = self.output_buffers[graph_key] output = self.output_buffers[graph_key]
if isinstance(output, LogitsProcessorOutput): if isinstance(output, LogitsProcessorOutput):
+109 -84
View File
@@ -348,6 +348,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.is_draft_worker = is_draft_worker self.is_draft_worker = is_draft_worker
self.memory_pool_config = memory_pool_config self.memory_pool_config = memory_pool_config
self.is_generation = model_config.is_generation self.is_generation = model_config.is_generation
self.device_timer = None
self.is_multimodal = model_config.is_multimodal self.is_multimodal = model_config.is_multimodal
self.is_multimodal_chunked_prefill_supported = ( self.is_multimodal_chunked_prefill_supported = (
model_config.is_multimodal_chunked_prefill_supported model_config.is_multimodal_chunked_prefill_supported
@@ -362,6 +363,13 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.is_hybrid_swa_compress = model_config.is_hybrid_swa_compress self.is_hybrid_swa_compress = model_config.is_hybrid_swa_compress
self.use_mla_backend = self.model_config.attention_arch == AttentionArch.MLA self.use_mla_backend = self.model_config.attention_arch == AttentionArch.MLA
self.attention_chunk_size = model_config.attention_chunk_size self.attention_chunk_size = model_config.attention_chunk_size
rope_scaling = getattr(
model_config.hf_text_config, "rope_parameters", None
) or getattr(model_config.hf_text_config, "rope_scaling", {})
self.model_is_mrope = (
rope_scaling is not None and "mrope_section" in rope_scaling
)
self.enable_elastic_ep = server_args.elastic_ep_backend is not None
self.forward_pass_id = 0 self.forward_pass_id = 0
self.init_new_workspace = False self.init_new_workspace = False
self.draft_model_idx = draft_model_idx self.draft_model_idx = draft_model_idx
@@ -2962,6 +2970,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
skip_attn_backend_init: bool = False, skip_attn_backend_init: bool = False,
pp_proxy_tensors=None, pp_proxy_tensors=None,
) -> Union[LogitsProcessorOutput, PPProxyTensors]: ) -> Union[LogitsProcessorOutput, PPProxyTensors]:
# Set extra arguments
if not skip_attn_backend_init: if not skip_attn_backend_init:
if hasattr(self.model, "prepare_forward_batch"): if hasattr(self.model, "prepare_forward_batch"):
# Prepare model-specific attention metadata before planning, # Prepare model-specific attention metadata before planning,
@@ -2976,12 +2985,20 @@ class ModelRunner(ModelRunnerKVCacheMixin):
kwargs = {} kwargs = {}
if self.support_pp: if self.support_pp:
kwargs["pp_proxy_tensors"] = pp_proxy_tensors kwargs["pp_proxy_tensors"] = pp_proxy_tensors
return self.model.forward(
forward_batch.input_ids, # Launch forward
forward_batch.positions, ctx = (
forward_batch, self.device_timer.wrap(metadata={"category": "decode"})
**kwargs, if self.device_timer
else contextlib.nullcontext()
) )
with ctx:
return self.model.forward(
forward_batch.input_ids,
forward_batch.positions,
forward_batch,
**kwargs,
)
def forward_extend( def forward_extend(
self, self,
@@ -2991,6 +3008,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
) -> Tuple[ ) -> Tuple[
Union[LogitsProcessorOutput, PPProxyTensors, EmbeddingPoolerOutput], bool Union[LogitsProcessorOutput, PPProxyTensors, EmbeddingPoolerOutput], bool
]: ]:
# Setup extra arguments
kwargs = {} kwargs = {}
if self.support_pp: if self.support_pp:
kwargs["pp_proxy_tensors"] = pp_proxy_tensors kwargs["pp_proxy_tensors"] = pp_proxy_tensors
@@ -3010,17 +3028,25 @@ class ModelRunner(ModelRunnerKVCacheMixin):
if not self.is_generation: if not self.is_generation:
kwargs["get_embedding"] = True kwargs["get_embedding"] = True
# Check piecewies cuda graph
can_run_graph = ( can_run_graph = (
self.piecewise_cuda_graph_runner is not None self.piecewise_cuda_graph_runner is not None
and self.piecewise_cuda_graph_runner.can_run(forward_batch) and self.piecewise_cuda_graph_runner.can_run(forward_batch)
) )
if can_run_graph: if can_run_graph:
return ( # TODO: device_timer.wrap is too broad here — it also includes
self.piecewise_cuda_graph_runner.replay(forward_batch, **kwargs), # replay_prepare time. Move timing into the piecewise cuda graph
can_run_graph, # runner to capture only the model.forward part.
ctx = (
self.device_timer.wrap(metadata={"category": "extend"})
if self.device_timer
else contextlib.nullcontext()
) )
with ctx:
ret = self.piecewise_cuda_graph_runner.replay(forward_batch, **kwargs)
return (ret, can_run_graph)
# Launch model forward
if not skip_attn_backend_init: if not skip_attn_backend_init:
if hasattr(self.model, "prepare_forward_batch"): if hasattr(self.model, "prepare_forward_batch"):
# Prepare model-specific attention metadata before planning, # Prepare model-specific attention metadata before planning,
@@ -3028,15 +3054,19 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.model.prepare_forward_batch(forward_batch) self.model.prepare_forward_batch(forward_batch)
self.attn_backend.init_forward_metadata(forward_batch) self.attn_backend.init_forward_metadata(forward_batch)
return ( ctx = (
self.model.forward( self.device_timer.wrap(metadata={"category": "extend"})
if self.device_timer
else contextlib.nullcontext()
)
with ctx:
ret = self.model.forward(
forward_batch.input_ids, forward_batch.input_ids,
forward_batch.positions, forward_batch.positions,
forward_batch, forward_batch,
**kwargs, **kwargs,
), )
can_run_graph, return (ret, can_run_graph)
)
def forward_idle( def forward_idle(
self, forward_batch: ForwardBatch, pp_proxy_tensors=None self, forward_batch: ForwardBatch, pp_proxy_tensors=None
@@ -3050,12 +3080,18 @@ class ModelRunner(ModelRunnerKVCacheMixin):
kwargs = {} kwargs = {}
if self.support_pp: if self.support_pp:
kwargs["pp_proxy_tensors"] = pp_proxy_tensors kwargs["pp_proxy_tensors"] = pp_proxy_tensors
return self.model.forward( ctx = (
forward_batch.input_ids, self.device_timer.wrap(metadata={"category": "idle"})
forward_batch.positions, if self.device_timer
forward_batch, else contextlib.nullcontext()
**kwargs,
) )
with ctx:
return self.model.forward(
forward_batch.input_ids,
forward_batch.positions,
forward_batch,
**kwargs,
)
def forward_split_prefill( def forward_split_prefill(
self, self,
@@ -3069,12 +3105,18 @@ class ModelRunner(ModelRunnerKVCacheMixin):
forward_batch.split_index + forward_count, forward_batch.split_index + forward_count,
self.model_config.num_hidden_layers, self.model_config.num_hidden_layers,
) )
ret = self.model.forward_split_prefill( ctx = (
forward_batch.input_ids, self.device_timer.wrap(metadata={"category": "split_prefill"})
forward_batch.positions, if self.device_timer
forward_batch, else contextlib.nullcontext()
(forward_batch.split_index, next_split_index),
) )
with ctx:
ret = self.model.forward_split_prefill(
forward_batch.input_ids,
forward_batch.positions,
forward_batch,
(forward_batch.split_index, next_split_index),
)
forward_batch.split_index = next_split_index forward_batch.split_index = next_split_index
return ret return ret
@@ -3088,12 +3130,14 @@ class ModelRunner(ModelRunnerKVCacheMixin):
) -> ModelRunnerOutput: ) -> ModelRunnerOutput:
self.forward_pass_id += 1 self.forward_pass_id += 1
# Try msprob debugger
if self.msprobe_debugger is not None: if self.msprobe_debugger is not None:
rank_id = ( rank_id = (
self.gpu_id if self.dp_size is not None and self.dp_size > 1 else None self.gpu_id if self.dp_size is not None and self.dp_size > 1 else None
) )
self.msprobe_debugger.start(model=self.model, rank_id=rank_id) self.msprobe_debugger.start(model=self.model, rank_id=rank_id)
# Step span
step_span_ctx = ( step_span_ctx = (
torch.profiler.record_function(_build_step_span_name(forward_batch)) torch.profiler.record_function(_build_step_span_name(forward_batch))
if torch.autograd._profiler_enabled() if torch.autograd._profiler_enabled()
@@ -3113,21 +3157,9 @@ class ModelRunner(ModelRunnerKVCacheMixin):
reinit_attn_backend, reinit_attn_backend,
split_forward_count, split_forward_count,
) )
elastic_ep_state = ElasticEPStateManager.instance() if self.enable_elastic_ep:
if ( output = self._maybe_rebalance_after_rank_fault(
elastic_ep_state is not None output,
and not elastic_ep_state.is_active_equal_last()
):
elastic_ep_state.snapshot_active_to_last()
elastic_ep_state.sync_active_to_cpu()
logging.info("EPLB due to rank faults")
gen = self.eplb_manager.rebalance()
while True:
try:
next(gen)
except StopIteration:
break
output = self._forward_raw(
forward_batch, forward_batch,
skip_attn_backend_init, skip_attn_backend_init,
pp_proxy_tensors, pp_proxy_tensors,
@@ -3167,6 +3199,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
reinit_attn_backend: bool = False, reinit_attn_backend: bool = False,
split_forward_count: int = 1, split_forward_count: int = 1,
) -> ModelRunnerOutput: ) -> ModelRunnerOutput:
# Check whether can run cuda graph
mode_check = ( mode_check = (
forward_batch.forward_mode.is_cpu_graph forward_batch.forward_mode.is_cpu_graph
if self.device == "cpu" if self.device == "cpu"
@@ -3178,12 +3211,14 @@ class ModelRunner(ModelRunnerKVCacheMixin):
and self.graph_runner.can_run(forward_batch) and self.graph_runner.can_run(forward_batch)
) )
# Hisparse coordinator
if ( if (
self.hisparse_coordinator is not None self.hisparse_coordinator is not None
and forward_batch.forward_mode.is_decode() and forward_batch.forward_mode.is_decode()
): ):
self.hisparse_coordinator.wait_for_pending_backup() self.hisparse_coordinator.wait_for_pending_backup()
# Replay cuda graph if applicable
if can_run_graph: if can_run_graph:
ret = self.graph_runner.replay( ret = self.graph_runner.replay(
forward_batch, forward_batch,
@@ -3213,10 +3248,12 @@ class ModelRunner(ModelRunnerKVCacheMixin):
if forward_batch.out_cache_loc_swa is not None: if forward_batch.out_cache_loc_swa is not None:
self.token_to_kv_pool.set_swa_loc(forward_batch.out_cache_loc_swa) self.token_to_kv_pool.set_swa_loc(forward_batch.out_cache_loc_swa)
# Hisparse coordinator
forward_batch.hisparse_coordinator = self.hisparse_coordinator forward_batch.hisparse_coordinator = self.hisparse_coordinator
if self.hisparse_coordinator is not None: if self.hisparse_coordinator is not None:
self.hisparse_coordinator.num_real_reqs.fill_(forward_batch.batch_size) self.hisparse_coordinator.num_real_reqs.fill_(forward_batch.batch_size)
# Forward without cuda graph
if forward_batch.forward_mode.is_decode(): if forward_batch.forward_mode.is_decode():
ret = self.forward_decode( ret = self.forward_decode(
forward_batch, forward_batch,
@@ -3279,14 +3316,8 @@ class ModelRunner(ModelRunnerKVCacheMixin):
Returns: Returns:
A list of next_token_ids A list of next_token_ids
""" """
# For duplex models with multiple output streams.
if isinstance(logits_output, tuple):
return torch.stack(
[self.sample(values, forward_batch) for values in logits_output],
axis=-1,
)
self._preprocess_logits(logits_output, forward_batch.sampling_info) self._preprocess_logits(logits_output, forward_batch.sampling_info)
# Sample the next tokens # Sample the next tokens
next_token_ids = self.sampler( next_token_ids = self.sampler(
logits_output, logits_output,
@@ -3336,18 +3367,6 @@ class ModelRunner(ModelRunnerKVCacheMixin):
forward_batch.token_ids_logprobs, forward_batch.token_ids_logprobs,
) )
@property
def model_is_mrope(self) -> bool:
"""Detect if the model has "mrope" rope_scaling type.
mrope requires keep "rope_deltas" between prompt and decoding phases."""
rope_scaling = getattr(
self.model_config.hf_text_config, "rope_parameters", None
) or getattr(self.model_config.hf_text_config, "rope_scaling", {})
if rope_scaling is None:
return False
is_mrope_enabled = "mrope_section" in rope_scaling
return is_mrope_enabled
def save_remote_model(self, url: str): def save_remote_model(self, url: str):
from sglang.srt.model_loader.loader import RemoteModelLoader from sglang.srt.model_loader.loader import RemoteModelLoader
@@ -3405,6 +3424,35 @@ class ModelRunner(ModelRunnerKVCacheMixin):
device=self.device, device=self.device,
) )
def _maybe_rebalance_after_rank_fault(
self,
output: ModelRunnerOutput,
forward_batch: ForwardBatch,
skip_attn_backend_init: bool,
pp_proxy_tensors: Optional[PPProxyTensors],
reinit_attn_backend: bool,
split_forward_count: int,
) -> ModelRunnerOutput:
elastic_ep_state = ElasticEPStateManager.instance()
if elastic_ep_state is not None and not elastic_ep_state.is_active_equal_last():
elastic_ep_state.snapshot_active_to_last()
elastic_ep_state.sync_active_to_cpu()
logging.info("EPLB due to rank faults")
gen = self.eplb_manager.rebalance()
while True:
try:
next(gen)
except StopIteration:
break
output = self._forward_raw(
forward_batch,
skip_attn_backend_init,
pp_proxy_tensors,
reinit_attn_backend,
split_forward_count,
)
return output
def _model_load_weights_direct(model, named_tensors: List[Tuple[str, torch.Tensor]]): def _model_load_weights_direct(model, named_tensors: List[Tuple[str, torch.Tensor]]):
params_dict = dict(model.named_parameters()) params_dict = dict(model.named_parameters())
@@ -3419,35 +3467,12 @@ def _unwrap_tensor(tensor, tp_rank, device):
def _build_step_span_name(forward_batch: ForwardBatch) -> str: def _build_step_span_name(forward_batch: ForwardBatch) -> str:
"""Build a profile-trace span name for one forward step. """Build a profile-trace span name for one forward step."""
Format:
step[decode bs=N] — decode-only batch
step[prefill bs=N toks=T] — extend-only (prefill) batch
step[mixed bs=N ext=T dec=D] — extend+decode mixed batch
step[idle] — idle/padding step
step[<MODE> bs=N] — other modes (target-verify, etc.)
Used by ModelRunner.forward to wrap each step in a torch.profile
record_function so Chrome traces show labeled step boundaries.
"""
mode = forward_batch.forward_mode mode = forward_batch.forward_mode
bs = forward_batch.batch_size bs = forward_batch.batch_size
if mode.is_idle(): if mode == ForwardMode.EXTEND:
return "step[idle]"
if mode.is_decode():
return f"step[decode bs={bs}]"
if mode.is_extend():
ext_toks = forward_batch.extend_num_tokens or 0 ext_toks = forward_batch.extend_num_tokens or 0
ext_seqs = ( return f"step[EXTEND bs={bs} toks={ext_toks}]"
forward_batch.extend_seq_lens.shape[0]
if forward_batch.extend_seq_lens is not None
else bs
)
dec_seqs = bs - ext_seqs
if dec_seqs > 0:
return f"step[mixed bs={bs} ext={ext_toks} dec={dec_seqs}]"
return f"step[prefill bs={bs} toks={ext_toks}]"
return f"step[{mode.name} bs={bs}]" return f"step[{mode.name} bs={bs}]"
@@ -40,20 +40,6 @@ SGLANG_TEST_REQUEST_TIME_STATS = get_bool_env_var("SGLANG_TEST_REQUEST_TIME_STAT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_histogram_conf_from_env(env_var_name: str) -> Optional[List[float]]:
"""
Get the histogram configuration from the environment variable.
env value should be like "0.1,0.2,0.5,1,2"
"""
if env_var_name not in os.environ:
return None
# if the env var is not set or empty, return None
env_var_value = os.environ[env_var_name]
if not env_var_value:
return None
return [float(x) for x in env_var_value.split(",")]
@dataclass @dataclass
class QueueCount: class QueueCount:
"""Holds both the total count and optional per-priority breakdown for a queue.""" """Holds both the total count and optional per-priority breakdown for a queue."""
@@ -78,22 +64,36 @@ class QueueCount:
class SchedulerStats: class SchedulerStats:
# Basics # Basics
num_running_reqs: QueueCount = field(default_factory=QueueCount) num_running_reqs: QueueCount = field(default_factory=QueueCount)
num_used_tokens: int = 0
# FIXME: token_usage is actually max usage across all pools (KV, SWA, mamba),
# not just KV token usage. Rename requires API deprecation.
token_usage: float = 0.0
full_token_usage: float = 0.0
pending_prealloc_token_usage: float = 0.0
swa_token_usage: float = 0.0
mamba_usage: float = 0.0
decode_sum_seq_lens: int = 0
gen_throughput: float = 0.0
num_queue_reqs: QueueCount = field(default_factory=QueueCount) num_queue_reqs: QueueCount = field(default_factory=QueueCount)
num_grammar_queue_reqs: int = 0 num_grammar_queue_reqs: int = 0
num_running_reqs_offline_batch: int = 0 gen_throughput: float = 0.0
cache_hit_rate: float = 0.0 cache_hit_rate: float = 0.0
decode_sum_seq_lens: int = 0
max_total_num_tokens: int = 0 # Memory pool usage ratios (0.0–1.0).
# Each pool tracks: used = total - available - evictable, usage = used / total.
#
# token_usage: max(full, swa, mamba) — the bottleneck across all pools.
# FIXME: misleadingly named "token_usage"; rename requires API deprecation.
# full_token_usage: full-attention KV cache pool usage (always active).
# swa_token_usage: sliding-window attention KV cache pool usage (hybrid SWA models only, e.g. Gemma2).
# mamba_usage: Mamba SSM state pool usage (hybrid SSM models only, e.g. Jamba).
token_usage: float = 0.0
full_token_usage: float = 0.0
swa_token_usage: float = 0.0
mamba_usage: float = 0.0
# Absolute token counts for the full-attention KV cache pool.
# Invariant: kv_available_tokens + kv_evictable_tokens + kv_used_tokens <= max_total_num_tokens
# (the gap accounts for protected/session-held tokens not exposed here).
# max_total_num_tokens is emitted once at startup via emit_constants.
#
# kv_available_tokens: free (unallocated) slots in the pool.
# kv_evictable_tokens: slots holding radix-cached KV data that can be evicted for new requests.
# kv_used_tokens: actively used slots (locked by running requests). Equals full_num_used.
# num_used_tokens: max(full_num_used, swa_num_used) for hybrid-SWA models, else full_num_used.
# Does NOT include the mamba pool.
num_used_tokens: int = 0
kv_available_tokens: int = 0 kv_available_tokens: int = 0
kv_evictable_tokens: int = 0 kv_evictable_tokens: int = 0
kv_used_tokens: int = 0 kv_used_tokens: int = 0
@@ -113,18 +113,16 @@ class SchedulerStats:
num_decode_transfer_queue_reqs: QueueCount = field(default_factory=QueueCount) num_decode_transfer_queue_reqs: QueueCount = field(default_factory=QueueCount)
kv_transfer_speed_gb_s: float = 0.0 kv_transfer_speed_gb_s: float = 0.0
kv_transfer_latency_ms: float = 0.0 kv_transfer_latency_ms: float = 0.0
pending_prealloc_token_usage: float = 0.0
# Utilization # Utilization
utilization: float = 0.0 utilization: float = 0.0
max_running_requests_under_SLO: Optional[int] = None
# Engine startup # Scheduler policy
engine_startup_time: float = 0.0
engine_load_weights_time: float = 0.0
new_token_ratio: float = 0.0 new_token_ratio: float = 0.0
# CUDA graph # CUDA graph
is_cuda_graph: float = 0.0 is_cuda_graph: int = 0
# LoRA pool metrics # LoRA pool metrics
lora_pool_slots_used: int = 0 lora_pool_slots_used: int = 0
@@ -196,60 +194,15 @@ class SchedulerMetricsCollector:
self.last_log_time = time.perf_counter() self.last_log_time = time.perf_counter()
self._known_priorities: Set[int] = set() self._known_priorities: Set[int] = set()
# =================================================================
# Basics
# =================================================================
self.num_running_reqs = Gauge( self.num_running_reqs = Gauge(
name="sglang:num_running_reqs", name="sglang:num_running_reqs",
documentation="The number of running requests.", documentation="The number of running requests.",
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
self.num_used_tokens = Gauge(
name="sglang:num_used_tokens",
documentation="The number of used tokens.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.token_usage = Gauge(
name="sglang:token_usage",
documentation="The token usage.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.full_token_usage = Gauge(
name="sglang:full_token_usage",
documentation="The token usage for full attention layers.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.pending_prealloc_token_usage = Gauge(
name="sglang:pending_prealloc_token_usage",
documentation="The token usage for pending preallocated tokens (not preallocated yet).",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.swa_token_usage = Gauge(
name="sglang:swa_token_usage",
documentation="The token usage for SWA layers.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.mamba_usage = Gauge(
name="sglang:mamba_usage",
documentation="The token usage for Mamba layers.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.decode_sum_seq_lens = Gauge(
name="sglang:decode_sum_seq_lens",
documentation="The sum of all sequence lengths in decode.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.gen_throughput = Gauge(
name="sglang:gen_throughput",
documentation="The generation throughput (token/s).",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.num_queue_reqs = Gauge( self.num_queue_reqs = Gauge(
name="sglang:num_queue_reqs", name="sglang:num_queue_reqs",
documentation="The number of requests in the waiting queue.", documentation="The number of requests in the waiting queue.",
@@ -262,9 +215,9 @@ class SchedulerMetricsCollector:
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
self.num_running_reqs_offline_batch = Gauge( self.gen_throughput = Gauge(
name="sglang:num_running_reqs_offline_batch", name="sglang:gen_throughput",
documentation="The number of running low-priority offline batch requests(label is 'batch').", documentation="The generation throughput (token/s).",
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
@@ -274,14 +227,50 @@ class SchedulerMetricsCollector:
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
self.decode_sum_seq_lens = Gauge(
self.max_total_num_tokens = Gauge( name="sglang:decode_sum_seq_lens",
name="sglang:max_total_num_tokens", documentation="The sum of all sequence lengths in decode.",
documentation="Maximum total number of tokens in the KV cache pool.",
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
# =================================================================
# Memory pool usage ratios
# =================================================================
self.token_usage = Gauge(
name="sglang:token_usage",
documentation="The token usage.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.full_token_usage = Gauge(
name="sglang:full_token_usage",
documentation="The token usage for full attention layers.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.swa_token_usage = Gauge(
name="sglang:swa_token_usage",
documentation="The token usage for SWA layers.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.mamba_usage = Gauge(
name="sglang:mamba_usage",
documentation="The token usage for Mamba layers.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
# =================================================================
# Absolute token counts
# =================================================================
self.num_used_tokens = Gauge(
name="sglang:num_used_tokens",
documentation="The number of used tokens.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.kv_available_tokens = Gauge( self.kv_available_tokens = Gauge(
name="sglang:kv_available_tokens", name="sglang:kv_available_tokens",
documentation="Number of free token slots in the KV cache pool.", documentation="Number of free token slots in the KV cache pool.",
@@ -301,7 +290,9 @@ class SchedulerMetricsCollector:
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
# =================================================================
# Speculative decoding # Speculative decoding
# =================================================================
self.spec_accept_length = Gauge( self.spec_accept_length = Gauge(
name="sglang:spec_accept_length", name="sglang:spec_accept_length",
documentation="Mean acceptance length of speculative decoding (accepted drafts + bonus token per forward).", documentation="Mean acceptance length of speculative decoding (accepted drafts + bonus token per forward).",
@@ -315,7 +306,9 @@ class SchedulerMetricsCollector:
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
# =================================================================
# Retract # Retract
# =================================================================
# TODO maybe remove this old gauge in favor of the new counter # TODO maybe remove this old gauge in favor of the new counter
self.num_retracted_reqs = Gauge( self.num_retracted_reqs = Gauge(
name="sglang:num_retracted_reqs", name="sglang:num_retracted_reqs",
@@ -344,7 +337,9 @@ class SchedulerMetricsCollector:
labelnames=labels.keys(), labelnames=labels.keys(),
) )
# =================================================================
# PD disaggregation # PD disaggregation
# =================================================================
self.num_prefill_prealloc_queue_reqs = Gauge( self.num_prefill_prealloc_queue_reqs = Gauge(
name="sglang:num_prefill_prealloc_queue_reqs", name="sglang:num_prefill_prealloc_queue_reqs",
documentation="The number of requests in the prefill prealloc queue.", documentation="The number of requests in the prefill prealloc queue.",
@@ -369,6 +364,24 @@ class SchedulerMetricsCollector:
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
self.kv_transfer_speed_gb_s = Histogram(
name="sglang:kv_transfer_speed_gb_s",
documentation="Histogram of KV cache transfer speed in GB/s.",
labelnames=labels.keys(),
buckets=(0.1, 0.5, 1, 5, 10, 25, 50, 100, 200, 400),
)
self.kv_transfer_latency_ms = Histogram(
name="sglang:kv_transfer_latency_ms",
documentation="Histogram of KV cache transfer latency in ms.",
labelnames=labels.keys(),
buckets=(1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000),
)
self.pending_prealloc_token_usage = Gauge(
name="sglang:pending_prealloc_token_usage",
documentation="The token usage for pending preallocated tokens (not preallocated yet).",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.num_bootstrap_failed_reqs = Counter( self.num_bootstrap_failed_reqs = Counter(
name="sglang:num_bootstrap_failed_reqs_total", name="sglang:num_bootstrap_failed_reqs_total",
documentation="The number of bootstrap failed requests.", documentation="The number of bootstrap failed requests.",
@@ -384,18 +397,6 @@ class SchedulerMetricsCollector:
documentation="Total number of prefill retries.", documentation="Total number of prefill retries.",
labelnames=labels.keys(), labelnames=labels.keys(),
) )
self.kv_transfer_speed_gb_s = Histogram(
name="sglang:kv_transfer_speed_gb_s",
documentation="Histogram of KV cache transfer speed in GB/s.",
labelnames=labels.keys(),
buckets=(0.1, 0.5, 1, 5, 10, 25, 50, 100, 200, 400),
)
self.kv_transfer_latency_ms = Histogram(
name="sglang:kv_transfer_latency_ms",
documentation="Histogram of KV cache transfer latency in ms.",
labelnames=labels.keys(),
buckets=(1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000),
)
self.kv_transfer_bootstrap_ms = Histogram( self.kv_transfer_bootstrap_ms = Histogram(
name="sglang:kv_transfer_bootstrap_ms", name="sglang:kv_transfer_bootstrap_ms",
documentation="Histogram of KV transfer bootstrap time in ms.", documentation="Histogram of KV transfer bootstrap time in ms.",
@@ -415,35 +416,124 @@ class SchedulerMetricsCollector:
buckets=(1, 5, 10, 50, 100, 500, 1000, 5000, 10000), buckets=(1, 5, 10, 50, 100, 500, 1000, 5000, 10000),
) )
# =================================================================
# Utilization # Utilization
# =================================================================
self.utilization = Gauge( self.utilization = Gauge(
name="sglang:utilization", name="sglang:utilization",
documentation="The utilization.", documentation="The utilization.",
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
self.max_running_requests_under_SLO = Gauge(
name="sglang:max_running_requests_under_SLO", # =================================================================
documentation="The maximum number of running requests under SLO.", # Scheduler policy
# =================================================================
self.new_token_ratio = Gauge(
name="sglang:new_token_ratio",
documentation="The new token ratio.",
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
# Engine startup # =================================================================
self.engine_startup_time = Gauge( # CUDA graph
name="sglang:engine_startup_time", # =================================================================
documentation="The time taken for the engine to start up.", # TODO maybe remove this old gauge in favor of the new counter
self.is_cuda_graph = Gauge(
name="sglang:is_cuda_graph",
documentation="Whether the batch is using CUDA graph.",
labelnames=labels.keys(), labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
self.engine_load_weights_time = Gauge( self.cuda_graph_passes_total = Counter(
name="sglang:engine_load_weights_time", name="sglang:cuda_graph_passes_total",
documentation="The time taken for the engine to load weights.", documentation="Total number of forward passes categorized by CUDA graph.",
labelnames=labels.keys(), labelnames=list(labels.keys()) + ["mode"],
multiprocess_mode="mostrecent",
) )
# Additional queueing time histogram # =================================================================
# LoRA pool metrics (only created when LoRA is enabled)
# =================================================================
if self.enable_lora:
self.lora_pool_slots_used = Gauge(
name="sglang:lora_pool_slots_used",
documentation="Number of LoRA adapter slots currently occupied in GPU memory.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.lora_pool_slots_total = Gauge(
name="sglang:lora_pool_slots_total",
documentation="Total number of LoRA adapter slots available (max_loras_per_batch).",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.lora_pool_utilization = Gauge(
name="sglang:lora_pool_utilization",
documentation="LoRA pool utilization ratio (used/total). 1.0 means pool is full.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
# =================================================================
# HiCache metrics (only created when hierarchical cache is enabled)
# =================================================================
if self.enable_hierarchical_cache:
self.hicache_host_used_tokens = Gauge(
name="sglang:hicache_host_used_tokens",
documentation="Number of tokens currently used in the host KV cache.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.hicache_host_total_tokens = Gauge(
name="sglang:hicache_host_total_tokens",
documentation="Total capacity of the host KV cache in tokens.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
# =================================================================
# Streaming session metrics (only created when streaming sessions are enabled)
# =================================================================
if self.enable_streaming_session:
self.num_streaming_sessions = Gauge(
name="sglang:num_streaming_sessions",
documentation="The number of streaming sessions.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.streaming_session_held_tokens = Gauge(
name="sglang:streaming_session_held_tokens",
documentation="The number of KV tokens currently held by streaming session slots.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
# =================================================================
# Routing key metrics
# =================================================================
self.num_unique_running_routing_keys = Gauge(
name="sglang:num_unique_running_routing_keys",
documentation="Number of unique routing keys in running batch.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.routing_key_running_req_count = GaugeHistogram(
name="sglang:routing_key_running_req_count",
documentation="Distribution of routing keys by running request count (gt < count <= le).",
labelnames=list(labels.keys()),
bucket_bounds=ROUTING_KEY_REQ_COUNT_BUCKET_BOUNDS,
)
self.routing_key_all_req_count = GaugeHistogram(
name="sglang:routing_key_all_req_count",
documentation="Distribution of routing keys by running+waiting request count (gt < count <= le).",
labelnames=list(labels.keys()),
bucket_bounds=ROUTING_KEY_REQ_COUNT_BUCKET_BOUNDS,
)
# =================================================================
# Request latency
# =================================================================
self.queue_time = Histogram( self.queue_time = Histogram(
name="sglang:queue_time_seconds", name="sglang:queue_time_seconds",
documentation="Histogram of queueing time in seconds.", documentation="Histogram of queueing time in seconds.",
@@ -487,8 +577,17 @@ class SchedulerMetricsCollector:
3000, 3000,
], ],
) )
self.per_stage_req_latency_seconds = Histogram(
name="sglang:per_stage_req_latency_seconds",
documentation="The latency of each stage of requests.",
# captures latency in range [1ms - ~1191s]
buckets=exponential_buckets(start=0.001, width=1.62, length=30),
labelnames=list(labels.keys()) + ["stage"],
)
# Grammar metrics # =================================================================
# Grammar
# =================================================================
self.grammar_compilation_time = Histogram( self.grammar_compilation_time = Histogram(
name="sglang:grammar_compilation_time_seconds", name="sglang:grammar_compilation_time_seconds",
documentation="Histogram of grammar compilation time in seconds.", documentation="Histogram of grammar compilation time in seconds.",
@@ -616,27 +715,9 @@ class SchedulerMetricsCollector:
buckets=tree_traversal_time_buckets, buckets=tree_traversal_time_buckets,
) )
self.per_stage_req_latency_seconds = Histogram( # =================================================================
name="sglang:per_stage_req_latency_seconds", # Execution
documentation="The latency of each stage of requests.", # =================================================================
# captures latency in range [1ms - ~1191s]
buckets=exponential_buckets(start=0.001, width=1.62, length=30),
labelnames=list(labels.keys()) + ["stage"],
)
# TODO maybe remove this old gauge in favor of the new counter
self.is_cuda_graph = Gauge(
name="sglang:is_cuda_graph",
documentation="Whether the batch is using CUDA graph.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.cuda_graph_passes_total = Counter(
name="sglang:cuda_graph_passes_total",
documentation="Total number of forward passes categorized by CUDA graph.",
labelnames=list(labels.keys()) + ["mode"],
)
if ( if (
labels["moe_ep_rank"] == 0 labels["moe_ep_rank"] == 0
) and envs.SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC.get(): ) and envs.SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC.get():
@@ -646,83 +727,6 @@ class SchedulerMetricsCollector:
labelnames=list(labels.keys()) + ["forward_mode"], labelnames=list(labels.keys()) + ["forward_mode"],
) )
# LoRA pool metrics (only created when LoRA is enabled)
if self.enable_lora:
self.lora_pool_slots_used = Gauge(
name="sglang:lora_pool_slots_used",
documentation="Number of LoRA adapter slots currently occupied in GPU memory.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.lora_pool_slots_total = Gauge(
name="sglang:lora_pool_slots_total",
documentation="Total number of LoRA adapter slots available (max_loras_per_batch).",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.lora_pool_utilization = Gauge(
name="sglang:lora_pool_utilization",
documentation="LoRA pool utilization ratio (used/total). 1.0 means pool is full.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
# HiCache host-tier metrics (only created when hierarchical cache is enabled)
if self.enable_hierarchical_cache:
self.hicache_host_used_tokens = Gauge(
name="sglang:hicache_host_used_tokens",
documentation="Number of tokens currently used in the host KV cache.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.hicache_host_total_tokens = Gauge(
name="sglang:hicache_host_total_tokens",
documentation="Total capacity of the host KV cache in tokens.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
# Streaming session metrics (only created when streaming sessions are enabled)
if self.enable_streaming_session:
self.num_streaming_sessions = Gauge(
name="sglang:num_streaming_sessions",
documentation="The number of streaming sessions.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.streaming_session_held_tokens = Gauge(
name="sglang:streaming_session_held_tokens",
documentation="The number of KV tokens currently held by streaming session slots.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.num_unique_running_routing_keys = Gauge(
name="sglang:num_unique_running_routing_keys",
documentation="Number of unique routing keys in running batch.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.routing_key_running_req_count = GaugeHistogram(
name="sglang:routing_key_running_req_count",
documentation="Distribution of routing keys by running request count (gt < count <= le).",
labelnames=list(labels.keys()),
bucket_bounds=ROUTING_KEY_REQ_COUNT_BUCKET_BOUNDS,
)
self.routing_key_all_req_count = GaugeHistogram(
name="sglang:routing_key_all_req_count",
documentation="Distribution of routing keys by running+waiting request count (gt < count <= le).",
labelnames=list(labels.keys()),
bucket_bounds=ROUTING_KEY_REQ_COUNT_BUCKET_BOUNDS,
)
self.new_token_ratio = Gauge(
name="sglang:new_token_ratio",
documentation="The new token ratio.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.realtime_tokens_total = Counter( self.realtime_tokens_total = Counter(
name="sglang:realtime_tokens_total", name="sglang:realtime_tokens_total",
documentation=( documentation=(
@@ -731,22 +735,14 @@ class SchedulerMetricsCollector:
), ),
labelnames=list(labels.keys()) + ["mode"], labelnames=list(labels.keys()) + ["mode"],
) )
self.gpu_execution_seconds_total = Counter( self.forward_execution_seconds_total = Counter(
name="sglang:gpu_execution_seconds_total", name="sglang:forward_execution_seconds_total",
documentation=( documentation=(
"Total time that GPU is busy executing a workload. " "Total time that GPU is busy executing model forward passes. "
"Refer to ForwardMode for category labels." "Refer to ForwardMode for category labels."
), ),
labelnames=list(labels.keys()) + ["category"], labelnames=list(labels.keys()) + ["category"],
) )
self.gpu_overlap_wait_seconds_total = Counter(
name="sglang:gpu_overlap_wait_seconds_total",
documentation=(
"Total time that GPU forward stream was idle waiting for "
"the CPU schedule stream (overlap bubble)."
),
labelnames=list(labels.keys()) + ["category"],
)
self.estimated_flops_per_gpu_total = Counter( self.estimated_flops_per_gpu_total = Counter(
name="sglang:estimated_flops_per_gpu_total", name="sglang:estimated_flops_per_gpu_total",
documentation=( documentation=(
@@ -780,15 +776,19 @@ class SchedulerMetricsCollector:
), ),
labelnames=list(labels.keys()) + ["mode", "num_prefill_ranks"], labelnames=list(labels.keys()) + ["mode", "num_prefill_ranks"],
) )
self.dp_cooperation_gpu_execution_seconds_total = Counter( self.dp_cooperation_forward_execution_seconds_total = Counter(
name="sglang:dp_cooperation_gpu_execution_seconds_total", name="sglang:dp_cooperation_forward_execution_seconds_total",
documentation=( documentation=(
"Total time that GPU is busy executing a workload with labels about DP cooperation. " "Total time that GPU is busy executing model forward passes, "
"with labels about DP cooperation. "
"Refer to ForwardMode for category labels." "Refer to ForwardMode for category labels."
), ),
labelnames=list(labels.keys()) + ["category", "num_prefill_ranks"], labelnames=list(labels.keys()) + ["category", "num_prefill_ranks"],
) )
# =================================================================
# Prefill delayer
# =================================================================
max_delay = server_args.prefill_delayer_max_delay_passes max_delay = server_args.prefill_delayer_max_delay_passes
self.prefill_delayer_wait_forward_passes = Histogram( self.prefill_delayer_wait_forward_passes = Histogram(
name="sglang:prefill_delayer_wait_forward_passes", name="sglang:prefill_delayer_wait_forward_passes",
@@ -832,13 +832,55 @@ class SchedulerMetricsCollector:
], ],
) )
# This is a work-around Info metric since Info metrics are not supported in Prometheus. # =================================================================
# Similar to vLLM, https://github.com/vllm-project/vllm/blob/main/vllm/v1/metrics/loggers.py # Constants (set once at startup via emit_constants)
# If more Info metrics are needed, we can create a common _log_info function. # =================================================================
self.cache_config_info = Gauge( self.max_total_num_tokens = Gauge(
name="sglang:cache_config_info", name="sglang:max_total_num_tokens",
documentation="Cache configuration information.", documentation="Maximum total number of tokens in the KV cache pool.",
labelnames=["page_size", "num_pages"], labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.max_running_requests_under_SLO = Gauge(
name="sglang:max_running_requests_under_SLO",
documentation="The maximum number of running requests under SLO.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.engine_startup_time = Gauge(
name="sglang:engine_startup_time",
documentation="The time taken for the engine to start up.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.engine_load_weights_time = Gauge(
name="sglang:engine_load_weights_time",
documentation="The time taken for the engine to load weights.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.page_size = Gauge(
name="sglang:page_size",
documentation="KV cache page size in tokens.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.num_pages = Gauge(
name="sglang:num_pages",
documentation="Number of KV cache pages.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.context_len = Gauge(
name="sglang:context_len",
documentation="Maximum context length.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent",
)
self.startup_available_gpu_memory_gb = Gauge(
name="sglang:startup_available_gpu_memory_gb",
documentation="Available GPU memory in GB at startup.",
labelnames=labels.keys(),
multiprocess_mode="mostrecent", multiprocess_mode="mostrecent",
) )
@@ -972,26 +1014,17 @@ class SchedulerMetricsCollector:
**dp_cooperation_info.to_labels(), **dp_cooperation_info.to_labels(),
).inc(delta) ).inc(delta)
def increment_gpu_overlap_wait_seconds( def increment_forward_execution_seconds(
self, self,
category: str, category: str,
t: float, t: float,
dp_cooperation_info: Optional[DPCooperationInfo], dp_cooperation_info: Optional[DPCooperationInfo] = None,
): ):
self.gpu_overlap_wait_seconds_total.labels( self.forward_execution_seconds_total.labels(
**self.labels, category=category **self.labels, category=category
).inc(t) ).inc(t)
def increment_gpu_execution_seconds(
self,
category: str,
t: float,
dp_cooperation_info: Optional[DPCooperationInfo],
):
logger.debug(f"GPU execution seconds: {category=} {t=:.3f}")
self.gpu_execution_seconds_total.labels(**self.labels, category=category).inc(t)
if dp_cooperation_info is not None: if dp_cooperation_info is not None:
self.dp_cooperation_gpu_execution_seconds_total.labels( self.dp_cooperation_forward_execution_seconds_total.labels(
**self.labels, **self.labels,
category=category, category=category,
**dp_cooperation_info.to_labels(), **dp_cooperation_info.to_labels(),
@@ -1017,25 +1050,22 @@ class SchedulerMetricsCollector:
) )
def log_stats(self, stats: SchedulerStats) -> None: def log_stats(self, stats: SchedulerStats) -> None:
# Basics
self._log_gauge_queue_count(self.num_running_reqs, stats.num_running_reqs) self._log_gauge_queue_count(self.num_running_reqs, stats.num_running_reqs)
self._log_gauge(self.num_used_tokens, stats.num_used_tokens)
self._log_gauge(self.token_usage, stats.token_usage)
self._log_gauge(self.full_token_usage, stats.full_token_usage)
self._log_gauge(
self.pending_prealloc_token_usage, stats.pending_prealloc_token_usage
)
self._log_gauge(self.swa_token_usage, stats.swa_token_usage)
self._log_gauge(self.mamba_usage, stats.mamba_usage)
self._log_gauge(self.decode_sum_seq_lens, stats.decode_sum_seq_lens)
self._log_gauge(self.gen_throughput, stats.gen_throughput)
self._log_gauge_queue_count(self.num_queue_reqs, stats.num_queue_reqs) self._log_gauge_queue_count(self.num_queue_reqs, stats.num_queue_reqs)
self._log_gauge(self.num_grammar_queue_reqs, stats.num_grammar_queue_reqs) self._log_gauge(self.num_grammar_queue_reqs, stats.num_grammar_queue_reqs)
self._log_gauge( self._log_gauge(self.gen_throughput, stats.gen_throughput)
self.num_running_reqs_offline_batch, stats.num_running_reqs_offline_batch
)
self._log_gauge(self.cache_hit_rate, stats.cache_hit_rate) self._log_gauge(self.cache_hit_rate, stats.cache_hit_rate)
self._log_gauge(self.decode_sum_seq_lens, stats.decode_sum_seq_lens)
self._log_gauge(self.max_total_num_tokens, stats.max_total_num_tokens) # Memory pool usage ratios
self._log_gauge(self.token_usage, stats.token_usage)
self._log_gauge(self.full_token_usage, stats.full_token_usage)
self._log_gauge(self.swa_token_usage, stats.swa_token_usage)
self._log_gauge(self.mamba_usage, stats.mamba_usage)
# Absolute token counts
self._log_gauge(self.num_used_tokens, stats.num_used_tokens)
self._log_gauge(self.kv_available_tokens, stats.kv_available_tokens) self._log_gauge(self.kv_available_tokens, stats.kv_available_tokens)
self._log_gauge(self.kv_evictable_tokens, stats.kv_evictable_tokens) self._log_gauge(self.kv_evictable_tokens, stats.kv_evictable_tokens)
self._log_gauge(self.kv_used_tokens, stats.kv_used_tokens) self._log_gauge(self.kv_used_tokens, stats.kv_used_tokens)
@@ -1044,6 +1074,10 @@ class SchedulerMetricsCollector:
self._log_gauge(self.spec_accept_length, stats.spec_accept_length) self._log_gauge(self.spec_accept_length, stats.spec_accept_length)
self._log_gauge(self.spec_accept_rate, stats.spec_accept_rate) self._log_gauge(self.spec_accept_rate, stats.spec_accept_rate)
# Retract
self._log_gauge(self.num_retracted_reqs, stats.num_retracted_reqs)
self._log_gauge(self.num_paused_reqs, stats.num_paused_reqs)
# PD disaggregation # PD disaggregation
self._log_gauge_queue_count( self._log_gauge_queue_count(
self.num_prefill_prealloc_queue_reqs, stats.num_prefill_prealloc_queue_reqs self.num_prefill_prealloc_queue_reqs, stats.num_prefill_prealloc_queue_reqs
@@ -1057,36 +1091,26 @@ class SchedulerMetricsCollector:
self._log_gauge_queue_count( self._log_gauge_queue_count(
self.num_decode_transfer_queue_reqs, stats.num_decode_transfer_queue_reqs self.num_decode_transfer_queue_reqs, stats.num_decode_transfer_queue_reqs
) )
# Retract self._log_gauge(
self._log_gauge(self.num_retracted_reqs, stats.num_retracted_reqs) self.pending_prealloc_token_usage, stats.pending_prealloc_token_usage
self._log_gauge(self.num_paused_reqs, stats.num_paused_reqs) )
# Utilization # Utilization
self._log_gauge(self.utilization, stats.utilization) self._log_gauge(self.utilization, stats.utilization)
if stats.max_running_requests_under_SLO is not None:
self._log_gauge(
self.max_running_requests_under_SLO,
stats.max_running_requests_under_SLO,
)
# Engine startup time # Scheduler policy
self._log_gauge(self.engine_startup_time, stats.engine_startup_time)
if stats.engine_load_weights_time is not None:
self._log_gauge(
self.engine_load_weights_time, stats.engine_load_weights_time
)
self._log_gauge(self.new_token_ratio, stats.new_token_ratio) self._log_gauge(self.new_token_ratio, stats.new_token_ratio)
# CUDA graph # CUDA graph
self._log_gauge(self.is_cuda_graph, stats.is_cuda_graph) self._log_gauge(self.is_cuda_graph, stats.is_cuda_graph)
# LoRA pool metrics (only logged if LoRA is enabled) # LoRA pool metrics
if self.enable_lora: if self.enable_lora:
self._log_gauge(self.lora_pool_slots_used, stats.lora_pool_slots_used) self._log_gauge(self.lora_pool_slots_used, stats.lora_pool_slots_used)
self._log_gauge(self.lora_pool_slots_total, stats.lora_pool_slots_total) self._log_gauge(self.lora_pool_slots_total, stats.lora_pool_slots_total)
self._log_gauge(self.lora_pool_utilization, stats.lora_pool_utilization) self._log_gauge(self.lora_pool_utilization, stats.lora_pool_utilization)
# HiCache host-tier metrics (only logged if hierarchical cache is enabled) # HiCache metrics
if self.enable_hierarchical_cache: if self.enable_hierarchical_cache:
self._log_gauge( self._log_gauge(
self.hicache_host_used_tokens, stats.hicache_host_used_tokens self.hicache_host_used_tokens, stats.hicache_host_used_tokens
@@ -1095,13 +1119,14 @@ class SchedulerMetricsCollector:
self.hicache_host_total_tokens, stats.hicache_host_total_tokens self.hicache_host_total_tokens, stats.hicache_host_total_tokens
) )
# Streaming session metrics (only logged if streaming sessions are enabled) # Streaming session metrics
if self.enable_streaming_session: if self.enable_streaming_session:
self._log_gauge(self.num_streaming_sessions, stats.num_streaming_sessions) self._log_gauge(self.num_streaming_sessions, stats.num_streaming_sessions)
self._log_gauge( self._log_gauge(
self.streaming_session_held_tokens, stats.streaming_session_held_tokens self.streaming_session_held_tokens, stats.streaming_session_held_tokens
) )
# Routing key metrics
self._log_gauge( self._log_gauge(
self.num_unique_running_routing_keys, stats.num_unique_running_routing_keys self.num_unique_running_routing_keys, stats.num_unique_running_routing_keys
) )
@@ -1139,8 +1164,30 @@ class SchedulerMetricsCollector:
) )
self.num_grammar_total.labels(**self.labels).inc(1) self.num_grammar_total.labels(**self.labels).inc(1)
def emit_cache_config_info(self, page_size: int, num_pages: int) -> None: def emit_constants(
self.cache_config_info.labels(page_size=page_size, num_pages=num_pages).set(1) self,
max_total_num_tokens: int,
max_running_requests_under_SLO: Optional[int],
engine_startup_time: float,
engine_load_weights_time: float,
page_size: int,
num_pages: int,
context_len: int,
startup_available_gpu_memory_gb: float,
) -> None:
self._log_gauge(self.max_total_num_tokens, max_total_num_tokens)
if max_running_requests_under_SLO is not None:
self._log_gauge(
self.max_running_requests_under_SLO, max_running_requests_under_SLO
)
self._log_gauge(self.engine_startup_time, engine_startup_time)
self._log_gauge(self.engine_load_weights_time, engine_load_weights_time)
self._log_gauge(self.page_size, page_size)
self._log_gauge(self.num_pages, num_pages)
self._log_gauge(self.context_len, context_len)
self._log_gauge(
self.startup_available_gpu_memory_gb, startup_available_gpu_memory_gb
)
class TokenizerMetricsCollector: class TokenizerMetricsCollector:
@@ -1162,7 +1209,6 @@ class TokenizerMetricsCollector:
documentation="Number of prefill tokens processed.", documentation="Number of prefill tokens processed.",
labelnames=labels.keys(), labelnames=labels.keys(),
) )
self.generation_tokens_total = Counter( self.generation_tokens_total = Counter(
name="sglang:generation_tokens_total", name="sglang:generation_tokens_total",
documentation="Number of generation tokens processed.", documentation="Number of generation tokens processed.",
@@ -1185,20 +1231,25 @@ class TokenizerMetricsCollector:
8000, 8000,
9000, 9000,
10000, 10000,
12000, 12500,
15000, 15000,
17500,
20000, 20000,
22000, 22500,
25000, 25000,
27500,
30000, 30000,
35000, 35000,
40000, 40000,
66000, 60000,
99000, 80000,
132000, 100000,
200000,
300000, 300000,
400000,
600000, 600000,
900000, 800000,
1000000,
1100000, 1100000,
] ]
self.prompt_tokens_histogram = Histogram( self.prompt_tokens_histogram = Histogram(
@@ -1339,34 +1390,6 @@ class TokenizerMetricsCollector:
buckets=bucket_e2e_request_latency, buckets=bucket_e2e_request_latency,
) )
# Retraction count histogram
self.num_retractions = Histogram(
name="sglang:num_retractions",
documentation="Histogram of retraction counts per request.",
labelnames=labels.keys(),
buckets=[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
15,
20,
25,
30,
40,
50,
75,
100,
],
)
def observe_one_finished_request( def observe_one_finished_request(
self, self,
labels: Dict[str, str], labels: Dict[str, str],
@@ -1375,7 +1398,6 @@ class TokenizerMetricsCollector:
cached_tokens: int, cached_tokens: int,
e2e_latency: float, e2e_latency: float,
has_grammar: bool, has_grammar: bool,
retraction_count: int,
cached_tokens_details: Optional[Dict[str, Any]] = None, cached_tokens_details: Optional[Dict[str, Any]] = None,
): ):
self.prompt_tokens_total.labels(**labels).inc(prompt_tokens) self.prompt_tokens_total.labels(**labels).inc(prompt_tokens)
@@ -1414,7 +1436,6 @@ class TokenizerMetricsCollector:
self.generation_tokens_histogram.labels(**labels).observe( self.generation_tokens_histogram.labels(**labels).observe(
float(generation_tokens) float(generation_tokens)
) )
self.num_retractions.labels(**labels).observe(retraction_count)
def observe_time_to_first_token(self, labels: Dict[str, str], value: float): def observe_time_to_first_token(self, labels: Dict[str, str], value: float):
self.histogram_time_to_first_token.labels(**labels).observe(value) self.histogram_time_to_first_token.labels(**labels).observe(value)
@@ -1661,3 +1682,17 @@ class RadixCacheMetricsCollector:
def observe_load_back_duration(self, duration_seconds: float) -> None: def observe_load_back_duration(self, duration_seconds: float) -> None:
self.load_back_duration_seconds.labels(**self.labels).observe(duration_seconds) self.load_back_duration_seconds.labels(**self.labels).observe(duration_seconds)
def get_histogram_conf_from_env(env_var_name: str) -> Optional[List[float]]:
"""
Get the histogram configuration from the environment variable.
env value should be like "0.1,0.2,0.5,1,2"
"""
if env_var_name not in os.environ:
return None
# if the env var is not set or empty, return None
env_var_value = os.environ[env_var_name]
if not env_var_value:
return None
return [float(x) for x in env_var_value.split(",")]
@@ -4,7 +4,6 @@ import dataclasses
import logging import logging
import time import time
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager
from typing import TYPE_CHECKING, List, Optional, Tuple, Union from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from sglang.srt.disaggregation.kv_events import EventPublisherFactory, KVEventBatch from sglang.srt.disaggregation.kv_events import EventPublisherFactory, KVEventBatch
@@ -28,7 +27,7 @@ from sglang.srt.observability.metrics_collector import (
SchedulerStats, SchedulerStats,
compute_routing_key_stats, compute_routing_key_stats,
) )
from sglang.srt.utils.device_timer import DeviceTimer, GapTimer from sglang.srt.utils.device_timer import DeviceTimer
from sglang.srt.utils.scheduler_status_logger import SchedulerStatusLogger from sglang.srt.utils.scheduler_status_logger import SchedulerStatusLogger
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -74,16 +73,16 @@ class PrefillStats:
) )
@dataclasses.dataclass
class KvMetrics: class KvMetrics:
def __init__(self): request_active_slots: int = 0
self.request_active_slots = None request_total_slots: int = 0
self.request_total_slots = None kv_active_blocks: int = 0
self.kv_active_blocks = None kv_total_blocks: int = 0
self.kv_total_blocks = None num_requests_waiting: int = 0
self.num_requests_waiting = None gpu_cache_usage_perc: float = 0.0
self.gpu_cache_usage_perc = None gpu_prefix_cache_hit_rate: float = 0.0
self.gpu_prefix_cache_hit_rate = None data_parallel_rank: int = 0
self.data_parallel_rank = None
class SchedulerMetricsMixin: class SchedulerMetricsMixin:
@@ -98,6 +97,12 @@ class SchedulerMetricsMixin:
self.last_gen_throughput: float = 0.0 self.last_gen_throughput: float = 0.0
self.last_input_throughput: float = 0.0 self.last_input_throughput: float = 0.0
self.step_time_dict = defaultdict(list) # Dict[batch size -> step time] self.step_time_dict = defaultdict(list) # Dict[batch size -> step time]
self.stats = SchedulerStats()
self._graph_backend_label = {
"cpu": "cpu graph",
"npu": "npu graph",
"musa": "musa graph",
}.get(getattr(self, "device", ""), "cuda graph")
# Cumulative spec-decoding counters (reset every decode_log_interval). # Cumulative spec-decoding counters (reset every decode_log_interval).
# Each update adds (num_accepted_drafts + bs, bs). # Each update adds (num_accepted_drafts + bs, bs).
@@ -111,15 +116,15 @@ class SchedulerMetricsMixin:
self.kv_transfer_speed_gb_s: float = 0.0 self.kv_transfer_speed_gb_s: float = 0.0
self.kv_transfer_latency_ms: float = 0.0 self.kv_transfer_latency_ms: float = 0.0
self.stats = SchedulerStats()
# Metrics # Metrics
self.enable_mfu_metrics = False
self.enable_metrics = self.server_args.enable_metrics self.enable_metrics = self.server_args.enable_metrics
self.is_stats_logging_rank = self.attn_tp_rank == 0 self.is_stats_logging_rank = self.attn_tp_rank == 0
self.current_scheduler_metrics_enabled = self.enable_metrics and ( self.current_scheduler_metrics_enabled = self.enable_metrics and (
self.attn_tp_rank == 0 or self.server_args.enable_metrics_for_all_schedulers self.is_stats_logging_rank
or self.server_args.enable_metrics_for_all_schedulers
) )
self.enable_mfu_metrics = False
if self.enable_metrics: if self.enable_metrics:
engine_type = DisaggregationMode.to_engine_type( engine_type = DisaggregationMode.to_engine_type(
self.server_args.disaggregation_mode self.server_args.disaggregation_mode
@@ -145,20 +150,27 @@ class SchedulerMetricsMixin:
enable_streaming_session=self.server_args.enable_streaming_session, enable_streaming_session=self.server_args.enable_streaming_session,
server_args=self.server_args, server_args=self.server_args,
) )
self.enable_mfu_metrics = bool(self.server_args.enable_mfu_metrics) self.enable_mfu_metrics = self.server_args.enable_mfu_metrics
if self.enable_mfu_metrics: if self.enable_mfu_metrics:
self._init_estimated_perf_constants() self._init_estimated_perf_constants()
self._mfu_log_flops = 0.0 self._mfu_log_flops = 0.0
self._mfu_log_read_bytes = 0.0 self._mfu_log_read_bytes = 0.0
self._mfu_log_write_bytes = 0.0 self._mfu_log_write_bytes = 0.0
if ENABLE_METRICS_DEVICE_TIMER: if ENABLE_METRICS_DEVICE_TIMER:
self.forward_pass_device_timer = DeviceTimer( self._device_timer_window_batch_count = 0
reporter=self.metrics_collector.increment_gpu_execution_seconds, self._device_timer_window_gpu_time = 0.0
) self._device_timer_window_start = None
self.bubble_timer = GapTimer( self.fwd_occupancy = float("nan")
reporter=self.metrics_collector.increment_gpu_overlap_wait_seconds,
) def _wrap_execution_reporter(**kwargs):
self._device_timer_window_gpu_time += kwargs["t"]
if self.enable_metrics:
self.metrics_collector.increment_forward_execution_seconds(**kwargs)
self.forward_pass_device_timer = DeviceTimer(
reporter=_wrap_execution_reporter,
)
self.init_kv_events(self.server_args.kv_events_config) self.init_kv_events(self.server_args.kv_events_config)
@@ -369,16 +381,8 @@ class SchedulerMetricsMixin:
and self.server_args.encoder_transfer_backend == "zmq_to_scheduler" and self.server_args.encoder_transfer_backend == "zmq_to_scheduler"
): ):
msg += f"waiting-image-req: {len(self.mm_receiver.waiting_list)}, " msg += f"waiting-image-req: {len(self.mm_receiver.waiting_list)}, "
graph_backend = defaultdict(
lambda: "cuda graph",
{
"cpu": "cpu graph",
"npu": "npu graph",
"musa": "musa graph",
},
)
msg += f"{graph_backend[self.device]}: {can_run_cuda_graph}, " msg += f"{self._graph_backend_label}: {can_run_cuda_graph}, "
msg += f"input throughput (token/s): {self.last_input_throughput:.2f}" msg += f"input throughput (token/s): {self.last_input_throughput:.2f}"
if self.enable_mfu_metrics and gap_latency > 0: if self.enable_mfu_metrics and gap_latency > 0:
@@ -386,9 +390,11 @@ class SchedulerMetricsMixin:
tflops_per_s = flops / gap_latency / 1e12 tflops_per_s = flops / gap_latency / 1e12
msg += f", est. prefill TFLOPS/s (per GPU): {tflops_per_s:.2f}" msg += f", est. prefill TFLOPS/s (per GPU): {tflops_per_s:.2f}"
if ENABLE_METRICS_DEVICE_TIMER:
msg += f", fwd occupancy: {self.fwd_occupancy:.2f}%"
if self.is_stats_logging_rank: if self.is_stats_logging_rank:
logger.info(msg) logger.info(msg)
if self.current_scheduler_metrics_enabled: if self.current_scheduler_metrics_enabled:
self.metrics_collector.increment_prefill_cuda_graph_pass( self.metrics_collector.increment_prefill_cuda_graph_pass(
value=can_run_cuda_graph value=can_run_cuda_graph
@@ -408,24 +414,22 @@ class SchedulerMetricsMixin:
num_write_bytes_per_gpu=write_bytes, num_write_bytes_per_gpu=write_bytes,
) )
# Basics priority_enabled = self.enable_priority_scheduling
total_tokens = prefill_stats.log_input_tokens + prefill_stats.log_hit_tokens total_tokens = prefill_stats.log_input_tokens + prefill_stats.log_hit_tokens
cache_hit_rate = ( cache_hit_rate = (
prefill_stats.log_hit_tokens / total_tokens if total_tokens > 0 else 0.0 prefill_stats.log_hit_tokens / total_tokens if total_tokens > 0 else 0.0
) )
# Basics
self.stats.num_running_reqs = prefill_stats.num_running_reqs self.stats.num_running_reqs = prefill_stats.num_running_reqs
self.stats.num_running_reqs_offline_batch = 0
pool_stats.update_scheduler_stats(self.stats)
priority_enabled = self.enable_priority_scheduling
self.stats.num_queue_reqs = QueueCount.from_reqs( self.stats.num_queue_reqs = QueueCount.from_reqs(
self.waiting_queue, priority_enabled self.waiting_queue, priority_enabled
) )
self.stats.num_grammar_queue_reqs = len(self.grammar_manager) self.stats.num_grammar_queue_reqs = len(self.grammar_manager)
self.stats.cache_hit_rate = cache_hit_rate self.stats.cache_hit_rate = cache_hit_rate
self.stats.max_total_num_tokens = self.max_total_num_tokens # Memory pool usage ratios / Absolute token counts
pool_stats.update_scheduler_stats(self.stats)
# Retract # Retract
self.stats.num_retracted_reqs = self.num_retracted_reqs self.stats.num_retracted_reqs = self.num_retracted_reqs
@@ -450,7 +454,7 @@ class SchedulerMetricsMixin:
self.disagg_decode_transfer_queue.queue, priority_enabled self.disagg_decode_transfer_queue.queue, priority_enabled
) )
# Others # Utilization / LoRA / HiCache
self.calculate_utilization() self.calculate_utilization()
self.update_lora_metrics() self.update_lora_metrics()
self._log_hicache_stats() self._log_hicache_stats()
@@ -505,7 +509,6 @@ class SchedulerMetricsMixin:
self.num_generated_tokens = 0 self.num_generated_tokens = 0
num_running_reqs = len(batch.reqs) num_running_reqs = len(batch.reqs)
num_running_reqs_offline_batch = 0
pool_stats = self.get_pool_stats() pool_stats = self.get_pool_stats()
token_usage_msg = ", ".join(pool_stats.get_decode_usage_msg_parts()) + ", " token_usage_msg = ", ".join(pool_stats.get_decode_usage_msg_parts()) + ", "
@@ -556,16 +559,8 @@ class SchedulerMetricsMixin:
): ):
msg += f"waiting-image-req: {len(self.mm_receiver.waiting_list)}, " msg += f"waiting-image-req: {len(self.mm_receiver.waiting_list)}, "
graph_backend = defaultdict(
lambda: "cuda graph",
{
"cpu": "cpu graph",
"npu": "npu graph",
"musa": "musa graph",
},
)
msg += ( msg += (
f"{graph_backend[self.device]}: {can_run_cuda_graph}, " f"{self._graph_backend_label}: {can_run_cuda_graph}, "
f"gen throughput (token/s): {self.last_gen_throughput:.2f}, " f"gen throughput (token/s): {self.last_gen_throughput:.2f}, "
f"#queue-req: {len(self.waiting_queue)}" f"#queue-req: {len(self.waiting_queue)}"
) )
@@ -586,31 +581,32 @@ class SchedulerMetricsMixin:
self._mfu_log_read_bytes = 0.0 self._mfu_log_read_bytes = 0.0
self._mfu_log_write_bytes = 0.0 self._mfu_log_write_bytes = 0.0
if ENABLE_METRICS_DEVICE_TIMER:
msg += f", fwd occupancy: {self.fwd_occupancy:.2f}%"
if self.is_stats_logging_rank: if self.is_stats_logging_rank:
logger.info(msg) logger.info(msg)
if self.current_scheduler_metrics_enabled: if self.current_scheduler_metrics_enabled:
priority_enabled = self.enable_priority_scheduling priority_enabled = self.enable_priority_scheduling
# Basics # Basics
self.stats.num_running_reqs = QueueCount.from_reqs( self.stats.num_running_reqs = QueueCount.from_reqs(
batch.reqs, priority_enabled batch.reqs, priority_enabled
) )
self.stats.num_running_reqs_offline_batch = num_running_reqs_offline_batch
pool_stats.update_scheduler_stats(self.stats)
self.stats.decode_sum_seq_lens = batch.seq_lens_cpu.sum().item()
self.stats.gen_throughput = self.last_gen_throughput
self.stats.num_queue_reqs = QueueCount.from_reqs( self.stats.num_queue_reqs = QueueCount.from_reqs(
self.waiting_queue, priority_enabled self.waiting_queue, priority_enabled
) )
self.stats.num_grammar_queue_reqs = len(self.grammar_manager) self.stats.num_grammar_queue_reqs = len(self.grammar_manager)
self.stats.gen_throughput = self.last_gen_throughput
self.stats.cache_hit_rate = cache_hit_rate self.stats.cache_hit_rate = cache_hit_rate
self.stats.decode_sum_seq_lens = batch.seq_lens_cpu.sum().item()
self.stats.max_total_num_tokens = self.max_total_num_tokens # Memory pool usage ratios / Absolute token counts
self.stats.num_streaming_sessions = self._streaming_session_count() pool_stats.update_scheduler_stats(self.stats)
self.stats.streaming_session_held_tokens = self._session_held_tokens()
# Speculative decoding # Speculative decoding
self.stats.spec_accept_rate = spec_accept_rate
self.stats.spec_accept_length = spec_accept_length self.stats.spec_accept_length = spec_accept_length
self.stats.spec_accept_rate = spec_accept_rate
# Retract # Retract
self.stats.num_retracted_reqs = self.num_retracted_reqs self.stats.num_retracted_reqs = self.num_retracted_reqs
@@ -632,17 +628,25 @@ class SchedulerMetricsMixin:
self.stats.num_decode_transfer_queue_reqs = QueueCount.from_reqs( self.stats.num_decode_transfer_queue_reqs = QueueCount.from_reqs(
self.disagg_decode_transfer_queue.queue, priority_enabled self.disagg_decode_transfer_queue.queue, priority_enabled
) )
running_routing_keys = [r.routing_key for r in batch.reqs]
waiting_routing_keys = [r.routing_key for r in self.waiting_queue]
(
self.stats.num_unique_running_routing_keys,
self.stats.routing_key_running_req_counts,
) = compute_routing_key_stats(running_routing_keys)
_, self.stats.routing_key_all_req_counts = compute_routing_key_stats(
running_routing_keys + waiting_routing_keys
)
# Others # Streaming session metrics
self.stats.num_streaming_sessions = self._streaming_session_count()
self.stats.streaming_session_held_tokens = self._session_held_tokens()
# Routing key metrics
# (to reduce the overhead, we only compute this when all requests have routing_key)
if all(r.routing_key is not None for r in batch.reqs):
running_routing_keys = [r.routing_key for r in batch.reqs]
waiting_routing_keys = [r.routing_key for r in self.waiting_queue]
(
self.stats.num_unique_running_routing_keys,
self.stats.routing_key_running_req_counts,
) = compute_routing_key_stats(running_routing_keys)
_, self.stats.routing_key_all_req_counts = compute_routing_key_stats(
running_routing_keys + waiting_routing_keys
)
# Utilization / LoRA / HiCache
self.calculate_utilization() self.calculate_utilization()
self.update_lora_metrics() self.update_lora_metrics()
self._log_hicache_stats() self._log_hicache_stats()
@@ -759,13 +763,10 @@ class SchedulerMetricsMixin:
if self.disaggregation_mode == DisaggregationMode.PREFILL: if self.disaggregation_mode == DisaggregationMode.PREFILL:
self.stats.utilization = -1 self.stats.utilization = -1
else: else:
if ( max_under_slo = getattr(self, "max_running_requests_under_SLO", None)
self.stats.max_running_requests_under_SLO is not None if max_under_slo is not None and max_under_slo > 0:
and self.stats.max_running_requests_under_SLO > 0
):
self.stats.utilization = max( self.stats.utilization = max(
self.stats.num_running_reqs.total self.stats.num_running_reqs.total / max_under_slo,
/ self.stats.max_running_requests_under_SLO,
self.stats.token_usage / 0.9, self.stats.token_usage / 0.9,
) )
@@ -918,36 +919,30 @@ class SchedulerMetricsMixin:
queues=queues, queues=queues,
) )
@contextmanager def update_device_timer(self: Scheduler):
def record_forward_metrics(self: Scheduler, batch: ScheduleBatch): if not ENABLE_METRICS_DEVICE_TIMER:
if not (self.enable_metrics and ENABLE_METRICS_DEVICE_TIMER):
yield
return return
self.forward_pass_device_timer._report()
category = "forward_" + batch.forward_mode.name.lower() now = time.perf_counter()
with self.forward_pass_device_timer.wrap( if self._device_timer_window_batch_count == 0:
metadata=dict( self._device_timer_window_start = now
category=category, self._device_timer_window_gpu_time = 0.0
dp_cooperation_info=batch.dp_cooperation_info, cpu_time = 0
), self.fwd_occupancy = float("nan")
else:
cpu_time = now - self._device_timer_window_start
self.fwd_occupancy = min(
self._device_timer_window_gpu_time / cpu_time * 100, 100
)
# ratio = self._device_timer_window_gpu_time / cpu_time if cpu_time > 0 else float("nan")
# print(f"{self._device_timer_window_batch_count=} {self.fwd_occupancy=}, {self._device_timer_window_gpu_time=}, {cpu_time=}, {ratio=}")
self._device_timer_window_batch_count += 1
if (
self._device_timer_window_batch_count
>= self.server_args.decode_log_interval
): ):
yield self._device_timer_window_batch_count = 0
@contextmanager def reset_device_timer_window(self: Scheduler):
def record_bubble_metrics(self: Scheduler, batch: ScheduleBatch): if ENABLE_METRICS_DEVICE_TIMER:
if not (self.enable_metrics and ENABLE_METRICS_DEVICE_TIMER): self._device_timer_window_batch_count = 0
yield
return
category = "forward_" + batch.forward_mode.name.lower()
with self.bubble_timer.wrap(
metadata=dict(
category=category,
dp_cooperation_info=batch.dp_cooperation_info,
),
):
yield
def cancel_bubble_timer(self: Scheduler):
if self.enable_metrics and ENABLE_METRICS_DEVICE_TIMER:
self.bubble_timer.cancel()
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import bisect import bisect
import contextlib
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Optional from typing import TYPE_CHECKING, Callable, Optional
@@ -216,7 +217,13 @@ class EAGLEDraftCudaGraphRunner:
return out return out
def _replay(self, forward_batch: ForwardBatch): def _replay(self, forward_batch: ForwardBatch):
self.graphs[self.bs].replay() ctx = (
self.model_runner.device_timer.wrap(metadata={"category": "eagle_draft"})
if self.model_runner.device_timer
else contextlib.nullcontext()
)
with ctx:
self.graphs[self.bs].replay()
def capture(self): def capture(self):
CudaGraphRunner.capture(self) CudaGraphRunner.capture(self)
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import bisect import bisect
import contextlib
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Optional from typing import TYPE_CHECKING, Callable, Optional
@@ -282,7 +283,15 @@ class EAGLEDraftExtendCudaGraphRunner:
return out return out
def _replay(self, forward_batch: ForwardBatch): def _replay(self, forward_batch: ForwardBatch):
self.graphs[self.bs].replay() ctx = (
self.model_runner.device_timer.wrap(
metadata={"category": "eagle_draft_extend"}
)
if self.model_runner.device_timer
else contextlib.nullcontext()
)
with ctx:
self.graphs[self.bs].replay()
def capture(self): def capture(self):
CudaGraphRunner.capture(self) CudaGraphRunner.capture(self)
+1
View File
@@ -28,6 +28,7 @@ class DeviceTimer:
self._intervals.popleft() self._intervals.popleft()
self._reporter(t=interval.elapsed_time() / 1000.0, **interval.metadata) self._reporter(t=interval.elapsed_time() / 1000.0, **interval.metadata)
# print(f"{interval.elapsed_time()=:.6f}, {interval.metadata=}")
class GapTimer(DeviceTimer): class GapTimer(DeviceTimer):
@@ -15,11 +15,9 @@ from __future__ import annotations
import dataclasses import dataclasses
import logging import logging
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.utils.common import get_bool_env_var
from sglang.srt.utils.log_utils import create_log_targets, log_json from sglang.srt.utils.log_utils import create_log_targets, log_json
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -237,12 +235,6 @@ class RequestLogger:
target.info(msg) target.info(msg)
# TODO remove this?
@lru_cache(maxsize=2)
def disable_request_logging() -> bool:
return get_bool_env_var("SGLANG_DISABLE_REQUEST_LOGGING")
# TODO unify this w/ `_transform_data_for_logging` if we find performance enough # TODO unify this w/ `_transform_data_for_logging` if we find performance enough
def _dataclass_to_string_truncated( def _dataclass_to_string_truncated(
data: Any, max_length: int = 2048, skip_names: Optional[Set[str]] = None data: Any, max_length: int = 2048, skip_names: Optional[Set[str]] = None
+17 -15
View File
@@ -62,12 +62,12 @@ class TestEnableMetrics(CustomTestCase):
{"mode": "decode"}, {"mode": "decode"},
), ),
( (
"sglang:dp_cooperation_gpu_execution_seconds_total", "sglang:dp_cooperation_forward_execution_seconds_total",
{"category": "forward_extend"}, {"category": "extend"},
), ),
( (
"sglang:dp_cooperation_gpu_execution_seconds_total", "sglang:dp_cooperation_forward_execution_seconds_total",
{"category": "forward_decode"}, {"category": "decode"},
), ),
] ]
_check_metrics_positive(self, metrics, metrics_to_check) _check_metrics_positive(self, metrics, metrics_to_check)
@@ -129,15 +129,17 @@ class TestEnableMetrics(CustomTestCase):
for _ in response.iter_lines(decode_unicode=False): for _ in response.iter_lines(decode_unicode=False):
pass pass
response = requests.post( for i in range(2):
f"{DEFAULT_URL_FOR_TEST}/generate", # Send the request twice to trigger cached token metrics
json={ response = requests.post(
"text": "Hello", f"{DEFAULT_URL_FOR_TEST}/generate",
"sampling_params": {"temperature": 0, "max_new_tokens": 5}, json={
}, "text": "Hello, " * 100,
headers={"x-smg-routing-key": "test-key"}, "sampling_params": {"temperature": 0, "max_new_tokens": 5},
) },
self.assertEqual(response.status_code, 200) headers={"x-smg-routing-key": "test-key"},
)
self.assertEqual(response.status_code, 200)
# Get metrics # Get metrics
metrics_response = requests.get(f"{DEFAULT_URL_FOR_TEST}/metrics") metrics_response = requests.get(f"{DEFAULT_URL_FOR_TEST}/metrics")
@@ -209,8 +211,8 @@ class TestEnableMetrics(CustomTestCase):
metrics_to_check = [ metrics_to_check = [
("sglang:realtime_tokens_total", {"mode": "prefill_compute"}), ("sglang:realtime_tokens_total", {"mode": "prefill_compute"}),
("sglang:realtime_tokens_total", {"mode": "decode"}), ("sglang:realtime_tokens_total", {"mode": "decode"}),
("sglang:gpu_execution_seconds_total", {"category": "forward_extend"}), ("sglang:forward_execution_seconds_total", {"category": "extend"}),
("sglang:gpu_execution_seconds_total", {"category": "forward_decode"}), ("sglang:forward_execution_seconds_total", {"category": "decode"}),
("sglang:process_cpu_seconds_total", {"component": "tokenizer"}), ("sglang:process_cpu_seconds_total", {"component": "tokenizer"}),
] ]
_check_metrics_positive(self, metrics, metrics_to_check) _check_metrics_positive(self, metrics, metrics_to_check)
@@ -1,12 +1,17 @@
import os
import re
import subprocess
import unittest import unittest
import numpy as np
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST, DEFAULT_MODEL_NAME_FOR_TEST,
DEFAULT_SMALL_MODEL_NAME_FOR_TEST, DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
CustomTestCase, CustomTestCase,
is_in_ci, is_in_ci,
run_bench_offline_throughput, kill_process_tree,
run_bench_one_batch, run_bench_one_batch,
write_github_step_summary, write_github_step_summary,
) )
@@ -24,10 +29,46 @@ class TestBenchOneBatch1GPU(CustomTestCase):
self.assertGreater(output_throughput, 50) self.assertGreater(output_throughput, 50)
def test_bs1_default(self): def test_bs1_default(self):
output_throughput = run_bench_offline_throughput( env = os.environ.copy()
DEFAULT_MODEL_NAME_FOR_TEST, ["--cuda-graph-max-bs", "2"] env["SGLANG_ENABLE_METRICS_DEVICE_TIMER"] = "1"
command = [
"python3",
"-m",
"sglang.bench_offline_throughput",
"--num-prompts",
"1",
"--dataset-name",
"random",
"--random-input-len",
"256",
"--random-output-len",
"1024",
"--model-path",
DEFAULT_MODEL_NAME_FOR_TEST,
"--cuda-graph-max-bs",
"2",
]
print(f"command={' '.join(command)}")
process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
) )
try:
stdout, stderr = process.communicate()
output = stdout.decode(errors="backslashreplace")
error = stderr.decode(errors="backslashreplace")
print(f"Output: {output}", flush=True)
print(f"Error: {error}", flush=True)
output_throughput = -1
for line in output.split("\n"):
if "Last generation throughput (tok/s):" in line:
output_throughput = float(line.split(":")[-1])
finally:
kill_process_tree(process.pid)
if is_in_ci(): if is_in_ci():
write_github_step_summary( write_github_step_summary(
f"### test_bs1_default (llama-3.1-8b)\n" f"### test_bs1_default (llama-3.1-8b)\n"
@@ -35,6 +76,23 @@ class TestBenchOneBatch1GPU(CustomTestCase):
) )
self.assertGreater(output_throughput, 135) self.assertGreater(output_throughput, 135)
fwd_occupancy_values = []
for line in error.split("\n"):
match = re.search(r"fwd occupancy:\s*([\d.]+|nan)%", line)
if match:
val = match.group(1)
if val != "nan":
fwd_occupancy_values.append(float(val))
print(f"{fwd_occupancy_values=}", flush=True)
self.assertGreater(
len(fwd_occupancy_values), 0, "No fwd occupancy values found in logs"
)
fwd_occupancy_p90 = float(np.percentile(fwd_occupancy_values, 90))
print(f"{fwd_occupancy_p90=}", flush=True)
self.assertGreater(fwd_occupancy_p90, 97.5)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()