Skip to main content

tract_core/ops/scan/
optimized.rs

1use super::*;
2use tract_data::internal::*;
3
4#[derive(Debug, Clone, new)]
5pub struct ScanOpParams {
6    pub skip: usize,
7    pub reset_every_turn: bool,
8    pub plan: Arc<TypedSimplePlan>,
9    pub input_mapping: Vec<InputMapping>,
10    pub output_mapping: Vec<OutputMapping<TDim>>,
11}
12
13#[derive(Debug, Clone, new)]
14pub struct OptScan(Arc<ScanOpParams>);
15
16impl std::ops::Deref for OptScan {
17    type Target = ScanOpParams;
18    fn deref(&self) -> &ScanOpParams {
19        &self.0
20    }
21}
22
23impl PartialEq for OptScan {
24    fn eq(&self, _other: &Self) -> bool {
25        false
26    }
27}
28impl Eq for OptScan {}
29
30impl OptScan {
31    pub fn iteration_count(&self, inputs: &[&TypedFact]) -> Option<TDim> {
32        super::iteration_count(&self.input_mapping, inputs)
33    }
34}
35
36impl Op for OptScan {
37    fn mints_runtime_symbols(&self) -> bool {
38        true
39    }
40
41    fn required_bound_symbols(&self, inputs: &[&TypedFact]) -> TVec<Symbol> {
42        let mut syms = super::decluttered::scan_required_bound_symbols(&self.0.plan.model);
43        for fact in inputs {
44            for t in [fact.konst.as_deref(), fact.uniform.as_deref()].into_iter().flatten() {
45                if t.datum_type() == TDim::datum_type()
46                    && let Some(slice) =
47                        t.try_as_plain_ram().ok().and_then(|v| v.as_slice::<TDim>().ok())
48                {
49                    syms.extend(slice.iter().flat_map(|d| d.symbols()));
50                }
51            }
52        }
53        syms
54    }
55
56    fn name(&self) -> StaticName {
57        "Scan".into()
58    }
59
60    fn info(&self) -> TractResult<Vec<String>> {
61        let mut lines = vec![];
62        for (ix, im) in self.input_mapping.iter().enumerate() {
63            lines.push(format!("Model input  #{ix}: {im:?}"));
64        }
65        for (ix, om) in self.output_mapping.iter().enumerate() {
66            lines.push(format!("Model output #{ix}: {om:?}"));
67        }
68        Ok(lines)
69    }
70
71    op_as_typed_op!();
72}
73
74impl EvalOp for OptScan {
75    not_out_of_plan!();
76
77    fn state(&self, _ctx: &EvalContext) -> TractResult<Option<Box<dyn OpState>>> {
78        Ok(Some(Box::new(State {
79            position: 0,
80            hidden_state: tvec!(),
81            model_state: self.plan.spawn()?,
82            op: Arc::clone(&self.0),
83        })))
84    }
85}
86
87#[derive(Clone, Debug)]
88pub struct State {
89    op: Arc<ScanOpParams>,
90    position: usize,
91    hidden_state: TVec<TValue>,
92    pub model_state: TypedSimpleState,
93}
94
95impl State {
96    pub fn iteration_count(&self, inputs: &TVec<TValue>) -> usize {
97        let (slot, info) = self
98            .op
99            .input_mapping
100            .iter()
101            .enumerate()
102            .find_map(|(ix, it)| it.as_scan().map(|scan| (ix, scan)))
103            .unwrap();
104        inputs[slot].shape()[info.axis].divceil(info.chunk.unsigned_abs())
105    }
106
107    pub(super) fn slice_input(
108        input: &Tensor,
109        axis: usize,
110        chunk_ix: usize,
111        chunk_dim: isize,
112    ) -> TractResult<Tensor> {
113        unsafe {
114            let full_len = input.shape()[axis];
115            let mut shape: TVec<usize> = input.shape().into();
116            shape[axis] = chunk_dim.unsigned_abs();
117            let mut t = Tensor::uninitialized_dt(input.datum_type(), &shape)?;
118            if chunk_dim < 0 {
119                let chunk_dim = (-chunk_dim) as usize;
120                for i in 0..chunk_dim {
121                    if chunk_dim * chunk_ix + i < full_len {
122                        let dst_ix = chunk_dim - i - 1;
123                        let src_ix = full_len - 1 - (chunk_ix * chunk_dim + i);
124                        t.assign_slice_unchecked(dst_ix..=dst_ix, input, src_ix..=src_ix, axis);
125                    }
126                }
127            } else if (chunk_ix + 1) * chunk_dim as usize > full_len {
128                let chunk_dim = chunk_dim as usize;
129                let remain = full_len - chunk_ix * chunk_dim;
130                let mut shape: TVec<usize> = input.shape().into();
131                shape[axis] = chunk_dim;
132                t.assign_slice_unchecked(..remain, input, chunk_ix * chunk_dim.., axis);
133            } else {
134                let start = chunk_dim as usize * chunk_ix;
135                let end = start + chunk_dim as usize;
136                t.assign_slice_unchecked(.., input, start..end, axis);
137            }
138            Ok(t)
139        }
140    }
141
142    pub(super) fn assign_output(
143        output: &mut Tensor,
144        axis: usize,
145        element_value: &Tensor,
146        i: usize,
147        backward: bool,
148    ) {
149        let full_len = output.shape()[axis];
150        let offset = if backward {
151            full_len - 1 - i * element_value.shape()[axis]
152        } else {
153            i * element_value.shape()[axis]
154        };
155        let count = element_value.shape()[axis].min(output.shape()[axis] - offset);
156        unsafe {
157            output.assign_slice_unchecked(offset..offset + count, element_value, ..count, axis)
158        };
159    }
160}
161
162impl OpState for State {
163    fn eval(
164        &mut self,
165        ctx: &EvalContext,
166        _op: &dyn Op,
167        inputs: TVec<TValue>,
168    ) -> TractResult<TVec<TValue>> {
169        let iters = self.iteration_count(&inputs);
170
171        let &mut State { ref op, ref mut hidden_state, ref mut position, ref mut model_state } =
172            self;
173
174        // initialize state at first pass, or when forced
175        if op.reset_every_turn {
176            hidden_state.clear()
177        }
178        if hidden_state.len() == 0 {
179            for (slot, input) in op.input_mapping.iter().enumerate() {
180                if input.is_state() {
181                    hidden_state.push(inputs[slot].clone());
182                }
183            }
184        }
185
186        // One full-coverage iteration yields each scan output as-is, so the body
187        // tensor is handed through instead of copied into a preallocation. Warm-up
188        // calls (position <= skip) leave outputs unassigned and need the general path.
189        let runs_now = *position + 1 > op.skip;
190        let mut single_shot: TVec<bool> = tvec!();
191        let mut outputs = tvec!();
192        for (ix, output) in op.output_mapping.iter().enumerate() {
193            let mut one_shot = false;
194            if let Some((slot, info)) = output.scan {
195                let fact = op.plan.model().output_fact(ix)?;
196                let mut shape: TVec<usize> = fact.shape.eval_to_usize(ctx.symbols)?.into_owned();
197                let scanning_dim = output
198                    .full_dim_hint
199                    .as_ref()
200                    .and_then(|d| d.as_usize())
201                    .unwrap_or(shape[info.axis] * iters);
202                one_shot = iters == 1 && runs_now && scanning_dim == shape[info.axis];
203                shape[info.axis] = scanning_dim;
204                let t = if one_shot {
205                    Tensor::default()
206                } else {
207                    unsafe { Tensor::uninitialized_dt(fact.datum_type, &shape)? }
208                };
209                outputs.push((slot, t));
210            }
211            single_shot.push(one_shot);
212            if let Some(slot) = output.last_value_slot {
213                outputs.push((slot, Tensor::default()));
214            }
215        }
216        outputs.sort_by_key(|a| a.0);
217        let mut outputs: TVec<Tensor> = outputs.into_iter().map(|(_slot, v)| v).collect();
218
219        // The body runs the SAME plan with the SAME shapes every iteration, so
220        // resolve its symbols once and keep them across iters (light per-iter
221        // reset), and reuse one drained input buffer. This avoids the per-timestep
222        // symbol re-resolution + reallocation a plain `model_state.run()` would do.
223        // Cleared up front since the body state persists across outer Scan calls.
224        model_state.clear_resolved_symbols();
225        let mut iter_inputs: TVec<TValue> = tvec!();
226        let mut symbols_resolved = false;
227
228        for i in 0..iters {
229            *position += 1;
230            if *position <= op.skip {
231                continue;
232            }
233            hidden_state.reverse();
234
235            iter_inputs.clear();
236            for (slot, m) in op.input_mapping.iter().enumerate() {
237                iter_inputs.push(match m {
238                    InputMapping::State => hidden_state.pop().unwrap(),
239                    InputMapping::Scan(info) => {
240                        let input = &inputs[slot];
241                        // A chunk covering the whole axis slices to the input
242                        // itself, forward or backward.
243                        if i == 0 && input.shape()[info.axis] == info.chunk.unsigned_abs() {
244                            input.clone()
245                        } else {
246                            Self::slice_input(input, info.axis, i, info.chunk)?.into_tvalue()
247                        }
248                    }
249                    InputMapping::Full => inputs[slot].clone(),
250                });
251            }
252            trace!("iter_inputs #{i}: {iter_inputs:?}");
253
254            // Lighter equivalent of `model_state.run(iter_inputs)`: resolve body
255            // symbols only on the first iteration, and reset between iters without
256            // discarding them.
257            model_state.set_inputs_drain(&mut iter_inputs).context("Setting body inputs")?;
258            if !symbols_resolved {
259                model_state.resolve_symbols_with_states()?;
260                symbols_resolved = true;
261            }
262            model_state.exec().with_context(|| "Evaluating inner body")?;
263            let iter_outputs = model_state.outputs()?;
264            model_state.reset_turn_keep_symbols();
265            trace!("iter_outputs #{i}: {iter_outputs:?}");
266
267            for (ix, (v, mapping)) in iter_outputs.into_iter().zip(&op.output_mapping).enumerate() {
268                if let Some((slot, info)) = mapping.scan {
269                    if single_shot[ix] && !mapping.state && mapping.last_value_slot.is_none() {
270                        outputs[slot] = v.into_tensor();
271                        continue;
272                    } else if single_shot[ix] {
273                        outputs[slot] = v.clone().into_tensor();
274                    } else {
275                        Self::assign_output(&mut outputs[slot], info.axis, &v, i, info.chunk < 0);
276                    }
277                }
278                if i == iters - 1
279                    && let Some(slot) = mapping.last_value_slot
280                {
281                    outputs[slot] = v.clone().into_tensor();
282                }
283                if mapping.state {
284                    hidden_state.push(v);
285                }
286            }
287        }
288
289        Ok(outputs.into_iter().map(|t| t.into_tvalue()).collect())
290    }
291
292    fn reset_lanes(&mut self, _lanes: &[LaneId]) -> TractResult<()> {
293        bail!("Scan is not lane-aware: its body is a nested state")
294    }
295}
296
297impl TypedOp for OptScan {
298    as_op!();
299
300    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
301        let mut outputs = tvec!();
302        let iters = super::iteration_count(&self.input_mapping, inputs).unwrap();
303        for (ix, output) in self.output_mapping.iter().enumerate() {
304            let fact = self.plan.model().output_fact(ix)?;
305            if let Some(slot) = output.last_value_slot {
306                outputs.push((slot, fact.datum_type.fact(fact.shape.clone())));
307            }
308            if let Some((slot, info)) = output.scan {
309                let mut shape = fact.shape.clone();
310                let scanning_dim =
311                    output.full_dim_hint.clone().unwrap_or(shape[info.axis].clone() * &iters);
312                shape.set(info.axis, scanning_dim);
313                outputs.push((slot, fact.datum_type.fact(shape)));
314            }
315        }
316        outputs.sort_by_key(|a| a.0);
317        let outputs: TVec<_> = outputs.into_iter().map(|(_slot, v)| v).collect();
318        Ok(outputs)
319    }
320
321    fn nested_model_multipliers(&self, inputs: &[&TypedFact]) -> Vec<(StaticName, TDim)> {
322        vec![(
323            "loop".into(),
324            super::iteration_count(&self.input_mapping, inputs).unwrap_or_else(|| 1.to_dim()),
325        )]
326    }
327}