Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
ae001a6
Sglang Tracing: Unify tracing and req stage metrics, add trace-level …
sufeng-buaa Nov 13, 2025
8395446
Sglang Tracing: update doc
sufeng-buaa Nov 13, 2025
5dac5da
Sglang Tracing: update test cases
sufeng-buaa Nov 13, 2025
e75d844
fix lint
sufeng-buaa Nov 13, 2025
71f9d2a
rename 'NoOpTimeRecorder' to 'NoOpStageContext'
sufeng-buaa Nov 13, 2025
a847eba
sglang tracing: fix crash when enable tracing but not install otlp
sufeng-buaa Nov 13, 2025
cd6c8d8
sglang tracing: rename 'Sglang***' to 'SGLang***'
sufeng-buaa Nov 13, 2025
a1c82a8
sglang tracing: fix dependence install
sufeng-buaa Nov 13, 2025
3ef8db2
trace: add more explanations
sufeng-buaa Nov 17, 2025
6d3735a
fix tracing doc
ShangmingCai Nov 17, 2025
2ed4718
lint
ShangmingCai Nov 17, 2025
9b13760
Merge branch 'main' into sufeng-buaa/unify-trace-metric
ShangmingCai Nov 17, 2025
c9efa58
trace: update doc
sufeng-buaa Nov 17, 2025
ea30182
trace: standardize naming
sufeng-buaa Nov 18, 2025
44d4e1d
trace: fix lint
sufeng-buaa Nov 18, 2025
bbc98f0
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Nov 25, 2025
dd7e5bb
SGLang Tracing: delete bootstrap_room span and 'trace_context' header
sufeng-buaa Nov 26, 2025
f2a92d0
sglang tracing: add module name to root span
sufeng-buaa Nov 26, 2025
2f6f961
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Nov 26, 2025
2be1623
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Nov 27, 2025
6c57dec
sglang tracing: remove SGLang prefix from classes related to trace
sufeng-buaa Nov 27, 2025
7ca0972
fix lint
sufeng-buaa Nov 28, 2025
66df528
fix 'prev_span' condition
sufeng-buaa Nov 28, 2025
bfddf4a
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 1, 2025
7631089
fix root span not be closed when scheduler have no propagation_context
sufeng-buaa Dec 2, 2025
5bbeb9d
fix warning for last extra decode when enabling overlap schedule
sufeng-buaa Dec 2, 2025
0f8dc9b
add more test cases
sufeng-buaa Dec 2, 2025
6f9b330
simplify trace function name
sufeng-buaa Dec 8, 2025
c6aa18c
check exception path
sufeng-buaa Dec 8, 2025
e5f5e2a
fix null reference when propagate_context is null
sufeng-buaa Dec 8, 2025
f120734
fix abort on error path
sufeng-buaa Dec 9, 2025
b4a75e4
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 10, 2025
a71fcf3
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 12, 2025
23d330c
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 14, 2025
f37e296
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 15, 2025
3911a8d
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 17, 2025
371f229
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 22, 2025
595367d
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 24, 2025
1c69b16
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Dec 25, 2025
2620d22
fix conflict
sufeng-buaa Dec 26, 2025
3b856a5
raise Exception if otel init error
sufeng-buaa Dec 26, 2025
4d995b5
fix lint
sufeng-buaa Dec 26, 2025
dddee15
fix conflict
sufeng-buaa Dec 27, 2025
68df11f
optimize code
sufeng-buaa Dec 27, 2025
606d3cf
fix conflict
sufeng-buaa Dec 27, 2025
59133cd
fix conflict
sufeng-buaa Dec 28, 2025
91566f0
remove SGLang prefix from classes related to trace
sufeng-buaa Dec 30, 2025
6358231
Merge 'upstream/main' and fix conflict
sufeng-buaa Jan 4, 2026
6c0c233
fix
sufeng-buaa Jan 4, 2026
93f998b
Merge branch 'upstream/main' and fix conflict
sufeng-buaa Jan 5, 2026
66b2fbf
Merge branch 'upstream/main' and fix conflict
sufeng-buaa Jan 6, 2026
954d642
Merge branch 'main' into sufeng-buaa/unify-trace-metric
sufeng-buaa Jan 8, 2026
c04a00f
optimize code
sufeng-buaa Jan 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions docs/references/production_request_trace.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SGlang exports request trace data based on the OpenTelemetry Collector. You can enable tracing by adding the `--enable-trace` and configure the OpenTelemetry Collector endpoint using `--otlp-traces-endpoint` when launching the server.
SGlang exports request trace data based on the OpenTelemetry Collector. You can enable tracing by adding the `--trace-level` and configure the OpenTelemetry Collector endpoint using `--otlp-traces-endpoint` when launching the server. The `--trace-level` option accepts configurable values from `1` to `3`, with higher numbers indicating more detailed tracing. Additionally, you can use `--trace-module` to specify the module to trace; currently, only `request` is supported.

You can find example screenshots of the visualization in https://github.com/sgl-project/sglang/issues/8965.

Expand Down Expand Up @@ -26,7 +26,7 @@ This section explains how to configure the request tracing and export the trace
export SGLANG_OTLP_EXPORTER_SCHEDULE_DELAY_MILLIS=500
export SGLANG_OTLP_EXPORTER_MAX_EXPORT_BATCH_SIZE=64
# start the prefill and decode server
python -m sglang.launch_server --enable-trace --otlp-traces-endpoint 0.0.0.0:4317 <other option>
python -m sglang.launch_server --trace-level 3 --otlp-traces-endpoint 0.0.0.0:4317 [--trace-module request] <other option>
# start the mini lb
python -m sglang_router.launch_router --enable-trace --otlp-traces-endpoint 0.0.0.0:4317 <other option>
```
Expand Down Expand Up @@ -61,62 +61,65 @@ We have already inserted instrumentation points in the tokenizer and scheduler m
```
The "thread label" can be regarded as the name of the thread, used to distinguish different threads in the visualization view.

2. Mark the beginning and end of a request
2. create a time recorder for a request
Each request needs to call `SglangStageContext()` to initialize a time recorder, which is used to generate slice spans and request stage metrics. You can either store it within the request object or maintain it as a global variable. A set of APIs for managing the global time recorder is provided in `python/sglang/srt/tracing/trace_metric_warpper.py`.

3. Mark the beginning and end of a request
```
trace_req_start(rid, bootstrap_room)
trace_req_finish(rid)
# The time recorder calls trace_req_start() by default when it is created.
stage_context.trace_req_finish()
```
These two APIs must be called within the same process, for example, in the tokenizer.
SglangStageContext() and trace_req_finish() must be called within the same process, for example, in the tokenizer.

3. Add tracing for slice
4. Add tracing for slice

* Add slice tracing normally:
```python
trace_slice_start("slice A", rid)
trace_slice_end("slice A", rid)
stage_context.metric_trace_slice_start(RequestStage.TOKENIZER)
stage_context.metric_trace_slice_end(RequestStage.TOKENIZER)
```

- Use the "anonymous" flag to not specify a slice name at the start of the slice, allowing the slice name to be determined by trace_slice_end.
- Use the `ANONYMOUS` to not specify a slice name at the start of the slice, allowing the slice name to be determined by trace_slice_end.
<br>Note: Anonymous slices must not be nested.
```python
trace_slice_start("", rid, anonymous = True)
trace_slice_end("slice A", rid)
stage_context.metric_trace_slice_start(RequestStage.ANONYMOUS)
stage_context.metric_trace_slice_end(RequestStage.TOKENIZER)
```

- In trace_slice_end, use auto_next_anon to automatically create the next anonymous slice, which can reduce the number of instrumentation points needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be?

Suggested change
- In trace_slice_end, use auto_next_anon to automatically create the next anonymous slice, which can reduce the number of instrumentation points needed.
- In metric_trace_slice_end, use auto_next_anon to automatically create the next anonymous slice, which can reduce the number of instrumentation points needed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I will recorrect it

```python
trace_slice_start("", rid, anonymous = True)
trace_slice_end("slice A", rid, auto_next_anon = True)
trace_slice_end("slice B", rid, auto_next_anon = True)
trace_slice_end("slice C", rid, auto_next_anon = True)
trace_slice_end("slice D", rid)
stage_context.metric_trace_slice_start(RequestStage.ANONYMOUS)
stage_context.metric_trace_slice_end(RequestStage.A, auto_next_anon = True)
stage_context.metric_trace_slice_end(RequestStage.B, auto_next_anon = True)
stage_context.metric_trace_slice_end(RequestStage.C, auto_next_anon = True)
stage_context.metric_trace_slice_end(RequestStage.D)
```
- The end of the last slice in a thread must be marked with thread_finish_flag=True; otherwise, the thread's span will not be properly generated.
```python
trace_slice_end("slice D", rid, thread_finish_flag = True)
stage_context.metric_trace_slice_end(RequestStage.D, thread_finish_flag = True)
```

4. When the request execution flow transfers to another thread, the trace context needs to be explicitly propagated.
5. When the request execution flow transfers to another thread, the trace context needs to be explicitly propagated.
- sender: Execute the following code before sending the request to another thread via ZMQ
```python
trace_context = trace_get_proc_propagate_context(rid)
req.trace_context = trace_context
trace_context = stage_context.trace_get_proc_propagate_context(rid)
req.stage_context = trace_context
```
- receiver: Execute the following code after receiving the request via ZMQ
```python
trace_set_proc_propagate_context(rid, req.trace_context)
stage_context = SglangStageContext(......,propagation_context = req.stage_context)
```

5. When the request execution flow transfers to another node(PD disaggregation), the trace context needs to be explicitly propagated.
6. When the request execution flow transfers to another node(PD disaggregation), the trace context needs to be explicitly propagated.
- sender: Execute the following code before sending the request to node thread via http
```python
trace_context = trace_get_remote_propagate_context(bootstrap_room_list)
trace_context = trace_get_remote_propagate_context_batch(bootstrap_room_list)
headers = {"trace_context": trace_context}
session.post(url, headers=headers)
```
- receiver: Execute the following code after receiving the request via http
```python
trace_set_remote_propagate_context(request.headers['trace_context'])
trace_set_remote_propagate_context_batch(request.headers['trace_context'])
```

## How to Extend the Tracing Framework to Support Complex Tracing Scenarios
Expand Down
24 changes: 13 additions & 11 deletions python/sglang/srt/disaggregation/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
prepare_abort,
)
from sglang.srt.layers.dp_attention import get_attention_tp_size
from sglang.srt.managers.schedule_batch import FINISH_ABORT, RequestStage, ScheduleBatch
from sglang.srt.managers.schedule_batch import FINISH_ABORT, ScheduleBatch
from sglang.srt.managers.utils import GenerationBatchResult
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
Expand All @@ -60,7 +60,7 @@
ReqToTokenPool,
SWAKVPool,
)
from sglang.srt.tracing.trace import trace_event_batch, trace_slice_end
from sglang.srt.tracing.trace_metric_warpper import RequestStage, trace_event_batch
from sglang.srt.utils import get_int_env_var
from sglang.srt.utils.torch_memory_saver_adapter import TorchMemorySaverAdapter

Expand Down Expand Up @@ -316,8 +316,9 @@ def add(self, req: Req, is_retracted: bool = False) -> None:
prefill_dp_rank=req.data_parallel_rank,
)

req.add_latency(RequestStage.DECODE_PREPARE)
trace_slice_end(RequestStage.DECODE_PREPARE, req.rid, auto_next_anon=True)
req.stage_context.metric_trace_slice_end(
RequestStage.DECODE_PREPARE, auto_next_anon=True
)
self.queue.append(
DecodeRequest(req=req, kv_receiver=kv_receiver, waiting_for_input=False)
)
Expand Down Expand Up @@ -531,9 +532,9 @@ def pop_preallocated(self) -> List[DecodeRequest]:
decode_req.req.time_stats.decode_transfer_queue_entry_time = (
time.perf_counter()
)
decode_req.req.add_latency(RequestStage.DECODE_BOOTSTRAP)
trace_slice_end(
RequestStage.DECODE_BOOTSTRAP, decode_req.req.rid, auto_next_anon=True

decode_req.req.stage_context.metric_trace_slice_end(
RequestStage.DECODE_BOOTSTRAP, auto_next_anon=True
)

self.queue = [
Expand Down Expand Up @@ -715,9 +716,8 @@ def _commit_transfer_to_req(self, decode_req: DecodeRequest) -> None:

decode_req.kv_receiver.clear()
decode_req.kv_receiver = None
trace_slice_end(
decode_req.req.stage_context.metric_trace_slice_end(
RequestStage.DECODE_TRANSFERRED,
decode_req.req.rid,
auto_next_anon=True,
)
decode_req.req.time_stats.wait_queue_entry_time = time.perf_counter()
Expand Down Expand Up @@ -757,6 +757,7 @@ def pop_transferred(self) -> List[Req]:
self._commit_transfer_to_req(decode_req)
indices_to_remove.add(i)
transferred_reqs.append(decode_req.req)

elif poll in [
KVPoll.Bootstrapping,
KVPoll.WaitingForInput,
Expand All @@ -769,7 +770,6 @@ def pop_transferred(self) -> List[Req]:
for i in indices_to_remove:
idx = self.queue[i].metadata_buffer_index
assert idx != -1
self.queue[i].req.add_latency(RequestStage.DECODE_TRANSFERRED)
self.req_to_metadata_buffer_idx_allocator.free(idx)

self.queue = [
Expand Down Expand Up @@ -905,7 +905,9 @@ def get_new_prebuilt_batch(self: Scheduler) -> Optional[ScheduleBatch]:
# we can only add at least `num_not_used_batch` new batch to the running queue
if i < num_not_used_batch:
can_run_list.append(req)
req.add_latency(RequestStage.DECODE_WAITING)
req.stage_context.metric_trace_slice_end(
RequestStage.DECODE_WAITING, auto_next_anon=True
)
req.init_next_round_input(self.tree_cache)
else:
waiting_queue.append(req)
Expand Down
34 changes: 15 additions & 19 deletions python/sglang/srt/disaggregation/prefill.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,14 @@
poll_and_all_reduce,
prepare_abort,
)
from sglang.srt.managers.schedule_batch import (
FINISH_LENGTH,
Req,
RequestStage,
ScheduleBatch,
)
from sglang.srt.managers.schedule_batch import FINISH_LENGTH, Req, ScheduleBatch
from sglang.srt.mem_cache.common import release_kv_cache
from sglang.srt.mem_cache.memory_pool import (
HybridLinearKVPool,
NSATokenToKVPool,
SWAKVPool,
)
from sglang.srt.tracing.trace import trace_event_batch, trace_slice, trace_slice_end
from sglang.srt.tracing.trace_metric_warpper import RequestStage, trace_event_batch
from sglang.srt.utils import broadcast_pyobj, point_to_point_pyobj

if TYPE_CHECKING:
Expand Down Expand Up @@ -198,9 +193,10 @@ def add(self, req: Req, num_kv_heads: int) -> None:
pp_rank=self.pp_rank,
)
self._process_req(req)
req.add_latency(RequestStage.PREFILL_PREPARE)
self.queue.append(req)
trace_slice_end(RequestStage.PREFILL_PREPARE, req.rid, auto_next_anon=True)
req.stage_context.metric_trace_slice_end(
RequestStage.PREFILL_PREPARE, auto_next_anon=True
)

def extend(self, reqs: List[Req], num_kv_heads: int) -> None:
for req in reqs:
Expand Down Expand Up @@ -290,10 +286,9 @@ def pop_bootstrapped(
bootstrapped_reqs.append(req)
indices_to_remove.add(i)
req.time_stats.wait_queue_entry_time = time.perf_counter()
req.add_latency(RequestStage.PREFILL_BOOTSTRAP)

trace_slice_end(
RequestStage.PREFILL_BOOTSTRAP, req.rid, auto_next_anon=True
req.stage_context.metric_trace_slice_end(
RequestStage.PREFILL_BOOTSTRAP, auto_next_anon=True
)

self.queue = [
Expand Down Expand Up @@ -431,8 +426,6 @@ def process_batch_result_disagg_prefill(
# There is no output_ids for prefill
req.output_ids.append(next_token_id)
self.tree_cache.cache_unfinished_req(req) # update the tree and lock
req.add_latency(RequestStage.PREFILL_FORWARD)
trace_slice(RequestStage.PREFILL_FORWARD, req.rid, auto_next_anon=True)
self.disagg_prefill_inflight_queue.append(req)
if self.spec_algorithm.is_eagle() and batch.spec_info is not None:
req.output_topk_p = batch.spec_info.topk_p[i]
Expand All @@ -459,6 +452,9 @@ def process_batch_result_disagg_prefill(
logprob_pt += num_input_logprobs
self.send_kv_chunk(req, last_chunk=True)
req.time_stats.prefill_transfer_queue_entry_time = time.perf_counter()
req.stage_context.metric_trace_slice(
RequestStage.PREFILL_FORWARD, auto_next_anon=True
)

if req.grammar is not None:
# FIXME: this try-except block is for handling unexpected xgrammar issue.
Expand Down Expand Up @@ -497,8 +493,9 @@ def process_batch_result_disagg_prefill(

if self.enable_overlap:
self.send_kv_chunk(req, last_chunk=False, end_idx=req.tmp_end_idx)
trace_slice(
RequestStage.PREFILL_CHUNKED_FORWARD, req.rid, auto_next_anon=True
req.stage_context.metric_trace_slice(
RequestStage.PREFILL_CHUNKED_FORWARD,
auto_next_anon=(req.is_chunked != 0),
)

self.maybe_send_health_check_signal()
Expand Down Expand Up @@ -568,11 +565,10 @@ def process_disagg_prefill_inflight_queue(
)
for req in done_reqs:
req: Req
req.add_latency(RequestStage.PREFILL_TRANSFER_KV_CACHE)
self.req_to_metadata_buffer_idx_allocator.free(req.metadata_buffer_index)
req.metadata_buffer_index = -1
trace_slice(
RequestStage.PREFILL_TRANSFER_KV_CACHE, req.rid, thread_finish_flag=True
req.stage_context.metric_trace_slice(
RequestStage.PREFILL_TRANSFER_KV_CACHE, thread_finish_flag=True
)

self.disagg_prefill_inflight_queue = undone_reqs
Expand Down
2 changes: 1 addition & 1 deletion python/sglang/srt/entrypoints/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def __init__(self, **kwargs):
)

# Enable tracing
if server_args.enable_trace:
if server_args.trace_level > 0:
process_tracing_init(server_args.otlp_traces_endpoint, "sglang")
thread_label = "Tokenizer"
if server_args.disaggregation_mode == "prefill":
Expand Down
2 changes: 1 addition & 1 deletion python/sglang/srt/entrypoints/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ async def lifespan(fast_api_app: FastAPI):
enable_func_timer()

# Init tracing
if server_args.enable_trace:
if server_args.trace_level > 0:
process_tracing_init(server_args.otlp_traces_endpoint, "sglang")
if server_args.disaggregation_mode == "prefill":
thread_label = "Prefill" + thread_label
Expand Down
37 changes: 21 additions & 16 deletions python/sglang/srt/managers/data_parallel_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,15 @@
TokenizedGenerateReqInput,
WatchLoadUpdateReq,
)
from sglang.srt.managers.schedule_batch import Req, RequestStage
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.managers.scheduler import run_scheduler_process
from sglang.srt.server_args import (
DP_ATTENTION_HANDSHAKE_PORT_DELTA,
PortArgs,
ServerArgs,
)
from sglang.srt.tracing.trace import (
process_tracing_init,
trace_get_proc_propagate_context,
trace_set_proc_propagate_context,
trace_set_thread_info,
trace_slice_end,
trace_slice_start,
)
from sglang.srt.tracing.trace import process_tracing_init, trace_set_thread_info
from sglang.srt.tracing.trace_metric_warpper import RequestStage, SglangStageContext
from sglang.srt.utils.common import (
bind_port,
configure_ipv6,
Expand Down Expand Up @@ -178,15 +172,26 @@ def handle_load_update_req(self, obj):
self.dp_budget.update_budget(obj)

def dispatching_with_trace(self, req: Req):
if self.server_args.enable_trace:
trace_set_proc_propagate_context(req.rid, req.trace_context)
trace_slice_start(RequestStage.DC_DISPATCH, req.rid)
req.trace_context = trace_get_proc_propagate_context(req.rid)
if self.server_args.trace_level > 0:
bootstrap_room = (
req.bootstrap_room if hasattr(req, "bootstrap_room") else None
)
stage_context = SglangStageContext(
req.rid,
bootstrap_room=bootstrap_room,
module_name="request",
server_args=self.server_args,
propagation_context=req.stage_context,
)
stage_context.metric_trace_slice_start(RequestStage.DC_DISPATCH)
req.stage_context = stage_context.trace_get_proc_propagate_context()

self.dispatching(req)

if self.server_args.enable_trace:
trace_slice_end(RequestStage.DC_DISPATCH, req.rid, thread_finish_flag=True)
if self.server_args.trace_level > 0:
stage_context.metric_trace_slice_end(
RequestStage.DC_DISPATCH, thread_finish_flag=True
)

def init_dispatcher(self):
self._request_dispatcher = TypeBasedDispatcher(
Expand Down Expand Up @@ -516,7 +521,7 @@ def run_data_parallel_controller_process(
parent_process = psutil.Process().parent()

configure_logger(server_args)
if server_args.enable_trace:
if server_args.trace_level > 0:
process_tracing_init(server_args.otlp_traces_endpoint, "sglang")
thread_label = "DP Controller"
if server_args.disaggregation_mode == "prefill":
Expand Down
Loading
Loading