Skip to main content

fuel_tensor/
planner.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Load-time planning driver.
3//!
4//! Program anchor: `docs/session-prompts/plan-is-graph-rebuild.md`
5//! (Phase D — load-time, input-independent graph build).
6//!
7//! [`Planner::warm`] is the explicit load-time entry: model loaders
8//! (and user code that builds a graph ahead of realizing it) may call
9//! it after graph construction to pre-optimize.
10//!
11//! ## Status post-PR-A3b-2
12//!
13//! The identity-keyed [`PlanStore`](fuel_dispatch) memoization layer
14//! `warm` used to populate was deleted in PR-A3b-2 (the "plan IS the
15//! graph" rebuild retires the separate `ExecutionPlan` source of truth
16//! and the per-graph plan store). With the load-time-build model not
17//! yet landed (Phase D), `warm` has nothing to memoize: the optimized
18//! form lives **in the graph**, built once per realize by
19//! `optimize_graph`, and there is no separate plan to stash and reuse.
20//!
21//! `warm` is therefore a **no-op** today, preserved as the seam Phase D
22//! will reimplement (optimize the graph in place at load so the first
23//! realize finds the multi-path form already written). The realize path
24//! remains authoritative — it runs `optimize_graph` itself.
25
26use std::sync::{Arc, RwLock};
27
28use fuel_graph::{Graph, NodeId};
29use fuel_ir::Result;
30
31use crate::Device;
32
33/// Load-time planning driver. Stateless.
34pub struct Planner;
35
36impl Planner {
37    /// Load-time pre-optimization hook for `(graph, device)`.
38    ///
39    /// **No-op as of PR-A3b-2**: the per-graph plan store this used to
40    /// populate was deleted with the legacy `compile_plan`/`PlanStore`
41    /// path. The "plan IS the graph" form is built once per realize by
42    /// `optimize_graph` (in `fuel-tensor::pipelined_bridge`), so there is
43    /// nothing to memoize ahead of the first realize until the
44    /// load-time-build model (Phase D) lands. This hook is preserved as
45    /// the seam Phase D will reimplement to optimize the graph in place
46    /// at load.
47    ///
48    /// Always `Ok(())`. Callers (e.g. the decode loop in
49    /// `fuel-tensor::lazy`) already treat `warm` as advisory and discard
50    /// its result; the realize path runs the authoritative optimization.
51    pub fn warm(_graph: &Arc<RwLock<Graph>>, _targets: &[NodeId], _device: &Device) -> Result<()> {
52        Ok(())
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use fuel_dispatch::pipelined::StorageCache;
60    use fuel_graph::{Node, Op};
61    use fuel_ir::{DType, Shape};
62    use fuel_memory::{BackendStorage, Storage};
63
64    fn push_node(g: &mut Graph, op: Op, inputs: Vec<NodeId>) -> NodeId {
65        g.push(Node {
66            op,
67            inputs,
68            shape: Shape::from_dims(&[4]),
69            dtype: DType::F32,
70        })
71    }
72
73    fn cpu_storage_f32(vals: &[f32]) -> Arc<RwLock<Storage>> {
74        let bytes: &[u8] = bytemuck::cast_slice(vals);
75        Arc::new(RwLock::new(Storage::new(
76            BackendStorage::Cpu(fuel_cpu_backend::byte_storage::CpuStorageBytes::from_bytes(
77                bytes,
78            )),
79            DType::F32,
80        )))
81    }
82
83    /// `warm` is a no-op (no store to populate post-A3b-2) but must
84    /// stay error-free, and a realize after `warm` must still produce
85    /// correct values — the realize path runs the authoritative
86    /// optimization itself.
87    #[test]
88    fn warm_is_noop_and_realize_still_correct() {
89        let device = crate::Device::cpu();
90        let graph = Arc::new(RwLock::new(Graph::new()));
91        let (c1, c2, add) = {
92            let mut g = graph.write().unwrap();
93            let c1 = push_node(&mut g, Op::Const, vec![]);
94            let c2 = push_node(&mut g, Op::Const, vec![]);
95            let add = push_node(&mut g, Op::Add, vec![c1, c2]);
96            (c1, c2, add)
97        };
98
99        Planner::warm(&graph, &[add], &device).expect("warm is a no-op Ok");
100
101        let mut initial = StorageCache::new();
102        initial.insert(c1, cpu_storage_f32(&[1.0, 2.0, 3.0, 4.0]));
103        initial.insert(c2, cpu_storage_f32(&[10.0, 20.0, 30.0, 40.0]));
104        let out = crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
105            &graph, add, &device, initial,
106        )
107        .expect("realize after warm");
108        assert_eq!(out, vec![11.0, 22.0, 33.0, 44.0]);
109    }
110
111    /// Warm on empty targets is a no-op (no error).
112    #[test]
113    fn warm_empty_targets_is_noop() {
114        let graph = Arc::new(RwLock::new(Graph::new()));
115        Planner::warm(&graph, &[], &crate::Device::cpu()).expect("no-op");
116    }
117}