candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
use super::*;
use crate::error::CanddingError;

/// `nomic-ai/nomic-embed-text-v1`'s published `config.json` (revision `3ac47f125a41`), the fields this struct reads.
pub(crate) const V1: &str = r#"{
    "activation_function": "swiglu",
    "causal": false,
    "layer_norm_epsilon": 1e-12,
    "max_position_embeddings": 8192,
    "mlp_fc1_bias": false,
    "mlp_fc2_bias": false,
    "n_embd": 768,
    "n_head": 12,
    "n_inner": 3072,
    "n_layer": 12,
    "pad_token_id": 0,
    "prenorm": false,
    "qkv_proj_bias": false,
    "rotary_emb_base": 1000,
    "rotary_emb_fraction": 1.0,
    "rotary_emb_interleaved": false,
    "rotary_emb_scale_base": null,
    "rotary_scaling_factor": 2,
    "type_vocab_size": 2,
    "vocab_size": 30528
  }"#;

/// `nomic-ai/nomic-embed-text-v1.5`'s published `config.json` (revision `e9b6763023c6`).
pub(crate) const V1_5: &str = r#"{
    "activation_function": "swiglu",
    "causal": false,
    "layer_norm_epsilon": 1e-12,
    "max_position_embeddings": 2048,
    "max_trained_positions": 2048,
    "mlp_fc1_bias": false,
    "mlp_fc2_bias": false,
    "n_embd": 768,
    "n_head": 12,
    "n_inner": 3072,
    "n_layer": 12,
    "pad_token_id": 0,
    "prenorm": false,
    "qkv_proj_bias": false,
    "rotary_emb_base": 1000,
    "rotary_emb_fraction": 1.0,
    "rotary_emb_interleaved": false,
    "rotary_emb_scale_base": null,
    "rotary_scaling_factor": null,
    "type_vocab_size": 2,
    "vocab_size": 30528
  }"#;

/// `Snowflake/snowflake-arctic-embed-m-long`'s published `config.json` (revision `92d97331f1f4`).
const SNOWFLAKE: &str = r#"{
    "activation_function": "swiglu",
    "causal": false,
    "layer_norm_epsilon": 1e-12,
    "max_trained_positions": 2048,
    "mlp_fc1_bias": false,
    "mlp_fc2_bias": false,
    "n_embd": 768,
    "n_head": 12,
    "n_inner": 3072,
    "n_layer": 12,
    "prenorm": false,
    "qkv_proj_bias": false,
    "rotary_emb_base": 1000,
    "rotary_emb_fraction": 1.0,
    "rotary_emb_interleaved": false,
    "rotary_emb_scale_base": null,
    "rotary_scaling_factor": 2,
    "type_vocab_size": 2,
    "vocab_size": 30528
  }"#;

#[test]
fn reads_the_three_published_configs() {
  let cases: [(&str, &str, Option<usize>, Option<f64>); 3] = [
    ("v1", V1, Some(8192), Some(2.0)),
    ("v1.5", V1_5, Some(2048), None),
    ("snowflake", SNOWFLAKE, None, Some(2.0)),
  ];
  for (name, json, max_position_embeddings, scaling_factor) in cases {
    let config: NomicBertConfig = serde_json::from_str(json).unwrap();
    assert_eq!(config.head_dim(), 64, "{name}");
    config.ensure_supported().unwrap();
    assert_eq!(config.scaling_factor(), scaling_factor, "{name}");
    assert_eq!(config.max_trained_positions, 2048, "{name}");
    assert_eq!(
      config.max_position_embeddings, max_position_embeddings,
      "{name}"
    );
  }
}

/// This family's base is 1000, not the usual 10000, so a missing `rotary_emb_base` must fail rather than default.
#[test]
fn a_missing_rotary_emb_base_is_a_deserialization_error() {
  let stripped = V1.replace("\"rotary_emb_base\": 1000,\n    ", "");
  assert!(serde_json::from_str::<NomicBertConfig>(&stripped).is_err());
}

#[test]
fn an_absent_rotary_scaling_factor_is_none() {
  let stripped = V1.replace("\"rotary_scaling_factor\": 2,\n    ", "");
  assert_ne!(
    stripped, V1,
    "rotary_scaling_factor: mutation did not match anything in V1"
  );
  let config: NomicBertConfig = serde_json::from_str(&stripped).unwrap();
  assert_eq!(config.scaling_factor(), None);
}

/// Every mutation parses, so each refusal is `ensure_supported`'s, not serde's.
#[test]
fn each_refusal_names_the_field_and_the_value() {
  let cases = [
    (
      "\"activation_function\": \"swiglu\",",
      "\"activation_function\": \"gelu\",",
      "activation_function \"gelu\" is not supported",
    ),
    (
      "\"activation_function\": \"swiglu\",",
      "\"activation_function\": \"glu\",",
      "activation_function \"glu\" is not supported",
    ),
    (
      "\"prenorm\": false,",
      "\"prenorm\": true,",
      "prenorm true is not supported",
    ),
    (
      "\"causal\": false,",
      "\"causal\": true,",
      "causal true is not supported",
    ),
    (
      "\"rotary_emb_fraction\": 1.0,",
      "\"rotary_emb_fraction\": 0.5,",
      "rotary_emb_fraction 0.5 is not supported",
    ),
    (
      "\"rotary_emb_fraction\": 1.0,",
      "\"rotary_emb_fraction\": 0.0,",
      "rotary_emb_fraction 0 is not supported",
    ),
    (
      "\"rotary_emb_interleaved\": false,",
      "\"rotary_emb_interleaved\": true,",
      "rotary_emb_interleaved true is not supported",
    ),
    (
      "\"qkv_proj_bias\": false,",
      "\"qkv_proj_bias\": true,",
      "qkv_proj_bias true is not supported",
    ),
    (
      "\"mlp_fc1_bias\": false,",
      "\"mlp_fc1_bias\": true,",
      "mlp_fc1_bias true is not supported",
    ),
    (
      "\"mlp_fc2_bias\": false,",
      "\"mlp_fc2_bias\": true,",
      "mlp_fc2_bias true is not supported",
    ),
    (
      "\"rotary_emb_scale_base\": null,",
      "\"rotary_emb_scale_base\": 8,",
      "rotary_emb_scale_base 8 is not supported",
    ),
    (
      "\"n_layer\": 12,",
      "\"n_layer\": 12,\n    \"norm_mlp\": true,",
      "norm_mlp true is not supported",
    ),
  ];
  for (from, to, needle) in cases {
    let mutated = V1.replace(from, to);
    assert_ne!(
      mutated, V1,
      "{needle}: mutation did not match anything in V1"
    );
    let config: NomicBertConfig = serde_json::from_str(&mutated).unwrap_or_else(|e| {
      panic!("{needle}: mutated body failed to parse: {e}");
    });
    let err = config.ensure_supported().unwrap_err();
    assert!(
      matches!(err, CanddingError::UnsupportedArchitecture(ref m) if m.contains(needle)),
      "{needle}: got {err}"
    );
  }
}