[tracing] sglang tracing v2: support exporting tracing data asynchronously (#30023)

This commit is contained in:
Feng Su
2026-08-10 15:23:05 +08:00
committed by GitHub
parent 06f32bab6b
commit fb3d1419fd
8 changed files with 1214 additions and 21 deletions
+2
View File
@@ -364,6 +364,8 @@ class Envs:
SGLANG_MEM_PROFILE_MAX_ENTRIES = EnvInt(100000)
SGLANG_OTLP_EXPORTER_SCHEDULE_DELAY_MILLIS = EnvInt(500)
SGLANG_OTLP_EXPORTER_MAX_EXPORT_BATCH_SIZE = EnvInt(64)
SGLANG_TRACE_ASYNC = EnvBool(False)
SGLANG_TRACE_ASYNC_FLUSH_THRESHOLD = EnvInt(100)
SGLANG_NATIVE_MOVE_KV_CACHE = EnvBool(False)
# Disable lazy compaction in the unified memory pool allocator and
# fall back to the per-free eager compaction. Used for production
+4
View File
@@ -268,6 +268,7 @@ from sglang.srt.model_loader.utils import get_resolved_model_impl
from sglang.srt.multiplex.multiplexing_mixin import SchedulerMultiplexMixin
from sglang.srt.observability.metrics_collector import SchedulerMetricsCollector
from sglang.srt.observability.req_time_stats import (
flush_trace_batch,
set_schedule_time_batch,
set_time_batch,
)
@@ -3889,6 +3890,9 @@ class Scheduler(
batch: ScheduleBatch,
result: Union[GenerationBatchResult, EmbeddingBatchResult],
):
# Flush async trace ops here: in overlap mode this CPU work runs while
# the next batch's GPU forward is in flight, giving free overlap.
flush_trace_batch(batch.reqs)
self.publish_load_snapshot(force=batch.forward_mode.is_extend())
if batch.forward_mode.is_decode():
@@ -37,6 +37,10 @@ from sglang.srt.observability.trace import (
TraceSliceContext,
get_global_tracing_enabled,
)
from sglang.srt.observability.trace_async import (
TraceReqContextAsync,
is_async_tracing_available,
)
from sglang.srt.utils import get_bool_env_var
if TYPE_CHECKING:
@@ -286,13 +290,22 @@ class ReqTimeStatsBase:
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 is_async_tracing_available():
self.trace_ctx = TraceReqContextAsync(
rid=rid,
bootstrap_room=bootstrap_room,
role=self.disagg_mode_str(),
module_name="request",
external_trace_header=external_trace_header,
)
else:
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()
@@ -339,8 +352,12 @@ class ReqTimeStatsBase:
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)
if trace_ctx_state.get("is_async"):
trace_ctx = object.__new__(TraceReqContextAsync)
trace_ctx.__setstate__(trace_ctx_state)
else:
trace_ctx = object.__new__(TraceReqContext)
trace_ctx.__setstate__(trace_ctx_state)
state["trace_ctx"] = trace_ctx
else:
state["trace_ctx"] = TraceNullContext()
@@ -1247,3 +1264,19 @@ def set_time_batch(
method(ts)
else:
method(ts, attrs)
def flush_trace_batch(reqs: List[Any]):
"""Proactively flush buffered trace ops for a batch of requests.
Call at natural CPU/GPU overlap points (e.g., right before run_batch)
so the ZMQ send overlaps with GPU forward compute.
"""
if reqs is None or not get_global_tracing_enabled():
return
for req in reqs:
time_stats = getattr(req, "time_stats", None)
if time_stats is not None:
trace_ctx = getattr(time_stats, "trace_ctx", None)
if trace_ctx is not None:
trace_ctx.flush()
+64 -12
View File
@@ -24,6 +24,7 @@ import uuid
from dataclasses import dataclass
from typing import Any, Dict, List, Mapping, Optional
from sglang.srt.environ import envs
from sglang.srt.utils import get_int_env_var
logger = logging.getLogger(__name__)
@@ -124,10 +125,29 @@ class TraceThreadContext:
class TraceCustomIdGenerator(id_generator.IdGenerator):
"""Custom ID generator with support for pre-setting the next span ID.
Why custom IDs are needed:
The default IdGenerator may produce duplicate trace IDs across
multiple TP scheduler processes.
Preset mechanism (used by async tracing):
When SGLANG_TRACE_ASYNC=1, span creation is deferred to an exporter
process while the caller process needs to know span IDs in advance
for cross-process span linking. The caller pre-generates a span ID
and sends it to the exporter. Before calling tracer.start_span(),
the exporter calls preset_next_span_id(id) — the next
generate_span_id() call consumes it, then falls back to random
generation. This avoids modifying the standard OTel start_span()
API while giving the caller full control over span IDs.
Thread-safety: _preset_local is a threading.local(), so concurrent
callers in different threads cannot interfere. The exporter process
is single-threaded, so no additional locking is needed.
"""
The default IdGenerator may produce duplicate trace IDs across multiple TP scheduler processes,
hence a custom IdGenerator is implemented.
"""
# Thread-local storage for the next span ID to use preset.
_preset_local = threading.local()
def __init__(self):
super().__init__()
@@ -138,12 +158,30 @@ class TraceCustomIdGenerator(id_generator.IdGenerator):
return self.local_random.getrandbits(64)
def generate_span_id(self) -> int:
# If a preset span ID was injected, consume it (one-shot).
preset = getattr(self._preset_local, "span_id", None)
if preset is not None:
self._preset_local.span_id = None
return preset
return self.local_random.getrandbits(64)
@classmethod
def preset_next_span_id(cls, span_id: int):
"""Inject a pre-generated span ID for the next start_span() call.
The ID is consumed exactly once by generate_span_id() and then
cleared. Call this immediately before tracer.start_span().
"""
cls._preset_local.span_id = span_id
# global variables
threads_info: Dict[int, TraceThreadInfo] = {}
# Optional callback invoked when a new thread registers its trace info.
# Used by trace_async to forward thread info to the exporter process.
_on_thread_info_set = None
get_cur_time_ns = lambda: int(time.time() * 1e9)
if hasattr(time, "time_ns"):
get_cur_time_ns = lambda: int(time.time_ns())
@@ -199,12 +237,8 @@ def process_tracing_init(
resource=resource, id_generator=TraceCustomIdGenerator()
)
schedule_delay_millis = get_int_env_var(
"SGLANG_OTLP_EXPORTER_SCHEDULE_DELAY_MILLIS", 500
)
max_export_batch_size = get_int_env_var(
"SGLANG_OTLP_EXPORTER_MAX_EXPORT_BATCH_SIZE", 64
)
schedule_delay_millis = envs.SGLANG_OTLP_EXPORTER_SCHEDULE_DELAY_MILLIS.get()
max_export_batch_size = envs.SGLANG_OTLP_EXPORTER_MAX_EXPORT_BATCH_SIZE.get()
processor = BatchSpanProcessor(
span_exporter=get_otlp_span_exporter(otlp_endpoint),
@@ -222,6 +256,12 @@ def process_tracing_init(
opentelemetry_initialized = True
tracer = trace.get_tracer("sglang server")
# Auto-start async trace exporter when SGLANG_TRACE_ASYNC=1
if envs.SGLANG_TRACE_ASYNC.get():
from sglang.srt.observability.trace_async import start_trace_exporter
start_trace_exporter(otlp_endpoint, server_name, trace_modules=trace_modules)
def get_global_tracing_enabled():
return opentelemetry_initialized
@@ -266,6 +306,9 @@ def trace_set_thread_info(
pp_rank=pp_rank,
)
if _on_thread_info_set is not None:
_on_thread_info_set(threads_info[pid])
class TraceReqContext:
def __init__(
@@ -275,9 +318,12 @@ class TraceReqContext:
role="unified",
module_name="",
external_trace_header: Optional[Dict[str, str]] = None,
trace_level: Optional[int] = None,
):
self.rid: str = str(rid)
self.trace_level = get_global_trace_level()
self.trace_level = (
trace_level if trace_level is not None else get_global_trace_level()
)
self.tracing_enable: bool = opentelemetry_initialized and self.trace_level > 0
# Filter by --trace-modules only for explicitly named modules; contexts
@@ -453,7 +499,7 @@ class TraceReqContext:
copied.trace_level = self.trace_level
copied.module_name = self.module_name
copied.is_copy = True # Mark as copy
copied.pid = self.pid
copied.pid = None
# thread_context is None, will be rebuilt via rebuild_thread_context()
copied.thread_context = None
@@ -476,11 +522,14 @@ class TraceReqContext:
return copied
def rebuild_thread_context(self, ts: Optional[int] = None):
def rebuild_thread_context(
self, ts: Optional[int] = None, pid: Optional[int] = None
):
if not self.tracing_enable:
return
ts = ts or get_cur_time_ns()
self.pid = pid if pid is not None else threading.get_native_id()
self.thread_context = self.__create_thread_context(ts)
def trace_req_start(
@@ -770,6 +819,9 @@ class TraceReqContext:
self.thread_context.thread_span.end(end_time=ts)
self.thread_context = None
def flush(self):
pass
def __del__(self):
self.abort(abort_info={"reason": "have unclosed span, auto closed"})
File diff suppressed because it is too large Load Diff