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
1247 lines
44 KiB
Python
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)
|