use super::metrics::{JobStage, Mark, MetricLine, TrainMetric};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Training,
Evaluating {
step: u64,
total: Option<u64>,
last: bool,
},
Finalizing,
Merging,
Exporting,
Comparing {
step: u64,
total: Option<u64>,
},
Retrieving,
}
impl Phase {
#[must_use]
pub fn label(self) -> String {
match self {
Self::Training => "training".to_string(),
Self::Evaluating { step, total, last } => {
let done = match total {
Some(total) => format!("{step}/{total}"),
None => step.to_string(),
};
if last {
format!("finalizing (evaluation {done})")
} else {
format!("evaluating {done}")
}
},
Self::Finalizing => "finalizing (saving model)".to_string(),
Self::Merging => "merging adapter".to_string(),
Self::Exporting => "exporting GGUF".to_string(),
Self::Comparing { total: None, .. } => "starting llama-server".to_string(),
Self::Comparing {
step,
total: Some(total),
} => format!("answering {step}/{total}"),
Self::Retrieving => "retrieving results".to_string(),
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Training => "training",
Self::Evaluating { .. } => "evaluating",
Self::Finalizing => "finalizing",
Self::Merging => "merging",
Self::Exporting => "exporting",
Self::Comparing { .. } => "comparing",
Self::Retrieving => "retrieving",
}
}
pub const NAMES: [&'static str; 7] = [
"training",
"evaluating",
"finalizing",
"merging",
"exporting",
"comparing",
"retrieving",
];
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Phases {
stage: Option<JobStage>,
eval: Option<(u64, Option<u64>)>,
ended: bool,
step: u64,
max_steps: Option<u64>,
}
impl Phases {
pub fn line(&mut self, line: &MetricLine) {
match line {
MetricLine::Begin { max_steps, .. } => self.max_steps = max_steps.or(self.max_steps),
MetricLine::Log(metric) => self.metric(metric),
other => {
if let Some(mark) = other.mark() {
self.mark(mark);
}
},
}
}
pub fn metric(&mut self, metric: &TrainMetric) {
self.step = self.step.max(metric.step);
self.max_steps = metric.max_steps.or(self.max_steps);
self.eval = None;
}
pub fn mark(&mut self, mark: Mark) {
match mark {
Mark::Stage(stage) => self.stage = Some(stage),
Mark::Eval { step, total } => self.eval = Some((step, total)),
Mark::End { step } => {
self.ended = true;
self.step = self.step.max(step);
},
}
}
#[must_use]
pub fn phase(&self, exited: bool) -> Phase {
if exited {
return Phase::Retrieving;
}
match self.stage {
Some(JobStage::Merge) => return Phase::Merging,
Some(JobStage::Export) => return Phase::Exporting,
Some(JobStage::Compare) => {
let (step, total) = self.eval.unwrap_or((0, None));
return Phase::Comparing { step, total };
},
Some(JobStage::Train) | None => {},
}
let last = self.ended
|| self
.max_steps
.is_some_and(|max| max > 0 && self.step >= max);
match self.eval {
Some((step, total)) => Phase::Evaluating { step, total, last },
None if last => Phase::Finalizing,
None => Phase::Training,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::train::parse_line;
fn phases(lines: &[&str]) -> Result<Phases, serde_json::Error> {
let mut phases = Phases::default();
for line in lines {
phases.line(&parse_line(line)?);
}
Ok(phases)
}
const BEGIN: &str = r#"{"event":"begin","time":1,"max_steps":100}"#;
const STAGE_TRAIN: &str = r#"{"event":"stage","name":"train","time":1}"#;
const STEP_50: &str = r#"{"event":"log","time":2,"step":50,"max_steps":100,"loss":1.0}"#;
const EVAL_50: &str = r#"{"event":"log","time":3,"step":50,"max_steps":100,"eval_loss":1.0}"#;
const STEP_100: &str = r#"{"event":"log","time":4,"step":100,"max_steps":100,"loss":0.9}"#;
const EVAL: &str = r#"{"event":"eval","time":3,"step":340,"total":1200}"#;
const END: &str = r#"{"event":"end","time":5,"step":100}"#;
#[test]
fn the_phase_follows_the_rule() -> Result<(), serde_json::Error> {
let mid_eval = Phase::Evaluating {
step: 340,
total: Some(1200),
last: false,
};
let final_eval = Phase::Evaluating {
step: 340,
total: Some(1200),
last: true,
};
let cases: [(&[&str], Phase); 9] = [
(&[], Phase::Training),
(&[BEGIN, STAGE_TRAIN, STEP_50], Phase::Training),
(&[BEGIN, STEP_50, EVAL], mid_eval),
(&[BEGIN, STEP_50, EVAL, EVAL_50], Phase::Training),
(&[BEGIN, STEP_100, EVAL], final_eval),
(&[BEGIN, STEP_50, END], Phase::Finalizing),
(&[BEGIN, STEP_100], Phase::Finalizing),
(
&[
STAGE_TRAIN,
STEP_100,
END,
r#"{"event":"stage","name":"merge","time":6}"#,
],
Phase::Merging,
),
(
&[STEP_100, r#"{"event":"stage","name":"export","time":6}"#],
Phase::Exporting,
),
];
for (lines, expected) in cases {
assert_eq!(phases(lines)?.phase(false), expected, "{lines:?}");
}
assert_eq!(phases(&[BEGIN, STEP_50])?.phase(true), Phase::Retrieving);
Ok(())
}
#[test]
fn labels_say_what_the_job_does() {
let labels = [
Phase::Training,
Phase::Evaluating {
step: 340,
total: Some(1200),
last: false,
},
Phase::Evaluating {
step: 340,
total: None,
last: true,
},
Phase::Finalizing,
Phase::Merging,
Phase::Exporting,
Phase::Retrieving,
]
.map(Phase::label);
assert_eq!(
labels,
[
"training",
"evaluating 340/1200",
"finalizing (evaluation 340)",
"finalizing (saving model)",
"merging adapter",
"exporting GGUF",
"retrieving results",
]
);
assert!(Phase::NAMES.contains(&Phase::Exporting.name()));
}
#[test]
fn a_compare_job_shows_its_questions_answered() -> Result<(), serde_json::Error> {
let stage = r#"{"event":"stage","name":"compare","time":1}"#;
let starting = phases(&[stage])?.phase(false);
assert_eq!(
starting,
Phase::Comparing {
step: 0,
total: None
}
);
assert_eq!(starting.label(), "starting llama-server");
let answering = phases(&[stage, EVAL])?.phase(false);
assert_eq!(
answering,
Phase::Comparing {
step: 340,
total: Some(1200)
}
);
assert_eq!(answering.label(), "answering 340/1200");
assert_eq!(answering.name(), "comparing");
assert_eq!(phases(&[stage, EVAL])?.phase(true), Phase::Retrieving);
assert!(Phase::NAMES.contains(&"comparing"));
Ok(())
}
}