#[cfg(test)]
mod tests {
use crate::lora::LoRALayer;
use crate::Tensor;
use approx::assert_abs_diff_eq;
use proptest::prelude::*;
proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(100))]
#[test]
fn prop_base_weight_always_frozen(
d_out in 2usize..16,
d_in in 2usize..16,
rank in 1usize..4,
alpha in 1.0f32..32.0,
) {
let size = d_out * d_in;
let base_weight = Tensor::from_vec(vec![1.0; size], false);
let lora = LoRALayer::new(base_weight, d_out, d_in, rank, alpha);
prop_assert!(
!lora.base_weight().requires_grad(),
"Base weight should always be frozen"
);
}
#[test]
fn prop_lora_params_always_trainable(
d_out in 2usize..16,
d_in in 2usize..16,
rank in 1usize..4,
alpha in 1.0f32..32.0,
) {
let size = d_out * d_in;
let base_weight = Tensor::from_vec(vec![1.0; size], false);
let lora = LoRALayer::new(base_weight, d_out, d_in, rank, alpha);
prop_assert!(lora.lora_a().requires_grad(), "LoRA A should be trainable");
prop_assert!(lora.lora_b().requires_grad(), "LoRA B should be trainable");
}
#[test]
fn prop_trainable_params_count(
d_out in 2usize..16,
d_in in 2usize..16,
rank in 1usize..4,
) {
let size = d_out * d_in;
let base_weight = Tensor::from_vec(vec![1.0; size], false);
let mut lora = LoRALayer::new(base_weight, d_out, d_in, rank, 4.0);
let params = lora.trainable_params();
prop_assert_eq!(params.len(), 2, "Should have exactly 2 trainable params");
prop_assert_eq!(params[0].len(), rank * d_in, "A should be [rank * d_in]");
prop_assert_eq!(params[1].len(), d_out * rank, "B should be [d_out * rank]");
}
#[test]
fn prop_gradient_accumulation(
d in 2usize..8,
initial_grad in prop::collection::vec(-10.0f32..10.0, 2..8),
additional_grad in prop::collection::vec(-10.0f32..10.0, 2..8),
) {
let len = initial_grad.len().min(additional_grad.len()).min(d * d);
if len < 2 { return Ok(()); }
let initial: Vec<f32> = initial_grad[..len].to_vec();
let additional: Vec<f32> = additional_grad[..len].to_vec();
let base_weight = Tensor::from_vec(vec![1.0; len], false);
let mut lora = LoRALayer::new(base_weight, len, 1, 1, 1.0);
let a_len = lora.lora_a().len();
if initial.len() < a_len { return Ok(()); }
let init_slice: Vec<f32> = initial[..a_len].to_vec();
let add_slice: Vec<f32> = additional[..a_len].to_vec();
lora.lora_a_mut().set_grad(ndarray::arr1(&init_slice));
lora.lora_a_mut().accumulate_grad(ndarray::arr1(&add_slice));
let grad = lora.lora_a().grad().expect("gradient should be available");
for i in 0..a_len {
let expected = init_slice[i] + add_slice[i];
prop_assert!(
(grad[i] - expected).abs() < 1e-5,
"Accumulated gradient at {} should be {}, got {}",
i, expected, grad[i]
);
}
}
}
#[test]
fn test_base_weight_frozen() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let lora = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
assert!(!lora.base_weight().requires_grad(), "Base weight should be frozen");
}
#[test]
fn test_lora_params_trainable() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let lora = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
assert!(lora.lora_a().requires_grad(), "LoRA A should be trainable");
assert!(lora.lora_b().requires_grad(), "LoRA B should be trainable");
}
#[test]
fn test_gradient_flow_to_lora_params() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let mut lora = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
*lora.lora_a_mut().data_mut() = ndarray::arr1(&[0.5, 0.5]);
*lora.lora_b_mut().data_mut() = ndarray::arr1(&[0.5, 0.5]);
let x = Tensor::from_vec(vec![1.0, 1.0], true);
let _output = lora.forward(&x);
lora.lora_a_mut().set_grad(ndarray::arr1(&[0.1, 0.1]));
lora.lora_b_mut().set_grad(ndarray::arr1(&[0.1, 0.1]));
assert!(lora.lora_a().grad().is_some(), "LoRA A should have gradient");
assert!(lora.lora_b().grad().is_some(), "LoRA B should have gradient");
}
#[test]
fn test_trainable_params_have_requires_grad() {
let base_weight = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], false);
let mut lora = LoRALayer::new(base_weight, 2, 2, 2, 4.0);
let params = lora.trainable_params();
for param in params {
assert!(param.requires_grad(), "Trainable parameter should require gradients");
}
}
#[test]
fn test_gradient_isolation_merged_vs_unmerged() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let mut lora_unmerged = LoRALayer::new(base_weight.clone(), 2, 2, 1, 1.0);
let mut lora_merged = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
*lora_unmerged.lora_a_mut().data_mut() = ndarray::arr1(&[0.5, 0.5]);
*lora_unmerged.lora_b_mut().data_mut() = ndarray::arr1(&[0.5, 0.5]);
*lora_merged.lora_a_mut().data_mut() = ndarray::arr1(&[0.5, 0.5]);
*lora_merged.lora_b_mut().data_mut() = ndarray::arr1(&[0.5, 0.5]);
lora_merged.merge();
assert!(lora_unmerged.lora_a().requires_grad());
assert!(lora_unmerged.lora_b().requires_grad());
assert!(lora_merged.lora_a().requires_grad());
assert!(lora_merged.lora_b().requires_grad());
}
#[test]
fn test_zero_grad_on_trainable_params() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let mut lora = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
lora.lora_a_mut().set_grad(ndarray::arr1(&[1.0, 2.0]));
lora.lora_b_mut().set_grad(ndarray::arr1(&[3.0, 4.0]));
assert!(lora.lora_a().grad().is_some());
assert!(lora.lora_b().grad().is_some());
lora.lora_a_mut().zero_grad();
lora.lora_b_mut().zero_grad();
assert!(lora.lora_a().grad().is_none());
assert!(lora.lora_b().grad().is_none());
}
#[test]
fn test_gradient_accumulation_on_lora_params() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let mut lora = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
lora.lora_a_mut().set_grad(ndarray::arr1(&[1.0, 2.0]));
lora.lora_a_mut().accumulate_grad(ndarray::arr1(&[0.5, 0.5]));
let grad = lora.lora_a().grad().expect("gradient should be available");
assert_abs_diff_eq!(grad[0], 1.5, epsilon = 1e-6); assert_abs_diff_eq!(grad[1], 2.5, epsilon = 1e-6); }
#[test]
fn test_multiple_forward_passes_gradient_ready() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let lora = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
let x = Tensor::from_vec(vec![1.0, 1.0], true);
for _ in 0..3 {
let _output = lora.forward(&x);
assert!(lora.lora_a().requires_grad());
assert!(lora.lora_b().requires_grad());
}
}
#[test]
fn test_lora_params_independent_gradients() {
let base_weight = Tensor::from_vec(vec![1.0, 0.0, 0.0, 1.0], false);
let mut lora = LoRALayer::new(base_weight, 2, 2, 1, 1.0);
lora.lora_a_mut().set_grad(ndarray::arr1(&[1.0, 2.0]));
lora.lora_b_mut().set_grad(ndarray::arr1(&[3.0, 4.0]));
let grad_a = lora.lora_a().grad().expect("gradient should be available");
let grad_b = lora.lora_b().grad().expect("gradient should be available");
assert_abs_diff_eq!(grad_a[0], 1.0, epsilon = 1e-6);
assert_abs_diff_eq!(grad_a[1], 2.0, epsilon = 1e-6);
assert_abs_diff_eq!(grad_b[0], 3.0, epsilon = 1e-6);
assert_abs_diff_eq!(grad_b[1], 4.0, epsilon = 1e-6);
}
#[test]
fn test_optimizer_integration_readiness() {
let base_weight = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], false);
let mut lora = LoRALayer::new(base_weight, 2, 2, 2, 4.0);
let params = lora.trainable_params();
assert_eq!(params.len(), 2);
for param in ¶ms {
assert!(param.requires_grad());
}
for param in params {
param.set_grad(ndarray::Array1::ones(param.len()));
let update = param.grad().expect("gradient should be available") * 0.01;
*param.data_mut() = param.data() - &update;
assert!(param.grad().is_some());
}
}
}