1use ddsketchy::DDSketch;
5use rustc_hash::FxHashMap;
6use serde::Serialize;
7use serde::ser::{SerializeMap, Serializer};
8use serde_json::Value;
9use std::fmt::{Display, Formatter, Result as FmtResult};
10use uuid::Uuid;
11
12use crate::engine::CacheTierAttribution;
13use crate::replay::PlacementCacheSample;
14use crate::replay::loadgen::{AgenticGraphIdentity, AgenticTrajectorySnapshot};
15
16const DDSKETCH_RELATIVE_ACCURACY: f64 = 0.001;
20const DDSKETCH_MAX_BINS: usize = 32_768;
21
22#[derive(Debug, Clone)]
24pub struct ReplayReport {
25 pub request_counts: TraceRequestCounts,
26 pub throughput: TraceThroughputStats,
27 pub prefix_cache_reused_ratio: f64,
28 pub first_admission_prefix_cache_reused_ratio: f64,
29 pub latency: TraceLatencyStats,
30 pub trajectories: Option<TraceTrajectoryStats>,
31 pub agentic_graph: Option<AgenticGraphIdentity>,
32 pub goodput: Option<TraceGoodputStats>,
36 pub per_request: Vec<PerRequestRecord>,
42 pub runtime_evidence: crate::replay::OfflineRuntimeEvidence,
46}
47
48#[derive(Debug, Clone)]
49pub struct TraceRequestCounts {
50 pub num_requests: usize,
51 pub completed_requests: usize,
52 pub total_input_tokens: usize,
53 pub total_output_tokens: usize,
54}
55
56#[derive(Debug, Clone)]
57pub struct TraceThroughputStats {
58 pub duration_ms: f64,
59 pub wall_time_ms: f64,
60 pub request_throughput_rps: f64,
61 pub input_throughput_tok_s: f64,
62 pub output_throughput_tok_s: f64,
63 pub total_throughput_tok_s: f64,
64 pub prefill_worker_seconds: f64,
77 pub decode_worker_seconds: f64,
78 pub prefill_gpus_per_worker: usize,
82 pub decode_gpus_per_worker: usize,
83 pub gpu_hours: f64,
88}
89
90#[derive(Debug, Clone)]
95pub struct TraceGoodputStats {
96 pub completed_requests: usize,
98 pub request_throughput_rps: f64,
100 pub output_throughput_tok_s: f64,
102}
103
104#[derive(Debug, Clone)]
105pub struct TraceDistributionStats {
106 pub mean_ms: f64,
107 pub min_ms: f64,
108 pub max_ms: f64,
109 pub median_ms: f64,
110 pub p75_ms: f64,
111 pub p90_ms: f64,
112 pub p95_ms: f64,
113 pub p99_ms: f64,
114 pub std_ms: f64,
115}
116
117#[derive(Debug, Clone)]
118pub struct TraceLatencyStats {
119 pub num_ttft_samples: usize,
120 pub num_tpot_samples: usize,
121 pub num_e2e_latency_samples: usize,
122 pub ttft: TraceDistributionStats,
123 pub ttst: TraceDistributionStats,
124 pub tpot: TraceDistributionStats,
125 pub itl: TraceInterTokenLatencyStats,
126 pub e2e: TraceDistributionStats,
127 pub output_token_throughput_per_user: TraceDistributionStats,
128}
129
130#[derive(Debug, Clone)]
131pub struct TraceTrajectoryStats {
132 pub total: usize,
133 pub completed: usize,
134 pub incomplete: usize,
135 pub e2e: TraceDistributionStats,
136}
137
138#[derive(Debug, Clone)]
139pub struct TraceInterTokenLatencyStats {
140 pub distribution: TraceDistributionStats,
141 pub max_ms: f64,
142}
143
144impl ReplayReport {
145 pub fn with_wall_time_ms(mut self, wall_time_ms: f64) -> Self {
146 self.throughput.wall_time_ms = wall_time_ms;
147 self
148 }
149
150 pub fn processed_tokens(&self) -> usize {
151 self.request_counts.total_input_tokens + self.request_counts.total_output_tokens
152 }
153
154 pub fn processed_tokens_per_s(&self) -> f64 {
155 if self.throughput.wall_time_ms <= 0.0 {
156 return 0.0;
157 }
158 self.processed_tokens() as f64 / self.throughput.wall_time_ms * 1000.0
159 }
160
161 pub fn processed_output_tokens_per_s(&self) -> f64 {
162 if self.throughput.wall_time_ms <= 0.0 {
163 return 0.0;
164 }
165 self.request_counts.total_output_tokens as f64 / self.throughput.wall_time_ms * 1000.0
166 }
167}
168
169impl Display for ReplayReport {
170 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
171 writeln!(
172 f,
173 " completed_requests: {}",
174 self.request_counts.completed_requests
175 )?;
176 writeln!(
177 f,
178 " request_throughput_rps: {:.6}",
179 self.throughput.request_throughput_rps
180 )?;
181 writeln!(
182 f,
183 " output_throughput_tok_s: {:.6}",
184 self.throughput.output_throughput_tok_s
185 )?;
186 writeln!(
187 f,
188 " total_input_tokens: {}",
189 self.request_counts.total_input_tokens
190 )?;
191 writeln!(
192 f,
193 " total_output_tokens: {}",
194 self.request_counts.total_output_tokens
195 )?;
196 writeln!(
197 f,
198 " processed_tokens_per_s: {:.6}",
199 self.processed_tokens_per_s()
200 )?;
201 writeln!(
202 f,
203 " processed_output_tokens_per_s: {:.6}",
204 self.processed_output_tokens_per_s()
205 )?;
206 writeln!(f, " mean_ttft_ms: {:.6}", self.latency.ttft.mean_ms)?;
207 writeln!(f, " mean_e2e_latency_ms: {:.6}", self.latency.e2e.mean_ms)?;
208 writeln!(
209 f,
210 " prefix_cache_reused_ratio: {:.6}",
211 self.prefix_cache_reused_ratio
212 )?;
213 writeln!(
214 f,
215 " first_admission_prefix_cache_reused_ratio: {:.6}",
216 self.first_admission_prefix_cache_reused_ratio
217 )?;
218 write!(f, " wall_time_ms: {:.6}", self.throughput.wall_time_ms)
219 }
220}
221
222impl Serialize for ReplayReport {
223 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
224 where
225 S: Serializer,
226 {
227 let mut map = serializer.serialize_map(Some(70))?;
228 map.serialize_entry("num_requests", &self.request_counts.num_requests)?;
229 map.serialize_entry(
230 "completed_requests",
231 &self.request_counts.completed_requests,
232 )?;
233 map.serialize_entry(
234 "total_input_tokens",
235 &self.request_counts.total_input_tokens,
236 )?;
237 map.serialize_entry(
238 "total_output_tokens",
239 &self.request_counts.total_output_tokens,
240 )?;
241 map.serialize_entry("duration_ms", &self.throughput.duration_ms)?;
242 map.serialize_entry("wall_time_ms", &self.throughput.wall_time_ms)?;
243 map.serialize_entry(
244 "request_throughput_rps",
245 &self.throughput.request_throughput_rps,
246 )?;
247 map.serialize_entry(
248 "input_throughput_tok_s",
249 &self.throughput.input_throughput_tok_s,
250 )?;
251 map.serialize_entry(
252 "output_throughput_tok_s",
253 &self.throughput.output_throughput_tok_s,
254 )?;
255 map.serialize_entry(
256 "total_throughput_tok_s",
257 &self.throughput.total_throughput_tok_s,
258 )?;
259 map.serialize_entry(
260 "prefill_worker_seconds",
261 &self.throughput.prefill_worker_seconds,
262 )?;
263 map.serialize_entry(
264 "decode_worker_seconds",
265 &self.throughput.decode_worker_seconds,
266 )?;
267 map.serialize_entry(
268 "prefill_gpus_per_worker",
269 &self.throughput.prefill_gpus_per_worker,
270 )?;
271 map.serialize_entry(
272 "decode_gpus_per_worker",
273 &self.throughput.decode_gpus_per_worker,
274 )?;
275 map.serialize_entry("gpu_hours", &self.throughput.gpu_hours)?;
276 if let Some(goodput) = &self.goodput {
277 map.serialize_entry("goodput_completed_requests", &goodput.completed_requests)?;
278 map.serialize_entry(
279 "goodput_request_throughput_rps",
280 &goodput.request_throughput_rps,
281 )?;
282 map.serialize_entry(
283 "goodput_output_throughput_tok_s",
284 &goodput.output_throughput_tok_s,
285 )?;
286 }
287 map.serialize_entry("processed_tokens", &self.processed_tokens())?;
288 map.serialize_entry("processed_tokens_per_s", &self.processed_tokens_per_s())?;
289 map.serialize_entry(
290 "processed_output_tokens_per_s",
291 &self.processed_output_tokens_per_s(),
292 )?;
293 map.serialize_entry("prefix_cache_reused_ratio", &self.prefix_cache_reused_ratio)?;
294 map.serialize_entry(
295 "first_admission_prefix_cache_reused_ratio",
296 &self.first_admission_prefix_cache_reused_ratio,
297 )?;
298 map.serialize_entry("num_ttft_samples", &self.latency.num_ttft_samples)?;
299 map.serialize_entry("num_tpot_samples", &self.latency.num_tpot_samples)?;
300 map.serialize_entry(
301 "num_e2e_latency_samples",
302 &self.latency.num_e2e_latency_samples,
303 )?;
304 serialize_distribution(&mut map, "ttft", &self.latency.ttft)?;
305 serialize_distribution(&mut map, "ttst", &self.latency.ttst)?;
306 serialize_distribution(&mut map, "tpot", &self.latency.tpot)?;
307 serialize_distribution(&mut map, "itl", &self.latency.itl.distribution)?;
308 map.serialize_entry("max_itl_ms", &self.latency.itl.max_ms)?;
309 if let Some(trajectories) = &self.trajectories {
310 map.serialize_entry("total_trajectories", &trajectories.total)?;
311 map.serialize_entry("completed_trajectories", &trajectories.completed)?;
312 map.serialize_entry("incomplete_trajectories", &trajectories.incomplete)?;
313 serialize_distribution(&mut map, "trajectory_e2e_latency", &trajectories.e2e)?;
314 map.serialize_entry("p50_trajectory_e2e_latency_ms", &trajectories.e2e.median_ms)?;
315 }
316 if let Some(agentic_graph) = &self.agentic_graph {
317 map.serialize_entry("agentic_graph", agentic_graph)?;
318 }
319 serialize_distribution(&mut map, "e2e_latency", &self.latency.e2e)?;
320 serialize_rate_distribution(
321 &mut map,
322 "output_token_throughput_per_user",
323 &self.latency.output_token_throughput_per_user,
324 )?;
325 map.end()
326 }
327}
328
329fn serialize_distribution<S>(
330 map: &mut S,
331 prefix: &str,
332 stats: &TraceDistributionStats,
333) -> Result<(), S::Error>
334where
335 S: SerializeMap,
336{
337 map.serialize_entry(&format!("mean_{prefix}_ms"), &stats.mean_ms)?;
338 map.serialize_entry(&format!("min_{prefix}_ms"), &stats.min_ms)?;
339 map.serialize_entry(&format!("max_{prefix}_ms"), &stats.max_ms)?;
340 map.serialize_entry(&format!("median_{prefix}_ms"), &stats.median_ms)?;
341 map.serialize_entry(&format!("p75_{prefix}_ms"), &stats.p75_ms)?;
342 map.serialize_entry(&format!("p90_{prefix}_ms"), &stats.p90_ms)?;
343 map.serialize_entry(&format!("p95_{prefix}_ms"), &stats.p95_ms)?;
344 map.serialize_entry(&format!("p99_{prefix}_ms"), &stats.p99_ms)?;
345 map.serialize_entry(&format!("std_{prefix}_ms"), &stats.std_ms)?;
346 Ok(())
347}
348
349fn serialize_rate_distribution<S>(
350 map: &mut S,
351 prefix: &str,
352 stats: &TraceDistributionStats,
353) -> Result<(), S::Error>
354where
355 S: SerializeMap,
356{
357 map.serialize_entry(&format!("mean_{prefix}"), &stats.mean_ms)?;
358 map.serialize_entry(&format!("min_{prefix}"), &stats.min_ms)?;
359 map.serialize_entry(&format!("max_{prefix}"), &stats.max_ms)?;
360 map.serialize_entry(&format!("median_{prefix}"), &stats.median_ms)?;
361 map.serialize_entry(&format!("p75_{prefix}"), &stats.p75_ms)?;
362 map.serialize_entry(&format!("p90_{prefix}"), &stats.p90_ms)?;
363 map.serialize_entry(&format!("p95_{prefix}"), &stats.p95_ms)?;
364 map.serialize_entry(&format!("p99_{prefix}"), &stats.p99_ms)?;
365 map.serialize_entry(&format!("std_{prefix}"), &stats.std_ms)?;
366 Ok(())
367}
368
369#[derive(Debug)]
370struct TraceRequestStats {
371 arrival_time_ms: f64,
372 first_admit_ms: Option<f64>,
373 terminal_time_ms: Option<f64>,
374 terminal_status: Option<ReplayTerminalStatus>,
375 token_timeline: TokenTimeline,
376 input_length: usize,
377 requested_output_length: usize,
378 reused_input_tokens: usize,
379 first_admission_reused_input_tokens: usize,
380 prefill_worker_idx: Option<usize>,
389 decode_worker_idx: Option<usize>,
391 session_id: Option<String>,
395 turn_index: Option<usize>,
396 authored_id: Option<String>,
397 play_id: Option<String>,
398 dispatched_at_ms: Option<f64>,
399 metadata: Value,
400 detail: Option<Box<PerRequestDetail>>,
401}
402
403#[derive(Debug)]
404enum TokenTimeline {
405 Recording(Vec<f64>),
406 Finalized(FinalizedTokenTimeline),
407}
408
409impl Default for TokenTimeline {
410 fn default() -> Self {
411 Self::Recording(Vec::new())
412 }
413}
414
415#[derive(Debug, Clone, Copy)]
416struct FinalizedTokenTimeline {
417 first_ms: f64,
418 second_ms: Option<f64>,
419 last_ms: f64,
420 len: usize,
421}
422
423#[derive(Debug)]
424struct StreamingDistribution {
425 sketch: DDSketch,
426 count: u64,
427 mean: f64,
428 sum_squared_deviations: f64,
429 min: f64,
430 max: f64,
431}
432
433impl Default for StreamingDistribution {
434 fn default() -> Self {
435 let sketch = match DDSketch::with_max_bins(DDSKETCH_RELATIVE_ACCURACY, DDSKETCH_MAX_BINS) {
436 Ok(sketch) => sketch,
437 Err(error) => panic!("invalid built-in DDSketch configuration: {error}"),
438 };
439 Self {
440 sketch,
441 count: 0,
442 mean: 0.0,
443 sum_squared_deviations: 0.0,
444 min: f64::INFINITY,
445 max: f64::NEG_INFINITY,
446 }
447 }
448}
449
450impl StreamingDistribution {
451 fn add(&mut self, value: f64) {
452 if !value.is_finite() {
453 return;
454 }
455
456 self.sketch.add(value);
457 self.count += 1;
458 let delta = value - self.mean;
459 self.mean += delta / self.count as f64;
460 let delta_after_mean_update = value - self.mean;
461 self.sum_squared_deviations += delta * delta_after_mean_update;
462 self.min = self.min.min(value);
463 self.max = self.max.max(value);
464 }
465
466 fn finish(&self) -> TraceDistributionStats {
467 if self.count == 0 {
468 return empty_distribution_stats();
469 }
470
471 TraceDistributionStats {
472 mean_ms: self.mean,
473 min_ms: self.min,
474 max_ms: self.max,
475 median_ms: self.percentile(50.0),
476 p75_ms: self.percentile(75.0),
477 p90_ms: self.percentile(90.0),
478 p95_ms: self.percentile(95.0),
479 p99_ms: self.percentile(99.0),
480 std_ms: (self.sum_squared_deviations / self.count as f64).sqrt(),
481 }
482 }
483
484 fn percentile(&self, percentile: f64) -> f64 {
487 let span = self.count.saturating_sub(1);
488 let rank = (span as f64 * percentile / 100.0).round() as u64;
489 let quantile = if span == 0 || rank >= span {
490 1.0
491 } else {
492 (rank as f64 + 0.5) / span as f64
493 };
494 match self.sketch.quantile(quantile) {
495 Ok(value) => value,
496 Err(error) => panic!("invalid built-in DDSketch quantile {quantile}: {error}"),
497 }
498 }
499}
500
501#[derive(Debug, Default)]
502struct PerRequestDetail {
503 first_admission_cache_tier_attribution: Option<CacheTierAttribution>,
504 prefill_reused_input_tokens: Option<usize>,
505 prefill_admit_ms: Option<f64>,
506 source_held_ms: Option<f64>,
507 destination_reserved_ms: Option<f64>,
508 destination_activated_ms: Option<f64>,
509 decode_admit_ms: Option<f64>,
510 source_released_ms: Option<f64>,
511 decode_reused_input_tokens: Option<usize>,
512 prefill_route_overlap_tokens: Option<usize>,
513 decode_route_overlap_tokens: Option<usize>,
514 routing_history: Vec<PerRequestRoutingRecord>,
515 admission_history: Vec<PerRequestAdmissionRecord>,
516 pool_admission_counts: [usize; 3],
517 pressure_record_ordinals: Vec<u64>,
518}
519
520#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
521#[serde(rename_all = "snake_case")]
522pub enum ReplayRequestPool {
523 Agg,
524 Prefill,
525 Decode,
526}
527
528impl ReplayRequestPool {
529 const fn index(self) -> usize {
530 match self {
531 Self::Agg => 0,
532 Self::Prefill => 1,
533 Self::Decode => 2,
534 }
535 }
536}
537
538#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
539#[serde(rename_all = "snake_case")]
540pub enum ReplayRoutingOutcome {
541 Immediate,
542 Queued,
543}
544
545#[derive(Debug, Clone, Serialize)]
546pub struct PerRequestRoutingRecord {
547 pub pool: ReplayRequestPool,
548 pub outcome: ReplayRoutingOutcome,
549 pub queue_entered_at_ms: Option<f64>,
550 pub released_at_ms: Option<f64>,
551 pub queue_wait_ms: Option<f64>,
552 pub logical_worker_id: Option<usize>,
553 pub scheduler_id: Option<usize>,
554 pub dp_rank: Option<u32>,
555 pub reported_overlap_tokens: Option<usize>,
556 pub selected_overlap_blocks: Option<u32>,
557 pub best_available_overlap_blocks: Option<u32>,
558 pub overlap_regret_blocks: Option<u32>,
559 pub placement_replica_id: Option<usize>,
560}
561
562#[derive(Debug, Clone, Serialize)]
563pub struct PerRequestAdmissionRecord {
564 pub admission_ordinal: usize,
565 pub pool_admission_ordinal: usize,
566 pub pool: ReplayRequestPool,
567 pub at_ms: f64,
568 pub reused_input_tokens: usize,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub g1_reused_input_tokens: Option<usize>,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub host_reused_input_tokens: Option<usize>,
573 pub is_readmission: bool,
574}
575
576#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
577#[serde(rename_all = "snake_case")]
578pub enum ReplayTerminalStatus {
579 Completed,
580 Rejected,
581 Canceled,
582 Failed,
583}
584
585#[derive(Debug, Clone, Serialize)]
590pub struct PerRequestRecord {
591 #[serde(skip_serializing_if = "Option::is_none")]
594 pub request_id: Option<String>,
595 #[serde(skip_serializing_if = "Option::is_none")]
596 pub play_id: Option<String>,
597 pub session_id: Option<String>,
603 pub turn_index: Option<usize>,
605 #[serde(skip_serializing_if = "Value::is_null")]
607 pub metadata: Value,
608 pub uuid: String,
609 pub arrival_time_ms: f64,
610 pub dispatched_at_ms: Option<f64>,
611 pub first_admit_ms: Option<f64>,
612 pub terminal_time_ms: f64,
613 pub first_token_ms: Option<f64>,
614 pub last_token_ms: Option<f64>,
615 pub ttft_ms: Option<f64>,
616 pub ttst_ms: Option<f64>,
617 pub e2e_latency_ms: Option<f64>,
618 pub itl_ms: Option<f64>,
621 pub input_length: usize,
622 pub requested_output_length: usize,
624 pub output_length: usize,
626 pub reused_input_tokens: usize,
627 #[serde(skip_serializing_if = "Option::is_none")]
628 pub first_admission_g1_reused_input_tokens: Option<usize>,
629 #[serde(skip_serializing_if = "Option::is_none")]
630 pub first_admission_host_reused_input_tokens: Option<usize>,
631 pub prefill_worker_idx: Option<usize>,
632 pub decode_worker_idx: Option<usize>,
633 pub prefill_admit_ms: Option<f64>,
634 pub source_held_ms: Option<f64>,
635 pub destination_reserved_ms: Option<f64>,
636 pub destination_activated_ms: Option<f64>,
637 pub decode_admit_ms: Option<f64>,
638 pub source_released_ms: Option<f64>,
639 pub decode_reused_input_tokens: Option<usize>,
640 pub prefill_route_overlap_tokens: Option<usize>,
641 pub decode_route_overlap_tokens: Option<usize>,
642 pub routing_history: Vec<PerRequestRoutingRecord>,
643 pub admission_history: Vec<PerRequestAdmissionRecord>,
644 pub admission_count: usize,
645 pub readmission_count: usize,
646 pub pressure_record_ordinals: Vec<u64>,
647 pub terminal_status: ReplayTerminalStatus,
648}
649
650#[cfg(test)]
651#[derive(Debug, Clone, PartialEq)]
652pub(crate) struct TraceRequestStatsSnapshot {
653 pub arrival_time_ms: f64,
654 pub first_admit_ms: Option<f64>,
655 pub first_token_ms: Option<f64>,
656 pub last_token_ms: Option<f64>,
657 pub input_length: usize,
658 pub requested_output_length: usize,
659 pub output_length: usize,
660 pub reused_input_tokens: usize,
661 pub first_admission_reused_input_tokens: usize,
662}
663
664#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, serde::Deserialize)]
668pub struct SlaThresholds {
669 pub ttft_ms: Option<f64>,
670 pub itl_ms: Option<f64>,
671 pub e2e_ms: Option<f64>,
672}
673
674impl SlaThresholds {
675 pub fn is_set(&self) -> bool {
676 self.ttft_ms.is_some() || self.itl_ms.is_some() || self.e2e_ms.is_some()
677 }
678
679 pub(crate) fn is_unset(&self) -> bool {
680 !self.is_set()
681 }
682
683 pub(crate) fn validate(&self) -> crate::replay::ReplayResult<()> {
684 let token_form = self.ttft_ms.is_some() || self.itl_ms.is_some();
685 if token_form && self.e2e_ms.is_some() {
686 return Err(crate::replay::ReplayError::InvalidSpec(
687 "sla.e2e_ms is mutually exclusive with sla.ttft_ms/itl_ms".to_string(),
688 ));
689 }
690 for (name, value) in [
691 ("sla.ttft_ms", self.ttft_ms),
692 ("sla.itl_ms", self.itl_ms),
693 ("sla.e2e_ms", self.e2e_ms),
694 ] {
695 if let Some(value) = value
696 && (!value.is_finite() || value <= 0.0)
697 {
698 return Err(crate::replay::ReplayError::InvalidSpec(format!(
699 "{name} must be finite and positive, got {value}"
700 )));
701 }
702 }
703 Ok(())
704 }
705
706 fn is_good(&self, ttft_ms: f64, e2e_ms: f64, output_length: usize) -> bool {
717 if let Some(bound) = self.e2e_ms
718 && e2e_ms > bound
719 {
720 return false;
721 }
722 if let Some(bound) = self.ttft_ms
723 && ttft_ms > bound
724 {
725 return false;
726 }
727 if let Some(bound) = self.itl_ms
728 && output_length > 1
729 {
730 let avg_itl_ms = (e2e_ms - ttft_ms) / (output_length as f64 - 1.0);
731 if avg_itl_ms > bound {
732 return false;
733 }
734 }
735 true
736 }
737
738 fn is_good_without_tokens(&self, e2e_ms: f64) -> bool {
739 self.ttft_ms.is_none()
740 && self.itl_ms.is_none()
741 && self.e2e_ms.is_some_and(|bound| e2e_ms <= bound)
742 }
743}
744
745#[doc(hidden)]
746#[derive(Debug, Default)]
747pub struct TraceCollector {
748 requests: FxHashMap<Uuid, TraceRequestStats>,
749 itl_distribution: StreamingDistribution,
752 output_token_throughput_per_user: StreamingDistribution,
753 defer_token_timeline_finalization: bool,
756 capture_per_request: bool,
760 sla: SlaThresholds,
763 prefill_worker_seconds: f64,
768 decode_worker_seconds: f64,
769 static_worker_count: Option<(usize, usize)>,
774 prefill_gpus_per_worker: usize,
777 decode_gpus_per_worker: usize,
778 runtime_evidence: crate::replay::OfflineRuntimeEvidence,
779 agentic_trajectory: Option<AgenticTrajectorySnapshot>,
780 agentic_graph: Option<AgenticGraphIdentity>,
781}
782
783impl TraceRequestStats {
784 fn first_token_ms(&self) -> Option<f64> {
785 match &self.token_timeline {
786 TokenTimeline::Recording(times) => times.first().copied(),
787 TokenTimeline::Finalized(summary) => Some(summary.first_ms),
788 }
789 }
790
791 fn last_token_ms(&self) -> Option<f64> {
792 match &self.token_timeline {
793 TokenTimeline::Recording(times) => times.last().copied(),
794 TokenTimeline::Finalized(summary) => Some(summary.last_ms),
795 }
796 }
797
798 fn actual_output_length(&self) -> usize {
799 match &self.token_timeline {
800 TokenTimeline::Recording(times) => times.len(),
801 TokenTimeline::Finalized(summary) => summary.len,
802 }
803 }
804
805 fn mean_tpot_ms(&self) -> Option<f64> {
806 let num_gaps = self.actual_output_length().saturating_sub(1);
807 if num_gaps == 0 {
808 return None;
809 }
810
811 let first_token_ms = self.first_token_ms()?;
812 let last_token_ms = self.last_token_ms()?;
813 Some((last_token_ms - first_token_ms).max(0.0) / num_gaps as f64)
814 }
815
816 fn ttst_ms(&self) -> Option<f64> {
817 let (first_token_ms, second_token_ms) = match &self.token_timeline {
818 TokenTimeline::Recording(times) => {
819 let [first_token_ms, second_token_ms, ..] = times.as_slice() else {
820 return None;
821 };
822 (*first_token_ms, *second_token_ms)
823 }
824 TokenTimeline::Finalized(summary) => (summary.first_ms, summary.second_ms?),
825 };
826 Some((second_token_ms - first_token_ms).max(0.0))
827 }
828
829 fn finalize_token_timeline(
830 &mut self,
831 include_in_distributions: bool,
832 itl_distribution: &mut StreamingDistribution,
833 output_token_throughput_per_user: &mut StreamingDistribution,
834 ) {
835 let TokenTimeline::Recording(times) = &self.token_timeline else {
836 return;
837 };
838
839 if include_in_distributions {
840 for window in times.windows(2) {
841 let itl_ms = (window[1] - window[0]).max(0.0);
842 itl_distribution.add(itl_ms);
843 if itl_ms > 0.0 {
844 output_token_throughput_per_user.add(1000.0 / itl_ms);
845 }
846 }
847 }
848
849 let Some(first_ms) = times.first().copied() else {
850 self.token_timeline = TokenTimeline::default();
851 return;
852 };
853 let summary = FinalizedTokenTimeline {
854 first_ms,
855 second_ms: times.get(1).copied(),
856 last_ms: times.last().copied().unwrap_or(first_ms),
857 len: times.len(),
858 };
859 self.token_timeline = TokenTimeline::Finalized(summary);
860 }
861}
862
863impl TraceCollector {
864 pub fn set_defer_token_timeline_finalization(&mut self, value: bool) {
866 self.defer_token_timeline_finalization = value;
867 }
868
869 pub fn set_capture_per_request(&mut self, value: bool) {
872 self.capture_per_request = value;
873 }
874
875 pub fn set_sla_thresholds(&mut self, sla: SlaThresholds) {
878 self.sla = sla;
879 }
880
881 pub(crate) fn add_worker_seconds(&mut self, prefill: f64, decode: f64) {
888 self.prefill_worker_seconds += prefill;
889 self.decode_worker_seconds += decode;
890 }
891
892 pub fn set_static_worker_count(&mut self, prefill: usize, decode: usize) {
897 self.static_worker_count = Some((prefill, decode));
898 }
899
900 pub(crate) fn clear_static_worker_count(&mut self) {
901 self.static_worker_count = None;
902 }
903
904 pub fn set_gpus_per_worker(&mut self, prefill: usize, decode: usize) {
907 self.prefill_gpus_per_worker = prefill;
908 self.decode_gpus_per_worker = decode;
909 }
910
911 pub(crate) fn set_runtime_evidence(&mut self, evidence: crate::replay::OfflineRuntimeEvidence) {
912 self.runtime_evidence = evidence;
913 }
914
915 pub fn on_arrival(
916 &mut self,
917 uuid: Uuid,
918 arrival_time_ms: f64,
919 input_length: usize,
920 requested_output_length: usize,
921 ) {
922 self.requests.insert(
923 uuid,
924 TraceRequestStats {
925 arrival_time_ms,
926 first_admit_ms: None,
927 terminal_time_ms: None,
928 terminal_status: None,
929 token_timeline: TokenTimeline::default(),
930 input_length,
931 requested_output_length,
932 reused_input_tokens: 0,
933 prefill_worker_idx: None,
934 decode_worker_idx: None,
935 session_id: None,
936 turn_index: None,
937 authored_id: None,
938 play_id: None,
939 dispatched_at_ms: None,
940 metadata: Value::Null,
941 first_admission_reused_input_tokens: 0,
942 detail: self
943 .capture_per_request
944 .then(|| Box::new(PerRequestDetail::default())),
945 },
946 );
947 }
948
949 pub fn on_session_metadata(&mut self, uuid: Uuid, session_id: String, turn_index: usize) {
954 if !self.capture_per_request {
955 return;
956 }
957 if let Some(stats) = self.requests.get_mut(&uuid)
958 && stats.session_id.is_none()
959 {
960 stats.session_id = Some(session_id);
961 stats.turn_index = Some(turn_index);
962 }
963 }
964
965 pub fn on_agentic_metadata(
966 &mut self,
967 uuid: Uuid,
968 request_id: String,
969 play_id: String,
970 dispatched_at_ms: f64,
971 ) {
972 if !self.capture_per_request {
973 return;
974 }
975 if let Some(stats) = self.requests.get_mut(&uuid) {
976 stats.authored_id = Some(request_id);
977 stats.play_id = Some(play_id);
978 stats.dispatched_at_ms = Some(dispatched_at_ms);
979 }
980 }
981
982 pub fn set_agentic_trajectory(&mut self, snapshot: AgenticTrajectorySnapshot) {
983 self.agentic_trajectory = Some(snapshot);
984 }
985
986 pub fn set_agentic_graph(&mut self, identity: AgenticGraphIdentity) {
987 self.agentic_graph = Some(identity);
988 }
989
990 pub fn on_request_context(
993 &mut self,
994 uuid: Uuid,
995 context: &crate::replay::ReplayRequestContext,
996 ) {
997 if !self.capture_per_request {
998 return;
999 }
1000 if let Some(stats) = self.requests.get_mut(&uuid) {
1001 stats.authored_id = Some(context.authored_id.clone());
1002 stats.session_id = context.session_id.clone().or(stats.session_id.take());
1003 stats.turn_index = context.turn_index.or(stats.turn_index);
1004 stats.metadata = context.metadata.clone();
1005 }
1006 }
1007
1008 pub(crate) fn on_prefill_assigned(&mut self, uuid: Uuid, worker_idx: usize) {
1013 if let Some(stats) = self.requests.get_mut(&uuid)
1014 && stats.prefill_worker_idx.is_none()
1015 {
1016 stats.prefill_worker_idx = Some(worker_idx);
1017 }
1018 }
1019
1020 pub fn on_decode_assigned(&mut self, uuid: Uuid, worker_idx: usize) {
1024 if let Some(stats) = self.requests.get_mut(&uuid)
1025 && stats.decode_worker_idx.is_none()
1026 {
1027 stats.decode_worker_idx = Some(worker_idx);
1028 }
1029 }
1030
1031 pub fn on_admit(&mut self, uuid: Uuid, admit_time_ms: f64, reused_input_tokens: usize) {
1032 self.on_admit_with_tier_attribution(uuid, admit_time_ms, reused_input_tokens, None);
1033 }
1034
1035 pub(crate) fn on_admit_with_tier_attribution(
1036 &mut self,
1037 uuid: Uuid,
1038 admit_time_ms: f64,
1039 reused_input_tokens: usize,
1040 attribution: Option<CacheTierAttribution>,
1041 ) {
1042 if let Some(attribution) = attribution {
1043 assert_eq!(
1044 attribution
1045 .g1_reused_input_tokens
1046 .checked_add(attribution.host_reused_input_tokens),
1047 Some(reused_input_tokens),
1048 "G1 and host attribution must sum to total reuse"
1049 );
1050 }
1051 if let Some(stats) = self.requests.get_mut(&uuid) {
1052 if stats.first_admit_ms.is_none() {
1053 stats.first_admission_reused_input_tokens = reused_input_tokens;
1054 if let Some(detail) = stats.detail.as_deref_mut() {
1055 detail.first_admission_cache_tier_attribution = attribution;
1056 }
1057 stats.first_admit_ms = Some(admit_time_ms);
1058 }
1059 stats.reused_input_tokens = stats.reused_input_tokens.max(reused_input_tokens);
1060 }
1061 }
1062
1063 pub(crate) fn on_prefill_admit(
1064 &mut self,
1065 uuid: Uuid,
1066 admit_time_ms: f64,
1067 reused_input_tokens: usize,
1068 ) {
1069 self.on_admit(uuid, admit_time_ms, reused_input_tokens);
1070 self.on_pool_admission(
1071 uuid,
1072 ReplayRequestPool::Prefill,
1073 admit_time_ms,
1074 reused_input_tokens,
1075 );
1076 if let Some(detail) = self.detail_mut(uuid) {
1077 detail.prefill_admit_ms.get_or_insert(admit_time_ms);
1078 detail.prefill_reused_input_tokens = Some(
1079 detail
1080 .prefill_reused_input_tokens
1081 .unwrap_or_default()
1082 .max(reused_input_tokens),
1083 );
1084 }
1085 }
1086
1087 pub(crate) fn on_decode_admit(
1088 &mut self,
1089 uuid: Uuid,
1090 admit_time_ms: f64,
1091 reused_input_tokens: usize,
1092 ) {
1093 self.on_admit(uuid, admit_time_ms, reused_input_tokens);
1094 self.on_pool_admission(
1095 uuid,
1096 ReplayRequestPool::Decode,
1097 admit_time_ms,
1098 reused_input_tokens,
1099 );
1100 if let Some(detail) = self.detail_mut(uuid) {
1101 detail.decode_admit_ms.get_or_insert(admit_time_ms);
1102 detail.decode_reused_input_tokens = Some(
1103 detail
1104 .decode_reused_input_tokens
1105 .unwrap_or_default()
1106 .max(reused_input_tokens),
1107 );
1108 }
1109 }
1110
1111 pub(crate) fn on_source_held(&mut self, uuid: Uuid, at_ms: f64) {
1112 if let Some(detail) = self.detail_mut(uuid) {
1113 detail.source_held_ms.get_or_insert(at_ms);
1114 }
1115 }
1116
1117 pub(crate) fn on_destination_reserved(&mut self, uuid: Uuid, at_ms: f64) {
1118 if let Some(detail) = self.detail_mut(uuid) {
1119 detail.destination_reserved_ms.get_or_insert(at_ms);
1120 }
1121 }
1122
1123 pub(crate) fn on_destination_activated(&mut self, uuid: Uuid, at_ms: f64) {
1124 if let Some(detail) = self.detail_mut(uuid) {
1125 detail.destination_activated_ms.get_or_insert(at_ms);
1126 }
1127 }
1128
1129 pub(crate) fn on_source_released(&mut self, uuid: Uuid, at_ms: f64) {
1130 if let Some(detail) = self.detail_mut(uuid) {
1131 detail.source_released_ms.get_or_insert(at_ms);
1132 }
1133 }
1134
1135 pub(crate) fn on_prefill_route_overlap(&mut self, uuid: Uuid, tokens: usize) {
1136 if let Some(detail) = self.detail_mut(uuid) {
1137 detail.prefill_route_overlap_tokens.get_or_insert(tokens);
1138 }
1139 }
1140
1141 pub(crate) fn on_decode_route_overlap(&mut self, uuid: Uuid, tokens: usize) {
1142 if let Some(detail) = self.detail_mut(uuid) {
1143 detail.decode_route_overlap_tokens.get_or_insert(tokens);
1144 }
1145 }
1146
1147 #[allow(clippy::too_many_arguments)]
1148 pub(crate) fn on_route_immediate(
1149 &mut self,
1150 uuid: Uuid,
1151 pool: ReplayRequestPool,
1152 logical_worker_id: usize,
1153 scheduler_id: usize,
1154 dp_rank: u32,
1155 reported_overlap_tokens: usize,
1156 cache_sample: Option<PlacementCacheSample>,
1157 placement_replica_id: Option<usize>,
1158 ) {
1159 let Some(detail) = self.detail_mut(uuid) else {
1160 return;
1161 };
1162 detail.routing_history.push(PerRequestRoutingRecord {
1163 pool,
1164 outcome: ReplayRoutingOutcome::Immediate,
1165 queue_entered_at_ms: None,
1166 released_at_ms: None,
1167 queue_wait_ms: Some(0.0),
1168 logical_worker_id: Some(logical_worker_id),
1169 scheduler_id: Some(scheduler_id),
1170 dp_rank: Some(dp_rank),
1171 reported_overlap_tokens: Some(reported_overlap_tokens),
1172 selected_overlap_blocks: cache_sample.map(|sample| sample.overlap_blocks),
1173 best_available_overlap_blocks: cache_sample
1174 .map(|sample| sample.best_available_overlap_blocks),
1175 overlap_regret_blocks: cache_sample.map(|sample| {
1176 sample
1177 .best_available_overlap_blocks
1178 .saturating_sub(sample.overlap_blocks)
1179 }),
1180 placement_replica_id,
1181 });
1182 }
1183
1184 pub(crate) fn on_route_queued(&mut self, uuid: Uuid, pool: ReplayRequestPool, at_ms: f64) {
1185 let Some(detail) = self.detail_mut(uuid) else {
1186 return;
1187 };
1188 detail.routing_history.push(PerRequestRoutingRecord {
1189 pool,
1190 outcome: ReplayRoutingOutcome::Queued,
1191 queue_entered_at_ms: Some(at_ms),
1192 released_at_ms: None,
1193 queue_wait_ms: None,
1194 logical_worker_id: None,
1195 scheduler_id: None,
1196 dp_rank: None,
1197 reported_overlap_tokens: None,
1198 selected_overlap_blocks: None,
1199 best_available_overlap_blocks: None,
1200 overlap_regret_blocks: None,
1201 placement_replica_id: None,
1202 });
1203 }
1204
1205 #[allow(clippy::too_many_arguments)]
1206 pub(crate) fn on_route_released(
1207 &mut self,
1208 uuid: Uuid,
1209 pool: ReplayRequestPool,
1210 at_ms: f64,
1211 logical_worker_id: usize,
1212 scheduler_id: usize,
1213 dp_rank: u32,
1214 reported_overlap_tokens: usize,
1215 cache_sample: Option<PlacementCacheSample>,
1216 placement_replica_id: Option<usize>,
1217 ) {
1218 let Some(detail) = self.detail_mut(uuid) else {
1219 return;
1220 };
1221 let Some(route) = detail.routing_history.iter_mut().rev().find(|route| {
1222 route.pool == pool
1223 && route.outcome == ReplayRoutingOutcome::Queued
1224 && route.released_at_ms.is_none()
1225 }) else {
1226 return;
1227 };
1228 let entered_at_ms = route.queue_entered_at_ms.unwrap_or(at_ms);
1229 route.released_at_ms = Some(at_ms);
1230 route.queue_wait_ms = Some((at_ms - entered_at_ms).max(0.0));
1231 route.logical_worker_id = Some(logical_worker_id);
1232 route.scheduler_id = Some(scheduler_id);
1233 route.dp_rank = Some(dp_rank);
1234 route.reported_overlap_tokens = Some(reported_overlap_tokens);
1235 route.selected_overlap_blocks = cache_sample.map(|sample| sample.overlap_blocks);
1236 route.best_available_overlap_blocks =
1237 cache_sample.map(|sample| sample.best_available_overlap_blocks);
1238 route.overlap_regret_blocks = cache_sample.map(|sample| {
1239 sample
1240 .best_available_overlap_blocks
1241 .saturating_sub(sample.overlap_blocks)
1242 });
1243 route.placement_replica_id = placement_replica_id;
1244 }
1245
1246 pub(crate) fn on_pool_admission(
1247 &mut self,
1248 uuid: Uuid,
1249 pool: ReplayRequestPool,
1250 at_ms: f64,
1251 reused_input_tokens: usize,
1252 ) {
1253 self.on_pool_admission_with_tier_attribution(uuid, pool, at_ms, reused_input_tokens, None);
1254 }
1255
1256 pub(crate) fn on_pool_admission_with_tier_attribution(
1257 &mut self,
1258 uuid: Uuid,
1259 pool: ReplayRequestPool,
1260 at_ms: f64,
1261 reused_input_tokens: usize,
1262 attribution: Option<CacheTierAttribution>,
1263 ) {
1264 let Some(detail) = self.detail_mut(uuid) else {
1265 return;
1266 };
1267 let admission_ordinal = detail.admission_history.len();
1268 let pool_count = &mut detail.pool_admission_counts[pool.index()];
1269 let pool_admission_ordinal = *pool_count;
1270 *pool_count += 1;
1271 detail.admission_history.push(PerRequestAdmissionRecord {
1272 admission_ordinal,
1273 pool_admission_ordinal,
1274 pool,
1275 at_ms,
1276 reused_input_tokens,
1277 g1_reused_input_tokens: attribution.map(|value| value.g1_reused_input_tokens),
1278 host_reused_input_tokens: attribution.map(|value| value.host_reused_input_tokens),
1279 is_readmission: pool_admission_ordinal > 0,
1280 });
1281 }
1282
1283 pub(crate) fn on_pressure_reference(&mut self, uuid: Uuid, pressure_ordinal: u64) {
1284 if let Some(detail) = self.detail_mut(uuid) {
1285 detail.pressure_record_ordinals.push(pressure_ordinal);
1286 }
1287 }
1288
1289 pub fn on_terminal(&mut self, uuid: Uuid, terminal_time_ms: f64, status: ReplayTerminalStatus) {
1290 let Self {
1291 requests,
1292 itl_distribution,
1293 output_token_throughput_per_user,
1294 defer_token_timeline_finalization,
1295 ..
1296 } = self;
1297 if let Some(stats) = requests.get_mut(&uuid)
1298 && stats.terminal_status.is_none()
1299 {
1300 stats.terminal_time_ms = Some(terminal_time_ms);
1301 stats.terminal_status = Some(status);
1302 if !*defer_token_timeline_finalization {
1303 stats.finalize_token_timeline(
1304 status == ReplayTerminalStatus::Completed && stats.first_admit_ms.is_some(),
1305 itl_distribution,
1306 output_token_throughput_per_user,
1307 );
1308 }
1309 }
1310 }
1311
1312 fn detail_mut(&mut self, uuid: Uuid) -> Option<&mut PerRequestDetail> {
1313 if !self.capture_per_request {
1314 return None;
1315 }
1316 self.requests.get_mut(&uuid)?.detail.as_deref_mut()
1317 }
1318
1319 pub fn on_token(&mut self, uuid: Uuid, token_time_ms: f64) {
1320 if let Some(stats) = self.requests.get_mut(&uuid)
1321 && let TokenTimeline::Recording(times) = &mut stats.token_timeline
1322 {
1323 times.push(token_time_ms);
1324 }
1325 }
1326
1327 pub(crate) fn request_latencies(&self, uuid: Uuid) -> Option<(f64, f64)> {
1329 let stats = self.requests.get(&uuid)?;
1330 let first_token_ms = stats.first_token_ms()?;
1331 let ttft_ms = (first_token_ms - stats.arrival_time_ms).max(0.0);
1332 let mean_itl_ms = stats.mean_tpot_ms().unwrap_or(0.0);
1333 Some((ttft_ms, mean_itl_ms))
1334 }
1335
1336 pub(crate) fn actual_output_length(&self, uuid: Uuid) -> Option<usize> {
1337 self.requests
1338 .get(&uuid)
1339 .map(TraceRequestStats::actual_output_length)
1340 }
1341
1342 pub fn finish(mut self) -> ReplayReport {
1343 let mut request_order = self.requests.keys().copied().collect::<Vec<_>>();
1344 request_order.sort_unstable_by(|left_uuid, right_uuid| {
1345 let left = self
1346 .requests
1347 .get(left_uuid)
1348 .expect("request order must reference retained request");
1349 let right = self
1350 .requests
1351 .get(right_uuid)
1352 .expect("request order must reference retained request");
1353 left.authored_id
1354 .as_deref()
1355 .cmp(&right.authored_id.as_deref())
1356 .then_with(|| left_uuid.cmp(right_uuid))
1357 });
1358
1359 for uuid in &request_order {
1360 let stats = self
1361 .requests
1362 .get_mut(uuid)
1363 .expect("request order must reference retained request");
1364 stats.finalize_token_timeline(
1365 stats.terminal_status == Some(ReplayTerminalStatus::Completed)
1366 && stats.first_admit_ms.is_some(),
1367 &mut self.itl_distribution,
1368 &mut self.output_token_throughput_per_user,
1369 );
1370 }
1371
1372 let per_request = if self.capture_per_request {
1378 self.per_request_records()
1379 } else {
1380 Vec::new()
1381 };
1382 let sla = self.sla;
1383 let static_worker_count = self.static_worker_count;
1384 let accumulated_prefill_worker_seconds = self.prefill_worker_seconds;
1385 let accumulated_decode_worker_seconds = self.decode_worker_seconds;
1386 let prefill_gpus_per_worker = self.prefill_gpus_per_worker;
1387 let decode_gpus_per_worker = self.decode_gpus_per_worker;
1388 let runtime_evidence = self.runtime_evidence;
1389 let agentic_graph = self.agentic_graph;
1390 let trajectories = self
1391 .agentic_trajectory
1392 .map(|snapshot| TraceTrajectoryStats {
1393 total: snapshot.total_trajectories,
1394 completed: snapshot.completed_trajectories,
1395 incomplete: snapshot
1396 .total_trajectories
1397 .saturating_sub(snapshot.completed_trajectories),
1398 e2e: build_distribution_stats(snapshot.e2e_latencies_ms),
1399 });
1400 let itl_distribution = self.itl_distribution.finish();
1401 let output_token_throughput_per_user = self.output_token_throughput_per_user.finish();
1402 let requests = self.requests;
1403 let request_count = requests.len();
1404 let mut ttfts = Vec::with_capacity(request_count);
1405 let mut ttsts = Vec::with_capacity(request_count);
1406 let mut tpots = Vec::with_capacity(request_count);
1407 let mut e2e_latencies = Vec::with_capacity(request_count);
1408 let mut duration_ms = 0.0_f64;
1409 let mut total_input_tokens = 0usize;
1410 let mut total_output_tokens = 0usize;
1411 let mut completed_requests = 0usize;
1412 let mut total_reused_tokens = 0usize;
1413 let mut total_first_admission_reused_tokens = 0usize;
1414 let mut goodput_requests = 0usize;
1416 let mut goodput_output_tokens = 0usize;
1417
1418 for uuid in request_order {
1419 let stats = requests
1420 .get(&uuid)
1421 .expect("request order must reference retained request");
1422 if stats.first_admit_ms.is_none() {
1423 continue;
1424 }
1425 if stats.terminal_status != Some(ReplayTerminalStatus::Completed) {
1426 continue;
1427 }
1428 let Some(terminal_time_ms) = stats.terminal_time_ms else {
1429 continue;
1430 };
1431
1432 completed_requests += 1;
1433 total_input_tokens += stats.input_length;
1434 let output_length = stats.actual_output_length();
1435 total_output_tokens += output_length;
1436 total_reused_tokens += stats.reused_input_tokens;
1437 total_first_admission_reused_tokens += stats.first_admission_reused_input_tokens;
1438 duration_ms = duration_ms.max(terminal_time_ms);
1439
1440 let (Some(first_token_ms), Some(last_token_ms)) =
1441 (stats.first_token_ms(), stats.last_token_ms())
1442 else {
1443 let e2e_ms = (terminal_time_ms - stats.arrival_time_ms).max(0.0);
1444 if sla.is_set() && sla.is_good_without_tokens(e2e_ms) {
1445 goodput_requests += 1;
1446 }
1447 continue;
1448 };
1449
1450 let ttft_ms = (first_token_ms - stats.arrival_time_ms).max(0.0);
1451 let e2e_ms = (last_token_ms - stats.arrival_time_ms).max(0.0);
1452 ttfts.push(ttft_ms);
1453 e2e_latencies.push(e2e_ms);
1454
1455 if sla.is_set() && sla.is_good(ttft_ms, e2e_ms, output_length) {
1457 goodput_requests += 1;
1458 goodput_output_tokens += output_length;
1459 }
1460
1461 if let Some(ttst_ms) = stats.ttst_ms() {
1462 ttsts.push(ttst_ms);
1463 }
1464
1465 if let Some(tpot_ms) = stats.mean_tpot_ms() {
1466 tpots.push(tpot_ms);
1467 }
1468 }
1469
1470 let num_ttft_samples = ttfts.len();
1471 let num_tpot_samples = tpots.len();
1472 let num_e2e_latency_samples = e2e_latencies.len();
1473 let duration_s = (duration_ms / 1000.0).max(1e-9);
1474 let (prefill_worker_seconds, decode_worker_seconds) = match static_worker_count {
1477 Some((prefill, decode)) => (prefill as f64 * duration_s, decode as f64 * duration_s),
1478 None => (
1479 accumulated_prefill_worker_seconds,
1480 accumulated_decode_worker_seconds,
1481 ),
1482 };
1483 let gpu_hours = (prefill_worker_seconds * prefill_gpus_per_worker as f64
1486 + decode_worker_seconds * decode_gpus_per_worker as f64)
1487 / 3600.0;
1488 let goodput = sla.is_set().then(|| TraceGoodputStats {
1490 completed_requests: goodput_requests,
1491 request_throughput_rps: goodput_requests as f64 / duration_s,
1492 output_throughput_tok_s: goodput_output_tokens as f64 / duration_s,
1493 });
1494 ReplayReport {
1495 request_counts: TraceRequestCounts {
1496 num_requests: request_count,
1497 completed_requests,
1498 total_input_tokens,
1499 total_output_tokens,
1500 },
1501 throughput: TraceThroughputStats {
1502 duration_ms,
1503 wall_time_ms: 0.0,
1504 request_throughput_rps: completed_requests as f64 / duration_s,
1505 input_throughput_tok_s: total_input_tokens as f64 / duration_s,
1506 output_throughput_tok_s: total_output_tokens as f64 / duration_s,
1507 total_throughput_tok_s: (total_input_tokens + total_output_tokens) as f64
1508 / duration_s,
1509 prefill_worker_seconds,
1510 decode_worker_seconds,
1511 prefill_gpus_per_worker,
1512 decode_gpus_per_worker,
1513 gpu_hours,
1514 },
1515 prefix_cache_reused_ratio: if total_input_tokens == 0 {
1516 0.0
1517 } else {
1518 total_reused_tokens as f64 / total_input_tokens as f64
1519 },
1520 first_admission_prefix_cache_reused_ratio: if total_input_tokens == 0 {
1521 0.0
1522 } else {
1523 total_first_admission_reused_tokens as f64 / total_input_tokens as f64
1524 },
1525 latency: TraceLatencyStats {
1526 num_ttft_samples,
1527 num_tpot_samples,
1528 num_e2e_latency_samples,
1529 ttft: build_distribution_stats(ttfts),
1530 ttst: build_distribution_stats(ttsts),
1531 tpot: build_distribution_stats(tpots),
1532 itl: TraceInterTokenLatencyStats {
1533 max_ms: itl_distribution.max_ms,
1534 distribution: itl_distribution,
1535 },
1536 e2e: build_distribution_stats(e2e_latencies),
1537 output_token_throughput_per_user,
1538 },
1539 trajectories,
1540 agentic_graph,
1541 goodput,
1542 per_request,
1543 runtime_evidence,
1544 }
1545 }
1546
1547 pub fn per_request_records(&self) -> Vec<PerRequestRecord> {
1554 let mut records = Vec::with_capacity(self.requests.len());
1555 for (uuid, stats) in &self.requests {
1556 let Some(detail) = stats.detail.as_deref() else {
1557 continue;
1558 };
1559 let Some(terminal_status) = stats.terminal_status else {
1560 continue;
1561 };
1562 let Some(terminal_time_ms) = stats.terminal_time_ms else {
1563 continue;
1564 };
1565 let first_token_ms = stats.first_token_ms();
1566 let last_token_ms = stats.last_token_ms();
1567 records.push(PerRequestRecord {
1568 request_id: stats.authored_id.clone(),
1569 play_id: stats.play_id.clone(),
1570 session_id: stats.session_id.clone(),
1571 turn_index: stats.turn_index,
1572 metadata: stats.metadata.clone(),
1573 uuid: uuid.to_string(),
1574 arrival_time_ms: stats.arrival_time_ms,
1575 dispatched_at_ms: stats.dispatched_at_ms,
1576 first_admit_ms: stats.first_admit_ms,
1577 terminal_time_ms,
1578 first_token_ms,
1579 last_token_ms,
1580 ttft_ms: first_token_ms.map(|time| (time - stats.arrival_time_ms).max(0.0)),
1581 ttst_ms: stats.ttst_ms(),
1582 e2e_latency_ms: last_token_ms.map(|time| (time - stats.arrival_time_ms).max(0.0)),
1583 itl_ms: stats.mean_tpot_ms(),
1584 input_length: stats.input_length,
1585 requested_output_length: stats.requested_output_length,
1586 output_length: stats.actual_output_length(),
1587 reused_input_tokens: detail
1588 .prefill_reused_input_tokens
1589 .unwrap_or(stats.reused_input_tokens),
1590 first_admission_g1_reused_input_tokens: detail
1591 .first_admission_cache_tier_attribution
1592 .map(|value| value.g1_reused_input_tokens),
1593 first_admission_host_reused_input_tokens: detail
1594 .first_admission_cache_tier_attribution
1595 .map(|value| value.host_reused_input_tokens),
1596 prefill_worker_idx: stats.prefill_worker_idx,
1597 decode_worker_idx: stats.decode_worker_idx,
1598 prefill_admit_ms: detail.prefill_admit_ms,
1599 source_held_ms: detail.source_held_ms,
1600 destination_reserved_ms: detail.destination_reserved_ms,
1601 destination_activated_ms: detail.destination_activated_ms,
1602 decode_admit_ms: detail.decode_admit_ms,
1603 source_released_ms: detail.source_released_ms,
1604 decode_reused_input_tokens: detail.decode_reused_input_tokens,
1605 prefill_route_overlap_tokens: detail.prefill_route_overlap_tokens,
1606 decode_route_overlap_tokens: detail.decode_route_overlap_tokens,
1607 routing_history: detail.routing_history.clone(),
1608 admission_count: detail.admission_history.len(),
1609 readmission_count: detail
1610 .pool_admission_counts
1611 .iter()
1612 .map(|count| count.saturating_sub(1))
1613 .sum(),
1614 admission_history: detail.admission_history.clone(),
1615 pressure_record_ordinals: detail.pressure_record_ordinals.clone(),
1616 terminal_status,
1617 });
1618 }
1619 records.sort_by(|a, b| {
1623 a.request_id
1624 .as_deref()
1625 .cmp(&b.request_id.as_deref())
1626 .then_with(|| a.arrival_time_ms.total_cmp(&b.arrival_time_ms))
1627 .then_with(|| a.uuid.cmp(&b.uuid))
1628 });
1629 records
1630 }
1631
1632 #[cfg(test)]
1633 pub(crate) fn snapshot(&self, uuid: Uuid) -> Option<TraceRequestStatsSnapshot> {
1634 self.requests
1635 .get(&uuid)
1636 .map(|stats| TraceRequestStatsSnapshot {
1637 arrival_time_ms: stats.arrival_time_ms,
1638 first_admit_ms: stats.first_admit_ms,
1639 first_token_ms: stats.first_token_ms(),
1640 last_token_ms: stats.last_token_ms(),
1641 input_length: stats.input_length,
1642 requested_output_length: stats.requested_output_length,
1643 output_length: stats.actual_output_length(),
1644 reused_input_tokens: stats.reused_input_tokens,
1645 first_admission_reused_input_tokens: stats.first_admission_reused_input_tokens,
1646 })
1647 }
1648
1649 #[cfg(test)]
1650 pub(crate) fn snapshots(&self) -> Vec<TraceRequestStatsSnapshot> {
1651 self.requests
1652 .values()
1653 .map(|stats| TraceRequestStatsSnapshot {
1654 arrival_time_ms: stats.arrival_time_ms,
1655 first_admit_ms: stats.first_admit_ms,
1656 first_token_ms: stats.first_token_ms(),
1657 last_token_ms: stats.last_token_ms(),
1658 input_length: stats.input_length,
1659 requested_output_length: stats.requested_output_length,
1660 output_length: stats.actual_output_length(),
1661 reused_input_tokens: stats.reused_input_tokens,
1662 first_admission_reused_input_tokens: stats.first_admission_reused_input_tokens,
1663 })
1664 .collect()
1665 }
1666
1667 #[cfg(test)]
1668 fn retained_token_timestamps(&self) -> usize {
1669 self.requests
1670 .values()
1671 .map(|stats| match &stats.token_timeline {
1672 TokenTimeline::Recording(times) => times.len(),
1673 TokenTimeline::Finalized(_) => 0,
1674 })
1675 .sum()
1676 }
1677}
1678
1679fn mean(values: &[f64]) -> f64 {
1680 if values.is_empty() {
1681 0.0
1682 } else {
1683 values.iter().sum::<f64>() / values.len() as f64
1684 }
1685}
1686
1687fn build_distribution_stats(mut values: Vec<f64>) -> TraceDistributionStats {
1688 if values.is_empty() {
1689 return empty_distribution_stats();
1690 }
1691
1692 let min_ms = values
1693 .iter()
1694 .copied()
1695 .min_by(|left, right| left.total_cmp(right))
1696 .expect("non-empty values must have a minimum");
1697 let max_ms = values
1698 .iter()
1699 .copied()
1700 .max_by(|left, right| left.total_cmp(right))
1701 .expect("non-empty values must have a maximum");
1702
1703 TraceDistributionStats {
1704 mean_ms: mean(&values),
1705 min_ms,
1706 max_ms,
1707 median_ms: percentile_in_place(&mut values, 50.0),
1708 p75_ms: percentile_in_place(&mut values, 75.0),
1709 p90_ms: percentile_in_place(&mut values, 90.0),
1710 p95_ms: percentile_in_place(&mut values, 95.0),
1711 p99_ms: percentile_in_place(&mut values, 99.0),
1712 std_ms: std_dev(&values),
1713 }
1714}
1715
1716fn empty_distribution_stats() -> TraceDistributionStats {
1717 TraceDistributionStats {
1718 mean_ms: 0.0,
1719 min_ms: 0.0,
1720 max_ms: 0.0,
1721 median_ms: 0.0,
1722 p75_ms: 0.0,
1723 p90_ms: 0.0,
1724 p95_ms: 0.0,
1725 p99_ms: 0.0,
1726 std_ms: 0.0,
1727 }
1728}
1729
1730fn percentile_in_place(values: &mut [f64], percentile: f64) -> f64 {
1731 let rank = percentile_rank(values.len(), percentile);
1732 let (_, selected, _) = values.select_nth_unstable_by(rank, |left, right| left.total_cmp(right));
1733 *selected
1734}
1735
1736fn percentile_rank(len: usize, percentile: f64) -> usize {
1737 let rank = ((len - 1) as f64 * percentile / 100.0).round() as usize;
1738 rank.min(len - 1)
1739}
1740
1741fn std_dev(values: &[f64]) -> f64 {
1742 if values.is_empty() {
1743 return 0.0;
1744 }
1745
1746 let mean = mean(values);
1747 let variance = values
1748 .iter()
1749 .map(|value| {
1750 let centered = value - mean;
1751 centered * centered
1752 })
1753 .sum::<f64>()
1754 / values.len() as f64;
1755 variance.sqrt()
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760 use super::*;
1761
1762 fn build_distribution_stats_sorted(values: &[f64]) -> TraceDistributionStats {
1763 if values.is_empty() {
1764 return TraceDistributionStats {
1765 mean_ms: 0.0,
1766 min_ms: 0.0,
1767 max_ms: 0.0,
1768 median_ms: 0.0,
1769 p75_ms: 0.0,
1770 p90_ms: 0.0,
1771 p95_ms: 0.0,
1772 p99_ms: 0.0,
1773 std_ms: 0.0,
1774 };
1775 }
1776
1777 let mut sorted = values.to_vec();
1778 sorted.sort_by(|left, right| left.total_cmp(right));
1779 TraceDistributionStats {
1780 mean_ms: mean(values),
1781 min_ms: sorted[0],
1782 max_ms: *sorted.last().expect("sorted values must be non-empty"),
1783 median_ms: sorted[percentile_rank(sorted.len(), 50.0)],
1784 p75_ms: sorted[percentile_rank(sorted.len(), 75.0)],
1785 p90_ms: sorted[percentile_rank(sorted.len(), 90.0)],
1786 p95_ms: sorted[percentile_rank(sorted.len(), 95.0)],
1787 p99_ms: sorted[percentile_rank(sorted.len(), 99.0)],
1788 std_ms: std_dev(values),
1789 }
1790 }
1791
1792 #[test]
1793 fn build_distribution_stats_matches_sorted_baseline() {
1794 let values = vec![
1795 0.0, 1.0, 1.0, 2.5, 4.0, 4.0, 7.25, 9.5, 15.0, 22.0, 22.0, 100.0,
1796 ];
1797
1798 let expected = build_distribution_stats_sorted(&values);
1799 let actual = build_distribution_stats(values);
1800
1801 assert_eq!(actual.mean_ms, expected.mean_ms);
1802 assert_eq!(actual.min_ms, expected.min_ms);
1803 assert_eq!(actual.max_ms, expected.max_ms);
1804 assert_eq!(actual.median_ms, expected.median_ms);
1805 assert_eq!(actual.p75_ms, expected.p75_ms);
1806 assert_eq!(actual.p90_ms, expected.p90_ms);
1807 assert_eq!(actual.p95_ms, expected.p95_ms);
1808 assert_eq!(actual.p99_ms, expected.p99_ms);
1809 assert_eq!(actual.std_ms, expected.std_ms);
1810 }
1811
1812 #[test]
1813 fn built_in_ddsketch_configuration_and_quantiles_are_valid() {
1814 let mut distribution = StreamingDistribution::default();
1815 assert!((distribution.sketch.alpha() - DDSKETCH_RELATIVE_ACCURACY).abs() < f64::EPSILON);
1816 for percentile in [50.0, 75.0, 90.0, 95.0, 99.0] {
1817 assert_eq!(distribution.percentile(percentile), 0.0);
1818 }
1819
1820 for value in [1e-9, 1e18] {
1823 distribution.add(value);
1824 }
1825 for (quantile, expected) in [(0.0, 1e-9), (1.0, 1e18)] {
1826 let actual = distribution.sketch.quantile(quantile).unwrap();
1827 assert!((actual - expected).abs() <= expected * DDSKETCH_RELATIVE_ACCURACY);
1828 }
1829 }
1830
1831 #[test]
1832 fn streaming_distribution_preserves_all_zero_samples() {
1833 let mut distribution = StreamingDistribution::default();
1834 for _ in 0..128 {
1835 distribution.add(0.0);
1836 }
1837
1838 assert_eq!(distribution.sketch.get_zero_count(), 128);
1839 let stats = distribution.finish();
1840 for value in [
1841 stats.mean_ms,
1842 stats.min_ms,
1843 stats.max_ms,
1844 stats.median_ms,
1845 stats.p75_ms,
1846 stats.p90_ms,
1847 stats.p95_ms,
1848 stats.p99_ms,
1849 stats.std_ms,
1850 ] {
1851 assert_eq!(value, 0.0);
1852 }
1853 }
1854
1855 #[test]
1856 fn streaming_percentiles_select_the_historical_rounded_rank() {
1857 let percentiles = [
1858 0.0, 1.0, 10.0, 25.0, 49.0, 50.0, 51.0, 75.0, 90.0, 95.0, 99.0, 100.0,
1859 ];
1860 for len in [2, 3, 4, 5, 10, 11, 100, 101, 256, 257] {
1861 let values = (0..len)
1862 .map(|index| 1_000.0 + index as f64 * 10.0)
1863 .collect::<Vec<_>>();
1864 let mut distribution = StreamingDistribution::default();
1865 for &value in &values {
1866 distribution.add(value);
1867 }
1868
1869 for percentile in percentiles {
1870 let expected = values[percentile_rank(values.len(), percentile)];
1871 let actual = distribution.percentile(percentile);
1872 assert!(
1873 (actual - expected).abs() <= expected * DDSKETCH_RELATIVE_ACCURACY,
1874 "len={len} percentile={percentile}: expected rank value {expected}, got {actual}"
1875 );
1876 }
1877 }
1878 }
1879
1880 #[test]
1881 fn completed_zero_output_request_counts_without_latency_samples() {
1882 let mut collector = TraceCollector::default();
1883 collector.set_static_worker_count(0, 1);
1884 collector.set_gpus_per_worker(0, 4);
1885 let uuid = Uuid::from_u128(99);
1886 collector.on_arrival(uuid, 0.0, 32, 0);
1887 collector.on_admit(uuid, 5.0, 8);
1888 collector.on_terminal(uuid, 25.0, ReplayTerminalStatus::Completed);
1889
1890 let report = collector.finish();
1891
1892 assert_eq!(report.request_counts.completed_requests, 1);
1893 assert_eq!(report.request_counts.total_input_tokens, 32);
1894 assert_eq!(report.request_counts.total_output_tokens, 0);
1895 assert_eq!(report.throughput.duration_ms, 25.0);
1896 assert_eq!(report.throughput.decode_worker_seconds, 0.025);
1897 assert!((report.throughput.gpu_hours - 0.1 / 3600.0).abs() < 1e-12);
1898 assert_eq!(report.latency.num_ttft_samples, 0);
1899 assert_eq!(report.latency.num_tpot_samples, 0);
1900 assert_eq!(report.latency.num_e2e_latency_samples, 0);
1901 assert_eq!(report.latency.ttft.mean_ms, 0.0);
1902 assert_eq!(report.latency.e2e.mean_ms, 0.0);
1903
1904 let summary = serde_json::to_value(&report).unwrap();
1905 assert_eq!(summary["num_ttft_samples"], 0);
1906 assert_eq!(summary["num_tpot_samples"], 0);
1907 assert_eq!(summary["num_e2e_latency_samples"], 0);
1908 }
1909
1910 #[test]
1911 fn token_before_simulation_cap_does_not_count_as_completion() {
1912 let mut collector = TraceCollector::default();
1913 let uuid = Uuid::from_u128(100);
1914 collector.on_arrival(uuid, 0.0, 32, 4);
1915 collector.on_admit(uuid, 5.0, 0);
1916 collector.on_token(uuid, 25.0);
1917
1918 let report = collector.finish();
1919
1920 assert_eq!(report.request_counts.completed_requests, 0);
1921 assert_eq!(report.request_counts.total_input_tokens, 0);
1922 assert_eq!(report.request_counts.total_output_tokens, 0);
1923 assert_eq!(report.throughput.duration_ms, 0.0);
1924 }
1925
1926 #[test]
1927 fn zero_output_goodput_requires_e2e_only_sla() {
1928 let collect = |sla| {
1929 let mut collector = TraceCollector::default();
1930 collector.set_sla_thresholds(sla);
1931 let uuid = Uuid::from_u128(101);
1932 collector.on_arrival(uuid, 0.0, 32, 0);
1933 collector.on_admit(uuid, 5.0, 0);
1934 collector.on_terminal(uuid, 100.0, ReplayTerminalStatus::Completed);
1935 collector.finish().goodput.unwrap().completed_requests
1936 };
1937
1938 assert_eq!(
1939 collect(SlaThresholds {
1940 e2e_ms: Some(100.0),
1941 ..Default::default()
1942 }),
1943 1
1944 );
1945 assert_eq!(
1946 collect(SlaThresholds {
1947 ttft_ms: Some(1_000.0),
1948 ..Default::default()
1949 }),
1950 0
1951 );
1952 }
1953
1954 #[test]
1958 fn per_request_disagg_record_populates_all_fields() {
1959 let mut collector = TraceCollector::default();
1960 collector.set_capture_per_request(true);
1961 let uuid = Uuid::from_u128(1);
1962 collector.on_arrival(uuid, 0.0, 100, 4);
1963 collector.on_prefill_route_overlap(uuid, 64);
1964 collector.on_prefill_admit(uuid, 5.0, 30);
1965 collector.on_source_held(uuid, 10.0);
1966 collector.on_destination_reserved(uuid, 12.0);
1967 collector.on_destination_activated(uuid, 20.0);
1968 collector.on_source_released(uuid, 21.0);
1969 collector.on_decode_route_overlap(uuid, 32);
1970 collector.on_decode_admit(uuid, 25.0, 40);
1971 collector.on_prefill_assigned(uuid, 2);
1972 collector.on_decode_assigned(uuid, 7);
1973 collector.on_token(uuid, 50.0);
1974 collector.on_token(uuid, 60.0);
1975 collector.on_token(uuid, 75.0);
1976 collector.on_token(uuid, 95.0);
1977 collector.on_terminal(uuid, 95.0, ReplayTerminalStatus::Completed);
1978
1979 let report = collector.finish();
1980 assert_eq!(report.per_request.len(), 1);
1981 let rec = &report.per_request[0];
1982 assert_eq!(rec.uuid, uuid.to_string());
1983 assert_eq!(rec.arrival_time_ms, 0.0);
1984 assert_eq!(rec.first_admit_ms, Some(5.0));
1985 assert_eq!(rec.terminal_time_ms, 95.0);
1986 assert_eq!(rec.first_token_ms, Some(50.0));
1987 assert_eq!(rec.last_token_ms, Some(95.0));
1988 assert_eq!(rec.ttft_ms, Some(50.0));
1989 assert_eq!(rec.ttst_ms, Some(10.0));
1990 assert_eq!(rec.e2e_latency_ms, Some(95.0));
1991 assert_eq!(rec.itl_ms, Some(15.0));
1993 assert_eq!(rec.input_length, 100);
1994 assert_eq!(rec.output_length, 4);
1995 assert_eq!(rec.reused_input_tokens, 30);
1996 assert_eq!(rec.prefill_worker_idx, Some(2));
1997 assert_eq!(rec.decode_worker_idx, Some(7));
1998 assert_eq!(rec.prefill_admit_ms, Some(5.0));
1999 assert_eq!(rec.source_held_ms, Some(10.0));
2000 assert_eq!(rec.destination_reserved_ms, Some(12.0));
2001 assert_eq!(rec.destination_activated_ms, Some(20.0));
2002 assert_eq!(rec.source_released_ms, Some(21.0));
2003 assert_eq!(rec.decode_admit_ms, Some(25.0));
2004 assert_eq!(rec.decode_reused_input_tokens, Some(40));
2005 assert_eq!(rec.prefill_route_overlap_tokens, Some(64));
2006 assert_eq!(rec.decode_route_overlap_tokens, Some(32));
2007 assert_eq!(rec.terminal_status, ReplayTerminalStatus::Completed);
2008 }
2009
2010 #[test]
2011 fn per_request_route_records_overlap_regret_and_policy_replica() {
2012 let mut collector = TraceCollector::default();
2013 collector.set_capture_per_request(true);
2014 let uuid = Uuid::from_u128(2);
2015 collector.on_arrival(uuid, 0.0, 2_048, 1);
2016 collector.on_route_immediate(
2017 uuid,
2018 ReplayRequestPool::Agg,
2019 3,
2020 3,
2021 0,
2022 1_024,
2023 Some(PlacementCacheSample {
2024 overlap_blocks: 4,
2025 best_available_overlap_blocks: 7,
2026 isl_blocks: 8,
2027 }),
2028 Some(1),
2029 );
2030 collector.on_terminal(uuid, 1.0, ReplayTerminalStatus::Completed);
2031
2032 let report = collector.finish();
2033 let route = &report.per_request[0].routing_history[0];
2034 assert_eq!(route.selected_overlap_blocks, Some(4));
2035 assert_eq!(route.best_available_overlap_blocks, Some(7));
2036 assert_eq!(route.overlap_regret_blocks, Some(3));
2037 assert_eq!(route.placement_replica_id, Some(1));
2038 }
2039
2040 #[test]
2044 fn per_request_bypass_leaves_prefill_worker_idx_none() {
2045 let mut collector = TraceCollector::default();
2046 collector.set_capture_per_request(true);
2047 let uuid = Uuid::from_u128(42);
2048 collector.on_arrival(uuid, 0.0, 100, 2);
2049 collector.on_admit(uuid, 5.0, 0);
2050 collector.on_decode_assigned(uuid, 1);
2052 collector.on_token(uuid, 30.0);
2053 collector.on_token(uuid, 45.0);
2054 collector.on_terminal(uuid, 45.0, ReplayTerminalStatus::Completed);
2055
2056 let report = collector.finish();
2057 assert_eq!(report.per_request.len(), 1);
2058 let rec = &report.per_request[0];
2059 assert!(
2060 rec.prefill_worker_idx.is_none(),
2061 "bypassed request must have prefill_worker_idx = None"
2062 );
2063 assert_eq!(rec.decode_worker_idx, Some(1));
2064 }
2065
2066 #[test]
2069 fn per_request_default_off() {
2070 let mut collector = TraceCollector::default();
2071 let uuid = Uuid::from_u128(1);
2073 collector.on_arrival(uuid, 0.0, 100, 2);
2074 collector.on_admit_with_tier_attribution(
2075 uuid,
2076 5.0,
2077 20,
2078 Some(CacheTierAttribution {
2079 g1_reused_input_tokens: 12,
2080 host_reused_input_tokens: 8,
2081 }),
2082 );
2083 collector.on_decode_assigned(uuid, 0);
2084 collector.on_token(uuid, 50.0);
2085 collector.on_token(uuid, 60.0);
2086 collector.on_terminal(uuid, 60.0, ReplayTerminalStatus::Completed);
2087
2088 assert!(collector.requests[&uuid].detail.is_none());
2089
2090 let report = collector.finish();
2091 assert!(report.per_request.is_empty());
2092 assert_eq!(report.request_counts.completed_requests, 1);
2094 assert_eq!(report.prefix_cache_reused_ratio, 0.2);
2095 assert_eq!(report.first_admission_prefix_cache_reused_ratio, 0.2);
2096 }
2097
2098 fn add_completed(
2101 collector: &mut TraceCollector,
2102 uuid_n: u128,
2103 arrival_ms: f64,
2104 output_length: usize,
2105 token_times_ms: &[f64],
2106 ) {
2107 let uuid = Uuid::from_u128(uuid_n);
2108 collector.on_arrival(uuid, arrival_ms, 100, output_length);
2109 collector.on_admit(uuid, arrival_ms, 0);
2110 collector.on_decode_assigned(uuid, 0);
2111 for &t in token_times_ms {
2112 collector.on_token(uuid, t);
2113 }
2114 let terminal_time_ms = token_times_ms.last().copied().unwrap_or(arrival_ms);
2115 collector.on_terminal(uuid, terminal_time_ms, ReplayTerminalStatus::Completed);
2116 }
2117
2118 #[test]
2122 fn goodput_classifies_by_aiperf_avg_itl() {
2123 let mut collector = TraceCollector::default();
2124 collector.set_sla_thresholds(SlaThresholds {
2125 ttft_ms: Some(150.0),
2126 itl_ms: Some(30.0),
2127 e2e_ms: None,
2128 });
2129 add_completed(&mut collector, 1, 0.0, 3, &[100.0, 150.0, 200.0]);
2131 add_completed(&mut collector, 2, 0.0, 3, &[100.0, 120.0, 140.0]);
2133 add_completed(&mut collector, 3, 0.0, 1, &[100.0]);
2135
2136 let goodput = collector
2137 .finish()
2138 .goodput
2139 .expect("SLA set → goodput present");
2140 assert_eq!(goodput.completed_requests, 2); assert!((goodput.output_throughput_tok_s - 4.0 / 0.2).abs() < 1e-6);
2143 assert!((goodput.request_throughput_rps - 2.0 / 0.2).abs() < 1e-6);
2144 }
2145
2146 #[test]
2147 fn sla_validation_accepts_independent_token_bounds() {
2148 for sla in [
2149 SlaThresholds {
2150 ttft_ms: Some(150.0),
2151 ..Default::default()
2152 },
2153 SlaThresholds {
2154 itl_ms: Some(30.0),
2155 ..Default::default()
2156 },
2157 ] {
2158 sla.validate().unwrap();
2159 }
2160 }
2161
2162 #[test]
2164 fn goodput_ttft_only_ignores_itl() {
2165 let mut collector = TraceCollector::default();
2166 collector.set_sla_thresholds(SlaThresholds {
2167 ttft_ms: Some(150.0),
2168 ..Default::default()
2169 });
2170 add_completed(&mut collector, 1, 0.0, 3, &[100.0, 250.0, 400.0]);
2172 add_completed(&mut collector, 2, 0.0, 3, &[200.0, 210.0, 220.0]);
2174
2175 assert_eq!(collector.finish().goodput.unwrap().completed_requests, 1);
2176 }
2177
2178 #[test]
2180 fn goodput_itl_boundary_is_inclusive() {
2181 let sla = SlaThresholds {
2182 ttft_ms: None,
2183 itl_ms: Some(50.0),
2184 e2e_ms: None,
2185 };
2186 let mut at_bound = TraceCollector::default();
2188 at_bound.set_sla_thresholds(sla);
2189 add_completed(&mut at_bound, 1, 0.0, 3, &[100.0, 150.0, 200.0]);
2190 assert_eq!(at_bound.finish().goodput.unwrap().completed_requests, 1);
2191 let mut over = TraceCollector::default();
2193 over.set_sla_thresholds(sla);
2194 add_completed(&mut over, 1, 0.0, 3, &[100.0, 150.0, 201.0]);
2195 assert_eq!(over.finish().goodput.unwrap().completed_requests, 0);
2196 }
2197
2198 #[test]
2200 fn goodput_e2e_only_sla() {
2201 let mut collector = TraceCollector::default();
2202 collector.set_sla_thresholds(SlaThresholds {
2203 ttft_ms: None,
2204 itl_ms: None,
2205 e2e_ms: Some(150.0),
2206 });
2207 add_completed(&mut collector, 1, 0.0, 2, &[100.0, 200.0]); add_completed(&mut collector, 2, 0.0, 2, &[60.0, 120.0]); assert_eq!(collector.finish().goodput.unwrap().completed_requests, 1);
2210 }
2211
2212 #[test]
2214 fn goodput_absent_without_sla() {
2215 let mut collector = TraceCollector::default();
2216 add_completed(&mut collector, 1, 0.0, 2, &[10.0, 20.0]);
2217 assert!(collector.finish().goodput.is_none());
2218 }
2219
2220 #[test]
2223 fn worker_seconds_accumulated_and_static() {
2224 let mut accumulated = TraceCollector::default();
2225 add_completed(&mut accumulated, 1, 0.0, 2, &[10.0, 20.0]);
2226 accumulated.add_worker_seconds(1.5, 4.0);
2227 accumulated.add_worker_seconds(0.5, 1.0);
2228 let report = accumulated.finish();
2229 assert!((report.throughput.prefill_worker_seconds - 2.0).abs() < 1e-9);
2230 assert!((report.throughput.decode_worker_seconds - 5.0).abs() < 1e-9);
2231
2232 let mut static_runtime = TraceCollector::default();
2233 static_runtime.set_static_worker_count(0, 1);
2234 add_completed(&mut static_runtime, 1, 0.0, 2, &[100.0, 200.0]); let report = static_runtime.finish();
2236 assert!(report.throughput.prefill_worker_seconds.abs() < 1e-9);
2237 assert!((report.throughput.decode_worker_seconds - 0.2).abs() < 1e-9);
2238 }
2239
2240 #[test]
2241 fn compact_summary_excludes_execution_evidence() {
2242 let mut collector = TraceCollector::default();
2243 add_completed(&mut collector, 1, 0.0, 2, &[10.0, 20.0]);
2244 let mut report = collector.finish();
2245 report.runtime_evidence.pressure = Some(crate::replay::PressureEvidence::default());
2246
2247 let summary = serde_json::to_value(&report).unwrap();
2248 assert!(summary.get("runtime_evidence").is_none());
2249 }
2250
2251 #[test]
2254 fn gpu_hours_from_worker_seconds_and_gpus_per_worker() {
2255 let mut collector = TraceCollector::default();
2256 collector.set_gpus_per_worker(2, 4); add_completed(&mut collector, 1, 0.0, 2, &[100.0, 200.0]);
2258 collector.add_worker_seconds(10.0, 5.0); let report = collector.finish();
2260 assert_eq!(report.throughput.prefill_gpus_per_worker, 2);
2261 assert_eq!(report.throughput.decode_gpus_per_worker, 4);
2262 assert!((report.throughput.gpu_hours - 40.0 / 3600.0).abs() < 1e-9);
2264 }
2265
2266 #[test]
2269 fn per_request_records_are_sorted_by_arrival_time() {
2270 let mut collector = TraceCollector::default();
2271 collector.set_capture_per_request(true);
2272 for (uuid_n, arrival) in [(3u128, 30.0), (1, 0.0), (2, 10.0)] {
2274 let uuid = Uuid::from_u128(uuid_n);
2275 collector.on_arrival(uuid, arrival, 100, 1);
2276 collector.on_admit(uuid, arrival + 1.0, 0);
2277 collector.on_decode_assigned(uuid, 0);
2278 collector.on_token(uuid, arrival + 5.0);
2279 collector.on_terminal(uuid, arrival + 5.0, ReplayTerminalStatus::Completed);
2280 }
2281 let report = collector.finish();
2282 let arrivals: Vec<f64> = report
2283 .per_request
2284 .iter()
2285 .map(|r| r.arrival_time_ms)
2286 .collect();
2287 assert_eq!(arrivals, vec![0.0, 10.0, 30.0]);
2288 }
2289
2290 #[test]
2291 fn per_request_records_have_total_order_with_mixed_authored_ids() {
2292 let mut collector = TraceCollector::default();
2293 collector.set_capture_per_request(true);
2294 for (uuid_n, arrival, request_id) in [
2295 (1_u128, 30.0, Some("request-b")),
2296 (2, 20.0, None),
2297 (3, 10.0, Some("request-a")),
2298 (4, 0.0, None),
2299 ] {
2300 let uuid = Uuid::from_u128(uuid_n);
2301 collector.on_arrival(uuid, arrival, 100, 1);
2302 if let Some(request_id) = request_id {
2303 collector.on_agentic_metadata(
2304 uuid,
2305 request_id.to_string(),
2306 "play".to_string(),
2307 arrival,
2308 );
2309 }
2310 collector.on_admit(uuid, arrival + 1.0, 0);
2311 collector.on_decode_assigned(uuid, 0);
2312 collector.on_token(uuid, arrival + 5.0);
2313 collector.on_terminal(uuid, arrival + 5.0, ReplayTerminalStatus::Completed);
2314 }
2315
2316 let report = collector.finish();
2317 assert_eq!(
2318 report
2319 .per_request
2320 .iter()
2321 .map(|record| (record.request_id.as_deref(), record.arrival_time_ms))
2322 .collect::<Vec<_>>(),
2323 vec![
2324 (None, 0.0),
2325 (None, 20.0),
2326 (Some("request-a"), 10.0),
2327 (Some("request-b"), 30.0),
2328 ]
2329 );
2330 }
2331
2332 #[test]
2333 fn agentic_summary_is_independent_of_runtime_uuid_and_insertion_order() {
2334 fn report(requests: [(&str, u128, f64); 3]) -> ReplayReport {
2335 let mut collector = TraceCollector::default();
2336 collector.set_capture_per_request(true);
2337 collector.set_defer_token_timeline_finalization(true);
2338 for (request_id, uuid, ttft_ms) in requests {
2339 let uuid = Uuid::from_u128(uuid);
2340 collector.on_arrival(uuid, 0.0, 100, 1);
2341 collector.on_agentic_metadata(
2342 uuid,
2343 request_id.to_string(),
2344 "play".to_string(),
2345 0.0,
2346 );
2347 collector.on_admit(uuid, 0.0, 0);
2348 collector.on_decode_assigned(uuid, 0);
2349 collector.on_token(uuid, ttft_ms);
2350 collector.on_terminal(uuid, ttft_ms, ReplayTerminalStatus::Completed);
2351 }
2352 collector.finish()
2353 }
2354
2355 let left = report([
2356 ("request-a", 3, 1.0e16),
2357 ("request-b", 1, 1.0),
2358 ("request-c", 2, 1.0),
2359 ]);
2360 let right = report([
2361 ("request-c", 300, 1.0),
2362 ("request-a", 100, 1.0e16),
2363 ("request-b", 200, 1.0),
2364 ]);
2365
2366 assert_eq!(
2367 serde_json::to_value(&left).unwrap(),
2368 serde_json::to_value(&right).unwrap()
2369 );
2370 for report in [&left, &right] {
2371 assert_eq!(
2372 report
2373 .per_request
2374 .iter()
2375 .map(|record| record.request_id.as_deref().unwrap())
2376 .collect::<Vec<_>>(),
2377 vec!["request-a", "request-b", "request-c"]
2378 );
2379 }
2380 }
2381
2382 #[test]
2386 fn per_request_record_serializes_to_json_object() {
2387 let mut collector = TraceCollector::default();
2388 collector.set_capture_per_request(true);
2389 let uuid = Uuid::from_u128(123);
2390 collector.on_arrival(uuid, 0.0, 50, 2);
2391 collector.on_admit(uuid, 1.0, 10);
2392 collector.on_prefill_assigned(uuid, 0);
2393 collector.on_decode_assigned(uuid, 1);
2394 collector.on_token(uuid, 20.0);
2395 collector.on_token(uuid, 25.0);
2396 collector.on_terminal(uuid, 25.0, ReplayTerminalStatus::Completed);
2397
2398 let report = collector.finish();
2399 let line = serde_json::to_string(&report.per_request[0])
2400 .expect("PerRequestRecord must serialize cleanly");
2401 let parsed: serde_json::Value =
2403 serde_json::from_str(&line).expect("emitted JSON must parse");
2404 assert!(parsed.is_object());
2405 assert_eq!(parsed["uuid"], uuid.to_string());
2406 assert_eq!(parsed["input_length"], 50);
2407 assert_eq!(parsed["output_length"], 2);
2408 assert_eq!(parsed["prefill_worker_idx"], 0);
2409 assert_eq!(parsed["decode_worker_idx"], 1);
2410 assert!(parsed["itl_ms"].is_number());
2411 assert_eq!(parsed["terminal_status"], "completed");
2412 }
2413
2414 #[test]
2415 fn terminal_failures_emit_nullable_latencies_and_unfinished_requests_are_omitted() {
2416 let mut collector = TraceCollector::default();
2417 collector.set_capture_per_request(true);
2418 for (uuid_n, status) in [
2419 (1, ReplayTerminalStatus::Rejected),
2420 (2, ReplayTerminalStatus::Canceled),
2421 (3, ReplayTerminalStatus::Failed),
2422 ] {
2423 let uuid = Uuid::from_u128(uuid_n);
2424 collector.on_arrival(uuid, uuid_n as f64, 64, 2);
2425 collector.on_terminal(uuid, uuid_n as f64 + 1.0, status);
2426 }
2427 collector.on_arrival(Uuid::from_u128(4), 4.0, 64, 2);
2428
2429 let report = collector.finish();
2430
2431 assert_eq!(report.per_request.len(), 3);
2432 assert_eq!(
2433 report
2434 .per_request
2435 .iter()
2436 .map(|record| record.terminal_status)
2437 .collect::<Vec<_>>(),
2438 vec![
2439 ReplayTerminalStatus::Rejected,
2440 ReplayTerminalStatus::Canceled,
2441 ReplayTerminalStatus::Failed,
2442 ]
2443 );
2444 assert!(report.per_request.iter().all(|record| {
2445 record.first_admit_ms.is_none()
2446 && record.first_token_ms.is_none()
2447 && record.last_token_ms.is_none()
2448 && record.ttft_ms.is_none()
2449 && record.e2e_latency_ms.is_none()
2450 }));
2451 }
2452
2453 #[test]
2454 fn first_admission_reuse_ignores_later_readmission_self_reuse() {
2455 let uuid = Uuid::from_u128(1);
2456 let mut collector = TraceCollector::default();
2457 collector.on_arrival(uuid, 0.0, 100, 1);
2458 collector.on_admit(uuid, 1.0, 0);
2459 collector.on_admit(uuid, 2.0, 80);
2460 collector.on_token(uuid, 3.0);
2461 collector.on_terminal(uuid, 3.0, ReplayTerminalStatus::Completed);
2462
2463 let report = collector.finish();
2464
2465 assert_eq!(report.prefix_cache_reused_ratio, 0.8);
2466 assert_eq!(report.first_admission_prefix_cache_reused_ratio, 0.0);
2467 }
2468
2469 #[test]
2470 fn terminal_request_releases_per_token_timestamps() {
2471 let uuid = Uuid::from_u128(7);
2472 let mut collector = TraceCollector::default();
2473 collector.on_arrival(uuid, 0.0, 128, 100_000);
2474 collector.on_admit(uuid, 1.0, 0);
2475 for token_index in 0..100_000 {
2476 collector.on_token(uuid, token_index as f64 + 10.0);
2477 }
2478 assert_eq!(collector.retained_token_timestamps(), 100_000);
2479
2480 collector.on_terminal(uuid, 100_009.0, ReplayTerminalStatus::Completed);
2481
2482 assert_eq!(collector.retained_token_timestamps(), 0);
2483 let snapshot = collector
2484 .snapshot(uuid)
2485 .expect("request must remain summarized");
2486 assert_eq!(snapshot.output_length, 100_000);
2487 assert_eq!(snapshot.first_token_ms, Some(10.0));
2488 assert_eq!(snapshot.last_token_ms, Some(100_009.0));
2489 let report = collector.finish();
2490 assert_eq!(report.latency.itl.distribution.mean_ms, 1.0);
2491 assert_eq!(report.latency.itl.distribution.min_ms, 1.0);
2492 assert_eq!(report.latency.itl.distribution.max_ms, 1.0);
2493 }
2494
2495 #[test]
2496 fn deferred_token_timeline_finalization_folds_at_finish() {
2497 let uuid = Uuid::from_u128(8);
2498 let mut collector = TraceCollector::default();
2499 collector.set_defer_token_timeline_finalization(true);
2500 collector.on_arrival(uuid, 0.0, 128, 3);
2501 collector.on_admit(uuid, 1.0, 0);
2502 collector.on_token(uuid, 10.0);
2503 collector.on_token(uuid, 12.0);
2504 collector.on_token(uuid, 15.0);
2505 collector.on_terminal(uuid, 15.0, ReplayTerminalStatus::Completed);
2506
2507 assert_eq!(collector.retained_token_timestamps(), 3);
2508
2509 let report = collector.finish();
2510 assert_eq!(report.latency.itl.distribution.mean_ms, 2.5);
2511 assert_eq!(report.latency.itl.distribution.min_ms, 2.0);
2512 assert_eq!(report.latency.itl.distribution.max_ms, 3.0);
2513 }
2514}