use candle_core::Tensor;
use tempfile::TempDir;
use tempfile::tempdir;
use super::*;
use crate::models::EncoderInput;
const TRANSFORMER_POOLING: &str = r#"[{"idx":0,"name":"0","path":"","type":"sentence_transformers.models.Transformer"},{"idx":1,"name":"1","path":"1_Pooling","type":"sentence_transformers.models.Pooling"}]"#;
const WITH_NORMALIZE: &str = r#"[{"idx":0,"name":"0","path":"","type":"sentence_transformers.models.Transformer"},{"idx":1,"name":"1","path":"1_Pooling","type":"sentence_transformers.models.Pooling"},{"idx":2,"name":"2","path":"2_Normalize","type":"sentence_transformers.models.Normalize"}]"#;
struct StubEncoder(usize);
impl Encoder for StubEncoder {
fn forward(&self, _input: &EncoderInput) -> Result<Tensor> {
unreachable!("a descriptor never runs the encoder")
}
fn hidden_size(&self) -> usize {
384
}
fn max_position_embeddings(&self) -> usize {
self.0
}
fn weights_dtype(&self) -> DType {
DType::F32
}
}
fn bare_snapshot() -> TempDir {
let dir = tempdir().unwrap();
fs::write(dir.path().join("config.json"), r#"{"model_type":"bert"}"#).unwrap();
fs::write(dir.path().join("tokenizer.json"), "{}").unwrap();
fs::write(dir.path().join("model.safetensors"), "").unwrap();
dir
}
fn snapshot_descriptor(dir: &TempDir, encoder: &dyn Encoder) -> ModelDescriptor {
let files = loader::discover(dir.path(), loader::Pipeline::Dense).unwrap();
descriptor_from_snapshot("local", Family::Bert, &files, encoder, &[]).unwrap()
}
#[test]
fn a_snapshot_descriptor_takes_its_max_length_from_sentence_bert_config() {
let dir = bare_snapshot();
assert_eq!(
snapshot_descriptor(&dir, &StubEncoder(512)).default_max_length,
512,
"without the file the rule is min(max_position_embeddings, 512)"
);
fs::write(
dir.path().join("sentence_bert_config.json"),
r#"{"max_seq_length": 256, "do_lower_case": false}"#,
)
.unwrap();
assert_eq!(
snapshot_descriptor(&dir, &StubEncoder(512)).default_max_length,
256
);
fs::write(
dir.path().join("sentence_bert_config.json"),
r#"{"max_seq_length": 8192}"#,
)
.unwrap();
assert_eq!(
snapshot_descriptor(&dir, &StubEncoder(512)).default_max_length,
512,
"the position table still caps it"
);
}
#[test]
fn a_snapshot_descriptor_normalizes_whatever_modules_json_lists() {
let dir = bare_snapshot();
assert!(
snapshot_descriptor(&dir, &StubEncoder(512)).normalize,
"no modules.json"
);
fs::write(dir.path().join("modules.json"), TRANSFORMER_POOLING).unwrap();
assert!(
snapshot_descriptor(&dir, &StubEncoder(512)).normalize,
"a modules.json without a Normalize module"
);
fs::write(dir.path().join("modules.json"), WITH_NORMALIZE).unwrap();
assert!(
snapshot_descriptor(&dir, &StubEncoder(512)).normalize,
"a modules.json with one"
);
}
#[test]
fn a_built_snapshot_returns_unit_rows_unless_normalize_is_turned_off() {
let mut tensors = synthetic::zero_bert("", 16);
let bias = Tensor::new(&[3f32, 4.0, 0.0, 0.0], &Device::Cpu).unwrap();
tensors.insert("embeddings.LayerNorm.bias".to_string(), bias);
let dir = synthetic::checkpoint(&synthetic::bert_config(16), "tiny_tokenizer.json", &tensors);
fs::write(dir.path().join("modules.json"), TRANSFORMER_POOLING).unwrap();
let spec = dir.path().to_str().unwrap();
let row = |builder: TextEmbeddingBuilder| {
let model = builder.device(Device::Cpu).build().unwrap();
model.embed(&["hello world"], None).unwrap().remove(0)
};
for (builder, expected) in [
(TextEmbedding::builder(spec), [0.6, 0.8, 0.0, 0.0]),
(
TextEmbedding::builder(spec).normalize(false),
[3.0, 4.0, 0.0, 0.0],
),
] {
let actual = row(builder);
assert_eq!(actual.len(), expected.len());
for (a, e) in actual.iter().zip(expected) {
assert!((a - e).abs() < 1e-6, "{actual:?} against {expected:?}");
}
}
}