use crate::error::Error;
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::layers::validation::{start_build, validate_weight_shape};
use crate::neural_network::layers::{built_layer_shape_functions, named_weight_layer_functions};
use crate::neural_network::traits::{LayerBase, ParamRef, UnaryLayer};
use crate::neural_network::{Ctx, Fans, Initializer, Shape, Tensor};
use crate::parallel_gates::{cheap_map_parallel_threshold, split_cap};
use ndarray::{Array2, IxDyn};
use rayon::prelude::*;
const INIT_LIMIT: f32 = 0.05;
const TASK_ELEMENTS: usize = 16_384;
tunable_gate! {
pub(crate) EMBEDDING_FORCED_TASK_ROWS
=> embedding_forced_task_rows / set_embedding_forced_task_rows = 0
}
#[derive(Debug)]
pub struct Embedding {
input_dim: usize,
output_dim: usize,
embeddings: Array2<f32>,
built: Option<Shape>,
random_state: Option<u64>,
}
impl Embedding {
pub fn new(input_dim: usize, output_dim: usize) -> Result<Self, Error> {
if input_dim == 0 {
return Err(Error::invalid_parameter(
"input_dim",
"is 0, and the table must hold at least 1 row",
));
}
if output_dim == 0 {
return Err(Error::invalid_parameter(
"output_dim",
"is 0, and an embedding vector must hold at least 1 element",
));
}
Ok(Self {
input_dim,
output_dim,
embeddings: Array2::zeros((0, 0)),
built: None,
random_state: None,
})
}
pub fn with_random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
if self.built.is_some() {
self.draw_parameters();
}
self
}
fn draw_parameters(&mut self) {
let mut rng = crate::random::make_rng(self.random_state);
self.embeddings = Initializer::Uniform { limit: INIT_LIMIT }.draw(
(self.input_dim, self.output_dim),
Fans::NONE,
&mut rng,
);
}
pub fn set_weights(&mut self, embeddings: Array2<f32>) -> Result<(), Error> {
if self.built.is_none() {
return Err(Error::not_built("Embedding"));
}
validate_weight_shape("embeddings", self.embeddings.shape(), embeddings.shape())?;
self.embeddings = embeddings.as_standard_layout().into_owned();
Ok(())
}
fn to_indices(&self, input: &Tensor) -> Result<Vec<usize>, Error> {
if input.ndim() == 0 {
return Err(Error::invalid_input(
"Embedding layer expects an input of rank 1 or more, got a scalar tensor",
));
}
if input.is_empty() {
return Err(Error::empty_input("input tensor"));
}
let mut indices = Vec::with_capacity(input.len());
for &value in input.iter() {
let index = value as usize;
if value.is_nan() || value <= -1.0 || index >= self.input_dim {
return Err(Error::invalid_input(format!(
"Embedding layer received the index {}, and every index must truncate into \
0..{}",
value, self.input_dim
)));
}
indices.push(index);
}
Ok(indices)
}
fn gather(&self, indices: &[usize], input_shape: &[usize]) -> Tensor {
let width = self.output_dim;
let elements = indices.len() * width;
let table = self
.embeddings
.as_slice()
.expect("the table is kept in C order");
let data = if elements >= cheap_map_parallel_threshold() {
let mut data = vec![0.0f32; elements];
let rows_per_task =
split_cap((TASK_ELEMENTS / width).max(1), embedding_forced_task_rows());
data.par_chunks_mut(rows_per_task * width)
.enumerate()
.for_each(|(task, block)| {
let first_row = task * rows_per_task;
for (row, destination) in block.chunks_mut(width).enumerate() {
let index = indices[first_row + row];
destination.copy_from_slice(&table[index * width..(index + 1) * width]);
}
});
data
} else {
let mut data = Vec::with_capacity(elements);
for &index in indices {
data.extend_from_slice(&table[index * width..(index + 1) * width]);
}
data
};
let mut shape = input_shape.to_vec();
shape.push(width);
Tensor::from_shape_vec(IxDyn(&shape), data).expect("the shape matches the data")
}
}
struct EmbeddingCache {
indices: Vec<usize>,
input_shape: Vec<usize>,
}
impl LayerBase for Embedding {
fn layer_type(&self) -> &str {
"Embedding"
}
fn param_count(&self) -> ParamCounts {
ParamCounts::trainable(self.embeddings.len())
}
fn parameters_mut(&mut self) -> Vec<ParamRef<'_>> {
vec![ParamRef::weight(
"embeddings",
self.embeddings
.as_slice_mut()
.expect("the table is kept in C order"),
)]
}
built_layer_shape_functions!();
named_weight_layer_functions!(
trainable "embeddings" => embeddings,
);
}
impl UnaryLayer for Embedding {
fn build(&mut self, input: &Shape) -> Result<(), Error> {
input.check_min_rank("Embedding", 1)?;
let canonical = Shape::new(vec![None; input.rank()]);
let Some(built) = start_build(&self.built, "Embedding", &canonical)? else {
return Ok(());
};
self.built = Some(built);
self.draw_parameters();
Ok(())
}
fn forward(&self, input: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
if !self.is_built() {
return Err(Error::not_built("Embedding"));
}
let indices = self.to_indices(input)?;
let output = self.gather(&indices, input.shape());
if ctx.is_training() {
ctx.push_cache(
"Embedding",
EmbeddingCache {
indices,
input_shape: input.shape().to_vec(),
},
);
}
Ok(output)
}
fn backward(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
let EmbeddingCache {
indices,
input_shape,
} = ctx.pop_cache::<EmbeddingCache>("Embedding")?;
let mut expected = input_shape.clone();
expected.push(self.output_dim);
if grad_output.shape() != expected.as_slice() {
return Err(Error::shape_mismatch(expected, grad_output.shape()));
}
let mut grad = Array2::zeros((self.input_dim, self.output_dim));
let table = grad
.as_slice_mut()
.expect("the gradient buffer is kept in C order");
let source = grad_output.as_standard_layout();
let source = source
.as_slice()
.expect("as_standard_layout gives a C-order array");
let width = self.output_dim;
for (row, &index) in source.chunks_exact(width).zip(&indices) {
let slot = &mut table[index * width..(index + 1) * width];
for (accumulator, &value) in slot.iter_mut().zip(row) {
*accumulator += value;
}
}
ctx.add_grad("embeddings", grad.into_dyn())?;
Ok(Tensor::zeros(IxDyn(&input_shape)))
}
fn compute_output_shape(&self, input: &Shape) -> Result<Shape, Error> {
input.check_min_rank("Embedding", 1)?;
let mut axes = input.axes().to_vec();
axes.push(Some(self.output_dim));
Ok(Shape::new(axes))
}
}