use candle_core::DType;
use candle_core::Device;
use super::METAL_FORWARD_LOCK;
use super::batching;
use super::builder::TextEmbeddingBuilder;
use super::postprocess;
use crate::error::CanddingError;
use crate::error::Result;
use crate::layers::dense::Dense;
use crate::models::Encoder;
use crate::models::EncoderInput;
use crate::pooling;
use crate::registry::ModelDescriptor;
use crate::template;
use crate::tokenizer::TextTokenizer;
pub struct TextEmbedding {
encoder: Box<dyn Encoder>,
tokenizer: TextTokenizer,
dense: Vec<Dense>,
descriptor: ModelDescriptor,
device: Device,
dtype: DType,
batch_size: usize,
}
impl TextEmbedding {
pub fn builder(spec: impl Into<String>) -> TextEmbeddingBuilder {
TextEmbeddingBuilder::new(spec)
}
pub(crate) fn new(
encoder: Box<dyn Encoder>,
tokenizer: TextTokenizer,
dense: Vec<Dense>,
descriptor: ModelDescriptor,
device: Device,
dtype: DType,
batch_size: usize,
) -> Self {
Self {
encoder,
tokenizer,
dense,
descriptor,
device,
dtype,
batch_size,
}
}
pub fn descriptor(&self) -> &ModelDescriptor {
&self.descriptor
}
pub fn dim(&self) -> usize {
self.descriptor.dim
}
pub fn max_length(&self) -> usize {
self.tokenizer.max_length()
}
pub fn device(&self) -> &Device {
&self.device
}
pub fn dtype(&self) -> DType {
self.dtype
}
pub fn weights_dtype(&self) -> DType {
self.encoder.weights_dtype()
}
pub fn embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
self.run(plain(texts), None, batch_size)
}
pub fn embed_dim<S: AsRef<str>>(
&self,
texts: &[S],
dim: usize,
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
self.run(plain(texts), Some(dim), batch_size)
}
pub fn query_embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
self.run(
self.queries(self.descriptor.default_instruction.as_deref(), texts),
None,
batch_size,
)
}
pub fn query_embed_dim<S: AsRef<str>>(
&self,
texts: &[S],
dim: usize,
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
self.run(
self.queries(self.descriptor.default_instruction.as_deref(), texts),
Some(dim),
batch_size,
)
}
pub fn query_embed_with_instruction<S: AsRef<str>>(
&self,
texts: &[S],
instruction: &str,
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
self.run(self.queries(Some(instruction), texts), None, batch_size)
}
pub fn passage_embed<S: AsRef<str>>(
&self,
texts: &[S],
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
self.run(self.passages(texts), None, batch_size)
}
pub fn passage_embed_dim<S: AsRef<str>>(
&self,
texts: &[S],
dim: usize,
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
self.run(self.passages(texts), Some(dim), batch_size)
}
pub fn token_count<S: AsRef<str>>(&self, texts: &[S]) -> Result<Vec<usize>> {
self.count(plain(texts))
}
pub fn query_token_count<S: AsRef<str>>(&self, texts: &[S]) -> Result<Vec<usize>> {
self.count(self.queries(self.descriptor.default_instruction.as_deref(), texts))
}
pub fn query_token_count_with_instruction<S: AsRef<str>>(
&self,
texts: &[S],
instruction: &str,
) -> Result<Vec<usize>> {
self.count(self.queries(Some(instruction), texts))
}
pub fn passage_token_count<S: AsRef<str>>(&self, texts: &[S]) -> Result<Vec<usize>> {
self.count(self.passages(texts))
}
fn count(&self, texts: Vec<String>) -> Result<Vec<usize>> {
let refs: Vec<&str> = texts.iter().map(String::as_str).collect();
self.tokenizer.count(&refs)
}
fn queries<S: AsRef<str>>(&self, instruction: Option<&str>, texts: &[S]) -> Vec<String> {
let template = self.descriptor.query_template.as_deref();
texts
.iter()
.map(|t| template::apply(template, instruction, t.as_ref()))
.collect()
}
fn passages<S: AsRef<str>>(&self, texts: &[S]) -> Vec<String> {
let template = self.descriptor.passage_template.as_deref();
texts
.iter()
.map(|t| template::apply(template, None, t.as_ref()))
.collect()
}
fn run(
&self,
texts: Vec<String>,
dim: Option<usize>,
batch_size: Option<usize>,
) -> Result<Vec<Vec<f32>>> {
if dim.is_some() && !self.descriptor.mrl {
return Err(CanddingError::MrlUnsupported(self.descriptor.id.clone()));
}
if texts.is_empty() {
return Ok(Vec::new());
}
let batch_size = batch_size.unwrap_or(self.batch_size).max(1);
let refs: Vec<&str> = texts.iter().map(String::as_str).collect();
let lengths = self.tokenizer.count(&refs)?;
let order = batching::sorted_indices(&lengths);
let mut rows = Vec::with_capacity(texts.len());
for chunk in order.chunks(batch_size) {
let chunk_texts: Vec<&str> = chunk.iter().map(|i| refs[*i]).collect();
rows.extend(self.embed_chunk(&chunk_texts, dim)?);
}
Ok(batching::restore(&order, rows))
}
fn embed_chunk(&self, texts: &[&str], dim: Option<usize>) -> Result<Vec<Vec<f32>>> {
let _guard = self.device.is_metal().then(|| {
METAL_FORWARD_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
});
let batch = self.tokenizer.encode(texts, &self.device)?;
let input = EncoderInput {
input_ids: &batch.input_ids,
attention_mask: &batch.attention_mask,
token_type_ids: &batch.token_type_ids,
lengths: &batch.lengths,
};
let hidden = self.encoder.forward(&input)?;
let pooled = pooling::pool(
self.descriptor.pooling,
&hidden,
&batch.attention_mask,
&batch.lengths,
)?
.to_dtype(DType::F32)?;
let pooled = self
.dense
.iter()
.try_fold(pooled, |xs, dense| dense.forward(&xs))?;
let pooled = if self.descriptor.layer_norm_after_pooling {
postprocess::layer_norm(&pooled, 1e-5)?
} else {
pooled
};
let pooled = match dim {
Some(dim) => postprocess::truncate_dim(&pooled, dim)?,
None => pooled,
};
let pooled = if self.descriptor.normalize {
postprocess::normalize(&pooled)?
} else {
pooled
};
postprocess::to_rows(&pooled)
}
}
fn plain<S: AsRef<str>>(texts: &[S]) -> Vec<String> {
texts.iter().map(|t| t.as_ref().to_string()).collect()
}
#[cfg(test)]
mod tests;