use std::time::Duration;
use serde::{Deserialize, Serialize};
pub const METRICS_PLUGIN: &str = include_str!("overbrainer_metrics.py");
pub const PLUGIN_FILE: &str = "overbrainer_metrics.py";
pub const PLUGIN_CLASS: &str = "overbrainer_metrics.OverbrainerMetricsPlugin";
pub const METRICS_ENV: &str = "OVERBRAINER_METRICS";
pub const SNAPSHOT_ENV: &str = "OVERBRAINER_SNAPSHOT";
pub const SNAPSHOT_REQUEST: &str = "snapshot.request";
pub const SNAPSHOT_FILE: &str = "snapshot.json";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TrainMetric {
pub time: f64,
pub step: u64,
#[serde(default)]
pub epoch: Option<f64>,
#[serde(default)]
pub max_steps: Option<u64>,
#[serde(default)]
pub loss: Option<f64>,
#[serde(default)]
pub eval_loss: Option<f64>,
#[serde(default)]
pub learning_rate: Option<f64>,
#[serde(default)]
pub grad_norm: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "lowercase")]
pub enum MetricLine {
Begin {
time: f64,
#[serde(default)]
max_steps: Option<u64>,
},
Log(TrainMetric),
Stage {
time: f64,
name: JobStage,
},
Eval {
time: f64,
step: u64,
#[serde(default)]
total: Option<u64>,
},
End {
time: f64,
step: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobStage {
Train,
Merge,
Export,
Compare,
}
impl JobStage {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Train => "train",
Self::Merge => "merge",
Self::Export => "export",
Self::Compare => "compare",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mark {
Stage(JobStage),
Eval {
step: u64,
total: Option<u64>,
},
End {
step: u64,
},
}
impl MetricLine {
#[must_use]
pub const fn mark(&self) -> Option<Mark> {
match *self {
Self::Stage { name, .. } => Some(Mark::Stage(name)),
Self::Eval { step, total, .. } => Some(Mark::Eval { step, total }),
Self::End { step, .. } => Some(Mark::End { step }),
Self::Begin { .. } | Self::Log(_) => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Pace {
first: Option<(u64, f64)>,
last: Option<(u64, f64)>,
step: u64,
max_steps: Option<u64>,
}
impl Pace {
pub fn add(&mut self, metric: &TrainMetric) {
self.step = metric.step;
self.max_steps = metric.max_steps.or(self.max_steps);
if metric.loss.is_some() {
let point = (metric.step, metric.time);
self.first.get_or_insert(point);
self.last = Some(point);
}
}
#[must_use]
pub const fn step(&self) -> u64 {
self.step
}
#[must_use]
pub const fn max_steps(&self) -> Option<u64> {
self.max_steps
}
#[must_use]
pub fn eta(&self) -> Option<Duration> {
let ((first_step, first_time), (last_step, last_time)) = self.first.zip(self.last)?;
if last_step <= first_step || last_time <= first_time {
return None;
}
let rate = float(last_step - first_step) / (last_time - first_time);
let left = float(self.max_steps?.saturating_sub(self.step));
Duration::try_from_secs_f64(left / rate).ok()
}
}
fn float(count: u64) -> f64 {
f64::from(u32::try_from(count).unwrap_or(u32::MAX))
}
pub fn parse_line(line: &str) -> Result<MetricLine, serde_json::Error> {
serde_json::from_str(line)
}
#[cfg(test)]
mod tests {
use super::*;
fn log(time: f64, step: u64, loss: Option<f64>) -> TrainMetric {
TrainMetric {
time,
step,
epoch: None,
max_steps: Some(400),
loss,
eval_loss: loss.is_none().then_some(1.0),
learning_rate: None,
grad_norm: None,
}
}
#[test]
fn the_pace_follows_the_training_logs_only() {
let mut pace = Pace::default();
pace.add(&log(100.0, 10, Some(2.0)));
assert_eq!(pace.eta(), None);
pace.add(&log(150.0, 60, None));
assert_eq!(pace.eta(), None);
pace.add(&log(200.0, 110, Some(1.5)));
assert_eq!((pace.step(), pace.max_steps()), (110, Some(400)));
assert_eq!(pace.eta(), Some(Duration::from_secs(290)));
}
#[test]
fn the_pace_has_no_eta_without_a_step_count() {
let mut pace = Pace::default();
for (time, step) in [(0.0, 1), (10.0, 2)] {
pace.add(&TrainMetric {
max_steps: None,
..log(time, step, Some(1.0))
});
}
assert_eq!(pace.eta(), None);
}
#[test]
fn log_lines_parse_with_missing_values() -> Result<(), serde_json::Error> {
let line = r#"{"event": "log", "time": 1.5, "step": 3, "epoch": 0.25, "max_steps": 12, "loss": 1.2, "grad_norm": null, "extra": 1}"#;
assert_eq!(
parse_line(line)?,
MetricLine::Log(TrainMetric {
time: 1.5,
step: 3,
epoch: Some(0.25),
max_steps: Some(12),
loss: Some(1.2),
eval_loss: None,
learning_rate: None,
grad_norm: None,
})
);
Ok(())
}
#[test]
fn begin_lines_parse() -> Result<(), serde_json::Error> {
assert_eq!(
parse_line(r#"{"event": "begin", "time": 1.0, "max_steps": 40}"#)?,
MetricLine::Begin {
time: 1.0,
max_steps: Some(40)
}
);
Ok(())
}
#[test]
fn stage_eval_and_end_lines_parse_to_marks() -> Result<(), serde_json::Error> {
let marks = [
r#"{"event":"stage","name":"train","time":1700000000}"#,
r#"{"event":"stage","name":"merge","time":1700000000}"#,
r#"{"event":"stage","name":"export","time":1700000000}"#,
r#"{"event": "eval", "step": 340, "total": 1200, "time": 1.5}"#,
r#"{"event": "eval", "step": 3, "time": 1.5}"#,
r#"{"event": "end", "step": 7206, "time": 2.0}"#,
]
.map(|line| parse_line(line).map(|line| line.mark()));
let marks: Vec<Option<Mark>> = marks.into_iter().collect::<Result<_, _>>()?;
assert_eq!(
marks,
[
Some(Mark::Stage(JobStage::Train)),
Some(Mark::Stage(JobStage::Merge)),
Some(Mark::Stage(JobStage::Export)),
Some(Mark::Eval {
step: 340,
total: Some(1200)
}),
Some(Mark::Eval {
step: 3,
total: None
}),
Some(Mark::End { step: 7206 }),
]
);
assert_eq!(
parse_line(r#"{"event": "begin", "time": 1.0}"#)?.mark(),
None
);
Ok(())
}
#[test]
fn other_lines_are_errors() {
assert!(parse_line(r#"{"event": "other"}"#).is_err());
assert!(parse_line(r#"{"event": "stage", "name": "deploy", "time": 1}"#).is_err());
assert!(parse_line("not json").is_err());
}
}