use super::*;
use std::sync::Arc;
use crate::data::BatchDataSet;
struct DeterministicDataset {
n: usize,
}
impl BatchDataSet for DeterministicDataset {
fn len(&self) -> usize {
self.n
}
fn get_batch(&self, indices: &[usize]) -> Result<Vec<Tensor>> {
let n = indices.len() as i64;
let opts = TensorOptions { dtype: DType::Float32, device: Device::CPU };
let s: usize = indices.iter().copied().sum();
let v = (s % 17) as f32 * 0.05 + 0.1;
Ok(vec![
Tensor::full(&[n, 4], v as f64, opts)?,
Tensor::full(&[n, 2], (v * 0.5 + 0.2) as f64, opts)?,
])
}
}
fn make_det_worker(total: usize) -> (GpuWorker<Linear>, WorkerChannels) {
let dev = test_device();
let tmp = Linear::on_device(4, 2, dev).unwrap();
let initial_params: Vec<Tensor> =
tmp.parameters().iter().map(|p| p.variable.data()).collect();
let initial_buffers: Vec<Tensor> = tmp.buffers().iter().map(|b| b.get()).collect();
drop(tmp);
let config = WorkerConfig {
rank: 0,
world_size: 1,
device: dev,
initial_params,
initial_buffers,
total_samples: total,
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,
batch_size: 4,
seed: 42,
max_grad_norm: None,
vram_pool: false,
easgd_alpha: None,
gamma: 1.0,
bf16_wire: false,
timeline: None,
policy: ApplyPolicy::Sync,
save_path: None,
coord_liveness_timeout_secs:
crate::distributed::ddp_run::DEFAULT_COORD_LIVENESS_TIMEOUT_SECS,
};
let ((timing_tx, metrics_tx, param_tx, final_param_tx, control_rx), channels) =
GpuWorker::<Linear>::channels();
let dataset: Arc<dyn BatchDataSet> = Arc::new(DeterministicDataset { n: total });
let worker = GpuWorker::new(
&config,
|d| Linear::on_device(4, 2, d),
|params| crate::nn::SGD::new(params, 0.01, 0.0),
dataset,
None, None, None, None, timing_tx,
metrics_tx,
param_tx,
final_param_tx,
control_rx,
None, )
.unwrap();
(worker, channels)
}
fn sync_params(dst: &mut GpuWorker<Linear>, src: &mut GpuWorker<Linear>) {
let snap = src.snapshot_params();
dst.load_averaged(&AveragedParams {
params: snap.params,
buffers: snap.buffers,
version: 0,
})
.unwrap();
if let Device::CUDA(idx) = test_device() {
crate::tensor::cuda_synchronize(idx);
}
}
#[test]
fn single_host_worker_trains_and_finishes() {
let total = 16; let epochs = 2;
let (w, _ch) = make_det_worker(total);
let mut coop = Worker::single(w, epochs, total);
let mut steps = 0usize;
while let Some(_plan) = coop.next_plan().unwrap() {
while let Some(batch) = coop.next_batch().unwrap() {
let loss = mse_train(coop.model(), &batch).unwrap();
loss.backward().unwrap();
let outcome = coop.step(&loss).unwrap();
assert!(!outcome.shutdown, "no shutdown expected on single device");
steps += 1;
}
}
assert_eq!(steps, epochs * (total / 4), "one step per batch per epoch");
let state = coop.finish().unwrap();
assert_eq!(state.params.len(), 2, "Linear: weight + bias");
assert_eq!(state.buffers.len(), 0, "Linear has no buffers");
}
#[test]
fn worker_next_plan_stops_after_num_epochs() {
let (w, _ch) = make_det_worker(16);
let mut coop = Worker::single(w, 3, 16);
assert!(coop.next_plan().unwrap().is_some());
assert!(coop.next_plan().unwrap().is_some());
assert!(coop.next_plan().unwrap().is_some());
assert!(coop.next_plan().unwrap().is_none(), "None after num_epochs");
assert!(coop.next_plan().unwrap().is_none(), "stays None");
}
#[test]
fn worker_step_without_batch_errs() {
let (w, _ch) = make_det_worker(16);
let mut coop = Worker::single(w, 1, 16);
let opts = test_opts();
let loss = Variable::new(Tensor::full(&[1], 1.0, opts).unwrap(), true);
assert!(
coop.step(&loss).is_err(),
"step with no batch in flight must error"
);
}
#[test]
fn worker_sync_now_drains_reduce_in_step() {
let total = 16;
let (w, ch) = make_det_worker(total);
let mut coop = Worker::single(w, 1, total);
assert!(coop.next_plan().unwrap().is_some());
let batch = coop.next_batch().unwrap().expect("first batch");
let loss = mse_train(coop.model(), &batch).unwrap();
loss.backward().unwrap();
ch.control_tx.send(ControlMsg::SyncNow).unwrap();
coop.step(&loss).unwrap();
let mut saw_sync_ack = false;
while let Ok(msg) = ch.timing_rx.try_recv() {
if matches!(msg, TimingMsg::SyncAck { .. }) {
saw_sync_ack = true;
}
}
assert!(
saw_sync_ack,
"step() must drain the SyncNow and emit a SyncAck (reduce path ran)"
);
}
#[test]
fn builder_into_worker_single_device_trains() {
if crate::tensor::usable_cuda_devices().len() >= 2 {
return;
}
use crate::distributed::Trainer;
let total = 16;
let dataset: Arc<dyn BatchDataSet> = Arc::new(DeterministicDataset { n: total });
let mut w = Trainer::builder(
|d| Linear::on_device(4, 2, d),
|p| crate::nn::SGD::new(p, 0.01, 0.0),
mse_train,
)
.dataset(dataset)
.batch_size(4)
.num_epochs(2)
.into_worker()
.unwrap();
let mut steps = 0usize;
while let Some(_plan) = w.next_plan().unwrap() {
while let Some(batch) = w.next_batch().unwrap() {
let loss = mse_train(w.model(), &batch).unwrap();
loss.backward().unwrap();
w.step(&loss).unwrap();
steps += 1;
}
}
assert_eq!(steps, 2 * (total / 4));
let state = w.finish().unwrap();
assert_eq!(state.params.len(), 2, "Linear: weight + bias");
}
#[test]
fn cooperative_worker_matches_managed_run_epoch_plan() {
let total = 16;
let epochs = 3;
let (mut managed, _cm) = make_det_worker(total);
let (mut coop_w, _cc) = make_det_worker(total);
sync_params(&mut coop_w, &mut managed);
for epoch in 0..epochs {
let plan = EpochPlan {
epoch,
partition_offset: 0,
partition_size: total,
};
managed.run_epoch_plan(&plan, &mse_train).unwrap();
}
let managed_snap = managed.snapshot_params();
let mut coop = Worker::single(coop_w, epochs, total);
while let Some(_plan) = coop.next_plan().unwrap() {
while let Some(batch) = coop.next_batch().unwrap() {
let loss = mse_train(coop.model(), &batch).unwrap();
loss.backward().unwrap();
coop.step(&loss).unwrap();
}
}
let coop_state = coop.finish().unwrap();
assert_eq!(managed_snap.params.len(), coop_state.params.len());
for (i, (m, c)) in managed_snap
.params
.iter()
.zip(&coop_state.params)
.enumerate()
{
let diff: f64 = m.sub(c).unwrap().abs().unwrap().sum().unwrap().item().unwrap();
assert!(
diff < 1e-6,
"param {i} diverged between managed and cooperative: L1 diff = {diff}"
);
}
}