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 candle_core::Device;
use candle_core::Tensor;

use super::*;

#[test]
fn padding_mask_zeroes_real_tokens_and_floors_padding() {
  let attention_mask = Tensor::new(&[[1u32, 1, 0], [1, 1, 1]], &Device::Cpu).unwrap();
  let mask = AttentionMask::padding(&attention_mask, &[2, 3], 4, 64, DType::F16).unwrap();
  let additive = mask.additive().expect("padded rows produce a mask");
  assert_eq!(additive.dims(), &[2, 1, 1, 3]);
  assert_eq!(additive.dtype(), DType::F16);
  let values = additive
    .to_dtype(DType::F32)
    .unwrap()
    .flatten_all()
    .unwrap()
    .to_vec1::<f32>()
    .unwrap();
  assert_eq!(values, vec![0.0, 0.0, -65504.0, 0.0, 0.0, 0.0]);
  assert!(!mask.is_causal());
}

#[test]
fn padding_mask_is_none_without_padding() {
  let attention_mask = Tensor::new(&[[1u32, 1], [1, 1]], &Device::Cpu).unwrap();
  let mask = AttentionMask::padding(&attention_mask, &[2, 2], 2, 64, DType::F32).unwrap();
  assert!(mask.additive().is_none() && !mask.is_causal());
}

#[test]
fn causal_mask_is_upper_triangular_on_cpu() {
  let mask = AttentionMask::causal(3, 128, DType::F32, &Device::Cpu).unwrap();
  let values = mask
    .additive()
    .unwrap()
    .squeeze(0)
    .unwrap()
    .squeeze(0)
    .unwrap()
    .to_vec2::<f32>()
    .unwrap();
  let floor = f32::MIN;
  assert_eq!(
    values,
    vec![
      vec![0.0, floor, floor],
      vec![0.0, 0.0, floor],
      vec![0.0, 0.0, 0.0]
    ]
  );
  assert!(
    !mask.is_causal(),
    "the flag is only raised for the Metal kernel"
  );
}

/// Too few lengths would leave rows unmasked; a `debug_assert` says so in a test build and nothing in release.
#[test]
fn a_lengths_slice_that_does_not_match_the_batch_is_refused() {
  let attention_mask = Tensor::new(&[[1u32, 1, 0], [1, 1, 1]], &Device::Cpu).unwrap();
  let bias = Tensor::zeros((1, 4, 3, 3), DType::F32, &Device::Cpu).unwrap();
  for lengths in [&[3usize][..], &[3, 3, 3][..], &[][..]] {
    for built in [
      AttentionMask::padding(&attention_mask, lengths, 4, 64, DType::F32),
      AttentionMask::padding_with_bias(&attention_mask, lengths, &bias, 4, 64, DType::F32),
      AttentionMask::sliding(&attention_mask, lengths, 2, 4, 64, DType::F32),
    ] {
      let err = built
        .err()
        .unwrap_or_else(|| panic!("{} lengths for 2 rows must be refused", lengths.len()));
      assert!(
        err.to_string().contains("for a batch of 2 rows"),
        "{lengths:?}: {err}"
      );
    }
  }
  assert!(AttentionMask::padding(&attention_mask, &[2, 3], 4, 64, DType::F32).is_ok());
}

#[test]
fn causal_mask_materializes_for_a_head_size_the_kernel_rejects() {
  let mask = AttentionMask::causal(3, 40, DType::F32, &Device::Cpu).unwrap();
  assert!(mask.additive().is_some());
}

/// Window 3 over six positions; row 1 has two padded keys.
#[test]
fn sliding_mask_combines_the_window_with_padding() {
  let attention_mask =
    Tensor::new(&[[1u32, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0]], &Device::Cpu).unwrap();
  let mask = AttentionMask::sliding(&attention_mask, &[6, 4], 3, 2, 64, DType::F32).unwrap();
  let additive = mask
    .additive()
    .expect("a window narrower than the sequence produces a mask");
  assert_eq!(additive.dims(), &[2, 1, 6, 6]);
  let rows = additive.squeeze(1).unwrap().to_vec3::<f32>().unwrap();
  let floor = f32::MIN;
  let (row0, row1) = (&rows[0], &rows[1]);
  for (i, (cols0, cols1)) in row0.iter().zip(row1.iter()).enumerate() {
    for (j, (&value0, &value1)) in cols0.iter().zip(cols1.iter()).enumerate() {
      let in_window = i.abs_diff(j) < 3;
      assert_eq!(value0 == 0.0, in_window, "row 0 ({i}, {j})");
      let visible1 = (in_window && j < 4) || i == j;
      assert_eq!(value1 == 0.0, visible1, "row 1 ({i}, {j})");
      if !visible1 {
        assert_eq!(value1, floor);
      }
    }
  }
  assert!(!mask.is_causal());
}

#[test]
fn sliding_mask_is_none_when_the_window_covers_everything() {
  let attention_mask = Tensor::new(&[[1u32, 1, 1]], &Device::Cpu).unwrap();
  let mask = AttentionMask::sliding(&attention_mask, &[3], 3, 1, 64, DType::F32).unwrap();
  assert!(mask.additive().is_none());
  let padded = Tensor::new(&[[1u32, 1, 0]], &Device::Cpu).unwrap();
  let mask = AttentionMask::sliding(&padded, &[2], 3, 1, 64, DType::F16).unwrap();
  let additive = mask.additive().expect("padding still needs a mask");
  assert_eq!(additive.dtype(), DType::F16);
  let rows = additive
    .to_dtype(DType::F32)
    .unwrap()
    .squeeze(1)
    .unwrap()
    .to_vec3::<f32>()
    .unwrap();
  assert_eq!(rows[0][0][2], -65504.0);
  assert_eq!(rows[0][0][1], 0.0);
}

/// Without its own key the row would be entirely floored, and candle's Metal kernel divides by a zero softmax sum there.
#[test]
fn sliding_mask_leaves_no_row_entirely_floored() {
  let attention_mask = Tensor::new(&[[1u32, 1, 0, 0, 0, 0, 0, 0]], &Device::Cpu).unwrap();
  let mask = AttentionMask::sliding(&attention_mask, &[2], 2, 1, 64, DType::F32).unwrap();
  let rows = mask
    .additive()
    .expect("a padded row produces a mask")
    .squeeze(1)
    .unwrap()
    .to_vec3::<f32>()
    .unwrap();
  for (i, row) in rows[0].iter().enumerate() {
    let open: Vec<usize> = row
      .iter()
      .enumerate()
      .filter(|(_, value)| **value == 0.0)
      .map(|(j, _)| j)
      .collect();
    assert!(!open.is_empty(), "row {i} has no visible key");
    assert!(
      open.contains(&i),
      "row {i} cannot see its own key: {open:?}"
    );
  }
  assert_eq!(
    rows[0][7],
    vec![
      f32::MIN,
      f32::MIN,
      f32::MIN,
      f32::MIN,
      f32::MIN,
      f32::MIN,
      f32::MIN,
      0.0
    ]
  );
}

/// candle's fused Metal kernel assumes only the last mask dimension is contiguous, so each constructor hands it a broadcast
/// view; every dtype is pinned because a cast after the broadcast would re-materialize the copy.
#[cfg(feature = "metal")]
#[test]
fn every_mask_reaches_the_metal_kernel_as_a_broadcast_view() {
  let device = Device::new_metal(0).expect("a metal device");
  let padded = Tensor::new(&[[1u32, 1, 0], [1, 1, 1]], &device).unwrap();
  let full = Tensor::new(&[[1u32, 1, 1], [1, 1, 1]], &device).unwrap();
  for dtype in [DType::F32, DType::F16, DType::BF16] {
    let bias = Tensor::zeros((1, 4, 3, 3), DType::F32, &device).unwrap();
    for (name, built, strides) in [
      (
        "padding",
        AttentionMask::padding(&padded, &[2, 3], 4, 64, dtype),
        [3usize, 0, 0, 1],
      ),
      (
        "padding_with_bias, unpadded",
        AttentionMask::padding_with_bias(&full, &[3, 3], &bias, 4, 64, dtype),
        [0, 9, 3, 1],
      ),
      (
        "sliding",
        AttentionMask::sliding(&padded, &[2, 3], 2, 4, 64, dtype),
        [9, 0, 3, 1],
      ),
    ] {
      let additive = built
        .unwrap()
        .additive()
        .unwrap_or_else(|| panic!("{name} in {dtype:?} produces an additive term"))
        .clone();
      assert_eq!(additive.dims(), &[2, 4, 3, 3], "{name} in {dtype:?}");
      assert_eq!(additive.dtype(), dtype, "{name} in {dtype:?}");
      assert_eq!(additive.stride(), &strides, "{name} in {dtype:?}");
      assert!(
        !additive.is_contiguous(),
        "{name} in {dtype:?} was materialized"
      );
    }
  }
}

#[test]
fn mask_floor_per_dtype() {
  assert_eq!(mask_min(DType::F16), -65504.0);
  assert_eq!(mask_min(DType::F32), f32::MIN as f64);
  assert!(mask_min(DType::BF16) < -3.0e38);
}

mod bias;