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::DType;
use candle_core::Device;
use candle_nn::Linear;
use candle_nn::Module;
use candle_nn::VarBuilder;
use candle_nn::VarMap;

use super::*;
use crate::layers::attention::scaled_dot_product;
use crate::layers::mask::AttentionMask;
use crate::layers::rope::RotaryEmbedding;
use crate::models::xlm_roberta_flash::attention::XlmRobertaFlashAttention;

/// Compares `forward` with the same `Wqkv` narrowed by hand and run through `layers`' public primitives.
/// Asymmetric blocks and tokens, since a symmetric block would hide a query-key swap as a transpose.
#[test]
fn the_fused_qkv_projection_splits_in_query_key_value_row_order() {
  let hidden = 4;
  let heads = 2;
  let head_dim = 2;
  let config = XlmRobertaFlashConfig {
    hidden_size: hidden,
    num_attention_heads: heads,
    ..tiny_config()
  };

  let mut varmap = VarMap::new();
  let vb = VarBuilder::from_varmap(&varmap, DType::F32, &Device::Cpu);
  let attention = XlmRobertaFlashAttention::load(&config, vb.pp("mixer")).unwrap();

  #[rustfmt::skip]
  let q_block = [
    0.7f32, -0.3, 0.2, 0.5,
    -0.1, 0.9, -0.4, 0.2,
    0.3, 0.1, 0.6, -0.2,
    -0.5, 0.4, 0.1, 0.8,
  ];
  #[rustfmt::skip]
  let k_block = [
    0.2f32, 0.6, -0.3, 0.1,
    0.9, -0.2, 0.4, -0.1,
    -0.4, 0.3, 0.7, 0.2,
    0.1, -0.5, 0.2, 0.6,
  ];
  #[rustfmt::skip]
  let v_block = [
    1.1f32, 0.2, -0.3, 0.4,
    -0.2, 1.3, 0.1, -0.4,
    0.3, -0.1, 1.2, 0.2,
    -0.4, 0.3, -0.2, 1.4,
  ];
  let mut rows = Vec::with_capacity(3 * hidden * hidden);
  rows.extend_from_slice(&q_block);
  rows.extend_from_slice(&k_block);
  rows.extend_from_slice(&v_block);
  let weight = Tensor::from_vec(rows, (3 * hidden, hidden), &Device::Cpu).unwrap();
  let bias = Tensor::from_vec(
    vec![
      0.1f32, -0.2, 0.3, 0.05, 0.4, 0.1, -0.3, 0.2, -0.15, 0.25, 0.05, -0.1,
    ],
    3 * hidden,
    &Device::Cpu,
  )
  .unwrap();
  varmap.set_one("mixer.Wqkv.weight", weight.clone()).unwrap();
  varmap.set_one("mixer.Wqkv.bias", bias.clone()).unwrap();
  varmap
    .set_one(
      "mixer.out_proj.weight",
      Tensor::eye(hidden, DType::F32, &Device::Cpu).unwrap(),
    )
    .unwrap();
  varmap
    .set_one(
      "mixer.out_proj.bias",
      Tensor::zeros(hidden, DType::F32, &Device::Cpu).unwrap(),
    )
    .unwrap();

  let hidden_input = Tensor::from_vec(
    vec![
      1.0f32, 0.5, -0.5, 0.2, -0.3, 0.8, 0.1, -0.6, 0.4, -0.2, 0.9, 0.3,
    ],
    (1, 3, hidden),
    &Device::Cpu,
  )
  .unwrap();
  let mask = AttentionMask::none();
  let rope = RotaryEmbedding::new(head_dim, 8, 10_000.0, DType::F32, &Device::Cpu).unwrap();

  let actual = attention
    .forward(&hidden_input, &mask, Some(&rope))
    .unwrap();

  let slice = |t: &Tensor, third: usize| t.narrow(0, third * hidden, hidden).unwrap();
  let q_lin = Linear::new(slice(&weight, 0), Some(slice(&bias, 0)));
  let k_lin = Linear::new(slice(&weight, 1), Some(slice(&bias, 1)));
  let v_lin = Linear::new(slice(&weight, 2), Some(slice(&bias, 2)));
  let split = |xs: Tensor| {
    xs.reshape((1, 3, heads, head_dim))
      .unwrap()
      .transpose(1, 2)
      .unwrap()
      .contiguous()
      .unwrap()
  };
  let q = split(q_lin.forward(&hidden_input).unwrap());
  let k = split(k_lin.forward(&hidden_input).unwrap());
  let v = split(v_lin.forward(&hidden_input).unwrap());
  let (q, k) = rope.apply(&q, &k).unwrap();
  let scale = 1.0 / (head_dim as f64).sqrt();
  let expected = scaled_dot_product(&q, &k, &v, &mask, scale)
    .unwrap()
    .transpose(1, 2)
    .unwrap()
    .reshape((1, 3, hidden))
    .unwrap();

  let diff = (actual - expected)
    .unwrap()
    .abs()
    .unwrap()
    .flatten_all()
    .unwrap()
    .max(0)
    .unwrap()
    .to_scalar::<f32>()
    .unwrap();
  assert!(
    diff < 1e-6,
    "max abs diff between the module and the independently-narrowed shadow computation: {diff}"
  );
}