use std::fs;
use candle_core::DType;
use candle_core::Device;
pub(crate) use dtype::reject_unsupported_dtype;
pub(crate) use dtype::resolve_dtype;
use super::fit;
use super::text::TextEmbedding;
use crate::device;
use crate::error::CanddingError;
use crate::error::Result;
use crate::hub;
use crate::hub::ModelSource;
use crate::layers::dense::Dense;
use crate::loader;
use crate::loader::ModelFiles;
use crate::models;
use crate::models::Encoder;
use crate::pooling::Pooling;
use crate::registry;
use crate::registry::Family;
use crate::registry::ModelDescriptor;
use crate::tokenizer::TextTokenizer;
use crate::tokenizer::read_pad_token;
use crate::weights;
mod dtype;
pub struct TextEmbeddingBuilder {
spec: String,
revision: String,
device: Option<Device>,
dtype: Option<DType>,
max_length: Option<usize>,
batch_size: usize,
descriptor: Option<ModelDescriptor>,
normalize: Option<bool>,
}
impl TextEmbeddingBuilder {
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: 32,
descriptor: None,
normalize: 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
}
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
}
pub fn descriptor(mut self, descriptor: ModelDescriptor) -> Self {
self.descriptor = Some(descriptor);
self
}
pub fn normalize(mut self, normalize: bool) -> Self {
self.normalize = Some(normalize);
self
}
pub fn build(self) -> Result<TextEmbedding> {
let source = canonical_source(&self.spec, &self.revision);
refuse_another_catalogs_id(&self.spec, &source)?;
let registered = self
.descriptor
.or_else(|| registry::dense::lookup(&self.spec));
let device = match self.device {
Some(device) => device,
None => device::auto()?,
};
let default_dtype = registered
.as_ref()
.map(|d| d.default_dtype)
.unwrap_or(DType::F32);
let dtype = resolve_dtype(&self.spec, self.dtype, default_dtype, &device)?;
if let Some(descriptor) = registered.as_ref() {
reject_unsupported_dtype(&self.spec, dtype, descriptor.dtype_backends, &device)?;
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, registry::weights_format(&self.spec))?;
let files = loader::discover(&dir, loader::Pipeline::Dense)?;
let mut weights = fit::at(&files.weights, dtype);
for dense_dir in &files.dense_dirs {
weights.push((dense_dir.join("model.safetensors"), DType::F32));
}
fit::check_files(&self.spec, &device, &weights)?;
let config_json = fs::read_to_string(&files.config)?;
let architecture = loader::read_architecture(&files.config)?;
let family = Family::from_model_type(&architecture)
.ok_or_else(|| CanddingError::UnsupportedArchitecture(architecture.clone()))?;
tracing::info!(model = %self.spec, device = device::describe(&device), dtype = dtype.as_str(), "loading");
let vb = unsafe { weights::var_builder(&files.weights, dtype, &device)? };
let encoder = models::load_encoder(&architecture, &config_json, vb)?;
let dense = files
.dense_dirs
.iter()
.map(|dir| Dense::load(dir, &device))
.collect::<Result<Vec<_>>>()?;
let mut descriptor = match registered {
Some(descriptor) => descriptor,
None => descriptor_from_snapshot(&self.spec, family, &files, encoder.as_ref(), &dense)?,
};
let found = dense_names(&files);
if descriptor.dense_dirs != found {
return Err(CanddingError::DenseMismatch {
expected: descriptor.dense_dirs.clone(),
found,
});
}
if let Some(normalize) = self.normalize {
descriptor.normalize = normalize;
}
let max_length = self
.max_length
.unwrap_or(descriptor.default_max_length)
.min(encoder.max_position_embeddings());
let pad = read_pad_token(&config_json, files.tokenizer_config.as_deref())?;
let tokenizer = TextTokenizer::from_file(&files.tokenizer, max_length, pad)?;
Ok(TextEmbedding::new(
encoder,
tokenizer,
dense,
descriptor,
device,
dtype,
self.batch_size,
))
}
}
fn refuse_another_catalogs_id(spec: &str, source: &ModelSource) -> Result<()> {
if let ModelSource::Local(_) = source {
return Ok(());
}
match registry::other_catalog(spec) {
Some(catalog) => Err(CanddingError::NonDenseModel {
model: spec.to_string(),
catalog,
}),
None => Ok(()),
}
}
fn canonical_source(spec: &str, revision: &str) -> ModelSource {
match ModelSource::parse(spec, revision) {
ModelSource::Hub { repo_id, revision } => {
let canonical = registry::canonical_id(&repo_id);
ModelSource::Hub {
repo_id: canonical.map_or(repo_id, str::to_string),
revision,
}
}
local => local,
}
}
fn dense_names(files: &ModelFiles) -> Vec<String> {
files
.dense_dirs
.iter()
.filter_map(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned())
.collect()
}
fn descriptor_from_snapshot(
id: &str,
family: Family,
files: &ModelFiles,
encoder: &dyn Encoder,
dense: &[Dense],
) -> Result<ModelDescriptor> {
let pooling = match &files.pooling_config {
Some(path) => loader::read_pooling(path)?,
None => Pooling::Mean,
};
let max_seq_length = match &files.sentence_bert_config {
Some(path) => loader::read_max_seq_length(path)?,
None => None,
};
let dim = dense
.last()
.map(Dense::out_features)
.unwrap_or(encoder.hidden_size());
let mut descriptor = ModelDescriptor::new(id, family, dim, pooling);
descriptor.default_max_length = max_seq_length
.unwrap_or(descriptor.default_max_length)
.min(encoder.max_position_embeddings());
descriptor.dense_dirs = dense_names(files);
Ok(descriptor)
}
#[cfg(test)]
mod tests;