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}