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 super::*;
use crate::embed::builder::reject_unsupported_dtype;
use crate::models::xlm_roberta_flash::PositionScheme;
use crate::registry::Backend;

fn row(cpu: Support, metal: Support, cuda: Support) -> BackendSupport {
  BackendSupport { cpu, metal, cuda }
}

/// No registered entry refuses a dtype on the processor alone, but a supplied descriptor can.
fn axis() -> DtypeBackendSupport {
  DtypeBackendSupport {
    f32: row(Support::Verified, Support::Verified, Support::Untested),
    f16: row(Support::Unsupported, Support::Verified, Support::Untested),
    bf16: row(
      Support::Unsupported,
      Support::Unsupported,
      Support::Unsupported,
    ),
  }
}

#[test]
fn a_cpu_only_unsupported_cell_is_refused_on_the_cpu_as_accelerator_only() {
  let err = reject_unsupported_dtype("m", DType::F16, axis(), &Device::Cpu).unwrap_err();
  assert!(
    matches!(&err, CanddingError::UnsupportedOnDevice { model, backend: Backend::Cpu, dtype: DType::F16 } if model == "m"),
    "{err}"
  );
  let message = err.to_string();
  assert!(message.contains("accelerator-only"), "{message}");
  assert!(!message.contains("NaN"), "{message}");
}

#[test]
fn a_dtype_unsupported_on_every_device_is_refused_as_unsupported() {
  let err = reject_unsupported_dtype("m", DType::BF16, axis(), &Device::Cpu).unwrap_err();
  assert!(
    matches!(&err, CanddingError::UnsupportedDtype { model, dtype: DType::BF16 } if model == "m"),
    "{err}"
  );
  assert!(err.to_string().contains("bf16"), "{err}");
}

#[test]
fn a_verified_cell_passes() {
  assert!(reject_unsupported_dtype("m", DType::F32, axis(), &Device::Cpu).is_ok());
}

#[test]
fn bge_m3_multi_vector_f16_is_refused_everywhere_through_its_registered_row() {
  let descriptor = crate::registry::multi_vector::lookup("BAAI/bge-m3").expect("registered");
  let err = reject_unsupported_dtype(
    "BAAI/bge-m3",
    DType::F16,
    descriptor.dtype_backends,
    &Device::Cpu,
  )
  .unwrap_err();
  assert!(
    matches!(
      err,
      CanddingError::UnsupportedDtype {
        dtype: DType::F16,
        ..
      }
    ),
    "{err}"
  );
}

/// Every field gets a distinct value, so a hardcoded field or two swapped ones fail the assertion.
#[test]
fn conventions_from_descriptor_carries_every_field_without_swapping_or_hardcoding_one() {
  let mut descriptor = stub_descriptor();
  descriptor.keeps_cls_row = true;
  descriptor.projection_in = 7;
  descriptor.projection_out = 5;
  descriptor.projection_bias = true;
  descriptor.normalize = true;
  descriptor.attends_to_mask_tokens = true;
  descriptor.position_scheme = Some(PositionScheme::Absolute);
  let conventions = build::conventions_from_descriptor(&descriptor);
  assert!(
    conventions.keeps_cls_row,
    "keeps_cls_row must carry through"
  );
  assert_eq!(
    conventions.projection_in, 7,
    "projection_in must not be swapped with projection_out"
  );
  assert_eq!(
    conventions.projection_out, 5,
    "projection_out must not be swapped with projection_in"
  );
  assert!(
    conventions.projection_bias,
    "projection_bias must carry through"
  );
  assert!(conventions.normalize, "normalize must carry through");
  assert!(
    conventions.attends_to_mask_tokens,
    "attends_to_mask_tokens must carry through"
  );
  assert_eq!(
    conventions.position_scheme,
    Some(PositionScheme::Absolute),
    "position_scheme must carry through"
  );
}

#[test]
fn the_registered_flash_entry_maps_onto_its_rotary_scheme() {
  let descriptor =
    crate::registry::multi_vector::lookup("jinaai/jina-colbert-v2").expect("registered");
  assert_eq!(
    build::conventions_from_descriptor(&descriptor).position_scheme,
    Some(PositionScheme::Rotary)
  );
}

/// `StanfordColbert` keeps every query row, so without a fixed `query_length` its `[PAD]` rows would count as content.
#[test]
fn a_stanford_colbert_descriptor_with_no_query_length_is_refused() {
  let mut descriptor = stub_descriptor();
  descriptor.head = MultiVectorHead::StanfordColbert;
  let err = build::require_fixed_query_length("m", &descriptor).unwrap_err();
  assert!(
    matches!(err, CanddingError::MissingQueryLength { .. }),
    "{err}"
  );
}

#[test]
fn a_stanford_colbert_descriptor_with_a_query_length_is_accepted() {
  let mut descriptor = stub_descriptor();
  descriptor.head = MultiVectorHead::StanfordColbert;
  descriptor.query_length = Some(32);
  assert!(build::require_fixed_query_length("m", &descriptor).is_ok());
}

/// `M3Colbert::forward_query` is `forward`, so bge-m3's head needs no `query_length`.
#[test]
fn an_m3_colbert_descriptor_with_no_query_length_is_unaffected() {
  let descriptor = stub_descriptor();
  assert!(build::require_fixed_query_length("m", &descriptor).is_ok());
}