Skip to main content

tract_core/ops/array/
gather_elements.rs

1use crate::internal::*;
2use ndarray::*;
3
4/// For every coordinate of `indices`, reads `data` at that same coordinate with
5/// `axis` replaced by the index value found there. The output has `indices`'
6/// shape. A negative index counts from the end of `axis`. `data` and `indices`
7/// must have the same rank; off `axis`, `indices` may be smaller than `data`.
8#[derive(Debug, Clone, new, Hash, PartialEq, Eq)]
9pub struct GatherElements {
10    pub axis: usize,
11}
12
13impl Op for GatherElements {
14    fn name(&self) -> StaticName {
15        "GatherElements".into()
16    }
17
18    op_as_typed_op!();
19}
20
21impl GatherElements {
22    /// Gathers when `axis` is the last one and the leading dimensions of both
23    /// operands are identical. Every leading coordinate then addresses one whole
24    /// row of both operands, so the op is a flat per-row lookup and none of the
25    /// per-element dynamic-rank stride arithmetic of the generic path is needed.
26    /// `Ok(None)` means the operands do not qualify and the caller must use the
27    /// generic path.
28    ///
29    /// Index resolution is identical to the generic path, but an out-of-range
30    /// index is reported as an error instead of panicking inside `ndarray`.
31    fn eval_contiguous_last_axis<T: Datum>(
32        &self,
33        data: &ArrayViewD<T>,
34        indices: &ArrayViewD<i64>,
35    ) -> TractResult<Option<ArrayD<T>>> {
36        let rank = data.ndim();
37        let Some(last_axis) = rank.checked_sub(1) else { return Ok(None) };
38        if self.axis != last_axis
39            || indices.ndim() != rank
40            || data.shape()[..last_axis] != indices.shape()[..last_axis]
41        {
42            return Ok(None);
43        }
44        // Both views come from a plain tract tensor, so they are contiguous and
45        // this never declines; it is handled rather than asserted.
46        let (Some(data_slice), Some(index_slice)) = (data.as_slice(), indices.as_slice()) else {
47            return Ok(None);
48        };
49        let row_len = data.shape()[last_axis];
50        let gathered_len = indices.shape()[last_axis];
51        // A zero gathered length still has leading coordinates to walk, and
52        // walking them would be pure waste for an empty output.
53        let rows = if indices.is_empty() { 0 } else { indices.len() / gathered_len };
54        let mut output = Vec::with_capacity(indices.len());
55        for row in 0..rows {
56            let data_row = &data_slice[row * row_len..][..row_len];
57            for &index in &index_slice[row * gathered_len..][..gathered_len] {
58                let resolved = if index < 0 { index + row_len as i64 } else { index };
59                let value = usize::try_from(resolved)
60                    .ok()
61                    .and_then(|resolved| data_row.get(resolved))
62                    .with_context(|| {
63                        format!(
64                            "Invalid GatherElements index {index} in row {row} on axis of len {row_len}"
65                        )
66                    })?;
67                output.push(value.clone());
68            }
69        }
70        Ok(Some(ArrayD::from_shape_vec(indices.shape(), output)?))
71    }
72
73    unsafe fn eval_t<T: Datum>(
74        &self,
75        data: TValue,
76        indices: &ArrayViewD<i64>,
77    ) -> TractResult<TValue> {
78        let data_plain = data.try_as_plain()?;
79        let data_view = unsafe { data_plain.to_array_view_unchecked::<T>() };
80        let output = match self.eval_contiguous_last_axis::<T>(&data_view, indices)? {
81            Some(output) => output,
82            None => ArrayD::<T>::from_shape_fn(indices.shape(), |mut coords| {
83                let index = indices[&coords];
84                coords[self.axis] =
85                    if index < 0 { index + data_view.shape()[self.axis] as i64 } else { index }
86                        as usize;
87                data_view[coords].clone()
88            }),
89        };
90        let mut tensor = output.into_tensor();
91        unsafe { tensor.set_datum_type(data.datum_type()) };
92        Ok(tensor.into_tvalue())
93    }
94}
95
96impl TypedOp for GatherElements {
97    as_op!();
98
99    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
100        ensure!(
101            inputs[0].rank() == inputs[1].rank(),
102            "GatherElements data and indices must have the same rank, got {} and {}",
103            inputs[0].rank(),
104            inputs[1].rank()
105        );
106        ensure!(
107            self.axis < inputs[0].rank(),
108            "GatherElements axis {} is out of range for rank {}",
109            self.axis,
110            inputs[0].rank()
111        );
112        Ok(tvec!(inputs[0].datum_type.fact(&*inputs[1].shape)))
113    }
114}
115
116impl EvalOp for GatherElements {
117    fn is_stateless(&self) -> bool {
118        true
119    }
120
121    fn eval(&self, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
122        let (data, indices) = args_2!(inputs);
123        let indices = indices.cast_to::<i64>()?;
124        let indices = indices.to_plain_array_view::<i64>()?;
125        unsafe {
126            Ok(tvec!(dispatch_datum_by_size!(Self::eval_t(data.datum_type())(
127                self, data, &indices
128            ))?))
129        }
130    }
131}
132
133/// The out-of-range branches are only reachable on the contiguous last-axis path
134/// (the generic path panics inside `ndarray` instead), so they cannot be covered
135/// by the output-comparing `suite-unit` cases.
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    fn gather(data_shape: &[usize], indices: &[i64]) -> TractResult<TValue> {
141        let len = data_shape.iter().product::<usize>();
142        let data = Tensor::from_shape(data_shape, &(0..len).map(|i| i as f32).collect::<Vec<_>>())?;
143        let indices = Tensor::from_shape(&[1, indices.len()], indices)?;
144        let mut outputs = GatherElements::new(data_shape.len() - 1)
145            .eval(tvec!(data.into_tvalue(), indices.into_tvalue()))?;
146        Ok(outputs.remove(0))
147    }
148
149    #[test]
150    fn last_axis_resolves_negative_indices() {
151        let output = gather(&[1, 4], &[-1, 0, -4, 2]).unwrap();
152        assert_eq!(output.try_as_plain().unwrap().as_slice::<f32>().unwrap(), [3., 0., 0., 2.]);
153    }
154
155    #[test]
156    fn last_axis_rejects_index_past_the_end() {
157        assert!(gather(&[1, 4], &[0, 4]).is_err());
158    }
159
160    #[test]
161    fn last_axis_rejects_index_before_the_start() {
162        assert!(gather(&[1, 4], &[0, -5]).is_err());
163    }
164
165    #[test]
166    fn last_axis_rejects_any_index_into_an_empty_axis() {
167        assert!(gather(&[1, 0], &[0]).is_err());
168    }
169}