use std::{
io::{self, Write},
time::{Duration, Instant},
};
use crate::{Error, Result};
use super::{
ItTrainingConfig, TrainingCallback, TrainingCallbackContext, TrainingControl, TrainingSnapshot,
};
mod checkpoint;
mod csv;
mod phase;
mod policy;
mod validation;
pub use checkpoint::{CbCheckpoint, Checkpoint};
pub use csv::{CbCsvLogger, CsvLogger};
pub use phase::{CbPhase, PhaseSchedule, TrainingPhase};
pub use policy::{CbEarlyStop, CbLrScheduler, EarlyStopMode, EarlyStopping, LearningRateScheduler};
pub use validation::{CbValidation, Validation, ValidationMetric, ValidationResult};
pub struct ProgressBar {
width: usize,
show_epoch_header: bool,
last_print: Option<Instant>,
last_finalized_step: u64,
}
impl ProgressBar {
pub fn new(width: usize) -> Self {
Self {
width: width.max(1),
show_epoch_header: true,
last_print: None,
last_finalized_step: 0,
}
}
pub fn set_show_epoch_header(&mut self, show: bool) {
self.show_epoch_header = show;
}
pub fn render_line(&self, state: TrainingSnapshot, config: &ItTrainingConfig) -> String {
let has_epochs = state.total_epochs() > 0;
let step = if has_epochs {
state.step_in_epoch()
} else {
state.step_count()
};
let total = if has_epochs {
state.steps_in_epoch()
} else {
state.total_steps()
};
let elapsed = if has_epochs {
state.epoch_elapsed()
} else {
state.elapsed()
};
let mut line = String::from("\r ");
if total > 0 {
let eighths = ((step as u128 * self.width as u128 * 8 + u128::from(total / 2))
/ u128::from(total)) as usize;
let full = (eighths / 8).min(self.width);
let partial = eighths % 8;
line.push_str(&format!("{step}/{total} |"));
line.push_str(&"█".repeat(full));
if full < self.width && partial > 0 {
line.push_str(["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"][partial]);
line.push_str(&"░".repeat(self.width - full - 1));
} else {
line.push_str(&"░".repeat(self.width - full));
}
line.push_str("| ");
} else {
line.push_str(&format!("step {step} "));
}
let seconds = elapsed.as_secs_f64();
let milliseconds = if step > 0 {
seconds * 1000.0 / step as f64
} else {
0.0
};
let samples_per_second = if seconds > 0.0 {
step as f64 * config.batch_size as f64 / seconds
} else {
0.0
};
line.push_str(&format!(
"{seconds:.2}s · {milliseconds:.2} ms/step · {} sample/s",
format_rate(samples_per_second)
));
if config.sequence_length > 0 {
line.push_str(&format!(
" · {} {}/s",
format_rate(samples_per_second * config.sequence_length as f64),
config.sequence_unit
));
}
if state.total_source_units() > 0 {
let rate = if has_epochs {
state.epoch_source_units_per_second()
} else {
state.wall_source_units_per_second()
};
line.push_str(&format!(
" · {} {}/s",
format_rate(rate),
config.source_unit
));
}
if let Some(loss) = state.last_loss() {
line.push_str(&format!(" · loss: {loss:.4}"));
}
line.push_str(" ");
line
}
fn render(&self, context: &TrainingCallbackContext<'_>, newline: bool) -> Result<()> {
let mut output = self.render_line(context.snapshot(), context.config());
if newline {
output.push('\n');
}
write_stdout(&output)
}
}
impl Default for ProgressBar {
fn default() -> Self {
Self::new(10)
}
}
impl TrainingCallback for ProgressBar {
fn on_train_begin(
&mut self,
_context: &mut TrainingCallbackContext<'_>,
) -> Result<TrainingControl> {
self.last_print = Some(Instant::now());
Ok(TrainingControl::Continue)
}
fn on_epoch_begin(
&mut self,
context: &mut TrainingCallbackContext<'_>,
) -> Result<TrainingControl> {
let state = context.snapshot();
if self.show_epoch_header && state.total_epochs() > 0 {
write_stdout(&format!(
"epoch {}/{}\n",
state.epoch(),
state.total_epochs()
))?;
}
Ok(TrainingControl::Continue)
}
fn on_step_end(&mut self, context: &mut TrainingCallbackContext<'_>) -> Result<TrainingControl> {
let state = context.snapshot();
if state.is_epoch_boundary() {
return Ok(TrainingControl::Continue);
}
let now = Instant::now();
let boundary = state.is_last_step();
if !boundary
&& self
.last_print
.is_some_and(|last| now.duration_since(last) < Duration::from_millis(33))
{
return Ok(TrainingControl::Continue);
}
self.last_print = Some(now);
self.render(context, boundary)?;
if boundary {
self.last_finalized_step = state.step_count();
}
Ok(TrainingControl::Continue)
}
fn on_epoch_end(&mut self, context: &mut TrainingCallbackContext<'_>) -> Result<TrainingControl> {
self.render(context, true)?;
self.last_finalized_step = context.snapshot().step_count();
Ok(TrainingControl::Continue)
}
fn on_train_end(&mut self, context: &mut TrainingCallbackContext<'_>) -> Result<TrainingControl> {
let state = context.snapshot();
if self.last_finalized_step != state.step_count() {
self.render(context, true)?;
}
Ok(TrainingControl::Continue)
}
}
pub struct TrainingSummary {
track_initial_loss: bool,
initial_loss: Option<f32>,
validation_metric: Option<ValidationMetric>,
}
impl TrainingSummary {
pub const fn new(track_initial_loss: bool) -> Self {
Self {
track_initial_loss,
initial_loss: None,
validation_metric: None,
}
}
pub fn set_validation_metric(&mut self, metric: ValidationMetric) {
self.validation_metric = Some(metric);
}
pub fn render_report(&self, state: TrainingSnapshot, config: &ItTrainingConfig) -> String {
let mut report = String::from("\nSummary:\n");
match (self.initial_loss, state.last_loss()) {
(_, None) => report.push_str(" loss: n/a (no loss recorded)\n"),
(Some(initial), Some(final_loss)) if self.track_initial_loss => report.push_str(&format!(
" loss: initial {initial:.6} · final {final_loss:.6} · mean {:.6}\n",
state.training_mean_loss()
)),
(_, Some(final_loss)) => report.push_str(&format!(
" loss: final {final_loss:.6} · mean {:.6}\n",
state.training_mean_loss()
)),
}
if let Some(metric) = &self.validation_metric
&& let Some(value) = metric.value()
{
report.push_str(&format!(" Validation: {} {value:.6}\n", metric.name()));
}
report.push_str(&format!(
" Wall: {:.2} ms/step · {} sample/s",
state.wall_ms_per_step(),
format_rate(state.wall_samples_per_second())
));
if config.sequence_length > 0 {
report.push_str(&format!(
" · {} {}/s",
format_rate(state.wall_units_per_second()),
config.sequence_unit
));
}
if state.total_source_units() > 0 {
report.push_str(&format!(
" · {} {}/s",
format_rate(state.wall_source_units_per_second()),
config.source_unit
));
}
report.push('\n');
let gpu = state.gpu_timing_stats();
if gpu.count == 0 {
report.push_str(&format!(
" GPU ({}): n/a (timer unavailable)\n",
config.timer_name
));
} else {
report.push_str(&format!(
" GPU ({}): mean {:.3} ms/step · p50 {:.3} · p95 {:.3} · min {:.3} · max {:.3} · {} sample/s",
config.timer_name,
gpu.mean_ms,
gpu.median_ms,
gpu.p95_ms,
gpu.min_ms,
gpu.max_ms,
format_rate(state.gpu_samples_per_second())
));
if config.sequence_length > 0 {
report.push_str(&format!(
" · {} {}/s",
format_rate(state.gpu_units_per_second()),
config.sequence_unit
));
}
if state.total_source_units() > 0 {
report.push_str(&format!(
" · {} {}/s",
format_rate(state.gpu_source_units_per_second()),
config.source_unit
));
}
let gap = if state.gpu_samples_per_second() > 0.0 {
(100.0 * (1.0 - state.wall_samples_per_second() / state.gpu_samples_per_second()))
.clamp(0.0, 100.0)
} else {
0.0
};
report.push_str(&format!(" · wall-GPU gap {gap:.0}%\n"));
}
report.push_str(&format!(
" run: {:.2}s · {} steps · batch {}",
state.elapsed().as_secs_f64(),
state.step_count(),
config.batch_size
));
if config.sequence_length > 0 {
report.push_str(&format!(
" · sequence {} {}/sample",
config.sequence_length, config.sequence_unit
));
}
report.push('\n');
report
}
}
impl Default for TrainingSummary {
fn default() -> Self {
Self::new(true)
}
}
impl TrainingCallback for TrainingSummary {
fn on_step_end(&mut self, context: &mut TrainingCallbackContext<'_>) -> Result<TrainingControl> {
if self.track_initial_loss
&& self.initial_loss.is_none()
&& context.snapshot().step_count() == 1
{
self.initial_loss = context.snapshot().last_loss();
}
Ok(TrainingControl::Continue)
}
fn on_train_end(&mut self, context: &mut TrainingCallbackContext<'_>) -> Result<TrainingControl> {
write_stdout(&self.render_report(context.snapshot(), context.config()))?;
Ok(TrainingControl::Continue)
}
}
pub type CbProgressBar = ProgressBar;
pub type CbSummary = TrainingSummary;
fn format_rate(rate: f64) -> String {
if rate >= 1.0e6 {
format!("{:.2}M", rate / 1.0e6)
} else if rate >= 1.0e3 {
format!("{:.2}K", rate / 1.0e3)
} else {
format!("{rate:.0}")
}
}
fn write_stdout(text: &str) -> Result<()> {
let mut stdout = io::stdout().lock();
stdout
.write_all(text.as_bytes())
.map_err(|error| Error::io("write training progress", error))?;
stdout
.flush()
.map_err(|error| Error::io("flush training progress", error))
}