#[inline]
pub fn ternarize(w: f32, threshold: f32) -> f32 {
if w > threshold {
1.0
} else if w < -threshold {
-1.0
} else {
0.0
}
}
pub fn ternary_threshold(weights: &[f32], ratio: f32) -> f32 {
if weights.is_empty() {
return 0.0;
}
let mean_abs = weights.iter().map(|w| w.abs()).sum::<f32>() / weights.len() as f32;
ratio * mean_abs
}
pub fn ternarize_weights(weights: &mut [f32], ratio: f32) -> f32 {
let thresh = ternary_threshold(weights, ratio);
let mut sum_abs = 0.0f64;
let mut count = 0usize;
for w in weights.iter() {
if w.abs() > thresh {
sum_abs += w.abs() as f64;
count += 1;
}
}
let scale = if count > 0 {
(sum_abs / count as f64) as f32
} else {
1.0
};
for w in weights.iter_mut() {
let t = ternarize(*w, thresh);
*w = t * scale;
}
scale
}
pub struct StraightThroughEstimator {
pub ratio: f32,
}
impl StraightThroughEstimator {
pub const fn new(ratio: f32) -> Self {
Self { ratio }
}
pub fn forward(&self, weights: &[f32]) -> (Vec<f32>, f32) {
let mut ternary = weights.to_vec();
let scale = ternarize_weights(&mut ternary, self.ratio);
(ternary, scale)
}
#[inline]
pub fn backward(&self, grad: &[f32], clip: bool) -> Vec<f32> {
if clip {
grad.iter().map(|g| g.clamp(-1.0, 1.0)).collect()
} else {
grad.to_vec()
}
}
}
pub struct QatLinear {
pub weights: Vec<f32>,
pub rows: usize,
pub cols: usize,
pub ste: StraightThroughEstimator,
pub grad: Vec<f32>,
}
impl QatLinear {
pub fn new(weights: Vec<f32>, rows: usize, cols: usize, ratio: f32) -> Self {
let n = rows * cols;
assert_eq!(weights.len(), n);
Self {
weights,
rows,
cols,
ste: StraightThroughEstimator::new(ratio),
grad: vec![0.0; n],
}
}
pub fn forward(&self, input: &[f32]) -> (Vec<f32>, Vec<f32>) {
assert_eq!(input.len(), self.cols);
let (ternary_w, _scale) = self.ste.forward(&self.weights);
let mut output = vec![0.0f32; self.rows];
for r in 0..self.rows {
let row_start = r * self.cols;
let mut sum = 0.0f32;
for c in 0..self.cols {
sum += ternary_w[row_start + c] * input[c];
}
output[r] = sum;
}
(output, ternary_w)
}
pub fn backward(&mut self, grad_output: &[f32], input: &[f32]) -> Vec<f32> {
assert_eq!(grad_output.len(), self.rows);
assert_eq!(input.len(), self.cols);
for r in 0..self.rows {
let row_start = r * self.cols;
for c in 0..self.cols {
self.grad[row_start + c] += grad_output[r] * input[c];
}
}
let mut grad_input = vec![0.0f32; self.cols];
for c in 0..self.cols {
let mut sum = 0.0f32;
for r in 0..self.rows {
sum += self.weights[r * self.cols + c] * grad_output[r];
}
grad_input[c] = sum;
}
grad_input
}
pub fn zero_grad(&mut self) {
self.grad.fill(0.0);
}
}
pub struct AdamW {
pub lr: f32,
pub beta1: f32,
pub beta2: f32,
pub eps: f32,
pub weight_decay: f32,
m: Vec<f32>,
v: Vec<f32>,
t: usize,
}
impl AdamW {
pub fn new(num_params: usize, lr: f32) -> Self {
Self {
lr,
beta1: 0.9,
beta2: 0.999,
eps: 1e-8,
weight_decay: 0.01,
m: vec![0.0; num_params],
v: vec![0.0; num_params],
t: 0,
}
}
pub fn step(&mut self, weights: &mut [f32], grads: &[f32]) {
assert_eq!(weights.len(), grads.len());
assert_eq!(weights.len(), self.m.len());
self.t += 1;
let bc1 = 1.0 - self.beta1.powi(self.t as i32);
let bc2 = 1.0 - self.beta2.powi(self.t as i32);
for i in 0..weights.len() {
weights[i] *= 1.0 - self.lr * self.weight_decay;
self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grads[i];
self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grads[i] * grads[i];
let m_hat = self.m[i] / bc1;
let v_hat = self.v[i] / bc2;
weights[i] -= self.lr * m_hat / (v_hat.sqrt() + self.eps);
}
}
}
pub fn mse_loss(pred: &[f32], target: &[f32]) -> f32 {
assert_eq!(pred.len(), target.len());
let n = pred.len() as f32;
pred.iter()
.zip(target.iter())
.map(|(p, t)| (p - t) * (p - t))
.sum::<f32>()
/ n
}
pub fn mse_loss_grad(pred: &[f32], target: &[f32]) -> Vec<f32> {
let n = pred.len() as f32;
pred.iter()
.zip(target.iter())
.map(|(p, t)| 2.0 * (p - t) / n)
.collect()
}
pub fn qat_train_step(
layer: &mut QatLinear,
optimizer: &mut AdamW,
input: &[f32],
target: &[f32],
) -> f32 {
qat_train_step_l1(layer, optimizer, input, target, 0.0)
}
pub fn qat_train_step_l1(
layer: &mut QatLinear,
optimizer: &mut AdamW,
input: &[f32],
target: &[f32],
lambda_l1: f32,
) -> f32 {
layer.zero_grad();
let (output, _ternary_w) = layer.forward(input);
let mse = mse_loss(&output, target);
let grad_output = mse_loss_grad(&output, target);
let l1_penalty = if lambda_l1 > 0.0 {
lambda_l1 * layer.weights.iter().map(|w| w.abs()).sum::<f32>()
} else {
0.0
};
let _grad_input = layer.backward(&grad_output, input);
if lambda_l1 > 0.0 {
for (g, w) in layer.grad.iter_mut().zip(layer.weights.iter()) {
*g += lambda_l1 * w.signum();
}
}
let clipped_grad = layer.ste.backward(&layer.grad, true);
optimizer.step(&mut layer.weights, &clipped_grad);
mse + l1_penalty
}
pub fn sparsity_ratio(weights: &[f32], eps: f32) -> f32 {
if weights.is_empty() {
return 0.0;
}
let zeros = weights.iter().filter(|w| w.abs() < eps).count();
zeros as f32 / weights.len() as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ternarize_basic() {
assert_eq!(ternarize(0.5, 0.3), 1.0);
assert_eq!(ternarize(-0.5, 0.3), -1.0);
assert_eq!(ternarize(0.1, 0.3), 0.0);
assert_eq!(ternarize(0.0, 0.3), 0.0);
}
#[test]
fn test_ternary_threshold() {
let w = [1.0, -2.0, 0.5, -0.5];
let t = ternary_threshold(&w, 0.7);
assert!((t - 0.7).abs() < 1e-6);
}
#[test]
fn test_ternary_threshold_empty() {
assert_eq!(ternary_threshold(&[], 0.7), 0.0);
}
#[test]
fn test_ternarize_weights() {
let mut w = vec![1.0, -2.0, 0.1, -0.1, 3.0];
let scale = ternarize_weights(&mut w, 0.5);
for &v in &w {
assert!(
v.abs() < 1e-6 || (v.abs() - scale).abs() < 1e-5,
"value {v} not in {{-{scale}, 0, +{scale}}}"
);
}
assert!(scale > 0.0);
}
#[test]
fn test_ste_forward_produces_ternary() {
let ste = StraightThroughEstimator::new(0.7);
let weights = vec![1.0, -2.0, 0.01, 3.0, -0.5];
let (ternary, scale) = ste.forward(&weights);
assert_eq!(ternary.len(), weights.len());
for &v in &ternary {
assert!(
v.abs() < 1e-6 || (v.abs() - scale).abs() < 1e-5,
"STE output {v} not ternary (scale={scale})"
);
}
}
#[test]
fn test_ste_backward_identity() {
let ste = StraightThroughEstimator::new(0.7);
let grad = vec![0.5, -0.3, 0.1];
let out = ste.backward(&grad, false);
assert_eq!(out, grad);
}
#[test]
fn test_ste_backward_clipped() {
let ste = StraightThroughEstimator::new(0.7);
let grad = vec![2.0, -3.0, 0.5];
let out = ste.backward(&grad, true);
assert_eq!(out, vec![1.0, -1.0, 0.5]);
}
#[test]
fn test_qat_linear_forward() {
let weights = vec![1.0, 0.0, -1.0, 0.5, 0.5, 0.0];
let layer = QatLinear::new(weights, 2, 3, 0.7);
let input = vec![1.0, 2.0, 3.0];
let (output, _ternary_w) = layer.forward(&input);
assert_eq!(output.len(), 2);
}
#[test]
fn test_qat_linear_backward_shapes() {
let weights = vec![1.0, 0.0, -1.0, 0.5, 0.5, 0.0];
let mut layer = QatLinear::new(weights, 2, 3, 0.7);
let input = vec![1.0, 2.0, 3.0];
let grad_output = vec![1.0, -1.0];
let grad_input = layer.backward(&grad_output, &input);
assert_eq!(grad_input.len(), 3);
assert!(layer.grad.iter().any(|g| g.abs() > 1e-6));
}
#[test]
fn test_adamw_step() {
let mut weights = vec![1.0, -1.0, 0.5];
let grads = vec![0.1, -0.2, 0.05];
let mut opt = AdamW::new(3, 0.01);
let original = weights.clone();
opt.step(&mut weights, &grads);
for i in 0..3 {
assert!(
(weights[i] - original[i]).abs() > 1e-8,
"weight[{i}] unchanged"
);
}
}
#[test]
fn test_mse_loss() {
let pred = vec![1.0, 2.0, 3.0];
let target = vec![1.0, 2.0, 3.0];
assert!(mse_loss(&pred, &target) < 1e-6);
let pred2 = vec![2.0, 3.0, 4.0];
assert!((mse_loss(&pred2, &target) - 1.0).abs() < 1e-6);
}
#[test]
fn test_mse_loss_grad() {
let pred = vec![2.0, 3.0];
let target = vec![1.0, 1.0];
let grad = mse_loss_grad(&pred, &target);
assert!((grad[0] - 1.0).abs() < 1e-6);
assert!((grad[1] - 2.0).abs() < 1e-6);
}
#[test]
fn test_qat_train_step_loss_decreases() {
let weights = vec![0.0, 0.0]; let mut layer = QatLinear::new(weights, 1, 2, 0.3);
let mut opt = AdamW::new(2, 0.1);
let input = vec![1.0, 0.0];
let target = vec![2.0];
let loss_0 = qat_train_step(&mut layer, &mut opt, &input, &target);
let mut last_loss = loss_0;
for _ in 0..50 {
last_loss = qat_train_step(&mut layer, &mut opt, &input, &target);
}
assert!(
last_loss < loss_0,
"loss should decrease: {loss_0} -> {last_loss}"
);
}
#[test]
fn test_qat_train_convergence() {
let weights = vec![0.1, 0.0, 0.0, 0.1];
let mut layer = QatLinear::new(weights, 2, 2, 0.3);
let mut opt = AdamW::new(4, 0.05);
let input = vec![1.0, -1.0];
let target = vec![1.0, -1.0];
for _ in 0..200 {
qat_train_step(&mut layer, &mut opt, &input, &target);
}
let (output, _) = layer.forward(&input);
let final_loss = mse_loss(&output, &target);
assert!(
final_loss < 1.0,
"should converge somewhat: loss={final_loss}"
);
}
#[test]
fn test_sparsity_ratio() {
let w = vec![0.0, 0.0, 1.0, 0.0, -1.0];
let r = sparsity_ratio(&w, 1e-6);
assert!((r - 0.6).abs() < 1e-6); }
#[test]
fn test_sparsity_ratio_empty() {
assert_eq!(sparsity_ratio(&[], 1e-6), 0.0);
}
#[test]
fn test_l1_increases_sparsity() {
let run = |lambda: f32| -> f32 {
let weights = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.6, 0.05, -0.02];
let mut layer = QatLinear::new(weights, 2, 4, 0.3);
let mut opt = AdamW::new(8, 0.01);
let input = vec![1.0, 0.0, 0.0, 0.0];
let target = vec![1.0, 0.0];
for _ in 0..100 {
qat_train_step_l1(&mut layer, &mut opt, &input, &target, lambda);
}
sparsity_ratio(&layer.weights, 0.05)
};
let sparsity_no_l1 = run(0.0);
let sparsity_with_l1 = run(0.1);
assert!(
sparsity_with_l1 >= sparsity_no_l1,
"L1 should increase sparsity: no_l1={sparsity_no_l1}, with_l1={sparsity_with_l1}"
);
}
#[test]
fn test_l1_grad_adds_sign() {
let weights = vec![1.0, -2.0]; let mut layer = QatLinear::new(weights, 1, 2, 0.3);
let _opt = AdamW::new(2, 0.01);
let input = vec![1.0, 1.0];
let target = vec![0.0];
layer.zero_grad();
let (output, _) = layer.forward(&input);
let grad_out = mse_loss_grad(&output, &target);
layer.backward(&grad_out, &input);
let grad_no_l1 = layer.grad.clone();
let lambda = 0.5f32;
let expected_0 = grad_no_l1[0] + lambda * 1.0f32.signum(); let expected_1 = grad_no_l1[1] + lambda * (-2.0f32).signum();
assert!(expected_0 > grad_no_l1[0]); assert!(expected_1 < grad_no_l1[1]); }
}