use std::path::Path;
use candle_core::DType;
use candle_core::Tensor;
use candle_core::pickle::PthTensors;
use candle_nn::Linear;
use candle_nn::Module;
use candle_nn::VarBuilder;
use crate::error::CanddingError;
use crate::error::Result;
use crate::models::Encoder;
use crate::models::EncoderInput;
use crate::models::MultiVectorConventions;
use crate::models::MultiVectorModel;
use crate::models::multi_vector::l2_normalize_rows;
use crate::models::xlm_roberta::XlmRobertaConfig;
use crate::models::xlm_roberta::XlmRobertaModel;
use crate::multi_vector::MultiVectorEmbedding;
#[derive(Debug)]
pub struct M3Colbert {
encoder: XlmRobertaModel,
weight: Tensor,
bias: Option<Tensor>,
dim: usize,
normalize: bool,
keeps_cls_row: bool,
}
fn read_tensor(pth: &PthTensors, name: &str, file: &Path) -> Result<Tensor> {
pth.get(name)?.ok_or_else(|| CanddingError::MissingTensor {
file: file.to_path_buf(),
name: name.to_string(),
})
}
impl M3Colbert {
pub fn load(
config: XlmRobertaConfig,
vb: VarBuilder,
colbert_linear: &Path,
conventions: MultiVectorConventions,
) -> Result<Self> {
if !colbert_linear.is_file() {
return Err(CanddingError::MissingFile(colbert_linear.to_path_buf()));
}
let pth = PthTensors::new(colbert_linear, None)?;
let weight = read_tensor(&pth, "weight", colbert_linear)?;
let bias = if conventions.projection_bias {
Some(read_tensor(&pth, "bias", colbert_linear)?)
} else {
None
};
Self::from_raw_head(config, vb, weight, bias, conventions)
}
fn from_raw_head(
config: XlmRobertaConfig,
vb: VarBuilder,
weight: Tensor,
bias: Option<Tensor>,
conventions: MultiVectorConventions,
) -> Result<Self> {
let actual_in = weight.dim(1)?;
if actual_in != conventions.projection_in {
return Err(CanddingError::ProjectionInMismatch {
declared: conventions.projection_in,
actual: actual_in,
});
}
let dim = weight.dim(0)?;
let dtype = vb.dtype();
let weight = weight.to_dtype(dtype)?.to_device(vb.device())?;
let bias = bias
.map(|b| -> Result<Tensor> { Ok(b.to_dtype(dtype)?.to_device(vb.device())?) })
.transpose()?;
let encoder = XlmRobertaModel::load(config, vb)?;
Ok(Self {
encoder,
weight,
bias,
dim,
normalize: conventions.normalize,
keeps_cls_row: conventions.keeps_cls_row,
})
}
fn project_rows(&self, hidden: &Tensor, lengths: &[usize]) -> Result<Vec<MultiVectorEmbedding>> {
let seq = hidden.dim(1)?;
let drop = if self.keeps_cls_row { 0 } else { 1 };
let width = seq.saturating_sub(drop);
let narrowed = hidden.narrow(1, seq.min(drop), width)?;
let linear = Linear::new(self.weight.clone(), self.bias.clone());
let projected_raw = linear.forward(&narrowed)?;
let projected = if self.normalize {
l2_normalize_rows(&projected_raw)?
} else {
projected_raw
};
let batch = projected
.to_dtype(candle_core::DType::F32)?
.to_vec3::<f32>()?;
Ok(
batch
.into_iter()
.zip(lengths)
.map(|(rows, &len)| {
let real_rows = len.saturating_sub(drop).min(rows.len());
MultiVectorEmbedding::from_rows(self.dim, rows.into_iter().take(real_rows))
})
.collect(),
)
}
}
impl MultiVectorModel for M3Colbert {
fn forward(&self, input: &EncoderInput) -> Result<Vec<MultiVectorEmbedding>> {
let hidden = self.encoder.forward(input)?;
self.project_rows(&hidden, input.lengths)
}
fn forward_query(&self, input: &EncoderInput) -> Result<Vec<MultiVectorEmbedding>> {
self.forward(input)
}
fn dim(&self) -> usize {
self.dim
}
fn max_position_embeddings(&self) -> usize {
self.encoder.max_position_embeddings()
}
fn weights_dtype(&self) -> DType {
self.encoder.weights_dtype()
}
}
#[cfg(test)]
mod tests;