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
//! The synthetic flash reranker's `config.json` names no `model_type`, as `jinaai/jina-reranker-v2-base-multilingual`'s names none.

use super::*;
use crate::error::LengthKind;
use crate::models::flash_fixture;
use crate::models::flash_fixture::Numbering;
use crate::models::xlm_roberta_flash::PositionScheme;

/// `[CLS]`'s id in `tiny_tokenizer_double_separator.json`.
const CLS: u32 = 2;

fn flash_checkpoint() -> TempDir {
  let dir = tempdir().unwrap();
  fs::write(
    dir.path().join("config.json"),
    flash_fixture::config_json("absolute"),
  )
  .unwrap();
  fs::copy(
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
      .join("tests/fixtures/tiny_tokenizer_double_separator.json"),
    dir.path().join("tokenizer.json"),
  )
  .unwrap();
  candle_core::safetensors::save(
    &flash_fixture::tensors(),
    dir.path().join("model.safetensors"),
  )
  .unwrap();
  dir
}

fn flash_descriptor(position_scheme: Option<PositionScheme>) -> RerankDescriptor {
  RerankDescriptor {
    id: "synthetic/flash-reranker".to_string(),
    family: Family::XlmRobertaFlash,
    head: RerankHead::RobertaClassificationHead,
    position_scheme,
    ..descriptor(ScoreActivation::Sigmoid)
  }
}

fn build(dir: &TempDir, position_scheme: Option<PositionScheme>) -> Result<TextCrossEncoder> {
  TextCrossEncoder::builder(dir.path().to_str().unwrap())
    .descriptor(flash_descriptor(position_scheme))
    .device(Device::Cpu)
    .build()
}

#[test]
fn build_reaches_the_flash_encoder_through_the_entrys_family_and_scheme() {
  let dir = flash_checkpoint();
  let config = fs::read_to_string(dir.path().join("config.json")).unwrap();
  assert!(!config.contains("model_type"), "{config}");
  let model = build(&dir, Some(PositionScheme::Absolute)).unwrap();
  assert_eq!(
    model.max_length(),
    flash_fixture::ROWS - flash_fixture::PAD - 1
  );
  let expected = ScoreActivation::Sigmoid
    .apply(flash_fixture::expected_logit(CLS, Numbering::FromPadPlusOne) as f32);
  let documents = ["c d", "", "the cats sat on the mat"];
  let scores = model.rerank("a b", &documents, None).unwrap();
  assert_eq!(scores.len(), documents.len());
  for score in scores {
    let value = score.value();
    assert!(
      (value - expected).abs() < 1e-6,
      "{value} against {expected}"
    );
  }
}

#[test]
fn build_refuses_a_flash_entry_whose_scheme_the_config_does_not_name() {
  let dir = flash_checkpoint();
  for declared in [Some(PositionScheme::Rotary), None] {
    match build(&dir, declared) {
      Err(CanddingError::PositionSchemeDisagreesWithConfig { config, .. }) => {
        assert_eq!(config, "absolute")
      }
      Ok(_) => panic!("{declared:?} against an absolute config must be refused"),
      Err(other) => panic!("{declared:?}: unexpected error: {other}"),
    }
  }
}

/// A cap of 3 keeps `[CLS] a [SEP]` and leaves one document token per window under the table's limit of 6.
#[test]
fn build_cuts_a_windowed_flash_entrys_documents_into_windows() {
  let dir = flash_checkpoint();
  let windowed = |query_cap| RerankDescriptor {
    input: RerankInput::Windowed {
      query_cap,
      overlap: 1,
    },
    ..flash_descriptor(Some(PositionScheme::Absolute))
  };
  let model = TextCrossEncoder::builder(dir.path().to_str().unwrap())
    .descriptor(windowed(3))
    .device(Device::Cpu)
    .build()
    .unwrap();
  let documents = ["c d", ""];
  assert_eq!(model.pair_window_count("a b", &documents).unwrap(), [2, 1]);
  assert_eq!(model.pair_token_count("a b", &documents).unwrap(), [12, 5]);
  let expected = ScoreActivation::Sigmoid
    .apply(flash_fixture::expected_logit(CLS, Numbering::FromPadPlusOne) as f32);
  for score in model.rerank("a b", &documents, None).unwrap() {
    let value = score.value();
    assert!(
      (value - expected).abs() < 1e-6,
      "{value} against {expected}"
    );
  }
  let refused = TextCrossEncoder::builder(dir.path().to_str().unwrap())
    .descriptor(windowed(4))
    .device(Device::Cpu)
    .build();
  assert!(
    matches!(
      refused,
      Err(CanddingError::LengthTooSmall {
        requested: 6,
        minimum: 8,
        kind: LengthKind::Windowed { query_cap: 4 }
      })
    ),
    "{:?}",
    refused.err()
  );
}