use rumus::nn::{self, Linear, Module};
use rumus::optim::AdamW;
use rumus::tensor::Tensor;
use rumus::train::Trainer;
#[derive(Module)]
struct ClassifierMLP {
fc1: Linear,
fc2: Linear,
}
impl ClassifierMLP {
fn new() -> Self {
Self {
fc1: Linear::new(4, 8, true),
fc2: Linear::new(8, 3, true),
}
}
fn forward(&self, x: &Tensor) -> Tensor {
let h = nn::relu(&self.fc1.forward(x));
self.fc2.forward(&h) }
}
fn make_dataset() -> (Tensor, Tensor) {
let mut data = Vec::with_capacity(12 * 4);
let mut targets = Vec::with_capacity(12);
for class in 0..3u32 {
for variant in 0..4u32 {
let mut row = [0.1f32; 4];
row[class as usize] = 1.0;
row[3] = variant as f32 * 0.05;
data.extend_from_slice(&row);
targets.push(class as f32);
}
}
(
Tensor::new(data, vec![12, 4]),
Tensor::new(targets, vec![12]),
)
}
#[test]
fn test_cpu_cross_entropy_training() {
let (inputs, targets) = make_dataset();
let model = ClassifierMLP::new();
let optimizer = AdamW::new(model.parameters(), 0.01);
let mut trainer = Trainer::new(optimizer);
let mut final_loss = f32::MAX;
for _epoch in 0..200 {
trainer.reset_epoch();
let loss_val = trainer
.train_step(|| {
let logits = model.forward(&inputs);
nn::cross_entropy_loss(&logits, &targets)
})
.expect("train_step failed");
final_loss = loss_val;
}
assert!(
final_loss < 0.15,
"CPU 3-class training did not converge: final loss = {:.6}",
final_loss,
);
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_cross_entropy_training() {
use rumus::backend::gpu::context::GpuContext;
use rumus::nn::ModuleToGpu;
if !GpuContext::is_available() {
eprintln!("Skipping GPU training test: no GPU available");
return;
}
let (inputs, targets) = make_dataset();
let model = ClassifierMLP::new();
model.to_gpu();
inputs.to_gpu();
targets.to_gpu();
let optimizer = AdamW::new(model.parameters(), 0.01);
let mut trainer = Trainer::new(optimizer);
let ctx = GpuContext::get().unwrap();
let mut final_loss = f32::MAX;
for epoch in 0..200 {
trainer.reset_epoch();
let loss_val = trainer
.train_step(|| {
let logits = model.forward(&inputs);
nn::cross_entropy_loss(&logits, &targets)
})
.expect("train_step failed");
final_loss = loss_val;
if epoch == 1 {
let count = ctx.pool.cached_count();
assert!(
count > 0,
"BufferPool is empty after epoch 1 — buffers are leaking \
instead of being recycled! cached_count = {}",
count,
);
}
}
assert!(
final_loss < 0.15,
"GPU 3-class training did not converge: final loss = {:.6}",
final_loss,
);
let pool_size = ctx.pool.cached_count();
assert!(
pool_size > 0,
"BufferPool is empty after 200 steps — Drop is not returning buffers!",
);
}
#[test]
fn test_trainer_api() {
let (inputs, targets) = make_dataset();
let model = ClassifierMLP::new();
let optimizer = AdamW::new(model.parameters(), 0.01);
let mut trainer = Trainer::new(optimizer);
assert_eq!(trainer.epoch_avg_loss(), 0.0);
for _ in 0..3 {
trainer
.train_step(|| {
let logits = model.forward(&inputs);
nn::cross_entropy_loss(&logits, &targets)
})
.unwrap();
}
let avg = trainer.epoch_avg_loss();
assert!(avg > 0.0 && avg.is_finite(), "bad epoch avg: {}", avg);
trainer.reset_epoch();
assert_eq!(trainer.epoch_avg_loss(), 0.0);
}