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}