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 candle_core::DType;
use candle_core::Module;
use candle_core::Tensor;
use candle_nn::Embedding;
use candle_nn::VarBuilder;
use candle_nn::embedding;

use super::config::Gemma3Config;
use super::layer::Gemma3Layer;
use super::layer::LayerContext;
use crate::error::CanddingError;
use crate::error::Result;
use crate::layers::activation::Activation;
use crate::layers::mask::AttentionMask;
use crate::layers::rms_norm::RmsNorm;
use crate::layers::rms_norm::RmsNormStyle;
use crate::layers::rope::RotaryEmbedding;
use crate::models::Encoder;
use crate::models::EncoderInput;

pub struct Gemma3Model {
  embed_tokens: Embedding,
  /// `sqrt(hidden_size)` in the model dtype, the rounding transformers applies too.
  embed_scale: Tensor,
  layers: Vec<Gemma3Layer>,
  norm: RmsNorm,
  rope_local: RotaryEmbedding,
  rope_global: RotaryEmbedding,
  config: Gemma3Config,
}

/// Manual because `RmsNorm`, `RotaryEmbedding` and `Gemma3Layer` don't derive `Debug`; tests need it for `unwrap_err`.
impl std::fmt::Debug for Gemma3Model {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("Gemma3Model").finish_non_exhaustive()
  }
}

impl Gemma3Model {
  /// Accepts the bare tensor names of the EmbeddingGemma checkpoint and the `model.` prefix of a full Gemma3 LM.
  pub fn load(config: Gemma3Config, vb: VarBuilder) -> Result<Self> {
    if !config.use_bidirectional_attention {
      return Err(CanddingError::UnsupportedArchitecture(
        "gemma3_text without bidirectional attention".to_string(),
      ));
    }
    if config.attn_logit_softcapping.is_some() {
      return Err(CanddingError::UnsupportedArchitecture(
        "gemma3_text with attention logit softcapping".to_string(),
      ));
    }
    if config.hidden_activation != Activation::GeluPytorchTanh {
      return Err(CanddingError::UnsupportedArchitecture(format!(
        "gemma3_text with hidden_activation {:?}",
        config.hidden_activation
      )));
    }
    if config.attention_bias {
      return Err(CanddingError::UnsupportedArchitecture(
        "gemma3_text with attention bias".to_string(),
      ));
    }
    if config.rope_scaling.is_some() {
      return Err(CanddingError::UnsupportedArchitecture(
        "gemma3_text with rope scaling".to_string(),
      ));
    }
    if let Some(types) = &config.layer_types
      && types.len() != config.num_hidden_layers
    {
      return Err(CanddingError::UnsupportedArchitecture(format!(
        "gemma3_text with {} layer_types for {} layers",
        types.len(),
        config.num_hidden_layers
      )));
    }
    let vb = if vb.contains_tensor("embed_tokens.weight") {
      vb
    } else {
      vb.pp("model")
    };
    let embed_tokens = embedding(config.vocab_size, config.hidden_size, vb.pp("embed_tokens"))?;
    let embed_scale =
      Tensor::new((config.hidden_size as f64).sqrt() as f32, vb.device())?.to_dtype(vb.dtype())?;
    let layers = config
      .layer_types()
      .into_iter()
      .enumerate()
      .map(|(i, layer_type)| Gemma3Layer::load(&config, layer_type, vb.pp(format!("layers.{i}"))))
      .collect::<Result<Vec<_>>>()?;
    let norm = RmsNorm::load(
      vb.pp("norm"),
      config.hidden_size,
      config.rms_norm_eps,
      RmsNormStyle::OnePlusScale,
    )?;
    let rope_local = RotaryEmbedding::new(
      config.head_dim,
      config.max_position_embeddings,
      config.rope_local_base_freq,
      vb.dtype(),
      vb.device(),
    )?;
    let rope_global = RotaryEmbedding::new(
      config.head_dim,
      config.max_position_embeddings,
      config.rope_theta,
      vb.dtype(),
      vb.device(),
    )?;
    Ok(Self {
      embed_tokens,
      embed_scale,
      layers,
      norm,
      rope_local,
      rope_global,
      config,
    })
  }
}

impl Encoder for Gemma3Model {
  /// Bidirectional attention needs the padding mask on every layer; sliding layers add the window on top.
  fn forward(&self, input: &EncoderInput) -> Result<Tensor> {
    let mut hidden = self
      .embed_tokens
      .forward(input.input_ids)?
      .broadcast_mul(&self.embed_scale)?;
    let heads = self.config.num_attention_heads;
    let mask_global = AttentionMask::padding(
      input.attention_mask,
      input.lengths,
      heads,
      self.config.head_dim,
      hidden.dtype(),
    )?;
    let mask_local = AttentionMask::sliding(
      input.attention_mask,
      input.lengths,
      self.config.attention_window(),
      heads,
      self.config.head_dim,
      hidden.dtype(),
    )?;
    let ctx = LayerContext {
      rope_local: &self.rope_local,
      rope_global: &self.rope_global,
      mask_local: &mask_local,
      mask_global: &mask_global,
    };
    for layer in &self.layers {
      hidden = layer.forward(&hidden, &ctx)?;
    }
    self.norm.forward(&hidden)
  }

  fn hidden_size(&self) -> usize {
    self.config.hidden_size
  }

  fn max_position_embeddings(&self) -> usize {
    self.config.max_position_embeddings
  }

  fn weights_dtype(&self) -> DType {
    self.embed_tokens.embeddings().dtype()
  }
}

#[cfg(test)]
mod tests;