Skip to main content

fuel_dispatch/
runtime_fused_arm.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Runtime-fused-op arm emission — the gated `Op::Branch` that makes an adopted
3//! (JIT-synthesized / imported) fused op reachable, mirroring [`crate::decode_flash`].
4//!
5//! Same constitutional posture as the flash arm: **the optimizer emits/prunes arms;
6//! backends never decide.** Arm 0 is the region's **existing primitive subgraph**
7//! (the decompose / correctness oracle the graph already holds); arm 1 is the
8//! synthesized `Op::Fused(runtime_id, Runtime)` kernel, offered **only** when a
9//! kernel is bound for the target backend ([`fused_kernel_available_in`]) and pinned
10//! to that backend. `finalize_branches` guarantees the merge reads arm 0, so a
11//! finalized-but-unpicked graph — and every backend with no synthesized arm —
12//! realizes on the primitive route, byte-identical. The route picker chooses arm 1
13//! when it wins; the kernel resolves from the `FusedOpId`-keyed runtime sidecar.
14//!
15//! Unlike the flash arm (emitted at graph-build by the decode builder), a runtime
16//! fused op is synthesized *after* load, so this arm is emitted during
17//! **(re-)optimization** — a pathfinder scans the base map for regions matching an
18//! adopted op and calls this per match. This module is the emitter; the pathfinder
19//! that finds matches + splices reconverge points registers it into `optimize_graph`.
20
21use fuel_graph::registry::{FusedOpId, FusedOpParams};
22use fuel_graph::{Graph, Node, NodeId, Op};
23use fuel_ir::Result;
24use fuel_ir::probe::BackendId;
25
26use crate::kernel::KernelBindingTable;
27use crate::runtime_fused_kernels::fused_kernel_available_in;
28
29/// A matched runtime-fused region the optimizer offers as an arm candidate. Arm 0
30/// (`primitive_sink`, the region's existing output) is already in the graph; this
31/// describes how to build arm 1 (the fused kernel) and where to splice the branch.
32///
33/// `inputs[0]` is the branch **diverge** point (the primitive chain and the fused
34/// node both depart from it); the other inputs are shared external operands.
35/// `primitive_sink` is arm 0's exit (same shape/dtype as the fused output).
36/// `reconverge` is the sole consumer of `primitive_sink` (the merge).
37#[derive(Clone, Debug)]
38pub struct RuntimeFusedSpec {
39    /// The adopted runtime fused op (`id >= RUNTIME_FUSED_BASE`).
40    pub runtime_id: FusedOpId,
41    /// The region's bound external inputs, in bind-index order (from `match_region`).
42    pub inputs: Vec<NodeId>,
43    /// Arm 0's exit — the region's existing primitive-subgraph output.
44    pub primitive_sink: NodeId,
45    /// The sole consumer of `primitive_sink` (the merge / reconverge point).
46    pub reconverge: NodeId,
47    /// The backend the synthesized kernel is bound for; arm 1 is pinned to it.
48    pub backend: BackendId,
49    /// The extracted scalar args for the fused op (the `extract:` slots); empty
50    /// for a parameterless runtime op.
51    pub scalars: Vec<f64>,
52}
53
54/// Offer a runtime-fused `Op::Branch` arm for an adopted op, gated on kernel
55/// availability. Returns `Ok(Some(branch))` when the arm is emitted, `Ok(None)`
56/// when the gate declines (no kernel for `backend` → no arm; the region stays on
57/// arm 0's primitives). Never a realize-time decision — the capability gate and
58/// the backend pin happen here, in the optimizer.
59///
60/// `table` is the binding table the capability gate reads (threaded from
61/// `OptimizationContext::bindings` — see [`fused_kernel_available_in`]).
62/// Reading through this reference rather than re-acquiring
63/// `crate::dispatch::global_bindings()` is what makes this call safe to run
64/// while a background adopt holds (or is queued for) the write lock on that
65/// same global table.
66pub fn offer_runtime_fused_arm(
67    graph: &mut Graph,
68    spec: &RuntimeFusedSpec,
69    table: &KernelBindingTable,
70) -> Result<Option<NodeId>> {
71    debug_assert!(
72        spec.runtime_id.is_runtime(),
73        "offer_runtime_fused_arm on a static id"
74    );
75
76    // The capability gate: only offer the fused arm when a kernel is bound for
77    // this backend. No kernel ⇒ no arm ⇒ the region stays on its primitives.
78    if !fused_kernel_available_in(table, spec.runtime_id, spec.backend) {
79        return Ok(None);
80    }
81    if spec.inputs.is_empty() {
82        return Ok(None); // no diverge point; nothing to branch from.
83    }
84
85    // Arm 1 MUST equal arm 0's shape + dtype (finalize_branches' cast-to-uniform).
86    let (shape, dtype) = {
87        let n = graph.node(spec.primitive_sink);
88        (n.shape.clone(), n.dtype)
89    };
90    let fused = graph.push(Node {
91        op: Op::Fused(
92            spec.runtime_id,
93            FusedOpParams::Runtime {
94                scalars: spec.scalars.clone(),
95            },
96        ),
97        inputs: spec.inputs.clone(),
98        shape,
99        dtype,
100    });
101    graph.set_target_backend(fused, spec.backend);
102
103    // Emit the branch: arm 0 = the primitive subgraph (fallback/oracle), arm 1 =
104    // the fused kernel. Diverge at inputs[0]; reconverge at the region's consumer.
105    let mut builder = graph.open_branch(spec.inputs[0]);
106    builder.add_arm(spec.primitive_sink); // arm 0 — the runnability fallback
107    builder.add_arm(fused); // arm 1 — the synthesized kernel
108    builder.finalize_branches(graph, spec.reconverge)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::runtime_fused_kernels::adopt_runtime_fused;
115    use fuel_graph::jit::{OpAttrs, OpTag, PatternNode};
116    use fuel_ir::{DType, Layout, Shape};
117    use std::sync::{Arc, RwLock as StdRwLock};
118
119    fn noop_kernel(
120        _inputs: &[Arc<StdRwLock<fuel_memory::Storage>>],
121        _outputs: &mut [Arc<StdRwLock<fuel_memory::Storage>>],
122        _layouts: &[Layout],
123        _params: &crate::kernel::OpParams,
124    ) -> fuel_ir::Result<()> {
125        Ok(())
126    }
127
128    /// relu(add(a, b)) as a PatternNode region.
129    fn relu_add_region() -> PatternNode {
130        PatternNode::Op {
131            op: OpTag::Relu,
132            attrs: OpAttrs::default(),
133            operands: vec![PatternNode::Op {
134                op: OpTag::Add,
135                attrs: OpAttrs::default(),
136                operands: vec![
137                    PatternNode::Bind { index: 0 },
138                    PatternNode::Bind { index: 1 },
139                ],
140            }],
141        }
142    }
143
144    /// Build relu(add(a,b)) with a downstream consumer (neg) and return
145    /// (graph, [a,b], relu_sink, neg_reconverge).
146    fn graph_with_region() -> (Graph, Vec<NodeId>, NodeId, NodeId) {
147        let mut g = Graph::new();
148        let s = Shape::from_dims(&[4]);
149        let leaf = |g: &mut Graph| {
150            g.push(Node {
151                op: Op::Const,
152                inputs: vec![],
153                shape: s.clone(),
154                dtype: DType::F32,
155            })
156        };
157        let a = leaf(&mut g);
158        let b = leaf(&mut g);
159        let add = g.push(Node {
160            op: Op::Add,
161            inputs: vec![a, b],
162            shape: s.clone(),
163            dtype: DType::F32,
164        });
165        let relu = g.push(Node {
166            op: Op::Relu,
167            inputs: vec![add],
168            shape: s.clone(),
169            dtype: DType::F32,
170        });
171        let neg = g.push(Node {
172            op: Op::Neg,
173            inputs: vec![relu],
174            shape: s.clone(),
175            dtype: DType::F32,
176        });
177        (g, vec![a, b], relu, neg)
178    }
179
180    #[test]
181    fn offers_a_gated_branch_when_a_kernel_is_bound() {
182        let rid = adopt_runtime_fused(
183            "test::arm::relu_add",
184            relu_add_region(),
185            noop_kernel as crate::kernel::KernelRef,
186            vec![DType::F32, DType::F32, DType::F32],
187            BackendId::Cpu,
188        )
189        .expect("registrable region");
190
191        let (mut g, inputs, relu, neg) = graph_with_region();
192        let spec = RuntimeFusedSpec {
193            runtime_id: rid,
194            inputs,
195            primitive_sink: relu,
196            reconverge: neg,
197            backend: BackendId::Cpu,
198            scalars: vec![],
199        };
200        // adopt_runtime_fused binds into the GLOBAL table; pass it explicitly
201        // as the threaded table this test exercises the arm-emitter against.
202        let branch = offer_runtime_fused_arm(&mut g, &spec, &crate::dispatch::global_bindings())
203            .expect("valid branch")
204            .expect("emitted");
205        assert!(
206            matches!(g.node(branch).op, Op::Branch { .. }),
207            "an Op::Branch decision point"
208        );
209    }
210
211    #[test]
212    fn declines_when_no_kernel_for_the_backend() {
213        let rid = adopt_runtime_fused(
214            "test::arm::relu_add::cpu_only",
215            relu_add_region(),
216            noop_kernel as crate::kernel::KernelRef,
217            vec![DType::F32, DType::F32, DType::F32],
218            BackendId::Cpu,
219        )
220        .expect("registrable region");
221
222        let (mut g, inputs, relu, neg) = graph_with_region();
223        // Kernel bound on Cpu, not Cuda → no arm for a Cuda request.
224        let spec = RuntimeFusedSpec {
225            runtime_id: rid,
226            inputs,
227            primitive_sink: relu,
228            reconverge: neg,
229            backend: BackendId::Cuda,
230            scalars: vec![],
231        };
232        assert!(
233            offer_runtime_fused_arm(&mut g, &spec, &crate::dispatch::global_bindings())
234                .expect("no error")
235                .is_none(),
236            "no kernel for Cuda ⇒ no fused arm (region stays primitive)",
237        );
238    }
239}