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 super::*;
use crate::embed::synthetic;
use crate::embed::synthetic::HIDDEN;
use crate::models::SparseHead;
use crate::registry::Backend;
use crate::registry::BackendSupport;
use crate::registry::DtypeBackendSupport;
use crate::registry::Family;
use crate::registry::Support;
use crate::registry::WeightsFormat;
use crate::registry::sparse::SparseComputation;

#[test]
fn default_batch_size_is_eight() {
  let builder = SparseTextEmbedding::builder("org/unregistered");
  assert_eq!(builder.batch_size, 8);
}

fn splade_descriptor() -> SparseDescriptor {
  SparseDescriptor {
    id: "synthetic/zero-splade".to_string(),
    family: Some(Family::Bert),
    head: SparseHead::SpladeMlm,
    vocab_size: 16,
    default_max_length: 32,
    backends: BackendSupport::untested(),
    dtype_backends: DtypeBackendSupport::untested(),
    license: "unknown".to_string(),
    size_gb: 0.0,
    phase: u8::MAX,
    weights: WeightsFormat::Safetensors,
    gated: false,
    head_source: None,
    head_revision: None,
    computation: SparseComputation::Encoded,
  }
}

#[test]
fn build_loads_the_weights_in_the_dtype_it_resolves() {
  let mut tensors = synthetic::zero_bert("bert.", 16);
  for (name, shape) in [
    (
      "cls.predictions.transform.dense.weight",
      vec![HIDDEN, HIDDEN],
    ),
    ("cls.predictions.transform.dense.bias", vec![HIDDEN]),
    ("cls.predictions.transform.LayerNorm.weight", vec![HIDDEN]),
    ("cls.predictions.transform.LayerNorm.bias", vec![HIDDEN]),
    ("cls.predictions.bias", vec![16]),
  ] {
    tensors.insert(name.to_string(), synthetic::zeros(&shape));
  }
  let config = synthetic::bert_config(16);
  let dir = synthetic::checkpoint(&config, "tiny_tokenizer.json", &tensors);
  for (requested, expected) in [(None, DType::F32), (Some(DType::F16), DType::F16)] {
    let mut builder = SparseTextEmbedding::builder(dir.path().to_str().unwrap())
      .descriptor(splade_descriptor())
      .device(Device::Cpu);
    if let Some(dtype) = requested {
      builder = builder.dtype(dtype);
    }
    let model = builder.build().unwrap();
    assert_eq!(model.dtype(), expected, "requested {requested:?}");
    assert_eq!(model.weights_dtype(), expected, "requested {requested:?}");
  }
}

/// The spec is an empty directory, so a refusal moved below `hub::resolve` or `loader::discover` surfaces as `MissingFile`.
#[test]
fn a_refused_dtype_is_refused_before_anything_is_fetched() {
  let dir = tempfile::tempdir().unwrap();
  let refused = SparseTextEmbedding::builder(dir.path().to_str().unwrap())
    .descriptor(splade_descriptor())
    .device(Device::Cpu)
    .dtype(DType::BF16)
    .build()
    .err();
  assert!(
    matches!(
      refused,
      Some(CanddingError::UnsupportedOnDevice {
        backend: Backend::Cpu,
        dtype: DType::BF16,
        ..
      })
    ),
    "{refused:?}"
  );
}

/// The dtype check reads the device's own cell: refused where only the CPU's refuses, and past the check, into the
/// empty directory, where only the accelerators' do.
#[test]
fn the_dtype_check_reads_the_device_s_own_cell() {
  let (v, u) = (Support::Verified, Support::Unsupported);
  for (f16, refused) in [
    (
      BackendSupport {
        cpu: u,
        metal: v,
        cuda: v,
      },
      true,
    ),
    (
      BackendSupport {
        cpu: v,
        metal: u,
        cuda: u,
      },
      false,
    ),
  ] {
    let dir = tempfile::tempdir().unwrap();
    let mut descriptor = splade_descriptor();
    descriptor.dtype_backends.f16 = f16;
    let err = SparseTextEmbedding::builder(dir.path().to_str().unwrap())
      .descriptor(descriptor)
      .device(Device::Cpu)
      .dtype(DType::F16)
      .build()
      .err()
      .expect("an empty directory loads nothing");
    let on_the_cpu = matches!(
      err,
      CanddingError::UnsupportedOnDevice {
        backend: Backend::Cpu,
        dtype: DType::F16,
        ..
      }
    );
    assert_eq!(on_the_cpu, refused, "{f16:?}: {err}");
    assert!(
      !matches!(err, CanddingError::UnsupportedDtype { .. }),
      "{f16:?}: {err}"
    );
  }
}