1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum InferencePhase {
24 DataLoad,
26 TapeBuild,
29 Forward,
32 DeviceTransfer,
36}
37
38pub(crate) trait PhaseRecorder {
43 fn record<T>(&mut self, phase: InferencePhase, f: impl FnOnce() -> T) -> T;
45}
46
47pub(crate) struct NoopPhaseRecorder;
52
53impl PhaseRecorder for NoopPhaseRecorder {
54 #[inline]
55 fn record<T>(&mut self, _phase: InferencePhase, f: impl FnOnce() -> T) -> T {
56 f()
57 }
58}
59
60pub use recorded::{
66 PhaseMetrics, PhaseStat, PredictBatchInput, get_phase_metrics, reset_phase_metrics,
67};
68pub(crate) use recorded::{TimingPhaseRecorder, merge_inference_phase_stats};
69
70mod recorded {
72 use std::cell::Cell;
73 use std::time::{Duration, Instant};
74
75 use super::{InferencePhase, PhaseRecorder};
76 use crate::Tensor;
77
78 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
83 pub struct PhaseStat {
84 total: Duration,
85 calls: u64,
86 }
87
88 impl PhaseStat {
89 fn add(&mut self, elapsed: Duration) {
92 self.total = self.total.saturating_add(elapsed);
93 self.calls = self.calls.saturating_add(1);
94 }
95
96 pub fn total_micros(&self) -> u128 {
99 self.total.as_micros()
100 }
101
102 pub fn calls(&self) -> u64 {
104 self.calls
105 }
106
107 pub fn total(&self) -> Duration {
110 self.total
111 }
112
113 fn saturating_sub(&self, before: &Self) -> Self {
115 Self {
116 total: self.total.saturating_sub(before.total),
117 calls: self.calls.saturating_sub(before.calls),
118 }
119 }
120
121 fn merged(&self, other: &Self) -> Self {
124 Self {
125 total: self.total.saturating_add(other.total),
126 calls: self.calls.saturating_add(other.calls),
127 }
128 }
129 }
130
131 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
173 #[non_exhaustive]
174 pub struct PhaseMetrics {
175 data_load: PhaseStat,
176 tape_build: PhaseStat,
177 forward: PhaseStat,
178 device_transfer: PhaseStat,
179 total: PhaseStat,
183 batches: u64,
184 samples: u64,
185 }
186
187 impl PhaseMetrics {
188 fn phase_mut(&mut self, phase: InferencePhase) -> &mut PhaseStat {
189 match phase {
190 InferencePhase::DataLoad => &mut self.data_load,
191 InferencePhase::TapeBuild => &mut self.tape_build,
192 InferencePhase::Forward => &mut self.forward,
193 InferencePhase::DeviceTransfer => &mut self.device_transfer,
194 }
195 }
196
197 pub fn phase(&self, phase: InferencePhase) -> PhaseStat {
199 match phase {
200 InferencePhase::DataLoad => self.data_load,
201 InferencePhase::TapeBuild => self.tape_build,
202 InferencePhase::Forward => self.forward,
203 InferencePhase::DeviceTransfer => self.device_transfer,
204 }
205 }
206
207 pub fn total(&self) -> PhaseStat {
209 self.total
210 }
211
212 pub fn batches(&self) -> u64 {
214 self.batches
215 }
216
217 pub fn samples(&self) -> u64 {
219 self.samples
220 }
221
222 fn merge(&mut self, other: &Self) {
226 self.data_load = self.data_load.merged(&other.data_load);
227 self.tape_build = self.tape_build.merged(&other.tape_build);
228 self.forward = self.forward.merged(&other.forward);
229 self.device_transfer = self.device_transfer.merged(&other.device_transfer);
230 self.total = self.total.merged(&other.total);
231 self.batches = self.batches.saturating_add(other.batches);
232 self.samples = self.samples.saturating_add(other.samples);
233 }
234
235 pub fn since(&self, before: &Self) -> Self {
238 Self {
239 data_load: self.data_load.saturating_sub(&before.data_load),
240 tape_build: self.tape_build.saturating_sub(&before.tape_build),
241 forward: self.forward.saturating_sub(&before.forward),
242 device_transfer: self.device_transfer.saturating_sub(&before.device_transfer),
243 total: self.total.saturating_sub(&before.total),
244 batches: self.batches.saturating_sub(before.batches),
245 samples: self.samples.saturating_sub(before.samples),
246 }
247 }
248
249 fn record_batch(&mut self, samples: u64) {
250 self.batches = self.batches.saturating_add(1);
251 self.samples = self.samples.saturating_add(samples);
252 }
253 }
254
255 #[derive(Debug, Clone, Copy, Default)]
258 pub(crate) struct TimingPhaseRecorder {
259 metrics: PhaseMetrics,
260 }
261
262 impl TimingPhaseRecorder {
263 pub(crate) fn into_metrics(self) -> PhaseMetrics {
264 self.metrics
265 }
266
267 pub(crate) fn record_batch(&mut self, samples: u64) {
270 self.metrics.record_batch(samples);
271 }
272
273 pub(crate) fn record_total(&mut self, elapsed: Duration) {
275 self.metrics.total.add(elapsed);
276 }
277 }
278
279 impl PhaseRecorder for TimingPhaseRecorder {
280 fn record<T>(&mut self, phase: InferencePhase, f: impl FnOnce() -> T) -> T {
281 let start = Instant::now();
282 let out = f();
283 self.metrics.phase_mut(phase).add(start.elapsed());
284 out
285 }
286 }
287
288 thread_local! {
289 static INFERENCE_PHASE_METRICS: Cell<PhaseMetrics> =
295 Cell::new(PhaseMetrics::default());
296 }
297
298 pub fn get_phase_metrics() -> PhaseMetrics {
303 INFERENCE_PHASE_METRICS.with(|cell| cell.get())
304 }
305
306 pub fn reset_phase_metrics() {
309 INFERENCE_PHASE_METRICS.with(|cell| cell.set(PhaseMetrics::default()));
310 }
311
312 pub(crate) fn merge_inference_phase_stats(delta: &PhaseMetrics) {
316 INFERENCE_PHASE_METRICS.with(|cell| {
317 let mut current = cell.get();
318 current.merge(delta);
319 cell.set(current);
320 });
321 }
322
323 mod sealed {
327 pub trait Sealed {}
328 }
329
330 pub trait PredictBatchInput: sealed::Sealed {
340 fn inference_input(&self) -> &Tensor<f32>;
342 }
343
344 impl sealed::Sealed for Tensor<f32> {}
345 impl<B> sealed::Sealed for (Tensor<f32>, B) {}
346 impl<B, C> sealed::Sealed for (Tensor<f32>, B, C) {}
347
348 impl PredictBatchInput for Tensor<f32> {
349 fn inference_input(&self) -> &Tensor<f32> {
350 self
351 }
352 }
353
354 impl<B> PredictBatchInput for (Tensor<f32>, B) {
355 fn inference_input(&self) -> &Tensor<f32> {
356 &self.0
357 }
358 }
359
360 impl<B, C> PredictBatchInput for (Tensor<f32>, B, C) {
361 fn inference_input(&self) -> &Tensor<f32> {
362 &self.0
363 }
364 }
365
366 #[cfg(test)]
367 mod tests {
368 use super::*;
369
370 #[test]
371 fn phase_stat_add_accumulates_total_and_calls() {
372 let mut stat = PhaseStat::default();
373 stat.add(Duration::from_micros(10));
374 stat.add(Duration::from_micros(5));
375 assert_eq!(stat.total_micros(), 15);
376 assert_eq!(stat.calls(), 2);
377 }
378
379 #[test]
380 fn phase_stat_total_matches_total_micros() {
381 let mut stat = PhaseStat::default();
382 stat.add(Duration::from_micros(1500));
383 assert_eq!(stat.total(), Duration::from_micros(1500));
384 assert_eq!(stat.total().as_micros(), stat.total_micros());
385 }
386
387 #[test]
388 fn phase_stat_add_saturates_instead_of_panicking() {
389 let mut stat = PhaseStat {
390 total: Duration::MAX,
391 calls: u64::MAX,
392 };
393 stat.add(Duration::from_secs(1));
394 assert_eq!(stat.total, Duration::MAX);
395 assert_eq!(stat.calls, u64::MAX);
396 }
397
398 #[test]
399 fn phase_stat_saturating_sub_never_underflows() {
400 let before = PhaseStat {
401 total: Duration::from_micros(100),
402 calls: 10,
403 };
404 let after = PhaseStat {
405 total: Duration::from_micros(50),
406 calls: 3,
407 };
408 let delta = after.saturating_sub(&before);
409 assert_eq!(delta.total, Duration::ZERO);
410 assert_eq!(delta.calls, 0);
411 }
412
413 #[test]
414 fn phase_metrics_since_returns_only_the_later_delta() {
415 let mut a = PhaseMetrics::default();
416 a.phase_mut(InferencePhase::Forward)
417 .add(Duration::from_micros(10));
418 a.record_batch(4);
419
420 let mut b = a;
421 b.phase_mut(InferencePhase::Forward)
422 .add(Duration::from_micros(7));
423 b.record_batch(4);
424
425 let delta = b.since(&a);
426 assert_eq!(delta.phase(InferencePhase::Forward).calls(), 1);
427 assert_eq!(delta.phase(InferencePhase::Forward).total_micros(), 7);
428 assert_eq!(delta.batches(), 1);
429 assert_eq!(delta.samples(), 4);
430 }
431
432 #[test]
433 fn merge_accumulates_across_multiple_deltas() {
434 let mut acc = PhaseMetrics::default();
435 let mut delta1 = PhaseMetrics::default();
436 delta1
437 .phase_mut(InferencePhase::DataLoad)
438 .add(Duration::from_micros(3));
439 delta1.record_batch(2);
440 let mut delta2 = PhaseMetrics::default();
441 delta2
442 .phase_mut(InferencePhase::DataLoad)
443 .add(Duration::from_micros(4));
444 delta2.record_batch(3);
445
446 acc.merge(&delta1);
447 acc.merge(&delta2);
448
449 assert_eq!(acc.phase(InferencePhase::DataLoad).calls(), 2);
450 assert_eq!(acc.phase(InferencePhase::DataLoad).total_micros(), 7);
451 assert_eq!(acc.batches(), 2);
452 assert_eq!(acc.samples(), 5);
453 }
454
455 #[test]
456 fn thread_local_snapshot_is_isolated_per_thread() {
457 reset_phase_metrics();
458 let mut delta = PhaseMetrics::default();
459 delta
460 .phase_mut(InferencePhase::Forward)
461 .add(Duration::from_micros(1));
462 delta.record_batch(1);
463 merge_inference_phase_stats(&delta);
464
465 let main_snapshot = get_phase_metrics();
466 assert_eq!(main_snapshot.batches(), 1);
467
468 let other_thread_snapshot = std::thread::spawn(get_phase_metrics).join().unwrap();
469 assert_eq!(other_thread_snapshot.batches(), 0);
472
473 reset_phase_metrics();
474 assert_eq!(get_phase_metrics().batches(), 0);
475 }
476
477 #[test]
478 fn predict_batch_input_ignores_labels() {
479 let x = Tensor::<f32>::new(vec![1.0, 2.0], &[2]).unwrap();
480 let y = 42i32;
481 let pair = (x.clone(), y);
482 assert_eq!(pair.inference_input().shape(), x.shape());
483 let triple = (x.clone(), y, "ignored");
484 assert_eq!(triple.inference_input().shape(), x.shape());
485 assert_eq!(x.inference_input().shape(), x.shape());
486 }
487 }
488}