1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//! Aggregated training-metrics vocabulary.
//!
//! A leaf module by design: [`EpochMetrics`] is plain data (no tensors,
//! no handles) consumed across the stack — the `nn` [`Module`] trait
//! exposes a shared slot for it, [`Graph`] owns such a slot for its
//! monitor integration, the distributed coordinator aggregates into it,
//! and [`Monitor`] renders it. Housing it here keeps those consumers
//! from depending on each other just to name the type. The historical
//! paths (`flodl::EpochMetrics`, `flodl::distributed::EpochMetrics`)
//! remain valid re-exports.
//!
//! [`Module`]: crate::nn::Module
//! [`Graph`]: crate::graph::Graph
//! [`Monitor`]: crate::monitor::Monitor
use HashMap;
/// Aggregated epoch metrics from all DDP workers.
///
/// Available via `DdpHandle::poll_metrics()`, `DdpHandle::next_metrics()`,
/// and the host-side [`crate::distributed::DdpBuilder::metrics_fn`] callback.
/// The coordinator aggregates per-rank metrics into this structure once
/// all ranks have reported for the same epoch; the same `EpochMetrics` reaches
/// the callback (if registered) and the polling queue, so both surfaces compose.
///
/// # Example: explicit polling
///
/// ```ignore
/// let handle = Trainer::builder(...).run()?;
/// while let Some(m) = handle.next_metrics() {
/// for (name, value) in &m.scalars {
/// monitor.record_scalar(name, *value);
/// }
/// }
/// let state = handle.join()?;
/// ```
///
/// # Example: chained `.run()?.join()?` with `metrics_fn`
///
/// ```ignore
/// Trainer::builder(model_factory, optim_factory, train_step)
/// .dataset(dataset).batch_size(32).num_epochs(N)
/// .metrics_fn(move |m| {
/// println!("epoch {}: loss={:.4}", m.epoch, m.avg_loss);
/// Ok(())
/// })
/// .run()?
/// .join()?;
/// ```