use std::sync::Mutex;
use bench_harness::rng::Xorshift64Star;
use fandhe_ai::Tensor;
use fandhe_ai::compat::Sequential;
use fandhe_ai::data::{
DataLoader, DataLoaderConfig, PrefetchConfig, PrefetchDataLoader, RandomSampler,
SamplerDataLoader, TensorDataset,
};
use fandhe_ai::optim::{Sgd, SgdConfig};
fn test_lock() -> &'static Mutex<()> {
static LOCK: Mutex<()> = Mutex::new(());
&LOCK
}
const N: usize = 32;
const D_IN: usize = 4;
const D_HIDDEN: usize = 8;
const D_OUT: usize = 2;
fn gen_dataset(seed: u64) -> (Tensor<f32>, Tensor<f32>) {
let mut rng = Xorshift64Star::new(seed);
let x = rng.fill_vec(N * D_IN);
let y = rng.fill_vec(N * D_OUT);
(
Tensor::new(x, &[N, D_IN])
.unwrap_or_else(|e| panic!("test fixture: x の shape 構築に失敗: {e}")),
Tensor::new(y, &[N, D_OUT])
.unwrap_or_else(|e| panic!("test fixture: y の shape 構築に失敗: {e}")),
)
}
fn build_model() -> Sequential {
Sequential::new()
.add_linear(D_IN, D_HIDDEN, 0x1111_1111)
.unwrap_or_else(|e| panic!("test fixture: 層 1 の構築に失敗: {e}"))
.add_relu()
.add_linear(D_HIDDEN, D_OUT, 0x2222_2222)
.unwrap_or_else(|e| panic!("test fixture: 層 2 の構築に失敗: {e}"))
}
fn scalar(t: &Tensor<f32>) -> f32 {
t.get(&[])
.unwrap_or_else(|| panic!("test fixture: スカラー shape [] のはず"))
}
#[test]
fn prefetch_data_loader_matches_facade_data_loader_bit_identical() {
let _guard = test_lock().lock().unwrap_or_else(|p| p.into_inner());
let (x_data, _) = gen_dataset(0x1111);
fandhe_ai::manual_seed(0x2222);
let expected: Vec<Vec<f32>> = {
let ds = TensorDataset::new(x_data.clone())
.unwrap_or_else(|e| panic!("test fixture: TensorDataset::new が失敗: {e}"));
let loader = DataLoader::new(ds, DataLoaderConfig::new(4).shuffle(true))
.unwrap_or_else(|e| panic!("test fixture: DataLoader::new が失敗: {e}"));
loader
.iter()
.map(|b| {
b.unwrap_or_else(|e| panic!("test fixture: batch が失敗: {e}"))
.host_slice()
.to_vec()
})
.collect()
};
for (workers, depth) in [(0usize, 1usize), (1, 2), (2, 2), (4, 8)] {
fandhe_ai::manual_seed(0x2222);
let ds = TensorDataset::new(x_data.clone())
.unwrap_or_else(|e| panic!("test fixture: TensorDataset::new が失敗: {e}"));
let sampler = RandomSampler::new(N, 4, false)
.unwrap_or_else(|e| panic!("test fixture: RandomSampler::new が失敗: {e}"));
let config = PrefetchConfig::new(workers, depth)
.unwrap_or_else(|e| panic!("test fixture: PrefetchConfig::new が失敗: {e}"));
let mut loader = PrefetchDataLoader::new(ds, sampler, config)
.unwrap_or_else(|e| panic!("test fixture: PrefetchDataLoader::new が失敗: {e}"));
let actual: Vec<Vec<f32>> = loader
.iter()
.map(|b| {
b.unwrap_or_else(|e| panic!("test fixture: batch が失敗: {e}"))
.host_slice()
.to_vec()
})
.collect();
assert_eq!(
actual, expected,
"workers={workers} depth={depth} で DataLoader{{shuffle=true}} と\
bit 完全一致するはず"
);
}
}
#[test]
fn prefetch_training_loop_matches_sequential_bit_identical_final_params() {
let _guard = test_lock().lock().unwrap_or_else(|p| p.into_inner());
const EPOCHS: usize = 10;
const SEED: u64 = 0x9999;
let (x_data, y_data) = gen_dataset(0xABCD);
fn train_with_batches(
mut epoch_batches: impl FnMut() -> Vec<(Tensor<f32>, Tensor<f32>)>,
) -> (Vec<f32>, Vec<Vec<f32>>) {
let mut model = build_model();
let mut sgd = Sgd::new(SgdConfig::new(0.05))
.unwrap_or_else(|e| panic!("test fixture: Sgd::new が失敗した: {e}"));
let mut epoch_losses = Vec::with_capacity(EPOCHS);
for _ in 0..EPOCHS {
let mut epoch_loss_sum = 0.0f32;
let mut batch_count = 0usize;
for (x_batch, y_batch) in epoch_batches() {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_batch);
let y = tape.var(&y_batch);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗: {e}"));
epoch_loss_sum += scalar(&loss.to_tensor());
batch_count += 1;
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗: {e}"));
let param_refs = model.trainable_parameters();
sgd.step(¶m_refs, &grad_refs)
.unwrap_or_else(|e| panic!("test fixture: Sgd::step が失敗: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗: {e}"));
}
epoch_losses.push(epoch_loss_sum / batch_count as f32);
}
let final_params: Vec<Vec<f32>> = model
.trainable_parameters()
.into_iter()
.map(|p| p.host_slice().to_vec())
.collect();
(epoch_losses, final_params)
}
fandhe_ai::manual_seed(SEED);
let (seq_losses, seq_final) = {
let ds_x = TensorDataset::new(x_data.clone())
.unwrap_or_else(|e| panic!("test fixture: TensorDataset::new(x) が失敗: {e}"));
let ds_y = TensorDataset::new(y_data.clone())
.unwrap_or_else(|e| panic!("test fixture: TensorDataset::new(y) が失敗: {e}"));
let sampler = RandomSampler::new(N, 4, false)
.unwrap_or_else(|e| panic!("test fixture: RandomSampler::new が失敗: {e}"));
let mut loader = SamplerDataLoader::new((ds_x, ds_y), sampler)
.unwrap_or_else(|e| panic!("test fixture: SamplerDataLoader::new が失敗: {e}"));
train_with_batches(|| {
loader
.iter()
.map(|b| b.unwrap_or_else(|e| panic!("test fixture: batch が失敗: {e}")))
.collect()
})
};
fandhe_ai::manual_seed(SEED);
let (par_losses, par_final) = {
let ds_x = TensorDataset::new(x_data.clone())
.unwrap_or_else(|e| panic!("test fixture: TensorDataset::new(x) が失敗: {e}"));
let ds_y = TensorDataset::new(y_data.clone())
.unwrap_or_else(|e| panic!("test fixture: TensorDataset::new(y) が失敗: {e}"));
let sampler = RandomSampler::new(N, 4, false)
.unwrap_or_else(|e| panic!("test fixture: RandomSampler::new が失敗: {e}"));
let config = PrefetchConfig::new(4, 4)
.unwrap_or_else(|e| panic!("test fixture: PrefetchConfig::new が失敗: {e}"));
let mut loader = PrefetchDataLoader::new((ds_x, ds_y), sampler, config)
.unwrap_or_else(|e| panic!("test fixture: PrefetchDataLoader::new が失敗: {e}"));
train_with_batches(|| {
loader
.iter()
.map(|b| b.unwrap_or_else(|e| panic!("test fixture: batch が失敗: {e}")))
.collect()
})
};
assert_eq!(
seq_losses, par_losses,
"SamplerDataLoader と PrefetchDataLoader(workers=4) の双方が\
yield した実バッチで学習した場合、loss 系列が一致するはず"
);
assert_eq!(
seq_final, par_final,
"PrefetchDataLoader(workers=4) のバッチで学習した最終パラメータが\
SamplerDataLoader 版と bit 完全一致するはず"
);
let first = seq_losses[0];
let last = *seq_losses.last().unwrap();
assert!(
last < first,
"ミニバッチ学習で loss が減少するはず: first={first} last={last}"
);
}