use std::collections::HashMap;
use rten_base::num::AsUsize;
use rten_shape_inference::einsum_parser::{EinsumExpr, ValidateError, expand_ellipsis};
use rten_shape_inference::ops as shape_ops;
use rten_tensor::layout::{MutLayout, OverlapPolicy};
use rten_tensor::prelude::*;
use rten_tensor::{Contiguous, CowTensor, DynLayout, Tensor, TensorView};
use smallvec::SmallVec;
use crate::buffer_pool::{AutoReturn, BufferPool, PoolRef};
use crate::infer_shapes::{InferShapes, impl_infer_shapes};
use crate::operator::{
IntoOpResult, OpError, OpRunContext, Operator, OutputList, OutputType, OutputTypeList,
OutputTypesContext,
};
use crate::ops::layout::expand_to;
use crate::ops::{matmul, mul, reduce_sum};
#[derive(Debug)]
pub struct Einsum {
pub equation: String,
}
impl Operator for Einsum {
fn name(&self) -> &str {
"Einsum"
}
fn max_inputs(&self) -> Option<usize> {
None
}
fn run(&self, ctx: &OpRunContext) -> Result<OutputList, OpError> {
let inputs = ctx.inputs();
let mut typed_inputs: SmallVec<[TensorView; 2]> = SmallVec::with_capacity(inputs.len());
for i in 0..inputs.len() {
typed_inputs.push(inputs.require_as(i)?);
}
einsum(ctx.pool(), &typed_inputs, &self.equation).into_op_result()
}
fn output_types(&self, _ctx: &OutputTypesContext) -> Option<OutputTypeList> {
Some([OutputType::CopyFromInput(0)].into())
}
fn as_infer_shapes(&self) -> Option<&dyn InferShapes> {
Some(self)
}
}
impl_infer_shapes!(
Einsum,
op,
shape_ops::Einsum {
equation: &op.equation,
}
);
pub fn einsum(
pool: &BufferPool,
inputs: &[TensorView],
equation_str: &str,
) -> Result<Tensor, OpError> {
let equation =
EinsumExpr::parse(equation_str).map_err(|err| OpError::InvalidValue(err.as_str()))?;
let broadcast_ndim = equation
.validate_inputs(inputs.iter().map(|view| Some(view.ndim())))
.map_err(|err| match err {
ValidateError::IncorrectInputCount => OpError::InvalidValue(
"Number of terms in Einsum equation does not match input tensor count",
),
ValidateError::TooManyDims => {
OpError::UnsupportedValue("Einsum input or term has too many dimensions")
}
ValidateError::BroadcastMismatch => {
OpError::InvalidValue("Number of broadcast dims does not match across inputs")
}
ValidateError::RankMismatch => {
OpError::InvalidValue("Einsum term dimension count does not match input tensor")
}
ValidateError::UnknownRank => unreachable!(),
})? as u8;
let path = einsum_path(&equation, broadcast_ndim);
let mut output: Option<PoolRef<Tensor>> = None;
for step in &path {
let output_view = output.as_ref().map(|o| o.view());
let x = match step.lhs.input {
EinsumInput::Index(idx) => &inputs[idx.as_usize()],
EinsumInput::PrevOutput => output_view.as_ref().expect("invalid einsum path"),
};
let y = step.rhs.as_ref().map(|rhs| match rhs.input {
EinsumInput::Index(idx) => &inputs[idx.as_usize()],
EinsumInput::PrevOutput => output_view.as_ref().expect("invalid einsum path"),
});
let new_output = einsum_step(pool, step, x, y)?.auto_return(pool);
output = Some(new_output);
}
Ok(output.expect("empty path").take())
}
fn take_diagonals<'a>(term: &str, x: &TensorView<'a>) -> Result<(String, TensorView<'a>), OpError> {
assert!(term.chars().count() == x.ndim());
let mut out_shape: Vec<usize> = Vec::new();
let mut out_strides: Vec<usize> = Vec::new();
let mut unique_dims = String::with_capacity(term.len());
for (i, label) in (0..x.ndim()).zip(term.chars()) {
if unique_dims.contains(label) {
continue;
}
unique_dims.push(label);
let dim_size = x.size(i);
out_shape.push(dim_size);
let mut diagonal_stride = 0;
for (k, other_label) in (0..x.ndim()).zip(term.chars()) {
if label != other_label {
continue;
}
if x.size(k) != dim_size {
return Err(OpError::InvalidValue(
"Dimension sizes for repeated labels in term do not match",
));
}
diagonal_stride += x.stride(k);
}
out_strides.push(diagonal_stride);
}
let out_layout =
DynLayout::from_shape_and_strides(&out_shape, &out_strides, OverlapPolicy::AllowOverlap)
.expect("failed to create diagonal layout");
let out_view = TensorView::from_storage_and_layout(x.storage(), out_layout);
Ok((unique_dims, out_view))
}
fn sum_lone_dims<'a>(
pool: &BufferPool,
view: TensorView<'a>,
term: String,
other_term: &str,
output: &str,
) -> Result<(String, CowTensor<'a, f32>), OpError> {
let mut lone_axes = Vec::new();
let mut new_term = String::with_capacity(term.len());
for (i, c) in term.chars().enumerate() {
if other_term.contains(c) || output.contains(c) {
new_term.push(c);
} else {
lone_axes.push(i as i32);
}
}
if lone_axes.is_empty() {
Ok((new_term, view.as_cow()))
} else {
let summed = reduce_sum(pool, view, Some(&lone_axes), false )?;
Ok((new_term, summed.into_cow()))
}
}
fn broadcast_size(a: usize, b: usize) -> Result<usize, OpError> {
match (a, b) {
(a, b) if a == b => Ok(a),
(1, size) | (size, 1) => Ok(size),
_ => Err(OpError::IncompatibleInputShapes(
"Einsum label has different sizes in different terms",
)),
}
}
fn expand_dim<'a>(
pool: &BufferPool,
view: TensorView<'a>,
size: usize,
from_end: usize,
) -> CowTensor<'a, f32> {
let dim = view.ndim() - from_end;
if view.size(dim) == size {
return view.as_cow();
}
let mut shape = view.shape().to_vec();
shape[dim] = size;
expand_to(pool, view, &shape).into_cow()
}
fn reduced_dims(lhs_term: &str, rhs_term: &str, output: &str) -> Vec<char> {
let mut dims = Vec::new();
for c in lhs_term.chars().chain(rhs_term.chars()) {
if !output.contains(c) && !dims.contains(&c) {
dims.push(c);
}
}
dims
}
fn einsum_step(
pool: &BufferPool,
step: &EinsumStep,
x: &TensorView,
y: Option<&TensorView>,
) -> Result<Tensor, OpError> {
let (lhs_term, x) = take_diagonals(&step.lhs.term, x)?;
let (Some(y), Some(rhs)) = (y, &step.rhs) else {
let reduced_dims = reduced_dims(&lhs_term, "", &step.output);
let common_order: String = step
.output
.chars()
.chain(reduced_dims.iter().copied())
.collect();
let xp = permute_and_insert_axes(&x, &lhs_term, &common_order);
if reduced_dims.is_empty() {
return Ok(xp.to_tensor_in(pool));
}
let reduced_dim_indices: Vec<i32> = (xp.ndim() - reduced_dims.len()..xp.ndim())
.map(|i| i as i32)
.collect();
return reduce_sum(
pool,
xp,
Some(reduced_dim_indices.as_slice()),
false,
);
};
let (rhs_term, y) = take_diagonals(&rhs.term, y)?;
let (lhs_term, x) = sum_lone_dims(pool, x, lhs_term, &rhs_term, &step.output)?;
let (rhs_term, y) = sum_lone_dims(pool, y, rhs_term, &lhs_term, &step.output)?;
let reduced_dims = reduced_dims(&lhs_term, &rhs_term, &step.output);
if let [matmul_k] = reduced_dims[..] {
einsum_matmul(
pool,
&x.view(),
&y.view(),
&lhs_term,
&rhs_term,
&step.output,
matmul_k,
)
} else {
let common_order: String = step
.output
.chars()
.chain(reduced_dims.iter().copied())
.collect();
let xp = permute_and_insert_axes(&x.view(), &lhs_term, &common_order);
let yp = permute_and_insert_axes(&y.view(), &rhs_term, &common_order);
if reduced_dims.is_empty() {
let output = mul(pool, xp, yp)?;
return Ok(output);
}
let mut tmp_x_shape = xp.shape().to_vec();
let mut tmp_y_shape = yp.shape().to_vec();
for i in xp.ndim() - reduced_dims.len()..xp.ndim() {
let size = broadcast_size(tmp_x_shape[i], tmp_y_shape[i])?;
tmp_x_shape[i] = size;
tmp_y_shape[i] = size;
}
let x = if tmp_x_shape == xp.shape() {
xp.to_contiguous_in(pool)
} else {
Contiguous::new(expand_to(pool, xp.view(), &tmp_x_shape).into_cow()).unwrap()
};
let y = if tmp_y_shape == yp.shape() {
yp.to_contiguous_in(pool)
} else {
Contiguous::new(expand_to(pool, yp.view(), &tmp_y_shape).into_cow()).unwrap()
};
let reduced_dims_start_index = xp.ndim() - reduced_dims.len();
let reduced_size: usize = tmp_x_shape[reduced_dims_start_index..].iter().product();
tmp_x_shape.truncate(reduced_dims_start_index);
tmp_x_shape.push(reduced_size);
let x = x.reshaped(tmp_x_shape.as_slice());
tmp_y_shape.truncate(reduced_dims_start_index);
tmp_y_shape.push(reduced_size);
let y = y.reshaped(tmp_y_shape.as_slice());
let reduced_dim = MERGED_K;
let term_simplified: String = step
.output
.chars()
.chain(std::iter::once(reduced_dim))
.collect();
einsum_matmul(
pool,
&x.view(),
&y.view(),
&term_simplified,
&term_simplified,
&step.output,
reduced_dim,
)
}
}
const INSERTED_M: char = '<';
const INSERTED_N: char = '>';
const MERGED_K: char = '*';
fn is_inserted_dim(c: char) -> bool {
matches!(c, INSERTED_M | INSERTED_N)
}
fn is_valid_permute_insert_spec(src: &str, dest: &str) -> bool {
if src.len() > dest.len() {
return false;
}
for src_ch in src.chars() {
let src_count = src.chars().filter(|c| *c == src_ch).count();
let dest_count = dest.chars().filter(|c| *c == src_ch).count();
if src_count != 1 || dest_count != 1 {
return false;
}
}
true
}
fn permute_and_insert_axes<'a, T>(
tensor: &TensorView<'a, T>,
in_order: &str,
out_order: &str,
) -> TensorView<'a, T> {
assert!(
is_valid_permute_insert_spec(in_order, out_order),
"invalid permute-and-insert spec {}->{}",
in_order,
out_order
);
assert!(
tensor.ndim() == in_order.len(),
"input order does not match tensor ndim"
);
let perm: Vec<usize> = out_order
.chars()
.filter_map(|c| in_order.chars().position(|ic| ic == c))
.collect();
let mut permuted = tensor.permuted(&perm);
for (i, c) in out_order.chars().enumerate() {
if !in_order.contains(c) {
permuted.insert_axis(i);
}
}
permuted
}
fn einsum_matmul(
pool: &BufferPool,
x: &TensorView,
y: &TensorView,
term1: &str,
term2: &str,
output: &str,
reduced_dim: char,
) -> Result<Tensor, OpError> {
let matmul_k = reduced_dim;
let matmul_n = term2
.chars()
.rev()
.find(|c| !term1.contains(*c))
.unwrap_or(INSERTED_N);
let matmul_m = term1
.chars()
.rev()
.find(|c| !term2.contains(*c))
.unwrap_or(INSERTED_M);
let mut batch_dims = String::new();
for c in term1.chars().chain(term2.chars()) {
if c != matmul_k && c != matmul_m && c != matmul_n && !batch_dims.contains(c) {
batch_dims.push(c);
}
}
let mut x_order: String = batch_dims.clone();
x_order.push(matmul_m);
x_order.push(matmul_k);
let mut y_order: String = batch_dims.clone();
y_order.push(matmul_k);
y_order.push(matmul_n);
let mut out_order: String = batch_dims;
if !is_inserted_dim(matmul_m) {
out_order.push(matmul_m);
}
if !is_inserted_dim(matmul_n) {
out_order.push(matmul_n);
}
let xp = permute_and_insert_axes(x, term1, &x_order);
let yp = permute_and_insert_axes(y, term2, &y_order);
let k_size = broadcast_size(xp.size(xp.ndim() - 1), yp.size(yp.ndim() - 2))?;
let xp = expand_dim(pool, xp, k_size, 1);
let yp = expand_dim(pool, yp, k_size, 2);
let mut out = matmul(pool, xp.view(), yp.view(), None)?;
if is_inserted_dim(matmul_m) {
out.remove_axis(out.ndim() - 2);
}
if is_inserted_dim(matmul_n) {
out.remove_axis(out.ndim() - 1);
}
if out_order == output {
Ok(out)
} else {
let out_permuted = permute_and_insert_axes(&out.view(), &out_order, output);
Ok(out_permuted.to_tensor_in(pool))
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum EinsumInput {
Index(u32),
PrevOutput,
}
#[derive(Clone, Debug, PartialEq)]
struct EinsumTerm {
term: String,
input: EinsumInput,
}
#[derive(Clone, Debug, PartialEq)]
struct EinsumStep {
lhs: EinsumTerm,
rhs: Option<EinsumTerm>,
output: String,
}
fn unique_dims(term: &str) -> impl Iterator<Item = char> + '_ {
term.chars()
.enumerate()
.filter_map(|(i, dim)| (!term.chars().take(i).any(|c| c == dim)).then_some(dim))
}
fn subtract_term_dims(reduced_dims: &mut HashMap<char, usize>, term: &str) {
for dim in unique_dims(term) {
if let Some(count) = reduced_dims.get_mut(&dim) {
*count -= 1;
}
}
}
fn step_output(
term_a: &str,
term_b: &str,
final_output: &str,
reduced_dims: &HashMap<char, usize>,
) -> String {
let mut output = String::new();
for dim in term_a.chars().chain(term_b.chars()) {
if !output.contains(dim)
&& (final_output.contains(dim) || reduced_dims.get(&dim).copied().unwrap_or(0) > 0)
{
output.push(dim);
}
}
output
}
fn einsum_path(expr: &EinsumExpr, broadcast_ndim: u8) -> Vec<EinsumStep> {
let output = expand_ellipsis(&expr.output, broadcast_ndim as usize);
let in_terms: Vec<String> = expr
.inputs
.iter()
.map(|term| expand_ellipsis(term, broadcast_ndim as usize))
.collect();
let input_term = |term: &str, index: u32| EinsumTerm {
term: term.to_string(),
input: EinsumInput::Index(index),
};
match &in_terms[..] {
[] => Vec::new(),
[term] => {
let step = EinsumStep {
lhs: input_term(term, 0),
rhs: None,
output,
};
[step].into()
}
[term_a, term_b] => {
let step = EinsumStep {
lhs: input_term(term_a, 0),
rhs: Some(input_term(term_b, 1)),
output,
};
[step].into()
}
all_terms @ [term_a, term_b, rest @ ..] => {
let mut steps = Vec::with_capacity(all_terms.len() - 1);
let mut reduced_dims: HashMap<char, usize> = HashMap::new();
for term in all_terms {
for dim in unique_dims(term) {
if !output.contains(dim) {
*reduced_dims.entry(dim).or_insert(0) += 1;
}
}
}
subtract_term_dims(&mut reduced_dims, term_a);
subtract_term_dims(&mut reduced_dims, term_b);
let mut next_output = step_output(term_a, term_b, &output, &reduced_dims);
steps.push(EinsumStep {
lhs: input_term(term_a, 0),
rhs: Some(input_term(term_b, 1)),
output: next_output.clone(),
});
for (term_idx, term) in rest.iter().enumerate() {
subtract_term_dims(&mut reduced_dims, term);
let prev_output = next_output;
if term_idx == rest.len() - 1 {
next_output = output.clone();
} else {
next_output = step_output(&prev_output, term, &output, &reduced_dims);
}
steps.push(EinsumStep {
lhs: EinsumTerm {
term: prev_output,
input: EinsumInput::PrevOutput,
},
rhs: Some(input_term(term, term_idx as u32 + 2)),
output: next_output.clone(),
});
}
steps
}
}
}
#[cfg(test)]
mod tests {
use rten_tensor::prelude::*;
use rten_tensor::{Tensor, TensorView};
use rten_testing::TestCases;
use super::{EinsumExpr, EinsumInput, EinsumStep, EinsumTerm, einsum_path};
use crate::buffer_pool::BufferPool;
use crate::operator::OpError;
use crate::ops::{einsum, matmul, mul, reduce_sum};
#[test]
fn test_einsum() {
#[derive(Debug)]
struct Case<'a> {
equation: &'a str,
inputs: Vec<TensorView<'a>>,
expected: Result<Tensor, OpError>,
}
let pool = BufferPool::new();
let scalar = Tensor::from(2.5);
let vec_a = Tensor::arange(1., 10., None);
let vec_b = Tensor::arange(1., 5., None);
let mat_a = Tensor::from([[1., 2., 3.], [4., 5., 6.]]);
let mat_b = Tensor::from([[1., 2., 3., 4.], [5., 6., 7., 8.], [9., 10., 11., 12.]]);
let matmul_ab = matmul(&pool, mat_a.view(), mat_b.view(), None).unwrap();
let matmul_ba = matmul_ab.transposed().to_tensor();
let outer_mat_ab = mul(
&pool,
mat_a
.reshaped([mat_a.size(0), mat_a.size(1), 1, 1])
.as_dyn(),
mat_b
.reshaped([1, 1, mat_b.size(0), mat_b.size(1)])
.as_dyn(),
)
.unwrap();
let square_mat = Tensor::from([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]]);
let cube = Tensor::arange(1., 28., None).into_shape([3, 3, 3].as_slice());
let bhwc = mat_a
.clone()
.into_shape([1, 1, mat_a.size(0), mat_a.size(1)]);
let hck = mat_b.clone().into_shape([1, mat_b.size(0), mat_b.size(1)]);
let bhwk = matmul_ab
.clone()
.into_shape([1, 1, mat_a.size(0), mat_b.size(1)]);
let ijk = Tensor::zeros(&[10, 5, 8]);
let row_1x3 = Tensor::from([[1., 2., 3.]]);
let mat_4x3 = Tensor::from([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.], [10., 11., 12.]]);
let empty_0x3 = Tensor::zeros(&[0, 3]);
let hmk = matmul_ab
.clone()
.into_shape([1, mat_a.size(0), mat_b.size(1)]);
let mat_c = matmul_ab.clone();
let sum_ij_ik = mul(
&pool,
reduce_sum(&pool, mat_a.view(), Some(&[1]), true )
.unwrap()
.view(),
mat_c.view(),
)
.unwrap();
let abf = Tensor::arange(1., (2 * 3 * 4 + 1) as f32, None).into_shape([2, 3, 4].as_slice());
let fcd = Tensor::arange(1., (4 * 5 * 6 + 1) as f32, None).into_shape([4, 5, 6].as_slice());
let abcd = matmul(
&pool,
abf.reshaped([2 * 3, 4].as_slice()).view(),
fcd.reshaped([4, 5 * 6].as_slice()).view(),
None,
)
.unwrap()
.into_shape([2, 3, 5, 6].as_slice());
let abf_sq_sum_ij = reduce_sum(
&pool,
mul(&pool, abf.view(), abf.view()).unwrap().view(),
Some(&[0, 1]),
false,
)
.unwrap();
let abf_summed_b =
reduce_sum(&pool, abf.view(), Some(&[1]), false ).unwrap();
let af_prod = mul(&pool, mat_c.view(), abf_summed_b.view()).unwrap();
let sum_af_abf = reduce_sum(
&pool,
af_prod.view(),
Some(&[1]),
false,
)
.unwrap();
let cases = [
Case {
equation: "ij->ij",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.clone()),
},
Case {
equation: "i j -> i j",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.clone()),
},
Case {
equation: "ij->ji",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.transposed().to_tensor()),
},
Case {
equation: " ij -> ji ",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.transposed().to_tensor()),
},
Case {
equation: "ba",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.transposed().to_tensor()),
},
Case {
equation: "ij->i",
inputs: vec![mat_a.view()],
expected: Ok(reduce_sum(
&pool,
mat_a.view(),
Some(&[-1]),
false,
)
.unwrap()),
},
Case {
equation: "abf->a",
inputs: vec![abf.view()],
expected: reduce_sum(&pool, abf.view(), Some(&[1, 2]), false ),
},
Case {
equation: "abf->f",
inputs: vec![abf.view()],
expected: reduce_sum(&pool, abf.view(), Some(&[0, 1]), false ),
},
Case {
equation: "i,j->ij",
inputs: vec![vec_a.view(), vec_b.view()],
expected: Ok(mul(
&pool,
vec_a.reshaped([vec_a.len(), 1]).as_dyn(),
vec_b.reshaped([1, vec_b.len()]).as_dyn(),
)
.unwrap()),
},
Case {
equation: "ij,kl->ijkl",
inputs: vec![mat_a.view(), mat_b.view()],
expected: Ok(outer_mat_ab),
},
Case {
equation: "a,b->ba",
inputs: vec![vec_a.view(), vec_b.view()],
expected: Ok(mul(
&pool,
vec_b.reshaped([vec_b.len(), 1]).as_dyn(),
vec_a.reshaped([1, vec_a.len()]).as_dyn(),
)
.unwrap()),
},
Case {
equation: "ij,jk->ik",
inputs: vec![mat_a.view(), mat_b.view()],
expected: Ok(matmul_ab.clone()),
},
Case {
equation: "ij,jk",
inputs: vec![mat_a.view(), mat_b.view()],
expected: Ok(matmul_ab.clone()),
},
Case {
equation: "ji,kj->ik",
inputs: vec![mat_a.transposed(), mat_b.transposed()],
expected: Ok(matmul_ab),
},
Case {
equation: "ij,jk->ki",
inputs: vec![mat_a.view(), mat_b.view()],
expected: Ok(matmul_ba),
},
Case {
equation: "bhwc,hkc->bhwk",
inputs: vec![bhwc.as_dyn(), hck.permuted([0, 2, 1]).as_dyn()],
expected: Ok(bhwk.into_dyn()),
},
Case {
equation: "mc,hck->hmk",
inputs: vec![mat_a.view(), hck.as_dyn()],
expected: Ok(hmk.clone().into_dyn()),
},
Case {
equation: "mc,hck->khm",
inputs: vec![mat_a.view(), hck.as_dyn()],
expected: Ok(hmk.permuted([2, 0, 1]).to_tensor().into_dyn()),
},
Case {
equation: "c,hck->hk",
inputs: vec![mat_a.slice(0), hck.as_dyn()],
expected: Ok(matmul(&pool, mat_a.slice((..1, ..)), mat_b.view(), None).unwrap()),
},
Case {
equation: "abf,fcd->abcd",
inputs: vec![abf.view(), fcd.view()],
expected: Ok(abcd),
},
Case {
equation: "ij,ik->ik",
inputs: vec![mat_a.view(), mat_c.view()],
expected: Ok(sum_ij_ik.clone()),
},
Case {
equation: "ik,ij->ik",
inputs: vec![mat_c.view(), mat_a.view()],
expected: Ok(sum_ij_ik),
},
Case {
equation: "af,abf->a",
inputs: vec![mat_c.view(), abf.view()],
expected: Ok(sum_af_abf),
},
Case {
equation: "ij,jk->ik",
inputs: vec![mat_a.view()],
expected: Err(OpError::InvalidValue(
"Number of terms in Einsum equation does not match input tensor count",
)),
},
Case {
equation: "i,i->",
inputs: vec![vec_a.view(), vec_a.view()],
expected: Ok(Tensor::from(vec_a.iter().map(|a| a * a).sum::<f32>())),
},
Case {
equation: "ij,j->i",
inputs: vec![mat_a.view(), mat_b.slice((.., 0))],
expected: Ok(matmul(&pool, mat_a.view(), mat_b.slice((.., ..1)), None)
.unwrap()
.into_shape([mat_a.size(0)].as_slice())),
},
Case {
equation: "j,jk->k",
inputs: vec![mat_a.slice(0), mat_b.view()],
expected: Ok(matmul(&pool, mat_a.slice((..1, ..)), mat_b.view(), None)
.unwrap()
.into_shape([mat_b.size(1)].as_slice())),
},
Case {
equation: "ij,ij->",
inputs: vec![mat_a.view(), mat_a.view()],
expected: Ok(Tensor::from(mat_a.iter().map(|x| x * x).sum::<f32>())),
},
Case {
equation: "bhwc,bhwc->",
inputs: vec![bhwc.as_dyn(), bhwc.as_dyn()],
expected: Ok(Tensor::from(bhwc.iter().map(|x| x * x).sum::<f32>())),
},
Case {
equation: "ij,ji->",
inputs: vec![mat_a.view(), mat_a.transposed()],
expected: Ok(Tensor::from(mat_a.iter().map(|x| x * x).sum::<f32>())),
},
Case {
equation: "ij,ij->",
inputs: vec![mat_a.slice((..1, ..)), mat_a.view()],
expected: Ok(Tensor::from(
mul(&pool, mat_a.slice((..1, ..)), mat_a.view())
.unwrap()
.iter()
.sum::<f32>(),
)),
},
Case {
equation: "ij,ij->j",
inputs: vec![row_1x3.view(), mat_4x3.view()],
expected: Ok(Tensor::from([22., 52., 90.])),
},
Case {
equation: "ij,ij->j",
inputs: vec![mat_4x3.view(), row_1x3.view()],
expected: Ok(Tensor::from([22., 52., 90.])),
},
Case {
equation: "ij,ij->",
inputs: vec![row_1x3.view(), empty_0x3.view()],
expected: Ok(Tensor::from(0.)),
},
Case {
equation: "ij,j->",
inputs: vec![mat_a.view(), mat_b.slice((.., 0))],
expected: Ok(Tensor::from(
mat_a
.iter()
.zip(mat_b.slice((.., 0)).broadcast(mat_a.shape()).iter())
.map(|(x, y)| x * y)
.sum::<f32>(),
)),
},
Case {
equation: "",
inputs: vec![],
expected: Err(OpError::InvalidValue(
"Number of terms in Einsum equation does not match input tensor count",
)),
},
Case {
equation: "",
inputs: vec![scalar.view()],
expected: Ok(scalar.clone()),
},
Case {
equation: "->",
inputs: vec![scalar.view()],
expected: Ok(scalar.clone()),
},
Case {
equation: "C,MCN->MN",
inputs: vec![mat_a.slice(0), hck.as_dyn()],
expected: Ok(matmul(&pool, mat_a.slice((..1, ..)), mat_b.view(), None).unwrap()),
},
Case {
equation: "IJK,IJK->K",
inputs: vec![abf.view(), abf.view()],
expected: Ok(abf_sq_sum_ij.clone()),
},
Case {
equation: "iI->Ii",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.transposed().to_tensor()),
},
Case {
equation: "II->I",
inputs: vec![square_mat.view()],
expected: Ok(Tensor::from([1., 5., 9.])),
},
Case {
equation: "aBc",
inputs: vec![abf.view()],
expected: Ok(abf.permuted(&[1, 0, 2]).to_tensor()),
},
Case {
equation: "I...J->J...I",
inputs: vec![ijk.view()],
expected: Ok(ijk.transposed().to_tensor()),
},
Case {
equation: "i1j", inputs: vec![mat_a.view()],
expected: Err(OpError::InvalidValue("Input term is invalid")),
},
Case {
equation: "i.j", inputs: vec![mat_a.view()],
expected: Err(OpError::InvalidValue("Input term is invalid")),
},
Case {
equation: "i...j...", inputs: vec![mat_a.view()],
expected: Err(OpError::InvalidValue("Input term is invalid")),
},
Case {
equation: "ii->i",
inputs: vec![square_mat.view()],
expected: Ok(Tensor::from([1., 5., 9.])),
},
Case {
equation: "iii->i",
inputs: vec![cube.view()],
expected: Ok(Tensor::from([1., 14., 27.])),
},
Case {
equation: "ii->",
inputs: vec![square_mat.view()],
expected: Ok(Tensor::from([1., 5., 9.].iter().sum::<f32>())),
},
Case {
equation: "ii->i",
inputs: vec![mat_a.view()],
expected: Err(OpError::InvalidValue(
"Dimension sizes for repeated labels in term do not match",
)),
},
Case {
equation: "ij,jk->i.k",
inputs: vec![mat_a.view(), mat_b.view()],
expected: Err(OpError::InvalidValue("Output term is invalid")),
},
Case {
equation: "ij,jk->IK",
inputs: vec![mat_a.view(), mat_b.view()],
expected: Err(OpError::InvalidValue(
"Einsum output term contains a label not present in any input term",
)),
},
Case {
equation: "ij->ii",
inputs: vec![mat_a.view()],
expected: Err(OpError::InvalidValue(
"Einsum output term contains repeated labels",
)),
},
Case {
equation: "ij",
inputs: vec![vec_a.view()],
expected: Err(OpError::InvalidValue(
"Einsum term dimension count does not match input tensor",
)),
},
Case {
equation: "i...j",
inputs: vec![vec_a.view()],
expected: Err(OpError::InvalidValue(
"Einsum term dimension count does not match input tensor",
)),
},
Case {
equation: "abcdefghijkl...",
inputs: vec![TensorView::from_data([0; 12].as_slice(), &[])],
expected: Err(OpError::UnsupportedValue(
"Einsum input or term has too many dimensions",
)),
},
Case {
equation: "...",
inputs: vec![TensorView::from_data([0; 11].as_slice(), &[])],
expected: Err(OpError::UnsupportedValue(
"Einsum input or term has too many dimensions",
)),
},
Case {
equation: "i,i,i->",
inputs: vec![vec_a.view(), vec_a.view(), vec_a.view()],
expected: Ok(Tensor::from(vec_a.map(|x| x * x * x).iter().sum::<f32>())),
},
Case {
equation: "...",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.clone()),
},
Case {
equation: "i...j->i...j",
inputs: vec![mat_a.view()],
expected: Ok(mat_a.clone()),
},
Case {
equation: "i...j->j...i",
inputs: vec![ijk.view()],
expected: Ok(ijk.transposed().to_tensor()),
},
Case {
equation: "i...j",
inputs: vec![ijk.view()],
expected: Ok(ijk.permuted(&[1, 0, 2]).to_tensor()),
},
Case {
equation: "...i->...",
inputs: vec![mat_a.view()],
expected: reduce_sum(&pool, mat_a.view(), Some(&[-1]), false ),
},
Case {
equation: "f,fc...->c...",
inputs: vec![mat_b.slice(0), fcd.view()],
expected: Ok(matmul(
&pool,
mat_b.slice((..1, ..)),
fcd.reshaped([4, 30].as_slice()).view(),
None,
)
.unwrap()
.into_shape([5, 6].as_slice())),
},
Case {
equation: "af,f...->a...",
inputs: vec![mat_c.view(), fcd.view()],
expected: Ok(matmul(
&pool,
mat_c.view(),
fcd.reshaped([4, 30].as_slice()).view(),
None,
)
.unwrap()
.into_shape([2, 5, 6].as_slice())),
},
Case {
equation: "...,...->...",
inputs: vec![vec_a.view(), mat_a.view()],
expected: Err(OpError::InvalidValue(
"Number of broadcast dims does not match across inputs",
)),
},
];
cases.test_each(|case| {
let Case {
equation,
inputs,
expected,
} = case;
let pool = BufferPool::new();
let output = einsum(&pool, inputs.as_slice(), equation);
assert_eq!(
&output, expected,
"result mismatch for equation {}",
equation
);
});
}
#[test]
fn test_einsum_path() {
#[derive(Debug)]
struct Case<'a> {
equation: &'a str,
broadcast_ndim: u8,
path: Vec<EinsumStep>,
}
let new_term = |term: &str, index: Option<u32>| EinsumTerm {
term: term.to_string(),
input: index
.map(EinsumInput::Index)
.unwrap_or(EinsumInput::PrevOutput),
};
let cases = [
Case {
equation: "i->i",
broadcast_ndim: 0,
path: [EinsumStep {
lhs: new_term("i", Some(0)),
rhs: None,
output: "i".to_string(),
}]
.into(),
},
Case {
equation: "ij,jk->ik",
broadcast_ndim: 0,
path: [EinsumStep {
lhs: new_term("ij", Some(0)),
rhs: Some(new_term("jk", Some(1))),
output: "ik".to_string(),
}]
.into(),
},
Case {
equation: "ab,bc,cd,de->ea",
broadcast_ndim: 0,
path: [
EinsumStep {
lhs: new_term("ab", Some(0)),
rhs: Some(new_term("bc", Some(1))),
output: "ac".to_string(),
},
EinsumStep {
lhs: new_term("ac", None),
rhs: Some(new_term("cd", Some(2))),
output: "ad".to_string(),
},
EinsumStep {
lhs: new_term("ad", None),
rhs: Some(new_term("de", Some(3))),
output: "ea".to_string(),
},
]
.into(),
},
Case {
equation: "ab,cd,ef",
broadcast_ndim: 0,
path: [
EinsumStep {
lhs: new_term("ab", Some(0)),
rhs: Some(new_term("cd", Some(1))),
output: "abcd".to_string(),
},
EinsumStep {
lhs: new_term("abcd", None),
rhs: Some(new_term("ef", Some(2))),
output: "abcdef".to_string(),
},
]
.into(),
},
Case {
equation: "ii,j,i->",
broadcast_ndim: 0,
path: [
EinsumStep {
lhs: new_term("ii", Some(0)),
rhs: Some(new_term("j", Some(1))),
output: "i".to_string(),
},
EinsumStep {
lhs: new_term("i", None),
rhs: Some(new_term("i", Some(2))),
output: "".to_string(),
},
]
.into(),
},
Case {
equation: "ii,i,j->",
broadcast_ndim: 0,
path: [
EinsumStep {
lhs: new_term("ii", Some(0)),
rhs: Some(new_term("i", Some(1))),
output: "".to_string(),
},
EinsumStep {
lhs: new_term("", None),
rhs: Some(new_term("j", Some(2))),
output: "".to_string(),
},
]
.into(),
},
Case {
equation: "ii,i,i->i",
broadcast_ndim: 0,
path: [
EinsumStep {
lhs: new_term("ii", Some(0)),
rhs: Some(new_term("i", Some(1))),
output: "i".to_string(),
},
EinsumStep {
lhs: new_term("i", None),
rhs: Some(new_term("i", Some(2))),
output: "i".to_string(),
},
]
.into(),
},
Case {
equation: "i...j->j...i",
broadcast_ndim: 3,
path: [EinsumStep {
lhs: new_term("i012j", Some(0)),
rhs: None,
output: "j012i".to_string(),
}]
.into(),
},
Case {
equation: "...i,...j,...k->...ijk",
broadcast_ndim: 2,
path: [
EinsumStep {
lhs: new_term("01i", Some(0)),
rhs: Some(new_term("01j", Some(1))),
output: "01ij".to_string(),
},
EinsumStep {
lhs: new_term("01ij", None),
rhs: Some(new_term("01k", Some(2))),
output: "01ijk".to_string(),
},
]
.into(),
},
];
cases.test_each(|case| {
let expr = EinsumExpr::parse(case.equation).unwrap();
assert_eq!(einsum_path(&expr, case.broadcast_ndim), case.path);
})
}
}