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 serde::Deserialize;

use crate::layers::activation::Activation;

#[derive(Clone, Debug, Deserialize)]
pub struct Qwen3Config {
  pub vocab_size: usize,
  pub hidden_size: usize,
  pub num_hidden_layers: usize,
  pub num_attention_heads: usize,
  pub num_key_value_heads: usize,
  pub head_dim: usize,
  pub intermediate_size: usize,
  #[serde(default = "default_act")]
  pub hidden_act: Activation,
  pub max_position_embeddings: usize,
  #[serde(default = "default_eps")]
  pub rms_norm_eps: f64,
  #[serde(default = "default_theta")]
  pub rope_theta: f64,
  #[serde(default)]
  pub attention_bias: bool,
  #[serde(default)]
  pub use_sliding_window: bool,
  /// `false` asks for bidirectional attention (splade-code); absent means `true`, keeping older checkpoints causal.
  /// Dense Qwen3 reads it too: a local `"is_causal": false` under last-token pooling is accepted without a warning.
  #[serde(default = "default_is_causal")]
  pub is_causal: bool,
  /// Whether the output projection is the token embedding table: `true` for the 0.6B and 4B rerankers, `false` for
  /// the 8B, which publishes `lm_head.weight`. transformers defaults it to `false`; only the logit-score head reads it.
  #[serde(default)]
  pub tie_word_embeddings: bool,
}

fn default_act() -> Activation {
  Activation::Silu
}

fn default_eps() -> f64 {
  1e-6
}

fn default_theta() -> f64 {
  1_000_000.0
}

fn default_is_causal() -> bool {
  true
}

#[cfg(test)]
mod tests {
  use super::*;

  const MINIMAL: &str = r#"{
    "vocab_size": 32,
    "hidden_size": 16,
    "num_hidden_layers": 2,
    "num_attention_heads": 4,
    "num_key_value_heads": 2,
    "head_dim": 4,
    "intermediate_size": 32,
    "max_position_embeddings": 16
  }"#;

  /// Earlier Qwen3 checkpoints never declared `is_causal`, so a missing key must stay causal.
  #[test]
  fn is_causal_defaults_to_true_when_the_key_is_absent() {
    let config: Qwen3Config = serde_json::from_str(MINIMAL).unwrap();
    assert!(config.is_causal);
  }

  #[test]
  fn is_causal_parses_false_when_present() {
    let json = MINIMAL.replacen('}', r#", "is_causal": false}"#, 1);
    let config: Qwen3Config = serde_json::from_str(&json).unwrap();
    assert!(!config.is_causal);
  }

  #[test]
  fn tie_word_embeddings_defaults_to_false_and_is_read_when_present() {
    let config: Qwen3Config = serde_json::from_str(MINIMAL).unwrap();
    assert!(!config.tie_word_embeddings);
    let json = MINIMAL.replacen('}', r#", "tie_word_embeddings": true}"#, 1);
    let config: Qwen3Config = serde_json::from_str(&json).unwrap();
    assert!(config.tie_word_embeddings);
  }
}