Skip to main content

fuel_dispatch/
runtime_fused_pathfinder.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Runtime-fused-op arm **pathfinder** — the match-finder
3//! [`crate::runtime_fused_arm`]'s module doc names as its remaining half:
4//! "the pathfinder that finds matches + splices reconverge points registers
5//! it into `optimize_graph`." This module is that pathfinder.
6//!
7//! For every op [adopted](fuel_graph::runtime_fused::runtime_entries) at
8//! runtime (JIT-synthesized or import-time), it scans the base map for
9//! subgraphs whose shape matches the op's region
10//! ([`fuel_graph::jit::match_region`]) and offers each match as a gated
11//! `Op::Branch` via [`offer_runtime_fused_arm`] — arm 0 stays the region's
12//! existing primitive subgraph (the correctness oracle), arm 1 is the
13//! synthesized kernel, offered only when
14//! [`fused_kernel_available_in`](crate::runtime_fused_kernels::fused_kernel_available_in)
15//! gates it open (reading the `OptimizationContext`-threaded binding table,
16//! never re-acquiring the global lock). Same constitutional posture as
17//! [`crate::decode_flash`]: the
18//! optimizer emits/prunes arms, backends never decide, and a graph with no
19//! adopted runtime op (or no structural match) is left byte-identical.
20//!
21//! v1 scope mirrors `fuel_graph::runtime_fused`'s own v1 note: every offered
22//! arm is pinned to [`BackendId::Cpu`]. A runtime op can be adopted for
23//! multiple backends (`adopt_runtime_fused` is called once per backend), so
24//! picking the *right* one — and potentially offering more than one fused
25//! arm per match — is a follow-up; see the module-level "unsure" note in the
26//! session report.
27
28use std::collections::HashMap;
29
30use fuel_graph::jit::match_region_extract;
31use fuel_graph::registry::FusedOpId;
32use fuel_graph::runtime_fused::runtime_entries;
33use fuel_graph::{Graph, NodeId};
34use fuel_ir::Result;
35use fuel_ir::probe::BackendId;
36
37use crate::driver::{OptimizationContext, Pathfinder};
38use crate::kernel::KernelBindingTable;
39use crate::runtime_fused_arm::{RuntimeFusedSpec, offer_runtime_fused_arm};
40
41/// A structural match of a registered runtime op's region against the base
42/// map, ready to become a [`RuntimeFusedSpec`] once collection is done (kept
43/// separate from the spec so the collection pass stays a read-only borrow of
44/// `graph`, mirroring [`crate::driver::PlacementForkPathfinder`]'s two-phase
45/// collect-then-mutate shape).
46struct RegionMatch {
47    runtime_id: FusedOpId,
48    inputs: Vec<NodeId>,
49    /// Live `extract:` values from the matched region's open scalar slots,
50    /// pattern pre-order (stamped into the arm's `Runtime { scalars }`).
51    scalars: Vec<f64>,
52    primitive_sink: NodeId,
53    reconverge: NodeId,
54}
55
56/// Scan `graph` for subgraphs matching an adopted runtime fused op's region
57/// and offer each as a gated `Op::Branch` (see [`offer_runtime_fused_arm`]).
58/// Returns the number of arms emitted — `0` for a graph with no adopted
59/// runtime op, or none whose region structurally matches anywhere in it.
60///
61/// For each registered runtime op, every node in `graph` is tried as the
62/// region's `root` (arm 0's exit / [`fuel_graph::jit::match_region`]'s sink). A match binds
63/// the region's external inputs; the candidate is only a genuine branch
64/// point when `root` has **exactly one** consumer (the sole reconverge —
65/// zero consumers is a dead end, ≥2 is ordinary fan-out neither arm can
66/// safely replace). Matches are collected first (read-only over `graph`),
67/// then spliced (mutating), exactly like the placement-fork pathfinder.
68///
69/// `table` is the binding table each match's capability gate reads —
70/// threaded straight through to [`offer_runtime_fused_arm`] (see its doc for
71/// why this is the non-nesting form of the gate).
72pub fn emit_runtime_fused_arms(graph: &mut Graph, table: &KernelBindingTable) -> Result<usize> {
73    let entries = runtime_entries();
74    if entries.is_empty() {
75        return Ok(0);
76    }
77
78    // Consumer counts over the WHOLE graph — a runtime region can sit
79    // anywhere in the base map, not just along a particular dispatch order
80    // (mirrors `match_region`'s own tests' `consumer_counts` helper).
81    let mut consumer_count: HashMap<NodeId, usize> = HashMap::new();
82    let mut sole_consumer: HashMap<NodeId, NodeId> = HashMap::new();
83    let len = graph.len();
84    for i in 0..len {
85        let id = NodeId(i);
86        for &input in &graph.node(id).inputs {
87            *consumer_count.entry(input).or_insert(0) += 1;
88            sole_consumer.insert(input, id);
89        }
90    }
91    let consumers = |n: NodeId| consumer_count.get(&n).copied().unwrap_or(0);
92
93    // A `root` claimed by one entry's match is not offered again for another
94    // entry: two runtime ops can register structurally-identical regions
95    // (e.g. the same fusion synthesized twice under different hashes), and a
96    // second `open_branch` at an already-branched diverge point is a
97    // malformed splice (the first branch's arm-0 already reads `root`, so the
98    // second attempt trips the "arm interior read from outside the branch"
99    // guard). First registered entry to match a given `root` wins it; this is
100    // a dedup, not a priority order — a graph rarely contains a genuine
101    // ambiguity like this, and when it does, offering one arm is still
102    // correct (arm 0 stays the primitive fallback either way).
103    let mut claimed: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
104    let mut matches: Vec<RegionMatch> = Vec::new();
105    for entry in &entries {
106        for i in 0..len {
107            let root = NodeId(i);
108            if claimed.contains(&root) {
109                continue;
110            }
111            let Some((inputs, scalars)) =
112                match_region_extract(graph, root, &entry.region, &consumers)
113            else {
114                continue;
115            };
116            // Exactly one consumer of `root` ⇒ a genuine reconverge point.
117            if consumers(root) != 1 {
118                continue;
119            }
120            claimed.insert(root);
121            matches.push(RegionMatch {
122                runtime_id: entry.id,
123                inputs,
124                scalars,
125                primitive_sink: root,
126                reconverge: sole_consumer[&root],
127            });
128        }
129    }
130
131    let mut emitted = 0usize;
132    for m in matches {
133        let spec = RuntimeFusedSpec {
134            runtime_id: m.runtime_id,
135            inputs: m.inputs,
136            primitive_sink: m.primitive_sink,
137            reconverge: m.reconverge,
138            // v1: pinned to Cpu — see the module doc's "unsure" note.
139            backend: BackendId::Cpu,
140            // The live `extract:` values (pattern pre-order) — stamped into the
141            // arm's `Runtime { scalars }`, launched as the trailing `p{i}` args.
142            scalars: m.scalars,
143        };
144        if offer_runtime_fused_arm(graph, &spec, table)?.is_some() {
145            emitted += 1;
146        }
147    }
148    Ok(emitted)
149}
150
151/// The registered [`Pathfinder`] wrapper around [`emit_runtime_fused_arms`].
152/// Ignores `ctx`'s ranked plan / dispatch order: a runtime-op region match is
153/// a purely structural question over the whole base map, independent of
154/// placement — the same reason [`crate::decode_flash`]'s arm is emitted by a
155/// dedicated call site rather than reading `ctx.plan`.
156pub struct RuntimeFusedArmPathfinder;
157
158impl Pathfinder for RuntimeFusedArmPathfinder {
159    fn name(&self) -> &'static str {
160        "RuntimeFusedArm"
161    }
162
163    fn propose(&self, graph: &mut Graph, ctx: &OptimizationContext<'_>) -> Result<()> {
164        emit_runtime_fused_arms(graph, ctx.bindings())?;
165        Ok(())
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::runtime_fused_kernels::adopt_runtime_fused;
173    use fuel_graph::jit::{OpAttrs, OpTag, PatternNode};
174    use fuel_graph::{Node, Op};
175    use fuel_ir::{DType, Layout, Shape};
176    use std::sync::{Arc, RwLock as StdRwLock};
177
178    fn noop_kernel(
179        _inputs: &[Arc<StdRwLock<fuel_memory::Storage>>],
180        _outputs: &mut [Arc<StdRwLock<fuel_memory::Storage>>],
181        _layouts: &[Layout],
182        _params: &crate::kernel::OpParams,
183    ) -> fuel_ir::Result<()> {
184        Ok(())
185    }
186
187    /// tanh(mul(a, b)) as a PatternNode region. Deliberately NOT the
188    /// `relu(add(a, b))` shape every other adopted-op test in this crate
189    /// uses (`fused_cost`, `jit_adopt`, `runtime_fused_arm`,
190    /// `runtime_fused_kernels`) — `runtime_entries()` is a process-global
191    /// sidecar shared by every test in this binary, so a region matching
192    /// one of those adopted shapes would non-deterministically match
193    /// whichever of them happened to register first, defeating the
194    /// `rid`-specific assertions below (and, before the `claimed`-root dedup
195    /// this pathfinder now has, would double-branch the same diverge point
196    /// and panic).
197    fn tanh_mul_region() -> PatternNode {
198        PatternNode::Op {
199            op: OpTag::Tanh,
200            attrs: OpAttrs::default(),
201            operands: vec![PatternNode::Op {
202                op: OpTag::Mul,
203                attrs: OpAttrs::default(),
204                operands: vec![
205                    PatternNode::Bind { index: 0 },
206                    PatternNode::Bind { index: 1 },
207                ],
208            }],
209        }
210    }
211
212    /// tanh(mul(a, b)) plus a downstream `neg` consumer (the reconverge).
213    fn graph_with_region() -> (Graph, NodeId) {
214        let mut g = Graph::new();
215        let s = Shape::from_dims(&[4]);
216        let leaf = |g: &mut Graph| {
217            g.push(Node {
218                op: Op::Const,
219                inputs: vec![],
220                shape: s.clone(),
221                dtype: DType::F32,
222            })
223        };
224        let a = leaf(&mut g);
225        let b = leaf(&mut g);
226        let mul = g.push(Node {
227            op: Op::Mul,
228            inputs: vec![a, b],
229            shape: s.clone(),
230            dtype: DType::F32,
231        });
232        let tanh = g.push(Node {
233            op: Op::Tanh,
234            inputs: vec![mul],
235            shape: s.clone(),
236            dtype: DType::F32,
237        });
238        let _neg = g.push(Node {
239            op: Op::Neg,
240            inputs: vec![tanh],
241            shape: s.clone(),
242            dtype: DType::F32,
243        });
244        (g, tanh)
245    }
246
247    #[test]
248    fn emits_a_fused_arm_for_an_adopted_region_match() {
249        let rid = adopt_runtime_fused(
250            "test::pathfinder::tanh_mul",
251            tanh_mul_region(),
252            noop_kernel as crate::kernel::KernelRef,
253            vec![DType::F32, DType::F32, DType::F32],
254            BackendId::Cpu,
255        )
256        .expect("registrable region");
257
258        let (mut g, tanh) = graph_with_region();
259        let before = g.len();
260
261        // adopt_runtime_fused binds into the GLOBAL table; pass it explicitly
262        // as the threaded table this test exercises the pathfinder against.
263        let emitted = emit_runtime_fused_arms(&mut g, &crate::dispatch::global_bindings())
264            .expect("no build-time error");
265        assert_eq!(emitted, 1, "exactly one match ⇒ one arm emitted");
266        assert!(g.len() > before, "the fused node + branch were appended");
267
268        // A Branch was recorded at (or after) `tanh`'s old position, and a
269        // Fused(rid, ..) node exists among the new nodes.
270        let mut saw_branch = false;
271        let mut saw_fused = false;
272        for i in before..g.len() {
273            match &g.node(NodeId(i)).op {
274                Op::Branch { .. } => saw_branch = true,
275                Op::Fused(id, _) if *id == rid => saw_fused = true,
276                _ => {}
277            }
278        }
279        assert!(saw_branch, "an Op::Branch decision point was emitted");
280        assert!(saw_fused, "an Op::Fused(rid, ..) arm was emitted");
281        // `tanh` (arm 0 / the primitive sink) is untouched — still the
282        // region's Tanh output the correctness oracle relies on.
283        assert!(matches!(g.node(tanh).op, Op::Tanh));
284    }
285
286    #[test]
287    fn no_adopted_op_emits_nothing() {
288        // No `adopt_runtime_fused` call in this test — `runtime_entries()`
289        // may still carry ops other tests adopted (a process-global sidecar),
290        // but NONE of them match a region this graph doesn't structurally
291        // contain: a bare `neg(a)` has no relu/add/tanh/mul anywhere.
292        let mut g = Graph::new();
293        let s = Shape::from_dims(&[4]);
294        let a = g.push(Node {
295            op: Op::Const,
296            inputs: vec![],
297            shape: s.clone(),
298            dtype: DType::F32,
299        });
300        let _n = g.push(Node {
301            op: Op::Neg,
302            inputs: vec![a],
303            shape: s.clone(),
304            dtype: DType::F32,
305        });
306        let before = g.len();
307
308        // The production table (any op other tests adopted lives here too —
309        // irrelevant, since none structurally matches this graph).
310        let emitted = emit_runtime_fused_arms(&mut g, &crate::dispatch::global_bindings())
311            .expect("no build-time error");
312        assert_eq!(emitted, 0, "no structural match ⇒ no arm emitted");
313        assert_eq!(g.len(), before, "graph untouched");
314    }
315
316    /// A slot-template region (`sigmoid(add_scalar(x))`, value open): the
317    /// pathfinder extracts the LIVE scalar from the matched graph node and
318    /// stamps it into the emitted arm's `Runtime { scalars }` — the `extract:`
319    /// round-trip at the arm level. Region shape unique to this test (the
320    /// sidecar is process-global; see `tanh_mul_region`'s rationale).
321    #[test]
322    fn emitted_arm_carries_the_extracted_live_scalar() {
323        let region = PatternNode::Op {
324            op: OpTag::Sigmoid,
325            attrs: OpAttrs::default(),
326            operands: vec![PatternNode::Op {
327                op: OpTag::AddScalar,
328                attrs: OpAttrs::default(), // empty = open slot
329                operands: vec![PatternNode::Bind { index: 0 }],
330            }],
331        };
332        let rid = adopt_runtime_fused(
333            "test::pathfinder::sigmoid_add_scalar",
334            region,
335            noop_kernel as crate::kernel::KernelRef,
336            vec![DType::F32, DType::F32],
337            BackendId::Cpu,
338        )
339        .expect("slot template is registrable");
340
341        // sigmoid(add_scalar(x, 7.25)) + a downstream consumer.
342        let mut g = Graph::new();
343        let s = Shape::from_dims(&[4]);
344        let x = g.push(Node {
345            op: Op::Const,
346            inputs: vec![],
347            shape: s.clone(),
348            dtype: DType::F32,
349        });
350        let asn = g.push(Node {
351            op: Op::AddScalar(7.25),
352            inputs: vec![x],
353            shape: s.clone(),
354            dtype: DType::F32,
355        });
356        let sig = g.push(Node {
357            op: Op::Sigmoid,
358            inputs: vec![asn],
359            shape: s.clone(),
360            dtype: DType::F32,
361        });
362        let _neg = g.push(Node {
363            op: Op::Neg,
364            inputs: vec![sig],
365            shape: s.clone(),
366            dtype: DType::F32,
367        });
368        let before = g.len();
369
370        // adopt_runtime_fused binds into the GLOBAL table; pass it explicitly
371        // as the threaded table this test exercises the pathfinder against.
372        let emitted = emit_runtime_fused_arms(&mut g, &crate::dispatch::global_bindings())
373            .expect("no build-time error");
374        assert_eq!(emitted, 1, "the slot region matched once");
375
376        let mut found = false;
377        for i in before..g.len() {
378            if let Op::Fused(fid, fuel_graph::registry::FusedOpParams::Runtime { scalars }) =
379                &g.node(NodeId(i)).op
380                && *fid == rid
381            {
382                assert_eq!(scalars, &vec![7.25], "the LIVE value rode into the arm");
383                found = true;
384            }
385        }
386        assert!(found, "an Op::Fused(rid, Runtime) arm was emitted");
387    }
388}