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, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LayerType {
  SlidingAttention,
  FullAttention,
}

#[derive(Clone, Debug, Deserialize)]
pub struct Gemma3Config {
  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_activation: Activation,
  pub max_position_embeddings: usize,
  #[serde(default = "default_eps")]
  pub rms_norm_eps: f64,
  #[serde(default = "default_global_theta")]
  pub rope_theta: f64,
  #[serde(default = "default_local_theta")]
  pub rope_local_base_freq: f64,
  #[serde(default = "default_window")]
  pub sliding_window: usize,
  #[serde(default)]
  pub layer_types: Option<Vec<LayerType>>,
  #[serde(default, rename = "_sliding_window_pattern")]
  pub sliding_window_pattern: Option<usize>,
  pub query_pre_attn_scalar: f64,
  #[serde(default)]
  pub use_bidirectional_attention: bool,
  #[serde(default)]
  pub attention_bias: bool,
  #[serde(default)]
  pub attn_logit_softcapping: Option<f64>,
  #[serde(default)]
  pub rope_scaling: Option<serde_json::Value>,
}

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

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

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

fn default_local_theta() -> f64 {
  10_000.0
}

fn default_window() -> usize {
  512
}

impl Gemma3Config {
  /// `layer_types` from the config, else every `pattern`-th layer (default 6) is global, the rest sliding.
  pub fn layer_types(&self) -> Vec<LayerType> {
    if let Some(types) = &self.layer_types {
      return types.clone();
    }
    let pattern = self.sliding_window_pattern.unwrap_or(6).max(1);
    (0..self.num_hidden_layers)
      .map(|i| {
        if (i + 1) % pattern == 0 {
          LayerType::FullAttention
        } else {
          LayerType::SlidingAttention
        }
      })
      .collect()
  }

  /// transformers halves the stored span and adds one when bidirectional: a stored 512 becomes `|i - j| < 257`.
  pub fn attention_window(&self) -> usize {
    if self.use_bidirectional_attention {
      self.sliding_window / 2 + 1
    } else {
      self.sliding_window
    }
  }
}

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

  fn config(sliding_window: usize) -> Gemma3Config {
    Gemma3Config {
      vocab_size: 1,
      hidden_size: 1,
      num_hidden_layers: 1,
      num_attention_heads: 1,
      num_key_value_heads: 1,
      head_dim: 1,
      intermediate_size: 1,
      hidden_activation: Activation::GeluPytorchTanh,
      max_position_embeddings: 1,
      rms_norm_eps: 1e-6,
      rope_theta: 1.0,
      rope_local_base_freq: 1.0,
      sliding_window,
      layer_types: None,
      sliding_window_pattern: None,
      query_pre_attn_scalar: 1.0,
      use_bidirectional_attention: true,
      attention_bias: false,
      attn_logit_softcapping: None,
      rope_scaling: None,
    }
  }

  #[test]
  fn attention_window_halves_the_stored_span_and_adds_one() {
    assert_eq!(config(512).attention_window(), 257);
    assert_eq!(config(6).attention_window(), 4);
    assert_eq!(config(513).attention_window(), 257);
  }

  #[test]
  fn attention_window_keeps_the_stored_span_without_bidirectional_attention() {
    let mut causal = config(512);
    causal.use_bidirectional_attention = false;
    assert_eq!(causal.attention_window(), 512);
  }
}