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
//! Compute device selection.

mod memory;

use candle_core::Device;
use candle_core::utils::cuda_is_available;
use candle_core::utils::metal_is_available;
pub(crate) use memory::budget_bytes;

use crate::error::CanddingError;
use crate::error::Result;
use crate::registry::Backend;

/// Metal first, then CUDA, then CPU, depending on compiled features.
pub fn auto() -> Result<Device> {
  if metal_is_available() {
    return metal(0);
  }
  if cuda_is_available() {
    return cuda(0);
  }
  Ok(Device::Cpu)
}

pub fn cpu() -> Device {
  Device::Cpu
}

pub fn metal(ordinal: usize) -> Result<Device> {
  Device::new_metal(ordinal).map_err(|e| device_error(Backend::Metal, e))
}

pub fn cuda(ordinal: usize) -> Result<Device> {
  Device::new_cuda(ordinal).map_err(|e| device_error(Backend::Cuda, e))
}

/// candle's refusal of a backend its build left out, as `BackendNotCompiled`; any other failure keeps its message.
fn device_error(backend: Backend, error: candle_core::Error) -> CanddingError {
  match error {
    candle_core::Error::NotCompiledWithMetalSupport
    | candle_core::Error::NotCompiledWithCudaSupport => {
      CanddingError::BackendNotCompiled { backend }
    }
    error => CanddingError::Device {
      backend,
      reason: error.to_string(),
    },
  }
}

/// Short backend name used in errors and the CLI.
pub fn describe(device: &Device) -> &'static str {
  match device {
    Device::Cpu => "cpu",
    Device::Metal(_) => "metal",
    Device::Cuda(_) => "cuda",
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn auto_matches_compiled_features() {
    let device = auto().unwrap();
    let expected = if cfg!(feature = "metal") {
      "metal"
    } else if cfg!(feature = "cuda") {
      "cuda"
    } else {
      "cpu"
    };
    assert_eq!(describe(&device), expected);
  }

  #[test]
  fn cpu_is_cpu() {
    assert_eq!(describe(&cpu()), "cpu");
  }

  #[test]
  fn a_backend_left_out_of_the_build_is_refused_as_not_compiled() {
    if !cfg!(feature = "metal") {
      assert!(
        matches!(
          metal(0),
          Err(CanddingError::BackendNotCompiled {
            backend: Backend::Metal
          })
        ),
        "{:?}",
        metal(0).err()
      );
    }
    if !cfg!(feature = "cuda") {
      assert!(
        matches!(
          cuda(0),
          Err(CanddingError::BackendNotCompiled {
            backend: Backend::Cuda
          })
        ),
        "{:?}",
        cuda(0).err()
      );
    }
  }

  #[test]
  fn any_other_device_failure_keeps_the_backend_and_candles_message() {
    let error = device_error(
      Backend::Cuda,
      candle_core::Error::Msg("no device 3".to_string()),
    );
    assert!(
      matches!(&error, CanddingError::Device { backend: Backend::Cuda, reason } if reason == "no device 3"),
      "{error:?}"
    );
    let left_out = device_error(
      Backend::Metal,
      candle_core::Error::NotCompiledWithMetalSupport,
    );
    assert!(
      matches!(
        left_out,
        CanddingError::BackendNotCompiled {
          backend: Backend::Metal
        }
      ),
      "{left_out:?}"
    );
  }
}