use std::sync::Arc;
use crate::data::BatchDataSet;
use crate::nn::Module;
use super::ddp_run::{
ApplyPolicy, AverageBackend, CheckpointFn, ConvergenceGuard,
EpochCallbackPolicy, EpochFn, EvalFn, EvalResultFn,
MetricsFn, SchedulerFn,
};
use super::launcher::FullCluster;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ElCheMode {
NcclSync,
NcclCadence,
CpuSync,
CpuCadence,
CpuAsync,
}
impl ElCheMode {
pub(crate) fn split(self) -> (ApplyPolicy, AverageBackend) {
match self {
Self::NcclSync => (ApplyPolicy::Sync, AverageBackend::Nccl),
Self::NcclCadence => (ApplyPolicy::Cadence, AverageBackend::Nccl),
Self::CpuSync => (ApplyPolicy::Sync, AverageBackend::Cpu),
Self::CpuCadence => (ApplyPolicy::Cadence, AverageBackend::Cpu),
Self::CpuAsync => (ApplyPolicy::Async, AverageBackend::Cpu),
}
}
pub(crate) fn from_parts(policy: ApplyPolicy, backend: AverageBackend) -> Self {
match (policy, backend) {
(ApplyPolicy::Sync, AverageBackend::Nccl) => Self::NcclSync,
(ApplyPolicy::Cadence, AverageBackend::Nccl) => Self::NcclCadence,
(ApplyPolicy::Sync, AverageBackend::Cpu) => Self::CpuSync,
(ApplyPolicy::Cadence, AverageBackend::Cpu) => Self::CpuCadence,
(ApplyPolicy::Async, AverageBackend::Cpu) => Self::CpuAsync,
(ApplyPolicy::Async, AverageBackend::Nccl) => {
debug_assert!(
false,
"NcclAsync was dropped; (Async, Nccl) has no ElCheMode"
);
Self::NcclCadence
}
}
}
}
#[derive(Clone)]
pub struct ElCheConfig {
pub mode: ElCheMode,
pub anchor: usize,
pub max_anchor: Option<usize>,
pub min_anchor: Option<usize>,
pub overhead_target: Option<f64>,
pub max_batch_diff: Option<usize>,
pub relax_up: bool,
pub partition_ratios: Option<Vec<f64>>,
pub meta_controller: bool,
pub convergence_guard: Option<Box<dyn ConvergenceGuard>>,
pub easgd_alpha: Option<f64>,
pub divergence_threshold: Option<f64>,
pub no_divergence_guard: bool,
pub max_overshoot: Option<usize>,
pub gamma: f64,
pub bf16_wire: bool,
}
impl ElCheConfig {
pub fn nccl_sync() -> Self {
Self {
mode: ElCheMode::NcclSync,
anchor: 1,
..Self::default_for(ElCheMode::NcclSync)
}
}
pub fn nccl_cadence() -> Self {
Self::default_for(ElCheMode::NcclCadence)
}
pub fn cpu_sync() -> Self {
Self {
mode: ElCheMode::CpuSync,
anchor: 1,
..Self::default_for(ElCheMode::CpuSync)
}
}
pub fn cpu_cadence() -> Self {
Self::default_for(ElCheMode::CpuCadence)
}
pub fn cpu_async() -> Self {
Self {
overhead_target: Some(0.05),
..Self::default_for(ElCheMode::CpuAsync)
}
}
pub(crate) fn default_for(mode: ElCheMode) -> Self {
Self {
mode,
anchor: 10,
max_anchor: None,
min_anchor: None,
overhead_target: None,
max_batch_diff: None,
relax_up: false,
partition_ratios: None,
meta_controller: true,
convergence_guard: None,
easgd_alpha: match mode {
ElCheMode::CpuAsync => Some(0.5),
_ => None,
},
divergence_threshold: None,
no_divergence_guard: false,
max_overshoot: None,
gamma: 1.0,
bf16_wire: false,
}
}
pub fn mode(mut self, m: ElCheMode) -> Self { self.mode = m; self }
pub fn anchor(mut self, n: usize) -> Self { self.anchor = n; self }
pub fn max_anchor(mut self, n: usize) -> Self { self.max_anchor = Some(n); self }
pub fn min_anchor(mut self, n: usize) -> Self { self.min_anchor = Some(n); self }
pub fn overhead_target(mut self, f: f64) -> Self { self.overhead_target = Some(f); self }
pub fn max_batch_diff(mut self, n: usize) -> Self { self.max_batch_diff = Some(n); self }
pub fn relax_up(mut self, on: bool) -> Self { self.relax_up = on; self }
pub fn partition_ratios(mut self, r: Vec<f64>) -> Self { self.partition_ratios = Some(r); self }
pub fn meta_controller(mut self, on: bool) -> Self { self.meta_controller = on; self }
pub fn convergence_guard<G>(mut self, g: G) -> Self
where
G: ConvergenceGuard + 'static,
{
self.convergence_guard = Some(Box::new(g));
self
}
pub fn easgd_alpha(mut self, a: f64) -> Self { self.easgd_alpha = Some(a); self }
pub fn divergence_threshold(mut self, t: f64) -> Self { self.divergence_threshold = Some(t); self }
pub fn no_divergence_guard(mut self) -> Self { self.no_divergence_guard = true; self }
pub fn max_overshoot(mut self, n: usize) -> Self { self.max_overshoot = Some(n); self }
pub fn gamma(mut self, g: f64) -> Self { self.gamma = g; self }
pub fn bf16_wire(mut self, on: bool) -> Self { self.bf16_wire = on; self }
}
impl Default for ElCheConfig {
fn default() -> Self {
Self::nccl_cadence()
}
}
impl std::fmt::Debug for ElCheConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ElCheConfig")
.field("mode", &self.mode)
.field("anchor", &self.anchor)
.field("max_anchor", &self.max_anchor)
.field("min_anchor", &self.min_anchor)
.field("overhead_target", &self.overhead_target)
.field("max_batch_diff", &self.max_batch_diff)
.field("relax_up", &self.relax_up)
.field("partition_ratios", &self.partition_ratios)
.field("meta_controller", &self.meta_controller)
.field("convergence_guard", &self.convergence_guard.as_ref().map(|_| "<dyn ConvergenceGuard>"))
.field("easgd_alpha", &self.easgd_alpha)
.field("divergence_threshold", &self.divergence_threshold)
.field("no_divergence_guard", &self.no_divergence_guard)
.field("max_overshoot", &self.max_overshoot)
.field("gamma", &self.gamma)
.field("bf16_wire", &self.bf16_wire)
.finish()
}
}
pub struct TrainerConfig<M: Module> {
pub dataset: Arc<dyn BatchDataSet>,
pub batch_size: usize,
pub num_epochs: usize,
pub elche: ElCheConfig,
pub max_grad_norm: Option<f64>,
pub vram_pool: bool,
pub augment: usize,
pub transform: Option<crate::data::TransformFn>,
pub vram_max_usage: f64,
pub ram_max_usage: f64,
pub sample_cache: bool,
pub disk_stage_gb: u64,
pub disk_stage_dir: Option<std::path::PathBuf>,
pub max_failure: Option<crate::distributed::max_failure::MaxFailureThreshold>,
pub checkpoint_every: Option<usize>,
pub save_path: Option<String>,
pub resume_from: Option<String>,
pub checkpoint_at_epoch: Option<usize>,
pub checkpoint_fn: Option<CheckpointFn<M>>,
pub epoch_fn: Option<EpochFn<M>>,
pub metrics_fn: Option<MetricsFn>,
pub scheduler_fn: Option<SchedulerFn>,
pub eval_fn: Option<EvalFn<M>>,
pub eval_dataset: Option<Arc<dyn BatchDataSet>>,
pub eval_result_fn: Option<EvalResultFn>,
pub eval_every: Option<usize>,
pub reports_per_epoch: Option<usize>,
pub record_log_dir: Option<String>,
pub max_log_size: Option<u64>,
pub dashboard_html: Option<String>,
pub dashboard_theme: Option<String>,
pub scalar_reductions: crate::monitor::record::Reductions,
pub epoch_callback_policy: EpochCallbackPolicy,
pub timeline: Option<Arc<crate::monitor::Timeline>>,
pub cluster: Option<FullCluster>,
pub outer_optimizer: Option<crate::distributed::outer_optimizer::OuterOptimizerFactory>,
}
impl<M: Module> TrainerConfig<M> {
pub fn new(dataset: Arc<dyn BatchDataSet>) -> Self {
Self {
dataset,
batch_size: 32,
num_epochs: 1,
elche: ElCheConfig::default(),
max_grad_norm: None,
vram_pool: crate::data::vram_pool::VRAM_POOL_DEFAULT,
augment: 1,
transform: None,
vram_max_usage: 0.90,
ram_max_usage: 0.50,
sample_cache: true,
disk_stage_gb: 0,
disk_stage_dir: None,
max_failure: None,
checkpoint_every: None,
save_path: None,
resume_from: None,
checkpoint_at_epoch: None,
checkpoint_fn: None,
epoch_fn: None,
metrics_fn: None,
scheduler_fn: None,
eval_fn: None,
eval_dataset: None,
eval_result_fn: None,
eval_every: None,
reports_per_epoch: None,
record_log_dir: None,
dashboard_html: None,
dashboard_theme: None,
scalar_reductions: crate::monitor::record::Reductions::new(),
max_log_size: None,
epoch_callback_policy: EpochCallbackPolicy::default(),
timeline: None,
cluster: None,
outer_optimizer: None,
}
}
pub fn from_dataset(dataset: impl crate::data::DataSet + 'static) -> Self {
Self::new(crate::data::batch_dataset_from(dataset))
}
pub fn batch_size(mut self, n: usize) -> Self { self.batch_size = n; self }
pub fn with_vram_pool(mut self, enabled: bool) -> Self { self.vram_pool = enabled; self }
pub fn with_augment(mut self, k: usize) -> Self { self.augment = k.max(1); self }
pub fn with_vram_max_usage(mut self, max_usage: f64) -> Self {
self.vram_max_usage = max_usage.clamp(0.50, 0.99);
self
}
pub fn with_ram_max_usage(mut self, max_usage: f64) -> Self {
self.ram_max_usage = max_usage.clamp(0.0, 0.90);
self
}
pub fn with_sample_cache(mut self, enabled: bool) -> Self {
self.sample_cache = enabled;
self
}
pub fn with_disk_stage(mut self, gb: u64) -> Self {
self.disk_stage_gb = gb;
self
}
pub fn with_disk_stage_dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
self.disk_stage_dir = Some(dir.into());
self
}
pub fn with_transform(
mut self,
f: impl Fn(Vec<crate::tensor::Tensor>, &[crate::data::PickKey]) -> crate::tensor::Result<Vec<crate::tensor::Tensor>>
+ Send
+ Sync
+ 'static,
) -> Self {
self.transform = Some(crate::data::TransformFn::new(f));
self
}
pub fn outer_optimizer<P>(mut self, factory: P) -> Self
where
P: Fn() -> Box<dyn crate::distributed::OuterOptimizer> + Send + Sync + 'static,
{
self.outer_optimizer = Some(Arc::new(factory));
self
}
pub fn num_epochs(mut self, n: usize) -> Self { self.num_epochs = n; self }
pub fn elche(mut self, cfg: ElCheConfig) -> Self { self.elche = cfg; self }
pub fn max_grad_norm(mut self, n: f64) -> Self { self.max_grad_norm = Some(n); self }
pub fn checkpoint_every(mut self, n: usize) -> Self { self.checkpoint_every = Some(n); self }
pub fn save_path(mut self, p: impl Into<String>) -> Self { self.save_path = Some(p.into()); self }
pub fn resume_from(mut self, p: impl Into<String>) -> Self { self.resume_from = Some(p.into()); self }
pub fn checkpoint_at_epoch(mut self, n: usize) -> Self { self.checkpoint_at_epoch = Some(n); self }
pub fn checkpoint_fn(mut self, f: CheckpointFn<M>) -> Self { self.checkpoint_fn = Some(f); self }
pub fn epoch_fn(mut self, f: EpochFn<M>) -> Self { self.epoch_fn = Some(f); self }
pub fn metrics_fn(mut self, f: MetricsFn) -> Self { self.metrics_fn = Some(f); self }
pub fn scheduler_fn(mut self, f: SchedulerFn) -> Self { self.scheduler_fn = Some(f); self }
pub fn eval_fn(mut self, f: EvalFn<M>) -> Self { self.eval_fn = Some(f); self }
pub fn eval_dataset(mut self, ds: Arc<dyn BatchDataSet>) -> Self { self.eval_dataset = Some(ds); self }
pub fn eval_result_fn(mut self, f: EvalResultFn) -> Self { self.eval_result_fn = Some(f); self }
pub fn eval_every(mut self, n: usize) -> Self { self.eval_every = Some(n); self }
pub fn reports_per_epoch(mut self, n: usize) -> Self {
self.reports_per_epoch = if n == 0 { None } else { Some(n) };
self
}
pub fn record_log(mut self, dir: impl Into<String>, max_bytes: u64) -> Self {
self.record_log_dir = Some(dir.into());
self.max_log_size = if max_bytes == 0 { None } else { Some(max_bytes) };
self
}
pub fn epoch_callback_policy(mut self, p: EpochCallbackPolicy) -> Self {
self.epoch_callback_policy = p;
self
}
pub fn timeline(mut self, t: Arc<crate::monitor::Timeline>) -> Self {
self.timeline = Some(t);
self
}
pub fn cluster(mut self, c: FullCluster) -> Self {
self.cluster = Some(c);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn elche_mode_split_round_trip() {
for m in [
ElCheMode::NcclSync, ElCheMode::NcclCadence,
ElCheMode::CpuSync, ElCheMode::CpuCadence, ElCheMode::CpuAsync,
] {
let (p, b) = m.split();
let expected_backend = match m {
ElCheMode::NcclSync | ElCheMode::NcclCadence => AverageBackend::Nccl,
ElCheMode::CpuSync | ElCheMode::CpuCadence | ElCheMode::CpuAsync => AverageBackend::Cpu,
};
assert_eq!(b, expected_backend, "{:?} backend split", m);
let expected_policy = match m {
ElCheMode::NcclSync | ElCheMode::CpuSync => ApplyPolicy::Sync,
ElCheMode::NcclCadence | ElCheMode::CpuCadence => ApplyPolicy::Cadence,
ElCheMode::CpuAsync => ApplyPolicy::Async,
};
assert_eq!(p, expected_policy, "{:?} policy split", m);
}
}
#[test]
fn elche_presets_carry_sane_defaults() {
assert_eq!(ElCheConfig::nccl_sync().anchor, 1);
assert_eq!(ElCheConfig::nccl_sync().mode, ElCheMode::NcclSync);
assert_eq!(ElCheConfig::cpu_sync().anchor, 1);
assert_eq!(ElCheConfig::nccl_cadence().anchor, 10);
assert_eq!(ElCheConfig::nccl_cadence().mode, ElCheMode::NcclCadence);
assert_eq!(ElCheConfig::cpu_async().mode, ElCheMode::CpuAsync);
}
#[test]
fn elche_chained_setters() {
let cfg = ElCheConfig::nccl_cadence()
.max_anchor(20)
.overhead_target(0.05)
.meta_controller(true)
.partition_ratios(vec![0.7, 0.3])
.relax_up(true);
assert_eq!(cfg.max_anchor, Some(20));
assert_eq!(cfg.overhead_target, Some(0.05));
assert!(cfg.meta_controller);
assert_eq!(cfg.partition_ratios.as_deref(), Some(&[0.7, 0.3][..]));
assert!(cfg.relax_up);
}
#[test]
fn elche_default_is_nccl_cadence() {
let cfg = ElCheConfig::default();
assert_eq!(cfg.mode, ElCheMode::NcclCadence);
assert_eq!(cfg.anchor, 10);
}
#[test]
fn meta_controller_default_is_on() {
let cfg = ElCheConfig::default();
assert!(cfg.meta_controller, "meta_controller defaults to true");
for preset in [
ElCheConfig::nccl_sync(),
ElCheConfig::nccl_cadence(),
ElCheConfig::cpu_sync(),
ElCheConfig::cpu_cadence(),
ElCheConfig::cpu_async(),
] {
assert!(preset.meta_controller, "preset for {:?}", preset.mode);
}
}
}