Skip to main content

tract_core/
plan.rs

1use std::borrow::Borrow;
2use std::fmt::{Debug, Display};
3
4use multithread::Executor;
5
6use crate::internal::*;
7use crate::model::{Fact, Graph, OutletId};
8use crate::ops::konst::Const;
9use crate::runtime::RunOptions;
10
11use self::order::{build_flush_list, eval_order_for_nodes, eval_order_opt_ram_for_nodes};
12
13/// Identifies one running state, so an op can key resources it manages itself
14/// per session and per node. Unique for the process; a state gets one at
15/// construction and keeps it until it is dropped, at which point the plan calls
16/// [`EvalOp::drop_session`] on every node.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct SessionId(u64);
19
20static NEXT_SESSION_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21
22impl SessionId {
23    /// For evaluating an op outside any plan -- const folding, shape inference.
24    /// No state is ever built against it, so nothing keys scratch on it.
25    pub const NONE: SessionId = SessionId(u64::MAX);
26
27    fn next() -> SessionId {
28        SessionId(NEXT_SESSION_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
29    }
30}
31
32/// Where one stream's state lives inside a state shared by several streams. An
33/// index into the lane axis the state's per-lane buffers are sized at.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub struct LaneId(pub usize);
36
37/// Which lane each seat of a turn's batch carries: the index is the seat, the
38/// value is the lane. `max_lanes` is the extent of the lane axis, fixed for the
39/// life of the state, so an op sizing a buffer on first eval knows how wide to
40/// make it -- a turn seating one lane of many still needs the full width.
41///
42/// A lane appears at most once, which is what makes a stream's state sequential:
43/// two seats of one turn cannot both advance the same lane. A model with no
44/// session-scoped state has nothing to address, so its seating is inert.
45///
46/// Seats and lanes both index axis 0 -- of the turn's tensors and of a laned
47/// state's buffers respectively. A runtime seating more than one lane is what
48/// must have checked that axis 0 of every stateful node is the model's batch
49/// axis; ops only assert that their input carries one stream per seat.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Seating {
52    lanes: Vec<LaneId>,
53    max_lanes: usize,
54}
55
56impl Seating {
57    pub fn new(max_lanes: usize, lanes: impl IntoIterator<Item = LaneId>) -> TractResult<Seating> {
58        let lanes: Vec<LaneId> = lanes.into_iter().collect();
59        for (seat, lane) in lanes.iter().enumerate() {
60            ensure!(lane.0 < max_lanes, "Seat {seat} takes lane {} of {max_lanes}", lane.0);
61            ensure!(!lanes[..seat].contains(lane), "Lane {} takes two seats in one turn", lane.0);
62        }
63        Ok(Seating { lanes, max_lanes })
64    }
65
66    /// The seating of a state one stream owns: one lane wide, that lane seated.
67    /// Every turn has a seating, and this is what an unbatched turn is.
68    pub fn single() -> Seating {
69        Seating { lanes: vec![LaneId(0)], max_lanes: 1 }
70    }
71
72    pub fn max_lanes(&self) -> usize {
73        self.max_lanes
74    }
75
76    pub fn lanes(&self) -> &[LaneId] {
77        &self.lanes
78    }
79
80    /// Seats filled this turn, i.e. the extent of the batch axis of the tensors
81    /// flowing through it.
82    pub fn occupancy(&self) -> usize {
83        self.lanes.len()
84    }
85
86    /// Where seat `ix` reads its stream and writes its state: the seat on axis 0
87    /// of the turn's tensors, the lane on axis 0 of the state's buffers. Both are
88    /// absent when the state is one lane wide, as its buffers then have no lane
89    /// axis and axis 0 of the tensors carries data rather than streams.
90    pub fn address(&self, ix: usize) -> (Option<usize>, Option<usize>) {
91        if self.max_lanes == 1 { (None, None) } else { (Some(ix), Some(self.lanes[ix].0)) }
92    }
93}
94
95/// Resources a [`TurnStateHandler`] installs for the turn and ops read while
96/// evaluating, keyed by the stored type. This is the extension point: ops see it
97/// read-only, only a handler mutates it.
98pub type TurnShared = anymap3::Map<dyn std::any::Any + Send>;
99
100/// Everything an op is given about where and when it is being evaluated. Ops
101/// receive it by shared reference: they can read the turn's symbols and the
102/// installed shared resources, and they can identify themselves with
103/// `(session, node_id)`, but they cannot reach another node's values or rebind a
104/// symbol.
105#[derive(Debug, Clone, Copy)]
106pub struct EvalContext<'a> {
107    pub session: SessionId,
108    pub node_id: usize,
109    pub symbols: &'a SymbolValues,
110    pub scenario: Option<usize>,
111    pub shared: Option<&'a TurnShared>,
112    /// Which lane each seat of this turn carries. A turn one stream owns is
113    /// [`Seating::single`], so an op reads the same field either way.
114    pub seating: &'a Seating,
115}
116
117pub struct TurnState {
118    pub resolved_symbols: SymbolValues,
119    pub scenario: Option<usize>,
120    pub values: Vec<Option<TVec<TValue>>>,
121    /// Resources installed for the turn by a [`TurnStateHandler`], reachable by
122    /// ops through [`EvalContext::shared`]. Entries outlive the turn --
123    /// `reset_turn` does not touch them -- so what a handler installs is reused
124    /// turn after turn unless it drops it in `after_plan_eval`.
125    pub shared: TurnShared,
126    /// Which lane each seat of this turn carries. A laned runtime sets it before
127    /// each turn; a state one stream owns keeps [`Seating::single`].
128    pub seating: Seating,
129}
130
131impl EvalContext<'static> {
132    /// Context for evaluating outside any plan -- const folding and shape
133    /// inference. What [`EvalOp::eval_out_of_plan`] hands the op: no symbol is
134    /// bound and no shared resource is reachable.
135    pub fn out_of_plan() -> EvalContext<'static> {
136        static SYMBOLS: std::sync::OnceLock<SymbolValues> = std::sync::OnceLock::new();
137        static SEATING: std::sync::OnceLock<Seating> = std::sync::OnceLock::new();
138        EvalContext {
139            session: SessionId::NONE,
140            node_id: usize::MAX,
141            symbols: SYMBOLS.get_or_init(SymbolValues::default),
142            scenario: None,
143            shared: None,
144            seating: SEATING.get_or_init(Seating::single),
145        }
146    }
147}
148
149impl TurnState {
150    pub fn context(&self, session: SessionId, node_id: usize) -> EvalContext<'_> {
151        EvalContext {
152            session,
153            node_id,
154            symbols: &self.resolved_symbols,
155            scenario: self.scenario,
156            shared: Some(&self.shared),
157            seating: &self.seating,
158        }
159    }
160}
161
162impl Default for TurnState {
163    fn default() -> Self {
164        TurnState {
165            resolved_symbols: SymbolValues::default(),
166            scenario: None,
167            values: vec![],
168            shared: TurnShared::new(),
169            seating: Seating::single(),
170        }
171    }
172}
173
174impl Clone for TurnState {
175    fn clone(&self) -> Self {
176        TurnState {
177            resolved_symbols: self.resolved_symbols.clone(),
178            scenario: self.scenario,
179            values: vec![],
180            shared: TurnShared::new(),
181            seating: self.seating.clone(),
182        }
183    }
184}
185
186pub trait TurnStateHandler: Send + Sync + Debug {
187    fn before_plan_eval(&self, turn: &mut TurnState) -> TractResult<()>;
188    fn after_plan_eval(&self, turn: &mut TurnState) -> TractResult<()>;
189}
190
191impl Debug for TurnState {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        write!(f, "TurnState({:?})", self.resolved_symbols)
194    }
195}
196
197#[derive(Debug, Clone)]
198pub struct SimplePlan<F, O>
199where
200    F: Fact + Clone + 'static,
201    O: Debug + Display + AsRef<dyn Op> + AsMut<dyn Op> + Clone + 'static,
202{
203    pub(crate) model: Arc<Graph<F, O>>,
204    outputs: Vec<OutletId>,
205    order: Vec<usize>,
206    flush_lists: Vec<TVec<usize>>,
207    has_unresolved_symbols: bool,
208    symbols: Vec<Symbol>,
209    executor: Option<Executor>,
210    turn_handler: Option<Arc<dyn TurnStateHandler + 'static>>,
211    /// The `Const` nodes and their values, seeded into the value table of every fresh
212    /// session's first turn.
213    const_values: Vec<(usize, TValue)>,
214}
215
216impl<F, O> SimplePlan<F, O>
217where
218    F: Fact + Clone + 'static,
219    O: Debug + Display + AsRef<dyn Op> + AsMut<dyn Op> + Clone + 'static,
220{
221    /// This contructor returns a plan that will compute all the model default outputs in one pass.
222    pub fn new(model: impl Into<Arc<Graph<F, O>>>) -> TractResult<Arc<SimplePlan<F, O>>> {
223        let model = model.into();
224        Self::build(model, &RunOptions::default()).map(Arc::new)
225    }
226
227    /// This contructor returns a plan that will compute all the model default outputs in one pass.
228    pub fn new_with_options(
229        model: impl Into<Arc<Graph<F, O>>>,
230        options: &RunOptions,
231    ) -> TractResult<Arc<SimplePlan<F, O>>> {
232        let model = model.into();
233        Self::build(model, options).map(Arc::new)
234    }
235
236    /// This contructor returns a plan that will compute the specified output.
237    #[deprecated]
238    pub fn new_for_output(
239        model: Graph<F, O>,
240        output: OutletId,
241    ) -> TractResult<Arc<SimplePlan<F, O>>> {
242        #[allow(deprecated)]
243        Self::build_with_outputs_and_deps(model, &[output], &[], &RunOptions::default())
244            .map(Arc::new)
245    }
246
247    /// This contructor returns a plan that will compute all specified outputs in one pass.
248    #[deprecated]
249    pub fn new_for_outputs(
250        model: impl Into<Arc<Graph<F, O>>>,
251        outputs: &[OutletId],
252    ) -> TractResult<Arc<SimplePlan<F, O>>> {
253        #[allow(deprecated)]
254        Self::build_with_outputs_and_deps(model, outputs, &[], &RunOptions::default()).map(Arc::new)
255    }
256
257    pub fn with_turn_handler<H: TurnStateHandler + 'static>(mut self, turn_handler: H) -> Self {
258        self.turn_handler = Some(Arc::new(turn_handler));
259        self
260    }
261
262    #[deprecated]
263    pub fn new_for_outputs_and_deps(
264        model: impl Into<Arc<Graph<F, O>>>,
265        outputs: &[OutletId],
266        deps: &[(usize, usize)],
267    ) -> TractResult<Arc<SimplePlan<F, O>>> {
268        #[allow(deprecated)]
269        Self::build_with_outputs_and_deps(model, outputs, deps, &RunOptions::default())
270            .map(Arc::new)
271    }
272
273    pub fn build(
274        model: impl Into<Arc<Graph<F, O>>>,
275        options: &RunOptions,
276    ) -> TractResult<SimplePlan<F, O>> {
277        let model = model.into();
278        let outputs = model.outputs.clone();
279        #[allow(deprecated)]
280        Self::build_with_outputs_and_deps(model, &outputs, &[], options)
281    }
282
283    /// Ordering dependencies derived from runtime symbols: symbols that only
284    /// get a value when their defining node produces a tensor (a dynamic
285    /// Range length, a NonZero count...), bound by the shape-feedback loop as
286    /// that node evaluates. Any other node mentioning such a symbol, in a
287    /// shape or in symbolic tensor values, must come after it. Tensor edges do
288    /// not express the relation, so the deps are derived from the facts.
289    #[allow(clippy::mutable_key_type)]
290    fn symbol_deps(
291        nodes: &[Node<F, O>],
292        inputs: &[usize],
293        outputs: &[usize],
294        deps: &[(usize, usize)],
295    ) -> TractResult<Vec<(usize, usize)>> {
296        fn tensor_value_symbols(t: &Tensor, acc: &mut std::collections::HashSet<Symbol>) {
297            if t.datum_type() == TDim::datum_type()
298                && let Some(slice) =
299                    t.try_as_plain_ram().ok().and_then(|v| v.as_slice::<TDim>().ok())
300            {
301                acc.extend(slice.iter().flat_map(|d| d.symbols()));
302            }
303        }
304        // A plain topological order on tensor edges: definer selection goes by
305        // dataflow, not node id (which optimization passes do not preserve),
306        // and dead nodes left behind by patches must not contribute.
307        let base_order = eval_order_for_nodes(nodes, inputs, outputs, &[])?;
308        // Inert on InferenceModel: the hir wrappers do not override
309        // mints_runtime_symbols, so the definer map stays empty.
310        let mut definer: std::collections::HashMap<Symbol, usize> = Default::default();
311        // Model inputs claim their symbols first: they may sit late in the
312        // dataflow order (an output-only definer, say), and letting a marked
313        // downstream op claim such a symbol instead would derive deps that
314        // order the input after that op.
315        for &id in inputs {
316            for output in &nodes[id].outputs {
317                if let Ok(fact) = output.fact.to_typed_fact() {
318                    for sym in fact.shape.iter().flat_map(|d| d.symbols()) {
319                        definer.entry(sym).or_insert(id);
320                    }
321                }
322            }
323        }
324        for &id in &base_order {
325            let node = &nodes[id];
326            if !node.op.as_ref().mints_runtime_symbols() {
327                continue;
328            }
329            for output in &node.outputs {
330                if let Ok(fact) = output.fact.to_typed_fact() {
331                    for sym in fact.shape.iter().flat_map(|d| d.symbols()) {
332                        definer.entry(sym).or_insert(id);
333                    }
334                }
335            }
336        }
337        let mut derived = vec![];
338        let mut seen: std::collections::HashSet<(usize, usize)> = deps.iter().copied().collect();
339        // Ancestor sets, memoized per definer: many consumers share one.
340        let mut definer_ancestors: std::collections::HashMap<
341            usize,
342            std::collections::HashSet<usize>,
343        > = Default::default();
344        for &id in &base_order {
345            let node = &nodes[id];
346            // The symbol can be needed by the node's own output shape, appear
347            // in symbolic tensor values flowing through it (a folded Shape
348            // materializes the dims as a TDim konst), or in uniform_tdim
349            // (optimization-time metadata today; scanned to keep the consumer
350            // set honest — exotic_fact is opaque).
351            let mut mentioned: std::collections::HashSet<Symbol> = Default::default();
352            for output in &node.outputs {
353                if let Ok(fact) = output.fact.to_typed_fact() {
354                    mentioned.extend(fact.shape.iter().flat_map(|d| d.symbols()));
355                    for t in [fact.konst.as_deref(), fact.uniform.as_deref()].into_iter().flatten()
356                    {
357                        tensor_value_symbols(t, &mut mentioned);
358                    }
359                    mentioned.extend(fact.uniform_tdim.iter().flat_map(TDim::symbols));
360                }
361            }
362            let input_facts: Vec<Cow<'_, TypedFact>> = node
363                .inputs
364                .iter()
365                .filter_map(|i| nodes[i.node].outputs[i.slot].fact.to_typed_fact().ok())
366                .collect();
367            let input_facts: Vec<&TypedFact> = input_facts.iter().map(|f| f.as_ref()).collect();
368            mentioned.extend(node.op.as_ref().required_bound_symbols(&input_facts));
369            // Deterministic iteration, so the deps and the plan order do not
370            // depend on HashSet randomization across processes.
371            let mut mentioned: Vec<_> = mentioned.into_iter().collect();
372            mentioned.sort_unstable();
373            for sym in mentioned {
374                let Some(&d) = definer.get(&sym) else { continue };
375                // symbols defined by model inputs are bound by set_input
376                if d == id || inputs.contains(&d) || !seen.insert((id, d)) {
377                    continue;
378                }
379                // Skip pairs already related by tensor edges: a direct input
380                // edge means the order is already right, and a consumer that
381                // is a transitive ancestor of its definer would close a cycle.
382                if node.inputs.iter().any(|i| i.node == d) {
383                    continue;
384                }
385                let ancestors = definer_ancestors.entry(d).or_insert_with(|| {
386                    let mut stack: TVec<usize> = nodes[d].inputs.iter().map(|i| i.node).collect();
387                    let mut set: std::collections::HashSet<usize> = Default::default();
388                    while let Some(n) = stack.pop() {
389                        if set.insert(n) {
390                            stack.extend(nodes[n].inputs.iter().map(|i| i.node));
391                        }
392                    }
393                    set
394                });
395                if !ancestors.contains(&id) {
396                    // deps pairs are (consumer, precursor)
397                    derived.push((id, d));
398                }
399            }
400        }
401        Ok(derived)
402    }
403
404    #[deprecated]
405    pub fn build_with_outputs_and_deps(
406        model: impl Into<Arc<Graph<F, O>>>,
407        outputs: &[OutletId],
408        deps: &[(usize, usize)],
409        options: &RunOptions,
410    ) -> TractResult<SimplePlan<F, O>> {
411        let model = model.into();
412        let inputs = model.input_outlets()?.iter().map(|n| n.node).collect::<Vec<usize>>();
413        let outputs_nodes = outputs.iter().map(|n| n.node).collect::<Vec<usize>>();
414        let mut all_deps = deps.to_vec();
415        all_deps.extend(Self::symbol_deps(model.nodes(), &inputs, &outputs_nodes, deps)?);
416        let mut order = if options.skip_order_opt_ram {
417            eval_order_for_nodes(model.nodes(), &inputs, &outputs_nodes, &all_deps)?
418        } else {
419            eval_order_opt_ram_for_nodes(model.nodes(), &inputs, &outputs_nodes, &all_deps)?
420        };
421        order.retain(|node| !model.node(*node).op_is::<Const>());
422        let flush_lists = build_flush_list(&*model, &order, outputs, |n| !n.op_is::<Const>());
423
424        #[allow(clippy::mutable_key_type)]
425        let mut symbols: std::collections::HashSet<Symbol> = Default::default();
426        for node in &model.nodes {
427            for output in &node.outputs {
428                if let Ok(fact) = output.fact.to_typed_fact() {
429                    symbols.extend(fact.shape.iter().flat_map(|d| d.symbols()))
430                }
431            }
432        }
433        let const_values = model
434            .nodes
435            .iter()
436            .filter_map(|n| n.op_as::<Const>().map(|k| (n.id, k.val().clone().into_tvalue())))
437            .collect();
438        Ok(SimplePlan {
439            model,
440            order,
441            flush_lists,
442            outputs: outputs.to_vec(),
443            has_unresolved_symbols: !symbols.is_empty(),
444            symbols: symbols.into_iter().collect(),
445            executor: options.executor.clone(),
446            turn_handler: None,
447            const_values,
448        })
449    }
450
451    pub fn order_without_consts(&self) -> &[usize] {
452        &self.order
453    }
454
455    pub fn run(self: &Arc<Self>, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
456        let mut state = self.spawn()?;
457        state.run(inputs)
458    }
459
460    pub fn model(&self) -> &Graph<F, O> {
461        self.model.borrow()
462    }
463
464    pub fn spawn(self: &Arc<Self>) -> TractResult<SimpleState<F, O>> {
465        SimpleState::new(self)
466    }
467}
468
469#[derive(Debug)]
470pub struct SimpleState<F, O>
471where
472    F: Fact + Clone + 'static,
473    O: Debug + Display + AsRef<dyn Op> + AsMut<dyn Op> + Clone + 'static,
474{
475    pub(crate) plan: Arc<SimplePlan<F, O>>,
476    pub op_states: Vec<Option<Box<dyn OpState>>>,
477    pub turn_state: TurnState,
478    session: SessionId,
479}
480
481/// A clone is a distinct session: it gets its own [`SessionId`], so whatever the
482/// ops key on `(session, node_id)` stays separate from the original's.
483impl<F, O> Clone for SimpleState<F, O>
484where
485    F: Fact + Clone + 'static,
486    O: Debug + Display + AsRef<dyn Op> + AsMut<dyn Op> + Clone + 'static,
487{
488    fn clone(&self) -> Self {
489        SimpleState {
490            plan: self.plan.clone(),
491            op_states: self.op_states.clone(),
492            turn_state: self.turn_state.clone(),
493            session: SessionId::next(),
494        }
495    }
496}
497
498impl<F, O> Drop for SimpleState<F, O>
499where
500    F: Fact + Clone + 'static,
501    O: Debug + Display + AsRef<dyn Op> + AsMut<dyn Op> + Clone + 'static,
502{
503    fn drop(&mut self) {
504        for (ix, node) in self.plan.model.nodes.iter().enumerate() {
505            node.op().drop_session(self.session, ix);
506        }
507    }
508}
509
510impl<F, O> SimpleState<F, O>
511where
512    F: Fact + Clone + 'static,
513    O: Debug + Display + AsRef<dyn Op> + AsMut<dyn Op> + Clone + 'static,
514{
515    pub fn new(plan: &Arc<SimplePlan<F, O>>) -> TractResult<SimpleState<F, O>> {
516        let plan = Arc::clone(plan);
517        let turn = TurnState::default();
518        let model = plan.model();
519        let states: Vec<Option<Box<dyn OpState>>> = vec![None; model.nodes.len()];
520        let mut state =
521            SimpleState { plan, op_states: states, turn_state: turn, session: SessionId::next() };
522        state.reset_op_states()?;
523        Ok(state)
524    }
525
526    pub fn new_from_inputs(
527        plan: &Arc<SimplePlan<F, O>>,
528        inputs: TVec<TValue>,
529    ) -> TractResult<SimpleState<F, O>> {
530        let mut state = SimpleState::new(plan)?;
531        state.set_inputs(inputs)?;
532        state.resolve_symbols_with_states()?;
533
534        Ok(state)
535    }
536
537    fn ready_turn(&mut self) {
538        if self.turn_state.values.len() == 0 {
539            self.turn_state.values = vec![None; self.plan.model.nodes().len()];
540            for (id, value) in &self.plan.const_values {
541                self.turn_state.values[*id] = Some(tvec!(value.clone()));
542            }
543        }
544    }
545    /// Reset wires state.
546    pub fn reset_turn(&mut self) -> TractResult<()> {
547        self.reset_turn_keep_symbols();
548        self.turn_state.resolved_symbols = SymbolValues::default();
549        Ok(())
550    }
551
552    /// Like [`reset_turn`] but keeps the resolved symbols (and scenario). Used by
553    /// `Scan`/`Loop` bodies, whose shapes are constant across iterations: it lets
554    /// the body resolve its symbols once and skip the per-iteration re-resolution
555    /// the full `reset_turn` + `run` cycle would otherwise force.
556    pub(crate) fn reset_turn_keep_symbols(&mut self) {
557        for node in &self.plan.order {
558            self.turn_state.values[*node] = None;
559        }
560    }
561
562    /// Clear resolved symbols (and scenario) without touching node values. Used at
563    /// the start of a fresh `Scan` evaluation, since the body state persists across
564    /// outer calls and a previous call may have left stale symbol resolutions.
565    pub(crate) fn clear_resolved_symbols(&mut self) {
566        self.turn_state.resolved_symbols = SymbolValues::default();
567        self.turn_state.scenario = None;
568    }
569
570    /// Seat the lanes carrying the coming turn's streams, one lane per seat of
571    /// axis 0 of its tensors.
572    pub fn seat(&mut self, seating: Seating) {
573        self.turn_state.seating = seating;
574    }
575
576    /// Drop the session state `lanes` hold, handing them to new streams.
577    pub fn reset_lanes(&mut self, lanes: &[LaneId]) -> TractResult<()> {
578        for op_state in self.op_states.iter_mut().flatten() {
579            op_state.reset_lanes(lanes)?;
580        }
581        Ok(())
582    }
583
584    /// Reset op inner state.
585    fn reset_op_states(&mut self) -> TractResult<()> {
586        let &mut SimpleState {
587            ref plan, ref turn_state, op_states: ref mut states, session, ..
588        } = self;
589        for (ix, n) in plan.model.nodes.iter().enumerate() {
590            states[ix] = n.op().state(&turn_state.context(session, ix))?;
591        }
592        Ok(())
593    }
594
595    pub(crate) fn resolve_symbols_with_states(&mut self) -> TractResult<()> {
596        for state in self
597            .op_states
598            .iter_mut()
599            .filter_map(Option::as_mut)
600            .filter(|s| s.has_init_tensor_fact())
601        {
602            state.resolve_symbols(&mut self.turn_state)?;
603        }
604        Ok(())
605    }
606
607    pub fn run(&mut self, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
608        self.run_plan_with_eval(inputs, self::eval)
609    }
610
611    pub fn exec(&mut self) -> TractResult<()> {
612        self.exec_plan_with_eval(self::eval)
613    }
614
615    pub fn run_plan_with_eval<Eval, E>(
616        &mut self,
617        inputs: TVec<TValue>,
618        eval: Eval,
619    ) -> TractResult<TVec<TValue>>
620    where
621        Eval: for<'a, 'b, 'c> FnMut(
622            &'a EvalContext<'a>,
623            Option<&'b mut (dyn OpState + 'static)>,
624            &'c Node<F, O>,
625            TVec<TValue>,
626        ) -> Result<TVec<TValue>, E>,
627        E: Into<anyhow::Error> + Send + Sync + 'static,
628    {
629        self.set_inputs(inputs)?;
630        self.resolve_symbols_with_states()?;
631        self.exec_plan_with_eval(eval)?;
632        let outputs = self.outputs()?;
633        self.reset_turn()?;
634        Ok(outputs)
635    }
636
637    pub fn exec_plan_with_eval<Eval, E>(&mut self, eval: Eval) -> TractResult<()>
638    where
639        Eval: for<'a, 'b, 'c> FnMut(
640            &'a EvalContext<'a>,
641            Option<&'b mut (dyn OpState + 'static)>,
642            &'c Node<F, O>,
643            TVec<TValue>,
644        ) -> Result<TVec<TValue>, E>,
645        E: Into<anyhow::Error> + Send + Sync + 'static,
646    {
647        if let Some(executor) = self.plan().executor.as_ref() {
648            tract_linalg::multithread::multithread_tract_scope(executor.clone(), || {
649                self.do_exec_plan_with_eval(eval)
650            })
651        } else {
652            self.do_exec_plan_with_eval(eval)
653        }
654    }
655
656    fn do_exec_plan_with_eval<Eval, E>(&mut self, mut eval: Eval) -> TractResult<()>
657    where
658        Eval: for<'a, 'b, 'c> FnMut(
659            &'a EvalContext<'a>,
660            Option<&'b mut (dyn OpState + 'static)>,
661            &'c Node<F, O>,
662            TVec<TValue>,
663        ) -> Result<TVec<TValue>, E>,
664        E: Into<anyhow::Error> + Send + Sync + 'static,
665    {
666        {
667            self.ready_turn();
668            self.plan
669                .turn_handler
670                .as_ref()
671                .map(|it| it.before_plan_eval(&mut self.turn_state))
672                .transpose()?;
673
674            let mut syms_done = !self.plan.has_unresolved_symbols
675                || self
676                    .plan
677                    .symbols
678                    .iter()
679                    .all(|s| self.turn_state.resolved_symbols.get(s).is_some());
680
681            for (step, n) in self.plan.order.iter().enumerate() {
682                let node = self.plan.model.node(*n);
683                trace!("Running step {step}, node {node}");
684                let mut inputs: TVec<TValue> = tvec![];
685                for i in &node.inputs {
686                    trace!("  use input {i:?}");
687                    let prec_node = self.plan.model.node(i.node);
688                    let prec = self.turn_state.values[i.node].as_ref().ok_or_else(|| {
689                        format_err!("Computing {}, precursor {} not done:", node, prec_node)
690                    })?;
691                    inputs.push(prec[i.slot].clone())
692                }
693                for flush in &self.plan.flush_lists[step] {
694                    trace!("  Ran {} can now flush {}", node, self.plan.model.node(*flush));
695                    self.turn_state.values[*flush] = None;
696                }
697
698                if cfg!(debug_assertions) {
699                    let facts = self.plan.model.node_input_facts(node.id)?;
700                    if facts.len() != inputs.len() {
701                        bail!(
702                            "Evaluating {}: expected {} inputs, got {}",
703                            node,
704                            facts.len(),
705                            inputs.len()
706                        );
707                    }
708                    for (ix, (v, f)) in inputs.iter().zip(facts.iter()).enumerate() {
709                        if !f.matches(v, Some(&self.turn_state.resolved_symbols))? {
710                            bail!(
711                                "Evaluating {}: input {:?}, expected {:?}, got {:?}",
712                                node,
713                                ix,
714                                f,
715                                v
716                            );
717                        }
718                    }
719                }
720
721                // A node with no precursors whose value is already set is a model
722                // input: `set_inputs` wrote it and `reset_turn` clears everything
723                // else, so hand it in rather than have the op reach for it. After
724                // the checks above, which are stated against the node's declared
725                // inputs -- a source declares none.
726                if node.inputs.is_empty()
727                    && let Some(preset) = self.turn_state.values[*n].as_ref()
728                {
729                    inputs = preset.clone();
730                }
731
732                let ctx = self.turn_state.context(self.session, node.id);
733                let vs = eval(&ctx, self.op_states[node.id].as_deref_mut(), node, inputs)
734                    .map_err(|e| e.into())?;
735
736                if !syms_done && self.plan.has_unresolved_symbols {
737                    for (o, v) in node.outputs.iter().zip(vs.iter()) {
738                        if let Ok(f) = o.fact.to_typed_fact() {
739                            for (dim_abstract, dim_concrete) in f.shape.iter().zip(v.shape()) {
740                                Self::resolve(
741                                    &mut self.turn_state,
742                                    dim_abstract,
743                                    *dim_concrete as i64,
744                                )?;
745                            }
746                        }
747                    }
748                    if self
749                        .plan
750                        .symbols
751                        .iter()
752                        .all(|s| self.turn_state.resolved_symbols.get(s).is_some())
753                    {
754                        syms_done = true;
755                    }
756                }
757                if cfg!(debug_assertions) {
758                    let facts = self.plan.model.node_output_facts(node.id)?;
759                    if facts.len() != vs.len() {
760                        bail!(
761                            "Evaluating {}: expected {} outputs, got {}",
762                            node,
763                            facts.len(),
764                            vs.len()
765                        );
766                    }
767                    for (ix, (v, f)) in vs.iter().zip(facts.iter()).enumerate() {
768                        if node.outputs[ix].successors.len() == 0 {
769                            continue;
770                        }
771                        if !f.matches(v, Some(&self.turn_state.resolved_symbols))? {
772                            bail!(
773                                "Evaluating {}: output {:?}, expected {:?}, got {:?}",
774                                node,
775                                ix,
776                                f,
777                                v
778                            );
779                        }
780                    }
781                }
782
783                self.turn_state.values[node.id] = Some(vs);
784            }
785            self.plan
786                .turn_handler
787                .as_ref()
788                .map(|it| it.after_plan_eval(&mut self.turn_state))
789                .transpose()?;
790        }
791        Ok(())
792    }
793
794    pub fn set_inputs(&mut self, inputs: TVec<TValue>) -> TractResult<()> {
795        ensure!(
796            inputs.len() == self.model().inputs.len(),
797            "Wrong number of inputs for model. Expected {} got {}",
798            self.model().inputs.len(),
799            inputs.len()
800        );
801
802        for (ix, t) in inputs.into_iter().enumerate() {
803            self.set_input(ix, t)?
804        }
805        Ok(())
806    }
807
808    /// Like [`set_inputs`] but drains the caller's buffer (leaving it empty with
809    /// its capacity intact) instead of consuming it, so a repeated caller (a
810    /// `Scan` body loop) can reuse one allocation across iterations.
811    pub(crate) fn set_inputs_drain(&mut self, inputs: &mut TVec<TValue>) -> TractResult<()> {
812        ensure!(
813            inputs.len() == self.model().inputs.len(),
814            "Wrong number of inputs for model. Expected {} got {}",
815            self.model().inputs.len(),
816            inputs.len()
817        );
818        for (ix, t) in inputs.drain(..).enumerate() {
819            self.set_input(ix, t)?
820        }
821        Ok(())
822    }
823
824    fn resolve(state: &mut TurnState, expression: &TDim, provided: i64) -> TractResult<()> {
825        if let TDim::Sym(sym) = expression
826            && state.resolved_symbols.get(sym).is_none()
827        {
828            state.resolved_symbols.set(sym, provided);
829            if state.scenario.is_none() {
830                let scope = sym.scope().with_context(|| {
831                    format!(
832                        "Symbol {sym:?} points to an invalid (dead ?) SymbolScope. \
833                         Make sure to create symbols using the model-managed SymbolScope."
834                    )
835                })?;
836                state.scenario = scope.guess_scenario(&state.resolved_symbols)?;
837            }
838            return Ok(());
839        }
840        let expected = expression.eval(&state.resolved_symbols);
841        if let Some(x) = expected.as_i64()
842            && x != provided
843        {
844            bail!("Clashing resolution for expression. {expression}={x} != {provided}. ({state:?})")
845        }
846        if expected.symbols().len() == 1 {
847            let sym = expected.symbols().into_iter().next().unwrap();
848            if let Some(v) = solve_for(&sym, &expected, &provided.to_dim()) {
849                debug!("Determined symbol {sym}={v}");
850                state.resolved_symbols.set(&sym, v.to_i64().unwrap());
851            }
852            if state.scenario.is_none() {
853                let scope = sym
854                    .scope()
855                    .with_context(|| format!("Symbol {sym:?} points to an invalid (dead ?) SymbolScope. Make sure to create symbols using the model-managed SymbolScope."))?;
856                state.scenario = scope.guess_scenario(&state.resolved_symbols)?;
857            }
858        }
859        Ok(())
860    }
861
862    pub fn set_input(&mut self, input: usize, t: TValue) -> TractResult<()> {
863        let outlet: OutletId = *self
864            .model()
865            .input_outlets()?
866            .get(input)
867            .with_context(|| format!("Invalid input id for model ({input})."))?;
868        if let Ok(fact) = self.plan.model.outlet_fact(outlet)?.to_typed_fact() {
869            for (expected, provided) in fact.shape.iter().zip(t.shape()) {
870                Self::resolve(&mut self.turn_state, expected, *provided as i64)?;
871            }
872        }
873        let fact = self.plan.model.outlet_fact(outlet)?;
874        ensure!(
875            fact.matches(&t, Some(&self.turn_state.resolved_symbols))
876                .with_context(|| format!("Setting input {input}"))?,
877            "Input at index {input} has incorrect dtype or shape (got {t:?}, expected to match fact {fact:?})",
878        );
879        self.ready_turn();
880        self.turn_state.values[outlet.node] = Some(tvec!(t));
881        Ok(())
882    }
883
884    pub fn output(&self, id: usize) -> TractResult<&TValue> {
885        let outlet = self.model().output_outlets()?.get(id).with_context(|| {
886            format!(
887                "Required output {}, only have {}",
888                id,
889                self.model().output_outlets().unwrap().len()
890            )
891        })?;
892        let value: &TValue = self
893            .turn_state
894            .values
895            .get(outlet.node)
896            .context("node id for output beyond node values array")?
897            .as_ref()
898            .context("node is not an output")?
899            .get(outlet.slot)
900            .context("slot id too high")?;
901        Ok(value)
902    }
903
904    pub fn outputs(&mut self) -> TractResult<TVec<TValue>> {
905        let &mut SimpleState { ref plan, ref mut turn_state, .. } = self;
906        let mut v = tvec![];
907        for o in plan.outputs.iter() {
908            let vs = turn_state.values[o.node].as_mut().ok_or_else(|| {
909                format_err!("Outputs of {:?} are not computed", plan.model.nodes()[o.node])
910            })?;
911            v.push(vs[o.slot].clone())
912        }
913        Ok(v)
914    }
915
916    pub fn set_values(&mut self, id: usize, values: TVec<TValue>) -> TractResult<()> {
917        self.turn_state.values[id] = Some(values);
918        Ok(())
919    }
920
921    pub fn set_value(&mut self, id: usize, value: TValue) -> TractResult<()> {
922        self.set_values(id, tvec!(value))
923    }
924
925    pub fn prepare_inputs(&self, node: usize) -> TractResult<TVec<TValue>> {
926        let SimpleState { plan, turn_state, .. } = self;
927        let nodes = plan.model.nodes();
928        let node = &nodes[node];
929        let mut inputs: TVec<TValue> = tvec![];
930        for i in &node.inputs {
931            let prec_node = &nodes[i.node];
932            let prec = turn_state.values[i.node].as_ref().ok_or_else(|| {
933                format_err!("Computing {}, precursor {} not done.", node, prec_node)
934            })?;
935            inputs.push(prec[i.slot].clone())
936        }
937        if node.inputs.is_empty()
938            && let Some(preset) = turn_state.values[node.id].as_ref()
939        {
940            inputs = preset.clone();
941        }
942        Ok(inputs)
943    }
944
945    pub fn compute_one(&mut self, node: usize) -> TractResult<()> {
946        let inputs = self.prepare_inputs(node)?;
947        self.compute_one_with_inputs(node, inputs)
948    }
949
950    pub fn compute_one_with_inputs(
951        &mut self,
952        node: usize,
953        inputs: TVec<TValue>,
954    ) -> TractResult<()> {
955        let &mut SimpleState {
956            ref plan,
957            ref mut turn_state,
958            op_states: ref mut states,
959            session,
960            ..
961        } = self;
962        let nodes = plan.model.nodes();
963        let node = &nodes[node];
964        let ctx = turn_state.context(session, node.id);
965        let vs = eval(&ctx, states[node.id].as_deref_mut(), node, inputs)?;
966        turn_state.values[node.id] = Some(vs);
967        Ok(())
968    }
969
970    pub fn compute_recursively(&mut self, node: usize) -> TractResult<&[TValue]> {
971        let values = {
972            #[allow(clippy::needless_collect)] // clippy bug ?
973            let precs: Vec<usize> =
974                self.model().nodes()[node].inputs.iter().map(|i| i.node).collect();
975            for i in precs.into_iter() {
976                if self.turn_state.values[i].is_none() {
977                    let _ = self.compute_recursively(i)?;
978                }
979            }
980            let mut inputs: TVec<TValue> = tvec![];
981            {
982                let node = &self.model().nodes()[node];
983                for i in &node.inputs {
984                    inputs.push(self.turn_state.values[i.node].as_ref().unwrap()[i.slot].clone())
985                }
986                if node.inputs.is_empty()
987                    && let Some(preset) = self.turn_state.values[node.id].as_ref()
988                {
989                    inputs = preset.clone();
990                }
991            }
992            let &mut Self {
993                op_states: ref mut states,
994                turn_state: ref mut turn,
995                ref plan,
996                session,
997                ..
998            } = self;
999            let ctx = turn.context(session, node);
1000            eval(&ctx, states[node].as_deref_mut(), &plan.model().nodes[node], inputs)?
1001        };
1002        self.turn_state.values[node] = Some(values);
1003        Ok(self.turn_state.values[node].as_ref().unwrap())
1004    }
1005
1006    pub fn take_by_name(&mut self, name: &str) -> TractResult<TVec<Tensor>> {
1007        let id = self.model().node_by_name(name)?.id;
1008        Self::take(self, id)
1009    }
1010
1011    pub fn take(&mut self, id: usize) -> TractResult<TVec<Tensor>> {
1012        Ok(self.turn_state.values[id]
1013            .take()
1014            .ok_or_else(|| format_err!("Node is not computed"))?
1015            .into_iter()
1016            .map(|v| v.into_tensor())
1017            .collect())
1018    }
1019
1020    pub fn plan(&self) -> &Arc<SimplePlan<F, O>> {
1021        &self.plan
1022    }
1023
1024    pub fn model(&self) -> &Graph<F, O> {
1025        &self.plan.model
1026    }
1027}
1028
1029pub fn eval<F, O>(
1030    ctx: &EvalContext,
1031    mut state: Option<&mut (dyn OpState + 'static)>,
1032    node: &Node<F, O>,
1033    input: TVec<TValue>,
1034) -> TractResult<TVec<TValue>>
1035where
1036    F: Fact + Clone + 'static,
1037    O: Debug + Display + AsRef<dyn Op> + AsMut<dyn Op> + Clone + 'static,
1038{
1039    match state {
1040        Some(ref mut state) => state.eval(ctx, node.op(), input),
1041        None => node.op().eval(ctx, input),
1042    }
1043    .with_context(|| format!("Evaluating {node}"))
1044}
1045
1046#[cfg(test)]
1047mod test {
1048    use super::*;
1049    fn is_send<T: Send>() {}
1050    fn is_sync<T: Sync>() {}
1051
1052    #[test]
1053    fn type_model_is_sync() {
1054        is_sync::<TypedModel>();
1055    }
1056
1057    #[test]
1058    fn type_model_is_send() {
1059        is_send::<TypedModel>();
1060    }
1061
1062    #[test]
1063    fn type_plan_is_send() {
1064        is_send::<TypedSimplePlan>();
1065    }
1066
1067    #[test]
1068    fn type_plan_is_sync() {
1069        is_sync::<TypedSimplePlan>();
1070    }
1071
1072    #[test]
1073    fn type_state_is_send() {
1074        is_send::<TypedSimpleState>();
1075    }
1076
1077    // A runtime symbol (here: the length of a dynamic Range) can be consumed by
1078    // a node with no tensor edge to its definer (a MultiBroadcastTo to a
1079    // symbolic target shape). The shape-feedback loop binds the symbol when the
1080    // definer evaluates, so the plan must order consumers after definers.
1081    // Regression test for https://github.com/sonos/tract/issues/2931
1082    #[test]
1083    fn symbol_consumers_run_after_their_definer() -> TractResult<()> {
1084        use crate::ops::array::MultiBroadcastTo;
1085        use crate::ops::array::Range;
1086
1087        let mut model = TypedModel::default();
1088        let r = model.symbols.sym("r").to_dim();
1089        let limit = model.add_source("limit", i64::datum_type().scalar_fact())?;
1090        let start = model.add_const("start", tensor0(0i64))?;
1091        let step = model.add_const("step", tensor0(1i64))?;
1092        let range = model.wire_node("range", Range::new(r.clone()), &[start, limit, step])?[0];
1093        // broadcast of a zero to [r, 2]: no tensor edge to the Range
1094        let zero = model.add_const("zero", tensor0(0i64))?;
1095        let expand = model.wire_node(
1096            "expand",
1097            MultiBroadcastTo::new(ShapeFact::from_dims(tvec![r, 2usize.to_dim()])),
1098            &[zero],
1099        )?[0];
1100        // consumer first in the outputs, so a naive order evaluates it first
1101        model.select_output_outlets(&[expand, range])?;
1102
1103        let found = TypedSimplePlan::new(model)?.run(tvec!(tensor0(5i64).into_tvalue()))?;
1104        assert_eq!(*found[0], tensor2(&[[0i64, 0], [0, 0], [0, 0], [0, 0], [0, 0]]));
1105        assert_eq!(*found[1], tensor1(&[0i64, 1, 2, 3, 4]));
1106        Ok(())
1107    }
1108    // A consumer that is a tensor-edge ancestor of its definer must not get a
1109    // symbol dep: adding one would close a cycle and fail plan *construction*
1110    // with "Loop detected". Here `expand` mentions both r_a (defined by the
1111    // upstream Range) and r_b (defined by the downstream Range, whose limit is
1112    // computed *from* expand).
1113    #[test]
1114    fn symbol_dep_skipped_when_consumer_is_ancestor() -> TractResult<()> {
1115        use crate::ops::array::MultiBroadcastTo;
1116        use crate::ops::array::Range;
1117
1118        let mut model = TypedModel::default();
1119        let r_a = model.symbols.sym("ra").to_dim();
1120        let r_b = model.symbols.sym("rb").to_dim();
1121        let limit = model.add_source("limit_in", i64::datum_type().scalar_fact())?;
1122        let start = model.add_const("start", tensor0(0i64))?;
1123        let step = model.add_const("step", tensor0(1i64))?;
1124        let range_a =
1125            model.wire_node("range_a", Range::new(r_a.clone()), &[start, limit, step])?[0];
1126        let zero = model.add_const("zero", tensor0(0i64))?;
1127        let expand = model.wire_node(
1128            "expand",
1129            MultiBroadcastTo::new(ShapeFact::from_dims(tvec![r_a.clone(), r_b.clone()])),
1130            &[zero],
1131        )?[0];
1132        // r_b's definer sits downstream of expand: its limit is reduced out of
1133        // expand's output
1134        let summed = model.wire_node(
1135            "summed",
1136            crate::ops::nn::Reduce::new(tvec!(0, 1), crate::ops::nn::Reducer::Sum),
1137            &[expand],
1138        )?[0];
1139        let summed =
1140            model.wire_node("summed_rm0", crate::ops::change_axes::AxisOp::Rm(0), &[summed])?[0];
1141        let summed =
1142            model.wire_node("summed_rm1", crate::ops::change_axes::AxisOp::Rm(0), &[summed])?[0];
1143        let range_b = model.wire_node("range_b", Range::new(r_b), &[start, summed, step])?;
1144        model.select_output_outlets(&[range_b[0], range_a])?;
1145        // The graph is deliberately runtime-unsatisfiable (expand needs r_b,
1146        // whose definer consumes expand's output): the guard's job is to keep
1147        // that a clear evaluation-time error instead of a bogus build-time
1148        // "Loop detected". Delete the guard and this test fails at
1149        // SimplePlan::new.
1150        TypedSimplePlan::new(model)?;
1151        Ok(())
1152    }
1153
1154    // Symbols defined by model inputs are bound by set_input before any
1155    // evaluation, so their consumers need no ordering dep. Sanity test: it
1156    // would only fail if input binding itself broke.
1157    #[test]
1158    fn input_defined_symbol_needs_no_dep() -> TractResult<()> {
1159        use crate::ops::array::MultiBroadcastTo;
1160
1161        let mut model = TypedModel::default();
1162        let s = model.symbols.sym("s").to_dim();
1163        let len = model.add_source("len", i64::datum_type().fact([s.clone()]))?;
1164        let zero = model.add_const("zero", tensor0(0i64))?;
1165        let expand = model.wire_node(
1166            "expand",
1167            MultiBroadcastTo::new(ShapeFact::from_dims(tvec![s, 2usize.to_dim()])),
1168            &[zero],
1169        )?[0];
1170        model.select_output_outlets(&[expand, len])?;
1171        let found = TypedSimplePlan::new(model)?.run(tvec!(tensor1(&[7i64; 7]).into_tvalue()))?;
1172        assert_eq!(found[0].shape(), &[7, 2]);
1173        Ok(())
1174    }
1175
1176    // The #2931 mechanism end-to-end: a folded Shape materializes a TDim
1177    // tensor whose *values* mention a runtime symbol; a consumer of that konst
1178    // (here a Cast) has no tensor edge to the symbol's definer (the Range) and
1179    // evaluates correctly only after it. Behavior test: on this small graph
1180    // both orderings happen to be definer-first, so unlike the guard and
1181    // regression tests above it does not pin a specific dep derivation.
1182    #[test]
1183    fn symbolic_konst_values_consumer_ordered_after_definer() -> TractResult<()> {
1184        use crate::ops::array::Range;
1185        use crate::ops::cast::cast;
1186        use crate::runtime::RunOptions;
1187
1188        let mut model = TypedModel::default();
1189        let r = model.symbols.sym("rk").to_dim();
1190        let limit = model.add_source("limit_in", i64::datum_type().scalar_fact())?;
1191        let start = model.add_const("start", tensor0(0i64))?;
1192        let step = model.add_const("step", tensor0(1i64))?;
1193        let range = model.wire_node("range", Range::new(r.clone()), &[start, limit, step])?[0];
1194        let shape_konst = model.add_const("shape_konst", tensor1(&[r]))?;
1195        let casted = model.wire_node("cast_konst", cast(i64::datum_type()), &[shape_konst])?;
1196        model.select_output_outlets(&[casted[0], range])?;
1197        let options = RunOptions { skip_order_opt_ram: true, ..Default::default() };
1198        let found = TypedSimplePlan::new_with_options(model, &options)?
1199            .run(tvec!(tensor0(4i64).into_tvalue()))?;
1200        assert_eq!(*found[0], tensor1(&[4i64]));
1201        assert_eq!(*found[1], tensor1(&[0i64, 1, 2, 3]));
1202        Ok(())
1203    }
1204}
1205
1206#[cfg(test)]
1207mod symbol_deps_tests {
1208    use super::*;
1209    use crate::ops::array::MultiBroadcastTo;
1210    use crate::ops::array::Range;
1211
1212    fn symbol_deps_of(model: &TypedModel) -> Vec<(usize, usize)> {
1213        let inputs = model.input_outlets().unwrap().iter().map(|n| n.node).collect::<Vec<_>>();
1214        let outputs = model.output_outlets().unwrap().iter().map(|n| n.node).collect::<Vec<_>>();
1215        SimplePlan::<TypedFact, Box<dyn TypedOp>>::symbol_deps(
1216            model.nodes(),
1217            &inputs,
1218            &outputs,
1219            &[],
1220        )
1221        .unwrap()
1222    }
1223
1224    fn range_over(model: &mut TypedModel, sym: &str) -> TractResult<TVec<OutletId>> {
1225        let r = model.symbols.sym(sym).to_dim();
1226        let start = model.add_const("start", tensor0(0i64))?;
1227        let step = model.add_const("step", tensor0(1i64))?;
1228        let limit = model.add_source("limit", i64::datum_type().scalar_fact())?;
1229        model.wire_node("range", Range::new(r), &[start, limit, step])
1230    }
1231
1232    #[test]
1233    fn no_edge_consumer_gets_a_dep() -> TractResult<()> {
1234        let mut model = TypedModel::default();
1235        let range = range_over(&mut model, "r")?[0];
1236        let zero = model.add_const("zero", tensor0(0i64))?;
1237        let r = model.symbols.sym("r").to_dim();
1238        let expand = model.wire_node(
1239            "expand",
1240            MultiBroadcastTo::new(ShapeFact::from_dims(tvec![r, 2usize.to_dim()])),
1241            &[zero],
1242        )?[0];
1243        model.select_output_outlets(&[expand, range])?;
1244        // (consumer, precursor): expand waits for the Range
1245        assert_eq!(symbol_deps_of(&model), vec![(expand.node, range.node)]);
1246        Ok(())
1247    }
1248
1249    #[test]
1250    fn input_definers_yield_no_dep() -> TractResult<()> {
1251        let mut model = TypedModel::default();
1252        let s = model.symbols.sym("s").to_dim();
1253        let len = model.add_source("len", i64::datum_type().fact([s.clone()]))?;
1254        let zero = model.add_const("zero", tensor0(0i64))?;
1255        let expand = model.wire_node(
1256            "expand",
1257            MultiBroadcastTo::new(ShapeFact::from_dims(tvec![s, 2usize.to_dim()])),
1258            &[zero],
1259        )?[0];
1260        model.select_output_outlets(&[expand, len])?;
1261        assert!(symbol_deps_of(&model).is_empty());
1262        Ok(())
1263    }
1264
1265    #[test]
1266    fn tensor_ancestor_consumer_gets_no_dep() -> TractResult<()> {
1267        let mut model = TypedModel::default();
1268        let range = range_over(&mut model, "r")?[0];
1269        // a consumer downstream of the definer: the tensor edge already
1270        // orders them
1271        let casted =
1272            model.wire_node("downstream", crate::ops::cast::cast(f32::datum_type()), &[range])?;
1273        model.select_output_outlets(&[casted[0]])?;
1274        assert!(symbol_deps_of(&model).is_empty());
1275        Ok(())
1276    }
1277
1278    #[test]
1279    fn scan_forwards_body_requirements() -> TractResult<()> {
1280        use crate::ops::array::Range;
1281        use crate::ops::cast::cast;
1282        use crate::ops::konst::Const;
1283        use crate::ops::scan::{InputMapping, OutputMapping, Scan, ScanInfo};
1284        let mut model = TypedModel::default();
1285        let r = model.symbols.sym("b");
1286
1287        // top-level binder of r
1288        let limit = model.add_source("limit", i64::datum_type().scalar_fact())?;
1289        let limit = model.wire_node("limit_tdim", cast(TDim::datum_type()), &[limit])?;
1290        let start =
1291            model.wire_node("start", Const::new(tensor0(TDim::Val(0)).into_arc_tensor())?, &[])?;
1292        let step =
1293            model.wire_node("step", Const::new(tensor0(TDim::Val(1)).into_arc_tensor())?, &[])?;
1294        let range =
1295            model.wire_node("range", Range::new(r.to_dim()), &[start[0], limit[0], step[0]])?;
1296
1297        // scan whose body requires r: a Cast over a symbolic konst inside the
1298        // body, invisible from the top level except through the forwarding
1299        let mut body = TypedModel { symbols: model.symbols.clone(), ..TypedModel::default() };
1300        let state = body.add_source("state_in", i64::datum_type().fact([1usize]))?;
1301        let scan_in = body.add_source("scan_in", i64::datum_type().fact([1usize]))?;
1302        let konst = body.wire_node(
1303            "body_konst",
1304            Const::new(tensor0(TDim::Sym(r)).into_arc_tensor())?,
1305            &[],
1306        )?;
1307        let _casted = body.wire_node("body_cast", cast(f32::datum_type()), &[konst[0]])?;
1308        let sum = body.wire_node("sum", crate::ops::math::add(), &[state, scan_in])?;
1309        body.select_output_outlets(&[sum[0]])?;
1310        let init = model.add_const("init", tensor1(&[0i64]))?;
1311        let scanned = model.add_source("scanned", i64::datum_type().fact([3usize]))?;
1312        let scan = model.wire_node(
1313            "scan",
1314            Scan::new(
1315                body,
1316                vec![InputMapping::Scan(ScanInfo { axis: 0, chunk: 1 }), InputMapping::State],
1317                vec![OutputMapping {
1318                    scan: Some((0, ScanInfo { axis: 0, chunk: 1 })),
1319                    full_dim_hint: None,
1320                    last_value_slot: None,
1321                    state: true,
1322                }],
1323                0,
1324            )?,
1325            &[scanned, init],
1326        )?;
1327        model.select_output_outlets(&[scan[0], range[0]])?;
1328        let deps = symbol_deps_of(&model);
1329        assert!(
1330            deps.contains(&(scan[0].node, range[0].node)),
1331            "expected the scan ordered after the top-level binder, got {deps:?}"
1332        );
1333        Ok(())
1334    }
1335
1336    #[test]
1337    fn hidden_input_konst_mention_yields_dep() -> TractResult<()> {
1338        use crate::ops::array::Range;
1339        use crate::ops::cast::cast;
1340        let mut model = TypedModel::default();
1341        let limit = model.add_source("limit", i64::datum_type().scalar_fact())?;
1342        let limit = model.wire_node("limit_tdim", cast(TDim::datum_type()), &[limit])?;
1343        let start = model.add_const("start", tensor0(TDim::Val(0)))?;
1344        let step = model.add_const("step", tensor0(TDim::Val(1)))?;
1345        let r = model.symbols.sym("h");
1346        let range = model.wire_node("range", Range::new(r.to_dim()), &[start, limit[0], step])?;
1347        // a multi-element symbolic konst (like a folded Shape): uniform_tdim
1348        // only represents scalars, so this mention is invisible on the output
1349        // side — only required_bound_symbols catches it
1350        let konst = model.wire_node(
1351            "konst",
1352            crate::ops::konst::Const::new(
1353                tensor1(&[TDim::Sym(r.clone()), TDim::Sym(r)]).into_arc_tensor(),
1354            )?,
1355            &[],
1356        )?;
1357        let casted = model.wire_node("cast", cast(f32::datum_type()), &[konst[0]])?;
1358        model.select_output_outlets(&[casted[0], range[0]])?;
1359        let deps = symbol_deps_of(&model);
1360        assert!(
1361            deps.contains(&(casted[0].node, range[0].node)),
1362            "expected the cast ordered after the binder, got {deps:?}"
1363        );
1364        Ok(())
1365    }
1366
1367    #[test]
1368    fn dead_nodes_contribute_nothing() -> TractResult<()> {
1369        let mut model = TypedModel::default();
1370        let range = range_over(&mut model, "r")?[0];
1371        let zero = model.add_const("zero", tensor0(0i64))?;
1372        let r = model.symbols.sym("r").to_dim();
1373        let expand = model.wire_node(
1374            "expand",
1375            MultiBroadcastTo::new(ShapeFact::from_dims(tvec![r.clone(), 2usize.to_dim()])),
1376            &[zero],
1377        )?[0];
1378        model.select_output_outlets(&[expand, range])?;
1379        let before = symbol_deps_of(&model);
1380        // wire a dead consumer of the same symbol, not reachable from outputs
1381        model.wire_node(
1382            "dead",
1383            MultiBroadcastTo::new(ShapeFact::from_dims(tvec![r, 3usize.to_dim()])),
1384            &[zero],
1385        )?;
1386        assert_eq!(symbol_deps_of(&model), before);
1387        Ok(())
1388    }
1389}
1390
1391#[cfg(test)]
1392mod late_input_definer_tests {
1393    use super::*;
1394    use crate::ops::array::Range;
1395
1396    // An input source mentioning a symbol that a marked op (a Range with that
1397    // symbol as its length) also outputs: the input must stay the definer
1398    // even though it sits late in the dataflow order, or the derived deps
1399    // order the input after the Range and the RAM-optimized ordering trips
1400    // its revisit assertion.
1401    #[test]
1402    fn late_input_definer_wins() -> TractResult<()> {
1403        let mut model = TypedModel::default();
1404        let s = model.symbols.sym("S");
1405        let definer = model.add_source("definer", f32::datum_type().fact([s.to_dim()]))?;
1406        let start = model.add_const("start", tensor0(TDim::Val(0)))?;
1407        let step = model.add_const("step", tensor0(TDim::Val(1)))?;
1408        let end = model.add_const("end", tensor0(TDim::Sym(s.clone())))?;
1409        let range = model.wire_node("range", Range::new(s.to_dim()), &[start, end, step])?;
1410        model.select_output_outlets(&[range[0], definer])?;
1411        crate::internal::TypedSimplePlan::new(model)?;
1412        Ok(())
1413    }
1414}