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 super::*;

const V: Support = Support::Verified;
const U: Support = Support::Unsupported;
const T: Support = Support::Untested;

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

#[test]
fn an_accelerator_only_dtype_is_unsupported_on_the_processor_and_keeps_its_flat_status_elsewhere() {
  let derived = DtypeBackendSupport::derive(row(V, V, V), DtypeSupport::verified(), &[DType::F16]);
  assert_eq!(derived.f16, row(U, V, V));
  assert_eq!(derived.f32, row(V, V, V));

  let pending = DtypeSupport {
    f16: Support::Pending,
    ..DtypeSupport::verified()
  };
  let derived = DtypeBackendSupport::derive(row(V, V, T), pending, &[DType::F16]);
  assert_eq!(
    derived.f16,
    row(U, Support::Pending, T),
    "metal carries the flat status, whatever it is"
  );
}

#[test]
fn a_flatly_unsupported_dtype_is_unsupported_on_every_device() {
  let dtypes = DtypeSupport {
    f16: U,
    ..DtypeSupport::verified()
  };
  let derived = DtypeBackendSupport::derive(row(V, V, T), dtypes, &[]);
  assert_eq!(derived.f16, row(U, U, U));
  assert_eq!(derived.f32, row(V, V, T));
}

#[test]
fn a_device_untested_at_the_model_level_is_untested_for_every_dtype_it_can_run() {
  let derived = DtypeBackendSupport::derive(row(T, V, T), DtypeSupport::verified(), &[]);
  assert_eq!(derived.f32, row(T, V, T));
  assert_eq!(derived.f16, row(T, V, T));
  assert_eq!(derived.bf16, row(U, V, T));
}

#[test]
fn a_device_pending_at_the_model_level_is_pending_for_every_dtype_it_does_not_refuse() {
  let pending = Support::Pending;
  let derived =
    DtypeBackendSupport::derive(row(pending, V, T), DtypeSupport::verified(), &[DType::F16]);
  assert_eq!(derived.f32, row(pending, V, T));
  assert_eq!(derived.f16, row(U, V, T));
  assert_eq!(derived.bf16, row(U, V, T));
}

#[test]
fn bf16_is_unsupported_on_the_processor_even_when_the_flat_and_model_wide_statuses_are_verified() {
  let derived = DtypeBackendSupport::derive(row(V, V, V), DtypeSupport::verified(), &[]);
  assert_eq!(derived.bf16, row(U, V, V));
  assert_eq!(derived.f32, row(V, V, V));
  assert_eq!(derived.f16, row(V, V, V));
}

#[test]
fn not_applicable_survives_in_all_nine_cells() {
  let derived = DtypeBackendSupport::derive(
    BackendSupport::not_applicable(),
    DtypeSupport::not_applicable(),
    &[],
  );
  let everywhere = BackendSupport::not_applicable();
  assert_eq!(
    derived,
    DtypeBackendSupport {
      f32: everywhere,
      f16: everywhere,
      bf16: everywhere,
    }
  );
}

const P: Support = Support::Pending;
const STATUSES: [Support; 5] = [V, P, T, U, Support::NotApplicable];

/// Searches all 125 000 combinations of device statuses, dtype statuses and accelerator-only dtypes.
fn derivable(wanted: impl Fn(&DtypeBackendSupport) -> bool) -> bool {
  let subsets: Vec<Vec<DType>> = (0..8)
    .map(|bits| {
      [DType::F32, DType::F16, DType::BF16]
        .into_iter()
        .enumerate()
        .filter(|(i, _)| bits & (1 << i) != 0)
        .map(|(_, dtype)| dtype)
        .collect()
    })
    .collect();
  STATUSES.iter().any(|&cpu| {
    STATUSES.iter().any(|&metal| {
      STATUSES.iter().any(|&cuda| {
        STATUSES.iter().any(|&f32| {
          STATUSES.iter().any(|&f16| {
            STATUSES.iter().any(|&bf16| {
              subsets.iter().any(|accel| {
                let dtypes = DtypeSupport { f32, f16, bf16 };
                wanted(&DtypeBackendSupport::derive(
                  row(cpu, metal, cuda),
                  dtypes,
                  accel,
                ))
              })
            })
          })
        })
      })
    })
  })
}

fn exception(dtype: DType, backend: Backend, status: Support) -> CellException {
  CellException {
    dtype,
    backend,
    status,
    reason: "a run that decided this cell",
  }
}

#[test]
fn a_cpu_f32_pass_beside_an_unrun_metal_f32_needs_one_exception() {
  let wanted = |r: &DtypeBackendSupport| r.f32.cpu == V && r.f32.metal == T && r.bf16.metal == V;
  assert!(!derivable(wanted));
  let dtypes = DtypeSupport {
    f32: V,
    f16: U,
    bf16: V,
  };
  let derived = DtypeBackendSupport::derive(row(V, V, T), dtypes, &[]);
  assert_eq!(derived.f32, row(V, V, T), "Metal F32 verified with no run");
  let exact = derived.with_exceptions(&[exception(DType::F32, Backend::Metal, T)]);
  assert!(wanted(&exact));
  assert_eq!(exact.f32, row(V, T, T));
  assert_eq!((exact.f16, exact.bf16), (derived.f16, derived.bf16));
}

#[test]
fn a_metal_f32_miss_beside_a_pending_cpu_f32_needs_one_exception() {
  let wanted = |r: &DtypeBackendSupport| r.f32.cpu == P && r.f32.metal == U && r.bf16.metal == V;
  assert!(!derivable(wanted));
  let dtypes = DtypeSupport {
    f32: T,
    f16: U,
    bf16: V,
  };
  let derived = DtypeBackendSupport::derive(row(P, V, T), dtypes, &[]);
  let exact = derived.with_exceptions(&[exception(DType::F32, Backend::Metal, U)]);
  assert!(wanted(&exact));
  assert_eq!(exact.f32, row(P, U, T));
  assert_eq!((exact.f16, exact.bf16), (derived.f16, derived.bf16));
}

#[test]
fn an_exception_replaces_exactly_its_own_cell() {
  let derived = DtypeBackendSupport::derive(row(V, V, V), DtypeSupport::verified(), &[]);
  for dtype in [DType::F32, DType::F16, DType::BF16] {
    for backend in [Backend::Cpu, Backend::Metal, Backend::Cuda] {
      let exact = derived.with_exceptions(&[exception(dtype, backend, P)]);
      for other_dtype in [DType::F32, DType::F16, DType::BF16] {
        for other in [Backend::Cpu, Backend::Metal, Backend::Cuda] {
          let expected = if (other_dtype, other) == (dtype, backend) {
            P
          } else {
            derived.row(other_dtype).get(other)
          };
          assert_eq!(
            exact.row(other_dtype).get(other),
            expected,
            "{dtype:?} on {backend:?}"
          );
        }
      }
    }
  }
}