use candle_core::DType;
use candle_core::Device;
use candle_core::Tensor;
use candle_nn::VarBuilder;
use super::RerankModel;
use super::Unembedding;
use crate::error::CanddingError;
use crate::error::Result;
use crate::models::Encoder;
use crate::models::EncoderInput;
use crate::models::qwen3::Qwen3Config;
use crate::models::qwen3::Qwen3Model;
use crate::pooling;
use crate::pooling::Pooling;
use crate::rerank::RerankScore;
use crate::rerank::ScoreActivation;
pub struct LogitScoreRerank {
encoder: Qwen3Model,
rows: Tensor,
score_activation: ScoreActivation,
}
impl LogitScoreRerank {
pub fn load(
model_type: &str,
config_json: &str,
vb: VarBuilder,
token_ids: [u32; 2],
unembedding: Unembedding,
score_activation: ScoreActivation,
) -> Result<Self> {
if model_type != "qwen3" {
return Err(CanddingError::UnsupportedArchitecture(
model_type.to_string(),
));
}
let config: Qwen3Config = serde_json::from_str(config_json)?;
let (vocab_size, hidden) = (config.vocab_size, config.hidden_size);
if let Some(&token_id) = token_ids.iter().find(|&&id| id as usize >= vocab_size) {
return Err(CanddingError::LogitTokenOutOfRange {
token_id,
vocab_size,
});
}
let tied = unembedding == Unembedding::Tied;
if tied != config.tie_word_embeddings {
return Err(CanddingError::UnembeddingDisagreesWithConfig {
declared: unembedding.name().to_string(),
tie_word_embeddings: config.tie_word_embeddings,
});
}
let lm_head = vb.contains_tensor("lm_head.weight");
let untied_rows = match (tied, lm_head) {
(true, false) => None,
(true, true) => return Err(CanddingError::TiedEmbeddingsWithLmHead),
(false, true) => {
let matrix = vb
.clone()
.set_device(Device::Cpu)
.get((vocab_size, hidden), "lm_head.weight")?;
Some(select_rows(&matrix, token_ids)?.to_device(vb.device())?)
}
(false, false) => return Err(CanddingError::MissingLmHead),
};
let encoder = Qwen3Model::load(config, vb.clone())?;
let rows = match untied_rows {
Some(rows) => rows,
None => select_rows(encoder.embed_tokens(), token_ids)?,
};
Ok(Self {
encoder,
rows: rows.t()?.contiguous()?,
score_activation,
})
}
}
fn select_rows(matrix: &Tensor, token_ids: [u32; 2]) -> Result<Tensor> {
let index = Tensor::new(&token_ids, matrix.device())?;
Ok(matrix.index_select(&index, 0)?)
}
impl RerankModel for LogitScoreRerank {
fn forward(&self, input: &EncoderInput) -> Result<Vec<RerankScore>> {
let hidden = self.encoder.forward(input)?;
let last = pooling::pool(
Pooling::LastToken,
&hidden,
input.attention_mask,
input.lengths,
)?
.contiguous()?;
let logits = last.matmul(&self.rows)?;
let difference = (logits.narrow(1, 0, 1)? - logits.narrow(1, 1, 1)?)?;
let raw = difference
.squeeze(1)?
.to_dtype(DType::F32)?
.to_vec1::<f32>()?;
Ok(
raw
.into_iter()
.map(|raw| RerankScore::new(self.score_activation.apply(raw)))
.collect(),
)
}
fn max_position_embeddings(&self) -> usize {
self.encoder.max_position_embeddings()
}
fn weights_dtype(&self) -> DType {
self.encoder.weights_dtype()
}
}
#[cfg(test)]
mod tests;