use std::collections::HashMap;
use candle_core::DType;
use candle_core::Device;
use candle_core::Tensor;
use candle_nn::VarBuilder;
use super::*;
use crate::error::CanddingError;
mod alibi;
mod flash;
const BIAS: f32 = 2.0;
const SIGMOID_OF_BIAS: f32 = 0.880_797_1;
fn zeros(shape: &[usize]) -> Tensor {
Tensor::zeros(shape, DType::F32, &Device::Cpu).unwrap()
}
fn embeddings(prefix: &str, type_vocab_size: usize) -> HashMap<String, Tensor> {
let mut tensors = HashMap::new();
for (name, shape) in [
("embeddings.word_embeddings.weight", vec![8, 4]),
("embeddings.position_embeddings.weight", vec![16, 4]),
(
"embeddings.token_type_embeddings.weight",
vec![type_vocab_size, 4],
),
("embeddings.LayerNorm.bias", vec![4]),
] {
tensors.insert(format!("{prefix}.{name}"), zeros(&shape));
}
tensors.insert(
format!("{prefix}.embeddings.LayerNorm.weight"),
Tensor::ones(4, DType::F32, &Device::Cpu).unwrap(),
);
tensors
}
fn bias() -> Tensor {
Tensor::new(&[BIAS], &Device::Cpu).unwrap()
}
fn score(model: &dyn RerankModel) -> Vec<f32> {
let input_ids = Tensor::new(&[[1u32, 2, 3, 4], [1u32, 5, 3, 0]], &Device::Cpu).unwrap();
let attention_mask = Tensor::new(&[[1u32, 1, 1, 1], [1u32, 1, 1, 0]], &Device::Cpu).unwrap();
let token_type_ids = input_ids.zeros_like().unwrap();
let lengths = vec![4, 3];
let input = EncoderInput {
input_ids: &input_ids,
attention_mask: &attention_mask,
token_type_ids: &token_type_ids,
lengths: &lengths,
};
model
.forward(&input)
.unwrap()
.iter()
.map(RerankScore::value)
.collect()
}
fn conventions(head: RerankHead, score_activation: ScoreActivation) -> RerankConventions {
RerankConventions {
head,
score_activation,
alibi_variant: None,
position_scheme: None,
}
}
#[test]
fn the_bert_pooler_head_applies_the_activation_load_rerank_hands_it() {
let mut tensors = embeddings("bert", 2);
tensors.insert("bert.pooler.dense.weight".to_string(), zeros(&[4, 4]));
tensors.insert("bert.pooler.dense.bias".to_string(), zeros(&[4]));
tensors.insert("classifier.weight".to_string(), zeros(&[1, 4]));
tensors.insert("classifier.bias".to_string(), bias());
let vb = VarBuilder::from_tensors(tensors, DType::F32, &Device::Cpu);
let config_json = r#"{"vocab_size":8,"hidden_size":4,"num_hidden_layers":0,
"num_attention_heads":1,"intermediate_size":1,"max_position_embeddings":16,
"type_vocab_size":2,"layer_norm_eps":1e-12}"#;
let model = load_rerank(
"bert",
config_json,
vb,
conventions(RerankHead::BertPooler, ScoreActivation::Sigmoid),
)
.unwrap();
for score in score(model.as_ref()) {
assert!((score - SIGMOID_OF_BIAS).abs() < 1e-6, "{score}");
}
}
#[test]
fn the_roberta_classification_head_applies_the_activation_load_rerank_hands_it() {
let mut tensors = embeddings("roberta", 1);
tensors.insert("classifier.dense.weight".to_string(), zeros(&[4, 4]));
tensors.insert("classifier.dense.bias".to_string(), zeros(&[4]));
tensors.insert("classifier.out_proj.weight".to_string(), zeros(&[1, 4]));
tensors.insert("classifier.out_proj.bias".to_string(), bias());
let vb = VarBuilder::from_tensors(tensors, DType::F32, &Device::Cpu);
let config_json = r#"{"vocab_size":8,"hidden_size":4,"num_hidden_layers":0,
"num_attention_heads":1,"intermediate_size":1,"max_position_embeddings":16,
"type_vocab_size":1,"layer_norm_eps":1e-12,"pad_token_id":1,
"position_embedding_type":"absolute"}"#;
let model = load_rerank(
"xlm-roberta",
config_json,
vb,
conventions(
RerankHead::RobertaClassificationHead,
ScoreActivation::Sigmoid,
),
)
.unwrap();
for score in score(model.as_ref()) {
assert!((score - SIGMOID_OF_BIAS).abs() < 1e-6, "{score}");
}
}
#[test]
fn head_serializes_with_a_kind_tag() {
assert_eq!(
serde_json::to_value(RerankHead::RobertaClassificationHead).unwrap(),
serde_json::json!({"kind": "roberta_classification_head"})
);
for (unembedding, name) in [(Unembedding::Tied, "tied"), (Unembedding::Untied, "untied")] {
let logit_score = RerankHead::LogitScore {
true_token_id: 9693,
false_token_id: 2152,
unembedding,
};
let json = serde_json::json!(
{"kind": "logit_score", "true_token_id": 9693, "false_token_id": 2152, "unembedding": name}
);
assert_eq!(serde_json::to_value(logit_score).unwrap(), json);
assert_eq!(
serde_json::from_value::<RerankHead>(json).unwrap(),
logit_score
);
assert_eq!(unembedding.name(), name);
}
assert_eq!(
serde_json::from_str::<RerankHead>(r#"{"kind": "bert_pooler"}"#).unwrap(),
RerankHead::BertPooler
);
}
#[test]
fn the_logit_score_head_takes_the_ids_and_the_activation_load_rerank_hands_it() {
let config_json = r#"{"vocab_size":8,"hidden_size":4,"num_hidden_layers":0,"num_attention_heads":1,
"num_key_value_heads":1,"head_dim":4,"intermediate_size":4,"max_position_embeddings":16,
"tie_word_embeddings":true}"#;
let build = |true_token_id: u32, false_token_id: u32, unembedding, activation| {
let mut embed = vec![1f32; 8 * 4];
embed[4..8].fill(0.0);
let mut tensors = HashMap::new();
tensors.insert(
"model.embed_tokens.weight".to_string(),
Tensor::from_vec(embed, (8, 4), &Device::Cpu).unwrap(),
);
tensors.insert(
"model.norm.weight".to_string(),
Tensor::ones(4, DType::F32, &Device::Cpu).unwrap(),
);
let vb = VarBuilder::from_tensors(tensors, DType::F32, &Device::Cpu);
let head = RerankHead::LogitScore {
true_token_id,
false_token_id,
unembedding,
};
load_rerank("qwen3", config_json, vb, conventions(head, activation))
};
let score_with = |true_token_id: u32, false_token_id: u32, activation: ScoreActivation| {
let model = build(true_token_id, false_token_id, Unembedding::Tied, activation).unwrap();
score(model.as_ref())
};
let untied = build(3, 1, Unembedding::Untied, ScoreActivation::Identity);
assert!(
matches!(
untied,
Err(CanddingError::UnembeddingDisagreesWithConfig { .. })
),
"{:?}",
untied.err()
);
for value in score_with(3, 1, ScoreActivation::Identity) {
assert!((value - 4.0).abs() < 1e-4, "{value}");
}
for value in score_with(1, 3, ScoreActivation::Identity) {
assert!((value + 4.0).abs() < 1e-4, "{value}");
}
for value in score_with(3, 1, ScoreActivation::Sigmoid) {
assert!(
(value - 1.0 / (1.0 + (-4.0f32).exp())).abs() < 1e-5,
"{value}"
);
}
}