candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
use std::f64::consts::FRAC_1_SQRT_2;

use super::*;

/// The closed form of `get_slopes_power_of_2`, `2^(-8(i+1)/n)`; eight heads is `small-en`.
#[test]
fn the_power_of_two_slopes_are_the_closed_form() {
  for n in [1usize, 2, 4, 8, 16] {
    let slopes = head_slopes(n);
    assert_eq!(slopes.len(), n);
    for (i, slope) in slopes.iter().enumerate() {
      let expected = 2f64.powf(-8.0 * (i + 1) as f64 / n as f64);
      assert!(
        (slope - expected).abs() < 1e-12,
        "n={n} head={i}: {slope} vs {expected}"
      );
    }
  }
  assert_eq!(head_slopes(8)[0], 0.5);
  assert_eq!(head_slopes(8)[7], 2f64.powi(-8));
}

/// The eight powers of two, then every second entry of the sixteen-head sequence; sorting them costs cosine 0.9044 on `base-en`.
#[test]
fn the_twelve_head_slopes_are_the_interpolated_sequence() {
  let expected = [
    0.5,
    0.25,
    0.125,
    0.0625,
    0.03125,
    0.015625,
    0.0078125,
    0.00390625,
    FRAC_1_SQRT_2,
    0.3535533905932738,
    0.1767766952966369,
    0.08838834764831845,
  ];
  let slopes = head_slopes(12);
  assert_eq!(slopes.len(), 12);
  assert_eq!(slopes[0], 0.5, "head 0 is exactly 2^-1, the ratio at c = 8");
  assert!(
    (slopes[8] - 2f64.powf(-0.5)).abs() < 1e-12,
    "head 8 is 2^-0.5"
  );
  for (i, (got, want)) in slopes.iter().zip(expected).enumerate() {
    assert!((got - want).abs() < 1e-12, "head {i}: {got} vs {want}");
  }
}

#[test]
fn the_slopes_are_not_monotonic_in_head_order() {
  let slopes = head_slopes(12);
  let mut sorted = slopes.clone();
  sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
  assert_ne!(
    slopes, sorted,
    "heads 8..11 carry the large slopes and sit last"
  );
  assert!(slopes[8] > slopes[0], "head 8 is larger than head 0");
}

/// Symmetric, unlike a causal-decoder ALiBi: `rebuild_alibi_tensor` takes `abs(memory_position - context_position)`.
#[test]
fn the_bias_is_symmetric_negative_and_zero_on_the_diagonal() {
  let bias = AlibiBias::new(12, AlibiSlopeVariant::Canonical)
    .forward(5, &Device::Cpu)
    .unwrap();
  assert_eq!(bias.dims(), &[1, 12, 5, 5]);
  assert_eq!(bias.dtype(), DType::F32);
  let flat = bias.flatten_all().unwrap().to_vec1::<f32>().unwrap();
  let at = |h: usize, i: usize, j: usize| flat[(h * 5 + i) * 5 + j];
  for h in 0..12 {
    for i in 0..5 {
      assert_eq!(at(h, i, i), 0.0, "head {h} diagonal {i}");
      for j in 0..5 {
        assert!(at(h, i, j) <= 0.0, "head {h} ({i},{j}) is {}", at(h, i, j));
        assert_eq!(
          at(h, i, j),
          at(h, j, i),
          "head {h} ({i},{j}) is not symmetric"
        );
      }
    }
  }
}

#[test]
fn the_first_offdiagonal_is_the_negated_slope_of_its_head() {
  let slopes = head_slopes(8);
  let bias = AlibiBias::new(8, AlibiSlopeVariant::Canonical)
    .forward(4, &Device::Cpu)
    .unwrap();
  let flat = bias.flatten_all().unwrap().to_vec1::<f32>().unwrap();
  for (h, slope) in slopes.iter().enumerate() {
    let got = flat[(h * 4) * 4 + 1];
    assert!(
      (got - -(*slope as f32)).abs() < 1e-7,
      "head {h}: {got} vs {}",
      -slope
    );
    let far = flat[(h * 4) * 4 + 3];
    assert!(
      (far - -3.0 * *slope as f32).abs() < 1e-6,
      "head {h} at distance 3"
    );
  }
}

/// Independent of weights, batch and row lengths, so one tensor built per forward serves every layer.
#[test]
fn the_bias_depends_only_on_the_head_count_and_the_length() {
  let a = AlibiBias::new(4, AlibiSlopeVariant::Canonical)
    .forward(7, &Device::Cpu)
    .unwrap();
  let b = AlibiBias::new(4, AlibiSlopeVariant::Canonical)
    .forward(7, &Device::Cpu)
    .unwrap();
  assert_eq!(
    a.flatten_all().unwrap().to_vec1::<f32>().unwrap(),
    b.flatten_all().unwrap().to_vec1::<f32>().unwrap()
  );
}

/// `jinaai/jina-reranker-v1-tiny-en`'s `modeling_bert.py` halves the four interpolated slopes ("quick fix on large jump at header=12").
#[test]
fn the_reranker_checkpoints_own_slopes_halve_the_interpolated_tail() {
  let canonical = AlibiBias::new(12, AlibiSlopeVariant::Canonical).slopes;
  let checkpoint_own = AlibiBias::new(12, AlibiSlopeVariant::HalvedInterpolatedTail).slopes;
  let expected = [
    0.5,
    0.25,
    0.125,
    0.0625,
    0.03125,
    0.015625,
    0.0078125,
    0.00390625,
    0.3535533905932738,
    0.17677669529663692,
    0.08838834764831846,
    0.044194173824159244,
  ];
  for (i, (got, want)) in checkpoint_own.iter().zip(expected).enumerate() {
    assert!((got - want).abs() < 1e-12, "head {i}: {got} vs {want}");
  }
  for i in 0..8 {
    assert_eq!(
      checkpoint_own[i], canonical[i],
      "the power-of-two prefix (heads 0..8) is untouched, head {i}"
    );
  }
  for i in 8..12 {
    assert!(
      (checkpoint_own[i] - canonical[i] / 2.0).abs() < 1e-12,
      "head {i} must be exactly half of the canonical value: {} vs {}",
      checkpoint_own[i],
      canonical[i] / 2.0
    );
  }
}

#[test]
fn a_power_of_two_head_count_is_unaffected_by_either_variant() {
  for n in [1usize, 2, 4, 8, 16] {
    let canonical = AlibiBias::new(n, AlibiSlopeVariant::Canonical).slopes;
    let halved = AlibiBias::new(n, AlibiSlopeVariant::HalvedInterpolatedTail).slopes;
    assert_eq!(
      canonical, halved,
      "n={n}: a power-of-two head count must agree between the two variants"
    );
  }
}