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 std::fs;

use candle_core::DType;
use candle_core::Device;

use super::InputTokenizer;
use super::TextCrossEncoder;
use super::build;
use crate::device;
use crate::embed::builder::reject_unsupported_dtype;
use crate::embed::builder::resolve_dtype;
use crate::embed::fit;
use crate::error::CanddingError;
use crate::error::Result;
use crate::hub;
use crate::hub::ModelSource;
use crate::loader;
use crate::models;
use crate::registry;
use crate::registry::Catalog;
use crate::registry::rerank::RerankDescriptor;
use crate::tokenizer::read_pad_token;
use crate::weights;

/// Eight, as in every other pipeline.
const DEFAULT_BATCH_SIZE: usize = 8;

pub struct TextCrossEncoderBuilder {
  spec: String,
  revision: String,
  device: Option<Device>,
  dtype: Option<DType>,
  max_length: Option<usize>,
  batch_size: usize,
  descriptor: Option<RerankDescriptor>,
}

impl TextCrossEncoderBuilder {
  pub(crate) fn new(spec: impl Into<String>) -> Self {
    Self {
      spec: spec.into(),
      revision: "main".to_string(),
      device: None,
      dtype: None,
      max_length: None,
      batch_size: DEFAULT_BATCH_SIZE,
      descriptor: None,
    }
  }

  pub fn device(mut self, device: Device) -> Self {
    self.device = Some(device);
    self
  }

  pub fn dtype(mut self, dtype: DType) -> Self {
    self.dtype = Some(dtype);
    self
  }

  /// Capped at the model's own `max_position_embeddings` when building.
  pub fn max_length(mut self, max_length: usize) -> Self {
    self.max_length = Some(max_length);
    self
  }

  pub fn batch_size(mut self, batch_size: usize) -> Self {
    self.batch_size = batch_size.max(1);
    self
  }

  pub fn revision(mut self, revision: impl Into<String>) -> Self {
    self.revision = revision.into();
    self
  }

  /// Supplies or overrides the descriptor, for a rerank head outside the registry.
  pub fn descriptor(mut self, descriptor: RerankDescriptor) -> Self {
    self.descriptor = Some(descriptor);
    self
  }

  /// A dtype the descriptor's row refuses on the device is refused before anything is fetched, so a refused
  /// request never downloads the weights it cannot run.
  pub fn build(self) -> Result<TextCrossEncoder> {
    let overridden = self.descriptor.is_some();
    let descriptor = match self.descriptor {
      Some(descriptor) => descriptor,
      None => {
        registry::rerank::lookup(&self.spec).ok_or_else(|| CanddingError::UnregisteredModel {
          id: self.spec.clone(),
          catalog: Catalog::Rerank,
        })?
      }
    };
    let spec = if overridden {
      self.spec.as_str()
    } else {
      descriptor.id.as_str()
    };
    let device = match self.device {
      Some(device) => device,
      None => device::auto()?,
    };
    let dtype = resolve_dtype(&self.spec, self.dtype, descriptor.default_dtype, &device)?;
    reject_unsupported_dtype(&self.spec, dtype, descriptor.dtype_backends, &device)?;
    let source = ModelSource::parse(spec, &self.revision);
    let (size_gb, stored) = (descriptor.size_gb, descriptor.default_dtype);
    fit::check_download(&self.spec, &device, &source, size_gb, stored, dtype)?;
    let dir = hub::resolve(&source, descriptor.weights)?;
    let files = loader::discover(&dir, loader::Pipeline::Rerank)?;
    fit::check_files(&self.spec, &device, &fit::at(&files.weights, dtype))?;
    let config_json = fs::read_to_string(&files.config)?;
    let architecture = build::architecture(&descriptor, &files.config)?;
    tracing::info!(model = %self.spec, device = device::describe(&device), dtype = dtype.as_str(), "loading rerank model");
    let vb = unsafe { weights::var_builder(&files.weights, dtype, &device)? };
    let model = models::load_rerank(
      &architecture,
      &config_json,
      vb,
      build::conventions_from_descriptor(&descriptor),
    )?;
    let max_length = self
      .max_length
      .unwrap_or(descriptor.default_max_length)
      .min(model.max_position_embeddings());
    let pad = read_pad_token(&config_json, files.tokenizer_config.as_deref())?;
    let tokenizer =
      InputTokenizer::from_file(&descriptor.input, &files.tokenizer, max_length, pad)?;
    Ok(TextCrossEncoder::new(
      model,
      tokenizer,
      descriptor,
      device,
      dtype,
      self.batch_size,
    ))
  }
}

#[cfg(test)]
mod tests;