Skip to main content

fuel_dispatch/
pipelined.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Pipelined compile + execute. Phase 7.5 B4.
3//!
4//! [`PipelinedExecutor::realize`] runs compilation and execution on
5//! separate threads connected by a channel: a compiler thread walks
6//! the graph in topological order and emits work items; the
7//! executor (this thread) consumes them, allocates output Storage,
8//! calls the kernel, and stores the result in an internal cache.
9//! Both threads run concurrently so execution can begin while
10//! compilation is still resolving later nodes.
11//!
12//! Today's "compile" step is a single binding-table lookup —
13//! roughly nanoseconds per node. The threading delivers no
14//! measurable speedup in this regime. The pipelining infrastructure
15//! exists *now* — built on the [`compile_node`] / [`crate::compiled::execute_compiled`]
16//! interface from B5 — so future work that grows the compile step
17//! (residency-aware planning, transfer-cost minimization, kernel
18//! auto-tuning) plugs in without revisiting call sites.
19//!
20//! ## Storage during the migration
21//!
22//! `fuel_graph::Graph::storage_map` uses the legacy
23//! `fuel_backend_contract::Storage` (the `Box<dyn DynBackendStorage>`
24//! newtype). The pipelined executor uses the new
25//! `fuel_memory::Storage` (BackendStorage enum + dtype). During
26//! the migration the two coexist — neither is converted on the fly.
27//! The pipelined executor takes pre-realized inputs as a
28//! `HashMap<NodeId, Arc<RwLock<fuel_memory::Storage>>>` rather
29//! than reading from the graph's storage_map. Phase D unifies the
30//! two paths once kernel migration completes.
31//!
32//! ## Op coverage
33//!
34//! B4 supports `Op::Const` (input-cache adoption — no kernel call)
35//! and `Op::Add` on f32 (mapped to `OpKind::AddElementwise`).
36//! Phase C adds the rest as more (op, dtype) bindings register.
37
38use std::collections::{HashMap, HashSet};
39use std::sync::mpsc::{Sender, channel};
40use std::sync::{Arc, RwLock};
41use std::thread;
42
43use fuel_graph::opt::{execution_plan, insert_safety_copies};
44use fuel_graph::registry::FusedOpParams;
45use fuel_graph::{Graph, Node, NodeId, Op, PickedRoute};
46use fuel_ir::dispatch::OpKind;
47use fuel_ir::probe::BackendId;
48use fuel_ir::{DType, DeviceLocation, Error, Layout, Result, SymEnv};
49
50use crate::compiled::{
51    CompiledNode, CompletionHandle, compile_node, execute_compiled_with_wait_hint,
52};
53use crate::dispatch::global_bindings;
54use crate::kernel::{KernelBindingTable, KernelDTypes, MatmulM, OpParams};
55use crate::optimize::OptimizedGraph;
56use crate::ranker::{BackendRuntimeLookup, RuntimeSelector, resolve_branch};
57// `pick_route` is called only by the streamed-vs-one-shot equivalence test
58// below; the executor itself resolves branch-by-branch via `resolve_branch`.
59#[cfg(test)]
60use crate::ranker::pick_route;
61use fuel_memory::Storage;
62
63/// How the executor derives its dispatch order + topology-generation
64/// stamp for the `TopologyChanged` chunk-boundary check.
65///
66/// PR-A3b-1 of the "plan IS the graph" rebuild: the realize bridge can
67/// now drive the executor from `optimize_graph`'s in-place form via
68/// [`OptimizedGraph::dispatch_order`] (`extract_runs`/`lower_run`)
69/// instead of the legacy `ExecutionPlan`-supplied order. Both arms
70/// compute the *same* `NodeId` sequence on a branchless graph (the
71/// A3a equivalence gate proved `dispatch_order == compile_plan(...).order`);
72/// the only difference is the source of truth.
73///
74/// - [`OrderSource::Default`] — recompute via `execution_plan` inside
75///   the executor (the pre-A3b-1 behavior; the legacy `ExecutionPlan`
76///   path and the plain `realize`/`realize_many` entries use this).
77/// - [`OrderSource::Optimized`] — derive from the `OptimizedGraph`'s
78///   run lowering, computed AFTER `insert_safety_copies` so the order
79///   covers any safety-copy nodes the executor just inserted. Carries
80///   the optimize-time `generation` so the chunk-boundary
81///   `TopologyChanged` check still fires (the new path dispatches with
82///   `plan: None`, so the generation must come from here instead).
83///
84///   PR-C1: it also carries an optional **route** ([`PickedRoute`]) — the
85///   runtime route picker (Picker 2) chose one arm per `Op::Branch`. When
86///   `Some`, the order is the route-aware lowering
87///   ([`fuel_graph::lower_picked_route`]); when `None` (or empty) it is
88///   the arm-0 lowering (`lower_runs_arm0`), byte-identical to Phase B.
89enum OrderSource<'a> {
90    Default,
91    Optimized {
92        optimized: &'a OptimizedGraph,
93        /// The route the picker resolved (one arm per branch). `None` ⇒
94        /// arm-0 everywhere (no picker / no pressure ⇒ Phase B behavior).
95        route: Option<&'a PickedRoute>,
96    },
97    /// Step E Phase C, PR C1 — the **streaming run-walk**. Instead of a
98    /// route resolved up front, the compiler thread resolves each branch
99    /// **lazily, as the topo walk reaches its arm-entry**, via
100    /// [`resolve_branch`] over the threaded [`StreamingPick`]. The order is
101    /// never pre-flattened: the compiler thread interleaves arm resolution,
102    /// node compilation, and dispatch (so C2 can later read live device
103    /// load at the moment a branch resolves). Still VRAM-only selection in
104    /// C1, so the streamed order equals the one-shot
105    /// [`OrderSource::Optimized`]`{route: Some(pick_route(..))}` order
106    /// byte-for-byte.
107    Streaming {
108        optimized: &'a OptimizedGraph,
109        pick: &'a StreamingPick,
110    },
111    /// `order_for` fix track (`docs/design/incremental-consumer-index.md`
112    /// §5): memoizes the `Optimized { route: None }` arm's dispatch order
113    /// WHOLESALE in `order_cache`, computed once and reused by the caller
114    /// across repeated calls over an unchanged graph/roots — the plan-once
115    /// persistent-decode shape, where only leaf values are rebound between
116    /// tokens, never the graph's structure. Safe because that recompute is
117    /// a pure function of (graph structure, roots, generation) when none of
118    /// those change — proven, not assumed, by
119    /// `dispatch_order_is_stable_across_repeated_calls_with_no_mutation`
120    /// (`fuel-dispatch/src/optimize.rs`). A cache miss falls back to the
121    /// exact `Optimized { route: None }` computation and populates the
122    /// cache for next time; `generation_for` still reports `optimized`'s
123    /// generation, so the existing `TopologyChanged` chunk-boundary check
124    /// fires exactly as it would for `Optimized` — if the topology changes
125    /// underneath a cached session, realize fails loudly (the existing
126    /// contract), it does not silently serve a stale order.
127    Cached {
128        optimized: &'a OptimizedGraph,
129        order_cache: &'a std::sync::OnceLock<Vec<NodeId>>,
130    },
131}
132
133/// Owned arm-resolution config threaded into the compiler thread for the
134/// [`OrderSource::Streaming`] walk (Step E Phase C, PR C1). The selector +
135/// lookup are owned `Arc`s (not borrows) so they can be `move`d into the
136/// spawned compiler thread; both are `Send + Sync`. The per-arm candidate
137/// bindings are read from [`global_bindings`] inside the thread (as the
138/// eager `pick_route_for` does), so they are not carried here.
139///
140/// The streaming walk consults the SAME selector chain as the one-shot
141/// picker (the production `ChainedSelector`). As of C2 that chain is
142/// load-aware: its key is `(pressure_tier, load_tier, latency_ns,
143/// original_index)`, and the `load_tier` leg reads B1's in-flight counter
144/// through the selector's own `BackendRuntimeLookup` handle (the
145/// `BackendStreams` seam). Under no load / no pressure every leg is the
146/// no-signal default, so the lazy resolution yields the same arm as the
147/// eager `pick_route` on every input — byte-identical to pre-C2.
148#[derive(Clone)]
149pub(crate) struct StreamingPick {
150    /// The runtime selector the streaming walk consults at each branch —
151    /// the production `ChainedSelector` (VRAM-pressure guard + load tier +
152    /// Judge rank). It holds its own `BackendRuntimeLookup`, so it reads
153    /// both VRAM fit and live device load off that lookup's handles.
154    pub selector: Arc<dyn RuntimeSelector>,
155}
156
157/// Map from NodeId to a realized Storage Arc. Used both as the
158/// input cache (passed in by the caller for pre-realized leaves)
159/// and as the output cache (built up during execution).
160pub type StorageCache = HashMap<NodeId, Arc<RwLock<Storage>>>;
161
162/// Persistent per-node output buffers for CUDA-graph capture (the
163/// "CapturedRun executor" build-out). Maps each compute node's [`NodeId`]
164/// to a FIXED-ADDRESS output [`Storage`] Arc that survives across realizes,
165/// so a captured graph's baked kernel-argument addresses stay valid on
166/// replay.
167///
168/// Two modes, both driven by the executor's Kernel arm:
169/// - **`Record`** (warm run): the executor allocates each kernel output as
170///   usual AND records the Arc here. Produces the fixed buffers.
171/// - **`Reuse`** (capture / replay run): the executor REUSES the recorded
172///   Arc for each node — no allocation (mandatory inside a capture scope) —
173///   and the kernel's write-into path writes into it in place.
174///
175/// `None` (the param default at every existing realize call site) preserves
176/// byte-identical behavior: fresh alloc every realize, nothing recorded.
177pub struct PersistentOutputs {
178    map: HashMap<NodeId, Arc<RwLock<Storage>>>,
179    mode: PersistentMode,
180}
181
182#[derive(Clone, Copy, PartialEq, Eq)]
183enum PersistentMode {
184    /// Warm run: allocate + record each kernel output.
185    Record,
186    /// Capture / replay run: reuse the recorded buffer (no alloc).
187    Reuse,
188}
189
190impl PersistentOutputs {
191    /// A fresh recording map for the warm run.
192    pub fn recording() -> Self {
193        Self {
194            map: HashMap::new(),
195            mode: PersistentMode::Record,
196        }
197    }
198
199    /// A reuse map for the capture / replay run, seeded with the buffers
200    /// a prior warm run recorded.
201    pub fn reuse(map: HashMap<NodeId, Arc<RwLock<Storage>>>) -> Self {
202        Self {
203            map,
204            mode: PersistentMode::Reuse,
205        }
206    }
207
208    /// Consume, yielding the recorded NodeId→buffer map (to seed a
209    /// [`reuse`](Self::reuse) map for the capture run).
210    pub fn into_map(self) -> HashMap<NodeId, Arc<RwLock<Storage>>> {
211        self.map
212    }
213}
214
215/// Whether a kernel op's CUDA dispatch wrapper writes its result INTO the
216/// executor-provided `outputs[0]` buffer (rather than allocating a fresh
217/// output and replacing it). Only write-into ops are safe in the
218/// executor's Reuse (capture) mode: a replace-the-buffer wrapper would
219/// allocate inside the capture scope AND change the output's device
220/// address, invalidating the captured graph. [`capture_decode`] rejects
221/// any graph whose compute nodes fall outside this set — a surfaced
222/// capability gap (telemetry-worthy), never a silent miscompile.
223///
224/// The set is the full decode op-set, all VERIFIED to replay bit-exact under
225/// capture (each with a forced-freed-memory-reuse test — see the `diag_*` and
226/// `capture_decode_*` GPU tests).
227///
228/// **History / the big correction (2026-07-10):** `MatMul` and `IndexSelect`
229/// were *briefly* excluded on the belief they weren't capture-safe (matmul's
230/// "cuBLAS internal state", index_select's "host arrays / driver anomaly").
231/// That was ALL WRONG — it was a single Fuel-side bug: `capture_decode`
232/// consumed `inputs` and retained only compute OUTPUTS, so any input buffer
233/// (weights, embedding table) the caller didn't independently hold was FREED
234/// after capture, and the captured graph replayed against freed/reused memory
235/// → intermittent partial-wrong output (cuBLAS "split-K columns", index_select
236/// "element 0"). Fixed by `CapturedDecode::retained_inputs`. cuBLAS gemm +
237/// index_select then replay bit-exact (0/50 even with forced reuse), so the
238/// baracuda GEMV + gather_rows are NOT needed for capture-safety.
239///
240/// **`Rope` (`OpKind::Rope`, corrected 2026-07-11)**: previously excluded on
241/// the (now-disproven) assumption that decode's rope always goes through
242/// `rope_with_tables` (decomposed to elementwise ops including `Concat`, see
243/// below) rather than this fused single-input kernel. Tracing a real Llama
244/// decode graph's capture_decode rejection (`capture_decode_matches_persistent`
245/// GPU test, `fuel-core`) found `Op::Rope` nodes placed by the optimizer
246/// (`rope_with_tables` is not always selected — shape-dependent). The
247/// dispatch wrapper (`cuda_rope_baracuda_wrapper!` in
248/// `baracuda_dispatch.rs`) already delegates to baracuda's write-into
249/// `rope_*_into` FFI — that mechanism predates this predicate fix, it was
250/// simply never added here. `rope.fkc.md`'s CUDA kernel is audited
251/// (`docs/architecture/10-decisions-log.md`, 2026-07-11): grid-stride,
252/// one thread per output element, reads only its own rotation-pair partner,
253/// no atomics/shared-memory/cross-thread state — genuinely capture-safe.
254///
255/// **`Concat` (4b-γ) is capture-safe ONLY for N==2 inputs** — baracuda's
256/// `concat2_into` write-into path covers exactly the decode graph's only
257/// Concat (the rope rotate-half's 2-input `concat(-x2, x1)`, from
258/// `rope_with_tables`'s CUDA decomposition). This predicate alone can't
259/// express the N-arity condition (a pure `OpKind -> bool` fn has no access
260/// to `OpParams`); `capture_decode`'s validation loop adds an explicit N>2
261/// rejection alongside the `op_kind_is_capture_writeinto` check so N>2
262/// Concat is refused at validation time, before the warm pass ever runs —
263/// the write-into wrapper's N>2 fallback (which still allocates internally
264/// for its N-2 intermediates) is therefore provably never invoked while
265/// `persistent.mode == Reuse`. N-ary write-into concat is a real future gap
266/// if a consumer needs it; not built here (no consumer today).
267///
268/// **`Op::Contiguize` (4b-γ)** — `WorkItemKind::ContiguizeOf`, not a
269/// `Kernel` work item, so this predicate never even sees it — became
270/// capture-safe via a separate write-into path (`contiguize_into`) wired
271/// directly into the `ContiguizeOf` executor arm's `PersistentOutputs`
272/// consultation, mirroring how `Kernel` nodes consult this predicate. No
273/// change needed here or in `capture_decode`'s validation loop: a given
274/// `ContiguizeOf` node's allocate-vs-zero-copy branch is a static
275/// layout/stride property, identical across the warm and capture passes
276/// (both replay the same compiled item list), so it's unconditionally safe
277/// once wired to `PersistentOutputs`.
278///
279/// LESSON: a bug reproduced through the full harness but not in isolation is
280/// the harness's fault, not the callee's — chase the differentiator (here,
281/// input lifetime), don't conclude "kernel/library not capture-safe" from it.
282///
283/// [`capture_decode`]: PipelinedExecutor::capture_decode
284/// GAP-217b: gated on `cuda` because its ONLY caller is
285/// `capture_decode` (`#[cfg(feature = "cuda")]`, ~:1266). Without this it
286/// reports as dead code on every default build — a false signal on a
287/// predicate whose doc comment is a record of hard-won capture-safety
288/// findings, i.e. exactly the kind of item someone eventually deletes for
289/// being "unused". Gating it makes the reachability claim honest instead of
290/// leaving a warning to argue the opposite.
291#[cfg(feature = "cuda")]
292fn op_kind_is_capture_writeinto(op: OpKind) -> bool {
293    matches!(
294        op,
295        // Binary elementwise — residual + gating.
296        OpKind::AddElementwise
297            | OpKind::SubElementwise
298            | OpKind::MulElementwise
299            | OpKind::DivElementwise
300            | OpKind::MaximumElementwise
301            | OpKind::MinimumElementwise
302            | OpKind::PowElementwise
303            | OpKind::RemElementwise
304            // Softmax.
305            | OpKind::SoftmaxLastDim
306            | OpKind::LogSoftmaxLastDim
307            // Affine (mul_scalar/add_scalar — the 1/√d attention scale).
308            | OpKind::Affine
309            // Dense matmul (cuBLAS gemm_dense — capture-safe; the earlier
310            // "not replay-stable" was the dangling-input bug, now fixed).
311            | OpKind::MatMul
312            // Embedding lookup (was the same dangling-input bug, not a kernel issue).
313            | OpKind::IndexSelect
314            // RMS-norm: capture-safe via the device's fixed workspace for its
315            // per-row RMS scratch (warm sizes it; capture + replay reuse, no alloc).
316            | OpKind::RmsNormLastDim
317            // Unary activations (write-into + workspace-free).
318            | OpKind::SiluElementwise
319            | OpKind::GeluTanhElementwise
320            | OpKind::GeluElementwise
321            // Negation (write-into + workspace-free) — the rope rotate-half
322            // decomposition's `neg_second` half (CapturedRun 4b-resume: rope
323            // runs decomposed on CUDA since there's no rotate-half fused kernel).
324            | OpKind::NegElementwise
325            // N==2 concat only (rope rotate-half) — see the doc comment
326            // above; N>2 is rejected explicitly in capture_decode's
327            // validation loop, not by this predicate.
328            | OpKind::Concat
329            // Rotary position embedding (fused single-input kernel) — see
330            // the doc comment above.
331            | OpKind::Rope
332    )
333}
334
335/// The product of [`PipelinedExecutor::capture_decode`]: a replayable
336/// CUDA-graph capture of a single-device decode step plus the handle to
337/// the fixed output buffer the captured launches write into.
338///
339/// Replay contract: update the fixed INPUT buffers (the Arcs passed into
340/// `capture_decode`) in place via H2D, call [`run`](Self::run)`.replay()`,
341/// then read [`output`](Self::output) — the captured launches recompute
342/// into it at the same device addresses. `persistent` holds every compute
343/// node's fixed buffer (the same addresses baked into the graph), exposed
344/// so a caller can inspect / retain them across replays.
345#[cfg(feature = "cuda")]
346pub struct CapturedDecode {
347    /// The instantiated, replayable capture.
348    pub run: fuel_cuda_backend::CapturedRun,
349    /// The realize target's fixed output buffer (written by replay).
350    pub output: Arc<RwLock<Storage>>,
351    /// The target's resolved output [`Layout`].
352    pub output_layout: Layout,
353    /// Every compute node's fixed output buffer (the capture-baked
354    /// addresses). Retained so they outlive the [`CapturedRun`].
355    pub persistent: HashMap<NodeId, Arc<RwLock<Storage>>>,
356    /// EVERY input buffer's Arc (weights, KV, per-token inputs), retained so
357    /// their device addresses — baked into the captured graph — stay valid
358    /// for the life of the capture. Without this, an input buffer the CALLER
359    /// doesn't independently hold is freed when `capture_decode` returns
360    /// (it consumes `inputs`), and replay reads freed/reused memory. This is
361    /// the fix for the "index_select / matmul mis-replay" — a dangling input
362    /// pointer, NOT a kernel/cuBLAS/driver capture issue.
363    pub retained_inputs: Vec<Arc<RwLock<Storage>>>,
364}
365
366/// Warm→capture→replay orchestration over [`capture_decode`] for a
367/// persistent decode loop (the CapturedRun build-out, Increment 4a).
368///
369/// Owns the FIXED per-token INPUT buffers — the ones that change each token
370/// (token-id, rope position, mask, KV write offset) — at stable device
371/// addresses, plus the [`CapturedDecode`]. Each generated token is one
372/// [`replay_token`](Self::replay_token): H2D-update the fixed input buffers
373/// IN PLACE (same addresses the captured graph reads), replay the captured
374/// graph (one `cuGraphLaunch`, no per-op launch overhead), then read the
375/// output at its fixed address.
376///
377/// The mechanism is graph-agnostic: [`capture`](Self::capture) runs a
378/// single-device CUDA decode graph through the persistent-output executor;
379/// the model layer (fuel-core's `DecodeSession`) supplies the held graph +
380/// the fixed buffers and drives the per-token updates. Every compute op must
381/// be capture-safe (`op_kind_is_capture_writeinto`) — `capture` surfaces any
382/// that are not (today: matmul, index_select — pending baracuda kernels).
383///
384/// [`capture_decode`]: PipelinedExecutor::capture_decode
385#[cfg(feature = "cuda")]
386pub struct CapturedDecodeSession {
387    device: fuel_cuda_backend::CudaDevice,
388    captured: CapturedDecode,
389    /// Per-token input buffers, keyed by NodeId — fixed device addresses
390    /// updated in place each token (the input addresses the captured graph
391    /// reads). A subset of the `inputs` handed to [`capture`](Self::capture).
392    per_token_inputs: HashMap<NodeId, Arc<RwLock<Storage>>>,
393    /// EVERY input buffer's Arc (weights, KV, per-token), retained so their
394    /// device addresses stay valid for the life of the capture. `capture_decode`
395    /// consumes `inputs` and only `CapturedDecode` retains the compute-output
396    /// buffers, so without this the WEIGHT input buffers would be freed and the
397    /// captured graph would replay against freed memory.
398    _retained_inputs: Vec<Arc<RwLock<Storage>>>,
399}
400
401#[cfg(feature = "cuda")]
402impl CapturedDecodeSession {
403    /// Capture the decode graph once against the given fixed input buffers.
404    ///
405    /// `inputs` are ALL the graph's leaves (weights, KV, per-token data) at
406    /// fixed device addresses; `per_token_input_ids` names the subset that
407    /// changes each token. The per-token Arcs are snapshotted (shared, not
408    /// copied) so overwriting them in place later feeds the captured graph's
409    /// fixed input reads. Returns a session ready for
410    /// [`replay_token`](Self::replay_token). Errors (never panics) if the
411    /// graph carries a capture-unsafe op or a per-token id is not an input.
412    pub fn capture(
413        graph: Arc<RwLock<Graph>>,
414        target: NodeId,
415        inputs: StorageCache,
416        per_token_input_ids: &[NodeId],
417        sym_env: SymEnv,
418    ) -> Result<Self> {
419        use fuel_memory::BackendStorage;
420        // Snapshot the per-token input Arcs + retain EVERY input Arc BEFORE
421        // `inputs` is consumed by capture_decode (so no input buffer's device
422        // address is freed while the captured graph still bakes it).
423        let retained_inputs: Vec<Arc<RwLock<Storage>>> = inputs.values().cloned().collect();
424        let mut per_token_inputs = HashMap::with_capacity(per_token_input_ids.len());
425        for &id in per_token_input_ids {
426            let arc = inputs.get(&id).cloned().ok_or_else(|| {
427                Error::Msg(format!(
428                    "CapturedDecodeSession::capture: per-token input {:?} not in inputs",
429                    id,
430                ))
431                .bt()
432            })?;
433            per_token_inputs.insert(id, arc);
434        }
435        // Derive the CUDA device from any input.
436        let device = {
437            let any = per_token_inputs
438                .values()
439                .next()
440                .or_else(|| inputs.values().next())
441                .ok_or_else(|| {
442                    Error::Msg("CapturedDecodeSession::capture: no inputs".into()).bt()
443                })?;
444            let guard = any.read().map_err(|_| poisoned("input storage"))?;
445            match &guard.inner {
446                BackendStorage::Cuda(c) => c.device().clone(),
447                other => {
448                    return Err(Error::Msg(format!(
449                        "CapturedDecodeSession::capture: input is BackendStorage::{:?}, not Cuda",
450                        std::mem::discriminant(other),
451                    ))
452                    .bt());
453                }
454            }
455        };
456        let captured = PipelinedExecutor::capture_decode(graph, target, inputs, sym_env)?;
457        Ok(Self {
458            device,
459            captured,
460            per_token_inputs,
461            _retained_inputs: retained_inputs,
462        })
463    }
464
465    /// Replay one token. For each `(node_id, host_bytes)` in `updates`,
466    /// overwrite that per-token input buffer IN PLACE (H2D, same device
467    /// address), then replay the captured graph and return the output buffer
468    /// (now holding this token's result). `host_bytes.len()` must equal the
469    /// buffer's byte length.
470    /// **Every compute node's output buffer, keyed by `NodeId`** — the
471    /// capture's inspection surface.
472    ///
473    /// Capture is usually described as a launch-overhead optimization, and that
474    /// is what Fuel currently exploits (measured 4.28x at k=1). But recording
475    /// the graph has a second, unexploited consequence: **every intermediate
476    /// gets a FIXED device address that survives across replays.** On the
477    /// ordinary path intermediates are transient — allocated, consumed, freed —
478    /// so there is nothing to look at after the fact.
479    ///
480    /// That makes a captured session a **whole-graph, per-node, addressable
481    /// snapshot of what actually executed**, which is a different artifact from
482    /// anything the planner records. `OptimizedGraph::placement_of` says what
483    /// the optimizer DECIDED; this says what the device HELD. Those can
484    /// disagree, and an independent second instrument is the only way to notice.
485    ///
486    /// Uses this enables, none of which need re-running the model:
487    /// - read any intermediate after a token;
488    /// - compare per-node against a CPU oracle — whole-graph differential,
489    ///   where FKC verification today is per-KERNEL;
490    /// - measure real per-op divergence for the tolerance/Judge work instead of
491    ///   modelling it.
492    ///
493    /// Buffers are live device memory owned by the capture; they are overwritten
494    /// by the next `replay_token`. Read them before replaying again.
495    pub fn node_outputs(&self) -> &HashMap<NodeId, Arc<RwLock<Storage>>> {
496        &self.captured.persistent
497    }
498
499    pub fn replay_token(&self, updates: &[(NodeId, &[u8])]) -> Result<Arc<RwLock<Storage>>> {
500        use fuel_memory::BackendStorage;
501        for (id, bytes) in updates {
502            let arc = self.per_token_inputs.get(id).ok_or_else(|| {
503                Error::Msg(format!(
504                    "CapturedDecodeSession::replay_token: {:?} is not a per-token input",
505                    id,
506                ))
507                .bt()
508            })?;
509            let guard = arc.read().map_err(|_| poisoned("per-token input"))?;
510            match &guard.inner {
511                BackendStorage::Cuda(c) => c.write_from_host(bytes)?,
512                other => {
513                    return Err(Error::Msg(format!(
514                        "CapturedDecodeSession::replay_token: input {:?} is \
515                         BackendStorage::{:?}, not Cuda",
516                        id,
517                        std::mem::discriminant(other),
518                    ))
519                    .bt());
520                }
521            }
522        }
523        self.captured.run.replay(&self.device)?;
524        self.device.synchronize()?;
525        Ok(Arc::clone(&self.captured.output))
526    }
527
528    /// The fixed output buffer, updated by each
529    /// [`replay_token`](Self::replay_token). Holds the warm-run result until
530    /// the first replay.
531    pub fn output(&self) -> &Arc<RwLock<Storage>> {
532        &self.captured.output
533    }
534
535    /// The output's resolved [`Layout`].
536    pub fn output_layout(&self) -> &Layout {
537        &self.captured.output_layout
538    }
539}
540
541/// What flavor of work item the executor is processing.
542/// Disambiguates the four cases:
543enum WorkItemKind {
544    /// `Op::Const` — its Storage Arc is already in the input cache.
545    /// Executor verifies the entry exists and moves on.
546    ConstAdopt,
547    /// Metadata-only view op (`Op::Transpose`, `Op::Permute`,
548    /// `Op::BroadcastTo`): the output's Storage Arc IS the
549    /// input's Storage Arc (bytes shared); `output_layout`
550    /// describes the strided view.
551    ViewOf { input: NodeId },
552    /// Reshape-style adoption: the output is contiguous in
553    /// `output_layout.shape()`. If the input is already contiguous
554    /// + zero offset, the output Arc is the input Arc (zero copy).
555    ///
556    /// Otherwise, the executor auto-contiguizes the input into a
557    /// fresh Arc and uses that.
558    ContiguizeOf { input: NodeId },
559    /// Computational kernel: allocate output, run the compiled
560    /// kernel, store the result. `compiled` is `Some(...)`.
561    Kernel,
562    /// `Op::Iota { len }` — 0-input F32 position generator
563    /// `[0.0, 1.0, …, (len-1)]`. Direct executor dispatch (no kernel /
564    /// binding-table lookup), like `WorkItemKind::Alloc`: a leaf has no
565    /// `inputs[0]` to inherit a backend from, so it can't take the normal
566    /// kernel path. The executor fills the sequence on CPU.
567    Iota { len: usize },
568    /// `Op::Release` — metadata-only "this storage is no longer
569    /// needed" directive. Per `Op::Release`'s contract: the output
570    /// is a zero-element marker (NodeId placeholder for graph
571    /// bookkeeping; never read by any consumer). The executor emits
572    /// a zero-byte CPU `Storage` at the node's slot so downstream
573    /// cache lookups don't surface "missing slot" errors; the actual
574    /// deallocation of `inputs[0]` is driven by `destructive_input`
575    /// in the realize loop (Phase B), which drops the Arc held by
576    /// the cache. The held Arc's Drop chain frees device memory
577    /// (sync for CPU, stream-deferred for async backends).
578    ReleaseMarker,
579    /// `Op::WriteSlice` — in-place scatter write. The output adopts
580    /// the destination input's Storage Arc (zero-copy alias); the
581    /// kernel mutates that Arc's bytes via a write lock. The source
582    /// input's slab is copied into the destination's rectangular
583    /// region defined by `OpParams::WriteSlice`'s `ranges`.
584    ///
585    /// Carries `dest`/`source` NodeIds so the executor can:
586    /// 1. Adopt the dest's Arc as the kernel's output slot.
587    /// 2. Pass only the source as a kernel input (the dest is not
588    ///    a read-input to the kernel; it's the destination buffer
589    ///    being mutated in place).
590    ///
591    /// The realize loop's `destructive_input` cleanup evicts the
592    /// `dest` NodeId from the cache after this op runs (per
593    /// `Op::WriteSlice`'s destructive_input == Some(0) contract).
594    /// Downstream consumers read post-write bytes via this op's
595    /// own NodeId, not the dest's.
596    WriteSlice { dest: NodeId, source: NodeId },
597    /// `Op::WriteSliceRotating` — like `WriteSlice` but the rotating
598    /// axis wraps modulo `modulus`. Carries `dest`/`source`/`position`
599    /// NodeIds; the kernel reads the U32 position from `position`'s
600    /// storage and splits the write across the ring boundary if
601    /// `position % modulus + slab_len > modulus`.
602    WriteSliceRotating {
603        dest: NodeId,
604        source: NodeId,
605        position: NodeId,
606    },
607    /// `Op::WriteSliceDoff` — like `WriteSlice` but the start on `axis`
608    /// is read from a device-resident rank-0 `I64` scalar (`offset`)
609    /// at kernel launch, not host-baked. Carries `dest`/`source`/
610    /// `offset` NodeIds; the CUDA wrapper threads `offset`'s device
611    /// pointer straight to the kernel (no D2H) so a captured graph
612    /// replays at the host-updated position. No ring wrap.
613    WriteSliceDoff {
614        dest: NodeId,
615        source: NodeId,
616        offset: NodeId,
617    },
618    /// `Op::Copy { target }` — produce a fresh Storage on
619    /// `target_location`, copying bytes from `inputs[0]`'s residency.
620    ///
621    /// The kernel lookup goes through the standard binding-table path
622    /// at `(OpKind::Copy, [dt, dt], source_backend)`, so D2H is a peer
623    /// of every other op (architecture identity check #1). The
624    /// dedicated WorkItemKind exists because Op::Copy is the one op
625    /// whose output's device differs from `target_backend`:
626    /// `target_backend` is the **source** backend (where the kernel
627    /// runs — it owns the download path), while `target_location`
628    /// drives output allocation. WorkItemKind::Kernel always allocates
629    /// output on `target_backend`; Copy needs the override.
630    ///
631    /// Phase 2 of the bridge-retirement trajectory. Replaces the
632    /// per-variant `match self` in `BackendStorage::read_to_cpu_bytes`
633    /// (deleted alongside this commit) with a graph-level node the
634    /// optimizer can see, cost, and eventually fuse.
635    Copy { target_location: DeviceLocation },
636    /// `Op::Move { target }` — `Op::Copy`'s destructive sibling:
637    /// produce a fresh Storage on `target_location` via the same
638    /// data-movement kernel (binding-table lookup at
639    /// `(OpKind::Copy, [dt, dt], source_backend)`; output allocation
640    /// driven by `target_location`), then release the source. The
641    /// release half is NOT handled in the executor's arm — it rides
642    /// the realize loop's `destructive_input` cleanup
643    /// (`Op::Move::destructive_input() == Some(0)`), exactly like
644    /// `Op::Release`: the cache's Arc to the source drops after the
645    /// move runs and the Drop chain frees device memory (sync for
646    /// CPU, stream-deferred for async backends).
647    ///
648    /// Ordering safety is the graph's job, not this arm's:
649    /// `execution_plan` integrates `derive_ordering`, which pins the
650    /// Move AFTER every non-destructive reader of the source's alias
651    /// set, and `insert_safety_copies` snapshots the source for any
652    /// reader that data-flow-depends on the Move's output. A Move
653    /// therefore never strands a sibling consumer of its source.
654    ///
655    /// Same-device moves are legal and match the legacy
656    /// `GraphExecutor` contract: a plain copy producing fresh
657    /// storage on the same device, with the source still evicted
658    /// afterward.
659    Move { target_location: DeviceLocation },
660    /// `Op::Alloc { target }` — produce a freshly-allocated, zero-
661    /// initialized Storage on `target_location` with the node's shape
662    /// + dtype. Zero inputs.
663    ///
664    /// Doesn't dispatch through the binding table — the executor's
665    /// arm calls each backend's native allocator directly. For
666    /// non-CPU targets it derives the device handle by searching the
667    /// input cache for any storage on `target_location`'s backend
668    /// (callers seed this via
669    /// `fuel-tensor::pipelined_bridge::device_seed_storage`).
670    ///
671    /// Phase 3a of the bridge-retirement trajectory (post-9c).
672    /// Replaces `fuel-tensor::inference_context::alloc_zeroed_on`'s
673    /// per-`DeviceLocation` match with a graph node the optimizer
674    /// can see; the per-backend match moves from fuel-core's bridge
675    /// layer into fuel-storage's executor (the architectural dispatch
676    /// layer).
677    Alloc { target_location: DeviceLocation },
678    /// `Op::ZeroFill` — fill the input's storage bytes with zero,
679    /// in place. Adopts the input's Storage Arc as the output (same
680    /// Storage; bytes mutated). Destructive on `inputs[0]`.
681    ///
682    /// Direct executor dispatch (no binding-table lookup) per the
683    /// same rationale as `WorkItemKind::Alloc`: structural op,
684    /// per-backend dispatch. Per-backend behaviour:
685    /// - CPU: `bytes_mut().fill(0)` via the CoW path.
686    /// - CUDA: `CudaStorageBytes::zero_async` via baracuda alpha.30's
687    ///   `DeviceBuffer::zero_async` (cuMemsetD8Async, in-place).
688    /// - Vulkan: `VulkanBackend::fill_bytes_zero` via
689    ///   `vkCmdFillBuffer` — device-side, ~2× the bandwidth of the
690    ///   old host-staged zeros path that `alloc_zeroed_on` used.
691    ///
692    /// Phase 3a follow-up of bridge-retirement (post-9c). Pairs with
693    /// the uninit-alloc `WorkItemKind::Alloc` to give the
694    /// architecturally clean "Op::Alloc (uninit) → Op::ZeroFill
695    /// (explicit fill)" pipeline.
696    ZeroFill,
697    /// In-place kernel — the output adopts the input at index
698    /// `target_idx`'s Storage Arc and the kernel mutates that Arc's
699    /// bytes through a single write lock. Phase 3 of the in-place ops
700    /// infrastructure
701    /// (`docs/session-prompts/in-place-ops-infrastructure.md`).
702    ///
703    /// Kernel lookup goes through the standard binding table at
704    /// `(op_to_op_kind, [target_dtype], target_backend)`. The wrapper's
705    /// `inputs` slice is empty; `outputs[0]` is the target Arc. The
706    /// wrapper acquires `outputs[0]`'s write lock and calls the
707    /// underlying single-pointer kernel (e.g. baracuda's
708    /// `affine_inplace_*` on CUDA, or the chassis with `src == dst`
709    /// on CPU).
710    ///
711    /// Output Arc adoption: identical to `WorkItemKind::WriteSlice`
712    /// (the output's slot = the target's Arc; the realize loop's
713    /// `destructive_input` cleanup evicts the target's NodeId from the
714    /// cache afterward). Downstream consumers read post-mutation bytes
715    /// through this op's NodeId, not the target's.
716    InplaceKernel {
717        /// Index into `inputs` whose Storage Arc the output adopts.
718        /// Always equal to the source `Op::destructive_input().unwrap()`;
719        /// stored explicitly so the executor doesn't re-derive it.
720        target_idx: usize,
721    },
722    /// `Op::View { slot }` — multi-output projection (Option C,
723    /// Session 4). The output's Storage Arc IS the producer's Arc;
724    /// the WorkItem's `output_layout` was prepared by
725    /// [`fuel_graph::NodeHandle::view`] with the slot's `byte_offset`
726    /// baked into `Layout::start_offset` so downstream kernels reading
727    /// the producer's bytes as `slot_dtype` elements land on the
728    /// slot's first byte. Structurally identical to [`Self::ViewOf`]
729    /// at execute time — kept distinct so error messages and
730    /// telemetry have a clean dispatch point.
731    SlotView { producer: NodeId },
732    /// `Op::ViewOwned { slot }` — multi-output projection with an
733    /// independent destination buffer. At realize time, allocate a
734    /// fresh contiguous Storage of the slot's `(shape, dtype)` on
735    /// the producer's device, then memcpy
736    /// `producer.bytes[slot.byte_offset .. slot.byte_offset + slot.len_bytes]`
737    /// into the new storage. Session 4 ships the CPU path; non-CPU
738    /// backends return a typed error until their copy-with-offset
739    /// hooks land in the followup session.
740    SlotOwn { producer: NodeId, slot: u32 },
741}
742
743/// One unit of work emitted by the compiler thread to the executor
744/// thread.
745/// Phase name of a work item's kind, for the executor's host-phase accounting
746/// (`op_profile::PhaseTimer`). One name per variant so the table splits the
747/// per-item executor time by what the item is.
748fn work_item_kind_name(kind: &WorkItemKind) -> &'static str {
749    match kind {
750        WorkItemKind::ConstAdopt => "item.ConstAdopt",
751        WorkItemKind::ViewOf { .. } => "item.ViewOf",
752        WorkItemKind::ContiguizeOf { .. } => "item.ContiguizeOf",
753        WorkItemKind::Kernel => "item.Kernel",
754        WorkItemKind::Iota { .. } => "item.Iota",
755        WorkItemKind::ReleaseMarker => "item.ReleaseMarker",
756        WorkItemKind::WriteSlice { .. } => "item.WriteSlice",
757        WorkItemKind::WriteSliceRotating { .. } => "item.WriteSliceRotating",
758        WorkItemKind::WriteSliceDoff { .. } => "item.WriteSliceDoff",
759        WorkItemKind::Copy { .. } => "item.Copy",
760        WorkItemKind::Move { .. } => "item.Move",
761        WorkItemKind::Alloc { .. } => "item.Alloc",
762        WorkItemKind::ZeroFill => "item.ZeroFill",
763        WorkItemKind::InplaceKernel { .. } => "item.InplaceKernel",
764        WorkItemKind::SlotView { .. } => "item.SlotView",
765        WorkItemKind::SlotOwn { .. } => "item.SlotOwn",
766    }
767}
768
769/// Work items that choose no backend of their own: adopting an input-cache entry
770/// (`Op::Const`), the metadata-only view items, and `ContiguizeOf` (it runs on its
771/// input's backend; its `target_backend` is a `Cpu` placeholder, which made every
772/// Qwen3 decode step look like ~900 Vulkan -> Cpu -> Vulkan switches and forced a
773/// batch submit at each one). Their `target_backend` is a placeholder
774/// (a `Const` is always stamped `Cpu`; a view inherits its producer's or
775/// defaults to `Cpu`), so letting them take part in the chunk-boundary logic made
776/// every weight `Const` that sits between two Vulkan kernels look like a
777/// Vulkan -> Cpu -> Vulkan backend switch: the executor armed `multi_backend`,
778/// submitted the open Vulkan batch at each "switch", and then waited for it
779/// before the next Vulkan op. In Qwen3-0.6B decode that was about 400 forced
780/// submit-and-wait round trips per step (`docs/research/qwen3-vulkan-decode-profile.md`).
781fn is_backend_neutral(kind: &WorkItemKind) -> bool {
782    matches!(
783        kind,
784        WorkItemKind::ConstAdopt
785            | WorkItemKind::ViewOf { .. }
786            | WorkItemKind::SlotView { .. }
787            | WorkItemKind::ContiguizeOf { .. }
788    )
789}
790
791struct WorkItem {
792    node_id: NodeId,
793    inputs: Vec<NodeId>,
794    /// Number of elements in the output (for output Storage
795    /// allocation; multiplied by dtype size at allocation time).
796    elem_count: usize,
797    dtype: DType,
798    target_backend: BackendId,
799    /// What kind of work this represents (kernel vs adopt vs view).
800    kind: WorkItemKind,
801    /// `Some` for [`WorkItemKind::Kernel`]; `None` for the other
802    /// two. Carries the resolved kernel ref + op_params.
803    compiled: Option<CompiledNode>,
804    /// The output's [`Layout`]. For kernels: always
805    /// `Layout::contiguous(node.shape)`. For metadata-only view
806    /// ops: a strided/broadcast Layout pointing at the input's
807    /// Storage. Carried so the executor can publish the right
808    /// Layout into its layout cache and ultimately return it from
809    /// [`PipelinedExecutor::realize`].
810    output_layout: Layout,
811    /// Index into `inputs` whose storage gets destroyed by this op
812    /// (`Op::Release` / `Op::Move` → `Some(0)`; non-destructive ops
813    /// → `None`). The executor evicts the destroyed input from the
814    /// cache after the op runs, unless it's also in the realize
815    /// target set. Snapshot of `node.op.destructive_input()` at
816    /// compile time.
817    destructive_input: Option<usize>,
818    /// Multi-output bundle metadata (Option C, item 3). `Some(_)`
819    /// when the node was declared multi-output via
820    /// `Graph::set_output_views`; the Kernel arm allocates one
821    /// contiguous output Storage sized to fit every slot's bytes,
822    /// then attaches the bundle via `Storage::with_bundle` so
823    /// downstream `Op::View`/`Op::ViewOwned` nodes resolve
824    /// correctly.
825    output_bundle: Option<Arc<[fuel_ir::storage::OutputView]>>,
826}
827
828/// Merge the graph's side-effect roots into the caller's requested
829/// roots. Preserves caller order (user roots come first), dedupes,
830/// and appends side-effect-bearing nodes (`Op::Print`, `Op::Save`,
831/// etc.) so their effects fire even when not reachable from the
832/// caller's roots. Mirrors `fuel-graph-executor::extend_with_side_effect_roots`.
833///
834/// Short-circuits to a borrow-equivalent `Vec<NodeId>` when the graph
835/// has no side-effect roots — the common case during inference.
836fn extend_with_side_effect_roots(graph: &Graph, user_roots: &[NodeId]) -> Vec<NodeId> {
837    let side = graph.side_effect_roots();
838    if side.is_empty() {
839        return user_roots.to_vec();
840    }
841    let mut out = Vec::with_capacity(user_roots.len() + side.len());
842    out.extend_from_slice(user_roots);
843    for &s in side {
844        if !out.contains(&s) {
845            out.push(s);
846        }
847    }
848    out
849}
850
851/// Pipelined executor: walks a graph, compiles each node in a
852/// dedicated thread, executes the compiled stream on the calling
853/// thread.
854///
855/// # Architectural commitments (Phase 7.6 step 9c)
856///
857/// ## Fail-fast dispatch — no runtime CPU fallback
858///
859/// The binding-table lookup is fail-fast. If
860/// `global_bindings().lookup(op_kind, dtypes, backend)` returns
861/// `None`, `realize` / `realize_many` surface a typed `Error::Msg`
862/// at compile time. There is **no runtime fallback** to a CPU
863/// sibling when a registered kernel returns `Err`; we commit to the
864/// backend contract that registered kernels must succeed for shapes
865/// matching their declared coverage.
866///
867/// Rationale: per-decision-point alternatives (architecture v1.0
868/// §04) already let multiple backends register at the same key. If
869/// a backend's coverage is partial, the right answer is to register
870/// only the shapes it actually handles — not to silently fall back
871/// at runtime, which masks bugs and slows the hot path. The
872/// long-term home for "this op on this device" is graph-level
873/// dispatch insertion (Op::Copy edges to a backend that does
874/// support the shape), which makes the routing decision visible in
875/// the IR rather than hidden in the executor. Picker-arc step 4b
876/// landed exactly that: `compile_plan`'s off-device fallback
877/// (`PlanOptions::fallback_placements_for`) admits a candidate on
878/// another device when the pinned device lacks the implementation,
879/// and the bridge's cross-device-copy pass stitches residency
880/// around the off-device winner. There is exactly ONE fallback
881/// owner — the plan-time picker; this executor still dispatches
882/// fail-fast, and ops with no implementation anywhere surface the
883/// plan-time `NoBackendForOp` error.
884///
885/// This is a deliberate departure from the legacy executor's
886/// `cpu_fallback(op, inputs, shape, dtype, cache)` semantics.
887///
888/// ## Optimizer separation — callers compose
889///
890/// The pipelined executor does **not** run `fuel_graph::opt` or any
891/// `RuleRegistry` pass itself. Callers that want graph-level
892/// optimization compose it at the call site:
893///
894/// ```ignore
895/// let optimized_targets = registry.optimize_to_fixpoint(&graph, &targets);
896/// PipelinedExecutor::realize_many(graph, &optimized_targets, inputs)?;
897/// ```
898///
899/// Rationale: the legacy `GraphExecutor<B>::with_optimization(bool)`
900/// coupling reflected its per-typed-entry API surface
901/// (`realize_f32`, etc.) where the convenience of a single
902/// optimize+realize call was load-bearing. The pipelined path has
903/// a uniform `realize_many` that composes trivially — no need to
904/// re-couple them at the executor layer. This keeps the executor
905/// focused on execution and lets the optimizer evolve
906/// independently (different rule registries per call site, mid-
907/// pipeline opt+realize+opt+realize patterns, etc.).
908pub struct PipelinedExecutor;
909
910impl PipelinedExecutor {
911    /// Realize `target` and every transitive dependency. Compilation
912    /// runs in a worker thread; execution runs on the calling
913    /// thread.
914    ///
915    /// Pre-conditions:
916    ///
917    /// - Every reachable `Op::Const` node must have a corresponding
918    ///   entry in `inputs` (pre-realized Storage Arc).
919    /// - Every reachable non-`Const` node must have its
920    ///   `target_backend` set in the graph
921    ///   (`Graph::set_target_backend`).
922    /// - The op + dtype must be registered in `global_bindings()`.
923    ///
924    /// Returns the realized `Storage` Arc for `target` plus its
925    /// resolved [`Layout`]. The Layout is contiguous for kernel
926    /// outputs; for graphs whose target is a metadata-only view
927    /// op (`Op::Transpose`, `Op::Permute`, `Op::BroadcastTo`),
928    /// the returned Storage shares its bytes with an upstream
929    /// node and the Layout encodes the view's strides + offset.
930    ///
931    /// Production-correct: errors on any unmet precondition rather
932    /// than panicking.
933    pub fn realize(
934        graph: Arc<RwLock<Graph>>,
935        target: NodeId,
936        inputs: StorageCache,
937    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
938        Self::realize_inner(
939            graph,
940            target,
941            inputs,
942            OrderSource::Default,
943            SymEnv::default(),
944        )
945        .map(|(s, l, _produced)| (s, l))
946    }
947
948    /// Env-carrying sibling of [`Self::realize`]: realize `target` with a
949    /// per-pass [`SymEnv`] supplying the runtime bindings for any
950    /// `DynScalar` op params (today: `Op::WriteSlice`'s dynamic start
951    /// offset — Phase D symbolic extents). An **empty** env is
952    /// byte-identical to [`Self::realize`]; the env is consulted only by ops
953    /// that carry a `DynScalar`, so a graph with none ignores it. Uses
954    /// the default (`execution_plan`) dispatch order.
955    ///
956    /// This is the input-determined path for persistent decode (the
957    /// per-token `cached_len` write offset). The `OptimizedGraph`/route
958    /// entry points keep an empty env until the realize bridge threads
959    /// the session env through them (Phase D step 1, fuel-core).
960    pub fn realize_with_env(
961        graph: Arc<RwLock<Graph>>,
962        target: NodeId,
963        inputs: StorageCache,
964        sym_env: SymEnv,
965    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
966        Self::realize_inner(graph, target, inputs, OrderSource::Default, sym_env)
967            .map(|(s, l, _produced)| (s, l))
968    }
969
970    /// Producing sibling of [`Self::realize_with_env`]: realize `target` and
971    /// ALSO return the [`SymEnv`] of symbols BOUND BY PRODUCERS during the
972    /// pass — data-determined dynamic shapes (`Op::NonZeroIndices`'s
973    /// runtime count, etc.). The returned env is disjoint from the input
974    /// `sym_env` (which is input-determined). Empty when the graph has no
975    /// data-determined producer. This is the observation point for the
976    /// increment-2a bind seam; a downstream pass consumes these bindings
977    /// as dynamic extents.
978    pub fn realize_with_env_producing(
979        graph: Arc<RwLock<Graph>>,
980        target: NodeId,
981        inputs: StorageCache,
982        sym_env: SymEnv,
983    ) -> Result<(Arc<RwLock<Storage>>, Layout, SymEnv)> {
984        Self::realize_inner(graph, target, inputs, OrderSource::Default, sym_env)
985    }
986
987    /// PR-A3b-1 entry: realize `target` driven from `optimized`'s
988    /// in-place "plan IS the graph" form. The dispatch order is the
989    /// `OptimizedGraph`'s `extract_runs`/`lower_run` lowering (computed
990    /// after safety-copy insertion); per-node kernel resolution uses
991    /// the binding-table-lookup path (no `ExecutionPlan`, so no
992    /// route-picking — A3b-1 is branchless). The optimize-time
993    /// `generation` drives the `TopologyChanged` chunk-boundary check.
994    ///
995    /// Pre-conditions match [`Self::realize`]: every reachable kernel-bearing
996    /// node must have `target_backend` set (the bridge's
997    /// `stamp_plan_backends` does this) and a registered binding.
998    pub fn realize_with_optimized(
999        graph: Arc<RwLock<Graph>>,
1000        target: NodeId,
1001        inputs: StorageCache,
1002        optimized: &OptimizedGraph,
1003    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1004        Self::realize_inner(
1005            graph,
1006            target,
1007            inputs,
1008            OrderSource::Optimized {
1009                optimized,
1010                route: None,
1011            },
1012            SymEnv::default(),
1013        )
1014        .map(|(s, l, _produced)| (s, l))
1015    }
1016
1017    /// Env-carrying sibling of [`Self::realize_with_optimized`]: same
1018    /// optimized-graph dispatch, but with a per-pass [`SymEnv`] supplying
1019    /// the runtime bindings for `DynScalar` op params (Phase D symbolic
1020    /// extents). An empty env is byte-identical to
1021    /// [`Self::realize_with_optimized`]. The realize bridge threads its session
1022    /// env through here for persistent decode.
1023    pub fn realize_with_optimized_env(
1024        graph: Arc<RwLock<Graph>>,
1025        target: NodeId,
1026        inputs: StorageCache,
1027        optimized: &OptimizedGraph,
1028        sym_env: SymEnv,
1029    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1030        Self::realize_inner(
1031            graph,
1032            target,
1033            inputs,
1034            OrderSource::Optimized {
1035                optimized,
1036                route: None,
1037            },
1038            sym_env,
1039        )
1040        .map(|(s, l, _produced)| (s, l))
1041    }
1042
1043    /// PR-C1 entry: realize `target` driven from `optimized`'s in-place
1044    /// form **following the runtime route picker's chosen arms**. The
1045    /// dispatch order is the route-aware lowering
1046    /// ([`fuel_graph::lower_picked_route`]) over `route` — the per-branch
1047    /// arm the picker (Picker 2) selected by live telemetry. A branch
1048    /// absent from `route` defaults to arm 0, so an **empty** route is
1049    /// byte-identical to [`Self::realize_with_optimized`] (the no-pressure /
1050    /// no-telemetry contract). A branchless graph has no branches ⇒ the
1051    /// route is empty ⇒ this is exactly the arm-0 path.
1052    pub fn realize_with_optimized_route(
1053        graph: Arc<RwLock<Graph>>,
1054        target: NodeId,
1055        inputs: StorageCache,
1056        optimized: &OptimizedGraph,
1057        route: &PickedRoute,
1058    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1059        Self::realize_inner(
1060            graph,
1061            target,
1062            inputs,
1063            OrderSource::Optimized {
1064                optimized,
1065                route: Some(route),
1066            },
1067            SymEnv::default(),
1068        )
1069        .map(|(s, l, _produced)| (s, l))
1070    }
1071
1072    /// Env-carrying sibling of [`Self::realize_with_optimized_route`] — the
1073    /// route-aware lowering with a per-pass [`SymEnv`]. An empty env is
1074    /// byte-identical to [`Self::realize_with_optimized_route`].
1075    pub fn realize_with_optimized_route_env(
1076        graph: Arc<RwLock<Graph>>,
1077        target: NodeId,
1078        inputs: StorageCache,
1079        optimized: &OptimizedGraph,
1080        route: &PickedRoute,
1081        sym_env: SymEnv,
1082    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1083        Self::realize_inner(
1084            graph,
1085            target,
1086            inputs,
1087            OrderSource::Optimized {
1088                optimized,
1089                route: Some(route),
1090            },
1091            sym_env,
1092        )
1093        .map(|(s, l, _produced)| (s, l))
1094    }
1095
1096    /// Cleanup Step C/D entry: the executor OWNS `Op::Branch` arm-selection.
1097    /// Given the runtime `selector` + the live device `lookup` (built
1098    /// bridge-side from the realize `Device` + Judge oracle and passed in,
1099    /// exactly like the residency `input_residency` provider), pick one arm per
1100    /// branch HERE — at dispatch — then lower the chosen route. The per-arm
1101    /// candidates are re-enumerated from the runtime binding registry + the
1102    /// graph (Step D: no threaded `ExecutionPlan`).
1103    ///
1104    /// No `selector` (the `runtime_selector_disabled()` opt-out) or a
1105    /// branchless graph yields the arm-0 lowering, byte-identical to Phase B.
1106    ///
1107    /// **Step E Phase C, PR C1 — STREAMING.** A branched graph WITH a
1108    /// selector no longer resolves the whole route up front: it routes to the
1109    /// `OrderSource::Streaming` walk, where the compiler thread resolves
1110    /// each branch lazily as the frontier reaches it (via
1111    /// [`resolve_branch`]). C1 keeps the SAME VRAM-only selector chain — so
1112    /// the streamed route equals the one-shot `pick_route` route byte-for-byte
1113    /// — and only changes *when* a branch resolves; C2 makes that moment read
1114    /// live device load.
1115    pub fn realize_with_optimized_picking_env(
1116        graph: Arc<RwLock<Graph>>,
1117        target: NodeId,
1118        inputs: StorageCache,
1119        optimized: &OptimizedGraph,
1120        selector: Option<Arc<dyn RuntimeSelector>>,
1121        lookup: Option<BackendRuntimeLookup>,
1122        sym_env: SymEnv,
1123    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1124        match Self::streaming_pick_for(&graph, selector, lookup)? {
1125            Some(pick) => Self::realize_inner(
1126                graph,
1127                target,
1128                inputs,
1129                OrderSource::Streaming {
1130                    optimized,
1131                    pick: &pick,
1132                },
1133                sym_env,
1134            )
1135            .map(|(s, l, _produced)| (s, l)),
1136            None => Self::realize_with_optimized_env(graph, target, inputs, optimized, sym_env),
1137        }
1138    }
1139
1140    /// `order_for` fix track (`docs/design/incremental-consumer-index.md`
1141    /// §5) — cached sibling of [`Self::realize_with_optimized_picking_env`]
1142    /// for the plan-once persistent-decode caller, which holds `order_cache`
1143    /// across repeated calls over the SAME graph/roots/`optimized` (one per
1144    /// `DecodeSession`, initially empty). On a branchless graph with no
1145    /// selector pressure (the decode shape this is for), dispatches via
1146    /// [`OrderSource::Cached`] — computed once, reused thereafter, see that
1147    /// variant's doc for the safety argument. A branched graph WITH a
1148    /// resolved selector pick falls back to the uncached
1149    /// [`OrderSource::Streaming`] path unchanged (caching doesn't apply
1150    /// there; this entry point exists for the decode case, not to cover
1151    /// every caller).
1152    pub fn realize_with_optimized_picking_cached_env(
1153        graph: Arc<RwLock<Graph>>,
1154        target: NodeId,
1155        inputs: StorageCache,
1156        optimized: &OptimizedGraph,
1157        order_cache: &std::sync::OnceLock<Vec<NodeId>>,
1158        selector: Option<Arc<dyn RuntimeSelector>>,
1159        lookup: Option<BackendRuntimeLookup>,
1160        sym_env: SymEnv,
1161    ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1162        match Self::streaming_pick_for(&graph, selector, lookup)? {
1163            Some(pick) => Self::realize_inner(
1164                graph,
1165                target,
1166                inputs,
1167                OrderSource::Streaming {
1168                    optimized,
1169                    pick: &pick,
1170                },
1171                sym_env,
1172            )
1173            .map(|(s, l, _produced)| (s, l)),
1174            None => Self::realize_inner(
1175                graph,
1176                target,
1177                inputs,
1178                OrderSource::Cached {
1179                    optimized,
1180                    order_cache,
1181                },
1182                sym_env,
1183            )
1184            .map(|(s, l, _produced)| (s, l)),
1185        }
1186    }
1187
1188    /// Build the [`StreamingPick`] for the executor's picking entry points —
1189    /// the streaming successor of the one-shot `pick_route_for` (which was the
1190    /// relocated body of the bridge's old `resolve_runtime_route`). Returns
1191    /// `None` (⇒ arm-0 lowering, the untouched `route: None` /
1192    /// `dispatch_order` path) when there is no `selector` (the disabled
1193    /// opt-out) or the graph has no `Op::Branch` (the branchless fast-path:
1194    /// skip the streaming machinery entirely so the common case is unchanged
1195    /// and byte-identical). Otherwise it carries the selector + lookup into
1196    /// the [`OrderSource::Streaming`] walk, which resolves each branch at the
1197    /// frontier (PR C1). The per-arm candidates are re-enumerated from the
1198    /// runtime binding registry inside the compiler thread (Step D — was the
1199    /// threaded `ExecutionPlan`).
1200    fn streaming_pick_for(
1201        graph: &Arc<RwLock<Graph>>,
1202        selector: Option<Arc<dyn RuntimeSelector>>,
1203        // Accepted for call-site parity with `pick_route` but not stored: the
1204        // streaming walk reads VRAM + load through the selector, which holds its
1205        // own `BackendRuntimeLookup`.
1206        _lookup: Option<BackendRuntimeLookup>,
1207    ) -> Result<Option<StreamingPick>> {
1208        let Some(selector) = selector else {
1209            return Ok(None);
1210        };
1211        let g = graph
1212            .read()
1213            .map_err(|_| Error::Msg("graph lock poisoned".into()).bt())?;
1214        let has_branch = (0..g.len()).any(|i| matches!(g.node(NodeId(i)).op, Op::Branch { .. }));
1215        if !has_branch {
1216            return Ok(None);
1217        }
1218        Ok(Some(StreamingPick { selector }))
1219    }
1220
1221    /// Capture a single-device CUDA decode step's kernel launches into a
1222    /// replayable [`CapturedRun`] (the CapturedRun executor build-out).
1223    ///
1224    /// Runs the graph TWICE against fixed-address buffers:
1225    ///   1. **Warm (record)**: realize normally into a fresh cache cloned
1226    ///      from `inputs`, recording every compute node's output Arc as its
1227    ///      fixed capture buffer, and synchronizing so the result is stable.
1228    ///   2. **Capture (reuse)**: re-issue the same launch sequence — each
1229    ///      node writing INTO its recorded buffer via the write-into kernel
1230    ///      path (no allocation) — inside [`CudaDevice::capture_run`],
1231    ///      producing an instantiated [`CapturedRun`].
1232    ///
1233    /// Both passes share the SAME input Arcs (cloned map, shared buffers)
1234    /// and the SAME recorded output buffers, so every device address the
1235    /// captured graph bakes in is stable for replay. To replay a later
1236    /// token: overwrite the fixed input buffers in place (H2D), call
1237    /// `run.replay(dev)`, then read `output`.
1238    ///
1239    /// Contract (surfaced as `Err`, never a panic or a mid-capture alloc):
1240    ///   - CUDA-only, single device: every compute node targets `Cuda`;
1241    ///     no cross-device `Op::Copy`/`Move` in the graph.
1242    ///   - Every compute node's op must have a write-into CUDA wrapper
1243    ///     (`op_kind_is_capture_writeinto`); an unsupported op surfaces a
1244    ///     clear capability gap rather than silently breaking the capture.
1245    ///   - Inputs must be contiguous (no auto-Contiguize alloc in capture).
1246    #[cfg(feature = "cuda")]
1247    pub fn capture_decode(
1248        graph: Arc<RwLock<Graph>>,
1249        target: NodeId,
1250        inputs: StorageCache,
1251        sym_env: SymEnv,
1252    ) -> Result<CapturedDecode> {
1253        use fuel_memory::BackendStorage;
1254
1255        // Front matter mirrors realize_inner: safety copies + frozen order.
1256        {
1257            let mut g = graph.write().map_err(|_| poisoned("graph lock"))?;
1258            let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1259            insert_safety_copies(&mut g, &effective_roots);
1260        }
1261        let (compiler_work, _wait_set, seed_layouts) = {
1262            let g = graph.read().map_err(|_| poisoned("graph lock"))?;
1263            let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1264            let (work, wait_set) =
1265                compiler_work_and_wait_set_for(&g, &effective_roots, &OrderSource::Default);
1266            let mut layouts: HashMap<NodeId, Layout> = HashMap::with_capacity(inputs.len());
1267            for &id in inputs.keys() {
1268                layouts.insert(id, g.layout(id));
1269            }
1270            (work, wait_set, layouts)
1271        };
1272
1273        // Compile every WorkItem once (one compiler thread) and collect them
1274        // so both passes replay the identical sequence.
1275        let (tx, rx) = channel::<Result<WorkItem>>();
1276        let graph_for_compiler = Arc::clone(&graph);
1277        let compiler = thread::spawn(move || {
1278            compiler_thread_body(graph_for_compiler, compiler_work, sym_env, tx);
1279        });
1280        let mut items: Vec<WorkItem> = Vec::new();
1281        for item in rx {
1282            items.push(item?);
1283        }
1284        compiler
1285            .join()
1286            .map_err(|_| Error::Msg("compiler thread panicked".to_string()).bt())?;
1287
1288        // CapturedRun 4b Step-4 diagnostic: name every kernel placed OFF Cuda
1289        // (an unseeded / audited-loser op that forces a cross-device copy and
1290        // blocks capture), so verification-ledger seeding can target it
1291        // precisely. Fires only when a capture is attempted AND something is
1292        // mis-placed — otherwise silent.
1293        {
1294            let off: Vec<String> = items
1295                .iter()
1296                .filter_map(|it| match &it.kind {
1297                    WorkItemKind::Kernel if it.target_backend != BackendId::Cuda => {
1298                        let desc = it
1299                            .compiled
1300                            .as_ref()
1301                            .map(|c| format!("{:?} dtypes={:?}", c.op, c.dtypes))
1302                            .unwrap_or_else(|| "?".to_string());
1303                        Some(format!(
1304                            "node {:?}: {desc} on {:?}",
1305                            it.node_id, it.target_backend
1306                        ))
1307                    }
1308                    _ => None,
1309                })
1310                .collect();
1311            if !off.is_empty() {
1312                eprintln!(
1313                    "[capture-diag] {} kernel(s) placed OFF Cuda (these block capture): {}",
1314                    off.len(),
1315                    off.join("; "),
1316                );
1317            }
1318        }
1319
1320        // Capture-safety validation — surface every gap up front, before any
1321        // launch, so a capture scope is never entered on an unsupported graph.
1322        for item in &items {
1323            match &item.kind {
1324                // Op::Move: the destructive half (releasing the source) is
1325                // driven by the realize loop's `destructive_input` cleanup,
1326                // which runs once per REALIZE call — not once per `replay()`.
1327                // A captured graph's `replay()` re-executes the same baked
1328                // launches repeatedly without re-running that surrounding
1329                // cleanup between replays, so whether a repeat-replayed Move
1330                // is even meaningful is unclear; out of scope here (4b-ζ) —
1331                // keep rejecting every Move unconditionally.
1332                WorkItemKind::Move { .. } => {
1333                    return Err(Error::Msg(format!(
1334                        "capture_decode: graph has an Op::Move at node {:?}; \
1335                         Move's destructive source-release half is driven by \
1336                         per-realize cleanup, not per-replay, so capture is \
1337                         not supported for Move (out of scope — see the \
1338                         `capture_decode` validation loop's Move arm).",
1339                        item.node_id,
1340                    ))
1341                    .bt());
1342                }
1343                // Op::Copy: a same-device CUDA→CUDA copy (e.g. an
1344                // `insert_safety_copies` snapshot) has a write-into wrapper
1345                // (`copy_from_cuda_wrapper`'s `BackendStorage::Cuda` branch,
1346                // via `CudaStorageBytes::copy_from_device`) and is
1347                // capture-safe. A copy whose `target_location` is `Cpu` or
1348                // `Vulkan` is a genuine host round-trip / cross-backend
1349                // transfer and can never be part of a CUDA graph capture —
1350                // keep rejecting those. (`gpu_id` is not cross-checked
1351                // against the capture device, matching the `Kernel` arm's
1352                // `target_backend` check just above: today's decode graphs
1353                // are single-GPU only.)
1354                WorkItemKind::Copy { target_location } => {
1355                    if !matches!(target_location, DeviceLocation::Cuda { .. }) {
1356                        return Err(Error::Msg(format!(
1357                            "capture_decode: graph has a cross-device Op::Copy at \
1358                             node {:?} (target {:?}); capture requires a \
1359                             single-device CUDA decode step — only same-device \
1360                             CUDA→CUDA copies are capture-safe.",
1361                            item.node_id, target_location,
1362                        ))
1363                        .bt());
1364                    }
1365                }
1366                WorkItemKind::Kernel => {
1367                    if item.target_backend != BackendId::Cuda {
1368                        return Err(Error::Msg(format!(
1369                            "capture_decode: kernel node {:?} targets {:?}, not Cuda",
1370                            item.node_id, item.target_backend,
1371                        ))
1372                        .bt());
1373                    }
1374                    let compiled = item.compiled.as_ref().ok_or_else(|| {
1375                        Error::Msg(format!(
1376                            "capture_decode: kernel node {:?} has no compiled op",
1377                            item.node_id,
1378                        ))
1379                        .bt()
1380                    })?;
1381                    let op = compiled.op;
1382                    if !op_kind_is_capture_writeinto(op) {
1383                        return Err(Error::Msg(format!(
1384                            "capture_decode: op {:?} (node {:?}) has no write-into CUDA \
1385                             wrapper yet — capture unsupported for this op. Extend \
1386                             op_kind_is_capture_writeinto once its write-into variant lands.",
1387                            op, item.node_id,
1388                        ))
1389                        .bt());
1390                    }
1391                    // Concat's capture-safety is conditional on arity — N==2
1392                    // has a write-into path (concat2_into), N>2 does not (its
1393                    // wrapper falls back to an allocating N-1 chain). This
1394                    // can't be expressed by op_kind_is_capture_writeinto (a
1395                    // pure OpKind -> bool predicate with no OpParams access),
1396                    // so reject N>2 explicitly here, BEFORE the warm pass —
1397                    // see op_kind_is_capture_writeinto's doc comment.
1398                    if op == OpKind::Concat {
1399                        if let OpParams::Concat {
1400                            input_dim_sizes, ..
1401                        } = &compiled.op_params
1402                        {
1403                            if input_dim_sizes.len() != 2 {
1404                                return Err(Error::Msg(format!(
1405                                    "capture_decode: Concat node {:?} has {} inputs; only \
1406                                     2-input concat (the rope rotate-half shape) has a \
1407                                     write-into CUDA path today — N-ary (N>2) concat would \
1408                                     allocate mid-capture.",
1409                                    item.node_id,
1410                                    input_dim_sizes.len(),
1411                                ))
1412                                .bt());
1413                            }
1414                        }
1415                    }
1416                }
1417                // View / adopt / slot kinds: no alloc, no launch — capture-safe.
1418                _ => {}
1419            }
1420        }
1421
1422        // ---- Pass 1: warm (record) — fixed buffers + a synchronized result --
1423        let mut persistent = PersistentOutputs::recording();
1424        let mut warm_cache: StorageCache = inputs.clone();
1425        let mut warm_layouts = seed_layouts.clone();
1426        let mut warm_syms = SymEnv::new();
1427        for item in &items {
1428            let _handle = execute_work_item(
1429                item,
1430                &mut warm_cache,
1431                &mut warm_layouts,
1432                &mut warm_syms,
1433                false,
1434                Some(&mut persistent),
1435            )
1436            .map_err(|e| with_node_location(&graph, item.node_id, e))?;
1437        }
1438        // Flush all warm launches before capture (device idle; result stable).
1439        sync_active_cuda_devices(&warm_cache)?;
1440
1441        let output = warm_cache.get(&target).cloned().ok_or_else(|| {
1442            Error::Msg(format!(
1443                "capture_decode: target {:?} not realized in warm run",
1444                target,
1445            ))
1446            .bt()
1447        })?;
1448        let output_layout = warm_layouts.get(&target).cloned().ok_or_else(|| {
1449            Error::Msg(format!("capture_decode: target {:?} has no layout", target)).bt()
1450        })?;
1451
1452        // Derive the CUDA device from the target's storage for capture_run.
1453        let device = {
1454            let guard = output.read().map_err(|_| poisoned("output storage"))?;
1455            match &guard.inner {
1456                BackendStorage::Cuda(c) => c.device().clone(),
1457                other => {
1458                    return Err(Error::Msg(format!(
1459                        "capture_decode: target {:?} is BackendStorage::{:?}, not Cuda",
1460                        target,
1461                        std::mem::discriminant(other),
1462                    ))
1463                    .bt());
1464                }
1465            }
1466        };
1467
1468        // ---- Pass 2: capture (reuse) — record the launch sequence ----------
1469        let mut reuse = PersistentOutputs::reuse(persistent.into_map());
1470        // Retain EVERY input buffer before `inputs` is consumed: the captured
1471        // graph bakes their device addresses, so they must outlive it even if
1472        // the caller doesn't independently hold them.
1473        let retained_inputs: Vec<Arc<RwLock<Storage>>> = inputs.values().cloned().collect();
1474        let mut cap_cache: StorageCache = inputs;
1475        let mut cap_layouts = seed_layouts;
1476        let mut cap_syms = SymEnv::new();
1477        let mut inner_err: Option<Error> = None;
1478        let items_ref = &items;
1479        let graph_ref = &graph;
1480        let run = device.capture_run(|_stream| {
1481            // Inside the capture scope: NO device alloc, NO host sync. Each
1482            // kernel launches on device.stream() (now capturing) and writes
1483            // into its reused fixed buffer.
1484            for item in items_ref {
1485                match execute_work_item(
1486                    item,
1487                    &mut cap_cache,
1488                    &mut cap_layouts,
1489                    &mut cap_syms,
1490                    false,
1491                    Some(&mut reuse),
1492                ) {
1493                    Ok(_handle) => {}
1494                    Err(e) => {
1495                        inner_err = Some(with_node_location(graph_ref, item.node_id, e));
1496                        break;
1497                    }
1498                }
1499            }
1500            Ok(())
1501        })?;
1502        if let Some(e) = inner_err {
1503            return Err(e);
1504        }
1505
1506        Ok(CapturedDecode {
1507            run,
1508            output,
1509            output_layout,
1510            retained_inputs,
1511            persistent: reuse.into_map(),
1512        })
1513    }
1514
1515    fn realize_inner(
1516        graph: Arc<RwLock<Graph>>,
1517        target: NodeId,
1518        inputs: StorageCache,
1519        order_source: OrderSource<'_>,
1520        sym_env: SymEnv,
1521    ) -> Result<(Arc<RwLock<Storage>>, Layout, SymEnv)> {
1522        // Auto-insert safety copies for in-place ops whose target
1523        // has additional readers in this realize set (residual-
1524        // connection cycle break). No-op when no destructive ops
1525        // are present.
1526        {
1527            let _t = crate::compiled::op_profile::PhaseTimer::start("realize.safety_copies");
1528            let mut g = graph.write().map_err(|_| poisoned("graph lock"))?;
1529            let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1530            insert_safety_copies(&mut g, &effective_roots);
1531        }
1532
1533        // Execution plan + initial layouts for the input cache
1534        // entries, computed on the calling thread to keep the
1535        // compiler thread free of graph-locking responsibilities.
1536        // Side-effect roots (Op::Print, Op::Save, etc.) are merged
1537        // into the walk so their effects fire even when not
1538        // reachable from the user's `target`. `execution_plan`
1539        // integrates `derive_ordering`'s view-aware pinning so
1540        // destructive ops run AFTER non-destructive readers of
1541        // their targets.
1542        let (compiler_work, wait_set, mut layout_cache): (
1543            CompilerWork,
1544            WaitSet,
1545            HashMap<NodeId, Layout>,
1546        ) = {
1547            let _t = crate::compiled::op_profile::PhaseTimer::start("realize.plan_order");
1548            let g = graph.read().map_err(|_| poisoned("graph lock"))?;
1549            let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1550            // PR-A3b-1: the OptimizedGraph path lowers via
1551            // `extract_runs`/`lower_run` (computed here, AFTER
1552            // `insert_safety_copies`, so the order covers freshly
1553            // inserted safety-copy nodes). The default path keeps the
1554            // pre-A3b-1 `execution_plan` walk. On a branchless graph the
1555            // two sequences are identical (A3a equivalence gate).
1556            //
1557            // PR-C1: a `Streaming` source is NOT flattened here — it is
1558            // handed to the compiler thread as a `CompilerWork::Streaming`
1559            // spec so the thread resolves each branch lazily at the
1560            // frontier (see `compiler_thread_body`).
1561            //
1562            // Step E A4b open question #4: the SAME pass also derives the
1563            // `WaitSet` from the materialized order ("event only where
1564            // waited" — see `compiler_work_and_wait_set_for`).
1565            let (work, wait_set) =
1566                compiler_work_and_wait_set_for(&g, &effective_roots, &order_source);
1567            let mut layouts = HashMap::with_capacity(inputs.len());
1568            for &id in inputs.keys() {
1569                layouts.insert(id, g.layout(id));
1570            }
1571            (work, wait_set, layouts)
1572        };
1573
1574        let (tx, rx) = channel::<Result<WorkItem>>();
1575        let graph_for_compiler = Arc::clone(&graph);
1576
1577        // Compiler thread: read graph nodes, resolve kernels,
1578        // push WorkItems. On error, push the error and bail. PR-C1: for a
1579        // `Streaming` work spec the thread also resolves each branch's arm
1580        // at the frontier (the SAME VRAM-only selector the one-shot picker
1581        // uses) before compiling that arm's runs.
1582        let compiler = {
1583            let _t = crate::compiled::op_profile::PhaseTimer::start("realize.spawn_compiler");
1584            thread::spawn(move || {
1585                let _t = crate::compiled::op_profile::PhaseTimer::start(
1586                    "compiler_thread (runs concurrently)",
1587                );
1588                compiler_thread_body(graph_for_compiler, compiler_work, sym_env, tx);
1589            })
1590        };
1591
1592        // Executor on this thread: consume WorkItems, gather
1593        // inputs from the cache, allocate outputs, call kernels,
1594        // populate the cache. After each destructive op
1595        // (Op::Release / Op::Move) evict the destroyed input from
1596        // the cache, unless it's the realize target.
1597        //
1598        // Phase 4.3: at each dispatch-chunk boundary (target_backend
1599        // change), check the live SystemTopology generation against
1600        // the plan's stamped generation. Mismatch surfaces
1601        // `Error::TopologyChanged`; the realize layer
1602        // (`pipelined_bridge`) catches it, rebuilds the plan against
1603        // the fresh topology, and retries.
1604        let plan_generation: Option<u64> = generation_for(&order_source);
1605        let mut current_chunk_backend: Option<BackendId> = None;
1606        let mut cache: StorageCache = inputs;
1607        // Data-dependent dynamic-shapes: symbols BOUND BY PRODUCERS during
1608        // this pass (e.g. `Op::NonZeroIndices`'s runtime count). Distinct
1609        // from the compile-time `sym_env` (input-determined, moved into the
1610        // compiler thread): these become known only as ops execute. Returned
1611        // to the caller so a downstream pass can consume them.
1612        let mut produced_syms = SymEnv::new();
1613        // Step E A4b-1: per-node async-completion handles. Each producing node's
1614        // `execute_compiled` returns a `CompletionHandle` (CUDA → a recorded
1615        // `Event`; CPU/Vulkan → `Ready`) instead of being `wait`ed inline. We
1616        // store them here, wait the SOURCE producer's handle before a
1617        // cross-device copy (A4b-3, below), and drain whatever remains at
1618        // realize-end.
1619        let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
1620        // Step E A4b-4: eagerly-submitted-but-not-yet-waited Vulkan batches. The
1621        // OVERLAP enabler — when we LEAVE a Vulkan chunk (or are about to block on
1622        // a CUDA copy) we `submit_pending` the open Vulkan batch so the iGPU runs
1623        // it while the executor records/dispatches the next (CUDA) chunk, instead
1624        // of letting it sit merely-recorded until realize-end (the A4b-2 behavior,
1625        // which never overlapped). Each submitted batch is tracked here and waited
1626        // at the in-flight-lifetime guard (before a Vulkan read / Vulkan-referenced
1627        // eviction / realize-end). `multi_backend` gates ALL eager submits so a
1628        // single-device realize never touches this path (byte-identical to A4b-2).
1629        let mut inflight_vulkan: Vec<InflightVulkan> = Vec::new();
1630        // Step E A4b-4: set true the first time the dispatch order transitions
1631        // between two DIFFERENT non-CPU... actually any two distinct backends — a
1632        // genuine backend switch. On a pure-CUDA or pure-Vulkan graph there is no
1633        // switch, so this stays false and the eager-submit / in-flight machinery is
1634        // unreachable (single-device byte-identical + throughput-neutral, §5).
1635        let mut multi_backend = false;
1636        let loop_total = crate::compiled::op_profile::PhaseTimer::start("realize.loop_total");
1637        loop {
1638            let t_wait =
1639                crate::compiled::op_profile::PhaseTimer::start("realize.wait_for_compiler");
1640            let Ok(item) = rx.recv() else {
1641                break;
1642            };
1643            drop(t_wait);
1644            let item = item?;
1645            // Chunk-boundary hook (target_backend change). Existing duty: the
1646            // `TopologyChanged` generation check. Step E A4b-4 adds two duties:
1647            // (1) detect a genuine backend switch → arm `multi_backend`; (2) when
1648            // LEAVING a Vulkan chunk on a multi-backend graph, eager-submit the
1649            // open Vulkan batch so the iGPU starts it concurrently with the chunk
1650            // we are about to dispatch (the overlap win).
1651            if !is_backend_neutral(&item.kind) && current_chunk_backend != Some(item.target_backend)
1652            {
1653                if let Some(plan_gen) = plan_generation {
1654                    let live = crate::dispatch::topology_generation();
1655                    if live != plan_gen {
1656                        return Err(Error::TopologyChanged {
1657                            plan_generation: plan_gen,
1658                            current_generation: live,
1659                        }
1660                        .bt());
1661                    }
1662                }
1663                let leaving = current_chunk_backend;
1664                if let Some(prev) = leaving
1665                    && prev != item.target_backend
1666                {
1667                    // A real backend switch occurred — this realize is
1668                    // genuinely multi-backend.
1669                    multi_backend = true;
1670                    // Leaving a Vulkan chunk → submit it NOW so it runs on the
1671                    // iGPU while we record/dispatch the next chunk. Whole-chunk
1672                    // granularity (the open batch IS the just-finished Vulkan
1673                    // chunk with all its intra-CB barriers) — UAF/race-safe per
1674                    // `eager_submit_all_vulkan`'s contract.
1675                    if prev == BackendId::Vulkan {
1676                        {
1677                            let _pt = crate::compiled::op_profile::PhaseTimer::start(
1678                                "realize.eager_submit_vulkan",
1679                            );
1680                            eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
1681                        }
1682                    }
1683                }
1684                current_chunk_backend = Some(item.target_backend);
1685            }
1686            // Vulkan realizes run the async path (deferred eviction, size-based eager
1687            // submit) even on a single device: ordering between batches is structural
1688            // (inter-batch barrier + epoch-gated buffer reuse), not a host wait.
1689            if item.target_backend == BackendId::Vulkan && matches!(item.kind, WorkItemKind::Kernel)
1690            {
1691                multi_backend = true;
1692            }
1693            // Same-device cross-chunk RAW/WAR safety between Vulkan batches (the old
1694            // "Invariant E": wait every in-flight Vulkan batch on the HOST before
1695            // recording a new Vulkan op) is now structural: the Vulkan recorder
1696            // starts every batch with a memory barrier whose first scope covers all
1697            // earlier submissions on the queue (`Recorder::record_inter_batch_barrier`),
1698            // so a later batch cannot read or overwrite what an earlier in-flight
1699            // one is still using. The host therefore keeps recording while the
1700            // GPU runs the batches already submitted; waits remain only where the
1701            // host needs a result (D2H reads), before freeing a buffer, and at
1702            // realize-end.
1703            // Step E A4b-3 + A4b-4: the cross-device residency boundary.
1704            if matches!(
1705                item.kind,
1706                WorkItemKind::Copy { .. } | WorkItemKind::Move { .. }
1707            ) {
1708                // A4b-4: before we (possibly) block the host on the CUDA producer,
1709                // eager-submit any open Vulkan chunk so the iGPU runs it WHILE we
1710                // wait on CUDA (the §5.1 ordering — submit Vulkan *before* draining
1711                // CUDA). Cheap + safe; gated to multi-backend (no-op single-device).
1712                if multi_backend {
1713                    {
1714                        let _pt = crate::compiled::op_profile::PhaseTimer::start(
1715                            "realize.eager_submit_vulkan",
1716                        );
1717                        eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
1718                    }
1719                }
1720                if let Some(&producer) = item.inputs.first() {
1721                    // A4b-4: a VULKAN-source D2H must read COMPLETED data, and the
1722                    // Vulkan download path only flushes the OPEN batch (not our
1723                    // in-flight ones) — so wait the in-flight Vulkan batches here.
1724                    // For a CUDA/CPU source we do NOT wait Vulkan (the copy reads no
1725                    // Vulkan buffer); waiting it would needlessly serialize the
1726                    // independent Vulkan sub-DAG and kill the overlap. The Vulkan
1727                    // work that was eager-submitted at the chunk boundary keeps
1728                    // running concurrently until ITS result is the one being copied.
1729                    if multi_backend && copy_source_is_vulkan(&cache, producer)? {
1730                        {
1731                            let _pt = crate::compiled::op_profile::PhaseTimer::start(
1732                                "realize.drain_inflight_vulkan",
1733                            );
1734                            drain_inflight_vulkan(&mut inflight_vulkan)?;
1735                        }
1736                    }
1737                    // Step E A4b-3: FINER CUDA source-drain — wait ONLY the source
1738                    // producer's recorded event (not the whole source device), so
1739                    // the OTHER sub-DAG's independent in-flight CUDA work keeps
1740                    // running. No-op for a Vulkan/CPU producer (no CUDA handle).
1741                    wait_producer_handle(&mut handles, producer)?;
1742                }
1743            }
1744            let t_exec =
1745                crate::compiled::op_profile::PhaseTimer::start(work_item_kind_name(&item.kind));
1746            let handle = execute_work_item(
1747                &item,
1748                &mut cache,
1749                &mut layout_cache,
1750                &mut produced_syms,
1751                will_be_waited(&wait_set, item.node_id),
1752                // No persistent-output mode on the standard realize paths —
1753                // fresh alloc per node, byte-identical to pre-CapturedRun.
1754                None,
1755            )
1756            .map_err(|e| with_node_location(&graph, item.node_id, e))?;
1757            drop(t_exec);
1758            store_handle(&mut handles, item.node_id, handle);
1759            if let Some(d_idx) = item.destructive_input
1760                && let Some(&destroyed) = item.inputs.get(d_idx)
1761                && destroyed != target
1762            {
1763                // Step E A2.1 (in-flight-batch DATA-BUFFER lifetime — the
1764                // UAF guard, now DEFERRED-DELETION instead of a drain). A
1765                // `SubmittedBatch` owns its CB/descriptors/transients but
1766                // NOT the DATA buffers it reads (those live here in the
1767                // cache); recycling a data buffer an in-flight/open CB still
1768                // reads is a use-after-free. PRE-A2.1 we host-blocked here —
1769                // `drain_inflight_vulkan` (wait every in-flight batch) +
1770                // `force_flush` (submit+wait the open batch) — which stalled
1771                // the iGPU work we were overlapping with CUDA at EVERY such
1772                // eviction. A2.1 instead RETAINS the evicted buffer until the
1773                // reader fences signal: it eager-submits the open batch (no
1774                // wait → it becomes a fenced in-flight batch) and moves a
1775                // refcount clone of the buffer into every in-flight batch, so
1776                // the buffer frees POST-fence on each batch's `Drop`
1777                // (follow-on #2) with NO host block. Conservative over all
1778                // batches (no cheap buffer→batch map — the same reason the old
1779                // drain waited all of them). CUDA eviction is stream-ordered-
1780                // safe via A3. Single-device (multi_backend == false) keeps the
1781                // pre-A2.1 blocking force_flush of the OPEN batch (the else
1782                // branch) — byte-identical + UAF-safe there; the deferred path is
1783                // unsafe single-device (no eager-submit/in-flight tracking, and the
1784                // cross-batch RAW guard at :832 doesn't run — see
1785                // force_flush_vulkan's doc).
1786                let _t =
1787                    crate::compiled::op_profile::PhaseTimer::start("realize.evict_destroyed_input");
1788                if multi_backend {
1789                    defer_evicted_vulkan_buffer(&cache, destroyed, &mut inflight_vulkan)?;
1790                } else if let Some(d_arc) = cache.get(&destroyed) {
1791                    let _f = crate::compiled::op_profile::PhaseTimer::start(
1792                        "realize.evict_force_flush_vulkan",
1793                    );
1794                    force_flush_vulkan(d_arc)?;
1795                }
1796                cache.remove(&destroyed);
1797                layout_cache.remove(&destroyed);
1798                // Drop the evicted node's CUDA handle (if still present)
1799                // so the realize-end drain's empty-map assert stays
1800                // meaningful. The free itself is stream-ordered-safe (A3).
1801                handles.remove(&destroyed);
1802            }
1803        }
1804
1805        drop(loop_total);
1806        {
1807            let _t = crate::compiled::op_profile::PhaseTimer::start("realize.join_compiler");
1808            compiler
1809                .join()
1810                .map_err(|_| Error::Msg("compiler thread panicked".to_string()).bt())?;
1811        }
1812
1813        // Step E A4b-1: drain every outstanding async handle before the result is
1814        // read / the cache drops (freeing intermediates). For CUDA this waits the
1815        // recorded events (one stream/device ⇒ waiting the latest drains all prior
1816        // stream work too; see `drain_handles`).
1817        let post_loop = crate::compiled::op_profile::PhaseTimer::start("realize.post_loop_drains");
1818        drain_handles(&mut handles)?;
1819        // Step E A4b open question #4 — "event only where waited": most CUDA
1820        // nodes in this realize never got an Event (elided per the `WaitSet`),
1821        // so `drain_handles` above only retired the (typically few) tracked
1822        // handles. This blanket per-device `cuStreamSynchronize` flushes the
1823        // elided work too — safe + sufficient because of single-stream-per-
1824        // device ordering (see `sync_active_cuda_devices`'s doc).
1825        sync_active_cuda_devices(&cache)?;
1826        // Step E A4b-4: wait every eagerly-submitted (in-flight) Vulkan batch
1827        // before the cache drops, freeing the data buffers their CBs read. Empty
1828        // on a single-device realize (nothing was eager-submitted).
1829        {
1830            let _pt =
1831                crate::compiled::op_profile::PhaseTimer::start("realize.drain_inflight_vulkan");
1832            drain_inflight_vulkan(&mut inflight_vulkan)?;
1833        }
1834        // Step E A4b-2: drain all deferred Vulkan work before reading/returning
1835        // the result (and before the cache drops, freeing buffers). The A2
1836        // submit+wait is now SPLIT — `drain_vulkan_pending` submits the open
1837        // batch then waits it via a `VulkanCompletion` handle. Byte-identical to
1838        // A2 for pure-Vulkan (one submission at realize-end, just split).
1839        drain_vulkan_pending(&cache)?;
1840        debug_assert!(
1841            inflight_vulkan.is_empty(),
1842            "A4b-4: in-flight Vulkan batch list must be empty after realize-end drain",
1843        );
1844
1845        drop(post_loop);
1846        let storage = cache.remove(&target).ok_or_else(|| {
1847            Error::Msg(format!(
1848                "PipelinedExecutor::realize: target slot {:?} not populated after execution",
1849                target
1850            ))
1851            .bt()
1852        })?;
1853        let layout = layout_cache.remove(&target).ok_or_else(|| {
1854            Error::Msg(format!(
1855                "PipelinedExecutor::realize: target layout {:?} not populated after execution",
1856                target
1857            ))
1858            .bt()
1859        })?;
1860        {
1861            let _t = crate::compiled::op_profile::PhaseTimer::start("realize.drop_cache");
1862            drop(cache);
1863            drop(handles);
1864            drop(layout_cache);
1865        }
1866        Ok((storage, layout, produced_syms))
1867    }
1868
1869    /// Realize multiple targets in one walk. Each target's transitive
1870    /// dependency set is collapsed into a single execution plan
1871    /// (via [`execution_plan`]); shared subgraphs are evaluated
1872    /// once. Returns a `Vec<(Storage, Layout)>` whose order matches
1873    /// `targets`.
1874    ///
1875    /// This is Phase A of Phase 7.6 step 9c — feature-parity with the
1876    /// legacy `GraphExecutor::realize_many_f32`. Multi-session
1877    /// migration target ([memory: project_phase_7_6_step_9c_parity_audit.md]).
1878    ///
1879    /// Pre-conditions: every reachable `Op::Const` must be in
1880    /// `inputs`; every reachable non-`Const` must have its
1881    /// `target_backend` set; the op + dtype must be registered in
1882    /// `global_bindings()`. Same as single-target [`Self::realize`].
1883    pub fn realize_many(
1884        graph: Arc<RwLock<Graph>>,
1885        targets: &[NodeId],
1886        inputs: StorageCache,
1887    ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1888        Self::realize_many_inner(
1889            graph,
1890            targets,
1891            inputs,
1892            OrderSource::Default,
1893            SymEnv::default(),
1894        )
1895    }
1896
1897    /// Multi-target PR-A3b-1 entry — the `realize_many` sibling of
1898    /// [`Self::realize_with_optimized`]. Drives the executor from the
1899    /// `OptimizedGraph`'s run lowering via the binding-table-lookup
1900    /// path, with the optimize-time `generation` keying the
1901    /// `TopologyChanged` chunk-boundary check.
1902    pub fn realize_many_with_optimized(
1903        graph: Arc<RwLock<Graph>>,
1904        targets: &[NodeId],
1905        inputs: StorageCache,
1906        optimized: &OptimizedGraph,
1907    ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1908        Self::realize_many_inner(
1909            graph,
1910            targets,
1911            inputs,
1912            OrderSource::Optimized {
1913                optimized,
1914                route: None,
1915            },
1916            SymEnv::default(),
1917        )
1918    }
1919
1920    /// Env-carrying sibling of [`Self::realize_many_with_optimized`] (Phase D
1921    /// symbolic extents). An empty env is byte-identical to it.
1922    pub fn realize_many_with_optimized_env(
1923        graph: Arc<RwLock<Graph>>,
1924        targets: &[NodeId],
1925        inputs: StorageCache,
1926        optimized: &OptimizedGraph,
1927        sym_env: SymEnv,
1928    ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1929        Self::realize_many_inner(
1930            graph,
1931            targets,
1932            inputs,
1933            OrderSource::Optimized {
1934                optimized,
1935                route: None,
1936            },
1937            sym_env,
1938        )
1939    }
1940
1941    /// Multi-target PR-C1 entry — the `realize_many` sibling of
1942    /// [`Self::realize_with_optimized_route`]. Lowers each target's runs
1943    /// following the picker's chosen arms via
1944    /// [`fuel_graph::lower_picked_route`].
1945    pub fn realize_many_with_optimized_route(
1946        graph: Arc<RwLock<Graph>>,
1947        targets: &[NodeId],
1948        inputs: StorageCache,
1949        optimized: &OptimizedGraph,
1950        route: &PickedRoute,
1951    ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1952        Self::realize_many_inner(
1953            graph,
1954            targets,
1955            inputs,
1956            OrderSource::Optimized {
1957                optimized,
1958                route: Some(route),
1959            },
1960            SymEnv::default(),
1961        )
1962    }
1963
1964    /// Env-carrying sibling of [`Self::realize_many_with_optimized_route`]
1965    /// (Phase D symbolic extents). An empty env is byte-identical to it.
1966    pub fn realize_many_with_optimized_route_env(
1967        graph: Arc<RwLock<Graph>>,
1968        targets: &[NodeId],
1969        inputs: StorageCache,
1970        optimized: &OptimizedGraph,
1971        route: &PickedRoute,
1972        sym_env: SymEnv,
1973    ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1974        Self::realize_many_inner(
1975            graph,
1976            targets,
1977            inputs,
1978            OrderSource::Optimized {
1979                optimized,
1980                route: Some(route),
1981            },
1982            sym_env,
1983        )
1984    }
1985
1986    /// Multi-target sibling of [`Self::realize_with_optimized_picking_env`] —
1987    /// the executor resolves one arm per `Op::Branch` (cleanup Step C/D) over
1988    /// the effective targets. PR C1: a branched graph routes to the
1989    /// `OrderSource::Streaming` walk (lazy per-branch resolution at the
1990    /// frontier); branchless / no-selector falls to the untouched arm-0 path.
1991    pub fn realize_many_with_optimized_picking_env(
1992        graph: Arc<RwLock<Graph>>,
1993        targets: &[NodeId],
1994        inputs: StorageCache,
1995        optimized: &OptimizedGraph,
1996        selector: Option<Arc<dyn RuntimeSelector>>,
1997        lookup: Option<BackendRuntimeLookup>,
1998        sym_env: SymEnv,
1999    ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
2000        match Self::streaming_pick_for(&graph, selector, lookup)? {
2001            Some(pick) => Self::realize_many_inner(
2002                graph,
2003                targets,
2004                inputs,
2005                OrderSource::Streaming {
2006                    optimized,
2007                    pick: &pick,
2008                },
2009                sym_env,
2010            ),
2011            None => {
2012                Self::realize_many_with_optimized_env(graph, targets, inputs, optimized, sym_env)
2013            }
2014        }
2015    }
2016
2017    fn realize_many_inner(
2018        graph: Arc<RwLock<Graph>>,
2019        targets: &[NodeId],
2020        inputs: StorageCache,
2021        order_source: OrderSource<'_>,
2022        sym_env: SymEnv,
2023    ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
2024        if targets.is_empty() {
2025            return Ok(Vec::new());
2026        }
2027
2028        // Auto-insert safety copies for in-place ops whose target
2029        // has additional readers in this realize set (residual-
2030        // connection cycle break). No-op when no destructive ops
2031        // are present.
2032        {
2033            let mut g = graph.write().map_err(|_| poisoned("graph lock"))?;
2034            let effective_roots = extend_with_side_effect_roots(&g, targets);
2035            insert_safety_copies(&mut g, &effective_roots);
2036        }
2037
2038        // Execution plan covering every target's dependency set +
2039        // initial layouts for the input cache entries. Side-effect
2040        // roots merge in so their effects fire even when not
2041        // reachable from any target. `execution_plan` integrates
2042        // `derive_ordering`'s view-aware pinning so destructive ops
2043        // run AFTER non-destructive readers of their targets.
2044        let (compiler_work, wait_set, mut layout_cache): (
2045            CompilerWork,
2046            WaitSet,
2047            HashMap<NodeId, Layout>,
2048        ) = {
2049            let g = graph.read().map_err(|_| poisoned("graph lock"))?;
2050            let effective_roots = extend_with_side_effect_roots(&g, targets);
2051            // PR-A3b-1: derive from the OptimizedGraph's run lowering
2052            // when present (post-safety-copies); else the legacy
2053            // `execution_plan` walk. Identical on branchless graphs.
2054            //
2055            // PR-C1: a `Streaming` source is handed to the compiler thread
2056            // unflattened (resolves branches at the frontier).
2057            //
2058            // Step E A4b open question #4: also derives the `WaitSet`
2059            // ("event only where waited") from the same materialized order.
2060            let (work, wait_set) =
2061                compiler_work_and_wait_set_for(&g, &effective_roots, &order_source);
2062            let mut layouts = HashMap::with_capacity(inputs.len());
2063            for &id in inputs.keys() {
2064                layouts.insert(id, g.layout(id));
2065            }
2066            (work, wait_set, layouts)
2067        };
2068
2069        // Caller's target set — used to gate destructive-input
2070        // eviction so we don't drop a tensor the caller asked for.
2071        let target_set: std::collections::HashSet<NodeId> = targets.iter().copied().collect();
2072
2073        let (tx, rx) = channel::<Result<WorkItem>>();
2074        let graph_for_compiler = Arc::clone(&graph);
2075
2076        let compiler = thread::spawn(move || {
2077            compiler_thread_body(graph_for_compiler, compiler_work, sym_env, tx);
2078        });
2079
2080        // Phase 4.3: per-chunk SystemTopology generation check (see
2081        // realize_inner for the rationale). PR-A3b-1: the OptimizedGraph
2082        // path supplies its own optimize-time generation.
2083        let plan_generation: Option<u64> = generation_for(&order_source);
2084        let mut current_chunk_backend: Option<BackendId> = None;
2085        let mut cache: StorageCache = inputs;
2086        // Data-dependent dynamic-shapes producer-bound symbols (see
2087        // `realize_inner`). `realize_many` does not expose them today; the
2088        // env is dropped at pass end (producers still publish, keeping the
2089        // seam uniform across both realize loops).
2090        let mut produced_syms = SymEnv::new();
2091        // Step E A4b-1/A4b-3: per-node async-completion handles (see realize_inner
2092        // for the full rationale). Map drained at realize-end; the cross-device
2093        // copy waits the SOURCE producer's handle (A4b-3).
2094        let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
2095        // Step E A4b-4: eagerly-submitted in-flight Vulkan batches + the
2096        // multi-backend gate. See realize_inner for the full rationale — this is
2097        // the identical eager-submit / in-flight-lifetime machinery applied to the
2098        // multi-target walk. `multi_backend` arms on a genuine backend switch and
2099        // gates every eager submit, so a single-device realize_many is
2100        // byte-identical to A4b-2.
2101        let mut inflight_vulkan: Vec<InflightVulkan> = Vec::new();
2102        let mut multi_backend = false;
2103        for item in rx {
2104            let item = item?;
2105            // Chunk-boundary hook: TopologyChanged check + (A4b-4) backend-switch
2106            // detection + eager-submit-on-leaving-a-Vulkan-chunk.
2107            if !is_backend_neutral(&item.kind) && current_chunk_backend != Some(item.target_backend)
2108            {
2109                if let Some(plan_gen) = plan_generation {
2110                    let live = crate::dispatch::topology_generation();
2111                    if live != plan_gen {
2112                        return Err(Error::TopologyChanged {
2113                            plan_generation: plan_gen,
2114                            current_generation: live,
2115                        }
2116                        .bt());
2117                    }
2118                }
2119                let leaving = current_chunk_backend;
2120                if let Some(prev) = leaving
2121                    && prev != item.target_backend
2122                {
2123                    multi_backend = true;
2124                    if prev == BackendId::Vulkan {
2125                        eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
2126                    }
2127                }
2128                current_chunk_backend = Some(item.target_backend);
2129            }
2130            // Vulkan realizes run the async path (deferred eviction, size-based eager
2131            // submit) even on a single device: ordering between batches is structural
2132            // (inter-batch barrier + epoch-gated buffer reuse), not a host wait.
2133            if item.target_backend == BackendId::Vulkan && matches!(item.kind, WorkItemKind::Kernel)
2134            {
2135                multi_backend = true;
2136            }
2137            // Inter-batch Vulkan ordering is structural (see realize_inner): no host
2138            // wait before recording a new Vulkan op.
2139            // Step E A4b-3 + A4b-4: cross-device residency boundary. See
2140            // realize_inner for the full rationale.
2141            if matches!(
2142                item.kind,
2143                WorkItemKind::Copy { .. } | WorkItemKind::Move { .. }
2144            ) {
2145                // A4b-4: submit any open Vulkan chunk so the iGPU runs while the
2146                // host (possibly) waits on the CUDA producer.
2147                if multi_backend {
2148                    eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
2149                }
2150                if let Some(&producer) = item.inputs.first() {
2151                    // A4b-4: wait in-flight Vulkan ONLY for a Vulkan-source D2H
2152                    // (reads completed data; download flushes only the OPEN batch).
2153                    // A CUDA/CPU source does NOT wait Vulkan (preserves overlap).
2154                    if multi_backend && copy_source_is_vulkan(&cache, producer)? {
2155                        drain_inflight_vulkan(&mut inflight_vulkan)?;
2156                    }
2157                    // Step E A4b-3: finer CUDA source-drain — wait ONLY the producer.
2158                    wait_producer_handle(&mut handles, producer)?;
2159                }
2160            }
2161            let handle = execute_work_item(
2162                &item,
2163                &mut cache,
2164                &mut layout_cache,
2165                &mut produced_syms,
2166                will_be_waited(&wait_set, item.node_id),
2167                // No persistent-output mode on the standard realize paths —
2168                // fresh alloc per node, byte-identical to pre-CapturedRun.
2169                None,
2170            )
2171            .map_err(|e| with_node_location(&graph, item.node_id, e))?;
2172            store_handle(&mut handles, item.node_id, handle);
2173            if let Some(d_idx) = item.destructive_input
2174                && let Some(&destroyed) = item.inputs.get(d_idx)
2175                && !target_set.contains(&destroyed)
2176            {
2177                // Step E A2.1 (in-flight DATA-BUFFER lifetime — UAF guard,
2178                // now DEFERRED-DELETION instead of a drain): retain the
2179                // evicted buffer until the reader fences signal — eager-
2180                // submit the open batch (no wait) + move a refcount clone of
2181                // the buffer into every in-flight batch, freeing it post-
2182                // fence on each batch's `Drop` with NO host block. Replaces
2183                // the pre-A2.1 `drain_inflight_vulkan` + open-batch
2184                // `force_flush` (two host-blocking fence waits). See
2185                // `defer_evicted_vulkan_buffer` / realize_inner for the
2186                // UAF/leak argument. CUDA eviction is A3 stream-safe.
2187                // Single-device (multi_backend == false) keeps the pre-A2.1
2188                // blocking force_flush of the OPEN batch (the else branch) —
2189                // byte-identical + UAF-safe; the deferred path is unsafe there
2190                // (see force_flush_vulkan's doc).
2191                if multi_backend {
2192                    defer_evicted_vulkan_buffer(&cache, destroyed, &mut inflight_vulkan)?;
2193                } else if let Some(d_arc) = cache.get(&destroyed) {
2194                    force_flush_vulkan(d_arc)?;
2195                }
2196                cache.remove(&destroyed);
2197                layout_cache.remove(&destroyed);
2198                // CUDA eviction is stream-ordered-safe (A3); drop the
2199                // handle so the realize-end empty-map assert stays valid.
2200                handles.remove(&destroyed);
2201            }
2202        }
2203
2204        compiler
2205            .join()
2206            .map_err(|_| Error::Msg("compiler thread panicked".to_string()).bt())?;
2207
2208        // Step E A4b-1: drain every outstanding async handle (CUDA events) before
2209        // results are read / the cache drops.
2210        drain_handles(&mut handles)?;
2211        // Step E A4b open question #4: blanket per-device stream sync for the
2212        // elided nodes `drain_handles` never tracked — see `realize_inner`'s
2213        // identical comment / `sync_active_cuda_devices`'s doc for the argument.
2214        sync_active_cuda_devices(&cache)?;
2215        // Step E A4b-4: wait every eagerly-submitted in-flight Vulkan batch before
2216        // the cache drops. Empty on a single-device realize.
2217        drain_inflight_vulkan(&mut inflight_vulkan)?;
2218        // Step E A4b-2: drain all deferred Vulkan work — submit the open batch
2219        // then wait it via a `VulkanCompletion` handle (the A2 submit+wait, split;
2220        // byte-identical for pure-Vulkan — one submission at realize-end).
2221        drain_vulkan_pending(&cache)?;
2222        debug_assert!(
2223            inflight_vulkan.is_empty(),
2224            "A4b-4: in-flight Vulkan batch list must be empty after realize-end drain",
2225        );
2226
2227        // Verify every target was realized + collect outputs in
2228        // target order. We don't `remove` from the cache because the
2229        // same NodeId can appear twice in `targets` (caller wants the
2230        // same storage twice) — clone the Arc instead.
2231        let mut out = Vec::with_capacity(targets.len());
2232        for &target in targets {
2233            let storage = cache.get(&target).cloned().ok_or_else(|| {
2234                Error::Msg(format!(
2235                    "PipelinedExecutor::realize_many: target {:?} not populated after execution",
2236                    target
2237                ))
2238                .bt()
2239            })?;
2240            let layout = layout_cache.get(&target).cloned().ok_or_else(|| {
2241                Error::Msg(format!(
2242                    "PipelinedExecutor::realize_many: target layout {:?} not populated after execution",
2243                    target
2244                ))
2245                .bt()
2246            })?;
2247            out.push((storage, layout));
2248        }
2249        Ok(out)
2250    }
2251}
2252
2253/// Resolve the executor's dispatch order from the configured
2254/// [`OrderSource`]. PR-A3b-1: `Optimized` lowers the in-place
2255/// "plan IS the graph" form via `extract_runs`/`lower_run`; `Default`
2256/// keeps the pre-A3b-1 `execution_plan` walk. Both produce the same
2257/// `NodeId` sequence on a branchless graph (A3a equivalence gate),
2258/// computed here AFTER safety-copy insertion so either path covers
2259/// any freshly inserted copies.
2260fn order_for(
2261    graph: &Graph,
2262    effective_roots: &[NodeId],
2263    order_source: &OrderSource<'_>,
2264) -> Vec<NodeId> {
2265    match order_source {
2266        OrderSource::Default => execution_plan(graph, effective_roots),
2267        // The streaming walk is driven in the compiler thread (it
2268        // interleaves arm resolution with compilation), so `order_for`
2269        // never flattens it on the production path. This arm exists only
2270        // for match totality / non-pipelined callers: it collects the SAME
2271        // lazily-resolved order eagerly (the streaming walk with the same
2272        // `resolve_branch` over the threaded selector), which equals the
2273        // one-shot `lower_picked_route(pick_route(..))` order.
2274        OrderSource::Streaming {
2275            optimized: _optimized,
2276            pick,
2277        } => {
2278            let bindings = global_bindings();
2279            fuel_graph::lower_picked_route_streaming(graph, effective_roots, |branch| {
2280                resolve_branch(graph, branch, &bindings, pick.selector.as_ref())
2281            })
2282        }
2283        OrderSource::Optimized { optimized, route } => {
2284            // Lower the runs over the *effective* roots (user targets +
2285            // any spliced/side-effect roots) so the order matches the
2286            // walk's reachable set rather than only the optimize-time
2287            // roots. On a branchless graph this is the same concatenated
2288            // `lower_run` sequence the OptimizedGraph reports.
2289            //
2290            // PR-C1: when the picker supplied a route, follow the chosen
2291            // arms (`lower_picked_route`); otherwise the arm-0 lowering
2292            // (`OptimizedGraph::dispatch_order`). An empty route is
2293            // byte-identical to arm-0, so the two paths agree whenever
2294            // the picker chose arm-0 everywhere (the no-pressure case).
2295            match route {
2296                Some(route) => fuel_graph::lower_picked_route(graph, effective_roots, route),
2297                None => {
2298                    // A transient view built solely to call `dispatch_order`;
2299                    // it does no planning, so it carries no placements. The
2300                    // real optimization's map lives on `optimized`.
2301                    let view = OptimizedGraph {
2302                        roots: effective_roots.to_vec(),
2303                        generation: optimized.generation,
2304                        placements: None,
2305                    };
2306                    view.dispatch_order(graph)
2307                }
2308            }
2309        }
2310        // Resolved by `compiler_work_and_wait_set_for` before delegating
2311        // here (the whole point of `Cached` is to skip this computation
2312        // on a hit) — never reached. Kept exhaustive rather than wildcarded
2313        // so a future `OrderSource` variant can't silently fall through.
2314        OrderSource::Cached { .. } => {
2315            unreachable!("OrderSource::Cached is resolved before order_for is called")
2316        }
2317    }
2318}
2319
2320/// Step E A4b open question #4 — "event only where waited". The producer
2321/// [`NodeId`]s that some later `Op::Copy`/`Op::Move` node in `order` will
2322/// `wait_producer_handle` on (the executor's cross-device residency
2323/// boundary — see `wait_producer_handle`, whose `item.inputs.first()` read
2324/// this mirrors exactly). A CUDA node whose id is absent from the returned
2325/// set has NO mid-walk reader: same-device consumers are correct via stream
2326/// ordering alone (they never `wait`), and nothing touches its handle before
2327/// the realize-end drain, so [`crate::compiled::execute_compiled_with_wait_hint`]
2328/// can skip recording an `Event` for it entirely.
2329///
2330/// Requires a materialized `order` (works for [`OrderSource::Default`] /
2331/// [`OrderSource::Optimized`], whose [`CompilerWork::Order`] is a frozen
2332/// `Vec<NodeId>` computed up front) — see
2333/// [`compiler_work_and_wait_set_for`] for the [`OrderSource::Streaming`]
2334/// fallback.
2335fn build_wait_set(graph: &Graph, order: &[NodeId]) -> HashSet<NodeId> {
2336    let mut set = HashSet::new();
2337    for &id in order {
2338        let node = graph.node(id);
2339        if matches!(node.op, Op::Copy { .. } | Op::Move { .. })
2340            && let Some(&producer) = node.inputs.first()
2341        {
2342            set.insert(producer);
2343        }
2344    }
2345    set
2346}
2347
2348/// Step E A4b open question #4: the executor's per-realize wait-set.
2349/// `None` is the CONSERVATIVE fallback — every CUDA node still gets an Event
2350/// (today's pre-elision behavior, byte-identical). `Some(set)` restricts
2351/// event production to `set`'s members ([`build_wait_set`]'s output);
2352/// everything else gets `CompletionHandle::Ready` with no Event recorded.
2353type WaitSet = Option<HashSet<NodeId>>;
2354
2355/// Whether `node_id`'s completion handle must be individually observable
2356/// (i.e. `execute_compiled_with_wait_hint`'s `will_be_waited`). `None` (the
2357/// [`OrderSource::Streaming`] fallback — never elides) is always `true`.
2358#[inline]
2359fn will_be_waited(wait_set: &WaitSet, node_id: NodeId) -> bool {
2360    match wait_set {
2361        None => true,
2362        Some(set) => set.contains(&node_id),
2363    }
2364}
2365
2366/// Build the [`CompilerWork`] the compiler thread walks for the configured
2367/// [`OrderSource`], plus the [`WaitSet`] "event only where waited" (open
2368/// question #4) consults. The non-streaming sources flatten up front via
2369/// [`order_for`] (unchanged) and the SAME materialized order feeds
2370/// [`build_wait_set`], so the pre-scan and the dispatch order never
2371/// disagree. [`OrderSource::Streaming`] is handed to the compiler thread as
2372/// a [`CompilerWork::Streaming`] spec — NOT flattened here — so the thread
2373/// resolves branches lazily at the frontier (Step E Phase C, PR C1); the lazy
2374/// order can't be pre-scanned without eagerly resolving it (which would
2375/// defeat the point of streaming), so it falls back to the conservative
2376/// `WaitSet::None` — event every node, unchanged behavior. Holds the graph
2377/// read-lock only for the flattening branches; the streaming branch defers
2378/// the lock to the compiler thread.
2379fn compiler_work_and_wait_set_for(
2380    graph: &Graph,
2381    effective_roots: &[NodeId],
2382    order_source: &OrderSource<'_>,
2383) -> (CompilerWork, WaitSet) {
2384    match order_source {
2385        OrderSource::Streaming { pick, .. } => (
2386            CompilerWork::Streaming {
2387                roots: effective_roots.to_vec(),
2388                pick: (*pick).clone(),
2389            },
2390            None,
2391        ),
2392        OrderSource::Cached {
2393            optimized,
2394            order_cache,
2395        } => {
2396            let order = order_cache.get_or_init(|| {
2397                order_for(
2398                    graph,
2399                    effective_roots,
2400                    &OrderSource::Optimized {
2401                        optimized,
2402                        route: None,
2403                    },
2404                )
2405            });
2406            let wait_set = build_wait_set(graph, order);
2407            (CompilerWork::Order(order.clone()), Some(wait_set))
2408        }
2409        other => {
2410            let order = order_for(graph, effective_roots, other);
2411            let wait_set = build_wait_set(graph, &order);
2412            (CompilerWork::Order(order), Some(wait_set))
2413        }
2414    }
2415}
2416
2417/// The topology generation that keys the executor's `TopologyChanged`
2418/// chunk-boundary check. Uses the `OptimizedGraph`'s optimize-time
2419/// generation (PR-A3b-1 path). `None` ⇒ no check (the plain
2420/// `realize`/`realize_many` entries).
2421fn generation_for(order_source: &OrderSource<'_>) -> Option<u64> {
2422    match order_source {
2423        OrderSource::Optimized { optimized, .. } => Some(optimized.generation),
2424        OrderSource::Streaming { optimized, .. } => Some(optimized.generation),
2425        OrderSource::Cached { optimized, .. } => Some(optimized.generation),
2426        OrderSource::Default => None,
2427    }
2428}
2429
2430/// What the compiler thread walks: either a pre-flattened dispatch
2431/// `Order` (the `Default` / `Optimized` paths — unchanged), or a
2432/// `Streaming` spec the thread resolves branch-by-branch at the frontier
2433/// (Step E Phase C, PR C1).
2434enum CompilerWork {
2435    /// A frozen topological order (computed up front by `order_for`). The
2436    /// compiler thread compiles each node and sends it in turn.
2437    Order(Vec<NodeId>),
2438    /// The streaming run-walk: the compiler thread drives
2439    /// [`fuel_graph::walk_picked_route_streaming`] over `roots`, resolving
2440    /// each `Op::Branch` lazily via [`resolve_branch`] as the frontier
2441    /// reaches its arm-entry, and compiling + sending only the chosen
2442    /// arm's runs. `roots` are the effective roots (same set `order_for`
2443    /// lowers over). Byte-identical to `Order(lower_picked_route(
2444    /// pick_route(..)))` in C1 (same VRAM-only selector).
2445    Streaming {
2446        roots: Vec<NodeId>,
2447        pick: StreamingPick,
2448    },
2449}
2450
2451/// Compiler thread body. Holds the graph read-lock + a thread-local
2452/// layout cache and emits `WorkItem`s in topological order. For
2453/// [`CompilerWork::Order`] it walks the frozen order; for
2454/// [`CompilerWork::Streaming`] it resolves each branch at the frontier and
2455/// emits only the chosen arm's runs (Step E Phase C, PR C1). Sends
2456/// `Err(...)` and stops on the first failure.
2457fn compiler_thread_body(
2458    graph: Arc<RwLock<Graph>>,
2459    work: CompilerWork,
2460    sym_env: SymEnv,
2461    tx: Sender<Result<WorkItem>>,
2462) {
2463    let bindings = global_bindings();
2464
2465    let g = match graph.read() {
2466        Ok(g) => g,
2467        Err(_) => {
2468            let _ = tx.send(Err(poisoned("graph lock in compiler")));
2469            return;
2470        }
2471    };
2472
2473    // Compiler-thread-local layout cache. Populated as compile_one
2474    // walks topologically; downstream nodes look up their inputs'
2475    // layouts here (rather than from the graph side-table) to honor
2476    // the strided layouts emitted by metadata-only view ops earlier
2477    // in the same realize call.
2478    let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
2479    // `compile_one` for one node + send; returns `false` to stop the walk
2480    // (channel closed or compile error). Shared by both work modes so the
2481    // streamed and frozen-order paths compile identically.
2482    let compile_and_send = |id: NodeId, layout_cache: &mut HashMap<NodeId, Layout>| -> bool {
2483        let item = compile_one(&g, id, layout_cache, &bindings, &sym_env);
2484        let stop_on_err = item.is_err();
2485        if tx.send(item).is_err() {
2486            return false;
2487        }
2488        !stop_on_err
2489    };
2490
2491    match work {
2492        CompilerWork::Order(order) => {
2493            for id in order {
2494                if !compile_and_send(id, &mut layout_cache) {
2495                    return;
2496                }
2497            }
2498        }
2499        CompilerWork::Streaming { roots, pick } => {
2500            // Resolve each branch lazily at its arm-entry. C1 built the
2501            // streaming frontier; C2 made `pick.selector` load-aware (the
2502            // production `ChainedSelector` now reads each arm's live device
2503            // load — `pending_work_count / slot_capacity` via the
2504            // `BackendStreams` seam — as a key leg under the VRAM guard),
2505            // so this resolution reads live device load at THIS moment: the
2506            // executor has been submitting/draining (and updating the B1
2507            // counter) while the compiler ran ahead. Emit only the chosen
2508            // arm's runs, compiling + sending each node as the walk reaches
2509            // it — genuine streaming (resolution interleaved with
2510            // dispatch), not a pre-flattened order.
2511            let mut stopped = false;
2512            fuel_graph::walk_picked_route_streaming(
2513                &g,
2514                &roots,
2515                |branch| resolve_branch(&g, branch, &bindings, pick.selector.as_ref()),
2516                |members| {
2517                    if stopped {
2518                        return;
2519                    }
2520                    for &id in members {
2521                        if !compile_and_send(id, &mut layout_cache) {
2522                            stopped = true;
2523                            return;
2524                        }
2525                    }
2526                },
2527            );
2528        }
2529    }
2530}
2531
2532/// Resolve the [`CompiledNode`] for a kernel-bearing graph node via
2533/// the first-registered [`compile_node`] binding-table lookup.
2534///
2535/// `op_params` always comes from the executor's
2536/// `op_to_op_params(graph, node, id, layout_cache, sym_env)` — the live
2537/// OpParams shape (reduce dims, conv geometry, the per-pass
2538/// `SymEnv`-resolved `WriteSlice` offset, etc.) derives from the graph
2539/// node + its resolved input layouts at execute time.
2540fn resolve_compiled(
2541    op_kind: OpKind,
2542    dtypes: &[DType],
2543    target_backend: BackendId,
2544    op_params: OpParams,
2545    bindings: &KernelBindingTable,
2546) -> Result<CompiledNode> {
2547    compile_node(op_kind, dtypes, target_backend, op_params, bindings)
2548}
2549
2550/// Resolve one node into a `WorkItem` and update `layout_cache`
2551/// with the node's output layout. Three op shapes:
2552///
2553/// - `Op::Const` — adopts the entry from the input cache; layout is
2554///   read from the graph's side-table (or its contiguous fallback).
2555///
2556/// - Metadata-only view op — output layout is read from the graph's
2557///   side-table (populated by `Graph::push` at construction time);
2558///   the executor adopts the input's Storage Arc (no allocation, no
2559///   kernel).
2560///
2561/// - Computational op — output layout is `Layout::contiguous(node.shape)`
2562///   because today's kernels write contiguous output. The compiler
2563///   resolves the kernel ref and emits a Kernel work item.
2564fn compile_one(
2565    graph: &Graph,
2566    id: NodeId,
2567    layout_cache: &mut HashMap<NodeId, Layout>,
2568    bindings: &KernelBindingTable,
2569    sym_env: &SymEnv,
2570) -> Result<WorkItem> {
2571    let node = graph.node(id);
2572    let elem_count = node.shape.elem_count();
2573    let inputs = node.inputs.clone();
2574    // Snapshot destructive-input metadata from the graph at compile
2575    // time so the executor can evict the destroyed input from its
2576    // cache after the op runs (Op::Release / Op::Move semantics).
2577    let destructive_input = node.op.destructive_input();
2578
2579    if matches!(node.op, Op::Const) {
2580        let output_layout = graph.layout(id);
2581        layout_cache.insert(id, output_layout.clone());
2582        return Ok(WorkItem {
2583            node_id: id,
2584            inputs,
2585            elem_count,
2586            dtype: node.dtype,
2587            target_backend: BackendId::Cpu,
2588            kind: WorkItemKind::ConstAdopt,
2589            compiled: None,
2590            output_layout,
2591            destructive_input,
2592            output_bundle: None,
2593        });
2594    }
2595
2596    if node.op.is_view_op() {
2597        if inputs.len() != 1 {
2598            return Err(Error::Msg(format!(
2599                "view op {:?} expects 1 input, got {}",
2600                node.op,
2601                inputs.len(),
2602            ))
2603            .bt());
2604        }
2605        // Layout is read from the graph's side-table — populated by
2606        // `Graph::push` at construction time for view ops, and by
2607        // graph-rewriting opt passes that emit view nodes. The
2608        // compiler does not re-derive: graph.layout(id) is the single
2609        // source of truth.
2610        let output_layout = graph.layout(id);
2611        layout_cache.insert(id, output_layout.clone());
2612        // Inherit the upstream's target_backend (or default CPU) —
2613        // metadata-only adoption doesn't actually run on a backend,
2614        // but downstream consumers look at target_backend so it
2615        // needs to be set sensibly. Any device works.
2616        let target_backend = graph
2617            .target_backend(id)
2618            .or_else(|| graph.target_backend(inputs[0]))
2619            .unwrap_or(BackendId::Cpu);
2620        return Ok(WorkItem {
2621            node_id: id,
2622            inputs,
2623            elem_count,
2624            dtype: node.dtype,
2625            target_backend,
2626            kind: WorkItemKind::ViewOf {
2627                input: node.inputs[0],
2628            },
2629            compiled: None,
2630            output_layout,
2631            destructive_input,
2632            output_bundle: None,
2633        });
2634    }
2635
2636    // Op::View — multi-output projection (Option C, Session 4).
2637    // Structurally identical to a metadata-only view op at realize
2638    // time: clone the producer's Storage Arc into the View's cache
2639    // slot. The graph's layout side-table, populated by
2640    // `Tensor::view`, already carries the slot's effective layout
2641    // (slot.byte_offset baked into start_offset in slot-dtype-element
2642    // units).
2643    if let Op::View { .. } = node.op {
2644        if inputs.len() != 1 {
2645            return Err(Error::Msg(format!(
2646                "Op::View expects 1 input (the producer), got {}",
2647                inputs.len(),
2648            ))
2649            .bt());
2650        }
2651        let output_layout = graph.layout(id);
2652        layout_cache.insert(id, output_layout.clone());
2653        let target_backend = graph
2654            .target_backend(id)
2655            .or_else(|| graph.target_backend(inputs[0]))
2656            .unwrap_or(BackendId::Cpu);
2657        return Ok(WorkItem {
2658            node_id: id,
2659            inputs: inputs.clone(),
2660            elem_count,
2661            dtype: node.dtype,
2662            target_backend,
2663            kind: WorkItemKind::SlotView {
2664                producer: inputs[0],
2665            },
2666            compiled: None,
2667            output_layout,
2668            destructive_input,
2669            output_bundle: None,
2670        });
2671    }
2672
2673    // Op::ViewOwned — copies the slot's bytes into a fresh
2674    // contiguous Storage at realize time. Output layout is
2675    // contiguous starting at offset 0 (the memcpy produced an
2676    // independent buffer).
2677    if let Op::ViewOwned { slot } = node.op {
2678        if inputs.len() != 1 {
2679            return Err(Error::Msg(format!(
2680                "Op::ViewOwned expects 1 input (the producer), got {}",
2681                inputs.len(),
2682            ))
2683            .bt());
2684        }
2685        let output_layout = Layout::contiguous(node.shape.clone());
2686        layout_cache.insert(id, output_layout.clone());
2687        let target_backend = graph
2688            .target_backend(id)
2689            .or_else(|| graph.target_backend(inputs[0]))
2690            .unwrap_or(BackendId::Cpu);
2691        return Ok(WorkItem {
2692            node_id: id,
2693            inputs: inputs.clone(),
2694            elem_count,
2695            dtype: node.dtype,
2696            target_backend,
2697            kind: WorkItemKind::SlotOwn {
2698                producer: inputs[0],
2699                slot,
2700            },
2701            compiled: None,
2702            output_layout,
2703            destructive_input,
2704            output_bundle: None,
2705        });
2706    }
2707
2708    if matches!(node.op, Op::Release) {
2709        // Op::Release produces a zero-element marker output. The
2710        // marker storage is CPU + 0 bytes — nothing reads it; it
2711        // exists so cache lookups on the Release NodeId resolve.
2712        // The actual deallocation of `inputs[0]` happens in the
2713        // realize loop via `destructive_input` cleanup.
2714        if inputs.len() != 1 {
2715            return Err(
2716                Error::Msg(format!("Op::Release expects 1 input, got {}", inputs.len(),)).bt(),
2717            );
2718        }
2719        let output_layout = Layout::contiguous(node.shape.clone());
2720        layout_cache.insert(id, output_layout.clone());
2721        // Inherit the source's target_backend so any downstream
2722        // backend-aware logic (telemetry, scheduling) sees a sensible
2723        // value. The marker doesn't actually run on any backend.
2724        let target_backend = graph
2725            .target_backend(id)
2726            .or_else(|| graph.target_backend(inputs[0]))
2727            .unwrap_or(BackendId::Cpu);
2728        return Ok(WorkItem {
2729            node_id: id,
2730            inputs,
2731            elem_count,
2732            dtype: node.dtype,
2733            target_backend,
2734            kind: WorkItemKind::ReleaseMarker,
2735            compiled: None,
2736            output_layout,
2737            destructive_input,
2738            output_bundle: None,
2739        });
2740    }
2741
2742    // In-place ops (Phase 3 of in-place ops infrastructure): kernel
2743    // lookup goes through the standard binding-table path, but the
2744    // executor adopts the target's Arc as the output instead of
2745    // allocating fresh bytes. The compile-time predicate is exactly
2746    // `op.destructive_input().is_some()` AND NOT one of the
2747    // structural ops (WriteSlice / ZeroFill / Release / Move) which
2748    // have their own dedicated WorkItemKind arms in this function.
2749    if !matches!(
2750        node.op,
2751        Op::WriteSlice { .. }
2752            | Op::WriteSliceRotating { .. }
2753            | Op::WriteSliceDoff { .. }
2754            | Op::ZeroFill
2755            | Op::Release
2756            | Op::Move { .. },
2757    ) && let Some(target_idx) = node.op.destructive_input()
2758    {
2759        if inputs.len() <= target_idx {
2760            return Err(Error::Msg(format!(
2761                "in-place op {:?} declares destructive_input={target_idx} \
2762                     but has only {} input(s)",
2763                node.op,
2764                inputs.len(),
2765            ))
2766            .bt());
2767        }
2768        let target_backend = graph.target_backend(id).ok_or_else(|| {
2769            Error::Msg(format!(
2770                "PipelinedExecutor: in-place op node {id:?} has no target_backend set",
2771            ))
2772            .bt()
2773        })?;
2774        let op_kind = op_to_op_kind(&node.op).ok_or_else(|| {
2775            Error::Msg(format!(
2776                "PipelinedExecutor: in-place op {:?} has no op_to_op_kind mapping",
2777                node.op,
2778            ))
2779            .bt()
2780        })?;
2781        let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2782        let dtypes = build_lookup_dtypes(graph, node);
2783        let compiled = resolve_compiled(op_kind, &dtypes, target_backend, op_params, bindings)?;
2784        // Output adopts target's Layout (same Storage Arc, same shape).
2785        let output_layout = graph.layout(inputs[target_idx]);
2786        layout_cache.insert(id, output_layout.clone());
2787        return Ok(WorkItem {
2788            node_id: id,
2789            inputs: inputs.clone(),
2790            elem_count,
2791            dtype: node.dtype,
2792            target_backend,
2793            kind: WorkItemKind::InplaceKernel { target_idx },
2794            compiled: Some(compiled),
2795            output_layout,
2796            destructive_input,
2797            output_bundle: None,
2798        });
2799    }
2800
2801    if matches!(node.op, Op::WriteSlice { .. }) {
2802        // WriteSlice: kernel lookup happens like a normal kernel, but
2803        // the work item carries a dedicated kind so the executor
2804        // knows to adopt the destination's Arc as the output rather
2805        // than allocating a fresh buffer. The destination's NodeId is
2806        // captured here so the executor doesn't have to re-derive it.
2807        if inputs.len() != 2 {
2808            return Err(Error::Msg(format!(
2809                "Op::WriteSlice expects 2 inputs (destination, source), got {}",
2810                inputs.len(),
2811            ))
2812            .bt());
2813        }
2814        let target_backend = graph.target_backend(id).ok_or_else(|| {
2815            Error::Msg(format!(
2816                "PipelinedExecutor: WriteSlice node {:?} has no target_backend set",
2817                id
2818            ))
2819            .bt()
2820        })?;
2821        let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2822        let dtypes = build_lookup_dtypes(graph, node);
2823        let compiled = resolve_compiled(
2824            OpKind::WriteSlice,
2825            &dtypes,
2826            target_backend,
2827            op_params,
2828            bindings,
2829        )?;
2830        // Output adopts the destination's Layout — same Storage Arc,
2831        // same shape. Downstream consumers that want a post-write
2832        // sub-extent compose an explicit Op::Slice (WriteSlice does
2833        // not encode partial extents).
2834        let output_layout = graph.layout(inputs[0]);
2835        layout_cache.insert(id, output_layout.clone());
2836        return Ok(WorkItem {
2837            node_id: id,
2838            inputs: inputs.clone(),
2839            elem_count,
2840            dtype: node.dtype,
2841            target_backend,
2842            kind: WorkItemKind::WriteSlice {
2843                dest: inputs[0],
2844                source: inputs[1],
2845            },
2846            compiled: Some(compiled),
2847            output_layout,
2848            destructive_input,
2849            output_bundle: None,
2850        });
2851    }
2852
2853    if matches!(node.op, Op::WriteSliceRotating { .. }) {
2854        // WriteSliceRotating: same shape as WriteSlice but with a
2855        // third input (position scalar). Dispatch through the
2856        // binding table at `OpKind::WriteSliceRotating`; the
2857        // executor's arm reads the position from `position`'s
2858        // storage and the kernel splits across the ring boundary.
2859        if inputs.len() != 3 {
2860            return Err(Error::Msg(format!(
2861                "Op::WriteSliceRotating expects 3 inputs (destination, source, position), got {}",
2862                inputs.len(),
2863            ))
2864            .bt());
2865        }
2866        let target_backend = graph.target_backend(id).ok_or_else(|| {
2867            Error::Msg(format!(
2868                "PipelinedExecutor: WriteSliceRotating node {:?} has no target_backend set",
2869                id
2870            ))
2871            .bt()
2872        })?;
2873        let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2874        let dtypes = build_lookup_dtypes(graph, node);
2875        let compiled = resolve_compiled(
2876            OpKind::WriteSliceRotating,
2877            &dtypes,
2878            target_backend,
2879            op_params,
2880            bindings,
2881        )?;
2882        let output_layout = graph.layout(inputs[0]);
2883        layout_cache.insert(id, output_layout.clone());
2884        return Ok(WorkItem {
2885            node_id: id,
2886            inputs: inputs.clone(),
2887            elem_count,
2888            dtype: node.dtype,
2889            target_backend,
2890            kind: WorkItemKind::WriteSliceRotating {
2891                dest: inputs[0],
2892                source: inputs[1],
2893                position: inputs[2],
2894            },
2895            compiled: Some(compiled),
2896            output_layout,
2897            destructive_input,
2898            output_bundle: None,
2899        });
2900    }
2901
2902    if matches!(node.op, Op::WriteSliceDoff { .. }) {
2903        // WriteSliceDoff: same shape as WriteSlice but with a third
2904        // input (device-resident I64 offset). Dispatch through the
2905        // binding table at `OpKind::WriteSliceDoff`; the executor's arm
2906        // threads the offset's device pointer straight to the kernel
2907        // (no D2H, capture-safe).
2908        if inputs.len() != 3 {
2909            return Err(Error::Msg(format!(
2910                "Op::WriteSliceDoff expects 3 inputs (destination, source, offset), got {}",
2911                inputs.len(),
2912            ))
2913            .bt());
2914        }
2915        let target_backend = graph.target_backend(id).ok_or_else(|| {
2916            Error::Msg(format!(
2917                "PipelinedExecutor: WriteSliceDoff node {:?} has no target_backend set",
2918                id
2919            ))
2920            .bt()
2921        })?;
2922        let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2923        let dtypes = build_lookup_dtypes(graph, node);
2924        let compiled = resolve_compiled(
2925            OpKind::WriteSliceDoff,
2926            &dtypes,
2927            target_backend,
2928            op_params,
2929            bindings,
2930        )?;
2931        let output_layout = graph.layout(inputs[0]);
2932        layout_cache.insert(id, output_layout.clone());
2933        return Ok(WorkItem {
2934            node_id: id,
2935            inputs: inputs.clone(),
2936            elem_count,
2937            dtype: node.dtype,
2938            target_backend,
2939            kind: WorkItemKind::WriteSliceDoff {
2940                dest: inputs[0],
2941                source: inputs[1],
2942                offset: inputs[2],
2943            },
2944            compiled: Some(compiled),
2945            output_layout,
2946            destructive_input,
2947            output_bundle: None,
2948        });
2949    }
2950
2951    if let Op::Alloc { target } = node.op {
2952        // Op::Alloc { target }: produce a fresh zero-init Storage on
2953        // `target` with node.shape * node.dtype. Zero inputs.
2954        // Direct executor dispatch via WorkItemKind::Alloc — no
2955        // binding-table lookup (the device handle threading isn't
2956        // expressible through the binding table's current key shape).
2957        if !inputs.is_empty() {
2958            return Err(
2959                Error::Msg(format!("Op::Alloc expects 0 inputs, got {}", inputs.len(),)).bt(),
2960            );
2961        }
2962        let output_layout = Layout::contiguous(node.shape.clone());
2963        layout_cache.insert(id, output_layout.clone());
2964        // target_backend is informational here (the executor's Alloc
2965        // arm consults target_location for allocation routing); set
2966        // it to target's backend for consistency with how downstream
2967        // nodes inherit/read it.
2968        let target_backend = match target {
2969            DeviceLocation::Cpu => BackendId::Cpu,
2970            DeviceLocation::Cuda { .. } => BackendId::Cuda,
2971            DeviceLocation::Vulkan { .. } => BackendId::Vulkan,
2972            DeviceLocation::Metal { .. } => BackendId::Metal,
2973        };
2974        return Ok(WorkItem {
2975            node_id: id,
2976            inputs,
2977            elem_count,
2978            dtype: node.dtype,
2979            target_backend,
2980            kind: WorkItemKind::Alloc {
2981                target_location: target,
2982            },
2983            compiled: None,
2984            output_layout,
2985            destructive_input,
2986            output_bundle: None,
2987        });
2988    }
2989
2990    if let Op::Iota { len } = node.op {
2991        // Op::Iota { len }: 0-input F32 position generator
2992        // [0.0, 1.0, …, (len-1)]. Direct executor dispatch via
2993        // WorkItemKind::Iota — no binding-table lookup (a leaf has no
2994        // inputs[0] to inherit a backend from). Modeled on Op::Alloc.
2995        if !inputs.is_empty() {
2996            return Err(
2997                Error::Msg(format!("Op::Iota expects 0 inputs, got {}", inputs.len(),)).bt(),
2998            );
2999        }
3000        let output_layout = Layout::contiguous(node.shape.clone());
3001        layout_cache.insert(id, output_layout.clone());
3002        // Leaf: the planner may not place a 0-input node, so fall back to
3003        // CPU. Iota is a tiny host-side generator; its only consumer today
3004        // is the CPU flash-attn decomposition's alibi bias.
3005        let target_backend = graph.target_backend(id).unwrap_or(BackendId::Cpu);
3006        return Ok(WorkItem {
3007            node_id: id,
3008            inputs,
3009            elem_count,
3010            dtype: node.dtype,
3011            target_backend,
3012            kind: WorkItemKind::Iota { len },
3013            compiled: None,
3014            output_layout,
3015            destructive_input,
3016            output_bundle: None,
3017        });
3018    }
3019
3020    if matches!(node.op, Op::ZeroFill) {
3021        // Op::ZeroFill: 1 input (the buffer to zero); output aliases
3022        // input's Storage Arc (destructive in-place). No binding-
3023        // table lookup — per-backend dispatch in execute_work_item.
3024        if inputs.len() != 1 {
3025            return Err(Error::Msg(format!(
3026                "Op::ZeroFill expects 1 input, got {}",
3027                inputs.len(),
3028            ))
3029            .bt());
3030        }
3031        // Output layout: contiguous in the node's shape. We don't
3032        // require the input to be contiguous (auto_contiguize in the
3033        // executor handles strided inputs), but the destructive
3034        // semantic means callers should only ZeroFill an Op::Alloc
3035        // output today.
3036        let output_layout = Layout::contiguous(node.shape.clone());
3037        layout_cache.insert(id, output_layout.clone());
3038        let target_backend = graph
3039            .target_backend(id)
3040            .or_else(|| graph.target_backend(inputs[0]))
3041            .unwrap_or(BackendId::Cpu);
3042        return Ok(WorkItem {
3043            node_id: id,
3044            inputs,
3045            elem_count,
3046            dtype: node.dtype,
3047            target_backend,
3048            kind: WorkItemKind::ZeroFill,
3049            compiled: None,
3050            output_layout,
3051            destructive_input,
3052            output_bundle: None,
3053        });
3054    }
3055
3056    if let Op::Copy { target } = node.op {
3057        // Op::Copy { target }: download bytes from the source residency
3058        // to a fresh Storage allocated on `target`. Kernel lookup uses
3059        // `target_backend` = source backend (set by `prepare()` on the
3060        // realize device the source is on); the wrapper does the actual
3061        // transfer (e.g. Vulkan::download_bytes → memcpy into a CPU
3062        // CpuStorageBytes). The output Storage's variant is determined
3063        // by `target_location` and allocated in `execute_work_item`'s
3064        // WorkItemKind::Copy arm.
3065        if inputs.len() != 1 {
3066            return Err(
3067                Error::Msg(format!("Op::Copy expects 1 input, got {}", inputs.len(),)).bt(),
3068            );
3069        }
3070        let target_backend = graph.target_backend(id).ok_or_else(|| {
3071            Error::Msg(format!(
3072                "PipelinedExecutor: Op::Copy node {:?} has no target_backend \
3073                 set (= source backend, the one whose kernel runs the \
3074                 download)",
3075                id
3076            ))
3077            .bt()
3078        })?;
3079        let op_params = OpParams::None;
3080        let dtypes = build_lookup_dtypes(graph, node);
3081        let compiled =
3082            resolve_compiled(OpKind::Copy, &dtypes, target_backend, op_params, bindings)?;
3083        // Output layout: contiguous in the node's shape (mirrors the
3084        // source's logical shape). Auto-contiguize on the input side
3085        // (in execute_work_item) handles strided source views by
3086        // materializing them into a contiguous source buffer before
3087        // the kernel runs.
3088        let output_layout = Layout::contiguous(node.shape.clone());
3089        layout_cache.insert(id, output_layout.clone());
3090        return Ok(WorkItem {
3091            node_id: id,
3092            inputs,
3093            elem_count,
3094            dtype: node.dtype,
3095            target_backend,
3096            kind: WorkItemKind::Copy {
3097                target_location: target,
3098            },
3099            compiled: Some(compiled),
3100            output_layout,
3101            destructive_input,
3102            output_bundle: None,
3103        });
3104    }
3105
3106    if let Op::Move { target } = node.op {
3107        // Op::Move { target }: identical data-movement half to
3108        // Op::Copy — kernel lookup at (OpKind::Copy, [dt, dt],
3109        // source_backend); output allocated on `target` by the
3110        // executor's Copy/Move arm. The destructive half (release
3111        // the source) rides the realize loop's `destructive_input`
3112        // cleanup via the snapshot taken at the top of this function
3113        // (`Op::Move::destructive_input() == Some(0)`). Ordering
3114        // safety (a Move must not strand a sibling consumer of its
3115        // source) is `execution_plan`/`derive_ordering`'s job — the
3116        // Move is pinned after every non-destructive reader of the
3117        // source's alias set before any work item is emitted.
3118        if inputs.len() != 1 {
3119            return Err(
3120                Error::Msg(format!("Op::Move expects 1 input, got {}", inputs.len(),)).bt(),
3121            );
3122        }
3123        let target_backend = graph.target_backend(id).ok_or_else(|| {
3124            Error::Msg(format!(
3125                "PipelinedExecutor: Op::Move node {:?} has no target_backend \
3126                 set (= source backend, the one whose kernel runs the \
3127                 transfer)",
3128                id
3129            ))
3130            .bt()
3131        })?;
3132        let op_params = OpParams::None;
3133        let dtypes = build_lookup_dtypes(graph, node);
3134        let compiled =
3135            resolve_compiled(OpKind::Copy, &dtypes, target_backend, op_params, bindings)?;
3136        // Output layout: contiguous in the node's shape, same as
3137        // Op::Copy (the transfer materializes strided sources via
3138        // auto-contiguize on the input side).
3139        let output_layout = Layout::contiguous(node.shape.clone());
3140        layout_cache.insert(id, output_layout.clone());
3141        return Ok(WorkItem {
3142            node_id: id,
3143            inputs,
3144            elem_count,
3145            dtype: node.dtype,
3146            target_backend,
3147            kind: WorkItemKind::Move {
3148                target_location: target,
3149            },
3150            compiled: Some(compiled),
3151            output_layout,
3152            destructive_input,
3153            output_bundle: None,
3154        });
3155    }
3156
3157    if matches!(node.op, Op::Reshape(_)) {
3158        if inputs.len() != 1 {
3159            return Err(
3160                Error::Msg(format!("Op::Reshape expects 1 input, got {}", inputs.len(),)).bt(),
3161            );
3162        }
3163        let input_layout = graph.layout(inputs[0]);
3164        // Output is contiguous in the new shape — bytes per element
3165        // are unchanged, so a contiguous input flows through with
3166        // zero copy. A non-contiguous input is auto-contiguized at
3167        // execute time and the result is naturally contiguous.
3168        let output_layout = Layout::contiguous(node.shape.clone());
3169        layout_cache.insert(id, output_layout.clone());
3170        let target_backend = graph
3171            .target_backend(id)
3172            .or_else(|| graph.target_backend(inputs[0]))
3173            .unwrap_or(BackendId::Cpu);
3174        // Sanity: same total element count.
3175        let in_elem_count = input_layout.shape().elem_count();
3176        if in_elem_count != elem_count {
3177            return Err(Error::Msg(format!(
3178                "Op::Reshape changes element count: input {} → output {}",
3179                in_elem_count, elem_count,
3180            ))
3181            .bt());
3182        }
3183        return Ok(WorkItem {
3184            node_id: id,
3185            inputs,
3186            elem_count,
3187            dtype: node.dtype,
3188            target_backend,
3189            kind: WorkItemKind::ContiguizeOf {
3190                input: node.inputs[0],
3191            },
3192            compiled: None,
3193            output_layout,
3194            destructive_input,
3195            output_bundle: None,
3196        });
3197    }
3198
3199    if matches!(node.op, Op::Contiguize) {
3200        // Op::Contiguize: same execute-time path as Reshape, but
3201        // output shape == input shape so there's no element-count
3202        // check. Zero-copy when input is already contiguous +
3203        // zero-offset (the ContiguizeOf arm in execute_work_item
3204        // adopts the input Arc); otherwise auto_contiguize allocates
3205        // a fresh contiguous Storage on the input's backend.
3206        if inputs.len() != 1 {
3207            return Err(Error::Msg(format!(
3208                "Op::Contiguize expects 1 input, got {}",
3209                inputs.len(),
3210            ))
3211            .bt());
3212        }
3213        let output_layout = Layout::contiguous(node.shape.clone());
3214        layout_cache.insert(id, output_layout.clone());
3215        let target_backend = graph
3216            .target_backend(id)
3217            .or_else(|| graph.target_backend(inputs[0]))
3218            .unwrap_or(BackendId::Cpu);
3219        return Ok(WorkItem {
3220            node_id: id,
3221            inputs,
3222            elem_count,
3223            dtype: node.dtype,
3224            target_backend,
3225            kind: WorkItemKind::ContiguizeOf {
3226                input: node.inputs[0],
3227            },
3228            compiled: None,
3229            output_layout,
3230            destructive_input,
3231            output_bundle: None,
3232        });
3233    }
3234
3235    // Runtime fused op — a JIT-adopted (Tier-2) kernel. `Op::Fused(fid, _)`
3236    // with `fid.is_runtime()` has no `OpKind` (the op was synthesized after
3237    // startup): its kernel resolves from the SAME binding table as every
3238    // static kernel, under `BindingKey::RuntimeFused(fid)` — the dtype tuple
3239    // is the key, so an absent kernel AND a dtype mismatch are one honest
3240    // `NoBackendForOp` miss. A deterministic *lookup*, not a decision: the
3241    // optimizer's gate emitted the arm only when this kernel was bound, and
3242    // pinned `target_backend` (`runtime-fused-op-registration.md` §6). The
3243    // guards are invariant checks, never live fallbacks.
3244    if let Op::Fused(fid, _) = &node.op
3245        && fid.is_runtime()
3246    {
3247        let target_backend = graph.target_backend(id).ok_or_else(|| {
3248            Error::Msg(format!(
3249                "PipelinedExecutor: runtime fused op {:?} (node {:?}) has no \
3250                     target_backend — the arm emitter pins it at emission",
3251                fid, id,
3252            ))
3253            .bt()
3254        })?;
3255        // v1: single-output only. Reject a multi-output bundle explicitly
3256        // rather than silently dropping it.
3257        if graph.output_views_arc(id).is_some() {
3258            return Err(Error::Msg(format!(
3259                "PipelinedExecutor: runtime fused op {:?} (node {:?}) carries a \
3260                     multi-output bundle — synthesized kernels are single-output in v1",
3261                fid, id,
3262            ))
3263            .bt());
3264        }
3265        let dtypes = build_lookup_dtypes(graph, node);
3266        let (kernel, caps) = bindings.lookup_with_caps(
3267            crate::kernel::BindingKey::RuntimeFused(*fid),
3268            &dtypes,
3269            target_backend,
3270        )?;
3271        let output_layout = Layout::contiguous(node.shape.clone());
3272        layout_cache.insert(id, output_layout.clone());
3273        // Deliberate, not a fallthrough to `op_to_op_params` (which doesn't
3274        // know runtime ops): the node's `Runtime { scalars }` — the live
3275        // `extract:` values, pattern pre-order — becomes `JitScalars` (the
3276        // launch appends them as the kernel's trailing `p{i}` args); a
3277        // param-free op carries `None` (its launch is layout-driven:
3278        // pointers + element count).
3279        let op_params = match &node.op {
3280            Op::Fused(_, FusedOpParams::Runtime { scalars }) if !scalars.is_empty() => {
3281                OpParams::JitScalars {
3282                    scalars: scalars.clone(),
3283                }
3284            }
3285            _ => OpParams::None,
3286        };
3287        let compiled = CompiledNode {
3288            op: OpKind::RuntimeFused,
3289            dtypes: KernelDTypes::from_slice(&dtypes),
3290            backend: target_backend,
3291            kernel,
3292            caps,
3293            op_params,
3294        };
3295        return Ok(WorkItem {
3296            node_id: id,
3297            inputs,
3298            elem_count,
3299            dtype: node.dtype,
3300            target_backend,
3301            kind: WorkItemKind::Kernel,
3302            compiled: Some(compiled),
3303            output_layout,
3304            destructive_input,
3305            output_bundle: None,
3306        });
3307    }
3308
3309    let target_backend = graph.target_backend(id).ok_or_else(|| {
3310        Error::Msg(format!(
3311            "PipelinedExecutor: node {:?} ({:?}) has no target_backend set",
3312            id, node.op
3313        ))
3314        .bt()
3315    })?;
3316
3317    let op_kind = op_to_op_kind(&node.op).ok_or_else(|| {
3318        Error::Msg(format!(
3319            "PipelinedExecutor: op {:?} not yet mapped to an OpKind \
3320             (Phase C migrates more ops as they're registered)",
3321            node.op,
3322        ))
3323        .bt()
3324    })?;
3325
3326    let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
3327    // Build the per-operand dtype list for the binding-table lookup —
3328    // inputs in order, then outputs. Variadic uniform-dtype ops
3329    // (Concat) collapse to the canonical `[T_in, T_out]` shorthand to
3330    // match how registrations index them.
3331    let dtypes = build_lookup_dtypes(graph, node);
3332    let compiled = resolve_compiled(op_kind, &dtypes, target_backend, op_params, bindings)?;
3333    let output_layout = Layout::contiguous(node.shape.clone());
3334    layout_cache.insert(id, output_layout.clone());
3335    // Multi-output: snapshot the producer's bundle metadata. When
3336    // `Some(_)`, the Kernel arm allocates a single Storage sized to
3337    // fit every slot and attaches the bundle via
3338    // `Storage::with_bundle` so downstream Op::View/Op::ViewOwned
3339    // resolve correctly.
3340    let output_bundle = graph.output_views_arc(id);
3341    Ok(WorkItem {
3342        node_id: id,
3343        inputs,
3344        elem_count,
3345        dtype: node.dtype,
3346        target_backend,
3347        kind: WorkItemKind::Kernel,
3348        compiled: Some(compiled),
3349        output_layout,
3350        destructive_input,
3351        output_bundle,
3352    })
3353}
3354
3355/// Build the per-operand dtype list used as the binding-table lookup
3356/// key. Inputs in order, then the output. Variadic-uniform ops
3357/// (Concat) collapse to the canonical `[T_in, T_out]` shorthand to
3358/// match how those wrappers are registered (otherwise an N-way concat
3359/// would need N+1 distinct registrations per dtype).
3360///
3361/// `pub(crate)` — `crate::plan::compile_plan` (step 9b) reuses this
3362/// to key the route picker's binding-table lookup the same way the
3363/// pipelined path does.
3364pub(crate) fn build_lookup_dtypes(graph: &Graph, node: &Node) -> Vec<DType> {
3365    if matches!(node.op, Op::Concat { .. }) {
3366        // Concat: all inputs share node.dtype by construction.
3367        let in_dt = node
3368            .inputs
3369            .first()
3370            .map(|&id| graph.node(id).dtype)
3371            .unwrap_or(node.dtype);
3372        return vec![in_dt, node.dtype];
3373    }
3374    if matches!(node.op, Op::WriteSlice { .. }) {
3375        // WriteSlice: the destination (inputs[0]) is exposed to the
3376        // executor as the OUTPUT slot (in-place adoption), not as an
3377        // input the kernel reads. Canonicalize the binding-table key
3378        // to `[T_source, T_out]` so registrations match the kernel's
3379        // actual surface (one input slab + one output buffer).
3380        let src_dt = node
3381            .inputs
3382            .get(1)
3383            .map(|&id| graph.node(id).dtype)
3384            .unwrap_or(node.dtype);
3385        return vec![src_dt, node.dtype];
3386    }
3387    if matches!(node.op, Op::WriteSliceRotating { .. }) {
3388        // Same canonicalization as WriteSlice: dest = output slot;
3389        // position (inputs[2]) is read by the kernel via the
3390        // position-input parameter, not the binding-table key.
3391        let src_dt = node
3392            .inputs
3393            .get(1)
3394            .map(|&id| graph.node(id).dtype)
3395            .unwrap_or(node.dtype);
3396        return vec![src_dt, node.dtype];
3397    }
3398    if matches!(node.op, Op::WriteSliceDoff { .. }) {
3399        // Same canonicalization as WriteSlice: dest = output slot;
3400        // offset (inputs[2], I64) is a non-key runtime operand read by
3401        // the kernel via its device pointer, not the binding-table key.
3402        let src_dt = node
3403            .inputs
3404            .get(1)
3405            .map(|&id| graph.node(id).dtype)
3406            .unwrap_or(node.dtype);
3407        return vec![src_dt, node.dtype];
3408    }
3409    let mut dts: Vec<DType> = node.inputs.iter().map(|&id| graph.node(id).dtype).collect();
3410    dts.push(node.dtype);
3411    dts
3412}
3413
3414/// SoftmaxLastDim is reachable only through `Op::Fused(FusedOps::
3415/// SOFTMAX_LAST_DIM, _)` post-step-5 (the legacy `Op::SoftmaxLastDim`
3416/// primitive variant was retired). This helper collapses the
3417/// op-to-OpKind/OpParams shapes for callers that want a single-arm
3418/// match.
3419fn op_is_softmax_last_dim(op: &Op) -> bool {
3420    matches!(op, Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM)
3421}
3422
3423/// FusedLinear is reachable only through `Op::Fused(FusedOps::
3424/// FUSED_LINEAR, _)` post-step-5.
3425fn op_is_fused_linear(op: &Op) -> bool {
3426    matches!(op, Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::FUSED_LINEAR)
3427}
3428
3429/// Map a `fuel_graph::Op` to a `fuel_ir::dispatch::OpKind`.
3430/// Returns `None` for ops that haven't been wired into the new
3431/// dispatch path yet — Phase C extends this as op families migrate.
3432///
3433/// `pub(crate)` — `crate::plan::compile_plan` (step 9b) reuses this
3434/// to skip nodes the binding-table doesn't index (view ops,
3435/// `Op::Const`, not-yet-migrated ops) when building bindings.
3436pub(crate) fn op_to_op_kind(op: &Op) -> Option<OpKind> {
3437    match op {
3438        Op::Add => Some(OpKind::AddElementwise),
3439        Op::Sub => Some(OpKind::SubElementwise),
3440        Op::Mul => Some(OpKind::MulElementwise),
3441        Op::Div => Some(OpKind::DivElementwise),
3442        Op::Relu => Some(OpKind::ReluElementwise),
3443        Op::Neg => Some(OpKind::NegElementwise),
3444        Op::Sqr => Some(OpKind::SqrElementwise),
3445        Op::Sqrt => Some(OpKind::SqrtElementwise),
3446        Op::Tanh => Some(OpKind::TanhElementwise),
3447        Op::Exp => Some(OpKind::ExpElementwise),
3448        Op::Log => Some(OpKind::LogElementwise),
3449        Op::Sin => Some(OpKind::SinElementwise),
3450        Op::Cos => Some(OpKind::CosElementwise),
3451        Op::Sigmoid => Some(OpKind::SigmoidElementwise),
3452        Op::Silu => Some(OpKind::SiluElementwise),
3453        Op::GeluTanh => Some(OpKind::GeluTanhElementwise),
3454        Op::Step => Some(OpKind::StepElementwise),
3455        Op::Recip => Some(OpKind::RecipElementwise),
3456        Op::Abs => Some(OpKind::AbsElementwise),
3457        Op::Equal => Some(OpKind::EqualElementwise),
3458        Op::Ne => Some(OpKind::NotEqualElementwise),
3459        Op::Lt => Some(OpKind::LessElementwise),
3460        Op::Le => Some(OpKind::LessEqualElementwise),
3461        Op::Gt => Some(OpKind::GreaterElementwise),
3462        Op::Ge => Some(OpKind::GreaterEqualElementwise),
3463        Op::Where => Some(OpKind::Where),
3464        Op::Floor => Some(OpKind::FloorElementwise),
3465        Op::Ceil => Some(OpKind::CeilElementwise),
3466        Op::Round => Some(OpKind::RoundElementwise),
3467        Op::Sign => Some(OpKind::SignElementwise),
3468        Op::Erf => Some(OpKind::ErfElementwise),
3469        Op::Gelu => Some(OpKind::GeluElementwise),
3470        Op::Pow => Some(OpKind::PowElementwise),
3471        Op::Rsqrt => Some(OpKind::RsqrtElementwise),
3472        Op::Rem => Some(OpKind::RemElementwise),
3473        Op::Flip { .. } => Some(OpKind::Flip),
3474        Op::Roll { .. } => Some(OpKind::Roll),
3475        Op::CumSum { .. } => Some(OpKind::CumSum),
3476        Op::Triu { .. } => Some(OpKind::Triu),
3477        Op::Tril { .. } => Some(OpKind::Tril),
3478        Op::LogSoftmaxLastDim => Some(OpKind::LogSoftmaxLastDim),
3479        Op::LogSoftmaxLastDimBackward => Some(OpKind::LogSoftmaxLastDimBackward),
3480        Op::MaskedFill { .. } => Some(OpKind::MaskedFill),
3481        Op::Pad { .. } => Some(OpKind::Pad),
3482        Op::PadBackward { .. } => Some(OpKind::PadBackward),
3483        Op::SumDim(_) => Some(OpKind::SumReduce),
3484        Op::MaxDim(_) => Some(OpKind::MaxReduce),
3485        Op::MinDim(_) => Some(OpKind::MinReduce),
3486        Op::MeanDim(_) => Some(OpKind::MeanReduce),
3487        Op::SumAll => Some(OpKind::SumReduce),
3488        Op::MaxAll => Some(OpKind::MaxReduce),
3489        Op::MinAll => Some(OpKind::MinReduce),
3490        Op::MeanAll => Some(OpKind::MeanReduce),
3491        Op::MatMul => Some(OpKind::MatMul),
3492        Op::Cast(_) => Some(OpKind::Cast),
3493        // Phase 7.6 step 5: fused-op dispatch routes through
3494        // `Op::Fused(fid, _)`; the legacy `Op::Conv2D` /
3495        // `Op::FusedLinear` / `Op::SoftmaxLastDim` /
3496        // `Op::RmsNormLastDim` / `Op::LayerNormLastDim` / `Op::Rope`
3497        // arms were dropped together with the variants.
3498        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::CONV2D => Some(OpKind::Conv2D),
3499        // Phase 7.6 step 5 (final): legacy `Op::ConvTranspose2D` /
3500        // `Op::FlashAttn` / `Op::PagedAttn` arms dropped with the
3501        // variants; dispatch flows only through `Op::Fused`.
3502        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D => {
3503            Some(OpKind::ConvTranspose2D)
3504        }
3505        Op::ReduceSumTo(_) => Some(OpKind::ReduceSumTo),
3506        Op::ReduceMaxTo(_) => Some(OpKind::ReduceMaxTo),
3507        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::FLASH_ATTN => {
3508            Some(OpKind::FlashAttn)
3509        }
3510        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::PAGED_ATTN => {
3511            Some(OpKind::PagedAttn)
3512        }
3513        Op::AddScalar(_) => Some(OpKind::Affine),
3514        Op::MulScalar(_) => Some(OpKind::Affine),
3515        Op::Clamp { .. } => Some(OpKind::ClampElementwise),
3516        Op::PowI(_) => Some(OpKind::PowIElementwise),
3517        Op::Maximum => Some(OpKind::MaximumElementwise),
3518        Op::Minimum => Some(OpKind::MinimumElementwise),
3519        Op::Concat { .. } => Some(OpKind::Concat),
3520        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM => {
3521            Some(OpKind::SoftmaxLastDim)
3522        }
3523        // Phase 7.6 step 6 follow-up: backward helpers now route
3524        // through the byte-level binding table too.
3525        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM_BACKWARD => {
3526            Some(OpKind::SoftmaxLastDimBackward)
3527        }
3528        Op::Fused(fid, _)
3529            if *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM_BACKWARD =>
3530        {
3531            Some(OpKind::LayerNormLastDimBackward)
3532        }
3533        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM_BACKWARD => {
3534            Some(OpKind::RmsNormLastDimBackward)
3535        }
3536        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::REDUCE_MAX_TO_BACKWARD => {
3537            Some(OpKind::ReduceMaxToBackward)
3538        }
3539        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::POWI_BACKWARD => {
3540            Some(OpKind::PowIElementwiseBackward)
3541        }
3542        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::FUSED_LINEAR => {
3543            Some(OpKind::FusedLinear)
3544        }
3545        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM => {
3546            Some(OpKind::RmsNormLastDim)
3547        }
3548        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM => {
3549            Some(OpKind::LayerNormLastDim)
3550        }
3551        Op::IndexSelect { .. } => Some(OpKind::IndexSelect),
3552        Op::Gather { .. } => Some(OpKind::Gather),
3553        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::ROPE => Some(OpKind::Rope),
3554        Op::IndexAdd { .. } => Some(OpKind::IndexAdd),
3555        Op::ScatterAdd { .. } => Some(OpKind::ScatterAdd),
3556        Op::NonZeroIndices { .. } => Some(OpKind::NonZeroIndices),
3557        Op::ArgMaxDim(_) => Some(OpKind::ArgMaxDim),
3558        Op::ArgMinDim(_) => Some(OpKind::ArgMinDim),
3559        // Phase 7.6 step 5 (final): legacy `Op::QMatMul` arm dropped.
3560        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::QMATMUL => {
3561            Some(OpKind::QMatMul)
3562        }
3563        Op::WriteSlice { .. } => Some(OpKind::WriteSlice),
3564        Op::WriteSliceRotating { .. } => Some(OpKind::WriteSliceRotating),
3565        Op::WriteSliceDoff { .. } => Some(OpKind::WriteSliceDoff),
3566        // Phase 2 of bridge-retirement (post-9c): Op::Copy dispatches
3567        // through the binding table at OpKind::Copy. The BackendId
3568        // axis encodes the source backend (the kernel runs there —
3569        // download from local memory). The executor handles output
3570        // allocation on the target via a dedicated WorkItemKind::Copy
3571        // arm, since target_location differs from target_backend for
3572        // this op (the only one with that property today).
3573        Op::Copy { .. } => Some(OpKind::Copy),
3574        // Op::Move dispatches the same data-movement kernel as
3575        // Op::Copy — the destructive release of the source is
3576        // realize-loop bookkeeping (`destructive_input` cleanup), not
3577        // a kernel. Same OpKind, same source-backend key convention.
3578        // `compile_plan` skips both (residency-determined, not a
3579        // picker decision).
3580        Op::Move { .. } => Some(OpKind::Copy),
3581        // Phase 3 of the in-place ops infrastructure: each in-place Op
3582        // variant maps to its OpKind so the binding-table dispatch can
3583        // resolve a kernel. The executor's dedicated WorkItemKind
3584        // arms (added alongside) handle the storage-Arc adoption.
3585        Op::ReluInplace => Some(OpKind::ReluInplace),
3586        Op::SiluInplace => Some(OpKind::SiluInplace),
3587        Op::GeluTanhInplace => Some(OpKind::GeluTanhInplace),
3588        Op::TanhInplace => Some(OpKind::TanhInplace),
3589        Op::SigmoidInplace => Some(OpKind::SigmoidInplace),
3590        Op::NegInplace => Some(OpKind::NegInplace),
3591        Op::AbsInplace => Some(OpKind::AbsInplace),
3592        Op::SqrInplace => Some(OpKind::SqrInplace),
3593        Op::SqrtInplace => Some(OpKind::SqrtInplace),
3594        Op::RsqrtInplace => Some(OpKind::RsqrtInplace),
3595        Op::RecipInplace => Some(OpKind::RecipInplace),
3596        Op::ExpInplace => Some(OpKind::ExpInplace),
3597        Op::LogInplace => Some(OpKind::LogInplace),
3598        Op::SinInplace => Some(OpKind::SinInplace),
3599        Op::CosInplace => Some(OpKind::CosInplace),
3600        Op::SignInplace => Some(OpKind::SignInplace),
3601        Op::FloorInplace => Some(OpKind::FloorInplace),
3602        Op::CeilInplace => Some(OpKind::CeilInplace),
3603        Op::RoundInplace => Some(OpKind::RoundInplace),
3604        Op::ErfInplace => Some(OpKind::ErfInplace),
3605        Op::GeluInplace => Some(OpKind::GeluInplace),
3606        Op::ClampInplace { .. } => Some(OpKind::ClampInplace),
3607        Op::PowIInplace(_) => Some(OpKind::PowIInplace),
3608        // In-place binary variants (Add/Sub/Mul/Div/MaskedFill) and
3609        // their OpKind siblings are tracked by a parallel session and
3610        // toggle in and out of the Op enum as that work progresses.
3611        // No multi-output arm references them; nothing to map here in
3612        // the current HEAD.
3613        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::INPLACE_AFFINE => {
3614            Some(OpKind::InplaceAffine)
3615        }
3616        Op::Fused(fid, _)
3617            if *fid == fuel_graph::registry::FusedOps::FUSED_SOFTMAX_CROSS_ENTROPY =>
3618        {
3619            Some(OpKind::FusedSoftmaxCrossEntropy)
3620        }
3621        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::CAUSAL_CONV1D => {
3622            Some(OpKind::CausalConv1d)
3623        }
3624        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SELECTIVE_SCAN => {
3625            Some(OpKind::SelectiveScan)
3626        }
3627        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SSD_CHUNK_SCAN => {
3628            Some(OpKind::SsdChunkScan)
3629        }
3630        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::NF4_MATMUL => {
3631            Some(OpKind::Nf4Matmul)
3632        }
3633        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::AWQ_MATMUL => {
3634            Some(OpKind::AwqMatmul)
3635        }
3636        // Phase 3a (post-9c): Op::Alloc + Op::ZeroFill are structural
3637        // ops dispatched directly via `WorkItemKind::Alloc` /
3638        // `WorkItemKind::ZeroFill` (no binding-table lookup).
3639        // `compile_plan` skips these.
3640        Op::Alloc { .. } | Op::ZeroFill => None,
3641        _ => None,
3642    }
3643}
3644
3645/// Build the [`OpParams`] for `node`'s op. Most ops use
3646/// `OpParams::None`; reductions / matmul / conv / slice carry their
3647/// op-specific extras here. The graph is consulted to read input
3648/// shapes (e.g. reductions need the input shape to walk the
3649/// multi-index — Storage only carries bytes + dtype).
3650///
3651/// Phase C — extends as op families migrate. Returns Err if a
3652/// graph-shape lookup fails (currently can't, but the signature is
3653/// `Result` so future cases needing validation slot in cleanly).
3654/// Encode an `f64` value into the byte pattern of `dtype`. Used by
3655/// `Op::Pad` (Constant mode) to pre-convert the fill value once at
3656/// op_params time, so the kernel itself stays dtype-agnostic.
3657fn encode_value_to_bytes(dtype: DType, value: f64) -> Result<Vec<u8>> {
3658    match dtype {
3659        DType::F32 => Ok((value as f32).to_le_bytes().to_vec()),
3660        DType::F64 => Ok(value.to_le_bytes().to_vec()),
3661        DType::BF16 => Ok(half::bf16::from_f32(value as f32).to_le_bytes().to_vec()),
3662        DType::F16 => Ok(half::f16::from_f32(value as f32).to_le_bytes().to_vec()),
3663        DType::U8 => Ok(vec![value as u8]),
3664        DType::U32 => Ok((value as u32).to_le_bytes().to_vec()),
3665        other => Err(Error::Msg(format!(
3666            "encode_value_to_bytes: dtype {other:?} not yet supported for Pad fill",
3667        ))
3668        .bt()),
3669    }
3670}
3671
3672/// Encode a typed `Scalar` to its little-endian byte representation.
3673/// Used by Op::MaskedFill so the kernel only sees bytes — never has
3674/// to know the value's dtype.
3675fn scalar_to_bytes(s: fuel_ir::Scalar) -> Vec<u8> {
3676    use fuel_ir::Scalar;
3677    match s {
3678        Scalar::U8(v) => vec![v],
3679        Scalar::I8(v) => vec![v as u8],
3680        Scalar::U32(v) => v.to_le_bytes().to_vec(),
3681        Scalar::I16(v) => v.to_le_bytes().to_vec(),
3682        Scalar::I32(v) => v.to_le_bytes().to_vec(),
3683        Scalar::I64(v) => v.to_le_bytes().to_vec(),
3684        Scalar::BF16(v) => v.to_le_bytes().to_vec(),
3685        Scalar::F16(v) => v.to_le_bytes().to_vec(),
3686        Scalar::F32(v) => v.to_le_bytes().to_vec(),
3687        Scalar::F64(v) => v.to_le_bytes().to_vec(),
3688        Scalar::Bool(b) => vec![b as u8],
3689        Scalar::F8E4M3(v) => vec![v.to_bits()],
3690        Scalar::F8E5M2(v) => vec![v.to_bits()],
3691        // OCP-MX E8M0 block scale is a raw byte on the wire (`2^(X-127)`,
3692        // decoded kernel-side); a single byte is its own LE encoding.
3693        Scalar::F8E8M0(v) => vec![v],
3694    }
3695}
3696
3697fn op_to_op_params(
3698    graph: &Graph,
3699    node: &Node,
3700    node_id: NodeId,
3701    layout_cache: &HashMap<NodeId, Layout>,
3702    sym_env: &SymEnv,
3703) -> Result<OpParams> {
3704    // Helper: read an input's layout from the compiler-thread-local
3705    // cache (which is current within the realize call), falling back
3706    // to the graph's side-table if the input wasn't visited (which
3707    // shouldn't happen in topo order, but the fallback keeps the
3708    // path safe).
3709    let input_layout = |input_id: NodeId| -> Layout {
3710        layout_cache
3711            .get(&input_id)
3712            .cloned()
3713            .unwrap_or_else(|| graph.layout(input_id))
3714    };
3715    Ok(match &node.op {
3716        // Phase 7.6 step 5 (final): legacy `Op::QMatMul` arm dropped.
3717        Op::Fused(_, fuel_graph::registry::FusedOpParams::QMatMul { quant_type, k, n }) => {
3718            // Inputs: (activations f32 [..., m, k], weight_bytes
3719            // u32-typed). Output shape (this Node's shape) is
3720            // [..., m, n]. Flatten leading dims into batch_count.
3721            if node.inputs.len() != 2 {
3722                return Err(Error::Msg(format!(
3723                    "Op::QMatMul expects 2 inputs (activations, weight_bytes), got {}",
3724                    node.inputs.len(),
3725                ))
3726                .bt());
3727            }
3728            let act_layout = input_layout(node.inputs[0]);
3729            let act_dims = act_layout.shape().dims();
3730            if act_dims.len() < 2 {
3731                return Err(Error::Msg(format!(
3732                    "Op::QMatMul: activations must be rank ≥ 2, got {act_dims:?}",
3733                ))
3734                .bt());
3735            }
3736            let m = act_dims[act_dims.len() - 2];
3737            let k_act = act_dims[act_dims.len() - 1];
3738            if k_act != *k {
3739                return Err(Error::Msg(format!(
3740                    "Op::QMatMul: activation last dim ({k_act}) must equal Op's k ({k})",
3741                ))
3742                .bt());
3743            }
3744            let batch_count: usize = act_dims[..act_dims.len() - 2].iter().product();
3745            OpParams::QMatMul {
3746                quant_type: *quant_type,
3747                batch_count,
3748                m,
3749                n: *n,
3750                k: *k,
3751            }
3752        }
3753        Op::ArgMaxDim(d) | Op::ArgMinDim(d) => {
3754            // Reuse OpParams::Reduce — same shape contract; the
3755            // single reduce dim is the argmax/argmin axis. Input
3756            // layout flows through KernelRef's `layouts[0]`.
3757            OpParams::Reduce {
3758                dims: vec![*d],
3759                keepdim: false,
3760            }
3761        }
3762        Op::SumDim(d) | Op::MaxDim(d) | Op::MinDim(d) | Op::MeanDim(d) => OpParams::Reduce {
3763            dims: vec![*d],
3764            keepdim: false,
3765        },
3766        Op::SumAll | Op::MaxAll | Op::MinAll | Op::MeanAll => {
3767            let il = input_layout(node.inputs[0]);
3768            let rank = il.shape().rank();
3769            OpParams::Reduce {
3770                dims: (0..rank).collect(),
3771                keepdim: false,
3772            }
3773        }
3774        Op::MatMul => {
3775            // Batched matmul: lhs `[..lhs_batch.., m, k]` @
3776            // rhs `[..rhs_batch.., k, n]` → out `[..lhs_batch.., m, n]`.
3777            // Per-axis the batch dims either match or follow GQA-style
3778            // divisibility (lhs_dim > rhs_dim && lhs_dim % rhs_dim == 0);
3779            // the kernel honors the latter via `rhs_idx = lhs_idx / n_rep`.
3780            if node.inputs.len() != 2 {
3781                return Err(Error::Msg(format!(
3782                    "Op::MatMul expects 2 inputs, got {}",
3783                    node.inputs.len(),
3784                ))
3785                .bt());
3786            }
3787            let lhs = input_layout(node.inputs[0]);
3788            let rhs = input_layout(node.inputs[1]);
3789            let lhs_dims = lhs.shape().dims();
3790            let rhs_dims = rhs.shape().dims();
3791            if lhs_dims.len() < 2 || rhs_dims.len() < 2 {
3792                return Err(Error::Msg(format!(
3793                    "Op::MatMul requires both inputs rank ≥ 2; got lhs={:?} rhs={:?}",
3794                    lhs_dims, rhs_dims,
3795                ))
3796                .bt());
3797            }
3798            if lhs_dims.len() != rhs_dims.len() {
3799                return Err(Error::Msg(format!(
3800                    "Op::MatMul: ranks must match (auto-broadcast happens at \
3801                     graph construction time); got lhs rank {} vs rhs rank {}",
3802                    lhs_dims.len(),
3803                    rhs_dims.len(),
3804                ))
3805                .bt());
3806            }
3807            let rank = lhs_dims.len();
3808            let batch_rank = rank - 2;
3809            // Per-axis validation: equal or GQA-divisible.
3810            for i in 0..batch_rank {
3811                let la = lhs_dims[i];
3812                let ra = rhs_dims[i];
3813                let ok = la == ra || (ra > 0 && la > ra && la % ra == 0);
3814                if !ok {
3815                    return Err(Error::Msg(format!(
3816                        "Op::MatMul: batch dim {i} disallowed combination \
3817                         (lhs={la}, rhs={ra}); must be equal or \
3818                         GQA-divisible (lhs > rhs && lhs % rhs == 0)",
3819                    ))
3820                    .bt());
3821                }
3822            }
3823            let lhs_batch_dims: Vec<usize> = lhs_dims[..batch_rank].to_vec();
3824            let rhs_batch_dims: Vec<usize> = rhs_dims[..batch_rank].to_vec();
3825            let (m, k_lhs) = (lhs_dims[rank - 2], lhs_dims[rank - 1]);
3826            let (k_rhs, n) = (rhs_dims[rank - 2], rhs_dims[rank - 1]);
3827            if k_lhs != k_rhs {
3828                return Err(Error::Msg(format!(
3829                    "Op::MatMul: contracting dims disagree — lhs trailing is \
3830                     [{m}, {k_lhs}], rhs trailing is [{k_rhs}, {n}]",
3831                ))
3832                .bt());
3833            }
3834            // Data-determined-M (sparse-MoE): a side-table row count makes
3835            // `m` the capacity and this the compute count. Dual-path,
3836            // mirroring WriteSlice: resolve in the compile-time SymEnv
3837            // (static / input-determined) now; else DEFER a producer-bound
3838            // sym to execute; else it is a genuine caller error.
3839            let m_compute = match graph.matmul_row_count(node_id) {
3840                None => MatmulM::All,
3841                Some(dyn_m) => match dyn_m.resolve(sym_env) {
3842                    Some(c) => {
3843                        if c > m {
3844                            return Err(Error::Msg(format!(
3845                                "Op::MatMul: resolved row count {c} exceeds capacity {m}",
3846                            ))
3847                            .bt());
3848                        }
3849                        MatmulM::Rows(c)
3850                    }
3851                    None => match dyn_m {
3852                        fuel_ir::DynScalar::Sym(s) if graph.binds_data_determined_sym(s) => {
3853                            MatmulM::Deferred(dyn_m)
3854                        }
3855                        _ => {
3856                            return Err(Error::Msg(format!(
3857                                "Op::MatMul: dynamic row count {dyn_m:?} is unbound in the \
3858                                 SymEnv at compile and no producer binds it (not \
3859                                 data-determined)",
3860                            ))
3861                            .bt());
3862                        }
3863                    },
3864                },
3865            };
3866            OpParams::Matmul {
3867                lhs_batch_dims,
3868                rhs_batch_dims,
3869                m,
3870                n,
3871                k: k_lhs,
3872                m_compute,
3873            }
3874        }
3875        // Phase 7.6 step 4: legacy `Op::FusedLinear` and registry-extended
3876        // `Op::Fused(FUSED_LINEAR, _)` share the same shape contract and
3877        // params encoding (reuses OpParams::Matmul). One body covers both.
3878        op if op_is_fused_linear(op) => {
3879            // Inputs: [a, b, bias]. Same shape semantics as MatMul on
3880            // a/b; bias is rank-1 [N] and broadcasts along all leading
3881            // dims. We reuse OpParams::Matmul (kernel reads bias from
3882            // inputs[2] directly).
3883            if node.inputs.len() != 3 {
3884                return Err(Error::Msg(format!(
3885                    "Op::FusedLinear expects 3 inputs (a, b, bias), got {}",
3886                    node.inputs.len(),
3887                ))
3888                .bt());
3889            }
3890            let lhs = input_layout(node.inputs[0]);
3891            let rhs = input_layout(node.inputs[1]);
3892            let bias = input_layout(node.inputs[2]);
3893            let lhs_dims = lhs.shape().dims();
3894            let rhs_dims = rhs.shape().dims();
3895            let bias_dims = bias.shape().dims();
3896            if lhs_dims.len() < 2 || rhs_dims.len() < 2 {
3897                return Err(Error::Msg(format!(
3898                    "Op::FusedLinear requires both a, b rank ≥ 2; got a={:?} b={:?}",
3899                    lhs_dims, rhs_dims,
3900                ))
3901                .bt());
3902            }
3903            if lhs_dims.len() != rhs_dims.len() {
3904                return Err(Error::Msg(format!(
3905                    "Op::FusedLinear: ranks must match (auto-broadcast happens at \
3906                     graph construction time); got a rank {} vs b rank {}",
3907                    lhs_dims.len(),
3908                    rhs_dims.len(),
3909                ))
3910                .bt());
3911            }
3912            let rank = lhs_dims.len();
3913            let batch_rank = rank - 2;
3914            for i in 0..batch_rank {
3915                let la = lhs_dims[i];
3916                let ra = rhs_dims[i];
3917                let ok = la == ra || (ra > 0 && la > ra && la % ra == 0);
3918                if !ok {
3919                    return Err(Error::Msg(format!(
3920                        "Op::FusedLinear: batch dim {i} disallowed (a={la}, b={ra})",
3921                    ))
3922                    .bt());
3923                }
3924            }
3925            let lhs_batch_dims: Vec<usize> = lhs_dims[..batch_rank].to_vec();
3926            let rhs_batch_dims: Vec<usize> = rhs_dims[..batch_rank].to_vec();
3927            let (m, k_lhs) = (lhs_dims[rank - 2], lhs_dims[rank - 1]);
3928            let (k_rhs, n) = (rhs_dims[rank - 2], rhs_dims[rank - 1]);
3929            if k_lhs != k_rhs {
3930                return Err(Error::Msg(format!(
3931                    "Op::FusedLinear: contracting dims disagree — a trailing is \
3932                     [{m}, {k_lhs}], b trailing is [{k_rhs}, {n}]",
3933                ))
3934                .bt());
3935            }
3936            if bias_dims.len() != 1 || bias_dims[0] != n {
3937                return Err(Error::Msg(format!(
3938                    "Op::FusedLinear: bias must be rank-1 [{n}], got {bias_dims:?}",
3939                ))
3940                .bt());
3941            }
3942            OpParams::Matmul {
3943                lhs_batch_dims,
3944                rhs_batch_dims,
3945                m,
3946                n,
3947                k: k_lhs,
3948                // FusedLinear is always static-M (dense linear layer).
3949                m_compute: MatmulM::All,
3950            }
3951        }
3952        // FusedSoftmaxCrossEntropy: flatten logits `[..., V]` to
3953        // `[n_rows, V]` for the kernel. n_rows is the product of all
3954        // leading dims (≥1 even for rank-1 logits, where targets are
3955        // a scalar). The kernel walks rows internally.
3956        Op::Fused(
3957            _,
3958            fuel_graph::registry::FusedOpParams::FusedSoftmaxCrossEntropy {
3959                reduction,
3960                ignore_index,
3961            },
3962        ) => {
3963            if node.inputs.len() != 2 {
3964                return Err(Error::Msg(format!(
3965                    "FusedSoftmaxCrossEntropy expects 2 inputs (logits, targets), got {}",
3966                    node.inputs.len(),
3967                ))
3968                .bt());
3969            }
3970            let logits_layout = input_layout(node.inputs[0]);
3971            let logits_dims = logits_layout.shape().dims();
3972            if logits_dims.is_empty() {
3973                return Err(Error::Msg(
3974                    "FusedSoftmaxCrossEntropy: logits must have rank ≥ 1".to_string(),
3975                )
3976                .bt());
3977            }
3978            let vocab = *logits_dims.last().unwrap();
3979            let n_rows: usize = logits_dims[..logits_dims.len() - 1]
3980                .iter()
3981                .product::<usize>()
3982                .max(1);
3983            let targets_layout = input_layout(node.inputs[1]);
3984            let target_count: usize = targets_layout
3985                .shape()
3986                .dims()
3987                .iter()
3988                .product::<usize>()
3989                .max(1);
3990            if target_count != n_rows {
3991                return Err(Error::Msg(format!(
3992                    "FusedSoftmaxCrossEntropy: targets element count {target_count} must \
3993                     equal logits row count {n_rows} (logits {logits_dims:?}, targets {:?})",
3994                    targets_layout.shape().dims(),
3995                ))
3996                .bt());
3997            }
3998            OpParams::FusedSoftmaxCrossEntropy {
3999                n_rows,
4000                vocab,
4001                reduction: *reduction,
4002                ignore_index: *ignore_index,
4003            }
4004        }
4005        // CausalConv1d: derive (batch, channels, seq_in, seq_out, kernel)
4006        // from the input layouts. x is `[batch, channels, seq_in]`
4007        // (caller pre-pads with kernel-1 zeros), weight is
4008        // `[channels, 1, kernel]`. seq_out = seq_in - (kernel - 1).
4009        Op::Fused(_, fuel_graph::registry::FusedOpParams::CausalConv1d { use_silu }) => {
4010            if node.inputs.len() != 3 {
4011                return Err(Error::Msg(format!(
4012                    "CausalConv1d expects 3 inputs (x, weight, bias), got {}",
4013                    node.inputs.len(),
4014                ))
4015                .bt());
4016            }
4017            let x_layout = input_layout(node.inputs[0]);
4018            let w_layout = input_layout(node.inputs[1]);
4019            let x_dims = x_layout.shape().dims();
4020            let w_dims = w_layout.shape().dims();
4021            if x_dims.len() != 3 {
4022                return Err(Error::Msg(format!(
4023                    "CausalConv1d: x must be rank 3 [batch, channels, seq+pad], got {x_dims:?}",
4024                ))
4025                .bt());
4026            }
4027            if w_dims.len() != 3 {
4028                return Err(Error::Msg(format!(
4029                    "CausalConv1d: weight must be rank 3 [channels, 1, kernel], got {w_dims:?}",
4030                ))
4031                .bt());
4032            }
4033            let batch = x_dims[0];
4034            let channels = x_dims[1];
4035            let seq_in = x_dims[2];
4036            let kernel = w_dims[2];
4037            if seq_in < kernel - 1 {
4038                return Err(Error::Msg(format!(
4039                    "CausalConv1d: x time dim {seq_in} must be ≥ kernel-1 = {} \
4040                     (caller must pre-pad with kernel-1 zeros)",
4041                    kernel - 1,
4042                ))
4043                .bt());
4044            }
4045            let seq_out = seq_in - (kernel - 1);
4046            OpParams::CausalConv1d {
4047                batch,
4048                channels,
4049                seq_in,
4050                seq_out,
4051                kernel,
4052                use_silu: *use_silu,
4053            }
4054        }
4055        // SelectiveScan: derive (batch, seqlen, dim, dstate) from the
4056        // input layouts. u: [batch, seqlen, dim]; a: [dim, dstate].
4057        Op::Fused(_, fuel_graph::registry::FusedOpParams::SelectiveScan { delta_softplus }) => {
4058            if node.inputs.len() != 5 {
4059                return Err(Error::Msg(format!(
4060                    "SelectiveScan expects 5 inputs (u, delta, a, b, c), got {}",
4061                    node.inputs.len(),
4062                ))
4063                .bt());
4064            }
4065            let u_layout = input_layout(node.inputs[0]);
4066            let a_layout = input_layout(node.inputs[2]);
4067            let u_dims = u_layout.shape().dims();
4068            let a_dims = a_layout.shape().dims();
4069            if u_dims.len() != 3 {
4070                return Err(Error::Msg(format!(
4071                    "SelectiveScan: u must be rank 3 [batch, seqlen, dim], got {u_dims:?}",
4072                ))
4073                .bt());
4074            }
4075            if a_dims.len() != 2 {
4076                return Err(Error::Msg(format!(
4077                    "SelectiveScan: a must be rank 2 [dim, dstate], got {a_dims:?}",
4078                ))
4079                .bt());
4080            }
4081            let batch = u_dims[0];
4082            let seqlen = u_dims[1];
4083            let dim = u_dims[2];
4084            let dstate = a_dims[1];
4085            if a_dims[0] != dim {
4086                return Err(Error::Msg(format!(
4087                    "SelectiveScan: a's first dim {} must equal dim {dim}",
4088                    a_dims[0],
4089                ))
4090                .bt());
4091            }
4092            OpParams::SelectiveScan {
4093                batch,
4094                seqlen,
4095                dim,
4096                dstate,
4097                delta_softplus: *delta_softplus,
4098            }
4099        }
4100        // SsdChunkScan: derive (batch, seqlen, heads, head_dim,
4101        // state_dim) from x and b layouts. x: [batch, seqlen, heads,
4102        // head_dim]; b: [batch, seqlen, heads, state_dim].
4103        Op::Fused(_, fuel_graph::registry::FusedOpParams::SsdChunkScan { chunk_size }) => {
4104            if node.inputs.len() != 5 {
4105                return Err(Error::Msg(format!(
4106                    "SsdChunkScan expects 5 inputs (x, dt, a, b, c), got {}",
4107                    node.inputs.len(),
4108                ))
4109                .bt());
4110            }
4111            let x_layout = input_layout(node.inputs[0]);
4112            let b_layout = input_layout(node.inputs[3]);
4113            let x_dims = x_layout.shape().dims();
4114            let b_dims = b_layout.shape().dims();
4115            if x_dims.len() != 4 {
4116                return Err(Error::Msg(format!(
4117                    "SsdChunkScan: x must be rank 4 [batch, seqlen, heads, head_dim], got {x_dims:?}",
4118                ))
4119                .bt());
4120            }
4121            if b_dims.len() != 4 {
4122                return Err(Error::Msg(format!(
4123                    "SsdChunkScan: b must be rank 4 [batch, seqlen, heads, state_dim], got {b_dims:?}",
4124                ))
4125                .bt());
4126            }
4127            let batch = x_dims[0];
4128            let seqlen = x_dims[1];
4129            let heads = x_dims[2];
4130            let head_dim = x_dims[3];
4131            let state_dim = b_dims[3];
4132            OpParams::SsdChunkScan {
4133                batch,
4134                seqlen,
4135                heads,
4136                head_dim,
4137                state_dim,
4138                chunk_size: *chunk_size,
4139            }
4140        }
4141        // Nf4Matmul: derive (batch, m, n, k) from activations + w_packed
4142        // layouts. activations: [..., m, k] (leading dims flatten into
4143        // batch); w_packed: [n, k/2] U8.
4144        Op::Fused(_, fuel_graph::registry::FusedOpParams::Nf4Matmul { block_size }) => {
4145            if node.inputs.len() != 3 {
4146                return Err(Error::Msg(format!(
4147                    "Nf4Matmul expects 3 inputs (activations, w_packed, absmax), got {}",
4148                    node.inputs.len(),
4149                ))
4150                .bt());
4151            }
4152            let a_layout = input_layout(node.inputs[0]);
4153            let w_layout = input_layout(node.inputs[1]);
4154            let a_dims = a_layout.shape().dims();
4155            let w_dims = w_layout.shape().dims();
4156            if a_dims.len() < 2 {
4157                return Err(Error::Msg(format!(
4158                    "Nf4Matmul: activations must be rank ≥ 2, got {a_dims:?}",
4159                ))
4160                .bt());
4161            }
4162            if w_dims.len() != 2 {
4163                return Err(Error::Msg(format!(
4164                    "Nf4Matmul: w_packed must be rank 2 [n, k/2], got {w_dims:?}",
4165                ))
4166                .bt());
4167            }
4168            let m = a_dims[a_dims.len() - 2];
4169            let k = a_dims[a_dims.len() - 1];
4170            let n = w_dims[0];
4171            let batch: usize = a_dims[..a_dims.len() - 2].iter().product::<usize>().max(1);
4172            if k % 2 != 0 {
4173                return Err(Error::Msg(format!(
4174                    "Nf4Matmul: k={k} must be even (w_packed holds 2 nibbles per byte along k)",
4175                ))
4176                .bt());
4177            }
4178            if k % *block_size != 0 {
4179                return Err(Error::Msg(format!(
4180                    "Nf4Matmul: k={k} must be a multiple of block_size={block_size}",
4181                ))
4182                .bt());
4183            }
4184            if w_dims[1] != k / 2 {
4185                return Err(Error::Msg(format!(
4186                    "Nf4Matmul: w_packed second dim {} must equal k/2 = {}",
4187                    w_dims[1],
4188                    k / 2,
4189                ))
4190                .bt());
4191            }
4192            OpParams::Nf4Matmul {
4193                batch,
4194                m,
4195                n,
4196                k,
4197                block_size: *block_size,
4198            }
4199        }
4200        // AwqMatmul: (batch, m, n, k) from activations [..., m, k] and the
4201        // kernel-layout weight [n, k/8]; the padded zeros/scales widths are the
4202        // op's own build-time invariant (checked by `awq_matmul`).
4203        Op::Fused(
4204            _,
4205            fuel_graph::registry::FusedOpParams::AwqMatmul {
4206                group_size,
4207                split_k_iters,
4208            },
4209        ) => {
4210            if node.inputs.len() != 4 {
4211                return Err(Error::Msg(format!(
4212                    "AwqMatmul expects 4 inputs (activations, weight, zeros, scales), got {}",
4213                    node.inputs.len(),
4214                ))
4215                .bt());
4216            }
4217            let a_layout = input_layout(node.inputs[0]);
4218            let w_layout = input_layout(node.inputs[1]);
4219            let a_dims = a_layout.shape().dims();
4220            let w_dims = w_layout.shape().dims();
4221            if a_dims.len() < 2 {
4222                return Err(Error::Msg(format!(
4223                    "AwqMatmul: activations must be rank ≥ 2, got {a_dims:?}",
4224                ))
4225                .bt());
4226            }
4227            if w_dims.len() != 2 {
4228                return Err(Error::Msg(format!(
4229                    "AwqMatmul: weight must be rank 2 [n, k/8], got {w_dims:?}",
4230                ))
4231                .bt());
4232            }
4233            let m = a_dims[a_dims.len() - 2];
4234            let k = a_dims[a_dims.len() - 1];
4235            let n = w_dims[0];
4236            let batch: usize = a_dims[..a_dims.len() - 2].iter().product::<usize>().max(1);
4237            if k % 8 != 0 || w_dims[1] != k / 8 {
4238                return Err(Error::Msg(format!(
4239                    "AwqMatmul: weight second dim {} must equal k/8 for k={k} (a multiple of 8)",
4240                    w_dims[1],
4241                ))
4242                .bt());
4243            }
4244            OpParams::AwqMatmul {
4245                batch,
4246                m,
4247                n,
4248                k,
4249                group_size: *group_size,
4250                split_k_iters: *split_k_iters,
4251            }
4252        }
4253        // Phase 7.6 step 3: SoftmaxLastDim flows through
4254        // `Op::Fused(FusedOps::SOFTMAX_LAST_DIM, _)`. The
4255        // SOFTMAX_LAST_DIM_BACKWARD variant added in step 6
4256        // follow-up shares the same `outer × last_dim` shape
4257        // contract (input 0 is `y` for forward / forward-output
4258        // for backward; both rank-≥1 with last-dim as the
4259        // reduction axis).
4260        op if op_is_softmax_last_dim(op)
4261            || matches!(op, Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM_BACKWARD) =>
4262        {
4263            let il = input_layout(node.inputs[0]);
4264            let dims = il.shape().dims();
4265            if dims.is_empty() {
4266                return Err(Error::Msg("Op::SoftmaxLastDim requires rank ≥ 1".to_string()).bt());
4267            }
4268            let last_dim = *dims.last().unwrap();
4269            let outer_count: usize = dims[..dims.len() - 1].iter().product();
4270            OpParams::SoftmaxLastDim {
4271                outer_count,
4272                last_dim,
4273            }
4274        }
4275        Op::Flip { dim } => {
4276            // Single input. Precompute the flat-3-axis split
4277            // (outer × dim × inner) from the input shape.
4278            if node.inputs.len() != 1 {
4279                return Err(Error::Msg(format!(
4280                    "Op::Flip expects 1 input, got {}",
4281                    node.inputs.len(),
4282                ))
4283                .bt());
4284            }
4285            let in_layout = input_layout(node.inputs[0]);
4286            let in_dims = in_layout.shape().dims();
4287            if *dim >= in_dims.len() {
4288                return Err(Error::Msg(format!(
4289                    "Op::Flip: dim {dim} out of range for rank {}",
4290                    in_dims.len(),
4291                ))
4292                .bt());
4293            }
4294            let outer_count: usize = in_dims[..*dim].iter().product();
4295            let dim_size = in_dims[*dim];
4296            let inner_count: usize = in_dims[*dim + 1..].iter().product();
4297            OpParams::Flip {
4298                outer_count,
4299                dim_size,
4300                inner_count,
4301                axis: *dim,
4302            }
4303        }
4304        Op::Roll { dim, shift } => {
4305            if node.inputs.len() != 1 {
4306                return Err(Error::Msg(format!(
4307                    "Op::Roll expects 1 input, got {}",
4308                    node.inputs.len(),
4309                ))
4310                .bt());
4311            }
4312            let in_layout = input_layout(node.inputs[0]);
4313            let in_dims = in_layout.shape().dims();
4314            if *dim >= in_dims.len() {
4315                return Err(Error::Msg(format!(
4316                    "Op::Roll: dim {dim} out of range for rank {}",
4317                    in_dims.len(),
4318                ))
4319                .bt());
4320            }
4321            let outer_count: usize = in_dims[..*dim].iter().product();
4322            let dim_size = in_dims[*dim];
4323            let inner_count: usize = in_dims[*dim + 1..].iter().product();
4324            OpParams::Roll {
4325                outer_count,
4326                dim_size,
4327                inner_count,
4328                shift: *shift,
4329                axis: *dim,
4330            }
4331        }
4332        Op::CumSum { dim } => {
4333            if node.inputs.len() != 1 {
4334                return Err(Error::Msg(format!(
4335                    "Op::CumSum expects 1 input, got {}",
4336                    node.inputs.len(),
4337                ))
4338                .bt());
4339            }
4340            let in_layout = input_layout(node.inputs[0]);
4341            let in_dims = in_layout.shape().dims();
4342            if *dim >= in_dims.len() {
4343                return Err(Error::Msg(format!(
4344                    "Op::CumSum: dim {dim} out of range for rank {}",
4345                    in_dims.len(),
4346                ))
4347                .bt());
4348            }
4349            let outer_count: usize = in_dims[..*dim].iter().product();
4350            let dim_size = in_dims[*dim];
4351            let inner_count: usize = in_dims[*dim + 1..].iter().product();
4352            OpParams::CumSum {
4353                outer_count,
4354                dim_size,
4355                inner_count,
4356                axis: *dim,
4357            }
4358        }
4359        Op::Triu { diagonal } | Op::Tril { diagonal } => {
4360            if node.inputs.len() != 1 {
4361                return Err(Error::Msg(format!(
4362                    "Op::Triu/Tril expects 1 input, got {}",
4363                    node.inputs.len(),
4364                ))
4365                .bt());
4366            }
4367            let in_layout = input_layout(node.inputs[0]);
4368            let in_dims = in_layout.shape().dims();
4369            if in_dims.len() < 2 {
4370                return Err(Error::Msg(format!(
4371                    "Op::Triu/Tril requires rank >= 2, got {}",
4372                    in_dims.len(),
4373                ))
4374                .bt());
4375            }
4376            let rows = in_dims[in_dims.len() - 2];
4377            let cols = in_dims[in_dims.len() - 1];
4378            let batch_count: usize = in_dims[..in_dims.len() - 2]
4379                .iter()
4380                .product::<usize>()
4381                .max(1);
4382            OpParams::Triangular {
4383                batch_count,
4384                rows,
4385                cols,
4386                diagonal: *diagonal,
4387            }
4388        }
4389        Op::LogSoftmaxLastDim => {
4390            if node.inputs.len() != 1 {
4391                return Err(Error::Msg(format!(
4392                    "Op::LogSoftmaxLastDim expects 1 input, got {}",
4393                    node.inputs.len(),
4394                ))
4395                .bt());
4396            }
4397            let in_layout = input_layout(node.inputs[0]);
4398            let in_dims = in_layout.shape().dims();
4399            if in_dims.is_empty() {
4400                return Err(Error::Msg("Op::LogSoftmaxLastDim requires rank ≥ 1".to_string()).bt());
4401            }
4402            let last_dim = *in_dims.last().unwrap();
4403            let outer_count: usize = in_dims[..in_dims.len() - 1].iter().product();
4404            OpParams::LogSoftmaxLastDim {
4405                outer_count,
4406                last_dim,
4407            }
4408        }
4409        Op::LogSoftmaxLastDimBackward => {
4410            // Inputs: (forward_output_y, upstream_grad). Same shape contract
4411            // as SoftmaxLastDimBackward — last-dim row-wise dispatch.
4412            if node.inputs.len() != 2 {
4413                return Err(Error::Msg(format!(
4414                    "Op::LogSoftmaxLastDimBackward expects 2 inputs, got {}",
4415                    node.inputs.len(),
4416                ))
4417                .bt());
4418            }
4419            let in_layout = input_layout(node.inputs[0]);
4420            let in_dims = in_layout.shape().dims();
4421            if in_dims.is_empty() {
4422                return Err(Error::Msg(
4423                    "Op::LogSoftmaxLastDimBackward requires rank ≥ 1".to_string(),
4424                )
4425                .bt());
4426            }
4427            let last_dim = *in_dims.last().unwrap();
4428            let outer_count: usize = in_dims[..in_dims.len() - 1].iter().product();
4429            OpParams::LogSoftmaxLastDim {
4430                outer_count,
4431                last_dim,
4432            }
4433        }
4434        Op::MaskedFill { value } => {
4435            if node.inputs.len() != 2 {
4436                return Err(Error::Msg(format!(
4437                    "Op::MaskedFill expects 2 inputs (x, mask), got {}",
4438                    node.inputs.len(),
4439                ))
4440                .bt());
4441            }
4442            let fill_bytes = scalar_to_bytes(*value);
4443            OpParams::MaskedFill { fill_bytes }
4444        }
4445        // Op::MaskedFillInplace tracked by the parallel in-place
4446        // binary family session — not in HEAD's Op enum right now.
4447        Op::PadBackward {
4448            in_shape,
4449            padding,
4450            mode,
4451        } => {
4452            // Single input is the upstream gradient; output is the
4453            // input gradient (shape == `in_shape`).
4454            if node.inputs.len() != 1 {
4455                return Err(Error::Msg(format!(
4456                    "Op::PadBackward expects 1 input, got {}",
4457                    node.inputs.len(),
4458                ))
4459                .bt());
4460            }
4461            let in_dims: Vec<usize> = in_shape.dims().to_vec();
4462            // Output of the original forward Pad is the input shape
4463            // of the backward (== upstream's shape).
4464            let out_dims: Vec<usize> = in_dims
4465                .iter()
4466                .zip(padding.iter())
4467                .map(|(&d, &(b, a))| d + b + a)
4468                .collect();
4469            let mode_tag: u8 = match mode {
4470                fuel_graph::PadMode::Constant => 0,
4471                fuel_graph::PadMode::Reflect => 1,
4472                fuel_graph::PadMode::Replicate => 2,
4473            };
4474            OpParams::PadBackward {
4475                in_shape: in_dims,
4476                out_shape: out_dims,
4477                padding: padding.clone(),
4478                mode_tag,
4479            }
4480        }
4481        Op::Pad {
4482            padding,
4483            mode,
4484            value,
4485        } => {
4486            if node.inputs.len() != 1 {
4487                return Err(Error::Msg(format!(
4488                    "Op::Pad expects 1 input, got {}",
4489                    node.inputs.len(),
4490                ))
4491                .bt());
4492            }
4493            let in_layout = input_layout(node.inputs[0]);
4494            let in_dims: Vec<usize> = in_layout.shape().dims().to_vec();
4495            if padding.len() != in_dims.len() {
4496                return Err(Error::Msg(format!(
4497                    "Op::Pad: padding.len() ({}) != input rank ({})",
4498                    padding.len(),
4499                    in_dims.len(),
4500                ))
4501                .bt());
4502            }
4503            let out_dims: Vec<usize> = in_dims
4504                .iter()
4505                .zip(padding.iter())
4506                .map(|(&d, &(b, a))| d + b + a)
4507                .collect();
4508            let mode_tag: u8 = match mode {
4509                fuel_graph::PadMode::Constant => 0,
4510                fuel_graph::PadMode::Reflect => 1,
4511                fuel_graph::PadMode::Replicate => 2,
4512            };
4513            // Encode fill value as bytes for the output dtype. The
4514            // kernel is dtype-agnostic — it just memcopies the
4515            // pattern. Conversion happens once here per node, not
4516            // per element in the kernel.
4517            let fill_bytes = encode_value_to_bytes(node.dtype, *value)?;
4518            OpParams::Pad {
4519                in_shape: in_dims,
4520                out_shape: out_dims,
4521                padding: padding.clone(),
4522                mode_tag,
4523                fill_bytes,
4524            }
4525        }
4526        Op::IndexAdd { dim } => {
4527            // Inputs: (base, indices, src). All same dtype except
4528            // indices is U32. Output shape == base shape.
4529            if node.inputs.len() != 3 {
4530                return Err(Error::Msg(format!(
4531                    "Op::IndexAdd expects 3 inputs (base, indices, src), got {}",
4532                    node.inputs.len(),
4533                ))
4534                .bt());
4535            }
4536            let base_layout = input_layout(node.inputs[0]);
4537            let idx_layout = input_layout(node.inputs[1]);
4538            let src_layout = input_layout(node.inputs[2]);
4539            let base_dims = base_layout.shape().dims();
4540            let idx_dims = idx_layout.shape().dims();
4541            let src_dims = src_layout.shape().dims();
4542            if *dim >= base_dims.len() {
4543                return Err(Error::Msg(format!(
4544                    "Op::IndexAdd: dim {dim} out of range for base rank {}",
4545                    base_dims.len(),
4546                ))
4547                .bt());
4548            }
4549            if idx_dims.len() != 1 {
4550                return Err(Error::Msg(format!(
4551                    "Op::IndexAdd: indices must be rank 1, got {idx_dims:?}",
4552                ))
4553                .bt());
4554            }
4555            if base_dims.len() != src_dims.len() {
4556                return Err(Error::Msg(format!(
4557                    "Op::IndexAdd: base rank ({}) != src rank ({})",
4558                    base_dims.len(),
4559                    src_dims.len(),
4560                ))
4561                .bt());
4562            }
4563            let outer_count: usize = base_dims[..*dim].iter().product();
4564            let base_dim_size = base_dims[*dim];
4565            let inner_count: usize = base_dims[*dim + 1..].iter().product();
4566            let n_indices = idx_dims[0];
4567            OpParams::IndexAdd {
4568                outer_count,
4569                base_dim_size,
4570                n_indices,
4571                inner_count,
4572            }
4573        }
4574        Op::ScatterAdd { dim } => {
4575            if node.inputs.len() != 3 {
4576                return Err(Error::Msg(format!(
4577                    "Op::ScatterAdd expects 3 inputs (base, indices, src), got {}",
4578                    node.inputs.len(),
4579                ))
4580                .bt());
4581            }
4582            let base_layout = input_layout(node.inputs[0]);
4583            let src_layout = input_layout(node.inputs[2]);
4584            let base_shape: Vec<usize> = base_layout.shape().dims().to_vec();
4585            let src_shape: Vec<usize> = src_layout.shape().dims().to_vec();
4586            if base_shape.len() != src_shape.len() {
4587                return Err(Error::Msg(format!(
4588                    "Op::ScatterAdd: base rank ({}) != src rank ({})",
4589                    base_shape.len(),
4590                    src_shape.len(),
4591                ))
4592                .bt());
4593            }
4594            if *dim >= base_shape.len() {
4595                return Err(Error::Msg(format!(
4596                    "Op::ScatterAdd: dim {dim} out of range for rank {}",
4597                    base_shape.len(),
4598                ))
4599                .bt());
4600            }
4601            OpParams::ScatterAdd {
4602                base_shape,
4603                src_shape,
4604                dim: *dim,
4605            }
4606        }
4607        // Phase 7.6 step 5: Rope routes through the registry; the
4608        // legacy `Op::Rope` arm was retired with the variant.
4609        Op::Fused(_, fuel_graph::registry::FusedOpParams::Rope) => {
4610            // Inputs: (x, cos, sin). x is [..., seq, head_dim];
4611            // cos/sin are [seq, head_dim] (validated at graph build).
4612            if node.inputs.len() != 3 {
4613                return Err(Error::Msg(format!(
4614                    "Op::Rope expects 3 inputs (x, cos, sin), got {}",
4615                    node.inputs.len(),
4616                ))
4617                .bt());
4618            }
4619            let x_layout = input_layout(node.inputs[0]);
4620            let x_dims = x_layout.shape().dims();
4621            if x_dims.len() < 2 {
4622                return Err(
4623                    Error::Msg(format!("Op::Rope: x must have rank ≥ 2, got {x_dims:?}",)).bt(),
4624                );
4625            }
4626            let head_dim = *x_dims.last().unwrap();
4627            let seq = x_dims[x_dims.len() - 2];
4628            let outer_count: usize = x_dims[..x_dims.len() - 2].iter().product();
4629            OpParams::Rope {
4630                outer_count,
4631                seq,
4632                head_dim,
4633            }
4634        }
4635        Op::Gather { dim } => {
4636            // inputs[0] = source, inputs[1] = U32 indices (same rank).
4637            if node.inputs.len() != 2 {
4638                return Err(Error::Msg(format!(
4639                    "Op::Gather expects 2 inputs (source, indices), got {}",
4640                    node.inputs.len(),
4641                ))
4642                .bt());
4643            }
4644            let src_layout = input_layout(node.inputs[0]);
4645            let idx_layout = input_layout(node.inputs[1]);
4646            let source_shape: Vec<usize> = src_layout.shape().dims().to_vec();
4647            let output_shape: Vec<usize> = idx_layout.shape().dims().to_vec();
4648            if source_shape.len() != output_shape.len() {
4649                return Err(Error::Msg(format!(
4650                    "Op::Gather: source rank ({}) != indices rank ({})",
4651                    source_shape.len(),
4652                    output_shape.len(),
4653                ))
4654                .bt());
4655            }
4656            if *dim >= source_shape.len() {
4657                return Err(Error::Msg(format!(
4658                    "Op::Gather: dim {dim} out of range for rank {}",
4659                    source_shape.len(),
4660                ))
4661                .bt());
4662            }
4663            OpParams::Gather {
4664                source_shape,
4665                output_shape,
4666                dim: *dim,
4667            }
4668        }
4669        Op::NonZeroIndices { count_sym } => {
4670            // inputs[0] = x (the value tensor). capacity == x.elem_count();
4671            // the bundled output is [indices [capacity] U32 ; count [1] U32].
4672            if node.inputs.len() != 1 {
4673                return Err(Error::Msg(format!(
4674                    "Op::NonZeroIndices expects 1 input, got {}",
4675                    node.inputs.len(),
4676                ))
4677                .bt());
4678            }
4679            let x_layout = input_layout(node.inputs[0]);
4680            let capacity = x_layout.shape().elem_count();
4681            OpParams::NonZeroIndices {
4682                capacity,
4683                count_sym: *count_sym,
4684            }
4685        }
4686        Op::IndexSelect { dim } => {
4687            // inputs[0] = source, inputs[1] = U32 indices (rank 1).
4688            if node.inputs.len() != 2 {
4689                return Err(Error::Msg(format!(
4690                    "Op::IndexSelect expects 2 inputs (source, indices), got {}",
4691                    node.inputs.len(),
4692                ))
4693                .bt());
4694            }
4695            let src_layout = input_layout(node.inputs[0]);
4696            let idx_layout = input_layout(node.inputs[1]);
4697            let src_dims = src_layout.shape().dims();
4698            let idx_dims = idx_layout.shape().dims();
4699            if *dim >= src_dims.len() {
4700                return Err(Error::Msg(format!(
4701                    "Op::IndexSelect: dim {dim} out of range for source rank {}",
4702                    src_dims.len(),
4703                ))
4704                .bt());
4705            }
4706            if idx_dims.len() != 1 {
4707                return Err(Error::Msg(format!(
4708                    "Op::IndexSelect: indices must be rank 1, got shape {idx_dims:?}",
4709                ))
4710                .bt());
4711            }
4712            let outer_count: usize = src_dims[..*dim].iter().product();
4713            let source_dim_size = src_dims[*dim];
4714            let inner_count: usize = src_dims[*dim + 1..].iter().product();
4715            let n_indices = idx_dims[0];
4716            OpParams::IndexSelect {
4717                outer_count,
4718                source_dim_size,
4719                n_indices,
4720                inner_count,
4721            }
4722        }
4723        // Phase 7.6 step 5: legacy `Op::RmsNormLastDim` /
4724        // `Op::LayerNormLastDim` arms retired with the variants;
4725        // both now route through `Op::Fused(NORM_LAST_DIM, _)`.
4726        // Step 6 follow-up extends this arm to also cover the
4727        // backward variants (same outer × last_dim + eps geometry).
4728        Op::Fused(fid, params)
4729            if *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM
4730                || *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM
4731                || *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM_BACKWARD
4732                || *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM_BACKWARD =>
4733        {
4734            let eps = match params {
4735                fuel_graph::registry::FusedOpParams::RmsNormLastDim { eps } => *eps,
4736                fuel_graph::registry::FusedOpParams::LayerNormLastDim { eps } => *eps,
4737                fuel_graph::registry::FusedOpParams::RmsNormLastDimBackward { eps } => *eps,
4738                fuel_graph::registry::FusedOpParams::LayerNormLastDimBackward { eps } => *eps,
4739                _ => {
4740                    return Err(Error::Msg(format!(
4741                        "Op::Fused(NORM_LAST_DIM[_BACKWARD], _) expected \
4742                     RmsNormLastDim[Backward] or LayerNormLastDim[Backward] params, got {params:?}",
4743                    ))
4744                    .bt());
4745                }
4746            };
4747            let il = input_layout(node.inputs[0]);
4748            let dims = il.shape().dims();
4749            if dims.is_empty() {
4750                return Err(Error::Msg(
4751                    "Op::Fused(NORM_LAST_DIM[_BACKWARD], _) requires rank ≥ 1".to_string(),
4752                )
4753                .bt());
4754            }
4755            let last_dim = *dims.last().unwrap();
4756            let outer_count: usize = dims[..dims.len() - 1].iter().product();
4757            OpParams::NormLastDim {
4758                outer_count,
4759                last_dim,
4760                eps,
4761            }
4762        }
4763        // ReduceMaxToBackward: shape pair via the new
4764        // OpParams::ReduceMaxToBackward variant. x's shape is
4765        // `input_shape`; upstream's shape is `output_shape`.
4766        Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::REDUCE_MAX_TO_BACKWARD => {
4767            if node.inputs.len() != 2 {
4768                return Err(Error::Msg(format!(
4769                    "Op::Fused(REDUCE_MAX_TO_BACKWARD) expects 2 inputs, got {}",
4770                    node.inputs.len(),
4771                ))
4772                .bt());
4773            }
4774            let x_layout = input_layout(node.inputs[0]);
4775            let up_layout = input_layout(node.inputs[1]);
4776            let input_shape: Vec<usize> = x_layout.shape().dims().to_vec();
4777            let output_shape: Vec<usize> = up_layout.shape().dims().to_vec();
4778            OpParams::ReduceMaxToBackward {
4779                input_shape,
4780                output_shape,
4781            }
4782        }
4783        Op::Concat { dim } => {
4784            // Output's shape: [..., total_dim, ...]. Compute outer
4785            // and inner counts from output_shape[..dim] and
4786            // [dim+1..]; per-input dim sizes from each input's
4787            // layout shape at index `dim`.
4788            if node.inputs.is_empty() {
4789                return Err(Error::Msg("Op::Concat requires at least 1 input".to_string()).bt());
4790            }
4791            let out_dims = node.shape.dims();
4792            if *dim >= out_dims.len() {
4793                return Err(Error::Msg(format!(
4794                    "Op::Concat: dim {dim} out of range for output rank {}",
4795                    out_dims.len(),
4796                ))
4797                .bt());
4798            }
4799            let outer_count: usize = out_dims[..*dim].iter().product();
4800            let inner_count: usize = out_dims[*dim + 1..].iter().product();
4801            let mut input_dim_sizes: Vec<usize> = Vec::with_capacity(node.inputs.len());
4802            for in_id in &node.inputs {
4803                let il = input_layout(*in_id);
4804                let il_dims = il.shape().dims();
4805                if *dim >= il_dims.len() {
4806                    return Err(Error::Msg(format!(
4807                        "Op::Concat: input {in_id:?} has rank {} but concat dim is {dim}",
4808                        il_dims.len(),
4809                    ))
4810                    .bt());
4811                }
4812                input_dim_sizes.push(il_dims[*dim]);
4813            }
4814            OpParams::Concat {
4815                outer_count,
4816                input_dim_sizes,
4817                inner_count,
4818                axis: *dim,
4819            }
4820        }
4821        Op::AddScalar(c) => OpParams::Affine { mul: 1.0, add: *c },
4822        Op::MulScalar(c) => OpParams::Affine { mul: *c, add: 0.0 },
4823        Op::Clamp { min, max } => OpParams::Clamp {
4824            min: *min,
4825            max: *max,
4826        },
4827        Op::PowI(exp) => OpParams::PowI { exp: *exp },
4828        Op::ClampInplace { min, max } => OpParams::Clamp {
4829            min: *min,
4830            max: *max,
4831        },
4832        Op::PowIInplace(exp) => OpParams::PowI { exp: *exp },
4833        // PowI backward — `(x, upstream) → grad_x = exp · x^(exp-1) ·
4834        // upstream`. Pulls the same `exp` as the forward through
4835        // FusedOpParams::PowIBackward (autograd carries it across).
4836        Op::Fused(_, fuel_graph::registry::FusedOpParams::PowIBackward { exp }) => {
4837            OpParams::PowI { exp: *exp }
4838        }
4839        // In-place affine (Phase 3 of the in-place ops infrastructure):
4840        // reuse the existing `OpParams::Affine` payload — the kernel
4841        // side doesn't care whether the destination is fresh or aliases
4842        // the input; the structural decision lives in the executor's
4843        // dedicated `WorkItemKind::InplaceKernel` arm.
4844        Op::Fused(_, fuel_graph::registry::FusedOpParams::InplaceAffine { mul, add }) => {
4845            OpParams::Affine {
4846                mul: *mul,
4847                add: *add,
4848            }
4849        }
4850        // Phase 7.6 step 5: legacy `Op::Conv2D` arm retired with the
4851        // variant; Conv2D routes through `Op::Fused(CONV2D, _)`.
4852        Op::Fused(
4853            _,
4854            fuel_graph::registry::FusedOpParams::Conv2D {
4855                stride,
4856                padding,
4857                groups,
4858            },
4859        ) => {
4860            // Inputs[0] = x [N, Cin, Hin, Win]; inputs[1] = weight
4861            // [Cout, Cin/groups, Kh, Kw]; inputs[2] (optional) = bias [Cout].
4862            // Output (this Node's shape) = [N, Cout, Hout, Wout].
4863            if node.inputs.len() != 2 && node.inputs.len() != 3 {
4864                return Err(Error::Msg(format!(
4865                    "Op::Conv2D expects 2 or 3 inputs, got {}",
4866                    node.inputs.len(),
4867                ))
4868                .bt());
4869            }
4870            let x_layout = input_layout(node.inputs[0]);
4871            let w_layout = input_layout(node.inputs[1]);
4872            let x_dims = x_layout.shape().dims();
4873            let w_dims = w_layout.shape().dims();
4874            if x_dims.len() != 4 || w_dims.len() != 4 {
4875                return Err(Error::Msg(format!(
4876                    "Op::Conv2D requires rank-4 x and weight; got x={x_dims:?} w={w_dims:?}",
4877                ))
4878                .bt());
4879            }
4880            let out_dims = node.shape.dims();
4881            if out_dims.len() != 4 {
4882                return Err(Error::Msg(format!(
4883                    "Op::Conv2D output must be rank 4, got {out_dims:?}",
4884                ))
4885                .bt());
4886            }
4887            let x_shape = [x_dims[0], x_dims[1], x_dims[2], x_dims[3]];
4888            let w_shape = [w_dims[0], w_dims[1], w_dims[2], w_dims[3]];
4889            let out_shape = [out_dims[0], out_dims[1], out_dims[2], out_dims[3]];
4890            OpParams::Conv2D {
4891                x_shape,
4892                w_shape,
4893                out_shape,
4894                stride: *stride,
4895                padding: *padding,
4896                dilation: (1, 1),
4897                groups: *groups,
4898            }
4899        }
4900        // Phase 7.6 step 5 (final): legacy `Op::PagedAttn` arm dropped.
4901        Op::Fused(
4902            _,
4903            fuel_graph::registry::FusedOpParams::PagedAttn {
4904                softmax_scale,
4905                block_size,
4906                softcap,
4907            },
4908        ) => {
4909            // Inputs[0]=q [B,Hq,Sq,D], inputs[1]=k_cache [num_blocks,
4910            // block_size, Hkv, D], inputs[2]=v_cache same shape,
4911            // inputs[3]=block_table [B, max_blocks_per_seq] U32,
4912            // inputs[4]=context_lens [B] U32, inputs[5]=alibi [Hq] (optional).
4913            if node.inputs.len() != 5 && node.inputs.len() != 6 {
4914                return Err(Error::Msg(format!(
4915                    "Op::PagedAttn expects 5 or 6 inputs, got {}",
4916                    node.inputs.len(),
4917                ))
4918                .bt());
4919            }
4920            let q_layout = input_layout(node.inputs[0]);
4921            let kc_layout = input_layout(node.inputs[1]);
4922            let vc_layout = input_layout(node.inputs[2]);
4923            let bt_layout = input_layout(node.inputs[3]);
4924            let q_dims = q_layout.shape().dims();
4925            let kc_dims = kc_layout.shape().dims();
4926            let vc_dims = vc_layout.shape().dims();
4927            let bt_dims = bt_layout.shape().dims();
4928            if q_dims.len() != 4 || kc_dims.len() != 4 || vc_dims.len() != 4 {
4929                return Err(Error::Msg(format!(
4930                    "Op::PagedAttn requires rank-4 q/k_cache/v_cache; \
4931                     got q={q_dims:?} k_cache={kc_dims:?} v_cache={vc_dims:?}",
4932                ))
4933                .bt());
4934            }
4935            if kc_dims != vc_dims {
4936                return Err(Error::Msg(format!(
4937                    "Op::PagedAttn: k_cache {kc_dims:?} and v_cache {vc_dims:?} must match",
4938                ))
4939                .bt());
4940            }
4941            if bt_dims.len() != 2 {
4942                return Err(Error::Msg(format!(
4943                    "Op::PagedAttn: block_table must be rank 2 [B, max_blocks_per_seq], got {bt_dims:?}",
4944                ))
4945                .bt());
4946            }
4947            if kc_dims[1] != *block_size {
4948                return Err(Error::Msg(format!(
4949                    "Op::PagedAttn: k_cache block_size dim ({}) must equal Op's block_size ({})",
4950                    kc_dims[1], block_size,
4951                ))
4952                .bt());
4953            }
4954            OpParams::PagedAttn {
4955                b: q_dims[0],
4956                hq: q_dims[1],
4957                hkv: kc_dims[2],
4958                sq: q_dims[2],
4959                d: q_dims[3],
4960                block_size: *block_size,
4961                max_blocks_per_seq: bt_dims[1],
4962                num_blocks: kc_dims[0],
4963                softmax_scale: *softmax_scale,
4964                softcap: *softcap,
4965            }
4966        }
4967        // Phase 7.6 step 5 (final): legacy `Op::FlashAttn` arm dropped.
4968        Op::Fused(
4969            _,
4970            fuel_graph::registry::FusedOpParams::FlashAttn {
4971                softmax_scale,
4972                causal,
4973                window_size_left,
4974                window_size_right,
4975                softcap,
4976                k_len,
4977            },
4978        ) => {
4979            // Inputs[0]=q [B,Hq,Sq,D], inputs[1]=k [B,Hkv,Sk,D],
4980            // inputs[2]=v [B,Hkv,Sk,D], inputs[3]=alibi_slopes [Hq] (optional).
4981            // `sk` is the PHYSICAL K extent (capacity, for strides/bytes);
4982            // `k_len` (resolved below) is the LOGICAL attended length.
4983            if node.inputs.len() != 3 && node.inputs.len() != 4 {
4984                return Err(Error::Msg(format!(
4985                    "Op::FlashAttn expects 3 or 4 inputs, got {}",
4986                    node.inputs.len(),
4987                ))
4988                .bt());
4989            }
4990            let q_layout = input_layout(node.inputs[0]);
4991            let k_layout = input_layout(node.inputs[1]);
4992            let v_layout = input_layout(node.inputs[2]);
4993            let q_dims = q_layout.shape().dims();
4994            let k_dims = k_layout.shape().dims();
4995            let v_dims = v_layout.shape().dims();
4996            if q_dims.len() != 4 || k_dims.len() != 4 || v_dims.len() != 4 {
4997                return Err(Error::Msg(format!(
4998                    "Op::FlashAttn requires rank-4 q/k/v; got q={q_dims:?} k={k_dims:?} v={v_dims:?}",
4999                ))
5000                .bt());
5001            }
5002            if k_dims != v_dims {
5003                return Err(Error::Msg(format!(
5004                    "Op::FlashAttn: k {k_dims:?} and v {v_dims:?} must share shape",
5005                ))
5006                .bt());
5007            }
5008            if q_dims[0] != k_dims[0] || q_dims[3] != k_dims[3] {
5009                return Err(Error::Msg(format!(
5010                    "Op::FlashAttn: q {q_dims:?} and k {k_dims:?} must share B and D",
5011                ))
5012                .bt());
5013            }
5014            let sk = k_dims[2];
5015            // Resolve the logical attended length. `None` ⇒ attend the
5016            // full K extent (today's behavior). `Some(dyn)` ⇒ resolve
5017            // against the per-pass SymEnv; the resolved length must fit
5018            // the capacity and cover Sq (a valid causal prefix).
5019            let attended_k_len = match k_len {
5020                None => sk,
5021                Some(dyn_k) => {
5022                    let kl = dyn_k.resolve(sym_env).ok_or_else(|| {
5023                        Error::Msg(format!(
5024                            "Op::FlashAttn: dynamic k_len {dyn_k:?} is unbound in the SymEnv at realize",
5025                        ))
5026                        .bt()
5027                    })?;
5028                    if kl > sk {
5029                        return Err(Error::Msg(format!(
5030                            "Op::FlashAttn: resolved k_len ({kl}) exceeds K capacity ({sk})",
5031                        ))
5032                        .bt());
5033                    }
5034                    if kl < q_dims[2] {
5035                        return Err(Error::Msg(format!(
5036                            "Op::FlashAttn: resolved k_len ({kl}) < Sq ({}) — not a valid causal prefix",
5037                            q_dims[2],
5038                        ))
5039                        .bt());
5040                    }
5041                    kl
5042                }
5043            };
5044            OpParams::FlashAttn {
5045                b: q_dims[0],
5046                hq: q_dims[1],
5047                hkv: k_dims[1],
5048                sq: q_dims[2],
5049                sk,
5050                d: q_dims[3],
5051                k_len: attended_k_len,
5052                softmax_scale: *softmax_scale,
5053                causal: *causal,
5054                window_size_left: *window_size_left,
5055                window_size_right: *window_size_right,
5056                softcap: *softcap,
5057            }
5058        }
5059        Op::ReduceSumTo(target_shape) => {
5060            if node.inputs.len() != 1 {
5061                return Err(Error::Msg(format!(
5062                    "Op::ReduceSumTo expects 1 input, got {}",
5063                    node.inputs.len(),
5064                ))
5065                .bt());
5066            }
5067            let in_layout = input_layout(node.inputs[0]);
5068            let input_shape: Vec<usize> = in_layout.shape().dims().to_vec();
5069            let output_shape: Vec<usize> = target_shape.dims().to_vec();
5070            OpParams::ReduceSumTo {
5071                input_shape,
5072                output_shape,
5073            }
5074        }
5075        Op::ReduceMaxTo(target_shape) => {
5076            if node.inputs.len() != 1 {
5077                return Err(Error::Msg(format!(
5078                    "Op::ReduceMaxTo expects 1 input, got {}",
5079                    node.inputs.len(),
5080                ))
5081                .bt());
5082            }
5083            let in_layout = input_layout(node.inputs[0]);
5084            let input_shape: Vec<usize> = in_layout.shape().dims().to_vec();
5085            let output_shape: Vec<usize> = target_shape.dims().to_vec();
5086            OpParams::ReduceMaxTo {
5087                input_shape,
5088                output_shape,
5089            }
5090        }
5091        // Phase 7.6 step 5 (final): legacy `Op::ConvTranspose2D` arm dropped.
5092        Op::Fused(
5093            _,
5094            fuel_graph::registry::FusedOpParams::ConvTranspose2D {
5095                stride,
5096                padding,
5097                output_padding,
5098                dilation,
5099                groups,
5100            },
5101        ) => {
5102            // Inputs[0] = x [N, Cin, Hin, Win]; inputs[1] = weight
5103            // [Cin, Cout/groups, Kh, Kw]; inputs[2] (optional) = bias [Cout].
5104            // Output (this Node's shape) = [N, Cout, Hout, Wout].
5105            if node.inputs.len() != 2 && node.inputs.len() != 3 {
5106                return Err(Error::Msg(format!(
5107                    "Op::ConvTranspose2D expects 2 or 3 inputs, got {}",
5108                    node.inputs.len(),
5109                ))
5110                .bt());
5111            }
5112            let x_layout = input_layout(node.inputs[0]);
5113            let w_layout = input_layout(node.inputs[1]);
5114            let x_dims = x_layout.shape().dims();
5115            let w_dims = w_layout.shape().dims();
5116            if x_dims.len() != 4 || w_dims.len() != 4 {
5117                return Err(Error::Msg(format!(
5118                    "Op::ConvTranspose2D requires rank-4 x and weight; got x={x_dims:?} w={w_dims:?}",
5119                ))
5120                .bt());
5121            }
5122            let out_dims = node.shape.dims();
5123            if out_dims.len() != 4 {
5124                return Err(Error::Msg(format!(
5125                    "Op::ConvTranspose2D output must be rank 4, got {out_dims:?}",
5126                ))
5127                .bt());
5128            }
5129            let x_shape = [x_dims[0], x_dims[1], x_dims[2], x_dims[3]];
5130            let w_shape = [w_dims[0], w_dims[1], w_dims[2], w_dims[3]];
5131            let out_shape = [out_dims[0], out_dims[1], out_dims[2], out_dims[3]];
5132            OpParams::ConvTranspose2D {
5133                x_shape,
5134                w_shape,
5135                out_shape,
5136                stride: *stride,
5137                padding: *padding,
5138                output_padding: *output_padding,
5139                dilation: *dilation,
5140                groups: *groups,
5141            }
5142        }
5143        Op::WriteSlice { ranges, dyn_offset } => {
5144            // inputs[0] = destination (its shape == this node's shape
5145            // since WriteSlice's output adopts the destination's
5146            // Layout). inputs[1] = source slab; its shape is implied
5147            // by `ranges`. The kernel needs the destination shape to
5148            // compute strides for the slab walk.
5149            if node.inputs.len() != 2 {
5150                return Err(Error::Msg(format!(
5151                    "Op::WriteSlice expects 2 inputs (destination, source), got {}",
5152                    node.inputs.len(),
5153                ))
5154                .bt());
5155            }
5156            let dest_dims = node.shape.dims().to_vec();
5157            if ranges.len() != dest_dims.len() {
5158                return Err(Error::Msg(format!(
5159                    "Op::WriteSlice: ranges.len() ({}) must equal destination rank ({})",
5160                    ranges.len(),
5161                    dest_dims.len(),
5162                ))
5163                .bt());
5164            }
5165            // Phase D symbolic extents: when present, resolve the runtime
5166            // start offset against the per-pass SymEnv, overriding
5167            // `ranges[axis].0`. The slab WIDTH on that axis stays
5168            // `ranges[axis].1 - ranges[axis].0`, so the effective range
5169            // becomes `(base, base + width)`. `None` ⇒ fully static, and
5170            // the SymEnv is never consulted (empty-env realize unchanged).
5171            //
5172            // DUAL-PATH (increment 2b — data-determined dynamic shapes):
5173            //  - The offset resolves in the compile-time `sym_env` (static,
5174            //    or an INPUT-determined `cached_len`): bake it into `ranges`
5175            //    now, exactly as before.
5176            //  - It is UNBOUND at compile AND a producer in this graph binds
5177            //    it (`Op::NonZeroIndices`'s count, etc.): it is
5178            //    DATA-DETERMINED — leave `ranges[axis]` as the static
5179            //    placeholder and DEFER, carrying the `DynScalar` in
5180            //    `deferred_dyn_offset`. The `WorkItemKind::WriteSlice`
5181            //    executor arm resolves it from `produced_syms` (after the
5182            //    producer runs) and bounds-checks the resolved range there.
5183            //  - It is unbound AND no producer binds it: a genuine caller
5184            //    error → surface it here (preserves the unbound-error path).
5185            let mut ranges = ranges.clone();
5186            let mut deferred_dyn_offset: Option<(usize, fuel_ir::DynScalar)> = None;
5187            if let Some((axis, off)) = dyn_offset {
5188                if *axis >= ranges.len() {
5189                    return Err(Error::Msg(format!(
5190                        "Op::WriteSlice: dyn_offset axis ({axis}) out of bounds for rank {}",
5191                        ranges.len(),
5192                    ))
5193                    .bt());
5194                }
5195                match off.resolve(sym_env) {
5196                    Some(base) => {
5197                        let (start, end) = ranges[*axis];
5198                        let width = end - start;
5199                        ranges[*axis] = (base, base + width);
5200                    }
5201                    None => {
5202                        let is_data_determined = matches!(off, fuel_ir::DynScalar::Sym(s)
5203                            if graph.binds_data_determined_sym(*s));
5204                        if is_data_determined {
5205                            deferred_dyn_offset = Some((*axis, *off));
5206                        } else {
5207                            return Err(Error::Msg(format!(
5208                                "Op::WriteSlice: dynamic offset {off:?} on axis {axis} is \
5209                                 unbound in the SymEnv at realize (and no producer in the \
5210                                 graph binds it, so it is not data-determined)",
5211                            ))
5212                            .bt());
5213                        }
5214                    }
5215                }
5216            }
5217            for (i, &(start, end)) in ranges.iter().enumerate() {
5218                // The deferred axis still holds its static placeholder; the
5219                // executor bounds-checks the resolved range after producing
5220                // the count. Every other axis is fully known now.
5221                if deferred_dyn_offset.is_some_and(|(a, _)| a == i) {
5222                    continue;
5223                }
5224                if end < start || end > dest_dims[i] {
5225                    return Err(Error::Msg(format!(
5226                        "Op::WriteSlice: ranges[{i}] = ({start}, {end}) invalid \
5227                         for destination dim {i} = {}",
5228                        dest_dims[i],
5229                    ))
5230                    .bt());
5231                }
5232            }
5233            OpParams::WriteSlice {
5234                dest_shape: dest_dims,
5235                ranges,
5236                deferred_dyn_offset,
5237            }
5238        }
5239        Op::WriteSliceRotating {
5240            axis,
5241            modulus,
5242            ranges,
5243        } => {
5244            // inputs[0] = destination, inputs[1] = source slab,
5245            // inputs[2] = dynamic position scalar (U32). The builder
5246            // already validated rank / dtype / shape parity; the
5247            // exec-time check below mirrors WriteSlice for the
5248            // structural invariants the executor depends on.
5249            if node.inputs.len() != 3 {
5250                return Err(Error::Msg(format!(
5251                    "Op::WriteSliceRotating expects 3 inputs (destination, source, position), got {}",
5252                    node.inputs.len(),
5253                ))
5254                .bt());
5255            }
5256            let dest_dims = node.shape.dims().to_vec();
5257            if ranges.len() != dest_dims.len() {
5258                return Err(Error::Msg(format!(
5259                    "Op::WriteSliceRotating: ranges.len() ({}) must equal destination rank ({})",
5260                    ranges.len(),
5261                    dest_dims.len(),
5262                ))
5263                .bt());
5264            }
5265            if *axis >= dest_dims.len() {
5266                return Err(Error::Msg(format!(
5267                    "Op::WriteSliceRotating: axis {axis} out of bounds for rank {}",
5268                    dest_dims.len(),
5269                ))
5270                .bt());
5271            }
5272            if *modulus == 0 || *modulus > dest_dims[*axis] {
5273                return Err(Error::Msg(format!(
5274                    "Op::WriteSliceRotating: modulus {modulus} invalid for dest_shape[{axis}] = {}",
5275                    dest_dims[*axis],
5276                ))
5277                .bt());
5278            }
5279            OpParams::WriteSliceRotating {
5280                dest_shape: dest_dims,
5281                axis: *axis,
5282                modulus: *modulus,
5283                ranges: ranges.clone(),
5284            }
5285        }
5286        Op::WriteSliceDoff { axis, ranges } => {
5287            // inputs[0] = destination, inputs[1] = source slab,
5288            // inputs[2] = device-resident I64 offset scalar. The
5289            // builder validated rank / dtype / static-width parity; the
5290            // exec-time check below mirrors WriteSliceRotating minus the
5291            // modulus (no wrap). Bounds on the *dynamic* offset are the
5292            // caller's contract (device-only, un-checkable here).
5293            if node.inputs.len() != 3 {
5294                return Err(Error::Msg(format!(
5295                    "Op::WriteSliceDoff expects 3 inputs (destination, source, offset), got {}",
5296                    node.inputs.len(),
5297                ))
5298                .bt());
5299            }
5300            let dest_dims = node.shape.dims().to_vec();
5301            if ranges.len() != dest_dims.len() {
5302                return Err(Error::Msg(format!(
5303                    "Op::WriteSliceDoff: ranges.len() ({}) must equal destination rank ({})",
5304                    ranges.len(),
5305                    dest_dims.len(),
5306                ))
5307                .bt());
5308            }
5309            if *axis >= dest_dims.len() {
5310                return Err(Error::Msg(format!(
5311                    "Op::WriteSliceDoff: axis {axis} out of bounds for rank {}",
5312                    dest_dims.len(),
5313                ))
5314                .bt());
5315            }
5316            OpParams::WriteSliceDoff {
5317                dest_shape: dest_dims,
5318                axis: *axis,
5319                ranges: ranges.clone(),
5320            }
5321        }
5322        _ => OpParams::None,
5323    })
5324}
5325
5326/// Execute one work item. Three branches by `WorkItemKind`:
5327///
5328/// - `ConstAdopt` — verify cache has an entry pre-seeded by the
5329///   caller; record the layout from the WorkItem.
5330/// - `ViewOf { input }` — clone the input's Storage Arc into the
5331///   output slot (bytes are shared); record the strided layout
5332///   from the WorkItem.
5333/// - `Kernel` — gather input Arcs, allocate the output, run the
5334///   compiled kernel, store the result; record the contiguous
5335///   layout from the WorkItem.
5336///
5337/// Best-effort: prefix a realize-time node error with the failing node's graph
5338/// location — `Graph::describe_node` renders `Node#N (op, out shape, dtype,
5339/// inputs=[...])`, enough to grep-locate the offending op in a large graph. The
5340/// legacy per-node executors (`fuel-graph-executor`, the retired
5341/// `fuel-reference-backend`) wrapped panics this way; the PipelinedExecutor
5342/// surfaces the raw kernel error, so a dtype/shape/OOB failure deep in a model
5343/// arrived with no graph coordinate. On a graph read-lock failure the original
5344/// error passes through unchanged — the location is a navigation aid, never
5345/// worth masking the real error with a lock issue. Zero happy-path cost: only
5346/// the error branch touches the graph.
5347fn with_node_location(graph: &Arc<RwLock<Graph>>, node_id: NodeId, err: Error) -> Error {
5348    match graph.read() {
5349        Ok(g) => err.context(g.describe_node(node_id)),
5350        Err(_) => err,
5351    }
5352}
5353
5354/// Execute one [`WorkItem`] against the cache, returning the node's
5355/// [`CompletionHandle`].
5356///
5357/// Step E A4b-1: the producing arms (Kernel / WriteSlice / WriteSliceRotating /
5358/// in-place / cross-device Copy alloc) return the handle from `execute_compiled`
5359/// *without* waiting it inline — the realize loop stores it in its per-node
5360/// handle map and drains at realize-end. Non-producing arms (const adopt, view,
5361/// slot projection, release marker, contiguize) are pure Arc-clones / host
5362/// allocs whose work is already complete → [`CompletionHandle::Ready`].
5363/// Read a data-determined runtime count from bundle slot 1 of a
5364/// just-realized producer and bind it into the per-pass `SymEnv` under
5365/// `count_sym` — the data-dependent dynamic-shapes seam (increment 2a).
5366/// The producer's bundle is `[indices [capacity] U32 ; count [1] U32]`;
5367/// slot 1's single U32 starts at byte `capacity * 4` (after the
5368/// `capacity`-element U32 primary slot). CPU only for now — device
5369/// backends need a D2H after the completion handle is waited (a documented
5370/// follow-up; the whole data-dependent path is CPU-first). Called AFTER
5371/// `execute_compiled`, whose CPU path returns a `Ready` handle, so the
5372/// bytes are already valid.
5373fn bind_data_determined_count(
5374    arc: &Arc<RwLock<Storage>>,
5375    capacity: usize,
5376    count_sym: fuel_ir::SymId,
5377    produced_syms: &mut SymEnv,
5378) -> Result<()> {
5379    let guard = arc
5380        .read()
5381        .map_err(|_| poisoned("NonZeroIndices output storage"))?;
5382    let count_byte_off = capacity * std::mem::size_of::<u32>();
5383    let end = count_byte_off + std::mem::size_of::<u32>();
5384    // The non-CPU `BackendStorage` variants are cfg-gated; with GPU
5385    // features off, `Cpu` is the only variant and the fallback arm is
5386    // unreachable (a documented CPU-first limitation, not dead logic).
5387    #[allow(unreachable_patterns)]
5388    let count = match &guard.inner {
5389        fuel_memory::BackendStorage::Cpu(cpu_bytes) => {
5390            let bytes = cpu_bytes.bytes();
5391            if end > bytes.len() {
5392                return Err(Error::Msg(format!(
5393                    "NonZeroIndices: count slot byte range [{count_byte_off}..{end}) \
5394                     exceeds bundle byte length {}",
5395                    bytes.len(),
5396                ))
5397                .bt());
5398            }
5399            u32::from_le_bytes(
5400                bytes[count_byte_off..end]
5401                    .try_into()
5402                    .expect("4 bytes → u32"),
5403            )
5404        }
5405        other => {
5406            return Err(Error::Msg(format!(
5407                "NonZeroIndices: data-determined count read is CPU-only today; got \
5408                 BackendStorage::{:?} (device backends need a D2H after the completion \
5409                 handle is waited — a documented follow-up)",
5410                std::mem::discriminant(other),
5411            ))
5412            .bt());
5413        }
5414    };
5415    produced_syms.bind(count_sym, count as usize)?;
5416    Ok(())
5417}
5418
5419/// Increment 2b — the consumer half of data-dependent dynamic shapes. When
5420/// a `WriteSlice`'s offset is DATA-DETERMINED (its `SymId` is produced
5421/// mid-pass, so `op_to_op_params` deferred it into
5422/// `OpParams::WriteSlice::deferred_dyn_offset` instead of baking it at
5423/// compile), resolve it HERE from `produced_syms` — populated by the
5424/// producer's execute step (single-threaded topological order guarantees
5425/// the producer already ran). Returns a patched clone of the `CompiledNode`
5426/// with the resolved `ranges` (and `deferred_dyn_offset` cleared) for the
5427/// kernel; `None` when there is nothing to defer (the common path — no
5428/// clone). Bounds-checks the resolved range (deferred past compile).
5429fn resolve_deferred_write_slice(
5430    compiled: &CompiledNode,
5431    produced_syms: &SymEnv,
5432    node_id: NodeId,
5433) -> Result<Option<CompiledNode>> {
5434    let OpParams::WriteSlice {
5435        dest_shape,
5436        ranges,
5437        deferred_dyn_offset,
5438    } = &compiled.op_params
5439    else {
5440        return Ok(None);
5441    };
5442    let Some((axis, off)) = deferred_dyn_offset else {
5443        return Ok(None);
5444    };
5445    let base = off.resolve(produced_syms).ok_or_else(|| {
5446        Error::Msg(format!(
5447            "Op::WriteSlice {node_id:?}: data-determined offset {off:?} on axis {axis} is \
5448             unbound in produced_syms at execute (its producer did not run / bind it)",
5449        ))
5450        .bt()
5451    })?;
5452    let mut ranges = ranges.clone();
5453    let (start, end) = ranges[*axis];
5454    let width = end - start;
5455    let resolved = (base, base + width);
5456    if resolved.1 > dest_shape[*axis] {
5457        return Err(Error::Msg(format!(
5458            "Op::WriteSlice {node_id:?}: resolved data-determined range {resolved:?} on axis \
5459             {axis} exceeds destination dim = {}",
5460            dest_shape[*axis],
5461        ))
5462        .bt());
5463    }
5464    ranges[*axis] = resolved;
5465    let mut patched = compiled.clone();
5466    patched.op_params = OpParams::WriteSlice {
5467        dest_shape: dest_shape.clone(),
5468        ranges,
5469        deferred_dyn_offset: None,
5470    };
5471    Ok(Some(patched))
5472}
5473
5474/// Dynamic-M matmul: when a matmul's row count is DATA-DETERMINED
5475/// (`MatmulM::Deferred`, deferred by `op_to_op_params` because a producer
5476/// binds its `SymId` mid-pass), resolve it HERE from `produced_syms` and
5477/// run the kernel against a patched clone whose `m_compute` is
5478/// `MatmulM::Rows(count)`. Mirrors `resolve_deferred_write_slice`. `None`
5479/// when nothing is deferred (the common path — no clone). The kernel then
5480/// computes exactly `count` rows of the `m`-row capacity buffer.
5481fn resolve_deferred_matmul(
5482    compiled: &CompiledNode,
5483    produced_syms: &SymEnv,
5484    node_id: NodeId,
5485) -> Result<Option<CompiledNode>> {
5486    let OpParams::Matmul {
5487        lhs_batch_dims,
5488        rhs_batch_dims,
5489        m,
5490        n,
5491        k,
5492        m_compute,
5493    } = &compiled.op_params
5494    else {
5495        return Ok(None);
5496    };
5497    let MatmulM::Deferred(dyn_m) = m_compute else {
5498        return Ok(None);
5499    };
5500    let rows = dyn_m.resolve(produced_syms).ok_or_else(|| {
5501        Error::Msg(format!(
5502            "Op::MatMul {node_id:?}: data-determined row count {dyn_m:?} is unbound in \
5503             produced_syms at execute (its producer did not run / bind it)",
5504        ))
5505        .bt()
5506    })?;
5507    if rows > *m {
5508        return Err(Error::Msg(format!(
5509            "Op::MatMul {node_id:?}: resolved row count {rows} exceeds capacity {m}",
5510        ))
5511        .bt());
5512    }
5513    let mut patched = compiled.clone();
5514    patched.op_params = OpParams::Matmul {
5515        lhs_batch_dims: lhs_batch_dims.clone(),
5516        rhs_batch_dims: rhs_batch_dims.clone(),
5517        m: *m,
5518        n: *n,
5519        k: *k,
5520        m_compute: MatmulM::Rows(rows),
5521    };
5522    Ok(Some(patched))
5523}
5524
5525fn execute_work_item(
5526    item: &WorkItem,
5527    cache: &mut StorageCache,
5528    layout_cache: &mut HashMap<NodeId, Layout>,
5529    produced_syms: &mut SymEnv,
5530    // Step E A4b open question #4 — "event only where waited": whether
5531    // THIS item's node id is in the realize's `WaitSet` (`will_be_waited`
5532    // in `pipelined.rs`, computed once per item by the caller). Forwarded
5533    // to every `execute_compiled_with_wait_hint` call below so a CUDA
5534    // kernel with no mid-walk reader skips its `Event` entirely.
5535    will_be_waited: bool,
5536    // CapturedRun executor build-out: persistent-output mode. `None` on
5537    // every existing realize path ⇒ byte-identical (fresh alloc, nothing
5538    // recorded). `Some(Record)` records each Kernel output's fixed Arc;
5539    // `Some(Reuse)` reuses the recorded buffer (no alloc — capture-safe)
5540    // and the write-into kernel path writes into it in place. Only the
5541    // `Kernel` arm consults it; every other arm ignores it.
5542    mut persistent: Option<&mut PersistentOutputs>,
5543) -> Result<CompletionHandle> {
5544    match &item.kind {
5545        WorkItemKind::ConstAdopt => {
5546            if !cache.contains_key(&item.node_id) {
5547                return Err(Error::Msg(format!(
5548                    "PipelinedExecutor: Const node {:?} not in input cache",
5549                    item.node_id
5550                ))
5551                .bt());
5552            }
5553            // Layout for input cache entries was seeded at realize
5554            // start; refresh from the WorkItem in case the side-table
5555            // was set after seeding.
5556            layout_cache.insert(item.node_id, item.output_layout.clone());
5557            Ok(CompletionHandle::Ready)
5558        }
5559        WorkItemKind::ViewOf { input } => {
5560            let input_arc = cache.get(input).cloned().ok_or_else(|| {
5561                Error::Msg(format!(
5562                    "PipelinedExecutor: view-op input {:?} of {:?} not realized",
5563                    input, item.node_id,
5564                ))
5565                .bt()
5566            })?;
5567            cache.insert(item.node_id, input_arc);
5568            layout_cache.insert(item.node_id, item.output_layout.clone());
5569            Ok(CompletionHandle::Ready)
5570        }
5571        WorkItemKind::SlotView { producer } => {
5572            // Op::View — multi-output projection. Same realization
5573            // shape as ViewOf: clone the producer's Storage Arc into
5574            // the View's cache slot. The WorkItem's output_layout was
5575            // computed at graph-build time by `Tensor::view`, with
5576            // the slot's byte_offset baked into start_offset (in
5577            // slot-dtype-element units). Downstream kernels reading
5578            // the producer's bytes as the slot's dtype starting at
5579            // that offset see the slot's typed window.
5580            let producer_arc = cache.get(producer).cloned().ok_or_else(|| {
5581                Error::Msg(format!(
5582                    "PipelinedExecutor: Op::View producer {:?} of {:?} not realized",
5583                    producer, item.node_id,
5584                ))
5585                .bt()
5586            })?;
5587            cache.insert(item.node_id, producer_arc);
5588            layout_cache.insert(item.node_id, item.output_layout.clone());
5589            Ok(CompletionHandle::Ready)
5590        }
5591        WorkItemKind::SlotOwn { producer, slot } => {
5592            // Op::ViewOwned — allocate a fresh Storage of the slot's
5593            // (shape, dtype) on the producer's device and copy the
5594            // slot's bytes in. Per-backend fast paths:
5595            //   - CPU:    direct &[u8]→&mut[u8] memcpy via CpuStorageBytes
5596            //   - CUDA:   D2D via CudaStorageBytes::slot_copy_to_new
5597            //             (cuMemcpyDtoDAsync with offset src pointer)
5598            //   - Vulkan: vkCmdCopyBuffer via
5599            //             VulkanBackend::slot_copy_to_new_handle
5600            //             (srcOffset/dstOffset/size)
5601            // For backends without a copy-with-offset hook we fall
5602            // back to a host round-trip (`to_host_buffer_dyn` →
5603            // slice → `storage_from_host_buffer_owned_dyn`), which
5604            // works but is wasteful.
5605            let producer_arc = cache.get(producer).cloned().ok_or_else(|| {
5606                Error::Msg(format!(
5607                    "PipelinedExecutor: Op::ViewOwned producer {:?} of {:?} not realized",
5608                    producer, item.node_id,
5609                ))
5610                .bt()
5611            })?;
5612            let new_storage = {
5613                let guard = producer_arc
5614                    .read()
5615                    .map_err(|_| poisoned("ViewOwned producer storage"))?;
5616                let bundle = guard.bundle().ok_or_else(|| {
5617                    Error::Msg(format!(
5618                        "Op::ViewOwned: producer {:?} has no bundle side-table \
5619                         — the multi-output authoring contract requires \
5620                         Storage::with_bundle / new_bundled at allocation \
5621                         time. Node {:?} slot {} cannot resolve its source.",
5622                        producer, item.node_id, slot,
5623                    ))
5624                    .bt()
5625                })?;
5626                let sv = bundle.get(*slot as usize).cloned().ok_or_else(|| {
5627                    Error::Msg(format!(
5628                        "Op::ViewOwned: slot {} out of range for producer {:?} \
5629                         (bundle has {} slots)",
5630                        slot,
5631                        producer,
5632                        bundle.len(),
5633                    ))
5634                    .bt()
5635                })?;
5636                use fuel_memory::BackendStorage;
5637                match &guard.inner {
5638                    BackendStorage::Cpu(cpu_bytes) => {
5639                        let src_bytes = cpu_bytes.bytes();
5640                        let end = sv.byte_offset + sv.len_bytes();
5641                        if end > src_bytes.len() {
5642                            return Err(Error::Msg(format!(
5643                                "Op::ViewOwned: slot {} byte range [{}..{}) \
5644                                 exceeds producer's CPU byte length {}",
5645                                slot,
5646                                sv.byte_offset,
5647                                end,
5648                                src_bytes.len(),
5649                            ))
5650                            .bt());
5651                        }
5652                        let owned = fuel_cpu_backend::CpuStorageBytes::from_bytes(
5653                            &src_bytes[sv.byte_offset..end],
5654                        );
5655                        fuel_memory::Storage::new(BackendStorage::Cpu(owned), sv.dtype)
5656                    }
5657                    #[cfg(feature = "cuda")]
5658                    BackendStorage::Cuda(cuda_bytes) => {
5659                        let dst = cuda_bytes.slot_copy_to_new(sv.byte_offset, sv.len_bytes())?;
5660                        fuel_memory::Storage::new(BackendStorage::Cuda(dst), sv.dtype)
5661                    }
5662                    #[cfg(feature = "vulkan")]
5663                    BackendStorage::Vulkan(vk_bytes) => {
5664                        let backend = vk_bytes.backend().ok_or_else(|| {
5665                            Error::Msg(format!(
5666                                "Op::ViewOwned: producer {:?} is on Vulkan but \
5667                                 its storage has no VulkanBackend handle. \
5668                                 Storages flowing through the pipelined executor \
5669                                 must be constructed via alloc_bytes_handle / \
5670                                 upload_bytes_handle so the bundle's slot extraction \
5671                                 path can reach the backend.",
5672                                producer,
5673                            ))
5674                            .bt()
5675                        })?;
5676                        let dst = backend.slot_copy_to_new_handle(
5677                            vk_bytes,
5678                            sv.byte_offset,
5679                            sv.len_bytes(),
5680                        )?;
5681                        fuel_memory::Storage::new(BackendStorage::Vulkan(dst), sv.dtype)
5682                    }
5683                    #[allow(unreachable_patterns)]
5684                    _ => {
5685                        return Err(Error::Msg(format!(
5686                            "Op::ViewOwned: producer {:?} is on a backend \
5687                             without a slot-copy-with-offset hook \
5688                             (CPU + CUDA + Vulkan wired). Add a per-backend \
5689                             `slot_copy_to_new`-equivalent and extend the \
5690                             match arm.",
5691                            producer,
5692                        ))
5693                        .bt());
5694                    }
5695                }
5696            };
5697            cache.insert(item.node_id, Arc::new(RwLock::new(new_storage)));
5698            layout_cache.insert(item.node_id, item.output_layout.clone());
5699            // A4b-1: Op::ViewOwned's slot copy (CUDA D2D) is same-stream; its
5700            // completion is carried by the realize-end full-stream sync in
5701            // `to_cpu_bytes` (unchanged) — `Ready` here is behavior-preserving.
5702            Ok(CompletionHandle::Ready)
5703        }
5704        WorkItemKind::WriteSlice { dest, source } => {
5705            let compiled = item.compiled.as_ref().ok_or_else(|| {
5706                Error::Msg(format!(
5707                    "PipelinedExecutor: WriteSlice work item {:?} has no compiled node",
5708                    item.node_id,
5709                ))
5710                .bt()
5711            })?;
5712            // Increment 2b: if the offset is DATA-DETERMINED, resolve it from
5713            // the producer-bound `produced_syms` and run the kernel against
5714            // the patched (resolved-range) node. `None` on the common path.
5715            let patched_ws = resolve_deferred_write_slice(compiled, produced_syms, item.node_id)?;
5716            let compiled = patched_ws.as_ref().unwrap_or(compiled);
5717            let dest_arc = cache.get(dest).cloned().ok_or_else(|| {
5718                Error::Msg(format!(
5719                    "PipelinedExecutor: WriteSlice destination {:?} of {:?} not realized",
5720                    dest, item.node_id,
5721                ))
5722                .bt()
5723            })?;
5724            let source_arc = cache.get(source).cloned().ok_or_else(|| {
5725                Error::Msg(format!(
5726                    "PipelinedExecutor: WriteSlice source {:?} of {:?} not realized",
5727                    source, item.node_id,
5728                ))
5729                .bt()
5730            })?;
5731            let dest_layout = layout_cache.get(dest).cloned().ok_or_else(|| {
5732                Error::Msg(format!(
5733                    "PipelinedExecutor: WriteSlice destination {:?} of {:?} has no cached layout",
5734                    dest, item.node_id,
5735                ))
5736                .bt()
5737            })?;
5738            let source_layout = layout_cache.get(source).cloned().ok_or_else(|| {
5739                Error::Msg(format!(
5740                    "PipelinedExecutor: WriteSlice source {:?} of {:?} has no cached layout",
5741                    source, item.node_id,
5742                ))
5743                .bt()
5744            })?;
5745            // v1 contract: destination must be contiguous + zero
5746            // offset. Strided dests would force the kernel to walk
5747            // both source and dest strides; defer until KV cache
5748            // exposes a non-contig dest. Source CAN be non-contig —
5749            // we auto-contiguize it here so the kernel sees a flat
5750            // slab.
5751            if !dest_layout.is_contiguous() || dest_layout.start_offset() != 0 {
5752                return Err(Error::Msg(format!(
5753                    "Op::WriteSlice (Phase E.3.2 v1): destination {:?} must be \
5754                     contiguous + zero-offset; got Layout {:?}",
5755                    dest, dest_layout,
5756                ))
5757                .bt());
5758            }
5759            let source_arc_contig = {
5760                let s_dtype = source_arc
5761                    .read()
5762                    .map_err(|_| poisoned("WriteSlice source storage"))?
5763                    .dtype;
5764                let s_len_bytes = source_arc
5765                    .read()
5766                    .map_err(|_| poisoned("WriteSlice source storage"))?
5767                    .inner
5768                    .len_bytes();
5769                let layout_bytes = source_layout.shape().elem_count() * s_dtype.size_in_bytes();
5770                let bytes_match_shape = s_len_bytes == layout_bytes;
5771                let already_contig = source_layout.is_contiguous()
5772                    && source_layout.start_offset() == 0
5773                    && bytes_match_shape;
5774                if already_contig {
5775                    source_arc
5776                } else {
5777                    auto_contiguize(&source_arc, &source_layout)?
5778                }
5779            };
5780            let source_layout_kernel = fuel_ir::Layout::contiguous(source_layout.shape().clone());
5781            // The kernel sees inputs=[source] and outputs=[dest_arc].
5782            // The dest input slot is intentionally absent — the kernel
5783            // doesn't read from dest; it only writes to its bytes
5784            // through the output slot's write lock.
5785            let input_arcs = vec![source_arc_contig];
5786            let mut output_arcs = vec![dest_arc.clone()];
5787            let kernel_layouts = vec![source_layout_kernel, dest_layout.clone()];
5788            // A4b-1: defer the wait — return the handle to the realize loop.
5789            let handle = execute_compiled_with_wait_hint(
5790                compiled,
5791                &input_arcs,
5792                &mut output_arcs,
5793                &kernel_layouts,
5794                will_be_waited,
5795            )?;
5796            // Adopt the dest Arc at this WriteSlice node's slot. The
5797            // realize loop's destructive_input cleanup evicts the
5798            // dest's own NodeId from the cache afterward — downstream
5799            // readers go through this node's NodeId.
5800            cache.insert(item.node_id, dest_arc);
5801            layout_cache.insert(item.node_id, item.output_layout.clone());
5802            Ok(handle)
5803        }
5804        WorkItemKind::WriteSliceRotating {
5805            dest,
5806            source,
5807            position,
5808        } => {
5809            // Same shape as WriteSlice but with a position input.
5810            // Kernel sees `[source, position]` as inputs; `dest` is the
5811            // pre-allocated output buffer (in-place adoption).
5812            let compiled = item.compiled.as_ref().ok_or_else(|| {
5813                Error::Msg(format!(
5814                    "PipelinedExecutor: WriteSliceRotating work item {:?} has no compiled node",
5815                    item.node_id,
5816                ))
5817                .bt()
5818            })?;
5819            let dest_arc = cache.get(dest).cloned().ok_or_else(|| {
5820                Error::Msg(format!(
5821                    "PipelinedExecutor: WriteSliceRotating destination {:?} of {:?} not realized",
5822                    dest, item.node_id,
5823                ))
5824                .bt()
5825            })?;
5826            let source_arc = cache.get(source).cloned().ok_or_else(|| {
5827                Error::Msg(format!(
5828                    "PipelinedExecutor: WriteSliceRotating source {:?} of {:?} not realized",
5829                    source, item.node_id,
5830                ))
5831                .bt()
5832            })?;
5833            let position_arc = cache.get(position).cloned().ok_or_else(|| {
5834                Error::Msg(format!(
5835                    "PipelinedExecutor: WriteSliceRotating position {:?} of {:?} not realized",
5836                    position, item.node_id,
5837                ))
5838                .bt()
5839            })?;
5840            let dest_layout = layout_cache.get(dest).cloned().ok_or_else(|| {
5841                Error::Msg(format!(
5842                    "PipelinedExecutor: WriteSliceRotating destination {:?} of {:?} has no cached layout",
5843                    dest, item.node_id,
5844                ))
5845                .bt()
5846            })?;
5847            let source_layout = layout_cache.get(source).cloned().ok_or_else(|| {
5848                Error::Msg(format!(
5849                    "PipelinedExecutor: WriteSliceRotating source {:?} of {:?} has no cached layout",
5850                    source, item.node_id,
5851                ))
5852                .bt()
5853            })?;
5854            // v1: destination contiguous + zero offset (same as WriteSlice).
5855            if !dest_layout.is_contiguous() || dest_layout.start_offset() != 0 {
5856                return Err(Error::Msg(format!(
5857                    "Op::WriteSliceRotating (v1): destination {:?} must be \
5858                     contiguous + zero-offset; got Layout {:?}",
5859                    dest, dest_layout,
5860                ))
5861                .bt());
5862            }
5863            let source_arc_contig = {
5864                let s_dtype = source_arc
5865                    .read()
5866                    .map_err(|_| poisoned("WriteSliceRotating source storage"))?
5867                    .dtype;
5868                let s_len_bytes = source_arc
5869                    .read()
5870                    .map_err(|_| poisoned("WriteSliceRotating source storage"))?
5871                    .inner
5872                    .len_bytes();
5873                let layout_bytes = source_layout.shape().elem_count() * s_dtype.size_in_bytes();
5874                let bytes_match_shape = s_len_bytes == layout_bytes;
5875                let already_contig = source_layout.is_contiguous()
5876                    && source_layout.start_offset() == 0
5877                    && bytes_match_shape;
5878                if already_contig {
5879                    source_arc
5880                } else {
5881                    auto_contiguize(&source_arc, &source_layout)?
5882                }
5883            };
5884            let source_layout_kernel = fuel_ir::Layout::contiguous(source_layout.shape().clone());
5885            let position_layout = fuel_ir::Layout::contiguous(fuel_ir::Shape::from_dims(&[]));
5886            let input_arcs = vec![source_arc_contig, position_arc];
5887            let mut output_arcs = vec![dest_arc.clone()];
5888            let kernel_layouts = vec![source_layout_kernel, position_layout, dest_layout.clone()];
5889            // A4b-1: defer the wait — return the handle to the realize loop.
5890            let handle = execute_compiled_with_wait_hint(
5891                compiled,
5892                &input_arcs,
5893                &mut output_arcs,
5894                &kernel_layouts,
5895                will_be_waited,
5896            )?;
5897            cache.insert(item.node_id, dest_arc);
5898            layout_cache.insert(item.node_id, item.output_layout.clone());
5899            Ok(handle)
5900        }
5901        WorkItemKind::WriteSliceDoff {
5902            dest,
5903            source,
5904            offset,
5905        } => {
5906            // Same shape as WriteSliceRotating: kernel sees
5907            // `[source, offset]` as inputs; `dest` is the pre-allocated
5908            // output buffer (in-place adoption). The offset (rank-0
5909            // I64) is passed through as a kernel input — the CUDA
5910            // wrapper threads its device pointer straight to the kernel
5911            // (no D2H), which is what makes a captured graph replay at
5912            // the host-updated position.
5913            let compiled = item.compiled.as_ref().ok_or_else(|| {
5914                Error::Msg(format!(
5915                    "PipelinedExecutor: WriteSliceDoff work item {:?} has no compiled node",
5916                    item.node_id,
5917                ))
5918                .bt()
5919            })?;
5920            let dest_arc = cache.get(dest).cloned().ok_or_else(|| {
5921                Error::Msg(format!(
5922                    "PipelinedExecutor: WriteSliceDoff destination {:?} of {:?} not realized",
5923                    dest, item.node_id,
5924                ))
5925                .bt()
5926            })?;
5927            let source_arc = cache.get(source).cloned().ok_or_else(|| {
5928                Error::Msg(format!(
5929                    "PipelinedExecutor: WriteSliceDoff source {:?} of {:?} not realized",
5930                    source, item.node_id,
5931                ))
5932                .bt()
5933            })?;
5934            let offset_arc = cache.get(offset).cloned().ok_or_else(|| {
5935                Error::Msg(format!(
5936                    "PipelinedExecutor: WriteSliceDoff offset {:?} of {:?} not realized",
5937                    offset, item.node_id,
5938                ))
5939                .bt()
5940            })?;
5941            let dest_layout = layout_cache.get(dest).cloned().ok_or_else(|| {
5942                Error::Msg(format!(
5943                    "PipelinedExecutor: WriteSliceDoff destination {:?} of {:?} has no cached layout",
5944                    dest, item.node_id,
5945                ))
5946                .bt()
5947            })?;
5948            let source_layout = layout_cache.get(source).cloned().ok_or_else(|| {
5949                Error::Msg(format!(
5950                    "PipelinedExecutor: WriteSliceDoff source {:?} of {:?} has no cached layout",
5951                    source, item.node_id,
5952                ))
5953                .bt()
5954            })?;
5955            // v1: destination contiguous + zero offset (same as WriteSlice).
5956            if !dest_layout.is_contiguous() || dest_layout.start_offset() != 0 {
5957                return Err(Error::Msg(format!(
5958                    "Op::WriteSliceDoff (v1): destination {:?} must be \
5959                     contiguous + zero-offset; got Layout {:?}",
5960                    dest, dest_layout,
5961                ))
5962                .bt());
5963            }
5964            let source_arc_contig = {
5965                let s_dtype = source_arc
5966                    .read()
5967                    .map_err(|_| poisoned("WriteSliceDoff source storage"))?
5968                    .dtype;
5969                let s_len_bytes = source_arc
5970                    .read()
5971                    .map_err(|_| poisoned("WriteSliceDoff source storage"))?
5972                    .inner
5973                    .len_bytes();
5974                let layout_bytes = source_layout.shape().elem_count() * s_dtype.size_in_bytes();
5975                let bytes_match_shape = s_len_bytes == layout_bytes;
5976                let already_contig = source_layout.is_contiguous()
5977                    && source_layout.start_offset() == 0
5978                    && bytes_match_shape;
5979                if already_contig {
5980                    source_arc
5981                } else {
5982                    auto_contiguize(&source_arc, &source_layout)?
5983                }
5984            };
5985            let source_layout_kernel = fuel_ir::Layout::contiguous(source_layout.shape().clone());
5986            let offset_layout = fuel_ir::Layout::contiguous(fuel_ir::Shape::from_dims(&[]));
5987            let input_arcs = vec![source_arc_contig, offset_arc];
5988            let mut output_arcs = vec![dest_arc.clone()];
5989            let kernel_layouts = vec![source_layout_kernel, offset_layout, dest_layout.clone()];
5990            // A4b-1: defer the wait — return the handle to the realize loop.
5991            let handle = execute_compiled_with_wait_hint(
5992                compiled,
5993                &input_arcs,
5994                &mut output_arcs,
5995                &kernel_layouts,
5996                will_be_waited,
5997            )?;
5998            cache.insert(item.node_id, dest_arc);
5999            layout_cache.insert(item.node_id, item.output_layout.clone());
6000            Ok(handle)
6001        }
6002        WorkItemKind::Alloc { target_location } => {
6003            // Op::Alloc { target }: allocate a fresh, zero-init storage
6004            // on target_location. No inputs, no kernel call — direct
6005            // executor dispatch. For non-CPU targets we derive the
6006            // per-backend device handle by searching the input cache
6007            // for any storage on the target backend (callers seed this
6008            // via `pipelined_bridge::device_seed_storage`).
6009            // Used only by the Vulkan arm below (alloc_bytes_handle); a
6010            // default-feature build sees it unused. GAP-229: 68161981's
6011            // unused_variables --fix renamed it `_n_bytes` at default
6012            // features and broke `--features vulkan` (E0425). Guard the
6013            // decline instead of the name so the binding stays usable.
6014            #[cfg_attr(not(feature = "vulkan"), allow(unused_variables))]
6015            let n_bytes = item.elem_count * item.dtype.size_in_bytes();
6016            let alloced: Storage = match target_location {
6017                // CPU has no separate uninit alloc primitive in safe
6018                // Rust (`vec![0; n]` is the canonical path). Op::Alloc
6019                // on CPU returns zero-init storage; the following
6020                // Op::ZeroFill is a redundant memset the optimizer
6021                // can elide and LLVM folds away regardless.
6022                DeviceLocation::Cpu => fuel_memory::alloc_cpu_zeroed(item.dtype, item.elem_count)?,
6023                #[cfg(feature = "cuda")]
6024                DeviceLocation::Cuda { gpu_id } => {
6025                    let cuda_dev = find_cuda_device_in_cache(cache, *gpu_id).ok_or_else(|| {
6026                        Error::Msg(format!(
6027                            "Op::Alloc on Cuda {{ gpu_id: {} }}: no CUDA \
6028                             storage in input cache to derive the device \
6029                             handle from. The caller must seed the cache \
6030                             (e.g. via `fuel-core::pipelined_bridge::\
6031                             device_seed_storage`) before realizing.",
6032                            gpu_id,
6033                        ))
6034                        .bt()
6035                    })?;
6036                    // Phase 3a follow-up: true uninit alloc via
6037                    // baracuda alpha.30's unsafe alloc. The bytes are
6038                    // uninit until a subsequent Op::ZeroFill or full-
6039                    // overwrite op runs. The byte-storage Arc has no
6040                    // typed-slice accessor that would dereference
6041                    // uninit bytes.
6042                    let cuda_bytes =
6043                        fuel_cuda_backend::CudaStorageBytes::alloc_uninit(&cuda_dev, n_bytes)?;
6044                    Storage::new(fuel_memory::BackendStorage::Cuda(cuda_bytes), item.dtype)
6045                }
6046                #[cfg(not(feature = "cuda"))]
6047                DeviceLocation::Cuda { .. } => {
6048                    return Err(Error::Msg(
6049                        "Op::Alloc on Cuda but fuel-storage wasn't built \
6050                         with --features cuda"
6051                            .to_string(),
6052                    )
6053                    .bt());
6054                }
6055                #[cfg(feature = "vulkan")]
6056                DeviceLocation::Vulkan { gpu_id } => {
6057                    let backend =
6058                        find_vulkan_backend_in_cache(cache, *gpu_id).ok_or_else(|| {
6059                            Error::Msg(format!(
6060                                "Op::Alloc on Vulkan {{ gpu_id: {} }}: no Vulkan \
6061                             storage in input cache to derive the backend \
6062                             handle from. The caller must seed the cache \
6063                             (e.g. via `fuel-core::pipelined_bridge::\
6064                             device_seed_storage`) before realizing.",
6065                                gpu_id,
6066                            ))
6067                            .bt()
6068                        })?;
6069                    // Phase 3a follow-up: true uninit alloc. The old
6070                    // `upload_bytes_handle(vec![0u8; n])` host-staged
6071                    // zeros (~2× the bandwidth of a device-side fill);
6072                    // we now allocate uninit and rely on a paired
6073                    // Op::ZeroFill (vkCmdFillBuffer) for the
6074                    // initialization. Faster KV-cache init on Vulkan.
6075                    let vk_bytes = backend.alloc_bytes_handle(n_bytes)?;
6076                    Storage::new(fuel_memory::BackendStorage::Vulkan(vk_bytes), item.dtype)
6077                }
6078                #[cfg(not(feature = "vulkan"))]
6079                DeviceLocation::Vulkan { .. } => {
6080                    return Err(Error::Msg(
6081                        "Op::Alloc on Vulkan but fuel-storage wasn't built \
6082                         with --features vulkan"
6083                            .to_string(),
6084                    )
6085                    .bt());
6086                }
6087                other => {
6088                    return Err(Error::Msg(format!(
6089                        "Op::Alloc on {:?}: target not yet wired (CPU + \
6090                         CUDA + Vulkan land in Phase 3a of bridge-retirement; \
6091                         Metal extends when its byte-storage substrate is \
6092                         ready).",
6093                        other,
6094                    ))
6095                    .bt());
6096                }
6097            };
6098            cache.insert(item.node_id, Arc::new(RwLock::new(alloced)));
6099            layout_cache.insert(item.node_id, item.output_layout.clone());
6100            Ok(CompletionHandle::Ready)
6101        }
6102        WorkItemKind::Iota { len } => {
6103            // Op::Iota { len }: 0-input F32 position generator
6104            // [0.0, 1.0, …, (len-1)]. Direct executor dispatch (no kernel /
6105            // binding-table lookup), like WorkItemKind::Alloc. CPU-only
6106            // today — GPU flash-attn keeps its fused kernel, so the alibi
6107            // decomposition (the sole Iota consumer) realizes on CPU.
6108            if item.target_backend != BackendId::Cpu {
6109                return Err(Error::Msg(format!(
6110                    "WorkItemKind::Iota: only CPU is wired today, got {:?}",
6111                    item.target_backend,
6112                ))
6113                .bt());
6114            }
6115            let v: Vec<f32> = (0..*len).map(|i| i as f32).collect();
6116            let iota = fuel_memory::from_slice_cpu(&v);
6117            cache.insert(item.node_id, Arc::new(RwLock::new(iota)));
6118            layout_cache.insert(item.node_id, item.output_layout.clone());
6119            Ok(CompletionHandle::Ready)
6120        }
6121        WorkItemKind::ZeroFill => {
6122            // Op::ZeroFill: in-place zero the input's bytes. Output
6123            // adopts the input's Storage Arc (same Storage; bytes
6124            // are mutated). Direct per-backend dispatch — no kernel
6125            // call, no binding-table lookup.
6126            if item.inputs.len() != 1 {
6127                return Err(Error::Msg(format!(
6128                    "PipelinedExecutor: ZeroFill work item {:?} expects \
6129                     1 input, got {}",
6130                    item.node_id,
6131                    item.inputs.len(),
6132                ))
6133                .bt());
6134            }
6135            let src_id = item.inputs[0];
6136            let src_arc = cache.get(&src_id).cloned().ok_or_else(|| {
6137                Error::Msg(format!(
6138                    "PipelinedExecutor: ZeroFill input {:?} of {:?} \
6139                     not realized",
6140                    src_id, item.node_id,
6141                ))
6142                .bt()
6143            })?;
6144            // Per-backend in-place fill. Acquired the write lock once
6145            // for the duration of the fill — kernels on async backends
6146            // (CUDA / Vulkan) issue the memset and return; the fence
6147            // happens when downstream readers acquire the read lock
6148            // through the executor's standard synchronization path.
6149            {
6150                let mut guard = src_arc
6151                    .write()
6152                    .map_err(|_| poisoned("ZeroFill destination storage"))?;
6153                match &mut guard.inner {
6154                    fuel_memory::BackendStorage::Cpu(c) => {
6155                        let bytes = c.bytes_mut();
6156                        bytes.fill(0);
6157                    }
6158                    #[cfg(feature = "cuda")]
6159                    fuel_memory::BackendStorage::Cuda(c) => {
6160                        c.zero_async()?;
6161                    }
6162                    #[cfg(feature = "vulkan")]
6163                    fuel_memory::BackendStorage::Vulkan(v) => {
6164                        let backend = v
6165                            .backend()
6166                            .ok_or_else(|| {
6167                                Error::Msg(
6168                                    "Op::ZeroFill on Vulkan: input has no \
6169                                 attached backend handle. Storage must \
6170                                 come from VulkanBackend::alloc_bytes_handle \
6171                                 / upload_bytes_handle."
6172                                        .to_string(),
6173                                )
6174                                .bt()
6175                            })?
6176                            .clone();
6177                        backend.fill_bytes_zero(v)?;
6178                    }
6179                    #[allow(unreachable_patterns)]
6180                    other => {
6181                        return Err(Error::Msg(format!(
6182                            "Op::ZeroFill: backend not wired ({other:?}); \
6183                             CPU + CUDA + Vulkan covered, Metal extends \
6184                             when its byte-storage substrate is ready.",
6185                        ))
6186                        .bt());
6187                    }
6188                }
6189            }
6190            // Adopt the (now-zero) Storage Arc at this node's slot.
6191            // Downstream readers go through `node_id`; the original
6192            // input's NodeId is evicted from the cache by the
6193            // realize loop's destructive_input cleanup.
6194            cache.insert(item.node_id, src_arc);
6195            layout_cache.insert(item.node_id, item.output_layout.clone());
6196            // A4b-1: ZeroFill's CUDA memset is same-stream (and Op::Alloc
6197            // typically zero-inits, so this is usually elided). Its completion
6198            // is carried by the realize-end full-stream sync in `to_cpu_bytes`
6199            // (unchanged) and same-stream ordering — `Ready` is behavior-preserving.
6200            Ok(CompletionHandle::Ready)
6201        }
6202        WorkItemKind::Copy { target_location } | WorkItemKind::Move { target_location } => {
6203            // Op::Copy / Op::Move { target }: kernel lookup at
6204            // (OpKind::Copy, [dt, dt], source_backend) — the wrapper
6205            // downloads from its own residency into a freshly-
6206            // allocated output on `target_location`. We auto-
6207            // contiguize the input first so the kernel always sees
6208            // the logical view's bytes (a transpose-view source
6209            // materializes into a contiguous buffer before download).
6210            //
6211            // Move shares this entire data-movement half; its
6212            // destructive half (release the source) is driven by the
6213            // realize loop's `destructive_input` cleanup after this
6214            // arm returns — see `WorkItemKind::Move`.
6215            let op_label = if matches!(item.kind, WorkItemKind::Move { .. }) {
6216                "Move"
6217            } else {
6218                "Copy"
6219            };
6220            let compiled = item.compiled.as_ref().ok_or_else(|| {
6221                Error::Msg(format!(
6222                    "PipelinedExecutor: {op_label} work item {:?} has no compiled node",
6223                    item.node_id,
6224                ))
6225                .bt()
6226            })?;
6227            if item.inputs.len() != 1 {
6228                return Err(Error::Msg(format!(
6229                    "PipelinedExecutor: {op_label} work item {:?} expects 1 input, got {}",
6230                    item.node_id,
6231                    item.inputs.len(),
6232                ))
6233                .bt());
6234            }
6235            let src_id = item.inputs[0];
6236            let src_arc = cache.get(&src_id).cloned().ok_or_else(|| {
6237                Error::Msg(format!(
6238                    "PipelinedExecutor: {op_label} input {:?} of {:?} not realized",
6239                    src_id, item.node_id,
6240                ))
6241                .bt()
6242            })?;
6243            // Multi-output Option C guard: Op::Copy on a bundled
6244            // producer would silently drop the bundle metadata on
6245            // the destination, and the auto-contiguize step below
6246            // would treat the bundle as a flat slot-0-shaped buffer
6247            // and corrupt non-primary slots. Until the per-backend
6248            // whole-bundle-Copy hook lands, point callers at the
6249            // supported per-slot path:
6250            // Op::View → Op::Copy → Op::ViewOwned.
6251            //
6252            // Exception: when the immediate input is an Op::View
6253            // projection (its layout's byte extent is strictly less
6254            // than the bundle's total bytes), the View has already
6255            // narrowed the slab. auto-contiguize walks the View's
6256            // layout and produces a flat slot-shaped buffer that's
6257            // correctly sized — no data loss, no slot corruption.
6258            // The guard only fires when the Copy targets the bundle
6259            // root directly.
6260            {
6261                let guard = src_arc
6262                    .read()
6263                    .map_err(|_| poisoned("Copy source storage"))?;
6264                if guard.is_bundled() {
6265                    let layout_bytes_for_guard = layout_cache
6266                        .get(&src_id)
6267                        .map(|l| l.shape().elem_count() * guard.dtype.size_in_bytes())
6268                        .unwrap_or(usize::MAX);
6269                    let bundle_bytes = guard.inner.len_bytes();
6270                    let is_whole_bundle = layout_bytes_for_guard >= bundle_bytes;
6271                    if is_whole_bundle {
6272                        return Err(Error::Msg(format!(
6273                            "PipelinedExecutor: Op::{op_label} on a bundled producer \
6274                             ({:?}) is not yet supported (Option C followup). \
6275                             Use Op::View → Op::Copy → Op::ViewOwned for \
6276                             per-slot cross-device moves, or wait for the \
6277                             per-backend whole-bundle-Copy hook.",
6278                            src_id,
6279                        ))
6280                        .bt());
6281                    }
6282                }
6283            }
6284            let src_layout = layout_cache.get(&src_id).cloned().ok_or_else(|| {
6285                Error::Msg(format!(
6286                    "PipelinedExecutor: {op_label} input {:?} of {:?} has no cached layout",
6287                    src_id, item.node_id,
6288                ))
6289                .bt()
6290            })?;
6291            let src_dtype = src_arc
6292                .read()
6293                .map_err(|_| poisoned("Copy source storage"))?
6294                .dtype;
6295            let src_len_bytes = src_arc
6296                .read()
6297                .map_err(|_| poisoned("Copy source storage"))?
6298                .inner
6299                .len_bytes();
6300            let layout_bytes = src_layout.shape().elem_count() * src_dtype.size_in_bytes();
6301            let bytes_match_shape = src_len_bytes == layout_bytes;
6302            let already_contig =
6303                src_layout.is_contiguous() && src_layout.start_offset() == 0 && bytes_match_shape;
6304            let src_input = if already_contig {
6305                src_arc
6306            } else {
6307                auto_contiguize(&src_arc, &src_layout)?
6308            };
6309            let kernel_input_layout = fuel_ir::Layout::contiguous(src_layout.shape().clone());
6310            // CapturedRun build-out (4b-ζ): in Reuse (capture/replay) mode,
6311            // reuse the fixed-address buffer recorded during the warm run
6312            // instead of allocating — device allocation is illegal inside a
6313            // capture scope. Only a same-device CUDA target_location ever
6314            // reaches Reuse mode for a Copy node in practice: capture_decode's
6315            // validation loop rejects every Cpu/Vulkan target_location before
6316            // a capture scope is ever entered (Move is rejected unconditionally
6317            // there too), so this arm trusts that invariant rather than
6318            // re-deriving it — the `DeviceLocation::Cuda` guard below is just
6319            // belt-and-suspenders for any other Reuse-mode caller.
6320            let reuse_arc = match &persistent {
6321                Some(p) if p.mode == PersistentMode::Reuse => match target_location {
6322                    DeviceLocation::Cuda { .. } => p.map.get(&item.node_id).cloned(),
6323                    _ => None,
6324                },
6325                _ => None,
6326            };
6327            // Allocate the output on `target_location`. Phase 2
6328            // covered D2H (target=Cpu); Phase 3b extends to H2D
6329            // (target=Cuda / Vulkan from a CPU source). For non-CPU
6330            // targets the executor's Op::Alloc-style device-handle
6331            // search applies (find_cuda_device_in_cache /
6332            // find_vulkan_backend_in_cache); callers seed the cache
6333            // (e.g. `pipelined_bridge::device_seed_storage`) before
6334            // realizing.
6335            // Used only by the Vulkan arm below (alloc_bytes_handle); a
6336            // default-feature build sees it unused. GAP-229: 68161981's
6337            // unused_variables --fix renamed it `_n_bytes` at default
6338            // features and broke `--features vulkan` (E0425). Guard the
6339            // decline instead of the name so the binding stays usable.
6340            #[cfg_attr(not(feature = "vulkan"), allow(unused_variables))]
6341            let n_bytes = item.elem_count * item.dtype.size_in_bytes();
6342            let input_arcs = vec![src_input];
6343            let mut output_arcs = if let Some(arc) = reuse_arc {
6344                vec![arc]
6345            } else {
6346                let output = match target_location {
6347                    DeviceLocation::Cpu => {
6348                        fuel_memory::alloc_cpu_zeroed(item.dtype, item.elem_count)?
6349                    }
6350                    #[cfg(feature = "cuda")]
6351                    DeviceLocation::Cuda { gpu_id } => {
6352                        let cuda_dev =
6353                            find_cuda_device_in_cache(cache, *gpu_id).ok_or_else(|| {
6354                                Error::Msg(format!(
6355                                    "Op::{op_label} on Cuda {{ gpu_id: {} }}: no CUDA \
6356                                 storage in input cache to derive the device \
6357                                 handle from. The caller must seed the cache \
6358                                 (e.g. via `fuel-core::pipelined_bridge::\
6359                                 device_seed_storage`) before realizing an \
6360                                 H2D Op::{op_label}.",
6361                                    gpu_id,
6362                                ))
6363                                .bt()
6364                            })?;
6365                        let cuda_bytes =
6366                            fuel_cuda_backend::CudaStorageBytes::alloc_uninit(&cuda_dev, n_bytes)?;
6367                        Storage::new(fuel_memory::BackendStorage::Cuda(cuda_bytes), item.dtype)
6368                    }
6369                    #[cfg(not(feature = "cuda"))]
6370                    DeviceLocation::Cuda { .. } => {
6371                        return Err(Error::Msg(format!(
6372                            "Op::{op_label} target Cuda but fuel-storage wasn't built \
6373                             with --features cuda",
6374                        ))
6375                        .bt());
6376                    }
6377                    #[cfg(feature = "vulkan")]
6378                    DeviceLocation::Vulkan { gpu_id } => {
6379                        let backend =
6380                            find_vulkan_backend_in_cache(cache, *gpu_id).ok_or_else(|| {
6381                                Error::Msg(format!(
6382                                    "Op::{op_label} on Vulkan {{ gpu_id: {} }}: no Vulkan \
6383                                 storage in input cache to derive the backend \
6384                                 handle from. The caller must seed the cache \
6385                                 (e.g. via `fuel-core::pipelined_bridge::\
6386                                 device_seed_storage`) before realizing an \
6387                                 H2D Op::{op_label}.",
6388                                    gpu_id,
6389                                ))
6390                                .bt()
6391                            })?;
6392                        let vk_bytes = backend.alloc_bytes_handle(n_bytes)?;
6393                        Storage::new(fuel_memory::BackendStorage::Vulkan(vk_bytes), item.dtype)
6394                    }
6395                    #[cfg(not(feature = "vulkan"))]
6396                    DeviceLocation::Vulkan { .. } => {
6397                        return Err(Error::Msg(format!(
6398                            "Op::{op_label} target Vulkan but fuel-storage wasn't built \
6399                             with --features vulkan",
6400                        ))
6401                        .bt());
6402                    }
6403                    other => {
6404                        return Err(Error::Msg(format!(
6405                            "PipelinedExecutor: Op::{op_label} target_location {:?} not yet \
6406                             wired (CPU + CUDA + Vulkan covered; Metal extends when \
6407                             its byte-storage substrate is ready).",
6408                            other
6409                        ))
6410                        .bt());
6411                    }
6412                };
6413                vec![Arc::new(RwLock::new(output))]
6414            };
6415            let kernel_layouts = vec![kernel_input_layout, item.output_layout.clone()];
6416            // A4b-1: defer the wait — return the handle to the realize loop.
6417            let handle = execute_compiled_with_wait_hint(
6418                compiled,
6419                &input_arcs,
6420                &mut output_arcs,
6421                &kernel_layouts,
6422                will_be_waited,
6423            )?;
6424            let arc = output_arcs.into_iter().next().expect("one output");
6425            // CapturedRun build-out (4b-ζ): Record (warm) mode remembers this
6426            // node's fixed output buffer so the capture run reuses the
6427            // identical device address. No-op in Reuse mode (the buffer came
6428            // FROM the map) and in None mode. Mirrors the Kernel arm's
6429            // Record-mode snapshot (above) exactly.
6430            if let Some(p) = persistent.as_deref_mut()
6431                && p.mode == PersistentMode::Record
6432            {
6433                p.map.insert(item.node_id, Arc::clone(&arc));
6434            }
6435            cache.insert(item.node_id, arc);
6436            layout_cache.insert(item.node_id, item.output_layout.clone());
6437            Ok(handle)
6438        }
6439        WorkItemKind::ReleaseMarker => {
6440            // Emit a zero-byte CPU storage at the Release node's
6441            // slot. Per Op::Release's contract the marker is never
6442            // read — this exists so any downstream cache lookup of
6443            // `release_id` resolves rather than failing. The actual
6444            // deallocation of `inputs[0]` is driven by the realize
6445            // loop's `destructive_input` cleanup (Phase B).
6446            let marker = fuel_memory::alloc_cpu_zeroed(item.dtype, 0)?;
6447            cache.insert(item.node_id, Arc::new(RwLock::new(marker)));
6448            layout_cache.insert(item.node_id, item.output_layout.clone());
6449            Ok(CompletionHandle::Ready)
6450        }
6451        WorkItemKind::ContiguizeOf { input } => {
6452            let input_arc = cache.get(input).cloned().ok_or_else(|| {
6453                Error::Msg(format!(
6454                    "PipelinedExecutor: reshape input {:?} of {:?} not realized",
6455                    input, item.node_id,
6456                ))
6457                .bt()
6458            })?;
6459            let input_layout = layout_cache.get(input).cloned().ok_or_else(|| {
6460                Error::Msg(format!(
6461                    "PipelinedExecutor: reshape input {:?} of {:?} has no cached layout",
6462                    input, item.node_id,
6463                ))
6464                .bt()
6465            })?;
6466            // CapturedRun build-out (4b-γ): in Reuse (capture/replay) mode,
6467            // reuse the fixed-address buffer recorded during the warm run —
6468            // device allocation is illegal inside a capture scope. Always
6469            // write-into when reusing (skip the zero-copy-vs-allocate
6470            // branch entirely): a given ContiguizeOf node's zero-copy-ness
6471            // is a static layout/stride property, identical across the warm
6472            // and capture passes (both replay the same compiled item list
6473            // with the same static layouts per capture_decode's own
6474            // design), so if this node ever needed the map entry it
6475            // allocated on warm too and the entry is guaranteed present.
6476            let reuse_arc = match &persistent {
6477                Some(p) if p.mode == PersistentMode::Reuse => p.map.get(&item.node_id).cloned(),
6478                _ => None,
6479            };
6480            let out_arc = if let Some(dest_arc) = reuse_arc {
6481                contiguize_into(&dest_arc, &input_arc, &input_layout)?;
6482                dest_arc
6483            } else if input_layout.is_contiguous() && input_layout.start_offset() == 0 {
6484                // Zero-copy when the input is already contiguous + zero
6485                // offset — never allocates, so Record mode has nothing to
6486                // record for this node (no fixed address to reuse).
6487                input_arc
6488            } else {
6489                // Allocate + copy via the contiguize kernel.
6490                let arc = auto_contiguize(&input_arc, &input_layout)?;
6491                if let Some(p) = persistent.as_deref_mut()
6492                    && p.mode == PersistentMode::Record
6493                {
6494                    p.map.insert(item.node_id, Arc::clone(&arc));
6495                }
6496                arc
6497            };
6498            cache.insert(item.node_id, out_arc);
6499            layout_cache.insert(item.node_id, item.output_layout.clone());
6500            // A4b-1: a materializing contiguize (CUDA) is same-stream; its
6501            // completion is carried by the realize-end full-stream sync in
6502            // `to_cpu_bytes` (unchanged) — `Ready` here is behavior-preserving.
6503            Ok(CompletionHandle::Ready)
6504        }
6505        WorkItemKind::Kernel => {
6506            let compiled = item.compiled.as_ref().ok_or_else(|| {
6507                Error::Msg(format!(
6508                    "PipelinedExecutor: Kernel work item {:?} has no compiled node",
6509                    item.node_id,
6510                ))
6511                .bt()
6512            })?;
6513            // Dynamic-M matmul (sparse MoE): resolve a DATA-DETERMINED row
6514            // count from the producer-bound `produced_syms` and run against
6515            // the patched (resolved-M) node. `None` on the common path.
6516            let patched_mm = resolve_deferred_matmul(compiled, produced_syms, item.node_id)?;
6517            let compiled = patched_mm.as_ref().unwrap_or(compiled);
6518            // Gather input Arcs from the cache, auto-contiguizing
6519            // any input whose layout is non-contiguous (typically
6520            // produced by an upstream metadata-only view op).
6521            // Today's kernels assume contiguous; this pass keeps
6522            // that invariant true at every kernel call site.
6523            //
6524            // We assemble a parallel `kernel_layouts` vec — the layout
6525            // of the bytes the kernel actually receives. After
6526            // auto-contiguize, that's `Layout::contiguous(shape)` for
6527            // the input's shape; for inputs already contiguous we use
6528            // the cached layout directly. Output layout comes last.
6529            let mut input_arcs: Vec<Arc<RwLock<Storage>>> = Vec::with_capacity(item.inputs.len());
6530            let mut kernel_layouts: Vec<fuel_ir::Layout> =
6531                Vec::with_capacity(item.inputs.len() + 1);
6532            // The kernel's `strided_input` cap lets non-contiguous
6533            // inputs (broadcast, transpose, etc.) flow through without
6534            // materialization — the kernel walks strides itself. Inputs
6535            // with non-zero `start_offset` still go through auto-
6536            // Contiguize today; offset honoring on the byte buffer is
6537            // a separate concern from stride support.
6538            let kernel_handles_strided = compiled.caps.strided_input;
6539            let t_gather = crate::compiled::op_profile::PhaseTimer::start("kernel.gather_inputs");
6540            for in_id in &item.inputs {
6541                let in_arc = cache.get(in_id).cloned().ok_or_else(|| {
6542                    Error::Msg(format!(
6543                        "PipelinedExecutor: input {:?} of {:?} not realized",
6544                        in_id, item.node_id,
6545                    ))
6546                    .bt()
6547                })?;
6548                let in_layout = layout_cache.get(in_id).cloned().ok_or_else(|| {
6549                    Error::Msg(format!(
6550                        "PipelinedExecutor: input {:?} of {:?} has no cached layout",
6551                        in_id, item.node_id,
6552                    ))
6553                    .bt()
6554                })?;
6555                // "Already contig" means more than `is_contiguous()`:
6556                // the LAYOUT's element count must also match the
6557                // STORAGE's byte count for shape-validating kernels
6558                // (concat_cpu, etc.) that read `storage.len_bytes()`.
6559                // A first-N slice view has `is_contiguous() == true`
6560                // (strides match canonical [1] for shape [N]) but its
6561                // storage is the parent's full bytes — too big. In
6562                // that case we auto-contiguize to materialize only
6563                // the view's portion as a fresh storage.
6564                //
6565                // Same constraint applies to strided_ok: kernels that
6566                // declare strided support can walk strided inputs, but
6567                // the byte count check at the kernel surface still
6568                // assumes storage.len_bytes() == layout.elem_count() *
6569                // dtype.size_bytes(). Until kernels universally trust
6570                // layout over storage, we conservatively materialize.
6571                let in_dtype = in_arc.read().map_err(|_| poisoned("input storage"))?.dtype;
6572                let in_len_bytes = in_arc
6573                    .read()
6574                    .map_err(|_| poisoned("input storage"))?
6575                    .inner
6576                    .len_bytes();
6577                let layout_bytes = in_layout.shape().elem_count() * in_dtype.size_in_bytes();
6578                let bytes_match_shape = in_len_bytes == layout_bytes;
6579                let already_contig =
6580                    in_layout.is_contiguous() && in_layout.start_offset() == 0 && bytes_match_shape;
6581                let strided_ok =
6582                    kernel_handles_strided && in_layout.start_offset() == 0 && bytes_match_shape;
6583                if already_contig || strided_ok {
6584                    input_arcs.push(in_arc);
6585                    kernel_layouts.push(in_layout);
6586                } else {
6587                    let contig_arc = auto_contiguize(&in_arc, &in_layout)?;
6588                    input_arcs.push(contig_arc);
6589                    kernel_layouts.push(fuel_ir::Layout::contiguous(in_layout.shape().clone()));
6590                }
6591            }
6592            drop(t_gather);
6593            kernel_layouts.push(item.output_layout.clone());
6594
6595            // Allocate output on the target backend. For GPU backends
6596            // we derive the device handle from the first input —
6597            // every kernel has ≥1 input (Op::Const is handled via
6598            // ConstAdopt, never via Kernel), and an input on the
6599            // target backend carries its own device. This avoids
6600            // threading a device handle through `realize`.
6601            //
6602            // Multi-output (Option C, item 3): when the WorkItem
6603            // carries an `output_bundle`, size the allocation to
6604            // span every slot's bytes (primary-dtype-element count
6605            // rounded up from the bundle's total byte span) and
6606            // attach the bundle metadata after allocation.
6607            let (alloc_elem_count, bundle_to_attach) = match &item.output_bundle {
6608                Some(bundle) => {
6609                    let dtype_bytes = item.dtype.size_in_bytes().max(1);
6610                    let total_bytes = bundle.iter().fold(0usize, |acc, s| {
6611                        let end = s.byte_offset.saturating_add(s.len_bytes());
6612                        acc.max(end)
6613                    });
6614                    let elems = total_bytes.div_ceil(dtype_bytes);
6615                    (elems, Some(Arc::clone(bundle)))
6616                }
6617                None => (item.elem_count, None),
6618            };
6619            // CapturedRun build-out: in Reuse (capture/replay) mode, reuse the
6620            // FIXED-ADDRESS buffer recorded during the warm run instead of
6621            // allocating — device allocation is illegal inside a capture scope,
6622            // and reusing the same address is what makes the captured graph's
6623            // baked kernel arguments valid on replay. In Record (warm) or None
6624            // mode, allocate as usual (below).
6625            let reuse_arc = match &persistent {
6626                Some(p) if p.mode == PersistentMode::Reuse => {
6627                    Some(p.map.get(&item.node_id).cloned().ok_or_else(|| {
6628                        Error::Msg(format!(
6629                            "PipelinedExecutor: capture (reuse) mode has no persistent \
6630                             output buffer for kernel node {:?}; the warm (record) run \
6631                             must realize every kernel node before capture",
6632                            item.node_id,
6633                        ))
6634                        .bt()
6635                    })?)
6636                }
6637                _ => None,
6638            };
6639            let t_alloc = crate::compiled::op_profile::PhaseTimer::start("kernel.alloc_output");
6640            let mut output_arcs = if let Some(arc) = reuse_arc {
6641                vec![arc]
6642            } else {
6643                let output = match item.target_backend {
6644                    BackendId::Cpu => fuel_memory::alloc_cpu_zeroed(item.dtype, alloc_elem_count)?,
6645                    #[cfg(feature = "cuda")]
6646                    BackendId::Cuda => {
6647                        let first_in = input_arcs.first().ok_or_else(|| {
6648                            Error::Msg(format!(
6649                                "PipelinedExecutor: kernel {:?} on Cuda has no inputs; \
6650                             cannot derive device for output allocation",
6651                                item.node_id,
6652                            ))
6653                            .bt()
6654                        })?;
6655                        let guard = first_in.read().map_err(|_| poisoned("input storage"))?;
6656                        let cuda_in = match &guard.inner {
6657                            fuel_memory::BackendStorage::Cuda(c) => c,
6658                            other => {
6659                                return Err(Error::Msg(format!(
6660                                    "PipelinedExecutor: kernel {:?} target_backend=Cuda but \
6661                                 input has BackendStorage::{:?}; mixed-backend kernels \
6662                                 require an explicit Op::Copy first",
6663                                    item.node_id,
6664                                    std::mem::discriminant(other),
6665                                ))
6666                                .bt());
6667                            }
6668                        };
6669                        let n_bytes = alloc_elem_count * item.dtype.size_in_bytes();
6670                        let cuda_bytes =
6671                            fuel_cuda_backend::CudaStorageBytes::alloc(cuda_in.device(), n_bytes)?;
6672                        fuel_memory::Storage::new(
6673                            fuel_memory::BackendStorage::Cuda(cuda_bytes),
6674                            item.dtype,
6675                        )
6676                    }
6677                    #[cfg(feature = "vulkan")]
6678                    BackendId::Vulkan => {
6679                        // Mirror the CUDA path: derive the backend handle
6680                        // from the first input's `VulkanStorageBytes::backend()`.
6681                        // Vulkan catch-up V.1.B (2026-05-21) — requires
6682                        // inputs constructed via `alloc_bytes_handle` /
6683                        // `upload_bytes_handle` so they carry the
6684                        // `Arc<VulkanBackend>` (mirroring CUDA's
6685                        // `Arc<CudaDevice>`).
6686                        let first_in = input_arcs.first().ok_or_else(|| {
6687                            Error::Msg(format!(
6688                                "PipelinedExecutor: kernel {:?} on Vulkan has no inputs; \
6689                             cannot derive backend handle for output allocation",
6690                                item.node_id,
6691                            ))
6692                            .bt()
6693                        })?;
6694                        let guard = first_in.read().map_err(|_| poisoned("input storage"))?;
6695                        let vk_in = match &guard.inner {
6696                            fuel_memory::BackendStorage::Vulkan(v) => v,
6697                            other => {
6698                                return Err(Error::Msg(format!(
6699                                    "PipelinedExecutor: kernel {:?} target_backend=Vulkan but \
6700                                 input has BackendStorage::{:?}; mixed-backend kernels \
6701                                 require an explicit Op::Copy first",
6702                                    item.node_id,
6703                                    std::mem::discriminant(other),
6704                                ))
6705                                .bt());
6706                            }
6707                        };
6708                        let backend = vk_in.backend().ok_or_else(|| {
6709                            Error::Msg(format!(
6710                                "PipelinedExecutor: Vulkan kernel {:?} input has no backend \
6711                             handle. Storages flowing through the pipelined executor's \
6712                             Vulkan path must be constructed via \
6713                             VulkanBackend::alloc_bytes_handle / upload_bytes_handle \
6714                             (not the legacy alloc_bytes / upload_bytes which leave \
6715                             VulkanStorageBytes::backend = None).",
6716                                item.node_id,
6717                            ))
6718                            .bt()
6719                        })?;
6720                        let n_bytes = alloc_elem_count * item.dtype.size_in_bytes();
6721                        let vk_bytes = backend.alloc_bytes_handle(n_bytes)?;
6722                        fuel_memory::Storage::new(
6723                            fuel_memory::BackendStorage::Vulkan(vk_bytes),
6724                            item.dtype,
6725                        )
6726                    }
6727                    other => {
6728                        return Err(Error::Msg(format!(
6729                            "PipelinedExecutor: target_backend {:?} output allocation \
6730                         not yet implemented (CPU + CUDA + Vulkan wired; Metal extends later)",
6731                            other
6732                        ))
6733                        .bt());
6734                    }
6735                };
6736                // Attach bundle metadata when this is a multi-output op.
6737                // The kernel sees the allocated bytes; the bundle lives
6738                // at the Storage wrapper level so downstream Op::View /
6739                // Op::ViewOwned can resolve their slot windows.
6740                let output = match bundle_to_attach {
6741                    Some(bundle) => output.with_bundle(bundle)?,
6742                    None => output,
6743                };
6744                vec![Arc::new(RwLock::new(output))]
6745            };
6746            drop(t_alloc);
6747
6748            // A4b-1: defer the wait — return the handle to the realize loop.
6749            let t_launch =
6750                crate::compiled::op_profile::PhaseTimer::start("kernel.execute_compiled");
6751            let handle = execute_compiled_with_wait_hint(
6752                compiled,
6753                &input_arcs,
6754                &mut output_arcs,
6755                &kernel_layouts,
6756                will_be_waited,
6757            )?;
6758            drop(t_launch);
6759
6760            let arc = output_arcs.into_iter().next().expect("one output");
6761
6762            // CapturedRun build-out: Record (warm) mode remembers this node's
6763            // fixed output buffer so the capture run reuses the identical
6764            // device address. No-op in Reuse mode (the buffer came FROM the
6765            // map) and in None mode.
6766            if let Some(p) = persistent
6767                && p.mode == PersistentMode::Record
6768            {
6769                p.map.insert(item.node_id, Arc::clone(&arc));
6770            }
6771
6772            // Data-determined dynamic-shapes seam (increment 2a): a
6773            // producer that computes a runtime count publishes it into the
6774            // per-pass `SymEnv` so downstream ops can consume it as a
6775            // dynamic extent. `Op::NonZeroIndices` writes its count into
6776            // bundle slot 1; read it back and bind `count_sym`. This is the
6777            // FIRST op to bind a *data-determined* symbol mid-realize (all
6778            // prior binds are host-known before realize). Single-threaded:
6779            // the executor runs items topologically, so this bind precedes
6780            // any consumer's execute-time resolution.
6781            if let OpParams::NonZeroIndices {
6782                capacity,
6783                count_sym,
6784            } = &compiled.op_params
6785            {
6786                bind_data_determined_count(&arc, *capacity, *count_sym, produced_syms)?;
6787            }
6788
6789            cache.insert(item.node_id, arc);
6790            layout_cache.insert(item.node_id, item.output_layout.clone());
6791            Ok(handle)
6792        }
6793        WorkItemKind::InplaceKernel { target_idx } => {
6794            let compiled = item.compiled.as_ref().ok_or_else(|| {
6795                Error::Msg(format!(
6796                    "PipelinedExecutor: InplaceKernel work item {:?} has no compiled node",
6797                    item.node_id,
6798                ))
6799                .bt()
6800            })?;
6801            let target_id = *item.inputs.get(*target_idx).ok_or_else(|| {
6802                Error::Msg(format!(
6803                    "PipelinedExecutor: InplaceKernel work item {:?} target_idx={} \
6804                     out of bounds (inputs.len()={})",
6805                    item.node_id,
6806                    target_idx,
6807                    item.inputs.len(),
6808                ))
6809                .bt()
6810            })?;
6811            let target_arc = cache.get(&target_id).cloned().ok_or_else(|| {
6812                Error::Msg(format!(
6813                    "PipelinedExecutor: InplaceKernel target {:?} of {:?} not realized",
6814                    target_id, item.node_id,
6815                ))
6816                .bt()
6817            })?;
6818            let target_layout = layout_cache.get(&target_id).cloned().ok_or_else(|| {
6819                Error::Msg(format!(
6820                    "PipelinedExecutor: InplaceKernel target {:?} of {:?} has no cached layout",
6821                    target_id, item.node_id,
6822                ))
6823                .bt()
6824            })?;
6825            // v1 contract: target must be contiguous + zero offset.
6826            // Strided in-place targets would force the kernel to walk
6827            // strides for writes too; defer until a concrete consumer
6828            // needs it (mirrors `Op::WriteSlice`'s v1 dest contract).
6829            if !target_layout.is_contiguous() || target_layout.start_offset() != 0 {
6830                return Err(Error::Msg(format!(
6831                    "InplaceKernel (Phase 3 v1): target {:?} must be contiguous + \
6832                     zero-offset; got Layout {:?}",
6833                    target_id, target_layout,
6834                ))
6835                .bt());
6836            }
6837            // Kernel sees inputs=[non-target IR inputs in order] and
6838            // outputs=[target_arc]. For unary in-place (1 IR input at
6839            // target_idx=0) the input vec is empty, matching the
6840            // original Phase 3e contract. For binary in-place
6841            // (target_idx=0, 2 IR inputs) the wrapper sees the
6842            // non-destructive RHS as `inputs[0]`. The wrapper acquires
6843            // outputs[0]'s write lock and mutates through its
6844            // bytes_mut(). Layout vector contains the kernel's
6845            // inputs+outputs layouts in that order.
6846            let mut input_arcs: Vec<Arc<RwLock<Storage>>> = Vec::new();
6847            let mut kernel_layouts: Vec<Layout> = Vec::new();
6848            for (i, &input_id) in item.inputs.iter().enumerate() {
6849                if i == *target_idx {
6850                    continue;
6851                }
6852                let arc = cache.get(&input_id).cloned().ok_or_else(|| {
6853                    Error::Msg(format!(
6854                        "PipelinedExecutor: InplaceKernel non-target input {:?} of {:?} \
6855                         not realized",
6856                        input_id, item.node_id,
6857                    ))
6858                    .bt()
6859                })?;
6860                let lay = layout_cache.get(&input_id).cloned().ok_or_else(|| {
6861                    Error::Msg(format!(
6862                        "PipelinedExecutor: InplaceKernel non-target input {:?} of {:?} \
6863                         has no cached layout",
6864                        input_id, item.node_id,
6865                    ))
6866                    .bt()
6867                })?;
6868                input_arcs.push(arc);
6869                kernel_layouts.push(lay);
6870            }
6871            let mut output_arcs = vec![target_arc.clone()];
6872            kernel_layouts.push(target_layout.clone());
6873            // A4b-1: defer the wait — return the handle to the realize loop.
6874            let handle = execute_compiled_with_wait_hint(
6875                compiled,
6876                &input_arcs,
6877                &mut output_arcs,
6878                &kernel_layouts,
6879                will_be_waited,
6880            )?;
6881            // Adopt the target Arc at this node's slot. The realize
6882            // loop's destructive_input cleanup evicts the target's own
6883            // NodeId from the cache afterward.
6884            cache.insert(item.node_id, target_arc);
6885            layout_cache.insert(item.node_id, item.output_layout.clone());
6886            Ok(handle)
6887        }
6888    }
6889}
6890
6891fn poisoned(what: &'static str) -> Error {
6892    Error::Msg(format!("PipelinedExecutor: {} poisoned", what)).bt()
6893}
6894
6895/// Step E A4b-1: record a node's [`CompletionHandle`] in the executor's
6896/// per-node handle map.
6897///
6898/// `Ready` handles (CPU / Vulkan / view-and-alloc arms) carry no async work, so
6899/// we DON'T store them — keeping the map to genuinely-pending entries makes the
6900/// realize-end empty-after-drain assert (open question #6) meaningful and keeps
6901/// the drain O(pending) rather than O(nodes). A re-used NodeId (in-place adopt
6902/// re-keys an existing slot) waits the prior handle first so nothing leaks.
6903#[inline]
6904fn store_handle(
6905    handles: &mut HashMap<NodeId, CompletionHandle>,
6906    node_id: NodeId,
6907    handle: CompletionHandle,
6908) {
6909    if matches!(handle, CompletionHandle::Ready) {
6910        // A prior pending handle at this slot must still be waited (don't drop a
6911        // Pending silently). In practice NodeIds are unique per realize, so this
6912        // is just defensive.
6913        if let Some(prev) = handles.remove(&node_id) {
6914            let _ = prev.wait();
6915        }
6916        return;
6917    }
6918    if let Some(prev) = handles.insert(node_id, handle) {
6919        let _ = prev.wait();
6920    }
6921}
6922
6923/// Step E A4b-3: the FINER cross-device producer wait.
6924///
6925/// Before dispatching a cross-device `Op::Copy` / `Op::Move` WorkItem, the
6926/// executor calls this on the copy's single source input. It removes the
6927/// source producer's [`CompletionHandle`] from the map and waits it — so the
6928/// specific producer P is complete before the D2H reads P's device buffer,
6929/// WITHOUT draining the whole source device (the conservative pre-A4b-3
6930/// behavior, where `to_cpu_bytes`'s `device.synchronize()` blocked on ALL
6931/// in-flight work on that device — including the OTHER sub-DAG's independent
6932/// work A4b-4 wants to overlap).
6933///
6934/// - **CUDA producer** → waits P's recorded `Event` (`cuEventSynchronize`):
6935///   fires after P + its already-ordered stream predecessors, nothing else.
6936/// - **Absent / `Ready` producer** (CPU producer, or a Vulkan producer — which
6937///   under A4b-2 is `Ready` mid-walk because nothing is submitted until
6938///   realize-end) → no-op. A Vulkan SOURCE's own ordering is handled inside
6939///   `download_bytes` (it `force_flush`es the open batch then reads), so there
6940///   is no mid-walk Vulkan handle to wait here; the cross-device Vulkan→CPU hop
6941///   stays correct without one.
6942///
6943/// Removing the handle here (rather than waiting it again at realize-end) keeps
6944/// `drain_handles`' post-drain empty-map assert meaningful and avoids a
6945/// double-wait. Waiting it twice would be harmless (`cuEventSynchronize` on an
6946/// already-signalled event is a fast no-op), but the consume-once contract is
6947/// cleaner. The producing node's *storage* stays in the cache for downstream
6948/// readers; only its completion handle is consumed.
6949fn wait_producer_handle(
6950    handles: &mut HashMap<NodeId, CompletionHandle>,
6951    producer: NodeId,
6952) -> Result<()> {
6953    if let Some(handle) = handles.remove(&producer) {
6954        handle.wait()?;
6955    }
6956    Ok(())
6957}
6958
6959/// Step E A4b-1: realize-end drain — wait every outstanding async handle, then
6960/// clear the map.
6961///
6962/// **Event-recording / wait strategy (A4b-1 §1.2).** CUDA has one stream per
6963/// device, and an `Event` recorded after a node's launch signals when that
6964/// node *and all prior stream work* have completed. So once the
6965/// latest-recorded event on a device has signalled, every earlier event on that
6966/// device is already signalled too: waiting them is a non-blocking
6967/// `cuEventQuery` fast-path, not N host stalls. We therefore wait every stored
6968/// handle (correct + simple).
6969///
6970/// **Open question #4 — "event only where waited" — RESOLVED.** Recording an
6971/// `Event` for every node was measured negligible on a 32-op synthetic, but
6972/// real decode graphs run 150-200+ ops; `pipelined`'s `WaitSet` pre-scan (see
6973/// `compiler_work_and_wait_set_for` / `build_wait_set`) now restricts event
6974/// production to the nodes some later cross-device `Copy`/`Move` will
6975/// individually wait — same-device-only ("elided") nodes get
6976/// `CompletionHandle::Ready` with no `cuEventCreate`/`cuEventRecord` at all
6977/// (`execute_compiled_with_wait_hint`). This drain therefore now retires only
6978/// the (typically few) wait-set handles that were never consumed mid-walk by
6979/// `wait_producer_handle`; it is no longer sufficient on its own to guarantee
6980/// every enqueued CUDA kernel has completed — `sync_active_cuda_devices` below
6981/// covers the elided remainder. We keep a per-node handle (rather than a
6982/// single per-device event) for the wait-set members because A4b-3's finer
6983/// cross-device wait needs to wait a *specific producer's* completion — the
6984/// per-node handle is that seam.
6985fn drain_handles(handles: &mut HashMap<NodeId, CompletionHandle>) -> Result<()> {
6986    for (_node, handle) in handles.drain() {
6987        handle.wait()?;
6988    }
6989    // Open question #6: once handles are stored instead of `wait`ed inline, the
6990    // `#[must_use]` on `CompletionHandle` can't catch a leaked (never-waited)
6991    // handle at the call site. The drain consumes the whole map, so it is empty
6992    // here by construction; assert it to catch a future leak (e.g. a new
6993    // dispatch path that forgets to thread its handle through `store_handle`).
6994    debug_assert!(
6995        handles.is_empty(),
6996        "A4b-1: handle map must be empty after drain"
6997    );
6998    Ok(())
6999}
7000
7001/// Step E A4b open question #4 step 3 — the realize-end complement to the
7002/// "event only where waited" elision. Because most CUDA nodes in a long
7003/// single-device chain no longer record an `Event` (see `drain_handles`'s
7004/// doc), this does ONE blanket `cuStreamSynchronize` per CUDA device that
7005/// still holds LIVE storage in `cache` — the realize target(s) plus any
7006/// surviving intermediates — instead of relying on per-node handles to cover
7007/// them.
7008///
7009/// **Why this is safe + sufficient.** CUDA has one stream per device, and
7010/// this executor dispatches strictly sequentially (one work item at a time,
7011/// in topological order) — so by the time a live node's own kernel was
7012/// launched, every same-device ancestor it depends on was ALREADY enqueued
7013/// earlier on the SAME stream. Stream ordering (not a host wait) guarantees
7014/// those ancestors run to completion no later than the live node's own
7015/// kernel. Syncing the live node's device therefore flushes its entire
7016/// same-device dependency chain, elided or not. Cross-device ancestors are
7017/// unaffected by elision — they always went through an `Op::Copy`/`Move`,
7018/// whose producer is in the `WaitSet` and was already waited explicitly at
7019/// the cross-device boundary (`wait_producer_handle`), mid-walk.
7020///
7021/// **Why evicted (non-live) intermediates don't need this.** A node whose
7022/// storage was evicted before realize-end (the `destructive_input` cleanup)
7023/// is never read by anything this realize returns; A3's stream-ordered
7024/// alloc/free already makes its eventual completion safe without a host
7025/// wait (the driver serializes the free after the freeing stream position,
7026/// same as any other stream-ordered op) — nothing to synchronize for.
7027///
7028/// No-op when `cache` holds no CUDA storage (CPU/Vulkan-only realize).
7029#[cfg(feature = "cuda")]
7030fn sync_active_cuda_devices(cache: &StorageCache) -> Result<()> {
7031    let mut devices: HashMap<fuel_ir::DeviceLocation, fuel_cuda_backend::CudaDevice> =
7032        HashMap::new();
7033    for arc in cache.values() {
7034        let guard = arc
7035            .read()
7036            .map_err(|_| poisoned("cache storage (cuda realize-end sync scan)"))?;
7037        if let fuel_memory::BackendStorage::Cuda(c) = &guard.inner {
7038            devices
7039                .entry(c.device().location())
7040                .or_insert_with(|| c.device().clone());
7041        }
7042    }
7043    for dev in devices.values() {
7044        dev.synchronize()?;
7045    }
7046    Ok(())
7047}
7048
7049/// Non-CUDA builds never produce CUDA storage, so there is nothing to sync —
7050/// keeps the call site unconditional across both feature configurations.
7051#[cfg(not(feature = "cuda"))]
7052fn sync_active_cuda_devices(_cache: &StorageCache) -> Result<()> {
7053    Ok(())
7054}
7055
7056/// Search the input cache for any `BackendStorage::Cuda(s)` whose
7057/// device matches `gpu_id`. Returns a clone of the `CudaDevice` Arc
7058/// the storage carries — the caller uses it to call
7059/// `CudaStorageBytes::alloc` for `Op::Alloc` on Cuda. Returns `None`
7060/// if no matching storage exists.
7061///
7062/// Phase 3a of bridge-retirement: callers seed the cache with a 0-byte
7063/// CUDA storage (`fuel-tensor::pipelined_bridge::device_seed_storage`)
7064/// so this lookup succeeds for the first Op::Alloc; subsequent
7065/// Op::Allocs see prior Op::Alloc outputs in the cache.
7066#[cfg(feature = "cuda")]
7067fn find_cuda_device_in_cache(
7068    cache: &StorageCache,
7069    gpu_id: usize,
7070) -> Option<fuel_cuda_backend::CudaDevice> {
7071    for arc in cache.values() {
7072        let guard = arc.read().ok()?;
7073        if let fuel_memory::BackendStorage::Cuda(c) = &guard.inner {
7074            // CudaDevice carries its DeviceLocation (gpu_id ordinal)
7075            // through `location()`; match against the target gpu_id.
7076            // Multi-GPU future work can refine the match; today's
7077            // single-GPU setups always match.
7078            if matches!(c.device().location(), DeviceLocation::Cuda { gpu_id: g } if g == gpu_id) {
7079                return Some(c.device().clone());
7080            }
7081        }
7082    }
7083    None
7084}
7085
7086/// Step E A4b-2: a Vulkan [`Completion`](crate::compiled::Completion) over an
7087/// in-flight (submitted, not-yet-waited) batch. `wait` blocks on the batch's
7088/// fence (`vkWaitForFences`) — which fires when the whole submitted command
7089/// buffer has retired on the single compute queue — then releases the batch
7090/// (frees the CB / descriptor sets / transient buffers / retired pool, all now
7091/// idle on the GPU).
7092///
7093/// The fence is per-BATCH, not per-node: one `submit_pending` flushes every op
7094/// recorded since the last submit, so this single handle covers a contiguous run
7095/// of Vulkan nodes (the A2 batching win, preserved — fewer fences, larger
7096/// submissions). `backend` keeps the [`VulkanBackend`] alive so the
7097/// post-fence pool retire + the `DeviceInner` the batch's resources reference
7098/// outlive the wait.
7099///
7100/// `Send` (required by `Completion`): every owned field is `Send` — `Fence` /
7101/// `CommandBuffer` / `DescriptorSet` / `Buffer` / `Allocation` / `CommandPool`
7102/// are all `Send` in vulkane (Arc-of-`DeviceInner` + explicit `unsafe impl`s),
7103/// and `Arc<VulkanBackend>` is `Send + Sync`.
7104/// Step E B1: `batch` is an `Option` ONLY so [`Drop`] can coexist with the
7105/// by-value `into_wait` consume. A type with a `Drop` impl cannot be
7106/// destructured by move (`let VulkanCompletion { backend, batch } = self`,
7107/// E0509), so `into_wait` `take()`s the batch out of the `Option` instead of
7108/// moving it out of the struct, then lets the normal `Drop` run. In every
7109/// reachable state the batch is `Some` until exactly one `into_wait` consumes it.
7110#[cfg(feature = "vulkan")]
7111struct VulkanCompletion {
7112    backend: Arc<fuel_vulkan_backend::VulkanBackend>,
7113    batch: Option<fuel_vulkan_backend::SubmittedBatch>,
7114    /// The device this submitted batch counts against in the in-flight counter
7115    /// (Step E B1). `Drop` decrements this slot.
7116    loc: fuel_ir::DeviceLocation,
7117}
7118
7119/// Step E A4b-4: the element type of the executor's in-flight-Vulkan-batch list.
7120/// A [`VulkanCompletion`] on a Vulkan build; a zero-sized placeholder otherwise
7121/// so the realize loops carry one `Vec<InflightVulkan>` local without `cfg` on
7122/// every use site (the helpers `eager_submit_all_vulkan` / `drain_inflight_vulkan`
7123/// are the only code that ever pushes/drains it, and they are `cfg`-gated).
7124#[cfg(feature = "vulkan")]
7125type InflightVulkan = VulkanCompletion;
7126#[cfg(not(feature = "vulkan"))]
7127type InflightVulkan = ();
7128
7129/// Step E A4b-4: is the cross-device copy's SOURCE buffer Vulkan-resident?
7130///
7131/// Used at the cross-device `Op::Copy`/`Op::Move` boundary to decide whether the
7132/// executor must wait the in-flight Vulkan batches before the copy. A Vulkan→CPU
7133/// D2H reads the source on the host, and the Vulkan `download` path only
7134/// `force_flush`es the OPEN batch (never the executor's eagerly-submitted
7135/// in-flight ones), so for a VULKAN source we must `drain_inflight_vulkan` first
7136/// to read completed data. For a CUDA / CPU source we must NOT wait Vulkan — the
7137/// copy doesn't read any Vulkan buffer, and waiting Vulkan there would needlessly
7138/// serialize the independent Vulkan sub-DAG (killing the overlap A4b-4 exists to
7139/// win). Returns `false` when the producer's storage isn't in the cache (treated
7140/// as not-Vulkan → no Vulkan wait; the CUDA/CPU producer handle still covers it).
7141#[cfg(feature = "vulkan")]
7142fn copy_source_is_vulkan(cache: &StorageCache, producer: NodeId) -> Result<bool> {
7143    if let Some(arc) = cache.get(&producer) {
7144        let guard = arc
7145            .read()
7146            .map_err(|_| poisoned("storage lock probing copy-source backend"))?;
7147        return Ok(matches!(
7148            guard.inner,
7149            fuel_memory::BackendStorage::Vulkan(_)
7150        ));
7151    }
7152    Ok(false)
7153}
7154
7155#[cfg(not(feature = "vulkan"))]
7156#[inline]
7157fn copy_source_is_vulkan(_cache: &StorageCache, _producer: NodeId) -> Result<bool> {
7158    Ok(false)
7159}
7160
7161#[cfg(feature = "vulkan")]
7162impl crate::compiled::Completion for VulkanCompletion {
7163    fn wait(self: Box<Self>) -> Result<()> {
7164        // Move the whole `VulkanCompletion` out of the `Box` (allowed even with a
7165        // `Drop` impl — it's a full move, not a destructure) and consume by value.
7166        (*self).into_wait()
7167    }
7168}
7169
7170#[cfg(feature = "vulkan")]
7171impl VulkanCompletion {
7172    /// Build the handle for an eagerly-submitted Vulkan batch and register it as
7173    /// one in-flight batch on `loc` (Step E B1). The matching decrement is in
7174    /// [`Drop`]. `loc` is the backend's `DeviceLocation::Vulkan { gpu_id }`.
7175    fn new(
7176        backend: Arc<fuel_vulkan_backend::VulkanBackend>,
7177        batch: fuel_vulkan_backend::SubmittedBatch,
7178        loc: fuel_ir::DeviceLocation,
7179    ) -> Self {
7180        crate::dispatch::inflight_inc(loc);
7181        Self {
7182            backend,
7183            batch: Some(batch),
7184            loc,
7185        }
7186    }
7187
7188    /// Block on this batch's fence, retire pools, then free the batch. Consumes
7189    /// `self` by value (the by-value sibling of the boxed `Completion::wait`),
7190    /// used by the executor's `inflight_vulkan` drain (A4b-4) where we hold the
7191    /// `VulkanCompletion` unboxed in a `Vec`.
7192    ///
7193    /// Step E B1: `take()`s the batch out of the `Option` (NOT a destructure
7194    /// move — that's illegal with a `Drop` impl) and waits it; the in-flight
7195    /// decrement then rides `self`'s `Drop` as this returns. So a wait-consume
7196    /// and a bare drop (e.g. an error unwinding before the wait) BOTH decrement
7197    /// exactly once, and `into_wait` never double-counts.
7198    fn into_wait(mut self) -> Result<()> {
7199        // `wait_submitted` consumes the batch by value: waits the fence, retires
7200        // pools, then drops the batch (UAF-safe — nothing the in-flight CB
7201        // references frees before the fence signals). After a `new` the batch is
7202        // always `Some`; `take` leaves `None` so `Drop` doesn't touch it again.
7203        if let Some(batch) = self.batch.take() {
7204            self.backend.wait_submitted(batch)?;
7205        }
7206        Ok(())
7207        // `self` drops here → `inflight_dec(self.loc)`.
7208    }
7209
7210    /// Step E A2.1 (deferred-deletion): retain an evicted DATA buffer until THIS
7211    /// in-flight batch's fence signals, then free it post-fence (on the batch's
7212    /// `Drop` — guaranteed post-fence by follow-on #2). Delegates to
7213    /// [`fuel_vulkan_backend::SubmittedBatch::retain_buffer`].
7214    ///
7215    /// The executor calls this for every in-flight batch when it evicts a Vulkan
7216    /// buffer those batches' CBs may read, INSTEAD of host-blocking on a drain.
7217    /// `batch` is `Some` in every reachable mid-walk state (it is `take`n only by
7218    /// `into_wait`/`wait`, which consume the whole `VulkanCompletion`), so the
7219    /// retain always lands on a live in-flight batch.
7220    fn retain_data(&mut self, buf: std::sync::Arc<fuel_vulkan_backend::VulkanBuffer>) {
7221        if let Some(batch) = self.batch.as_mut() {
7222            batch.retain_buffer(buf);
7223        }
7224    }
7225}
7226
7227#[cfg(feature = "vulkan")]
7228impl Drop for VulkanCompletion {
7229    fn drop(&mut self) {
7230        // The single in-flight decrement for a Vulkan batch — fires whether the
7231        // batch was waited (`into_wait` / `wait`) or merely dropped (error
7232        // unwind), so the per-device count returns to baseline. Relaxed +
7233        // saturating (a scheduling hint, never a correctness gate).
7234        crate::dispatch::inflight_dec(self.loc);
7235    }
7236}
7237
7238/// Step E A4b-4 — eager Vulkan submission during the walk (the OVERLAP enabler).
7239///
7240/// Submits each DISTINCT Vulkan backend's currently-open batch WITHOUT waiting,
7241/// pushing the resulting in-flight [`VulkanCompletion`]s onto `inflight` so the
7242/// executor waits them later (at the in-flight-lifetime guard / realize-end). An
7243/// empty open batch yields `None` and is skipped (idempotent).
7244///
7245/// This is what makes a Vulkan sub-DAG actually START on the iGPU while the
7246/// executor records/dispatches the next (CUDA) chunk — without it the Vulkan
7247/// batch sits merely *recorded* until realize-end and never overlaps the CUDA
7248/// stream (design §5). Called ONLY on a genuinely multi-backend realize (gated by
7249/// the executor's `multi_backend` flag); on a single-device graph it is never
7250/// reached, so pure-Vulkan stays byte-identical to A4b-2 (one realize-end submit).
7251///
7252/// **Whole-chunk granularity (UAF/race-safety, design §4 + the Vulkan
7253/// single-queue OOO-completion semantics).** We submit only at chunk boundaries
7254/// and before a cross-device wait — never mid-chunk — so the batch handed to the
7255/// GPU is a COMPLETE contiguous run of Vulkan nodes with all its intra-CB
7256/// dependency barriers (`recorder.rs` `vkCmdPipelineBarrier`) already in place.
7257/// Vulkan only guarantees that batches on one queue BEGIN in submission order;
7258/// they may overlap / complete out of order, so a split mid-chunk would lose the
7259/// write-before-read ordering for ops that read an earlier op's buffer in the
7260/// SAME chunk. By submitting whole chunks and re-syncing (`drain_inflight_vulkan`)
7261/// before any later Vulkan op records, before any cross-device copy, and before
7262/// any Vulkan-referenced eviction, no in-flight batch is ever read or freed while
7263/// still running.
7264#[cfg(feature = "vulkan")]
7265fn eager_submit_all_vulkan(
7266    cache: &StorageCache,
7267    inflight: &mut Vec<VulkanCompletion>,
7268) -> Result<()> {
7269    // Collect distinct Vulkan backends referenced by the cache (dedup by gpu_id).
7270    let mut seen: Vec<usize> = Vec::new();
7271    let mut backends: Vec<Arc<fuel_vulkan_backend::VulkanBackend>> = Vec::new();
7272    for arc in cache.values() {
7273        let guard = arc
7274            .read()
7275            .map_err(|_| poisoned("storage lock during eager vulkan submit"))?;
7276        if let fuel_memory::BackendStorage::Vulkan(v) = &guard.inner
7277            && let Some(backend) = v.backend()
7278            && !seen.contains(&backend.gpu_id)
7279        {
7280            seen.push(backend.gpu_id);
7281            backends.push(backend.clone());
7282        }
7283    }
7284    for backend in backends {
7285        // Step E B1: the per-device location for the in-flight counter. A batch
7286        // submitted here is one in-flight unit on this iGPU until its handle's
7287        // `Drop` (after `drain_inflight_vulkan` / realize-end).
7288        let loc = DeviceLocation::Vulkan {
7289            gpu_id: backend.gpu_id,
7290        };
7291        if let Some(batch) = backend.submit_pending()? {
7292            inflight.push(VulkanCompletion::new(backend, batch, loc));
7293        }
7294    }
7295    Ok(())
7296}
7297
7298#[cfg(not(feature = "vulkan"))]
7299#[inline]
7300fn eager_submit_all_vulkan(
7301    _cache: &StorageCache,
7302    _inflight: &mut Vec<InflightVulkan>,
7303) -> Result<()> {
7304    Ok(())
7305}
7306
7307/// Step E A4b-4 — wait every in-flight (eagerly-submitted) Vulkan batch, then
7308/// clear the list. The re-sync half of [`eager_submit_all_vulkan`].
7309///
7310/// Each `into_wait` blocks on the batch fence (`vkWaitForFences`), retires the
7311/// backend's descriptor pools, and frees the batch (CB / descriptor sets /
7312/// transient buffers / retired command pool — all now idle on the GPU). After
7313/// this returns, NO submitted Vulkan command buffer is still executing, so it is
7314/// safe to (a) record a new Vulkan op that may read a just-produced buffer,
7315/// (b) D2H-copy a Vulkan buffer on the host, or (c) free a buffer a batch read.
7316///
7317/// Because the eager submit happened at the PRIOR chunk boundary and an
7318/// intervening (e.g. CUDA) chunk has since been recorded/dispatched, the fence is
7319/// typically already signalled here — the wait is a fast no-op, and the iGPU work
7320/// genuinely overlapped the other device's stream. Waiting the Vulkan fence does
7321/// NOT touch the CUDA stream, so the other device keeps running (concurrency
7322/// preserved — design §5 "the per-device guards don't cross-stall").
7323#[cfg(feature = "vulkan")]
7324fn drain_inflight_vulkan(inflight: &mut Vec<VulkanCompletion>) -> Result<()> {
7325    for vc in inflight.drain(..) {
7326        vc.into_wait()?;
7327    }
7328    Ok(())
7329}
7330
7331#[cfg(not(feature = "vulkan"))]
7332#[inline]
7333fn drain_inflight_vulkan(_inflight: &mut Vec<InflightVulkan>) -> Result<()> {
7334    Ok(())
7335}
7336
7337/// Step E A4b-2: the realize-end Vulkan drain, SPLIT into submit-then-wait.
7338///
7339/// Replaces A2's `force_flush_all_vulkan` (which submitted + waited atomically
7340/// inside `force_flush`). For each DISTINCT Vulkan backend referenced by the
7341/// cache, `submit_pending` submits the still-open batch WITHOUT waiting (→ a
7342/// `SubmittedBatch`), wraps it in the executor's [`VulkanCompletion`] handle, and
7343/// `wait`s it. Net for a pure-Vulkan realize: the batch still accumulates across
7344/// the whole walk (the per-op path keeps returning `Ready`; no eager submit —
7345/// that is A4b-4) and is submitted ONCE here, then waited — **byte-identical to
7346/// A2**, same single submission at realize-end, just split through the handle.
7347///
7348/// Idempotent across repeated cache arcs: the first `submit_pending` on a backend
7349/// returns `Some(batch)`; subsequent calls on the same backend see an empty batch
7350/// and return `None` (skipped) — exactly the `force_flush` idempotence A2 relied
7351/// on. No-op for a non-Vulkan build.
7352#[cfg(feature = "vulkan")]
7353fn drain_vulkan_pending(cache: &StorageCache) -> Result<()> {
7354    use crate::compiled::CompletionHandle;
7355    for arc in cache.values() {
7356        let backend = {
7357            let guard = arc
7358                .read()
7359                .map_err(|_| poisoned("storage lock during vulkan submit_pending"))?;
7360            match &guard.inner {
7361                fuel_memory::BackendStorage::Vulkan(v) => v.backend().cloned(),
7362                _ => None,
7363            }
7364        };
7365        let Some(backend) = backend else { continue };
7366        // Submit the open batch (if any) without waiting, then wait via the
7367        // completion handle. Empty batch → None → nothing to wait.
7368        // Step E B1: `new` increments the in-flight counter for this device;
7369        // `handle.wait()` waits + drops the `VulkanCompletion`, decrementing it
7370        // — a balanced submit/retire even on this transient realize-end path.
7371        let loc = DeviceLocation::Vulkan {
7372            gpu_id: backend.gpu_id,
7373        };
7374        if let Some(batch) = backend.submit_pending()? {
7375            let handle: CompletionHandle =
7376                CompletionHandle::Pending(Box::new(VulkanCompletion::new(backend, batch, loc)));
7377            handle.wait()?;
7378        }
7379    }
7380    Ok(())
7381}
7382
7383#[cfg(not(feature = "vulkan"))]
7384fn drain_vulkan_pending(_cache: &StorageCache) -> Result<()> {
7385    Ok(())
7386}
7387
7388/// Step E A2: force the Vulkan backend behind `arc` (if any) to submit + WAIT
7389/// its open batch — the blocking drain before freeing a buffer the batch may
7390/// reference (a destructive eviction) so a recorded-but-unsubmitted command never
7391/// reads freed memory. No-op for non-Vulkan storage, an empty batch, or a
7392/// non-Vulkan build.
7393///
7394/// **Retained for the SINGLE-DEVICE path (byte-identical to pre-A2.1).** A2.1's
7395/// deferred-deletion ([`defer_evicted_vulkan_buffer`]) replaces this only on a
7396/// MULTI-backend realize, where eager-submit + in-flight tracking + the
7397/// realize-end `drain_inflight_vulkan` are all live. On a single-device Vulkan
7398/// realize the A4b machinery is gated off (`multi_backend == false`): the
7399/// cross-batch RAW guard (`pipelined.rs:832`) does not run, so a later Vulkan op
7400/// reading a buffer an eager-submitted batch wrote could race (separate
7401/// submissions only BEGIN in order, may overlap). The blocking `force_flush`
7402/// (submit + wait) is the coherent eviction drain there — exactly the original
7403/// behavior — so single-device stays byte-identical and UAF-safe.
7404#[cfg(feature = "vulkan")]
7405fn force_flush_vulkan(arc: &Arc<RwLock<Storage>>) -> Result<()> {
7406    let guard = arc
7407        .read()
7408        .map_err(|_| poisoned("storage lock during vulkan force_flush"))?;
7409    if let fuel_memory::BackendStorage::Vulkan(v) = &guard.inner
7410        && let Some(backend) = v.backend()
7411    {
7412        backend.force_flush()?;
7413    }
7414    Ok(())
7415}
7416
7417#[cfg(not(feature = "vulkan"))]
7418fn force_flush_vulkan(_arc: &Arc<RwLock<Storage>>) -> Result<()> {
7419    Ok(())
7420}
7421
7422/// Step E A2.1 — DEFERRED-DELETION of a Vulkan data buffer evicted while Vulkan
7423/// work is in flight (the throughput refinement that REPLACES the eviction-time
7424/// drain on a MULTI-backend realize). Called at the per-NodeId destructive
7425/// eviction (`Op::Release`/`Op::Move` cleanup) for the buffer about to leave the
7426/// cache, INSTEAD of `drain_inflight_vulkan` + `force_flush` (two host-blocking
7427/// fence waits) — so the iGPU work the executor was overlapping with CUDA is not
7428/// stalled at every eviction.
7429///
7430/// Two classes of Vulkan reader can hold a reference to `destroyed`'s buffer at
7431/// the eviction:
7432///   1. the **in-flight** `SubmittedBatch`es in `inflight` (submitted CBs, each
7433///      fenced), and
7434///   2. the **open**, not-yet-submitted recorder batch (recorded commands, NO
7435///      fence yet).
7436///
7437/// We make BOTH fence-gated without blocking the host:
7438///   - `eager_submit_all_vulkan` submits the open batch WITHOUT waiting, turning
7439///     it into one more in-flight `SubmittedBatch` (now fenced) pushed onto
7440///     `inflight`. (Whole-chunk granularity — UAF/race-safe per its contract; at a
7441///     destructive eviction the open batch is a contiguous Vulkan run.)
7442///   - We then move a refcount clone of the evicted buffer's `Arc<VulkanBuffer>`
7443///     into EVERY in-flight batch (conservative: we cannot cheaply map
7444///     buffer→batch, exactly the reason the old drain waited *all* of them). Each
7445///     batch frees its retained clone POST-fence on `Drop` (follow-on #2).
7446///
7447/// After this returns the caller `cache.remove`s `destroyed`; the cache's
7448/// `Arc<RwLock<Storage>>` drops, but the buffer's recycler
7449/// (`VulkanBuffer::drop`) reclaims it only when its LAST `Arc` drops — and the
7450/// retained clones keep it alive until their owning batches' fences signal. So the
7451/// GPU's last read of the buffer strictly precedes the free (NO use-after-free),
7452/// the host never blocks at the eviction (the iGPU keeps overlapping CUDA — the
7453/// throughput win), and every batch (hence every retained buffer) is drained by
7454/// realize-end at the latest (`drain_inflight_vulkan`, `pipelined.rs:916/1238`,
7455/// runs unconditionally → NO leak).
7456///
7457/// **Multi-backend only** (the call site gates `if multi_backend`). On a
7458/// single-device realize the open batch is drained by the blocking
7459/// [`force_flush_vulkan`] instead (byte-identical to pre-A2.1; see its doc for
7460/// why the deferred path is unsafe without the cross-batch RAW guard). No-op for a
7461/// non-Vulkan storage, an empty open + no in-flight batches, or a non-Vulkan build.
7462/// Open-batch size (dispatches) at which a deferred eviction submits the batch so the
7463/// GPU runs it while the host keeps recording.
7464#[cfg(feature = "vulkan")]
7465const EVICT_SUBMIT_MIN_DISPATCHES: usize = 48;
7466
7467#[cfg(feature = "vulkan")]
7468fn defer_evicted_vulkan_buffer(
7469    cache: &StorageCache,
7470    destroyed: NodeId,
7471    inflight: &mut Vec<VulkanCompletion>,
7472) -> Result<()> {
7473    // Pull the evicted buffer's device Arc (and its backend) out of the cache BEFORE
7474    // it is removed. Non-Vulkan storage, a host-evicted (no device buffer) Vulkan
7475    // storage, or an absent entry → nothing to retain (no Vulkan reader can hold a
7476    // device buffer).
7477    let (buf, backend) = {
7478        let Some(arc) = cache.get(&destroyed) else {
7479            return Ok(());
7480        };
7481        let guard = arc
7482            .read()
7483            .map_err(|_| poisoned("storage lock during deferred-eviction retain"))?;
7484        match &guard.inner {
7485            fuel_memory::BackendStorage::Vulkan(v) => (v.device_buffer_arc(), v.backend().cloned()),
7486            _ => (None, None),
7487        }
7488    };
7489    let Some(buf) = buf else { return Ok(()) };
7490
7491    // The OPEN batch may hold recorded-but-unsubmitted commands that read `destroyed`:
7492    // retain the buffer on the open batch itself (freed post-fence once that batch is
7493    // submitted and waited). This neither submits nor waits — it used to eager-submit
7494    // here, i.e. one queue submit per eviction (61 per Qwen3 decode step).
7495    if let Some(backend) = &backend {
7496        backend.retain_in_open_batch(buf.clone());
7497    }
7498    // Retain a clone of the evicted buffer in EVERY in-flight batch. Each frees it
7499    // POST-fence on `Drop` — the buffer outlives the GPU's last read with no host
7500    // block. Conservative over all batches (no cheap buffer→batch map); over-retention
7501    // only lengthens the lifetime, never a UAF, and all batches drain by realize-end.
7502    for vc in inflight.iter_mut() {
7503        vc.retain_data(buf.clone());
7504    }
7505    // Keep the GPU fed: submit the open batch once it has grown by a chunk.
7506    if let Some(backend) = backend
7507        && backend.open_batch_len() >= EVICT_SUBMIT_MIN_DISPATCHES
7508    {
7509        eager_submit_all_vulkan(cache, inflight)?;
7510    }
7511    // Step E A2.1 — throughput-observability. Count a DEFERRED retain that
7512    // actually replaced a host block: an evicted Vulkan buffer retained onto ≥1
7513    // in-flight batch (the open-batch eager-submit and/or pre-existing in-flight
7514    // batches). Pre-A2.1 every such site host-blocked (`force_flush` and/or
7515    // `drain_inflight_vulkan`); now it does not. A test reads
7516    // [`deferred_eviction_retains`] to assert the deferred path fired without a
7517    // drain (deterministic — no wall-clock, which is noisy on this box). When
7518    // `inflight` is empty here there was genuinely no in-flight/open Vulkan reader
7519    // to wait for, so no host block was avoided (and none was needed) — not counted.
7520    if !inflight.is_empty() {
7521        DEFERRED_EVICTION_RETAINS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
7522    }
7523    Ok(())
7524}
7525
7526/// Step E A2.1 — cumulative count of deferred-deletion retains that replaced a
7527/// host-blocking eviction drain (an evicted Vulkan buffer retained onto ≥1
7528/// in-flight batch instead of waiting it). Process-global, `Relaxed` — a pure
7529/// test/telemetry observation, never a correctness gate (mirrors the B1 in-flight
7530/// counter idiom). Read by the live deferred-eviction throughput test.
7531#[cfg(feature = "vulkan")]
7532pub static DEFERRED_EVICTION_RETAINS: std::sync::atomic::AtomicUsize =
7533    std::sync::atomic::AtomicUsize::new(0);
7534
7535/// Read the A2.1 deferred-eviction-retain counter (see
7536/// [`DEFERRED_EVICTION_RETAINS`]). `pub` so the live throughput test can assert a
7537/// deferred retain fired (the eviction did NOT host-block on a Vulkan fence).
7538#[cfg(feature = "vulkan")]
7539pub fn deferred_eviction_retains() -> usize {
7540    DEFERRED_EVICTION_RETAINS.load(std::sync::atomic::Ordering::Relaxed)
7541}
7542
7543#[cfg(not(feature = "vulkan"))]
7544#[inline]
7545fn defer_evicted_vulkan_buffer(
7546    _cache: &StorageCache,
7547    _destroyed: NodeId,
7548    _inflight: &mut Vec<InflightVulkan>,
7549) -> Result<()> {
7550    Ok(())
7551}
7552
7553/// Vulkan counterpart of [`find_cuda_device_in_cache`]. Returns an
7554/// `Arc<VulkanBackend>` clone derived from any cached Vulkan storage
7555/// whose backend's `gpu_id` matches.
7556#[cfg(feature = "vulkan")]
7557fn find_vulkan_backend_in_cache(
7558    cache: &StorageCache,
7559    gpu_id: usize,
7560) -> Option<std::sync::Arc<fuel_vulkan_backend::VulkanBackend>> {
7561    for arc in cache.values() {
7562        let guard = arc.read().ok()?;
7563        if let fuel_memory::BackendStorage::Vulkan(v) = &guard.inner
7564            && let Some(backend) = v.backend()
7565            && backend.gpu_id == gpu_id
7566        {
7567            return Some(backend.clone());
7568        }
7569    }
7570    None
7571}
7572
7573/// Materialize a contiguous Storage Arc from a non-contiguous one.
7574/// Allocates a fresh buffer on the input's backend and copies the
7575/// strided / offset / broadcast input into it via the backend's
7576/// contiguize kernel. The returned Arc is a brand-new buffer; the
7577/// caller is responsible for replacing the cache entry only for the
7578/// duration of one kernel call (the upstream view op's output stays
7579/// in the cache so other consumers still see the strided view).
7580///
7581/// Stage 4 of Layout-on-Node — auto-Contiguize.
7582fn auto_contiguize(arc: &Arc<RwLock<Storage>>, layout: &Layout) -> Result<Arc<RwLock<Storage>>> {
7583    let in_guard = arc
7584        .read()
7585        .map_err(|_| poisoned("input storage lock during auto_contiguize"))?;
7586    let dtype = in_guard.dtype;
7587    let dtype_size = dtype.size_in_bytes();
7588    let new_storage = match &in_guard.inner {
7589        fuel_memory::BackendStorage::Cpu(c) => {
7590            let new_bytes = fuel_cpu_backend::byte_kernels::contiguize_cpu(c, layout, dtype_size)?;
7591            Storage::new(fuel_memory::BackendStorage::Cpu(new_bytes), dtype)
7592        }
7593        #[cfg(feature = "cuda")]
7594        fuel_memory::BackendStorage::Cuda(c) => {
7595            // Native baracuda contiguize (alpha.29). Byte-width-
7596            // dispatched: 1/2/4/8/16 byte elements all route to the
7597            // appropriate kernel. Handles signed strides (Flip),
7598            // zero strides (BroadcastTo), and non-zero element offset.
7599            //
7600            // Three host-side fast paths bake into the baracuda
7601            // launchers: already-contiguous → single cuMemcpyDtoDAsync;
7602            // innermost-stride-1 → per-outer-coord run copy; generic
7603            // → one thread per output element. Retires the prior
7604            // D2H → CPU contiguize_cpu → H2D fallback (two device
7605            // round-trips per non-contig input).
7606            let contig = fuel_cuda_backend::baracuda::contiguize::contiguize_to_fresh(
7607                c, layout, dtype_size,
7608            )?;
7609            Storage::new(fuel_memory::BackendStorage::Cuda(contig), dtype)
7610        }
7611        #[cfg(feature = "vulkan")]
7612        fuel_memory::BackendStorage::Vulkan(v) => {
7613            // Native `strided_copy_signed_*` kernel when it applies (2/4/8-byte
7614            // elements; see `contiguize_vulkan`). Otherwise the V.1.B host round trip:
7615            // D2H → CPU contiguize_cpu → H2D, which forces a flush and a wait
7616            // per call.
7617            let backend = v.backend().ok_or_else(|| {
7618                Error::Msg(
7619                    "auto_contiguize: Vulkan input has no backend handle. \
7620                     Storages flowing through the pipelined executor's Vulkan \
7621                     path must be constructed via VulkanBackend::alloc_bytes_handle \
7622                     / upload_bytes_handle (not the legacy alloc_bytes / \
7623                     upload_bytes which leave VulkanStorageBytes::backend = None)."
7624                        .to_string(),
7625                )
7626                .bt()
7627            })?;
7628            if let Some(native) = crate::vulkan_dispatch::contiguize_vulkan(v, layout, dtype_size)?
7629            {
7630                return Ok(Arc::new(RwLock::new(Storage::new(
7631                    fuel_memory::BackendStorage::Vulkan(native),
7632                    dtype,
7633                ))));
7634            }
7635            let _t = crate::compiled::op_profile::PhaseTimer::start(
7636                "auto_contiguize.vulkan_host_roundtrip",
7637            );
7638            let host_bytes = backend.download_bytes(v)?;
7639            let host_cpu = fuel_cpu_backend::byte_storage::CpuStorageBytes::from_bytes(&host_bytes);
7640            let contig_cpu =
7641                fuel_cpu_backend::byte_kernels::contiguize_cpu(&host_cpu, layout, dtype_size)?;
7642            let host_contig_bytes = contig_cpu.bytes().to_vec();
7643            let vk_bytes = backend.upload_bytes_handle(&host_contig_bytes)?;
7644            Storage::new(fuel_memory::BackendStorage::Vulkan(vk_bytes), dtype)
7645        }
7646        #[allow(unreachable_patterns)]
7647        _ => {
7648            return Err(Error::Msg(
7649                "auto_contiguize: backend not wired (CPU + CUDA + Vulkan are \
7650                 wired today; Metal extends this match when its byte-storage \
7651                 substrate lands)"
7652                    .to_string(),
7653            )
7654            .bt());
7655        }
7656    };
7657    Ok(Arc::new(RwLock::new(new_storage)))
7658}
7659
7660/// Write-into sibling of [`auto_contiguize`] (CapturedRun executor
7661/// build-out, 4b-γ): materializes a contiguous copy of `arc` at `layout`
7662/// INTO `dest`'s EXISTING buffer instead of allocating a fresh one — the
7663/// path `WorkItemKind::ContiguizeOf`'s Reuse-mode branch needs (device
7664/// allocation is illegal inside a capture scope). `dest` must already be
7665/// sized to the contiguized output's byte count — true by construction,
7666/// since the persistent map only ever holds a buffer THIS SAME code path
7667/// (the warm-run `auto_contiguize` allocation) sized.
7668///
7669/// CUDA-only today: `PersistentOutputs::reuse` (Reuse mode) is only ever
7670/// constructed inside [`PipelinedExecutor::capture_decode`], which is
7671/// `#[cfg(feature = "cuda")]`-gated and validates every compute node
7672/// targets `Cuda` — a CPU or Vulkan storage can never actually reach this
7673/// function in practice. Per this project's "never panic on production
7674/// paths" rule, the other backends still surface a typed gap rather than
7675/// silently mishandling it (never a crash).
7676#[cfg_attr(not(feature = "cuda"), allow(unused_variables))]
7677fn contiguize_into(
7678    dest: &Arc<RwLock<Storage>>,
7679    arc: &Arc<RwLock<Storage>>,
7680    layout: &Layout,
7681) -> Result<()> {
7682    let in_guard = arc
7683        .read()
7684        .map_err(|_| poisoned("input storage lock during contiguize_into"))?;
7685    match &in_guard.inner {
7686        #[cfg(feature = "cuda")]
7687        fuel_memory::BackendStorage::Cuda(src_cuda) => {
7688            let dtype_size = in_guard.dtype.size_in_bytes();
7689            let mut dest_guard = dest
7690                .write()
7691                .map_err(|_| poisoned("dest storage lock during contiguize_into"))?;
7692            let dest_cuda = match &mut dest_guard.inner {
7693                fuel_memory::BackendStorage::Cuda(d) => d,
7694                other => {
7695                    return Err(Error::Msg(format!(
7696                        "contiguize_into: dest is BackendStorage::{:?}, not Cuda",
7697                        std::mem::discriminant(other),
7698                    ))
7699                    .bt());
7700                }
7701            };
7702            fuel_cuda_backend::baracuda::contiguize::contiguize_into(
7703                src_cuda, layout, dest_cuda, dtype_size,
7704            )
7705        }
7706        #[allow(unreachable_patterns)]
7707        _ => Err(Error::Msg(
7708            "contiguize_into: only CUDA is wired (PersistentOutputs::reuse / \
7709             capture_decode is CUDA-only today — no CPU/Vulkan consumer exists \
7710             yet; extend this match if one appears)"
7711                .to_string(),
7712        )
7713        .bt()),
7714    }
7715}
7716
7717#[cfg(test)]
7718mod backend_neutral_tests {
7719    use super::*;
7720
7721    #[test]
7722    fn only_deviceless_items_are_backend_neutral() {
7723        let n = NodeId(0);
7724        // Adopting an input-cache entry or aliasing a producer touches no device.
7725        assert!(is_backend_neutral(&WorkItemKind::ConstAdopt));
7726        assert!(is_backend_neutral(&WorkItemKind::ViewOf { input: n }));
7727        assert!(is_backend_neutral(&WorkItemKind::SlotView { producer: n }));
7728        // Contiguize runs on its input's backend; its own `target_backend` is a
7729        // placeholder, so it must not count as a backend switch.
7730        assert!(is_backend_neutral(&WorkItemKind::ContiguizeOf { input: n }));
7731        // Sabotage siblings: anything that runs a kernel of its own or moves bytes
7732        // across devices is NOT, so a real backend switch still arms the
7733        // chunk-boundary logic.
7734        assert!(!is_backend_neutral(&WorkItemKind::Kernel));
7735        assert!(!is_backend_neutral(&WorkItemKind::Copy {
7736            target_location: DeviceLocation::Cpu
7737        }));
7738        assert!(!is_backend_neutral(&WorkItemKind::WriteSlice {
7739            dest: n,
7740            source: n
7741        }));
7742    }
7743}
7744
7745#[cfg(test)]
7746mod tests {
7747    use super::*;
7748    use fuel_graph::Node;
7749    use fuel_ir::Shape;
7750
7751    /// Task 5 (C6): `Op::Scan` has no native kernel in Phase 1 — it is
7752    /// always lowered/re-fused before execution — so `op_to_op_kind` must
7753    /// fall through its `_ => None` wildcard for it, exactly like any other
7754    /// not-yet-wired op. Regression lock, not born-red: this is expected to
7755    /// pass immediately.
7756    #[test]
7757    fn scan_has_no_dispatch_op_kind() {
7758        use fuel_graph::{Op, ScanEmit};
7759        assert_eq!(
7760            crate::pipelined::op_to_op_kind(&Op::Scan {
7761                n_carries: 1,
7762                n_xs: 0,
7763                bound: 2,
7764                emit: ScanEmit::All,
7765                early_exit: None
7766            }),
7767            None,
7768        );
7769    }
7770
7771    /// Step E A4b open question #4 — "event only where waited", step 1.
7772    /// `build_wait_set` collects exactly the producers of `Op::Copy`/`Move`
7773    /// nodes in the order: a same-device-only node (never copied/moved) must
7774    /// be ABSENT, and the Copy's own node id must not appear either (it is
7775    /// not its own producer).
7776    #[test]
7777    fn build_wait_set_collects_only_cross_device_copy_producers() {
7778        let mut g = Graph::new();
7779        let const_id = g.push(Node {
7780            op: Op::Const,
7781            inputs: vec![],
7782            shape: Shape::from_dims(&[4]),
7783            dtype: DType::F32,
7784        });
7785        // Same-device-only consumer of const_id — never fed into a Copy/Move.
7786        let same_device_id = g.push(Node {
7787            op: Op::Contiguize,
7788            inputs: vec![const_id],
7789            shape: Shape::from_dims(&[4]),
7790            dtype: DType::F32,
7791        });
7792        // A cross-device Copy whose producer is `same_device_id`.
7793        let copy_id = g.push(Node {
7794            op: Op::Copy {
7795                target: DeviceLocation::Cuda { gpu_id: 0 },
7796            },
7797            inputs: vec![same_device_id],
7798            shape: Shape::from_dims(&[4]),
7799            dtype: DType::F32,
7800        });
7801        let order = vec![const_id, same_device_id, copy_id];
7802
7803        let set = build_wait_set(&g, &order);
7804        assert!(
7805            set.contains(&same_device_id),
7806            "the Copy's producer must be in the wait set"
7807        );
7808        assert!(
7809            !set.contains(&const_id),
7810            "a node with no Copy/Move consumer must be absent from the wait set"
7811        );
7812        assert!(
7813            !set.contains(&copy_id),
7814            "the Copy node itself is not its own producer"
7815        );
7816        assert_eq!(
7817            set.len(),
7818            1,
7819            "only the one true cross-device producer is in the set"
7820        );
7821    }
7822
7823    /// A `Move` producer is collected the same way as a `Copy` producer.
7824    #[test]
7825    fn build_wait_set_collects_move_producers_too() {
7826        let mut g = Graph::new();
7827        let const_id = g.push(Node {
7828            op: Op::Const,
7829            inputs: vec![],
7830            shape: Shape::from_dims(&[4]),
7831            dtype: DType::F32,
7832        });
7833        let move_id = g.push(Node {
7834            op: Op::Move {
7835                target: DeviceLocation::Cpu,
7836            },
7837            inputs: vec![const_id],
7838            shape: Shape::from_dims(&[4]),
7839            dtype: DType::F32,
7840        });
7841        let order = vec![const_id, move_id];
7842
7843        let set = build_wait_set(&g, &order);
7844        assert!(
7845            set.contains(&const_id),
7846            "the Move's producer must be in the wait set"
7847        );
7848        assert_eq!(set.len(), 1);
7849    }
7850
7851    /// A CPU-only order (no `Op::Copy`/`Op::Move` anywhere) yields an empty
7852    /// wait set — every node in it is eligible for elision.
7853    #[test]
7854    fn build_wait_set_is_empty_for_cpu_only_order() {
7855        let mut g = Graph::new();
7856        let const_id = g.push(Node {
7857            op: Op::Const,
7858            inputs: vec![],
7859            shape: Shape::from_dims(&[4]),
7860            dtype: DType::F32,
7861        });
7862        let contig_id = g.push(Node {
7863            op: Op::Contiguize,
7864            inputs: vec![const_id],
7865            shape: Shape::from_dims(&[4]),
7866            dtype: DType::F32,
7867        });
7868        let order = vec![const_id, contig_id];
7869
7870        let set = build_wait_set(&g, &order);
7871        assert!(
7872            set.is_empty(),
7873            "a CPU-only order (no Copy/Move) yields an empty wait set"
7874        );
7875    }
7876
7877    /// `will_be_waited`'s two arms: the conservative `None` fallback (the
7878    /// `OrderSource::Streaming` case) is always `true`; a `Some(set)` checks
7879    /// membership.
7880    #[test]
7881    fn will_be_waited_none_is_conservative_true_for_any_node() {
7882        let ws: WaitSet = None;
7883        assert!(will_be_waited(&ws, NodeId(0)));
7884        assert!(will_be_waited(&ws, NodeId(12345)));
7885    }
7886
7887    #[test]
7888    fn will_be_waited_some_checks_membership() {
7889        let mut set = HashSet::new();
7890        set.insert(NodeId(3));
7891        let ws: WaitSet = Some(set);
7892        assert!(will_be_waited(&ws, NodeId(3)), "member must be waited");
7893        assert!(!will_be_waited(&ws, NodeId(4)), "non-member must be elided");
7894    }
7895
7896    /// A4b-1 test double: a `Completion` whose `wait` flips a shared flag, so a
7897    /// test can observe whether the drain actually waited it.
7898    struct FlagCompletion(std::sync::Arc<std::sync::atomic::AtomicUsize>);
7899    impl crate::compiled::Completion for FlagCompletion {
7900        fn wait(self: Box<Self>) -> Result<()> {
7901            self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
7902            Ok(())
7903        }
7904    }
7905
7906    /// A4b-1: `store_handle` keeps only `Pending` handles (so the post-drain
7907    /// empty-map assert is meaningful and the drain is O(pending)), and
7908    /// `drain_handles` waits each pending handle exactly once then empties the map.
7909    #[test]
7910    fn handle_map_stores_pending_and_drains_once() {
7911        use std::sync::Arc as StdArc;
7912        use std::sync::atomic::{AtomicUsize, Ordering};
7913
7914        let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
7915
7916        // Ready handles are not stored.
7917        store_handle(&mut handles, NodeId(1), CompletionHandle::Ready);
7918        assert!(handles.is_empty(), "Ready handles must not be stored");
7919
7920        // Two pending handles are stored and each waited once on drain.
7921        let waited = StdArc::new(AtomicUsize::new(0));
7922        store_handle(
7923            &mut handles,
7924            NodeId(2),
7925            CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7926        );
7927        store_handle(
7928            &mut handles,
7929            NodeId(3),
7930            CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7931        );
7932        assert_eq!(handles.len(), 2);
7933
7934        drain_handles(&mut handles).expect("drain");
7935        assert_eq!(
7936            waited.load(Ordering::SeqCst),
7937            2,
7938            "each pending handle waited once"
7939        );
7940        assert!(handles.is_empty(), "map empty after drain");
7941    }
7942
7943    /// A4b-1: re-keying a NodeId with a new Pending handle waits the prior one
7944    /// (no leaked async work), and storing `Ready` over a prior Pending also
7945    /// waits it. Defensive — NodeIds are unique per realize in practice.
7946    #[test]
7947    fn handle_map_rekey_waits_previous() {
7948        use std::sync::Arc as StdArc;
7949        use std::sync::atomic::{AtomicUsize, Ordering};
7950
7951        let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
7952        let waited = StdArc::new(AtomicUsize::new(0));
7953
7954        store_handle(
7955            &mut handles,
7956            NodeId(7),
7957            CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7958        );
7959        // Re-key with another Pending → prior is waited.
7960        store_handle(
7961            &mut handles,
7962            NodeId(7),
7963            CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7964        );
7965        assert_eq!(
7966            waited.load(Ordering::SeqCst),
7967            1,
7968            "rekey waited the prior pending"
7969        );
7970
7971        // Store Ready over the live Pending → it is waited too.
7972        store_handle(&mut handles, NodeId(7), CompletionHandle::Ready);
7973        assert_eq!(
7974            waited.load(Ordering::SeqCst),
7975            2,
7976            "Ready-over-Pending waited it"
7977        );
7978        assert!(handles.is_empty());
7979
7980        drain_handles(&mut handles).expect("drain empty");
7981    }
7982
7983    /// A4b-3: `wait_producer_handle` waits the named producer's handle EXACTLY
7984    /// once and removes it from the map (the finer cross-device source-drain
7985    /// consumes the producer's completion before the Copy reads its buffer), and
7986    /// is a no-op for an absent producer (CPU / Vulkan-`Ready` source, or an
7987    /// already-consumed handle). The realize-end drain then waits only what
7988    /// remains — no double-wait.
7989    #[test]
7990    fn wait_producer_handle_consumes_named_handle_once() {
7991        use std::sync::Arc as StdArc;
7992        use std::sync::atomic::{AtomicUsize, Ordering};
7993
7994        let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
7995        let waited = StdArc::new(AtomicUsize::new(0));
7996
7997        // Two producers in flight; the Copy's source is NodeId(2).
7998        store_handle(
7999            &mut handles,
8000            NodeId(2),
8001            CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
8002        );
8003        store_handle(
8004            &mut handles,
8005            NodeId(5),
8006            CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
8007        );
8008
8009        // Absent producer → no-op (a CPU/Vulkan-Ready source has no handle).
8010        wait_producer_handle(&mut handles, NodeId(99)).expect("absent is no-op");
8011        assert_eq!(
8012            waited.load(Ordering::SeqCst),
8013            0,
8014            "absent producer waited nothing"
8015        );
8016        assert_eq!(handles.len(), 2);
8017
8018        // The cross-device copy's source: waited once, removed from the map.
8019        wait_producer_handle(&mut handles, NodeId(2)).expect("wait source");
8020        assert_eq!(
8021            waited.load(Ordering::SeqCst),
8022            1,
8023            "source producer waited once"
8024        );
8025        assert!(!handles.contains_key(&NodeId(2)), "source handle consumed");
8026        assert!(
8027            handles.contains_key(&NodeId(5)),
8028            "unrelated handle untouched"
8029        );
8030
8031        // Re-waiting the (now absent) source is a no-op — no double-wait.
8032        wait_producer_handle(&mut handles, NodeId(2)).expect("re-wait is no-op");
8033        assert_eq!(waited.load(Ordering::SeqCst), 1, "no double-wait");
8034
8035        // Realize-end drain waits only the remaining unrelated producer.
8036        drain_handles(&mut handles).expect("drain");
8037        assert_eq!(
8038            waited.load(Ordering::SeqCst),
8039            2,
8040            "drain waited the remaining one"
8041        );
8042        assert!(handles.is_empty());
8043    }
8044
8045    /// Op::Contiguize on a contiguous-already input is zero-copy:
8046    /// the executor adopts the input Storage Arc unchanged.
8047    #[test]
8048    fn op_contiguize_zero_copy_on_contiguous_input() {
8049        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
8050
8051        let graph = Arc::new(RwLock::new(Graph::new()));
8052        let (const_id, contig_id) = {
8053            let mut g = graph.write().unwrap();
8054            let const_id = g.push(Node {
8055                op: Op::Const,
8056                inputs: vec![],
8057                shape: Shape::from_dims(&[2, 2]),
8058                dtype: DType::F32,
8059            });
8060            let contig_id = g.push(Node {
8061                op: Op::Contiguize,
8062                inputs: vec![const_id],
8063                shape: Shape::from_dims(&[2, 2]),
8064                dtype: DType::F32,
8065            });
8066            // No need to set target_backend on Contiguize — the
8067            // executor inherits or defaults to CPU.
8068            (const_id, contig_id)
8069        };
8070
8071        let mut inputs = StorageCache::new();
8072        let input_arc = Arc::new(RwLock::new(storage));
8073        let input_arc_for_compare = Arc::clone(&input_arc);
8074        inputs.insert(const_id, input_arc);
8075
8076        let (result_arc, _) =
8077            PipelinedExecutor::realize(graph, contig_id, inputs).expect("realize");
8078
8079        // Same Arc → zero-copy adoption. Compare by raw pointer.
8080        assert!(
8081            Arc::ptr_eq(&result_arc, &input_arc_for_compare),
8082            "Op::Contiguize on contiguous input must adopt the input Arc (zero copy)",
8083        );
8084    }
8085
8086    /// Op::Contiguize on a strided input (here: a transposed view)
8087    /// materializes a fresh contiguous Storage. The output bytes
8088    /// reflect the strided view's logical content, not the input
8089    /// storage's underlying memory order.
8090    #[test]
8091    fn op_contiguize_materializes_on_strided_input() {
8092        // 2x3 row-major: [[1,2,3],[4,5,6]] → transpose → 3x2:
8093        // [[1,4],[2,5],[3,6]]. The transposed view shares the
8094        // original buffer; Op::Contiguize on the transpose
8095        // materializes contiguous bytes matching the transposed
8096        // logical order.
8097        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
8098
8099        let graph = Arc::new(RwLock::new(Graph::new()));
8100        let (const_id, _transpose_id, contig_id) = {
8101            let mut g = graph.write().unwrap();
8102            let const_id = g.push(Node {
8103                op: Op::Const,
8104                inputs: vec![],
8105                shape: Shape::from_dims(&[2, 3]),
8106                dtype: DType::F32,
8107            });
8108            let transpose_id = g.push(Node {
8109                op: Op::Transpose,
8110                inputs: vec![const_id],
8111                shape: Shape::from_dims(&[3, 2]),
8112                dtype: DType::F32,
8113            });
8114            let contig_id = g.push(Node {
8115                op: Op::Contiguize,
8116                inputs: vec![transpose_id],
8117                shape: Shape::from_dims(&[3, 2]),
8118                dtype: DType::F32,
8119            });
8120            (const_id, transpose_id, contig_id)
8121        };
8122
8123        let mut inputs = StorageCache::new();
8124        inputs.insert(const_id, Arc::new(RwLock::new(storage)));
8125
8126        let (result_arc, _) =
8127            PipelinedExecutor::realize(graph, contig_id, inputs).expect("realize");
8128
8129        let guard = result_arc.read().unwrap();
8130        let cpu = if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
8131            c
8132        } else {
8133            panic!("expected Cpu storage");
8134        };
8135        let typed: &[f32] = cpu.as_slice().expect("f32 cast");
8136        // Transposed logical order: [1,4,2,5,3,6].
8137        assert_eq!(
8138            typed,
8139            &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0],
8140            "contiguized transpose must reflect logical (transposed) byte order",
8141        );
8142    }
8143
8144    /// E2E: 3-node graph (Const + Const + Add), pre-seeded inputs,
8145    /// realized through the compiler+executor thread pair, returns
8146    /// expected sum bytes.
8147    #[test]
8148    fn pipelined_realize_const_const_add() {
8149        let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
8150        let rhs_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
8151
8152        let graph = Arc::new(RwLock::new(Graph::new()));
8153        let (lhs_id, rhs_id, add_id) = {
8154            let mut g = graph.write().unwrap();
8155            let lhs_id = g.push(Node {
8156                op: Op::Const,
8157                inputs: vec![],
8158                shape: Shape::from_dims(&[3]),
8159                dtype: DType::F32,
8160            });
8161            let rhs_id = g.push(Node {
8162                op: Op::Const,
8163                inputs: vec![],
8164                shape: Shape::from_dims(&[3]),
8165                dtype: DType::F32,
8166            });
8167            let add_id = g.push(Node {
8168                op: Op::Add,
8169                inputs: vec![lhs_id, rhs_id],
8170                shape: Shape::from_dims(&[3]),
8171                dtype: DType::F32,
8172            });
8173            g.set_target_backend(add_id, BackendId::Cpu);
8174            (lhs_id, rhs_id, add_id)
8175        };
8176
8177        let mut inputs = StorageCache::new();
8178        inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
8179        inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
8180
8181        let (result_arc, _result_layout) =
8182            PipelinedExecutor::realize(graph, add_id, inputs).expect("realize");
8183
8184        let guard = result_arc.read().unwrap();
8185        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
8186            let typed: &[f32] = c.as_slice().expect("f32 cast");
8187            assert_eq!(typed, &[11.0, 22.0, 33.0]);
8188        } else {
8189            panic!("expected CPU output");
8190        }
8191    }
8192
8193    /// CapturedRun executor build-out (Increment 1): capture a 2-op binary
8194    /// chain `z = (a + b) * c` THROUGH the pipelined executor's
8195    /// persistent-output mode, then prove the three capture guarantees on a
8196    /// live GPU:
8197    ///   1. after capture, the fixed output buffer holds the warm-run result
8198    ///      (capture RECORDS launches, it does not execute them);
8199    ///   2. `replay()` recomputes bit-exactly into the SAME fixed buffer;
8200    ///   3. replay reads the FIXED input addresses — overwriting an input
8201    ///      buffer in place and replaying yields the new result, matching an
8202    ///      independent uncaptured `realize`. This is the whole mechanism:
8203    ///      the write-into binary wrapper + the executor's reuse mode +
8204    ///      `capture_run` compose into a replayable decode-step graph.
8205    #[test]
8206    #[ignore = "requires a live CUDA device"]
8207    #[cfg(feature = "cuda")]
8208    fn capture_decode_binary_chain_replays_bit_exact_cuda() {
8209        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8210        use fuel_ir::Shape;
8211        use fuel_memory::BackendStorage;
8212
8213        let Ok(dev) = CudaDevice::new(0) else {
8214            return fuel_test_support::hardware::skip(
8215                fuel_test_support::hardware::Hardware::Cuda,
8216                fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8217            );
8218        };
8219
8220        // z = (a + b) * c, shape [4]. `y = a + b` is a persistent
8221        // INTERMEDIATE; `z` is the persistent target — both fixed-address.
8222        let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId, NodeId) {
8223            let graph = Arc::new(RwLock::new(Graph::new()));
8224            let (a, b, c, z) = {
8225                let mut g = graph.write().unwrap();
8226                let a = g.push(Node {
8227                    op: Op::Const,
8228                    inputs: vec![],
8229                    shape: Shape::from_dims(&[4]),
8230                    dtype: DType::F32,
8231                });
8232                let b = g.push(Node {
8233                    op: Op::Const,
8234                    inputs: vec![],
8235                    shape: Shape::from_dims(&[4]),
8236                    dtype: DType::F32,
8237                });
8238                let c = g.push(Node {
8239                    op: Op::Const,
8240                    inputs: vec![],
8241                    shape: Shape::from_dims(&[4]),
8242                    dtype: DType::F32,
8243                });
8244                let y = g.push(Node {
8245                    op: Op::Add,
8246                    inputs: vec![a, b],
8247                    shape: Shape::from_dims(&[4]),
8248                    dtype: DType::F32,
8249                });
8250                let z = g.push(Node {
8251                    op: Op::Mul,
8252                    inputs: vec![y, c],
8253                    shape: Shape::from_dims(&[4]),
8254                    dtype: DType::F32,
8255                });
8256                g.set_target_backend(y, BackendId::Cuda);
8257                g.set_target_backend(z, BackendId::Cuda);
8258                (a, b, c, z)
8259            };
8260            (graph, a, b, c, z)
8261        };
8262
8263        let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8264            let bytes: &[u8] = bytemuck::cast_slice(v);
8265            let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytes).expect("h2d");
8266            Arc::new(RwLock::new(Storage::new(
8267                BackendStorage::Cuda(cb),
8268                DType::F32,
8269            )))
8270        };
8271        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8272            let g = arc.read().unwrap();
8273            let BackendStorage::Cuda(c) = &g.inner else {
8274                panic!("not cuda")
8275            };
8276            let host = c.to_cpu_bytes().expect("d2h");
8277            bytemuck::cast_slice(&host).to_vec()
8278        };
8279
8280        let a_data = [1.0_f32, 2.0, 3.0, 4.0];
8281        let b_data = [10.0_f32, 20.0, 30.0, 40.0];
8282        let c_data = [2.0_f32, 2.0, 2.0, 2.0];
8283        let expect0 = [22.0_f32, 44.0, 66.0, 88.0]; // (a+b)*c
8284
8285        // Fixed input Arcs (shared into capture; overwritten in place later).
8286        let a_arc = up(&a_data);
8287        let b_arc = up(&b_data);
8288        let c_arc = up(&c_data);
8289
8290        let (graph, a, b, c, z) = build();
8291        let mut inputs = StorageCache::new();
8292        inputs.insert(a, a_arc.clone());
8293        inputs.insert(b, b_arc.clone());
8294        inputs.insert(c, c_arc.clone());
8295
8296        let captured =
8297            PipelinedExecutor::capture_decode(Arc::clone(&graph), z, inputs, SymEnv::default())
8298                .expect("capture_decode");
8299
8300        // (1) warm-run result sits in the captured output buffer.
8301        assert_eq!(
8302            read(&captured.output),
8303            &expect0,
8304            "warm-run result after capture"
8305        );
8306
8307        // (2) replay with the same inputs recomputes bit-exactly.
8308        captured.run.replay(&dev).expect("replay");
8309        dev.synchronize().expect("sync");
8310        assert_eq!(read(&captured.output), &expect0, "replay reproduces result");
8311
8312        // (3) overwrite input `a` IN PLACE (same device address), replay ->
8313        //     the new result flows from the FIXED input address. Compare to
8314        //     an independent uncaptured realize with the same new input.
8315        let a2 = [100.0_f32, 200.0, 300.0, 400.0];
8316        {
8317            let g = a_arc.read().unwrap();
8318            let BackendStorage::Cuda(c) = &g.inner else {
8319                panic!("a not cuda")
8320            };
8321            c.write_from_host(bytemuck::cast_slice(&a2))
8322                .expect("in-place H2D");
8323        }
8324        captured.run.replay(&dev).expect("replay 2");
8325        dev.synchronize().expect("sync 2");
8326        let got = read(&captured.output);
8327
8328        let (graph_ref, ar, br, cr, zr) = build();
8329        let mut inputs_ref = StorageCache::new();
8330        inputs_ref.insert(ar, up(&a2));
8331        inputs_ref.insert(br, up(&b_data));
8332        inputs_ref.insert(cr, up(&c_data));
8333        let (ref_arc, _) =
8334            PipelinedExecutor::realize(graph_ref, zr, inputs_ref).expect("ref realize");
8335        let expect1 = read(&ref_arc); // (a2+b)*c = [220,440,660,880]
8336        assert_eq!(
8337            got, expect1,
8338            "replay after in-place input update matches uncaptured realize"
8339        );
8340        assert_eq!(expect1, vec![220.0, 440.0, 660.0, 880.0]);
8341    }
8342
8343    /// CapturedRun executor build-out (Increment 3): capture a DECODE-SHAPED
8344    /// graph — the full dense op-set composing in one capture — and prove it
8345    /// replays bit-exactly against an independent uncaptured realize.
8346    ///
8347    /// Graph (an FFN block + norms, batch=seq=1 like real decode):
8348    ///   n1 = rms_norm(x);  u = n1 · w_up;  s = silu(u);
8349    /// cuBLAS `gemm_dense` matmul IS graph-capturable (regression for the
8350    /// dangling-input bug that once masqueraded as "cuBLAS not replay-safe").
8351    /// Captures `y = a·w` where the caller does NOT retain `w`, FORCES the
8352    /// freed-`w` slot to be reused by garbage buffers, then replays 50× — all
8353    /// must equal the uncaptured realize. Passes iff `capture_decode` retains
8354    /// its input buffers (`CapturedDecode::retained_inputs`).
8355    #[test]
8356    #[ignore = "requires a live CUDA device"]
8357    #[cfg(feature = "cuda")]
8358    fn capture_decode_matmul_replays_bit_exact_cuda() {
8359        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8360        use fuel_ir::Shape;
8361        use fuel_memory::BackendStorage;
8362        let Ok(dev) = CudaDevice::new(0) else { return };
8363        const H: usize = 4;
8364        const F: usize = 8;
8365        let a_data: Vec<f32> = (0..H).map(|i| 0.5 + i as f32).collect();
8366        let w_data: Vec<f32> = (0..H * F).map(|i| 0.01 * (i as f32 + 1.0)).collect();
8367        let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId) {
8368            let graph = Arc::new(RwLock::new(Graph::new()));
8369            let (a, w, y) = {
8370                let mut g = graph.write().unwrap();
8371                let a = g.push(Node {
8372                    op: Op::Const,
8373                    inputs: vec![],
8374                    shape: Shape::from_dims(&[1, H]),
8375                    dtype: DType::F32,
8376                });
8377                let w = g.push(Node {
8378                    op: Op::Const,
8379                    inputs: vec![],
8380                    shape: Shape::from_dims(&[H, F]),
8381                    dtype: DType::F32,
8382                });
8383                let y = g.push(Node {
8384                    op: Op::MatMul,
8385                    inputs: vec![a, w],
8386                    shape: Shape::from_dims(&[1, F]),
8387                    dtype: DType::F32,
8388                });
8389                g.set_target_backend(y, BackendId::Cuda);
8390                (a, w, y)
8391            };
8392            (graph, a, w, y)
8393        };
8394        let up = |v: &[f32]| {
8395            let b: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
8396            Arc::new(RwLock::new(Storage::new(
8397                BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8398                DType::F32,
8399            )))
8400        };
8401        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8402            let g = arc.read().unwrap();
8403            let BackendStorage::Cuda(c) = &g.inner else {
8404                panic!("not cuda")
8405            };
8406            c.to_cpu_bytes()
8407                .unwrap()
8408                .chunks_exact(4)
8409                .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
8410                .collect()
8411        };
8412        // Reference (uncaptured).
8413        let (gr, ar, wr, yr) = build();
8414        let mut insr = StorageCache::new();
8415        insr.insert(ar, up(&a_data));
8416        insr.insert(wr, up(&w_data));
8417        let (refarc, _) = PipelinedExecutor::realize(gr, yr, insr).unwrap();
8418        let expect = read(&refarc);
8419        // Capture (a + w NOT retained by caller).
8420        let (graph, a, w, y) = build();
8421        let mut inputs = StorageCache::new();
8422        inputs.insert(a, up(&a_data));
8423        inputs.insert(w, up(&w_data));
8424        let captured =
8425            PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8426                .expect("capture");
8427        assert_eq!(read(&captured.output), expect, "warm result after capture");
8428        // FORCE freed-memory reuse: same-size (H*F) garbage buffers. If `w`
8429        // were dangling, replay would read 999.0s.
8430        let mut squatters = Vec::new();
8431        for _ in 0..8 {
8432            let g: Vec<u8> = (0..H * F).flat_map(|_| 999.0f32.to_le_bytes()).collect();
8433            squatters.push(CudaStorageBytes::from_cpu_bytes(&dev, &g).unwrap());
8434        }
8435        dev.synchronize().unwrap();
8436        let mut bad = 0usize;
8437        for _ in 0..50 {
8438            captured.run.replay(&dev).unwrap();
8439            dev.synchronize().unwrap();
8440            if read(&captured.output) != expect {
8441                bad += 1;
8442            }
8443        }
8444        assert_eq!(
8445            bad, 0,
8446            "cuBLAS matmul capture replays bit-exact ({bad}/50 wrong)"
8447        );
8448    }
8449
8450    /// `Op::MulScalar` / `OpKind::Affine` (the `1/√d` attention scale) is
8451    /// capture-safe (write-into variant + forced-reuse regression).
8452    #[test]
8453    #[ignore = "requires a live CUDA device"]
8454    #[cfg(feature = "cuda")]
8455    fn capture_decode_affine_replays_bit_exact_cuda() {
8456        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8457        use fuel_ir::Shape;
8458        use fuel_memory::BackendStorage;
8459        let Ok(dev) = CudaDevice::new(0) else { return };
8460        const N: usize = 6;
8461        let x_data: Vec<f32> = (0..N).map(|i| 1.0 + i as f32).collect();
8462        let expect: Vec<f32> = x_data.iter().map(|v| v * 0.5).collect();
8463        let graph = Arc::new(RwLock::new(Graph::new()));
8464        let (x, y) = {
8465            let mut g = graph.write().unwrap();
8466            let x = g.push(Node {
8467                op: Op::Const,
8468                inputs: vec![],
8469                shape: Shape::from_dims(&[1, N]),
8470                dtype: DType::F32,
8471            });
8472            let y = g.push(Node {
8473                op: Op::MulScalar(0.5),
8474                inputs: vec![x],
8475                shape: Shape::from_dims(&[1, N]),
8476                dtype: DType::F32,
8477            });
8478            g.set_target_backend(y, BackendId::Cuda);
8479            (x, y)
8480        };
8481        let up = |v: &[f32]| {
8482            let b: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
8483            Arc::new(RwLock::new(Storage::new(
8484                BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8485                DType::F32,
8486            )))
8487        };
8488        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8489            let g = arc.read().unwrap();
8490            let BackendStorage::Cuda(c) = &g.inner else {
8491                panic!("not cuda")
8492            };
8493            c.to_cpu_bytes()
8494                .unwrap()
8495                .chunks_exact(4)
8496                .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
8497                .collect()
8498        };
8499        let mut inputs = StorageCache::new();
8500        inputs.insert(x, up(&x_data));
8501        let captured =
8502            PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8503                .expect("capture");
8504        assert_eq!(read(&captured.output), expect, "warm result after capture");
8505        let mut sq = Vec::new();
8506        for _ in 0..8 {
8507            let g: Vec<u8> = (0..N).flat_map(|_| 999.0f32.to_le_bytes()).collect();
8508            sq.push(CudaStorageBytes::from_cpu_bytes(&dev, &g).unwrap());
8509        }
8510        dev.synchronize().unwrap();
8511        let mut bad = 0usize;
8512        for _ in 0..50 {
8513            captured.run.replay(&dev).unwrap();
8514            dev.synchronize().unwrap();
8515            if read(&captured.output) != expect {
8516                bad += 1;
8517            }
8518        }
8519        assert_eq!(bad, 0, "affine capture replays bit-exact ({bad}/50 wrong)");
8520    }
8521
8522    /// `index_select` (embedding lookup) IS graph-capturable (regression for
8523    /// the dangling-input bug that once looked like "element 0 dropped on
8524    /// replay"). Captures `emb = index_select(wte, tok)` with `wte` NOT
8525    /// retained by the caller, FORCES the freed-`wte` slot to be reused by
8526    /// garbage buffers, then replays 50× — all must be `[1,2,3,4]`. Passes iff
8527    /// `capture_decode` retains its input buffers.
8528    #[test]
8529    #[ignore = "requires a live CUDA device"]
8530    #[cfg(feature = "cuda")]
8531    fn capture_decode_index_select_replays_bit_exact_cuda() {
8532        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8533        use fuel_ir::Shape;
8534        use fuel_memory::BackendStorage;
8535        let Ok(dev) = CudaDevice::new(0) else { return };
8536        const V: usize = 3;
8537        const H: usize = 4;
8538        let wte_data: Vec<f32> = (0..V)
8539            .flat_map(|v| (0..H).map(move |c| 1.0 + 10.0 * v as f32 + c as f32))
8540            .collect();
8541        let graph = Arc::new(RwLock::new(Graph::new()));
8542        let (tok, wte, emb) = {
8543            let mut g = graph.write().unwrap();
8544            let tok = g.push(Node {
8545                op: Op::Const,
8546                inputs: vec![],
8547                shape: Shape::from_dims(&[1]),
8548                dtype: DType::U32,
8549            });
8550            let wte = g.push(Node {
8551                op: Op::Const,
8552                inputs: vec![],
8553                shape: Shape::from_dims(&[V, H]),
8554                dtype: DType::F32,
8555            });
8556            let emb = g.push(Node {
8557                op: Op::IndexSelect { dim: 0 },
8558                inputs: vec![wte, tok],
8559                shape: Shape::from_dims(&[1, H]),
8560                dtype: DType::F32,
8561            });
8562            g.set_target_backend(emb, BackendId::Cuda);
8563            (tok, wte, emb)
8564        };
8565        let up_f32 = |v: &[f32]| {
8566            let b: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
8567            Arc::new(RwLock::new(Storage::new(
8568                BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8569                DType::F32,
8570            )))
8571        };
8572        let up_u32 = |v: &[u32]| {
8573            let b: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
8574            Arc::new(RwLock::new(Storage::new(
8575                BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8576                DType::U32,
8577            )))
8578        };
8579        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8580            let g = arc.read().unwrap();
8581            let BackendStorage::Cuda(c) = &g.inner else {
8582                panic!("not cuda")
8583            };
8584            c.to_cpu_bytes()
8585                .unwrap()
8586                .chunks_exact(4)
8587                .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
8588                .collect()
8589        };
8590        let tok_arc = up_u32(&[0]);
8591        let mut inputs = StorageCache::new();
8592        inputs.insert(tok, tok_arc.clone());
8593        inputs.insert(wte, up_f32(&wte_data));
8594        let captured =
8595            PipelinedExecutor::capture_decode(Arc::clone(&graph), emb, inputs, SymEnv::default())
8596                .expect("capture");
8597        assert_eq!(
8598            read(&captured.output),
8599            vec![1.0, 2.0, 3.0, 4.0],
8600            "warm result after capture"
8601        );
8602        // FORCE freed-memory reuse: same-size (V*H) garbage buffers reclaim
8603        // `wte`'s slot if it were dangling — replay would then read 999.0s.
8604        let mut squatters = Vec::new();
8605        for _ in 0..8 {
8606            let g: Vec<u8> = (0..V * H).flat_map(|_| 999.0f32.to_le_bytes()).collect();
8607            squatters.push(CudaStorageBytes::from_cpu_bytes(&dev, &g).unwrap());
8608        }
8609        dev.synchronize().unwrap();
8610        let mut bad = 0usize;
8611        for _ in 0..50 {
8612            captured.run.replay(&dev).unwrap();
8613            dev.synchronize().unwrap();
8614            if read(&captured.output) != vec![1.0, 2.0, 3.0, 4.0] {
8615                bad += 1;
8616            }
8617        }
8618        assert_eq!(
8619            bad, 0,
8620            "index_select capture replays bit-exact ({bad}/50 wrong)"
8621        );
8622    }
8623
8624    ///   n1 = rms_norm(x);  s = silu(n1);  p = softmax(s);
8625    ///   r  = p + x;  n2 = rms_norm(r);  y = n2 + x
8626    /// Exercises rms_norm (×2 — proves the per-device RMS workspace is reused
8627    /// across sequential norms inside ONE captured graph), silu + softmax
8628    /// (write-into), and residual adds through the persistent-output capture
8629    /// path. Verified across two input value-sets (robust, not luck).
8630    ///
8631    /// Deliberately EXCLUDES matmul (cuBLAS not graph-replay-safe) and
8632    /// index_select (its host-side shape/stride arrays dangle under capture —
8633    /// proven to mis-replay). Both are surfaced `capture_decode` gaps, not
8634    /// silent mis-replays. See `op_kind_is_capture_writeinto`.
8635    #[test]
8636    #[ignore = "requires a live CUDA device"]
8637    #[cfg(feature = "cuda")]
8638    fn capture_decode_norm_silu_softmax_replays_bit_exact_cuda() {
8639        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8640        use fuel_graph::registry::{FusedOpParams, FusedOps};
8641        use fuel_ir::Shape;
8642        use fuel_memory::BackendStorage;
8643
8644        let Ok(dev) = CudaDevice::new(0) else {
8645            return fuel_test_support::hardware::skip(
8646                fuel_test_support::hardware::Hardware::Cuda,
8647                fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8648            );
8649        };
8650        const H: usize = 6;
8651        let eps = 1e-5f64;
8652
8653        let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId) {
8654            let graph = Arc::new(RwLock::new(Graph::new()));
8655            let (x, y) = {
8656                let mut g = graph.write().unwrap();
8657                let mk = |g: &mut Graph, op: Op, ins: Vec<NodeId>, dims: &[usize]| {
8658                    g.push(Node {
8659                        op,
8660                        inputs: ins,
8661                        shape: Shape::from_dims(dims),
8662                        dtype: DType::F32,
8663                    })
8664                };
8665                let rms = || {
8666                    Op::Fused(
8667                        FusedOps::RMS_NORM_LAST_DIM,
8668                        FusedOpParams::RmsNormLastDim { eps },
8669                    )
8670                };
8671                let sm = || Op::Fused(FusedOps::SOFTMAX_LAST_DIM, FusedOpParams::SoftmaxLastDim);
8672                let x = mk(&mut g, Op::Const, vec![], &[1, H]);
8673                let n1 = mk(&mut g, rms(), vec![x], &[1, H]);
8674                let s = mk(&mut g, Op::Silu, vec![n1], &[1, H]);
8675                let p = mk(&mut g, sm(), vec![s], &[1, H]);
8676                let r = mk(&mut g, Op::Add, vec![p, x], &[1, H]);
8677                let n2 = mk(&mut g, rms(), vec![r], &[1, H]);
8678                let y = mk(&mut g, Op::Add, vec![n2, x], &[1, H]);
8679                for id in [n1, s, p, r, n2, y] {
8680                    g.set_target_backend(id, BackendId::Cuda);
8681                }
8682                (x, y)
8683            };
8684            (graph, x, y)
8685        };
8686
8687        let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8688            let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
8689            Arc::new(RwLock::new(Storage::new(
8690                BackendStorage::Cuda(cb),
8691                DType::F32,
8692            )))
8693        };
8694        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8695            let g = arc.read().unwrap();
8696            let BackendStorage::Cuda(c) = &g.inner else {
8697                panic!("not cuda")
8698            };
8699            bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
8700        };
8701
8702        let x_data: Vec<f32> = (0..H).map(|c| 0.1 * (c as f32 + 1.0)).collect();
8703        let x_arc = up(&x_data);
8704        let (graph, x, y) = build();
8705        let mut inputs = StorageCache::new();
8706        inputs.insert(x, x_arc.clone());
8707
8708        let captured =
8709            PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8710                .expect("capture_decode norm/silu/softmax");
8711
8712        let realize_ref = |xv: &[f32]| -> Vec<f32> {
8713            let (g, xr, yr) = build();
8714            let mut ins = StorageCache::new();
8715            ins.insert(xr, up(xv));
8716            let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
8717            read(&arc)
8718        };
8719        let expect0 = realize_ref(&x_data);
8720
8721        // (1) after capture the output holds the warm result.
8722        assert_eq!(read(&captured.output), expect0, "warm result after capture");
8723        // (2) replay reproduces bit-exactly (incl. RMS workspace reuse on replay).
8724        captured.run.replay(&dev).expect("replay");
8725        dev.synchronize().expect("sync");
8726        assert_eq!(read(&captured.output), expect0, "replay reproduces result");
8727
8728        // (3) overwrite x in place, replay -> matches uncaptured realize (a
8729        //     DIFFERENT value-set — so the chain is robust, not input-lucky).
8730        let x2: Vec<f32> = (0..H).map(|c| 0.3 * (c as f32 + 1.0)).collect();
8731        {
8732            let g = x_arc.read().unwrap();
8733            let BackendStorage::Cuda(c) = &g.inner else {
8734                panic!("x not cuda")
8735            };
8736            c.write_from_host(bytemuck::cast_slice(&x2))
8737                .expect("in-place H2D");
8738        }
8739        captured.run.replay(&dev).expect("replay 2");
8740        dev.synchronize().expect("sync 2");
8741        assert_eq!(
8742            read(&captured.output),
8743            realize_ref(&x2),
8744            "replay after x update matches realize"
8745        );
8746    }
8747
8748    /// CapturedRun executor build-out (Increment 4b-γ): make `ContiguizeOf`
8749    /// (a non-contiguous reshape after `Permute` — the real decode graph's
8750    /// attention-merge reshape shape) AND a 2-input `Op::Concat` (the shape
8751    /// RoPE's CUDA decomposition emits for its rotate-half) BOTH
8752    /// capture-safe in ONE graph, so the crux 4b-γ exists for — allocation
8753    /// of an INTERMEDIATE, non-final buffer inside a capture scope — is
8754    /// actually exercised, not just each gap in isolation.
8755    ///
8756    /// Graph: `x [2,3,4] --Permute[1,0,2]--> [3,2,4]` (non-contiguous)
8757    /// `--Reshape[3,8]--> r` (`ContiguizeOf`, MUST allocate: its input is
8758    /// non-contiguous) `--Concat(dim=1, with b [3,8])--> y [3,16]` (target).
8759    #[test]
8760    #[ignore = "requires a live CUDA device"]
8761    #[cfg(feature = "cuda")]
8762    fn capture_decode_contiguize_then_concat_replays_bit_exact_cuda() {
8763        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8764        use fuel_ir::Shape;
8765        use fuel_memory::BackendStorage;
8766
8767        let Ok(dev) = CudaDevice::new(0) else {
8768            return fuel_test_support::hardware::skip(
8769                fuel_test_support::hardware::Hardware::Cuda,
8770                fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8771            );
8772        };
8773
8774        let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId) {
8775            let graph = Arc::new(RwLock::new(Graph::new()));
8776            let (x, b, y) = {
8777                let mut g = graph.write().unwrap();
8778                let x = g.push(Node {
8779                    op: Op::Const,
8780                    inputs: vec![],
8781                    shape: Shape::from_dims(&[2, 3, 4]),
8782                    dtype: DType::F32,
8783                });
8784                let p = g.push(Node {
8785                    op: Op::Permute(vec![1, 0, 2]),
8786                    inputs: vec![x],
8787                    shape: Shape::from_dims(&[3, 2, 4]),
8788                    dtype: DType::F32,
8789                });
8790                let r = g.push(Node {
8791                    op: Op::Reshape(Shape::from_dims(&[3, 8])),
8792                    inputs: vec![p],
8793                    shape: Shape::from_dims(&[3, 8]),
8794                    dtype: DType::F32,
8795                });
8796                let b = g.push(Node {
8797                    op: Op::Const,
8798                    inputs: vec![],
8799                    shape: Shape::from_dims(&[3, 8]),
8800                    dtype: DType::F32,
8801                });
8802                let y = g.push(Node {
8803                    op: Op::Concat { dim: 1 },
8804                    inputs: vec![r, b],
8805                    shape: Shape::from_dims(&[3, 16]),
8806                    dtype: DType::F32,
8807                });
8808                g.set_target_backend(p, BackendId::Cuda);
8809                g.set_target_backend(r, BackendId::Cuda);
8810                g.set_target_backend(y, BackendId::Cuda);
8811                (x, b, y)
8812            };
8813            (graph, x, b, y)
8814        };
8815
8816        let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8817            let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
8818            Arc::new(RwLock::new(Storage::new(
8819                BackendStorage::Cuda(cb),
8820                DType::F32,
8821            )))
8822        };
8823        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8824            let g = arc.read().unwrap();
8825            let BackendStorage::Cuda(c) = &g.inner else {
8826                panic!("not cuda")
8827            };
8828            bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
8829        };
8830
8831        let x_data: Vec<f32> = (0..24).map(|i| i as f32).collect();
8832        let b_data: Vec<f32> = (0..24).map(|i| 100.0 + i as f32).collect();
8833        let x_arc = up(&x_data);
8834        let (graph, x, b, y) = build();
8835        let mut inputs = StorageCache::new();
8836        inputs.insert(x, x_arc.clone());
8837        // `b` deliberately NOT retained by a local variable — forces
8838        // reliance on `capture_decode`'s own `retained_inputs`, mirroring
8839        // the matmul/index_select regression precedent.
8840        inputs.insert(b, up(&b_data));
8841
8842        let captured =
8843            PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8844                .expect("capture_decode contiguize+concat");
8845
8846        let realize_ref = |xv: &[f32], bv: &[f32]| -> Vec<f32> {
8847            let (g, xr, br, yr) = build();
8848            let mut ins = StorageCache::new();
8849            ins.insert(xr, up(xv));
8850            ins.insert(br, up(bv));
8851            let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
8852            read(&arc)
8853        };
8854        let expect0 = realize_ref(&x_data, &b_data);
8855
8856        // (1) warm-run result sits in the captured output buffer (capture
8857        //     RECORDS launches, it does not execute them at capture time).
8858        assert_eq!(read(&captured.output), expect0, "warm result after capture");
8859
8860        // (2) replay TWICE with unchanged inputs — proves the fixed-address
8861        //     reuse (both the ContiguizeOf intermediate and the Concat
8862        //     output) holds across multiple replay() calls, not just one.
8863        for i in 0..2 {
8864            captured
8865                .run
8866                .replay(&dev)
8867                .unwrap_or_else(|e| panic!("replay {i}: {e:?}"));
8868            dev.synchronize().expect("sync");
8869            assert_eq!(
8870                read(&captured.output),
8871                expect0,
8872                "replay {i} reproduces result"
8873            );
8874        }
8875
8876        // (3) overwrite x in place (a DIFFERENT value-set), replay again ->
8877        //     the new result flows from the FIXED input address, through
8878        //     the fixed ContiguizeOf intermediate, into the fixed Concat
8879        //     output — matches an independent uncaptured realize.
8880        let x2: Vec<f32> = (0..24).map(|i| 1000.0 + i as f32).collect();
8881        {
8882            let g = x_arc.read().unwrap();
8883            let BackendStorage::Cuda(c) = &g.inner else {
8884                panic!("x not cuda")
8885            };
8886            c.write_from_host(bytemuck::cast_slice(&x2))
8887                .expect("in-place H2D");
8888        }
8889        captured.run.replay(&dev).expect("replay 3");
8890        dev.synchronize().expect("sync 3");
8891        assert_eq!(
8892            read(&captured.output),
8893            realize_ref(&x2, &b_data),
8894            "replay after x update matches independent uncaptured realize"
8895        );
8896    }
8897
8898    /// CapturedRun executor build-out (Increment 4b-ζ): a same-device
8899    /// CUDA→CUDA `Op::Copy` — the shape `insert_safety_copies` emits for its
8900    /// aliasing-hazard snapshots in the real Llama decode graph — is
8901    /// capture-safe via `copy_from_cuda_wrapper`'s write-into
8902    /// `BackendStorage::Cuda` branch (`CudaStorageBytes::copy_from_device`).
8903    ///
8904    /// Graph: `x [2,4]`, `b [2,4]` --Copy{target: Cuda{0}}--> `c` (same-device
8905    /// snapshot of `x`) --Add(c, b)--> `y`. Capture, replay twice unchanged,
8906    /// then update `x` in place and replay again — every step must match an
8907    /// independent uncaptured `realize`.
8908    #[test]
8909    #[ignore = "requires a live CUDA device"]
8910    #[cfg(feature = "cuda")]
8911    fn capture_decode_same_device_copy_replays_bit_exact_cuda() {
8912        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8913        use fuel_ir::Shape;
8914        use fuel_memory::BackendStorage;
8915
8916        let Ok(dev) = CudaDevice::new(0) else {
8917            return fuel_test_support::hardware::skip(
8918                fuel_test_support::hardware::Hardware::Cuda,
8919                fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8920            );
8921        };
8922
8923        let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId) {
8924            let graph = Arc::new(RwLock::new(Graph::new()));
8925            let (x, b, y) = {
8926                let mut g = graph.write().unwrap();
8927                let x = g.push(Node {
8928                    op: Op::Const,
8929                    inputs: vec![],
8930                    shape: Shape::from_dims(&[2, 4]),
8931                    dtype: DType::F32,
8932                });
8933                let b = g.push(Node {
8934                    op: Op::Const,
8935                    inputs: vec![],
8936                    shape: Shape::from_dims(&[2, 4]),
8937                    dtype: DType::F32,
8938                });
8939                // Same-device CUDA->CUDA copy — the write-into-safe case.
8940                let c = g.push(Node {
8941                    op: Op::Copy {
8942                        target: DeviceLocation::Cuda { gpu_id: 0 },
8943                    },
8944                    inputs: vec![x],
8945                    shape: Shape::from_dims(&[2, 4]),
8946                    dtype: DType::F32,
8947                });
8948                let y = g.push(Node {
8949                    op: Op::Add,
8950                    inputs: vec![c, b],
8951                    shape: Shape::from_dims(&[2, 4]),
8952                    dtype: DType::F32,
8953                });
8954                // Copy's target_backend = SOURCE backend (x is Cuda-resident).
8955                g.set_target_backend(c, BackendId::Cuda);
8956                g.set_target_backend(y, BackendId::Cuda);
8957                (x, b, y)
8958            };
8959            (graph, x, b, y)
8960        };
8961
8962        let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8963            let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
8964            Arc::new(RwLock::new(Storage::new(
8965                BackendStorage::Cuda(cb),
8966                DType::F32,
8967            )))
8968        };
8969        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8970            let g = arc.read().unwrap();
8971            let BackendStorage::Cuda(c) = &g.inner else {
8972                panic!("not cuda")
8973            };
8974            bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
8975        };
8976
8977        let x_data: Vec<f32> = (0..8).map(|i| i as f32).collect();
8978        let b_data: Vec<f32> = (0..8).map(|i| 100.0 + i as f32).collect();
8979        let x_arc = up(&x_data);
8980        let (graph, x, b, y) = build();
8981        let mut inputs = StorageCache::new();
8982        inputs.insert(x, x_arc.clone());
8983        // `b` deliberately NOT retained by a local variable — forces
8984        // reliance on `capture_decode`'s own `retained_inputs`.
8985        inputs.insert(b, up(&b_data));
8986
8987        let captured =
8988            PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8989                .expect("capture_decode same-device copy");
8990
8991        let realize_ref = |xv: &[f32], bv: &[f32]| -> Vec<f32> {
8992            let (g, xr, br, yr) = build();
8993            let mut ins = StorageCache::new();
8994            ins.insert(xr, up(xv));
8995            ins.insert(br, up(bv));
8996            let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
8997            read(&arc)
8998        };
8999        let expect0 = realize_ref(&x_data, &b_data);
9000
9001        // (1) warm-run result sits in the captured output buffer.
9002        assert_eq!(read(&captured.output), expect0, "warm result after capture");
9003
9004        // (2) replay TWICE with unchanged inputs — the Copy's write-into
9005        //     destination buffer must stay bit-exact across replays.
9006        for i in 0..2 {
9007            captured
9008                .run
9009                .replay(&dev)
9010                .unwrap_or_else(|e| panic!("replay {i}: {e:?}"));
9011            dev.synchronize().expect("sync");
9012            assert_eq!(
9013                read(&captured.output),
9014                expect0,
9015                "replay {i} reproduces result"
9016            );
9017        }
9018
9019        // (3) overwrite x in place (a DIFFERENT value-set), replay again ->
9020        //     the Copy re-runs on the fixed input address into its fixed
9021        //     destination buffer -> matches an independent uncaptured realize.
9022        let x2: Vec<f32> = (0..8).map(|i| 1000.0 + i as f32).collect();
9023        {
9024            let g = x_arc.read().unwrap();
9025            let BackendStorage::Cuda(c) = &g.inner else {
9026                panic!("x not cuda")
9027            };
9028            c.write_from_host(bytemuck::cast_slice(&x2))
9029                .expect("in-place H2D");
9030        }
9031        captured.run.replay(&dev).expect("replay 3");
9032        dev.synchronize().expect("sync 3");
9033        assert_eq!(
9034            read(&captured.output),
9035            realize_ref(&x2, &b_data),
9036            "replay after x update matches independent uncaptured realize"
9037        );
9038    }
9039
9040    /// CapturedRun executor build-out (Increment 4b-ζ): a genuinely
9041    /// cross-device `Op::Copy` (D2H — `target: DeviceLocation::Cpu` from a
9042    /// CUDA-resident source) must still be REJECTED by `capture_decode`'s
9043    /// validation loop — a host round-trip can never be part of a CUDA
9044    /// graph capture. Fast check: no replay, just confirm the typed `Err`.
9045    #[test]
9046    #[ignore = "requires a live CUDA device"]
9047    #[cfg(feature = "cuda")]
9048    fn capture_decode_rejects_cross_device_copy_cuda() {
9049        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
9050        use fuel_ir::Shape;
9051        use fuel_memory::BackendStorage;
9052
9053        let Ok(dev) = CudaDevice::new(0) else {
9054            return fuel_test_support::hardware::skip(
9055                fuel_test_support::hardware::Hardware::Cuda,
9056                fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
9057            );
9058        };
9059
9060        let graph = Arc::new(RwLock::new(Graph::new()));
9061        let (x, y) = {
9062            let mut g = graph.write().unwrap();
9063            let x = g.push(Node {
9064                op: Op::Const,
9065                inputs: vec![],
9066                shape: Shape::from_dims(&[2, 4]),
9067                dtype: DType::F32,
9068            });
9069            // Genuinely cross-device: D2H.
9070            let y = g.push(Node {
9071                op: Op::Copy {
9072                    target: DeviceLocation::Cpu,
9073                },
9074                inputs: vec![x],
9075                shape: Shape::from_dims(&[2, 4]),
9076                dtype: DType::F32,
9077            });
9078            g.set_target_backend(y, BackendId::Cuda); // source backend = Cuda
9079            (x, y)
9080        };
9081
9082        let x_data: Vec<f32> = (0..8).map(|i| i as f32).collect();
9083        let cb =
9084            CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(&x_data)).expect("h2d");
9085        let x_arc = Arc::new(RwLock::new(Storage::new(
9086            BackendStorage::Cuda(cb),
9087            DType::F32,
9088        )));
9089        let mut inputs = StorageCache::new();
9090        inputs.insert(x, x_arc);
9091
9092        let result = PipelinedExecutor::capture_decode(graph, y, inputs, SymEnv::default());
9093        let msg = match result {
9094            Ok(_) => panic!("D2H Op::Copy must be rejected by capture_decode"),
9095            Err(e) => e.to_string(),
9096        };
9097        assert!(
9098            msg.contains("cross-device"),
9099            "error should describe the cross-device rejection: {msg}"
9100        );
9101    }
9102
9103    /// CapturedDecodeSession (Increment 4a): drive the warm→capture→replay
9104    /// orchestration over MANY tokens on a synthetic decode-shaped session —
9105    /// the real per-token decode pattern. Capture once, then per token
9106    /// H2D-update the fixed input buffer IN PLACE + replay, asserting each
9107    /// token's output is bit-exact against an independent uncaptured realize.
9108    /// A kernel-agnostic proof of the state machine + fixed-buffer mechanics
9109    /// (matmul + index_select excluded pending baracuda capture-safe kernels).
9110    #[test]
9111    #[ignore = "requires a live CUDA device"]
9112    #[cfg(feature = "cuda")]
9113    fn captured_decode_session_replays_many_tokens_cuda() {
9114        use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
9115        use fuel_graph::registry::{FusedOpParams, FusedOps};
9116        use fuel_ir::Shape;
9117        use fuel_memory::BackendStorage;
9118
9119        let Ok(dev) = CudaDevice::new(0) else {
9120            return fuel_test_support::hardware::skip(
9121                fuel_test_support::hardware::Hardware::Cuda,
9122                fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
9123            );
9124        };
9125        const H: usize = 6;
9126        let eps = 1e-5f64;
9127
9128        let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId) {
9129            let graph = Arc::new(RwLock::new(Graph::new()));
9130            let (x, y) = {
9131                let mut g = graph.write().unwrap();
9132                let mk = |g: &mut Graph, op: Op, ins: Vec<NodeId>, dims: &[usize]| {
9133                    g.push(Node {
9134                        op,
9135                        inputs: ins,
9136                        shape: Shape::from_dims(dims),
9137                        dtype: DType::F32,
9138                    })
9139                };
9140                let rms = || {
9141                    Op::Fused(
9142                        FusedOps::RMS_NORM_LAST_DIM,
9143                        FusedOpParams::RmsNormLastDim { eps },
9144                    )
9145                };
9146                let sm = || Op::Fused(FusedOps::SOFTMAX_LAST_DIM, FusedOpParams::SoftmaxLastDim);
9147                let x = mk(&mut g, Op::Const, vec![], &[1, H]);
9148                let n1 = mk(&mut g, rms(), vec![x], &[1, H]);
9149                let s = mk(&mut g, Op::Silu, vec![n1], &[1, H]);
9150                let p = mk(&mut g, sm(), vec![s], &[1, H]);
9151                let r = mk(&mut g, Op::Add, vec![p, x], &[1, H]);
9152                let n2 = mk(&mut g, rms(), vec![r], &[1, H]);
9153                let y = mk(&mut g, Op::Add, vec![n2, x], &[1, H]);
9154                for id in [n1, s, p, r, n2, y] {
9155                    g.set_target_backend(id, BackendId::Cuda);
9156                }
9157                (x, y)
9158            };
9159            (graph, x, y)
9160        };
9161        let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
9162            let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
9163            Arc::new(RwLock::new(Storage::new(
9164                BackendStorage::Cuda(cb),
9165                DType::F32,
9166            )))
9167        };
9168        let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
9169            let g = arc.read().unwrap();
9170            let BackendStorage::Cuda(c) = &g.inner else {
9171                panic!("not cuda")
9172            };
9173            bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
9174        };
9175        let realize_ref = |xv: &[f32]| -> Vec<f32> {
9176            let (g, xr, yr) = build();
9177            let mut ins = StorageCache::new();
9178            ins.insert(xr, up(xv));
9179            let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
9180            read(&arc)
9181        };
9182
9183        // 5 distinct "tokens" (distinct per-token input value-sets).
9184        let tokens: Vec<Vec<f32>> = (0..5)
9185            .map(|t| {
9186                (0..H)
9187                    .map(|c| 0.1 * (t as f32 + 1.0) * (c as f32 + 1.0))
9188                    .collect()
9189            })
9190            .collect();
9191
9192        // Capture ONCE against token 0's fixed input buffer.
9193        let x_arc = up(&tokens[0]);
9194        let (graph, x, y) = build();
9195        let mut inputs = StorageCache::new();
9196        inputs.insert(x, x_arc.clone());
9197        let session =
9198            CapturedDecodeSession::capture(Arc::clone(&graph), y, inputs, &[x], SymEnv::default())
9199                .expect("capture session");
9200
9201        // Warm result (token 0) already sits in the output buffer.
9202        assert_eq!(
9203            read(session.output()),
9204            realize_ref(&tokens[0]),
9205            "warm token 0"
9206        );
9207
9208        // The decode loop: replay every token (incl. re-replaying token 0) via
9209        // in-place H2D update of the fixed input + one graph launch.
9210        for (t, tok) in tokens.iter().enumerate() {
9211            let out = session
9212                .replay_token(&[(x, bytemuck::cast_slice(tok))])
9213                .expect("replay_token");
9214            assert_eq!(
9215                read(&out),
9216                realize_ref(tok),
9217                "replayed token {t} matches uncaptured realize"
9218            );
9219        }
9220        // Distinct tokens must give distinct outputs (live recompute, not stale).
9221        assert_ne!(
9222            realize_ref(&tokens[0]),
9223            realize_ref(&tokens[1]),
9224            "tokens must differ"
9225        );
9226    }
9227
9228    /// PR-C1 behavior contract: `realize_with_optimized_route` with an
9229    /// **empty** route (the no-pressure / no-telemetry case) is
9230    /// byte-identical to `realize_with_optimized` (arm-0). On a branchless
9231    /// graph the route has no branches to resolve, so both must produce
9232    /// the same bytes — realize is unchanged from Phase B.
9233    #[test]
9234    fn realize_with_optimized_route_empty_equals_arm0() {
9235        use crate::optimize::OptimizedGraph;
9236
9237        let build = || {
9238            let graph = Arc::new(RwLock::new(Graph::new()));
9239            let (lhs_id, rhs_id, add_id) = {
9240                let mut g = graph.write().unwrap();
9241                let lhs_id = g.push(Node {
9242                    op: Op::Const,
9243                    inputs: vec![],
9244                    shape: Shape::from_dims(&[3]),
9245                    dtype: DType::F32,
9246                });
9247                let rhs_id = g.push(Node {
9248                    op: Op::Const,
9249                    inputs: vec![],
9250                    shape: Shape::from_dims(&[3]),
9251                    dtype: DType::F32,
9252                });
9253                let add_id = g.push(Node {
9254                    op: Op::Add,
9255                    inputs: vec![lhs_id, rhs_id],
9256                    shape: Shape::from_dims(&[3]),
9257                    dtype: DType::F32,
9258                });
9259                g.set_target_backend(add_id, BackendId::Cpu);
9260                (lhs_id, rhs_id, add_id)
9261            };
9262            let mut inputs = StorageCache::new();
9263            inputs.insert(
9264                lhs_id,
9265                Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9266                    1.0_f32, 2.0, 3.0,
9267                ]))),
9268            );
9269            inputs.insert(
9270                rhs_id,
9271                Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9272                    10.0_f32, 20.0, 30.0,
9273                ]))),
9274            );
9275            (graph, add_id, inputs)
9276        };
9277
9278        let read_bytes = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
9279            let guard = arc.read().unwrap();
9280            match &guard.inner {
9281                fuel_memory::BackendStorage::Cpu(c) => {
9282                    c.as_slice::<f32>().expect("f32 cast").to_vec()
9283                }
9284                _ => panic!("expected CPU output"),
9285            }
9286        };
9287
9288        // Stamp the fixture with the LIVE topology generation rather than a
9289        // hard-coded 0. The executor's chunk-boundary check compares the plan's
9290        // stamped generation against `topology_generation()`; a real
9291        // `OptimizedGraph` carries the generation snapshotted at optimize time
9292        // (`plan.rs` `compile_plan`, which reads it *after* the bindings table
9293        // is warm), so it already reflects every registered backend. Force
9294        // `global_bindings()` init here so any feature-gated auto-registration
9295        // has run and settled the counter before we read it — otherwise a stale
9296        // read would be bumped out from under us the first time realize warms
9297        // the bindings. Under default features this is 0; under
9298        // `--features vulkan`, `register_derived_gpu_caps` bumps it to 1.
9299        drop(global_bindings());
9300        let live_gen = crate::dispatch::topology_generation();
9301
9302        // Arm-0 path.
9303        let (g_a, add_a, in_a) = build();
9304        let opt_a = OptimizedGraph {
9305            roots: vec![add_a],
9306            generation: live_gen,
9307            placements: None,
9308        };
9309        let (arm0_arc, _) = PipelinedExecutor::realize_with_optimized(g_a, add_a, in_a, &opt_a)
9310            .expect("arm-0 realize");
9311        let arm0 = read_bytes(&arm0_arc);
9312
9313        // Empty-route path — must match arm-0 exactly.
9314        let (g_r, add_r, in_r) = build();
9315        let opt_r = OptimizedGraph {
9316            roots: vec![add_r],
9317            generation: live_gen,
9318            placements: None,
9319        };
9320        let empty_route = PickedRoute::new();
9321        let (route_arc, _) =
9322            PipelinedExecutor::realize_with_optimized_route(g_r, add_r, in_r, &opt_r, &empty_route)
9323                .expect("empty-route realize");
9324        let routed = read_bytes(&route_arc);
9325
9326        assert_eq!(arm0, vec![11.0, 22.0, 33.0]);
9327        assert_eq!(
9328            routed, arm0,
9329            "an empty route realizes byte-identically to arm-0 (Phase B contract)",
9330        );
9331    }
9332
9333    /// `order_for` fix track (`docs/design/incremental-consumer-index.md`
9334    /// §5): [`PipelinedExecutor::realize_with_optimized_picking_cached_env`]
9335    /// over a shared `order_cache` must (a) realize byte-identically to the
9336    /// uncached arm-0 path, on both the cold (empty cache) and warm
9337    /// (populated cache) call, and (b) actually populate the cache on the
9338    /// first call rather than silently never using it.
9339    #[test]
9340    fn realize_with_optimized_picking_cached_env_matches_arm0_cold_and_warm() {
9341        use crate::optimize::OptimizedGraph;
9342        use std::sync::OnceLock;
9343
9344        let build = || {
9345            let graph = Arc::new(RwLock::new(Graph::new()));
9346            let (lhs_id, rhs_id, add_id) = {
9347                let mut g = graph.write().unwrap();
9348                let lhs_id = g.push(Node {
9349                    op: Op::Const,
9350                    inputs: vec![],
9351                    shape: Shape::from_dims(&[3]),
9352                    dtype: DType::F32,
9353                });
9354                let rhs_id = g.push(Node {
9355                    op: Op::Const,
9356                    inputs: vec![],
9357                    shape: Shape::from_dims(&[3]),
9358                    dtype: DType::F32,
9359                });
9360                let add_id = g.push(Node {
9361                    op: Op::Add,
9362                    inputs: vec![lhs_id, rhs_id],
9363                    shape: Shape::from_dims(&[3]),
9364                    dtype: DType::F32,
9365                });
9366                g.set_target_backend(add_id, BackendId::Cpu);
9367                (lhs_id, rhs_id, add_id)
9368            };
9369            let mut inputs = StorageCache::new();
9370            inputs.insert(
9371                lhs_id,
9372                Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9373                    1.0_f32, 2.0, 3.0,
9374                ]))),
9375            );
9376            inputs.insert(
9377                rhs_id,
9378                Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9379                    10.0_f32, 20.0, 30.0,
9380                ]))),
9381            );
9382            (graph, add_id, inputs)
9383        };
9384        let read_bytes = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
9385            let guard = arc.read().unwrap();
9386            match &guard.inner {
9387                fuel_memory::BackendStorage::Cpu(c) => {
9388                    c.as_slice::<f32>().expect("f32 cast").to_vec()
9389                }
9390                _ => panic!("expected CPU output"),
9391            }
9392        };
9393
9394        drop(global_bindings());
9395        let live_gen = crate::dispatch::topology_generation();
9396
9397        let (g, add, inputs) = build();
9398        let optimized = OptimizedGraph {
9399            roots: vec![add],
9400            generation: live_gen,
9401            placements: None,
9402        };
9403        let order_cache: OnceLock<Vec<NodeId>> = OnceLock::new();
9404        assert!(order_cache.get().is_none(), "sanity: cache starts empty");
9405
9406        // Cold call: cache miss, must compute AND populate.
9407        let (cold_arc, _) = PipelinedExecutor::realize_with_optimized_picking_cached_env(
9408            g.clone(),
9409            add,
9410            inputs,
9411            &optimized,
9412            &order_cache,
9413            None,
9414            None,
9415            SymEnv::default(),
9416        )
9417        .expect("cold cached realize");
9418        assert_eq!(read_bytes(&cold_arc), vec![11.0, 22.0, 33.0]);
9419        assert!(
9420            order_cache.get().is_some(),
9421            "the cold call must populate order_cache, not leave it empty"
9422        );
9423        let cached_order_after_cold = order_cache.get().unwrap().clone();
9424
9425        // Warm call: cache hit. Fresh inputs (same graph, new values) so the
9426        // realized OUTPUT differs from the cold call while the dispatch
9427        // ORDER (what's cached) must not change.
9428        let mut warm_inputs = StorageCache::new();
9429        warm_inputs.insert(
9430            g.read().unwrap().node(add).inputs[0],
9431            Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9432                100.0_f32, 200.0, 300.0,
9433            ]))),
9434        );
9435        warm_inputs.insert(
9436            g.read().unwrap().node(add).inputs[1],
9437            Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9438                1.0_f32, 2.0, 3.0,
9439            ]))),
9440        );
9441        let (warm_arc, _) = PipelinedExecutor::realize_with_optimized_picking_cached_env(
9442            g,
9443            add,
9444            warm_inputs,
9445            &optimized,
9446            &order_cache,
9447            None,
9448            None,
9449            SymEnv::default(),
9450        )
9451        .expect("warm cached realize");
9452        assert_eq!(
9453            read_bytes(&warm_arc),
9454            vec![101.0, 202.0, 303.0],
9455            "warm call must realize its OWN rebound inputs, not replay stale output",
9456        );
9457        assert_eq!(
9458            order_cache.get().unwrap(),
9459            &cached_order_after_cold,
9460            "the warm call must reuse the SAME cached order, not recompute a different one",
9461        );
9462    }
9463
9464    /// `order_for` fix track (`docs/design/incremental-consumer-index.md`
9465    /// §5), PM review question: does `OrderSource::Cached` weaken the
9466    /// existing `TopologyChanged` chunk-boundary check? No — proven at the
9467    /// unit level (not a live topology-bump integration test, deliberately:
9468    /// `bump_topology_generation()` mutates a process-wide atomic shared by
9469    /// every concurrently-running test in this binary, including the cold/
9470    /// warm cache test above, which reads it into a local at the start of
9471    /// ITS OWN body and would flake if another test's bump landed mid-call;
9472    /// introducing that cross-test hazard isn't worth it when the two arms'
9473    /// `generation_for` expressions are provably identical). `generation_for`
9474    /// is private to this module, so it's tested directly here: given the
9475    /// SAME `&OptimizedGraph`, `Cached` and `Optimized` must report the
9476    /// IDENTICAL generation — the chunk-boundary check that compares this
9477    /// value against the LIVE `topology_generation()` fires exactly the
9478    /// same way for both, so caching cannot silently skip or weaken it.
9479    #[test]
9480    fn cached_generation_for_matches_optimized_generation_for() {
9481        let optimized = OptimizedGraph {
9482            roots: vec![NodeId(0)],
9483            generation: 42,
9484            placements: None,
9485        };
9486        let order_cache: std::sync::OnceLock<Vec<NodeId>> = std::sync::OnceLock::new();
9487
9488        let optimized_gen = generation_for(&OrderSource::Optimized {
9489            optimized: &optimized,
9490            route: None,
9491        });
9492        let cached_gen = generation_for(&OrderSource::Cached {
9493            optimized: &optimized,
9494            order_cache: &order_cache,
9495        });
9496        assert_eq!(
9497            optimized_gen,
9498            Some(42),
9499            "sanity: Optimized reports the OptimizedGraph's own generation",
9500        );
9501        assert_eq!(
9502            cached_gen, optimized_gen,
9503            "Cached must report the SAME generation as Optimized for the same \
9504             OptimizedGraph -- the TopologyChanged check cannot tell the two apart, \
9505             by construction, so caching cannot silently weaken it",
9506        );
9507    }
9508
9509    /// **PR-C3 reorder-invariance gate (CPU, runnable — ORDERING level).**
9510    /// The auto-overlap reorder is a HINT: it changes only *which order*
9511    /// independent device runs dispatch in, never *which* nodes run nor the
9512    /// result. The byte-identical-OUTPUT proof is necessarily LIVE (the
9513    /// reorder fires only on a ≥2-device graph, which needs ≥2 real backends
9514    /// to realize — see `cuda_vulkan_multidevice_realize_live`'s
9515    /// `[22,86,192,340]` oracle, which now flows through this reorder, plus
9516    /// the dual-GPU `cuda_vulkan_overlap_bench_live` arbitrary-graph
9517    /// auto-overlap gate). THIS test proves the ordering-level invariant on a
9518    /// CPU-only path: the C3 reorder of a two-device graph is a valid
9519    /// topological **permutation** of the un-reordered topo order — same node
9520    /// multiset, every node still after its inputs — that genuinely DIFFERS
9521    /// from the un-reordered order (so the reorder fired). Because the
9522    /// executor caches each node's result and every op is deterministic,
9523    /// realizing any valid topological order yields identical bytes; this
9524    /// test guards the precondition (a valid permutation) that makes that so.
9525    #[test]
9526    fn c3_reorder_is_a_valid_topo_permutation() {
9527        // Two INDEPENDENT device sub-chains over distinct consts reconverging
9528        // at a final CUDA add, the Vulkan root bridged to CUDA by an explicit
9529        // Op::Copy (a residency seam = its own run). Reconverge inputs in the
9530        // "wrong" (cuda-first) order, the order the un-reordered topo DFS pops
9531        // CUDA-last — exactly the arbitrary-graph shape the auto-overlap pass
9532        // must fix. The chains are UNARY (Relu) so each stays a single
9533        // contiguous run (no const-re-read fan-in fragmenting them), exactly
9534        // like the live benchmark's `relu(mul·add)` chains; the CUDA chain is
9535        // HEAVIER (4 vs 2) so the critical-path reorder emits it first.
9536        // (Stamps only; not realized here — the ordering proof, CPU-runnable.)
9537        let n = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9538            g.push(Node {
9539                op,
9540                inputs,
9541                shape: Shape::from_dims(&[3]),
9542                dtype: DType::F32,
9543            })
9544        };
9545        let mut g = Graph::new();
9546        let ca = n(&mut g, Op::Const, vec![]);
9547        let cb = n(&mut g, Op::Const, vec![]);
9548        // CUDA chain — HEAVIER (4 unary stages, one contiguous run).
9549        let mut cprev = ca;
9550        for _ in 0..4 {
9551            cprev = n(&mut g, Op::Relu, vec![cprev]);
9552            g.set_target_backend(cprev, BackendId::Cuda);
9553        }
9554        let c2 = cprev; // CUDA chunk root
9555        // Vulkan chain — lighter (2 unary stages).
9556        let mut vprev = cb;
9557        for _ in 0..2 {
9558            vprev = n(&mut g, Op::Relu, vec![vprev]);
9559            g.set_target_backend(vprev, BackendId::Vulkan);
9560        }
9561        let v2 = vprev; // Vulkan chunk root
9562        // Vulkan -> CUDA copy (residency seam), then reconverge on CUDA with
9563        // inputs [cuda_root, copy] (cuda-first).
9564        let copy = g.push(Node {
9565            op: Op::Copy {
9566                target: DeviceLocation::Cuda { gpu_id: 0 },
9567            },
9568            inputs: vec![v2],
9569            shape: Shape::from_dims(&[3]),
9570            dtype: DType::F32,
9571        });
9572        g.set_target_backend(copy, BackendId::Cuda);
9573        let out = n(&mut g, Op::Add, vec![c2, copy]);
9574        g.set_target_backend(out, BackendId::Cuda);
9575
9576        let unreordered = execution_plan(&g, &[out]);
9577        let reordered = fuel_graph::lower_runs_arm0(&g, &[out]);
9578
9579        // (1) The reorder FIRED — the order changed (else the test is
9580        //     vacuous + auto-overlap did nothing on an arbitrary graph).
9581        assert_ne!(
9582            reordered, unreordered,
9583            "the C3 reorder must change the order on a two-device graph; \
9584             reordered={reordered:?} unreordered={unreordered:?}",
9585        );
9586
9587        // (2) Same node multiset — a permutation, no add/drop.
9588        let mut a = unreordered.clone();
9589        let mut b = reordered.clone();
9590        a.sort();
9591        b.sort();
9592        assert_eq!(a, b, "the reorder is a permutation of the same node set");
9593
9594        // (3) The reordered order is a VALID topological order: every node
9595        //     appears after all of its inputs that are in the order.
9596        let pos: std::collections::HashMap<NodeId, usize> = reordered
9597            .iter()
9598            .enumerate()
9599            .map(|(i, &node)| (node, i))
9600            .collect();
9601        for (i, &node) in reordered.iter().enumerate() {
9602            for &inp in &g.node(node).inputs {
9603                if let Some(&j) = pos.get(&inp) {
9604                    assert!(
9605                        j < i,
9606                        "input {inp:?} of {node:?} must precede it in the reordered \
9607                         order; reordered={reordered:?}",
9608                    );
9609                }
9610            }
9611        }
9612
9613        // (4) The overlap-relevant property: BOTH device producer chunks
9614        //     (the CUDA root c2, the Vulkan root v2) are emitted BEFORE the
9615        //     host-blocking cross-device copy — the A4b overlap topology the
9616        //     un-reordered cuda-last DFS violates — AND the HEAVIER CUDA chunk
9617        //     is emitted first (critical-path: the auto-submit device runs
9618        //     earliest, the live-overlap regression guard).
9619        let p = |node: NodeId| pos[&node];
9620        assert!(p(c2) < p(copy), "CUDA chunk before the cross-device copy");
9621        assert!(p(v2) < p(copy), "Vulkan chunk before the cross-device copy");
9622        assert!(
9623            p(c2) < p(v2),
9624            "the heavier CUDA chunk is emitted before the lighter Vulkan chunk \
9625             (critical-path heavy-first); reordered={reordered:?}",
9626        );
9627    }
9628
9629    /// **C3 follow-on — the OPERAND-ORDER-INVARIANCE gate (CPU, runnable —
9630    /// the reliable operand-independence proof).** The residual the live
9631    /// `cuda_vulkan_overlap_bench_live` benchmark targets is whether the
9632    /// reconverge OPERAND order (`xc.add(&xv)` vs `xv.add(&xc)`) changes the
9633    /// wall-clock overlap. Wall-clock is thermally noisy on a heterogeneous
9634    /// dev box; the DETERMINISTIC root guarantee is this: the C3-reordered
9635    /// dispatch order the executor consumes is **byte-identical for both
9636    /// operand orders**. If the lowered order is the same, the executor
9637    /// processes the SAME WorkItem stream with the SAME cross-device
9638    /// submit/wait timing, so overlap cannot depend on the operand order.
9639    ///
9640    /// This builds the SAME two-device reconverge as
9641    /// `c3_reorder_is_a_valid_topo_permutation` but TWICE — once cuda-first
9642    /// (`add(cuda_root, copy)`), once vulkan-first (`add(copy, cuda_root)`)
9643    /// — over structurally identical graphs (same node ids, only the
9644    /// reconverge's two inputs swapped) and asserts `lower_runs_arm0` emits
9645    /// the identical op/device run sequence. The live executor trace confirms
9646    /// the same: both operand orders produce the identical WorkItem stream
9647    /// (the eager-submit/drain/wait sequence at every cross-device copy is the
9648    /// same, and the CUDA chunk is enqueued first in both).
9649    #[test]
9650    fn c3_reorder_is_operand_order_invariant() {
9651        // Build the two-device reconverge with the reconverge inputs in the
9652        // given order; return (graph, out) over a FRESH graph so node ids line
9653        // up 1:1 between the two builds (only the add's input order differs).
9654        let build = |cuda_first: bool| -> (Graph, NodeId) {
9655            let n = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9656                g.push(Node {
9657                    op,
9658                    inputs,
9659                    shape: Shape::from_dims(&[3]),
9660                    dtype: DType::F32,
9661                })
9662            };
9663            let mut g = Graph::new();
9664            let ca = n(&mut g, Op::Const, vec![]);
9665            let cb = n(&mut g, Op::Const, vec![]);
9666            let mut cprev = ca;
9667            for _ in 0..4 {
9668                cprev = n(&mut g, Op::Relu, vec![cprev]);
9669                g.set_target_backend(cprev, BackendId::Cuda);
9670            }
9671            let c2 = cprev;
9672            let mut vprev = cb;
9673            for _ in 0..2 {
9674                vprev = n(&mut g, Op::Relu, vec![vprev]);
9675                g.set_target_backend(vprev, BackendId::Vulkan);
9676            }
9677            let v2 = vprev;
9678            let copy = g.push(Node {
9679                op: Op::Copy {
9680                    target: DeviceLocation::Cuda { gpu_id: 0 },
9681                },
9682                inputs: vec![v2],
9683                shape: Shape::from_dims(&[3]),
9684                dtype: DType::F32,
9685            });
9686            g.set_target_backend(copy, BackendId::Cuda);
9687            // The ONE knob: the reconverge's two inputs, swapped.
9688            let inputs = if cuda_first {
9689                vec![c2, copy]
9690            } else {
9691                vec![copy, c2]
9692            };
9693            let out = n(&mut g, Op::Add, inputs);
9694            g.set_target_backend(out, BackendId::Cuda);
9695            (g, out)
9696        };
9697
9698        let (g_cf, out_cf) = build(true);
9699        let (g_vf, out_vf) = build(false);
9700
9701        // Lower both via the production path (dispatch_order → lower_runs_arm0
9702        // → device_alternating_order, the C3 reorder).
9703        let order_cf = fuel_graph::lower_runs_arm0(&g_cf, &[out_cf]);
9704        let order_vf = fuel_graph::lower_runs_arm0(&g_vf, &[out_vf]);
9705
9706        // The decisive guarantee: the lowered dispatch order — as an
9707        // (op, device) sequence — is IDENTICAL for both operand orders. (Node
9708        // ids can differ if the build assigned them differently, but here the
9709        // builds are structurally identical so even the ids match; compare the
9710        // op/device sequence to be robust + descriptive on failure.)
9711        let tag = |g: &Graph, ids: &[NodeId]| -> Vec<(String, Option<BackendId>)> {
9712            ids.iter()
9713                .map(|&id| (format!("{:?}", g.node(id).op), g.target_backend(id)))
9714                .collect()
9715        };
9716        let seq_cf = tag(&g_cf, &order_cf);
9717        let seq_vf = tag(&g_vf, &order_vf);
9718        assert_eq!(
9719            seq_cf, seq_vf,
9720            "the C3-reordered dispatch order MUST be identical for both \
9721             reconverge operand orders (operand-order-invariance — the \
9722             executor then sees the same WorkItem stream regardless of \
9723             operand order, so overlap is operand-insensitive).\n\
9724             cuda-first = {seq_cf:?}\nvulkan-first = {seq_vf:?}",
9725        );
9726    }
9727
9728    /// A selector that always takes the LAST arm (arm 1) so a non-arm-0
9729    /// pick is observable (a `WinnerSelector` picks arm-0 ⇒ empty route).
9730    #[cfg(test)]
9731    #[derive(Debug)]
9732    struct PickLast;
9733    #[cfg(test)]
9734    impl RuntimeSelector for PickLast {
9735        fn select<'a>(
9736            &self,
9737            set: &'a crate::ranker::AlternativeSet,
9738        ) -> Option<&'a crate::ranker::Candidate> {
9739            set.alternatives().last()
9740        }
9741    }
9742
9743    /// Build the 2-arm diamond (mirrors `route_picker::tests::diamond`):
9744    /// arm-0 on CUDA, arm-1 on CPU; the arm exits carry their
9745    /// `target_backend` so the picker reads each arm's placement. Returns
9746    /// `(graph_arc, branch, post)`.
9747    #[cfg(test)]
9748    fn picking_diamond() -> (Arc<RwLock<Graph>>, NodeId, NodeId) {
9749        let node = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9750            g.push(Node {
9751                op,
9752                inputs,
9753                shape: Shape::from_dims(&[2]),
9754                dtype: DType::F32,
9755            })
9756        };
9757        let mut g = Graph::new();
9758        let pre = node(&mut g, Op::Const, vec![]);
9759        let diverge = node(&mut g, Op::Relu, vec![pre]);
9760        let arm0 = node(&mut g, Op::Silu, vec![diverge]);
9761        let arm1 = node(&mut g, Op::GeluTanh, vec![diverge]);
9762        g.set_target_backend(arm0, BackendId::Cuda);
9763        g.set_target_backend(arm1, BackendId::Cpu);
9764        let reconverge = node(&mut g, Op::Relu, vec![arm0]);
9765        let mut b = g.open_branch(diverge);
9766        b.add_arm(arm0);
9767        b.add_arm(arm1);
9768        let branch = b
9769            .finalize_branches(&mut g, reconverge)
9770            .expect("well-formed 2-arm branch")
9771            .expect("2 arms survive");
9772        let post = node(&mut g, Op::Tanh, vec![reconverge]);
9773        (Arc::new(RwLock::new(g)), branch, post)
9774    }
9775
9776    /// Step E Phase C, PR C1: the streaming picking entry's gate.
9777    /// `streaming_pick_for` (the streaming successor of the old one-shot
9778    /// `pick_route_for`) must (1) return `Some(pick)` when a selector + a
9779    /// branch are present (⇒ the `OrderSource::Streaming` walk), (2) return
9780    /// `None` with no selector (the `runtime_selector_disabled` opt-out ⇒
9781    /// arm-0), and (3) short-circuit to `None` on a branchless graph (the
9782    /// fast-path ⇒ the untouched `dispatch_order` lowering).
9783    #[test]
9784    fn streaming_pick_for_gates() {
9785        let (graph, _branch, post) = picking_diamond();
9786        let _ = post;
9787
9788        // (1) selector + branch ⇒ Some(pick).
9789        let sel: Arc<dyn RuntimeSelector> = Arc::new(PickLast);
9790        assert!(
9791            PipelinedExecutor::streaming_pick_for(&graph, Some(sel.clone()), None)
9792                .expect("ok")
9793                .is_some(),
9794            "selector + branch ⇒ a streaming pick",
9795        );
9796
9797        // (2) no selector ⇒ None (arm-0 lowering).
9798        assert!(
9799            PipelinedExecutor::streaming_pick_for(&graph, None, None)
9800                .expect("ok")
9801                .is_none(),
9802            "no selector ⇒ no streaming pick (arm-0 lowering)",
9803        );
9804
9805        // (3) branchless graph + selector ⇒ None (the fast-path).
9806        let node = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9807            g.push(Node {
9808                op,
9809                inputs,
9810                shape: Shape::from_dims(&[2]),
9811                dtype: DType::F32,
9812            })
9813        };
9814        let bl_graph = {
9815            let mut g = Graph::new();
9816            let c1 = node(&mut g, Op::Const, vec![]);
9817            let c2 = node(&mut g, Op::Const, vec![]);
9818            let add = node(&mut g, Op::Add, vec![c1, c2]);
9819            g.set_target_backend(add, BackendId::Cpu);
9820            Arc::new(RwLock::new(g))
9821        };
9822        assert!(
9823            PipelinedExecutor::streaming_pick_for(&bl_graph, Some(sel), None)
9824                .expect("ok")
9825                .is_none(),
9826            "branchless graph ⇒ no branches ⇒ no streaming pick (fast-path)",
9827        );
9828    }
9829
9830    /// **C1 byte-identity gate (executor layer).** The STREAMED dispatch
9831    /// order — produced by the compiler thread's lazy per-branch resolution
9832    /// (`walk_picked_route_streaming` over `resolve_branch`, the same path
9833    /// `order_for(OrderSource::Streaming)` collects) — equals the ONE-SHOT
9834    /// order (`lower_picked_route` over the route the eager `pick_route`
9835    /// resolves) on the branched diamond, for both arm-0 (WinnerSelector)
9836    /// and arm-1 (PickLast). This is the proof that C1 changes *when* a
9837    /// branch resolves, not *which* arm — the same selector, the same
9838    /// `resolve_branch`, the same emitted order.
9839    #[test]
9840    fn streamed_order_equals_one_shot() {
9841        let (graph, _branch, post) = picking_diamond();
9842        let g = graph.read().unwrap();
9843        let bindings = global_bindings();
9844
9845        for sel in [
9846            Arc::new(crate::ranker::WinnerSelector) as Arc<dyn RuntimeSelector>,
9847            Arc::new(PickLast) as Arc<dyn RuntimeSelector>,
9848        ] {
9849            // One-shot: resolve the whole route, then lower.
9850            let one_shot_route = pick_route(&g, &[post], &bindings, sel.as_ref(), None);
9851            let one_shot = fuel_graph::lower_picked_route(&g, &[post], &one_shot_route);
9852
9853            // Streamed: resolve each branch lazily at the frontier via the
9854            // SAME `resolve_branch` the compiler thread uses.
9855            let streamed = fuel_graph::lower_picked_route_streaming(&g, &[post], |branch| {
9856                resolve_branch(&g, branch, &bindings, sel.as_ref())
9857            });
9858
9859            assert_eq!(
9860                streamed, one_shot,
9861                "streamed order must equal one-shot for selector {sel:?}",
9862            );
9863        }
9864    }
9865
9866    /// Realizing a node whose target_backend isn't set surfaces a
9867    /// typed error (no panic).
9868    #[test]
9869    fn pipelined_errors_on_unset_target_backend() {
9870        let graph = Arc::new(RwLock::new(Graph::new()));
9871        let (lhs_id, rhs_id, add_id) = {
9872            let mut g = graph.write().unwrap();
9873            let lhs = g.push(Node {
9874                op: Op::Const,
9875                inputs: vec![],
9876                shape: Shape::from_dims(&[2]),
9877                dtype: DType::F32,
9878            });
9879            let rhs = g.push(Node {
9880                op: Op::Const,
9881                inputs: vec![],
9882                shape: Shape::from_dims(&[2]),
9883                dtype: DType::F32,
9884            });
9885            // Deliberately do NOT call set_target_backend on the Add.
9886            let add = g.push(Node {
9887                op: Op::Add,
9888                inputs: vec![lhs, rhs],
9889                shape: Shape::from_dims(&[2]),
9890                dtype: DType::F32,
9891            });
9892            (lhs, rhs, add)
9893        };
9894
9895        let mut inputs = StorageCache::new();
9896        inputs.insert(
9897            lhs_id,
9898            Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]))),
9899        );
9900        inputs.insert(
9901            rhs_id,
9902            Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[3.0_f32, 4.0]))),
9903        );
9904
9905        let result = PipelinedExecutor::realize(graph, add_id, inputs);
9906        assert!(result.is_err(), "missing target_backend must error");
9907        let msg = result.unwrap_err().to_string();
9908        assert!(
9909            msg.contains("target_backend"),
9910            "error names the unmet precondition: {msg}"
9911        );
9912    }
9913
9914    /// Realizing a Const-only graph adopts the pre-seeded input
9915    /// without calling any kernel.
9916    #[test]
9917    fn pipelined_realize_const_only() {
9918        let storage = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0, 7.0]);
9919
9920        let graph = Arc::new(RwLock::new(Graph::new()));
9921        let const_id = {
9922            let mut g = graph.write().unwrap();
9923            g.push(Node {
9924                op: Op::Const,
9925                inputs: vec![],
9926                shape: Shape::from_dims(&[3]),
9927                dtype: DType::F32,
9928            })
9929        };
9930
9931        let mut inputs = StorageCache::new();
9932        inputs.insert(const_id, Arc::new(RwLock::new(storage)));
9933
9934        let (result_arc, _) =
9935            PipelinedExecutor::realize(graph, const_id, inputs).expect("realize const");
9936        let guard = result_arc.read().unwrap();
9937        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
9938            let typed: &[f32] = c.as_slice().unwrap();
9939            assert_eq!(typed, &[5.0, 6.0, 7.0]);
9940        }
9941    }
9942
9943    /// Missing input-cache entry for a Const surfaces a typed error.
9944    #[test]
9945    fn pipelined_errors_on_missing_const_input() {
9946        let graph = Arc::new(RwLock::new(Graph::new()));
9947        let const_id = {
9948            let mut g = graph.write().unwrap();
9949            g.push(Node {
9950                op: Op::Const,
9951                inputs: vec![],
9952                shape: Shape::from_dims(&[2]),
9953                dtype: DType::F32,
9954            })
9955        };
9956        // Deliberately empty input cache.
9957        let inputs = StorageCache::new();
9958        let result = PipelinedExecutor::realize(graph, const_id, inputs);
9959        assert!(result.is_err());
9960    }
9961
9962    /// E2E: 2-node graph (Const + Relu) — exercises the unary
9963    /// dispatch wrapper + kernel through the pipelined executor.
9964    #[test]
9965    fn pipelined_realize_const_relu() {
9966        let storage = fuel_memory::from_slice_cpu(&[-1.0_f32, 0.0, 0.5, -3.5, 7.25]);
9967        let graph = Arc::new(RwLock::new(Graph::new()));
9968        let (in_id, relu_id) = {
9969            let mut g = graph.write().unwrap();
9970            let in_id = g.push(Node {
9971                op: Op::Const,
9972                inputs: vec![],
9973                shape: Shape::from_dims(&[5]),
9974                dtype: DType::F32,
9975            });
9976            let relu_id = g.push(Node {
9977                op: Op::Relu,
9978                inputs: vec![in_id],
9979                shape: Shape::from_dims(&[5]),
9980                dtype: DType::F32,
9981            });
9982            g.set_target_backend(relu_id, BackendId::Cpu);
9983            (in_id, relu_id)
9984        };
9985        let mut inputs = StorageCache::new();
9986        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
9987
9988        let (result_arc, _) = PipelinedExecutor::realize(graph, relu_id, inputs).expect("realize");
9989        let guard = result_arc.read().unwrap();
9990        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
9991            let typed: &[f32] = c.as_slice().expect("f32 cast");
9992            assert_eq!(typed, &[0.0, 0.0, 0.5, 0.0, 7.25]);
9993        }
9994    }
9995
9996    /// E2E: Const + Const + Sub + Mul + Div — exercises three more
9997    /// of the freshly-migrated binary kernels in one graph. Verifies
9998    /// that intermediates flow through the cache as expected.
9999    #[test]
10000    fn pipelined_realize_chained_binary_ops() {
10001        let a_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
10002        let b_storage = fuel_memory::from_slice_cpu(&[3.0_f32, 5.0]);
10003        let c_storage = fuel_memory::from_slice_cpu(&[2.0_f32, 4.0]);
10004
10005        let graph = Arc::new(RwLock::new(Graph::new()));
10006        let (a_id, b_id, c_id, sub_id, mul_id, div_id) = {
10007            let mut g = graph.write().unwrap();
10008            let a = g.push(Node {
10009                op: Op::Const,
10010                inputs: vec![],
10011                shape: Shape::from_dims(&[2]),
10012                dtype: DType::F32,
10013            });
10014            let b = g.push(Node {
10015                op: Op::Const,
10016                inputs: vec![],
10017                shape: Shape::from_dims(&[2]),
10018                dtype: DType::F32,
10019            });
10020            let c = g.push(Node {
10021                op: Op::Const,
10022                inputs: vec![],
10023                shape: Shape::from_dims(&[2]),
10024                dtype: DType::F32,
10025            });
10026            // (a - b)         = [7, 15]
10027            let sub = g.push(Node {
10028                op: Op::Sub,
10029                inputs: vec![a, b],
10030                shape: Shape::from_dims(&[2]),
10031                dtype: DType::F32,
10032            });
10033            // (a - b) * c     = [14, 60]
10034            let mul = g.push(Node {
10035                op: Op::Mul,
10036                inputs: vec![sub, c],
10037                shape: Shape::from_dims(&[2]),
10038                dtype: DType::F32,
10039            });
10040            // ((a-b)*c) / b   = [14/3, 12]
10041            let div = g.push(Node {
10042                op: Op::Div,
10043                inputs: vec![mul, b],
10044                shape: Shape::from_dims(&[2]),
10045                dtype: DType::F32,
10046            });
10047            g.set_target_backend(sub, BackendId::Cpu);
10048            g.set_target_backend(mul, BackendId::Cpu);
10049            g.set_target_backend(div, BackendId::Cpu);
10050            (a, b, c, sub, mul, div)
10051        };
10052
10053        let _ = (sub_id, mul_id);
10054        let mut inputs = StorageCache::new();
10055        inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
10056        inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
10057        inputs.insert(c_id, Arc::new(RwLock::new(c_storage)));
10058
10059        let (result_arc, _) = PipelinedExecutor::realize(graph, div_id, inputs).expect("realize");
10060        let guard = result_arc.read().unwrap();
10061        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
10062            let typed: &[f32] = c.as_slice().unwrap();
10063            assert!((typed[0] - (14.0_f32 / 3.0)).abs() < 1e-6);
10064            assert!((typed[1] - 12.0).abs() < 1e-6);
10065        }
10066    }
10067
10068    /// E2E: Const + Transpose — verifies metadata-only view ops
10069    /// share the input's Storage Arc and produce a strided Layout.
10070    /// Stage 3 of Layout-on-Node.
10071    #[test]
10072    fn pipelined_realize_transpose_is_metadata_only() {
10073        // shape [2, 3]; transpose → shape [3, 2], strided
10074        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
10075        let graph = Arc::new(RwLock::new(Graph::new()));
10076        let (in_id, t_id) = {
10077            let mut g = graph.write().unwrap();
10078            let in_id = g.push(Node {
10079                op: Op::Const,
10080                inputs: vec![],
10081                shape: Shape::from_dims(&[2, 3]),
10082                dtype: DType::F32,
10083            });
10084            let t_id = g.push(Node {
10085                op: Op::Transpose,
10086                inputs: vec![in_id],
10087                shape: Shape::from_dims(&[3, 2]),
10088                dtype: DType::F32,
10089            });
10090            // Note: NO set_target_backend — transpose is metadata-only
10091            // and doesn't run on a backend.
10092            (in_id, t_id)
10093        };
10094        let mut inputs = StorageCache::new();
10095        let in_arc = Arc::new(RwLock::new(storage));
10096        inputs.insert(in_id, Arc::clone(&in_arc));
10097
10098        let (result_arc, result_layout) =
10099            PipelinedExecutor::realize(graph, t_id, inputs).expect("realize");
10100
10101        // The output Storage Arc is the SAME Arc as the input —
10102        // metadata-only adoption shares bytes.
10103        assert!(
10104            Arc::ptr_eq(&result_arc, &in_arc),
10105            "transpose must share input bytes"
10106        );
10107
10108        // The output Layout is the transposed view.
10109        assert_eq!(result_layout.shape().dims(), &[3, 2]);
10110        assert_eq!(result_layout.stride(), &[1, 3]);
10111        assert!(!result_layout.is_contiguous());
10112    }
10113
10114    /// E2E: Const + Flip(dim=0) — verifies Op::Flip is metadata-only.
10115    /// The output Storage Arc must be the SAME Arc (no copy);
10116    /// the output Layout has a negated stride at dim 0 and a
10117    /// shifted start_offset.
10118    #[test]
10119    fn pipelined_realize_flip_is_metadata_only() {
10120        // shape [3, 4]; flip dim 0 → shape [3, 4], stride [-4, 1], offset 8.
10121        let data: Vec<f32> = (1..=12).map(|x| x as f32).collect();
10122        let storage = fuel_memory::from_slice_cpu(&data);
10123        let graph = Arc::new(RwLock::new(Graph::new()));
10124        let (in_id, f_id) = {
10125            let mut g = graph.write().unwrap();
10126            let in_id = g.push(Node {
10127                op: Op::Const,
10128                inputs: vec![],
10129                shape: Shape::from_dims(&[3, 4]),
10130                dtype: DType::F32,
10131            });
10132            let f_id = g.push(Node {
10133                op: Op::Flip { dim: 0 },
10134                inputs: vec![in_id],
10135                shape: Shape::from_dims(&[3, 4]),
10136                dtype: DType::F32,
10137            });
10138            (in_id, f_id)
10139        };
10140        let mut inputs = StorageCache::new();
10141        let in_arc = Arc::new(RwLock::new(storage));
10142        inputs.insert(in_id, Arc::clone(&in_arc));
10143
10144        let (result_arc, result_layout) =
10145            PipelinedExecutor::realize(graph, f_id, inputs).expect("realize");
10146
10147        assert!(
10148            Arc::ptr_eq(&result_arc, &in_arc),
10149            "flip must share input bytes"
10150        );
10151        assert_eq!(result_layout.shape().dims(), &[3, 4]);
10152        assert_eq!(result_layout.stride(), &[-4_isize, 1]);
10153        assert_eq!(result_layout.start_offset(), 8);
10154        assert!(!result_layout.is_contiguous());
10155    }
10156
10157    /// E2E: Const + Permute(rank-3 axes [2, 0, 1]) — verifies the
10158    /// general permute path through metadata-only adoption.
10159    #[test]
10160    fn pipelined_realize_permute_is_metadata_only() {
10161        // shape [2, 3, 4]; permute axes [2, 0, 1] → shape [4, 2, 3]
10162        let data: Vec<f32> = (1..=24).map(|x| x as f32).collect();
10163        let storage = fuel_memory::from_slice_cpu(&data);
10164        let graph = Arc::new(RwLock::new(Graph::new()));
10165        let (in_id, p_id) = {
10166            let mut g = graph.write().unwrap();
10167            let in_id = g.push(Node {
10168                op: Op::Const,
10169                inputs: vec![],
10170                shape: Shape::from_dims(&[2, 3, 4]),
10171                dtype: DType::F32,
10172            });
10173            let p_id = g.push(Node {
10174                op: Op::Permute(vec![2, 0, 1]),
10175                inputs: vec![in_id],
10176                shape: Shape::from_dims(&[4, 2, 3]),
10177                dtype: DType::F32,
10178            });
10179            (in_id, p_id)
10180        };
10181        let mut inputs = StorageCache::new();
10182        let in_arc = Arc::new(RwLock::new(storage));
10183        inputs.insert(in_id, Arc::clone(&in_arc));
10184
10185        let (result_arc, result_layout) =
10186            PipelinedExecutor::realize(graph, p_id, inputs).expect("realize");
10187
10188        assert!(Arc::ptr_eq(&result_arc, &in_arc));
10189        assert_eq!(result_layout.shape().dims(), &[4, 2, 3]);
10190        // Original strides for shape [2, 3, 4] are [12, 4, 1].
10191        // After permute axes [2, 0, 1]: [strides[2], strides[0], strides[1]] = [1, 12, 4].
10192        assert_eq!(result_layout.stride(), &[1, 12, 4]);
10193    }
10194
10195    /// E2E: Const + Cast(f32→f64) — verifies cast through the
10196    /// pipelined executor. Output Storage has the target dtype;
10197    /// bytes encode the widened values.
10198    #[test]
10199    fn pipelined_realize_cast_f32_to_f64() {
10200        let storage = fuel_memory::from_slice_cpu(&[1.5_f32, -2.25, 100.0]);
10201        let graph = Arc::new(RwLock::new(Graph::new()));
10202        let (in_id, c_id) = {
10203            let mut g = graph.write().unwrap();
10204            let in_id = g.push(Node {
10205                op: Op::Const,
10206                inputs: vec![],
10207                shape: Shape::from_dims(&[3]),
10208                dtype: DType::F32,
10209            });
10210            let c_id = g.push(Node {
10211                op: Op::Cast(DType::F64),
10212                inputs: vec![in_id],
10213                shape: Shape::from_dims(&[3]),
10214                dtype: DType::F64,
10215            });
10216            g.set_target_backend(c_id, BackendId::Cpu);
10217            (in_id, c_id)
10218        };
10219        let mut inputs = StorageCache::new();
10220        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10221
10222        let (result_arc, result_layout) =
10223            PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
10224        assert_eq!(result_layout.shape().dims(), &[3]);
10225
10226        let guard = result_arc.read().unwrap();
10227        assert_eq!(guard.dtype, DType::F64);
10228        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10229            panic!("expected Cpu storage");
10230        };
10231        assert_eq!(c.as_slice::<f64>().unwrap(), &[1.5_f64, -2.25, 100.0]);
10232    }
10233
10234    /// E2E: Const + Cast(f32→bf16) + Cast(bf16→f32) — round trip
10235    /// through bf16; verifies the Cast wrapper's source-dtype
10236    /// dispatch (different sources hit different match arms).
10237    /// Inputs chosen to round-trip exactly through bf16.
10238    #[test]
10239    fn pipelined_realize_cast_round_trip_via_bf16() {
10240        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, -3.0, 0.5]);
10241        let graph = Arc::new(RwLock::new(Graph::new()));
10242        let (in_id, c1_id, c2_id) = {
10243            let mut g = graph.write().unwrap();
10244            let in_id = g.push(Node {
10245                op: Op::Const,
10246                inputs: vec![],
10247                shape: Shape::from_dims(&[4]),
10248                dtype: DType::F32,
10249            });
10250            let c1_id = g.push(Node {
10251                op: Op::Cast(DType::BF16),
10252                inputs: vec![in_id],
10253                shape: Shape::from_dims(&[4]),
10254                dtype: DType::BF16,
10255            });
10256            let c2_id = g.push(Node {
10257                op: Op::Cast(DType::F32),
10258                inputs: vec![c1_id],
10259                shape: Shape::from_dims(&[4]),
10260                dtype: DType::F32,
10261            });
10262            g.set_target_backend(c1_id, BackendId::Cpu);
10263            g.set_target_backend(c2_id, BackendId::Cpu);
10264            (in_id, c1_id, c2_id)
10265        };
10266        let _ = c1_id;
10267        let mut inputs = StorageCache::new();
10268        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10269
10270        let (result_arc, _) = PipelinedExecutor::realize(graph, c2_id, inputs).expect("realize");
10271        let guard = result_arc.read().unwrap();
10272        assert_eq!(guard.dtype, DType::F32);
10273        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10274            panic!("expected Cpu storage");
10275        };
10276        assert_eq!(c.as_slice::<f32>().unwrap(), &[1.0_f32, 2.0, -3.0, 0.5]);
10277    }
10278
10279    /// E2E: Const + Slice — slice is metadata-only; the output Arc
10280    /// shares bytes with the input, and the Layout's start_offset
10281    /// + narrowed shape reflect the slice. Stage 3 of Layout-on-Node
10282    ///
10283    /// extended to cover Op::Slice via Layout::narrow.
10284    #[test]
10285    fn pipelined_realize_slice_is_metadata_only() {
10286        // shape [5]; slice dim 0 from index 1 with len 3 → shape [3]
10287        // Source: [10, 20, 30, 40, 50]; slice → [20, 30, 40]
10288        let storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0]);
10289        let graph = Arc::new(RwLock::new(Graph::new()));
10290        let (in_id, s_id) = {
10291            let mut g = graph.write().unwrap();
10292            let in_id = g.push(Node {
10293                op: Op::Const,
10294                inputs: vec![],
10295                shape: Shape::from_dims(&[5]),
10296                dtype: DType::F32,
10297            });
10298            let s_id = g.push(Node {
10299                op: Op::Slice {
10300                    dim: 0,
10301                    start: 1,
10302                    len: 3,
10303                },
10304                inputs: vec![in_id],
10305                shape: Shape::from_dims(&[3]),
10306                dtype: DType::F32,
10307            });
10308            (in_id, s_id)
10309        };
10310        let mut inputs = StorageCache::new();
10311        let in_arc = Arc::new(RwLock::new(storage));
10312        inputs.insert(in_id, Arc::clone(&in_arc));
10313
10314        let (result_arc, result_layout) =
10315            PipelinedExecutor::realize(graph, s_id, inputs).expect("realize");
10316
10317        // Bytes shared with the input.
10318        assert!(Arc::ptr_eq(&result_arc, &in_arc));
10319        assert_eq!(result_layout.shape().dims(), &[3]);
10320        // Slice into a contiguous source: the resulting layout has
10321        // start_offset = original_stride[0] * start = 1 * 1 = 1,
10322        // and stride [1] (still contiguous within the narrowed dim).
10323        assert_eq!(result_layout.start_offset(), 1);
10324        assert_eq!(result_layout.stride(), &[1]);
10325    }
10326
10327    /// E2E: Const + Slice + SumAll — slice is metadata-only, but
10328    /// sum needs contiguous bytes, so auto-Contiguize materializes
10329    /// the slice before reduce. Tests the stage 3+4 integration
10330    /// through Op::Slice. Sum of `[20, 30, 40]` is 90.
10331    #[test]
10332    fn pipelined_realize_slice_then_sum_all() {
10333        let storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0]);
10334        let graph = Arc::new(RwLock::new(Graph::new()));
10335        let (in_id, s_id, sum_id) = {
10336            let mut g = graph.write().unwrap();
10337            let in_id = g.push(Node {
10338                op: Op::Const,
10339                inputs: vec![],
10340                shape: Shape::from_dims(&[5]),
10341                dtype: DType::F32,
10342            });
10343            let s_id = g.push(Node {
10344                op: Op::Slice {
10345                    dim: 0,
10346                    start: 1,
10347                    len: 3,
10348                },
10349                inputs: vec![in_id],
10350                shape: Shape::from_dims(&[3]),
10351                dtype: DType::F32,
10352            });
10353            let sum_id = g.push(Node {
10354                op: Op::SumAll,
10355                inputs: vec![s_id],
10356                shape: Shape::from_dims(&[]),
10357                dtype: DType::F32,
10358            });
10359            g.set_target_backend(sum_id, BackendId::Cpu);
10360            (in_id, s_id, sum_id)
10361        };
10362        let _ = s_id;
10363        let mut inputs = StorageCache::new();
10364        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10365
10366        let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
10367        let guard = result_arc.read().unwrap();
10368        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10369            panic!("expected Cpu storage");
10370        };
10371        assert_eq!(c.as_slice::<f32>().unwrap(), &[90.0]);
10372    }
10373
10374    /// E2E: Const + Flip + ReluElementwise — Flip is metadata-only,
10375    /// then a kernel that doesn't yet handle negative strides forces
10376    /// auto-Contiguize to materialize through StridedIndex (which
10377    /// handles signed strides natively). Verifies the post-flip
10378    /// data ordering ([4, 3, 2, 1]) is correctly seen by the kernel.
10379    #[test]
10380    fn pipelined_realize_flip_then_relu_materializes_in_reverse() {
10381        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
10382        let graph = Arc::new(RwLock::new(Graph::new()));
10383        let (in_id, _f_id, r_id) = {
10384            let mut g = graph.write().unwrap();
10385            let in_id = g.push(Node {
10386                op: Op::Const,
10387                inputs: vec![],
10388                shape: Shape::from_dims(&[4]),
10389                dtype: DType::F32,
10390            });
10391            let f_id = g.push(Node {
10392                op: Op::Flip { dim: 0 },
10393                inputs: vec![in_id],
10394                shape: Shape::from_dims(&[4]),
10395                dtype: DType::F32,
10396            });
10397            let r_id = g.push(Node {
10398                op: Op::Relu,
10399                inputs: vec![f_id],
10400                shape: Shape::from_dims(&[4]),
10401                dtype: DType::F32,
10402            });
10403            g.set_target_backend(r_id, BackendId::Cpu);
10404            (in_id, f_id, r_id)
10405        };
10406        let mut inputs = StorageCache::new();
10407        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10408
10409        let (result_arc, result_layout) =
10410            PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
10411        let guard = result_arc.read().unwrap();
10412        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10413            panic!("expected Cpu storage");
10414        };
10415        assert_eq!(c.as_slice::<f32>().unwrap(), &[4.0, 3.0, 2.0, 1.0]);
10416        assert_eq!(result_layout.shape().dims(), &[4]);
10417        assert!(result_layout.is_contiguous(), "relu output is contiguous");
10418    }
10419
10420    /// E2E: Const + BroadcastTo — verifies that broadcast layouts
10421    /// have stride 0 on the broadcast dim while sharing the input's
10422    /// bytes. Stage 3 of Layout-on-Node.
10423    #[test]
10424    fn pipelined_realize_broadcast_is_metadata_only() {
10425        // shape [3]; broadcast to [4, 3] — leading dim is 0-stride
10426        let storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
10427        let graph = Arc::new(RwLock::new(Graph::new()));
10428        let (in_id, b_id) = {
10429            let mut g = graph.write().unwrap();
10430            let in_id = g.push(Node {
10431                op: Op::Const,
10432                inputs: vec![],
10433                shape: Shape::from_dims(&[3]),
10434                dtype: DType::F32,
10435            });
10436            let b_id = g.push(Node {
10437                op: Op::BroadcastTo(Shape::from_dims(&[4, 3])),
10438                inputs: vec![in_id],
10439                shape: Shape::from_dims(&[4, 3]),
10440                dtype: DType::F32,
10441            });
10442            (in_id, b_id)
10443        };
10444        let mut inputs = StorageCache::new();
10445        let in_arc = Arc::new(RwLock::new(storage));
10446        inputs.insert(in_id, Arc::clone(&in_arc));
10447
10448        let (result_arc, result_layout) =
10449            PipelinedExecutor::realize(graph, b_id, inputs).expect("realize");
10450
10451        assert!(Arc::ptr_eq(&result_arc, &in_arc));
10452        assert_eq!(result_layout.shape().dims(), &[4, 3]);
10453        // Broadcasted leading dim has stride 0.
10454        assert_eq!(result_layout.stride(), &[0, 1]);
10455    }
10456
10457    /// E2E: Const + Const + MatMul — exercises rank-2 matmul
10458    /// through the pipelined executor. Inputs are contiguous (no
10459    /// auto-Contiguize needed); the kernel walks them via the
10460    /// (m, n, k) carried in OpParams::Matmul.
10461    #[test]
10462    fn pipelined_realize_matmul_2x3_times_3x2() {
10463        // [[1, 2, 3], [4, 5, 6]] @ [[7, 8], [9, 10], [11, 12]]
10464        //   = [[58, 64], [139, 154]]
10465        let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
10466        let rhs_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0, 9.0, 10.0, 11.0, 12.0]);
10467
10468        let graph = Arc::new(RwLock::new(Graph::new()));
10469        let (lhs_id, rhs_id, mm_id) = {
10470            let mut g = graph.write().unwrap();
10471            let lhs = g.push(Node {
10472                op: Op::Const,
10473                inputs: vec![],
10474                shape: Shape::from_dims(&[2, 3]),
10475                dtype: DType::F32,
10476            });
10477            let rhs = g.push(Node {
10478                op: Op::Const,
10479                inputs: vec![],
10480                shape: Shape::from_dims(&[3, 2]),
10481                dtype: DType::F32,
10482            });
10483            let mm = g.push(Node {
10484                op: Op::MatMul,
10485                inputs: vec![lhs, rhs],
10486                shape: Shape::from_dims(&[2, 2]),
10487                dtype: DType::F32,
10488            });
10489            g.set_target_backend(mm, BackendId::Cpu);
10490            (lhs, rhs, mm)
10491        };
10492        let mut inputs = StorageCache::new();
10493        inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
10494        inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
10495
10496        let (result_arc, result_layout) =
10497            PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
10498        assert_eq!(result_layout.shape().dims(), &[2, 2]);
10499        assert!(result_layout.is_contiguous());
10500
10501        let guard = result_arc.read().unwrap();
10502        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10503            panic!("expected Cpu storage");
10504        };
10505        assert_eq!(c.as_slice::<f32>().unwrap(), &[58.0, 64.0, 139.0, 154.0]);
10506    }
10507
10508    /// E2E: batched matmul through the pipelined executor. Two
10509    /// batches of [2, 2] @ [2, 2]; the kernel iterates over
10510    /// `batch_count` and produces concatenated outputs.
10511    #[test]
10512    fn pipelined_realize_matmul_batched_2x_2x2_times_2x2() {
10513        let lhs_storage = fuel_memory::from_slice_cpu(&[
10514            1.0_f32, 2.0, 3.0, 4.0, // batch 0
10515            1.0, 0.0, 0.0, 1.0, // batch 1 (identity)
10516        ]);
10517        let rhs_storage = fuel_memory::from_slice_cpu(&[
10518            5.0_f32, 6.0, 7.0, 8.0, // batch 0
10519            10.0, 20.0, 30.0, 40.0, // batch 1
10520        ]);
10521
10522        let graph = Arc::new(RwLock::new(Graph::new()));
10523        let (lhs_id, rhs_id, mm_id) = {
10524            let mut g = graph.write().unwrap();
10525            let lhs = g.push(Node {
10526                op: Op::Const,
10527                inputs: vec![],
10528                shape: Shape::from_dims(&[2, 2, 2]),
10529                dtype: DType::F32,
10530            });
10531            let rhs = g.push(Node {
10532                op: Op::Const,
10533                inputs: vec![],
10534                shape: Shape::from_dims(&[2, 2, 2]),
10535                dtype: DType::F32,
10536            });
10537            let mm = g.push(Node {
10538                op: Op::MatMul,
10539                inputs: vec![lhs, rhs],
10540                shape: Shape::from_dims(&[2, 2, 2]),
10541                dtype: DType::F32,
10542            });
10543            g.set_target_backend(mm, BackendId::Cpu);
10544            (lhs, rhs, mm)
10545        };
10546        let mut inputs = StorageCache::new();
10547        inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
10548        inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
10549
10550        let (result_arc, result_layout) =
10551            PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
10552        assert_eq!(result_layout.shape().dims(), &[2, 2, 2]);
10553        assert!(result_layout.is_contiguous());
10554
10555        let guard = result_arc.read().unwrap();
10556        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10557            panic!("expected Cpu storage");
10558        };
10559        // batch 0: [[1,2],[3,4]] @ [[5,6],[7,8]] = [[19,22],[43,50]]
10560        // batch 1: identity @ [[10,20],[30,40]]   = [[10,20],[30,40]]
10561        assert_eq!(
10562            c.as_slice::<f32>().unwrap(),
10563            &[19.0, 22.0, 43.0, 50.0, 10.0, 20.0, 30.0, 40.0]
10564        );
10565    }
10566
10567    /// E2E: F64 elementwise add through the pipelined executor.
10568    /// Verifies that capability-driven dispatch correctly routes
10569    /// (AddElementwise, F64) to the f64 wrapper/kernel.
10570    #[test]
10571    fn pipelined_realize_add_f64() {
10572        let lhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0]);
10573        let rhs = fuel_memory::from_slice_cpu(&[10.0_f64, 20.0, 30.0]);
10574        let graph = Arc::new(RwLock::new(Graph::new()));
10575        let (l_id, r_id, op_id) = {
10576            let mut g = graph.write().unwrap();
10577            let l = g.push(Node {
10578                op: Op::Const,
10579                inputs: vec![],
10580                shape: Shape::from_dims(&[3]),
10581                dtype: DType::F64,
10582            });
10583            let r = g.push(Node {
10584                op: Op::Const,
10585                inputs: vec![],
10586                shape: Shape::from_dims(&[3]),
10587                dtype: DType::F64,
10588            });
10589            let op = g.push(Node {
10590                op: Op::Add,
10591                inputs: vec![l, r],
10592                shape: Shape::from_dims(&[3]),
10593                dtype: DType::F64,
10594            });
10595            g.set_target_backend(op, BackendId::Cpu);
10596            (l, r, op)
10597        };
10598        let mut inputs = StorageCache::new();
10599        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10600        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10601        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
10602        let guard = result_arc.read().unwrap();
10603        assert_eq!(guard.dtype, DType::F64);
10604        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10605            panic!("expected Cpu storage");
10606        };
10607        assert_eq!(c.as_slice::<f64>().unwrap(), &[11.0_f64, 22.0, 33.0]);
10608    }
10609
10610    /// E2E: Op::Equal F32 → Bool mask through the pipelined executor (GAP-168c).
10611    /// Verifies (a) the binding-table key `(EqualElementwise, [F32, F32, U8],
10612    /// Cpu)` resolves, (b) the executor allocates a U8-sized output
10613    /// buffer (1 byte per element, not 4), (c) the kernel writes the
10614    /// expected mask bits including IEEE-754 NaN handling
10615    /// (`NaN == NaN` is false).
10616    #[test]
10617    fn pipelined_realize_eq_f32_to_bool_mask() {
10618        let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, f32::NAN, 0.0]);
10619        let rhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -0.0]);
10620        let graph = Arc::new(RwLock::new(Graph::new()));
10621        let (l_id, r_id, eq_id) = {
10622            let mut g = graph.write().unwrap();
10623            let l = g.push(Node {
10624                op: Op::Const,
10625                inputs: vec![],
10626                shape: Shape::from_dims(&[5]),
10627                dtype: DType::F32,
10628            });
10629            let r = g.push(Node {
10630                op: Op::Const,
10631                inputs: vec![],
10632                shape: Shape::from_dims(&[5]),
10633                dtype: DType::F32,
10634            });
10635            let eq = g.push(Node {
10636                op: Op::Equal,
10637                inputs: vec![l, r],
10638                shape: Shape::from_dims(&[5]),
10639                dtype: DType::Bool,
10640            });
10641            g.set_target_backend(eq, BackendId::Cpu);
10642            (l, r, eq)
10643        };
10644        let mut inputs = StorageCache::new();
10645        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10646        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10647        let (result_arc, _) = PipelinedExecutor::realize(graph, eq_id, inputs).expect("realize");
10648        let guard = result_arc.read().unwrap();
10649        assert_eq!(guard.dtype, DType::Bool);
10650        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10651            panic!("expected Cpu storage");
10652        };
10653        let mask: &[u8] = c.as_slice().expect("u8 view");
10654        // Index 0: 1.0 == 1.0 → 1.
10655        // Index 1: 2.0 != 5.0 → 0.
10656        // Index 2: 3.0 == 3.0 → 1.
10657        // Index 3: NaN == NaN → 0 (IEEE-754).
10658        // Index 4: 0.0 == -0.0 → 1 (IEEE-754 zero equality).
10659        assert_eq!(mask, &[1, 0, 1, 0, 1]);
10660    }
10661
10662    /// E2E: Op::Ne F32 → Bool mask. Mirrors the Eq F32 test with
10663    /// inverted predicate; NaN-vs-NaN slot now yields `1` (since
10664    /// `NaN != NaN` per IEEE-754).
10665    #[test]
10666    fn pipelined_realize_ne_f32_to_bool_mask() {
10667        let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, f32::NAN, 0.0]);
10668        let rhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -0.0]);
10669        let graph = Arc::new(RwLock::new(Graph::new()));
10670        let (l_id, r_id, ne_id) = {
10671            let mut g = graph.write().unwrap();
10672            let l = g.push(Node {
10673                op: Op::Const,
10674                inputs: vec![],
10675                shape: Shape::from_dims(&[5]),
10676                dtype: DType::F32,
10677            });
10678            let r = g.push(Node {
10679                op: Op::Const,
10680                inputs: vec![],
10681                shape: Shape::from_dims(&[5]),
10682                dtype: DType::F32,
10683            });
10684            let ne = g.push(Node {
10685                op: Op::Ne,
10686                inputs: vec![l, r],
10687                shape: Shape::from_dims(&[5]),
10688                dtype: DType::Bool,
10689            });
10690            g.set_target_backend(ne, BackendId::Cpu);
10691            (l, r, ne)
10692        };
10693        let mut inputs = StorageCache::new();
10694        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10695        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10696        let (result_arc, _) = PipelinedExecutor::realize(graph, ne_id, inputs).expect("realize");
10697        let guard = result_arc.read().unwrap();
10698        assert_eq!(guard.dtype, DType::Bool);
10699        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10700            panic!("expected Cpu storage");
10701        };
10702        let mask: &[u8] = c.as_slice().expect("u8 view");
10703        // Inverse of the Eq test:
10704        // 1.0 != 1.0 → 0;  2.0 != 5.0 → 1;  3.0 != 3.0 → 0;
10705        // NaN != NaN → 1 (IEEE-754);  0.0 != -0.0 → 0 (IEEE-754).
10706        assert_eq!(mask, &[0, 1, 0, 1, 0]);
10707    }
10708
10709    /// E2E: Op::Lt F32 → Bool mask. Confirms strict-less-than semantics
10710    /// + IEEE-754 NaN handling (any comparison with NaN is unordered →
10711    ///
10712    /// `0`).
10713    #[test]
10714    fn pipelined_realize_lt_f32_to_bool_mask() {
10715        let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -1.0]);
10716        let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 3.0, 0.0, 0.0]);
10717        let graph = Arc::new(RwLock::new(Graph::new()));
10718        let (l_id, r_id, lt_id) = {
10719            let mut g = graph.write().unwrap();
10720            let l = g.push(Node {
10721                op: Op::Const,
10722                inputs: vec![],
10723                shape: Shape::from_dims(&[5]),
10724                dtype: DType::F32,
10725            });
10726            let r = g.push(Node {
10727                op: Op::Const,
10728                inputs: vec![],
10729                shape: Shape::from_dims(&[5]),
10730                dtype: DType::F32,
10731            });
10732            let lt = g.push(Node {
10733                op: Op::Lt,
10734                inputs: vec![l, r],
10735                shape: Shape::from_dims(&[5]),
10736                dtype: DType::Bool,
10737            });
10738            g.set_target_backend(lt, BackendId::Cpu);
10739            (l, r, lt)
10740        };
10741        let mut inputs = StorageCache::new();
10742        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10743        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10744        let (result_arc, _) = PipelinedExecutor::realize(graph, lt_id, inputs).expect("realize");
10745        let guard = result_arc.read().unwrap();
10746        assert_eq!(guard.dtype, DType::Bool);
10747        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10748            panic!("expected Cpu storage");
10749        };
10750        let mask: &[u8] = c.as_slice().expect("u8 view");
10751        // 1.0 < 2.0 → 1;  5.0 < 5.0 → 0 (strict);  3.0 < 3.0 → 0;
10752        // NaN < 0.0 → 0 (unordered);  -1.0 < 0.0 → 1.
10753        assert_eq!(mask, &[1, 0, 0, 0, 1]);
10754    }
10755
10756    /// E2E: Op::Le F32 → Bool mask. Distinct from Lt at the equal slot
10757    /// (`5.0 <= 5.0` = 1, vs Lt's `5.0 < 5.0` = 0). NaN unordered → 0.
10758    #[test]
10759    fn pipelined_realize_le_f32_to_bool_mask() {
10760        let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -1.0]);
10761        let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 2.0, 0.0, 0.0]);
10762        let graph = Arc::new(RwLock::new(Graph::new()));
10763        let (l_id, r_id, le_id) = {
10764            let mut g = graph.write().unwrap();
10765            let l = g.push(Node {
10766                op: Op::Const,
10767                inputs: vec![],
10768                shape: Shape::from_dims(&[5]),
10769                dtype: DType::F32,
10770            });
10771            let r = g.push(Node {
10772                op: Op::Const,
10773                inputs: vec![],
10774                shape: Shape::from_dims(&[5]),
10775                dtype: DType::F32,
10776            });
10777            let le = g.push(Node {
10778                op: Op::Le,
10779                inputs: vec![l, r],
10780                shape: Shape::from_dims(&[5]),
10781                dtype: DType::Bool,
10782            });
10783            g.set_target_backend(le, BackendId::Cpu);
10784            (l, r, le)
10785        };
10786        let mut inputs = StorageCache::new();
10787        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10788        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10789        let (result_arc, _) = PipelinedExecutor::realize(graph, le_id, inputs).expect("realize");
10790        let guard = result_arc.read().unwrap();
10791        assert_eq!(guard.dtype, DType::Bool);
10792        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10793            panic!("expected Cpu storage");
10794        };
10795        let mask: &[u8] = c.as_slice().expect("u8 view");
10796        // 1.0 <= 2.0 → 1;  5.0 <= 5.0 → 1 (key Lt difference);
10797        // 3.0 <= 2.0 → 0;  NaN <= 0.0 → 0 (unordered);  -1.0 <= 0.0 → 1.
10798        assert_eq!(mask, &[1, 1, 0, 0, 1]);
10799    }
10800
10801    /// E2E: Op::Gt F32 → Bool mask. Strict-greater: equality slot is
10802    /// `0`. NaN unordered → `0`.
10803    #[test]
10804    fn pipelined_realize_gt_f32_to_bool_mask() {
10805        let lhs = fuel_memory::from_slice_cpu(&[3.0_f32, 5.0, 2.0, f32::NAN, 1.0]);
10806        let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 3.0, 0.0, 0.0]);
10807        let graph = Arc::new(RwLock::new(Graph::new()));
10808        let (l_id, r_id, gt_id) = {
10809            let mut g = graph.write().unwrap();
10810            let l = g.push(Node {
10811                op: Op::Const,
10812                inputs: vec![],
10813                shape: Shape::from_dims(&[5]),
10814                dtype: DType::F32,
10815            });
10816            let r = g.push(Node {
10817                op: Op::Const,
10818                inputs: vec![],
10819                shape: Shape::from_dims(&[5]),
10820                dtype: DType::F32,
10821            });
10822            let gt = g.push(Node {
10823                op: Op::Gt,
10824                inputs: vec![l, r],
10825                shape: Shape::from_dims(&[5]),
10826                dtype: DType::Bool,
10827            });
10828            g.set_target_backend(gt, BackendId::Cpu);
10829            (l, r, gt)
10830        };
10831        let mut inputs = StorageCache::new();
10832        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10833        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10834        let (result_arc, _) = PipelinedExecutor::realize(graph, gt_id, inputs).expect("realize");
10835        let guard = result_arc.read().unwrap();
10836        assert_eq!(guard.dtype, DType::Bool);
10837        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10838            panic!("expected Cpu storage");
10839        };
10840        let mask: &[u8] = c.as_slice().expect("u8 view");
10841        // 3.0 > 2.0 → 1;  5.0 > 5.0 → 0 (strict);  2.0 > 3.0 → 0;
10842        // NaN > 0.0 → 0 (unordered);  1.0 > 0.0 → 1.
10843        assert_eq!(mask, &[1, 0, 0, 0, 1]);
10844    }
10845
10846    /// E2E: Op::Ge F32 → Bool mask. Greater-or-equal: equality slot
10847    /// is `1` (distinguishes from Gt). NaN unordered → `0`. Closes
10848    /// the comparison family with full `[Eq, Ne, Lt, Le, Gt, Ge]`
10849    /// coverage.
10850    #[test]
10851    fn pipelined_realize_ge_f32_to_bool_mask() {
10852        let lhs = fuel_memory::from_slice_cpu(&[3.0_f32, 5.0, 2.0, f32::NAN, 0.0]);
10853        let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 3.0, 0.0, 0.0]);
10854        let graph = Arc::new(RwLock::new(Graph::new()));
10855        let (l_id, r_id, ge_id) = {
10856            let mut g = graph.write().unwrap();
10857            let l = g.push(Node {
10858                op: Op::Const,
10859                inputs: vec![],
10860                shape: Shape::from_dims(&[5]),
10861                dtype: DType::F32,
10862            });
10863            let r = g.push(Node {
10864                op: Op::Const,
10865                inputs: vec![],
10866                shape: Shape::from_dims(&[5]),
10867                dtype: DType::F32,
10868            });
10869            let ge = g.push(Node {
10870                op: Op::Ge,
10871                inputs: vec![l, r],
10872                shape: Shape::from_dims(&[5]),
10873                dtype: DType::Bool,
10874            });
10875            g.set_target_backend(ge, BackendId::Cpu);
10876            (l, r, ge)
10877        };
10878        let mut inputs = StorageCache::new();
10879        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10880        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10881        let (result_arc, _) = PipelinedExecutor::realize(graph, ge_id, inputs).expect("realize");
10882        let guard = result_arc.read().unwrap();
10883        assert_eq!(guard.dtype, DType::Bool);
10884        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10885            panic!("expected Cpu storage");
10886        };
10887        let mask: &[u8] = c.as_slice().expect("u8 view");
10888        // 3.0 >= 2.0 → 1;  5.0 >= 5.0 → 1 (key Gt difference);
10889        // 2.0 >= 3.0 → 0;  NaN >= 0.0 → 0 (unordered);  0.0 >= 0.0 → 1.
10890        assert_eq!(mask, &[1, 1, 0, 0, 1]);
10891    }
10892
10893    /// E2E: Op::Equal F64 → Bool mask. Confirms the F64 wrapper is
10894    /// independently registered and routed (binding-table key
10895    /// `(EqualElementwise, [F64, F64, U8], Cpu)`).
10896    #[test]
10897    fn pipelined_realize_eq_f64_to_bool_mask() {
10898        let lhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0]);
10899        let rhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 4.0]);
10900        let graph = Arc::new(RwLock::new(Graph::new()));
10901        let (l_id, r_id, eq_id) = {
10902            let mut g = graph.write().unwrap();
10903            let l = g.push(Node {
10904                op: Op::Const,
10905                inputs: vec![],
10906                shape: Shape::from_dims(&[3]),
10907                dtype: DType::F64,
10908            });
10909            let r = g.push(Node {
10910                op: Op::Const,
10911                inputs: vec![],
10912                shape: Shape::from_dims(&[3]),
10913                dtype: DType::F64,
10914            });
10915            let eq = g.push(Node {
10916                op: Op::Equal,
10917                inputs: vec![l, r],
10918                shape: Shape::from_dims(&[3]),
10919                dtype: DType::Bool,
10920            });
10921            g.set_target_backend(eq, BackendId::Cpu);
10922            (l, r, eq)
10923        };
10924        let mut inputs = StorageCache::new();
10925        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10926        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10927        let (result_arc, _) = PipelinedExecutor::realize(graph, eq_id, inputs).expect("realize");
10928        let guard = result_arc.read().unwrap();
10929        assert_eq!(guard.dtype, DType::Bool);
10930        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10931            panic!("expected Cpu storage");
10932        };
10933        let mask: &[u8] = c.as_slice().expect("u8 view");
10934        assert_eq!(mask, &[1, 1, 0]);
10935    }
10936
10937    /// E2E: Op::Where ternary select — `out[i] = if cond[i] != 0 { a[i] } else { b[i] }`.
10938    /// Validates (a) the binding-table key `(Where, [Bool, F32, F32, F32], Cpu)`
10939    /// resolves to the where_f32 wrapper, (b) the Bool cond input drives
10940    /// the per-slot pick, (c) outputs preserve the input dtype.
10941    ///
10942    /// GAP-168(c): the cond STORAGE is tagged Bool, not merely the graph node.
10943    /// Bool and U8 are byte-identical, so leaving the storage U8 under a Bool
10944    /// node is exactly the mismatch this cut exists to make visible.
10945    #[test]
10946    fn pipelined_realize_where_f32_picks_per_slot_from_bool_mask() {
10947        // cond = [1, 0, 1, 0, 1]; a = [1, 2, 3, 4, 5]; b = [10, 20, 30, 40, 50]
10948        // expected = [1, 20, 3, 40, 5]
10949        let mut cond_storage = fuel_memory::from_slice_cpu(&[1u8, 0, 1, 0, 1]);
10950        cond_storage.dtype = DType::Bool; // GAP-168(c): a Bool mask, not U8 bytes
10951        let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0]);
10952        let b_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0]);
10953        let graph = Arc::new(RwLock::new(Graph::new()));
10954        let (cond_id, a_id, b_id, where_id) = {
10955            let mut g = graph.write().unwrap();
10956            let cond = g.push(Node {
10957                op: Op::Const,
10958                inputs: vec![],
10959                shape: Shape::from_dims(&[5]),
10960                dtype: DType::Bool,
10961            });
10962            let a = g.push(Node {
10963                op: Op::Const,
10964                inputs: vec![],
10965                shape: Shape::from_dims(&[5]),
10966                dtype: DType::F32,
10967            });
10968            let b = g.push(Node {
10969                op: Op::Const,
10970                inputs: vec![],
10971                shape: Shape::from_dims(&[5]),
10972                dtype: DType::F32,
10973            });
10974            let w = g.push(Node {
10975                op: Op::Where,
10976                inputs: vec![cond, a, b],
10977                shape: Shape::from_dims(&[5]),
10978                dtype: DType::F32,
10979            });
10980            g.set_target_backend(w, BackendId::Cpu);
10981            (cond, a, b, w)
10982        };
10983        let mut inputs = StorageCache::new();
10984        inputs.insert(cond_id, Arc::new(RwLock::new(cond_storage)));
10985        inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
10986        inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
10987        let (result_arc, _) = PipelinedExecutor::realize(graph, where_id, inputs).expect("realize");
10988        let guard = result_arc.read().unwrap();
10989        assert_eq!(guard.dtype, DType::F32);
10990        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10991            panic!("expected Cpu storage");
10992        };
10993        let out: &[f32] = c.as_slice().expect("f32 view");
10994        assert_eq!(out, &[1.0, 20.0, 3.0, 40.0, 5.0]);
10995    }
10996
10997    /// E2E: full chain `eq → where`. Compares two f32 vectors, then
10998    /// uses the resulting Bool mask to pick from a third tensor (or a
10999    /// fallback). Validates the comparison-family + Where ops compose
11000    /// end-to-end.
11001    #[test]
11002    fn pipelined_realize_eq_then_where_full_chain() {
11003        // a = [1, 2, 3]; b = [1, 5, 3] → eq = [1, 0, 1]
11004        // pick = [10, 20, 30]; fallback = [99, 99, 99]
11005        // result = [10, 99, 30]
11006        let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
11007        let b_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0]);
11008        let pick_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
11009        let fb_storage = fuel_memory::from_slice_cpu(&[99.0_f32, 99.0, 99.0]);
11010        let graph = Arc::new(RwLock::new(Graph::new()));
11011        let (a_id, b_id, pick_id, fb_id, where_id) = {
11012            let mut g = graph.write().unwrap();
11013            let a = g.push(Node {
11014                op: Op::Const,
11015                inputs: vec![],
11016                shape: Shape::from_dims(&[3]),
11017                dtype: DType::F32,
11018            });
11019            let b = g.push(Node {
11020                op: Op::Const,
11021                inputs: vec![],
11022                shape: Shape::from_dims(&[3]),
11023                dtype: DType::F32,
11024            });
11025            let pick = g.push(Node {
11026                op: Op::Const,
11027                inputs: vec![],
11028                shape: Shape::from_dims(&[3]),
11029                dtype: DType::F32,
11030            });
11031            let fb = g.push(Node {
11032                op: Op::Const,
11033                inputs: vec![],
11034                shape: Shape::from_dims(&[3]),
11035                dtype: DType::F32,
11036            });
11037            let eq = g.push(Node {
11038                op: Op::Equal,
11039                inputs: vec![a, b],
11040                shape: Shape::from_dims(&[3]),
11041                dtype: DType::Bool,
11042            });
11043            let w = g.push(Node {
11044                op: Op::Where,
11045                inputs: vec![eq, pick, fb],
11046                shape: Shape::from_dims(&[3]),
11047                dtype: DType::F32,
11048            });
11049            g.set_target_backend(eq, BackendId::Cpu);
11050            g.set_target_backend(w, BackendId::Cpu);
11051            (a, b, pick, fb, w)
11052        };
11053        let mut inputs = StorageCache::new();
11054        inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
11055        inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
11056        inputs.insert(pick_id, Arc::new(RwLock::new(pick_storage)));
11057        inputs.insert(fb_id, Arc::new(RwLock::new(fb_storage)));
11058        let (result_arc, _) = PipelinedExecutor::realize(graph, where_id, inputs).expect("realize");
11059        let guard = result_arc.read().unwrap();
11060        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11061            panic!("expected Cpu storage");
11062        };
11063        let out: &[f32] = c.as_slice().expect("f32 view");
11064        assert_eq!(out, &[10.0, 99.0, 30.0]);
11065    }
11066
11067    /// E2E: Q4_0 QMatMul through the pipelined executor — proves
11068    /// quantized weights can flow into the unified path. Activations
11069    /// are F32, weights are U32-typed (raw block bytes).
11070    /// Construct a Q4_0 weight tensor where every weight = 1.0
11071    /// (d=1.0, every nibble=9 → 1*(9-8)=1), so A @ W^T computes
11072    /// the per-row sum of activations.
11073    #[test]
11074    fn pipelined_realize_qmatmul_q4_0_unit_weight_sums_activations() {
11075        use fuel_graph::QuantType;
11076        use half::f16;
11077        let block_size = std::mem::size_of::<fuel_quantized::BlockQ4_0>();
11078        let mut w_bytes = vec![0u8; 2 * block_size];
11079        for block_idx in 0..2 {
11080            let off = block_idx * block_size;
11081            let d_bytes = f16::from_f32(1.0).to_le_bytes();
11082            w_bytes[off..off + 2].copy_from_slice(&d_bytes);
11083            for i in 0..16 {
11084                w_bytes[off + 2 + i] = 0x99;
11085            }
11086        }
11087        // Weight tensor is U32-typed (rank-1, length = bytes/4)
11088        let w_storage = fuel_memory::Storage::new(
11089            fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_bytes(
11090                &w_bytes,
11091            )),
11092            DType::U32,
11093        );
11094
11095        let act_vec: Vec<f32> = (1..=32).map(|x| x as f32).collect();
11096        let act_storage = fuel_memory::from_slice_cpu(&act_vec);
11097
11098        let graph = Arc::new(RwLock::new(Graph::new()));
11099        let (act_id, w_id, mm_id) = {
11100            let mut g = graph.write().unwrap();
11101            let act = g.push(Node {
11102                op: Op::Const,
11103                inputs: vec![],
11104                shape: Shape::from_dims(&[1, 32]),
11105                dtype: DType::F32,
11106            });
11107            let w = g.push(Node {
11108                op: Op::Const,
11109                inputs: vec![],
11110                shape: Shape::from_dims(&[w_bytes.len() / 4]),
11111                dtype: DType::U32,
11112            });
11113            let mm = g.push(Node {
11114                op: Op::Fused(
11115                    fuel_graph::registry::FusedOps::QMATMUL,
11116                    fuel_graph::registry::FusedOpParams::QMatMul {
11117                        quant_type: QuantType::Q4_0,
11118                        k: 32,
11119                        n: 2,
11120                    },
11121                ),
11122                inputs: vec![act, w],
11123                shape: Shape::from_dims(&[1, 2]),
11124                dtype: DType::F32,
11125            });
11126            g.set_target_backend(mm, BackendId::Cpu);
11127            (act, w, mm)
11128        };
11129        let mut inputs = StorageCache::new();
11130        inputs.insert(act_id, Arc::new(RwLock::new(act_storage)));
11131        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
11132        let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11133        let guard = result_arc.read().unwrap();
11134        assert_eq!(guard.dtype, DType::F32);
11135        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11136            panic!("expected Cpu storage");
11137        };
11138        let r: &[f32] = c.as_slice().unwrap();
11139        // Both rows = sum(1..=32) = 528, within Q8_1 round-trip
11140        // tolerance.
11141        assert!((r[0] - 528.0).abs() < 0.5, "got {}, want 528", r[0]);
11142        assert!((r[1] - 528.0).abs() < 0.5, "got {}, want 528", r[1]);
11143    }
11144
11145    /// E2E: QMatMul with Q5_0 weights — verifies the new quant
11146    /// dispatch arm picks `qmatmul_q5_0_f32`. We build weights by
11147    /// quantizing all-ones via `BlockQ5_0::from_float`, then
11148    /// compare pipelined output against the direct fuel_quantized
11149    /// matmul on the same blocks.
11150    #[test]
11151    fn pipelined_realize_qmatmul_q5_0_against_reference() {
11152        use fuel_graph::QuantType;
11153        use fuel_quantized::{BlockQ5_0, GgmlType};
11154        let n = 2;
11155        let k = 64; // 2 blocks per row (Q5_0 vec_dot pairs blocks)
11156        // Quantize an all-ones [n, k] weight matrix.
11157        let w_f32 = vec![1.0_f32; n * k];
11158        let blocks_per_row = k / BlockQ5_0::BLCK_SIZE;
11159        let mut w_blocks = vec![BlockQ5_0::zeros(); n * blocks_per_row];
11160        BlockQ5_0::from_float(&w_f32, &mut w_blocks);
11161        // Reinterpret block slice as bytes (BlockQ5_0 is #[repr(C)]).
11162        let bytes_per_block = std::mem::size_of::<BlockQ5_0>();
11163        let w_bytes: Vec<u8> = unsafe {
11164            std::slice::from_raw_parts(
11165                w_blocks.as_ptr() as *const u8,
11166                w_blocks.len() * bytes_per_block,
11167            )
11168        }
11169        .to_vec();
11170        let w_storage = fuel_memory::Storage::new(
11171            fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_bytes(
11172                &w_bytes,
11173            )),
11174            DType::U32,
11175        );
11176        let act_vec: Vec<f32> = (1..=k).map(|x| x as f32).collect();
11177        let act_storage = fuel_memory::from_slice_cpu(&act_vec);
11178        // Reference: direct matmul through fuel_quantized.
11179        let mut ref_out = vec![0.0_f32; n];
11180        fuel_quantized::matmul::<BlockQ5_0>((1, k, n), &act_vec, &w_blocks, &mut ref_out)
11181            .expect("ref matmul");
11182
11183        let graph = Arc::new(RwLock::new(Graph::new()));
11184        let (act_id, w_id, mm_id) = {
11185            let mut g = graph.write().unwrap();
11186            let act = g.push(Node {
11187                op: Op::Const,
11188                inputs: vec![],
11189                shape: Shape::from_dims(&[1, k]),
11190                dtype: DType::F32,
11191            });
11192            let w = g.push(Node {
11193                op: Op::Const,
11194                inputs: vec![],
11195                shape: Shape::from_dims(&[w_bytes.len() / 4]),
11196                dtype: DType::U32,
11197            });
11198            let mm = g.push(Node {
11199                op: Op::Fused(
11200                    fuel_graph::registry::FusedOps::QMATMUL,
11201                    fuel_graph::registry::FusedOpParams::QMatMul {
11202                        quant_type: QuantType::Q5_0,
11203                        k,
11204                        n,
11205                    },
11206                ),
11207                inputs: vec![act, w],
11208                shape: Shape::from_dims(&[1, n]),
11209                dtype: DType::F32,
11210            });
11211            g.set_target_backend(mm, BackendId::Cpu);
11212            (act, w, mm)
11213        };
11214        let mut inputs = StorageCache::new();
11215        inputs.insert(act_id, Arc::new(RwLock::new(act_storage)));
11216        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
11217        let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11218        let guard = result_arc.read().unwrap();
11219        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11220            panic!("expected Cpu storage");
11221        };
11222        let r: &[f32] = c.as_slice().unwrap();
11223        // Bit-exact against same kernel via the trait, since both
11224        // paths run fuel_quantized::matmul<BlockQ5_0>.
11225        assert_eq!(
11226            r,
11227            ref_out.as_slice(),
11228            "pipelined Q5_0 differs from reference: got {r:?}, want {ref_out:?}"
11229        );
11230    }
11231
11232    /// E2E: QMatMul with Q6K (256-element super-block k-quant).
11233    /// Same idea as the Q5_0 test — bit-exact against the
11234    /// reference fuel_quantized::matmul<BlockQ6K>. Confirms the
11235    /// dispatch arm wires `qmatmul_q6k_f32` correctly.
11236    #[test]
11237    fn pipelined_realize_qmatmul_q6k_against_reference() {
11238        use fuel_graph::QuantType;
11239        use fuel_quantized::{BlockQ6K, GgmlType};
11240        let n = 2;
11241        let k = 256; // 1 super-block per row
11242        let w_f32 = vec![1.0_f32; n * k];
11243        let blocks_per_row = k / BlockQ6K::BLCK_SIZE;
11244        let mut w_blocks = vec![BlockQ6K::zeros(); n * blocks_per_row];
11245        BlockQ6K::from_float(&w_f32, &mut w_blocks);
11246        let bytes_per_block = std::mem::size_of::<BlockQ6K>();
11247        let w_bytes: Vec<u8> = unsafe {
11248            std::slice::from_raw_parts(
11249                w_blocks.as_ptr() as *const u8,
11250                w_blocks.len() * bytes_per_block,
11251            )
11252        }
11253        .to_vec();
11254        let w_storage = fuel_memory::Storage::new(
11255            fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_bytes(
11256                &w_bytes,
11257            )),
11258            DType::U32,
11259        );
11260        let act_vec: Vec<f32> = (1..=k).map(|x| x as f32 / 100.0).collect();
11261        let act_storage = fuel_memory::from_slice_cpu(&act_vec);
11262        let mut ref_out = vec![0.0_f32; n];
11263        fuel_quantized::matmul::<BlockQ6K>((1, k, n), &act_vec, &w_blocks, &mut ref_out)
11264            .expect("ref matmul");
11265
11266        let graph = Arc::new(RwLock::new(Graph::new()));
11267        let (act_id, w_id, mm_id) = {
11268            let mut g = graph.write().unwrap();
11269            let act = g.push(Node {
11270                op: Op::Const,
11271                inputs: vec![],
11272                shape: Shape::from_dims(&[1, k]),
11273                dtype: DType::F32,
11274            });
11275            let w = g.push(Node {
11276                op: Op::Const,
11277                inputs: vec![],
11278                shape: Shape::from_dims(&[w_bytes.len() / 4]),
11279                dtype: DType::U32,
11280            });
11281            let mm = g.push(Node {
11282                op: Op::Fused(
11283                    fuel_graph::registry::FusedOps::QMATMUL,
11284                    fuel_graph::registry::FusedOpParams::QMatMul {
11285                        quant_type: QuantType::Q6K,
11286                        k,
11287                        n,
11288                    },
11289                ),
11290                inputs: vec![act, w],
11291                shape: Shape::from_dims(&[1, n]),
11292                dtype: DType::F32,
11293            });
11294            g.set_target_backend(mm, BackendId::Cpu);
11295            (act, w, mm)
11296        };
11297        let mut inputs = StorageCache::new();
11298        inputs.insert(act_id, Arc::new(RwLock::new(act_storage)));
11299        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
11300        let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11301        let guard = result_arc.read().unwrap();
11302        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11303            panic!("expected Cpu storage");
11304        };
11305        let r: &[f32] = c.as_slice().unwrap();
11306        assert_eq!(
11307            r,
11308            ref_out.as_slice(),
11309            "pipelined Q6K differs from reference: got {r:?}, want {ref_out:?}"
11310        );
11311    }
11312
11313    /// E2E: BF16 RmsNormLastDim through the pipelined executor.
11314    /// Verifies that capability-driven dispatch routes the
11315    /// half-float norm op to the bf16-specific kernel (which
11316    /// accumulates in f32 internally).
11317    #[test]
11318    fn pipelined_realize_rms_norm_bf16() {
11319        let v: Vec<half::bf16> = [3.0_f32, 4.0]
11320            .iter()
11321            .map(|&x| half::bf16::from_f32(x))
11322            .collect();
11323        let storage = fuel_memory::from_slice_cpu(&v);
11324        let graph = Arc::new(RwLock::new(Graph::new()));
11325        let (in_id, op_id) = {
11326            let mut g = graph.write().unwrap();
11327            let in_id = g.push(Node {
11328                op: Op::Const,
11329                inputs: vec![],
11330                shape: Shape::from_dims(&[1, 2]),
11331                dtype: DType::BF16,
11332            });
11333            let op_id = g.push(Node {
11334                op: Op::Fused(
11335                    fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM,
11336                    fuel_graph::registry::FusedOpParams::RmsNormLastDim { eps: 0.0 },
11337                ),
11338                inputs: vec![in_id],
11339                shape: Shape::from_dims(&[1, 2]),
11340                dtype: DType::BF16,
11341            });
11342            g.set_target_backend(op_id, BackendId::Cpu);
11343            (in_id, op_id)
11344        };
11345        let mut inputs = StorageCache::new();
11346        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11347        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11348        let guard = result_arc.read().unwrap();
11349        assert_eq!(guard.dtype, DType::BF16);
11350        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11351            panic!("expected Cpu storage");
11352        };
11353        let r: &[half::bf16] = c.as_slice().unwrap();
11354        let rms = (12.5_f32).sqrt();
11355        // bf16's ~3-digit mantissa absorbs the divisor; allow ~5%.
11356        assert!((r[0].to_f32() - 3.0 / rms).abs() < 0.05);
11357        assert!((r[1].to_f32() - 4.0 / rms).abs() < 0.05);
11358    }
11359
11360    /// E2E: BF16 matmul through the pipelined executor — proves
11361    /// the LLM forward-pass blocker (every transformer layer
11362    /// is dominated by matmul). Identity matmul on bf16 round-
11363    /// trips small integers exactly.
11364    #[test]
11365    fn pipelined_realize_matmul_bf16_identity() {
11366        let lhs_v: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0]
11367            .iter()
11368            .map(|&x| half::bf16::from_f32(x))
11369            .collect();
11370        let rhs_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0]
11371            .iter()
11372            .map(|&x| half::bf16::from_f32(x))
11373            .collect();
11374        let lhs = fuel_memory::from_slice_cpu(&lhs_v);
11375        let rhs = fuel_memory::from_slice_cpu(&rhs_v);
11376        let graph = Arc::new(RwLock::new(Graph::new()));
11377        let (l_id, r_id, mm_id) = {
11378            let mut g = graph.write().unwrap();
11379            let l = g.push(Node {
11380                op: Op::Const,
11381                inputs: vec![],
11382                shape: Shape::from_dims(&[2, 2]),
11383                dtype: DType::BF16,
11384            });
11385            let r = g.push(Node {
11386                op: Op::Const,
11387                inputs: vec![],
11388                shape: Shape::from_dims(&[2, 2]),
11389                dtype: DType::BF16,
11390            });
11391            let mm = g.push(Node {
11392                op: Op::MatMul,
11393                inputs: vec![l, r],
11394                shape: Shape::from_dims(&[2, 2]),
11395                dtype: DType::BF16,
11396            });
11397            g.set_target_backend(mm, BackendId::Cpu);
11398            (l, r, mm)
11399        };
11400        let mut inputs = StorageCache::new();
11401        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
11402        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
11403        let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11404        let guard = result_arc.read().unwrap();
11405        assert_eq!(guard.dtype, DType::BF16);
11406        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11407            panic!("expected Cpu storage");
11408        };
11409        let r: &[half::bf16] = c.as_slice().unwrap();
11410        let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11411        assert_eq!(result_f32, vec![1.0, 2.0, 3.0, 4.0]);
11412    }
11413
11414    /// E2E: F16 sum-reduce — verifies bf16/f16 reduction dispatch
11415    /// works through the executor.
11416    #[test]
11417    fn pipelined_realize_sum_dim_f16() {
11418        let v: Vec<half::f16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]
11419            .iter()
11420            .map(|&x| half::f16::from_f32(x))
11421            .collect();
11422        let storage = fuel_memory::from_slice_cpu(&v);
11423        let graph = Arc::new(RwLock::new(Graph::new()));
11424        let (in_id, sum_id) = {
11425            let mut g = graph.write().unwrap();
11426            let in_id = g.push(Node {
11427                op: Op::Const,
11428                inputs: vec![],
11429                shape: Shape::from_dims(&[2, 3]),
11430                dtype: DType::F16,
11431            });
11432            let sum_id = g.push(Node {
11433                op: Op::SumDim(1),
11434                inputs: vec![in_id],
11435                shape: Shape::from_dims(&[2]),
11436                dtype: DType::F16,
11437            });
11438            g.set_target_backend(sum_id, BackendId::Cpu);
11439            (in_id, sum_id)
11440        };
11441        let mut inputs = StorageCache::new();
11442        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11443        let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
11444        let guard = result_arc.read().unwrap();
11445        assert_eq!(guard.dtype, DType::F16);
11446        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11447            panic!("expected Cpu storage");
11448        };
11449        let r: &[half::f16] = c.as_slice().unwrap();
11450        let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11451        assert_eq!(result_f32, vec![6.0, 15.0]);
11452    }
11453
11454    /// E2E: BF16 elementwise add through the pipelined executor.
11455    #[test]
11456    fn pipelined_realize_add_bf16() {
11457        let lhs_vec: Vec<half::bf16> = [1.0_f32, 2.0, 3.0]
11458            .iter()
11459            .map(|&x| half::bf16::from_f32(x))
11460            .collect();
11461        let rhs_vec: Vec<half::bf16> = [10.0_f32, 20.0, 30.0]
11462            .iter()
11463            .map(|&x| half::bf16::from_f32(x))
11464            .collect();
11465        let lhs = fuel_memory::from_slice_cpu(&lhs_vec);
11466        let rhs = fuel_memory::from_slice_cpu(&rhs_vec);
11467        let graph = Arc::new(RwLock::new(Graph::new()));
11468        let (l_id, r_id, op_id) = {
11469            let mut g = graph.write().unwrap();
11470            let l = g.push(Node {
11471                op: Op::Const,
11472                inputs: vec![],
11473                shape: Shape::from_dims(&[3]),
11474                dtype: DType::BF16,
11475            });
11476            let r = g.push(Node {
11477                op: Op::Const,
11478                inputs: vec![],
11479                shape: Shape::from_dims(&[3]),
11480                dtype: DType::BF16,
11481            });
11482            let op = g.push(Node {
11483                op: Op::Add,
11484                inputs: vec![l, r],
11485                shape: Shape::from_dims(&[3]),
11486                dtype: DType::BF16,
11487            });
11488            g.set_target_backend(op, BackendId::Cpu);
11489            (l, r, op)
11490        };
11491        let mut inputs = StorageCache::new();
11492        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
11493        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
11494        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11495        let guard = result_arc.read().unwrap();
11496        assert_eq!(guard.dtype, DType::BF16);
11497        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11498            panic!("expected Cpu storage");
11499        };
11500        let r: &[half::bf16] = c.as_slice().unwrap();
11501        let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11502        assert_eq!(result_f32, vec![11.0, 22.0, 33.0]);
11503    }
11504
11505    /// E2E: F16 unary chain — Const + Sqr + Sqrt — verifies F16
11506    /// dispatch works and the via-f32 round-trip kernels behave
11507    /// correctly through the executor.
11508    #[test]
11509    fn pipelined_realize_sqr_then_sqrt_f16() {
11510        let v: Vec<half::f16> = [1.0_f32, 4.0, 9.0]
11511            .iter()
11512            .map(|&x| half::f16::from_f32(x))
11513            .collect();
11514        let storage = fuel_memory::from_slice_cpu(&v);
11515        let graph = Arc::new(RwLock::new(Graph::new()));
11516        let (in_id, sqrt_id) = {
11517            let mut g = graph.write().unwrap();
11518            let in_id = g.push(Node {
11519                op: Op::Const,
11520                inputs: vec![],
11521                shape: Shape::from_dims(&[3]),
11522                dtype: DType::F16,
11523            });
11524            let sqr_id = g.push(Node {
11525                op: Op::Sqr,
11526                inputs: vec![in_id],
11527                shape: Shape::from_dims(&[3]),
11528                dtype: DType::F16,
11529            });
11530            let sqrt_id = g.push(Node {
11531                op: Op::Sqrt,
11532                inputs: vec![sqr_id],
11533                shape: Shape::from_dims(&[3]),
11534                dtype: DType::F16,
11535            });
11536            g.set_target_backend(sqr_id, BackendId::Cpu);
11537            g.set_target_backend(sqrt_id, BackendId::Cpu);
11538            (in_id, sqrt_id)
11539        };
11540        let mut inputs = StorageCache::new();
11541        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11542        let (result_arc, _) = PipelinedExecutor::realize(graph, sqrt_id, inputs).expect("realize");
11543        let guard = result_arc.read().unwrap();
11544        assert_eq!(guard.dtype, DType::F16);
11545        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11546            panic!("expected Cpu storage");
11547        };
11548        let r: &[half::f16] = c.as_slice().unwrap();
11549        let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11550        // f16 has ~3 decimal digits; sqrt(sqr(x)) = |x| within rounding.
11551        for (got, want) in result_f32.iter().zip(&[1.0_f32, 4.0, 9.0]) {
11552            assert!((got - want).abs() < 0.05, "got {got}, want {want}");
11553        }
11554    }
11555
11556    /// E2E: F64 sum-reduce along one dim through the pipelined
11557    /// executor.
11558    #[test]
11559    fn pipelined_realize_sum_dim_f64() {
11560        let storage = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
11561        let graph = Arc::new(RwLock::new(Graph::new()));
11562        let (in_id, sum_id) = {
11563            let mut g = graph.write().unwrap();
11564            let in_id = g.push(Node {
11565                op: Op::Const,
11566                inputs: vec![],
11567                shape: Shape::from_dims(&[2, 3]),
11568                dtype: DType::F64,
11569            });
11570            let sum_id = g.push(Node {
11571                op: Op::SumDim(1),
11572                inputs: vec![in_id],
11573                shape: Shape::from_dims(&[2]),
11574                dtype: DType::F64,
11575            });
11576            g.set_target_backend(sum_id, BackendId::Cpu);
11577            (in_id, sum_id)
11578        };
11579        let mut inputs = StorageCache::new();
11580        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11581        let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
11582        let guard = result_arc.read().unwrap();
11583        assert_eq!(guard.dtype, DType::F64);
11584        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11585            panic!("expected Cpu storage");
11586        };
11587        assert_eq!(c.as_slice::<f64>().unwrap(), &[6.0_f64, 15.0]);
11588    }
11589
11590    /// E2E: F64 matmul through the pipelined executor.
11591    #[test]
11592    fn pipelined_realize_matmul_2x3_times_3x2_f64() {
11593        let lhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
11594        let rhs = fuel_memory::from_slice_cpu(&[7.0_f64, 8.0, 9.0, 10.0, 11.0, 12.0]);
11595        let graph = Arc::new(RwLock::new(Graph::new()));
11596        let (l_id, r_id, mm_id) = {
11597            let mut g = graph.write().unwrap();
11598            let l = g.push(Node {
11599                op: Op::Const,
11600                inputs: vec![],
11601                shape: Shape::from_dims(&[2, 3]),
11602                dtype: DType::F64,
11603            });
11604            let r = g.push(Node {
11605                op: Op::Const,
11606                inputs: vec![],
11607                shape: Shape::from_dims(&[3, 2]),
11608                dtype: DType::F64,
11609            });
11610            let mm = g.push(Node {
11611                op: Op::MatMul,
11612                inputs: vec![l, r],
11613                shape: Shape::from_dims(&[2, 2]),
11614                dtype: DType::F64,
11615            });
11616            g.set_target_backend(mm, BackendId::Cpu);
11617            (l, r, mm)
11618        };
11619        let mut inputs = StorageCache::new();
11620        inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
11621        inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
11622        let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11623        let guard = result_arc.read().unwrap();
11624        assert_eq!(guard.dtype, DType::F64);
11625        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11626            panic!("expected Cpu storage");
11627        };
11628        assert_eq!(
11629            c.as_slice::<f64>().unwrap(),
11630            &[58.0_f64, 64.0, 139.0, 154.0]
11631        );
11632    }
11633
11634    /// E2E: F64 unary chain — Const + Sqr + Sqrt — verifies that
11635    /// the kernel-binding lookup picks the f64 entries when the
11636    /// graph nodes carry DType::F64.
11637    #[test]
11638    fn pipelined_realize_sqr_then_sqrt_f64() {
11639        let storage = fuel_memory::from_slice_cpu(&[1.0_f64, 4.0, 9.0]);
11640        let graph = Arc::new(RwLock::new(Graph::new()));
11641        let (in_id, sqrt_id) = {
11642            let mut g = graph.write().unwrap();
11643            let in_id = g.push(Node {
11644                op: Op::Const,
11645                inputs: vec![],
11646                shape: Shape::from_dims(&[3]),
11647                dtype: DType::F64,
11648            });
11649            let sqr_id = g.push(Node {
11650                op: Op::Sqr,
11651                inputs: vec![in_id],
11652                shape: Shape::from_dims(&[3]),
11653                dtype: DType::F64,
11654            });
11655            let sqrt_id = g.push(Node {
11656                op: Op::Sqrt,
11657                inputs: vec![sqr_id],
11658                shape: Shape::from_dims(&[3]),
11659                dtype: DType::F64,
11660            });
11661            g.set_target_backend(sqr_id, BackendId::Cpu);
11662            g.set_target_backend(sqrt_id, BackendId::Cpu);
11663            (in_id, sqrt_id)
11664        };
11665        let mut inputs = StorageCache::new();
11666        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11667        let (result_arc, _) = PipelinedExecutor::realize(graph, sqrt_id, inputs).expect("realize");
11668        let guard = result_arc.read().unwrap();
11669        assert_eq!(guard.dtype, DType::F64);
11670        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11671            panic!("expected Cpu storage");
11672        };
11673        assert_eq!(c.as_slice::<f64>().unwrap(), &[1.0_f64, 4.0, 9.0]);
11674    }
11675
11676    /// E2E: ArgMaxDim — produces U32 output indices.
11677    #[test]
11678    fn pipelined_realize_argmax_dim() {
11679        // input [2, 3] = [[1, 5, 2], [9, 0, 4]]
11680        // argmax dim=1 → [1, 0]
11681        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 2.0, 9.0, 0.0, 4.0]);
11682        let graph = Arc::new(RwLock::new(Graph::new()));
11683        let (in_id, op_id) = {
11684            let mut g = graph.write().unwrap();
11685            let in_id = g.push(Node {
11686                op: Op::Const,
11687                inputs: vec![],
11688                shape: Shape::from_dims(&[2, 3]),
11689                dtype: DType::F32,
11690            });
11691            let op_id = g.push(Node {
11692                op: Op::ArgMaxDim(1),
11693                inputs: vec![in_id],
11694                shape: Shape::from_dims(&[2]),
11695                dtype: DType::U32,
11696            });
11697            g.set_target_backend(op_id, BackendId::Cpu);
11698            (in_id, op_id)
11699        };
11700        let mut inputs = StorageCache::new();
11701        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11702        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11703        let guard = result_arc.read().unwrap();
11704        assert_eq!(guard.dtype, DType::U32);
11705        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11706            panic!("expected Cpu storage");
11707        };
11708        assert_eq!(c.as_slice::<u32>().unwrap(), &[1u32, 0]);
11709    }
11710
11711    /// E2E: IndexAdd along outer dim — accumulate updates into a
11712    /// rank-1 base tensor at indexed positions.
11713    #[test]
11714    fn pipelined_realize_index_add_simple() {
11715        let base = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
11716        let indices = fuel_memory::from_slice_cpu(&[0u32, 0]);
11717        let src = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
11718        let graph = Arc::new(RwLock::new(Graph::new()));
11719        let (b_id, i_id, s_id, op_id) = {
11720            let mut g = graph.write().unwrap();
11721            let b = g.push(Node {
11722                op: Op::Const,
11723                inputs: vec![],
11724                shape: Shape::from_dims(&[3]),
11725                dtype: DType::F32,
11726            });
11727            let i = g.push(Node {
11728                op: Op::Const,
11729                inputs: vec![],
11730                shape: Shape::from_dims(&[2]),
11731                dtype: DType::U32,
11732            });
11733            let s = g.push(Node {
11734                op: Op::Const,
11735                inputs: vec![],
11736                shape: Shape::from_dims(&[2]),
11737                dtype: DType::F32,
11738            });
11739            let op = g.push(Node {
11740                op: Op::IndexAdd { dim: 0 },
11741                inputs: vec![b, i, s],
11742                shape: Shape::from_dims(&[3]),
11743                dtype: DType::F32,
11744            });
11745            g.set_target_backend(op, BackendId::Cpu);
11746            (b, i, s, op)
11747        };
11748        let mut inputs = StorageCache::new();
11749        inputs.insert(b_id, Arc::new(RwLock::new(base)));
11750        inputs.insert(i_id, Arc::new(RwLock::new(indices)));
11751        inputs.insert(s_id, Arc::new(RwLock::new(src)));
11752        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11753        let guard = result_arc.read().unwrap();
11754        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11755            panic!("expected Cpu storage");
11756        };
11757        // 10 + 1 + 2 = 13; 20 untouched; 30 untouched.
11758        assert_eq!(c.as_slice::<f32>().unwrap(), &[13.0, 20.0, 30.0]);
11759    }
11760
11761    /// E2E: ScatterAdd along outer dim — same-rank indices, base
11762    /// starts as zeros, src adds values at scatter positions.
11763    #[test]
11764    fn pipelined_realize_scatter_add_outer_dim() {
11765        // base [3, 2] = zeros; indices [2, 2] = [[0, 1], [2, 0]];
11766        // src [2, 2] = [[1, 2], [3, 4]]; dim=0
11767        // → out = [[1, 4], [0, 2], [3, 0]]
11768        let base = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
11769        let indices = fuel_memory::from_slice_cpu(&[0u32, 1, 2, 0]);
11770        let src = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
11771        let graph = Arc::new(RwLock::new(Graph::new()));
11772        let (b_id, i_id, s_id, op_id) = {
11773            let mut g = graph.write().unwrap();
11774            let b = g.push(Node {
11775                op: Op::Const,
11776                inputs: vec![],
11777                shape: Shape::from_dims(&[3, 2]),
11778                dtype: DType::F32,
11779            });
11780            let i = g.push(Node {
11781                op: Op::Const,
11782                inputs: vec![],
11783                shape: Shape::from_dims(&[2, 2]),
11784                dtype: DType::U32,
11785            });
11786            let s = g.push(Node {
11787                op: Op::Const,
11788                inputs: vec![],
11789                shape: Shape::from_dims(&[2, 2]),
11790                dtype: DType::F32,
11791            });
11792            let op = g.push(Node {
11793                op: Op::ScatterAdd { dim: 0 },
11794                inputs: vec![b, i, s],
11795                shape: Shape::from_dims(&[3, 2]),
11796                dtype: DType::F32,
11797            });
11798            g.set_target_backend(op, BackendId::Cpu);
11799            (b, i, s, op)
11800        };
11801        let mut inputs = StorageCache::new();
11802        inputs.insert(b_id, Arc::new(RwLock::new(base)));
11803        inputs.insert(i_id, Arc::new(RwLock::new(indices)));
11804        inputs.insert(s_id, Arc::new(RwLock::new(src)));
11805        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11806        let guard = result_arc.read().unwrap();
11807        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11808            panic!("expected Cpu storage");
11809        };
11810        assert_eq!(
11811            c.as_slice::<f32>().unwrap(),
11812            &[1.0, 4.0, 0.0, 2.0, 3.0, 0.0]
11813        );
11814    }
11815
11816    /// E2E: Rope through the pipelined executor. cos=0, sin=1
11817    /// rotates the head_dim halves with sign per the rotate_half
11818    /// convention.
11819    #[test]
11820    fn pipelined_realize_rope_pi_over_two() {
11821        // x [1, 1, 4] = [1, 2, 3, 4]. cos=[0,0,0,0], sin=[1,1,1,1].
11822        // Expected: [-3, -4, 1, 2].
11823        let x = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
11824        let cos = fuel_memory::from_slice_cpu(&[0.0_f32, 0.0, 0.0, 0.0]);
11825        let sin = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
11826        let graph = Arc::new(RwLock::new(Graph::new()));
11827        let (x_id, cos_id, sin_id, r_id) = {
11828            let mut g = graph.write().unwrap();
11829            let x = g.push(Node {
11830                op: Op::Const,
11831                inputs: vec![],
11832                shape: Shape::from_dims(&[1, 1, 4]),
11833                dtype: DType::F32,
11834            });
11835            let c = g.push(Node {
11836                op: Op::Const,
11837                inputs: vec![],
11838                shape: Shape::from_dims(&[1, 4]),
11839                dtype: DType::F32,
11840            });
11841            let s = g.push(Node {
11842                op: Op::Const,
11843                inputs: vec![],
11844                shape: Shape::from_dims(&[1, 4]),
11845                dtype: DType::F32,
11846            });
11847            let r = g.push(Node {
11848                op: Op::Fused(
11849                    fuel_graph::registry::FusedOps::ROPE,
11850                    fuel_graph::registry::FusedOpParams::Rope,
11851                ),
11852                inputs: vec![x, c, s],
11853                shape: Shape::from_dims(&[1, 1, 4]),
11854                dtype: DType::F32,
11855            });
11856            g.set_target_backend(r, BackendId::Cpu);
11857            (x, c, s, r)
11858        };
11859        let mut inputs = StorageCache::new();
11860        inputs.insert(x_id, Arc::new(RwLock::new(x)));
11861        inputs.insert(cos_id, Arc::new(RwLock::new(cos)));
11862        inputs.insert(sin_id, Arc::new(RwLock::new(sin)));
11863        let (result_arc, _) = PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
11864        let guard = result_arc.read().unwrap();
11865        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11866            panic!("expected Cpu storage");
11867        };
11868        assert_eq!(c.as_slice::<f32>().unwrap(), &[-3.0, -4.0, 1.0, 2.0]);
11869    }
11870
11871    /// E2E: Gather along inner dim. Source [2, 4]; indices [2, 3];
11872    /// output [2, 3] = picks from each row by per-row indices.
11873    #[test]
11874    fn pipelined_realize_gather_inner_dim() {
11875        let source =
11876            fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0]);
11877        let indices = fuel_memory::from_slice_cpu(&[0u32, 2, 1, 3, 0, 0]);
11878        let graph = Arc::new(RwLock::new(Graph::new()));
11879        let (src_id, idx_id, g_id) = {
11880            let mut g = graph.write().unwrap();
11881            let s = g.push(Node {
11882                op: Op::Const,
11883                inputs: vec![],
11884                shape: Shape::from_dims(&[2, 4]),
11885                dtype: DType::F32,
11886            });
11887            let i = g.push(Node {
11888                op: Op::Const,
11889                inputs: vec![],
11890                shape: Shape::from_dims(&[2, 3]),
11891                dtype: DType::U32,
11892            });
11893            let g_id = g.push(Node {
11894                op: Op::Gather { dim: 1 },
11895                inputs: vec![s, i],
11896                shape: Shape::from_dims(&[2, 3]),
11897                dtype: DType::F32,
11898            });
11899            g.set_target_backend(g_id, BackendId::Cpu);
11900            (s, i, g_id)
11901        };
11902        let mut inputs = StorageCache::new();
11903        inputs.insert(src_id, Arc::new(RwLock::new(source)));
11904        inputs.insert(idx_id, Arc::new(RwLock::new(indices)));
11905        let (result_arc, _) = PipelinedExecutor::realize(graph, g_id, inputs).expect("realize");
11906        let guard = result_arc.read().unwrap();
11907        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11908            panic!("expected Cpu storage");
11909        };
11910        assert_eq!(
11911            c.as_slice::<f32>().unwrap(),
11912            &[10.0, 30.0, 20.0, 80.0, 50.0, 50.0]
11913        );
11914    }
11915
11916    /// E2E: IndexSelect — embedding-table lookup. Source is a
11917    /// `[vocab=4, d_model=3]` table; indices are token IDs;
11918    /// output is `[seq=3, d_model=3]` with the picked rows.
11919    #[test]
11920    fn pipelined_realize_index_select_embedding_lookup() {
11921        let table = fuel_memory::from_slice_cpu(&[
11922            10.0_f32, 11.0, 12.0, // row 0
11923            20.0, 21.0, 22.0, // row 1
11924            30.0, 31.0, 32.0, // row 2
11925            40.0, 41.0, 42.0, // row 3
11926        ]);
11927        let indices = fuel_memory::from_slice_cpu(&[2u32, 0, 2]);
11928        let graph = Arc::new(RwLock::new(Graph::new()));
11929        let (table_id, idx_id, sel_id) = {
11930            let mut g = graph.write().unwrap();
11931            let t = g.push(Node {
11932                op: Op::Const,
11933                inputs: vec![],
11934                shape: Shape::from_dims(&[4, 3]),
11935                dtype: DType::F32,
11936            });
11937            let i = g.push(Node {
11938                op: Op::Const,
11939                inputs: vec![],
11940                shape: Shape::from_dims(&[3]),
11941                dtype: DType::U32,
11942            });
11943            let s = g.push(Node {
11944                op: Op::IndexSelect { dim: 0 },
11945                inputs: vec![t, i],
11946                shape: Shape::from_dims(&[3, 3]),
11947                dtype: DType::F32,
11948            });
11949            g.set_target_backend(s, BackendId::Cpu);
11950            (t, i, s)
11951        };
11952        let mut inputs = StorageCache::new();
11953        inputs.insert(table_id, Arc::new(RwLock::new(table)));
11954        inputs.insert(idx_id, Arc::new(RwLock::new(indices)));
11955        let (result_arc, result_layout) =
11956            PipelinedExecutor::realize(graph, sel_id, inputs).expect("realize");
11957        assert_eq!(result_layout.shape().dims(), &[3, 3]);
11958        let guard = result_arc.read().unwrap();
11959        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11960            panic!("expected Cpu storage");
11961        };
11962        assert_eq!(
11963            c.as_slice::<f32>().unwrap(),
11964            &[30.0, 31.0, 32.0, 10.0, 11.0, 12.0, 30.0, 31.0, 32.0]
11965        );
11966    }
11967
11968    /// E2E: RmsNormLastDim on a 2-row input. Each row's output
11969    /// has unit RMS up to the eps-induced bias.
11970    #[test]
11971    fn pipelined_realize_rms_norm_last_dim() {
11972        let storage = fuel_memory::from_slice_cpu(&[
11973            3.0_f32, 4.0, // row 0: rms = sqrt(12.5)
11974            6.0, 8.0, // row 1: rms = sqrt(50.0) = 5*sqrt(2)
11975        ]);
11976        let graph = Arc::new(RwLock::new(Graph::new()));
11977        let (in_id, op_id) = {
11978            let mut g = graph.write().unwrap();
11979            let in_id = g.push(Node {
11980                op: Op::Const,
11981                inputs: vec![],
11982                shape: Shape::from_dims(&[2, 2]),
11983                dtype: DType::F32,
11984            });
11985            let op_id = g.push(Node {
11986                op: Op::Fused(
11987                    fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM,
11988                    fuel_graph::registry::FusedOpParams::RmsNormLastDim { eps: 0.0 },
11989                ),
11990                inputs: vec![in_id],
11991                shape: Shape::from_dims(&[2, 2]),
11992                dtype: DType::F32,
11993            });
11994            g.set_target_backend(op_id, BackendId::Cpu);
11995            (in_id, op_id)
11996        };
11997        let mut inputs = StorageCache::new();
11998        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11999        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12000        let guard = result_arc.read().unwrap();
12001        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12002            panic!("expected Cpu storage");
12003        };
12004        let result: &[f32] = c.as_slice().unwrap();
12005        // Row 0: rms = sqrt(12.5). Output = [3, 4] / sqrt(12.5).
12006        let rms0 = (12.5_f32).sqrt();
12007        assert!((result[0] - 3.0 / rms0).abs() < 1e-6);
12008        assert!((result[1] - 4.0 / rms0).abs() < 1e-6);
12009        // Row 1: rms = sqrt(50). Output = [6, 8] / sqrt(50).
12010        let rms1 = (50.0_f32).sqrt();
12011        assert!((result[2] - 6.0 / rms1).abs() < 1e-6);
12012        assert!((result[3] - 8.0 / rms1).abs() < 1e-6);
12013    }
12014
12015    /// E2E: LayerNormLastDim — each row's output has zero mean and
12016    /// unit variance.
12017    #[test]
12018    fn pipelined_realize_layer_norm_last_dim() {
12019        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0]);
12020        let graph = Arc::new(RwLock::new(Graph::new()));
12021        let (in_id, op_id) = {
12022            let mut g = graph.write().unwrap();
12023            let in_id = g.push(Node {
12024                op: Op::Const,
12025                inputs: vec![],
12026                shape: Shape::from_dims(&[2, 3]),
12027                dtype: DType::F32,
12028            });
12029            let op_id = g.push(Node {
12030                op: Op::Fused(
12031                    fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM,
12032                    fuel_graph::registry::FusedOpParams::LayerNormLastDim { eps: 0.0 },
12033                ),
12034                inputs: vec![in_id],
12035                shape: Shape::from_dims(&[2, 3]),
12036                dtype: DType::F32,
12037            });
12038            g.set_target_backend(op_id, BackendId::Cpu);
12039            (in_id, op_id)
12040        };
12041        let mut inputs = StorageCache::new();
12042        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12043        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12044        let guard = result_arc.read().unwrap();
12045        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12046            panic!("expected Cpu storage");
12047        };
12048        let result: &[f32] = c.as_slice().unwrap();
12049        // Each row should have mean ~0 and var ~1.
12050        for row in 0..2 {
12051            let off = row * 3;
12052            let sum: f32 = result[off..off + 3].iter().sum();
12053            let mean = sum / 3.0;
12054            assert!(mean.abs() < 1e-6, "row {row} mean should be 0, got {mean}");
12055            let var: f32 = result[off..off + 3]
12056                .iter()
12057                .map(|v| (v - mean).powi(2))
12058                .sum::<f32>()
12059                / 3.0;
12060            assert!(
12061                (var - 1.0).abs() < 1e-6,
12062                "row {row} var should be 1, got {var}"
12063            );
12064        }
12065    }
12066
12067    /// E2E: SoftmaxLastDim on a 2-row input. Each row should sum
12068    /// to 1; uniform row gives uniform output.
12069    #[test]
12070    fn pipelined_realize_softmax_last_dim() {
12071        // Row 0: [1, 1, 1, 1] → uniform 0.25 each
12072        // Row 1: [0, 0, 0, 100] → effectively a one-hot at position 3
12073        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 100.0]);
12074        let graph = Arc::new(RwLock::new(Graph::new()));
12075        let (in_id, op_id) = {
12076            let mut g = graph.write().unwrap();
12077            let in_id = g.push(Node {
12078                op: Op::Const,
12079                inputs: vec![],
12080                shape: Shape::from_dims(&[2, 4]),
12081                dtype: DType::F32,
12082            });
12083            let op_id = g.push(Node {
12084                op: Op::Fused(
12085                    fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM,
12086                    fuel_graph::registry::FusedOpParams::SoftmaxLastDim,
12087                ),
12088                inputs: vec![in_id],
12089                shape: Shape::from_dims(&[2, 4]),
12090                dtype: DType::F32,
12091            });
12092            g.set_target_backend(op_id, BackendId::Cpu);
12093            (in_id, op_id)
12094        };
12095        let mut inputs = StorageCache::new();
12096        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12097        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12098        let guard = result_arc.read().unwrap();
12099        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12100            panic!("expected Cpu storage");
12101        };
12102        let result: &[f32] = c.as_slice().unwrap();
12103
12104        // Row 0: uniform 0.25
12105        for v in &result[..4] {
12106            assert!((v - 0.25).abs() < 1e-7);
12107        }
12108        // Row 1: positions 0..3 ≈ 0, position 4 (= last column) ≈ 1
12109        // (e^100 dominates).
12110        for v in &result[4..7] {
12111            assert!(
12112                *v < 1e-30,
12113                "row-1 leading positions should be near 0, got {v}"
12114            );
12115        }
12116        assert!(
12117            result[7] > 0.999,
12118            "row-1 last position should dominate, got {}",
12119            result[7]
12120        );
12121        // Each row sums to 1.
12122        let row0_sum: f32 = result[..4].iter().sum();
12123        let row1_sum: f32 = result[4..].iter().sum();
12124        assert!((row0_sum - 1.0).abs() < 1e-6);
12125        assert!((row1_sum - 1.0).abs() < 1e-6);
12126    }
12127
12128    /// E2E: Concat along inner dim — two [2, 3] tensors → [2, 6].
12129    #[test]
12130    fn pipelined_realize_concat_inner_dim() {
12131        let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
12132        let b = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0, 9.0, 10.0, 11.0, 12.0]);
12133        let graph = Arc::new(RwLock::new(Graph::new()));
12134        let (a_id, b_id, c_id) = {
12135            let mut g = graph.write().unwrap();
12136            let a = g.push(Node {
12137                op: Op::Const,
12138                inputs: vec![],
12139                shape: Shape::from_dims(&[2, 3]),
12140                dtype: DType::F32,
12141            });
12142            let b = g.push(Node {
12143                op: Op::Const,
12144                inputs: vec![],
12145                shape: Shape::from_dims(&[2, 3]),
12146                dtype: DType::F32,
12147            });
12148            let c = g.push(Node {
12149                op: Op::Concat { dim: 1 },
12150                inputs: vec![a, b],
12151                shape: Shape::from_dims(&[2, 6]),
12152                dtype: DType::F32,
12153            });
12154            g.set_target_backend(c, BackendId::Cpu);
12155            (a, b, c)
12156        };
12157        let mut inputs = StorageCache::new();
12158        inputs.insert(a_id, Arc::new(RwLock::new(a)));
12159        inputs.insert(b_id, Arc::new(RwLock::new(b)));
12160        let (result_arc, result_layout) =
12161            PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12162        assert_eq!(result_layout.shape().dims(), &[2, 6]);
12163        let guard = result_arc.read().unwrap();
12164        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12165            panic!("expected Cpu storage");
12166        };
12167        assert_eq!(
12168            c.as_slice::<f32>().unwrap(),
12169            &[
12170                1.0, 2.0, 3.0, 7.0, 8.0, 9.0, 4.0, 5.0, 6.0, 10.0, 11.0, 12.0
12171            ]
12172        );
12173    }
12174
12175    /// E2E: Concat with three inputs along outer dim — verifies
12176    /// variable-arity input handling through the executor.
12177    #[test]
12178    fn pipelined_realize_concat_three_inputs_outer() {
12179        let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
12180        let b = fuel_memory::from_slice_cpu(&[3.0_f32, 4.0]);
12181        let c = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0]);
12182        let graph = Arc::new(RwLock::new(Graph::new()));
12183        let (a_id, b_id, c_id, cat_id) = {
12184            let mut g = graph.write().unwrap();
12185            let a = g.push(Node {
12186                op: Op::Const,
12187                inputs: vec![],
12188                shape: Shape::from_dims(&[2]),
12189                dtype: DType::F32,
12190            });
12191            let b = g.push(Node {
12192                op: Op::Const,
12193                inputs: vec![],
12194                shape: Shape::from_dims(&[2]),
12195                dtype: DType::F32,
12196            });
12197            let c = g.push(Node {
12198                op: Op::Const,
12199                inputs: vec![],
12200                shape: Shape::from_dims(&[2]),
12201                dtype: DType::F32,
12202            });
12203            let cat = g.push(Node {
12204                op: Op::Concat { dim: 0 },
12205                inputs: vec![a, b, c],
12206                shape: Shape::from_dims(&[6]),
12207                dtype: DType::F32,
12208            });
12209            g.set_target_backend(cat, BackendId::Cpu);
12210            (a, b, c, cat)
12211        };
12212        let mut inputs = StorageCache::new();
12213        inputs.insert(a_id, Arc::new(RwLock::new(a)));
12214        inputs.insert(b_id, Arc::new(RwLock::new(b)));
12215        inputs.insert(c_id, Arc::new(RwLock::new(c)));
12216        let (result_arc, _) = PipelinedExecutor::realize(graph, cat_id, inputs).expect("realize");
12217        let guard = result_arc.read().unwrap();
12218        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12219            panic!("expected Cpu storage");
12220        };
12221        assert_eq!(
12222            c.as_slice::<f32>().unwrap(),
12223            &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
12224        );
12225    }
12226
12227    /// E2E: AddScalar — graph emits Op::AddScalar; the executor
12228    /// maps it to OpKind::Affine with mul=1, add=c.
12229    #[test]
12230    fn pipelined_realize_add_scalar() {
12231        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
12232        let graph = Arc::new(RwLock::new(Graph::new()));
12233        let (in_id, op_id) = {
12234            let mut g = graph.write().unwrap();
12235            let in_id = g.push(Node {
12236                op: Op::Const,
12237                inputs: vec![],
12238                shape: Shape::from_dims(&[3]),
12239                dtype: DType::F32,
12240            });
12241            let op_id = g.push(Node {
12242                op: Op::AddScalar(10.0),
12243                inputs: vec![in_id],
12244                shape: Shape::from_dims(&[3]),
12245                dtype: DType::F32,
12246            });
12247            g.set_target_backend(op_id, BackendId::Cpu);
12248            (in_id, op_id)
12249        };
12250        let mut inputs = StorageCache::new();
12251        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12252        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12253        let guard = result_arc.read().unwrap();
12254        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12255            panic!("expected Cpu storage");
12256        };
12257        assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 12.0, 13.0]);
12258    }
12259
12260    /// E2E: Clamp — clamp values to [-2, 2].
12261    #[test]
12262    fn pipelined_realize_clamp() {
12263        let storage = fuel_memory::from_slice_cpu(&[-5.0_f32, 0.5, 100.0]);
12264        let graph = Arc::new(RwLock::new(Graph::new()));
12265        let (in_id, op_id) = {
12266            let mut g = graph.write().unwrap();
12267            let in_id = g.push(Node {
12268                op: Op::Const,
12269                inputs: vec![],
12270                shape: Shape::from_dims(&[3]),
12271                dtype: DType::F32,
12272            });
12273            let op_id = g.push(Node {
12274                op: Op::Clamp {
12275                    min: -2.0,
12276                    max: 2.0,
12277                },
12278                inputs: vec![in_id],
12279                shape: Shape::from_dims(&[3]),
12280                dtype: DType::F32,
12281            });
12282            g.set_target_backend(op_id, BackendId::Cpu);
12283            (in_id, op_id)
12284        };
12285        let mut inputs = StorageCache::new();
12286        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12287        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12288        let guard = result_arc.read().unwrap();
12289        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12290            panic!("expected Cpu storage");
12291        };
12292        assert_eq!(c.as_slice::<f32>().unwrap(), &[-2.0, 0.5, 2.0]);
12293    }
12294
12295    /// E2E: Maximum — elementwise tensor max.
12296    #[test]
12297    fn pipelined_realize_maximum_elementwise() {
12298        let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, -3.0]);
12299        let rhs_storage = fuel_memory::from_slice_cpu(&[2.0_f32, 1.0, -1.0]);
12300        let graph = Arc::new(RwLock::new(Graph::new()));
12301        let (lhs_id, rhs_id, op_id) = {
12302            let mut g = graph.write().unwrap();
12303            let lhs = g.push(Node {
12304                op: Op::Const,
12305                inputs: vec![],
12306                shape: Shape::from_dims(&[3]),
12307                dtype: DType::F32,
12308            });
12309            let rhs = g.push(Node {
12310                op: Op::Const,
12311                inputs: vec![],
12312                shape: Shape::from_dims(&[3]),
12313                dtype: DType::F32,
12314            });
12315            let op = g.push(Node {
12316                op: Op::Maximum,
12317                inputs: vec![lhs, rhs],
12318                shape: Shape::from_dims(&[3]),
12319                dtype: DType::F32,
12320            });
12321            g.set_target_backend(op, BackendId::Cpu);
12322            (lhs, rhs, op)
12323        };
12324        let mut inputs = StorageCache::new();
12325        inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
12326        inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
12327        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12328        let guard = result_arc.read().unwrap();
12329        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12330            panic!("expected Cpu storage");
12331        };
12332        assert_eq!(c.as_slice::<f32>().unwrap(), &[2.0, 5.0, -1.0]);
12333    }
12334
12335    /// E2E: Const + Const + Conv2D — the 2×2 sum-kernel test from
12336    /// byte_kernels driven through the pipelined executor.
12337    #[test]
12338    fn pipelined_realize_conv2d_2x2_sum_kernel() {
12339        // x [1, 1, 3, 3]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
12340        // weight [1, 1, 2, 2]: all-ones
12341        // → out [1, 1, 2, 2]: [[12, 16], [24, 28]]
12342        let x_storage =
12343            fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
12344        let w_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
12345
12346        let graph = Arc::new(RwLock::new(Graph::new()));
12347        let (x_id, w_id, c_id) = {
12348            let mut g = graph.write().unwrap();
12349            let x = g.push(Node {
12350                op: Op::Const,
12351                inputs: vec![],
12352                shape: Shape::from_dims(&[1, 1, 3, 3]),
12353                dtype: DType::F32,
12354            });
12355            let w = g.push(Node {
12356                op: Op::Const,
12357                inputs: vec![],
12358                shape: Shape::from_dims(&[1, 1, 2, 2]),
12359                dtype: DType::F32,
12360            });
12361            let c = g.push(Node {
12362                op: Op::Fused(
12363                    fuel_graph::registry::FusedOps::CONV2D,
12364                    fuel_graph::registry::FusedOpParams::Conv2D {
12365                        stride: (1, 1),
12366                        padding: (0, 0),
12367                        groups: 1,
12368                    },
12369                ),
12370                inputs: vec![x, w],
12371                shape: Shape::from_dims(&[1, 1, 2, 2]),
12372                dtype: DType::F32,
12373            });
12374            g.set_target_backend(c, BackendId::Cpu);
12375            (x, w, c)
12376        };
12377        let mut inputs = StorageCache::new();
12378        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12379        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12380
12381        let (result_arc, result_layout) =
12382            PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12383        assert_eq!(result_layout.shape().dims(), &[1, 1, 2, 2]);
12384
12385        let guard = result_arc.read().unwrap();
12386        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12387            panic!("expected Cpu storage");
12388        };
12389        assert_eq!(c.as_slice::<f32>().unwrap(), &[12.0, 16.0, 24.0, 28.0]);
12390    }
12391
12392    /// E2E: Conv2D with bias (3 inputs).
12393    #[test]
12394    fn pipelined_realize_conv2d_with_bias() {
12395        let x_storage =
12396            fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
12397        let w_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
12398        let bias_storage = fuel_memory::from_slice_cpu(&[100.0_f32]);
12399
12400        let graph = Arc::new(RwLock::new(Graph::new()));
12401        let (x_id, w_id, b_id, c_id) = {
12402            let mut g = graph.write().unwrap();
12403            let x = g.push(Node {
12404                op: Op::Const,
12405                inputs: vec![],
12406                shape: Shape::from_dims(&[1, 1, 3, 3]),
12407                dtype: DType::F32,
12408            });
12409            let w = g.push(Node {
12410                op: Op::Const,
12411                inputs: vec![],
12412                shape: Shape::from_dims(&[1, 1, 2, 2]),
12413                dtype: DType::F32,
12414            });
12415            let b = g.push(Node {
12416                op: Op::Const,
12417                inputs: vec![],
12418                shape: Shape::from_dims(&[1]),
12419                dtype: DType::F32,
12420            });
12421            let c = g.push(Node {
12422                op: Op::Fused(
12423                    fuel_graph::registry::FusedOps::CONV2D,
12424                    fuel_graph::registry::FusedOpParams::Conv2D {
12425                        stride: (1, 1),
12426                        padding: (0, 0),
12427                        groups: 1,
12428                    },
12429                ),
12430                inputs: vec![x, w, b],
12431                shape: Shape::from_dims(&[1, 1, 2, 2]),
12432                dtype: DType::F32,
12433            });
12434            g.set_target_backend(c, BackendId::Cpu);
12435            (x, w, b, c)
12436        };
12437        let mut inputs = StorageCache::new();
12438        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12439        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12440        inputs.insert(b_id, Arc::new(RwLock::new(bias_storage)));
12441
12442        let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12443        let guard = result_arc.read().unwrap();
12444        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12445            panic!("expected Cpu storage");
12446        };
12447        assert_eq!(c.as_slice::<f32>().unwrap(), &[112.0, 116.0, 124.0, 128.0]);
12448    }
12449
12450    /// E2E: Conv2D in F64 — same 2x2 sum-kernel test as F32, on doubles.
12451    #[test]
12452    fn pipelined_realize_conv2d_f64() {
12453        let x_storage =
12454            fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
12455        let w_storage = fuel_memory::from_slice_cpu(&[1.0_f64, 1.0, 1.0, 1.0]);
12456
12457        let graph = Arc::new(RwLock::new(Graph::new()));
12458        let (x_id, w_id, c_id) = {
12459            let mut g = graph.write().unwrap();
12460            let x = g.push(Node {
12461                op: Op::Const,
12462                inputs: vec![],
12463                shape: Shape::from_dims(&[1, 1, 3, 3]),
12464                dtype: DType::F64,
12465            });
12466            let w = g.push(Node {
12467                op: Op::Const,
12468                inputs: vec![],
12469                shape: Shape::from_dims(&[1, 1, 2, 2]),
12470                dtype: DType::F64,
12471            });
12472            let c = g.push(Node {
12473                op: Op::Fused(
12474                    fuel_graph::registry::FusedOps::CONV2D,
12475                    fuel_graph::registry::FusedOpParams::Conv2D {
12476                        stride: (1, 1),
12477                        padding: (0, 0),
12478                        groups: 1,
12479                    },
12480                ),
12481                inputs: vec![x, w],
12482                shape: Shape::from_dims(&[1, 1, 2, 2]),
12483                dtype: DType::F64,
12484            });
12485            g.set_target_backend(c, BackendId::Cpu);
12486            (x, w, c)
12487        };
12488        let mut inputs = StorageCache::new();
12489        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12490        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12491        let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12492        let guard = result_arc.read().unwrap();
12493        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12494            panic!("expected Cpu storage");
12495        };
12496        assert_eq!(c.as_slice::<f64>().unwrap(), &[12.0, 16.0, 24.0, 28.0]);
12497    }
12498
12499    /// E2E: Conv2D in BF16 — f32-accumulator path. Tolerant compare.
12500    #[test]
12501    fn pipelined_realize_conv2d_bf16() {
12502        let x_data: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
12503            .iter()
12504            .map(|v| half::bf16::from_f32(*v))
12505            .collect();
12506        let w_data: Vec<half::bf16> = [1.0_f32, 1.0, 1.0, 1.0]
12507            .iter()
12508            .map(|v| half::bf16::from_f32(*v))
12509            .collect();
12510        let x_storage = fuel_memory::from_slice_cpu(&x_data);
12511        let w_storage = fuel_memory::from_slice_cpu(&w_data);
12512
12513        let graph = Arc::new(RwLock::new(Graph::new()));
12514        let (x_id, w_id, c_id) = {
12515            let mut g = graph.write().unwrap();
12516            let x = g.push(Node {
12517                op: Op::Const,
12518                inputs: vec![],
12519                shape: Shape::from_dims(&[1, 1, 3, 3]),
12520                dtype: DType::BF16,
12521            });
12522            let w = g.push(Node {
12523                op: Op::Const,
12524                inputs: vec![],
12525                shape: Shape::from_dims(&[1, 1, 2, 2]),
12526                dtype: DType::BF16,
12527            });
12528            let c = g.push(Node {
12529                op: Op::Fused(
12530                    fuel_graph::registry::FusedOps::CONV2D,
12531                    fuel_graph::registry::FusedOpParams::Conv2D {
12532                        stride: (1, 1),
12533                        padding: (0, 0),
12534                        groups: 1,
12535                    },
12536                ),
12537                inputs: vec![x, w],
12538                shape: Shape::from_dims(&[1, 1, 2, 2]),
12539                dtype: DType::BF16,
12540            });
12541            g.set_target_backend(c, BackendId::Cpu);
12542            (x, w, c)
12543        };
12544        let mut inputs = StorageCache::new();
12545        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12546        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12547        let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12548        let guard = result_arc.read().unwrap();
12549        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12550            panic!("expected Cpu storage");
12551        };
12552        let got: Vec<f32> = c
12553            .as_slice::<half::bf16>()
12554            .unwrap()
12555            .iter()
12556            .map(|v| v.to_f32())
12557            .collect();
12558        let want = [12.0_f32, 16.0, 24.0, 28.0];
12559        for (g, w) in got.iter().zip(want.iter()) {
12560            assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
12561        }
12562    }
12563
12564    /// E2E: Conv2D in F16 — f32-accumulator path. Tolerant compare.
12565    #[test]
12566    fn pipelined_realize_conv2d_f16() {
12567        let x_data: Vec<half::f16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
12568            .iter()
12569            .map(|v| half::f16::from_f32(*v))
12570            .collect();
12571        let w_data: Vec<half::f16> = [1.0_f32, 1.0, 1.0, 1.0]
12572            .iter()
12573            .map(|v| half::f16::from_f32(*v))
12574            .collect();
12575        let x_storage = fuel_memory::from_slice_cpu(&x_data);
12576        let w_storage = fuel_memory::from_slice_cpu(&w_data);
12577
12578        let graph = Arc::new(RwLock::new(Graph::new()));
12579        let (x_id, w_id, c_id) = {
12580            let mut g = graph.write().unwrap();
12581            let x = g.push(Node {
12582                op: Op::Const,
12583                inputs: vec![],
12584                shape: Shape::from_dims(&[1, 1, 3, 3]),
12585                dtype: DType::F16,
12586            });
12587            let w = g.push(Node {
12588                op: Op::Const,
12589                inputs: vec![],
12590                shape: Shape::from_dims(&[1, 1, 2, 2]),
12591                dtype: DType::F16,
12592            });
12593            let c = g.push(Node {
12594                op: Op::Fused(
12595                    fuel_graph::registry::FusedOps::CONV2D,
12596                    fuel_graph::registry::FusedOpParams::Conv2D {
12597                        stride: (1, 1),
12598                        padding: (0, 0),
12599                        groups: 1,
12600                    },
12601                ),
12602                inputs: vec![x, w],
12603                shape: Shape::from_dims(&[1, 1, 2, 2]),
12604                dtype: DType::F16,
12605            });
12606            g.set_target_backend(c, BackendId::Cpu);
12607            (x, w, c)
12608        };
12609        let mut inputs = StorageCache::new();
12610        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12611        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12612        let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12613        let guard = result_arc.read().unwrap();
12614        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12615            panic!("expected Cpu storage");
12616        };
12617        let got: Vec<f32> = c
12618            .as_slice::<half::f16>()
12619            .unwrap()
12620            .iter()
12621            .map(|v| v.to_f32())
12622            .collect();
12623        let want = [12.0_f32, 16.0, 24.0, 28.0];
12624        for (g, w) in got.iter().zip(want.iter()) {
12625            assert!((g - w).abs() < 0.05, "got {got:?} want {want:?}");
12626        }
12627    }
12628
12629    /// E2E: Op::PagedAttn — F32, single-head, B=1, Sq=1.
12630    /// Same setup as the FlashAttn smoke test, but the K/V live in a
12631    /// paged cache that we look up via block_table.
12632    /// Layout:
12633    ///   block_size=2, num_blocks=1, max_blocks_per_seq=1.
12634    ///   k_cache shape [1, 2, 1, 2] = num_blocks × block_size × Hkv × D.
12635    ///   k_cache[block 0, slot 0, h 0] = [1, 0]
12636    ///   k_cache[block 0, slot 1, h 0] = [0, 1]
12637    ///   v_cache values: [10, 0] / [0, 10]
12638    ///   block_table[b=0, logical_block 0] = 0 (physical)
12639    ///   context_lens[0] = 2
12640    ///   q[0, 0, 0] = [2, 0]
12641    /// Causal is implicit (q_pos = ctx_len - Sq + sq = 2 - 1 + 0 = 1, both keys admissible).
12642    /// Same softmax math as FlashAttn → ~[8.808, 1.192].
12643    #[test]
12644    fn pipelined_realize_paged_attn_f32() {
12645        let q = fuel_memory::from_slice_cpu(&[2.0_f32, 0.0]);
12646        let k_cache = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12647        let v_cache = fuel_memory::from_slice_cpu(&[10.0_f32, 0.0, 0.0, 10.0]);
12648        let block_table_u32 = fuel_memory::Storage::new(
12649            fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12650                0_u32,
12651            ])),
12652            DType::U32,
12653        );
12654        let context_lens_u32 = fuel_memory::Storage::new(
12655            fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12656                2_u32,
12657            ])),
12658            DType::U32,
12659        );
12660        let graph = Arc::new(RwLock::new(Graph::new()));
12661        let (q_id, kc_id, vc_id, bt_id, cl_id, op_id) = {
12662            let mut g = graph.write().unwrap();
12663            let q = g.push(Node {
12664                op: Op::Const,
12665                inputs: vec![],
12666                shape: Shape::from_dims(&[1, 1, 1, 2]),
12667                dtype: DType::F32,
12668            });
12669            let kc = g.push(Node {
12670                op: Op::Const,
12671                inputs: vec![],
12672                shape: Shape::from_dims(&[1, 2, 1, 2]),
12673                dtype: DType::F32,
12674            });
12675            let vc = g.push(Node {
12676                op: Op::Const,
12677                inputs: vec![],
12678                shape: Shape::from_dims(&[1, 2, 1, 2]),
12679                dtype: DType::F32,
12680            });
12681            let bt = g.push(Node {
12682                op: Op::Const,
12683                inputs: vec![],
12684                shape: Shape::from_dims(&[1, 1]),
12685                dtype: DType::U32,
12686            });
12687            let cl = g.push(Node {
12688                op: Op::Const,
12689                inputs: vec![],
12690                shape: Shape::from_dims(&[1]),
12691                dtype: DType::U32,
12692            });
12693            let op = g.push(Node {
12694                op: Op::Fused(
12695                    fuel_graph::registry::FusedOps::PAGED_ATTN,
12696                    fuel_graph::registry::FusedOpParams::PagedAttn {
12697                        softmax_scale: 1.0,
12698                        block_size: 2,
12699                        softcap: None,
12700                    },
12701                ),
12702                inputs: vec![q, kc, vc, bt, cl],
12703                shape: Shape::from_dims(&[1, 1, 1, 2]),
12704                dtype: DType::F32,
12705            });
12706            g.set_target_backend(op, BackendId::Cpu);
12707            (q, kc, vc, bt, cl, op)
12708        };
12709        let mut inputs = StorageCache::new();
12710        inputs.insert(q_id, Arc::new(RwLock::new(q)));
12711        inputs.insert(kc_id, Arc::new(RwLock::new(k_cache)));
12712        inputs.insert(vc_id, Arc::new(RwLock::new(v_cache)));
12713        inputs.insert(bt_id, Arc::new(RwLock::new(block_table_u32)));
12714        inputs.insert(cl_id, Arc::new(RwLock::new(context_lens_u32)));
12715        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12716        let guard = result_arc.read().unwrap();
12717        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12718            panic!("expected Cpu storage");
12719        };
12720        let r = c.as_slice::<f32>().unwrap();
12721        let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
12722        let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
12723        assert!(
12724            (r[0] - 10.0 * expected_p0).abs() < 1e-5,
12725            "row[0]: got {} expected {}",
12726            r[0],
12727            10.0 * expected_p0
12728        );
12729        assert!(
12730            (r[1] - 10.0 * expected_p1).abs() < 1e-5,
12731            "row[1]: got {} expected {}",
12732            r[1],
12733            10.0 * expected_p1
12734        );
12735    }
12736
12737    /// E2E: PagedAttn BF16 — same single-row test, tolerant.
12738    #[test]
12739    fn pipelined_realize_paged_attn_bf16() {
12740        let q_v: Vec<half::bf16> = [2.0_f32, 0.0]
12741            .iter()
12742            .map(|x| half::bf16::from_f32(*x))
12743            .collect();
12744        let kc_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0]
12745            .iter()
12746            .map(|x| half::bf16::from_f32(*x))
12747            .collect();
12748        let vc_v: Vec<half::bf16> = [10.0_f32, 0.0, 0.0, 10.0]
12749            .iter()
12750            .map(|x| half::bf16::from_f32(*x))
12751            .collect();
12752        let q = fuel_memory::from_slice_cpu(&q_v);
12753        let k_cache = fuel_memory::from_slice_cpu(&kc_v);
12754        let v_cache = fuel_memory::from_slice_cpu(&vc_v);
12755        let bt = fuel_memory::Storage::new(
12756            fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12757                0_u32,
12758            ])),
12759            DType::U32,
12760        );
12761        let cl = fuel_memory::Storage::new(
12762            fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12763                2_u32,
12764            ])),
12765            DType::U32,
12766        );
12767        let graph = Arc::new(RwLock::new(Graph::new()));
12768        let (q_id, kc_id, vc_id, bt_id, cl_id, op_id) = {
12769            let mut g = graph.write().unwrap();
12770            let q = g.push(Node {
12771                op: Op::Const,
12772                inputs: vec![],
12773                shape: Shape::from_dims(&[1, 1, 1, 2]),
12774                dtype: DType::BF16,
12775            });
12776            let kc = g.push(Node {
12777                op: Op::Const,
12778                inputs: vec![],
12779                shape: Shape::from_dims(&[1, 2, 1, 2]),
12780                dtype: DType::BF16,
12781            });
12782            let vc = g.push(Node {
12783                op: Op::Const,
12784                inputs: vec![],
12785                shape: Shape::from_dims(&[1, 2, 1, 2]),
12786                dtype: DType::BF16,
12787            });
12788            let bt = g.push(Node {
12789                op: Op::Const,
12790                inputs: vec![],
12791                shape: Shape::from_dims(&[1, 1]),
12792                dtype: DType::U32,
12793            });
12794            let cl = g.push(Node {
12795                op: Op::Const,
12796                inputs: vec![],
12797                shape: Shape::from_dims(&[1]),
12798                dtype: DType::U32,
12799            });
12800            let op = g.push(Node {
12801                op: Op::Fused(
12802                    fuel_graph::registry::FusedOps::PAGED_ATTN,
12803                    fuel_graph::registry::FusedOpParams::PagedAttn {
12804                        softmax_scale: 1.0,
12805                        block_size: 2,
12806                        softcap: None,
12807                    },
12808                ),
12809                inputs: vec![q, kc, vc, bt, cl],
12810                shape: Shape::from_dims(&[1, 1, 1, 2]),
12811                dtype: DType::BF16,
12812            });
12813            g.set_target_backend(op, BackendId::Cpu);
12814            (q, kc, vc, bt, cl, op)
12815        };
12816        let mut inputs = StorageCache::new();
12817        inputs.insert(q_id, Arc::new(RwLock::new(q)));
12818        inputs.insert(kc_id, Arc::new(RwLock::new(k_cache)));
12819        inputs.insert(vc_id, Arc::new(RwLock::new(v_cache)));
12820        inputs.insert(bt_id, Arc::new(RwLock::new(bt)));
12821        inputs.insert(cl_id, Arc::new(RwLock::new(cl)));
12822        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12823        let guard = result_arc.read().unwrap();
12824        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12825            panic!("expected Cpu storage");
12826        };
12827        let got: Vec<f32> = c
12828            .as_slice::<half::bf16>()
12829            .unwrap()
12830            .iter()
12831            .map(|v| v.to_f32())
12832            .collect();
12833        let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
12834        let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
12835        let want = [10.0 * expected_p0, 10.0 * expected_p1];
12836        for (g, w) in got.iter().zip(want.iter()) {
12837            assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
12838        }
12839    }
12840
12841    /// E2E: Op::FlashAttn — F32 single-head, single-batch, no mask.
12842    /// q = [[2.0, 0.0]], k = [[1.0, 0.0], [0.0, 1.0]], v = [[10, 0], [0, 10]]
12843    /// scale = 1.0
12844    /// scores = q · kᵀ = [2.0, 0.0]
12845    /// softmax = [e^2/(e^2+1), 1/(e^2+1)] ≈ [0.8808, 0.1192]
12846    /// out = softmax @ v = [10*0.8808, 10*0.1192] ≈ [8.808, 1.192]
12847    #[test]
12848    fn pipelined_realize_flash_attn_f32() {
12849        // [B=1, H=1, S=1or2, D=2]
12850        let q = fuel_memory::from_slice_cpu(&[2.0_f32, 0.0]);
12851        let k = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12852        let v = fuel_memory::from_slice_cpu(&[10.0_f32, 0.0, 0.0, 10.0]);
12853        let graph = Arc::new(RwLock::new(Graph::new()));
12854        let (q_id, k_id, v_id, op_id) = {
12855            let mut g = graph.write().unwrap();
12856            let q = g.push(Node {
12857                op: Op::Const,
12858                inputs: vec![],
12859                shape: Shape::from_dims(&[1, 1, 1, 2]),
12860                dtype: DType::F32,
12861            });
12862            let k = g.push(Node {
12863                op: Op::Const,
12864                inputs: vec![],
12865                shape: Shape::from_dims(&[1, 1, 2, 2]),
12866                dtype: DType::F32,
12867            });
12868            let v = g.push(Node {
12869                op: Op::Const,
12870                inputs: vec![],
12871                shape: Shape::from_dims(&[1, 1, 2, 2]),
12872                dtype: DType::F32,
12873            });
12874            let op = g.push(Node {
12875                op: Op::Fused(
12876                    fuel_graph::registry::FusedOps::FLASH_ATTN,
12877                    fuel_graph::registry::FusedOpParams::FlashAttn {
12878                        softmax_scale: 1.0,
12879                        causal: false,
12880                        window_size_left: None,
12881                        window_size_right: None,
12882                        softcap: None,
12883                        k_len: None,
12884                    },
12885                ),
12886                inputs: vec![q, k, v],
12887                shape: Shape::from_dims(&[1, 1, 1, 2]),
12888                dtype: DType::F32,
12889            });
12890            g.set_target_backend(op, BackendId::Cpu);
12891            (q, k, v, op)
12892        };
12893        let mut inputs = StorageCache::new();
12894        inputs.insert(q_id, Arc::new(RwLock::new(q)));
12895        inputs.insert(k_id, Arc::new(RwLock::new(k)));
12896        inputs.insert(v_id, Arc::new(RwLock::new(v)));
12897        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12898        let guard = result_arc.read().unwrap();
12899        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12900            panic!("expected Cpu storage");
12901        };
12902        let r = c.as_slice::<f32>().unwrap();
12903        let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
12904        let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
12905        assert!(
12906            (r[0] - 10.0 * expected_p0).abs() < 1e-5,
12907            "row[0]: got {} expected {}",
12908            r[0],
12909            10.0 * expected_p0
12910        );
12911        assert!(
12912            (r[1] - 10.0 * expected_p1).abs() < 1e-5,
12913            "row[1]: got {} expected {}",
12914            r[1],
12915            10.0 * expected_p1
12916        );
12917    }
12918
12919    /// E2E: FlashAttn with causal mask — second query position
12920    /// attends to both keys (positions 0 and 1), first only attends
12921    /// to key 0 (everything beyond is masked).
12922    #[test]
12923    fn pipelined_realize_flash_attn_causal_f32() {
12924        // q [1,1,2,2]: query 0 = [1, 0], query 1 = [0, 1]
12925        // k [1,1,2,2]: keys = [[1, 0], [0, 1]]
12926        // v [1,1,2,2]: values = [[5, 6], [7, 8]]
12927        // softmax_scale=1, causal:
12928        //   query 0: only key 0 admissible → out = v[0] = [5, 6]
12929        //   query 1: both admissible. scores = q1·k = [0, 1]
12930        //            softmax = [1/(e+1), e/(e+1)]
12931        //            out = scores · v = [(5)/(e+1) + 7e/(e+1), 6/(e+1) + 8e/(e+1)]
12932        let q = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12933        let k = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12934        let v = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0, 7.0, 8.0]);
12935        let graph = Arc::new(RwLock::new(Graph::new()));
12936        let (q_id, k_id, v_id, op_id) = {
12937            let mut g = graph.write().unwrap();
12938            let q = g.push(Node {
12939                op: Op::Const,
12940                inputs: vec![],
12941                shape: Shape::from_dims(&[1, 1, 2, 2]),
12942                dtype: DType::F32,
12943            });
12944            let k = g.push(Node {
12945                op: Op::Const,
12946                inputs: vec![],
12947                shape: Shape::from_dims(&[1, 1, 2, 2]),
12948                dtype: DType::F32,
12949            });
12950            let v = g.push(Node {
12951                op: Op::Const,
12952                inputs: vec![],
12953                shape: Shape::from_dims(&[1, 1, 2, 2]),
12954                dtype: DType::F32,
12955            });
12956            let op = g.push(Node {
12957                op: Op::Fused(
12958                    fuel_graph::registry::FusedOps::FLASH_ATTN,
12959                    fuel_graph::registry::FusedOpParams::FlashAttn {
12960                        softmax_scale: 1.0,
12961                        causal: true,
12962                        window_size_left: None,
12963                        window_size_right: None,
12964                        softcap: None,
12965                        k_len: None,
12966                    },
12967                ),
12968                inputs: vec![q, k, v],
12969                shape: Shape::from_dims(&[1, 1, 2, 2]),
12970                dtype: DType::F32,
12971            });
12972            g.set_target_backend(op, BackendId::Cpu);
12973            (q, k, v, op)
12974        };
12975        let mut inputs = StorageCache::new();
12976        inputs.insert(q_id, Arc::new(RwLock::new(q)));
12977        inputs.insert(k_id, Arc::new(RwLock::new(k)));
12978        inputs.insert(v_id, Arc::new(RwLock::new(v)));
12979        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12980        let guard = result_arc.read().unwrap();
12981        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12982            panic!("expected Cpu storage");
12983        };
12984        let r = c.as_slice::<f32>().unwrap();
12985        // Query 0 sees only key 0 → output = v[0]
12986        assert!((r[0] - 5.0).abs() < 1e-5, "got {}", r[0]);
12987        assert!((r[1] - 6.0).abs() < 1e-5, "got {}", r[1]);
12988        // Query 1 sees both. softmax([0, 1]) = [1/(e+1), e/(e+1)]
12989        let denom = (1.0_f32).exp() + 1.0;
12990        let expected_a = 5.0 / denom + 7.0 * (1.0_f32).exp() / denom;
12991        let expected_b = 6.0 / denom + 8.0 * (1.0_f32).exp() / denom;
12992        assert!(
12993            (r[2] - expected_a).abs() < 1e-5,
12994            "row1[0]: got {} expected {}",
12995            r[2],
12996            expected_a
12997        );
12998        assert!(
12999            (r[3] - expected_b).abs() < 1e-5,
13000            "row1[1]: got {} expected {}",
13001            r[3],
13002            expected_b
13003        );
13004    }
13005
13006    /// E2E: FlashAttn over a fixed-**capacity** K/V with a runtime
13007    /// `k_len` resolved through a `SymEnv` (Phase D symbolic extents).
13008    /// Only the first `k_len` rows are attended; trailing "poison" rows
13009    /// in the capacity buffer are ignored, and the causal mask is
13010    /// bottom-right-aligned at offset `k_len - Sq`. This is flash decode
13011    /// over a persistent KV-cache: K/V are `[.., max_seq, ..]` capacity
13012    /// and the live prefix grows per token.
13013    #[test]
13014    fn pipelined_realize_flash_attn_dynamic_k_len() {
13015        use fuel_ir::{DynScalar, SymId};
13016        // q [1,1,2,2]; K/V capacity [1,1,4,2]; bind k_len = 3 so row 3
13017        // (the "poison" row) is never attended.
13018        let q = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
13019        let k = fuel_memory::from_slice_cpu(&[
13020            1.0_f32, 0.0, 0.0, 1.0, 1.0, 1.0, 100.0,
13021            100.0, // poison row 3 — must be ignored (k_len=3)
13022        ]);
13023        let v = fuel_memory::from_slice_cpu(&[
13024            5.0_f32, 6.0, 7.0, 8.0, 9.0, 10.0, 999.0,
13025            999.0, // poison row 3 — must be ignored
13026        ]);
13027        let sym = SymId(0);
13028        let graph = Arc::new(RwLock::new(Graph::new()));
13029        let (q_id, k_id, v_id, op_id) = {
13030            let mut g = graph.write().unwrap();
13031            let q = g.push(Node {
13032                op: Op::Const,
13033                inputs: vec![],
13034                shape: Shape::from_dims(&[1, 1, 2, 2]),
13035                dtype: DType::F32,
13036            });
13037            let k = g.push(Node {
13038                op: Op::Const,
13039                inputs: vec![],
13040                shape: Shape::from_dims(&[1, 1, 4, 2]),
13041                dtype: DType::F32,
13042            });
13043            let v = g.push(Node {
13044                op: Op::Const,
13045                inputs: vec![],
13046                shape: Shape::from_dims(&[1, 1, 4, 2]),
13047                dtype: DType::F32,
13048            });
13049            let op = g.push(Node {
13050                op: Op::Fused(
13051                    fuel_graph::registry::FusedOps::FLASH_ATTN,
13052                    fuel_graph::registry::FusedOpParams::FlashAttn {
13053                        softmax_scale: 1.0,
13054                        causal: true,
13055                        window_size_left: None,
13056                        window_size_right: None,
13057                        softcap: None,
13058                        k_len: Some(DynScalar::Sym(sym)),
13059                    },
13060                ),
13061                inputs: vec![q, k, v],
13062                shape: Shape::from_dims(&[1, 1, 2, 2]),
13063                dtype: DType::F32,
13064            });
13065            g.set_target_backend(op, BackendId::Cpu);
13066            (q, k, v, op)
13067        };
13068        let mut inputs = StorageCache::new();
13069        inputs.insert(q_id, Arc::new(RwLock::new(q)));
13070        inputs.insert(k_id, Arc::new(RwLock::new(k)));
13071        inputs.insert(v_id, Arc::new(RwLock::new(v)));
13072        let mut env = SymEnv::new();
13073        env.bind(sym, 3).unwrap();
13074        let (result_arc, _) = PipelinedExecutor::realize_with_env(graph, op_id, inputs, env)
13075            .expect("realize_with_env");
13076        let guard = result_arc.read().unwrap();
13077        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13078            panic!("expected Cpu storage");
13079        };
13080        let r = c.as_slice::<f32>().unwrap();
13081        let e = 1.0_f32.exp();
13082        // causal_offset = k_len - Sq = 3 - 2 = 1.
13083        // Query 0 (abs pos 1): keys {0,1}. scores [1,0] → softmax [e,1]/(e+1).
13084        let d0 = e + 1.0;
13085        let e0x = (5.0 * e + 7.0) / d0;
13086        let e0y = (6.0 * e + 8.0) / d0;
13087        // Query 1 (abs pos 2): keys {0,1,2}. scores [0,1,1] → [1,e,e]/(1+2e).
13088        let d1 = 1.0 + 2.0 * e;
13089        let e1x = (5.0 + 7.0 * e + 9.0 * e) / d1;
13090        let e1y = (6.0 + 8.0 * e + 10.0 * e) / d1;
13091        for (got, want) in r.iter().zip([e0x, e0y, e1x, e1y].iter()) {
13092            assert!(
13093                (got - want).abs() < 1e-4,
13094                "flash dyn k_len: got {got}, want {want} (full output {r:?}); \
13095                 a poison-contaminated value means the capacity rows past k_len leaked in",
13096            );
13097        }
13098    }
13099
13100    /// A flash `k_len` symbol unbound in the `SymEnv` surfaces a typed
13101    /// error at realize (never a panic).
13102    #[test]
13103    fn pipelined_realize_flash_attn_dynamic_k_len_unbound_errors() {
13104        use fuel_ir::{DynScalar, SymId};
13105        let q = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
13106        let k = fuel_memory::from_slice_cpu(&[0.0_f32; 16]);
13107        let v = fuel_memory::from_slice_cpu(&[0.0_f32; 16]);
13108        let graph = Arc::new(RwLock::new(Graph::new()));
13109        let (q_id, k_id, v_id, op_id) = {
13110            let mut g = graph.write().unwrap();
13111            let q = g.push(Node {
13112                op: Op::Const,
13113                inputs: vec![],
13114                shape: Shape::from_dims(&[1, 1, 2, 2]),
13115                dtype: DType::F32,
13116            });
13117            let k = g.push(Node {
13118                op: Op::Const,
13119                inputs: vec![],
13120                shape: Shape::from_dims(&[1, 1, 4, 2]),
13121                dtype: DType::F32,
13122            });
13123            let v = g.push(Node {
13124                op: Op::Const,
13125                inputs: vec![],
13126                shape: Shape::from_dims(&[1, 1, 4, 2]),
13127                dtype: DType::F32,
13128            });
13129            let op = g.push(Node {
13130                op: Op::Fused(
13131                    fuel_graph::registry::FusedOps::FLASH_ATTN,
13132                    fuel_graph::registry::FusedOpParams::FlashAttn {
13133                        softmax_scale: 1.0,
13134                        causal: true,
13135                        window_size_left: None,
13136                        window_size_right: None,
13137                        softcap: None,
13138                        k_len: Some(DynScalar::Sym(SymId(0))),
13139                    },
13140                ),
13141                inputs: vec![q, k, v],
13142                shape: Shape::from_dims(&[1, 1, 2, 2]),
13143                dtype: DType::F32,
13144            });
13145            g.set_target_backend(op, BackendId::Cpu);
13146            (q, k, v, op)
13147        };
13148        let mut inputs = StorageCache::new();
13149        inputs.insert(q_id, Arc::new(RwLock::new(q)));
13150        inputs.insert(k_id, Arc::new(RwLock::new(k)));
13151        inputs.insert(v_id, Arc::new(RwLock::new(v)));
13152        // Empty env — the k_len symbol is unbound.
13153        let result = PipelinedExecutor::realize_with_env(graph, op_id, inputs, SymEnv::new());
13154        assert!(
13155            result.is_err(),
13156            "unbound flash k_len symbol must surface a typed error"
13157        );
13158    }
13159
13160    /// E2E: FlashAttn BF16 — same single-row test as f32, tolerant.
13161    #[test]
13162    fn pipelined_realize_flash_attn_bf16() {
13163        let q_v: Vec<half::bf16> = [2.0_f32, 0.0]
13164            .iter()
13165            .map(|x| half::bf16::from_f32(*x))
13166            .collect();
13167        let k_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0]
13168            .iter()
13169            .map(|x| half::bf16::from_f32(*x))
13170            .collect();
13171        let v_v: Vec<half::bf16> = [10.0_f32, 0.0, 0.0, 10.0]
13172            .iter()
13173            .map(|x| half::bf16::from_f32(*x))
13174            .collect();
13175        let q = fuel_memory::from_slice_cpu(&q_v);
13176        let k = fuel_memory::from_slice_cpu(&k_v);
13177        let v = fuel_memory::from_slice_cpu(&v_v);
13178        let graph = Arc::new(RwLock::new(Graph::new()));
13179        let (q_id, k_id, v_id, op_id) = {
13180            let mut g = graph.write().unwrap();
13181            let q = g.push(Node {
13182                op: Op::Const,
13183                inputs: vec![],
13184                shape: Shape::from_dims(&[1, 1, 1, 2]),
13185                dtype: DType::BF16,
13186            });
13187            let k = g.push(Node {
13188                op: Op::Const,
13189                inputs: vec![],
13190                shape: Shape::from_dims(&[1, 1, 2, 2]),
13191                dtype: DType::BF16,
13192            });
13193            let v = g.push(Node {
13194                op: Op::Const,
13195                inputs: vec![],
13196                shape: Shape::from_dims(&[1, 1, 2, 2]),
13197                dtype: DType::BF16,
13198            });
13199            let op = g.push(Node {
13200                op: Op::Fused(
13201                    fuel_graph::registry::FusedOps::FLASH_ATTN,
13202                    fuel_graph::registry::FusedOpParams::FlashAttn {
13203                        softmax_scale: 1.0,
13204                        causal: false,
13205                        window_size_left: None,
13206                        window_size_right: None,
13207                        softcap: None,
13208                        k_len: None,
13209                    },
13210                ),
13211                inputs: vec![q, k, v],
13212                shape: Shape::from_dims(&[1, 1, 1, 2]),
13213                dtype: DType::BF16,
13214            });
13215            g.set_target_backend(op, BackendId::Cpu);
13216            (q, k, v, op)
13217        };
13218        let mut inputs = StorageCache::new();
13219        inputs.insert(q_id, Arc::new(RwLock::new(q)));
13220        inputs.insert(k_id, Arc::new(RwLock::new(k)));
13221        inputs.insert(v_id, Arc::new(RwLock::new(v)));
13222        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13223        let guard = result_arc.read().unwrap();
13224        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13225            panic!("expected Cpu storage");
13226        };
13227        let got: Vec<f32> = c
13228            .as_slice::<half::bf16>()
13229            .unwrap()
13230            .iter()
13231            .map(|v| v.to_f32())
13232            .collect();
13233        let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
13234        let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
13235        let want = [10.0 * expected_p0, 10.0 * expected_p1];
13236        for (g, w) in got.iter().zip(want.iter()) {
13237            assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13238        }
13239    }
13240
13241    /// E2E: Op::FusedLinear — F32. a[1,2,3] @ b[1,3,2] + bias[2].
13242    /// a = [[[1,2,3],[4,5,6]]], b = [[[1,0],[0,1],[1,1]]], bias=[10,20]
13243    /// matmul = [[1+0+3, 0+2+3], [4+0+6, 0+5+6]] = [[4, 5], [10, 11]]
13244    /// + bias = [[14, 25], [20, 31]]
13245    #[test]
13246    fn pipelined_realize_fused_linear_f32() {
13247        let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13248        let b = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0, 1.0, 1.0]);
13249        let bias = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
13250        let graph = Arc::new(RwLock::new(Graph::new()));
13251        let (a_id, b_id, bias_id, op_id) = {
13252            let mut g = graph.write().unwrap();
13253            let a = g.push(Node {
13254                op: Op::Const,
13255                inputs: vec![],
13256                shape: Shape::from_dims(&[1, 2, 3]),
13257                dtype: DType::F32,
13258            });
13259            let b = g.push(Node {
13260                op: Op::Const,
13261                inputs: vec![],
13262                shape: Shape::from_dims(&[1, 3, 2]),
13263                dtype: DType::F32,
13264            });
13265            let bias = g.push(Node {
13266                op: Op::Const,
13267                inputs: vec![],
13268                shape: Shape::from_dims(&[2]),
13269                dtype: DType::F32,
13270            });
13271            let op = g.push(Node {
13272                op: Op::Fused(
13273                    fuel_graph::registry::FusedOps::FUSED_LINEAR,
13274                    fuel_graph::registry::FusedOpParams::FusedLinear,
13275                ),
13276                inputs: vec![a, b, bias],
13277                shape: Shape::from_dims(&[1, 2, 2]),
13278                dtype: DType::F32,
13279            });
13280            g.set_target_backend(op, BackendId::Cpu);
13281            (a, b, bias, op)
13282        };
13283        let mut inputs = StorageCache::new();
13284        inputs.insert(a_id, Arc::new(RwLock::new(a)));
13285        inputs.insert(b_id, Arc::new(RwLock::new(b)));
13286        inputs.insert(bias_id, Arc::new(RwLock::new(bias)));
13287        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13288        let guard = result_arc.read().unwrap();
13289        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13290            panic!("expected Cpu storage");
13291        };
13292        assert_eq!(c.as_slice::<f32>().unwrap(), &[14.0, 25.0, 20.0, 31.0]);
13293    }
13294
13295    /// E2E: FusedLinear F64 — same shape test on doubles.
13296    #[test]
13297    fn pipelined_realize_fused_linear_f64() {
13298        let a = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
13299        let b = fuel_memory::from_slice_cpu(&[1.0_f64, 0.0, 0.0, 1.0, 1.0, 1.0]);
13300        let bias = fuel_memory::from_slice_cpu(&[10.0_f64, 20.0]);
13301        let graph = Arc::new(RwLock::new(Graph::new()));
13302        let (a_id, b_id, bias_id, op_id) = {
13303            let mut g = graph.write().unwrap();
13304            let a = g.push(Node {
13305                op: Op::Const,
13306                inputs: vec![],
13307                shape: Shape::from_dims(&[1, 2, 3]),
13308                dtype: DType::F64,
13309            });
13310            let b = g.push(Node {
13311                op: Op::Const,
13312                inputs: vec![],
13313                shape: Shape::from_dims(&[1, 3, 2]),
13314                dtype: DType::F64,
13315            });
13316            let bias = g.push(Node {
13317                op: Op::Const,
13318                inputs: vec![],
13319                shape: Shape::from_dims(&[2]),
13320                dtype: DType::F64,
13321            });
13322            let op = g.push(Node {
13323                op: Op::Fused(
13324                    fuel_graph::registry::FusedOps::FUSED_LINEAR,
13325                    fuel_graph::registry::FusedOpParams::FusedLinear,
13326                ),
13327                inputs: vec![a, b, bias],
13328                shape: Shape::from_dims(&[1, 2, 2]),
13329                dtype: DType::F64,
13330            });
13331            g.set_target_backend(op, BackendId::Cpu);
13332            (a, b, bias, op)
13333        };
13334        let mut inputs = StorageCache::new();
13335        inputs.insert(a_id, Arc::new(RwLock::new(a)));
13336        inputs.insert(b_id, Arc::new(RwLock::new(b)));
13337        inputs.insert(bias_id, Arc::new(RwLock::new(bias)));
13338        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13339        let guard = result_arc.read().unwrap();
13340        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13341            panic!("expected Cpu storage");
13342        };
13343        assert_eq!(c.as_slice::<f64>().unwrap(), &[14.0, 25.0, 20.0, 31.0]);
13344    }
13345
13346    /// E2E: FusedLinear BF16 — tolerant compare.
13347    #[test]
13348    fn pipelined_realize_fused_linear_bf16() {
13349        let a_v: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]
13350            .iter()
13351            .map(|x| half::bf16::from_f32(*x))
13352            .collect();
13353        let b_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0, 1.0, 1.0]
13354            .iter()
13355            .map(|x| half::bf16::from_f32(*x))
13356            .collect();
13357        let bias_v: Vec<half::bf16> = [10.0_f32, 20.0]
13358            .iter()
13359            .map(|x| half::bf16::from_f32(*x))
13360            .collect();
13361        let a = fuel_memory::from_slice_cpu(&a_v);
13362        let b = fuel_memory::from_slice_cpu(&b_v);
13363        let bias = fuel_memory::from_slice_cpu(&bias_v);
13364        let graph = Arc::new(RwLock::new(Graph::new()));
13365        let (a_id, b_id, bias_id, op_id) = {
13366            let mut g = graph.write().unwrap();
13367            let a = g.push(Node {
13368                op: Op::Const,
13369                inputs: vec![],
13370                shape: Shape::from_dims(&[1, 2, 3]),
13371                dtype: DType::BF16,
13372            });
13373            let b = g.push(Node {
13374                op: Op::Const,
13375                inputs: vec![],
13376                shape: Shape::from_dims(&[1, 3, 2]),
13377                dtype: DType::BF16,
13378            });
13379            let bias = g.push(Node {
13380                op: Op::Const,
13381                inputs: vec![],
13382                shape: Shape::from_dims(&[2]),
13383                dtype: DType::BF16,
13384            });
13385            let op = g.push(Node {
13386                op: Op::Fused(
13387                    fuel_graph::registry::FusedOps::FUSED_LINEAR,
13388                    fuel_graph::registry::FusedOpParams::FusedLinear,
13389                ),
13390                inputs: vec![a, b, bias],
13391                shape: Shape::from_dims(&[1, 2, 2]),
13392                dtype: DType::BF16,
13393            });
13394            g.set_target_backend(op, BackendId::Cpu);
13395            (a, b, bias, op)
13396        };
13397        let mut inputs = StorageCache::new();
13398        inputs.insert(a_id, Arc::new(RwLock::new(a)));
13399        inputs.insert(b_id, Arc::new(RwLock::new(b)));
13400        inputs.insert(bias_id, Arc::new(RwLock::new(bias)));
13401        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13402        let guard = result_arc.read().unwrap();
13403        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13404            panic!("expected Cpu storage");
13405        };
13406        let got: Vec<f32> = c
13407            .as_slice::<half::bf16>()
13408            .unwrap()
13409            .iter()
13410            .map(|v| v.to_f32())
13411            .collect();
13412        let want = [14.0_f32, 25.0, 20.0, 31.0];
13413        for (g, w) in got.iter().zip(want.iter()) {
13414            assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13415        }
13416    }
13417
13418    /// E2E: Op::ReduceSumTo — sum the leading axis of a [2,3] tensor.
13419    /// Input [[1,2,3],[4,5,6]] → output [5,7,9].
13420    #[test]
13421    fn pipelined_realize_reduce_sum_to_f32_drops_leading_axis() {
13422        let v = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13423        let graph = Arc::new(RwLock::new(Graph::new()));
13424        let (in_id, op_id) = {
13425            let mut g = graph.write().unwrap();
13426            let in_id = g.push(Node {
13427                op: Op::Const,
13428                inputs: vec![],
13429                shape: Shape::from_dims(&[2, 3]),
13430                dtype: DType::F32,
13431            });
13432            let op_id = g.push(Node {
13433                op: Op::ReduceSumTo(Shape::from_dims(&[3])),
13434                inputs: vec![in_id],
13435                shape: Shape::from_dims(&[3]),
13436                dtype: DType::F32,
13437            });
13438            g.set_target_backend(op_id, BackendId::Cpu);
13439            (in_id, op_id)
13440        };
13441        let mut inputs = StorageCache::new();
13442        inputs.insert(in_id, Arc::new(RwLock::new(v)));
13443        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13444        let guard = result_arc.read().unwrap();
13445        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13446            panic!("expected Cpu storage");
13447        };
13448        assert_eq!(c.as_slice::<f32>().unwrap(), &[5.0, 7.0, 9.0]);
13449    }
13450
13451    /// E2E: Op::ReduceSumTo — keep-dim with 1 in the middle.
13452    /// Input [2,3,4] → [2,1,4] sums along dim 1.
13453    #[test]
13454    fn pipelined_realize_reduce_sum_to_f32_keepdim_middle() {
13455        // [2,3,4]: layer 0 = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
13456        //          layer 1 = [[13..16],[17..20],[21..24]]
13457        let mut v: Vec<f32> = (1..=24).map(|x| x as f32).collect();
13458        let _ = &mut v;
13459        let s = fuel_memory::from_slice_cpu(&v);
13460        let graph = Arc::new(RwLock::new(Graph::new()));
13461        let (in_id, op_id) = {
13462            let mut g = graph.write().unwrap();
13463            let in_id = g.push(Node {
13464                op: Op::Const,
13465                inputs: vec![],
13466                shape: Shape::from_dims(&[2, 3, 4]),
13467                dtype: DType::F32,
13468            });
13469            let op_id = g.push(Node {
13470                op: Op::ReduceSumTo(Shape::from_dims(&[2, 1, 4])),
13471                inputs: vec![in_id],
13472                shape: Shape::from_dims(&[2, 1, 4]),
13473                dtype: DType::F32,
13474            });
13475            g.set_target_backend(op_id, BackendId::Cpu);
13476            (in_id, op_id)
13477        };
13478        let mut inputs = StorageCache::new();
13479        inputs.insert(in_id, Arc::new(RwLock::new(s)));
13480        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13481        let guard = result_arc.read().unwrap();
13482        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13483            panic!("expected Cpu storage");
13484        };
13485        // layer0 dim1-sum: col j = 1+5+9, 2+6+10, 3+7+11, 4+8+12 = [15,18,21,24]
13486        // layer1 dim1-sum: col j = 13+17+21, 14+18+22, 15+19+23, 16+20+24 = [51,54,57,60]
13487        assert_eq!(
13488            c.as_slice::<f32>().unwrap(),
13489            &[15.0, 18.0, 21.0, 24.0, 51.0, 54.0, 57.0, 60.0],
13490        );
13491    }
13492
13493    /// E2E: ReduceSumTo F64 — same drop-leading-axis test on doubles.
13494    #[test]
13495    fn pipelined_realize_reduce_sum_to_f64() {
13496        let v = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
13497        let graph = Arc::new(RwLock::new(Graph::new()));
13498        let (in_id, op_id) = {
13499            let mut g = graph.write().unwrap();
13500            let in_id = g.push(Node {
13501                op: Op::Const,
13502                inputs: vec![],
13503                shape: Shape::from_dims(&[2, 3]),
13504                dtype: DType::F64,
13505            });
13506            let op_id = g.push(Node {
13507                op: Op::ReduceSumTo(Shape::from_dims(&[3])),
13508                inputs: vec![in_id],
13509                shape: Shape::from_dims(&[3]),
13510                dtype: DType::F64,
13511            });
13512            g.set_target_backend(op_id, BackendId::Cpu);
13513            (in_id, op_id)
13514        };
13515        let mut inputs = StorageCache::new();
13516        inputs.insert(in_id, Arc::new(RwLock::new(v)));
13517        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13518        let guard = result_arc.read().unwrap();
13519        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13520            panic!("expected Cpu storage");
13521        };
13522        assert_eq!(c.as_slice::<f64>().unwrap(), &[5.0, 7.0, 9.0]);
13523    }
13524
13525    /// E2E: Op::ReduceMaxTo F32 — drop the leading axis with max-reduce.
13526    #[test]
13527    fn pipelined_realize_reduce_max_to_f32_drops_leading_axis() {
13528        // Input [2,3]: row 0 = [1, 7, 3], row 1 = [4, 2, 6]. Max along
13529        // dim 0: [4, 7, 6].
13530        let v = fuel_memory::from_slice_cpu(&[1.0_f32, 7.0, 3.0, 4.0, 2.0, 6.0]);
13531        let graph = Arc::new(RwLock::new(Graph::new()));
13532        let (in_id, op_id) = {
13533            let mut g = graph.write().unwrap();
13534            let in_id = g.push(Node {
13535                op: Op::Const,
13536                inputs: vec![],
13537                shape: Shape::from_dims(&[2, 3]),
13538                dtype: DType::F32,
13539            });
13540            let op_id = g.push(Node {
13541                op: Op::ReduceMaxTo(Shape::from_dims(&[3])),
13542                inputs: vec![in_id],
13543                shape: Shape::from_dims(&[3]),
13544                dtype: DType::F32,
13545            });
13546            g.set_target_backend(op_id, BackendId::Cpu);
13547            (in_id, op_id)
13548        };
13549        let mut inputs = StorageCache::new();
13550        inputs.insert(in_id, Arc::new(RwLock::new(v)));
13551        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13552        let guard = result_arc.read().unwrap();
13553        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13554            panic!("expected Cpu storage");
13555        };
13556        assert_eq!(c.as_slice::<f32>().unwrap(), &[4.0, 7.0, 6.0]);
13557    }
13558
13559    /// E2E: Op::ReduceMaxTo F32 — keep-dim with 1 in the trailing axis.
13560    /// Mirrors the SoftmaxLastDim lowering's max-side shape: input
13561    /// [..., last] → [..., 1].
13562    #[test]
13563    fn pipelined_realize_reduce_max_to_f32_keepdim_trailing() {
13564        // Input [2, 3]: row maxes = [3, 6].
13565        let v = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13566        let graph = Arc::new(RwLock::new(Graph::new()));
13567        let (in_id, op_id) = {
13568            let mut g = graph.write().unwrap();
13569            let in_id = g.push(Node {
13570                op: Op::Const,
13571                inputs: vec![],
13572                shape: Shape::from_dims(&[2, 3]),
13573                dtype: DType::F32,
13574            });
13575            let op_id = g.push(Node {
13576                op: Op::ReduceMaxTo(Shape::from_dims(&[2, 1])),
13577                inputs: vec![in_id],
13578                shape: Shape::from_dims(&[2, 1]),
13579                dtype: DType::F32,
13580            });
13581            g.set_target_backend(op_id, BackendId::Cpu);
13582            (in_id, op_id)
13583        };
13584        let mut inputs = StorageCache::new();
13585        inputs.insert(in_id, Arc::new(RwLock::new(v)));
13586        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13587        let guard = result_arc.read().unwrap();
13588        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13589            panic!("expected Cpu storage");
13590        };
13591        assert_eq!(c.as_slice::<f32>().unwrap(), &[3.0, 6.0]);
13592    }
13593
13594    /// E2E: ReduceSumTo BF16 — tolerant compare via f32-acc.
13595    #[test]
13596    fn pipelined_realize_reduce_sum_to_bf16() {
13597        let v: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]
13598            .iter()
13599            .map(|x| half::bf16::from_f32(*x))
13600            .collect();
13601        let s = fuel_memory::from_slice_cpu(&v);
13602        let graph = Arc::new(RwLock::new(Graph::new()));
13603        let (in_id, op_id) = {
13604            let mut g = graph.write().unwrap();
13605            let in_id = g.push(Node {
13606                op: Op::Const,
13607                inputs: vec![],
13608                shape: Shape::from_dims(&[2, 3]),
13609                dtype: DType::BF16,
13610            });
13611            let op_id = g.push(Node {
13612                op: Op::ReduceSumTo(Shape::from_dims(&[3])),
13613                inputs: vec![in_id],
13614                shape: Shape::from_dims(&[3]),
13615                dtype: DType::BF16,
13616            });
13617            g.set_target_backend(op_id, BackendId::Cpu);
13618            (in_id, op_id)
13619        };
13620        let mut inputs = StorageCache::new();
13621        inputs.insert(in_id, Arc::new(RwLock::new(s)));
13622        let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13623        let guard = result_arc.read().unwrap();
13624        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13625            panic!("expected Cpu storage");
13626        };
13627        let got: Vec<f32> = c
13628            .as_slice::<half::bf16>()
13629            .unwrap()
13630            .iter()
13631            .map(|v| v.to_f32())
13632            .collect();
13633        let want = [5.0_f32, 7.0, 9.0];
13634        for (g, w) in got.iter().zip(want.iter()) {
13635            assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13636        }
13637    }
13638
13639    /// E2E: Op::ConvTranspose2D — F32 spread test.
13640    /// x = [[1, 2], [3, 4]] shape [1,1,2,2], all-ones kernel
13641    /// shape [1,1,2,2], stride=1, padding=0, dilation=1, no bias.
13642    /// Expected output (3x3):
13643    ///   [[1,  3, 2],
13644    ///    [4, 10, 6],
13645    ///    [3,  7, 4]]
13646    #[test]
13647    fn pipelined_realize_conv_transpose2d_f32() {
13648        let x_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
13649        let w_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
13650        let graph = Arc::new(RwLock::new(Graph::new()));
13651        let (x_id, w_id, c_id) = {
13652            let mut g = graph.write().unwrap();
13653            let x = g.push(Node {
13654                op: Op::Const,
13655                inputs: vec![],
13656                shape: Shape::from_dims(&[1, 1, 2, 2]),
13657                dtype: DType::F32,
13658            });
13659            let w = g.push(Node {
13660                op: Op::Const,
13661                inputs: vec![],
13662                shape: Shape::from_dims(&[1, 1, 2, 2]),
13663                dtype: DType::F32,
13664            });
13665            let c = g.push(Node {
13666                op: Op::Fused(
13667                    fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D,
13668                    fuel_graph::registry::FusedOpParams::ConvTranspose2D {
13669                        stride: (1, 1),
13670                        padding: (0, 0),
13671                        output_padding: (0, 0),
13672                        dilation: (1, 1),
13673                        groups: 1,
13674                    },
13675                ),
13676                inputs: vec![x, w],
13677                shape: Shape::from_dims(&[1, 1, 3, 3]),
13678                dtype: DType::F32,
13679            });
13680            g.set_target_backend(c, BackendId::Cpu);
13681            (x, w, c)
13682        };
13683        let mut inputs = StorageCache::new();
13684        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
13685        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
13686        let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
13687        let guard = result_arc.read().unwrap();
13688        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13689            panic!("expected Cpu storage");
13690        };
13691        assert_eq!(
13692            c.as_slice::<f32>().unwrap(),
13693            &[1.0, 3.0, 2.0, 4.0, 10.0, 6.0, 3.0, 7.0, 4.0],
13694        );
13695    }
13696
13697    /// E2E: ConvTranspose2D F64 — same shape test.
13698    #[test]
13699    fn pipelined_realize_conv_transpose2d_f64() {
13700        let x_storage = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0]);
13701        let w_storage = fuel_memory::from_slice_cpu(&[1.0_f64, 1.0, 1.0, 1.0]);
13702        let graph = Arc::new(RwLock::new(Graph::new()));
13703        let (x_id, w_id, c_id) = {
13704            let mut g = graph.write().unwrap();
13705            let x = g.push(Node {
13706                op: Op::Const,
13707                inputs: vec![],
13708                shape: Shape::from_dims(&[1, 1, 2, 2]),
13709                dtype: DType::F64,
13710            });
13711            let w = g.push(Node {
13712                op: Op::Const,
13713                inputs: vec![],
13714                shape: Shape::from_dims(&[1, 1, 2, 2]),
13715                dtype: DType::F64,
13716            });
13717            let c = g.push(Node {
13718                op: Op::Fused(
13719                    fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D,
13720                    fuel_graph::registry::FusedOpParams::ConvTranspose2D {
13721                        stride: (1, 1),
13722                        padding: (0, 0),
13723                        output_padding: (0, 0),
13724                        dilation: (1, 1),
13725                        groups: 1,
13726                    },
13727                ),
13728                inputs: vec![x, w],
13729                shape: Shape::from_dims(&[1, 1, 3, 3]),
13730                dtype: DType::F64,
13731            });
13732            g.set_target_backend(c, BackendId::Cpu);
13733            (x, w, c)
13734        };
13735        let mut inputs = StorageCache::new();
13736        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
13737        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
13738        let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
13739        let guard = result_arc.read().unwrap();
13740        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13741            panic!("expected Cpu storage");
13742        };
13743        assert_eq!(
13744            c.as_slice::<f64>().unwrap(),
13745            &[1.0, 3.0, 2.0, 4.0, 10.0, 6.0, 3.0, 7.0, 4.0],
13746        );
13747    }
13748
13749    /// E2E: ConvTranspose2D BF16 — tolerant compare via f32-acc.
13750    #[test]
13751    fn pipelined_realize_conv_transpose2d_bf16() {
13752        let x: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0]
13753            .iter()
13754            .map(|v| half::bf16::from_f32(*v))
13755            .collect();
13756        let w: Vec<half::bf16> = [1.0_f32, 1.0, 1.0, 1.0]
13757            .iter()
13758            .map(|v| half::bf16::from_f32(*v))
13759            .collect();
13760        let x_storage = fuel_memory::from_slice_cpu(&x);
13761        let w_storage = fuel_memory::from_slice_cpu(&w);
13762        let graph = Arc::new(RwLock::new(Graph::new()));
13763        let (x_id, w_id, c_id) = {
13764            let mut g = graph.write().unwrap();
13765            let x = g.push(Node {
13766                op: Op::Const,
13767                inputs: vec![],
13768                shape: Shape::from_dims(&[1, 1, 2, 2]),
13769                dtype: DType::BF16,
13770            });
13771            let w = g.push(Node {
13772                op: Op::Const,
13773                inputs: vec![],
13774                shape: Shape::from_dims(&[1, 1, 2, 2]),
13775                dtype: DType::BF16,
13776            });
13777            let c = g.push(Node {
13778                op: Op::Fused(
13779                    fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D,
13780                    fuel_graph::registry::FusedOpParams::ConvTranspose2D {
13781                        stride: (1, 1),
13782                        padding: (0, 0),
13783                        output_padding: (0, 0),
13784                        dilation: (1, 1),
13785                        groups: 1,
13786                    },
13787                ),
13788                inputs: vec![x, w],
13789                shape: Shape::from_dims(&[1, 1, 3, 3]),
13790                dtype: DType::BF16,
13791            });
13792            g.set_target_backend(c, BackendId::Cpu);
13793            (x, w, c)
13794        };
13795        let mut inputs = StorageCache::new();
13796        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
13797        inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
13798        let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
13799        let guard = result_arc.read().unwrap();
13800        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13801            panic!("expected Cpu storage");
13802        };
13803        let got: Vec<f32> = c
13804            .as_slice::<half::bf16>()
13805            .unwrap()
13806            .iter()
13807            .map(|v| v.to_f32())
13808            .collect();
13809        let want = [1.0_f32, 3.0, 2.0, 4.0, 10.0, 6.0, 3.0, 7.0, 4.0];
13810        for (g, w) in got.iter().zip(want.iter()) {
13811            assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13812        }
13813    }
13814
13815    /// E2E: GQA-style matmul through the pipelined executor.
13816    /// lhs has 4 batch heads, rhs has 2; each rhs head is shared
13817    /// by 2 lhs heads. Output's batch dim follows lhs (4 heads).
13818    #[test]
13819    fn pipelined_realize_matmul_gqa() {
13820        // lhs [4, 1, 2]: heads 0..3 are [[1,2]], [[3,4]], [[5,6]], [[7,8]]
13821        // rhs [2, 2, 1]: heads 0,1 are [[1],[0]], [[0],[1]]
13822        // Expected out [4, 1, 1]: [[1]], [[3]], [[6]], [[8]]
13823        let lhs_storage =
13824            fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
13825        let rhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
13826
13827        let graph = Arc::new(RwLock::new(Graph::new()));
13828        let (lhs_id, rhs_id, mm_id) = {
13829            let mut g = graph.write().unwrap();
13830            let lhs = g.push(Node {
13831                op: Op::Const,
13832                inputs: vec![],
13833                shape: Shape::from_dims(&[4, 1, 2]),
13834                dtype: DType::F32,
13835            });
13836            let rhs = g.push(Node {
13837                op: Op::Const,
13838                inputs: vec![],
13839                shape: Shape::from_dims(&[2, 2, 1]),
13840                dtype: DType::F32,
13841            });
13842            let mm = g.push(Node {
13843                op: Op::MatMul,
13844                inputs: vec![lhs, rhs],
13845                shape: Shape::from_dims(&[4, 1, 1]),
13846                dtype: DType::F32,
13847            });
13848            g.set_target_backend(mm, BackendId::Cpu);
13849            (lhs, rhs, mm)
13850        };
13851        let mut inputs = StorageCache::new();
13852        inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
13853        inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
13854
13855        let (result_arc, result_layout) =
13856            PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
13857        assert_eq!(result_layout.shape().dims(), &[4, 1, 1]);
13858
13859        let guard = result_arc.read().unwrap();
13860        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13861            panic!("expected Cpu storage");
13862        };
13863        assert_eq!(c.as_slice::<f32>().unwrap(), &[1.0, 3.0, 6.0, 8.0]);
13864    }
13865
13866    /// E2E: matmul with a transposed rhs — proves stage 3+4
13867    /// integration carries through the matmul path. The transpose
13868    /// is metadata-only; auto-Contiguize materializes the strided
13869    /// rhs before the matmul kernel sees it.
13870    #[test]
13871    fn pipelined_realize_matmul_with_transposed_rhs() {
13872        // lhs [[1, 2], [3, 4]], rhs original [[5, 6], [7, 8]]
13873        // rhs.T = [[5, 7], [6, 8]]
13874        // lhs @ rhs.T = [[1*5+2*6, 1*7+2*8], [3*5+4*6, 3*7+4*8]]
13875        //             = [[17, 23], [39, 53]]
13876        let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
13877        let rhs_storage = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0, 7.0, 8.0]);
13878
13879        let graph = Arc::new(RwLock::new(Graph::new()));
13880        let (lhs_id, rhs_id, t_id, mm_id) = {
13881            let mut g = graph.write().unwrap();
13882            let lhs = g.push(Node {
13883                op: Op::Const,
13884                inputs: vec![],
13885                shape: Shape::from_dims(&[2, 2]),
13886                dtype: DType::F32,
13887            });
13888            let rhs = g.push(Node {
13889                op: Op::Const,
13890                inputs: vec![],
13891                shape: Shape::from_dims(&[2, 2]),
13892                dtype: DType::F32,
13893            });
13894            let t = g.push(Node {
13895                op: Op::Transpose,
13896                inputs: vec![rhs],
13897                shape: Shape::from_dims(&[2, 2]),
13898                dtype: DType::F32,
13899            });
13900            let mm = g.push(Node {
13901                op: Op::MatMul,
13902                inputs: vec![lhs, t],
13903                shape: Shape::from_dims(&[2, 2]),
13904                dtype: DType::F32,
13905            });
13906            g.set_target_backend(mm, BackendId::Cpu);
13907            (lhs, rhs, t, mm)
13908        };
13909        let _ = t_id;
13910        let mut inputs = StorageCache::new();
13911        inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
13912        inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
13913
13914        let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
13915        let guard = result_arc.read().unwrap();
13916        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13917            panic!("expected Cpu storage");
13918        };
13919        assert_eq!(c.as_slice::<f32>().unwrap(), &[17.0, 23.0, 39.0, 53.0]);
13920    }
13921
13922    /// E2E: Const + Reshape — contiguous-input reshape is zero
13923    /// copy. The output Storage Arc is the input Arc; the layout
13924    /// is contiguous in the new shape.
13925    #[test]
13926    fn pipelined_realize_reshape_zero_copy_when_contiguous() {
13927        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13928        let graph = Arc::new(RwLock::new(Graph::new()));
13929        let (in_id, r_id) = {
13930            let mut g = graph.write().unwrap();
13931            let in_id = g.push(Node {
13932                op: Op::Const,
13933                inputs: vec![],
13934                shape: Shape::from_dims(&[2, 3]),
13935                dtype: DType::F32,
13936            });
13937            let r_id = g.push(Node {
13938                op: Op::Reshape(Shape::from_dims(&[3, 2])),
13939                inputs: vec![in_id],
13940                shape: Shape::from_dims(&[3, 2]),
13941                dtype: DType::F32,
13942            });
13943            (in_id, r_id)
13944        };
13945        let mut inputs = StorageCache::new();
13946        let in_arc = Arc::new(RwLock::new(storage));
13947        inputs.insert(in_id, Arc::clone(&in_arc));
13948
13949        let (result_arc, result_layout) =
13950            PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
13951
13952        // Zero copy — same Arc.
13953        assert!(
13954            Arc::ptr_eq(&result_arc, &in_arc),
13955            "contiguous reshape must zero-copy"
13956        );
13957        assert_eq!(result_layout.shape().dims(), &[3, 2]);
13958        assert!(result_layout.is_contiguous());
13959
13960        // Bytes are unchanged; just reinterpreted as [3, 2].
13961        let guard = result_arc.read().unwrap();
13962        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13963            panic!("expected Cpu storage");
13964        };
13965        assert_eq!(
13966            c.as_slice::<f32>().unwrap(),
13967            &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
13968        );
13969    }
13970
13971    /// E2E: Const + Transpose + Reshape — reshape on a strided
13972    /// input auto-contiguizes the bytes. Output Arc is fresh
13973    /// (NOT the input Arc); the bytes are the materialized
13974    /// transposed layout.
13975    #[test]
13976    fn pipelined_realize_reshape_materializes_when_strided() {
13977        // shape [2, 3]: 1 2 3 / 4 5 6
13978        // Transpose → [3, 2] strided
13979        // Reshape → [6] (forces materialization)
13980        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13981        let graph = Arc::new(RwLock::new(Graph::new()));
13982        let (in_id, t_id, r_id) = {
13983            let mut g = graph.write().unwrap();
13984            let in_id = g.push(Node {
13985                op: Op::Const,
13986                inputs: vec![],
13987                shape: Shape::from_dims(&[2, 3]),
13988                dtype: DType::F32,
13989            });
13990            let t_id = g.push(Node {
13991                op: Op::Transpose,
13992                inputs: vec![in_id],
13993                shape: Shape::from_dims(&[3, 2]),
13994                dtype: DType::F32,
13995            });
13996            let r_id = g.push(Node {
13997                op: Op::Reshape(Shape::from_dims(&[6])),
13998                inputs: vec![t_id],
13999                shape: Shape::from_dims(&[6]),
14000                dtype: DType::F32,
14001            });
14002            (in_id, t_id, r_id)
14003        };
14004        let _ = t_id;
14005        let mut inputs = StorageCache::new();
14006        let in_arc = Arc::new(RwLock::new(storage));
14007        inputs.insert(in_id, Arc::clone(&in_arc));
14008
14009        let (result_arc, result_layout) =
14010            PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
14011
14012        // Fresh Arc — auto-contiguize allocated new bytes.
14013        assert!(!Arc::ptr_eq(&result_arc, &in_arc));
14014        assert_eq!(result_layout.shape().dims(), &[6]);
14015        assert!(result_layout.is_contiguous());
14016
14017        // Materialized transposed bytes flattened: [1, 4, 2, 5, 3, 6].
14018        let guard = result_arc.read().unwrap();
14019        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14020            panic!("expected Cpu storage");
14021        };
14022        assert_eq!(
14023            c.as_slice::<f32>().unwrap(),
14024            &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]
14025        );
14026    }
14027
14028    /// E2E: Const + Transpose + SumDim — exercises stage 3
14029    /// (metadata-only Transpose, strided intermediate Layout) +
14030    /// stage 4 (auto-Contiguize before reduce kernel) end-to-end.
14031    /// The transpose makes the intermediate non-contiguous; the
14032    /// reduce wrapper would have failed in stage 2; with stage 4's
14033    /// auto-Contiguize, the kernel sees the materialized contiguous
14034    /// transposed bytes and produces the right answer.
14035    #[test]
14036    fn pipelined_realize_transpose_then_sum_dim_e2e() {
14037        // shape [2, 3]: rows are [1, 2, 3], [4, 5, 6]
14038        // After transpose: shape [3, 2], rows are [1, 4], [2, 5], [3, 6]
14039        // After SumDim(1): shape [3], values [5, 7, 9]
14040        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
14041        let graph = Arc::new(RwLock::new(Graph::new()));
14042        let (in_id, t_id, sum_id) = {
14043            let mut g = graph.write().unwrap();
14044            let in_id = g.push(Node {
14045                op: Op::Const,
14046                inputs: vec![],
14047                shape: Shape::from_dims(&[2, 3]),
14048                dtype: DType::F32,
14049            });
14050            let t_id = g.push(Node {
14051                op: Op::Transpose,
14052                inputs: vec![in_id],
14053                shape: Shape::from_dims(&[3, 2]),
14054                dtype: DType::F32,
14055            });
14056            let sum_id = g.push(Node {
14057                op: Op::SumDim(1),
14058                inputs: vec![t_id],
14059                shape: Shape::from_dims(&[3]),
14060                dtype: DType::F32,
14061            });
14062            // Only the reduce kernel runs on the backend; the
14063            // transpose is metadata-only and doesn't need a target.
14064            g.set_target_backend(sum_id, BackendId::Cpu);
14065            (in_id, t_id, sum_id)
14066        };
14067        let _ = t_id;
14068        let mut inputs = StorageCache::new();
14069        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14070
14071        let (result_arc, result_layout) =
14072            PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
14073
14074        // The reduce output is contiguous (kernel-produced).
14075        assert_eq!(result_layout.shape().dims(), &[3]);
14076        assert!(result_layout.is_contiguous());
14077
14078        let guard = result_arc.read().unwrap();
14079        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14080            panic!("expected Cpu storage");
14081        };
14082        let typed: &[f32] = c.as_slice().unwrap();
14083        assert_eq!(typed, &[5.0, 7.0, 9.0]);
14084    }
14085
14086    /// E2E: Const + BroadcastTo + Add — broadcast intermediate
14087    /// auto-contiguizes for the Add kernel; the result is the
14088    /// expected sum.
14089    #[test]
14090    fn pipelined_realize_broadcast_then_add_e2e() {
14091        // shape [3]: [10, 20, 30]
14092        // BroadcastTo [2, 3]: [[10, 20, 30], [10, 20, 30]]
14093        // Plus shape [2, 3]: [[1, 2, 3], [4, 5, 6]]
14094        // Result: [[11, 22, 33], [14, 25, 36]]
14095        let bc_input = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
14096        let plus_input = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
14097        let graph = Arc::new(RwLock::new(Graph::new()));
14098        let (bc_in_id, plus_in_id, b_id, add_id) = {
14099            let mut g = graph.write().unwrap();
14100            let bc_in = g.push(Node {
14101                op: Op::Const,
14102                inputs: vec![],
14103                shape: Shape::from_dims(&[3]),
14104                dtype: DType::F32,
14105            });
14106            let plus_in = g.push(Node {
14107                op: Op::Const,
14108                inputs: vec![],
14109                shape: Shape::from_dims(&[2, 3]),
14110                dtype: DType::F32,
14111            });
14112            let b = g.push(Node {
14113                op: Op::BroadcastTo(Shape::from_dims(&[2, 3])),
14114                inputs: vec![bc_in],
14115                shape: Shape::from_dims(&[2, 3]),
14116                dtype: DType::F32,
14117            });
14118            let add = g.push(Node {
14119                op: Op::Add,
14120                inputs: vec![b, plus_in],
14121                shape: Shape::from_dims(&[2, 3]),
14122                dtype: DType::F32,
14123            });
14124            g.set_target_backend(add, BackendId::Cpu);
14125            (bc_in, plus_in, b, add)
14126        };
14127        let _ = b_id;
14128        let mut inputs = StorageCache::new();
14129        inputs.insert(bc_in_id, Arc::new(RwLock::new(bc_input)));
14130        inputs.insert(plus_in_id, Arc::new(RwLock::new(plus_input)));
14131
14132        let (result_arc, _result_layout) =
14133            PipelinedExecutor::realize(graph, add_id, inputs).expect("realize");
14134        let guard = result_arc.read().unwrap();
14135        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14136            panic!("expected Cpu storage");
14137        };
14138        let typed: &[f32] = c.as_slice().unwrap();
14139        assert_eq!(typed, &[11.0, 22.0, 33.0, 14.0, 25.0, 36.0]);
14140    }
14141
14142    /// E2E: Const + SumDim — verifies that `OpParams::Reduce`
14143    /// flows from the graph (input shape via `op_to_op_params`)
14144    /// through compile_one and reaches the reduce kernel.
14145    #[test]
14146    fn pipelined_realize_sum_dim() {
14147        // shape [2, 3]; reduce dim 1 → output shape [2]
14148        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
14149        let graph = Arc::new(RwLock::new(Graph::new()));
14150        let (in_id, sum_id) = {
14151            let mut g = graph.write().unwrap();
14152            let in_id = g.push(Node {
14153                op: Op::Const,
14154                inputs: vec![],
14155                shape: Shape::from_dims(&[2, 3]),
14156                dtype: DType::F32,
14157            });
14158            let sum_id = g.push(Node {
14159                op: Op::SumDim(1),
14160                inputs: vec![in_id],
14161                shape: Shape::from_dims(&[2]),
14162                dtype: DType::F32,
14163            });
14164            g.set_target_backend(sum_id, BackendId::Cpu);
14165            (in_id, sum_id)
14166        };
14167        let mut inputs = StorageCache::new();
14168        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14169
14170        let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
14171        let guard = result_arc.read().unwrap();
14172        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14173            let typed: &[f32] = c.as_slice().unwrap();
14174            assert_eq!(typed, &[6.0, 15.0]);
14175        }
14176    }
14177
14178    /// E2E: SumAll on a rank-3 input, exercising the all-dims branch
14179    /// of `op_to_op_params` (every dim reduced, rank-0 output).
14180    #[test]
14181    fn pipelined_realize_sum_all_rank3() {
14182        let data: Vec<f32> = (1..=24).map(|x| x as f32).collect();
14183        let storage = fuel_memory::from_slice_cpu(&data);
14184        let graph = Arc::new(RwLock::new(Graph::new()));
14185        let (in_id, sum_id) = {
14186            let mut g = graph.write().unwrap();
14187            let in_id = g.push(Node {
14188                op: Op::Const,
14189                inputs: vec![],
14190                shape: Shape::from_dims(&[2, 3, 4]),
14191                dtype: DType::F32,
14192            });
14193            let sum_id = g.push(Node {
14194                op: Op::SumAll,
14195                inputs: vec![in_id],
14196                shape: Shape::from_dims(&[]),
14197                dtype: DType::F32,
14198            });
14199            g.set_target_backend(sum_id, BackendId::Cpu);
14200            (in_id, sum_id)
14201        };
14202        let mut inputs = StorageCache::new();
14203        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14204
14205        let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
14206        let guard = result_arc.read().unwrap();
14207        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14208            let typed: &[f32] = c.as_slice().unwrap();
14209            // 1 + 2 + ... + 24 = 300
14210            assert_eq!(typed, &[300.0]);
14211        }
14212    }
14213
14214    /// E2E: MaxDim + MeanDim chained — verifies all four reduce
14215    /// OpKinds reach their wrappers via the OpParams plumbing.
14216    #[test]
14217    fn pipelined_realize_max_then_mean() {
14218        // shape [2, 3], MaxDim(1) → [2], MeanDim(0) → []
14219        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 9.0, 3.0, 4.0, 2.0, 8.0]);
14220        let graph = Arc::new(RwLock::new(Graph::new()));
14221        let (in_id, mean_id) = {
14222            let mut g = graph.write().unwrap();
14223            let in_id = g.push(Node {
14224                op: Op::Const,
14225                inputs: vec![],
14226                shape: Shape::from_dims(&[2, 3]),
14227                dtype: DType::F32,
14228            });
14229            let max_id = g.push(Node {
14230                op: Op::MaxDim(1),
14231                inputs: vec![in_id],
14232                shape: Shape::from_dims(&[2]),
14233                dtype: DType::F32,
14234            });
14235            let mean_id = g.push(Node {
14236                op: Op::MeanDim(0),
14237                inputs: vec![max_id],
14238                shape: Shape::from_dims(&[]),
14239                dtype: DType::F32,
14240            });
14241            g.set_target_backend(max_id, BackendId::Cpu);
14242            g.set_target_backend(mean_id, BackendId::Cpu);
14243            (in_id, mean_id)
14244        };
14245        let mut inputs = StorageCache::new();
14246        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14247
14248        let (result_arc, _) = PipelinedExecutor::realize(graph, mean_id, inputs).expect("realize");
14249        let guard = result_arc.read().unwrap();
14250        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14251            let typed: &[f32] = c.as_slice().unwrap();
14252            // MaxDim(1) on [[1,9,3],[4,2,8]] = [9, 8]; MeanDim(0) = 8.5
14253            assert_eq!(typed, &[8.5]);
14254        }
14255    }
14256
14257    /// E2E: Sigmoid + Silu — exercises two of the more compositional
14258    /// new unary kernels through the pipelined executor. Verifies
14259    /// the additional `op_to_op_kind` mappings reach the right
14260    /// dispatch wrappers.
14261    #[test]
14262    fn pipelined_realize_sigmoid_then_silu() {
14263        let storage = fuel_memory::from_slice_cpu(&[0.0_f32, 1.0, -1.0]);
14264        let graph = Arc::new(RwLock::new(Graph::new()));
14265        let (in_id, sig_id, silu_id) = {
14266            let mut g = graph.write().unwrap();
14267            let in_id = g.push(Node {
14268                op: Op::Const,
14269                inputs: vec![],
14270                shape: Shape::from_dims(&[3]),
14271                dtype: DType::F32,
14272            });
14273            let sig_id = g.push(Node {
14274                op: Op::Sigmoid,
14275                inputs: vec![in_id],
14276                shape: Shape::from_dims(&[3]),
14277                dtype: DType::F32,
14278            });
14279            // Silu of the sigmoid output — chains to confirm cache flow.
14280            let silu_id = g.push(Node {
14281                op: Op::Silu,
14282                inputs: vec![sig_id],
14283                shape: Shape::from_dims(&[3]),
14284                dtype: DType::F32,
14285            });
14286            g.set_target_backend(sig_id, BackendId::Cpu);
14287            g.set_target_backend(silu_id, BackendId::Cpu);
14288            (in_id, sig_id, silu_id)
14289        };
14290        let _ = sig_id;
14291        let mut inputs = StorageCache::new();
14292        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14293
14294        let (result_arc, _) = PipelinedExecutor::realize(graph, silu_id, inputs).expect("realize");
14295        let guard = result_arc.read().unwrap();
14296        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14297            let typed: &[f32] = c.as_slice().unwrap();
14298            // sigmoid(0) = 0.5; silu(0.5) = 0.5 * sigmoid(0.5) ≈ 0.3112
14299            assert!((typed[0] - 0.5 * (1.0 / (1.0 + (-0.5_f32).exp()))).abs() < 1e-6);
14300        }
14301    }
14302
14303    /// E2E: chained unary ops — Const + Sqr + Sqrt should be a noop
14304    /// for non-negative inputs. Exercises the cache reuse path.
14305    #[test]
14306    fn pipelined_realize_chained_unary_sqr_then_sqrt() {
14307        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 4.0, 9.0]);
14308        let graph = Arc::new(RwLock::new(Graph::new()));
14309        let (in_id, sqrt_id) = {
14310            let mut g = graph.write().unwrap();
14311            let in_id = g.push(Node {
14312                op: Op::Const,
14313                inputs: vec![],
14314                shape: Shape::from_dims(&[3]),
14315                dtype: DType::F32,
14316            });
14317            let sqr_id = g.push(Node {
14318                op: Op::Sqr,
14319                inputs: vec![in_id],
14320                shape: Shape::from_dims(&[3]),
14321                dtype: DType::F32,
14322            });
14323            let sqrt_id = g.push(Node {
14324                op: Op::Sqrt,
14325                inputs: vec![sqr_id],
14326                shape: Shape::from_dims(&[3]),
14327                dtype: DType::F32,
14328            });
14329            g.set_target_backend(sqr_id, BackendId::Cpu);
14330            g.set_target_backend(sqrt_id, BackendId::Cpu);
14331            (in_id, sqrt_id)
14332        };
14333        let mut inputs = StorageCache::new();
14334        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14335
14336        let (result_arc, _) = PipelinedExecutor::realize(graph, sqrt_id, inputs).expect("realize");
14337        let guard = result_arc.read().unwrap();
14338        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14339            let typed: &[f32] = c.as_slice().unwrap();
14340            // sqrt(sqr(x)) == |x| == x for non-negative inputs.
14341            assert_eq!(typed, &[1.0, 4.0, 9.0]);
14342        }
14343    }
14344
14345    /// Multi-stage pipelined: Const + Const + Add + Add (chain of
14346    /// two adds). Tests that work items are processed in topo order
14347    /// and intermediate results are cached and reused.
14348    #[test]
14349    fn pipelined_realize_chained_adds() {
14350        let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
14351        let b_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
14352        let c_storage = fuel_memory::from_slice_cpu(&[100.0_f32, 200.0]);
14353
14354        let graph = Arc::new(RwLock::new(Graph::new()));
14355        let (a_id, b_id, c_id, ab_id, abc_id) = {
14356            let mut g = graph.write().unwrap();
14357            let a = g.push(Node {
14358                op: Op::Const,
14359                inputs: vec![],
14360                shape: Shape::from_dims(&[2]),
14361                dtype: DType::F32,
14362            });
14363            let b = g.push(Node {
14364                op: Op::Const,
14365                inputs: vec![],
14366                shape: Shape::from_dims(&[2]),
14367                dtype: DType::F32,
14368            });
14369            let c = g.push(Node {
14370                op: Op::Const,
14371                inputs: vec![],
14372                shape: Shape::from_dims(&[2]),
14373                dtype: DType::F32,
14374            });
14375            let ab = g.push(Node {
14376                op: Op::Add,
14377                inputs: vec![a, b],
14378                shape: Shape::from_dims(&[2]),
14379                dtype: DType::F32,
14380            });
14381            let abc = g.push(Node {
14382                op: Op::Add,
14383                inputs: vec![ab, c],
14384                shape: Shape::from_dims(&[2]),
14385                dtype: DType::F32,
14386            });
14387            g.set_target_backend(ab, BackendId::Cpu);
14388            g.set_target_backend(abc, BackendId::Cpu);
14389            (a, b, c, ab, abc)
14390        };
14391
14392        let mut inputs = StorageCache::new();
14393        inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
14394        inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
14395        inputs.insert(c_id, Arc::new(RwLock::new(c_storage)));
14396
14397        let (result_arc, _) = PipelinedExecutor::realize(graph, abc_id, inputs).expect("realize");
14398        // Suppress unused warning for the intermediate id.
14399        let _ = ab_id;
14400
14401        let guard = result_arc.read().unwrap();
14402        if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14403            let typed: &[f32] = c.as_slice().unwrap();
14404            // (1+10) + 100 = 111;  (2+20) + 200 = 222.
14405            assert_eq!(typed, &[111.0, 222.0]);
14406        }
14407    }
14408
14409    /// E2E: triu(diagonal=0) on a 3×3 matrix.
14410    /// Input:  [1 2 3 / 4 5 6 / 7 8 9]
14411    /// Output: [1 2 3 / 0 5 6 / 0 0 9]
14412    #[test]
14413    fn pipelined_realize_triu_3x3_diag0() {
14414        let storage =
14415            fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
14416        let graph = Arc::new(RwLock::new(Graph::new()));
14417        let (in_id, out_id) = {
14418            let mut g = graph.write().unwrap();
14419            let in_id = g.push(Node {
14420                op: Op::Const,
14421                inputs: vec![],
14422                shape: Shape::from_dims(&[3, 3]),
14423                dtype: DType::F32,
14424            });
14425            let out_id = g.push(Node {
14426                op: Op::Triu { diagonal: 0 },
14427                inputs: vec![in_id],
14428                shape: Shape::from_dims(&[3, 3]),
14429                dtype: DType::F32,
14430            });
14431            g.set_target_backend(out_id, BackendId::Cpu);
14432            (in_id, out_id)
14433        };
14434        let mut inputs = StorageCache::new();
14435        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14436        let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
14437        let guard = result_arc.read().unwrap();
14438        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14439            panic!("expected Cpu storage");
14440        };
14441        let out: &[f32] = c.as_slice().unwrap();
14442        assert_eq!(out, &[1.0, 2.0, 3.0, 0.0, 5.0, 6.0, 0.0, 0.0, 9.0,]);
14443    }
14444
14445    /// E2E: tril(diagonal=0) on a 3×3 matrix — the canonical causal mask.
14446    /// Output: [1 0 0 / 4 5 0 / 7 8 9]
14447    #[test]
14448    fn pipelined_realize_tril_3x3_diag0_causal_mask() {
14449        let storage =
14450            fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
14451        let graph = Arc::new(RwLock::new(Graph::new()));
14452        let (in_id, out_id) = {
14453            let mut g = graph.write().unwrap();
14454            let in_id = g.push(Node {
14455                op: Op::Const,
14456                inputs: vec![],
14457                shape: Shape::from_dims(&[3, 3]),
14458                dtype: DType::F32,
14459            });
14460            let out_id = g.push(Node {
14461                op: Op::Tril { diagonal: 0 },
14462                inputs: vec![in_id],
14463                shape: Shape::from_dims(&[3, 3]),
14464                dtype: DType::F32,
14465            });
14466            g.set_target_backend(out_id, BackendId::Cpu);
14467            (in_id, out_id)
14468        };
14469        let mut inputs = StorageCache::new();
14470        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14471        let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
14472        let guard = result_arc.read().unwrap();
14473        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14474            panic!("expected Cpu storage");
14475        };
14476        let out: &[f32] = c.as_slice().unwrap();
14477        assert_eq!(out, &[1.0, 0.0, 0.0, 4.0, 5.0, 0.0, 7.0, 8.0, 9.0,]);
14478    }
14479
14480    /// E2E: log_softmax over a 2×3 input. Rows are [1,2,3] and [3,2,1].
14481    /// log_softmax(row) = row - max - log(sum exp(row - max)).
14482    /// Row 0: max=3, exp(-2)+exp(-1)+exp(0) = 0.135+0.368+1.0 = 1.503;
14483    ///        log(1.503) ≈ 0.4076; out = [1-3-0.4076, 2-3-0.4076, 3-3-0.4076]
14484    ///                              = [-2.4076, -1.4076, -0.4076]
14485    /// Row 1 is row 0 reversed: [-0.4076, -1.4076, -2.4076].
14486    #[test]
14487    fn pipelined_realize_log_softmax_last_dim() {
14488        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 3.0, 2.0, 1.0]);
14489        let graph = Arc::new(RwLock::new(Graph::new()));
14490        let (in_id, out_id) = {
14491            let mut g = graph.write().unwrap();
14492            let in_id = g.push(Node {
14493                op: Op::Const,
14494                inputs: vec![],
14495                shape: Shape::from_dims(&[2, 3]),
14496                dtype: DType::F32,
14497            });
14498            let out_id = g.push(Node {
14499                op: Op::LogSoftmaxLastDim,
14500                inputs: vec![in_id],
14501                shape: Shape::from_dims(&[2, 3]),
14502                dtype: DType::F32,
14503            });
14504            g.set_target_backend(out_id, BackendId::Cpu);
14505            (in_id, out_id)
14506        };
14507        let mut inputs = StorageCache::new();
14508        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14509        let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
14510        let guard = result_arc.read().unwrap();
14511        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14512            panic!("expected Cpu storage");
14513        };
14514        let out: &[f32] = c.as_slice().unwrap();
14515        let expected = [
14516            -2.407_606,
14517            -1.4076059,
14518            -0.40760595_f32,
14519            -0.40760595,
14520            -1.4076059,
14521            -2.407_606,
14522        ];
14523        for (a, b) in out.iter().zip(expected.iter()) {
14524            assert!((a - b).abs() < 1e-5, "log_softmax mismatch: {a} vs {b}");
14525        }
14526        // The exp() of the output should sum to 1 per row.
14527        for row in out.chunks(3) {
14528            let sum: f32 = row.iter().map(|x| x.exp()).sum();
14529            assert!(
14530                (sum - 1.0).abs() < 1e-5,
14531                "softmax(log_softmax) row sum != 1: {sum}"
14532            );
14533        }
14534    }
14535
14536    // --- side-effect roots + destructive cleanup (9c Phase B) -----
14537
14538    /// Side-effect roots get merged into the realize walk even when
14539    /// not reachable from the user's targets. The production trigger
14540    /// is `Op::Release` (emitted by `ResidencyEvictionRule`); this
14541    /// test uses that exact shape.
14542    ///
14543    /// Graph: Const(a) + Const(b) → Add (user target); separate
14544    /// Const(c) → Release(c) (side-effect root, not reachable from
14545    /// Add). After realize_many on `[add]`, the Add should be in the
14546    /// output AND the Release should have fired (verified by the
14547    /// fact that realize succeeded — Op::Release in the walk used to
14548    /// fail compilation pre-Phase B+).
14549    #[test]
14550    fn pipelined_realize_merges_op_release_side_effect_root() {
14551        let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
14552        let b = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
14553        let c = fuel_memory::from_slice_cpu(&[100.0_f32, 200.0]);
14554        let graph = Arc::new(RwLock::new(Graph::new()));
14555        let (a_id, b_id, c_id, add_id, release_id) = {
14556            let mut g = graph.write().unwrap();
14557            let a = g.push(Node {
14558                op: Op::Const,
14559                inputs: vec![],
14560                shape: Shape::from_dims(&[2]),
14561                dtype: DType::F32,
14562            });
14563            let b = g.push(Node {
14564                op: Op::Const,
14565                inputs: vec![],
14566                shape: Shape::from_dims(&[2]),
14567                dtype: DType::F32,
14568            });
14569            // c is NOT reachable from add; its Release IS marked as
14570            // a side-effect root.
14571            let c = g.push(Node {
14572                op: Op::Const,
14573                inputs: vec![],
14574                shape: Shape::from_dims(&[2]),
14575                dtype: DType::F32,
14576            });
14577            let add = g.push(Node {
14578                op: Op::Add,
14579                inputs: vec![a, b],
14580                shape: Shape::from_dims(&[2]),
14581                dtype: DType::F32,
14582            });
14583            // Production shape: Op::Release on c, marked as
14584            // side-effect root so it fires even though no graph
14585            // node reads its output.
14586            let release = g.push(Node {
14587                op: Op::Release,
14588                inputs: vec![c],
14589                shape: Shape::from_dims(&[0]),
14590                dtype: DType::F32,
14591            });
14592            g.set_target_backend(add, BackendId::Cpu);
14593            g.set_target_backend(release, BackendId::Cpu);
14594            g.add_side_effect_root(release);
14595            (a, b, c, add, release)
14596        };
14597        let mut inputs = StorageCache::new();
14598        inputs.insert(a_id, Arc::new(RwLock::new(a)));
14599        inputs.insert(b_id, Arc::new(RwLock::new(b)));
14600        let c_arc = Arc::new(RwLock::new(c));
14601        let c_arc_external = Arc::clone(&c_arc);
14602        inputs.insert(c_id, c_arc);
14603
14604        // Only `add` is requested. The Release on c is a side-effect
14605        // root that must still fire.
14606        let out = PipelinedExecutor::realize_many(graph, &[add_id], inputs).expect("realize_many");
14607        assert_eq!(out.len(), 1);
14608        let add_guard = out[0].0.read().unwrap();
14609        let fuel_memory::BackendStorage::Cpu(arr) = &add_guard.inner else {
14610            panic!()
14611        };
14612        assert_eq!(arr.as_slice::<f32>().unwrap(), &[11.0, 22.0]);
14613
14614        // The Release's destructive_input cleanup dropped the
14615        // cache's Arc to c. The only Arc to c that survives is the
14616        // external one this test holds — confirms the eviction
14617        // actually freed the cache's reference.
14618        assert_eq!(
14619            Arc::strong_count(&c_arc_external),
14620            1,
14621            "Op::Release should evict the cache's Arc to its source; \
14622             only the test's external clone should remain",
14623        );
14624        let _ = release_id;
14625    }
14626
14627    /// `compile_one` emits a `WorkItemKind::ReleaseMarker` for
14628    /// `Op::Release` with `destructive_input = Some(0)`. Verify
14629    /// the WorkItem shape via a direct compile_one call.
14630    #[test]
14631    fn compile_one_emits_release_marker_with_destructive_input() {
14632        let graph_rc = Arc::new(RwLock::new(Graph::new()));
14633        let (src_id, release_id) = {
14634            let mut g = graph_rc.write().unwrap();
14635            let src = g.push(Node {
14636                op: Op::Const,
14637                inputs: vec![],
14638                shape: Shape::from_dims(&[2]),
14639                dtype: DType::F32,
14640            });
14641            let release = g.push(Node {
14642                op: Op::Release,
14643                inputs: vec![src],
14644                shape: Shape::from_dims(&[0]),
14645                dtype: DType::F32,
14646            });
14647            g.set_target_backend(release, BackendId::Cpu);
14648            (src, release)
14649        };
14650        let g = graph_rc.read().unwrap();
14651        let bindings = crate::dispatch::global_bindings();
14652        let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14653        let item = compile_one(
14654            &g,
14655            release_id,
14656            &mut layout_cache,
14657            &bindings,
14658            &SymEnv::default(),
14659        )
14660        .expect("compile_one Op::Release");
14661        assert!(matches!(item.kind, WorkItemKind::ReleaseMarker));
14662        assert_eq!(item.destructive_input, Some(0));
14663        assert_eq!(item.inputs, vec![src_id]);
14664        assert_eq!(
14665            item.elem_count, 0,
14666            "Release output is the zero-element marker"
14667        );
14668    }
14669
14670    // --- Op::Move (executor-unification Session 1, gap 13) --------
14671
14672    /// `compile_one` emits a `WorkItemKind::Move` for `Op::Move` with
14673    /// `destructive_input = Some(0)` and a resolved transfer kernel
14674    /// (binding-table lookup at `(OpKind::Copy, [dt, dt], source
14675    /// backend)` — Move shares Op::Copy's data-movement kernel).
14676    #[test]
14677    fn compile_one_emits_move_with_destructive_input() {
14678        let graph_rc = Arc::new(RwLock::new(Graph::new()));
14679        let (src_id, move_id) = {
14680            let mut g = graph_rc.write().unwrap();
14681            let src = g.push(Node {
14682                op: Op::Const,
14683                inputs: vec![],
14684                shape: Shape::from_dims(&[2]),
14685                dtype: DType::F32,
14686            });
14687            let mv = g.push(Node {
14688                op: Op::Move {
14689                    target: DeviceLocation::Cpu,
14690                },
14691                inputs: vec![src],
14692                shape: Shape::from_dims(&[2]),
14693                dtype: DType::F32,
14694            });
14695            g.set_target_backend(mv, BackendId::Cpu);
14696            (src, mv)
14697        };
14698        let g = graph_rc.read().unwrap();
14699        let bindings = crate::dispatch::global_bindings();
14700        let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14701        let item = compile_one(
14702            &g,
14703            move_id,
14704            &mut layout_cache,
14705            &bindings,
14706            &SymEnv::default(),
14707        )
14708        .expect("compile_one Op::Move");
14709        assert!(matches!(
14710            item.kind,
14711            WorkItemKind::Move {
14712                target_location: DeviceLocation::Cpu
14713            },
14714        ));
14715        assert_eq!(item.destructive_input, Some(0), "Move destroys its source");
14716        assert_eq!(item.inputs, vec![src_id]);
14717        assert!(
14718            item.compiled.is_some(),
14719            "Move resolves a transfer kernel at (OpKind::Copy, [dt, dt], source backend)",
14720        );
14721    }
14722
14723    // --- runtime fused op (JIT-adopted Tier-2 kernel) --------------
14724
14725    /// `compile_one` resolves an `Op::Fused(fid, Runtime)` node with
14726    /// `fid.is_runtime()` from the runtime-kernel sidecar (never the
14727    /// binding table): `WorkItemKind::Kernel`, `compiled.op ==
14728    /// OpKind::RuntimeFused`, and the kernel pointer is exactly the one
14729    /// `adopt_runtime_fused` bound.
14730    #[test]
14731    fn compile_one_resolves_runtime_fused_from_the_sidecar() {
14732        fn sentinel_kernel(
14733            _inputs: &[Arc<RwLock<fuel_memory::Storage>>],
14734            _outputs: &mut [Arc<RwLock<fuel_memory::Storage>>],
14735            _layouts: &[Layout],
14736            _params: &crate::kernel::OpParams,
14737        ) -> Result<()> {
14738            Ok(())
14739        }
14740        // A region shape unique to this test (process-global sidecar; see
14741        // runtime_fused_pathfinder's tests for the collision rationale).
14742        let region = fuel_graph::jit::PatternNode::Op {
14743            op: fuel_graph::jit::OpTag::Exp,
14744            attrs: fuel_graph::jit::OpAttrs::default(),
14745            operands: vec![fuel_graph::jit::PatternNode::Op {
14746                op: fuel_graph::jit::OpTag::Sub,
14747                attrs: fuel_graph::jit::OpAttrs::default(),
14748                operands: vec![
14749                    fuel_graph::jit::PatternNode::Bind { index: 0 },
14750                    fuel_graph::jit::PatternNode::Bind { index: 1 },
14751                ],
14752            }],
14753        };
14754        let rid = crate::runtime_fused_kernels::adopt_runtime_fused(
14755            "test::compile_one::exp_sub",
14756            region,
14757            sentinel_kernel as crate::kernel::KernelRef,
14758            vec![DType::F32, DType::F32, DType::F32],
14759            BackendId::Cpu,
14760        )
14761        .expect("registrable region");
14762
14763        let graph_rc = Arc::new(RwLock::new(Graph::new()));
14764        let fused_id = {
14765            let mut g = graph_rc.write().unwrap();
14766            let s = Shape::from_dims(&[4]);
14767            let a = g.push(Node {
14768                op: Op::Const,
14769                inputs: vec![],
14770                shape: s.clone(),
14771                dtype: DType::F32,
14772            });
14773            let b = g.push(Node {
14774                op: Op::Const,
14775                inputs: vec![],
14776                shape: s.clone(),
14777                dtype: DType::F32,
14778            });
14779            let fused = g.push(Node {
14780                op: Op::Fused(
14781                    rid,
14782                    fuel_graph::registry::FusedOpParams::Runtime {
14783                        scalars: Vec::new(),
14784                    },
14785                ),
14786                inputs: vec![a, b],
14787                shape: s,
14788                dtype: DType::F32,
14789            });
14790            // The arm emitter pins the backend; mirror that here.
14791            g.set_target_backend(fused, BackendId::Cpu);
14792            fused
14793        };
14794        let g = graph_rc.read().unwrap();
14795        let bindings = crate::dispatch::global_bindings();
14796        let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14797        let item = compile_one(
14798            &g,
14799            fused_id,
14800            &mut layout_cache,
14801            &bindings,
14802            &SymEnv::default(),
14803        )
14804        .expect("compile_one runtime fused op");
14805        assert!(matches!(item.kind, WorkItemKind::Kernel));
14806        let compiled = item.compiled.expect("resolved a kernel");
14807        assert_eq!(compiled.op, fuel_ir::dispatch::OpKind::RuntimeFused);
14808        assert_eq!(compiled.backend, BackendId::Cpu);
14809        assert!(
14810            std::ptr::fn_addr_eq(compiled.kernel, sentinel_kernel as crate::kernel::KernelRef),
14811            "the kernel pointer is exactly the adopted one",
14812        );
14813        assert!(
14814            layout_cache.contains_key(&fused_id),
14815            "output layout cached for consumers"
14816        );
14817    }
14818
14819    /// The dtype belt-and-suspenders guard: an `Op::Fused(runtime)` node
14820    /// whose per-operand dtypes disagree with the adopted kernel's tuple is
14821    /// a typed error, not a silent wrong-kernel dispatch.
14822    #[test]
14823    fn compile_one_rejects_runtime_fused_dtype_mismatch() {
14824        fn noop_kernel(
14825            _inputs: &[Arc<RwLock<fuel_memory::Storage>>],
14826            _outputs: &mut [Arc<RwLock<fuel_memory::Storage>>],
14827            _layouts: &[Layout],
14828            _params: &crate::kernel::OpParams,
14829        ) -> Result<()> {
14830            Ok(())
14831        }
14832        let region = fuel_graph::jit::PatternNode::Op {
14833            op: fuel_graph::jit::OpTag::Log,
14834            attrs: fuel_graph::jit::OpAttrs::default(),
14835            operands: vec![fuel_graph::jit::PatternNode::Op {
14836                op: fuel_graph::jit::OpTag::Maximum,
14837                attrs: fuel_graph::jit::OpAttrs::default(),
14838                operands: vec![
14839                    fuel_graph::jit::PatternNode::Bind { index: 0 },
14840                    fuel_graph::jit::PatternNode::Bind { index: 1 },
14841                ],
14842            }],
14843        };
14844        // Adopted for F32 operands…
14845        let rid = crate::runtime_fused_kernels::adopt_runtime_fused(
14846            "test::compile_one::log_maximum",
14847            region,
14848            noop_kernel as crate::kernel::KernelRef,
14849            vec![DType::F32, DType::F32, DType::F32],
14850            BackendId::Cpu,
14851        )
14852        .expect("registrable region");
14853
14854        let graph_rc = Arc::new(RwLock::new(Graph::new()));
14855        let fused_id = {
14856            let mut g = graph_rc.write().unwrap();
14857            let s = Shape::from_dims(&[4]);
14858            // …but the node's operands are F64.
14859            let a = g.push(Node {
14860                op: Op::Const,
14861                inputs: vec![],
14862                shape: s.clone(),
14863                dtype: DType::F64,
14864            });
14865            let b = g.push(Node {
14866                op: Op::Const,
14867                inputs: vec![],
14868                shape: s.clone(),
14869                dtype: DType::F64,
14870            });
14871            let fused = g.push(Node {
14872                op: Op::Fused(
14873                    rid,
14874                    fuel_graph::registry::FusedOpParams::Runtime {
14875                        scalars: Vec::new(),
14876                    },
14877                ),
14878                inputs: vec![a, b],
14879                shape: s,
14880                dtype: DType::F64,
14881            });
14882            g.set_target_backend(fused, BackendId::Cpu);
14883            fused
14884        };
14885        let g = graph_rc.read().unwrap();
14886        let bindings = crate::dispatch::global_bindings();
14887        let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14888        let err = match compile_one(
14889            &g,
14890            fused_id,
14891            &mut layout_cache,
14892            &bindings,
14893            &SymEnv::default(),
14894        ) {
14895            Ok(_) => panic!("dtype mismatch must be a typed error"),
14896            Err(e) => e,
14897        };
14898        // Post-fold, the dtype tuple IS the binding key: a mismatch is the
14899        // same honest `NoBackendForOp` miss as an absent kernel — reported
14900        // with the `RuntimeFused` diagnostic op tag, never a wrong-kernel bind.
14901        assert!(
14902            matches!(
14903                err,
14904                fuel_ir::Error::NoBackendForOp {
14905                    op: fuel_ir::dispatch::OpKind::RuntimeFused,
14906                    ..
14907                }
14908            ),
14909            "a dtype mismatch is an honest binding miss: {err}",
14910        );
14911    }
14912
14913    /// A runtime fused node carrying live `Runtime { scalars }` compiles with
14914    /// `OpParams::JitScalars` — the launch channel for the kernel's trailing
14915    /// `p{i}` args. (A param-free node keeps `OpParams::None`, covered by
14916    /// `compile_one_resolves_runtime_fused_from_the_sidecar`.)
14917    #[test]
14918    fn compile_one_maps_runtime_scalars_to_jit_scalars() {
14919        fn noop_kernel(
14920            _inputs: &[Arc<RwLock<fuel_memory::Storage>>],
14921            _outputs: &mut [Arc<RwLock<fuel_memory::Storage>>],
14922            _layouts: &[Layout],
14923            _params: &crate::kernel::OpParams,
14924        ) -> Result<()> {
14925            Ok(())
14926        }
14927        // Unique slot-template region (process-global sidecar).
14928        let region = fuel_graph::jit::PatternNode::Op {
14929            op: fuel_graph::jit::OpTag::Floor,
14930            attrs: fuel_graph::jit::OpAttrs::default(),
14931            operands: vec![fuel_graph::jit::PatternNode::Op {
14932                op: fuel_graph::jit::OpTag::MulScalar,
14933                attrs: fuel_graph::jit::OpAttrs::default(), // open slot
14934                operands: vec![fuel_graph::jit::PatternNode::Bind { index: 0 }],
14935            }],
14936        };
14937        let rid = crate::runtime_fused_kernels::adopt_runtime_fused(
14938            "test::compile_one::floor_mul_scalar",
14939            region,
14940            noop_kernel as crate::kernel::KernelRef,
14941            vec![DType::F32, DType::F32],
14942            BackendId::Cpu,
14943        )
14944        .expect("slot template registrable");
14945
14946        let graph_rc = Arc::new(RwLock::new(Graph::new()));
14947        let fused_id = {
14948            let mut g = graph_rc.write().unwrap();
14949            let s = Shape::from_dims(&[4]);
14950            let x = g.push(Node {
14951                op: Op::Const,
14952                inputs: vec![],
14953                shape: s.clone(),
14954                dtype: DType::F32,
14955            });
14956            let fused = g.push(Node {
14957                op: Op::Fused(
14958                    rid,
14959                    fuel_graph::registry::FusedOpParams::Runtime {
14960                        scalars: vec![3.5, -1.25],
14961                    },
14962                ),
14963                inputs: vec![x],
14964                shape: s,
14965                dtype: DType::F32,
14966            });
14967            g.set_target_backend(fused, BackendId::Cpu);
14968            fused
14969        };
14970        let g = graph_rc.read().unwrap();
14971        let bindings = crate::dispatch::global_bindings();
14972        let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14973        let item = compile_one(
14974            &g,
14975            fused_id,
14976            &mut layout_cache,
14977            &bindings,
14978            &SymEnv::default(),
14979        )
14980        .expect("compile_one runtime fused op with scalars");
14981        let compiled = item.compiled.expect("resolved a kernel");
14982        assert!(
14983            matches!(
14984                &compiled.op_params,
14985                crate::kernel::OpParams::JitScalars { scalars } if scalars == &vec![3.5, -1.25]
14986            ),
14987            "Runtime scalars ride into OpParams::JitScalars, got {:?}",
14988            compiled.op_params,
14989        );
14990    }
14991
14992    /// Move-to-same-device is the degenerate case: per the legacy
14993    /// `GraphExecutor` contract (`Op::Copy | Op::Move` shared arm →
14994    /// `backend.copy_to`) it is a plain copy — fresh storage on the
14995    /// same device, data intact — and the source is still evicted
14996    /// afterward (destructive semantics don't depend on the devices
14997    /// differing).
14998    #[test]
14999    fn pipelined_op_move_same_device_is_plain_copy() {
15000        let src = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
15001        let graph = Arc::new(RwLock::new(Graph::new()));
15002        let (src_id, move_id) = {
15003            let mut g = graph.write().unwrap();
15004            let src = g.push(Node {
15005                op: Op::Const,
15006                inputs: vec![],
15007                shape: Shape::from_dims(&[4]),
15008                dtype: DType::F32,
15009            });
15010            let mv = g.push(Node {
15011                op: Op::Move {
15012                    target: DeviceLocation::Cpu,
15013                },
15014                inputs: vec![src],
15015                shape: Shape::from_dims(&[4]),
15016                dtype: DType::F32,
15017            });
15018            g.set_target_backend(mv, BackendId::Cpu);
15019            (src, mv)
15020        };
15021        let src_arc = Arc::new(RwLock::new(src));
15022        let src_arc_external = Arc::clone(&src_arc);
15023        let mut inputs = StorageCache::new();
15024        inputs.insert(src_id, src_arc);
15025
15026        let (out, layout) =
15027            PipelinedExecutor::realize(graph, move_id, inputs).expect("realize Op::Move");
15028        let guard = out.read().unwrap();
15029        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15030            panic!()
15031        };
15032        assert_eq!(
15033            c.as_slice::<f32>().unwrap(),
15034            &[1.0, 2.0, 3.0, 4.0],
15035            "Op::Move output should carry input's data to target",
15036        );
15037        assert!(layout.is_contiguous());
15038        drop(guard);
15039        assert!(
15040            !Arc::ptr_eq(&out, &src_arc_external),
15041            "same-device Move is a plain copy — fresh storage, not an alias",
15042        );
15043        // The realize loop's destructive_input cleanup dropped the
15044        // cache's Arc to the source; only the test's external clone
15045        // survives.
15046        assert_eq!(
15047            Arc::strong_count(&src_arc_external),
15048            1,
15049            "Op::Move should evict the cache's Arc to its source",
15050        );
15051    }
15052
15053    /// The multiple-consumer-source case: a Move must NOT strand
15054    /// another consumer of its source. `execution_plan` (via
15055    /// `derive_ordering`, keyed off `destructive_input`) pins the
15056    /// Move AFTER the sibling reader; the executor's post-Move cache
15057    /// eviction then can't break the reader. Mirrors the legacy
15058    /// `op_move_pinned_after_sibling_reader_via_derive_ordering`
15059    /// test in fuel-graph-router/tests/cross_device.rs.
15060    ///
15061    ///   a   = const [1, 2, 3, 4]
15062    ///   b   = relu(a)        (non-destructive sibling reader)
15063    ///   m   = move(a, Cpu)   (destructive — evicts a once run)
15064    ///   out = add(b, m)      → [2, 4, 6, 8]
15065    #[test]
15066    fn pipelined_op_move_multi_consumer_source_not_stranded() {
15067        let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
15068        let graph = Arc::new(RwLock::new(Graph::new()));
15069        let (a_id, out_id) = {
15070            let mut g = graph.write().unwrap();
15071            let a = g.push(Node {
15072                op: Op::Const,
15073                inputs: vec![],
15074                shape: Shape::from_dims(&[4]),
15075                dtype: DType::F32,
15076            });
15077            let b = g.push(Node {
15078                op: Op::Relu,
15079                inputs: vec![a],
15080                shape: Shape::from_dims(&[4]),
15081                dtype: DType::F32,
15082            });
15083            let m = g.push(Node {
15084                op: Op::Move {
15085                    target: DeviceLocation::Cpu,
15086                },
15087                inputs: vec![a],
15088                shape: Shape::from_dims(&[4]),
15089                dtype: DType::F32,
15090            });
15091            let out = g.push(Node {
15092                op: Op::Add,
15093                inputs: vec![b, m],
15094                shape: Shape::from_dims(&[4]),
15095                dtype: DType::F32,
15096            });
15097            g.set_target_backend(b, BackendId::Cpu);
15098            g.set_target_backend(m, BackendId::Cpu);
15099            g.set_target_backend(out, BackendId::Cpu);
15100            (a, out)
15101        };
15102        let mut inputs = StorageCache::new();
15103        inputs.insert(a_id, Arc::new(RwLock::new(a)));
15104
15105        let (out, _) = PipelinedExecutor::realize(graph, out_id, inputs)
15106            .expect("realize add(relu(a), move(a))");
15107        let guard = out.read().unwrap();
15108        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15109            panic!()
15110        };
15111        assert_eq!(
15112            c.as_slice::<f32>().unwrap(),
15113            &[2.0, 4.0, 6.0, 8.0],
15114            "relu(a) must run before move(a) evicts a",
15115        );
15116    }
15117
15118    /// A Move whose source is itself in the realize target set must
15119    /// not evict it — the caller asked for the source's storage. The
15120    /// destructive cleanup's `target_set` gate covers Move exactly
15121    /// like Release.
15122    #[test]
15123    fn pipelined_op_move_source_in_target_set_not_evicted() {
15124        let a = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0]);
15125        let graph = Arc::new(RwLock::new(Graph::new()));
15126        let (a_id, move_id) = {
15127            let mut g = graph.write().unwrap();
15128            let a = g.push(Node {
15129                op: Op::Const,
15130                inputs: vec![],
15131                shape: Shape::from_dims(&[2]),
15132                dtype: DType::F32,
15133            });
15134            let mv = g.push(Node {
15135                op: Op::Move {
15136                    target: DeviceLocation::Cpu,
15137                },
15138                inputs: vec![a],
15139                shape: Shape::from_dims(&[2]),
15140                dtype: DType::F32,
15141            });
15142            g.set_target_backend(mv, BackendId::Cpu);
15143            (a, mv)
15144        };
15145        let a_arc = Arc::new(RwLock::new(a));
15146        let a_arc_external = Arc::clone(&a_arc);
15147        let mut inputs = StorageCache::new();
15148        inputs.insert(a_id, a_arc);
15149
15150        let out = PipelinedExecutor::realize_many(graph, &[move_id, a_id], inputs)
15151            .expect("realize_many [move, source]");
15152        assert_eq!(out.len(), 2);
15153        let mv_guard = out[0].0.read().unwrap();
15154        let fuel_memory::BackendStorage::Cpu(c) = &mv_guard.inner else {
15155            panic!()
15156        };
15157        assert_eq!(c.as_slice::<f32>().unwrap(), &[5.0, 6.0]);
15158        assert!(
15159            Arc::ptr_eq(&out[1].0, &a_arc_external),
15160            "requested source must survive the Move's destructive cleanup",
15161        );
15162    }
15163
15164    // --- realize_many (Phase 7.6 step 9c Phase A) ----------------
15165
15166    /// realize_many on empty targets returns an empty Vec — no graph
15167    /// walk, no panic.
15168    #[test]
15169    fn pipelined_realize_many_empty_targets() {
15170        let graph = Arc::new(RwLock::new(Graph::new()));
15171        let inputs = StorageCache::new();
15172        let out = PipelinedExecutor::realize_many(graph, &[], inputs).expect("realize_many");
15173        assert!(out.is_empty());
15174    }
15175
15176    /// realize_many with two independent target chains. Each chain's
15177    /// output is in the cache; the shared topo walk realizes both.
15178    /// Verifies parallel chain handling + return order matches input.
15179    #[test]
15180    fn pipelined_realize_many_two_independent_targets() {
15181        let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
15182        let rhs = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
15183        let graph = Arc::new(RwLock::new(Graph::new()));
15184        let (lhs_id, rhs_id, add_id, mul_id) = {
15185            let mut g = graph.write().unwrap();
15186            let lhs = g.push(Node {
15187                op: Op::Const,
15188                inputs: vec![],
15189                shape: Shape::from_dims(&[3]),
15190                dtype: DType::F32,
15191            });
15192            let rhs = g.push(Node {
15193                op: Op::Const,
15194                inputs: vec![],
15195                shape: Shape::from_dims(&[3]),
15196                dtype: DType::F32,
15197            });
15198            let add = g.push(Node {
15199                op: Op::Add,
15200                inputs: vec![lhs, rhs],
15201                shape: Shape::from_dims(&[3]),
15202                dtype: DType::F32,
15203            });
15204            let mul = g.push(Node {
15205                op: Op::Mul,
15206                inputs: vec![lhs, rhs],
15207                shape: Shape::from_dims(&[3]),
15208                dtype: DType::F32,
15209            });
15210            g.set_target_backend(add, BackendId::Cpu);
15211            g.set_target_backend(mul, BackendId::Cpu);
15212            (lhs, rhs, add, mul)
15213        };
15214        let mut inputs = StorageCache::new();
15215        inputs.insert(lhs_id, Arc::new(RwLock::new(lhs)));
15216        inputs.insert(rhs_id, Arc::new(RwLock::new(rhs)));
15217
15218        // Pass mul first to confirm return order matches targets order
15219        // (not graph order — graph order would put add first).
15220        let out = PipelinedExecutor::realize_many(graph, &[mul_id, add_id], inputs)
15221            .expect("realize_many");
15222        assert_eq!(out.len(), 2);
15223
15224        let mul_guard = out[0].0.read().unwrap();
15225        let fuel_memory::BackendStorage::Cpu(c) = &mul_guard.inner else {
15226            panic!()
15227        };
15228        assert_eq!(c.as_slice::<f32>().unwrap(), &[10.0, 40.0, 90.0]);
15229
15230        let add_guard = out[1].0.read().unwrap();
15231        let fuel_memory::BackendStorage::Cpu(c) = &add_guard.inner else {
15232            panic!()
15233        };
15234        assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 22.0, 33.0]);
15235    }
15236
15237    /// realize_many with the same NodeId twice — caller asking for
15238    /// the same output twice. Both outputs are the same Arc (cheap).
15239    #[test]
15240    fn pipelined_realize_many_duplicate_target_returns_same_arc() {
15241        let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
15242        let graph = Arc::new(RwLock::new(Graph::new()));
15243        let (in_id, neg_id) = {
15244            let mut g = graph.write().unwrap();
15245            let in_id = g.push(Node {
15246                op: Op::Const,
15247                inputs: vec![],
15248                shape: Shape::from_dims(&[3]),
15249                dtype: DType::F32,
15250            });
15251            let neg = g.push(Node {
15252                op: Op::Neg,
15253                inputs: vec![in_id],
15254                shape: Shape::from_dims(&[3]),
15255                dtype: DType::F32,
15256            });
15257            g.set_target_backend(neg, BackendId::Cpu);
15258            (in_id, neg)
15259        };
15260        let mut inputs = StorageCache::new();
15261        inputs.insert(in_id, Arc::new(RwLock::new(storage)));
15262
15263        let out = PipelinedExecutor::realize_many(graph, &[neg_id, neg_id], inputs)
15264            .expect("realize_many");
15265        assert_eq!(out.len(), 2);
15266        assert!(
15267            Arc::ptr_eq(&out[0].0, &out[1].0),
15268            "duplicate target should share the same storage Arc",
15269        );
15270    }
15271
15272    /// realize_many with shared upstream nodes evaluates the shared
15273    /// chunk exactly once. Two targets `f = a + b` and `g = (a + b) * a`
15274    /// — `a + b` is computed once.
15275    #[test]
15276    fn pipelined_realize_many_shared_subgraph_evaluates_once() {
15277        let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
15278        let b_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
15279        let graph = Arc::new(RwLock::new(Graph::new()));
15280        let (a_id, b_id, add_id, mul_id) = {
15281            let mut g = graph.write().unwrap();
15282            let a = g.push(Node {
15283                op: Op::Const,
15284                inputs: vec![],
15285                shape: Shape::from_dims(&[3]),
15286                dtype: DType::F32,
15287            });
15288            let b = g.push(Node {
15289                op: Op::Const,
15290                inputs: vec![],
15291                shape: Shape::from_dims(&[3]),
15292                dtype: DType::F32,
15293            });
15294            let add = g.push(Node {
15295                op: Op::Add,
15296                inputs: vec![a, b],
15297                shape: Shape::from_dims(&[3]),
15298                dtype: DType::F32,
15299            });
15300            let mul = g.push(Node {
15301                op: Op::Mul,
15302                inputs: vec![add, a],
15303                shape: Shape::from_dims(&[3]),
15304                dtype: DType::F32,
15305            });
15306            g.set_target_backend(add, BackendId::Cpu);
15307            g.set_target_backend(mul, BackendId::Cpu);
15308            (a, b, add, mul)
15309        };
15310        let mut inputs = StorageCache::new();
15311        inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
15312        inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
15313
15314        let out = PipelinedExecutor::realize_many(graph, &[add_id, mul_id], inputs)
15315            .expect("realize_many");
15316        assert_eq!(out.len(), 2);
15317
15318        let add_guard = out[0].0.read().unwrap();
15319        let fuel_memory::BackendStorage::Cpu(c) = &add_guard.inner else {
15320            panic!()
15321        };
15322        assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 22.0, 33.0]);
15323
15324        let mul_guard = out[1].0.read().unwrap();
15325        let fuel_memory::BackendStorage::Cpu(c) = &mul_guard.inner else {
15326            panic!()
15327        };
15328        // (a + b) * a = [11*1, 22*2, 33*3] = [11, 44, 99].
15329        assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 44.0, 99.0]);
15330    }
15331
15332    /// E2E: masked_fill with -inf — the attention masking pattern.
15333    /// x = [1, 2, 3, 4]; mask = [0, 1, 0, 1]; value = -1000.
15334    /// out = [1, -1000, 3, -1000].
15335    #[test]
15336    fn pipelined_realize_masked_fill_attention_pattern() {
15337        let x_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
15338        let mut mask_storage = fuel_memory::from_slice_cpu(&[0u8, 1, 0, 1]);
15339        mask_storage.dtype = DType::Bool; // GAP-168(c): a Bool mask, not U8 bytes
15340        let graph = Arc::new(RwLock::new(Graph::new()));
15341        let (x_id, mask_id, out_id) = {
15342            let mut g = graph.write().unwrap();
15343            let x = g.push(Node {
15344                op: Op::Const,
15345                inputs: vec![],
15346                shape: Shape::from_dims(&[4]),
15347                dtype: DType::F32,
15348            });
15349            let mask = g.push(Node {
15350                op: Op::Const,
15351                inputs: vec![],
15352                shape: Shape::from_dims(&[4]),
15353                dtype: DType::Bool,
15354            });
15355            let out = g.push(Node {
15356                op: Op::MaskedFill {
15357                    value: fuel_ir::Scalar::F32(-1000.0),
15358                },
15359                inputs: vec![x, mask],
15360                shape: Shape::from_dims(&[4]),
15361                dtype: DType::F32,
15362            });
15363            g.set_target_backend(out, BackendId::Cpu);
15364            (x, mask, out)
15365        };
15366        let mut inputs = StorageCache::new();
15367        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
15368        inputs.insert(mask_id, Arc::new(RwLock::new(mask_storage)));
15369        let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
15370        let guard = result_arc.read().unwrap();
15371        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15372            panic!("expected Cpu storage");
15373        };
15374        let out: &[f32] = c.as_slice().unwrap();
15375        assert_eq!(out, &[1.0, -1000.0, 3.0, -1000.0]);
15376    }
15377
15378    // ---- WriteSlice (Phase 7.6 step 9c E.3.2) -------------------------------
15379
15380    /// E2E: pipelined realize of `Op::WriteSlice` writing a [1, 3, 2]
15381    /// source slab into a [4, 3, 2] destination at axis-0 row 2 — the
15382    /// canonical KV-cache append shape.
15383    #[test]
15384    fn pipelined_realize_write_slice_kv_cache_append() {
15385        let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 24]);
15386        let src_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
15387        let graph = Arc::new(RwLock::new(Graph::new()));
15388        let (dest_id, src_id, ws_id) = {
15389            let mut g = graph.write().unwrap();
15390            let dest = g.push(Node {
15391                op: Op::Const,
15392                inputs: vec![],
15393                shape: Shape::from_dims(&[4, 3, 2]),
15394                dtype: DType::F32,
15395            });
15396            let src = g.push(Node {
15397                op: Op::Const,
15398                inputs: vec![],
15399                shape: Shape::from_dims(&[1, 3, 2]),
15400                dtype: DType::F32,
15401            });
15402            let ws = g.push(Node {
15403                op: Op::WriteSlice {
15404                    ranges: vec![(2, 3), (0, 3), (0, 2)],
15405                    dyn_offset: None,
15406                },
15407                inputs: vec![dest, src],
15408                shape: Shape::from_dims(&[4, 3, 2]), // adopts dest shape
15409                dtype: DType::F32,
15410            });
15411            g.set_target_backend(ws, BackendId::Cpu);
15412            (dest, src, ws)
15413        };
15414        let mut inputs = StorageCache::new();
15415        inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15416        inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15417        let (result_arc, _) = PipelinedExecutor::realize(graph, ws_id, inputs).expect("realize");
15418        let guard = result_arc.read().unwrap();
15419        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15420            panic!("expected Cpu storage");
15421        };
15422        let out: &[f32] = c.as_slice().unwrap();
15423        let expected = [
15424            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0,
15425            6.0, // row 2 = source
15426            0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
15427        ];
15428        assert_eq!(out, &expected);
15429    }
15430
15431    /// E2E: pipelined realize of `Op::WriteSlice` adopts the
15432    /// destination's Storage Arc as the output slot (in-place
15433    /// alias) — the resulting Arc points to the SAME bytes the
15434    /// destination was constructed from.
15435    #[test]
15436    fn pipelined_realize_write_slice_aliases_dest_storage() {
15437        let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15438        let src_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
15439        let graph = Arc::new(RwLock::new(Graph::new()));
15440        let (dest_id, src_id, ws_id, dest_arc_external) = {
15441            let mut g = graph.write().unwrap();
15442            let dest = g.push(Node {
15443                op: Op::Const,
15444                inputs: vec![],
15445                shape: Shape::from_dims(&[3, 2]),
15446                dtype: DType::F32,
15447            });
15448            let src = g.push(Node {
15449                op: Op::Const,
15450                inputs: vec![],
15451                shape: Shape::from_dims(&[1, 2]),
15452                dtype: DType::F32,
15453            });
15454            let ws = g.push(Node {
15455                op: Op::WriteSlice {
15456                    ranges: vec![(1, 2), (0, 2)],
15457                    dyn_offset: None,
15458                },
15459                inputs: vec![dest, src],
15460                shape: Shape::from_dims(&[3, 2]),
15461                dtype: DType::F32,
15462            });
15463            g.set_target_backend(ws, BackendId::Cpu);
15464            let arc = Arc::new(RwLock::new(dest_storage));
15465            (dest, src, ws, arc)
15466        };
15467        let mut inputs = StorageCache::new();
15468        inputs.insert(dest_id, Arc::clone(&dest_arc_external));
15469        inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15470        let (result_arc, _) = PipelinedExecutor::realize(graph, ws_id, inputs).expect("realize");
15471        // Bytes were written into the same Arc the caller held —
15472        // the WriteSlice op aliases dest's Storage Arc, not allocates
15473        // a fresh buffer.
15474        assert!(
15475            Arc::ptr_eq(&result_arc, &dest_arc_external),
15476            "WriteSlice output Arc must be the same Arc as the destination's"
15477        );
15478        let guard = dest_arc_external.read().unwrap();
15479        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15480            panic!("expected Cpu storage");
15481        };
15482        let out: &[f32] = c.as_slice().unwrap();
15483        assert_eq!(out, &[0.0, 0.0, 10.0, 20.0, 0.0, 0.0]);
15484    }
15485
15486    // ---- WriteSlice dynamic offset (Phase D symbolic extents) ---------------
15487
15488    /// E2E: pipelined realize of `Op::WriteSlice` with a runtime start
15489    /// offset (`dyn_offset`) resolved through a per-pass `SymEnv`. The
15490    /// width-2 source slab lands at the bound offset (3), not the static
15491    /// `ranges[0].0` placeholder (0) — the persistent-decode KV-cache
15492    /// write at a per-token `cached_len`.
15493    #[test]
15494    fn pipelined_realize_write_slice_dynamic_offset() {
15495        use fuel_ir::{DynScalar, SymId};
15496        let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15497        let src_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0]);
15498        let graph = Arc::new(RwLock::new(Graph::new()));
15499        let sym = SymId(0);
15500        let (dest_id, src_id, ws_id) = {
15501            let mut g = graph.write().unwrap();
15502            let dest = g.push(Node {
15503                op: Op::Const,
15504                inputs: vec![],
15505                shape: Shape::from_dims(&[6]),
15506                dtype: DType::F32,
15507            });
15508            let src = g.push(Node {
15509                op: Op::Const,
15510                inputs: vec![],
15511                shape: Shape::from_dims(&[2]),
15512                dtype: DType::F32,
15513            });
15514            let ws = g.push(Node {
15515                // ranges[0] = (0, 2): the start is a placeholder (ignored —
15516                // overridden by dyn_offset); only the width (2) is read.
15517                op: Op::WriteSlice {
15518                    ranges: vec![(0, 2)],
15519                    dyn_offset: Some((0, DynScalar::Sym(sym))),
15520                },
15521                inputs: vec![dest, src],
15522                shape: Shape::from_dims(&[6]),
15523                dtype: DType::F32,
15524            });
15525            g.set_target_backend(ws, BackendId::Cpu);
15526            (dest, src, ws)
15527        };
15528        let mut inputs = StorageCache::new();
15529        inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15530        inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15531
15532        // Bind cached_len = 3: the slab must land at indices [3, 4].
15533        let mut env = SymEnv::new();
15534        env.bind(sym, 3).unwrap();
15535
15536        let (result_arc, _) = PipelinedExecutor::realize_with_env(graph, ws_id, inputs, env)
15537            .expect("realize_with_env");
15538        let guard = result_arc.read().unwrap();
15539        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15540            panic!("expected Cpu storage");
15541        };
15542        let out: &[f32] = c.as_slice().unwrap();
15543        assert_eq!(
15544            out,
15545            &[0.0, 0.0, 0.0, 7.0, 8.0, 0.0],
15546            "slab must land at the SymEnv-bound offset 3, not the static placeholder 0",
15547        );
15548    }
15549
15550    /// Increment 2a — the data-determined bind seam. After
15551    /// `Op::NonZeroIndices` realizes, the executor reads its runtime count
15552    /// from bundle slot 1 and PUBLISHES it into the produced `SymEnv` under
15553    /// `count_sym`. This is the first op to bind a *data-determined* symbol
15554    /// mid-realize (all prior binds are host-known before realize) — the
15555    /// keystone capability for SSM decode / MoE sparsity / MLA KV.
15556    #[test]
15557    fn pipelined_realize_nonzero_indices_publishes_count() {
15558        use fuel_ir::SymId;
15559        use fuel_ir::storage::{OutputViewSpec, compose_bundle};
15560        // flat [0,1,0,1,1,0] → 3 nonzeros at 1,3,4; capacity 6.
15561        let x_storage = fuel_memory::from_slice_cpu(&[0.0_f32, 1.0, 0.0, 1.0, 1.0, 0.0]);
15562        let graph = Arc::new(RwLock::new(Graph::new()));
15563        let count_sym = SymId(0);
15564        let (x_id, nzi_id) = {
15565            let mut g = graph.write().unwrap();
15566            let x = g.push(Node {
15567                op: Op::Const,
15568                inputs: vec![],
15569                shape: Shape::from_dims(&[2, 3]),
15570                dtype: DType::F32,
15571            });
15572            let capacity = 6usize;
15573            let idx_shape = Shape::from_dims(&[capacity]);
15574            let cnt_shape = Shape::from_dims(&[1]);
15575            let specs = vec![
15576                OutputViewSpec {
15577                    dtype: DType::U32,
15578                    shape: idx_shape.clone(),
15579                    layout: Layout::contiguous(idx_shape.clone()),
15580                    name: Some("indices"),
15581                },
15582                OutputViewSpec {
15583                    dtype: DType::U32,
15584                    shape: cnt_shape.clone(),
15585                    layout: Layout::contiguous(cnt_shape),
15586                    name: Some("count"),
15587                },
15588            ];
15589            let (_bytes, views) = compose_bundle(&specs).unwrap();
15590            let nzi = g.push(Node {
15591                op: Op::NonZeroIndices { count_sym },
15592                inputs: vec![x],
15593                shape: idx_shape,
15594                dtype: DType::U32,
15595            });
15596            g.set_output_views(nzi, Arc::from(views.into_boxed_slice()))
15597                .unwrap();
15598            g.set_target_backend(nzi, BackendId::Cpu);
15599            (x, nzi)
15600        };
15601        let mut inputs = StorageCache::new();
15602        inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
15603
15604        let (_arc, _layout, produced) =
15605            PipelinedExecutor::realize_with_env_producing(graph, nzi_id, inputs, SymEnv::new())
15606                .expect("realize_with_env_producing");
15607
15608        assert_eq!(
15609            produced.get(count_sym),
15610            Some(3),
15611            "the executor must publish the data-determined nonzero count into the SymEnv",
15612        );
15613    }
15614
15615    /// A `dyn_offset` whose symbol is unbound in the `SymEnv` surfaces a
15616    /// typed error at realize (never a panic).
15617    #[test]
15618    fn pipelined_realize_write_slice_dynamic_offset_unbound_errors() {
15619        use fuel_ir::{DynScalar, SymId};
15620        let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15621        let src_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0]);
15622        let graph = Arc::new(RwLock::new(Graph::new()));
15623        let (dest_id, src_id, ws_id) = {
15624            let mut g = graph.write().unwrap();
15625            let dest = g.push(Node {
15626                op: Op::Const,
15627                inputs: vec![],
15628                shape: Shape::from_dims(&[6]),
15629                dtype: DType::F32,
15630            });
15631            let src = g.push(Node {
15632                op: Op::Const,
15633                inputs: vec![],
15634                shape: Shape::from_dims(&[2]),
15635                dtype: DType::F32,
15636            });
15637            let ws = g.push(Node {
15638                op: Op::WriteSlice {
15639                    ranges: vec![(0, 2)],
15640                    dyn_offset: Some((0, DynScalar::Sym(SymId(0)))),
15641                },
15642                inputs: vec![dest, src],
15643                shape: Shape::from_dims(&[6]),
15644                dtype: DType::F32,
15645            });
15646            g.set_target_backend(ws, BackendId::Cpu);
15647            (dest, src, ws)
15648        };
15649        let mut inputs = StorageCache::new();
15650        inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15651        inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15652        // Empty env — the symbol is unbound.
15653        let result = PipelinedExecutor::realize_with_env(graph, ws_id, inputs, SymEnv::new());
15654        assert!(
15655            result.is_err(),
15656            "unbound dyn_offset symbol must surface a typed error, not a panic",
15657        );
15658    }
15659
15660    /// A runtime offset whose resolved slab runs past the destination
15661    /// capacity errors at realize (never an out-of-bounds write).
15662    #[test]
15663    fn pipelined_realize_write_slice_dynamic_offset_out_of_capacity_errors() {
15664        use fuel_ir::{DynScalar, SymId};
15665        let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15666        let src_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0]);
15667        let graph = Arc::new(RwLock::new(Graph::new()));
15668        let sym = SymId(0);
15669        let (dest_id, src_id, ws_id) = {
15670            let mut g = graph.write().unwrap();
15671            let dest = g.push(Node {
15672                op: Op::Const,
15673                inputs: vec![],
15674                shape: Shape::from_dims(&[6]),
15675                dtype: DType::F32,
15676            });
15677            let src = g.push(Node {
15678                op: Op::Const,
15679                inputs: vec![],
15680                shape: Shape::from_dims(&[2]),
15681                dtype: DType::F32,
15682            });
15683            let ws = g.push(Node {
15684                op: Op::WriteSlice {
15685                    ranges: vec![(0, 2)],
15686                    dyn_offset: Some((0, DynScalar::Sym(sym))),
15687                },
15688                inputs: vec![dest, src],
15689                shape: Shape::from_dims(&[6]),
15690                dtype: DType::F32,
15691            });
15692            g.set_target_backend(ws, BackendId::Cpu);
15693            (dest, src, ws)
15694        };
15695        let mut inputs = StorageCache::new();
15696        inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15697        inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15698        // offset 5 + width 2 = 7 > capacity 6.
15699        let mut env = SymEnv::new();
15700        env.bind(sym, 5).unwrap();
15701        let result = PipelinedExecutor::realize_with_env(graph, ws_id, inputs, env);
15702        assert!(
15703            result.is_err(),
15704            "resolved slab past destination capacity must surface a typed error",
15705        );
15706    }
15707
15708    /// Regression for the PipelinedExecutor ordering-integration session.
15709    ///
15710    /// Graph: `x = Const`, `step_x = Step(x)`, `y = SigmoidInplace(x)`.
15711    /// `Step` and `SigmoidInplace` are both readers of `x`; `Step` is
15712    /// non-destructive, `SigmoidInplace` mutates `x`'s storage. The
15713    /// correct semantic is for `Step` to read PRE-mutation bytes.
15714    ///
15715    /// Pre-this-session (raw `topo_order_multi`): the two are siblings
15716    /// in topo order and the tie-break is unspecified — `SigmoidInplace`
15717    /// could run first, making `step_x` see post-sigmoid bytes
15718    /// (`[1, 1, 1, 1]` because every sigmoid output is positive).
15719    ///
15720    /// Post-this-session: `execution_plan` consults `derive_ordering`,
15721    /// which pins `SigmoidInplace` AFTER every other reader of `x`.
15722    /// `step_x` deterministically sees `x` and is `[1, 0, 1, 0]`.
15723    #[test]
15724    fn pipelined_inplace_with_multiple_readers_orders_correctly() {
15725        use fuel_ir::Shape;
15726        let data = [1.0_f32, -2.0, 3.0, -4.0];
15727        let src_storage = fuel_memory::from_slice_cpu(&data);
15728
15729        let graph = Arc::new(RwLock::new(Graph::new()));
15730        let (x_id, step_id, sig_id) = {
15731            let mut g = graph.write().unwrap();
15732            let x = g.push(Node {
15733                op: Op::Const,
15734                inputs: vec![],
15735                shape: Shape::from_dims(&[4]),
15736                dtype: DType::F32,
15737            });
15738            let step = g.push(Node {
15739                op: Op::Step,
15740                inputs: vec![x],
15741                shape: Shape::from_dims(&[4]),
15742                dtype: DType::F32,
15743            });
15744            let sig = g.push(Node {
15745                op: Op::SigmoidInplace,
15746                inputs: vec![x],
15747                shape: Shape::from_dims(&[4]),
15748                dtype: DType::F32,
15749            });
15750            g.set_target_backend(step, BackendId::Cpu);
15751            g.set_target_backend(sig, BackendId::Cpu);
15752            (x, step, sig)
15753        };
15754
15755        let mut cache = StorageCache::new();
15756        cache.insert(x_id, Arc::new(RwLock::new(src_storage)));
15757
15758        let results = PipelinedExecutor::realize_many(graph, &[step_id, sig_id], cache)
15759            .expect("realize_many");
15760
15761        // step_x must reflect pre-mutation x: sign of [1, -2, 3, -4] →
15762        // [1, 0, 1, 0]. If the executor ran SigmoidInplace first, step
15763        // would see all-positive sigmoid outputs and produce [1, 1, 1, 1].
15764        let step_guard = results[0].0.read().unwrap();
15765        let fuel_memory::BackendStorage::Cpu(c) = &step_guard.inner else {
15766            panic!("expected Cpu storage for step output");
15767        };
15768        let step_out: &[f32] = c.as_slice().expect("f32 cast");
15769        assert_eq!(
15770            step_out,
15771            &[1.0_f32, 0.0, 1.0, 0.0],
15772            "Step must run before SigmoidInplace; got post-mutation bytes"
15773        );
15774        drop(step_guard);
15775
15776        // SigmoidInplace's output Arc IS the mutated cache entry — bytes
15777        // approximate sigmoid([1, -2, 3, -4]).
15778        let sig_guard = results[1].0.read().unwrap();
15779        let fuel_memory::BackendStorage::Cpu(c) = &sig_guard.inner else {
15780            panic!("expected Cpu storage for sigmoid output");
15781        };
15782        let sig_out: &[f32] = c.as_slice().expect("f32 cast");
15783        let sig_ref = [
15784            1.0_f32 / (1.0 + (-1.0_f32).exp()),
15785            1.0_f32 / (1.0 + (2.0_f32).exp()),
15786            1.0_f32 / (1.0 + (-3.0_f32).exp()),
15787            1.0_f32 / (1.0 + (4.0_f32).exp()),
15788        ];
15789        for (got, want) in sig_out.iter().zip(sig_ref.iter()) {
15790            assert!((got - want).abs() < 1e-6, "sigmoid: got {got}, want {want}");
15791        }
15792    }
15793
15794    /// Regression for the PipelinedExecutor ordering-integration session.
15795    ///
15796    /// Canonical residual-connection pattern:
15797    ///   `x = Const`, `y = ReluInplace(x)`, `z = Add(y, x)`.
15798    ///
15799    /// Pre-this-session: `derive_ordering` was never run, so the cycle
15800    /// (Add must precede ReluInplace per ordering edge; Add must follow
15801    /// ReluInplace per data-flow edge through y) was undetected. The
15802    /// executor produced `z = relu(x) + relu(x) = [2, 0, 6, 0]` —
15803    /// silently wrong.
15804    ///
15805    /// Post-this-session: `insert_safety_copies` runs before
15806    /// `execution_plan`, inserts `Op::Copy(x) → x_safe`, rewires Add's
15807    /// `x` input to `x_safe`. Result: `z = relu(x) + x = [2, -2, 6, -4]`.
15808    #[test]
15809    fn pipelined_residual_connection_inserts_safety_copy() {
15810        use fuel_ir::Shape;
15811        let data = [1.0_f32, -2.0, 3.0, -4.0];
15812        let src_storage = fuel_memory::from_slice_cpu(&data);
15813
15814        let graph = Arc::new(RwLock::new(Graph::new()));
15815        let (x_id, _y_id, z_id) = {
15816            let mut g = graph.write().unwrap();
15817            let x = g.push(Node {
15818                op: Op::Const,
15819                inputs: vec![],
15820                shape: Shape::from_dims(&[4]),
15821                dtype: DType::F32,
15822            });
15823            let y = g.push(Node {
15824                op: Op::ReluInplace,
15825                inputs: vec![x],
15826                shape: Shape::from_dims(&[4]),
15827                dtype: DType::F32,
15828            });
15829            let z = g.push(Node {
15830                op: Op::Add,
15831                inputs: vec![y, x],
15832                shape: Shape::from_dims(&[4]),
15833                dtype: DType::F32,
15834            });
15835            g.set_target_backend(y, BackendId::Cpu);
15836            g.set_target_backend(z, BackendId::Cpu);
15837            (x, y, z)
15838        };
15839
15840        let mut cache = StorageCache::new();
15841        cache.insert(x_id, Arc::new(RwLock::new(src_storage)));
15842
15843        let (result_arc, _) = PipelinedExecutor::realize(graph, z_id, cache)
15844            .expect("realize z — insert_safety_copies must break the cycle");
15845
15846        let guard = result_arc.read().unwrap();
15847        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15848            panic!("expected Cpu storage");
15849        };
15850        let got: &[f32] = c.as_slice().expect("f32 cast");
15851        assert_eq!(
15852            got,
15853            &[2.0_f32, -2.0, 6.0, -4.0],
15854            "Add must read pre-mutation x via the inserted safety copy"
15855        );
15856    }
15857
15858    // ------------------------------------------------------------------
15859    // Item 3 end-to-end: SelectiveScan through the bundled-output
15860    // PipelinedExecutor path. The producer allocates bundled
15861    // (y_bytes + last_state_bytes), the kernel writes both slots,
15862    // and the bundled-tuple builder returns each via Op::View.
15863    // ------------------------------------------------------------------
15864
15865    fn cpu_const_f32(
15866        graph: &Arc<RwLock<Graph>>,
15867        cache: &mut StorageCache,
15868        data: &[f32],
15869        dims: &[usize],
15870    ) -> NodeId {
15871        let id = {
15872            let mut g = graph.write().unwrap();
15873            g.push(Node {
15874                op: Op::Const,
15875                inputs: vec![],
15876                shape: Shape::from_dims(dims),
15877                dtype: DType::F32,
15878            })
15879        };
15880        cache.insert(id, Arc::new(RwLock::new(fuel_memory::from_slice_cpu(data))));
15881        id
15882    }
15883
15884    /// `selective_scan_bundled` realizes both `y` and `last_state` in
15885    /// a single `realize_many` pass; the Views project the same
15886    /// producer's bundled Storage Arc and the numerics match the
15887    /// hand-computed single-step kernel test (b=1,s=1,d=1,k=1).
15888    #[test]
15889    fn pipelined_realize_selective_scan_bundled_produces_y_and_last_state() {
15890        let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
15891        let mut cache = StorageCache::new();
15892
15893        let u_id = cpu_const_f32(&graph, &mut cache, &[3.0], &[1, 1, 1]);
15894        let delta_id = cpu_const_f32(&graph, &mut cache, &[1.0], &[1, 1, 1]);
15895        let a_id = cpu_const_f32(&graph, &mut cache, &[-1.0], &[1, 1]);
15896        let b_id = cpu_const_f32(&graph, &mut cache, &[2.0], &[1, 1, 1]);
15897        let c_id = cpu_const_f32(&graph, &mut cache, &[0.5], &[1, 1, 1]);
15898
15899        let (y_id, last_state_id) = {
15900            let u_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), u_id);
15901            let delta_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), delta_id);
15902            let a_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), a_id);
15903            let b_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), b_id);
15904            let c_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), c_id);
15905            let (y, last_state) = u_t
15906                .selective_scan_bundled(&delta_t, &a_t, &b_t, &c_t, /* delta_softplus */ false)
15907                .expect("selective_scan_bundled");
15908            (y.id(), last_state.id())
15909        };
15910        // Set target_backend on the producer (View nodes inherit it).
15911        {
15912            let mut g = graph.write().unwrap();
15913            let producer = g.node(y_id).inputs[0];
15914            g.set_target_backend(producer, BackendId::Cpu);
15915        }
15916
15917        let outputs =
15918            PipelinedExecutor::realize_many(Arc::clone(&graph), &[y_id, last_state_id], cache)
15919                .expect("realize_many");
15920        let (y_arc, y_layout) = outputs[0].clone();
15921        let (last_state_arc, last_state_layout) = outputs[1].clone();
15922
15923        // Both Views project the same bundled producer Storage Arc.
15924        assert!(
15925            Arc::ptr_eq(&y_arc, &last_state_arc),
15926            "y and last_state Views project the SAME bundled producer Storage",
15927        );
15928
15929        // Hand-computed numerics (single-step recurrence):
15930        // h[0,0,0] = exp(1.0 * -1.0) * 0 + 1.0 * 2.0 * 3.0 = 6.0
15931        // y[0,0,0] = h * c = 6.0 * 0.5 = 3.0
15932        // last_state[0,0,0] = h = 6.0
15933        let guard = y_arc.read().unwrap();
15934        let fuel_memory::BackendStorage::Cpu(c_bytes) = &guard.inner else {
15935            panic!("expected Cpu storage");
15936        };
15937        let typed: &[f32] = c_bytes.as_slice().expect("f32 cast");
15938        assert_eq!(y_layout.start_offset(), 0);
15939        let y_value = typed[y_layout.start_offset()];
15940        assert!((y_value - 3.0).abs() < 1e-5, "y expected 3.0 got {y_value}");
15941        assert_eq!(
15942            last_state_layout.start_offset(),
15943            1,
15944            "slot 1 byte_offset 4 / sizeof(f32) = 1",
15945        );
15946        let ls_value = typed[last_state_layout.start_offset()];
15947        assert!(
15948            (ls_value - 6.0).abs() < 1e-5,
15949            "last_state expected 6.0 got {ls_value}"
15950        );
15951    }
15952
15953    // ---- Ordering-defect repros (MLA decode multi-round WriteSlice) -----
15954
15955    /// DIAGNOSTIC (not a repro by itself): a plain single-hop view chain
15956    /// `S = Slice(X); K = Relu(S)` competing with a SECOND destructive
15957    /// WriteSlice on `X` (`Y = WriteSlice(X, ...)`) within one
15958    /// `realize_many` call. `collect_alias_set` adds `S` to `X`'s alias
15959    /// set (Slice IS `is_view_op`), so `derive_ordering` pins `Y` after
15960    /// `S`'s reader `K` directly. This form is expected to STAY GREEN —
15961    /// recorded here as the control that isolates the Reshape-specific
15962    /// gap exercised by the next test.
15963    #[test]
15964    fn pipelined_write_slice_round2_plain_slice_reader_not_stranded() {
15965        let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
15966        let mut cache = StorageCache::new();
15967
15968        let b_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
15969        let src1_id = cpu_const_f32(&graph, &mut cache, &[1.0, 2.0], &[2]);
15970        let src2_id = cpu_const_f32(&graph, &mut cache, &[99.0, 98.0], &[2]);
15971
15972        let (k_id, y_id) = {
15973            let mut g = graph.write().unwrap();
15974            let x_id = g.push(Node {
15975                op: Op::WriteSlice {
15976                    ranges: vec![(0, 2)],
15977                    dyn_offset: None,
15978                },
15979                inputs: vec![b_id, src1_id],
15980                shape: Shape::from_dims(&[4]),
15981                dtype: DType::F32,
15982            });
15983            let s_id = g.push(Node {
15984                op: Op::Slice {
15985                    dim: 0,
15986                    start: 0,
15987                    len: 2,
15988                },
15989                inputs: vec![x_id],
15990                shape: Shape::from_dims(&[2]),
15991                dtype: DType::F32,
15992            });
15993            let k_id = g.push(Node {
15994                op: Op::Relu,
15995                inputs: vec![s_id],
15996                shape: Shape::from_dims(&[2]),
15997                dtype: DType::F32,
15998            });
15999            // Round 2 OVERLAPS round 1's [0,2) range so a wrong-order
16000            // execution would be numerically visible in K's output.
16001            let y_id = g.push(Node {
16002                op: Op::WriteSlice {
16003                    ranges: vec![(0, 2)],
16004                    dyn_offset: None,
16005                },
16006                inputs: vec![x_id, src2_id],
16007                shape: Shape::from_dims(&[4]),
16008                dtype: DType::F32,
16009            });
16010            g.set_target_backend(x_id, BackendId::Cpu);
16011            g.set_target_backend(k_id, BackendId::Cpu);
16012            g.set_target_backend(y_id, BackendId::Cpu);
16013            (k_id, y_id)
16014        };
16015
16016        let plan = fuel_graph::opt::execution_plan(&graph.read().unwrap(), &[k_id, y_id]);
16017        eprintln!("[diagnostic plain-slice] execution_plan = {:?}", plan,);
16018
16019        let outputs = PipelinedExecutor::realize_many(Arc::clone(&graph), &[k_id, y_id], cache)
16020            .expect("realize_many: plain slice reader must not error");
16021        let (k_arc, _) = &outputs[0];
16022        let guard = k_arc.read().unwrap();
16023        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
16024            panic!("expected Cpu storage");
16025        };
16026        let out: &[f32] = c.as_slice().unwrap();
16027        assert_eq!(
16028            out,
16029            &[1.0, 2.0],
16030            "K = relu(slice(X,0,2)) must read PRE-round-2 bytes; got {:?}",
16031            out,
16032        );
16033    }
16034
16035    /// CRITICAL SUSPECT #2 repro: `Op::Reshape`/`Op::Contiguize` compile
16036    /// to `WorkItemKind::ContiguizeOf`, which zero-copy ADOPTS the
16037    /// input's Storage Arc when the input is contiguous at offset 0 —
16038    /// but `Op::Reshape` is NOT in `collect_alias_set`'s alias-extending
16039    /// predicate (only `Op::is_view_op` + `Op::View{..}`). So for
16040    /// `X = write_slice(B, src1); S = X.slice(0, 0, 2) [offset-0
16041    /// contiguous]; R = S.reshape([2]); K = relu(R); Y =
16042    /// write_slice(X, src2)`, `collect_alias_set(X)` stops at `{X, S}`
16043    /// — `R` never enters the alias set, so `derive_ordering` never
16044    /// scans `R`'s consumers (`K`) for pinning against `Y`. `K` and `Y`
16045    /// become simultaneously "ready" once `R` completes, and
16046    /// `execution_plan`'s stable tiebreak (smaller `topo_order_multi`
16047    /// position wins) can schedule `Y` (destructive) BEFORE `K` — `K`
16048    /// then dereferences `R`'s adopted Arc (== `X`'s Arc, mutated
16049    /// in-place by `Y`) and silently reads POST-`Y` bytes. No error;
16050    /// wrong numbers.
16051    #[test]
16052    fn pipelined_write_slice_reshape_adopted_arc_reader_reads_pre_write_bytes() {
16053        let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
16054        let mut cache = StorageCache::new();
16055
16056        let b_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
16057        let src1_id = cpu_const_f32(&graph, &mut cache, &[1.0, 2.0], &[2]);
16058        let src2_id = cpu_const_f32(&graph, &mut cache, &[99.0, 98.0], &[2]);
16059
16060        let (x_id, k_id, y_id) = {
16061            let mut g = graph.write().unwrap();
16062            let x_id = g.push(Node {
16063                op: Op::WriteSlice {
16064                    ranges: vec![(0, 2)],
16065                    dyn_offset: None,
16066                },
16067                inputs: vec![b_id, src1_id],
16068                shape: Shape::from_dims(&[4]),
16069                dtype: DType::F32,
16070            });
16071            let s_id = g.push(Node {
16072                // OFFSET-0 contiguous prefix slice — ContiguizeOf's
16073                // zero-copy fast path requires this at execute time.
16074                op: Op::Slice {
16075                    dim: 0,
16076                    start: 0,
16077                    len: 2,
16078                },
16079                inputs: vec![x_id],
16080                shape: Shape::from_dims(&[2]),
16081                dtype: DType::F32,
16082            });
16083            let r_id = g.push(Node {
16084                op: Op::Reshape(Shape::from_dims(&[2])),
16085                inputs: vec![s_id],
16086                shape: Shape::from_dims(&[2]),
16087                dtype: DType::F32,
16088            });
16089            let k_id = g.push(Node {
16090                op: Op::Relu,
16091                inputs: vec![r_id],
16092                shape: Shape::from_dims(&[2]),
16093                dtype: DType::F32,
16094            });
16095            // Round 2 OVERLAPS round 1's [0,2) range (not the [2,4)
16096            // tail) so a wrong-order execution is numerically visible
16097            // in K's output — a disjoint round 2 would corrupt bytes
16098            // K never reads, masking the defect.
16099            let y_id = g.push(Node {
16100                op: Op::WriteSlice {
16101                    ranges: vec![(0, 2)],
16102                    dyn_offset: None,
16103                },
16104                inputs: vec![x_id, src2_id],
16105                shape: Shape::from_dims(&[4]),
16106                dtype: DType::F32,
16107            });
16108            g.set_target_backend(x_id, BackendId::Cpu);
16109            g.set_target_backend(k_id, BackendId::Cpu);
16110            g.set_target_backend(y_id, BackendId::Cpu);
16111            (x_id, k_id, y_id)
16112        };
16113        let _ = x_id;
16114
16115        let plan = fuel_graph::opt::execution_plan(&graph.read().unwrap(), &[k_id, y_id]);
16116        eprintln!(
16117            "[diagnostic reshape-adopted-arc] execution_plan = {:?}",
16118            plan,
16119        );
16120
16121        let outputs = PipelinedExecutor::realize_many(Arc::clone(&graph), &[k_id, y_id], cache)
16122            .expect("realize_many");
16123        let (k_arc, _) = &outputs[0];
16124        let guard = k_arc.read().unwrap();
16125        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
16126            panic!("expected Cpu storage");
16127        };
16128        let out: &[f32] = c.as_slice().unwrap();
16129        // K = relu(reshape(slice(X,0,2))) must read PRE-round-2 bytes:
16130        // relu([1.0, 2.0]) = [1.0, 2.0]. Round 2 overwrites the SAME
16131        // [0,2) range with [99.0, 98.0] — if Y (round 2) runs before K
16132        // (the ordering defect), K instead observes [99.0, 98.0].
16133        assert_eq!(
16134            out,
16135            &[1.0, 2.0],
16136            "K must read PRE-round-2 bytes through the adopted Arc; got {:?} \
16137             (post-round-2 corruption if this fails)",
16138            out,
16139        );
16140    }
16141
16142    /// The MLA-decode shape from `rebind_latent_cache_fresh_graph`'s doc
16143    /// (`fuel-core/src/lazy_deepseek2.rs`): layer L's post-append buffer
16144    /// `X_L_N` (round N) is read TWICE within one realize call — once
16145    /// (non-destructively) through a Slice+Reshape "attention math" view
16146    /// chain whose result becomes an ancestor of layer L+1's round-N
16147    /// write, and again as the DESTINATION of layer L's round-(N+1)
16148    /// append. Reproduces the same Reshape-adoption gap as
16149    /// `pipelined_write_slice_reshape_adopted_arc_reader_reads_pre_write_bytes`
16150    /// at 2-layer scale, confirming the defect is not an artifact of the
16151    /// single-hop minimal form.
16152    #[test]
16153    fn pipelined_write_slice_mla_two_layer_round2_reshape_reader_not_stranded() {
16154        let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
16155        let mut cache = StorageCache::new();
16156
16157        let b_l_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
16158        let src_l_n_id = cpu_const_f32(&graph, &mut cache, &[1.0, 2.0], &[2]);
16159        let b_l1_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
16160        let src_l_n1_id = cpu_const_f32(&graph, &mut cache, &[99.0, 98.0], &[2]);
16161
16162        let (x_l_n1_id, x_l1_n_id) = {
16163            let mut g = graph.write().unwrap();
16164            // Round N, layer L: append src_l_n into b_l.
16165            let x_l_n = g.push(Node {
16166                op: Op::WriteSlice {
16167                    ranges: vec![(0, 2)],
16168                    dyn_offset: None,
16169                },
16170                inputs: vec![b_l_id, src_l_n_id],
16171                shape: Shape::from_dims(&[4]),
16172                dtype: DType::F32,
16173            });
16174            // "Attention math" for round N at layer L: read the filled
16175            // prefix, reshape (head-split-style hop), then a kernel.
16176            let s_l_n = g.push(Node {
16177                op: Op::Slice {
16178                    dim: 0,
16179                    start: 0,
16180                    len: 2,
16181                },
16182                inputs: vec![x_l_n],
16183                shape: Shape::from_dims(&[2]),
16184                dtype: DType::F32,
16185            });
16186            let rs_l_n = g.push(Node {
16187                op: Op::Reshape(Shape::from_dims(&[2])),
16188                inputs: vec![s_l_n],
16189                shape: Shape::from_dims(&[2]),
16190                dtype: DType::F32,
16191            });
16192            let r_l_n = g.push(Node {
16193                op: Op::Relu,
16194                inputs: vec![rs_l_n],
16195                shape: Shape::from_dims(&[2]),
16196                dtype: DType::F32,
16197            });
16198            // Round N, layer L+1: append the attention-math result.
16199            let x_l1_n = g.push(Node {
16200                op: Op::WriteSlice {
16201                    ranges: vec![(0, 2)],
16202                    dyn_offset: None,
16203                },
16204                inputs: vec![b_l1_id, r_l_n],
16205                shape: Shape::from_dims(&[4]),
16206                dtype: DType::F32,
16207            });
16208            // Round N+1, layer L: append into x_l_n directly (SAME node
16209            // s_l_n/rs_l_n/r_l_n read from) — OVERLAPPING range so a
16210            // wrong-order execution is numerically visible downstream.
16211            let x_l_n1 = g.push(Node {
16212                op: Op::WriteSlice {
16213                    ranges: vec![(0, 2)],
16214                    dyn_offset: None,
16215                },
16216                inputs: vec![x_l_n, src_l_n1_id],
16217                shape: Shape::from_dims(&[4]),
16218                dtype: DType::F32,
16219            });
16220            g.set_target_backend(x_l_n, BackendId::Cpu);
16221            g.set_target_backend(r_l_n, BackendId::Cpu);
16222            g.set_target_backend(x_l1_n, BackendId::Cpu);
16223            g.set_target_backend(x_l_n1, BackendId::Cpu);
16224            (x_l_n1, x_l1_n)
16225        };
16226
16227        let plan = fuel_graph::opt::execution_plan(&graph.read().unwrap(), &[x_l1_n_id, x_l_n1_id]);
16228        eprintln!("[diagnostic mla-2-layer] execution_plan = {:?}", plan);
16229
16230        let outputs =
16231            PipelinedExecutor::realize_many(Arc::clone(&graph), &[x_l1_n_id, x_l_n1_id], cache)
16232                .expect("realize_many");
16233        // outputs[0] = x_l1_n = write_slice(b_l1, relu(reshape(slice(x_l_n))))
16234        // Must reflect round-N's PRE-round-(N+1) bytes: relu([1,2]) = [1,2].
16235        let (x_l1_n_arc, _) = &outputs[0];
16236        let guard = x_l1_n_arc.read().unwrap();
16237        let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
16238            panic!("expected Cpu storage");
16239        };
16240        let out: &[f32] = c.as_slice().unwrap();
16241        assert_eq!(
16242            out,
16243            &[1.0, 2.0, 0.0, 0.0],
16244            "layer L+1's round-N append must reflect PRE-round-(N+1) attention \
16245             math output; got {:?} (post-round-(N+1) corruption if this fails)",
16246            out,
16247        );
16248    }
16249}