Problem
Currently, SGLang lacks per-request observability in two key areas:
- TTFT cannot be decomposed — there's no way to know how much of TTFT is queue wait, schedule delay, or GPU forward time. Operators can only see total TTFT, making prefill performance tuning guesswork. This problem exists regardless of whether HiCache is enabled.
- HiCache tier performance is invisible — with three-tier KV cache (GPU→CPU→NVMe) enabled, L2 DMA time, L3 prefetch I/O time, and per-tier hit/miss counts are not available per-request. No Prometheus metrics exist for cache hit rates, prefetch latency distributions, or eviction pressure.
Proposed Solution
A lightweight per-request trace + Prometheus metrics system covering both TTFT decomposition and HiCache observability, implemented across 5 files.
Part A: Per-request Trace (JSON log, debug level)
One JSON line per request at logger.debug level. The TTFT fields work universally; HiCache fields are gated by --enable-hierarchical-cache:
{
"rid": "b3f3e0e1...",
"prompt_tokens": 136921,
"input_tokens_cache_hit": 119040,
"input_tokens_no_cache_hit": 17881,
"output_tokens": 4096,
"L1_hit": 0,
"L1_hit_pct": 0.0,
"L2_hit": 119040,
"L2_hit_pct": 86.9,
"L3_hit": 0,
"L3_hit_pct": 0.0,
"L3_pf_alloc_ms": 0,
"L3_pf_io_ms": 0,
"L3_pf_tree_ms": 0,
"queue_ms": 9.9,
"schedule_ms": 2.4,
"forward_ms": 1174.6,
"TTFT_ms": 78907.9
}
TTFT Decomposition (universal — works with or without HiCache)
TTFT ≈ queue_ms + schedule_ms + forward_ms
| Field |
Source |
Meaning |
queue_ms |
time_stats.scheduler_recv_time → wait_queue_entry_time |
Tokenizer → enqueue |
schedule_ms |
time_stats.wait_queue_entry_time → forward_entry_time |
Wait in queue + chunk waits |
forward_ms |
time_stats.forward_entry_time → prefill_finished_time |
GPU prefill compute |
HiCache Fields (gated by --enable-hierarchical-cache)
Per-tier hit token counts with percentages, L3 prefetch phase breakdown.
Why no L2 DMA time? cache_controller.load() is asynchronous — it allocates GPU memory and queues the DMA operation, then returns immediately. The actual DMA runs on a CUDA stream in a background thread (start_loading()). CPU-side time.perf_counter() cannot capture the DMA completion time. Since DMA bandwidth is fixed (PCIe 5.0 × 16 ≈ 64 GB/s), the transfer time is a known constant: L2_hit_tokens × 48 KB/token ÷ 64 GB/s. L2 token count alone is sufficient to characterize L2 performance.
L3 prefetch phase breakdown (key finding made possible by this instrumentation):
| Phase |
P50 |
Meaning |
L3_pf_alloc_ms |
0.1ms |
Host memory allocation |
L3_pf_io_ms |
91ms |
Pure NVMe→CPU I/O |
L3_pf_tree_ms |
0.4ms |
Radix tree insertion |
Without this breakdown, the total L3 prefetch time is often conflated with scheduler queue delays (~2.4s vs actual I/O ~91ms).
Part B: Prometheus Metrics
6 Histograms + 2 Counters exposed at /metrics:
| Metric |
Type |
Description |
sglang:hicache_l1_hit_tokens |
Histogram |
L1 hit tokens per request |
sglang:hicache_l2_hit_tokens |
Histogram |
L2 hit tokens per request |
sglang:hicache_l3_hit_tokens |
Histogram |
L3 hit tokens per request |
sglang:hicache_l3_prefetch_seconds |
Histogram |
L3→L2 prefetch total duration |
sglang:hicache_l3_pf_alloc_seconds |
Histogram |
L3 prefetch: alloc phase |
sglang:hicache_l3_pf_io_wait_seconds |
Histogram |
L3 prefetch: I/O phase |
sglang:hicache_l3_pf_tree_insert_seconds |
Histogram |
L3 prefetch: tree insert phase |
sglang:hicache_cached_input_tokens_total |
Counter |
Cumulative cached tokens |
sglang:hicache_uncached_input_tokens_total |
Counter |
Cumulative uncached tokens |
Part C: Eviction Metrics
RadixCacheMetricsCollector.increment_eviction_num_tokens() called from cache eviction code paths (previously unused in HiMambaRadixCache):
- L1→L2 eviction counter (tokens evicted from GPU to CPU)
- L2→L3 eviction counter (tokens evicted from CPU to NVMe)
Write-back timing is excluded for the same reason as L2 DMA — cache_controller.write() is also asynchronous.
Implementation Scope
5 files, ~100 lines of instrumentation total:
| File |
Change |
scheduler.py |
_hicache_trace_one_request() method: trace JSON + metrics observe |
metrics_collector.py |
7 HiCache Histograms + 2 Counters |
hi_mamba_radix_cache.py |
L2/L3 timing in load_back/write_backup/evict/evict_host |
hiradix_cache.py |
Same for MHA models |
cache_controller.py |
_page_transfer I/O timing via try/finally |
Compatibility
- TTFT decomposition fields work universally (no flag required). HiCache fields gated by
--enable-hierarchical-cache
- Trace output at
logger.debug level only (no overhead at default info level), tp_rank == 0 only
- HiCache metrics guarded by
enable_metrics and --enable-hierarchical-cache
- No breaking changes to existing APIs
Related
Problem
Currently, SGLang lacks per-request observability in two key areas:
Proposed Solution
A lightweight per-request trace + Prometheus metrics system covering both TTFT decomposition and HiCache observability, implemented across 5 files.
Part A: Per-request Trace (JSON log, debug level)
One JSON line per request at
logger.debuglevel. The TTFT fields work universally; HiCache fields are gated by--enable-hierarchical-cache:{ "rid": "b3f3e0e1...", "prompt_tokens": 136921, "input_tokens_cache_hit": 119040, "input_tokens_no_cache_hit": 17881, "output_tokens": 4096, "L1_hit": 0, "L1_hit_pct": 0.0, "L2_hit": 119040, "L2_hit_pct": 86.9, "L3_hit": 0, "L3_hit_pct": 0.0, "L3_pf_alloc_ms": 0, "L3_pf_io_ms": 0, "L3_pf_tree_ms": 0, "queue_ms": 9.9, "schedule_ms": 2.4, "forward_ms": 1174.6, "TTFT_ms": 78907.9 }TTFT Decomposition (universal — works with or without HiCache)
TTFT ≈ queue_ms + schedule_ms + forward_msqueue_mstime_stats.scheduler_recv_time → wait_queue_entry_timeschedule_mstime_stats.wait_queue_entry_time → forward_entry_timeforward_mstime_stats.forward_entry_time → prefill_finished_timeHiCache Fields (gated by
--enable-hierarchical-cache)Per-tier hit token counts with percentages, L3 prefetch phase breakdown.
L3 prefetch phase breakdown (key finding made possible by this instrumentation):
L3_pf_alloc_msL3_pf_io_msL3_pf_tree_msWithout this breakdown, the total L3 prefetch time is often conflated with scheduler queue delays (~2.4s vs actual I/O ~91ms).
Part B: Prometheus Metrics
6 Histograms + 2 Counters exposed at
/metrics:sglang:hicache_l1_hit_tokenssglang:hicache_l2_hit_tokenssglang:hicache_l3_hit_tokenssglang:hicache_l3_prefetch_secondssglang:hicache_l3_pf_alloc_secondssglang:hicache_l3_pf_io_wait_secondssglang:hicache_l3_pf_tree_insert_secondssglang:hicache_cached_input_tokens_totalsglang:hicache_uncached_input_tokens_totalPart C: Eviction Metrics
RadixCacheMetricsCollector.increment_eviction_num_tokens()called from cache eviction code paths (previously unused inHiMambaRadixCache):Implementation Scope
5 files, ~100 lines of instrumentation total:
scheduler.py_hicache_trace_one_request()method: trace JSON + metrics observemetrics_collector.pyhi_mamba_radix_cache.pyload_back/write_backup/evict/evict_hosthiradix_cache.pycache_controller.py_page_transferI/O timing viatry/finallyCompatibility
--enable-hierarchical-cachelogger.debuglevel only (no overhead at defaultinfolevel),tp_rank == 0onlyenable_metricsand--enable-hierarchical-cacheRelated