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 resolved: TVec<usize> = indices
65                .iter()
66                .map(|i| if *i < 0 { i + axis_len as i64 } else { *i } as usize)
67                .collect();
68            let mut out_offset = 0;
69            if block_len == 1 {
70                // Gathering the innermost axis: each block is one datum, so a
71                // slice copy per element costs more than the read itself.
72                for outer in 0..outer_len {
73                    let input_base = outer * axis_len;
74                    for index in &resolved {
75                        output_slice[out_offset] = input_slice[input_base + index].clone();
76                        out_offset += 1;
77                    }
78                }
79            } else {
80                for outer in 0..outer_len {
81                    let input_base = outer * axis_len * block_len;
82                    for index in &resolved {
83                        let input_offset = input_base + index * block_len;
84                        output_slice[out_offset..out_offset + block_len]
85                            .clone_from_slice(&input_slice[input_offset..input_offset + block_len]);
86                        out_offset += block_len;
87                    }
88                }
89            }
90        } else {
91            let ic_len = self.axis + 1 + output_shape.len() - (self.axis + indices.ndim());
92            let mut icoords = vec![0; ic_len];
93            let axis = self.axis;
94            for coords in tract_ndarray::indices(output_shape) {
95                let ocoords = coords.as_array_view();
96                let ocoords = ocoords.as_slice().unwrap();
97
98                let kcoords = &ocoords[self.axis..][..indices.ndim()];
99                let k = indices[kcoords];
100                let k = if k < 0 { k + data_view.shape()[self.axis] as i64 } else { k } as usize;
101                icoords[0..axis].copy_from_slice(&ocoords[..self.axis]);
102                icoords[self.axis] = k;
103                icoords[self.axis + 1..].clone_from_slice(&ocoords[self.axis + indices.ndim()..]);
104                output_view[ocoords] =
105                    data_view.get(&*icoords).cloned().context("Invalid gather")?;
106            }
107        }
108        // Tensor::uninitialized stamps the plain datum type, dropping any
109        // quantization the data carried.
110        unsafe { output.set_datum_type(data.datum_type()) };
111        Ok(output)
112    }
113
114    fn eval_bq<F: Datum>(
115        &self,
116        data: &BlockQuantStorage,
117        m: usize,
118        k: usize,
119        indices: &TValue,
120    ) -> TractResult<Tensor> {
121        ensure!(self.axis == 0);
122        let data_shape = &[m, k];
123        let output_shape = &*self.compute_output_shape(data_shape, indices.shape())?;
124        let mut output = unsafe { Tensor::uninitialized::<F>(output_shape)? };
125        let indices_plain = indices.try_as_plain()?;
126        let indices_slice = indices_plain.as_slice::<i64>()?;
127        let vector_len = k;
128        let blob = data.value();
129
130        let block_len = data.format().block_len();
131        let block_bytes = data.format().block_bytes();
132        if F::datum_type() == f16::datum_type() {
133            let mut output_plain = output.try_as_plain_mut()?;
134            let output_slice = output_plain.as_slice_mut::<f16>()?;
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_f16(
141                        &blob[block_id * block_bytes..][..block_bytes],
142                        &mut slice[i..i + block_len],
143                    );
144                }
145            }
146        } else {
147            let mut output_plain = output.try_as_plain_mut()?;
148            let output_slice = output_plain.as_slice_mut::<f32>()?;
149            for (pos, ix) in indices_slice.iter().enumerate() {
150                let slice = &mut output_slice[pos * vector_len..][..vector_len];
151                for i in (0..vector_len).step_by(block_len) {
152                    let offset = k * *ix as usize + i;
153                    let block_id = offset / block_len;
154                    data.format().dequant_block_f32(
155                        &blob[block_id * block_bytes..][..block_bytes],
156                        &mut slice[i..i + block_len],
157                    );
158                }
159            }
160        }
161        Ok(output)
162    }
163
164    fn eval_input_store<F: Datum>(
165        &self,
166        data: &dyn MMMInputValue,
167        indices: &TValue,
168    ) -> TractResult<Tensor> {
169        ensure!(self.axis == 0);
170        let data_shape = &[data.mn(), data.k()];
171        let output_shape = &*self.compute_output_shape(data_shape, indices.shape())?;
172        let mut output = unsafe { Tensor::uninitialized::<F>(output_shape)? };
173        let indices_plain = indices.try_as_plain()?;
174        let indices_slice = indices_plain.as_slice::<i64>()?;
175        let vector_len = data_shape[1];
176        if F::datum_type() == f16::datum_type() {
177            let mut output_plain = output.try_as_plain_mut()?;
178            let output_slice = output_plain.as_slice_mut::<f16>()?;
179            for (pos, m) in indices_slice.iter().enumerate() {
180                let slice = &mut output_slice[pos * vector_len..][..vector_len];
181                data.extract_at_mn_f16(*m as usize, slice)?;
182            }
183        } else {
184            let mut output_plain = output.try_as_plain_mut()?;
185            let output_slice = output_plain.as_slice_mut::<f32>()?;
186            for (pos, m) in indices_slice.iter().enumerate() {
187                let slice = &mut output_slice[pos * vector_len..][..vector_len];
188                data.extract_at_mn_f32(*m as usize, slice)?;
189            }
190        }
191        Ok(output)
192    }
193}
194
195impl TypedOp for Gather {
196    as_op!();
197
198    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
199        if let Some(dt) = self.output_type {
200            ensure!(
201                inputs[0].is_exotic() || inputs[0].datum_type == dt,
202                "Inconsistent datum_type in Gather: attribute is {:?}, but inputs[0] is {:?}",
203                dt,
204                inputs[0].datum_type
205            );
206        } else {
207            ensure!(
208                inputs[0].is_plain(),
209                "Gather applied to compressed data requires an explicit datum_type attribute for its output"
210            );
211        }
212        ensure!(inputs[1].datum_type == i64::datum_type());
213        if inputs[0].is_exotic() {
214            let data_shape = block_quant_aware_input_shape(inputs[0])?;
215            Ok(tvec!(
216                self.output_type
217                    .unwrap()
218                    .fact(&*self.compute_output_shape(&data_shape, &inputs[1].shape)?)
219            ))
220        } else {
221            Ok(tvec!(
222                inputs[0]
223                    .datum_type
224                    .fact(&*self.compute_output_shape(&inputs[0].shape, &inputs[1].shape)?)
225            ))
226        }
227    }
228
229    fn axes_mapping(
230        &self,
231        inputs: &[&TypedFact],
232        _outputs: &[&TypedFact],
233    ) -> TractResult<AxesMapping> {
234        // Output = data[..axis] ++ indices ++ data[axis+1..].  Track:
235        // - data axes [0..axis)        → output [0..axis)
236        // - data axis  self.axis       consumed (no output)
237        // - data axes (axis..data_rank)→ output [axis + indices_rank..)
238        // - indices axes [0..ir)       → output [axis..axis + ir)
239        // Fall back to disconnected for exotic data (block-quant): the
240        // storage rank can differ from the logical rank.
241        if !inputs[0].is_plain() {
242            return AxesMapping::disconnected(
243                inputs,
244                &[&inputs[0].datum_type.fact(&[0i64.to_dim()])],
245            );
246        }
247        let data_rank = inputs[0].rank();
248        let indices_rank = inputs[1].rank();
249        let mut axes: TVec<crate::axes::Axis> = tvec!();
250        let mut alphabet = 'a'..;
251        for k in 0..self.axis {
252            axes.push(
253                crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1).input(0, k).output(0, k),
254            );
255        }
256        axes.push(crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1).input(0, self.axis));
257        for k in self.axis + 1..data_rank {
258            let out_pos = k - 1 + indices_rank;
259            axes.push(
260                crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1)
261                    .input(0, k)
262                    .output(0, out_pos),
263            );
264        }
265        for k in 0..indices_rank {
266            let out_pos = self.axis + k;
267            axes.push(
268                crate::axes::Axis::new(alphabet.next().unwrap(), 2, 1)
269                    .input(1, k)
270                    .output(0, out_pos),
271            );
272        }
273        AxesMapping::new(2, 1, axes)
274    }
275
276    fn declutter(
277        &self,
278        model: &TypedModel,
279        node: &TypedNode,
280    ) -> TractResult<Option<TypedModelPatch>> {
281        let (input_fact, indices_fact) = args_2!(model.node_input_facts(node.id)?);
282        if let Some(indices) = indices_fact.konst.as_ref()
283            && indices.rank() == 1
284            && indices.len() == 1
285            && input_fact.is_plain()
286            && input_fact.datum_type.is_number()
287        {
288            let mut patch = TypedModelPatch::default();
289            let mut wire = patch.tap_model(model, node.inputs[0])?;
290            let index = indices.cast_to_scalar::<i64>()?;
291            let index = if index < 0 {
292                let data_fact = model.outlet_fact(node.inputs[0])?;
293                data_fact.shape[self.axis].clone() + index.to_dim()
294            } else {
295                index.to_dim()
296            };
297            wire = patch.wire_node(
298                format!("{}.slice", node.name),
299                crate::ops::array::Slice { axis: self.axis, start: index.clone(), end: index + 1 },
300                &[wire],
301            )?[0];
302            patch.shunt_outside(model, node.id.into(), wire)?;
303            return Ok(Some(patch));
304        }
305        if input_fact.konst.is_some() {
306            // look for a OptSimpleMatMulPack *sibling*
307            if let Some(sibling) = model
308                .outlet_successors(node.inputs[0])
309                .iter()
310                .find(|o| o.node != node.id && model.node(o.node).op_is::<OptSimpleMatMulPack>())
311            {
312                let mut patch = TypedModelPatch::default();
313                let mut taps = patch.taps(model, &node.inputs)?;
314                taps[0] = patch.tap_model(model, sibling.node.into())?;
315                let wire = patch.wire_node(&node.name, self.clone(), &taps)?[0];
316                patch.shunt_outside(model, node.id.into(), wire)?;
317                return Ok(Some(patch));
318            }
319        }
320        Ok(None)
321    }
322}
323
324impl EvalOp for Gather {
325    op_out_of_plan!();
326
327    fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
328        let (data, indices) = args_2!(inputs);
329        let result = if let Some(bqs) = data.storage_as::<BlockQuantStorage>() {
330            let dt = self.output_type.unwrap();
331            let m = data.shape()[data.rank() - 2];
332            let k = *data.shape().last().unwrap();
333            dispatch_floatlike!(Self::eval_bq(dt)(self, bqs, m, k, &indices))?
334        } else if let Some(storage) = data.storage_as::<PackedMatrixStorage>()
335            && storage.batch_shape().is_empty()
336        {
337            let dt = self.output_type.unwrap();
338            let data_val = storage.value();
339            dispatch_floatlike!(Self::eval_input_store(dt)(self, data_val, &indices))?
340        } else {
341            dispatch_datum!(Self::eval_t(data.datum_type())(self, data, &indices))?
342        };
343        Ok(tvec!(result.into_tvalue()))
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_should_gather_scalar_index() {
353        let data = Tensor::from(arr1(&[1i64, 2, 3]));
354        let gatherer = Gather::new(0);
355        for idx in 2..3 {
356            let index = Tensor::from(arr0(idx));
357            let outputs = gatherer
358                .eval(
359                    &EvalContext::out_of_plan(),
360                    tvec![data.clone().into_tvalue(), index.into_tvalue()],
361                )
362                .unwrap();
363            let output = &outputs[0];
364            assert_eq!(output.shape().len(), 0);
365            assert_eq!(*output.try_as_plain().unwrap().to_scalar::<i64>().unwrap(), idx + 1);
366        }
367    }
368}