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::Module;
use candle_core::Tensor;
use candle_nn::Linear;
use candle_nn::VarBuilder;
use candle_nn::linear;

use super::config::BertConfig;
use crate::error::Result;
use crate::layers::attention::scaled_dot_product;
use crate::layers::attention::scaled_dot_product_with_weights;
use crate::layers::layer_norm::LayerNorm;
use crate::layers::mask::AttentionMask;

pub struct BertAttention {
  query: Linear,
  key: Linear,
  value: Linear,
  output: Linear,
  layer_norm: LayerNorm,
  num_heads: usize,
  head_dim: usize,
}

impl BertAttention {
  pub fn load(config: &BertConfig, vb: VarBuilder) -> Result<Self> {
    let hidden = config.hidden_size;
    Ok(Self {
      query: linear(hidden, hidden, vb.pp("self.query"))?,
      key: linear(hidden, hidden, vb.pp("self.key"))?,
      value: linear(hidden, hidden, vb.pp("self.value"))?,
      output: linear(hidden, hidden, vb.pp("output.dense"))?,
      layer_norm: LayerNorm::load(vb.pp("output.LayerNorm"), hidden, config.layer_norm_eps)?,
      num_heads: config.num_attention_heads,
      head_dim: config.head_dim(),
    })
  }

  /// Splits `(batch, seq, hidden)` into per-head q, k and v; shared by both forwards so they cannot diverge.
  fn project(&self, hidden: &Tensor) -> Result<(Tensor, Tensor, Tensor, usize, usize)> {
    let (batch, seq, _) = hidden.dims3()?;
    let split = |xs: Tensor| -> Result<Tensor> {
      Ok(
        xs.reshape((batch, seq, self.num_heads, self.head_dim))?
          .transpose(1, 2)?
          .contiguous()?,
      )
    };
    let q = split(self.query.forward(hidden)?)?;
    let k = split(self.key.forward(hidden)?)?;
    let v = split(self.value.forward(hidden)?)?;
    Ok((q, k, v, batch, seq))
  }

  /// Merges the heads, projects, and applies the residual and layer norm; shared by both forwards.
  fn finish(&self, context: Tensor, hidden: &Tensor, batch: usize, seq: usize) -> Result<Tensor> {
    let context = context
      .transpose(1, 2)?
      .reshape((batch, seq, self.num_heads * self.head_dim))?;
    let projected = self.output.forward(&context)?;
    Ok(self.layer_norm.forward(&(projected + hidden)?)?)
  }

  pub fn forward(&self, hidden: &Tensor, mask: &AttentionMask) -> Result<Tensor> {
    let (q, k, v, batch, seq) = self.project(hidden)?;
    let scale = 1.0 / (self.head_dim as f64).sqrt();
    let context = scaled_dot_product(&q, &k, &v, mask, scale)?;
    self.finish(context, hidden, batch, seq)
  }

  /// `forward` plus the softmax weights, `(batch, heads, seq, seq)`; always takes the matmul path.
  pub fn forward_with_weights(
    &self,
    hidden: &Tensor,
    mask: &AttentionMask,
  ) -> Result<(Tensor, Tensor)> {
    let (q, k, v, batch, seq) = self.project(hidden)?;
    let scale = 1.0 / (self.head_dim as f64).sqrt();
    let (context, weights) = scaled_dot_product_with_weights(&q, &k, &v, mask, scale)?;
    Ok((self.finish(context, hidden, batch, seq)?, weights))
  }
}