use super::*;
#[test]
fn test_checkpoint_msg_is_send() {
fn assert_send<T: Send>() {}
assert_send::<ControlMsg>();
}
#[test]
fn test_checkpoint_fn_called_on_dispatch() {
use std::sync::atomic::{AtomicU64, Ordering};
let (mut worker, ch) = make_test_worker();
let called_version = Arc::new(AtomicU64::new(0));
let cv = called_version.clone();
worker.checkpoint_fn = Some(Arc::new(move |ver, _model| {
cv.store(ver, Ordering::Relaxed);
Ok(())
}));
ch.control_tx.send(ControlMsg::Checkpoint { version: 7, target_rank: 0 }).unwrap();
worker.handle_control().unwrap();
assert_eq!(called_version.load(Ordering::Relaxed), 7);
}
#[test]
fn test_checkpoint_error_logged_not_propagated() {
let (mut worker, ch) = make_test_worker();
worker.checkpoint_fn = Some(Arc::new(|_ver, _model| {
Err(TensorError::new("disk full"))
}));
ch.control_tx.send(ControlMsg::Checkpoint { version: 1, target_rank: 0 }).unwrap();
let shutdown = worker.handle_control().unwrap();
assert!(!shutdown);
}
#[test]
fn test_eval_fn_called_on_dispatch_and_emits_result() {
use std::sync::atomic::{AtomicU64, Ordering};
let (mut worker, ch) = make_test_worker();
let called_epoch = Arc::new(AtomicU64::new(u64::MAX));
let ce = called_epoch.clone();
worker.eval_fn = Some(Arc::new(move |_model, _ds| {
ce.store(7, Ordering::Relaxed);
Ok(0.42)
}));
worker.eval_dataset = Some(Arc::new(TestDataset { n: 4 }));
ch.control_tx
.send(ControlMsg::ExecuteEvalCallback {
schedule_id: 99,
epoch: 7,
target_rank: 0,
})
.unwrap();
let shutdown = worker.handle_control().unwrap();
assert!(!shutdown);
assert_eq!(called_epoch.load(Ordering::Relaxed), 7);
let mut got = None;
while let Ok(m) = ch.timing_rx.try_recv() {
if let TimingMsg::EvalResult {
schedule_id,
epoch,
metric,
elapsed_ms,
error,
rank,
} = m
{
got = Some((rank, schedule_id, epoch, metric, elapsed_ms, error));
break;
}
}
let (rank, schedule_id, epoch, metric, elapsed_ms, error) =
got.expect("EvalResult should be emitted");
assert_eq!(rank, 0);
assert_eq!(schedule_id, 99);
assert_eq!(epoch, 7);
assert!((metric - 0.42).abs() < 1e-9);
assert!(error.is_none());
assert!(
elapsed_ms >= 0.0,
"elapsed_ms should be non-negative, got {elapsed_ms}",
);
}
#[test]
fn test_eval_fn_error_surfaces_in_timing_msg() {
let (mut worker, ch) = make_test_worker();
worker.eval_fn = Some(Arc::new(|_model, _ds| {
Err(TensorError::new("eval blew up"))
}));
worker.eval_dataset = Some(Arc::new(TestDataset { n: 4 }));
ch.control_tx
.send(ControlMsg::ExecuteEvalCallback {
schedule_id: 1,
epoch: 2,
target_rank: 0,
})
.unwrap();
let shutdown = worker.handle_control().unwrap();
assert!(!shutdown);
let mut got_err = None;
while let Ok(m) = ch.timing_rx.try_recv() {
if let TimingMsg::EvalResult { error, metric, .. } = m {
got_err = Some((error, metric));
break;
}
}
let (error, metric) = got_err.expect("EvalResult should be emitted");
assert!(error.unwrap().contains("eval blew up"));
assert!(metric.is_nan());
}
#[test]
fn test_drain_pending_shutdown_consumes_queued_shutdown() {
let (mut worker, ch) = make_test_worker();
assert!(!worker.drain_pending_shutdown());
ch.control_tx.send(ControlMsg::Shutdown).unwrap();
assert!(worker.drain_pending_shutdown());
ch.control_tx
.send(ControlMsg::ShutdownWithSave {
reason: crate::distributed::SaveReason::SingleSurvivor,
})
.unwrap();
assert!(worker.drain_pending_shutdown());
ch.control_tx
.send(ControlMsg::SetGlobalStep(42))
.unwrap();
assert!(!worker.drain_pending_shutdown());
}
#[test]
fn test_eval_fn_none_is_noop() {
let (mut worker, ch) = make_test_worker();
ch.control_tx
.send(ControlMsg::ExecuteEvalCallback {
schedule_id: 1,
epoch: 2,
target_rank: 0,
})
.unwrap();
let shutdown = worker.handle_control().unwrap();
assert!(!shutdown);
let mut found_eval_result = false;
while let Ok(m) = ch.timing_rx.try_recv() {
if matches!(m, TimingMsg::EvalResult { .. }) {
found_eval_result = true;
break;
}
}
assert!(
!found_eval_result,
"non-chosen rank should not emit EvalResult"
);
}
#[test]
fn shutdown_with_save_writes_model_and_optim_to_save_path() {
let dev = test_device();
let dir = std::env::temp_dir().join(format!(
"flodl_shutdown_with_save_{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let stem = dir.join("ckpt_final");
let stem_str = stem.to_str().unwrap().to_string();
let tmp_model = Linear::on_device(4, 2, dev).unwrap();
let tmp_params: Vec<Tensor> =
tmp_model.parameters().iter().map(|p| p.variable.data()).collect();
let tmp_buffers: Vec<Tensor> =
tmp_model.buffers().iter().map(|b| b.get()).collect();
drop(tmp_model);
let config = WorkerConfig {
rank: 0,
world_size: 3,
device: dev,
initial_params: tmp_params,
initial_buffers: tmp_buffers,
total_samples: 16,
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: Some(stem_str.clone()),
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), ch) =
GpuWorker::<Linear>::channels();
let dataset: Arc<dyn crate::data::BatchDataSet> =
Arc::new(TestDataset { n: 16 });
let mut 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();
ch.control_tx
.send(ControlMsg::ShutdownWithSave {
reason: crate::distributed::SaveReason::MaxFailureExceeded,
})
.unwrap();
let shutdown = worker.handle_control().unwrap();
assert!(shutdown, "ShutdownWithSave must trigger shutdown");
let model_path =
crate::distributed::CheckpointBundle::model_path(&stem_str);
let optim_path =
crate::distributed::CheckpointBundle::optim_path(&stem_str);
let meta_path = crate::distributed::CheckpointBundle::meta_path(&stem_str);
assert!(model_path.exists(), "model file missing: {}", model_path.display());
assert!(optim_path.exists(), "optim file missing: {}", optim_path.display());
assert!(
!meta_path.exists(),
"meta file should NOT be written by worker (controller's job): {}",
meta_path.display(),
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn shutdown_with_save_no_path_exits_without_write() {
let (mut worker, ch) = make_test_worker();
ch.control_tx
.send(ControlMsg::ShutdownWithSave {
reason: crate::distributed::SaveReason::SingleSurvivor,
})
.unwrap();
let shutdown = worker.handle_control().unwrap();
assert!(
shutdown,
"ShutdownWithSave must trigger shutdown even without save_path"
);
}