use bench_harness::rng::Xorshift64Star;
use fandhe_ai::Tensor;
use fandhe_ai::compat::Sequential;
use fandhe_ai::optim::{
GradScaler, GradScalerConfig, Sgd, SgdConfig, UnscaleResult, clip_grad_norm,
};
const BATCH: usize = 4;
const D_IN: usize = 8;
const D_HIDDEN: usize = 16;
const D_OUT: usize = 4;
const SEED_DATA: u64 = 0xC0FFEE;
const SEED_L1: u64 = 0x1111_1111;
const SEED_L2: u64 = 0x2222_2222;
fn gen_regression_data(seed: u64) -> (Tensor<f32>, Tensor<f32>) {
let mut rng = Xorshift64Star::new(seed);
let x = rng.fill_vec(BATCH * D_IN);
let y = rng.fill_vec(BATCH * D_OUT);
(
Tensor::new(x, &[BATCH, D_IN])
.unwrap_or_else(|e| panic!("test fixture: x の shape 構築に失敗: {e}")),
Tensor::new(y, &[BATCH, D_OUT])
.unwrap_or_else(|e| panic!("test fixture: y の shape 構築に失敗: {e}")),
)
}
fn build_model() -> Sequential {
Sequential::new()
.add_linear(D_IN, D_HIDDEN, SEED_L1)
.unwrap_or_else(|e| panic!("test fixture: 層 1 の構築に失敗: {e}"))
.add_relu()
.add_linear(D_HIDDEN, D_OUT, SEED_L2)
.unwrap_or_else(|e| panic!("test fixture: 層 2 の構築に失敗: {e}"))
}
fn scalar(t: &Tensor<f32>) -> f32 {
t.get(&[])
.unwrap_or_else(|| panic!("test fixture: スカラー shape [] のはず"))
}
fn train_with_sgd_amp_and_clip(
model: &mut Sequential,
steps: usize,
lr: f32,
max_norm: f32,
config: GradScalerConfig,
) -> (Vec<f32>, usize) {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut sgd = Sgd::new(SgdConfig::new(lr))
.unwrap_or_else(|e| panic!("test fixture: Sgd::new が失敗した: {e}"));
let mut scaler = GradScaler::new(config)
.unwrap_or_else(|e| panic!("test fixture: GradScaler::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
let mut skipped_steps = 0usize;
for _ in 0..steps {
let outcome = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
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}"));
log.push(scalar(&loss.to_tensor()));
let scaled_loss = scaler
.scale_loss(&loss)
.unwrap_or_else(|e| panic!("test fixture: scale_loss が失敗した: {e}"));
let grads = tape
.backward(&scaled_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 unscale_result: UnscaleResult = scaler
.unscale(&grad_refs)
.unwrap_or_else(|e| panic!("test fixture: GradScaler::unscale が失敗した: {e}"));
if unscale_result.should_skip_step() {
None
} else {
let unscaled_refs: Vec<&Tensor<f32>> = unscale_result.grads.iter().collect();
let clip_result = clip_grad_norm(&unscaled_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let clipped_refs: Vec<&Tensor<f32>> = clip_result.grads.iter().collect();
let param_refs = model.trainable_parameters();
let updated = sgd
.step(¶m_refs, &clipped_refs)
.unwrap_or_else(|e| panic!("test fixture: Sgd::step が失敗した: {e}"));
Some(updated)
}
};
match outcome {
Some(updated) => {
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
scaler
.update(false)
.unwrap_or_else(|e| panic!("test fixture: GradScaler::update が失敗した: {e}"));
}
None => {
skipped_steps += 1;
scaler
.update(true)
.unwrap_or_else(|e| panic!("test fixture: GradScaler::update が失敗した: {e}"));
}
}
}
(log, skipped_steps)
}
#[test]
fn sgd_with_amp_and_clip_converges_via_facade_only() {
const STEPS: usize = 100;
const LR: f32 = 0.05;
const MAX_NORM: f32 = 10.0;
let config = GradScalerConfig {
init_scale: 256.0,
growth_factor: 2.0,
backoff_factor: 0.5,
growth_interval: 1000,
};
let mut model = build_model();
let (log, skipped_steps) = train_with_sgd_amp_and_clip(&mut model, STEPS, LR, MAX_NORM, config);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
assert_eq!(
skipped_steps, 0,
"この回帰データ・init_scale では非有限勾配は発生しないはず"
);
}
#[test]
fn amp_one_step_grads_match_non_amp_bit_exact_on_cpu() {
const SCALE: f32 = 256.0; let (x_data, y_data) = gen_regression_data(SEED_DATA);
let model_a = build_model();
let scaled_bits: Vec<Vec<u32>> = {
let tape = fandhe_ai::tape();
let bound = model_a.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward(A) が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss(A) が失敗した: {e}"));
let scaled_loss = fandhe_ai::optim::scale_loss(&loss, SCALE)
.unwrap_or_else(|e| panic!("test fixture: scale_loss が失敗した: {e}"));
let grads = tape
.backward(&scaled_loss)
.unwrap_or_else(|e| panic!("test fixture: backward(A) が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads(A) が失敗した: {e}"));
let unscale_result = fandhe_ai::optim::unscale_grads(&grad_refs, SCALE)
.unwrap_or_else(|e| panic!("test fixture: unscale_grads が失敗した: {e}"));
assert!(
!unscale_result.found_non_finite,
"test fixture: この形状・スケールで非有限勾配は発生しないはず"
);
unscale_result
.grads
.iter()
.map(|t| t.host_slice().iter().map(|v| v.to_bits()).collect())
.collect()
};
let model_b = build_model();
let unscaled_bits: Vec<Vec<u32>> = {
let tape = fandhe_ai::tape();
let bound = model_b.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward(B) が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss(B) が失敗した: {e}"));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward(B) が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads(B) が失敗した: {e}"));
grad_refs
.iter()
.map(|t| t.host_slice().iter().map(|v| v.to_bits()).collect())
.collect()
};
assert_eq!(
scaled_bits.len(),
unscaled_bits.len(),
"test fixture: 勾配テンソルの本数が一致するはず(weight/bias × 2 層)"
);
for (i, (a, b)) in scaled_bits.iter().zip(unscaled_bits.iter()).enumerate() {
assert_eq!(
a, b,
"勾配テンソル #{i}: scale_loss→backward→unscale と非スケール backward の\
勾配が bit 単位で一致しない(2 のべき乗スケールの丸め可換性が facade 経路で崩れている)"
);
}
}
#[test]
fn amp_non_finite_step_is_skipped_and_scaler_backs_off() {
let config = GradScalerConfig {
init_scale: 256.0,
growth_factor: 2.0,
backoff_factor: 0.5,
growth_interval: 1000,
};
let mut scaler = GradScaler::new(config)
.unwrap_or_else(|e| panic!("test fixture: GradScaler::new が失敗した: {e}"));
assert_eq!(scaler.scale(), 256.0);
let n = 4usize;
let inf_grad = Tensor::new(vec![f32::INFINITY; n], &[n])
.unwrap_or_else(|e| panic!("test fixture: inf_grad の構築に失敗: {e}"));
let unscale_result = scaler
.unscale(&[&inf_grad])
.unwrap_or_else(|e| panic!("test fixture: unscale が失敗した: {e}"));
assert!(
unscale_result.should_skip_step(),
"非有限勾配は should_skip_step()=true になるはず"
);
scaler
.update(true)
.unwrap_or_else(|e| panic!("test fixture: GradScaler::update(true) が失敗した: {e}"));
assert_eq!(
scaler.scale(),
128.0,
"backoff 後の scale は init_scale * backoff_factor = 256.0 * 0.5 = 128.0 のはず"
);
assert_eq!(
scaler.growth_tracker(),
0,
"backoff は growth_tracker を 0 へリセットするはず"
);
}