1use ddsketchy::DDSketch;
5use rustc_hash::FxHashMap;
6use serde::Serialize;
7use serde::ser::{SerializeMap, Serializer};
8use std::fmt::{Display, Formatter, Result as FmtResult};
9use uuid::Uuid;
10
11use crate::common::protocols::OutputSignal;
12
13const DDSKETCH_RELATIVE_ACCURACY: f64 = 0.001;
17const DDSKETCH_MAX_BINS: usize = 32_768;
18
19#[derive(Debug, Clone)]
20pub struct TraceSimulationReport {
21 pub request_counts: TraceRequestCounts,
22 pub throughput: TraceThroughputStats,
23 pub prefix_cache_reused_ratio: f64,
24 pub first_admission_prefix_cache_reused_ratio: f64,
25 pub latency: TraceLatencyStats,
26 pub goodput: Option<TraceGoodputStats>,
30 pub per_request: Vec<PerRequestRecord>,
36}
37
38#[derive(Debug, Clone)]
39pub struct TraceRequestCounts {
40 pub num_requests: usize,
41 pub completed_requests: usize,
42 pub total_input_tokens: usize,
43 pub total_output_tokens: usize,
44}
45
46#[derive(Debug, Clone)]
47pub struct TraceThroughputStats {
48 pub duration_ms: f64,
49 pub wall_time_ms: f64,
50 pub request_throughput_rps: f64,
51 pub input_throughput_tok_s: f64,
52 pub output_throughput_tok_s: f64,
53 pub total_throughput_tok_s: f64,
54 pub prefill_worker_seconds: f64,
66 pub decode_worker_seconds: f64,
67 pub prefill_gpus_per_worker: usize,
71 pub decode_gpus_per_worker: usize,
72 pub gpu_hours: f64,
77}
78
79#[derive(Debug, Clone)]
84pub struct TraceGoodputStats {
85 pub completed_requests: usize,
87 pub request_throughput_rps: f64,
89 pub output_throughput_tok_s: f64,
91}
92
93#[derive(Debug, Clone)]
94pub struct TraceDistributionStats {
95 pub mean_ms: f64,
96 pub min_ms: f64,
97 pub max_ms: f64,
98 pub median_ms: f64,
99 pub p75_ms: f64,
100 pub p90_ms: f64,
101 pub p95_ms: f64,
102 pub p99_ms: f64,
103 pub std_ms: f64,
104}
105
106#[derive(Debug, Clone)]
107pub struct TraceLatencyStats {
108 pub ttft: TraceDistributionStats,
109 pub ttst: TraceDistributionStats,
110 pub tpot: TraceDistributionStats,
111 pub itl: TraceInterTokenLatencyStats,
112 pub e2e: TraceDistributionStats,
113 pub output_token_throughput_per_user: TraceDistributionStats,
114}
115
116#[derive(Debug, Clone)]
117pub struct TraceInterTokenLatencyStats {
118 pub distribution: TraceDistributionStats,
119 pub max_ms: f64,
120}
121
122impl TraceSimulationReport {
123 pub fn with_wall_time_ms(mut self, wall_time_ms: f64) -> Self {
124 self.throughput.wall_time_ms = wall_time_ms;
125 self
126 }
127
128 pub fn processed_tokens(&self) -> usize {
129 self.request_counts.total_input_tokens + self.request_counts.total_output_tokens
130 }
131
132 pub fn processed_tokens_per_s(&self) -> f64 {
133 if self.throughput.wall_time_ms <= 0.0 {
134 return 0.0;
135 }
136 self.processed_tokens() as f64 / self.throughput.wall_time_ms * 1000.0
137 }
138
139 pub fn processed_output_tokens_per_s(&self) -> f64 {
140 if self.throughput.wall_time_ms <= 0.0 {
141 return 0.0;
142 }
143 self.request_counts.total_output_tokens as f64 / self.throughput.wall_time_ms * 1000.0
144 }
145}
146
147impl Display for TraceSimulationReport {
148 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
149 writeln!(
150 f,
151 " completed_requests: {}",
152 self.request_counts.completed_requests
153 )?;
154 writeln!(
155 f,
156 " request_throughput_rps: {:.6}",
157 self.throughput.request_throughput_rps
158 )?;
159 writeln!(
160 f,
161 " output_throughput_tok_s: {:.6}",
162 self.throughput.output_throughput_tok_s
163 )?;
164 writeln!(
165 f,
166 " total_input_tokens: {}",
167 self.request_counts.total_input_tokens
168 )?;
169 writeln!(
170 f,
171 " total_output_tokens: {}",
172 self.request_counts.total_output_tokens
173 )?;
174 writeln!(
175 f,
176 " processed_tokens_per_s: {:.6}",
177 self.processed_tokens_per_s()
178 )?;
179 writeln!(
180 f,
181 " processed_output_tokens_per_s: {:.6}",
182 self.processed_output_tokens_per_s()
183 )?;
184 writeln!(f, " mean_ttft_ms: {:.6}", self.latency.ttft.mean_ms)?;
185 writeln!(f, " mean_e2e_latency_ms: {:.6}", self.latency.e2e.mean_ms)?;
186 writeln!(
187 f,
188 " prefix_cache_reused_ratio: {:.6}",
189 self.prefix_cache_reused_ratio
190 )?;
191 writeln!(
192 f,
193 " first_admission_prefix_cache_reused_ratio: {:.6}",
194 self.first_admission_prefix_cache_reused_ratio
195 )?;
196 write!(f, " wall_time_ms: {:.6}", self.throughput.wall_time_ms)
197 }
198}
199
200impl Serialize for TraceSimulationReport {
201 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
202 where
203 S: Serializer,
204 {
205 let mut map = serializer.serialize_map(Some(70))?;
206 map.serialize_entry("num_requests", &self.request_counts.num_requests)?;
207 map.serialize_entry(
208 "completed_requests",
209 &self.request_counts.completed_requests,
210 )?;
211 map.serialize_entry(
212 "total_input_tokens",
213 &self.request_counts.total_input_tokens,
214 )?;
215 map.serialize_entry(
216 "total_output_tokens",
217 &self.request_counts.total_output_tokens,
218 )?;
219 map.serialize_entry("duration_ms", &self.throughput.duration_ms)?;
220 map.serialize_entry("wall_time_ms", &self.throughput.wall_time_ms)?;
221 map.serialize_entry(
222 "request_throughput_rps",
223 &self.throughput.request_throughput_rps,
224 )?;
225 map.serialize_entry(
226 "input_throughput_tok_s",
227 &self.throughput.input_throughput_tok_s,
228 )?;
229 map.serialize_entry(
230 "output_throughput_tok_s",
231 &self.throughput.output_throughput_tok_s,
232 )?;
233 map.serialize_entry(
234 "total_throughput_tok_s",
235 &self.throughput.total_throughput_tok_s,
236 )?;
237 map.serialize_entry(
238 "prefill_worker_seconds",
239 &self.throughput.prefill_worker_seconds,
240 )?;
241 map.serialize_entry(
242 "decode_worker_seconds",
243 &self.throughput.decode_worker_seconds,
244 )?;
245 map.serialize_entry(
246 "prefill_gpus_per_worker",
247 &self.throughput.prefill_gpus_per_worker,
248 )?;
249 map.serialize_entry(
250 "decode_gpus_per_worker",
251 &self.throughput.decode_gpus_per_worker,
252 )?;
253 map.serialize_entry("gpu_hours", &self.throughput.gpu_hours)?;
254 if let Some(goodput) = &self.goodput {
255 map.serialize_entry("goodput_completed_requests", &goodput.completed_requests)?;
256 map.serialize_entry(
257 "goodput_request_throughput_rps",
258 &goodput.request_throughput_rps,
259 )?;
260 map.serialize_entry(
261 "goodput_output_throughput_tok_s",
262 &goodput.output_throughput_tok_s,
263 )?;
264 }
265 map.serialize_entry("processed_tokens", &self.processed_tokens())?;
266 map.serialize_entry("processed_tokens_per_s", &self.processed_tokens_per_s())?;
267 map.serialize_entry(
268 "processed_output_tokens_per_s",
269 &self.processed_output_tokens_per_s(),
270 )?;
271 map.serialize_entry("prefix_cache_reused_ratio", &self.prefix_cache_reused_ratio)?;
272 map.serialize_entry(
273 "first_admission_prefix_cache_reused_ratio",
274 &self.first_admission_prefix_cache_reused_ratio,
275 )?;
276 serialize_distribution(&mut map, "ttft", &self.latency.ttft)?;
277 serialize_distribution(&mut map, "ttst", &self.latency.ttst)?;
278 serialize_distribution(&mut map, "tpot", &self.latency.tpot)?;
279 serialize_distribution(&mut map, "itl", &self.latency.itl.distribution)?;
280 map.serialize_entry("max_itl_ms", &self.latency.itl.max_ms)?;
281 serialize_distribution(&mut map, "e2e_latency", &self.latency.e2e)?;
282 serialize_rate_distribution(
283 &mut map,
284 "output_token_throughput_per_user",
285 &self.latency.output_token_throughput_per_user,
286 )?;
287 map.end()
288 }
289}
290
291fn serialize_distribution<S>(
292 map: &mut S,
293 prefix: &str,
294 stats: &TraceDistributionStats,
295) -> Result<(), S::Error>
296where
297 S: SerializeMap,
298{
299 map.serialize_entry(&format!("mean_{prefix}_ms"), &stats.mean_ms)?;
300 map.serialize_entry(&format!("min_{prefix}_ms"), &stats.min_ms)?;
301 map.serialize_entry(&format!("max_{prefix}_ms"), &stats.max_ms)?;
302 map.serialize_entry(&format!("median_{prefix}_ms"), &stats.median_ms)?;
303 map.serialize_entry(&format!("p75_{prefix}_ms"), &stats.p75_ms)?;
304 map.serialize_entry(&format!("p90_{prefix}_ms"), &stats.p90_ms)?;
305 map.serialize_entry(&format!("p95_{prefix}_ms"), &stats.p95_ms)?;
306 map.serialize_entry(&format!("p99_{prefix}_ms"), &stats.p99_ms)?;
307 map.serialize_entry(&format!("std_{prefix}_ms"), &stats.std_ms)?;
308 Ok(())
309}
310
311fn serialize_rate_distribution<S>(
312 map: &mut S,
313 prefix: &str,
314 stats: &TraceDistributionStats,
315) -> Result<(), S::Error>
316where
317 S: SerializeMap,
318{
319 map.serialize_entry(&format!("mean_{prefix}"), &stats.mean_ms)?;
320 map.serialize_entry(&format!("min_{prefix}"), &stats.min_ms)?;
321 map.serialize_entry(&format!("max_{prefix}"), &stats.max_ms)?;
322 map.serialize_entry(&format!("median_{prefix}"), &stats.median_ms)?;
323 map.serialize_entry(&format!("p75_{prefix}"), &stats.p75_ms)?;
324 map.serialize_entry(&format!("p90_{prefix}"), &stats.p90_ms)?;
325 map.serialize_entry(&format!("p95_{prefix}"), &stats.p95_ms)?;
326 map.serialize_entry(&format!("p99_{prefix}"), &stats.p99_ms)?;
327 map.serialize_entry(&format!("std_{prefix}"), &stats.std_ms)?;
328 Ok(())
329}
330
331#[derive(Debug)]
332struct TraceRequestStats {
333 arrival_time_ms: f64,
334 first_admit_ms: Option<f64>,
335 terminal_time_ms: Option<f64>,
336 terminal_status: Option<ReplayTerminalStatus>,
337 token_timeline: TokenTimeline,
338 input_length: usize,
339 requested_output_length: usize,
340 reused_input_tokens: usize,
341 first_admission_reused_input_tokens: usize,
342 prefill_worker_idx: Option<usize>,
351 decode_worker_idx: Option<usize>,
353 session_id: Option<String>,
357 turn_index: Option<usize>,
358 detail: Option<Box<PerRequestDetail>>,
359}
360
361#[derive(Debug)]
362enum TokenTimeline {
363 Recording(Vec<f64>),
364 Finalized(FinalizedTokenTimeline),
365}
366
367impl Default for TokenTimeline {
368 fn default() -> Self {
369 Self::Recording(Vec::new())
370 }
371}
372
373#[derive(Debug, Clone, Copy)]
374struct FinalizedTokenTimeline {
375 first_ms: f64,
376 second_ms: Option<f64>,
377 last_ms: f64,
378 len: usize,
379}
380
381#[derive(Debug)]
382struct StreamingDistribution {
383 sketch: DDSketch,
384 count: u64,
385 mean: f64,
386 sum_squared_deviations: f64,
387 min: f64,
388 max: f64,
389}
390
391impl Default for StreamingDistribution {
392 fn default() -> Self {
393 let sketch = match DDSketch::with_max_bins(DDSKETCH_RELATIVE_ACCURACY, DDSKETCH_MAX_BINS) {
394 Ok(sketch) => sketch,
395 Err(error) => panic!("invalid built-in DDSketch configuration: {error}"),
396 };
397 Self {
398 sketch,
399 count: 0,
400 mean: 0.0,
401 sum_squared_deviations: 0.0,
402 min: f64::INFINITY,
403 max: f64::NEG_INFINITY,
404 }
405 }
406}
407
408impl StreamingDistribution {
409 fn add(&mut self, value: f64) {
410 if !value.is_finite() {
411 return;
412 }
413
414 self.sketch.add(value);
415 self.count += 1;
416 let delta = value - self.mean;
417 self.mean += delta / self.count as f64;
418 let delta_after_mean_update = value - self.mean;
419 self.sum_squared_deviations += delta * delta_after_mean_update;
420 self.min = self.min.min(value);
421 self.max = self.max.max(value);
422 }
423
424 fn finish(&self) -> TraceDistributionStats {
425 if self.count == 0 {
426 return empty_distribution_stats();
427 }
428
429 TraceDistributionStats {
430 mean_ms: self.mean,
431 min_ms: self.min,
432 max_ms: self.max,
433 median_ms: self.percentile(50.0),
434 p75_ms: self.percentile(75.0),
435 p90_ms: self.percentile(90.0),
436 p95_ms: self.percentile(95.0),
437 p99_ms: self.percentile(99.0),
438 std_ms: (self.sum_squared_deviations / self.count as f64).sqrt(),
439 }
440 }
441
442 fn percentile(&self, percentile: f64) -> f64 {
445 let span = self.count.saturating_sub(1);
446 let rank = (span as f64 * percentile / 100.0).round() as u64;
447 let quantile = if span == 0 || rank >= span {
448 1.0
449 } else {
450 (rank as f64 + 0.5) / span as f64
451 };
452 match self.sketch.quantile(quantile) {
453 Ok(value) => value,
454 Err(error) => panic!("invalid built-in DDSketch quantile {quantile}: {error}"),
455 }
456 }
457}
458
459#[derive(Debug, Default)]
460struct PerRequestDetail {
461 prefill_reused_input_tokens: Option<usize>,
462 prefill_admit_ms: Option<f64>,
463 source_held_ms: Option<f64>,
464 destination_reserved_ms: Option<f64>,
465 destination_activated_ms: Option<f64>,
466 decode_admit_ms: Option<f64>,
467 source_released_ms: Option<f64>,
468 decode_reused_input_tokens: Option<usize>,
469 prefill_route_overlap_tokens: Option<usize>,
470 decode_route_overlap_tokens: Option<usize>,
471}
472
473#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
474#[serde(rename_all = "snake_case")]
475pub enum ReplayTerminalStatus {
476 Completed,
477 Rejected,
478 Canceled,
479 Failed,
480}
481
482#[derive(Debug, Clone, Serialize)]
487pub struct PerRequestRecord {
488 pub session_id: Option<String>,
494 pub turn_index: Option<usize>,
496 pub uuid: String,
497 pub arrival_time_ms: f64,
498 pub first_admit_ms: Option<f64>,
499 pub terminal_time_ms: f64,
500 pub first_token_ms: Option<f64>,
501 pub last_token_ms: Option<f64>,
502 pub ttft_ms: Option<f64>,
503 pub ttst_ms: Option<f64>,
504 pub e2e_latency_ms: Option<f64>,
505 pub itl_ms: Option<f64>,
508 pub input_length: usize,
509 pub requested_output_length: usize,
511 pub output_length: usize,
513 pub reused_input_tokens: usize,
514 pub prefill_worker_idx: Option<usize>,
515 pub decode_worker_idx: Option<usize>,
516 pub prefill_admit_ms: Option<f64>,
517 pub source_held_ms: Option<f64>,
518 pub destination_reserved_ms: Option<f64>,
519 pub destination_activated_ms: Option<f64>,
520 pub decode_admit_ms: Option<f64>,
521 pub source_released_ms: Option<f64>,
522 pub decode_reused_input_tokens: Option<usize>,
523 pub prefill_route_overlap_tokens: Option<usize>,
524 pub decode_route_overlap_tokens: Option<usize>,
525 pub terminal_status: ReplayTerminalStatus,
526}
527
528#[cfg(test)]
529#[derive(Debug, Clone, PartialEq)]
530pub(crate) struct TraceRequestStatsSnapshot {
531 pub arrival_time_ms: f64,
532 pub first_admit_ms: Option<f64>,
533 pub first_token_ms: Option<f64>,
534 pub last_token_ms: Option<f64>,
535 pub input_length: usize,
536 pub requested_output_length: usize,
537 pub output_length: usize,
538 pub reused_input_tokens: usize,
539 pub first_admission_reused_input_tokens: usize,
540}
541
542#[derive(Debug, Clone, Copy, Default)]
548pub struct SlaThresholds {
549 pub ttft_ms: Option<f64>,
550 pub itl_ms: Option<f64>,
551 pub e2e_ms: Option<f64>,
552}
553
554impl SlaThresholds {
555 pub(crate) fn is_set(&self) -> bool {
556 self.ttft_ms.is_some() || self.itl_ms.is_some() || self.e2e_ms.is_some()
557 }
558
559 fn is_good(&self, ttft_ms: f64, e2e_ms: f64, output_length: usize) -> bool {
570 if let Some(bound) = self.e2e_ms
571 && e2e_ms > bound
572 {
573 return false;
574 }
575 if let Some(bound) = self.ttft_ms
576 && ttft_ms > bound
577 {
578 return false;
579 }
580 if let Some(bound) = self.itl_ms
581 && output_length > 1
582 {
583 let avg_itl_ms = (e2e_ms - ttft_ms) / (output_length as f64 - 1.0);
584 if avg_itl_ms > bound {
585 return false;
586 }
587 }
588 true
589 }
590
591 fn is_good_without_tokens(&self, e2e_ms: f64) -> bool {
592 self.ttft_ms.is_none()
593 && self.itl_ms.is_none()
594 && self.e2e_ms.is_some_and(|bound| e2e_ms <= bound)
595 }
596}
597
598#[derive(Debug, Default)]
599pub(crate) struct TraceCollector {
600 requests: FxHashMap<Uuid, TraceRequestStats>,
601 itl_distribution: StreamingDistribution,
604 output_token_throughput_per_user: StreamingDistribution,
605 defer_token_timeline_finalization: bool,
608 capture_per_request: bool,
612 sla: SlaThresholds,
615 prefill_worker_seconds: f64,
620 decode_worker_seconds: f64,
621 static_worker_count: Option<(usize, usize)>,
626 prefill_gpus_per_worker: usize,
629 decode_gpus_per_worker: usize,
630}
631
632impl TraceRequestStats {
633 fn first_token_ms(&self) -> Option<f64> {
634 match &self.token_timeline {
635 TokenTimeline::Recording(times) => times.first().copied(),
636 TokenTimeline::Finalized(summary) => Some(summary.first_ms),
637 }
638 }
639
640 fn last_token_ms(&self) -> Option<f64> {
641 match &self.token_timeline {
642 TokenTimeline::Recording(times) => times.last().copied(),
643 TokenTimeline::Finalized(summary) => Some(summary.last_ms),
644 }
645 }
646
647 fn actual_output_length(&self) -> usize {
648 match &self.token_timeline {
649 TokenTimeline::Recording(times) => times.len(),
650 TokenTimeline::Finalized(summary) => summary.len,
651 }
652 }
653
654 fn mean_tpot_ms(&self) -> Option<f64> {
655 let num_gaps = self.actual_output_length().saturating_sub(1);
656 if num_gaps == 0 {
657 return None;
658 }
659
660 let first_token_ms = self.first_token_ms()?;
661 let last_token_ms = self.last_token_ms()?;
662 Some((last_token_ms - first_token_ms).max(0.0) / num_gaps as f64)
663 }
664
665 fn ttst_ms(&self) -> Option<f64> {
666 let (first_token_ms, second_token_ms) = match &self.token_timeline {
667 TokenTimeline::Recording(times) => {
668 let [first_token_ms, second_token_ms, ..] = times.as_slice() else {
669 return None;
670 };
671 (*first_token_ms, *second_token_ms)
672 }
673 TokenTimeline::Finalized(summary) => (summary.first_ms, summary.second_ms?),
674 };
675 Some((second_token_ms - first_token_ms).max(0.0))
676 }
677
678 fn finalize_token_timeline(
679 &mut self,
680 include_in_distributions: bool,
681 itl_distribution: &mut StreamingDistribution,
682 output_token_throughput_per_user: &mut StreamingDistribution,
683 ) {
684 let TokenTimeline::Recording(times) = &self.token_timeline else {
685 return;
686 };
687
688 if include_in_distributions {
689 for window in times.windows(2) {
690 let itl_ms = (window[1] - window[0]).max(0.0);
691 itl_distribution.add(itl_ms);
692 if itl_ms > 0.0 {
693 output_token_throughput_per_user.add(1000.0 / itl_ms);
694 }
695 }
696 }
697
698 let Some(first_ms) = times.first().copied() else {
699 self.token_timeline = TokenTimeline::default();
700 return;
701 };
702 let summary = FinalizedTokenTimeline {
703 first_ms,
704 second_ms: times.get(1).copied(),
705 last_ms: times.last().copied().unwrap_or(first_ms),
706 len: times.len(),
707 };
708 self.token_timeline = TokenTimeline::Finalized(summary);
709 }
710}
711
712impl TraceCollector {
713 pub(crate) fn set_defer_token_timeline_finalization(&mut self, value: bool) {
715 self.defer_token_timeline_finalization = value;
716 }
717
718 pub(crate) fn set_capture_per_request(&mut self, value: bool) {
721 self.capture_per_request = value;
722 }
723
724 pub(crate) fn set_sla_thresholds(&mut self, sla: SlaThresholds) {
727 self.sla = sla;
728 }
729
730 pub(crate) fn add_worker_seconds(&mut self, prefill: f64, decode: f64) {
737 self.prefill_worker_seconds += prefill;
738 self.decode_worker_seconds += decode;
739 }
740
741 pub(crate) fn set_static_worker_count(&mut self, prefill: usize, decode: usize) {
745 self.static_worker_count = Some((prefill, decode));
746 }
747
748 pub(crate) fn clear_static_worker_count(&mut self) {
749 self.static_worker_count = None;
750 }
751
752 pub(crate) fn set_gpus_per_worker(&mut self, prefill: usize, decode: usize) {
755 self.prefill_gpus_per_worker = prefill;
756 self.decode_gpus_per_worker = decode;
757 }
758
759 pub(crate) fn on_arrival(
760 &mut self,
761 uuid: Uuid,
762 arrival_time_ms: f64,
763 input_length: usize,
764 requested_output_length: usize,
765 ) {
766 self.requests.insert(
767 uuid,
768 TraceRequestStats {
769 arrival_time_ms,
770 first_admit_ms: None,
771 terminal_time_ms: None,
772 terminal_status: None,
773 token_timeline: TokenTimeline::default(),
774 input_length,
775 requested_output_length,
776 reused_input_tokens: 0,
777 prefill_worker_idx: None,
778 decode_worker_idx: None,
779 session_id: None,
780 turn_index: None,
781 first_admission_reused_input_tokens: 0,
782 detail: self
783 .capture_per_request
784 .then(|| Box::new(PerRequestDetail::default())),
785 },
786 );
787 }
788
789 pub(crate) fn on_session_metadata(
794 &mut self,
795 uuid: Uuid,
796 session_id: String,
797 turn_index: usize,
798 ) {
799 if !self.capture_per_request {
800 return;
801 }
802 if let Some(stats) = self.requests.get_mut(&uuid)
803 && stats.session_id.is_none()
804 {
805 stats.session_id = Some(session_id);
806 stats.turn_index = Some(turn_index);
807 }
808 }
809
810 pub(crate) fn on_prefill_assigned(&mut self, uuid: Uuid, worker_idx: usize) {
815 if let Some(stats) = self.requests.get_mut(&uuid)
816 && stats.prefill_worker_idx.is_none()
817 {
818 stats.prefill_worker_idx = Some(worker_idx);
819 }
820 }
821
822 pub(crate) fn on_decode_assigned(&mut self, uuid: Uuid, worker_idx: usize) {
826 if let Some(stats) = self.requests.get_mut(&uuid)
827 && stats.decode_worker_idx.is_none()
828 {
829 stats.decode_worker_idx = Some(worker_idx);
830 }
831 }
832
833 pub(crate) fn on_admit(&mut self, uuid: Uuid, admit_time_ms: f64, reused_input_tokens: usize) {
834 if let Some(stats) = self.requests.get_mut(&uuid) {
835 if stats.first_admit_ms.is_none() {
836 stats.first_admission_reused_input_tokens = reused_input_tokens;
837 stats.first_admit_ms = Some(admit_time_ms);
838 }
839 stats.reused_input_tokens = stats.reused_input_tokens.max(reused_input_tokens);
840 }
841 }
842
843 pub(crate) fn on_prefill_admit(
844 &mut self,
845 uuid: Uuid,
846 admit_time_ms: f64,
847 reused_input_tokens: usize,
848 ) {
849 self.on_admit(uuid, admit_time_ms, reused_input_tokens);
850 if let Some(detail) = self.detail_mut(uuid) {
851 detail.prefill_admit_ms.get_or_insert(admit_time_ms);
852 detail.prefill_reused_input_tokens = Some(
853 detail
854 .prefill_reused_input_tokens
855 .unwrap_or_default()
856 .max(reused_input_tokens),
857 );
858 }
859 }
860
861 pub(crate) fn on_decode_admit(
862 &mut self,
863 uuid: Uuid,
864 admit_time_ms: f64,
865 reused_input_tokens: usize,
866 ) {
867 self.on_admit(uuid, admit_time_ms, reused_input_tokens);
868 if let Some(detail) = self.detail_mut(uuid) {
869 detail.decode_admit_ms.get_or_insert(admit_time_ms);
870 detail.decode_reused_input_tokens = Some(
871 detail
872 .decode_reused_input_tokens
873 .unwrap_or_default()
874 .max(reused_input_tokens),
875 );
876 }
877 }
878
879 pub(crate) fn on_source_held(&mut self, uuid: Uuid, at_ms: f64) {
880 if let Some(detail) = self.detail_mut(uuid) {
881 detail.source_held_ms.get_or_insert(at_ms);
882 }
883 }
884
885 pub(crate) fn on_destination_reserved(&mut self, uuid: Uuid, at_ms: f64) {
886 if let Some(detail) = self.detail_mut(uuid) {
887 detail.destination_reserved_ms.get_or_insert(at_ms);
888 }
889 }
890
891 pub(crate) fn on_destination_activated(&mut self, uuid: Uuid, at_ms: f64) {
892 if let Some(detail) = self.detail_mut(uuid) {
893 detail.destination_activated_ms.get_or_insert(at_ms);
894 }
895 }
896
897 pub(crate) fn on_source_released(&mut self, uuid: Uuid, at_ms: f64) {
898 if let Some(detail) = self.detail_mut(uuid) {
899 detail.source_released_ms.get_or_insert(at_ms);
900 }
901 }
902
903 pub(crate) fn on_prefill_route_overlap(&mut self, uuid: Uuid, tokens: usize) {
904 if let Some(detail) = self.detail_mut(uuid) {
905 detail.prefill_route_overlap_tokens.get_or_insert(tokens);
906 }
907 }
908
909 pub(crate) fn on_decode_route_overlap(&mut self, uuid: Uuid, tokens: usize) {
910 if let Some(detail) = self.detail_mut(uuid) {
911 detail.decode_route_overlap_tokens.get_or_insert(tokens);
912 }
913 }
914
915 pub(crate) fn on_terminal(
916 &mut self,
917 uuid: Uuid,
918 terminal_time_ms: f64,
919 status: ReplayTerminalStatus,
920 ) {
921 let Self {
922 requests,
923 itl_distribution,
924 output_token_throughput_per_user,
925 defer_token_timeline_finalization,
926 ..
927 } = self;
928 if let Some(stats) = requests.get_mut(&uuid)
929 && stats.terminal_status.is_none()
930 {
931 stats.terminal_time_ms = Some(terminal_time_ms);
932 stats.terminal_status = Some(status);
933 if !*defer_token_timeline_finalization {
934 stats.finalize_token_timeline(
935 status == ReplayTerminalStatus::Completed && stats.first_admit_ms.is_some(),
936 itl_distribution,
937 output_token_throughput_per_user,
938 );
939 }
940 }
941 }
942
943 fn detail_mut(&mut self, uuid: Uuid) -> Option<&mut PerRequestDetail> {
944 if !self.capture_per_request {
945 return None;
946 }
947 self.requests.get_mut(&uuid)?.detail.as_deref_mut()
948 }
949
950 pub(crate) fn on_token(&mut self, uuid: Uuid, token_time_ms: f64) {
951 if let Some(stats) = self.requests.get_mut(&uuid)
952 && let TokenTimeline::Recording(times) = &mut stats.token_timeline
953 {
954 times.push(token_time_ms);
955 }
956 }
957
958 pub(crate) fn align_pass_token_times(
963 &mut self,
964 output_signals: &[OutputSignal],
965 completion_time_ms: f64,
966 ) {
967 let mut emitted_by_request = FxHashMap::default();
968 for signal in output_signals {
969 if signal.token_id.is_some() {
970 *emitted_by_request.entry(signal.uuid).or_insert(0usize) += 1;
971 }
972 }
973
974 for (uuid, emitted) in emitted_by_request {
975 let Some(stats) = self.requests.get_mut(&uuid) else {
976 continue;
977 };
978 let TokenTimeline::Recording(times) = &mut stats.token_timeline else {
979 continue;
980 };
981 let start = times
982 .len()
983 .checked_sub(emitted)
984 .expect("scheduler emitted more output signals than collector tokens");
985 times[start..].fill(completion_time_ms);
986 }
987 }
988
989 pub(crate) fn request_latencies(&self, uuid: Uuid) -> Option<(f64, f64)> {
991 let stats = self.requests.get(&uuid)?;
992 let first_token_ms = stats.first_token_ms()?;
993 let ttft_ms = (first_token_ms - stats.arrival_time_ms).max(0.0);
994 let mean_itl_ms = stats.mean_tpot_ms().unwrap_or(0.0);
995 Some((ttft_ms, mean_itl_ms))
996 }
997
998 pub(crate) fn actual_output_length(&self, uuid: Uuid) -> Option<usize> {
999 self.requests
1000 .get(&uuid)
1001 .map(TraceRequestStats::actual_output_length)
1002 }
1003
1004 pub(crate) fn finish(mut self) -> TraceSimulationReport {
1005 let Self {
1006 requests,
1007 itl_distribution,
1008 output_token_throughput_per_user,
1009 ..
1010 } = &mut self;
1011 for stats in requests.values_mut() {
1012 stats.finalize_token_timeline(
1013 stats.terminal_status == Some(ReplayTerminalStatus::Completed)
1014 && stats.first_admit_ms.is_some(),
1015 itl_distribution,
1016 output_token_throughput_per_user,
1017 );
1018 }
1019
1020 let per_request = if self.capture_per_request {
1026 self.per_request_records()
1027 } else {
1028 Vec::new()
1029 };
1030 let sla = self.sla;
1031 let static_worker_count = self.static_worker_count;
1032 let accumulated_prefill_worker_seconds = self.prefill_worker_seconds;
1033 let accumulated_decode_worker_seconds = self.decode_worker_seconds;
1034 let prefill_gpus_per_worker = self.prefill_gpus_per_worker;
1035 let decode_gpus_per_worker = self.decode_gpus_per_worker;
1036 let itl_distribution = self.itl_distribution.finish();
1037 let output_token_throughput_per_user = self.output_token_throughput_per_user.finish();
1038 let requests = self.requests;
1039 let request_count = requests.len();
1040 let mut ttfts = Vec::with_capacity(request_count);
1041 let mut ttsts = Vec::with_capacity(request_count);
1042 let mut tpots = Vec::with_capacity(request_count);
1043 let mut e2e_latencies = Vec::with_capacity(request_count);
1044 let mut duration_ms = 0.0_f64;
1045 let mut total_input_tokens = 0usize;
1046 let mut total_output_tokens = 0usize;
1047 let mut completed_requests = 0usize;
1048 let mut total_reused_tokens = 0usize;
1049 let mut total_first_admission_reused_tokens = 0usize;
1050 let mut goodput_requests = 0usize;
1052 let mut goodput_output_tokens = 0usize;
1053
1054 for stats in requests.values() {
1055 if stats.first_admit_ms.is_none() {
1056 continue;
1057 }
1058 if stats.terminal_status != Some(ReplayTerminalStatus::Completed) {
1059 continue;
1060 }
1061 let Some(terminal_time_ms) = stats.terminal_time_ms else {
1062 continue;
1063 };
1064
1065 completed_requests += 1;
1066 total_input_tokens += stats.input_length;
1067 let output_length = stats.actual_output_length();
1068 total_output_tokens += output_length;
1069 total_reused_tokens += stats.reused_input_tokens;
1070 total_first_admission_reused_tokens += stats.first_admission_reused_input_tokens;
1071 duration_ms = duration_ms.max(terminal_time_ms);
1072
1073 let (Some(first_token_ms), Some(last_token_ms)) =
1074 (stats.first_token_ms(), stats.last_token_ms())
1075 else {
1076 let e2e_ms = (terminal_time_ms - stats.arrival_time_ms).max(0.0);
1077 if sla.is_set() && sla.is_good_without_tokens(e2e_ms) {
1078 goodput_requests += 1;
1079 }
1080 continue;
1081 };
1082
1083 let ttft_ms = (first_token_ms - stats.arrival_time_ms).max(0.0);
1084 let e2e_ms = (last_token_ms - stats.arrival_time_ms).max(0.0);
1085 ttfts.push(ttft_ms);
1086 e2e_latencies.push(e2e_ms);
1087
1088 if sla.is_set() && sla.is_good(ttft_ms, e2e_ms, output_length) {
1090 goodput_requests += 1;
1091 goodput_output_tokens += output_length;
1092 }
1093
1094 if let Some(ttst_ms) = stats.ttst_ms() {
1095 ttsts.push(ttst_ms);
1096 }
1097
1098 if let Some(tpot_ms) = stats.mean_tpot_ms() {
1099 tpots.push(tpot_ms);
1100 }
1101 }
1102
1103 let duration_s = (duration_ms / 1000.0).max(1e-9);
1104 let (prefill_worker_seconds, decode_worker_seconds) = match static_worker_count {
1107 Some((prefill, decode)) => (prefill as f64 * duration_s, decode as f64 * duration_s),
1108 None => (
1109 accumulated_prefill_worker_seconds,
1110 accumulated_decode_worker_seconds,
1111 ),
1112 };
1113 let gpu_hours = (prefill_worker_seconds * prefill_gpus_per_worker as f64
1116 + decode_worker_seconds * decode_gpus_per_worker as f64)
1117 / 3600.0;
1118 let goodput = sla.is_set().then(|| TraceGoodputStats {
1120 completed_requests: goodput_requests,
1121 request_throughput_rps: goodput_requests as f64 / duration_s,
1122 output_throughput_tok_s: goodput_output_tokens as f64 / duration_s,
1123 });
1124 TraceSimulationReport {
1125 request_counts: TraceRequestCounts {
1126 num_requests: request_count,
1127 completed_requests,
1128 total_input_tokens,
1129 total_output_tokens,
1130 },
1131 throughput: TraceThroughputStats {
1132 duration_ms,
1133 wall_time_ms: 0.0,
1134 request_throughput_rps: completed_requests as f64 / duration_s,
1135 input_throughput_tok_s: total_input_tokens as f64 / duration_s,
1136 output_throughput_tok_s: total_output_tokens as f64 / duration_s,
1137 total_throughput_tok_s: (total_input_tokens + total_output_tokens) as f64
1138 / duration_s,
1139 prefill_worker_seconds,
1140 decode_worker_seconds,
1141 prefill_gpus_per_worker,
1142 decode_gpus_per_worker,
1143 gpu_hours,
1144 },
1145 prefix_cache_reused_ratio: if total_input_tokens == 0 {
1146 0.0
1147 } else {
1148 total_reused_tokens as f64 / total_input_tokens as f64
1149 },
1150 first_admission_prefix_cache_reused_ratio: if total_input_tokens == 0 {
1151 0.0
1152 } else {
1153 total_first_admission_reused_tokens as f64 / total_input_tokens as f64
1154 },
1155 latency: TraceLatencyStats {
1156 ttft: build_distribution_stats(ttfts),
1157 ttst: build_distribution_stats(ttsts),
1158 tpot: build_distribution_stats(tpots),
1159 itl: TraceInterTokenLatencyStats {
1160 max_ms: itl_distribution.max_ms,
1161 distribution: itl_distribution,
1162 },
1163 e2e: build_distribution_stats(e2e_latencies),
1164 output_token_throughput_per_user,
1165 },
1166 goodput,
1167 per_request,
1168 }
1169 }
1170
1171 pub fn per_request_records(&self) -> Vec<PerRequestRecord> {
1178 let mut records = Vec::with_capacity(self.requests.len());
1179 for (uuid, stats) in &self.requests {
1180 let Some(detail) = stats.detail.as_deref() else {
1181 continue;
1182 };
1183 let Some(terminal_status) = stats.terminal_status else {
1184 continue;
1185 };
1186 let Some(terminal_time_ms) = stats.terminal_time_ms else {
1187 continue;
1188 };
1189 let first_token_ms = stats.first_token_ms();
1190 let last_token_ms = stats.last_token_ms();
1191 records.push(PerRequestRecord {
1192 session_id: stats.session_id.clone(),
1193 turn_index: stats.turn_index,
1194 uuid: uuid.to_string(),
1195 arrival_time_ms: stats.arrival_time_ms,
1196 first_admit_ms: stats.first_admit_ms,
1197 terminal_time_ms,
1198 first_token_ms,
1199 last_token_ms,
1200 ttft_ms: first_token_ms.map(|time| (time - stats.arrival_time_ms).max(0.0)),
1201 ttst_ms: stats.ttst_ms(),
1202 e2e_latency_ms: last_token_ms.map(|time| (time - stats.arrival_time_ms).max(0.0)),
1203 itl_ms: stats.mean_tpot_ms(),
1204 input_length: stats.input_length,
1205 requested_output_length: stats.requested_output_length,
1206 output_length: stats.actual_output_length(),
1207 reused_input_tokens: detail
1208 .prefill_reused_input_tokens
1209 .unwrap_or(stats.reused_input_tokens),
1210 prefill_worker_idx: stats.prefill_worker_idx,
1211 decode_worker_idx: stats.decode_worker_idx,
1212 prefill_admit_ms: detail.prefill_admit_ms,
1213 source_held_ms: detail.source_held_ms,
1214 destination_reserved_ms: detail.destination_reserved_ms,
1215 destination_activated_ms: detail.destination_activated_ms,
1216 decode_admit_ms: detail.decode_admit_ms,
1217 source_released_ms: detail.source_released_ms,
1218 decode_reused_input_tokens: detail.decode_reused_input_tokens,
1219 prefill_route_overlap_tokens: detail.prefill_route_overlap_tokens,
1220 decode_route_overlap_tokens: detail.decode_route_overlap_tokens,
1221 terminal_status,
1222 });
1223 }
1224 records.sort_by(|a, b| {
1228 a.arrival_time_ms
1229 .total_cmp(&b.arrival_time_ms)
1230 .then_with(|| a.uuid.cmp(&b.uuid))
1231 });
1232 records
1233 }
1234
1235 #[cfg(test)]
1236 pub(crate) fn snapshot(&self, uuid: Uuid) -> Option<TraceRequestStatsSnapshot> {
1237 self.requests
1238 .get(&uuid)
1239 .map(|stats| TraceRequestStatsSnapshot {
1240 arrival_time_ms: stats.arrival_time_ms,
1241 first_admit_ms: stats.first_admit_ms,
1242 first_token_ms: stats.first_token_ms(),
1243 last_token_ms: stats.last_token_ms(),
1244 input_length: stats.input_length,
1245 requested_output_length: stats.requested_output_length,
1246 output_length: stats.actual_output_length(),
1247 reused_input_tokens: stats.reused_input_tokens,
1248 first_admission_reused_input_tokens: stats.first_admission_reused_input_tokens,
1249 })
1250 }
1251
1252 #[cfg(test)]
1253 pub(crate) fn snapshots(&self) -> Vec<TraceRequestStatsSnapshot> {
1254 self.requests
1255 .values()
1256 .map(|stats| TraceRequestStatsSnapshot {
1257 arrival_time_ms: stats.arrival_time_ms,
1258 first_admit_ms: stats.first_admit_ms,
1259 first_token_ms: stats.first_token_ms(),
1260 last_token_ms: stats.last_token_ms(),
1261 input_length: stats.input_length,
1262 requested_output_length: stats.requested_output_length,
1263 output_length: stats.actual_output_length(),
1264 reused_input_tokens: stats.reused_input_tokens,
1265 first_admission_reused_input_tokens: stats.first_admission_reused_input_tokens,
1266 })
1267 .collect()
1268 }
1269
1270 #[cfg(test)]
1271 fn retained_token_timestamps(&self) -> usize {
1272 self.requests
1273 .values()
1274 .map(|stats| match &stats.token_timeline {
1275 TokenTimeline::Recording(times) => times.len(),
1276 TokenTimeline::Finalized(_) => 0,
1277 })
1278 .sum()
1279 }
1280}
1281
1282fn mean(values: &[f64]) -> f64 {
1283 if values.is_empty() {
1284 0.0
1285 } else {
1286 values.iter().sum::<f64>() / values.len() as f64
1287 }
1288}
1289
1290fn build_distribution_stats(mut values: Vec<f64>) -> TraceDistributionStats {
1291 if values.is_empty() {
1292 return empty_distribution_stats();
1293 }
1294
1295 let min_ms = values
1296 .iter()
1297 .copied()
1298 .min_by(|left, right| left.total_cmp(right))
1299 .expect("non-empty values must have a minimum");
1300 let max_ms = values
1301 .iter()
1302 .copied()
1303 .max_by(|left, right| left.total_cmp(right))
1304 .expect("non-empty values must have a maximum");
1305
1306 TraceDistributionStats {
1307 mean_ms: mean(&values),
1308 min_ms,
1309 max_ms,
1310 median_ms: percentile_in_place(&mut values, 50.0),
1311 p75_ms: percentile_in_place(&mut values, 75.0),
1312 p90_ms: percentile_in_place(&mut values, 90.0),
1313 p95_ms: percentile_in_place(&mut values, 95.0),
1314 p99_ms: percentile_in_place(&mut values, 99.0),
1315 std_ms: std_dev(&values),
1316 }
1317}
1318
1319fn empty_distribution_stats() -> TraceDistributionStats {
1320 TraceDistributionStats {
1321 mean_ms: 0.0,
1322 min_ms: 0.0,
1323 max_ms: 0.0,
1324 median_ms: 0.0,
1325 p75_ms: 0.0,
1326 p90_ms: 0.0,
1327 p95_ms: 0.0,
1328 p99_ms: 0.0,
1329 std_ms: 0.0,
1330 }
1331}
1332
1333fn percentile_in_place(values: &mut [f64], percentile: f64) -> f64 {
1334 let rank = percentile_rank(values.len(), percentile);
1335 let (_, selected, _) = values.select_nth_unstable_by(rank, |left, right| left.total_cmp(right));
1336 *selected
1337}
1338
1339fn percentile_rank(len: usize, percentile: f64) -> usize {
1340 let rank = ((len - 1) as f64 * percentile / 100.0).round() as usize;
1341 rank.min(len - 1)
1342}
1343
1344fn std_dev(values: &[f64]) -> f64 {
1345 if values.is_empty() {
1346 return 0.0;
1347 }
1348
1349 let mean = mean(values);
1350 let variance = values
1351 .iter()
1352 .map(|value| {
1353 let centered = value - mean;
1354 centered * centered
1355 })
1356 .sum::<f64>()
1357 / values.len() as f64;
1358 variance.sqrt()
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363 use super::*;
1364
1365 fn build_distribution_stats_sorted(values: &[f64]) -> TraceDistributionStats {
1366 if values.is_empty() {
1367 return TraceDistributionStats {
1368 mean_ms: 0.0,
1369 min_ms: 0.0,
1370 max_ms: 0.0,
1371 median_ms: 0.0,
1372 p75_ms: 0.0,
1373 p90_ms: 0.0,
1374 p95_ms: 0.0,
1375 p99_ms: 0.0,
1376 std_ms: 0.0,
1377 };
1378 }
1379
1380 let mut sorted = values.to_vec();
1381 sorted.sort_by(|left, right| left.total_cmp(right));
1382 TraceDistributionStats {
1383 mean_ms: mean(values),
1384 min_ms: sorted[0],
1385 max_ms: *sorted.last().expect("sorted values must be non-empty"),
1386 median_ms: sorted[percentile_rank(sorted.len(), 50.0)],
1387 p75_ms: sorted[percentile_rank(sorted.len(), 75.0)],
1388 p90_ms: sorted[percentile_rank(sorted.len(), 90.0)],
1389 p95_ms: sorted[percentile_rank(sorted.len(), 95.0)],
1390 p99_ms: sorted[percentile_rank(sorted.len(), 99.0)],
1391 std_ms: std_dev(values),
1392 }
1393 }
1394
1395 #[test]
1396 fn build_distribution_stats_matches_sorted_baseline() {
1397 let values = vec![
1398 0.0, 1.0, 1.0, 2.5, 4.0, 4.0, 7.25, 9.5, 15.0, 22.0, 22.0, 100.0,
1399 ];
1400
1401 let expected = build_distribution_stats_sorted(&values);
1402 let actual = build_distribution_stats(values);
1403
1404 assert_eq!(actual.mean_ms, expected.mean_ms);
1405 assert_eq!(actual.min_ms, expected.min_ms);
1406 assert_eq!(actual.max_ms, expected.max_ms);
1407 assert_eq!(actual.median_ms, expected.median_ms);
1408 assert_eq!(actual.p75_ms, expected.p75_ms);
1409 assert_eq!(actual.p90_ms, expected.p90_ms);
1410 assert_eq!(actual.p95_ms, expected.p95_ms);
1411 assert_eq!(actual.p99_ms, expected.p99_ms);
1412 assert_eq!(actual.std_ms, expected.std_ms);
1413 }
1414
1415 #[test]
1416 fn built_in_ddsketch_configuration_and_quantiles_are_valid() {
1417 let mut distribution = StreamingDistribution::default();
1418 assert!((distribution.sketch.alpha() - DDSKETCH_RELATIVE_ACCURACY).abs() < f64::EPSILON);
1419 for percentile in [50.0, 75.0, 90.0, 95.0, 99.0] {
1420 assert_eq!(distribution.percentile(percentile), 0.0);
1421 }
1422
1423 for value in [1e-9, 1e18] {
1426 distribution.add(value);
1427 }
1428 for (quantile, expected) in [(0.0, 1e-9), (1.0, 1e18)] {
1429 let actual = distribution.sketch.quantile(quantile).unwrap();
1430 assert!((actual - expected).abs() <= expected * DDSKETCH_RELATIVE_ACCURACY);
1431 }
1432 }
1433
1434 #[test]
1435 fn streaming_distribution_preserves_all_zero_samples() {
1436 let mut distribution = StreamingDistribution::default();
1437 for _ in 0..128 {
1438 distribution.add(0.0);
1439 }
1440
1441 assert_eq!(distribution.sketch.get_zero_count(), 128);
1442 let stats = distribution.finish();
1443 for value in [
1444 stats.mean_ms,
1445 stats.min_ms,
1446 stats.max_ms,
1447 stats.median_ms,
1448 stats.p75_ms,
1449 stats.p90_ms,
1450 stats.p95_ms,
1451 stats.p99_ms,
1452 stats.std_ms,
1453 ] {
1454 assert_eq!(value, 0.0);
1455 }
1456 }
1457
1458 #[test]
1459 fn streaming_percentiles_select_the_historical_rounded_rank() {
1460 let percentiles = [
1461 0.0, 1.0, 10.0, 25.0, 49.0, 50.0, 51.0, 75.0, 90.0, 95.0, 99.0, 100.0,
1462 ];
1463 for len in [2, 3, 4, 5, 10, 11, 100, 101, 256, 257] {
1464 let values = (0..len)
1465 .map(|index| 1_000.0 + index as f64 * 10.0)
1466 .collect::<Vec<_>>();
1467 let mut distribution = StreamingDistribution::default();
1468 for &value in &values {
1469 distribution.add(value);
1470 }
1471
1472 for percentile in percentiles {
1473 let expected = values[percentile_rank(values.len(), percentile)];
1474 let actual = distribution.percentile(percentile);
1475 assert!(
1476 (actual - expected).abs() <= expected * DDSKETCH_RELATIVE_ACCURACY,
1477 "len={len} percentile={percentile}: expected rank value {expected}, got {actual}"
1478 );
1479 }
1480 }
1481 }
1482
1483 #[test]
1484 fn completed_zero_output_request_counts_without_latency_samples() {
1485 let mut collector = TraceCollector::default();
1486 collector.set_static_worker_count(0, 1);
1487 collector.set_gpus_per_worker(0, 4);
1488 let uuid = Uuid::from_u128(99);
1489 collector.on_arrival(uuid, 0.0, 32, 0);
1490 collector.on_admit(uuid, 5.0, 8);
1491 collector.on_terminal(uuid, 25.0, ReplayTerminalStatus::Completed);
1492
1493 let report = collector.finish();
1494
1495 assert_eq!(report.request_counts.completed_requests, 1);
1496 assert_eq!(report.request_counts.total_input_tokens, 32);
1497 assert_eq!(report.request_counts.total_output_tokens, 0);
1498 assert_eq!(report.throughput.duration_ms, 25.0);
1499 assert_eq!(report.throughput.decode_worker_seconds, 0.025);
1500 assert!((report.throughput.gpu_hours - 0.1 / 3600.0).abs() < 1e-12);
1501 assert_eq!(report.latency.ttft.mean_ms, 0.0);
1502 assert_eq!(report.latency.e2e.mean_ms, 0.0);
1503 }
1504
1505 #[test]
1506 fn token_before_simulation_cap_does_not_count_as_completion() {
1507 let mut collector = TraceCollector::default();
1508 let uuid = Uuid::from_u128(100);
1509 collector.on_arrival(uuid, 0.0, 32, 4);
1510 collector.on_admit(uuid, 5.0, 0);
1511 collector.on_token(uuid, 25.0);
1512
1513 let report = collector.finish();
1514
1515 assert_eq!(report.request_counts.completed_requests, 0);
1516 assert_eq!(report.request_counts.total_input_tokens, 0);
1517 assert_eq!(report.request_counts.total_output_tokens, 0);
1518 assert_eq!(report.throughput.duration_ms, 0.0);
1519 }
1520
1521 #[test]
1522 fn zero_output_goodput_requires_e2e_only_sla() {
1523 let collect = |sla| {
1524 let mut collector = TraceCollector::default();
1525 collector.set_sla_thresholds(sla);
1526 let uuid = Uuid::from_u128(101);
1527 collector.on_arrival(uuid, 0.0, 32, 0);
1528 collector.on_admit(uuid, 5.0, 0);
1529 collector.on_terminal(uuid, 100.0, ReplayTerminalStatus::Completed);
1530 collector.finish().goodput.unwrap().completed_requests
1531 };
1532
1533 assert_eq!(
1534 collect(SlaThresholds {
1535 e2e_ms: Some(100.0),
1536 ..Default::default()
1537 }),
1538 1
1539 );
1540 assert_eq!(
1541 collect(SlaThresholds {
1542 ttft_ms: Some(1_000.0),
1543 ..Default::default()
1544 }),
1545 0
1546 );
1547 }
1548
1549 #[test]
1553 fn per_request_disagg_record_populates_all_fields() {
1554 let mut collector = TraceCollector::default();
1555 collector.set_capture_per_request(true);
1556 let uuid = Uuid::from_u128(1);
1557 collector.on_arrival(uuid, 0.0, 100, 4);
1558 collector.on_prefill_route_overlap(uuid, 64);
1559 collector.on_prefill_admit(uuid, 5.0, 30);
1560 collector.on_source_held(uuid, 10.0);
1561 collector.on_destination_reserved(uuid, 12.0);
1562 collector.on_destination_activated(uuid, 20.0);
1563 collector.on_source_released(uuid, 21.0);
1564 collector.on_decode_route_overlap(uuid, 32);
1565 collector.on_decode_admit(uuid, 25.0, 40);
1566 collector.on_prefill_assigned(uuid, 2);
1567 collector.on_decode_assigned(uuid, 7);
1568 collector.on_token(uuid, 50.0);
1569 collector.on_token(uuid, 60.0);
1570 collector.on_token(uuid, 75.0);
1571 collector.on_token(uuid, 95.0);
1572 collector.on_terminal(uuid, 95.0, ReplayTerminalStatus::Completed);
1573
1574 let report = collector.finish();
1575 assert_eq!(report.per_request.len(), 1);
1576 let rec = &report.per_request[0];
1577 assert_eq!(rec.uuid, uuid.to_string());
1578 assert_eq!(rec.arrival_time_ms, 0.0);
1579 assert_eq!(rec.first_admit_ms, Some(5.0));
1580 assert_eq!(rec.terminal_time_ms, 95.0);
1581 assert_eq!(rec.first_token_ms, Some(50.0));
1582 assert_eq!(rec.last_token_ms, Some(95.0));
1583 assert_eq!(rec.ttft_ms, Some(50.0));
1584 assert_eq!(rec.ttst_ms, Some(10.0));
1585 assert_eq!(rec.e2e_latency_ms, Some(95.0));
1586 assert_eq!(rec.itl_ms, Some(15.0));
1588 assert_eq!(rec.input_length, 100);
1589 assert_eq!(rec.output_length, 4);
1590 assert_eq!(rec.reused_input_tokens, 30);
1591 assert_eq!(rec.prefill_worker_idx, Some(2));
1592 assert_eq!(rec.decode_worker_idx, Some(7));
1593 assert_eq!(rec.prefill_admit_ms, Some(5.0));
1594 assert_eq!(rec.source_held_ms, Some(10.0));
1595 assert_eq!(rec.destination_reserved_ms, Some(12.0));
1596 assert_eq!(rec.destination_activated_ms, Some(20.0));
1597 assert_eq!(rec.source_released_ms, Some(21.0));
1598 assert_eq!(rec.decode_admit_ms, Some(25.0));
1599 assert_eq!(rec.decode_reused_input_tokens, Some(40));
1600 assert_eq!(rec.prefill_route_overlap_tokens, Some(64));
1601 assert_eq!(rec.decode_route_overlap_tokens, Some(32));
1602 assert_eq!(rec.terminal_status, ReplayTerminalStatus::Completed);
1603 }
1604
1605 #[test]
1609 fn per_request_bypass_leaves_prefill_worker_idx_none() {
1610 let mut collector = TraceCollector::default();
1611 collector.set_capture_per_request(true);
1612 let uuid = Uuid::from_u128(42);
1613 collector.on_arrival(uuid, 0.0, 100, 2);
1614 collector.on_admit(uuid, 5.0, 0);
1615 collector.on_decode_assigned(uuid, 1);
1617 collector.on_token(uuid, 30.0);
1618 collector.on_token(uuid, 45.0);
1619 collector.on_terminal(uuid, 45.0, ReplayTerminalStatus::Completed);
1620
1621 let report = collector.finish();
1622 assert_eq!(report.per_request.len(), 1);
1623 let rec = &report.per_request[0];
1624 assert!(
1625 rec.prefill_worker_idx.is_none(),
1626 "bypassed request must have prefill_worker_idx = None"
1627 );
1628 assert_eq!(rec.decode_worker_idx, Some(1));
1629 }
1630
1631 #[test]
1634 fn per_request_default_off() {
1635 let mut collector = TraceCollector::default();
1636 let uuid = Uuid::from_u128(1);
1638 collector.on_arrival(uuid, 0.0, 100, 2);
1639 collector.on_admit(uuid, 5.0, 0);
1640 collector.on_decode_assigned(uuid, 0);
1641 collector.on_token(uuid, 50.0);
1642 collector.on_token(uuid, 60.0);
1643 collector.on_terminal(uuid, 60.0, ReplayTerminalStatus::Completed);
1644
1645 assert!(collector.requests[&uuid].detail.is_none());
1646
1647 let report = collector.finish();
1648 assert!(report.per_request.is_empty());
1649 assert_eq!(report.request_counts.completed_requests, 1);
1651 }
1652
1653 fn add_completed(
1656 collector: &mut TraceCollector,
1657 uuid_n: u128,
1658 arrival_ms: f64,
1659 output_length: usize,
1660 token_times_ms: &[f64],
1661 ) {
1662 let uuid = Uuid::from_u128(uuid_n);
1663 collector.on_arrival(uuid, arrival_ms, 100, output_length);
1664 collector.on_admit(uuid, arrival_ms, 0);
1665 collector.on_decode_assigned(uuid, 0);
1666 for &t in token_times_ms {
1667 collector.on_token(uuid, t);
1668 }
1669 let terminal_time_ms = token_times_ms.last().copied().unwrap_or(arrival_ms);
1670 collector.on_terminal(uuid, terminal_time_ms, ReplayTerminalStatus::Completed);
1671 }
1672
1673 #[test]
1677 fn goodput_classifies_by_aiperf_avg_itl() {
1678 let mut collector = TraceCollector::default();
1679 collector.set_sla_thresholds(SlaThresholds {
1680 ttft_ms: Some(150.0),
1681 itl_ms: Some(30.0),
1682 e2e_ms: None,
1683 });
1684 add_completed(&mut collector, 1, 0.0, 3, &[100.0, 150.0, 200.0]);
1686 add_completed(&mut collector, 2, 0.0, 3, &[100.0, 120.0, 140.0]);
1688 add_completed(&mut collector, 3, 0.0, 1, &[100.0]);
1690
1691 let goodput = collector
1692 .finish()
1693 .goodput
1694 .expect("SLA set → goodput present");
1695 assert_eq!(goodput.completed_requests, 2); assert!((goodput.output_throughput_tok_s - 4.0 / 0.2).abs() < 1e-6);
1698 assert!((goodput.request_throughput_rps - 2.0 / 0.2).abs() < 1e-6);
1699 }
1700
1701 #[test]
1703 fn goodput_itl_boundary_is_inclusive() {
1704 let sla = SlaThresholds {
1705 ttft_ms: None,
1706 itl_ms: Some(50.0),
1707 e2e_ms: None,
1708 };
1709 let mut at_bound = TraceCollector::default();
1711 at_bound.set_sla_thresholds(sla);
1712 add_completed(&mut at_bound, 1, 0.0, 3, &[100.0, 150.0, 200.0]);
1713 assert_eq!(at_bound.finish().goodput.unwrap().completed_requests, 1);
1714 let mut over = TraceCollector::default();
1716 over.set_sla_thresholds(sla);
1717 add_completed(&mut over, 1, 0.0, 3, &[100.0, 150.0, 201.0]);
1718 assert_eq!(over.finish().goodput.unwrap().completed_requests, 0);
1719 }
1720
1721 #[test]
1723 fn goodput_e2e_only_sla() {
1724 let mut collector = TraceCollector::default();
1725 collector.set_sla_thresholds(SlaThresholds {
1726 ttft_ms: None,
1727 itl_ms: None,
1728 e2e_ms: Some(150.0),
1729 });
1730 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);
1733 }
1734
1735 #[test]
1737 fn goodput_absent_without_sla() {
1738 let mut collector = TraceCollector::default();
1739 add_completed(&mut collector, 1, 0.0, 2, &[10.0, 20.0]);
1740 assert!(collector.finish().goodput.is_none());
1741 }
1742
1743 #[test]
1746 fn worker_seconds_accumulated_and_static() {
1747 let mut accumulated = TraceCollector::default();
1748 add_completed(&mut accumulated, 1, 0.0, 2, &[10.0, 20.0]);
1749 accumulated.add_worker_seconds(1.5, 4.0);
1750 accumulated.add_worker_seconds(0.5, 1.0);
1751 let report = accumulated.finish();
1752 assert!((report.throughput.prefill_worker_seconds - 2.0).abs() < 1e-9);
1753 assert!((report.throughput.decode_worker_seconds - 5.0).abs() < 1e-9);
1754
1755 let mut static_single = TraceCollector::default();
1756 static_single.set_static_worker_count(0, 1);
1757 add_completed(&mut static_single, 1, 0.0, 2, &[100.0, 200.0]); let report = static_single.finish();
1759 assert!(report.throughput.prefill_worker_seconds.abs() < 1e-9);
1760 assert!((report.throughput.decode_worker_seconds - 0.2).abs() < 1e-9);
1761 }
1762
1763 #[test]
1766 fn gpu_hours_from_worker_seconds_and_gpus_per_worker() {
1767 let mut collector = TraceCollector::default();
1768 collector.set_gpus_per_worker(2, 4); add_completed(&mut collector, 1, 0.0, 2, &[100.0, 200.0]);
1770 collector.add_worker_seconds(10.0, 5.0); let report = collector.finish();
1772 assert_eq!(report.throughput.prefill_gpus_per_worker, 2);
1773 assert_eq!(report.throughput.decode_gpus_per_worker, 4);
1774 assert!((report.throughput.gpu_hours - 40.0 / 3600.0).abs() < 1e-9);
1776 }
1777
1778 #[test]
1781 fn per_request_records_are_sorted_by_arrival_time() {
1782 let mut collector = TraceCollector::default();
1783 collector.set_capture_per_request(true);
1784 for (uuid_n, arrival) in [(3u128, 30.0), (1, 0.0), (2, 10.0)] {
1786 let uuid = Uuid::from_u128(uuid_n);
1787 collector.on_arrival(uuid, arrival, 100, 1);
1788 collector.on_admit(uuid, arrival + 1.0, 0);
1789 collector.on_decode_assigned(uuid, 0);
1790 collector.on_token(uuid, arrival + 5.0);
1791 collector.on_terminal(uuid, arrival + 5.0, ReplayTerminalStatus::Completed);
1792 }
1793 let report = collector.finish();
1794 let arrivals: Vec<f64> = report
1795 .per_request
1796 .iter()
1797 .map(|r| r.arrival_time_ms)
1798 .collect();
1799 assert_eq!(arrivals, vec![0.0, 10.0, 30.0]);
1800 }
1801
1802 #[test]
1806 fn per_request_record_serializes_to_json_object() {
1807 let mut collector = TraceCollector::default();
1808 collector.set_capture_per_request(true);
1809 let uuid = Uuid::from_u128(123);
1810 collector.on_arrival(uuid, 0.0, 50, 2);
1811 collector.on_admit(uuid, 1.0, 10);
1812 collector.on_prefill_assigned(uuid, 0);
1813 collector.on_decode_assigned(uuid, 1);
1814 collector.on_token(uuid, 20.0);
1815 collector.on_token(uuid, 25.0);
1816 collector.on_terminal(uuid, 25.0, ReplayTerminalStatus::Completed);
1817
1818 let report = collector.finish();
1819 let line = serde_json::to_string(&report.per_request[0])
1820 .expect("PerRequestRecord must serialize cleanly");
1821 let parsed: serde_json::Value =
1823 serde_json::from_str(&line).expect("emitted JSON must parse");
1824 assert!(parsed.is_object());
1825 assert_eq!(parsed["uuid"], uuid.to_string());
1826 assert_eq!(parsed["input_length"], 50);
1827 assert_eq!(parsed["output_length"], 2);
1828 assert_eq!(parsed["prefill_worker_idx"], 0);
1829 assert_eq!(parsed["decode_worker_idx"], 1);
1830 assert!(parsed["itl_ms"].is_number());
1831 assert_eq!(parsed["terminal_status"], "completed");
1832 }
1833
1834 #[test]
1835 fn terminal_failures_emit_nullable_latencies_and_unfinished_requests_are_omitted() {
1836 let mut collector = TraceCollector::default();
1837 collector.set_capture_per_request(true);
1838 for (uuid_n, status) in [
1839 (1, ReplayTerminalStatus::Rejected),
1840 (2, ReplayTerminalStatus::Canceled),
1841 (3, ReplayTerminalStatus::Failed),
1842 ] {
1843 let uuid = Uuid::from_u128(uuid_n);
1844 collector.on_arrival(uuid, uuid_n as f64, 64, 2);
1845 collector.on_terminal(uuid, uuid_n as f64 + 1.0, status);
1846 }
1847 collector.on_arrival(Uuid::from_u128(4), 4.0, 64, 2);
1848
1849 let report = collector.finish();
1850
1851 assert_eq!(report.per_request.len(), 3);
1852 assert_eq!(
1853 report
1854 .per_request
1855 .iter()
1856 .map(|record| record.terminal_status)
1857 .collect::<Vec<_>>(),
1858 vec![
1859 ReplayTerminalStatus::Rejected,
1860 ReplayTerminalStatus::Canceled,
1861 ReplayTerminalStatus::Failed,
1862 ]
1863 );
1864 assert!(report.per_request.iter().all(|record| {
1865 record.first_admit_ms.is_none()
1866 && record.first_token_ms.is_none()
1867 && record.last_token_ms.is_none()
1868 && record.ttft_ms.is_none()
1869 && record.e2e_latency_ms.is_none()
1870 }));
1871 }
1872
1873 #[test]
1874 fn first_admission_reuse_ignores_later_readmission_self_reuse() {
1875 let uuid = Uuid::from_u128(1);
1876 let mut collector = TraceCollector::default();
1877 collector.on_arrival(uuid, 0.0, 100, 1);
1878 collector.on_admit(uuid, 1.0, 0);
1879 collector.on_admit(uuid, 2.0, 80);
1880 collector.on_token(uuid, 3.0);
1881 collector.on_terminal(uuid, 3.0, ReplayTerminalStatus::Completed);
1882
1883 let report = collector.finish();
1884
1885 assert_eq!(report.prefix_cache_reused_ratio, 0.8);
1886 assert_eq!(report.first_admission_prefix_cache_reused_ratio, 0.0);
1887 }
1888
1889 #[test]
1890 fn terminal_request_releases_per_token_timestamps() {
1891 let uuid = Uuid::from_u128(7);
1892 let mut collector = TraceCollector::default();
1893 collector.on_arrival(uuid, 0.0, 128, 100_000);
1894 collector.on_admit(uuid, 1.0, 0);
1895 for token_index in 0..100_000 {
1896 collector.on_token(uuid, token_index as f64 + 10.0);
1897 }
1898 assert_eq!(collector.retained_token_timestamps(), 100_000);
1899
1900 collector.on_terminal(uuid, 100_009.0, ReplayTerminalStatus::Completed);
1901
1902 assert_eq!(collector.retained_token_timestamps(), 0);
1903 let snapshot = collector
1904 .snapshot(uuid)
1905 .expect("request must remain summarized");
1906 assert_eq!(snapshot.output_length, 100_000);
1907 assert_eq!(snapshot.first_token_ms, Some(10.0));
1908 assert_eq!(snapshot.last_token_ms, Some(100_009.0));
1909 let report = collector.finish();
1910 assert_eq!(report.latency.itl.distribution.mean_ms, 1.0);
1911 assert_eq!(report.latency.itl.distribution.min_ms, 1.0);
1912 assert_eq!(report.latency.itl.distribution.max_ms, 1.0);
1913 }
1914
1915 #[test]
1916 fn deferred_token_timeline_finalization_folds_at_finish() {
1917 let uuid = Uuid::from_u128(8);
1918 let mut collector = TraceCollector::default();
1919 collector.set_defer_token_timeline_finalization(true);
1920 collector.on_arrival(uuid, 0.0, 128, 3);
1921 collector.on_admit(uuid, 1.0, 0);
1922 collector.on_token(uuid, 10.0);
1923 collector.on_token(uuid, 12.0);
1924 collector.on_token(uuid, 15.0);
1925 collector.on_terminal(uuid, 15.0, ReplayTerminalStatus::Completed);
1926
1927 assert_eq!(collector.retained_token_timestamps(), 3);
1928
1929 let report = collector.finish();
1930 assert_eq!(report.latency.itl.distribution.mean_ms, 2.5);
1931 assert_eq!(report.latency.itl.distribution.min_ms, 2.0);
1932 assert_eq!(report.latency.itl.distribution.max_ms, 3.0);
1933 }
1934}