1use serde::Serialize;
12
13#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ReplayTelemetrySampleKind {
17 Baseline,
20 Periodic,
22 Final,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize)]
29pub struct ReplaySchedulerMetricsSnapshot {
30 pub worker_id: usize,
31 pub dp_rank: u32,
32 pub active_blocks: u64,
35 pub inactive_blocks: u64,
38 pub total_blocks: u64,
39 pub active_cache_usage: f64,
41 pub physical_cache_usage: f64,
43 pub running_requests: u64,
44 pub waiting_requests: u64,
45}
46
47#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize)]
52pub struct ReplaySchedulerIntervalMetrics {
53 pub cache_hit_tokens: u64,
56 pub cache_total_tokens: u64,
59 pub preemptions: u64,
61}
62
63impl ReplaySchedulerIntervalMetrics {
64 pub(crate) const fn has_observations(&self) -> bool {
65 self.cache_hit_tokens != 0 || self.cache_total_tokens != 0 || self.preemptions != 0
66 }
67
68 pub(crate) fn checked_add_assign(&mut self, other: Self) -> anyhow::Result<()> {
69 self.cache_hit_tokens = self
70 .cache_hit_tokens
71 .checked_add(other.cache_hit_tokens)
72 .ok_or_else(|| anyhow::anyhow!("scheduler cache-hit token counter overflow"))?;
73 self.cache_total_tokens = self
74 .cache_total_tokens
75 .checked_add(other.cache_total_tokens)
76 .ok_or_else(|| anyhow::anyhow!("scheduler cache-total token counter overflow"))?;
77 self.preemptions = self
78 .preemptions
79 .checked_add(other.preemptions)
80 .ok_or_else(|| anyhow::anyhow!("scheduler preemption counter overflow"))?;
81 Ok(())
82 }
83}
84
85#[derive(Debug, Clone, Default, PartialEq, Serialize)]
87pub struct ReplayTrafficMetricsSnapshot {
88 pub duration_s: f64,
89 pub arriving_requests: usize,
91 pub completed_requests: usize,
93 pub avg_isl: f64,
95 pub avg_osl: f64,
96 pub avg_ttft_ms: f64,
97 pub avg_itl_ms: f64,
98 pub ttft_count: usize,
99 pub itl_count: usize,
100 pub avg_router_kv_hit_rate: f64,
102 pub router_kv_hit_rate_count: usize,
103 pub avg_accept_length: Option<f64>,
104 pub accept_length_forward_count: usize,
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize)]
109pub struct ReplayTelemetrySnapshot {
110 pub sample_ordinal: u64,
111 pub kind: ReplayTelemetrySampleKind,
112 pub interval_start_ms: f64,
114 pub sampled_at_ms: f64,
116 pub traffic: ReplayTrafficMetricsSnapshot,
117 pub prefill_scheduler_metrics: Vec<ReplaySchedulerMetricsSnapshot>,
120 pub decode_scheduler_metrics: Vec<ReplaySchedulerMetricsSnapshot>,
121 pub prefill_interval_metrics: ReplaySchedulerIntervalMetrics,
122 pub decode_interval_metrics: ReplaySchedulerIntervalMetrics,
123 pub router_pending_prefill_requests: usize,
125 pub router_pending_decode_requests: usize,
126 pub active_prefill_ids: Vec<usize>,
127 pub active_decode_ids: Vec<usize>,
128 pub starting_prefill_ids: Vec<usize>,
129 pub starting_decode_ids: Vec<usize>,
130 pub draining_prefill_ids: Vec<usize>,
131 pub draining_decode_ids: Vec<usize>,
132}
133
134pub trait ReplayTelemetryObserver: Send {
139 fn on_sample(&mut self, snapshot: ReplayTelemetrySnapshot) -> anyhow::Result<()>;
140}
141
142pub(crate) struct ReplayTelemetryRuntime {
143 observer: Box<dyn ReplayTelemetryObserver>,
144 sample_interval_ms: f64,
145 next_sample_ordinal: u64,
146 sampling_origin_ms: Option<f64>,
147 interval_start_ms: f64,
148}
149
150impl ReplayTelemetryRuntime {
151 pub(crate) fn new(sample_interval_ms: f64, observer: Box<dyn ReplayTelemetryObserver>) -> Self {
152 Self {
153 observer,
154 sample_interval_ms,
155 next_sample_ordinal: 0,
156 sampling_origin_ms: None,
157 interval_start_ms: 0.0,
158 }
159 }
160
161 pub(crate) const fn next_sample_ordinal(&self) -> u64 {
162 self.next_sample_ordinal
163 }
164
165 pub(crate) const fn interval_start_ms(&self) -> f64 {
166 self.interval_start_ms
167 }
168
169 pub(crate) fn start_at(&mut self, now_ms: f64) {
170 self.sampling_origin_ms = Some(now_ms);
171 self.interval_start_ms = now_ms;
172 }
173
174 pub(crate) fn next_periodic_at_ms(&self) -> anyhow::Result<f64> {
177 let origin_ms = self
178 .sampling_origin_ms
179 .ok_or_else(|| anyhow::anyhow!("replay telemetry baseline was not initialized"))?;
180 let at_ms = origin_ms + self.next_sample_ordinal as f64 * self.sample_interval_ms;
181 if !at_ms.is_finite() {
182 return Err(anyhow::anyhow!(
183 "replay telemetry sample timestamp overflow"
184 ));
185 }
186 if at_ms <= self.interval_start_ms {
187 return Err(anyhow::anyhow!(
188 "replay telemetry cadence is below virtual-clock precision at {} ms",
189 self.interval_start_ms
190 ));
191 }
192 Ok(at_ms)
193 }
194
195 pub(crate) fn publish(&mut self, snapshot: ReplayTelemetrySnapshot) -> anyhow::Result<()> {
196 self.observer.on_sample(snapshot)?;
197 self.next_sample_ordinal = self
198 .next_sample_ordinal
199 .checked_add(1)
200 .ok_or_else(|| anyhow::anyhow!("replay telemetry sample ordinal overflow"))?;
201 Ok(())
202 }
203
204 pub(crate) fn close_interval(&mut self, sampled_at_ms: f64) {
205 self.interval_start_ms = sampled_at_ms;
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 struct NoopObserver;
214
215 impl ReplayTelemetryObserver for NoopObserver {
216 fn on_sample(&mut self, _snapshot: ReplayTelemetrySnapshot) -> anyhow::Result<()> {
217 Ok(())
218 }
219 }
220
221 fn sample(
222 sample_ordinal: u64,
223 kind: ReplayTelemetrySampleKind,
224 interval_start_ms: f64,
225 sampled_at_ms: f64,
226 ) -> ReplayTelemetrySnapshot {
227 ReplayTelemetrySnapshot {
228 sample_ordinal,
229 kind,
230 interval_start_ms,
231 sampled_at_ms,
232 traffic: ReplayTrafficMetricsSnapshot::default(),
233 prefill_scheduler_metrics: Vec::new(),
234 decode_scheduler_metrics: Vec::new(),
235 prefill_interval_metrics: ReplaySchedulerIntervalMetrics::default(),
236 decode_interval_metrics: ReplaySchedulerIntervalMetrics::default(),
237 router_pending_prefill_requests: 0,
238 router_pending_decode_requests: 0,
239 active_prefill_ids: Vec::new(),
240 active_decode_ids: Vec::new(),
241 starting_prefill_ids: Vec::new(),
242 starting_decode_ids: Vec::new(),
243 draining_prefill_ids: Vec::new(),
244 draining_decode_ids: Vec::new(),
245 }
246 }
247
248 #[test]
249 fn submillisecond_cadence_stays_anchored_to_the_sampling_origin() {
250 let mut runtime = ReplayTelemetryRuntime::new(0.1, Box::new(NoopObserver));
251 runtime.start_at(0.0);
252 runtime
253 .publish(sample(0, ReplayTelemetrySampleKind::Baseline, 0.0, 0.0))
254 .unwrap();
255
256 for ordinal in 1..=10_000 {
257 let at_ms = runtime.next_periodic_at_ms().unwrap();
258 assert_eq!(at_ms, ordinal as f64 * 0.1);
259 runtime
260 .publish(sample(
261 ordinal,
262 ReplayTelemetrySampleKind::Periodic,
263 runtime.interval_start_ms(),
264 at_ms,
265 ))
266 .unwrap();
267 runtime.close_interval(at_ms);
268 }
269 }
270
271 #[test]
272 fn cadence_rejects_intervals_below_virtual_clock_precision() {
273 let origin_ms = 1.0e20;
274 let mut runtime = ReplayTelemetryRuntime::new(0.1, Box::new(NoopObserver));
275 runtime.start_at(origin_ms);
276 runtime
277 .publish(sample(
278 0,
279 ReplayTelemetrySampleKind::Baseline,
280 origin_ms,
281 origin_ms,
282 ))
283 .unwrap();
284
285 let error = runtime.next_periodic_at_ms().unwrap_err();
286 assert!(error.to_string().contains("below virtual-clock precision"));
287 }
288}