Repository navigation
feat: add router-level Prometheus metrics and centralize request tracking - #6146
Conversation
Signed-off-by: PeaBrane <yanrpei@gmail.com>
WalkthroughThis pull request refactors metrics instrumentation across the KV router subsystem by centralizing scattered Prometheus gauge definitions and event publishing into a new dedicated metrics module, while reworking timing tracker internals to support multiple phase updates via mutex-backed storage instead of single-shot locking. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/llm/src/protocols/common/timing.rs (1)
60-72:⚠️ Potential issue | 🟡 MinorStale doc comments reference
OnceLockfor fields now backed byMutex.Lines 65–66 mention "set once via OnceLock" for
first_token_time, and lines 70–72 state "TheOnceLockfields ensure that values are set exactly once." Bothfirst_token_timeandrequest_finish_timeare nowMutex<Option<Instant>>and can be overwritten. The doc should reflect the new semantics.Proposed doc fix
-/// - `first_token_time`: When the first token was generated (set once via OnceLock) -/// - `request_finish_time`: When the request finished (set once via OnceLock) +/// - `first_token_time`: When the first token was generated (Mutex-protected, may be updated across phases) +/// - `request_finish_time`: When the request finished (Mutex-protected, may be updated across phases) /// - KV cache hit rate information /// - Worker IDs and types for per-worker Prometheus metrics /// -/// The `OnceLock` fields ensure that values are set exactly once, -/// which is important for disaggregated serving where the "first token" -/// might appear multiple times. +/// The `Mutex`-protected time fields allow updates from multiple router phases +/// (prefill/decode) in disaggregated serving, where timing events may be +/// recorded by different pipeline stages.
🤖 Fix all issues with AI agents
In `@lib/llm/src/kv_router/metrics.rs`:
- Around line 258-287: The fallback constructor new_unregistered uses plain
Histogram::with_opts(...) and thus ends up with default Prometheus buckets;
update new_unregistered to mirror the same custom bucket configuration used in
from_component for the router histograms (e.g.,
router_time_to_first_token_seconds, router_inter_token_latency_seconds,
router_input_sequence_tokens, router_output_sequence_tokens) by applying the
identical HistogramOpts::buckets(...) call(s) so tests/fallbacks use the same
bucket boundaries as from_component.
- Around line 200-256: The router histograms currently pass buckets: None to
metrics.create_histogram which causes Prometheus to use inappropriate default
time buckets; update from_component (and the histograms created via
metrics.create_histogram for "router_time_to_first_token_seconds",
"router_inter_token_latency_seconds", "router_input_sequence_tokens", and
"router_output_sequence_tokens" inside the ROUTER_REQUEST_METRICS
initialization) to supply explicit bucket arrays matching the frontend Metrics
(log-spaced token buckets for input/output and appropriate time buckets for TTFT
and ITL). Replace the None bucket arguments with arrays mirroring the frontend
ranges (e.g., input sequence tokens ~50–128000 log-spaced, output sequence
tokens ~50–32000 log-spaced, TTFT ~0.001–480 seconds log-spaced, ITL ~0.001–2
seconds log-spaced) so the histograms preserve distribution visibility and then
re-run tests to ensure no metric creation errors.
🧹 Nitpick comments (3)
lib/llm/src/protocols/common/timing.rs (1)
256-265:avg_itl_mslooks correct; note the implicit panic if times are inverted.
finish.duration_since(first_token)will panic iffinish < first_token. Under normal operation this is impossible, but with the new overwrite semantics, a mis-ordered call torecord_first_token/record_finishfrom different phases could trigger it. If you'd prefer a defensive approach,checked_duration_sincewould returnNoneinstead of panicking. Given the fail-fast philosophy this is fine as-is, just flagging for awareness.lib/llm/src/kv_router/metrics.rs (2)
132-151: Duration subtraction panics if cumulative timestamps arrive out of order.The delta calculations (e.g.,
find_matches_elapsed - hash_elapsed) useDurationsubtraction, which panics on underflow. This relies on callers passing strictly monotonic cumulative durations. If that invariant is guaranteed by the routing pipeline, this is fine — just calling it out since theobserve()method accepts arbitraryDurationarguments from the outside.Defensive alternative using saturating subtraction
self.indexer_find_matches - .observe((find_matches_elapsed - hash_elapsed).as_secs_f64() * 1000.0); + .observe(find_matches_elapsed.saturating_sub(hash_elapsed).as_secs_f64() * 1000.0); self.seq_hashing - .observe((seq_hash_elapsed - find_matches_elapsed).as_secs_f64() * 1000.0); + .observe(seq_hash_elapsed.saturating_sub(find_matches_elapsed).as_secs_f64() * 1000.0); self.scheduling - .observe((total_elapsed - seq_hash_elapsed).as_secs_f64() * 1000.0); + .observe(total_elapsed.saturating_sub(seq_hash_elapsed).as_secs_f64() * 1000.0);
50-73: Hardcodeddynamo_frontend_prefix bypasses the configurableDYN_METRICS_PREFIX.The frontend
Metrics(inhttp/service/metrics.rs) readsDYN_METRICS_PREFIXto allow users to customize the metric namespace. The worker load gauges here hardcodedynamo_frontend_, so they won't follow any user-configured prefix. This appears to be pre-existing behavior carried over from the old globals, but worth noting if prefix consistency is a goal.
keivenchang
left a comment
There was a problem hiding this comment.
Hey Rudy, good direction -- always-on routing overhead histograms and aggregate TTFT/ITL/ISL/OSL at the router level. Consolidating the record_worker methods is a nice cleanup too.
-
I see mixed metric patterns: RoutingOverheadMetrics uses raw LazyLock
(dynamo_routing_overhead_), RouterRequestMetrics uses from_component()
(dynamo_component_router_), can you address this in the comments. -
Duration subtraction in RoutingOverheadMetrics::observe() can panic.
-
Can you add tests for the added Prometheus metrics? You can re-use prometheus_names.rs in the test names.
Add #[cfg(test)] module in your lib/llm/src/kv_router/metrics.rs and lib/llm/src/protocols/common/timing.rs. At minimum, test that the Prometheus Exposition Format (PEF text output) is generated -- compare against a golden output. An example of PEF is at lib/runtime/src/metrics.rs (search for expected_namespace_output), which has a PEF like this:
let expected_namespace_output = r#"# HELP dynamo_component_testcounter A test counter
# TYPE dynamo_component_testcounter counter
dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789
# HELP dynamo_component_testgauge A test gauge
# TYPE dynamo_component_testgauge gauge
dynamo_component_testgauge{dynamo_component="comp345",dynamo_namespace="ns345"} 50000
# HELP dynamo_component_testintcounter A test int counter
# TYPE dynamo_component_testintcounter counter
dynamo_component_testintcounter{dynamo_namespace="ns345"} 12345"#.to_string();
Signed-off-by: PeaBrane <yanrpei@gmail.com>
Signed-off-by: PeaBrane <yanrpei@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: PeaBrane <yanrpei@gmail.com>
…king (ai-dynamo#6146) Signed-off-by: PeaBrane <yanrpei@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…king (ai-dynamo#6146) Signed-off-by: PeaBrane <yanrpei@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Add router-level Prometheus metrics and centralize request tracking
Summary
RequestTracker: ISL tokens, cached tokens, OSL, and average ITLChanges
Aggregate metrics (
RouterRequestMetrics)router_requests_total(counter),router_time_to_first_token_seconds,router_inter_token_latency_seconds,router_input_sequence_tokens,router_output_sequence_tokens(histograms)from_component()pattern,router_prefix distinguishes fromResponseMetricCollectormetricspush_router.rs: ISL after routing, TTFT on first token, OSL/avg ITL/num_req at stream endRequestTracker fields
isl_tokensandcached_tokens: recorded at routing timeosl_tokens: recorded at stream endfirst_token_timeandrequest_finish_time: now recorded inpush_router.rs(Mutex-based to support both prefill and decode phases in disaggregated serving)avg_itl_ms(): computed from finish time, first token time, and OSLPer-worker last gauges for Python path
observe_first_token_gauges()onRequestTracker: writes TTFT and ISL toWORKER_LAST_TIME_TO_FIRST_TOKEN_GAUGEandWORKER_LAST_INPUT_SEQUENCE_TOKENS_GAUGEobserve_finish_gauges()onRequestTracker: writes avg ITL toWORKER_LAST_INTER_TOKEN_LATENCY_GAUGEkv.rsstream processing (Python path only)DIS-1412
Summary by CodeRabbit
Release Notes
New Features
Refactor