#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InferencePhase {
DataLoad,
TapeBuild,
Forward,
DeviceTransfer,
}
pub(crate) trait PhaseRecorder {
fn record<T>(&mut self, phase: InferencePhase, f: impl FnOnce() -> T) -> T;
}
pub(crate) struct NoopPhaseRecorder;
impl PhaseRecorder for NoopPhaseRecorder {
#[inline]
fn record<T>(&mut self, _phase: InferencePhase, f: impl FnOnce() -> T) -> T {
f()
}
}
pub use recorded::{
PhaseMetrics, PhaseStat, PredictBatchInput, get_phase_metrics, reset_phase_metrics,
};
pub(crate) use recorded::{TimingPhaseRecorder, merge_inference_phase_stats};
mod recorded {
use std::cell::Cell;
use std::time::{Duration, Instant};
use super::{InferencePhase, PhaseRecorder};
use crate::Tensor;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PhaseStat {
total: Duration,
calls: u64,
}
impl PhaseStat {
fn add(&mut self, elapsed: Duration) {
self.total = self.total.saturating_add(elapsed);
self.calls = self.calls.saturating_add(1);
}
pub fn total_micros(&self) -> u128 {
self.total.as_micros()
}
pub fn calls(&self) -> u64 {
self.calls
}
pub fn total(&self) -> Duration {
self.total
}
fn saturating_sub(&self, before: &Self) -> Self {
Self {
total: self.total.saturating_sub(before.total),
calls: self.calls.saturating_sub(before.calls),
}
}
fn merged(&self, other: &Self) -> Self {
Self {
total: self.total.saturating_add(other.total),
calls: self.calls.saturating_add(other.calls),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PhaseMetrics {
data_load: PhaseStat,
tape_build: PhaseStat,
forward: PhaseStat,
device_transfer: PhaseStat,
total: PhaseStat,
batches: u64,
samples: u64,
}
impl PhaseMetrics {
fn phase_mut(&mut self, phase: InferencePhase) -> &mut PhaseStat {
match phase {
InferencePhase::DataLoad => &mut self.data_load,
InferencePhase::TapeBuild => &mut self.tape_build,
InferencePhase::Forward => &mut self.forward,
InferencePhase::DeviceTransfer => &mut self.device_transfer,
}
}
pub fn phase(&self, phase: InferencePhase) -> PhaseStat {
match phase {
InferencePhase::DataLoad => self.data_load,
InferencePhase::TapeBuild => self.tape_build,
InferencePhase::Forward => self.forward,
InferencePhase::DeviceTransfer => self.device_transfer,
}
}
pub fn total(&self) -> PhaseStat {
self.total
}
pub fn batches(&self) -> u64 {
self.batches
}
pub fn samples(&self) -> u64 {
self.samples
}
fn merge(&mut self, other: &Self) {
self.data_load = self.data_load.merged(&other.data_load);
self.tape_build = self.tape_build.merged(&other.tape_build);
self.forward = self.forward.merged(&other.forward);
self.device_transfer = self.device_transfer.merged(&other.device_transfer);
self.total = self.total.merged(&other.total);
self.batches = self.batches.saturating_add(other.batches);
self.samples = self.samples.saturating_add(other.samples);
}
pub fn since(&self, before: &Self) -> Self {
Self {
data_load: self.data_load.saturating_sub(&before.data_load),
tape_build: self.tape_build.saturating_sub(&before.tape_build),
forward: self.forward.saturating_sub(&before.forward),
device_transfer: self.device_transfer.saturating_sub(&before.device_transfer),
total: self.total.saturating_sub(&before.total),
batches: self.batches.saturating_sub(before.batches),
samples: self.samples.saturating_sub(before.samples),
}
}
fn record_batch(&mut self, samples: u64) {
self.batches = self.batches.saturating_add(1);
self.samples = self.samples.saturating_add(samples);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct TimingPhaseRecorder {
metrics: PhaseMetrics,
}
impl TimingPhaseRecorder {
pub(crate) fn into_metrics(self) -> PhaseMetrics {
self.metrics
}
pub(crate) fn record_batch(&mut self, samples: u64) {
self.metrics.record_batch(samples);
}
pub(crate) fn record_total(&mut self, elapsed: Duration) {
self.metrics.total.add(elapsed);
}
}
impl PhaseRecorder for TimingPhaseRecorder {
fn record<T>(&mut self, phase: InferencePhase, f: impl FnOnce() -> T) -> T {
let start = Instant::now();
let out = f();
self.metrics.phase_mut(phase).add(start.elapsed());
out
}
}
thread_local! {
static INFERENCE_PHASE_METRICS: Cell<PhaseMetrics> =
Cell::new(PhaseMetrics::default());
}
pub fn get_phase_metrics() -> PhaseMetrics {
INFERENCE_PHASE_METRICS.with(|cell| cell.get())
}
pub fn reset_phase_metrics() {
INFERENCE_PHASE_METRICS.with(|cell| cell.set(PhaseMetrics::default()));
}
pub(crate) fn merge_inference_phase_stats(delta: &PhaseMetrics) {
INFERENCE_PHASE_METRICS.with(|cell| {
let mut current = cell.get();
current.merge(delta);
cell.set(current);
});
}
mod sealed {
pub trait Sealed {}
}
pub trait PredictBatchInput: sealed::Sealed {
fn inference_input(&self) -> &Tensor<f32>;
}
impl sealed::Sealed for Tensor<f32> {}
impl<B> sealed::Sealed for (Tensor<f32>, B) {}
impl<B, C> sealed::Sealed for (Tensor<f32>, B, C) {}
impl PredictBatchInput for Tensor<f32> {
fn inference_input(&self) -> &Tensor<f32> {
self
}
}
impl<B> PredictBatchInput for (Tensor<f32>, B) {
fn inference_input(&self) -> &Tensor<f32> {
&self.0
}
}
impl<B, C> PredictBatchInput for (Tensor<f32>, B, C) {
fn inference_input(&self) -> &Tensor<f32> {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phase_stat_add_accumulates_total_and_calls() {
let mut stat = PhaseStat::default();
stat.add(Duration::from_micros(10));
stat.add(Duration::from_micros(5));
assert_eq!(stat.total_micros(), 15);
assert_eq!(stat.calls(), 2);
}
#[test]
fn phase_stat_total_matches_total_micros() {
let mut stat = PhaseStat::default();
stat.add(Duration::from_micros(1500));
assert_eq!(stat.total(), Duration::from_micros(1500));
assert_eq!(stat.total().as_micros(), stat.total_micros());
}
#[test]
fn phase_stat_add_saturates_instead_of_panicking() {
let mut stat = PhaseStat {
total: Duration::MAX,
calls: u64::MAX,
};
stat.add(Duration::from_secs(1));
assert_eq!(stat.total, Duration::MAX);
assert_eq!(stat.calls, u64::MAX);
}
#[test]
fn phase_stat_saturating_sub_never_underflows() {
let before = PhaseStat {
total: Duration::from_micros(100),
calls: 10,
};
let after = PhaseStat {
total: Duration::from_micros(50),
calls: 3,
};
let delta = after.saturating_sub(&before);
assert_eq!(delta.total, Duration::ZERO);
assert_eq!(delta.calls, 0);
}
#[test]
fn phase_metrics_since_returns_only_the_later_delta() {
let mut a = PhaseMetrics::default();
a.phase_mut(InferencePhase::Forward)
.add(Duration::from_micros(10));
a.record_batch(4);
let mut b = a;
b.phase_mut(InferencePhase::Forward)
.add(Duration::from_micros(7));
b.record_batch(4);
let delta = b.since(&a);
assert_eq!(delta.phase(InferencePhase::Forward).calls(), 1);
assert_eq!(delta.phase(InferencePhase::Forward).total_micros(), 7);
assert_eq!(delta.batches(), 1);
assert_eq!(delta.samples(), 4);
}
#[test]
fn merge_accumulates_across_multiple_deltas() {
let mut acc = PhaseMetrics::default();
let mut delta1 = PhaseMetrics::default();
delta1
.phase_mut(InferencePhase::DataLoad)
.add(Duration::from_micros(3));
delta1.record_batch(2);
let mut delta2 = PhaseMetrics::default();
delta2
.phase_mut(InferencePhase::DataLoad)
.add(Duration::from_micros(4));
delta2.record_batch(3);
acc.merge(&delta1);
acc.merge(&delta2);
assert_eq!(acc.phase(InferencePhase::DataLoad).calls(), 2);
assert_eq!(acc.phase(InferencePhase::DataLoad).total_micros(), 7);
assert_eq!(acc.batches(), 2);
assert_eq!(acc.samples(), 5);
}
#[test]
fn thread_local_snapshot_is_isolated_per_thread() {
reset_phase_metrics();
let mut delta = PhaseMetrics::default();
delta
.phase_mut(InferencePhase::Forward)
.add(Duration::from_micros(1));
delta.record_batch(1);
merge_inference_phase_stats(&delta);
let main_snapshot = get_phase_metrics();
assert_eq!(main_snapshot.batches(), 1);
let other_thread_snapshot = std::thread::spawn(get_phase_metrics).join().unwrap();
assert_eq!(other_thread_snapshot.batches(), 0);
reset_phase_metrics();
assert_eq!(get_phase_metrics().batches(), 0);
}
#[test]
fn predict_batch_input_ignores_labels() {
let x = Tensor::<f32>::new(vec![1.0, 2.0], &[2]).unwrap();
let y = 42i32;
let pair = (x.clone(), y);
assert_eq!(pair.inference_input().shape(), x.shape());
let triple = (x.clone(), y, "ignored");
assert_eq!(triple.inference_input().shape(), x.shape());
assert_eq!(x.inference_input().shape(), x.shape());
}
}
}