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
//! The encoder: embeddings, pre-norm blocks alternating two rotary tables and masks, a final norm, no pooler.

use candle_core::DType;
use candle_core::Tensor;
use candle_nn::VarBuilder;

use super::config::ModernBertConfig;
use super::embeddings::ModernBertEmbeddings;
use super::layer::ModernBertLayer;
use crate::error::Result;
use crate::layers::layer_norm::LayerNorm;
use crate::layers::mask::AttentionMask;
use crate::layers::rope::RotaryEmbedding;
use crate::models::Encoder;
use crate::models::EncoderInput;

pub struct ModernBertModel {
  embeddings: ModernBertEmbeddings,
  layers: Vec<ModernBertLayer>,
  global_rope: RotaryEmbedding,
  local_rope: RotaryEmbedding,
  final_norm: LayerNorm,
  config: ModernBertConfig,
}

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

impl ModernBertModel {
  pub fn load(config: ModernBertConfig, vb: VarBuilder) -> Result<Self> {
    config.ensure_supported()?;
    let embeddings = ModernBertEmbeddings::load(&config, vb.clone())?;
    let layers = (0..config.num_hidden_layers)
      .map(|i| ModernBertLayer::load(&config, vb.pp(format!("layers.{i}")), i))
      .collect::<Result<Vec<_>>>()?;
    let global_rope = RotaryEmbedding::new(
      config.head_dim(),
      config.max_position_embeddings,
      config.global_rope_theta,
      vb.dtype(),
      vb.device(),
    )?;
    let local_rope = RotaryEmbedding::new(
      config.head_dim(),
      config.max_position_embeddings,
      config.local_rope_theta,
      vb.dtype(),
      vb.device(),
    )?;
    let final_norm =
      LayerNorm::load_without_bias(vb.pp("final_norm"), config.hidden_size, config.norm_eps)?;
    Ok(Self {
      embeddings,
      layers,
      global_rope,
      local_rope,
      final_norm,
      config,
    })
  }

  pub(super) fn rope_for(&self, layer: usize) -> &RotaryEmbedding {
    if self.config.is_global(layer) {
      &self.global_rope
    } else {
      &self.local_rope
    }
  }
}

impl Encoder for ModernBertModel {
  fn forward(&self, input: &EncoderInput) -> Result<Tensor> {
    let mut hidden = self.embeddings.forward(input.input_ids)?;
    let dtype = hidden.dtype();
    let heads = self.config.num_attention_heads;
    let head_dim = self.config.head_dim();
    let global =
      AttentionMask::padding(input.attention_mask, input.lengths, heads, head_dim, dtype)?;
    let sliding = AttentionMask::sliding(
      input.attention_mask,
      input.lengths,
      self.config.attention_window(),
      heads,
      head_dim,
      dtype,
    )?;
    for (i, layer) in self.layers.iter().enumerate() {
      let mask = if self.config.is_global(i) {
        &global
      } else {
        &sliding
      };
      hidden = layer.forward(&hidden, mask, self.rope_for(i))?;
    }
    Ok(self.final_norm.forward(&hidden)?)
  }

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

  /// The rotary tables' size, so the builder's length cap keeps requests from failing in `narrow`.
  fn max_position_embeddings(&self) -> usize {
    self.config.max_position_embeddings
  }

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

#[cfg(test)]
mod tests;