fastnn 0.3.3

A robust deep learning library in Rust with CUDA kernels
Documentation
//! Embedding table and positional encoding.

use crate::tensor::{Tensor, ops::index::index_from_f32};

use super::module::Module;
use super::param::Param;

const INIT_STD: f32 = 0.02;
/// Base of the geometric wavelength progression in the sinusoidal encoding.
const WAVELENGTH_BASE: f32 = 10000.0;

/// A learnable `[vocab, dim]` table looked up by token id.
pub struct Embedding {
    pub weight: Param,
    vocab: usize,
    dim: usize,
}

impl Embedding {
    pub fn new(vocab: usize, dim: usize) -> Embedding {
        // small init so embeddings don't dominate the residual stream early on
        Embedding {
            weight: Param::new(Tensor::randn(&[vocab, dim]).mul_scalar(INIT_STD)),
            vocab,
            dim,
        }
    }

    /// Look up a flat list of ids, giving `[ids.len(), dim]`.
    pub fn lookup(&self, ids: &[usize]) -> Tensor {
        self.weight.tensor().index_select(ids)
    }

    /// Look up a `[batch, sequence]` id grid, giving `[batch, sequence, dim]`.
    pub fn lookup_batch(&self, ids: &[Vec<usize>]) -> Tensor {
        let (batch, sequence) = (ids.len(), ids.first().map_or(0, |row| row.len()));
        let flat: Vec<usize> = ids
            .iter()
            .inspect(|row| assert_eq!(row.len(), sequence, "lookup_batch: ragged id grid"))
            .flatten()
            .copied()
            .collect();
        self.lookup(&flat)
            .reshape(&[batch as i64, sequence as i64, self.dim as i64])
    }

    pub fn vocab(&self) -> usize {
        self.vocab
    }

    pub fn dim(&self) -> usize {
        self.dim
    }
}

impl Module for Embedding {
    /// Treats finite, non-negative integer values as ids and panics on anything
    /// else. Prefer [`lookup`](Embedding::lookup).
    fn forward(&self, input: &Tensor) -> Tensor {
        let ids: Vec<usize> = input.to_vec().into_iter().map(index_from_f32).collect();
        let mut shape: Vec<i64> = input.shape().iter().map(|&d| d as i64).collect();
        shape.push(self.dim as i64);
        self.lookup(&ids).reshape(&shape)
    }

    fn named_parameters(&self) -> Vec<(String, Param)> {
        vec![("weight".into(), self.weight.clone())]
    }
}

/// Fixed sinusoidal position signal, added to token embeddings.
pub struct PositionalEncoding {
    table: Tensor,
    dim: usize,
}

impl PositionalEncoding {
    pub fn new(dim: usize, max_len: usize) -> PositionalEncoding {
        let mut table = vec![0.0f32; max_len * dim];
        for position in 0..max_len {
            for i in (0..dim).step_by(2) {
                let angle = position as f32 / WAVELENGTH_BASE.powf(i as f32 / dim as f32);
                table[position * dim + i] = angle.sin();
                if i + 1 < dim {
                    table[position * dim + i + 1] = angle.cos();
                }
            }
        }
        PositionalEncoding {
            table: Tensor::from_vec(table, &[max_len, dim]),
            dim,
        }
    }
}

impl Module for PositionalEncoding {
    /// Adds positions to a `[batch, sequence, dim]` input.
    fn forward(&self, input: &Tensor) -> Tensor {
        let sequence = input.dim(1);
        assert!(
            sequence <= self.table.dim(0),
            "PositionalEncoding: sequence {sequence} exceeds max_len {}",
            self.table.dim(0)
        );
        let positions: Vec<usize> = (0..sequence).collect();
        let slice = self
            .table
            .to(input.device())
            .index_select(&positions)
            .reshape(&[1, sequence as i64, self.dim as i64]);
        input.add(&slice)
    }
}