use super::MultiBroadcastTo;
use crate::internal::*;
use tract_itertools::Itertools;
use tract_num_traits::Zero;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct CumSum {
pub axis: usize,
pub exclusive: bool,
pub reverse: bool,
}
impl CumSum {
fn eval_t<T: Datum + Copy + std::ops::AddAssign + Zero>(
&self,
tensor: &mut Tensor,
) -> TractResult<()> {
let shape = tensor.shape().to_vec();
let len = shape[self.axis];
let inner: usize = shape[self.axis + 1..].iter().product();
if len == 0 || inner == 0 {
return Ok(());
}
let mut plain = tensor.try_as_plain_ram_mut()?;
let data = plain.as_slice_mut::<T>()?;
for block in data.chunks_exact_mut(len * inner) {
if self.reverse {
for i in (0..len - 1).rev() {
let (head, tail) = block.split_at_mut((i + 1) * inner);
add_row(&mut head[i * inner..], &tail[..inner]);
}
if self.exclusive {
block.copy_within(inner.., 0);
block[(len - 1) * inner..].fill(T::zero());
}
} else {
for i in 1..len {
let (head, tail) = block.split_at_mut(i * inner);
add_row(&mut tail[..inner], &head[(i - 1) * inner..]);
}
if self.exclusive {
block.copy_within(..(len - 1) * inner, inner);
block[..inner].fill(T::zero());
}
}
}
Ok(())
}
}
fn add_row<T: Copy + std::ops::AddAssign>(acc: &mut [T], row: &[T]) {
acc.iter_mut().zip(row).for_each(|(a, r)| *a += *r);
}
impl Op for CumSum {
fn name(&self) -> StaticName {
"CumSum".into()
}
fn info(&self) -> TractResult<Vec<String>> {
Ok(vec![format!(
"axis: {} exclusive: {} reverse: {}",
self.axis, self.exclusive, self.reverse
)])
}
op_as_typed_op!();
}
impl EvalOp for CumSum {
op_out_of_plan!();
fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
let mut tensor = args_1!(inputs).into_tensor();
dispatch_numbers!(Self::eval_t(tensor.datum_type())(self, &mut tensor))?;
Ok(tvec!(tensor.into_tvalue()))
}
}
impl TypedOp for CumSum {
fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
ensure!(self.axis < inputs[0].rank());
ensure!(!inputs[0].datum_type.is_quantized(), "CumSum does not take quantized inputs");
Ok(tvec!(inputs[0].datum_type.fact(inputs[0].shape.clone())))
}
fn axes_mapping(
&self,
inputs: &[&TypedFact],
outputs: &[&TypedFact],
) -> TractResult<AxesMapping> {
let mut letters = 'a'..;
let axes = (0..inputs[0].rank())
.flat_map(|ix| {
if ix == self.axis {
tvec!(
Axis::new(letters.next().unwrap(), inputs.len(), outputs.len())
.input(0, ix),
Axis::new(letters.next().unwrap(), inputs.len(), outputs.len())
.output(0, ix),
)
} else {
tvec!(
Axis::new(letters.next().unwrap(), inputs.len(), outputs.len())
.input(0, ix)
.output(0, ix)
)
}
})
.collect_vec();
AxesMapping::new(1, 1, axes)
}
fn declutter(
&self,
model: &TypedModel,
node: &TypedNode,
) -> TractResult<Option<TypedModelPatch>> {
let fact = model.outlet_fact(node.inputs[0])?;
rule_if!(fact.shape[self.axis].is_one());
if !self.exclusive {
return TypedModelPatch::shunt_one_op(model, node);
}
let zero = Tensor::zero_dt(fact.datum_type, &tvec![1; fact.rank()])?;
let mut patch = TypedModelPatch::default();
let zero = patch.add_const(format!("{}.zero", node.name), zero)?;
let wire =
patch.wire_node(&node.name, MultiBroadcastTo::new(fact.shape.clone()), &[zero])?;
patch.shunt_outside(model, node.id.into(), wire[0])?;
Ok(Some(patch))
}
fn change_axes(
&self,
model: &TypedModel,
node: &TypedNode,
_io: InOut,
change: &AxisOp,
) -> TractResult<Option<AxisChangeConsequence>> {
rule_if_some!(axis = change.transform_axis(self.axis));
let op = Some(Box::new(Self { axis, ..self.clone() }) as _);
Ok(Some(AxisChangeConsequence::new(model, node, op, change)))
}
fn slice(
&self,
patch: &mut TypedModelPatch,
_model: &TypedModel,
node: &TypedNode,
_prefix: &str,
inputs: &[OutletId],
output_axis: usize,
_start: &TDim,
_end: &TDim,
) -> TractResult<Option<TVec<OutletId>>> {
rule_if!(output_axis != self.axis);
patch.wire_node(&node.name, &node.op, inputs).map(Some)
}
as_op!();
}
#[cfg(test)]
mod tests {
use super::*;
fn declutter_single_step(exclusive: bool) -> TractResult<TypedModel> {
let mut model = TypedModel::default();
let t = model.symbols.sym("T");
let input = model.add_source("input", f32::fact(&[1.to_dim(), t.to_dim()]))?;
let op = CumSum { axis: 0, exclusive, reverse: false };
let output = model.wire_node("cumsum", op, &[input])?;
model.select_output_outlets(&output)?;
model.into_decluttered()
}
#[test]
fn single_step_inclusive_is_identity() -> TractResult<()> {
let model = declutter_single_step(false)?;
assert_eq!(model.nodes().len(), 1);
Ok(())
}
#[test]
fn single_step_exclusive_is_zeros() -> TractResult<()> {
let model = declutter_single_step(true)?;
assert!(model.nodes().iter().all(|n| !n.op_is::<CumSum>()));
let input = tensor2(&[[1f32, 2., 3.]]);
let output = model.into_runnable()?.run(tvec!(input.into_tvalue()))?;
output[0].close_enough(&tensor2(&[[0f32, 0., 0.]]), Approximation::Exact)?;
Ok(())
}
}