[SKILL] Upgrade sglang profile and auto_benchmark skills (#24250)

This commit is contained in:
Xiaoyu Zhang
2026-05-02 10:12:47 +08:00
committed by GitHub
parent 4c2ed9a254
commit 321298da75
105 changed files with 10996 additions and 3813 deletions
@@ -0,0 +1,291 @@
---
name: sglang-prod-incident-triage
description: Replay-first debug flow for SGLang serving problems. Use when a live or recent server shows health-check failures, latency or throughput regressions, queue growth, timeouts, distributed stalls, crash dumps, wrong outputs after deploys, or PD/EP/HiCache issues, and the job is to turn the problem into a replay plus the right next debug tool.
---
# SGLang Serving Debug
## Overview
Use this skill to turn a live serving problem into a debug path you can replay.
Use one loop:
- collect a baseline bundle
- save the failing request or crash dump
- replay on a clean target
- only then switch tools
Do not start with profiling.
This skill should work with more focused skills instead of re-implementing them:
- `debug-cuda-crash` when replay plus coredump points to a CUDA crash path
- `debug-distributed-hang` when the problem is clearly a TP/PP/DP/EP hang
- `llm-torch-profiler-analysis` when the issue is already narrowed to a
compute-side path
Three examples are included:
- TTFT spike with low queue time
- replay-first CUDA crash flow
- request-shaped distributed hang flow
## Output Contract
Return:
- problem class
- what was checked
- strongest signal so far
- current best guess
- what was ruled out
- next step
- production risk
## When To Use It
- `/health` or `/health_generate` is unhealthy
- latency or throughput regressed under serving load
- queue size grows while health still looks green
- one request class times out or hangs
- the server crashes only after some requests
- outputs changed after a deploy, topology change, or weight switch
- one older commit is known-good and a newer commit is known-bad
## Workflow
### 1. Collect a baseline bundle
If a live server is reachable, collect a read-only bundle before anything more
intrusive:
```bash
python3 scripts/incident_artifact_tool.py collect-bundle \
--base-url http://127.0.0.1:30000 \
--outdir /tmp/incident_bundle
python3 scripts/incident_artifact_tool.py summarize-bundle \
/tmp/incident_bundle
```
If the server is protected:
```bash
python3 scripts/incident_artifact_tool.py collect-bundle \
--base-url http://127.0.0.1:30000 \
--token "$SGLANG_BEARER_TOKEN" \
--outdir /tmp/incident_bundle
```
The bundle script collects:
- `/health`
- `/health_generate`
- `/model_info`
- `/server_info`
- `/v1/loads?include=all`
- `/v1/loads?include=core,queues,disagg,spec`
- `/metrics`
- `/hicache/storage-backend` on a best-effort basis
Use the summary for a quick read on:
- health vs. active health state
- topology and runtime flags
- point-in-time queue and token usage
- TTFT / E2E / queue-time heuristics from Prometheus metrics
If the summary says the bundle was captured while the server was idle, recollect
it during traffic or move quickly to dump plus replay.
If no live server is reachable, start from the best dump or log already available:
- crash dump
- request dump
- logs
- CUDA coredump
- OTel trace
- torch profile
### 2. Save the failing request
Read [references/decision-tree.md](references/decision-tree.md) only if the
problem class is still unclear:
- server down or unhealthy
- latency or throughput regression
- wrong output or behavior regression
- intermittent timeout or hang
Then preserve the request payload that actually triggers the problem:
- crash path: use `--crash-dump-folder`
- non-crash path: enable request dump or save the exact trigger request
Do not jump straight from a live symptom to low-level debugging without first
saving something you can replay.
### 3. Replay on a clean target
Read [references/endpoints-and-signals.md](references/endpoints-and-signals.md)
when you need help reading the baseline bundle or the replay target.
Read [references/replay-trace-profile.md](references/replay-trace-profile.md)
when you need the replay, trace, profile, or bisect paths.
Standard order:
1. collect baseline bundle
2. capture request dump or crash dump
3. restart a clean debug target if needed
4. replay the same issue
5. collect replay-time logs and dumps
### 4. Only go deeper after replay
#### Replay
Use replay when:
- a crash dump exists
- a request dump exists
- the problem depends on request shape or workload mix
If a crash dump exists, summarize it first:
```bash
python3 scripts/incident_artifact_tool.py summarize-dump \
--input-file /path/to/crash_dump.pkl
```
Then replay:
```bash
python3 /path/to/sglang/scripts/playground/replay_request_dump.py \
--input-file /path/to/crash_dump.pkl \
--host 127.0.0.1 \
--port 30000 \
--parallel 128
```
If `safe_pickle_load` blocks a locally captured trusted dump, use:
```bash
python3 scripts/replay_trusted_request_dump.py \
--input-file /path/to/request_dump.pkl \
--host 127.0.0.1 \
--port 30000 \
--parallel 1
```
If replay indicates a CUDA crash path, restart the same build with coredumps
enabled before reproducing again:
```bash
SGLANG_CUDA_COREDUMP=1 \
SGLANG_CUDA_COREDUMP_DIR=/tmp/sglang_cuda_coredumps \
python -m sglang.launch_server \
--model-path ... \
--crash-dump-folder /tmp/sglang_crash_dump \
...
```
Then inspect the generated coredump:
```bash
cuda-gdb "$(which python3)" \
-ex "target cudacore /tmp/sglang_cuda_coredumps/cuda_coredump_<host>.<pid>.<ts>"
```
For a replay-first crash example, read
[references/case-studies.md](references/case-studies.md).
#### OTel trace
Use tracing when:
- request-stage timing is unclear
- router vs. worker attribution is unclear
- PD prefill/decode transfer may be implicated
If tracing was enabled at startup, you can change the level without restart:
```bash
curl "http://127.0.0.1:30000/set_trace_level?level=1"
curl "http://127.0.0.1:30000/set_trace_level?level=2"
```
#### Torch profile
Use profiling when:
- the issue is already narrowed to compute-side ownership
- replay already reproduces the problem
- metrics and loads do not explain the regression
At that point, switch to `llm-torch-profiler-analysis`. Do not duplicate
its profiling workflow here.
For a low-noise latency example, read
[references/case-studies.md](references/case-studies.md).
#### Distributed hang
If this looks like a collective stall, save the failing request, replay it on a
clean target, collect the replay-time bundle and stacks, then switch to
`debug-distributed-hang`.
For an example of that flow, read
[references/case-studies.md](references/case-studies.md).
#### Regression between two commits
If one commit is known-good and another is known-bad, build a deterministic
harness before doing deeper manual debugging:
1. choose a stable reproducer: request replay, benchmark command, or correctness check
2. make the harness return `0` on good behavior and non-zero on bad behavior
3. run `git bisect start <bad> <good>`
4. run `git bisect run <harness>`
5. return here only after a candidate commit is isolated
Prefer replay-backed bisect when the regression depends on request shape or
long-running serving state.
### 6. Switch tools when the boundary is clear
Switch tools once the fault class is clear:
- `llm-torch-profiler-analysis` for kernel and overlap attribution
- `debug-distributed-hang` for collective or rank-divergence hangs
- `debug-cuda-crash` for CUDA crash reproduction and kernel API logging
Do not switch tools before collecting the first bundle unless the user already has
decisive logs or dumps.
## References
Load only what the current step needs:
- [references/decision-tree.md](references/decision-tree.md)
- problem classes, tool switch points, return shape
- [references/endpoints-and-signals.md](references/endpoints-and-signals.md)
- endpoint behavior, auth notes, field reading
- [references/replay-trace-profile.md](references/replay-trace-profile.md)
- request dump, crash dump, replay, trace, profiler step, bisect
- [references/case-studies.md](references/case-studies.md)
- compact examples for replay-first CUDA crash, latency, and distributed-hang triage
## Scripts
- [scripts/incident_artifact_tool.py](scripts/incident_artifact_tool.py)
- collect a read-only live bundle
- summarize a collected bundle into a compact debug note
- summarize a trusted request dump or crash dump before replay
- [scripts/replay_trusted_request_dump.py](scripts/replay_trusted_request_dump.py)
- replay a trusted request dump when `safe_pickle_load` blocks stock replay
If a live bundle was collected, include its path.
If replay, trace, or profiling was chosen, say why bundle plus dump were not enough.
@@ -0,0 +1,81 @@
# Case Studies
Use these examples only after the live bundle and request dump point toward the
same class of failure. They are patterns for how to reason from replayable
evidence, not recipes to copy blindly.
## CUDA Crash: Upstream Top-K Corruption, Downstream MoE OOB
Use when a replayed CUDA crash lands in a MoE align or shared-memory kernel but
the suspicious data was produced by an earlier routing kernel.
Shape that made the original case useful:
- model family: Qwen3 MoE
- visible crash: `moe_align_block_size_kernel`
- likely producer: `topkGatingSoftmax` / MoE top-k routing
- evidence path: crash dump -> replay -> CUDA coredump -> walk one kernel
upstream from the visible fault
Triage loop:
```text
summarize crash dump
-> replay the exact request
-> enable CUDA coredump on the replay target
-> identify the failing kernel
-> inspect the immediately preceding producer kernel and tensors
```
Key lesson: a consumer kernel can be the first one to fault even when the bad
index was produced earlier. Preserve the request shape before changing prompts.
## Latency: TTFT Spike With Low Queue Time
Use when `/health` and `/health_generate` are green, queue depth is low, but TTFT
is still high.
Signals from the original case:
- `waiting=0`
- average queue time was tiny
- TTFT was high
- scheduler stage timing pointed to prefill forward time
Triage loop:
```text
collect live bundle
-> save the slow request
-> replay the same request on a clean target
-> profile only after replay reproduces compute-side ownership
```
Key lesson: rule out queue pressure with `/v1/loads`, `/metrics`, and stage
timing before opening a profiler trace.
## Distributed Hang: Request-Shaped TP Collective Mismatch
Use when one request hangs, ranks stop making progress differently, and the
failure looks like a generic serving stall until replay isolates it.
Shape that made the original case useful:
- a prompt tokenized to a specific extend length
- one TP rank skipped a logits `all_gather`
- the peer rank still entered the real collective
- the request never returned
Triage loop:
```text
collect healthy bundle
-> save the trigger request
-> replay on a clean target
-> collect rank stacks and replay-time bundle
-> switch to debug-distributed-hang
```
Key lesson: once the symptom looks like rank divergence or a collective mismatch,
do not keep profiling kernels. Preserve the replay and move to distributed-hang
debugging.
@@ -0,0 +1,197 @@
# SGLang First Checks
Use this reference when the problem class is still unclear and you need a fast
starting point.
## Default Order
1. classify the symptom
2. collect the fastest useful signal
3. save the failing request or dump
4. replay before you profile
Do not start with `torch.profiler` unless the issue is already clearly
compute-side.
If one commit is known-good and another is known-bad, turn the problem into a
stable `git bisect run <harness>` first.
## Problem Classes
### Server down or unhealthy
Check:
- `/health`
- `/health_generate`
- `/server_info`
- recent stderr/stdout
- crash dump status if `--crash-dump-folder` is enabled
Likely directions:
- startup or weight-load failure
- deadlock or blocked scheduler
- CUDA crash or OOM
- auth or routing mismatch
### High latency or low throughput
Check:
- `/v1/loads?include=all`
- `/metrics`
- `/server_info`
- the exact request shape or benchmark command
Likely directions:
- queueing or capacity pressure
- cache hit rate collapse
- PD or EP topology mismatch
- speculative decoding disabled or ineffective
- kernel or backend regression
### Wrong output or behavior regression
Check:
- exact request and expected output
- `/model_info`
- `/server_info`
- current weights or recent config change
Likely directions:
- wrong weights or wrong revision
- chat template, parser, or tool config drift
- multimodal preprocessing drift
- quantization or kernel correctness bug
### Timeout or hang
Check:
- `/health`
- `/health_generate`
- `/v1/loads?include=all`
- request dumps if enabled
- per-rank logs
- OTel trace if already enabled
Likely directions:
- distributed divergence or collective hang
- queue starvation or retraction storm
- PD transfer stall
- storage or HiCache backend stall
## Quick Paths
### TTFT spike
Start with:
- `/v1/loads?include=all`
- `/metrics`
- `/server_info`
Watch for:
- `num_waiting_reqs` growth
- `token_usage` saturation
- `cache_hit_rate` drop
- PD queue buildup
If queue pressure does not explain the slowdown, save the slow request and
replay it.
### Throughput collapse
Start with:
- `/v1/loads?include=all`
- `/metrics`
- benchmark reproduction if available
Watch for:
- low `gen_throughput`
- queue growth
- low cache hit rate
- speculative metrics collapse
- PD transfer or decode prealloc queues backing up
### Crash after some requests
Start with:
- crash dump folder
- stderr/stdout
- request dump folder if available
Then replay the crash dump or recent request dump.
### Regression between two commits
Start with:
- known-good commit
- known-bad commit
- one stable pass/fail harness
Best move:
- `git bisect run <harness>`
### One request class fails
Start with:
- exact request payload
- request dump if available
- smallest reproduction request
Typical categories:
- multimodal edge case
- parser or structured output bug
- model-specific kernel path
- tool-call formatting issue
## When To Switch Tools
### Use replay when
- a crash dump or request dump already exists
- the issue depends on request shape or workload mix
- you need one stable reproducer before going deeper
### Use OTel trace when
- request-stage timing is unclear
- router vs. worker ownership is unclear
- PD boundaries may be involved
### Use torch profiler when
- replay already reproduces the issue
- queueing and routing are mostly ruled out
- you need kernel-level attribution
At that point, switch to `llm-torch-profiler-analysis`.
### Use lower-level debug paths when
- replay plus trace still leave ambiguity
- the problem looks like a specific crash, hang, or correctness bug
## What To Return
- problem class
- what was checked
- strongest signal so far
- current best guess
- what was ruled out
- next step
- production risk
@@ -0,0 +1,218 @@
# SGLang Endpoints and Signals
Use this reference when checking a live server.
## Auth
Most read endpoints are public unless the server is protected by `api_key` or
`admin_api_key`.
Use:
```bash
curl -H "Authorization: Bearer <token>" ...
```
Rules:
- normal protected endpoints require `api_key`
- admin endpoints require `admin_api_key`
- some HiCache endpoints fail if `admin_api_key` is not configured at all
- `/health` and metrics-style health checks are usually still exposed
## Core Endpoints
### `/health`
Cheap liveness check.
- `200`: process is alive enough to answer health
- `503`: starting, shutting down, or unhealthy
`/health` alone is not enough for latency or hang diagnosis.
### `/health_generate`
Active health check.
- exercises a real generate or embedding path
- catches stuck schedulers or broken worker paths that `/health` can miss
Use this when requests time out but `/health` is still green.
### `/model_info`
Use for model identity:
- `model_path`
- `tokenizer_path`
- `is_generation`
- `weight_version`
- multimodal flags
- model type or architectures
This is the first check for wrong-output or wrong-weight problems.
### `/server_info`
Use for runtime shape:
- serialized `server_args`
- scheduler info
- per-DP `internal_states`
- SGLang version
This is usually the single best live snapshot.
## Load And Capacity
### `/v1/loads?include=all`
Best structured load endpoint for a first pass.
Useful fields:
- `num_running_reqs`
- `num_waiting_reqs`
- `num_total_tokens`
- `num_used_tokens`
- `token_usage`
- `gen_throughput`
- `cache_hit_rate`
- `memory`
- `speculative`
- `disaggregation`
- `queues`
Useful queries:
```bash
curl -s http://127.0.0.1:30000/v1/loads
curl -s "http://127.0.0.1:30000/v1/loads?include=all"
curl -s "http://127.0.0.1:30000/v1/loads?include=core,queues,disagg"
curl -s "http://127.0.0.1:30000/v1/loads?format=prometheus"
```
What to look for:
- high `num_waiting_reqs` with low compute throughput usually means queueing or capacity pressure
- `token_usage` near `1.0` usually means KV or token-capacity pressure
- low `cache_hit_rate` after a deploy can explain TTFT regressions
- PD queue fields often explain transfer or prealloc bottlenecks hidden by plain queue size
### `/metrics`
Prometheus endpoint. Use it when you need trends rather than one live snapshot.
High-value metrics:
- `sglang:time_to_first_token_seconds`
- `sglang:time_per_output_token_seconds`
- `sglang:e2e_request_latency_seconds`
- `sglang:num_running_reqs`
- `sglang:num_queue_reqs`
- `sglang:num_used_tokens`
- `sglang:cache_hit_rate`
- `sglang:gen_throughput`
- `sglang:token_usage`
## Request Capture
### `/configure_logging`
Used by `python -m sglang.srt.managers.configure_logging`.
Main use:
- enable request logging
- set request logging level
- enable request dump folder
- set request dump threshold
Typical payload:
```json
{
"log_requests": true,
"log_requests_level": 3,
"dump_requests_folder": "/tmp/sglang_request_dump",
"dump_requests_threshold": 100
}
```
Use this when the problem is ongoing and you need the next failing request
without restarting the service.
## HiCache
### `GET /hicache/storage-backend`
Returns tokenizer-side HiCache storage status:
- `hicache_storage_backend`
- `hicache_storage_backend_extra_config`
- `hicache_storage_prefetch_policy`
- `hicache_write_policy`
Use this when long-context or PD problems may involve storage-backed KV reuse.
### `PUT /hicache/storage-backend`
### `DELETE /hicache/storage-backend`
Runtime attach or detach. These are operational actions, not passive checks.
## Profiling And Tracing Controls
### `/start_profile`
### `/stop_profile`
Use only after the problem is already narrowed down.
### `/set_trace_level?level=N`
Changes trace verbosity when tracing was enabled at startup.
Levels:
- `0`: disabled
- `1`: important slices
- `2`: all slices except nested ones
- `3`: all slices
## Quick Reads By Problem Type
### TTFT spike
Read:
- `/server_info`
- `/v1/loads?include=all`
- `/metrics`
Compare:
- queue size
- token usage
- cache hit rate
- PD disaggregation queues
### Hang or timeout
Read:
- `/health`
- `/health_generate`
- `/server_info`
- `/v1/loads?include=all`
If tracing is already enabled, look at trace data before heavier profiling.
### Wrong model behavior
Read:
- `/model_info`
- `/server_info`
- exact request payload and parser or template config
Do not jump to kernel profiling until config drift is ruled out.
@@ -0,0 +1,236 @@
# Replay, Trace, Profile, and Bisect
Use this reference after the first live checks. The goal is to turn the problem
into something repeatable.
## Save Requests
### Request dump
```bash
python3 -m sglang.srt.managers.configure_logging \
--url http://127.0.0.1:30000 \
--dump-requests-folder /tmp/sglang_request_dump \
--dump-requests-threshold 100
```
Use this when:
- the problem is intermittent
- you need the real request shape
- you do not want to restart the server
### Crash dump
If the server already runs with:
```bash
--crash-dump-folder /tmp/crash_dump
```
SGLang saves recent requests before a crash. Treat that dump as the best
starting point.
Summarize it first:
```bash
python3 scripts/incident_artifact_tool.py summarize-dump \
--input-file /path/to/crash_dump.pkl
```
Current crash-dump tests show at least:
- `server_args`
- `requests`
- `launch_command`
## Replay
Use the stock replay tool:
```bash
python3 scripts/playground/replay_request_dump.py \
--input-file /path/to/crash_dump.pkl \
--host 127.0.0.1 \
--port 30000 \
--parallel 128
```
Or replay a folder:
```bash
python3 scripts/playground/replay_request_dump.py \
--input-folder /path/to/request_dump_dir \
--file-number 10 \
--parallel 128
```
If `safe_pickle_load` blocks a locally captured trusted dump, use:
```bash
python3 scripts/replay_trusted_request_dump.py \
--input-file /path/to/request_dump.pkl \
--host 127.0.0.1 \
--port 30000 \
--parallel 1
```
If that happens, the allowlist is the problem, not the dump.
Use replay before profiling when:
- the issue depends on workload mix
- it only appears after some number of requests
- you need to compare two builds on the same traffic
## CUDA Restart-And-Replay
If replay points to a CUDA crash path, restart the same build with coredumps:
```bash
SGLANG_CUDA_COREDUMP=1 \
SGLANG_CUDA_COREDUMP_DIR=/tmp/sglang_cuda_coredumps \
python -m sglang.launch_server \
--model-path ... \
--crash-dump-folder /tmp/sglang_crash_dump \
...
```
Then inspect the coredump:
```bash
cuda-gdb "$(which python3)" \
-ex "target cudacore /tmp/sglang_cuda_coredumps/cuda_coredump_<host>.<pid>.<ts>"
```
Good first commands:
- `where`
- `info cuda kernels`
- `x/10i <pc>`
Use the coredump to find the failing kernel, not automatically the root-cause
kernel.
See:
- [case-studies.md](case-studies.md)
## Trace
Tracing must be enabled at startup:
```bash
python -m sglang.launch_server \
--enable-trace \
--otlp-traces-endpoint localhost:4317 \
...
```
Optional router command:
```bash
python -m sglang_router.launch_router \
--enable-trace \
--otlp-traces-endpoint localhost:4317 \
...
```
Useful environment variables:
```bash
export SGLANG_OTLP_EXPORTER_SCHEDULE_DELAY_MILLIS=500
export SGLANG_OTLP_EXPORTER_MAX_EXPORT_BATCH_SIZE=64
```
If tracing is already enabled, change the level without restart:
```bash
curl "http://127.0.0.1:30000/set_trace_level?level=1"
curl "http://127.0.0.1:30000/set_trace_level?level=2"
curl "http://127.0.0.1:30000/set_trace_level?level=3"
```
Use tracing for:
- router vs. worker delay
- tokenizer / scheduler / detokenizer timing
- PD transfer timing
- request timing across processes
If you already have OTEL JSON or JSONL, convert it for timeline inspection:
```bash
python3 scripts/convert_otel_2_perfetto.py \
--input /tmp/otel_trace.json \
--output /tmp/sglang_trace_perfetto.json
```
## Torch Profiler
Switch to `llm-torch-profiler-analysis` when:
- replay already reproduces the issue
- metrics and loads do not explain it
- the problem now looks compute-side
This skill should decide when to profile, not duplicate the profiler workflow.
## Bisect
If one commit is known-good and a newer commit is known-bad:
1. build a deterministic harness from the problem
2. prefer replay-based harnesses when the failure depends on request mix
3. use `git bisect run <harness>`
4. only then go back to trace or profile if needed
Example:
```bash
git bisect start <bad> <good>
git bisect run bash ./repro_or_check.sh
```
## Common Paths
### Crash
1. crash dump
2. summarize dump
3. replay
4. CUDA coredump plus `cuda-gdb`
5. `debug-cuda-crash` or narrower instrumentation
### TTFT regression
1. baseline metrics and loads
2. request dump
3. replay the slow request
4. trace if stage ownership is unclear
5. `llm-torch-profiler-analysis` if it still looks compute-side
See:
- [case-studies.md](case-studies.md)
### Distributed hang
1. healthy baseline bundle
2. save the trigger request
3. replay on a clean target
4. collect replay-time bundle and stacks
5. identify the NCCL or collective path
6. switch to `debug-distributed-hang`
See:
- [case-studies.md](case-studies.md)
### Throughput regression after deploy
1. compare `server_info`
2. compare `/metrics` and `/v1/loads`
3. replay stable workload
4. bisect if one older commit is known-good
5. profile only if compute still looks suspicious
@@ -0,0 +1,735 @@
#!/usr/bin/env python3
"""Collect or inspect serving bundles and dumps for SGLang debug."""
from __future__ import annotations
import argparse
import glob
import json
import math
import os
import pickle
import re
import time
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional, Sequence
from urllib import error, parse, request
METRIC_RE = re.compile(
r"^(?P<name>[^{\s]+)(?:\{(?P<labels>[^}]*)\})?\s+(?P<value>[-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)$"
)
LABEL_RE = re.compile(r'([a-zA-Z_:][a-zA-Z0-9_:]*)="((?:[^"\\]|\\.)*)"')
ENDPOINT_SPECS = (
("text", "health.txt", "/health"),
("text", "health_generate.txt", "/health_generate"),
("text", "metrics.txt", "/metrics"),
("json", "model_info.json", "/model_info"),
("json", "server_info.json", "/server_info"),
("json", "loads_all.json", "/v1/loads?include=all"),
(
"json",
"loads_core_queues_disagg.json",
"/v1/loads?include=core,queues,disagg,spec",
),
("json", "hicache_storage_backend.json", "/hicache/storage-backend"),
)
BUNDLE_NOTES = [
"This bundle is read-only. It does not start profiling or change trace level.",
"HiCache status may fail if admin_api_key is not configured or the wrong bearer token was used.",
"loads_all.json is the best point-in-time load snapshot in this bundle.",
"metrics.txt is raw Prometheus text intended for follow-up parsing.",
]
def request_text(
base_url: str,
path: str,
token: Optional[str],
timeout: float = 10.0,
) -> tuple[bool, int, str]:
url = parse.urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
req = request.Request(url)
if token:
req.add_header("Authorization", f"Bearer {token}")
try:
with request.urlopen(req, timeout=timeout) as resp:
body = resp.read().decode("utf-8", errors="replace")
return True, resp.status, body
except error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
return False, e.code, body
except Exception as e: # noqa: BLE001
return False, -1, f"{type(e).__name__}: {e}"
def request_endpoint(
base_url: str,
path: str,
token: Optional[str],
parse_json: bool,
timeout: float = 10.0,
) -> Dict[str, Any]:
ok, status, body = request_text(base_url, path, token, timeout=timeout)
result: Dict[str, Any] = {"ok": ok, "status": status, "path": path}
if not ok:
result["error"] = body
return result
if not parse_json:
result["text"] = body
return result
try:
result["json"] = json.loads(body)
except json.JSONDecodeError:
result["text"] = body
result["decode_error"] = "response was not valid JSON"
return result
def write_json(path: Path, obj: Dict[str, Any]) -> None:
path.write_text(
json.dumps(obj, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)
def write_text(path: Path, text: str) -> None:
path.write_text(text, encoding="utf-8")
def format_summary_line(filename: str, result: Dict[str, Any]) -> str:
if result.get("ok"):
return f"{filename}: ok"
return (
f"{filename}: failed status={result.get('status')} "
f"error={result.get('error')}"
)
def collect_bundle(
base_url: str,
token: Optional[str],
outdir: Optional[str],
timeout: float,
) -> Path:
timestamp = time.strftime("%Y%m%d_%H%M%S")
bundle_dir = Path(outdir or f"./incident_bundle_{timestamp}").resolve()
bundle_dir.mkdir(parents=True, exist_ok=True)
metadata = {
"artifact_type": "incident_bundle",
"base_url": base_url,
"collected_at": timestamp,
"token_provided": bool(token),
"timeout_seconds": timeout,
}
write_json(bundle_dir / "metadata.json", metadata)
summary_lines = []
for kind, filename, path in ENDPOINT_SPECS:
result = request_endpoint(
base_url, path, token, parse_json=(kind == "json"), timeout=timeout
)
output_path = bundle_dir / filename
if kind == "text" and result.get("ok"):
write_text(output_path, str(result.get("text", "")))
else:
write_json(
(
output_path
if kind == "json"
else bundle_dir / f"{filename}.error.json"
),
result,
)
summary_lines.append(format_summary_line(filename, result))
write_text(
bundle_dir / "SUMMARY.txt",
"\n".join(summary_lines + [""] + BUNDLE_NOTES) + "\n",
)
return bundle_dir
def load_json(path: Path) -> Optional[Dict[str, Any]]:
if not path.exists():
return None
return json.loads(path.read_text(encoding="utf-8"))
def unwrap_result(path: Path) -> Optional[Dict[str, Any]]:
obj = load_json(path)
if obj is None:
return None
if isinstance(obj, dict) and "json" in obj:
return obj.get("json")
return obj
def read_text(path: Path) -> Optional[str]:
if not path.exists():
return None
return path.read_text(encoding="utf-8")
def endpoint_ok(bundle_dir: Path, stem: str) -> bool:
return (bundle_dir / f"{stem}.txt").exists() and not (
bundle_dir / f"{stem}.txt.error.json"
).exists()
def parse_labels(raw: Optional[str]) -> Dict[str, str]:
if not raw:
return {}
labels = {}
for key, value in LABEL_RE.findall(raw):
labels[key] = bytes(value, "utf-8").decode("unicode_escape")
return labels
def parse_metrics(metrics_text: str) -> Dict[str, list[dict[str, Any]]]:
series: Dict[str, list[dict[str, Any]]] = defaultdict(list)
for line in metrics_text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
match = METRIC_RE.match(line)
if not match:
continue
series[match.group("name")].append(
{
"labels": parse_labels(match.group("labels")),
"value": float(match.group("value")),
}
)
return series
def metric_sum(metrics: Dict[str, list[dict[str, Any]]], name: str) -> float:
return sum(item["value"] for item in metrics.get(name, []))
def safe_div(
numerator: Optional[float], denominator: Optional[float]
) -> Optional[float]:
if numerator is None or denominator in (None, 0):
return None
return numerator / denominator
def coalesce(*values: Any) -> Any:
for value in values:
if value is not None:
return value
return None
def fmt_float(value: Optional[float], digits: int = 3) -> str:
if value is None or (
isinstance(value, float) and (math.isnan(value) or math.isinf(value))
):
return "n/a"
return f"{value:.{digits}f}"
def is_positive_number(value: Any, threshold: float = 0.0) -> bool:
return (
isinstance(value, (int, float))
and not math.isnan(value)
and not math.isinf(value)
and value > threshold
)
def compute_stage_averages(
metrics: Dict[str, list[dict[str, Any]]], sum_name: str, count_name: str
) -> Dict[str, float]:
grouped_sum: Dict[str, float] = defaultdict(float)
grouped_count: Dict[str, float] = defaultdict(float)
for item in metrics.get(sum_name, []):
stage = item["labels"].get("stage", "")
rank = item["labels"].get("tp_rank", "")
grouped_sum[f"{stage}|{rank}"] += item["value"]
for item in metrics.get(count_name, []):
stage = item["labels"].get("stage", "")
rank = item["labels"].get("tp_rank", "")
grouped_count[f"{stage}|{rank}"] += item["value"]
result: Dict[str, float] = {}
for key, total_sum in grouped_sum.items():
stage, _rank = key.split("|", 1)
avg = safe_div(total_sum, grouped_count.get(key))
if avg is None:
continue
result[stage] = max(result.get(stage, 0.0), avg)
return result
def add_signal(signals: list[str], text: str) -> None:
if text not in signals:
signals.append(text)
def build_bundle_summary(bundle_dir: Path) -> Dict[str, Any]:
metadata = load_json(bundle_dir / "metadata.json") or {}
model_info = unwrap_result(bundle_dir / "model_info.json") or {}
server_info = unwrap_result(bundle_dir / "server_info.json") or {}
loads_info = unwrap_result(bundle_dir / "loads_all.json") or {}
metrics_text = read_text(bundle_dir / "metrics.txt") or ""
metrics = parse_metrics(metrics_text)
aggregate = loads_info.get("aggregate") or {}
loads = loads_info.get("loads") or []
load0 = loads[0] if loads else {}
internal_states = server_info.get("internal_states") or []
runtime_state = internal_states[0] if internal_states else {}
memory_usage = runtime_state.get("memory_usage") or load0.get("memory") or {}
ttft_avg = safe_div(
metric_sum(metrics, "sglang:time_to_first_token_seconds_sum"),
metric_sum(metrics, "sglang:time_to_first_token_seconds_count"),
)
e2e_avg = safe_div(
metric_sum(metrics, "sglang:e2e_request_latency_seconds_sum"),
metric_sum(metrics, "sglang:e2e_request_latency_seconds_count"),
)
queue_avg = safe_div(
metric_sum(metrics, "sglang:queue_time_seconds_sum"),
metric_sum(metrics, "sglang:queue_time_seconds_count"),
)
per_stage_avg = compute_stage_averages(
metrics,
"sglang:per_stage_req_latency_seconds_sum",
"sglang:per_stage_req_latency_seconds_count",
)
summary: Dict[str, Any] = {
"artifact_type": "incident_bundle",
"bundle_dir": str(bundle_dir),
"base_url": metadata.get("base_url"),
"collected_at": metadata.get("collected_at"),
"health": {
"health_ok": endpoint_ok(bundle_dir, "health"),
"health_generate_ok": endpoint_ok(bundle_dir, "health_generate"),
},
"model": {
"model_path": model_info.get("model_path") or server_info.get("model_path"),
"served_model_name": server_info.get("served_model_name"),
"weight_version": model_info.get("weight_version")
or server_info.get("weight_version"),
"model_type": model_info.get("model_type"),
"is_generation": model_info.get("is_generation"),
},
"topology": {
"tp_size": server_info.get("tp_size"),
"dp_size": server_info.get("dp_size"),
"pp_size": server_info.get("pp_size"),
"ep_size": server_info.get("ep_size"),
"disaggregation_mode": server_info.get("disaggregation_mode"),
"attention_backend": server_info.get("attention_backend"),
"sampling_backend": server_info.get("sampling_backend"),
"schedule_policy": server_info.get("schedule_policy"),
"enable_trace": server_info.get("enable_trace"),
"enable_metrics": server_info.get("enable_metrics"),
},
"capacity": {
"max_total_num_tokens": server_info.get("max_total_num_tokens"),
"max_req_input_len": server_info.get("max_req_input_len"),
"effective_max_running_requests_per_dp": coalesce(
runtime_state.get("effective_max_running_requests_per_dp"),
load0.get("max_running_requests"),
),
"weight_gb": coalesce(
memory_usage.get("weight"), memory_usage.get("weight_gb")
),
"kv_cache_gb": coalesce(
memory_usage.get("kvcache"), memory_usage.get("kv_cache_gb")
),
"graph_gb": coalesce(
memory_usage.get("graph"), memory_usage.get("graph_gb")
),
"token_capacity": memory_usage.get("token_capacity"),
},
"point_in_time_load": {
"running_reqs": coalesce(
aggregate.get("total_running_reqs"), load0.get("num_running_reqs")
),
"waiting_reqs": coalesce(
aggregate.get("total_waiting_reqs"), load0.get("num_waiting_reqs")
),
"total_reqs": coalesce(
aggregate.get("total_reqs"), load0.get("num_total_reqs")
),
"token_usage": coalesce(
aggregate.get("avg_token_usage"), load0.get("token_usage")
),
"avg_throughput": coalesce(
aggregate.get("avg_throughput"), load0.get("gen_throughput")
),
"avg_utilization": coalesce(
aggregate.get("avg_utilization"), load0.get("utilization")
),
"cache_hit_rate": load0.get("cache_hit_rate"),
"queues": load0.get("queues"),
"disaggregation": load0.get("disaggregation"),
},
"metrics": {
"request_count": metric_sum(metrics, "sglang:num_requests_total"),
"prompt_tokens_total": metric_sum(metrics, "sglang:prompt_tokens_total"),
"generation_tokens_total": metric_sum(
metrics, "sglang:generation_tokens_total"
),
"avg_ttft_seconds": ttft_avg,
"avg_e2e_seconds": e2e_avg,
"avg_queue_time_seconds": queue_avg,
"stage_avg_seconds_max_tp_rank": per_stage_avg,
},
"signals": [],
}
signals = summary["signals"]
health = summary["health"]
point_in_time_load = summary["point_in_time_load"]
running_reqs = point_in_time_load.get("running_reqs")
waiting_reqs = point_in_time_load.get("waiting_reqs")
if health["health_ok"] and not health["health_generate_ok"]:
add_signal(
signals,
"/health is green but /health_generate failed. Suspect runtime or scheduler path, not just HTTP liveness.",
)
if not health["health_ok"]:
add_signal(
signals,
"/health failed. Start with startup, crash, or global unhealthy paths.",
)
if is_positive_number(waiting_reqs):
add_signal(
signals,
f"Point-in-time load shows queue buildup: waiting_reqs={waiting_reqs}.",
)
if (
point_in_time_load.get("token_usage") is not None
and point_in_time_load["token_usage"] >= 0.9
):
add_signal(
signals,
"Token usage is near saturation. KV or token-capacity pressure may explain latency.",
)
if (
ttft_avg is not None
and queue_avg is not None
and ttft_avg > 2.0
and queue_avg < 0.2
):
add_signal(
signals,
f"Average TTFT is high ({fmt_float(ttft_avg)}s) while average queue time is low ({fmt_float(queue_avg)}s). This looks more like prefill or request-path work than queue pressure.",
)
prefill_forward = per_stage_avg.get("prefill_forward")
request_process = per_stage_avg.get("request_process")
if (
prefill_forward is not None
and request_process is not None
and prefill_forward > max(0.5, request_process * 10)
):
add_signal(
signals,
f"Prefill forward dominates quick stage timing: prefill_forward~{fmt_float(prefill_forward)}s vs request_process~{fmt_float(request_process)}s.",
)
if running_reqs == 0 and waiting_reqs == 0:
add_signal(
signals,
"Bundle snapshot was captured while the server was effectively idle. Reproduce under live traffic or replayed workload if the problem is intermittent.",
)
return summary
def render_bundle_text(summary: Dict[str, Any]) -> str:
health = summary["health"]
model = summary["model"]
topology = summary["topology"]
capacity = summary["capacity"]
load = summary["point_in_time_load"]
metrics = summary["metrics"]
stage_avgs = metrics["stage_avg_seconds_max_tp_rank"]
lines = [
f"Bundle: {summary['bundle_dir']}",
f"Base URL: {summary.get('base_url') or 'n/a'}",
f"Collected At: {summary.get('collected_at') or 'n/a'}",
"",
f"Health: /health={'ok' if health['health_ok'] else 'failed'} /health_generate={'ok' if health['health_generate_ok'] else 'failed'}",
f"Model: {model.get('model_path') or 'n/a'} weight_version={model.get('weight_version') or 'n/a'} type={model.get('model_type') or 'n/a'}",
"Topology: "
f"tp={topology.get('tp_size')} dp={topology.get('dp_size')} pp={topology.get('pp_size')} ep={topology.get('ep_size')} "
f"disagg={topology.get('disaggregation_mode')} trace={topology.get('enable_trace')} metrics={topology.get('enable_metrics')}",
"Capacity: "
f"max_total_tokens={capacity.get('max_total_num_tokens')} "
f"max_running_reqs={capacity.get('effective_max_running_requests_per_dp')} "
f"weight_gb={fmt_float(capacity.get('weight_gb'))} "
f"kv_cache_gb={fmt_float(capacity.get('kv_cache_gb'))} "
f"graph_gb={fmt_float(capacity.get('graph_gb'))}",
"Point-in-time load: "
f"running={load.get('running_reqs')} waiting={load.get('waiting_reqs')} total={load.get('total_reqs')} "
f"token_usage={fmt_float(load.get('token_usage'))} throughput={fmt_float(load.get('avg_throughput'))} "
f"cache_hit_rate={fmt_float(load.get('cache_hit_rate'))}",
"Metrics: "
f"requests={fmt_float(metrics.get('request_count'), 0)} "
f"prompt_tokens={fmt_float(metrics.get('prompt_tokens_total'), 0)} "
f"generation_tokens={fmt_float(metrics.get('generation_tokens_total'), 0)} "
f"avg_ttft_s={fmt_float(metrics.get('avg_ttft_seconds'))} "
f"avg_e2e_s={fmt_float(metrics.get('avg_e2e_seconds'))} "
f"avg_queue_s={fmt_float(metrics.get('avg_queue_time_seconds'))}",
]
if stage_avgs:
stage_parts = [
f"{name}={fmt_float(value)}s" for name, value in sorted(stage_avgs.items())
]
lines.append("Stage Averages (max across TP ranks): " + ", ".join(stage_parts))
queues = load.get("queues") or {}
if queues:
lines.append(
"Queues: "
+ ", ".join(f"{key}={value}" for key, value in sorted(queues.items()))
)
disagg = load.get("disaggregation") or {}
if disagg:
lines.append(
"Disaggregation: "
+ ", ".join(f"{key}={value}" for key, value in sorted(disagg.items()))
)
lines.append("")
lines.append("What stands out:")
if summary["signals"]:
lines.extend(f"- {signal}" for signal in summary["signals"])
else:
lines.append("- No strong signal from this bundle.")
return "\n".join(lines) + "\n"
def get_field(obj: Any, name: str, default: Any = None) -> Any:
if obj is None:
return default
if isinstance(obj, dict):
return obj.get(name, default)
return getattr(obj, name, default)
def iter_dump_files(
input_file: Optional[str], input_folder: Optional[str]
) -> Sequence[Path]:
if input_file:
return [Path(input_file)]
if input_folder:
return [Path(p) for p in sorted(glob.glob(f"{input_folder}/*.pkl"))]
raise SystemExit("Either --input-file or --input-folder must be provided.")
def load_dump_payload(path: Path) -> dict[str, Any]:
with path.open("rb") as fh:
payload = pickle.load(fh)
if isinstance(payload, dict):
return payload
return {"requests": payload}
def pick_text_preview(req: Any) -> str:
candidates = [
get_field(req, "origin_input_text"),
get_field(req, "text"),
get_field(req, "prompt"),
]
for value in candidates:
if isinstance(value, str) and value:
return value
if isinstance(value, list) and value:
first = value[0]
if isinstance(first, str) and first:
return first
return ""
def format_timestamp(ts: Any) -> str:
if not isinstance(ts, (int, float)):
return "n/a"
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
def summarize_request(
record: tuple[Any, dict[str, Any], Any, Any], idx: int, preview_chars: int
) -> list[str]:
req, output, start_time, end_time = record
preview = pick_text_preview(req).replace("\n", " ").strip()
if len(preview) > preview_chars:
preview = preview[: preview_chars - 3] + "..."
output_dict = output if isinstance(output, dict) else {}
meta_info = get_field(output_dict, "meta_info", {}) or {}
rid = get_field(req, "rid") or get_field(meta_info, "id")
stream = bool(get_field(req, "stream", False))
prompt_tokens = get_field(meta_info, "prompt_tokens")
completion_tokens = get_field(meta_info, "completion_tokens")
duration = (
end_time - start_time
if isinstance(start_time, (int, float)) and isinstance(end_time, (int, float))
else None
)
elapsed_str = f"{duration:.3f}" if duration is not None else "n/a"
lines = [
f"[{idx}] rid={rid or 'n/a'} stream={stream} "
f"prompt_tokens={prompt_tokens if prompt_tokens is not None else 'n/a'} "
f"completion_tokens={completion_tokens if completion_tokens is not None else 'n/a'} "
f"start={format_timestamp(start_time)} elapsed_s={elapsed_str}"
]
if preview:
lines.append(f" text={preview}")
return lines
def summarize_dump_file(path: Path, max_requests: int, preview_chars: int) -> str:
payload = load_dump_payload(path)
requests = payload.get("requests") or []
server_args = payload.get("server_args")
launch_command = payload.get("launch_command")
model_path = get_field(server_args, "model_path")
tp_size = get_field(server_args, "tp_size")
dp_size = get_field(server_args, "dp_size")
pp_size = get_field(server_args, "pp_size")
host = get_field(server_args, "host")
port = get_field(server_args, "port")
timestamps = [
record[2]
for record in requests
if isinstance(record, tuple)
and len(record) >= 4
and isinstance(record[2], (int, float))
]
time_span = (
max(timestamps) - min(timestamps)
if len(timestamps) >= 2
else 0.0 if len(timestamps) == 1 else None
)
lines = [
f"File: {path}",
"Dump Type: request_or_crash_dump",
f"Requests: {len(requests)}",
f"Model: {model_path or 'n/a'}",
f"Topology: tp={tp_size if tp_size is not None else 'n/a'} "
f"dp={dp_size if dp_size is not None else 'n/a'} "
f"pp={pp_size if pp_size is not None else 'n/a'}",
f"Endpoint: {host or 'n/a'}:{port if port is not None else 'n/a'}",
(
f"Time span seconds: {time_span:.3f}"
if time_span is not None
else "Time span seconds: n/a"
),
]
if launch_command:
lines.append(f"Launch command: {launch_command}")
for idx, record in enumerate(requests[:max_requests]):
if not isinstance(record, tuple) or len(record) < 4:
lines.append(f"[{idx}] Unsupported record shape: {type(record)!r}")
continue
lines.extend(summarize_request(record, idx, preview_chars))
if len(requests) > max_requests:
lines.append(f"... truncated {len(requests) - max_requests} more requests")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(
description="Collect or inspect serving bundles and dumps for SGLang debug."
)
subparsers = parser.add_subparsers(dest="command", required=True)
collect_parser = subparsers.add_parser(
"collect-bundle", help="Collect a read-only live bundle from a running server"
)
collect_parser.add_argument("--base-url", required=True)
collect_parser.add_argument(
"--token",
default=os.environ.get("SGLANG_BEARER_TOKEN"),
help="Bearer token for protected endpoints. Defaults to $SGLANG_BEARER_TOKEN.",
)
collect_parser.add_argument("--outdir", default=None)
collect_parser.add_argument("--timeout", type=float, default=10.0)
bundle_parser = subparsers.add_parser(
"summarize-bundle", help="Summarize a bundle directory"
)
bundle_parser.add_argument("bundle_dir")
bundle_parser.add_argument("--out", default=None)
bundle_parser.add_argument("--json-out", default=None)
bundle_parser.add_argument("--stdout-json", action="store_true")
dump_parser = subparsers.add_parser(
"summarize-dump", help="Summarize a trusted request dump or crash dump"
)
dump_parser.add_argument("--input-file", default=None)
dump_parser.add_argument("--input-folder", default=None)
dump_parser.add_argument("--max-requests", type=int, default=20)
dump_parser.add_argument("--preview-chars", type=int, default=160)
args = parser.parse_args()
if args.command == "collect-bundle":
bundle_dir = collect_bundle(
args.base_url, args.token, args.outdir, args.timeout
)
print(bundle_dir)
return 0
if args.command == "summarize-bundle":
bundle_dir = Path(args.bundle_dir).resolve()
if not bundle_dir.is_dir():
raise SystemExit(
f"bundle_dir does not exist or is not a directory: {bundle_dir}"
)
summary = build_bundle_summary(bundle_dir)
out_text = render_bundle_text(summary)
text_path = Path(args.out) if args.out else bundle_dir / "SUMMARY_REPORT.txt"
json_path = (
Path(args.json_out) if args.json_out else bundle_dir / "SUMMARY_REPORT.json"
)
text_path.write_text(out_text, encoding="utf-8")
json_path.write_text(
json.dumps(summary, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
if args.stdout_json:
print(json.dumps(summary, indent=2, ensure_ascii=False))
else:
print(out_text, end="")
return 0
files = iter_dump_files(args.input_file, args.input_folder)
if not files:
raise SystemExit("No .pkl files matched the provided input.")
for idx, path in enumerate(files):
if idx:
print()
print(
summarize_dump_file(
path=path,
max_requests=args.max_requests,
preview_chars=args.preview_chars,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""Replay a trusted SGLang request dump directly over HTTP.
Use this only for locally captured or otherwise trusted dump files.
It uses plain pickle loading to bypass SafeUnpickler restrictions that may block
the stock replay helper on newer SGLang builds.
"""
from __future__ import annotations
import argparse
import glob
import json
import pickle
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import asdict, is_dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Sequence
import requests
Record = tuple[object, dict[str, Any], float, float]
def normalize_mm_data_item(item: Any) -> Any:
if isinstance(item, dict) and "url" in item:
return item["url"]
return item
def normalize_mm_data(data: Any) -> Any:
if data is None:
return None
if isinstance(data, list):
return [
(
[normalize_mm_data_item(item) for item in sublist]
if isinstance(sublist, list)
else normalize_mm_data_item(sublist)
)
for sublist in data
]
return normalize_mm_data_item(data)
def normalize_request_data(json_data: dict[str, Any]) -> dict[str, Any]:
for field in ["image_data", "video_data", "audio_data"]:
if field in json_data and json_data[field] is not None:
json_data[field] = normalize_mm_data(json_data[field])
return json_data
def to_plain_dict(obj: Any) -> dict[str, Any]:
if obj is None:
return {}
if isinstance(obj, dict):
return dict(obj)
if is_dataclass(obj):
return asdict(obj)
model_dump = getattr(obj, "model_dump", None)
if callable(model_dump):
dumped = model_dump()
if isinstance(dumped, dict):
return dumped
dict_method = getattr(obj, "dict", None)
if callable(dict_method):
dumped = dict_method()
if isinstance(dumped, dict):
return dumped
obj_dict = getattr(obj, "__dict__", None)
if isinstance(obj_dict, dict):
return {
key: value for key, value in obj_dict.items() if not key.startswith("_")
}
raise TypeError(f"Unsupported request object type: {type(obj)!r}")
def request_to_json_data(req: Any) -> dict[str, Any]:
json_data = normalize_request_data(to_plain_dict(req))
sampling_params = json_data.get("sampling_params")
if sampling_params is not None and not isinstance(sampling_params, dict):
json_data["sampling_params"] = to_plain_dict(sampling_params)
return json_data
def load_records(path: Path) -> list[Record]:
with path.open("rb") as fh:
payload = pickle.load(fh)
if isinstance(payload, dict) and "requests" in payload:
return payload["requests"]
return payload
def iter_files(args: argparse.Namespace) -> Sequence[Path]:
if args.input_file:
return [Path(args.input_file)]
if args.input_folder:
return [
Path(p)
for p in sorted(glob.glob(f"{args.input_folder}/*.pkl"))[: args.file_number]
]
raise SystemExit("Either --input-file or --input-folder must be provided.")
def run_one_request(
record: Record,
args: argparse.Namespace,
replay_init_time: float,
base_time: float,
idx: int,
) -> None:
req, output, start_time, end_time = record
relative_start = start_time - base_time
delay = max(0.0, (relative_start - (time.time() - replay_init_time)) / args.speed)
if delay:
time.sleep(delay)
json_data = request_to_json_data(req)
if args.ignore_eos:
json_data.setdefault("sampling_params", {})["ignore_eos"] = True
completion_tokens = output.get("meta_info", {}).get("completion_tokens")
if completion_tokens:
json_data["sampling_params"]["max_new_tokens"] = completion_tokens
t0 = time.time()
response = requests.post(
f"http://{args.host}:{args.port}/generate",
json=json_data,
timeout=args.timeout,
stream=bool(json_data.get("stream")),
)
elapsed = time.time() - t0
if json_data.get("stream"):
last = None
for chunk in response.iter_lines(decode_unicode=False):
decoded = chunk.decode("utf-8")
if decoded and decoded.startswith("data:"):
if decoded == "data: [DONE]":
break
last = json.loads(decoded[5:].strip())
result = last or {}
else:
result = response.json()
meta = result.get("meta_info", {})
print(
json.dumps(
{
"idx": idx,
"status_code": response.status_code,
"elapsed_seconds": round(elapsed, 3),
"prompt_tokens": meta.get("prompt_tokens"),
"completion_tokens": meta.get("completion_tokens"),
"rid": meta.get("id"),
},
ensure_ascii=False,
)
)
def main() -> int:
parser = argparse.ArgumentParser(
description="Replay a trusted SGLang request dump or crash dump directly over HTTP."
)
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=30000)
parser.add_argument("--input-folder", default=None)
parser.add_argument("--input-file", default=None)
parser.add_argument("--file-number", type=int, default=1)
parser.add_argument("--req-number", type=int, default=1_000_000)
parser.add_argument("--req-start", type=int, default=0)
parser.add_argument("--parallel", type=int, default=1)
parser.add_argument("--ignore-eos", action="store_true")
parser.add_argument("--speed", type=float, default=1.0)
parser.add_argument("--timeout", type=float, default=120.0)
args = parser.parse_args()
files = iter_files(args)
print(f"Replay files: {[str(p) for p in files]}")
records: list[Record] = []
for path in files:
records.extend(load_records(path))
if not records:
print("No requests found.")
return 0
records.sort(key=lambda x: x[-2])
records = records[args.req_start : args.req_start + args.req_number]
print(f"Replay requests: {len(records)}")
base_time = records[0][-2]
print(
"Base time: " + datetime.fromtimestamp(base_time).strftime("%Y-%m-%d %H:%M:%S")
)
replay_init_time = time.time()
with ThreadPoolExecutor(max_workers=args.parallel) as executor:
futures = []
for idx, record in enumerate(records):
futures.append(
executor.submit(
run_one_request, record, args, replay_init_time, base_time, idx
)
)
for future in futures:
future.result()
return 0
if __name__ == "__main__":
raise SystemExit(main())