Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1247 lines
44 KiB
Python

# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Utilities for Request Time Stats."""
from __future__ import annotations
import logging
import time
import uuid
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from typing_extensions import Self
from sglang.srt.disaggregation.utils import DisaggregationMode
from sglang.srt.model_executor.forward_batch_info import ForwardMode
from sglang.srt.observability.metrics_collector import (
EncoderMetricsCollector,
SchedulerMetricsCollector,
TokenizerMetricsCollector,
)
from sglang.srt.observability.trace import (
SpanAttributes,
TraceNullContext,
TraceReqContext,
TraceSliceContext,
get_global_tracing_enabled,
)
from sglang.srt.utils import get_bool_env_var
if TYPE_CHECKING:
from sglang.srt.disaggregation.base.conn import KVTransferMetric
from sglang.srt.managers.schedule_batch import ScheduleBatch
SGLANG_TEST_REQUEST_TIME_STATS = get_bool_env_var("SGLANG_TEST_REQUEST_TIME_STATS")
logger = logging.getLogger(__name__)
# Reduce system time calls by computing time.time() based on calibrated perf_counter() values.
global_diff_realtime_monotonic = time.time() - time.perf_counter()
def calibrate_time_diff():
# due to NTP, the diff between time.time() and time.perf_counter() can change
# periodically calibrate the diff
global global_diff_realtime_monotonic
global_diff_realtime_monotonic = time.time() - time.perf_counter()
real_time = time.time
monotonic_time = time.perf_counter
def convert_time_to_realtime(time_value: float) -> float:
# note: Within the time scale of a single request's latency,
# we assume that the diff does not change significantly.
return time_value + global_diff_realtime_monotonic
def convert_time_to_realtime_ns(time_value: float) -> int:
return int((time_value + global_diff_realtime_monotonic) * 1e9)
def convert_time_cross_thread(
time_value: float, old_diff: float, new_diff: float
) -> float:
# note: precision loss
return time_value + old_diff - new_diff
@dataclass
class RequestStageConfig:
"""Configuration for a request pipeline stage.
Attributes:
stage_name: Name used for metrics labels and trace span names.
level: Trace hierarchy depth.
1 = leaf stages (atomic operations, e.g. TOKENIZE, PREFILL_FORWARD),
2 = parent/dispatch stages (e.g. API_SERVER_DISPATCH, REQUEST_PROCESS),
3 = composite/nested stages (e.g. DECODE_LOOP, PREFILL_CHUNKED_FORWARD).
metrics_is_observed: Whether to call metrics_collector.observe_per_stage_req_latency.
"""
stage_name: str
level: int = 0
metrics_is_observed: bool = False
class RequestStage:
# Tokenizer/gRPC Server
TOKENIZE = RequestStageConfig(
"tokenize",
level=1,
)
API_SERVER_DISPATCH = RequestStageConfig(
"api_server_dispatch",
level=2,
)
# DP controller
DPC_DISPATCH = RequestStageConfig(
"dpc_dispatch",
level=2,
)
# common/non-disaggregation
REQUEST_PROCESS = RequestStageConfig(
"request_process",
level=2,
metrics_is_observed=True,
)
PREFILL_WAITING = RequestStageConfig(
"prefill_waiting",
level=1,
# equal to "observe_queue_time"
metrics_is_observed=False,
)
DECODE_FORWARD = RequestStageConfig(
"decode_forward",
level=1,
)
DECODE_LOOP = RequestStageConfig(
"decode_loop",
level=3,
)
PREFILL_FORWARD = RequestStageConfig(
"prefill_forward",
level=1,
metrics_is_observed=True,
)
PREFILL_CHUNKED_FORWARD = RequestStageConfig(
"chunked_prefill",
level=3,
metrics_is_observed=True,
)
# EPD disaggregation Encode process
MM_ENCODE = RequestStageConfig(
"mm_encode",
level=1,
)
# disaggregation prefill
PREFILL_PREPARE = RequestStageConfig(
"prefill_prepare",
level=1,
)
PREFILL_BOOTSTRAP = RequestStageConfig(
"prefill_bootstrap",
level=1,
metrics_is_observed=True,
)
PREFILL_TRANSFER_KV_CACHE = RequestStageConfig(
"prefill_transfer_kv_cache",
level=1,
metrics_is_observed=True,
)
# disaggregation decode
DECODE_PREPARE = RequestStageConfig(
"decode_prepare",
level=1,
metrics_is_observed=True,
)
DECODE_BOOTSTRAP = RequestStageConfig(
"decode_bootstrap",
level=1,
metrics_is_observed=True,
)
DECODE_WAITING = RequestStageConfig(
"decode_waiting",
level=1,
metrics_is_observed=True,
)
DECODE_TRANSFERRED = RequestStageConfig(
"decode_transferred",
level=1,
metrics_is_observed=True,
)
DECODE_FAKE_OUTPUT = RequestStageConfig(
"fake_output",
level=3,
metrics_is_observed=True,
)
DECODE_QUICK_FINISH = RequestStageConfig(
"quick_finish",
level=1,
metrics_is_observed=True,
)
# speculative decode
SPEC_DRAFT = RequestStageConfig(
"spec_draft",
level=2,
)
SPEC_VERIFY = RequestStageConfig(
"spec_verify",
level=2,
)
# CPU-side run batch
RUN_BATCH_CPU = RequestStageConfig(
"run_batch_cpu",
level=4,
)
# other
ANONYMOUS = RequestStageConfig("")
@dataclass
class ReqTimeStatsBase:
enable_metrics: bool = False
metrics_collector: Optional[
Union[
SchedulerMetricsCollector,
TokenizerMetricsCollector,
EncoderMetricsCollector,
]
] = None
trace_ctx: Union[TraceReqContext, TraceNullContext] = field(
default_factory=TraceNullContext
)
disagg_mode: DisaggregationMode = DisaggregationMode.NULL
diff_realtime_monotonic: float = 0.0
@classmethod
def new_from_obj(cls, obj: Optional[ReqTimeStatsBase], *args, **kwargs) -> Self:
calibrate_time_diff()
new_obj = cls(*args, **kwargs)
if obj is None:
return new_obj
for key, value in obj.__dict__.items():
if hasattr(new_obj, key):
setattr(new_obj, key, value)
if new_obj.trace_ctx.tracing_enable:
new_obj.trace_ctx.rebuild_thread_context()
return new_obj
def disagg_mode_str(self) -> str:
if self.disagg_mode == DisaggregationMode.NULL:
return "unified"
elif self.disagg_mode == DisaggregationMode.DECODE:
return "decode"
elif self.disagg_mode == DisaggregationMode.PREFILL:
return "prefill"
else:
return "unknown"
def set_metrics_collector(
self,
collector: Union[
SchedulerMetricsCollector,
TokenizerMetricsCollector,
EncoderMetricsCollector,
],
):
if collector:
self.enable_metrics = True
self.metrics_collector = collector
def observe_per_stage_req_latency(self, stage: RequestStageConfig, latency: float):
if self.enable_metrics and stage.metrics_is_observed:
self.metrics_collector.observe_per_stage_req_latency(
stage.stage_name, latency
)
def init_trace_ctx(
self,
rid: str,
bootstrap_room: Optional[int],
external_trace_header: Optional[Dict[str, str]] = None,
):
self.trace_ctx = TraceReqContext(
rid=rid,
bootstrap_room=bootstrap_room,
role=self.disagg_mode_str(),
module_name="request",
external_trace_header=external_trace_header,
)
if not self.trace_ctx.tracing_enable:
self.trace_ctx = TraceNullContext()
def trace_slice(
self,
stage: RequestStageConfig,
start_time: float,
end_time: float,
attrs: Optional[Dict] = None,
):
if self.trace_ctx.tracing_enable:
_slice = TraceSliceContext(
slice_name=stage.stage_name,
start_time_ns=convert_time_to_realtime_ns(start_time),
end_time_ns=convert_time_to_realtime_ns(end_time),
level=stage.level,
attrs=attrs,
)
self.trace_ctx.trace_slice(_slice)
def __getstate__(self) -> object:
# The object is propagated to other processes via serialization and deserialization methods,
# requiring the metric collector to be reconfigured.
trace_ctx_state = (
self.trace_ctx.__getstate__()
if self.trace_ctx.tracing_enable
else {"tracing_enable": False}
)
return {
"disagg_mode": self.disagg_mode.value if self.disagg_mode else None,
"enable_metrics": False,
"trace_ctx": trace_ctx_state,
"diff_realtime_monotonic": global_diff_realtime_monotonic,
}
def __setstate__(self, state: object):
# Reconstruct disagg_mode from string value if needed
disagg_mode_val = state.get("disagg_mode")
if isinstance(disagg_mode_val, str):
state["disagg_mode"] = DisaggregationMode(disagg_mode_val)
# Reconstruct trace_ctx from serialized dict if needed
trace_ctx_state = state.get("trace_ctx")
if isinstance(trace_ctx_state, dict):
if trace_ctx_state.get("tracing_enable"):
trace_ctx = object.__new__(TraceReqContext)
trace_ctx.__setstate__(trace_ctx_state)
state["trace_ctx"] = trace_ctx
else:
state["trace_ctx"] = TraceNullContext()
for key in state.keys():
if key.endswith("time"):
state[key] = convert_time_cross_thread(
state[key],
state["diff_realtime_monotonic"],
global_diff_realtime_monotonic,
)
self.__dict__.update(state)
def encode_json(self) -> Dict[str, Any]:
return self.__getstate__()
def decode_json(self, state: Dict[str, Any]):
self.__setstate__(state)
@dataclass
class APIServerReqTimeStats(ReqTimeStatsBase):
# get by time.perf_counter()
created_time: float = 0.0
finished_time: float = 0.0
first_token_time: float = 0.0
last_time: float = 0.0
tokenize_finish_time: float = 0.0
api_server_dispatch_time: float = 0.0
api_server_dispatch_finish_time: float = 0.0
response_sent_to_client_time: float = 0.0
def __getstate__(self) -> object:
state = {}
# send to DP controller or Scheduler
# If necessary, can propagate the timestamp here, for example:
# state = {
# "created_time": self.created_time,
# "api_server_dispatch_time": self.api_server_dispatch_time,
# }
state.update(super().__getstate__())
return state
def set_created_time(self, ts=None):
ts = ts or time.perf_counter()
self.created_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_req_start(convert_time_to_realtime_ns(ts))
# Start tokenize span early so that EPD encode dispatch can capture
# it as the predecessor span context when serializing trace_ctx.
self.trace_ctx.trace_slice_start(
RequestStage.TOKENIZE.stage_name,
RequestStage.TOKENIZE.level,
convert_time_to_realtime_ns(ts),
)
def set_finished_time(self, ts=None):
ts = ts or time.perf_counter()
self.finished_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_req_finish(convert_time_to_realtime_ns(ts))
def set_first_token_time(self, ts=None):
ts = ts or time.perf_counter()
self.first_token_time = ts
self.last_time = ts
def set_last_time(self, ts=None):
ts = ts or time.perf_counter()
self.last_time = ts
def set_tokenize_finish_time(self, ts=None):
ts = ts or time.perf_counter()
self.tokenize_finish_time = ts
# tokenize span was started in set_created_time(); end it here.
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_slice_end(
RequestStage.TOKENIZE.stage_name,
RequestStage.TOKENIZE.level,
convert_time_to_realtime_ns(ts),
)
def set_api_server_dispatch_time(self, ts=None):
ts = ts or time.perf_counter()
self.api_server_dispatch_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_slice_start(
RequestStage.API_SERVER_DISPATCH.stage_name,
RequestStage.API_SERVER_DISPATCH.level,
convert_time_to_realtime_ns(ts),
)
def set_api_server_dispatch_finish_time(self, ts=None):
ts = ts or time.perf_counter()
self.api_server_dispatch_finish_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_slice_end(
RequestStage.API_SERVER_DISPATCH.stage_name,
RequestStage.API_SERVER_DISPATCH.level,
convert_time_to_realtime_ns(ts),
thread_finish_flag=True,
)
def set_response_sent_to_client_time(self, ts=None):
ts = ts or time.perf_counter()
self.response_sent_to_client_time = ts
def get_interval(self):
return time.perf_counter() - self.last_time
def get_first_token_latency(self):
return self.first_token_time - self.created_time
def get_e2e_latency(self):
return self.finished_time - self.created_time
def get_decode_latency(self):
return self.finished_time - self.first_token_time
def get_response_sent_to_client_realtime(self):
return convert_time_to_realtime(self.response_sent_to_client_time)
def convert_to_output_meta_info(
self, scheduler_time_stats=None, completion_tokens=0
):
meta_info = {}
if self.created_time > 0.0:
meta_info["request_received_ts"] = convert_time_to_realtime(
self.created_time
)
if self.api_server_dispatch_finish_time > 0.0:
meta_info["api_server_dispatch_finish_ts"] = convert_time_to_realtime(
self.api_server_dispatch_finish_time
)
if self.response_sent_to_client_time > 0.0:
meta_info["response_sent_to_client_ts"] = convert_time_to_realtime(
self.response_sent_to_client_time
)
if self.finished_time > 0.0:
meta_info["request_finished_ts"] = convert_time_to_realtime(
self.finished_time
)
decode_latency = self.get_decode_latency()
if decode_latency > 0.0 and completion_tokens > 1:
meta_info["decode_throughput"] = (completion_tokens - 1) / decode_latency
return meta_info
def convert_to_gen_ai_span_attrs(self):
span_attrs = {}
if self.first_token_time and self.created_time:
span_attrs[SpanAttributes.GEN_AI_LATENCY_TIME_TO_FIRST_TOKEN] = (
self.first_token_time - self.created_time
)
if self.finished_time and self.created_time:
span_attrs[SpanAttributes.GEN_AI_LATENCY_E2E] = (
self.finished_time - self.created_time
)
if self.first_token_time and self.finished_time:
span_attrs[SpanAttributes.GEN_AI_LATENCY_TIME_IN_MODEL_DECODE] = (
self.finished_time - self.first_token_time
)
if self.api_server_dispatch_finish_time and self.finished_time:
span_attrs[SpanAttributes.GEN_AI_LATENCY_TIME_IN_MODEL_INFERENCE] = (
self.finished_time - self.api_server_dispatch_finish_time
)
if self.api_server_dispatch_finish_time and self.first_token_time:
span_attrs[SpanAttributes.GEN_AI_LATENCY_TIME_IN_MODEL_PREFILL] = (
self.first_token_time - self.api_server_dispatch_finish_time
)
return span_attrs
@dataclass
class DPControllerReqTimeStats(ReqTimeStatsBase):
# propagated from tokenizer/grpc_server, get by time.perf_counter()
created_time: float = 0.0
api_server_dispatch_time: float = 0.0
# new timestamp, get by time.perf_counter()
dpc_dispatch_time: float = 0.0
dpc_dispatch_finish_time: float = 0.0
def __getstate__(self) -> object:
state = {}
# send to Scheduler
# If necessary, can propagate the timestamp here, for example:
# state = {
# "created_time": self.created_time,
# "api_server_dispatch_time": self.api_server_dispatch_time,
# "dpc_dispatch_time": self.dpc_dispatch_time,
# }
state.update(super().__getstate__())
return state
def set_dp_dispatch_time(self, ts=None):
ts = ts or time.perf_counter()
self.dpc_dispatch_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_slice_start(
RequestStage.DPC_DISPATCH.stage_name,
RequestStage.DPC_DISPATCH.level,
convert_time_to_realtime_ns(ts),
)
def set_dp_dispatch_finish_time(self, ts=None):
ts = ts or time.perf_counter()
self.dpc_dispatch_finish_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_slice_end(
RequestStage.DPC_DISPATCH.stage_name,
RequestStage.DPC_DISPATCH.level,
convert_time_to_realtime_ns(ts),
thread_finish_flag=True,
)
@dataclass
class SchedulerReqTimeStats(ReqTimeStatsBase):
"""
Store the timestamps for each stage of a request.
Unified: wait_queue -> forward -> completion
Prefill: bootstrap_queue -> wait_queue -> forward -> transfer_queue -> completion
Decode: prealloc_queue -> transfer_queue -> wait_queue -> forward -> completion
"""
# Placeholder: not used currently
# propagated from tokenizer/grpc_server or dp controller
created_time: float = 0.0
api_server_dispatch_time: float = 0.0
dpc_dispatch_time: float = 0.0
# common, get by time.perf_counter()
wait_queue_entry_time: float = 0.0
forward_entry_time: float = 0.0
prefill_finished_time: float = 0.0
completion_time: float = 0.0
# prefill node, get by time.perf_counter()
prefill_bootstrap_queue_entry_time: float = 0.0
prefill_transfer_queue_entry_time: float = 0.0
prefill_kv_transfer_finish_time: float = 0.0
# decode node, get by time.perf_counter()
decode_prealloc_queue_entry_time: float = 0.0
decode_transfer_queue_entry_time: float = 0.0
decode_prebuilt_finish_time: float = 0.0
# bootstrap sub-phase tracking (PD disagg)
bootstrap_done_time: float = 0.0
# only for request tracing
scheduler_recv_time: float = 0.0
last_chunked_prefill_finish_time: float = 0.0
last_decode_finish_time: float = 0.0
decode_ct: int = 0
last_decode_scheduled_time: float = 0.0
last_forward_entry_time: float = 0.0
last_prefill_finished_time: float = 0.0
run_batch_cpu_start_time: float = 0.0
# speculative decoding
spec_draft_start_time: float = 0.0
spec_verify_start_time: float = 0.0
# other
transfer_speed_gb_s: float = 0.0
transfer_total_mb: float = 0.0
def __getstate__(self) -> object:
# send to detokenizer/tokenizer
if not self.enable_metrics:
return {}
state = {
"wait_queue_entry_time": self.wait_queue_entry_time,
"forward_entry_time": self.forward_entry_time,
"prefill_finished_time": self.prefill_finished_time,
"diff_realtime_monotonic": global_diff_realtime_monotonic,
}
return state
def set_scheduler_recv_time(self, ts=None):
calibrate_time_diff()
ts = ts or time.perf_counter()
self.scheduler_recv_time = ts
def set_spec_draft_start_time(self, ts=None):
ts = ts or time.perf_counter()
self.spec_draft_start_time = ts
def set_spec_draft_end_time(self, ts=None):
ts = ts or time.perf_counter()
if self.trace_ctx.tracing_enable:
stage = RequestStage.SPEC_DRAFT
self.trace_slice(stage, self.spec_draft_start_time, ts)
def set_spec_verify_start_time(self, ts=None):
ts = ts or time.perf_counter()
self.spec_verify_start_time = ts
def set_spec_verify_end_time(
self,
ts=None,
num_correct_drafts: int = 0,
# FIXME: backward-compat alias, remove in next release.
accepted_tokens: Optional[int] = None,
):
if accepted_tokens is not None:
num_correct_drafts = accepted_tokens
ts = ts or time.perf_counter()
if self.trace_ctx.tracing_enable:
stage = RequestStage.SPEC_VERIFY
self.trace_slice(
stage,
self.spec_verify_start_time,
ts,
{
"num_correct_drafts": num_correct_drafts,
# FIXME: backward-compat alias, remove in next release.
"accepted_tokens": num_correct_drafts,
},
)
def set_run_batch_cpu_start_time(self, ts=None, attrs=None):
ts = ts or time.perf_counter()
self.run_batch_cpu_start_time = ts
def set_run_batch_cpu_end_time(self, ts=None, attrs=None):
ts = ts or time.perf_counter()
if self.run_batch_cpu_start_time > 0.0:
self.trace_slice(
RequestStage.RUN_BATCH_CPU, self.run_batch_cpu_start_time, ts, attrs
)
self.run_batch_cpu_start_time = 0.0
def set_retract_time(self, ts=None):
ts = ts or time.perf_counter()
# retract
self.last_forward_entry_time = 0.0
self.last_prefill_finished_time = 0.0
self.last_chunked_prefill_finish_time = 0.0
self.last_decode_finish_time = 0.0
self.last_decode_scheduled_time = 0.0
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_event("retract", 1, convert_time_to_realtime_ns(ts))
def reset_prefill_retry_time(self):
self.wait_queue_entry_time = 0.0
self.forward_entry_time = 0.0
self.prefill_finished_time = 0.0
self.completion_time = 0.0
self.prefill_transfer_queue_entry_time = 0.0
self.prefill_kv_transfer_finish_time = 0.0
self.last_forward_entry_time = 0.0
self.last_prefill_finished_time = 0.0
self.last_chunked_prefill_finish_time = 0.0
def set_wait_queue_entry_time(self, ts=None):
ts = ts or time.perf_counter()
if self.wait_queue_entry_time == 0.0:
if self.enable_metrics or self.trace_ctx.tracing_enable:
if self.disagg_mode == DisaggregationMode.PREFILL:
stage = RequestStage.PREFILL_BOOTSTRAP
slice_start_time = self.prefill_bootstrap_queue_entry_time
elif self.disagg_mode == DisaggregationMode.DECODE:
stage = RequestStage.DECODE_TRANSFERRED
slice_start_time = self.decode_transfer_queue_entry_time
else:
stage = RequestStage.REQUEST_PROCESS
slice_start_time = self.scheduler_recv_time
self.observe_per_stage_req_latency(stage, ts - slice_start_time)
self.trace_slice(stage, slice_start_time, ts)
else:
self.set_retract_time(ts)
self.wait_queue_entry_time = ts
def set_forward_entry_time(self, ts=None):
ts = ts or time.perf_counter()
if self.forward_entry_time == 0.0:
self.forward_entry_time = ts
self.last_forward_entry_time = ts
if self.enable_metrics:
self.metrics_collector.observe_queue_time(self.get_queueing_time())
if self.enable_metrics or self.trace_ctx.tracing_enable:
if self.disagg_mode == DisaggregationMode.DECODE:
stage = RequestStage.DECODE_WAITING
else:
stage = RequestStage.PREFILL_WAITING
slice_start_time = self.wait_queue_entry_time
self.observe_per_stage_req_latency(stage, ts - slice_start_time)
self.trace_slice(stage, slice_start_time, ts)
if self.disagg_mode == DisaggregationMode.DECODE:
self.trace_ctx.trace_slice_start(
RequestStage.DECODE_FORWARD.stage_name,
RequestStage.DECODE_FORWARD.level,
convert_time_to_realtime_ns(ts),
)
else:
self.trace_ctx.trace_slice_start(
RequestStage.PREFILL_FORWARD.stage_name,
RequestStage.PREFILL_FORWARD.level,
convert_time_to_realtime_ns(ts),
)
elif self.last_forward_entry_time == 0.0:
self.last_forward_entry_time = ts
def set_last_chunked_prefill_finish_time(self, ts=None):
ts = ts or time.perf_counter()
last_time = self.last_chunked_prefill_finish_time
self.last_chunked_prefill_finish_time = ts
if last_time == 0.0:
last_time = self.last_forward_entry_time
stage = RequestStage.PREFILL_CHUNKED_FORWARD
self.observe_per_stage_req_latency(stage, ts - last_time)
self.trace_slice(stage, last_time, ts)
def set_prefill_finished_time(self, ts=None):
ts = ts or time.perf_counter()
if self.prefill_finished_time == 0.0:
self.prefill_finished_time = ts
self.last_prefill_finished_time = ts
stage = RequestStage.PREFILL_FORWARD
self.observe_per_stage_req_latency(stage, ts - self.last_forward_entry_time)
if self.trace_ctx.tracing_enable:
if self.last_chunked_prefill_finish_time > 0:
self.trace_slice(
RequestStage.PREFILL_CHUNKED_FORWARD,
self.last_chunked_prefill_finish_time,
ts,
)
self.trace_ctx.trace_slice_end(
stage.stage_name, stage.level, convert_time_to_realtime_ns(ts)
)
if (
self.disagg_mode == DisaggregationMode.NULL
and self.last_decode_scheduled_time > 0
):
self.trace_ctx.trace_slice_start(
RequestStage.DECODE_FORWARD.stage_name,
RequestStage.DECODE_FORWARD.level,
convert_time_to_realtime_ns(ts),
)
elif self.last_prefill_finished_time == 0.0:
# retract
self.last_prefill_finished_time = ts
if self.last_chunked_prefill_finish_time > 0:
self.trace_slice(
RequestStage.PREFILL_CHUNKED_FORWARD,
self.last_chunked_prefill_finish_time,
ts,
)
else:
self.trace_slice(
RequestStage.PREFILL_FORWARD, self.last_forward_entry_time, ts
)
def set_last_decode_finish_time(self, ts=None):
ts = ts or time.perf_counter()
last_time = self.last_decode_finish_time
self.last_decode_finish_time = ts
if self.enable_metrics or self.trace_ctx.tracing_enable:
if last_time == 0.0:
if self.disagg_mode == DisaggregationMode.DECODE:
last_time = self.decode_prebuilt_finish_time
else:
if (
self.last_decode_scheduled_time
< self.last_prefill_finished_time
):
last_time = self.last_prefill_finished_time
else:
last_time = self.last_decode_scheduled_time
stage = RequestStage.DECODE_LOOP
self.observe_per_stage_req_latency(stage, ts - last_time)
attrs = {"decode_ct": self.decode_ct}
self.trace_slice(stage, last_time, ts, attrs)
self.decode_ct += 1
def set_last_scheduled_time(self, forward_mode: ForwardMode, ts=None, attrs=None):
ts = ts or time.perf_counter()
if self.trace_ctx.tracing_enable:
if (
self.disagg_mode == DisaggregationMode.NULL
and forward_mode.is_decode()
and self.last_decode_scheduled_time == 0.0
and self.last_prefill_finished_time > 0
):
self.trace_slice(
RequestStage.DECODE_WAITING, self.last_prefill_finished_time, ts
)
self.trace_ctx.trace_slice_start(
RequestStage.DECODE_FORWARD.stage_name,
RequestStage.DECODE_FORWARD.level,
convert_time_to_realtime_ns(ts),
)
self.last_decode_finish_time = ts
self.trace_ctx.trace_event(
"schedule", 3, convert_time_to_realtime_ns(ts), attrs
)
if forward_mode.is_decode():
self.last_decode_scheduled_time = ts
def set_completion_time(self, ts=None):
ts = ts or time.perf_counter()
self.completion_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.abort()
def compute_and_observe_kv_transfer_metrics(
self,
transfer_metric: KVTransferMetric,
) -> Optional[dict]:
"""Compute KV transfer metrics and observe them via the metrics collector.
Returns a dict with latency_ms, total_mb, speed_gb_s if computable, else None.
"""
result = {}
if transfer_metric.transfer_total_bytes is None:
return result if result else None
# Transfer latency, size, and speed
if transfer_metric.transfer_latency_s is not None:
transfer_latency_s = transfer_metric.transfer_latency_s
else:
if self.prefill_transfer_queue_entry_time <= 0 or self.completion_time <= 0:
return result if result else None
# Note: This only capture the last chunk time
transfer_latency_s = (
self.completion_time - self.prefill_transfer_queue_entry_time
)
if transfer_latency_s > 0:
latency_ms = transfer_latency_s * 1000
total_bytes = transfer_metric.transfer_total_bytes
total_mb = total_bytes / (1024 * 1024)
self.transfer_total_mb = total_mb
speed_gb_s = 0.0
if transfer_latency_s > 0:
speed_gb_s = (total_mb / 1024) / transfer_latency_s
self.transfer_speed_gb_s = speed_gb_s
result["latency_ms"] = latency_ms
result["total_mb"] = total_mb
result["speed_gb_s"] = speed_gb_s
if self.enable_metrics:
self.metrics_collector.observe_kv_transfer_metrics(
latency_ms=latency_ms,
total_mb=total_mb,
speed_gb_s=speed_gb_s,
)
# Bootstrap and alloc durations
if (
self.prefill_bootstrap_queue_entry_time > 0
and self.bootstrap_done_time > 0
and self.wait_queue_entry_time > 0
):
bootstrap_ms = (
self.bootstrap_done_time - self.prefill_bootstrap_queue_entry_time
) * 1000
if transfer_metric.alloc_latency_s is not None:
alloc_ms = transfer_metric.alloc_latency_s * 1000
else:
alloc_ms = (
max(0.0, self.wait_queue_entry_time - self.bootstrap_done_time)
* 1000
)
result["bootstrap_ms"] = bootstrap_ms
result["alloc_ms"] = alloc_ms
if self.enable_metrics:
self.metrics_collector.observe_kv_transfer_bootstrap(
bootstrap_ms=bootstrap_ms,
alloc_ms=alloc_ms,
)
return result if result else None
def set_quick_finish_time(self, ts=None):
ts = ts or time.perf_counter()
self.set_completion_time(ts)
self.forward_entry_time = ts
def set_prefill_bootstrap_queue_entry_time(self, ts=None):
ts = ts or time.perf_counter()
self.prefill_bootstrap_queue_entry_time = ts
stage = RequestStage.PREFILL_PREPARE
self.observe_per_stage_req_latency(stage, ts - self.scheduler_recv_time)
self.trace_slice(stage, self.scheduler_recv_time, ts)
def set_prefill_transfer_queue_entry_time(self, ts=None):
ts = ts or time.perf_counter()
self.prefill_transfer_queue_entry_time = ts
def set_prefill_kv_transfer_finish_time(self, ts=None):
ts = ts or time.perf_counter()
self.prefill_kv_transfer_finish_time = ts
stage = RequestStage.PREFILL_TRANSFER_KV_CACHE
self.observe_per_stage_req_latency(
stage, ts - self.prefill_transfer_queue_entry_time
)
self.trace_slice(stage, self.prefill_transfer_queue_entry_time, ts)
def set_decode_prealloc_queue_entry_time(self, ts=None):
ts = ts or time.perf_counter()
self.decode_prealloc_queue_entry_time = ts
stage = RequestStage.DECODE_PREPARE
self.observe_per_stage_req_latency(stage, ts - self.scheduler_recv_time)
self.trace_slice(stage, self.scheduler_recv_time, ts)
def set_decode_transfer_queue_entry_time(self, ts=None):
ts = ts or time.perf_counter()
self.decode_transfer_queue_entry_time = ts
stage = RequestStage.DECODE_BOOTSTRAP
self.observe_per_stage_req_latency(
stage, ts - self.decode_prealloc_queue_entry_time
)
self.trace_slice(stage, self.decode_prealloc_queue_entry_time, ts)
if self.enable_metrics and self.bootstrap_done_time > 0:
bootstrap_ms = (
self.bootstrap_done_time - self.decode_prealloc_queue_entry_time
) * 1000
alloc_ms = (ts - self.bootstrap_done_time) * 1000
self.metrics_collector.observe_kv_transfer_bootstrap(
bootstrap_ms=bootstrap_ms,
alloc_ms=alloc_ms,
)
def set_bootstrap_done_time(self, ts=None):
ts = ts or time.perf_counter()
if self.bootstrap_done_time == 0.0:
self.bootstrap_done_time = ts
def set_decode_prebuilt_finish_time(self, ts=None):
ts = ts or time.perf_counter()
self.decode_prebuilt_finish_time = ts
stage = RequestStage.DECODE_FAKE_OUTPUT
self.observe_per_stage_req_latency(stage, ts - self.last_forward_entry_time)
self.trace_slice(stage, self.last_forward_entry_time, ts)
def get_queueing_time(self) -> float:
return self.forward_entry_time - self.wait_queue_entry_time
def convert_to_duration(self) -> str:
if self.disagg_mode == DisaggregationMode.NULL:
queue_duration = self.duration_between(
self.wait_queue_entry_time, self.forward_entry_time
)
forward_duration = self.duration_between(
self.forward_entry_time, self.completion_time
)
if SGLANG_TEST_REQUEST_TIME_STATS:
assert (
queue_duration >= 0 and forward_duration >= 0
), f"queue_duration={queue_duration} < 0 or forward_duration={forward_duration} < 0"
return f"queue_duration={self.format_duration(queue_duration)}, forward_duration={self.format_duration(forward_duration)}, entry_time={self.format_wallclock(self.wait_queue_entry_time)}"
elif self.disagg_mode == DisaggregationMode.PREFILL:
bootstrap_queue_duration = self.duration_between(
self.prefill_bootstrap_queue_entry_time, self.wait_queue_entry_time
)
queue_duration = self.duration_between(
self.wait_queue_entry_time, self.forward_entry_time
)
forward_duration = self.duration_between(
self.forward_entry_time, self.completion_time
)
if SGLANG_TEST_REQUEST_TIME_STATS:
if self.wait_queue_entry_time > 0:
assert (
bootstrap_queue_duration >= 0
and queue_duration >= 0
and forward_duration >= 0
), f"bootstrap_queue_duration={bootstrap_queue_duration} < 0 or queue_duration={queue_duration} < 0 or forward_duration={forward_duration} < 0"
if (
self.bootstrap_done_time > 0
and self.prefill_bootstrap_queue_entry_time > 0
):
bootstrap_duration = self.duration_between(
self.prefill_bootstrap_queue_entry_time, self.bootstrap_done_time
)
if SGLANG_TEST_REQUEST_TIME_STATS:
assert (
bootstrap_duration >= 0
), f"bootstrap_duration={bootstrap_duration} < 0"
bootstrap_fields = (
f"bootstrap_duration={self.format_duration(bootstrap_duration)}, "
)
elif self.bootstrap_done_time > 0:
bootstrap_fields = f"bootstrap_done_time={self.format_wallclock(self.bootstrap_done_time)}, "
else:
bootstrap_fields = f"bootstrap_queue_duration={self.format_duration(bootstrap_queue_duration)}, "
return (
f"{bootstrap_fields}"
f"queue_duration={self.format_duration(queue_duration)}, "
f"forward_duration={self.format_duration(forward_duration)}, "
f"entry_time={self.format_wallclock(self.prefill_bootstrap_queue_entry_time)}, "
f"transfer_speed={self.transfer_speed_gb_s:.2f} GB/s, "
f"transfer_total={self.transfer_total_mb:.2f} MB"
)
elif self.disagg_mode == DisaggregationMode.DECODE:
prealloc_duration = self.duration_between(
self.decode_prealloc_queue_entry_time,
self.decode_transfer_queue_entry_time,
)
transfer_duration = self.duration_between(
self.decode_transfer_queue_entry_time,
self.wait_queue_entry_time,
)
queue_duration = self.duration_between(
self.wait_queue_entry_time,
self.forward_entry_time,
)
forward_duration = self.duration_between(
self.forward_entry_time,
self.completion_time,
)
if SGLANG_TEST_REQUEST_TIME_STATS:
if self.wait_queue_entry_time > 0:
assert (
prealloc_duration >= 0
and transfer_duration >= 0
and queue_duration >= 0
and forward_duration >= 0
), f"prealloc_duration={prealloc_duration} < 0 or transfer_duration={transfer_duration} < 0 or queue_duration={queue_duration} < 0 or forward_duration={forward_duration} < 0. {self=}"
# Break down prealloc_duration into sub-phases
if self.bootstrap_done_time > 0:
bootstrap_duration = self.duration_between(
self.decode_prealloc_queue_entry_time, self.bootstrap_done_time
)
alloc_wait_duration = self.duration_between(
self.bootstrap_done_time, self.decode_transfer_queue_entry_time
)
if SGLANG_TEST_REQUEST_TIME_STATS:
assert (
bootstrap_duration >= 0 and alloc_wait_duration >= 0
), f"bootstrap_duration={bootstrap_duration} < 0 or alloc_wait_duration={alloc_wait_duration} < 0"
prealloc_fields = (
f"bootstrap_duration={self.format_duration(bootstrap_duration)}, "
f"alloc_wait_duration={self.format_duration(alloc_wait_duration)}, "
)
else:
prealloc_fields = f"prealloc_queue_duration={self.format_duration(prealloc_duration)}, "
return (
f"{prealloc_fields}"
f"transfer_duration={self.format_duration(transfer_duration)}, "
f"queue_duration={self.format_duration(queue_duration)}, "
f"forward_duration={self.format_duration(forward_duration)}, "
f"entry_time={self.format_wallclock(self.decode_prealloc_queue_entry_time)}"
)
else:
return "Unknown Time Stats"
def convert_to_output_meta_info(self):
meta_data = {}
if self.forward_entry_time > 0.0:
meta_data["forward_entry_time"] = convert_time_to_realtime(
self.forward_entry_time
)
if self.prefill_finished_time > 0.0:
meta_data["prefill_finished_time"] = convert_time_to_realtime(
self.prefill_finished_time
)
meta_data.update(
{
"queue_time": self.get_queueing_time(),
}
)
return meta_data
def format_duration(self, duration: float) -> str:
return f"{duration * 1e3:.2f}ms"
def duration_between(self, start: float, end: float) -> float:
if start <= 0 or end <= 0:
return 0.0
return end - start
@staticmethod
def format_wallclock(perf_counter_time: float) -> str:
return f"{convert_time_to_realtime(perf_counter_time):.3f}"
@dataclass
class EncoderReqTimeStats(ReqTimeStatsBase):
mm_encode_start_time: float = 0.0
mm_encode_end_time: float = 0.0
modality: str = "image"
def set_mm_encode_start_time(self, ts=None):
ts = ts or time.perf_counter()
self.mm_encode_start_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.rebuild_thread_context()
self.trace_ctx.trace_slice_start(
RequestStage.MM_ENCODE.stage_name,
RequestStage.MM_ENCODE.level,
convert_time_to_realtime_ns(ts),
)
def set_mm_encode_end_time(self, ts=None):
ts = ts or time.perf_counter()
self.mm_encode_end_time = ts
if self.trace_ctx.tracing_enable:
self.trace_ctx.trace_slice_end(
RequestStage.MM_ENCODE.stage_name,
RequestStage.MM_ENCODE.level,
convert_time_to_realtime_ns(ts),
thread_finish_flag=True,
)
if self.enable_metrics:
self.metrics_collector.observe_request_e2e_latency(
ts - self.mm_encode_start_time, modality=self.modality
)
def set_schedule_time_batch(batch: ScheduleBatch):
# only for tracing
if not get_global_tracing_enabled():
return
ts = time.perf_counter()
bid = uuid.uuid4().hex[:8]
_attrs = {"bid": bid, "batch_size": len(batch.reqs)}
if batch.forward_mode.is_decode():
_attrs["forward_mode"] = "decode"
elif batch.forward_mode.is_prefill():
_attrs["forward_mode"] = "prefill"
elif batch.forward_mode.is_prebuilt():
_attrs["forward_mode"] = "prebuilt"
for req in batch.reqs:
req.time_stats.set_last_scheduled_time(batch.forward_mode, ts, _attrs)
def set_time_batch(
reqs: List[Any],
set_func: str,
trace_only: bool = False,
attrs: Optional[Dict[str, Any]] = None,
):
if reqs is None or len(reqs) == 0:
return
if trace_only and not get_global_tracing_enabled():
return
ts = time.perf_counter()
for req in reqs:
method = getattr(req.time_stats, set_func)
if attrs is None:
method(ts)
else:
method(ts, attrs)