mod worker;
pub(crate) use worker::NcclAbortSlot;
mod cooperative;
mod orchestrator;
mod shared;
pub mod convergence;
pub use cooperative::{StepOutcome, Worker};
pub use worker::*;
pub(crate) use shared::{
aggregate_epoch_metrics, equal_sizes, ratio_to_sizes, throughput_sizes,
};
pub use orchestrator::*;
pub use convergence::{
ConvergenceAction, ConvergenceGuard, DivergenceReport, LambdaEstimator, LambdaSample,
MsfGuard, NoGuard, TrendGuard,
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use crate::tensor::{Device, Result, Tensor};
thread_local! {
static SCALAR_ACCUM: RefCell<HashMap<String, (f64, usize)>> = RefCell::new(HashMap::new());
}
pub fn record_scalar(name: &str, value: f64) {
SCALAR_ACCUM.with(|acc| {
let mut map = acc.borrow_mut();
let entry = map.entry(name.to_string()).or_insert((0.0, 0));
entry.0 += value;
entry.1 += 1;
});
}
pub fn drain_scalars() -> HashMap<String, (f64, usize)> {
SCALAR_ACCUM.with(|acc| std::mem::take(&mut *acc.borrow_mut()))
}
pub(crate) fn ensure_trainable_params(n_params: usize, entry: &str) -> Result<()> {
if n_params == 0 {
return Err(crate::tensor::TensorError::new(&format!(
"{entry}: model has zero trainable parameters — nothing to train. \
If this is a custom leaf module, note that Module::parameters() \
defaults to an empty list; override it to return the module's \
parameters."
)));
}
Ok(())
}
pub type CheckpointFn<M> = Arc<dyn Fn(u64, &M) -> Result<()> + Send + Sync>;
pub type EpochFn<M> = Arc<dyn Fn(usize, &mut GpuWorker<M>) + Send + Sync>;
pub type MetricsFn = Arc<dyn Fn(&EpochMetrics) -> Result<()> + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvalCadence {
Epochs(usize),
}
pub type EvalFn<M> = std::sync::Arc<
dyn Fn(&M, &dyn crate::data::BatchDataSet) -> Result<f64> + Send + Sync,
>;
pub type EvalResultFn = std::sync::Arc<
dyn Fn(usize, f64) -> Result<()> + Send + Sync,
>;
pub type SchedulerFn = Box<dyn Fn(usize) -> Arc<dyn crate::nn::Scheduler> + Send + Sync>;
pub(crate) struct RankCallbacks<M: crate::nn::Module> {
pub checkpoint_fn: Option<CheckpointFn<M>>,
pub epoch_fn: Option<EpochFn<M>>,
pub eval_fn: Option<EvalFn<M>>,
pub eval_dataset: Option<Arc<dyn crate::data::BatchDataSet>>,
pub outer_optimizer_factory:
Option<crate::distributed::outer_optimizer::OuterOptimizerFactory>,
}
impl<M: crate::nn::Module> Default for RankCallbacks<M> {
fn default() -> Self {
RankCallbacks {
checkpoint_fn: None,
epoch_fn: None,
eval_fn: None,
eval_dataset: None,
outer_optimizer_factory: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum EpochCallbackPolicy {
Rank(usize),
#[default]
Fastest,
}
#[derive(Clone, Debug)]
pub struct TrainedState {
pub params: Vec<Tensor>,
pub buffers: Vec<Tensor>,
}
pub use crate::metrics::EpochMetrics;
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum ApplyPolicy {
Sync,
Cadence,
Async,
}
impl ApplyPolicy {
pub fn is_barrier_paced(&self) -> bool {
matches!(self, ApplyPolicy::Sync | ApplyPolicy::Cadence)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum AverageBackend {
Nccl,
Cpu,
}
#[derive(Clone, Debug)]
pub struct DdpRunConfig {
pub elche: crate::distributed::ElCheConfig,
pub checkpoint_every: Option<usize>,
pub snapshot_timeout_secs: u64,
pub progressive_dispatch: Option<bool>,
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 timeline: Option<Arc<crate::monitor::Timeline>>,
pub lr_scale_ratio: f64,
pub save_path: Option<String>,
pub max_failure: Option<crate::distributed::max_failure::MaxFailureThreshold>,
pub heartbeat_timeout_secs: Option<u64>,
pub epoch_callback_policy: EpochCallbackPolicy,
pub eval_every_epochs: 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 resume_from: Option<String>,
pub checkpoint_at_epoch: Option<usize>,
}
impl Default for DdpRunConfig {
fn default() -> Self {
Self::new()
}
}
impl DdpRunConfig {
pub fn new() -> Self {
DdpRunConfig {
elche: crate::distributed::ElCheConfig::default(),
checkpoint_every: None,
snapshot_timeout_secs: 5,
progressive_dispatch: None,
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,
timeline: None,
lr_scale_ratio: 1.0,
save_path: None,
max_failure: None,
heartbeat_timeout_secs: None,
epoch_callback_policy: EpochCallbackPolicy::default(),
eval_every_epochs: None,
reports_per_epoch: None,
record_log_dir: None,
max_log_size: None,
dashboard_html: None,
dashboard_theme: None,
scalar_reductions: crate::monitor::record::Reductions::new(),
resume_from: None,
checkpoint_at_epoch: None,
}
}
pub fn with_resume_from(mut self, stem: impl Into<String>) -> Self {
self.resume_from = Some(stem.into());
self
}
pub fn with_checkpoint_at_epoch(mut self, epoch: usize) -> Self {
self.checkpoint_at_epoch = Some(epoch);
self
}
pub fn with_epoch_callback_policy(mut self, policy: EpochCallbackPolicy) -> Self {
self.epoch_callback_policy = policy;
self
}
pub fn with_heartbeat_timeout_secs(mut self, secs: u64) -> Self {
self.heartbeat_timeout_secs = Some(secs);
self
}
pub fn with_save_path(mut self, path: impl Into<String>) -> Self {
self.save_path = Some(path.into());
self
}
pub fn with_max_failure(
mut self,
threshold: crate::distributed::max_failure::MaxFailureThreshold,
) -> Self {
self.max_failure = Some(threshold);
self
}
pub fn with_overhead_target(mut self, target: f64) -> Self {
self.elche.overhead_target = Some(target);
self
}
pub fn with_max_anchor(mut self, max: usize) -> Self {
self.elche.max_anchor = Some(max);
self
}
pub fn with_min_anchor(mut self, min: usize) -> Self {
self.elche.min_anchor = Some(min);
self
}
pub fn with_anchor(mut self, anchor: usize) -> Self {
self.elche.anchor = anchor;
self
}
pub fn with_divergence_threshold(mut self, threshold: f64) -> Self {
self.elche.divergence_threshold = Some(threshold);
self
}
pub fn with_no_divergence_guard(mut self) -> Self {
self.elche.no_divergence_guard = true;
self
}
pub fn with_max_batch_diff(mut self, max: usize) -> Self {
self.elche.max_batch_diff = Some(max);
self
}
pub fn with_max_overshoot(mut self, max: usize) -> Self {
self.elche.max_overshoot = Some(max);
self
}
pub fn with_checkpoint_every(mut self, n: usize) -> Self {
self.checkpoint_every = Some(n);
self
}
pub fn with_eval_every_epochs(mut self, n: usize) -> Self {
self.eval_every_epochs = if n == 0 { None } else { Some(n) };
self
}
pub fn with_reports_per_epoch(mut self, n: usize) -> Self {
self.reports_per_epoch = if n == 0 { None } else { Some(n) };
self
}
pub fn with_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 with_dashboard_html(mut self, path: impl Into<String>) -> Self {
self.dashboard_html = Some(path.into());
self
}
pub fn with_dashboard_theme(mut self, theme: impl Into<String>) -> Self {
self.dashboard_theme = Some(theme.into());
self
}
pub fn with_scalar_reduction(
mut self,
key: impl Into<String>,
reduction: crate::monitor::record::Reduction,
) -> Self {
self.scalar_reductions.insert(key.into(), reduction);
self
}
pub fn with_snapshot_timeout(mut self, secs: u64) -> Self {
self.snapshot_timeout_secs = secs;
self
}
pub fn with_partition_ratios(mut self, ratios: &[f64]) -> Self {
self.elche.partition_ratios = Some(ratios.to_vec());
self
}
pub fn with_progressive_dispatch(mut self, enabled: bool) -> Self {
self.progressive_dispatch = Some(enabled);
self
}
pub fn with_max_grad_norm(mut self, max_norm: f64) -> Self {
self.max_grad_norm = Some(max_norm);
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_transform(
mut self,
f: impl Fn(Vec<Tensor>, &[crate::data::PickKey]) -> crate::tensor::Result<Vec<Tensor>>
+ Send
+ Sync
+ 'static,
) -> Self {
self.transform = Some(crate::data::TransformFn::new(f));
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_timeline(mut self, tl: Arc<crate::monitor::Timeline>) -> Self {
self.timeline = Some(tl);
self
}
pub fn with_lr_scale_ratio(mut self, ratio: f64) -> Self {
self.lr_scale_ratio = ratio;
self
}
pub fn with_elche_relax_up(mut self, enabled: bool) -> Self {
self.elche.relax_up = enabled;
self
}
pub fn with_easgd_alpha(mut self, alpha: f64) -> Self {
assert!(
alpha > 0.0 && alpha <= 1.0,
"easgd_alpha must be in (0, 1], got {alpha}"
);
self.elche.easgd_alpha = Some(alpha);
self
}
pub fn with_meta_controller(mut self, enabled: bool) -> Self {
self.elche.meta_controller = enabled;
self
}
}
#[derive(Clone, Debug)]
pub(crate) enum TimingMsg {
Batch {
rank: usize,
batch_ms: f64,
data_ms: f64,
step_count: usize,
param_norm: Option<f64>,
batch_loss: f64,
sync_divergence: Option<f64>,
},
SyncAck {
rank: usize,
step_count: usize,
divergence: Option<f64>,
post_norm: Option<f64>,
pre_norm: Option<f64>,
},
Exiting {
rank: usize,
},
LrUpdate {
rank: usize,
lr: f64,
},
Intent {
rank: usize,
kind: crate::distributed::wire::IntentKind,
},
Heartbeat {
rank: usize,
step_count: usize,
},
SnapshotReady {
rank: usize,
},
NewNcclIdGenerated {
rank: usize,
uid_bytes: Vec<u8>,
},
EvalResult {
rank: usize,
schedule_id: u64,
epoch: u64,
metric: f64,
elapsed_ms: f64,
error: Option<String>,
},
CheckpointResult {
rank: usize,
version: u64,
elapsed_ms: f64,
error: Option<String>,
},
EpochFnElapsed {
rank: usize,
epoch: usize,
elapsed_ms: f64,
},
}
#[derive(Clone, Debug, Default)]
pub struct MetricsMsg {
pub rank: usize,
pub epoch: usize,
pub avg_loss: f64,
pub batches_processed: usize,
pub epoch_ms: f64,
pub samples_processed: usize,
pub share_complete_ms: f64,
pub compute_only_ms: f64,
pub data_starve_ms: f64,
pub scalars: HashMap<String, (f64, usize)>,
}
#[derive(Clone)]
pub struct ParamSnapshot {
pub rank: usize,
pub params: Vec<Tensor>,
pub buffers: Vec<Tensor>,
pub batch_count: usize,
}
#[derive(Clone, Debug)]
pub struct EpochPlan {
pub epoch: usize,
pub partition_offset: usize,
pub partition_size: usize,
}
#[derive(Clone, Debug)]
pub struct AveragedParams {
pub params: Vec<Tensor>,
pub buffers: Vec<Tensor>,
pub version: u64,
}
#[derive(Debug)]
pub(crate) enum ControlMsg {
RequestParams,
Update(AveragedParams),
SyncNow,
StartEpoch(EpochPlan),
ExtendPartition {
partition_offset: usize,
partition_size: usize,
},
DeclareDead,
NewNcclSession,
RequestNewNcclId,
StageAdvisory {
counts: Vec<usize>,
segments: Vec<(usize, Vec<(usize, usize)>)>,
},
Throttle,
SetGlobalStep(usize),
Checkpoint {
version: u64,
target_rank: usize,
},
ExecuteEvalCallback {
schedule_id: u64,
epoch: u64,
target_rank: usize,
},
SetEpochCallbackRole {
rank: usize,
},
Shutdown,
ShutdownWithSave {
reason: crate::distributed::checkpoint_meta::SaveReason,
},
EpochAggregated(Box<EpochMetrics>),
EvalBroadcast { epoch: usize, metric: f64 },
SaveConsensusModel {
target_rank: usize,
},
}
pub const SHUFFLE_BASE_SEED: u64 = 42;
pub(crate) fn resolve_shuffle_seed(resume_from: Option<&str>) -> crate::tensor::Result<u64> {
let Some(stem) = resume_from else {
return Ok(SHUFFLE_BASE_SEED);
};
let meta = crate::distributed::CheckpointMeta::read_from_file(
&crate::distributed::CheckpointBundle::meta_path(stem),
)?;
Ok(meta
.coverage
.as_ref()
.map(|c| c.seed)
.unwrap_or(SHUFFLE_BASE_SEED))
}
#[derive(Clone)]
pub struct WorkerConfig {
pub rank: usize,
pub world_size: usize,
pub device: Device,
pub initial_params: Vec<Tensor>,
pub initial_buffers: Vec<Tensor>,
pub total_samples: usize,
pub batch_size: usize,
pub augment: usize,
pub transform: Option<crate::data::TransformFn>,
pub seed: u64,
pub max_grad_norm: Option<f64>,
pub vram_pool: bool,
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 easgd_alpha: Option<f64>,
pub gamma: f64,
pub bf16_wire: bool,
pub timeline: Option<Arc<crate::monitor::Timeline>>,
pub policy: ApplyPolicy,
pub save_path: Option<String>,
pub coord_liveness_timeout_secs: u64,
}
pub const DEFAULT_COORD_LIVENESS_TIMEOUT_SECS: u64 = 30;
fn make_partition(
offset: usize,
size: usize,
total: usize,
epoch: usize,
seed: u64,
) -> Vec<usize> {
let all = crate::rng::epoch_permutation(seed, epoch, total);
let end = (offset + size).min(total);
all[offset..end].to_vec()
}
#[cfg(test)]
mod tests;