Skip to main content

tract_core/ops/array/
gather.rs

1use crate::internal::*;
2use crate::ops::einsum::block_quant_aware_input_shape;
3use crate::ops::matmul::pack::OptSimpleMatMulPack;
4use ndarray::*;
5use tract_linalg::block_quant::BlockQuantStorage;
6use tract_linalg::mmm::{MMMInputValue, PackedMatrixStorage};
7
8#[derive(Debug, Clone, Hash, PartialEq, Eq)]
9pub struct Gather {
10    pub axis: usize,
11    pub output_type: Option<DatumType>,
12}
13
14impl Op for Gather {
15    fn name(&self) -> StaticName {
16        "Gather".into()
17    }
18
19    op_as_typed_op!();
20}
21
22impl Gather {
23    pub fn new(axis: usize) -> Gather {
24        Gather { axis, output_type: None }
25    }
26
27    pub fn compute_output_shape<D: DimLike>(
28        &self,
29        input_shape: &[D],
30        indices_shape: &[D],
31    ) -> TractResult<TVec<D>> {
32        ensure!(input_shape.len() > self.axis);
33        let mut output_shape: TVec<D> = input_shape[..self.axis].into();
34        output_shape.extend(indices_shape.iter().cloned());
35        output_shape.extend(input_shape[self.axis + 1..].iter().cloned());
36        Ok(output_shape)
37    }
38
39    fn eval_t<T: Datum>(&self, data: TValue, indices: &TValue) -> TractResult<Tensor> {
40        let data_plain = data.try_as_plain()?;
41        let data_view = unsafe { data_plain.to_array_view_unchecked::<T>() };
42        let indices = indices.to_plain_array_view::<i64>()?;
43        let output_shape = &*self.compute_output_shape(data.shape(), indices.shape())?;
44        let mut output = unsafe { Tensor::uninitialized::<T>(output_shape)? };
45        let mut output_plain = output.try_as_plain_mut()?;
46        let mut output_view = output_plain.to_array_view_mut::<T>()?;
47
48        let data_shape = data.shape();
49        let data_axis = self.axis;
50
51        let block_len = data_shape[data_axis + 1..].iter().product::<usize>();
52
53        // Both shapes agree on the axes before the gathered one, so one outer
54        // stride walks data and output together.
55        let outer_len = data_shape[..data_axis].iter().product::<usize>();
56        let can_block_copy = data_shape[..data_axis] == output_shape[..data_axis]
57            && data_view.is_standard_layout()
58            && output_view.is_standard_layout();
59
60        if can_block_copy {
61            let axis_len = data_shape[data_axis];
62            let input_slice = data_view.as_slice().unwrap();
63            let output_slice = &mut output_view.as_slice_mut().unwrap();
64            let mut out_offset = 0;
65            for outer in 0..outer_len {
66                let input_base = outer * axis_len * block_len;
67                for index in indices.iter() {
68                    let resolved_index =
69                        if *index < 0 { index + axis_len as i64 } else { *index } as usize;
70                    let input_offset = input_base + resolved_index * block_len;
71                    output_slice[out_offset..out_offset + block_len]
72                        .clone_from_slice(&input_slice[input_offset..input_offset + block_len]);
73                    out_offset += block_len;
74                }
75            }
76        } else {
77            let ic_len = self.axis + 1 + output_shape.len() - (self.axis + indices.ndim());
78            let mut icoords = vec![0; ic_len];
79            let axis = self.axis;
80            for coords in tract_ndarray::indices(output_shape) {
81                let ocoords = coords.as_array_view();
82                let ocoords = ocoords.as_slice().unwrap();
83
84                let kcoords = &ocoords[self.axis..][..indices.ndim()];
85                let k = indices[kcoords];
86                let k = if k < 0 { k + data_view.shape()[self.axis] as i64 } else { k } as usize;
87                icoords[0..axis].copy_from_slice(&ocoords[..self.axis]);
88                icoords[self.axis] = k;
89                icoords[self.axis + 1..].clone_from_slice(&ocoords[self.axis + indices.ndim()..]);
90                output_view[ocoords] =
91                    data_view.get(&*icoords).cloned().context("Invalid gather")?;
92            }
93        }
94        // Tensor::uninitialized stamps the plain datum type, dropping any
95        // quantization the data carried.
96        unsafe { output.set_datum_type(data.datum_type()) };
97        Ok(output)
98    }
99
100    fn eval_bq<F: Datum>(
101        &self,
102        data: &BlockQuantStorage,
103        m: usize,
104        k: usize,
105        indices: &TValue,
106    ) -> TractResult<Tensor> {
107        ensure!(self.axis == 0);
108        let data_shape = &[m, k];
109        let output_shape = &*self.compute_output_shape(data_shape, indices.shape())?;
110        let mut output = unsafe { Tensor::uninitialized::<F>(output_shape)? };
111        let indices_plain = indices.try_as_plain()?;
112        let indices_slice = indices_plain.as_slice::<i64>()?;
113        let vector_len = k;
114        let blob = data.value();
115
116        let block_len = data.format().block_len();
117        let block_bytes = data.format().block_bytes();
118        if F::datum_type() == f16::datum_type() {
119            let mut output_plain = output.try_as_plain_mut()?;
120            let output_slice = output_plain.as_slice_mut::<f16>()?;
121            for (pos, ix) in indices_slice.iter().enumerate() {
122                let slice = &mut output_slice[pos * vector_len..][..vector_len];
123                for i in (0..vector_len).step_by(block_len) {
124                    let offset = k * *ix as usize + i;
125                    let block_id = offset / block_len;
126                    data.format().dequant_block_f16(
127                        &blob[block_id * block_bytes..][..block_bytes],
128                        &mut slice[i..i + block_len],
129                    );
130                }
131            }
132        } else {
133            let mut output_plain = output.try_as_plain_mut()?;
134            let output_slice = output_plain.as_slice_mut::<f32>()?;
135            for (pos, ix) in indices_slice.iter().enumerate() {
136                let slice = &mut output_slice[pos * vector_len..][..vector_len];
137                for i in (0..vector_len).step_by(block_len) {
138                    let offset = k * *ix as usize + i;
139                    let block_id = offset / block_len;
140                    data.format().dequant_block_f32(
141                        &blob[block_id * block_bytes..][..block_bytes],
142                        &mut slice[i..i + block_len],
143                    );
144                }
145            }
146        }
147        Ok(output)
148    }
149
150    fn eval_input_store<F: Datum>(
151        &self,
152        data: &dyn MMMInputValue,
153        indices: &TValue,
154    ) -> TractResult<Tensor> {
155        ensure!(self.axis == 0);
156        let data_shape = &[data.mn(), data.k()];
157        let output_shape = &*self.compute_output_shape(data_shape, indices.shape())?;
158        let mut output = unsafe { Tensor::uninitialized::<F>(output_shape)? };
159        let indices_plain = indices.try_as_plain()?;
160        let indices_slice = indices_plain.as_slice::<i64>()?;
161        let vector_len = data_shape[1];
162        if F::datum_type() == f16::datum_type() {
163            let mut output_plain = output.try_as_plain_mut()?;
164            let output_slice = output_plain.as_slice_mut::<f16>()?;
165            for (pos, m) in indices_slice.iter().enumerate() {
166                let slice = &mut output_slice[pos * vector_len..][..vector_len];
167                data.extract_at_mn_f16(*m as usize, slice)?;
168            }
169        } else {
170            let mut output_plain = output.try_as_plain_mut()?;
171            let output_slice = output_plain.as_slice_mut::<f32>()?;
172            for (pos, m) in indices_slice.iter().enumerate() {
173                let slice = &mut output_slice[pos * vector_len..][..vector_len];
174                data.extract_at_mn_f32(*m as usize, slice)?;
175            }
176        }
177        Ok(output)
178    }
179}
180
181impl TypedOp for Gather {
182    as_op!();
183
184    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
185        if let Some(dt) = self.output_type {
186            ensure!(
187                inputs[0].is_exotic() || inputs[0].datum_type == dt,
188                "Inconsistent datum_type in Gather: attribute is {:?}, but inputs[0] is {:?}",
189                dt,
190                inputs[0].datum_type
191            );
192        } else {
193            ensure!(
194                inputs[0].is_plain(),
195                "Gather applied to compressed data requires an explicit datum_type attribute for its output"
196            );
197        }
198        ensure!(inputs[1].datum_type == i64::datum_type());
199        if inputs[0].is_exotic() {
200            let data_shape = block_quant_aware_input_shape(inputs[0])?;
201            Ok(tvec!(
202                self.output_type
203                    .unwrap()
204                    .fact(&*self.compute_output_shape(&data_shape, &inputs[1].shape)?)
205            ))
206        } else {
207            Ok(tvec!(
208                inputs[0]
209                    .datum_type
210                    .fact(&*self.compute_output_shape(&inputs[0].shape, &inputs[1].shape)?)
211            ))
212        }
213    }
214
215    fn axes_mapping(
216        &self,
217        inputs: &[&TypedFact],
218        _outputs: &[&TypedFact],
219    ) -> TractResult<AxesMapping> {
220        // Output = data[..axis] ++ indices ++ data[axis+1..].  Track:
221        // - data axes [0..axis)        → output [0..axis)
222        // - data axis  self.axis       consumed (no output)
223        // - data axes (axis..data_rank)→ output [axis + indices_rank..)
224        // - indices axes [0..ir)       → output [axis..axis + ir)
225        // Fall back to disconnected for exotic data (block-quant): the
226        // storage rank can differ from the logical rank.
227        if !inputs[0].is_plain() {
228            return AxesMapping::disconnected(
229                inputs,
230                &[&inputs[0].datum_type.fact(&[0i64.to_dim()])],
231            );
232        }
233        let data_rank = inputs[0].rank();
234        let indices_rank = inputs[1].rank();
235        let mut axes: TVec<crate::axes::Axis> = tvec!();
236        let mut alphabet = 'a'..;
237        for k in 0..self.axis {
238            axes.push(
239                crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1).input(0, k).output(0, k),
240            );
241        }
242        axes.push(crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1).input(0, self.axis));
243        for k in self.axis + 1..data_rank {
244            let out_pos = k - 1 + indices_rank;
245            axes.push(
246                crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1)
247                    .input(0, k)
248                    .output(0, out_pos),
249            );
250        }
251        for k in 0..indices_rank {
252            let out_pos = self.axis + k;
253            axes.push(
254                crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1)
255                    .input(1, k)
256                    .output(0, out_pos),
257            );
258        }
259        AxesMapping::new(2, 1, axes)
260    }
261
262    fn declutter(
263        &self,
264        model: &TypedModel,
265        node: &TypedNode,
266    ) -> TractResult<Option<TypedModelPatch>> {
267        let (input_fact, indices_fact) = args_2!(model.node_input_facts(node.id)?);
268        if let Some(indices) = indices_fact.konst.as_ref()
269            && indices.rank() == 1
270            && indices.len() == 1
271            && input_fact.is_plain()
272            && input_fact.datum_type.is_number()
273        {
274            let mut patch = TypedModelPatch::default();
275            let mut wire = patch.tap_model(model, node.inputs[0])?;
276            let index = indices.cast_to_scalar::<i64>()?;
277            let index = if index < 0 {
278                let data_fact = model.outlet_fact(node.inputs[0])?;
279                data_fact.shape[self.axis].clone() + index.to_dim()
280            } else {
281                index.to_dim()
282            };
283            wire = patch.wire_node(
284                format!("{}.slice", node.name),
285                crate::ops::array::Slice { axis: self.axis, start: index.clone(), end: index + 1 },
286                &[wire],
287            )?[0];
288            patch.shunt_outside(model, node.id.into(), wire)?;
289            return Ok(Some(patch));
290        }
291        if input_fact.konst.is_some() {
292            // look for a OptSimpleMatMulPack *sibling*
293            if let Some(sibling) = model
294                .outlet_successors(node.inputs[0])
295                .iter()
296                .find(|o| o.node != node.id && model.node(o.node).op_is::<OptSimpleMatMulPack>())
297            {
298                let mut patch = TypedModelPatch::default();
299                let mut taps = patch.taps(model, &node.inputs)?;
300                taps[0] = patch.tap_model(model, sibling.node.into())?;
301                let wire = patch.wire_node(&node.name, self.clone(), &taps)?[0];
302                patch.shunt_outside(model, node.id.into(), wire)?;
303                return Ok(Some(patch));
304            }
305        }
306        Ok(None)
307    }
308}
309
310impl EvalOp for Gather {
311    fn is_stateless(&self) -> bool {
312        true
313    }
314
315    fn eval(&self, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
316        let (data, indices) = args_2!(inputs);
317        let result = if let Some(bqs) = data.storage_as::<BlockQuantStorage>() {
318            let dt = self.output_type.unwrap();
319            let m = data.shape()[data.rank() - 2];
320            let k = *data.shape().last().unwrap();
321            dispatch_floatlike!(Self::eval_bq(dt)(self, bqs, m, k, &indices))?
322        } else if let Some(storage) = data.storage_as::<PackedMatrixStorage>()
323            && storage.batch_shape().is_empty()
324        {
325            let dt = self.output_type.unwrap();
326            let data_val = storage.value();
327            dispatch_floatlike!(Self::eval_input_store(dt)(self, data_val, &indices))?
328        } else {
329            dispatch_datum!(Self::eval_t(data.datum_type())(self, data, &indices))?
330        };
331        Ok(tvec!(result.into_tvalue()))
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn test_should_gather_scalar_index() {
341        let data = Tensor::from(arr1(&[1i64, 2, 3]));
342        let gatherer = Gather::new(0);
343        for idx in 2..3 {
344            let index = Tensor::from(arr0(idx));
345            let outputs =
346                gatherer.eval(tvec![data.clone().into_tvalue(), index.into_tvalue()]).unwrap();
347            let output = &outputs[0];
348            assert_eq!(output.shape().len(), 0);
349            assert_eq!(*output.try_as_plain().unwrap().to_scalar::<i64>().unwrap(), idx + 1);
350        }
351    }
352}