Skip to main content

fuel_tensor/
lazy.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Phase 6a bridge: a lazy-computation-graph tensor that wraps
3//! [`fuel_graph::NodeHandle`] and presents it through an API compatible
4//! with fuel-core's eager `Tensor`.
5//!
6//! # Purpose
7//!
8//! The Phase 6 architectural pivot moves fuel from eager execution to a
9//! lazy computation graph. End state: `fuel_core::tensor::Tensor` *is* a
10//! `fuel_graph::NodeHandle` and every model in `fuel-transformers` runs
11//! through the lazy backend without per-model porting.
12//!
13//! The bridge is the intermediate stage. [`Tensor`] is a wrapper
14//! around [`fuel_graph::NodeHandle`] that exposes the fuel-core-style
15//! method API (`.add()`, `.mul()`, `.matmul()`, `.relu()`, `.shape()`,
16//! `.to_vec0()`, `.to_vec1()`, ...) so callers can gradually migrate
17//! from eager to lazy one function at a time. Each method appends a
18//! node to the underlying [`fuel_graph::Graph`]; nothing runs until
19//! you call [`Tensor::realize_f32`] or a sibling.
20//!
21//! This is NOT intended as a permanent user-facing type. It's the
22//! scaffolding that makes the final merge incremental: each
23//! `fuel-transformers` model can be converted to `Tensor` in a
24//! separate PR, and once they all compile against the wrapper, the
25//! type alias flips and `fuel_core::tensor::Tensor` becomes the lazy variant.
26//!
27//! # What's here today
28//!
29//! A minimal but real subset: constructors from `Vec<f32>`/`Vec<f64>`
30//! and friends, shape/dtype inspection, the element-wise arithmetic
31//! and unary ops most models use, matmul, softmax, layer_norm,
32//! rms_norm, and realization to a typed `Vec`. Everything routes
33//! through `fuel_graph::NodeHandle` underneath.
34//!
35//! Missing: autograd integration via `fuel_core::Var`, the
36//! `backward()` / `apply_op*` convenience methods, safetensors
37//! loading directly into `Tensor`s, and many of the niche
38//! methods on `fuel_core::tensor::Tensor`. All of these are additive
39//! extensions — they do not require changes to the bridge's
40//! structural design.
41
42use crate::inference_context::InferenceContext;
43use crate::{DType, Device, Shape};
44use fuel_ir::shape::{Dim, Dims};
45use std::sync::Arc;
46
47/// A lazy tensor that builds a `fuel_graph::Graph` as its methods are
48/// called. Cheap to clone — the underlying `fuel_graph::NodeHandle` is a
49/// cheap handle pair `(Rc<RefCell<Graph>>, NodeId)`, so cloning just
50/// bumps the `Rc` and copies the id.
51///
52/// # Tensors are graph-affine — read this before building anything
53///
54/// **Every tensor in one computation must descend from a common root.**
55/// Each `from_*` constructor ([`from_f32`](Self::from_f32),
56/// [`from_bf16`](Self::from_bf16), …) — and [`zeros`](Self::zeros) /
57/// [`full`](Self::full), which delegate to them — **mints a brand-new
58/// graph**. Two independently-constructed tensors therefore live on two
59/// different graphs and **cannot be combined**: every binary op asserts
60/// `"… must live on the same graph"`.
61///
62/// So the obvious shape does *not* work:
63///
64/// ```ignore
65/// let a = Tensor::from_f32(a_data, [2, 3], &device);
66/// let w = Tensor::from_f32(w_data, [3, 2], &device);  // ← a SECOND graph
67/// let y = a.matmul(&w);                                   // ← panics
68/// ```
69///
70/// There are two ways to put a second tensor on an existing graph.
71///
72/// **1. `from_*_on` — pass the graph.** Use this when you have the graph, or
73/// simply want the construction site to say which graph it targets:
74///
75/// ```ignore
76/// let a = Tensor::from_f32(a_data, [2, 3], &device);          // the root
77/// let w = Tensor::from_f32_on(a.graph(), w_data, [3, 2], &device);
78/// let y = a.matmul(&w);                                           // ✓
79/// ```
80///
81/// **2. `const_*_like` — pass an anchor tensor.** Identical effect; use it when
82/// what you have to hand is a sibling tensor rather than the graph:
83///
84/// ```ignore
85/// let a = Tensor::from_f32(a_data, [2, 3], &device);  // the root
86/// let w = a.const_f32_like(w_data, [3, 2].into());        // same graph ✓
87/// let y = a.matmul(&w);                                   // ✓
88/// ```
89///
90/// Every tensor also reports its [`graph_id`](Self::graph_id), so if two do end
91/// up on different graphs the panic names which is which.
92///
93/// In a model this means the **activation tensor is the root and the
94/// weights are `const_*_like` off it**. `const_bf16_like` exists for
95/// precisely the mixed-precision case — f32 activations with bf16 weight
96/// matrices sharing one graph.
97#[derive(Clone, Debug)]
98pub struct Tensor {
99    // `pub(crate)`: the shared decode build path lives in
100    // `crate::persistent_decode` (GAP-029 increment 3) and needs the graph +
101    // NodeId handles to mint Consts and name realize roots. Still private to
102    // the crate — no consumer sees the graph representation.
103    pub(crate) inner: fuel_graph::NodeHandle,
104}
105
106impl Tensor {
107    // ---- constructors ----
108
109    /// Build an `f32` lazy tensor from flat data, a shape, and a device.
110    ///
111    /// `data` takes `impl Into<Arc<[f32]>>` so both `Vec<f32>` and
112    /// `Arc<[f32]>` callers work without conversion. Pass an `Arc`
113    /// when you already have one (e.g. model weights loaded once at
114    /// startup) to avoid any copy.
115    ///
116    /// Phase 7.5 G2: `device` selects where the realized Storage is
117    /// allocated. The graph's storage_map slot for the new node is
118    /// populated and `Op::Const(None)` is emitted — no host-side
119    /// `ConstData` payload rides on the graph node.
120    ///
121    /// # Mints a NEW graph
122    ///
123    /// This starts a **fresh graph**. A tensor built here cannot be combined
124    /// with one built by another `from_*` call — ops assert both operands
125    /// share a graph. To add a second tensor to *this* one's graph, use
126    /// [`const_f32_like`](Self::const_f32_like) (or a sibling
127    /// `const_*_like`). See [graph affinity](Self#tensors-are-graph-affine--read-this-before-building-anything).
128    pub fn from_f32(
129        data: impl Into<Arc<[f32]>>,
130        shape: impl Into<Shape>,
131        device: &crate::Device,
132    ) -> fuel_ir::Result<Self> {
133        Ok(Self {
134            inner: fuel_graph::NodeHandle::from_f32(data, shape, device.as_dyn())?,
135        })
136    }
137
138    /// This tensor's graph identity — see [`fuel_graph::GraphId`]. Two tensors
139    /// can be combined **iff** their `graph_id`s match.
140    pub fn graph_id(&self) -> fuel_graph::GraphId {
141        self.inner.graph_id()
142    }
143
144    /// The shared graph this tensor belongs to — hand it to a `from_*_on`
145    /// constructor to build a sibling on the same graph.
146    pub fn graph(&self) -> &fuel_graph::SharedGraph {
147        self.inner.graph()
148    }
149
150    /// Build an `f32` lazy tensor **on the graph you pass in**, not a fresh one.
151    ///
152    /// The non-anchor route for adding a tensor to an existing graph.
153    /// [`from_f32`](Self::from_f32) mints a NEW graph, so two `from_*` tensors
154    /// can never be combined; this puts the new leaf on `graph`.
155    /// [`const_f32_like`](Self::const_f32_like) does the same when what you have
156    /// to hand is a sibling *tensor* rather than the graph itself.
157    ///
158    /// ```ignore
159    /// let a = Tensor::from_f32(a_data, [2, 3], &device);          // root
160    /// let w = Tensor::from_f32_on(a.graph(), w_data, [3, 2], &device);
161    /// let y = a.matmul(&w)?;                                          // ✓
162    /// ```
163    pub fn from_f32_on(
164        graph: &fuel_graph::SharedGraph,
165        data: impl Into<Arc<[f32]>>,
166        shape: impl Into<Shape>,
167        device: &crate::Device,
168    ) -> fuel_ir::Result<Self> {
169        Ok(Self {
170            inner: fuel_graph::NodeHandle::from_f32_on(graph, data, shape, device.as_dyn())?,
171        })
172    }
173
174    /// `f64` sibling of [`from_f32_on`](Self::from_f32_on).
175    pub fn from_f64_on(
176        graph: &fuel_graph::SharedGraph,
177        data: impl Into<Arc<[f64]>>,
178        shape: impl Into<Shape>,
179        device: &crate::Device,
180    ) -> fuel_ir::Result<Self> {
181        Ok(Self {
182            inner: fuel_graph::NodeHandle::from_f64_on(graph, data, shape, device.as_dyn())?,
183        })
184    }
185
186    /// `bf16` sibling of [`from_f32_on`](Self::from_f32_on) — the mixed-precision
187    /// shape (f32 activations, bf16 weights) is this plus
188    /// [`from_f32_on`](Self::from_f32_on) against one shared graph.
189    pub fn from_bf16_on(
190        graph: &fuel_graph::SharedGraph,
191        data: impl Into<Arc<[half::bf16]>>,
192        shape: impl Into<Shape>,
193        device: &crate::Device,
194    ) -> fuel_ir::Result<Self> {
195        Ok(Self {
196            inner: fuel_graph::NodeHandle::from_bf16_on(graph, data, shape, device.as_dyn())?,
197        })
198    }
199
200    /// `f16` sibling of [`from_f32_on`](Self::from_f32_on).
201    pub fn from_f16_on(
202        graph: &fuel_graph::SharedGraph,
203        data: impl Into<Arc<[half::f16]>>,
204        shape: impl Into<Shape>,
205        device: &crate::Device,
206    ) -> fuel_ir::Result<Self> {
207        Ok(Self {
208            inner: fuel_graph::NodeHandle::from_f16_on(graph, data, shape, device.as_dyn())?,
209        })
210    }
211
212    /// `u32` sibling of [`from_f32_on`](Self::from_f32_on) — index tensors built
213    /// straight onto the graph they will be gathered on.
214    pub fn from_u32_on(
215        graph: &fuel_graph::SharedGraph,
216        data: impl Into<Arc<[u32]>>,
217        shape: impl Into<Shape>,
218        device: &crate::Device,
219    ) -> fuel_ir::Result<Self> {
220        Ok(Self {
221            inner: fuel_graph::NodeHandle::from_u32_on(graph, data, shape, device.as_dyn())?,
222        })
223    }
224
225    /// Build an `f64` lazy tensor. `device` selects where the realized
226    /// Storage is allocated.
227    ///
228    /// **Mints a NEW graph** — to build on an existing tensor's graph use
229    /// [`const_f64_like`](Self::const_f64_like). See
230    /// [graph affinity](Self#tensors-are-graph-affine--read-this-before-building-anything).
231    pub fn from_f64(
232        data: impl Into<Arc<[f64]>>,
233        shape: impl Into<Shape>,
234        device: &crate::Device,
235    ) -> fuel_ir::Result<Self> {
236        Ok(Self {
237            inner: fuel_graph::NodeHandle::from_f64(data, shape, device.as_dyn())?,
238        })
239    }
240
241    /// Build a `bf16` lazy tensor. `device` selects where the realized
242    /// Storage is allocated.
243    ///
244    /// **Mints a NEW graph.** For the common case — bf16 weights alongside
245    /// f32 activations — build the weights with
246    /// [`const_bf16_like`](Self::const_bf16_like) off the activation tensor
247    /// so both share one graph. See
248    /// [graph affinity](Self#tensors-are-graph-affine--read-this-before-building-anything).
249    pub fn from_bf16(
250        data: impl Into<Arc<[half::bf16]>>,
251        shape: impl Into<Shape>,
252        device: &crate::Device,
253    ) -> fuel_ir::Result<Self> {
254        Ok(Self {
255            inner: fuel_graph::NodeHandle::from_bf16(data, shape, device.as_dyn())?,
256        })
257    }
258
259    /// Build an `f16` lazy tensor. `device` selects where the realized
260    /// Storage is allocated.
261    pub fn from_f16(
262        data: impl Into<Arc<[half::f16]>>,
263        shape: impl Into<Shape>,
264        device: &crate::Device,
265    ) -> fuel_ir::Result<Self> {
266        Ok(Self {
267            inner: fuel_graph::NodeHandle::from_f16(data, shape, device.as_dyn())?,
268        })
269    }
270
271    /// Build a `u32` (index) lazy tensor. Used for gather/scatter/
272    /// index_select and similar discrete operations. `device` selects
273    /// where the realized Storage is allocated.
274    pub fn from_u32(
275        data: impl Into<Arc<[u32]>>,
276        shape: impl Into<Shape>,
277        device: &crate::Device,
278    ) -> fuel_ir::Result<Self> {
279        Ok(Self {
280            inner: fuel_graph::NodeHandle::from_u32(data, shape, device.as_dyn())?,
281        })
282    }
283
284    /// Build a const tensor of the same dtype and graph as `self`.
285    /// This is the most convenient way to attach new input data to an
286    /// existing computation.
287    ///
288    /// Phase 7.5 G2: the realized Storage is allocated on the device
289    /// derived from `self`'s graph (any existing slot's device — the
290    /// graph always has at least one slot-bearing leaf by the time
291    /// const_*_like is called). Use [`Self::from_f32`] with an explicit
292    /// `&Device` when you need a const on a different device than
293    /// `self`.
294    pub fn const_f32_like(
295        &self,
296        data: impl Into<Arc<[f32]>>,
297        shape: impl Into<Shape>,
298    ) -> std::result::Result<Self, fuel_ir::Error> {
299        Ok(Self {
300            inner: self.inner.const_f32_like(data, shape)?,
301        })
302    }
303
304    /// Build a const f16 tensor on the same graph as `self`.
305    pub fn const_f16_like(
306        &self,
307        data: impl Into<Arc<[half::f16]>>,
308        shape: impl Into<Shape>,
309    ) -> std::result::Result<Self, fuel_ir::Error> {
310        Ok(Self {
311            inner: self.inner.const_f16_like(data, shape)?,
312        })
313    }
314
315    /// Build a const bf16 tensor on the same graph as `self`. Used for
316    /// bf16-on-device weights in the mixed-precision matmul path —
317    /// activations stay f32, weight matrices live as bf16.
318    pub fn const_bf16_like(
319        &self,
320        data: impl Into<Arc<[half::bf16]>>,
321        shape: impl Into<Shape>,
322    ) -> std::result::Result<Self, fuel_ir::Error> {
323        Ok(Self {
324            inner: self.inner.const_bf16_like(data, shape)?,
325        })
326    }
327
328    /// Build a const tensor on the same graph as `self`, holding
329    /// `data`'s values encoded in `dtype`. Only `F32` (identity copy)
330    /// and `BF16` (host `half::bf16::from_f32` conversion) are
331    /// supported today — the two activation dtypes the BF16-throughout
332    /// decode seam (Phase D increment A: LlamaModel's cached decode
333    /// path) threads through small per-model host constants (norm
334    /// gains, biases, the causal mask) that must track the running
335    /// activation dtype instead of being hardcoded f32. Any other
336    /// dtype is a caller bug (not a data-dependent runtime
337    /// possibility), so it surfaces a typed error rather than silently
338    /// emitting wrong bytes.
339    pub fn const_like_dtype(
340        &self,
341        data: &[f32],
342        shape: impl Into<Shape>,
343        dtype: fuel_ir::DType,
344    ) -> std::result::Result<Self, fuel_ir::Error> {
345        match dtype {
346            fuel_ir::DType::F32 => self.const_f32_like(data.to_vec(), shape),
347            fuel_ir::DType::BF16 => {
348                let converted: Vec<half::bf16> =
349                    data.iter().map(|&v| half::bf16::from_f32(v)).collect();
350                self.const_bf16_like(converted, shape)
351            }
352            other => Err(fuel_ir::Error::Msg(format!(
353                "const_like_dtype: unsupported activation dtype {other:?} \
354                 (expected F32 or BF16)",
355            ))
356            .bt()),
357        }
358    }
359
360    /// Unwrap the underlying `fuel_graph::NodeHandle`. Used by callers that
361    /// need to drop down to the graph layer for operations the bridge
362    /// does not yet expose.
363    pub fn into_graph_tensor(self) -> fuel_graph::NodeHandle {
364        self.inner
365    }
366
367    /// Borrow the underlying `fuel_graph::NodeHandle`.
368    pub fn graph_tensor(&self) -> &fuel_graph::NodeHandle {
369        &self.inner
370    }
371
372    /// Wrap an existing `fuel_graph::NodeHandle` in a `Tensor`. Useful
373    /// when you have code that already builds a graph and want to
374    /// present its outputs through this API.
375    pub fn from_graph_tensor(t: fuel_graph::NodeHandle) -> Self {
376        Self { inner: t }
377    }
378
379    // ---- shape / dtype inspection ----
380
381    /// The tensor's shape.
382    pub fn shape(&self) -> Shape {
383        self.inner.shape()
384    }
385
386    /// The tensor's dtype.
387    pub fn dtype(&self) -> DType {
388        self.inner.dtype()
389    }
390
391    /// This tensor's underlying graph handle. Exposed for call sites
392    /// outside this module that need to drive
393    /// [`InferenceContext`](crate::inference_context::InferenceContext)
394    /// directly — e.g. a persistent cross-graph decode cache implemented
395    /// in a sibling model module (`lazy_deepseek2.rs`'s
396    /// `forward_with_latent_kv_context`). Most callers should prefer
397    /// [`Self::realize_f32`] and friends instead.
398    pub fn graph_handle(&self) -> &fuel_graph::SharedGraph {
399        self.inner.graph()
400    }
401
402    /// This tensor's `NodeId` on its graph. See [`Self::graph_handle`].
403    pub fn node_id(&self) -> fuel_graph::NodeId {
404        self.inner.id()
405    }
406
407    /// The tensor's rank (number of dimensions).
408    pub fn rank(&self) -> usize {
409        self.inner.shape().dims().len()
410    }
411
412    /// Total element count.
413    pub fn elem_count(&self) -> usize {
414        self.inner.shape().elem_count()
415    }
416
417    /// PyTorch-convention alias of [`Self::elem_count`].
418    pub fn numel(&self) -> usize {
419        self.elem_count()
420    }
421
422    /// Size of the tensor along dimension `dim`. Returns a typed error
423    /// rather than panicking on out-of-range — matches eager's
424    /// `crate::Tensor::dim` signature.
425    pub fn dim<D: Dim>(&self, dim: D) -> std::result::Result<usize, fuel_ir::Error> {
426        let shape = self.inner.shape();
427        let dim = dim.to_index(&shape, "dim")?;
428        Ok(shape.dims()[dim])
429    }
430
431    // ---- arithmetic (element-wise, strict shape) ----
432
433    /// Element-wise addition. Shapes and dtypes must match — mismatches
434    /// surface as typed errors at build time.
435    pub fn add(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
436        self.check_strict_binary("add", other)?;
437        Ok(Self {
438            inner: self.inner.add(&other.inner),
439        })
440    }
441
442    /// Element-wise subtraction.
443    pub fn sub(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
444        self.check_strict_binary("sub", other)?;
445        Ok(Self {
446            inner: self.inner.sub(&other.inner),
447        })
448    }
449
450    /// Element-wise multiplication.
451    pub fn mul(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
452        self.check_strict_binary("mul", other)?;
453        Ok(Self {
454            inner: self.inner.mul(&other.inner),
455        })
456    }
457
458    /// Element-wise division.
459    pub fn div(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
460        self.check_strict_binary("div", other)?;
461        Ok(Self {
462            inner: self.inner.div(&other.inner),
463        })
464    }
465
466    /// Element-wise maximum.
467    pub fn maximum(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
468        self.check_strict_binary("maximum", other)?;
469        Ok(Self {
470            inner: self.inner.maximum(&other.inner),
471        })
472    }
473
474    /// Element-wise minimum.
475    pub fn minimum(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
476        self.check_strict_binary("minimum", other)?;
477        Ok(Self {
478            inner: self.inner.minimum(&other.inner),
479        })
480    }
481
482    /// IEEE-754 NaN-**suppressing** maximum (`fmax_ieee`, KISS-OPS §6.15-0001) —
483    /// DISTINCT from [`maximum`](Self::maximum) (NaN-propagating). Resolves through
484    /// the spec-pinned §6.13 decomposition; does not substitute `maximum`. GAP-048.
485    pub fn fmax_ieee(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
486        self.check_strict_binary("fmax_ieee", other)?;
487        Ok(Self {
488            inner: self.inner.fmax_ieee(&other.inner),
489        })
490    }
491
492    /// IEEE-754 NaN-**suppressing** minimum (`fmin_ieee`, KISS-OPS §6.15-0001) —
493    /// DISTINCT from [`minimum`](Self::minimum) (NaN-propagating). GAP-048.
494    pub fn fmin_ieee(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
495        self.check_strict_binary("fmin_ieee", other)?;
496        Ok(Self {
497            inner: self.inner.fmin_ieee(&other.inner),
498        })
499    }
500
501    /// Truncated remainder (`rem_trunc`, KISS-OPS §6.15-0003) — sign of the
502    /// DIVIDEND (C99 `fmod`), DISTINCT from [`rem`](Self::rem) (floored, sign of
503    /// the divisor). Resolves through the spec-pinned §6.13 decomposition. GAP-048.
504    pub fn rem_trunc(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
505        self.check_strict_binary("rem_trunc", other)?;
506        Ok(Self {
507            inner: self.inner.rem_trunc(&other.inner),
508        })
509    }
510
511    /// Round toward zero (`trunc`, KISS-OPS §6.3 floor op; GAP-300). Fuel's own
512    /// expansion (`q>=0 ? floor(q) : ceil(q)`), NOT spec-pinned — its ±0/NaN/inf
513    /// correctness is Fuel's liability. Integer dtypes are identity.
514    pub fn trunc(&self) -> Self {
515        Self {
516            inner: self.inner.trunc(),
517        }
518    }
519
520    /// Element-wise equality (`self == other`) producing a `U8` mask:
521    /// `1` where equal, `0` otherwise. Both operands must share dtype
522    /// and shape. NaN follows IEEE-754 (`NaN == NaN` is false). The
523    /// resulting tensor's dtype is `DType::U8`. Non-differentiable.
524    pub fn eq(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
525        self.check_strict_binary("eq", other)?;
526        Ok(Self {
527            inner: self.inner.eq(&other.inner),
528        })
529    }
530
531    /// Element-wise inequality (`self != other`) producing a `U8`
532    /// mask. NaN follows IEEE-754 (`NaN != NaN` is true → `1`).
533    /// Non-differentiable.
534    pub fn ne(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
535        self.check_strict_binary("ne", other)?;
536        Ok(Self {
537            inner: self.inner.ne(&other.inner),
538        })
539    }
540
541    /// Element-wise strictly-less (`self < other`) producing a `U8`
542    /// mask. NaN-on-either-side is `0` (IEEE-754 unordered).
543    /// Non-differentiable.
544    pub fn lt(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
545        self.check_strict_binary("lt", other)?;
546        Ok(Self {
547            inner: self.inner.lt(&other.inner),
548        })
549    }
550
551    /// Element-wise less-or-equal (`self <= other`) producing a `U8`
552    /// mask. NaN-on-either-side is `0`. Non-differentiable.
553    pub fn le(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
554        self.check_strict_binary("le", other)?;
555        Ok(Self {
556            inner: self.inner.le(&other.inner),
557        })
558    }
559
560    /// Element-wise strictly-greater (`self > other`) producing a
561    /// `U8` mask. NaN-on-either-side is `0`. Non-differentiable.
562    pub fn gt(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
563        self.check_strict_binary("gt", other)?;
564        Ok(Self {
565            inner: self.inner.gt(&other.inner),
566        })
567    }
568
569    /// Element-wise greater-or-equal (`self >= other`) producing a
570    /// `U8` mask. NaN-on-either-side is `0`. Non-differentiable.
571    /// Final variant of the comparison family (`eq` / `ne` / `lt` /
572    /// `le` / `gt` / `ge`).
573    pub fn ge(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
574        self.check_strict_binary("ge", other)?;
575        Ok(Self {
576            inner: self.inner.ge(&other.inner),
577        })
578    }
579
580    /// Ternary select (typically used to consume a comparison-op
581    /// mask): `result[i] = if self[i] { a[i] } else { b[i] }`.
582    /// `self` is the cond mask (must be `DType::Bool`, GAP-168(c)); `a` and `b`
583    /// share dtype + shape with `self`. Output dtype matches `a`/`b`,
584    /// shape matches `self`.
585    ///
586    /// Differentiable through `a` and `b` only.
587    pub fn where_cond(&self, a: &Self, b: &Self) -> std::result::Result<Self, fuel_ir::Error> {
588        // GAP-168(c): the cond mask is Bool, matching `masked_fill` and the
589        // `fixed(BOOL)` comparison contracts. The DOC above already said Bool
590        // while this guard still required U8 — prose and check disagreeing, with
591        // the prose right; every comparison feeds `where_cond`, so 22 model tests
592        // failed on it. Cast a numeric mask explicitly (the cast is deliberately
593        // NOT implicit — a silent coercion is what the Bool dtype exists to stop).
594        if self.inner.dtype() != fuel_ir::DType::Bool {
595            return Err(fuel_ir::Error::Msg(format!(
596                "where_cond: cond mask must be Bool, got {:?} — cast a numeric \
597                 mask to Bool explicitly (GAP-168(c))",
598                self.inner.dtype(),
599            ))
600            .bt());
601        }
602        if a.inner.dtype() != b.inner.dtype() {
603            return Err(fuel_ir::Error::Msg(format!(
604                "where_cond: branches must share dtype, got a={:?} b={:?}",
605                a.inner.dtype(),
606                b.inner.dtype(),
607            ))
608            .bt());
609        }
610        let cond_dims = self.inner.shape();
611        let a_dims = a.inner.shape();
612        let b_dims = b.inner.shape();
613        if a_dims.dims() != cond_dims.dims() || b_dims.dims() != cond_dims.dims() {
614            return Err(fuel_ir::Error::Msg(format!(
615                "where_cond: shapes must match cond, got cond={:?} a={:?} b={:?}",
616                cond_dims.dims(),
617                a_dims.dims(),
618                b_dims.dims(),
619            ))
620            .bt());
621        }
622        Ok(Self {
623            inner: self.inner.where_cond(&a.inner, &b.inner),
624        })
625    }
626
627    // ---- broadcast-aware arithmetic ----
628
629    /// Element-wise addition with auto-broadcasting.
630    pub fn broadcast_add(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
631        self.check_broadcast_binary("broadcast_add", other)?;
632        Ok(Self {
633            inner: self.inner.broadcast_add(&other.inner),
634        })
635    }
636
637    /// Element-wise subtraction with auto-broadcasting.
638    pub fn broadcast_sub(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
639        self.check_broadcast_binary("broadcast_sub", other)?;
640        Ok(Self {
641            inner: self.inner.broadcast_sub(&other.inner),
642        })
643    }
644
645    /// Element-wise multiplication with auto-broadcasting.
646    pub fn broadcast_mul(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
647        self.check_broadcast_binary("broadcast_mul", other)?;
648        Ok(Self {
649            inner: self.inner.broadcast_mul(&other.inner),
650        })
651    }
652
653    /// Element-wise division with auto-broadcasting.
654    pub fn broadcast_div(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
655        self.check_broadcast_binary("broadcast_div", other)?;
656        Ok(Self {
657            inner: self.inner.broadcast_div(&other.inner),
658        })
659    }
660
661    fn check_strict_binary(
662        &self,
663        name: &'static str,
664        other: &Self,
665    ) -> std::result::Result<(), fuel_ir::Error> {
666        if self.inner.dtype() != other.inner.dtype() {
667            return Err(fuel_ir::Error::Msg(format!(
668                "{name}: dtype mismatch lhs={:?} rhs={:?}",
669                self.inner.dtype(),
670                other.inner.dtype(),
671            ))
672            .bt());
673        }
674        let a_shape = self.inner.shape();
675        let b_shape = other.inner.shape();
676        if a_shape.dims() != b_shape.dims() {
677            return Err(fuel_ir::Error::Msg(format!(
678                "{name}: shape mismatch lhs={:?} rhs={:?}",
679                a_shape.dims(),
680                b_shape.dims(),
681            ))
682            .bt());
683        }
684        Ok(())
685    }
686
687    fn check_broadcast_binary(
688        &self,
689        name: &'static str,
690        other: &Self,
691    ) -> std::result::Result<(), fuel_ir::Error> {
692        if self.inner.dtype() != other.inner.dtype() {
693            return Err(fuel_ir::Error::Msg(format!(
694                "{name}: dtype mismatch lhs={:?} rhs={:?}",
695                self.inner.dtype(),
696                other.inner.dtype(),
697            ))
698            .bt());
699        }
700        let a_shape = self.inner.shape();
701        let b_shape = other.inner.shape();
702        let a_dims = a_shape.dims();
703        let b_dims = b_shape.dims();
704        // Standard NumPy-style broadcast compatibility: from the right,
705        // each pair of dims must be equal, or one of them must be 1.
706        let rank = a_dims.len().max(b_dims.len());
707        for i in 0..rank {
708            let ad = a_dims
709                .get(a_dims.len().wrapping_sub(1 + i))
710                .copied()
711                .unwrap_or(1);
712            let bd = b_dims
713                .get(b_dims.len().wrapping_sub(1 + i))
714                .copied()
715                .unwrap_or(1);
716            if ad != bd && ad != 1 && bd != 1 {
717                return Err(fuel_ir::Error::Msg(format!(
718                    "{name}: shapes {:?} and {:?} are not broadcast-compatible",
719                    a_dims, b_dims,
720                ))
721                .bt());
722            }
723        }
724        Ok(())
725    }
726
727    // ---- unary ----
728
729    /// Element-wise negation.
730    pub fn neg(&self) -> Self {
731        Self {
732            inner: self.inner.neg(),
733        }
734    }
735
736    /// Element-wise square.
737    pub fn sqr(&self) -> Self {
738        Self {
739            inner: self.inner.sqr(),
740        }
741    }
742
743    /// Element-wise square root.
744    pub fn sqrt(&self) -> Self {
745        Self {
746            inner: self.inner.sqrt(),
747        }
748    }
749
750    /// Element-wise exponential.
751    pub fn exp(&self) -> Self {
752        Self {
753            inner: self.inner.exp(),
754        }
755    }
756
757    /// Element-wise natural logarithm.
758    pub fn log(&self) -> Self {
759        Self {
760            inner: self.inner.log(),
761        }
762    }
763
764    /// Rectified linear unit.
765    pub fn relu(&self) -> Self {
766        Self {
767            inner: self.inner.relu(),
768        }
769    }
770
771    /// SiLU / Swish activation.
772    pub fn silu(&self) -> Self {
773        Self {
774            inner: self.inner.silu(),
775        }
776    }
777
778    /// GELU activation (tanh approximation).
779    pub fn gelu_tanh(&self) -> Self {
780        Self {
781            inner: self.inner.gelu_tanh(),
782        }
783    }
784
785    /// Logistic sigmoid.
786    pub fn sigmoid(&self) -> Self {
787        Self {
788            inner: self.inner.sigmoid(),
789        }
790    }
791
792    /// Hyperbolic tangent.
793    pub fn tanh(&self) -> Self {
794        Self {
795            inner: self.inner.tanh(),
796        }
797    }
798
799    /// Element-wise sine.
800    pub fn sin(&self) -> Self {
801        Self {
802            inner: self.inner.sin(),
803        }
804    }
805
806    /// Element-wise cosine.
807    pub fn cos(&self) -> Self {
808        Self {
809            inner: self.inner.cos(),
810        }
811    }
812
813    /// Heaviside step (`1` where `x > 0`, else `0`) — the derivative
814    /// of [`Self::relu`].
815    pub fn step(&self) -> Self {
816        Self {
817            inner: self.inner.step(),
818        }
819    }
820
821    /// Element-wise reciprocal (`1 / x`).
822    pub fn recip(&self) -> Self {
823        Self {
824            inner: self.inner.recip(),
825        }
826    }
827
828    /// Element-wise absolute value (`|x|`).
829    pub fn abs(&self) -> Self {
830        Self {
831            inner: self.inner.abs(),
832        }
833    }
834
835    /// Element-wise floor (`⌊x⌋`). Same dtype as input.
836    /// Backward is silently zero (non-differentiable almost everywhere).
837    pub fn floor(&self) -> Self {
838        Self {
839            inner: self.inner.floor(),
840        }
841    }
842
843    /// Element-wise ceiling (`⌈x⌉`). Same dtype as input.
844    /// Backward is silently zero.
845    pub fn ceil(&self) -> Self {
846        Self {
847            inner: self.inner.ceil(),
848        }
849    }
850
851    /// Element-wise round-to-nearest with **banker's rounding**
852    /// (round-half-to-even, IEEE 754 roundeven). Backward is silently
853    /// zero. Differs from C99 `round()` at exact halves: 0.5 → 0,
854    /// 2.5 → 2, etc.
855    pub fn round(&self) -> Self {
856        Self {
857            inner: self.inner.round(),
858        }
859    }
860
861    /// Element-wise sign (`-1` / `0` / `1`); `sign(0) = 0` by
862    /// subgradient convention. Same dtype as input. Backward is
863    /// silently zero.
864    pub fn sign(&self) -> Self {
865        Self {
866            inner: self.inner.sign(),
867        }
868    }
869
870    /// Element-wise Gauss error function (`erf(x)`). Same dtype as
871    /// input. Differentiable: `d/dx erf(x) = (2/√π) * exp(-x²)`.
872    pub fn erf(&self) -> Self {
873        Self {
874            inner: self.inner.erf(),
875        }
876    }
877
878    /// GELU activation, **exact erf form** (`0.5 * x * (1 + erf(x/√2))`).
879    /// Distinct from [`Self::gelu_tanh`] (tanh approximation). Same dtype
880    /// as input. Differentiable.
881    pub fn gelu(&self) -> Self {
882        Self {
883            inner: self.inner.gelu(),
884        }
885    }
886
887    /// Element-wise binary power `pow(self, other)` (real exponent).
888    /// Both operands must share dtype + shape. Distinct from
889    /// [`Self::powi`] (scalar `i32` exponent). Differentiable.
890    /// **Returns `Result`**: dtype/shape mismatch surfaces as a
891    /// typed error.
892    pub fn pow(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
893        Ok(Self {
894            inner: self.inner.pow(&other.inner)?,
895        })
896    }
897
898    /// Element-wise reciprocal square root (`1 / sqrt(x)`). Same
899    /// dtype as input. One op rather than `sqrt(x).recip()` — saves
900    /// a kernel launch and matches the RMSNorm shape. Differentiable.
901    pub fn rsqrt(&self) -> Self {
902        Self {
903            inner: self.inner.rsqrt(),
904        }
905    }
906
907    /// Element-wise remainder, **PyTorch convention**:
908    /// `a - floor(a/b) * b` (sign of result matches divisor; matches
909    /// `torch.remainder`, not C99 fmod). Differentiable through `a`
910    /// and `b`. **Returns `Result`**: dtype/shape mismatch surfaces
911    /// as a typed error.
912    pub fn rem(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
913        Ok(Self {
914            inner: self.inner.rem(&other.inner)?,
915        })
916    }
917
918    /// Reverse element order along `dim`. Materializing op (real
919    /// byte shuffle). Differentiable; backward is itself.
920    /// Accepts any [`Dim`] (`usize`, `D::Minus1`, etc.).
921    pub fn flip<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
922        let shape = self.inner.shape();
923        let dim = dim.to_index(&shape, "flip")?;
924        Ok(Self {
925            inner: self.inner.flip(dim)?,
926        })
927    }
928
929    /// Cyclic shift along `dim` by `shift` positions (positive →
930    /// higher indices, wrapping). Differentiable; backward is
931    /// `roll(dim, -shift)`.
932    pub fn roll<D: Dim>(&self, dim: D, shift: i64) -> std::result::Result<Self, fuel_ir::Error> {
933        let shape = self.inner.shape();
934        let dim = dim.to_index(&shape, "roll")?;
935        Ok(Self {
936            inner: self.inner.roll(dim, shift)?,
937        })
938    }
939
940    /// Running cumulative sum along `dim`. Same shape as input.
941    /// Differentiable; backward is reverse cumsum (`flip → cumsum
942    /// → flip`).
943    pub fn cumsum<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
944        let shape = self.inner.shape();
945        let dim = dim.to_index(&shape, "cumsum")?;
946        Ok(Self {
947            inner: self.inner.cumsum(dim)?,
948        })
949    }
950
951    /// Multi-dim Pad: `padding[i] = (before, after)` for axis `i`,
952    /// length must equal tensor rank. Output shape:
953    /// `out[i] = in[i] + padding[i].0 + padding[i].1`. Only Constant
954    /// mode is implemented; Reflect / Replicate exist as enum stubs
955    /// that error at realize time. Differentiable for Constant.
956    /// **Returns `Result`**: rank mismatch surfaces as a typed error.
957    pub fn pad(
958        &self,
959        padding: Vec<(usize, usize)>,
960        mode: fuel_graph::PadMode,
961        value: f64,
962    ) -> std::result::Result<Self, fuel_ir::Error> {
963        Ok(Self {
964            inner: self.inner.pad(padding, mode, value)?,
965        })
966    }
967
968    /// Element-wise integer power (`x.powi(n)`).
969    pub fn powi(&self, n: i32) -> Self {
970        Self {
971            inner: self.inner.powi(n),
972        }
973    }
974
975    // ---- linear algebra & shape ----
976
977    /// N-D batched matrix multiply with automatic rank-2 broadcasting.
978    /// Shape incompatibility (rank < 2 or contracting-dim mismatch)
979    /// surfaces as a typed error at build time.
980    pub fn matmul(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
981        let a_dims = self.inner.shape().dims().to_vec();
982        let b_dims = other.inner.shape().dims().to_vec();
983        if a_dims.len() < 2 || b_dims.len() < 2 {
984            return Err(fuel_ir::Error::Msg(format!(
985                "matmul: both operands must be rank >= 2, got lhs={a_dims:?} rhs={b_dims:?}",
986            ))
987            .bt());
988        }
989        let a_k = a_dims[a_dims.len() - 1];
990        let b_k = b_dims[b_dims.len() - 2];
991        if a_k != b_k {
992            return Err(fuel_ir::Error::Msg(format!(
993                "matmul: contracting dim mismatch lhs[..., M, {a_k}] vs rhs[..., {b_k}, N]",
994            ))
995            .bt());
996        }
997        Ok(Self {
998            inner: self.inner.matmul(&other.inner),
999        })
1000    }
1001
1002    /// Data-determined-M matmul (sparse-MoE / capacity-buffer): like
1003    /// [`Self::matmul`], but computes only `row_count` rows of the
1004    /// `self.shape[-2]`-row capacity buffer, the rest left zeroed.
1005    /// `row_count` is a [`fuel_ir::DynScalar`] resolved at compile if
1006    /// input-determined, else at execute from the producer-bound `SymEnv`
1007    /// (e.g. `Op::NonZeroIndices`'s per-expert count). F32-only today.
1008    pub fn matmul_dyn_m(
1009        &self,
1010        other: &Self,
1011        row_count: fuel_ir::DynScalar,
1012    ) -> std::result::Result<Self, fuel_ir::Error> {
1013        let a_dims = self.inner.shape().dims().to_vec();
1014        let b_dims = other.inner.shape().dims().to_vec();
1015        if a_dims.len() < 2 || b_dims.len() < 2 {
1016            return Err(fuel_ir::Error::Msg(format!(
1017                "matmul_dyn_m: both operands must be rank >= 2, got lhs={a_dims:?} rhs={b_dims:?}",
1018            ))
1019            .bt());
1020        }
1021        let a_k = a_dims[a_dims.len() - 1];
1022        let b_k = b_dims[b_dims.len() - 2];
1023        if a_k != b_k {
1024            return Err(fuel_ir::Error::Msg(format!(
1025                "matmul_dyn_m: contracting dim mismatch lhs[..., M, {a_k}] vs rhs[..., {b_k}, N]",
1026            ))
1027            .bt());
1028        }
1029        Ok(Self {
1030            inner: self.inner.matmul_dyn_m(&other.inner, row_count),
1031        })
1032    }
1033
1034    /// Quantized matmul: `C = self @ dequant(W_Q)`. See
1035    /// [`fuel_graph::NodeHandle::qmatmul`] for details. The weight bytes
1036    /// tensor must be a flat U32 const holding the raw Q-block byte
1037    /// stream (length = n_bytes / 4).
1038    ///
1039    /// Dtype / rank / k / block-alignment / byte-count mismatches
1040    /// surface as typed errors at build time rather than panicking
1041    /// inside the inner `fuel_graph` call.
1042    pub fn qmatmul(
1043        &self,
1044        weight_bytes: &Self,
1045        quant_type: fuel_graph::QuantType,
1046        k: usize,
1047        n: usize,
1048    ) -> std::result::Result<Self, fuel_ir::Error> {
1049        if self.inner.dtype() != fuel_ir::DType::F32 {
1050            return Err(fuel_ir::Error::Msg(format!(
1051                "qmatmul: activations must be F32, got {:?}",
1052                self.inner.dtype(),
1053            ))
1054            .bt());
1055        }
1056        if weight_bytes.inner.dtype() != fuel_ir::DType::U32 {
1057            return Err(fuel_ir::Error::Msg(format!(
1058                "qmatmul: weight_bytes must be U32 (raw block bytes reinterpreted), got {:?}",
1059                weight_bytes.inner.dtype(),
1060            ))
1061            .bt());
1062        }
1063        let a_shape = self.inner.shape();
1064        let a_dims = a_shape.dims();
1065        if a_dims.len() < 2 {
1066            return Err(fuel_ir::Error::Msg(format!(
1067                "qmatmul: activations must be rank >= 2, got {a_dims:?}",
1068            ))
1069            .bt());
1070        }
1071        if a_dims[a_dims.len() - 1] != k {
1072            return Err(fuel_ir::Error::Msg(format!(
1073                "qmatmul: last dim of activations ({}) must equal k ({k})",
1074                a_dims[a_dims.len() - 1],
1075            ))
1076            .bt());
1077        }
1078        let block_size = quant_type.elements_per_block();
1079        if !k.is_multiple_of(block_size) {
1080            return Err(fuel_ir::Error::Msg(format!(
1081                "qmatmul: k={k} must be a multiple of {quant_type:?}'s block size ({block_size})",
1082            ))
1083            .bt());
1084        }
1085        let expected_bytes = n * (k / block_size) * quant_type.bytes_per_block();
1086        let expected_u32_elems = expected_bytes / 4;
1087        let actual_elems = weight_bytes.inner.shape().elem_count();
1088        if actual_elems != expected_u32_elems {
1089            return Err(fuel_ir::Error::Msg(format!(
1090                "qmatmul: weight_bytes has {actual_elems} u32 elements, expected {expected_u32_elems} for N={n}, K={k}, {quant_type:?}",
1091            )).bt());
1092        }
1093        Ok(Self {
1094            inner: self.inner.qmatmul(&weight_bytes.inner, quant_type, k, n),
1095        })
1096    }
1097
1098    /// Transpose the last two dims. Returns a typed error on rank < 2
1099    /// rather than panicking — build-time validation surfaces a useful
1100    /// diagnostic.
1101    pub fn transpose(&self) -> std::result::Result<Self, fuel_ir::Error> {
1102        Ok(Self {
1103            inner: self.inner.try_transpose()?,
1104        })
1105    }
1106
1107    /// Permute axes by the given ordering. Accepts any [`Dims`]
1108    /// implementer — `(0, 2, 1)`, `[0, 2, 1]`, `&[0, 2, 1]`, etc.
1109    /// Validates rank match + dim bounds + duplicate check at build time.
1110    pub fn permute<D: Dims>(&self, axes: D) -> std::result::Result<Self, fuel_ir::Error> {
1111        let shape = self.inner.shape();
1112        let axes = axes.to_indexes(&shape, "permute")?;
1113        Ok(Self {
1114            inner: self.inner.try_permute(&axes)?,
1115        })
1116    }
1117
1118    /// Reshape to a new shape with matching element count.
1119    /// Element-count mismatch surfaces as a typed error at build time.
1120    pub fn reshape(&self, shape: impl Into<Shape>) -> std::result::Result<Self, fuel_ir::Error> {
1121        Ok(Self {
1122            inner: self.inner.try_reshape(shape)?,
1123        })
1124    }
1125
1126    /// Drop the size-1 dimension at position `dim` (range `0..rank`).
1127    /// Metadata-only view; bytes shared with `self`. **Returns
1128    /// `Result`** rather than panicking — bad `dim` (out of bounds
1129    /// or `shape[dim] != 1`) surfaces as a typed error.
1130    ///
1131    /// Accepts any [`Dim`] implementer — `usize`, `D::Minus1`, `D::Minus2`,
1132    /// `D::Minus(n)`.
1133    pub fn squeeze<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1134        let shape = self.inner.shape();
1135        let dim = dim.to_index(&shape, "squeeze")?;
1136        Ok(Self {
1137            inner: self.inner.squeeze(dim)?,
1138        })
1139    }
1140
1141    /// Broadcast to a larger shape. Shape-incompatibility surfaces as a
1142    /// typed error at build time.
1143    pub fn broadcast_to(
1144        &self,
1145        shape: impl Into<Shape>,
1146    ) -> std::result::Result<Self, fuel_ir::Error> {
1147        Ok(Self {
1148            inner: self.inner.try_broadcast_to(shape)?,
1149        })
1150    }
1151
1152    /// Apply LayerNorm along the last dim with an affine
1153    /// `gain · x + bias` post-step. Both `gain` and `bias` are
1154    /// length-`hidden` vectors materialized fresh on `self`'s
1155    /// graph; they're broadcast across all leading dims of the
1156    /// output.
1157    ///
1158    /// Equivalent to the per-port `apply_layer_norm(x, ln, hidden,
1159    /// eps)` helpers that several ports inlined — promoted here so
1160    /// the call sites stop drifting.
1161    pub fn layer_norm_affine(
1162        &self,
1163        gain: std::sync::Arc<[f32]>,
1164        bias: std::sync::Arc<[f32]>,
1165        eps: f64,
1166    ) -> std::result::Result<Self, fuel_ir::Error> {
1167        let hidden = gain.len();
1168        debug_assert_eq!(
1169            bias.len(),
1170            hidden,
1171            "layer_norm_affine: gain ({}) and bias ({}) must have the same length",
1172            gain.len(),
1173            bias.len()
1174        );
1175        let normed = self.layer_norm_last_dim(eps)?;
1176        let dims_v: Vec<usize> = self.inner.shape().dims().to_vec();
1177        let mut affine_shape = vec![1_usize; dims_v.len()];
1178        affine_shape[dims_v.len() - 1] = hidden;
1179        let bc_shape = Shape::from_dims(&dims_v);
1180        let g = normed
1181            .const_f32_like(gain, Shape::from_dims(&[hidden]))?
1182            .reshape(Shape::from_dims(&affine_shape))?
1183            .broadcast_to(bc_shape.clone())?;
1184        let b = normed
1185            .const_f32_like(bias, Shape::from_dims(&[hidden]))?
1186            .reshape(Shape::from_dims(&affine_shape))?
1187            .broadcast_to(bc_shape)?;
1188        normed.mul(&g)?.add(&b)
1189    }
1190
1191    /// L2-normalize along `dim`: `x / sqrt(sum(x²) + eps)`. Output
1192    /// shape equals input shape; the normalization divisor is
1193    /// broadcast across `dim` after a keepdim reduction.
1194    ///
1195    /// Common values: `eps = 1e-12` (PyTorch default), `eps = 1e-6`
1196    /// (some retrieval pipelines), `eps = 0.0` (no epsilon — caller
1197    /// guarantees no all-zero rows).
1198    pub fn l2_normalize<D: Dim>(
1199        &self,
1200        dim: D,
1201        eps: f64,
1202    ) -> std::result::Result<Self, fuel_ir::Error> {
1203        let sq = self.sqr();
1204        let summed = sq.sum_keepdim(dim)?;
1205        let with_eps = if eps == 0.0 {
1206            summed
1207        } else {
1208            summed.add_scalar(eps)
1209        };
1210        let l2 = with_eps.sqrt();
1211        let dims_v: Vec<usize> = self.inner.shape().dims().to_vec();
1212        let l2_bc = l2.broadcast_to(Shape::from_dims(&dims_v))?;
1213        self.div(&l2_bc)
1214    }
1215
1216    /// Equivalent to `torch.repeat_interleave(x, repeats, dim)`.
1217    /// Replaces each element along `dim` with `repeats` consecutive
1218    /// copies of itself, expanding that dim by a factor of `repeats`.
1219    /// Implemented via reshape+broadcast+reshape — no new graph op.
1220    ///
1221    /// `repeats == 1` is a no-op clone. `repeats == 0` returns an
1222    /// error at build time.
1223    pub fn repeat_interleave<D: Dim>(
1224        &self,
1225        dim: D,
1226        repeats: usize,
1227    ) -> std::result::Result<Self, fuel_ir::Error> {
1228        let shape = self.inner.shape();
1229        let dim = dim.to_index(&shape, "repeat_interleave")?;
1230        if repeats == 0 {
1231            return Err(fuel_ir::Error::Msg("repeat_interleave: repeats must be ≥ 1".into()).bt());
1232        }
1233        if repeats == 1 {
1234            return Ok(self.clone());
1235        }
1236        let dims_v: Vec<usize> = shape.dims().to_vec();
1237        let mut unsq_shape = dims_v.clone();
1238        unsq_shape.insert(dim + 1, 1);
1239        let mut bc_shape = unsq_shape.clone();
1240        bc_shape[dim + 1] = repeats;
1241        let unsq = self.reshape(Shape::from_dims(&unsq_shape))?;
1242        let bc = unsq.broadcast_to(Shape::from_dims(&bc_shape))?;
1243        let mut out_shape = dims_v.clone();
1244        out_shape[dim] *= repeats;
1245        bc.reshape(Shape::from_dims(&out_shape))
1246    }
1247
1248    /// Slice (narrow) along `dim`: take elements `[start, start+len)`.
1249    /// Bad `dim` / out-of-range slice surfaces as a typed error at build
1250    /// time. Accepts any [`Dim`].
1251    pub fn slice<D: Dim>(
1252        &self,
1253        dim: D,
1254        start: usize,
1255        len: usize,
1256    ) -> std::result::Result<Self, fuel_ir::Error> {
1257        let shape = self.inner.shape();
1258        let dim = dim.to_index(&shape, "slice")?;
1259        let dim_size = shape.dims()[dim];
1260        if start.saturating_add(len) > dim_size {
1261            return Err(fuel_ir::Error::Msg(format!(
1262                "slice: start={start} + len={len} exceeds dim {dim} size {dim_size}",
1263            ))
1264            .bt());
1265        }
1266        Ok(Self {
1267            inner: self.inner.slice(dim, start, len),
1268        })
1269    }
1270
1271    /// Concatenate two tensors along `dim`. Shape mismatch or bad `dim`
1272    /// surfaces as a typed error at build time. Accepts any [`Dim`].
1273    pub fn concat<D: Dim>(
1274        &self,
1275        other: &Self,
1276        dim: D,
1277    ) -> std::result::Result<Self, fuel_ir::Error> {
1278        let shape = self.inner.shape();
1279        let dim = dim.to_index(&shape, "concat")?;
1280        let self_dims = shape.dims().to_vec();
1281        let other_dims = other.inner.shape().dims().to_vec();
1282        if self_dims.len() != other_dims.len() {
1283            return Err(fuel_ir::Error::Msg(format!(
1284                "concat: rank mismatch lhs={self_dims:?} rhs={other_dims:?}",
1285            ))
1286            .bt());
1287        }
1288        for (i, (&a, &b)) in self_dims.iter().zip(other_dims.iter()).enumerate() {
1289            if i != dim && a != b {
1290                return Err(fuel_ir::Error::Msg(format!(
1291                    "concat: dim {i} mismatch lhs={a} rhs={b} (concat dim is {dim})",
1292                ))
1293                .bt());
1294            }
1295        }
1296        Ok(Self {
1297            inner: self.inner.concat(&other.inner, dim),
1298        })
1299    }
1300
1301    /// Add a scalar to every element.
1302    pub fn add_scalar(&self, c: f64) -> Self {
1303        Self {
1304            inner: self.inner.add_scalar(c),
1305        }
1306    }
1307
1308    /// Multiply every element by a scalar.
1309    pub fn mul_scalar(&self, c: f64) -> Self {
1310        Self {
1311            inner: self.inner.mul_scalar(c),
1312        }
1313    }
1314
1315    /// Argmax along a dim, returning a U32 tensor with the reduced
1316    /// dim removed. Non-differentiable. Bad `dim` surfaces as a typed
1317    /// error at build time. Accepts any [`Dim`].
1318    pub fn argmax_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1319        let shape = self.inner.shape();
1320        let dim = dim.to_index(&shape, "argmax_dim")?;
1321        Ok(Self {
1322            inner: self.inner.argmax_dim(dim),
1323        })
1324    }
1325
1326    /// Realize as a `u32` (index) `Vec`.
1327    ///
1328    /// Routes through the [`fuel_dispatch::pipelined::PipelinedExecutor`] like [`Self::realize_f32`]
1329    /// — the legacy fuel-reference-backend executor predates U8-output
1330    /// ops (comparison masks feeding argmin/argmax) and rejects them.
1331    pub fn realize_u32(&self) -> Vec<u32> {
1332        let graph = self.inner.graph().clone();
1333        let target = self.inner.id();
1334        let device = crate::Device::cpu();
1335        crate::pipelined_bridge::realize_one_as::<u32>(&graph, target, &device)
1336            .expect("realize_u32 via PipelinedExecutor")
1337    }
1338
1339    /// Realize as raw bytes (`u8`). A BYTE VIEW: reads a `Bool` mask (0/1 per byte) or a
1340    /// `U8` tensor back to host (GAP-168(c)), and by design does NOT guard the root dtype —
1341    /// it routes through [`crate::pipelined_bridge::realize_one_bytes`] (the raw byte entry,
1342    /// GAP-327), so a `Bool` root (dtype != `U8`) is read as its bytes rather than rejected.
1343    ///
1344    /// **PANICS** on a realize failure; for a fallible byte view use
1345    /// [`crate::pipelined_bridge::realize_one_bytes`]. See [`Self::realize_f32`] for the
1346    /// documented-not-enforced rationale (GAP-186).
1347    pub fn realize_u8(&self) -> Vec<u8> {
1348        let graph = self.inner.graph().clone();
1349        let target = self.inner.id();
1350        let device = crate::Device::cpu();
1351        crate::pipelined_bridge::realize_one_bytes(&graph, target, &device)
1352            .expect("realize_u8 via PipelinedExecutor")
1353    }
1354
1355    // ---- reductions ----
1356
1357    /// Sum of all elements, producing a scalar.
1358    pub fn sum_all(&self) -> Self {
1359        Self {
1360            inner: self.inner.sum_all(),
1361        }
1362    }
1363
1364    /// Arithmetic mean of all elements, producing a scalar.
1365    pub fn mean_all(&self) -> Self {
1366        Self {
1367            inner: self.inner.mean_all(),
1368        }
1369    }
1370
1371    /// Maximum of every element, producing a scalar.
1372    pub fn max_all(&self) -> Self {
1373        Self {
1374            inner: self.inner.max_all(),
1375        }
1376    }
1377
1378    /// Minimum of every element, producing a scalar.
1379    pub fn min_all(&self) -> Self {
1380        Self {
1381            inner: self.inner.min_all(),
1382        }
1383    }
1384
1385    /// Sum along a single dimension (dim removed from output). Bad
1386    /// `dim` surfaces as a typed error at build time. Accepts any [`Dim`].
1387    pub fn sum_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1388        let shape = self.inner.shape();
1389        let dim = dim.to_index(&shape, "sum_dim")?;
1390        Ok(Self {
1391            inner: self.inner.sum_dim(dim),
1392        })
1393    }
1394
1395    /// Max along a single dimension (dim removed from output).
1396    pub fn max_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1397        let shape = self.inner.shape();
1398        let dim = dim.to_index(&shape, "max_dim")?;
1399        Ok(Self {
1400            inner: self.inner.max_dim(dim),
1401        })
1402    }
1403
1404    /// Min along a single dimension (dim removed from output).
1405    pub fn min_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1406        let shape = self.inner.shape();
1407        let dim = dim.to_index(&shape, "min_dim")?;
1408        Ok(Self {
1409            inner: self.inner.min_dim(dim),
1410        })
1411    }
1412
1413    /// Element-wise clamp to `[min, max]`.
1414    pub fn clamp(&self, min: f64, max: f64) -> Self {
1415        Self {
1416            inner: self.inner.clamp(min, max),
1417        }
1418    }
1419
1420    /// Mean along a single dimension.
1421    pub fn mean_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1422        let shape = self.inner.shape();
1423        let dim = dim.to_index(&shape, "mean_dim")?;
1424        Ok(Self {
1425            inner: self.inner.mean_dim(dim),
1426        })
1427    }
1428
1429    /// Sum-reduce to a smaller broadcast-compatible shape. Inverse of
1430    /// [`Self::broadcast_to`]; reduces over any dim where the source
1431    /// was broadcast against the target.
1432    pub fn reduce_sum_to(&self, target: impl Into<Shape>) -> Self {
1433        Self {
1434            inner: self.inner.reduce_sum_to(target),
1435        }
1436    }
1437
1438    /// Max-reduce to a smaller broadcast-compatible shape — the
1439    /// max-symmetric counterpart of [`Self::reduce_sum_to`].
1440    pub fn reduce_max_to(&self, target: impl Into<Shape>) -> Self {
1441        Self {
1442            inner: self.inner.reduce_max_to(target),
1443        }
1444    }
1445
1446    // ---- compositions ----
1447
1448    /// Softmax along the last dim. Rank-0 input surfaces as a typed
1449    /// error at build time rather than panicking inside `fuel_graph`.
1450    pub fn softmax_last_dim(&self) -> std::result::Result<Self, fuel_ir::Error> {
1451        let shape = self.inner.shape();
1452        let dims = shape.dims();
1453        if dims.is_empty() {
1454            return Err(fuel_ir::Error::Msg(
1455                "softmax_last_dim: input must be rank >= 1, got scalar".into(),
1456            )
1457            .bt());
1458        }
1459        Ok(Self {
1460            inner: self.inner.softmax_last_dim(),
1461        })
1462    }
1463
1464    /// bitsandbytes-style 4-bit NormalFloat quantized matrix
1465    /// multiply. See [`fuel_graph::NodeHandle::nf4_matmul`] for the full
1466    /// shape contract. v1 covers F32/F16/BF16 activations.
1467    pub fn nf4_matmul(&self, w_packed: &Self, absmax: &Self, block_size: usize) -> Self {
1468        Self {
1469            inner: self
1470                .inner
1471                .nf4_matmul(&w_packed.inner, &absmax.inner, block_size),
1472        }
1473    }
1474
1475    /// Mamba-2's State-Space Duality chunked scan (forward). See
1476    /// [`fuel_graph::NodeHandle::ssd_chunk_scan`] for the full shape
1477    /// contract. `chunk_size` is a GPU-parallelism granularity knob;
1478    /// the CPU kernel runs sequential regardless.
1479    pub fn ssd_chunk_scan(
1480        &self,
1481        dt: &Self,
1482        a: &Self,
1483        b: &Self,
1484        c: &Self,
1485        chunk_size: usize,
1486    ) -> Self {
1487        Self {
1488            inner: self
1489                .inner
1490                .ssd_chunk_scan(&dt.inner, &a.inner, &b.inner, &c.inner, chunk_size),
1491        }
1492    }
1493
1494    /// Mamba-1's selective state-space scan (forward). See
1495    /// [`fuel_graph::NodeHandle::selective_scan`] for the full shape
1496    /// contract. Returns just `y` — for the bundled `(y, last_state)`
1497    /// form needed by autoregressive resumption use
1498    /// [`Self::selective_scan_bundled`].
1499    pub fn selective_scan(
1500        &self,
1501        delta: &Self,
1502        a: &Self,
1503        b: &Self,
1504        c: &Self,
1505        delta_softplus: bool,
1506    ) -> Self {
1507        Self {
1508            inner: self.inner.selective_scan(
1509                &delta.inner,
1510                &a.inner,
1511                &b.inner,
1512                &c.inner,
1513                delta_softplus,
1514            ),
1515        }
1516    }
1517
1518    /// Multi-output Mamba-1 SSM scan: returns `(y, last_state)`. `y`
1519    /// matches the single-output [`Self::selective_scan`] result;
1520    /// `last_state` is the final hidden state `[batch, dim, dstate]`
1521    /// used by autoregressive callers to resume from a prefill
1522    /// snapshot. Both Tensors are `Op::View` projections of the
1523    /// same bundled producer Storage — realizing them in the same
1524    /// pass shares the bundle.
1525    pub fn selective_scan_bundled(
1526        &self,
1527        delta: &Self,
1528        a: &Self,
1529        b: &Self,
1530        c: &Self,
1531        delta_softplus: bool,
1532    ) -> std::result::Result<(Self, Self), fuel_ir::Error> {
1533        let (y, last_state) = self.inner.selective_scan_bundled(
1534            &delta.inner,
1535            &a.inner,
1536            &b.inner,
1537            &c.inner,
1538            delta_softplus,
1539        )?;
1540        Ok((Self { inner: y }, Self { inner: last_state }))
1541    }
1542
1543    /// Multi-output Mamba-2 SSD scan: returns `(y, last_state)`.
1544    /// Mirrors [`Self::selective_scan_bundled`]. `last_state` has
1545    /// shape `[batch, heads, head_dim, state_dim]`.
1546    pub fn ssd_chunk_scan_bundled(
1547        &self,
1548        dt: &Self,
1549        a: &Self,
1550        b: &Self,
1551        c: &Self,
1552        chunk_size: usize,
1553    ) -> std::result::Result<(Self, Self), fuel_ir::Error> {
1554        let (y, last_state) = self
1555            .inner
1556            .ssd_chunk_scan_bundled(&dt.inner, &a.inner, &b.inner, &c.inner, chunk_size)?;
1557        Ok((Self { inner: y }, Self { inner: last_state }))
1558    }
1559
1560    /// Data-determined nonzero-index extraction — the keystone primitive
1561    /// for **data-dependent dynamic shapes**. Returns `(indices, count)`:
1562    /// `indices` is `[capacity]` U32 (`capacity == self.elem_count()`),
1563    /// the first `count` entries being the ascending flat indices of
1564    /// `self`'s nonzero elements; `count` is `[1]` U32, the runtime
1565    /// nonzero count. Both are `Op::View` projections of one bundled
1566    /// producer. The executor also publishes `count`'s realized value into
1567    /// the per-pass `SymEnv` under `count_sym`, so downstream ops can
1568    /// consume it as a dynamic extent (the KV-cache `cached_len` pattern,
1569    /// generalized to a data-determined count). `count_sym` is allocated
1570    /// by the caller (from a [`fuel_ir::SymGen`]).
1571    pub fn nonzero_indices_bundled(
1572        &self,
1573        count_sym: fuel_ir::SymId,
1574    ) -> std::result::Result<(Self, Self), fuel_ir::Error> {
1575        let (indices, count) = self.inner.nonzero_indices_bundled(count_sym)?;
1576        Ok((Self { inner: indices }, Self { inner: count }))
1577    }
1578
1579    /// Allocate a data-determined [`fuel_ir::SymId`] not yet used by any
1580    /// producer on this tensor's graph — see
1581    /// [`fuel_graph::Graph::next_data_determined_sym`]. Lets a builder that
1582    /// emits several [`Self::nonzero_indices_bundled`] producers on one
1583    /// graph (per-expert MoE dispatch, stacked layers) claim non-colliding
1584    /// count syms without threading a [`fuel_ir::SymGen`]: each call
1585    /// reflects the producers already added.
1586    pub fn fresh_data_determined_sym(&self) -> fuel_ir::SymId {
1587        let graph = self.inner.graph();
1588        let g = graph.read().unwrap();
1589        g.next_data_determined_sym()
1590    }
1591
1592    /// Depthwise 1-D causal convolution + bias + optional fused SiLU
1593    /// — the Mamba-1 / Mamba-2 prefill convolution fusion. See
1594    /// [`fuel_graph::NodeHandle::causal_conv1d`] for the full shape
1595    /// contract (caller must left-pad x with `kernel - 1` zeros).
1596    pub fn causal_conv1d(&self, weight: &Self, bias: &Self, use_silu: bool) -> Self {
1597        Self {
1598            inner: self
1599                .inner
1600                .causal_conv1d(&weight.inner, &bias.inner, use_silu),
1601        }
1602    }
1603
1604    /// Fused softmax + cross-entropy with integer (class-index)
1605    /// targets — the standard PyTorch CE loss. See
1606    /// [`fuel_graph::NodeHandle::fused_softmax_cross_entropy`] for the full
1607    /// shape contract.
1608    pub fn fused_softmax_cross_entropy(
1609        &self,
1610        targets: &Self,
1611        reduction: fuel_graph::registry::Reduction,
1612        ignore_index: i64,
1613    ) -> Self {
1614        Self {
1615            inner: self
1616                .inner
1617                .fused_softmax_cross_entropy(&targets.inner, reduction, ignore_index),
1618        }
1619    }
1620
1621    /// LayerNorm along the last dim with the given epsilon. Rank-0
1622    /// or zero-last-dim input surfaces as a typed error at build time.
1623    pub fn layer_norm_last_dim(&self, eps: f64) -> std::result::Result<Self, fuel_ir::Error> {
1624        let shape = self.inner.shape();
1625        let dims = shape.dims();
1626        if dims.last().copied().unwrap_or(0) == 0 {
1627            return Err(fuel_ir::Error::Msg(format!(
1628                "layer_norm_last_dim: input must have non-zero last dim, got {dims:?}",
1629            ))
1630            .bt());
1631        }
1632        Ok(Self {
1633            inner: self.inner.layer_norm_last_dim(eps),
1634        })
1635    }
1636
1637    /// RmsNorm along the last dim (LLaMA's normalization). Rank-0 or
1638    /// zero-last-dim input surfaces as a typed error at build time.
1639    pub fn rms_norm_last_dim(&self, eps: f64) -> std::result::Result<Self, fuel_ir::Error> {
1640        let shape = self.inner.shape();
1641        let dims = shape.dims();
1642        if dims.last().copied().unwrap_or(0) == 0 {
1643            return Err(fuel_ir::Error::Msg(format!(
1644                "rms_norm_last_dim: input must have non-zero last dim, got {dims:?}",
1645            ))
1646            .bt());
1647        }
1648        Ok(Self {
1649            inner: self.inner.rms_norm_last_dim(eps),
1650        })
1651    }
1652
1653    /// Apply rotary position embeddings. See [`fuel_graph::NodeHandle::rope`].
1654    /// Rank < 2 surfaces as a typed error at build time.
1655    pub fn rope(&self, base: f64, start_pos: usize) -> std::result::Result<Self, fuel_ir::Error> {
1656        let shape = self.inner.shape();
1657        let dims = shape.dims();
1658        if dims.len() < 2 {
1659            return Err(fuel_ir::Error::Msg(format!(
1660                "rope: input must have rank >= 2, got {dims:?}",
1661            ))
1662            .bt());
1663        }
1664        Ok(Self {
1665            inner: self.inner.rope(base, start_pos),
1666        })
1667    }
1668
1669    /// Apply RoPE using caller-supplied `cos` and `sin` tables so they
1670    /// can be shared across many layers. See
1671    /// [`fuel_graph::NodeHandle::rope_with_tables`].
1672    ///
1673    /// Rank / dtype / table-shape mismatches surface as typed errors
1674    /// at build time rather than panicking inside `fuel_graph`.
1675    pub fn rope_with_tables(
1676        &self,
1677        cos: &Self,
1678        sin: &Self,
1679    ) -> std::result::Result<Self, fuel_ir::Error> {
1680        if self.inner.dtype() != fuel_ir::DType::F32 {
1681            return Err(fuel_ir::Error::Msg(format!(
1682                "rope: only f32 is supported today, got {:?} (cast explicitly for other dtypes)",
1683                self.inner.dtype(),
1684            ))
1685            .bt());
1686        }
1687        let in_shape = self.inner.shape();
1688        let dims = in_shape.dims();
1689        let rank = dims.len();
1690        if rank < 2 {
1691            return Err(fuel_ir::Error::Msg(format!(
1692                "rope: input must have rank >= 2, got {dims:?}",
1693            ))
1694            .bt());
1695        }
1696        let seq = dims[rank - 2];
1697        let d = dims[rank - 1];
1698        if !d.is_multiple_of(2) {
1699            return Err(fuel_ir::Error::Msg(format!("rope: feature dim {d} must be even",)).bt());
1700        }
1701        let cos_shape = cos.inner.shape();
1702        let cos_dims = cos_shape.dims();
1703        if cos_dims != [seq, d] {
1704            return Err(fuel_ir::Error::Msg(format!(
1705                "rope_with_tables: cos shape {cos_dims:?} does not match [seq, d] = [{seq}, {d}]",
1706            ))
1707            .bt());
1708        }
1709        let sin_shape = sin.inner.shape();
1710        let sin_dims = sin_shape.dims();
1711        if sin_dims != [seq, d] {
1712            return Err(fuel_ir::Error::Msg(format!(
1713                "rope_with_tables: sin shape {sin_dims:?} does not match [seq, d] = [{seq}, {d}]",
1714            ))
1715            .bt());
1716        }
1717        Ok(Self {
1718            inner: self.inner.rope_with_tables(&cos.inner, &sin.inner),
1719        })
1720    }
1721
1722    /// Apply RoPE using caller-supplied `cos`/`sin` tables, emitting the
1723    /// PRIMITIVE decomposition (slice + neg + concat + mul + add — the
1724    /// rotate-half recipe) instead of a single fused `FusedOps::ROPE` node.
1725    /// See [`fuel_graph::NodeHandle::rope_with_tables_decomposed`].
1726    ///
1727    /// CapturedRun 4b-resume: Fuel's `FusedOps::ROPE` is rotate-half (Llama/HF)
1728    /// but has NO correct (rotate-half) CUDA kernel — baracuda's is interleaved
1729    /// (see the rope-convention note). The fused op therefore places on CPU,
1730    /// which breaks CUDA-graph capture. Emitting the decomposition makes every
1731    /// step a primitive with a capture-safe CUDA kernel, so the decode runs
1732    /// entirely on CUDA. Forward-compatible: once a rotate-half fused CUDA rope
1733    /// kernel exists, the optimizer's re-fusion pattern will fold this subgraph
1734    /// back into the fused op automatically (recipe principle).
1735    ///
1736    /// Byte-identical semantics to [`Self::rope_with_tables`] (same rotate-half
1737    /// math); same validation.
1738    pub fn rope_with_tables_decomposed(
1739        &self,
1740        cos: &Self,
1741        sin: &Self,
1742    ) -> std::result::Result<Self, fuel_ir::Error> {
1743        if self.inner.dtype() != fuel_ir::DType::F32 {
1744            return Err(fuel_ir::Error::Msg(format!(
1745                "rope: only f32 is supported today, got {:?} (cast explicitly for other dtypes)",
1746                self.inner.dtype(),
1747            ))
1748            .bt());
1749        }
1750        let in_shape = self.inner.shape();
1751        let dims = in_shape.dims();
1752        let rank = dims.len();
1753        if rank < 2 {
1754            return Err(fuel_ir::Error::Msg(format!(
1755                "rope: input must have rank >= 2, got {dims:?}",
1756            ))
1757            .bt());
1758        }
1759        let seq = dims[rank - 2];
1760        let d = dims[rank - 1];
1761        if !d.is_multiple_of(2) {
1762            return Err(fuel_ir::Error::Msg(format!("rope: feature dim {d} must be even",)).bt());
1763        }
1764        let cos_dims = cos.inner.shape();
1765        if cos_dims.dims() != [seq, d] {
1766            return Err(fuel_ir::Error::Msg(format!(
1767                "rope_with_tables_decomposed: cos shape {:?} does not match [seq, d] = [{seq}, {d}]",
1768                cos_dims.dims(),
1769            )).bt());
1770        }
1771        let sin_dims = sin.inner.shape();
1772        if sin_dims.dims() != [seq, d] {
1773            return Err(fuel_ir::Error::Msg(format!(
1774                "rope_with_tables_decomposed: sin shape {:?} does not match [seq, d] = [{seq}, {d}]",
1775                sin_dims.dims(),
1776            )).bt());
1777        }
1778        Ok(Self {
1779            inner: self
1780                .inner
1781                .rope_with_tables_decomposed(&cos.inner, &sin.inner),
1782        })
1783    }
1784
1785    // ---- indexing ----
1786
1787    /// Pick slices along `dim` using a 1-D U32 index tensor. Accepts
1788    /// any [`Dim`]. Dim bounds / index dtype / index rank mismatches
1789    /// surface as typed errors at build time.
1790    pub fn index_select<D: Dim>(
1791        &self,
1792        dim: D,
1793        indices: &Self,
1794    ) -> std::result::Result<Self, fuel_ir::Error> {
1795        let shape = self.inner.shape();
1796        let dim = dim.to_index(&shape, "index_select")?;
1797        if indices.inner.dtype() != fuel_ir::DType::U32 {
1798            return Err(fuel_ir::Error::Msg(format!(
1799                "index_select: index tensor must be U32, got {:?}",
1800                indices.inner.dtype(),
1801            ))
1802            .bt());
1803        }
1804        let idx_shape = indices.inner.shape();
1805        let idx_dims = idx_shape.dims();
1806        if idx_dims.len() != 1 {
1807            return Err(fuel_ir::Error::Msg(format!(
1808                "index_select: index tensor must be rank 1, got {idx_dims:?}",
1809            ))
1810            .bt());
1811        }
1812        Ok(Self {
1813            inner: self.inner.index_select(dim, &indices.inner),
1814        })
1815    }
1816
1817    /// N-D gather along `dim` using a U32 index tensor with the same
1818    /// rank as `self`; output shape equals the index shape. Accepts
1819    /// any [`Dim`]. Dim bounds / index dtype / rank mismatches surface
1820    /// as typed errors at build time.
1821    pub fn gather<D: Dim>(
1822        &self,
1823        dim: D,
1824        indices: &Self,
1825    ) -> std::result::Result<Self, fuel_ir::Error> {
1826        let shape = self.inner.shape();
1827        let dim = dim.to_index(&shape, "gather")?;
1828        if indices.inner.dtype() != fuel_ir::DType::U32 {
1829            return Err(fuel_ir::Error::Msg(format!(
1830                "gather: index tensor must be U32, got {:?}",
1831                indices.inner.dtype(),
1832            ))
1833            .bt());
1834        }
1835        let data_rank = shape.dims().len();
1836        let idx_shape = indices.inner.shape();
1837        let idx_rank = idx_shape.dims().len();
1838        if data_rank != idx_rank {
1839            return Err(fuel_ir::Error::Msg(format!(
1840                "gather: data and index must have the same rank, got {data_rank} vs {idx_rank}",
1841            ))
1842            .bt());
1843        }
1844        Ok(Self {
1845            inner: self.inner.gather(dim, &indices.inner),
1846        })
1847    }
1848
1849    // ---- dtype ----
1850
1851    /// Convert to a different dtype. Same-dtype is a fast-path no-op
1852    /// (returns a clone) rather than emitting a redundant graph node.
1853    ///
1854    /// The name follows the eager-API convention: users shouldn't need
1855    /// to care whether the underlying bytes are reinterpreted (e.g.
1856    /// integer widening) or transcoded (e.g. f32 → bf16). Build-time
1857    /// validation is currently minimal — Cast itself is unfailing in
1858    /// `fuel_graph`; the Result return is reserved for future
1859    /// kernel-registry checks (Phase A.8c-extension).
1860    pub fn to_dtype(&self, dtype: DType) -> std::result::Result<Self, fuel_ir::Error> {
1861        if self.inner.dtype() == dtype {
1862            return Ok(self.clone());
1863        }
1864        Ok(Self {
1865            inner: self.inner.cast(dtype),
1866        })
1867    }
1868
1869    /// Detach this tensor from autograd. On lazy, autograd is structural
1870    /// (every graph edge participates in backward unless explicitly cut
1871    /// by a non-differentiable op), so there's no per-tensor toggle —
1872    /// `detach()` is the identity function. Provided for eager-API
1873    /// parity so consumer code that calls `.detach()` compiles
1874    /// unchanged.
1875    pub fn detach(&self) -> Self {
1876        self.clone()
1877    }
1878
1879    /// Whether autograd is tracking this tensor. On lazy, every tensor
1880    /// participates in autograd structurally; `track_op` returns true
1881    /// unconditionally for API parity with eager.
1882    pub fn track_op(&self) -> bool {
1883        true
1884    }
1885
1886    // ---- realization (the pipelined bridge) ----
1887    //
1888    // Signature note (executor-unification Session 1, re-audit gap 8):
1889    // all five typed realize entries (`realize_f32` / `_f64` / `_bf16`
1890    // / `_f16` / `_u32`) return `Vec<T>` and panic via `.expect` on
1891    // executor errors. The signatures predate the Result-returning
1892    // policy and `realize_f32` alone has 350+ in-repo call sites
1893    // across ~60 files — converting the family to `Result` is a
1894    // coordinated breaking sweep that must move all five together
1895    // (one consistent error story), so it gets its own session
1896    // rather than riding an executor-port commit. Until then the
1897    // `.expect`s stay, uniformly.
1898
1899    /// Realize this tensor as an `f32` `Vec`.
1900    ///
1901    /// Routes unconditionally through the pipelined bridge: walk the
1902    /// graph, pre-realize Consts onto CPU, plan + dispatch through
1903    /// `PipelinedExecutor`, read back the root's bytes.
1904    ///
1905    /// Judge profile data still shapes dispatch — on this same path.
1906    /// When a profile is cached ([`crate::judge::populate_dispatch_table`]
1907    /// ran this process, or a prior run persisted one for this
1908    /// hardware), [`crate::judge::cached_oracle`] feeds the picker:
1909    /// `compile_plan`'s Layer-2 cost refinement and the JudgeAware
1910    /// runtime selector both rank alternatives (portable CPU vs
1911    /// AOCL/MKL kernel-source siblings included) by measured latency.
1912    /// Executor-unification Session 3 (2026-06-11) deleted the legacy
1913    /// `judge::cached()` branch that swapped in a Router-backed
1914    /// `GraphExecutor` instead — the picker consumes the same Judge
1915    /// data without leaving the production executor.
1916    ///
1917    /// **Convenience accessor — PANICS on a realize failure** (`.expect()`).
1918    /// That is a deliberate choice for the test / example / notebook callers
1919    /// who want `.realize_f32()[0]` without error-handling clutter, NOT a trap:
1920    /// a caller that must PROPAGATE a realize failure (any serving / production
1921    /// path) has a documented fallible sibling —
1922    /// [`crate::pipelined_bridge::realize_one_as`] (with `::<f32>`) — which
1923    /// returns `Result`. GAP-186: this is DOCUMENTED, NOT ENFORCED. `realize_*`
1924    /// is a genuine public API (13 `fuel-core/tests/` integration crates plus
1925    /// `fuel-examples` call it), so it cannot be made
1926    /// test-only; the never-panic obligation lives on the production callers,
1927    /// which use the fallible sibling directly (as `train.rs::param_to_host`
1928    /// does).
1929    pub fn realize_f32(&self) -> Vec<f32> {
1930        let graph = self.inner.graph().clone();
1931        let target = self.inner.id();
1932        let device = crate::Device::cpu();
1933        crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &device)
1934            .expect("realize_f32 via PipelinedExecutor")
1935    }
1936
1937    /// Realize on CPU as an **independent correctness oracle**: like
1938    /// [`Self::realize_f32`] but with cost-based cross-device placement
1939    /// suppressed, so the whole graph runs on the CPU backend's bit-stable
1940    /// kernels and is never relocated to a GPU by the optimizer.
1941    ///
1942    /// [`Self::realize_f32`] pins CPU only as a *soft* host anchor — since the
1943    /// Step-E cost-based auto-placement, its optimizer may price model nodes
1944    /// onto a present GPU and insert an H2D `Op::Copy`, which both defeats the
1945    /// oracle's independence (it would validate a backend against itself) and,
1946    /// on a single-device realize, crashes for lack of a seeded GPU handle.
1947    /// This entry hard-pins CPU (`allow_cost_placement = false`); by the
1948    /// always-built coverage commitment the CPU backend supplies a kernel for
1949    /// every primitive op, so nothing is ever stranded. This is the
1950    /// pairwise-consensus oracle that replaces the retiring
1951    /// `fuel-reference-backend`.
1952    pub fn realize_f32_reference(&self) -> Vec<f32> {
1953        let graph = self.inner.graph().clone();
1954        let target = self.inner.id();
1955        let device = crate::Device::cpu();
1956        crate::pipelined_bridge::realize_one_reference_as::<f32>(&graph, target, &device)
1957            .expect("realize_f32_reference via PipelinedExecutor")
1958    }
1959
1960    /// Realize as an `f64` `Vec`.
1961    ///
1962    /// Routes through the [`fuel_dispatch::pipelined::PipelinedExecutor`] like
1963    /// [`Self::realize_f32`] — executor-unification Session 1
1964    /// (re-audit gap 8) retires the typed `fuel_graph_cpu` recursive
1965    /// evaluator from the public API. The root must already be
1966    /// F64-dtype (insert [`Self::to_dtype`] otherwise). The dtype guard
1967    /// lives at the single realize funnel
1968    /// [`crate::pipelined_bridge::realize_one_as`] (GAP-327): a mismatch
1969    /// returns [`fuel_ir::Error::UnexpectedDType`], which the `.expect`
1970    /// below turns into a panic here — so the byte reinterpretation never
1971    /// silently returns garbage.
1972    pub fn realize_f64(&self) -> Vec<f64> {
1973        let graph = self.inner.graph().clone();
1974        let target = self.inner.id();
1975        let device = crate::Device::cpu();
1976        crate::pipelined_bridge::realize_one_as::<f64>(&graph, target, &device)
1977            .expect("realize_f64 via PipelinedExecutor")
1978    }
1979
1980    /// Realize as a `bf16` `Vec`. See [`Self::realize_f64`] for the
1981    /// routing + dtype-guard rationale.
1982    pub fn realize_bf16(&self) -> Vec<half::bf16> {
1983        let graph = self.inner.graph().clone();
1984        let target = self.inner.id();
1985        let device = crate::Device::cpu();
1986        crate::pipelined_bridge::realize_one_as::<half::bf16>(&graph, target, &device)
1987            .expect("realize_bf16 via PipelinedExecutor")
1988    }
1989
1990    /// Realize as an `f16` `Vec`. See [`Self::realize_f64`] for the
1991    /// routing + dtype-guard rationale.
1992    pub fn realize_f16(&self) -> Vec<half::f16> {
1993        let graph = self.inner.graph().clone();
1994        let target = self.inner.id();
1995        let device = crate::Device::cpu();
1996        crate::pipelined_bridge::realize_one_as::<half::f16>(&graph, target, &device)
1997            .expect("realize_f16 via PipelinedExecutor")
1998    }
1999
2000    /// Realize on a CUDA GPU via [`PipelinedExecutor`].
2001    ///
2002    /// Phase 7.6 step 9c E.2: signature change from
2003    /// `&mut GraphExecutor<CudaBackend>` to `&CudaDevice`. The
2004    /// pipelined executor doesn't carry a const_pool — each call
2005    /// re-uploads weights. For autoregressive decoding loops where
2006    /// const_pool was load-bearing, use the persistent-StorageCache
2007    /// pattern shipped in Phase E.3 (KVCache migration).
2008    #[cfg(feature = "cuda")]
2009    pub fn realize_f32_cuda(&self, device: &fuel_cuda_backend::CudaDevice) -> Vec<f32> {
2010        let graph = self.inner.graph().clone();
2011        let target = self.inner.id();
2012        let fc_device: crate::Device = device.clone().into();
2013        crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &fc_device)
2014            .expect("realize_f32_cuda via PipelinedExecutor")
2015    }
2016
2017    // The legacy-executor-signature `realize_f32_vulkan` was deleted in
2018    // executor-unification Session 2 (2026-06-11). This bridge-based
2019    // wrapper restores Vulkan/CUDA realize parity (2026-06-15): it goes
2020    // through `pipelined_bridge::realize_one_as` on a Vulkan `Device`,
2021    // the same production path `realize_f32` / `realize_f32_cuda` use —
2022    // so it exercises the `optimize_graph` realize path on the Vulkan
2023    // backend.
2024    #[cfg(feature = "vulkan")]
2025    pub fn realize_f32_vulkan(
2026        &self,
2027        backend: &std::sync::Arc<fuel_vulkan_backend::VulkanBackend>,
2028    ) -> Vec<f32> {
2029        let graph = self.inner.graph().clone();
2030        let target = self.inner.id();
2031        let fc_device: crate::Device = backend.clone().into();
2032        crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &fc_device)
2033            .expect("realize_f32_vulkan via PipelinedExecutor")
2034    }
2035}
2036
2037/// Realize many tensors in a single CPU topo-walk. Phase 7.6 step 9c E.2.
2038pub fn realize_many_f32(tensors: &[&Tensor]) -> Vec<Vec<f32>> {
2039    if tensors.is_empty() {
2040        return Vec::new();
2041    }
2042    let graph = tensors[0].inner.graph().clone();
2043    let targets: Vec<fuel_graph::NodeId> = tensors.iter().map(|t| t.inner.id()).collect();
2044    let device = crate::Device::cpu();
2045    crate::pipelined_bridge::realize_many_as::<f32>(&graph, &targets, &device)
2046        .expect("realize_many_f32 via PipelinedExecutor")
2047}
2048
2049/// CUDA variant of realize_many_f32. Phase 7.6 step 9c E.2: signature
2050/// change from `&mut GraphExecutor<CudaBackend>` to `&CudaDevice`.
2051#[cfg(feature = "cuda")]
2052pub fn realize_many_f32_cuda(
2053    tensors: &[&Tensor],
2054    device: &fuel_cuda_backend::CudaDevice,
2055) -> Vec<Vec<f32>> {
2056    if tensors.is_empty() {
2057        return Vec::new();
2058    }
2059    let graph = tensors[0].inner.graph().clone();
2060    let targets: Vec<fuel_graph::NodeId> = tensors.iter().map(|t| t.inner.id()).collect();
2061    let fc_device: crate::Device = device.clone().into();
2062    crate::pipelined_bridge::realize_many_as::<f32>(&graph, &targets, &fc_device)
2063        .expect("realize_many_f32_cuda via PipelinedExecutor")
2064}
2065
2066#[cfg(test)]
2067mod tests {
2068    use super::*;
2069    use crate::Shape;
2070
2071    #[test]
2072    fn constructors_wrap_graph_tensor_correctly() {
2073        let t =
2074            Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2075        assert_eq!(t.dtype(), DType::F32);
2076        assert_eq!(t.shape().dims(), &[3]);
2077        assert_eq!(t.rank(), 1);
2078        assert_eq!(t.elem_count(), 3);
2079    }
2080
2081    #[test]
2082    fn add_builds_add_node_in_underlying_graph() {
2083        let a =
2084            Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2085        let b = a
2086            .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
2087            .unwrap();
2088        let c = a.add(&b).unwrap();
2089        assert_eq!(c.shape().dims(), &[3]);
2090        // All three tensors share one underlying graph (by Arc cloning
2091        // via const_f32_like / add).
2092        assert!(std::sync::Arc::ptr_eq(
2093            c.graph_tensor().graph(),
2094            a.graph_tensor().graph(),
2095        ));
2096    }
2097
2098    #[test]
2099    fn chained_lazy_method_call_builds_sensible_graph() {
2100        // Exercise a small pipeline typical of what an early LLaMA
2101        // port would write: RmsNorm → matmul → RMS-style residual.
2102        // We just verify the shapes thread through cleanly and the
2103        // final tensor is consistent.
2104        let x = Tensor::from_f32(
2105            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
2106            Shape::from_dims(&[2, 3]),
2107            &Device::cpu(),
2108        )
2109        .unwrap();
2110        let w = x
2111            .const_f32_like(
2112                vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0],
2113                Shape::from_dims(&[3, 3]),
2114            )
2115            .unwrap();
2116        let y = x
2117            .rms_norm_last_dim(1e-6)
2118            .unwrap()
2119            .matmul(&w)
2120            .unwrap()
2121            .relu();
2122        assert_eq!(y.shape().dims(), &[2, 3]);
2123        assert_eq!(y.dtype(), DType::F32);
2124    }
2125
2126    #[test]
2127    fn rope_through_lazy_wrapper() {
2128        // Verify the RoPE builder is reachable through Tensor.
2129        let x = Tensor::from_f32(
2130            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
2131            Shape::from_dims(&[2, 4]),
2132            &Device::cpu(),
2133        )
2134        .unwrap();
2135        let y = x.rope(10000.0, 0).unwrap();
2136        assert_eq!(y.shape().dims(), &[2, 4]);
2137        assert_eq!(y.dtype(), DType::F32);
2138    }
2139
2140    #[test]
2141    fn rope_delta_rotate_equals_direct_shift() {
2142        // rung-2 primitive: a K block rotated at its ORIGINAL positions then
2143        // uniformly delta-rotated by M is byte-equal to the same raw K rotated
2144        // DIRECTLY at the shifted positions. This is the exactness that lets a
2145        // cached prefix be reused at a non-zero offset. Everything is in the POOL
2146        // block layout [block_size, n_kv_heads, head_dim] that `read_block` yields.
2147        let dev = Device::cpu();
2148        let (n_kv_heads, head_dim, bs) = (2usize, 4usize, 4usize);
2149        let theta = 10000.0;
2150        let (p0, m) = (0usize, 8usize); // block at positions 0..4, shift by 8 → 8..12
2151        let raw: Vec<f32> = (0..bs * n_kv_heads * head_dim)
2152            .map(|i| (i as f32 * 0.017).sin())
2153            .collect();
2154        // Reference: rope a pool-layout [bs,Hkv,D] block DIRECTLY at `start_pos`
2155        // (heads forward → rope → back), mirroring the helper's permute so the two
2156        // paths are compared in the same layout.
2157        let rope_pool_at = |data: &[f32], start_pos: usize| -> Vec<f32> {
2158            let k = Tensor::from_f32(
2159                data.to_vec(),
2160                Shape::from_dims(&[bs, n_kv_heads, head_dim]),
2161                &dev,
2162            )
2163            .unwrap();
2164            let k4 = k
2165                .permute([1, 0, 2])
2166                .unwrap()
2167                .reshape(Shape::from_dims(&[1, n_kv_heads, bs, head_dim]))
2168                .unwrap();
2169            let (c, s) = k4.rope_tables_const(theta, start_pos, bs, head_dim);
2170            k4.rope_with_tables_decomposed(&c, &s)
2171                .unwrap()
2172                .reshape(Shape::from_dims(&[n_kv_heads, bs, head_dim]))
2173                .unwrap()
2174                .permute([1, 0, 2])
2175                .unwrap()
2176                .realize_f32()
2177        };
2178        let direct = rope_pool_at(&raw, p0 + m); // rope directly at shifted positions
2179        let at_p0 = rope_pool_at(&raw, p0); // cached, rotated at original positions
2180        let shifted =
2181            Tensor::rope_delta_rotate_block_f32(&dev, &at_p0, theta, m, bs, n_kv_heads, head_dim);
2182        let maxdiff = direct
2183            .iter()
2184            .zip(&shifted)
2185            .map(|(a, b)| (a - b).abs())
2186            .fold(0.0f32, f32::max);
2187        assert!(
2188            maxdiff < 1e-5,
2189            "delta-rotate == direct shift (maxdiff {maxdiff})"
2190        );
2191    }
2192
2193    #[test]
2194    fn cast_switches_dtype_through_wrapper() {
2195        let x =
2196            Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2197        let y = x.to_dtype(DType::F64).unwrap();
2198        assert_eq!(y.dtype(), DType::F64);
2199        assert_eq!(y.shape().dims(), &[3]);
2200    }
2201
2202    #[test]
2203    fn indexing_builds_correct_output_shape() {
2204        let data =
2205            Tensor::from_f32(vec![1.0; 12], Shape::from_dims(&[3, 4]), &Device::cpu()).unwrap();
2206        let idx = data
2207            .const_u32_like(vec![0, 2, 1], Shape::from_dims(&[3]))
2208            .unwrap();
2209        let out = data.index_select(0, &idx).unwrap();
2210        assert_eq!(out.shape().dims(), &[3, 4]);
2211    }
2212
2213    // ---- Bridge realization tests ----
2214
2215    #[test]
2216    fn realize_f32_executes_the_underlying_graph() {
2217        // The moment of truth: build a graph through Tensor and
2218        // then realize it end-to-end. (a + b) * a for a = [1, 2, 3],
2219        // b = [4, 5, 6] should yield [5, 14, 27].
2220        let a =
2221            Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2222        let b = a
2223            .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
2224            .unwrap();
2225        let c = a.add(&b).unwrap().mul(&a).unwrap();
2226        let result = c.realize_f32();
2227        assert_eq!(result, vec![5.0, 14.0, 27.0]);
2228    }
2229
2230    #[test]
2231    fn realize_f32_relu_and_maximum_propagate_nan_end_to_end() {
2232        // Pinned NaN-semantics convention (2026-07-08,
2233        // docs/architecture/10-decisions-log.md): Relu/Maximum/Minimum are
2234        // NaN-propagating (torch parity). This guards the full lazy path
2235        // (graph build -> optimize -> dispatch -> CPU kernel -> realize),
2236        // not just the scalar-kernel unit tests in fuel-cpu-backend.
2237        let a = Tensor::from_f32(
2238            vec![f32::NAN, -2.0, 3.0],
2239            Shape::from_dims(&[3]),
2240            &Device::cpu(),
2241        )
2242        .unwrap();
2243        let relu_result = a.relu().realize_f32();
2244        assert!(
2245            relu_result[0].is_nan(),
2246            "relu(NaN) must propagate NaN through realize, got {}",
2247            relu_result[0]
2248        );
2249        assert_eq!(relu_result[1], 0.0, "relu(-2) must clip to 0");
2250        assert_eq!(relu_result[2], 3.0, "relu(3) must pass through");
2251
2252        let b = a
2253            .const_f32_like(vec![1.0, f32::NAN, 2.0], Shape::from_dims(&[3]))
2254            .unwrap();
2255        let max_result = a.maximum(&b).unwrap().realize_f32();
2256        assert!(
2257            max_result[0].is_nan(),
2258            "maximum(NaN, 1) must propagate NaN through realize, got {}",
2259            max_result[0]
2260        );
2261        assert!(
2262            max_result[1].is_nan(),
2263            "maximum(-2, NaN) must propagate NaN through realize, got {}",
2264            max_result[1]
2265        );
2266        assert_eq!(max_result[2], 3.0, "maximum(3, 2) non-NaN sanity");
2267    }
2268
2269    // ===== GAP-048: KISS-Ops §6.15 resolution helpers (fmax_ieee / fmin_ieee /
2270    // rem_trunc) — realized end-to-end because fuel-graph tests only BUILD; the
2271    // VALUE divergence needs a backend. Do NOT move these "closer to the code" in
2272    // fuel-graph: there they cannot realize and become vacuous. =====
2273
2274    #[test]
2275    fn fmax_fmin_ieee_suppress_nan_where_prop_propagates() {
2276        // §6.15-0001 divergence born-red. The IEEE (suppress) and torch-parity
2277        // (propagate) halves diverge ONLY on a NaN operand — a no-NaN fixture
2278        // passes under BOTH and is vacuous, so the fixture must carry NaN.
2279        let a = Tensor::from_f32(
2280            vec![f32::NAN, 2.0, 5.0],
2281            Shape::from_dims(&[3]),
2282            &Device::cpu(),
2283        )
2284        .unwrap();
2285        let b = a
2286            .const_f32_like(vec![3.0, f32::NAN, 4.0], Shape::from_dims(&[3]))
2287            .unwrap();
2288
2289        let fmax = a.fmax_ieee(&b).unwrap().realize_f32();
2290        assert_eq!(fmax[0], 3.0, "fmax_ieee(NaN,3) suppresses NaN → 3");
2291        assert_eq!(fmax[1], 2.0, "fmax_ieee(2,NaN) suppresses NaN → 2");
2292        assert_eq!(fmax[2], 5.0, "fmax_ieee(5,4) → 5");
2293        let max = a.maximum(&b).unwrap().realize_f32();
2294        assert!(
2295            max[0].is_nan() && max[1].is_nan(),
2296            "maximum PROPAGATES NaN — the divergence that makes fmax_ieee distinct"
2297        );
2298
2299        let fmin = a.fmin_ieee(&b).unwrap().realize_f32();
2300        assert_eq!(fmin[0], 3.0, "fmin_ieee(NaN,3) suppresses NaN → 3");
2301        assert_eq!(fmin[1], 2.0, "fmin_ieee(2,NaN) suppresses NaN → 2");
2302        assert_eq!(fmin[2], 4.0, "fmin_ieee(5,4) → 4");
2303        let min = a.minimum(&b).unwrap().realize_f32();
2304        assert!(
2305            min[0].is_nan() && min[1].is_nan(),
2306            "minimum PROPAGATES NaN — the divergence that makes fmin_ieee distinct"
2307        );
2308    }
2309
2310    #[test]
2311    fn fmax_fmin_ieee_signed_zero_tie_operand_a_wins_bitwise() {
2312        // KISS Appendix A.3: on a ±0 tie (cmp_ge and cmp_le both true) operand `a`
2313        // (self) wins in all four minmax ops. ⚠️ Asserted on to_bits() — a value
2314        // compare cannot tell +0.0 from -0.0 and would pass VACUOUSLY. Fuel has
2315        // live ±0 tie-bias on CPU (#67)/Vulkan (#76) minmax paths, so if the CPU
2316        // `ge`/`where` path is tie-biased this born-red REVEALS it rather than
2317        // hiding it behind a value compare.
2318        //
2319        // ⚠️ SENSITIVITY PROVEN, not assumed (this test passed on first write, and
2320        // a first-write pass demonstrates nothing about its own sensitivity):
2321        // sabotaging the fmax_ieee decomposition `cmp_ge` → `cmp_gt` flips the tie
2322        // to b-wins and this goes RED with left=0 (+0.0) vs right=0x80000000 (-0.0).
2323        // So the a-wins result below is EARNED — the CPU ge/where path resolves the
2324        // tie a-wins, not tie-biased. Testing BOTH operand orders (each asserting
2325        // the *first* operand's zero) is the permanent discriminator: an order
2326        // swap or a collapse-to-constant flips one side; a return-`a`/return-`b`
2327        // collapse is caught by the sibling NaN test (fmax(NaN,3)=3 needs `b`,
2328        // fmax(2,NaN)=2 needs `a`). GAP-048.
2329        let a = Tensor::from_f32(vec![-0.0, 0.0], Shape::from_dims(&[2]), &Device::cpu()).unwrap();
2330        let b = a
2331            .const_f32_like(vec![0.0, -0.0], Shape::from_dims(&[2]))
2332            .unwrap();
2333
2334        let fmax = a.fmax_ieee(&b).unwrap().realize_f32();
2335        assert_eq!(
2336            fmax[0].to_bits(),
2337            (-0.0f32).to_bits(),
2338            "fmax_ieee(-0.0,+0.0): operand a wins the tie → -0.0 (bitwise)"
2339        );
2340        assert_eq!(
2341            fmax[1].to_bits(),
2342            (0.0f32).to_bits(),
2343            "fmax_ieee(+0.0,-0.0): operand a wins the tie → +0.0 (bitwise)"
2344        );
2345
2346        let fmin = a.fmin_ieee(&b).unwrap().realize_f32();
2347        assert_eq!(
2348            fmin[0].to_bits(),
2349            (-0.0f32).to_bits(),
2350            "fmin_ieee(-0.0,+0.0): operand a wins the tie → -0.0 (bitwise)"
2351        );
2352        assert_eq!(
2353            fmin[1].to_bits(),
2354            (0.0f32).to_bits(),
2355            "fmin_ieee(+0.0,-0.0): operand a wins the tie → +0.0 (bitwise)"
2356        );
2357    }
2358
2359    #[test]
2360    fn rem_trunc_diverges_from_floored_rem_on_opposite_signs() {
2361        // §6.15-0003 divergence born-red. rem_trunc (sign of the DIVIDEND, C99
2362        // fmod) vs rem (floored, sign of the divisor) diverge ONLY when the
2363        // operands have opposite signs — a same-sign fixture is vacuous.
2364        let a = Tensor::from_f32(
2365            vec![-7.0, 7.0, -7.0],
2366            Shape::from_dims(&[3]),
2367            &Device::cpu(),
2368        )
2369        .unwrap();
2370        let b = a
2371            .const_f32_like(vec![3.0, -3.0, -3.0], Shape::from_dims(&[3]))
2372            .unwrap();
2373
2374        let rt = a.rem_trunc(&b).unwrap().realize_f32();
2375        assert_eq!(rt[0], -1.0, "-7 rem_trunc 3 = -1 (sign of dividend)");
2376        assert_eq!(rt[1], 1.0, "7 rem_trunc -3 = 1 (sign of dividend)");
2377        assert_eq!(rt[2], -1.0, "-7 rem_trunc -3 = -1");
2378        let rf = a.rem(&b).unwrap().realize_f32();
2379        assert_eq!(
2380            rf[0], 2.0,
2381            "-7 rem 3 = 2 (floored, sign of divisor) — the divergence from rem_trunc"
2382        );
2383    }
2384
2385    #[test]
2386    fn trunc_edges_signed_zero_nan_inf_bitwise() {
2387        // GAP-300: trunc's expansion (q>=0 ? floor : ceil) is FUEL's own — no KISS
2388        // vector catches an error in it, so its edges are verified HERE. ⚠️ ±0 on
2389        // to_bits() (value compare 0.0 == -0.0 is vacuous).
2390        let q = Tensor::from_f32(
2391            vec![
2392                2.7,
2393                -2.7,
2394                -0.0,
2395                0.0,
2396                f32::NAN,
2397                f32::INFINITY,
2398                f32::NEG_INFINITY,
2399            ],
2400            Shape::from_dims(&[7]),
2401            &Device::cpu(),
2402        )
2403        .unwrap();
2404        let t = q.trunc().realize_f32();
2405        assert_eq!(t[0], 2.0, "trunc(2.7) = 2 (toward zero)");
2406        assert_eq!(t[1], -2.0, "trunc(-2.7) = -2 (toward zero, NOT -3)");
2407        assert_eq!(
2408            t[2].to_bits(),
2409            (-0.0f32).to_bits(),
2410            "trunc(-0.0) must PRESERVE -0.0 (bitwise, not +0.0)"
2411        );
2412        assert_eq!(
2413            t[3].to_bits(),
2414            (0.0f32).to_bits(),
2415            "trunc(+0.0) = +0.0 (bitwise)"
2416        );
2417        assert!(t[4].is_nan(), "trunc(NaN) = NaN");
2418        assert_eq!(t[5], f32::INFINITY, "trunc(+inf) = +inf");
2419        assert_eq!(t[6], f32::NEG_INFINITY, "trunc(-inf) = -inf");
2420    }
2421
2422    #[test]
2423    fn realize_f32_matmul_hand_computed() {
2424        // Classic 2x3 @ 3x2 matmul through the bridge.
2425        let a = Tensor::from_f32(
2426            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
2427            Shape::from_dims(&[2, 3]),
2428            &Device::cpu(),
2429        )
2430        .unwrap();
2431        let b = a
2432            .const_f32_like(
2433                vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0],
2434                Shape::from_dims(&[3, 2]),
2435            )
2436            .unwrap();
2437        let c = a.matmul(&b).unwrap();
2438        assert_eq!(c.shape().dims(), &[2, 2]);
2439        assert_eq!(c.realize_f32(), vec![58.0, 64.0, 139.0, 154.0]);
2440    }
2441
2442    #[test]
2443    fn fast_and_reference_agree_on_medium_matmul() {
2444        // 64 × 96 @ 96 × 32 — bigger than anything we could hand-check
2445        // but small enough to verify every element. The fast path
2446        // goes through gemm, the reference path uses the naive triple
2447        // loop; results should agree within float-rounding tolerance.
2448        let m = 64;
2449        let k = 96;
2450        let n = 32;
2451        let a_data: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.01).sin()).collect();
2452        let b_data: Vec<f32> = (0..k * n).map(|i| (i as f32 * 0.013).cos()).collect();
2453        let a = Tensor::from_f32(a_data, Shape::from_dims(&[m, k]), &Device::cpu()).unwrap();
2454        let b = a.const_f32_like(b_data, Shape::from_dims(&[k, n])).unwrap();
2455        let c = a.matmul(&b).unwrap();
2456        let fast = c.realize_f32();
2457        let reference = c.realize_f32();
2458        assert_eq!(fast.len(), reference.len());
2459        for (i, (&f, &r)) in fast.iter().zip(&reference).enumerate() {
2460            // Accept either absolute or relative tolerance — gemm's
2461            // blocked accumulation order differs from the naive triple
2462            // loop, so values near zero can have large relative diffs
2463            // on tiny absolute diffs. Both bounds are loose enough for
2464            // float-noise but tight enough to catch real bugs.
2465            let diff = (f - r).abs();
2466            let rel = if r.abs() > 1e-6 { diff / r.abs() } else { 0.0 };
2467            assert!(
2468                diff < 1e-4 || rel < 1e-3,
2469                "at index {i}: fast={f}, reference={r}, diff={diff}, rel={rel}",
2470            );
2471        }
2472    }
2473
2474    /// Reference attention (B=1, multi-head, GQA-aware) in plain Rust:
2475    /// `out = softmax( softcap(scale·QKᵀ) [+ causal -inf] ) · V`. q
2476    /// `[hq,sq,d]`, k/v `[hkv,sk,d]` (GQA: q-head `h` attends kv-head
2477    /// `h / (hq/hkv)`), out `[hq,sq,d]`.
2478    fn ref_attention(
2479        q: &[f32],
2480        k: &[f32],
2481        v: &[f32],
2482        hq: usize,
2483        hkv: usize,
2484        sq: usize,
2485        sk: usize,
2486        d: usize,
2487        scale: f32,
2488        causal: bool,
2489        softcap: Option<f32>,
2490        alibi: Option<&[f32]>,
2491    ) -> Vec<f32> {
2492        let g = hq / hkv;
2493        let mut out = vec![0.0f32; hq * sq * d];
2494        for h in 0..hq {
2495            let hk = h / g;
2496            let qh = &q[h * sq * d..(h + 1) * sq * d];
2497            let kh = &k[hk * sk * d..(hk + 1) * sk * d];
2498            let vh = &v[hk * sk * d..(hk + 1) * sk * d];
2499            for i in 0..sq {
2500                let mut scores = vec![0.0f32; sk];
2501                for j in 0..sk {
2502                    let mut s = 0.0f32;
2503                    for l in 0..d {
2504                        s += qh[i * d + l] * kh[j * d + l];
2505                    }
2506                    let mut sc = scale * s;
2507                    if let Some(cap) = softcap {
2508                        sc = cap * (sc / cap).tanh();
2509                    }
2510                    if let Some(slopes) = alibi {
2511                        // alibi bias = slope[h] · (key_pos - query_pos).
2512                        sc += slopes[h] * (j as f32 - i as f32);
2513                    }
2514                    scores[j] = if causal && j > i {
2515                        f32::NEG_INFINITY
2516                    } else {
2517                        sc
2518                    };
2519                }
2520                let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2521                let mut sum = 0.0f32;
2522                let exps: Vec<f32> = scores
2523                    .iter()
2524                    .map(|&s| {
2525                        let e = (s - m).exp();
2526                        sum += e;
2527                        e
2528                    })
2529                    .collect();
2530                for l in 0..d {
2531                    let mut o = 0.0f32;
2532                    for j in 0..sk {
2533                        o += (exps[j] / sum) * vh[j * d + l];
2534                    }
2535                    out[(h * sq + i) * d + l] = o;
2536                }
2537            }
2538        }
2539        out
2540    }
2541
2542    /// Numerical parity: lower a FlashAttn node to its primitive
2543    /// decomposition, realize it on CPU, and assert it matches the plain-Rust
2544    /// reference — verifying the recipe's *math* (scale·QKᵀ → softmax → ·V),
2545    /// the `Triu` `-inf` causal mask, GQA head-repeat, the `tanh` softcap, and
2546    /// the alibi bias (which is the sole config that lowers to `Op::Iota`).
2547    fn flash_decompose_vs_reference(
2548        hq: usize,
2549        hkv: usize,
2550        causal: bool,
2551        softcap: Option<f32>,
2552        alibi: bool,
2553    ) {
2554        let dev = Device::cpu();
2555        let (sq, sk, d) = (2usize, 2usize, 2usize);
2556        // deterministic, varied inputs so the comparison is meaningful.
2557        let q_data: Vec<f32> = (0..hq * sq * d).map(|i| (i as f32 * 0.1).sin()).collect();
2558        let k_data: Vec<f32> = (0..hkv * sk * d).map(|i| (i as f32 * 0.13).cos()).collect();
2559        let v_data: Vec<f32> = (0..hkv * sk * d).map(|i| i as f32 * 0.07 + 1.0).collect();
2560        // 4-digit test scale (~1/sqrt(2)), deliberately NOT FRAC_1_SQRT_2 (0.70710678);
2561        // swapping the exact constant would perturb these attention-scale parity tests.
2562        #[allow(clippy::approx_constant)]
2563        let scale = 0.7071f32;
2564        let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, sq, d]), &dev).unwrap();
2565        let k = q
2566            .const_f32_like(k_data.clone(), Shape::from_dims(&[1, hkv, sk, d]))
2567            .unwrap();
2568        let v = q
2569            .const_f32_like(v_data.clone(), Shape::from_dims(&[1, hkv, sk, d]))
2570            .unwrap();
2571        // Distinct positive slope per head (powers of 1/2 — the alibi default).
2572        let alibi_slopes: Option<Vec<f32>> = if alibi {
2573            Some((0..hq).map(|h| 0.5f32.powi(h as i32 + 1)).collect())
2574        } else {
2575            None
2576        };
2577        let alibi_t = alibi_slopes.as_ref().map(|s| {
2578            q.const_f32_like(s.clone(), Shape::from_dims(&[hq]))
2579                .unwrap()
2580        });
2581        let attn = q
2582            .flash_attn(&k, &v, alibi_t.as_ref(), scale, causal, None, None, softcap)
2583            .unwrap();
2584
2585        // Decompose explicitly, then realize the primitive subgraph.
2586        let graph = attn.inner.graph().clone();
2587        let id = attn.inner.id();
2588        let roots =
2589            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
2590        assert_eq!(roots.len(), 1, "lowering should keep a single root");
2591        let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
2592            .expect("realize decomposed FlashAttn on CPU");
2593
2594        let expected = ref_attention(
2595            &q_data,
2596            &k_data,
2597            &v_data,
2598            hq,
2599            hkv,
2600            sq,
2601            sk,
2602            d,
2603            scale,
2604            causal,
2605            softcap,
2606            alibi_slopes.as_deref(),
2607        );
2608        assert_eq!(got.len(), expected.len());
2609        for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
2610            assert!(
2611                (g - e).abs() < 1e-4,
2612                "FlashAttn decompose mismatch (hq={hq} hkv={hkv} causal={causal} \
2613                 softcap={softcap:?} alibi={alibi}) at {i}: got {g}, expected {e}",
2614            );
2615        }
2616    }
2617
2618    #[test]
2619    fn flash_attn_decompose_vanilla() {
2620        flash_decompose_vs_reference(1, 1, false, None, false);
2621    }
2622
2623    #[test]
2624    fn flash_attn_decompose_causal() {
2625        flash_decompose_vs_reference(1, 1, true, None, false);
2626    }
2627
2628    #[test]
2629    fn flash_attn_decompose_gqa_causal() {
2630        flash_decompose_vs_reference(2, 1, true, None, false); // Hq=2, Hkv=1 (head-repeat)
2631    }
2632
2633    #[test]
2634    fn flash_attn_decompose_softcap() {
2635        flash_decompose_vs_reference(1, 1, false, Some(0.5), false); // small cap → tanh saturates
2636    }
2637
2638    #[test]
2639    fn flash_attn_decompose_alibi() {
2640        // alibi is the only config that lowers to Op::Iota (relative-position
2641        // values) — this exercises the new 0-input Iota execution path.
2642        flash_decompose_vs_reference(2, 2, false, None, true);
2643    }
2644
2645    #[test]
2646    fn flash_attn_decompose_alibi_causal() {
2647        flash_decompose_vs_reference(2, 2, true, None, true);
2648    }
2649
2650    /// Plain-Rust reference for **capacity-K decode attention**: attend `q`
2651    /// (`Sq` queries at absolute positions `[kl−Sq, kl)`) against the first
2652    /// `kl` keys/values of a `cap`-capacity KV buffer, with bottom-right
2653    /// causal masking (query `i` attends keys `j ≤ (kl−Sq)+i`). GQA folds
2654    /// `h → h/(Hq/Hkv)`.
2655    fn ref_decode_attn(
2656        q: &[f32],
2657        k: &[f32],
2658        v: &[f32],
2659        hq: usize,
2660        hkv: usize,
2661        sq: usize,
2662        cap: usize,
2663        kl: usize,
2664        d: usize,
2665        scale: f32,
2666        causal: bool,
2667    ) -> Vec<f32> {
2668        let g = hq / hkv;
2669        let offset = kl - sq;
2670        let mut out = vec![0f32; hq * sq * d];
2671        for h in 0..hq {
2672            let hk = h / g;
2673            for i in 0..sq {
2674                let qh = &q[((h * sq) + i) * d..][..d];
2675                let mut scores = vec![f32::NEG_INFINITY; kl];
2676                for j in 0..kl {
2677                    if causal && j > offset + i {
2678                        continue;
2679                    }
2680                    let kh = &k[((hk * cap) + j) * d..][..d];
2681                    let s: f32 = (0..d).map(|l| qh[l] * kh[l]).sum();
2682                    scores[j] = scale * s;
2683                }
2684                let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2685                let mut sum = 0f32;
2686                let exps: Vec<f32> = scores
2687                    .iter()
2688                    .map(|&s| {
2689                        let e = (s - m).exp();
2690                        sum += e;
2691                        e
2692                    })
2693                    .collect();
2694                for l in 0..d {
2695                    let mut o = 0f32;
2696                    for j in 0..kl {
2697                        o += (exps[j] / sum) * v[((hk * cap) + j) * d + l];
2698                    }
2699                    out[(h * sq + i) * d + l] = o;
2700                }
2701            }
2702        }
2703        out
2704    }
2705
2706    /// Recipe principle (G2): a **concrete** `k_len` FlashAttn is a *static*
2707    /// config that used to return self (`k_len.is_some()` short-circuit). It
2708    /// now decomposes: `Slice` K/V to the live prefix and run the SDPA recipe
2709    /// bottom-right-aligned (`q_pos_offset = kl − Sq`). RED before the fix (an
2710    /// `Op::Fused(FLASH_ATTN)` island survives lowering); GREEN after, matching
2711    /// the decode-attention reference. Uses GQA (Hq=2, Hkv=1), Sq=2 (so the
2712    /// offset causal band is exercised, not a Sq=1 no-op), capacity 4, kl=3.
2713    #[test]
2714    fn flash_attn_decompose_concrete_klen() {
2715        use fuel_graph::Op;
2716        use fuel_graph::registry::FusedOps;
2717        use fuel_ir::DynScalar;
2718        let dev = Device::cpu();
2719        let (hq, hkv, sq, cap, kl, d) = (2usize, 1usize, 2usize, 4usize, 3usize, 2usize);
2720        // 4-digit test scale (~1/sqrt(2)), deliberately NOT FRAC_1_SQRT_2 (0.70710678);
2721        // swapping the exact constant would perturb these attention-scale parity tests.
2722        #[allow(clippy::approx_constant)]
2723        let scale = 0.7071f32;
2724        let causal = true;
2725        let q_data: Vec<f32> = (0..hq * sq * d).map(|i| (i as f32 * 0.1).sin()).collect();
2726        let k_data: Vec<f32> = (0..hkv * cap * d)
2727            .map(|i| (i as f32 * 0.13).cos())
2728            .collect();
2729        let v_data: Vec<f32> = (0..hkv * cap * d).map(|i| i as f32 * 0.07 + 1.0).collect();
2730        let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, sq, d]), &dev).unwrap();
2731        let k = q
2732            .inner
2733            .const_f32_like(k_data.clone(), Shape::from_dims(&[1, hkv, cap, d]))
2734            .unwrap();
2735        let v = q
2736            .inner
2737            .const_f32_like(v_data.clone(), Shape::from_dims(&[1, hkv, cap, d]))
2738            .unwrap();
2739        // Concrete k_len — the fused node that formerly returned self.
2740        let attn = q.inner.flash_attn_dyn(
2741            &k,
2742            &v,
2743            None,
2744            scale,
2745            causal,
2746            None,
2747            None,
2748            None,
2749            DynScalar::Concrete(kl),
2750        );
2751
2752        let graph = attn.graph().clone();
2753        let id = attn.id();
2754        let roots =
2755            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
2756        assert_eq!(roots.len(), 1, "lowering should keep a single root");
2757
2758        // Born-red discriminator: no Op::Fused(FLASH_ATTN) reachable from the root.
2759        {
2760            let g = graph.read().unwrap();
2761            let mut stack = vec![roots[0]];
2762            let mut seen = std::collections::HashSet::new();
2763            while let Some(nid) = stack.pop() {
2764                if !seen.insert(nid) {
2765                    continue;
2766                }
2767                let node = g.node(nid);
2768                assert!(
2769                    !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::FLASH_ATTN),
2770                    "concrete-k_len FlashAttn still fused after lowering (self-return)",
2771                );
2772                for &inp in &node.inputs {
2773                    stack.push(inp);
2774                }
2775            }
2776        }
2777
2778        let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
2779            .expect("realize decomposed concrete-k_len FlashAttn on CPU");
2780        let expected = ref_decode_attn(
2781            &q_data, &k_data, &v_data, hq, hkv, sq, cap, kl, d, scale, causal,
2782        );
2783        assert_eq!(got.len(), expected.len());
2784        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
2785            assert!(
2786                (gv - ev).abs() < 1e-4,
2787                "concrete-k_len decode mismatch at {i}: got {gv}, expected {ev}",
2788            );
2789        }
2790    }
2791
2792    /// Bit-exact comparison of two `f32` slices (via raw bit patterns), so a
2793    /// last-ULP divergence is not masked by `==` and NaNs compare by pattern.
2794    fn assert_f32_bits_eq(got: &[f32], want: &[f32], ctx: &str) {
2795        assert_eq!(got.len(), want.len(), "{ctx}: length mismatch");
2796        for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
2797            assert_eq!(
2798                g.to_bits(),
2799                w.to_bits(),
2800                "{ctx}: bit mismatch at {i}: got {g} (0x{:08x}), want {w} (0x{:08x})",
2801                g.to_bits(),
2802                w.to_bits(),
2803            );
2804        }
2805    }
2806
2807    /// Assert two positive-`f32` slices agree to within `max_ulp` ULPs. Used
2808    /// to document the (migration-orthogonal) 1-ULP gap between a primitive
2809    /// `Div` and the fused kernel's reciprocal-multiply.
2810    fn assert_f32_within_ulp(got: &[f32], want: &[f32], max_ulp: i64, ctx: &str) {
2811        assert_eq!(got.len(), want.len(), "{ctx}: length mismatch");
2812        for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
2813            let d = (g.to_bits() as i64 - w.to_bits() as i64).abs();
2814            assert!(
2815                d <= max_ulp,
2816                "{ctx}: {d} ULP at {i} (> {max_ulp}): got {g} (0x{:08x}), want {w} (0x{:08x})",
2817                g.to_bits(),
2818                w.to_bits(),
2819            );
2820        }
2821    }
2822
2823    /// Real-backend accumulation-order parity for the Increment-C-slice-1
2824    /// "shrink-via-swap" reduce spellings (prior-review nit, I3).
2825    ///
2826    /// The migration parity tests in `fuel-graph`
2827    /// (`*_matches_frozen_legacy`) run BOTH the recipe decompose and the
2828    /// frozen legacy builder through a shared toy-`f64` reference
2829    /// interpreter, so they pin recipe STRUCTURE only and — by construction
2830    /// — cannot exercise the concern that the CPU backend's `SumDim`
2831    /// (reduction chassis `reduce`) and `ReduceSumTo` (chassis `reduce_to`),
2832    /// or `MaxDim` vs `ReduceMaxTo`, might accumulate a last-dim reduction in
2833    /// a DIFFERENT order and diverge in `f32`. The recipe swapped
2834    /// `ReduceSumTo(keepdim)` → `SumDim(last)`+`Unsqueeze` (and the max
2835    /// side likewise), so the two spellings dispatch two DIFFERENT CPU
2836    /// reduce kernels.
2837    ///
2838    /// This realizes both spellings as BARE reduce roots on the ACTUAL CPU
2839    /// backend — a bare reduce cannot re-fuse into a `SoftmaxLastDim`
2840    /// (no surrounding `Div`/`Exp`), and there is no `SumDim`↔`ReduceSumTo`
2841    /// canonicalization, so the two reduce kernels genuinely execute — over
2842    /// data engineered so a left-fold and a right-fold `f32` sum DIFFER,
2843    /// which gives the bit-exact equality real teeth. Result: bit-exact,
2844    /// because both chassis fold every element into its output slot in
2845    /// row-major flat order (`reduce`: `for flat in 0..total_input`;
2846    /// `reduce_to`: `for in_flat in 0..in_elems`).
2847    #[test]
2848    fn reduce_swap_sumdim_vs_reducesumto_bit_exact_on_cpu_backend() {
2849        let dev = Device::cpu();
2850        let (rows, cols) = (2usize, 4usize);
2851        // Each row is order-sensitive in f32 (catastrophic cancellation):
2852        // its left-fold sum differs from its right-fold sum, so bit-exact
2853        // equality of the two spellings is a NON-trivial statement about the
2854        // backend's accumulation order.
2855        let data: Vec<f32> = vec![1e8, 1.0, -1e8, 1.0, 3.0e7, 2.0, -3.0e7, -1.0];
2856
2857        // Independent host references: fold each row left-to-right and
2858        // right-to-left in f32. They disagree — the data is adversarial.
2859        let mut left_fold = vec![0.0f32; rows];
2860        let mut right_fold = vec![0.0f32; rows];
2861        for r in 0..rows {
2862            let row = &data[r * cols..(r + 1) * cols];
2863            let mut acc = 0.0f32;
2864            for &v in row.iter() {
2865                acc += v;
2866            }
2867            left_fold[r] = acc;
2868            let mut acc_r = 0.0f32;
2869            for &v in row.iter().rev() {
2870                acc_r += v;
2871            }
2872            right_fold[r] = acc_r;
2873        }
2874        assert_ne!(
2875            left_fold, right_fold,
2876            "test data must be accumulation-order-sensitive so the parity has teeth",
2877        );
2878
2879        let x = Tensor::from_f32(data.clone(), Shape::from_dims(&[rows, cols]), &dev).unwrap();
2880        // Recipe spelling: rank-reducing SumDim(last) (chassis `reduce`).
2881        let sumdim = x.inner.sum_dim(1);
2882        let got_sumdim = crate::pipelined_bridge::realize_one_as::<f32>(
2883            &sumdim.graph().clone(),
2884            sumdim.id(),
2885            &dev,
2886        )
2887        .expect("realize SumDim(last) on CPU");
2888        // Legacy spelling: ReduceSumTo(keepdim [rows,1]) (chassis `reduce_to`).
2889        let reducesumto = x.inner.reduce_sum_to(Shape::from_dims(&[rows, 1]));
2890        let got_reducesumto = crate::pipelined_bridge::realize_one_as::<f32>(
2891            &reducesumto.graph().clone(),
2892            reducesumto.id(),
2893            &dev,
2894        )
2895        .expect("realize ReduceSumTo(keepdim) on CPU");
2896
2897        // The load-bearing parity: the two reduce kernels agree bit-exactly.
2898        assert_f32_bits_eq(&got_sumdim, &got_reducesumto, "SumDim vs ReduceSumTo");
2899        // Correctness + which order: both equal the row-major left-fold (and,
2900        // because the data is adversarial, NOT the right-fold).
2901        assert_f32_bits_eq(&got_sumdim, &left_fold, "SumDim vs left-fold");
2902
2903        // Max side: MaxDim(last) (chassis `reduce`) vs ReduceMaxTo(keepdim)
2904        // (chassis `reduce_to`). Max is order-independent, but the swap still
2905        // dispatches two DIFFERENT CPU kernels; confirm they agree bit-exactly
2906        // and equal the per-row maximum.
2907        let maxdim = x.inner.max_dim(1);
2908        let got_maxdim = crate::pipelined_bridge::realize_one_as::<f32>(
2909            &maxdim.graph().clone(),
2910            maxdim.id(),
2911            &dev,
2912        )
2913        .expect("realize MaxDim(last) on CPU");
2914        let reducemaxto = x.inner.reduce_max_to(Shape::from_dims(&[rows, 1]));
2915        let got_reducemaxto = crate::pipelined_bridge::realize_one_as::<f32>(
2916            &reducemaxto.graph().clone(),
2917            reducemaxto.id(),
2918            &dev,
2919        )
2920        .expect("realize ReduceMaxTo(keepdim) on CPU");
2921        let row_max: Vec<f32> = (0..rows)
2922            .map(|r| {
2923                data[r * cols..(r + 1) * cols]
2924                    .iter()
2925                    .cloned()
2926                    .fold(f32::NEG_INFINITY, f32::max)
2927            })
2928            .collect();
2929        assert_f32_bits_eq(&got_maxdim, &got_reducemaxto, "MaxDim vs ReduceMaxTo");
2930        assert_f32_bits_eq(&got_maxdim, &row_max, "MaxDim vs per-row max");
2931    }
2932
2933    /// End-to-end real-backend parity for the migrated `SoftmaxLastDim`
2934    /// (Increment C slice 1 pilot, I3). The migration swapped the keepdim
2935    /// reduce spelling `ReduceMaxTo`/`ReduceSumTo(keepdim)` →
2936    /// `MaxDim`/`SumDim(last)` + `Unsqueeze(append)` (the D3 shrink-via-swap).
2937    /// This realizes the WHOLE softmax on the ACTUAL CPU backend two ways and
2938    /// asserts the swap is numerically inert:
2939    /// * RECIPE path — `lowering_only` lowers the fused node to its recipe
2940    ///   (`SumDim`/`MaxDim` spelling) primitive subgraph, then realize it;
2941    /// * LEGACY path — the pre-migration `ReduceMaxTo`/`ReduceSumTo` primitive
2942    ///   subgraph, hand-built here with the same `Sub`/`Exp`/`Div`.
2943    ///
2944    /// The two must be BIT-EXACT: `reduce_swap_…` proves `SumDim`≡`ReduceSumTo`
2945    /// and `MaxDim`≡`ReduceMaxTo` bit-exactly on the CPU backend, and every
2946    /// other node (and the true-`Div` normalize) is identical, so the whole
2947    /// pipeline agrees to the bit. (The runtime-fusion pass does NOT re-fuse
2948    /// either primitive subgraph back to the `SoftmaxLastDim` kernel — see the
2949    /// secondary assertion, which measures the migration-ORTHOGONAL 1-ULP gap
2950    /// between the primitive `Div` and the fused kernel's reciprocal-multiply
2951    /// `e*(1/sum)`.) Adversarial mixed-magnitude rows make it non-trivial.
2952    #[test]
2953    fn softmax_last_dim_recipe_decompose_matches_legacy_on_cpu() {
2954        let dev = Device::cpu();
2955        let (rows, cols) = (3usize, 5usize);
2956        let data: Vec<f32> = (0..rows * cols)
2957            .map(|i| ((i as f32) * 0.37).sin() * 4.0 - (i as f32) * 0.11)
2958            .collect();
2959        let shape = || Shape::from_dims(&[rows, cols]);
2960        let keepdim = || Shape::from_dims(&[rows, 1]);
2961
2962        // RECIPE path: lower the fused node to the recipe (SumDim spelling).
2963        let recipe = Tensor::from_f32(data.clone(), shape(), &dev)
2964            .unwrap()
2965            .softmax_last_dim()
2966            .expect("softmax recipe build");
2967        let graph = recipe.inner.graph().clone();
2968        let id = recipe.inner.id();
2969        let roots =
2970            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
2971        assert_eq!(roots.len(), 1, "lowering keeps a single root");
2972        let recipe_out = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
2973            .expect("realize recipe-decomposed softmax on CPU");
2974
2975        // LEGACY path: the pre-migration ReduceMaxTo/ReduceSumTo(keepdim)
2976        // spelling, hand-built with identical Sub/Exp/Div.
2977        let x = Tensor::from_f32(data.clone(), shape(), &dev).unwrap().inner;
2978        let m = x.reduce_max_to(keepdim());
2979        let mb = m.broadcast_to(shape());
2980        let s = x.sub(&mb);
2981        let e = s.exp();
2982        let d = e.reduce_sum_to(keepdim());
2983        let db = d.broadcast_to(shape());
2984        let legacy = e.div(&db);
2985        let legacy_out = crate::pipelined_bridge::realize_one_as::<f32>(
2986            &legacy.graph().clone(),
2987            legacy.id(),
2988            &dev,
2989        )
2990        .expect("realize legacy softmax on CPU");
2991
2992        // PRIMARY: the migration is numerically inert — bit-exact.
2993        assert_f32_bits_eq(&recipe_out, &legacy_out, "softmax recipe vs legacy");
2994        // Sanity: it is a real softmax (each row sums to ~1).
2995        for r in 0..rows {
2996            let sum: f32 = recipe_out[r * cols..(r + 1) * cols].iter().sum();
2997            assert!((sum - 1.0).abs() < 1e-5, "row {r} softmax sum = {sum}");
2998        }
2999
3000        // SECONDARY (documentation): the fused/production kernel differs by
3001        // at most 1 ULP — NOT from the migration but from its reciprocal-
3002        // multiply normalize (`e*(1/sum)`) vs the primitive true-`Div`.
3003        let fused_out = Tensor::from_f32(data.clone(), shape(), &dev)
3004            .unwrap()
3005            .softmax_last_dim()
3006            .expect("softmax fused build")
3007            .realize_f32();
3008        assert_f32_within_ulp(&recipe_out, &fused_out, 1, "softmax recipe vs fused kernel");
3009    }
3010
3011    /// End-to-end real-backend parity for the migrated `RmsNormLastDim`
3012    /// (Increment C slice 1, T7, I3) — the norm counterpart of
3013    /// `softmax_last_dim_recipe_decompose_matches_legacy_on_cpu`. Here the D3
3014    /// shrink-via-swap replaced the keepdim `Reshape` with `Unsqueeze`
3015    /// (append) while the reducing `MeanDim(last)` is UNCHANGED, so the swap
3016    /// is metadata-only; the recipe-decompose realize must match a hand-built
3017    /// legacy `Reshape`-keepdim subgraph bit-exactly on the CPU backend.
3018    #[test]
3019    fn rms_norm_last_dim_recipe_decompose_matches_legacy_on_cpu() {
3020        let dev = Device::cpu();
3021        let (rows, cols) = (3usize, 6usize);
3022        let eps = 1e-6f64;
3023        let data: Vec<f32> = (0..rows * cols)
3024            .map(|i| ((i as f32) * 0.29).cos() * 2.5 + 0.5)
3025            .collect();
3026        let shape = || Shape::from_dims(&[rows, cols]);
3027        let keepdim = || Shape::from_dims(&[rows, 1]);
3028
3029        // RECIPE path: lower the fused node (MeanDim + Unsqueeze spelling).
3030        let recipe = Tensor::from_f32(data.clone(), shape(), &dev)
3031            .unwrap()
3032            .rms_norm_last_dim(eps)
3033            .expect("rms_norm recipe build");
3034        let graph = recipe.inner.graph().clone();
3035        let id = recipe.inner.id();
3036        let roots =
3037            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
3038        assert_eq!(roots.len(), 1, "lowering keeps a single root");
3039        let recipe_out = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
3040            .expect("realize recipe-decomposed rms_norm on CPU");
3041
3042        // LEGACY path: MeanDim(last) + Reshape-keepdim spelling, same eps and
3043        // same true-`Div` normalize.
3044        let x = Tensor::from_f32(data.clone(), shape(), &dev).unwrap().inner;
3045        let sq = x.sqr();
3046        let mean = sq.mean_dim(1);
3047        let mean_kd = mean.reshape(keepdim());
3048        let denom_sq = mean_kd.add_scalar(eps);
3049        let denom = denom_sq.sqrt();
3050        let denom_b = denom.broadcast_to(shape());
3051        let legacy = x.div(&denom_b);
3052        let legacy_out = crate::pipelined_bridge::realize_one_as::<f32>(
3053            &legacy.graph().clone(),
3054            legacy.id(),
3055            &dev,
3056        )
3057        .expect("realize legacy rms_norm on CPU");
3058
3059        // PRIMARY: the keepdim swap is metadata-only — bit-exact.
3060        assert_f32_bits_eq(&recipe_out, &legacy_out, "rms_norm recipe vs legacy");
3061
3062        // SECONDARY (documentation): the fused kernel's reciprocal-multiply
3063        // (`x*rms_inv`) differs from the primitive true-`Div` by ≤1 ULP.
3064        let fused_out = Tensor::from_f32(data.clone(), shape(), &dev)
3065            .unwrap()
3066            .rms_norm_last_dim(eps)
3067            .expect("rms_norm fused build")
3068            .realize_f32();
3069        assert_f32_within_ulp(
3070            &recipe_out,
3071            &fused_out,
3072            1,
3073            "rms_norm recipe vs fused kernel",
3074        );
3075    }
3076
3077    /// Recipe principle (G2/G3 — part 1): SelectiveScan is the constitution's
3078    /// canonical **basis gap** (a higher-order `Scan` primitive). Fuel now HAS
3079    /// that primitive (`Op::Scan`), so `decompose` lowers to it — closing G3.
3080    /// This test flips the former surfaced-gap posture: (a) NON-REGRESSION —
3081    /// the fused CPU kernel still realizes `y = 12`; (b) VERIFICATION —
3082    /// `lowering_only` leaves NO `Op::Fused(SELECTIVE_SCAN)`, an `Op::Scan`
3083    /// terminal is present, and `unroll_scan` + realize of the ys oracle
3084    /// contains `y = 12` (`h = 3`). The fused kernel remains the executed
3085    /// production path; the `Op::Scan` recipe is the optimizer base-map cover
3086    /// + the numeric verify oracle.
3087    #[test]
3088    fn selective_scan_decompose_lowers_to_scan_and_matches() {
3089        use fuel_graph::Op;
3090        use fuel_graph::registry::FusedOps;
3091        let dev = Device::cpu();
3092        let u = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1]), &dev).unwrap();
3093        let delta = u
3094            .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3095            .unwrap();
3096        let a = u
3097            .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1, 1]))
3098            .unwrap();
3099        let b = u
3100            .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1]))
3101            .unwrap();
3102        let c = u
3103            .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1]))
3104            .unwrap();
3105        let y = u.selective_scan(&delta, &a, &b, &c, /* delta_softplus */ false);
3106
3107        // (a) NON-REGRESSION: the fused kernel still runs and produces 12.0.
3108        let got = y.realize_f32();
3109        assert_eq!(got.len(), 1);
3110        assert!((got[0] - 12.0).abs() < 1e-4, "fused kernel y: {}", got[0]);
3111
3112        // (b) VERIFICATION: lowering leaves NO Op::Fused(SELECTIVE_SCAN); an
3113        // Op::Scan terminal is present; unroll+realize matches h=3,y=12.
3114        let graph = y.inner.graph().clone();
3115        let id = y.inner.id();
3116        let roots =
3117            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
3118        assert_eq!(roots.len(), 1);
3119        let scan_id = {
3120            let g = graph.read().unwrap();
3121            let mut stack = vec![roots[0]];
3122            let mut seen = std::collections::HashSet::new();
3123            let mut scan_id = None;
3124            while let Some(nid) = stack.pop() {
3125                if !seen.insert(nid) {
3126                    continue;
3127                }
3128                let node = g.node(nid);
3129                assert!(
3130                    !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::SELECTIVE_SCAN),
3131                    "SelectiveScan must lower to Op::Scan, not remain fused"
3132                );
3133                if matches!(node.op, Op::Scan { .. }) {
3134                    scan_id = Some(nid);
3135                }
3136                for &inp in &node.inputs {
3137                    stack.push(inp);
3138                }
3139            }
3140            scan_id.expect("an Op::Scan terminal must be present after lowering")
3141        };
3142        // Unroll the Op::Scan (seqlen = 1) and realize the ys oracle.
3143        let ys = {
3144            let mut g = graph.write().unwrap();
3145            fuel_graph::scan::unroll_scan(&mut g, scan_id, 1)
3146                .expect("unroll")
3147                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
3148        };
3149        let oracle = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
3150            .expect("realize unrolled selective_scan oracle on CPU");
3151        assert!(
3152            oracle.iter().any(|&v| (v - 12.0).abs() < 1e-4),
3153            "unroll oracle must contain y = 12, got {oracle:?}"
3154        );
3155    }
3156
3157    /// Recipe principle (G2/G3 — part 2): SsdChunkScan (Mamba-2's State-Space
3158    /// Duality scan) was the constitution's twin **basis gap** to SelectiveScan.
3159    /// Fuel now HAS the higher-order `Op::Scan` primitive, so `decompose` lowers
3160    /// to it — closing G3 part 2. The SSD recurrence carries a per-head SCALAR
3161    /// gate `exp(dt·a_h)` (`a` is `[heads]`, broadcast across the whole
3162    /// `head_dim×state_dim` block), with NO softplus. This test mirrors the
3163    /// SelectiveScan gate test: (a) NON-REGRESSION — the fused CPU kernel still
3164    /// realizes `y = 12`; (b) VERIFICATION — `lowering_only` leaves NO
3165    /// `Op::Fused(SSD_CHUNK_SCAN)`, an `Op::Scan` terminal is present, and
3166    /// `unroll_scan` + realize of the ys oracle contains `y = 12`.
3167    /// Fixture (batch=heads=head_dim=state_dim=1, seqlen=chunk_size=1):
3168    /// `x=2, dt=0.5, a=-1, b=3, c=4` → `exp(0.5·-1)·0 + 0.5·3·2 = 3`, `y = 3·4 = 12`.
3169    #[test]
3170    fn ssd_chunk_scan_decompose_lowers_to_scan_and_matches() {
3171        use fuel_graph::Op;
3172        use fuel_graph::registry::FusedOps;
3173        let dev = Device::cpu();
3174        // x [batch, seqlen, heads, head_dim] = [1,1,1,1]; dt [b,s,h]=[1,1,1];
3175        // a [heads]=[1]; b/c [b,s,h,state]=[1,1,1,1].
3176        let x = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1, 1]), &dev).unwrap();
3177        let dt = x
3178            .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3179            .unwrap();
3180        let a = x
3181            .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1]))
3182            .unwrap();
3183        let b = x
3184            .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3185            .unwrap();
3186        let c = x
3187            .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3188            .unwrap();
3189        let y = x.ssd_chunk_scan(&dt, &a, &b, &c, /* chunk_size */ 1);
3190
3191        // (a) NON-REGRESSION: the fused kernel still runs and produces 12.0.
3192        let got = y.realize_f32();
3193        assert!(
3194            (got[0] - 12.0).abs() < 1e-4,
3195            "fused ssd kernel y: {}",
3196            got[0]
3197        );
3198
3199        // (b) VERIFICATION: lowering leaves NO Op::Fused(SSD_CHUNK_SCAN); an
3200        // Op::Scan terminal is present; unroll+realize matches h=3, y=12.
3201        let graph = y.inner.graph().clone();
3202        let id = y.inner.id();
3203        let roots =
3204            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
3205        let scan_id = {
3206            let g = graph.read().unwrap();
3207            let mut stack = vec![roots[0]];
3208            let mut seen = std::collections::HashSet::new();
3209            let mut scan_id = None;
3210            while let Some(nid) = stack.pop() {
3211                if !seen.insert(nid) {
3212                    continue;
3213                }
3214                let node = g.node(nid);
3215                assert!(
3216                    !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::SSD_CHUNK_SCAN),
3217                    "SsdChunkScan must lower to Op::Scan, not remain fused"
3218                );
3219                if matches!(node.op, Op::Scan { .. }) {
3220                    scan_id = Some(nid);
3221                }
3222                for &inp in &node.inputs {
3223                    stack.push(inp);
3224                }
3225            }
3226            scan_id.expect("an Op::Scan terminal must be present after lowering")
3227        };
3228        let ys = {
3229            let mut g = graph.write().unwrap();
3230            fuel_graph::scan::unroll_scan(&mut g, scan_id, 1)
3231                .expect("unroll")
3232                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
3233        };
3234        let oracle = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
3235            .expect("realize unrolled ssd_chunk_scan oracle on CPU");
3236        assert!(
3237            oracle.iter().any(|&v| (v - 12.0).abs() < 1e-4),
3238            "oracle y=12, got {oracle:?}"
3239        );
3240    }
3241
3242    // ---- Op::Scan Phase 2 (C3/C4/C5): numeric BPTT finite-difference gates ---
3243    //
3244    // Prove the lower_scans_for_backward pre-pass gradients are correct against
3245    // finite differences over the SAME unrolled graph (self-consistent, decoupled
3246    // from kernel F64-accumulate parity). These need realize, so they live here.
3247
3248    #[test]
3249    fn affine_scan_bptt_matches_finite_difference() {
3250        use fuel_graph::{Node, NodeHandle, Op, ScanEmit, ScanRole};
3251        use fuel_ir::{DType, Shape};
3252        let dev = Device::cpu();
3253        // f(init, a, b): carry_{t+1} = a*carry_t + b, carry_0 = init, bound = 3, loss = carry_3.
3254        // Closed form: carry_3 = a^3*init + b*(a^2 + a + 1). d/dinit = a^3.
3255        let build = |init_v: f32,
3256                     a_v: f32,
3257                     b_v: f32|
3258         -> (
3259            std::sync::Arc<std::sync::RwLock<fuel_graph::Graph>>,
3260            fuel_graph::NodeId,
3261            fuel_graph::NodeHandle,
3262            fuel_graph::NodeHandle,
3263            fuel_graph::NodeHandle,
3264        ) {
3265            let init =
3266                NodeHandle::from_f32(vec![init_v], Shape::from_dims(&[1]), dev.as_dyn()).unwrap();
3267            let g = init.graph().clone();
3268            let a = NodeHandle::from_existing(g.clone(), init.id())
3269                .const_f32_like(vec![a_v], Shape::from_dims(&[1]))
3270                .unwrap();
3271            let b = NodeHandle::from_existing(g.clone(), init.id())
3272                .const_f32_like(vec![b_v], Shape::from_dims(&[1]))
3273                .unwrap();
3274            let nc = {
3275                let mut gw = g.write().unwrap();
3276                let s = Shape::from_dims(&[1]);
3277                let hole = gw.push(Node {
3278                    op: Op::ScanPlaceholder {
3279                        role: ScanRole::Carry,
3280                        index: 0,
3281                    },
3282                    inputs: vec![],
3283                    shape: s.clone(),
3284                    dtype: DType::F32,
3285                });
3286                let ac = gw.push(Node {
3287                    op: Op::Mul,
3288                    inputs: vec![a.id(), hole],
3289                    shape: s.clone(),
3290                    dtype: DType::F32,
3291                });
3292                gw.push(Node {
3293                    op: Op::Add,
3294                    inputs: vec![ac, b.id()],
3295                    shape: s.clone(),
3296                    dtype: DType::F32,
3297                })
3298            };
3299            let nc_t = NodeHandle::from_existing(g.clone(), nc);
3300            let out = init
3301                .scan(
3302                    &[],
3303                    &[a.clone(), b.clone()],
3304                    &nc_t,
3305                    &nc_t,
3306                    3,
3307                    ScanEmit::Final,
3308                )
3309                .expect("scan");
3310            (g, out.id(), init, a, b)
3311        };
3312        // Forward value via unroll+realize (self-consistent oracle).
3313        let fwd = |init_v: f32, a_v: f32, b_v: f32| -> f32 {
3314            let (g, out_id, _i, _a, _b) = build(init_v, a_v, b_v);
3315            let scan_id = {
3316                let gr = g.read().unwrap();
3317                gr.node(out_id).inputs[0]
3318            };
3319            let carry = {
3320                let mut gw = g.write().unwrap();
3321                fuel_graph::scan::unroll_scan(&mut gw, scan_id, 3)
3322                    .expect("unroll")
3323                    .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
3324            }; // emit=Final -> selected=final_carry
3325            crate::pipelined_bridge::realize_one_as::<f32>(&g, carry, &dev).expect("realize")[0]
3326        };
3327        // Autograd grad w.r.t. init at (1.0, 0.5, 0.1).
3328        let (g, out_id, init, a, b) = build(1.0, 0.5, 0.1);
3329        let out = fuel_graph::NodeHandle::from_existing(g.clone(), out_id);
3330        let grads = out.backward();
3331        let realize_grad = |t: &fuel_graph::NodeHandle| {
3332            crate::pipelined_bridge::realize_one_as::<f32>(
3333                &g,
3334                grads.get(t).expect("grad").id(),
3335                &dev,
3336            )
3337            .expect("realize grad")[0]
3338        };
3339        let g_init = realize_grad(&init);
3340        let g_a = realize_grad(&a);
3341        let g_b = realize_grad(&b);
3342        // Central finite differences.
3343        let h = 1e-3f32;
3344        let fd = |dinit: f32, da: f32, db: f32| {
3345            (fwd(1.0 + dinit, 0.5 + da, 0.1 + db) - fwd(1.0 - dinit, 0.5 - da, 0.1 - db))
3346                / (2.0 * h)
3347        };
3348        assert!(
3349            (g_init - fd(h, 0.0, 0.0)).abs() < 2e-2,
3350            "d/dinit: autograd {g_init} vs FD {}",
3351            fd(h, 0.0, 0.0)
3352        );
3353        assert!(
3354            (g_a - fd(0.0, h, 0.0)).abs() < 2e-2,
3355            "d/da: autograd {g_a} vs FD {}",
3356            fd(0.0, h, 0.0)
3357        );
3358        assert!(
3359            (g_b - fd(0.0, 0.0, h)).abs() < 2e-2,
3360            "d/db: autograd {g_b} vs FD {}",
3361            fd(0.0, 0.0, h)
3362        );
3363    }
3364
3365    #[test]
3366    fn selective_scan_is_differentiable_backward_matches_fd() {
3367        let dev = Device::cpu();
3368        let fwd = |u_v: f32| -> f32 {
3369            let u = Tensor::from_f32(vec![u_v], Shape::from_dims(&[1, 1, 1]), &dev).unwrap();
3370            let delta = u
3371                .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3372                .unwrap();
3373            let a = u
3374                .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1, 1]))
3375                .unwrap();
3376            let b = u
3377                .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1]))
3378                .unwrap();
3379            let c = u
3380                .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1]))
3381                .unwrap();
3382            u.selective_scan(&delta, &a, &b, &c, false).realize_f32()[0]
3383        };
3384        // Autograd at u=2.0.
3385        let u = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1]), &dev).unwrap();
3386        let delta = u
3387            .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3388            .unwrap();
3389        let a = u
3390            .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1, 1]))
3391            .unwrap();
3392        let b = u
3393            .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1]))
3394            .unwrap();
3395        let c = u
3396            .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1]))
3397            .unwrap();
3398        let y = u.selective_scan(&delta, &a, &b, &c, false);
3399        let grads = y.inner.backward();
3400        let g_u_id = grads.get(u.graph_tensor()).expect("grad u").id();
3401        let g_u =
3402            crate::pipelined_bridge::realize_one_as::<f32>(&u.inner.graph().clone(), g_u_id, &dev)
3403                .expect("realize")[0];
3404        let h = 1e-3;
3405        let fd = (fwd(2.0 + h) - fwd(2.0 - h)) / (2.0 * h);
3406        assert!(
3407            (g_u - fd).abs() < 5e-2,
3408            "selective_scan d/du: autograd {g_u} vs FD {fd}"
3409        );
3410    }
3411
3412    #[test]
3413    fn ssd_chunk_scan_is_differentiable_backward_matches_fd() {
3414        let dev = Device::cpu();
3415        // x [batch,seqlen,heads,head_dim]=[1,1,1,1]; dt [b,s,h]=[1,1,1]; a [heads]=[1];
3416        // b/c [b,s,h,state]=[1,1,1,1]. Single step: h = dt*b*x = 1.5x, y = c*h = 6x (linear).
3417        let fwd = |x_v: f32| -> f32 {
3418            let x = Tensor::from_f32(vec![x_v], Shape::from_dims(&[1, 1, 1, 1]), &dev).unwrap();
3419            let dt = x
3420                .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3421                .unwrap();
3422            let a = x
3423                .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1]))
3424                .unwrap();
3425            let b = x
3426                .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3427                .unwrap();
3428            let c = x
3429                .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3430                .unwrap();
3431            x.ssd_chunk_scan(&dt, &a, &b, &c, 1).realize_f32()[0]
3432        };
3433        // Autograd at x=2.0.
3434        let x = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1, 1]), &dev).unwrap();
3435        let dt = x
3436            .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3437            .unwrap();
3438        let a = x
3439            .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1]))
3440            .unwrap();
3441        let b = x
3442            .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3443            .unwrap();
3444        let c = x
3445            .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3446            .unwrap();
3447        let y = x.ssd_chunk_scan(&dt, &a, &b, &c, 1);
3448        let grads = y.inner.backward();
3449        let g_x_id = grads.get(x.graph_tensor()).expect("grad x").id();
3450        let g_x =
3451            crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_x_id, &dev)
3452                .expect("realize")[0];
3453        let h = 1e-3;
3454        let fd = (fwd(2.0 + h) - fwd(2.0 - h)) / (2.0 * h);
3455        assert!(
3456            (g_x - fd).abs() < 5e-2,
3457            "ssd_chunk_scan d/dx: autograd {g_x} vs FD {fd}"
3458        );
3459    }
3460
3461    /// Autograd gradient of `act(x)` at a scalar `x`, and the central finite
3462    /// difference of the functional forward at the same point.
3463    fn act_grad_and_fd(
3464        x_v: f32,
3465        act: impl Fn(&Tensor) -> Tensor,
3466        fd_act: impl Fn(&Tensor) -> Tensor,
3467    ) -> (f32, f32) {
3468        let dev = Device::cpu();
3469        let x = Tensor::from_f32(vec![x_v], Shape::from_dims(&[1]), &dev).unwrap();
3470        let y = act(&x);
3471        let grads = y.inner.backward();
3472        let g_id = grads.get(x.graph_tensor()).expect("grad x").id();
3473        let g =
3474            crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_id, &dev)
3475                .expect("realize")[0];
3476        let fwd = |v: f32| -> f32 {
3477            let t = Tensor::from_f32(vec![v], Shape::from_dims(&[1]), &dev).unwrap();
3478            fd_act(&t).realize_f32()[0]
3479        };
3480        let h = 1e-2f32;
3481        (g, (fwd(x_v + h) - fwd(x_v - h)) / (2.0 * h))
3482    }
3483
3484    /// GeluTanh backward used to panic ("inference-only"); it must now match
3485    /// the finite difference of the forward across the curve, including the
3486    /// negative lobe and x = 0.
3487    #[test]
3488    fn gelu_tanh_backward_matches_finite_difference() {
3489        for x_v in [-3.0f32, -1.5, -0.5, 0.0, 0.3, 1.0, 2.5] {
3490            let (g, fd) = act_grad_and_fd(x_v, |x| x.gelu_tanh(), |x| x.gelu_tanh());
3491            assert!(
3492                (g - fd).abs() < 2e-3,
3493                "gelu_tanh d/dx at {x_v}: autograd {g} vs FD {fd}"
3494            );
3495        }
3496    }
3497
3498    /// Same gradient through the in-place op (its input is read pre-mutation).
3499    #[test]
3500    fn gelu_tanh_inplace_backward_matches_finite_difference() {
3501        for x_v in [-3.0f32, -1.5, -0.5, 0.0, 0.3, 1.0, 2.5] {
3502            let (g, fd) = act_grad_and_fd(x_v, |x| x.gelu_tanh_inplace(), |x| x.gelu_tanh());
3503            assert!(
3504                (g - fd).abs() < 2e-3,
3505                "gelu_tanh_inplace d/dx at {x_v}: autograd {g} vs FD {fd}"
3506            );
3507        }
3508    }
3509
3510    /// Retained sabotage sibling: the finite-difference harness must be able to
3511    /// tell GeluTanh's derivative from a wrong one. SiLU's autograd gradient is
3512    /// checked against GeluTanh's FD at a point where they differ by far more
3513    /// than the tolerance above; if this ever passes the tolerance the two
3514    /// gates above have gone vacuous.
3515    #[test]
3516    fn finite_difference_harness_rejects_a_wrong_activation_gradient() {
3517        let (g_silu, fd_gelu) = act_grad_and_fd(-1.5, |x| x.silu(), |x| x.gelu_tanh());
3518        assert!(
3519            (g_silu - fd_gelu).abs() > 2e-2,
3520            "silu grad {g_silu} too close to gelu_tanh FD {fd_gelu}: harness cannot discriminate"
3521        );
3522    }
3523
3524    /// Autograd gradient of `sum(build(x))` wrt the input `x` and its central
3525    /// finite difference, per element. `build` receives the differentiated
3526    /// input (a graph leaf) and must build every other operand on its graph.
3527    fn grad_vs_fd(
3528        x_vals: &[f32],
3529        x_shape: &[usize],
3530        build: impl Fn(&Tensor) -> Tensor,
3531    ) -> (Vec<f32>, Vec<f32>) {
3532        grad_vs_fd_impl(x_vals, x_shape, build, false)
3533    }
3534
3535    /// As [`grad_vs_fd`], for a fused op with no backend kernel: the FORWARD
3536    /// values are realized after lowering the fused node to primitives.
3537    fn grad_vs_fd_lowered(
3538        x_vals: &[f32],
3539        x_shape: &[usize],
3540        build: impl Fn(&Tensor) -> Tensor,
3541    ) -> (Vec<f32>, Vec<f32>) {
3542        grad_vs_fd_impl(x_vals, x_shape, build, true)
3543    }
3544
3545    fn grad_vs_fd_impl(
3546        x_vals: &[f32],
3547        x_shape: &[usize],
3548        build: impl Fn(&Tensor) -> Tensor,
3549        lower_forward: bool,
3550    ) -> (Vec<f32>, Vec<f32>) {
3551        let dev = Device::cpu();
3552        let x = Tensor::from_f32(x_vals.to_vec(), Shape::from_dims(x_shape), &dev).unwrap();
3553        let y = build(&x);
3554        let grads = y.inner.backward();
3555        let g_id = grads.get(x.graph_tensor()).expect("grad x").id();
3556        let g =
3557            crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_id, &dev)
3558                .expect("realize");
3559        let fwd_sum = |vals: &[f32]| -> f32 {
3560            let t = Tensor::from_f32(vals.to_vec(), Shape::from_dims(x_shape), &dev).unwrap();
3561            let y = build(&t);
3562            if lower_forward {
3563                let graph = y.inner.graph().clone();
3564                let roots = fuel_graph::opt::RuleRegistry::lowering_only()
3565                    .optimize_to_fixpoint(&graph, &[y.inner.id()]);
3566                crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
3567                    .expect("realize lowered forward")
3568                    .iter()
3569                    .sum()
3570            } else {
3571                y.realize_f32().iter().sum()
3572            }
3573        };
3574        let h = 1e-2f32;
3575        let fd = (0..x_vals.len())
3576            .map(|i| {
3577                let (mut p, mut m) = (x_vals.to_vec(), x_vals.to_vec());
3578                p[i] += h;
3579                m[i] -= h;
3580                (fwd_sum(&p) - fwd_sum(&m)) / (2.0 * h)
3581            })
3582            .collect();
3583        (g, fd)
3584    }
3585
3586    fn assert_grad_close(label: &str, g: &[f32], fd: &[f32]) {
3587        assert_eq!(g.len(), fd.len(), "{label}: length");
3588        for (i, (a, b)) in g.iter().zip(fd).enumerate() {
3589            assert!(
3590                (a - b).abs() < 5e-3 + 5e-3 * b.abs(),
3591                "{label}[{i}]: autograd {a} vs FD {b}"
3592            );
3593        }
3594    }
3595
3596    /// A fused op with no hand-written backward arm used to reach the "no
3597    /// backward arm wired yet" panic. Its total `decompose` is differentiable,
3598    /// so the backward pre-pass now lowers it first (ROADMAP item 17, H11).
3599    #[test]
3600    fn nf4_matmul_activation_gradient_matches_finite_difference() {
3601        let (g, fd) = grad_vs_fd(&[1.0, 2.0, 2.0, 4.0], &[1, 4], |act| {
3602            let w_packed = Tensor::from_graph_tensor(
3603                act.graph_tensor()
3604                    .const_u8_like(vec![247_u8, 247, 127, 127], Shape::from_dims(&[2, 2]))
3605                    .unwrap(),
3606            );
3607            let absmax = act
3608                .const_f32_like(vec![1.0_f32, 2.0, 10.0, 20.0], Shape::from_dims(&[2, 2]))
3609                .unwrap();
3610            act.nf4_matmul(&w_packed, &absmax, 2)
3611        });
3612        assert_grad_close("nf4_matmul d/dact", &g, &fd);
3613    }
3614
3615    /// AWQ: arbitrary packed words are valid data (any nibble is a code), and
3616    /// the gradient wrt the activation does not depend on the packing
3617    /// convention, so a fixed set of words exercises the decompose's
3618    /// dequantize-then-matmul path under autograd.
3619    #[test]
3620    fn awq_matmul_activation_gradient_matches_finite_difference() {
3621        let (g, fd) = grad_vs_fd_lowered(
3622            &[0.5, -1.0, 2.0, 0.25, -0.75, 1.5, 0.1, -0.3],
3623            &[1, 8],
3624            |act| {
3625                let qweight = act
3626                    .graph_tensor()
3627                    .const_u32_like(
3628                        vec![
3629                            0x1234_5678,
3630                            0x9ABC_DEF0,
3631                            0x0FED_CBA9,
3632                            0x8765_4321,
3633                            0x1357_9BDF,
3634                            0x2468_ACE0,
3635                            0xFEDC_BA98,
3636                            0x0123_4567,
3637                        ],
3638                        Shape::from_dims(&[8, 1]),
3639                    )
3640                    .unwrap();
3641                let qzeros = act
3642                    .graph_tensor()
3643                    .const_u32_like(vec![0x7777_7777], Shape::from_dims(&[1, 1]))
3644                    .unwrap();
3645                let scales = act
3646                    .const_f32_like(
3647                        vec![0.1, 0.2, 0.15, 0.05, 0.3, 0.25, 0.12, 0.08],
3648                        Shape::from_dims(&[1, 8]),
3649                    )
3650                    .unwrap();
3651                Tensor::from_graph_tensor(act.graph_tensor().awq_matmul(
3652                    &qweight,
3653                    &qzeros,
3654                    scales.graph_tensor(),
3655                    8,
3656                    8,
3657                ))
3658            },
3659        );
3660        assert_grad_close("awq_matmul d/dact", &g, &fd);
3661    }
3662
3663    #[test]
3664    fn causal_conv1d_weight_and_bias_gradients_match_finite_difference() {
3665        let x_data = vec![0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7];
3666        for use_silu in [false, true] {
3667            // d/dweight: weight is the differentiated leaf.
3668            let (g, fd) = grad_vs_fd(&[0.5, -0.25, 0.75, 0.1, 0.9, -0.4], &[2, 1, 3], |w| {
3669                let x = w
3670                    .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3671                    .unwrap();
3672                let b = w
3673                    .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3674                    .unwrap();
3675                x.causal_conv1d(w, &b, use_silu)
3676            });
3677            assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dw"), &g, &fd);
3678            // d/dbias.
3679            let (g, fd) = grad_vs_fd(&[0.05, -0.05], &[2], |b| {
3680                let x = b
3681                    .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3682                    .unwrap();
3683                let w = b
3684                    .const_f32_like(
3685                        vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3686                        Shape::from_dims(&[2, 1, 3]),
3687                    )
3688                    .unwrap();
3689                x.causal_conv1d(&w, b, use_silu)
3690            });
3691            assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dbias"), &g, &fd);
3692        }
3693    }
3694
3695    #[test]
3696    fn causal_conv1d_input_gradient_matches_finite_difference() {
3697        // x [1, 2, 4] (channels 2, seq 2, kernel 3 -> left-padded length 4).
3698        for use_silu in [false, true] {
3699            let (g, fd) = grad_vs_fd(
3700                &[0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7],
3701                &[1, 2, 4],
3702                |x| {
3703                    let w = x
3704                        .const_f32_like(
3705                            vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3706                            Shape::from_dims(&[2, 1, 3]),
3707                        )
3708                        .unwrap();
3709                    let b = x
3710                        .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3711                        .unwrap();
3712                    x.causal_conv1d(&w, &b, use_silu)
3713                },
3714            );
3715            assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dx"), &g, &fd);
3716        }
3717    }
3718
3719    // Review Fix 2: the seqlen=1 SSM gates above multiply exp(dt*a) by a zero
3720    // initial carry, so `y` is linear in the input and the multi-step recurrent
3721    // gate is never FD-checked for the SSM ops. These seqlen>=2 gates carry a
3722    // NON-ZERO h across steps (a < 0), so d(sum y)/d(early input) flows THROUGH
3723    // the gate exp(dt*a) — the multi-step BPTT the headline SSM deliverable needs.
3724
3725    #[test]
3726    fn selective_scan_seqlen2_bptt_matches_finite_difference() {
3727        let dev = Device::cpu();
3728        // u/delta [batch,seqlen,dim]=[1,2,1]; a [dim,dstate]=[1,1] (< 0, stable gate);
3729        // b/c [batch,seqlen,dstate]=[1,2,1]. loss = sum(y) (ones-seed over [1,2,1]).
3730        let fwd = |u_vals: &[f32]| -> f32 {
3731            let u = Tensor::from_f32(u_vals.to_vec(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3732            let delta = u
3733                .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3734                .unwrap();
3735            let a = u
3736                .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3737                .unwrap();
3738            let b = u
3739                .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3740                .unwrap();
3741            let c = u
3742                .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3743                .unwrap();
3744            u.selective_scan(&delta, &a, &b, &c, false)
3745                .realize_f32()
3746                .iter()
3747                .sum()
3748        };
3749        let u0 = vec![1.0f32, 2.0];
3750        let u = Tensor::from_f32(u0.clone(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3751        let delta = u
3752            .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3753            .unwrap();
3754        let a = u
3755            .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3756            .unwrap();
3757        let b = u
3758            .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3759            .unwrap();
3760        let c = u
3761            .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3762            .unwrap();
3763        let y = u.selective_scan(&delta, &a, &b, &c, false);
3764        let grads = y.inner.backward(); // ones-seed over [1,2,1] == grad of sum(y)
3765        let g_u_id = grads.get(u.graph_tensor()).expect("grad u").id();
3766        let g_u =
3767            crate::pipelined_bridge::realize_one_as::<f32>(&u.inner.graph().clone(), g_u_id, &dev)
3768                .expect("realize");
3769        // FD over u_1 (index 0): d(sum y)/d u_1 = dB + c*exp(dt*a)*dB — the second
3770        // term is the recurrent gate carrying u_1 into y_2.
3771        let h = 1e-3f32;
3772        let mut up = u0.clone();
3773        up[0] += h;
3774        let mut um = u0.clone();
3775        um[0] -= h;
3776        let fd0 = (fwd(&up) - fwd(&um)) / (2.0 * h);
3777        assert!(
3778            (g_u[0] - fd0).abs() < 5e-2,
3779            "selective_scan seqlen2 dL/du_1: autograd {} vs FD {fd0}",
3780            g_u[0]
3781        );
3782        // Non-triviality: the gate contribution makes this strictly > the pure-dB
3783        // seqlen=1 grad — a real, non-vacuous multi-step gradient (~1.19 here).
3784        assert!(
3785            g_u[0].abs() > 0.5,
3786            "recurrent-gate grad must be non-trivial, got {g_u:?}"
3787        );
3788    }
3789
3790    #[test]
3791    fn ssd_chunk_scan_seqlen4_chunk2_bptt_matches_finite_difference() {
3792        let dev = Device::cpu();
3793        // seqlen=4, chunk=2 -> a 2-CHUNK Op::Scan (bound=2): the inter-chunk state
3794        // carry passes exp(dt*a) across chunks. x [b,s,h,hd]=[1,4,1,1]; dt [b,s,h]=[1,4,1];
3795        // a [heads]=[1] (< 0); b/c [b,s,h,state]=[1,4,1,1].
3796        let fwd = |x_vals: &[f32]| -> f32 {
3797            let x =
3798                Tensor::from_f32(x_vals.to_vec(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3799            let dt = x
3800                .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3801                .unwrap();
3802            let a = x
3803                .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3804                .unwrap();
3805            let b = x
3806                .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3807                .unwrap();
3808            let c = x
3809                .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3810                .unwrap();
3811            x.ssd_chunk_scan(&dt, &a, &b, &c, 2)
3812                .realize_f32()
3813                .iter()
3814                .sum()
3815        };
3816        let x0 = vec![1.0f32, 2.0, 3.0, 4.0];
3817        let x = Tensor::from_f32(x0.clone(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3818        let dt = x
3819            .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3820            .unwrap();
3821        let a = x
3822            .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3823            .unwrap();
3824        let b = x
3825            .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3826            .unwrap();
3827        let c = x
3828            .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3829            .unwrap();
3830        let y = x.ssd_chunk_scan(&dt, &a, &b, &c, 2);
3831        let grads = y.inner.backward();
3832        let g_x_id = grads.get(x.graph_tensor()).expect("grad x").id();
3833        let g_x =
3834            crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_x_id, &dev)
3835                .expect("realize");
3836        // FD over x_0 (a chunk-0 input): its contribution to y_3/y_4 (chunk 1)
3837        // rides the cross-chunk gate exp(dt*a) — multi-step BPTT.
3838        let h = 1e-3f32;
3839        let mut xp = x0.clone();
3840        xp[0] += h;
3841        let mut xm = x0.clone();
3842        xm[0] -= h;
3843        let fd0 = (fwd(&xp) - fwd(&xm)) / (2.0 * h);
3844        assert!(
3845            (g_x[0] - fd0).abs() < 5e-2,
3846            "ssd_chunk_scan seqlen4 dL/dx_0: autograd {} vs FD {fd0}",
3847            g_x[0]
3848        );
3849        assert!(
3850            g_x[0].abs() > 0.5,
3851            "cross-chunk-gate grad must be non-trivial, got {g_x:?}"
3852        );
3853    }
3854
3855    // ---- Task 8: general-dimension SSM parity gate (C7 non-regression) ------
3856    //
3857    // The all-1s gap-test fixtures above (Task 6/7) share a single flat order
3858    // across every axis, so they cannot catch an axis transposition in the
3859    // decompose. The tests below run each SSM op at DISTINGUISHING multi-dim
3860    // sizes and assert the `unroll_scan` oracle (over the lowered `Op::Scan`)
3861    // matches the fused kernel element-wise, which is only possible if every
3862    // per-step slice / broadcast / reduce axis in the Task-6/7 decompose lines
3863    // up with the kernel's layout.
3864
3865    /// Walk the lowered graph from `root`, assert NO `Op::Fused(fused_id)`
3866    /// survives the `lowering_only()` pass, and return the `Op::Scan` terminal
3867    /// the decompose emitted (there must be exactly one on the SSM path).
3868    fn find_scan_terminal(
3869        graph: &std::sync::Arc<std::sync::RwLock<fuel_graph::Graph>>,
3870        root: fuel_graph::NodeId,
3871        fused_id: fuel_graph::registry::FusedOpId,
3872    ) -> fuel_graph::NodeId {
3873        use fuel_graph::Op;
3874        let g = graph.read().unwrap();
3875        let mut stack = vec![root];
3876        let mut seen = std::collections::HashSet::new();
3877        let mut scan_id = None;
3878        while let Some(nid) = stack.pop() {
3879            if !seen.insert(nid) {
3880                continue;
3881            }
3882            let node = g.node(nid);
3883            assert!(
3884                !matches!(node.op, Op::Fused(fid, _) if fid == fused_id),
3885                "SSM op must lower to Op::Scan, not remain fused",
3886            );
3887            if matches!(node.op, Op::Scan { .. }) {
3888                scan_id = Some(nid);
3889            }
3890            for &inp in &node.inputs {
3891                stack.push(inp);
3892            }
3893        }
3894        scan_id.expect("an Op::Scan terminal must be present after lowering")
3895    }
3896
3897    /// Reindex a flat row-major `[seqlen, batch, tail]` buffer — the layout
3898    /// `unroll_scan` stacks its per-step ys into (scan axis 0) — to
3899    /// `[batch, seqlen, tail]`, the fused kernel's `y` layout, so the two align
3900    /// element-wise. `tail` is the product of the trailing per-token dims (`dim`
3901    /// for selective_scan; `heads*head_dim` for ssd) — a contiguous block whose
3902    /// internal order is preserved by the batch/seqlen swap.
3903    ///
3904    /// This is done in host code deliberately: the executor treats `Op::Permute`
3905    /// as a metadata-only view (shared bytes + strided layout), so realizing a
3906    /// `Permute` *root* can hand back byte-shared storage still in seqlen-major
3907    /// order — an unreliable comparand. Realizing the contiguous unroll `Concat`
3908    /// (`ys`) and permuting the flat host bytes here is unambiguous.
3909    fn scan_ys_to_fused_layout(ys: &[f32], seqlen: usize, batch: usize, tail: usize) -> Vec<f32> {
3910        assert_eq!(ys.len(), seqlen * batch * tail, "ys layout mismatch");
3911        let mut out = vec![0f32; ys.len()];
3912        for s in 0..seqlen {
3913            for bb in 0..batch {
3914                for t in 0..tail {
3915                    out[(bb * seqlen + s) * tail + t] = ys[(s * batch + bb) * tail + t];
3916                }
3917            }
3918        }
3919        out
3920    }
3921
3922    /// Task 8 (part 1) — `selective_scan` general-dimension parity.
3923    ///
3924    /// **C7 non-regression:** `PipelinedExecutor::realize` dispatches
3925    /// `Op::Fused(SELECTIVE_SCAN)` **directly** to its kernel and never runs a
3926    /// `RuleRegistry` lowering pass, so `decompose`→`Op::Scan` is reached ONLY by
3927    /// the `lowering_only()` verification below — the fused kernel is the
3928    /// executed path *by construction*. The `realize_f32()` leg (a) is the proof
3929    /// the fused kernel still runs (no `O(seqlen)` unroll on the hot path).
3930    ///
3931    /// **Distinguishing dims** batch=2, seqlen=3, dim=2, dstate=2 with EVERY
3932    /// element distinct and asymmetric: `u`/`delta` index `dim`, `b`/`c` index
3933    /// `dstate`, and `a` is a full `[dim,dstate]` gate — so a dim↔dstate
3934    /// broadcast swap multiplies wrong pairs, and because `h` threads across the
3935    /// (order-sensitive) seqlen recurrence, a batch↔seqlen transpose changes the
3936    /// result. The all-1s fixture cannot see either; this can.
3937    #[test]
3938    fn selective_scan_multidim_unroll_matches_fused_kernel() {
3939        use fuel_graph::registry::FusedOps;
3940        let dev = Device::cpu();
3941        let (batch, seqlen, dim, dstate) = (2usize, 3usize, 2usize, 2usize);
3942        // [batch, seqlen, dim] — all distinct, mixed signs.
3943        let u = Tensor::from_f32(
3944            vec![
3945                0.5, -0.3, 1.0, 0.2, -0.5, 0.7, 0.25, 0.9, -0.8, 0.4, 0.6, -0.1,
3946            ],
3947            Shape::from_dims(&[batch, seqlen, dim]),
3948            &dev,
3949        )
3950        .unwrap();
3951        // [batch, seqlen, dim] — positive (delta is a rate); all distinct.
3952        let delta = u
3953            .const_f32_like(
3954                vec![
3955                    0.1, 0.2, 0.3, 0.15, 0.25, 0.4, 0.35, 0.05, 0.2, 0.3, 0.12, 0.28,
3956                ],
3957                Shape::from_dims(&[batch, seqlen, dim]),
3958            )
3959            .unwrap();
3960        // [dim, dstate] — negative (Mamba a<0 → stable gate in (0,1)); distinct.
3961        let a = u
3962            .const_f32_like(
3963                vec![-0.7, -0.3, -0.5, -0.9],
3964                Shape::from_dims(&[dim, dstate]),
3965            )
3966            .unwrap();
3967        // [batch, seqlen, dstate] — distinct.
3968        let b = u
3969            .const_f32_like(
3970                vec![
3971                    1.0, 0.5, 0.25, 0.75, 0.6, 0.2, 0.8, 0.4, 0.3, 0.9, 0.55, 0.15,
3972                ],
3973                Shape::from_dims(&[batch, seqlen, dstate]),
3974            )
3975            .unwrap();
3976        // [batch, seqlen, dstate] — distinct.
3977        let c = u
3978            .const_f32_like(
3979                vec![2.0, 1.0, 0.5, 1.5, 0.8, 0.3, 1.2, 0.6, 0.9, 0.4, 0.7, 1.1],
3980                Shape::from_dims(&[batch, seqlen, dstate]),
3981            )
3982            .unwrap();
3983        let y = u.selective_scan(&delta, &a, &b, &c, /* delta_softplus */ false);
3984
3985        // (a) Fused kernel (executed production path) runs and is well-shaped.
3986        let fused = y.realize_f32();
3987        assert_eq!(fused.len(), batch * seqlen * dim);
3988
3989        // (b) Unroll oracle over the lowered Op::Scan matches the fused kernel.
3990        let graph = y.inner.graph().clone();
3991        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
3992            .optimize_to_fixpoint(&graph, &[y.inner.id()]);
3993        assert_eq!(roots.len(), 1);
3994        let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
3995        let ys = {
3996            let mut g = graph.write().unwrap();
3997            fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
3998                .expect("unroll")
3999                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
4000        };
4001        let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4002            .expect("realize selective_scan unroll oracle on CPU");
4003        // ys is [seqlen,batch,dim]; fused y is [batch,seqlen,dim].
4004        let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
4005        assert_eq!(oracle.len(), fused.len());
4006        let mut max_drift = 0f32;
4007        for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4008            let d = (o - f).abs();
4009            max_drift = max_drift.max(d);
4010            // 1e-3 sabotage-calibrated: native-F32 unroll vs F64-accumulate
4011            // kernel. Honest drift here is ~1e-6 (printed); a batch/seqlen or
4012            // dim/dstate transpose diverges by O(0.1..10) — far above 1e-3.
4013            assert!(
4014                d < 1e-3,
4015                "selective_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4016            );
4017        }
4018        eprintln!("selective_scan multidim: max honest drift = {max_drift:e}");
4019    }
4020
4021    /// Task 8 (part 1, softplus) — `selective_scan` with `delta_softplus=true`,
4022    /// the branch unexercised at T=1. `delta` carries mixed-sign values so the
4023    /// stable softplus form `Relu(x)+Log(1+Exp(Neg(Abs(x))))` does real work on
4024    /// both the `x>0` and `x<0` sides before the scan. Same fused-vs-oracle
4025    /// parity as the non-softplus case.
4026    #[test]
4027    fn selective_scan_softplus_multitoken_unroll_matches_fused_kernel() {
4028        use fuel_graph::registry::FusedOps;
4029        let dev = Device::cpu();
4030        let (batch, seqlen, dim, dstate) = (1usize, 3usize, 2usize, 2usize);
4031        let u = Tensor::from_f32(
4032            vec![0.5, -0.3, 1.0, 0.2, -0.5, 0.7],
4033            Shape::from_dims(&[batch, seqlen, dim]),
4034            &dev,
4035        )
4036        .unwrap();
4037        // Mixed signs → softplus exercises max(x,0) AND ln(1+exp(-|x|)).
4038        let delta = u
4039            .const_f32_like(
4040                vec![-0.5, 0.2, 0.8, -1.0, 0.3, -0.2],
4041                Shape::from_dims(&[batch, seqlen, dim]),
4042            )
4043            .unwrap();
4044        let a = u
4045            .const_f32_like(
4046                vec![-0.7, -0.3, -0.5, -0.9],
4047                Shape::from_dims(&[dim, dstate]),
4048            )
4049            .unwrap();
4050        let b = u
4051            .const_f32_like(
4052                vec![1.0, 0.5, 0.25, 0.75, 0.6, 0.2],
4053                Shape::from_dims(&[batch, seqlen, dstate]),
4054            )
4055            .unwrap();
4056        let c = u
4057            .const_f32_like(
4058                vec![2.0, 1.0, 0.5, 1.5, 0.8, 0.3],
4059                Shape::from_dims(&[batch, seqlen, dstate]),
4060            )
4061            .unwrap();
4062        let y = u.selective_scan(&delta, &a, &b, &c, /* delta_softplus */ true);
4063
4064        let fused = y.realize_f32();
4065        assert_eq!(fused.len(), batch * seqlen * dim);
4066
4067        let graph = y.inner.graph().clone();
4068        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4069            .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4070        let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
4071        let ys = {
4072            let mut g = graph.write().unwrap();
4073            fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4074                .expect("unroll")
4075                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
4076        };
4077        let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4078            .expect("realize selective_scan softplus oracle on CPU");
4079        let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
4080        let mut max_drift = 0f32;
4081        for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4082            let d = (o - f).abs();
4083            max_drift = max_drift.max(d);
4084            assert!(
4085                d < 1e-3,
4086                "selective_scan softplus mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4087            );
4088        }
4089        eprintln!("selective_scan softplus: max honest drift = {max_drift:e}");
4090    }
4091
4092    /// Task 8 (part 2) — `ssd_chunk_scan` general-dimension parity.
4093    ///
4094    /// **C7 non-regression** (same construction as part 1): the executor
4095    /// dispatches `Op::Fused(SSD_CHUNK_SCAN)` straight to its kernel; the
4096    /// `decompose`→`Op::Scan` recipe is reached only by `lowering_only()` here.
4097    ///
4098    /// **Distinguishing dims** batch=2, seqlen=4, heads=2, head_dim=2,
4099    /// state_dim=2, chunk_size=2. Per-head `a = [-0.6, -1.0]` (head 0's gate
4100    /// leaking into head 1 flips the decay); `x` (indexes head_dim), `b`/`c`
4101    /// (index state_dim) use three DIFFERENT value ramps, so a head_dim↔state_dim
4102    /// broadcast swap (`bc_x` vs `bc_state`) mixes the wrong axes, and the
4103    /// seqlen recurrence + batch↔seqlen permute are exercised because batch>1.
4104    /// Every element is distinct.
4105    #[test]
4106    fn ssd_chunk_scan_multidim_unroll_matches_fused_kernel() {
4107        use fuel_graph::registry::FusedOps;
4108        let dev = Device::cpu();
4109        let (batch, seqlen, heads, head_dim, state_dim, chunk) =
4110            (2usize, 4usize, 2usize, 2usize, 2usize, 2usize);
4111        // Distinct value ramps (different base + slope per tensor) guarantee no
4112        // accidental symmetry; dt stays positive, a stays negative (stable gate).
4113        let x_data: Vec<f32> = (0..batch * seqlen * heads * head_dim)
4114            .map(|i| 0.20 + 0.05 * i as f32)
4115            .collect();
4116        let dt_data: Vec<f32> = (0..batch * seqlen * heads)
4117            .map(|i| 0.10 + 0.04 * i as f32)
4118            .collect();
4119        let b_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4120            .map(|i| 0.15 + 0.03 * i as f32)
4121            .collect();
4122        let c_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4123            .map(|i| 0.25 + 0.035 * i as f32)
4124            .collect();
4125        let x = Tensor::from_f32(
4126            x_data,
4127            Shape::from_dims(&[batch, seqlen, heads, head_dim]),
4128            &dev,
4129        )
4130        .unwrap();
4131        let dt = x
4132            .const_f32_like(dt_data, Shape::from_dims(&[batch, seqlen, heads]))
4133            .unwrap();
4134        let a = x
4135            .const_f32_like(vec![-0.6f32, -1.0], Shape::from_dims(&[heads]))
4136            .unwrap();
4137        let b = x
4138            .const_f32_like(b_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4139            .unwrap();
4140        let c = x
4141            .const_f32_like(c_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4142            .unwrap();
4143        let y = x.ssd_chunk_scan(&dt, &a, &b, &c, chunk);
4144
4145        // (a) Fused kernel (executed production path) runs and is well-shaped.
4146        let fused = y.realize_f32();
4147        assert_eq!(fused.len(), batch * seqlen * heads * head_dim);
4148
4149        // (b) Unroll oracle over the lowered Op::Scan matches the fused kernel.
4150        let graph = y.inner.graph().clone();
4151        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4152            .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4153        assert_eq!(roots.len(), 1);
4154        let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SSD_CHUNK_SCAN);
4155        let ys = {
4156            let mut g = graph.write().unwrap();
4157            fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4158                .expect("unroll")
4159                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
4160        };
4161        let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4162            .expect("realize ssd_chunk_scan unroll oracle on CPU");
4163        // ys is [seqlen,batch,heads,head_dim]; fused y is
4164        // [batch,seqlen,heads,head_dim]. tail = heads*head_dim (contiguous).
4165        let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, heads * head_dim);
4166        assert_eq!(oracle.len(), fused.len());
4167        let mut max_drift = 0f32;
4168        for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4169            let d = (o - f).abs();
4170            max_drift = max_drift.max(d);
4171            assert!(
4172                d < 1e-3,
4173                "ssd_chunk_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4174            );
4175        }
4176        eprintln!("ssd_chunk_scan multidim: max honest drift = {max_drift:e}");
4177    }
4178
4179    /// Recipe principle (G2): Nf4Matmul must decompose to a **fused-free**
4180    /// primitive subgraph whose realize matches the fused kernel. RED before
4181    /// the total decompose landed (the self-return left an
4182    /// `Op::Fused(NF4_MATMUL)` opaque island in the base map); GREEN after.
4183    /// Uses the hand-computed two-outputs / two-blocks case (expected `[10,
4184    /// 50]`) shared with `fuel_cpu_backend`'s byte-kernel test, so the
4185    /// indicator-sum codebook + nibble unpack + per-block scale are checked
4186    /// against the same numbers the fused CPU kernel produces.
4187    #[test]
4188    fn nf4_matmul_decompose_matches_kernel() {
4189        use fuel_graph::Op;
4190        use fuel_graph::registry::FusedOps;
4191        let dev = Device::cpu();
4192        // n=2, k=4, block_size=2; w_packed [2, 2] U8, absmax [2, 2] F32.
4193        let weight = crate::nf4::nf4_from_bytes(
4194            vec![247_u8, 247, 127, 127],
4195            vec![1.0_f32, 2.0, 10.0, 20.0],
4196            2,
4197            4,
4198            2,
4199            &dev,
4200        )
4201        .expect("nf4_from_bytes");
4202        let act = Tensor::from_graph_tensor(
4203            weight
4204                .w_packed
4205                .graph_tensor()
4206                .const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
4207                .unwrap(),
4208        );
4209        let y = weight.matmul(&act);
4210
4211        // Decompose explicitly, then realize the primitive subgraph.
4212        let graph = y.inner.graph().clone();
4213        let id = y.inner.id();
4214        let roots =
4215            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
4216        assert_eq!(roots.len(), 1, "lowering should keep a single root");
4217
4218        // Born-red discriminator: no Op::Fused(NF4_MATMUL) reachable from the
4219        // realized root — a self-returning decompose fails exactly here.
4220        {
4221            let g = graph.read().unwrap();
4222            let mut stack = vec![roots[0]];
4223            let mut seen = std::collections::HashSet::new();
4224            while let Some(nid) = stack.pop() {
4225                if !seen.insert(nid) {
4226                    continue;
4227                }
4228                let node = g.node(nid);
4229                assert!(
4230                    !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::NF4_MATMUL),
4231                    "decomposed graph still contains an Op::Fused(NF4_MATMUL) island",
4232                );
4233                for &inp in &node.inputs {
4234                    stack.push(inp);
4235                }
4236            }
4237        }
4238
4239        let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
4240            .expect("realize decomposed Nf4Matmul on CPU");
4241        assert_eq!(got.len(), 2);
4242        assert!((got[0] - 10.0).abs() < 1e-4, "out 0: {}", got[0]);
4243        assert!((got[1] - 50.0).abs() < 1e-4, "out 1: {}", got[1]);
4244    }
4245
4246    /// Build a one-output fused-op node directly on `anchor`'s graph, lower it
4247    /// to primitives, realize on CPU, and return the F32 values. The decompose
4248    /// parity tests for backward helpers use this — those ops have no public
4249    /// builder (autograd creates them), so the test constructs the
4250    /// `Op::Fused` node by hand.
4251    fn lower_realize_fused(
4252        anchor: &Tensor,
4253        op: fuel_graph::Op,
4254        inputs: Vec<fuel_graph::NodeId>,
4255        shape: Shape,
4256    ) -> Vec<f32> {
4257        let graph = anchor.inner.graph().clone();
4258        let fused_id = {
4259            let mut g = graph.write().unwrap();
4260            g.push(fuel_graph::Node {
4261                op,
4262                inputs,
4263                shape,
4264                dtype: DType::F32,
4265            })
4266        };
4267        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4268            .optimize_to_fixpoint(&graph, &[fused_id]);
4269        assert_eq!(roots.len(), 1, "lowering keeps a single root");
4270        crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &Device::cpu())
4271            .expect("realize decomposed fused op on CPU")
4272    }
4273
4274    #[test]
4275    fn powi_backward_decompose_matches_reference() {
4276        use fuel_graph::registry::{FusedOpParams, FusedOps};
4277        let dev = Device::cpu();
4278        let exp = 3i32;
4279        let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4280        let up_data = vec![1.0f32, 0.5, 2.0, -1.0];
4281        let shape = Shape::from_dims(&[4]);
4282        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4283        let up = x.const_f32_like(up_data.clone(), shape.clone()).unwrap();
4284        let got = lower_realize_fused(
4285            &x,
4286            fuel_graph::Op::Fused(FusedOps::POWI_BACKWARD, FusedOpParams::PowIBackward { exp }),
4287            vec![x.inner.id(), up.inner.id()],
4288            shape,
4289        );
4290        // grad_x = exp · x^(exp-1) · upstream
4291        let expected: Vec<f32> = x_data
4292            .iter()
4293            .zip(&up_data)
4294            .map(|(&x, &u)| exp as f32 * x.powi(exp - 1) * u)
4295            .collect();
4296        assert_eq!(got.len(), expected.len());
4297        for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4298            assert!(
4299                (g - e).abs() < 1e-4,
4300                "powi_backward at {i}: got {g}, expected {e}"
4301            );
4302        }
4303    }
4304
4305    /// Increment C — `inplace_affine`'s migrated functional value recipe:
4306    /// `decompose` lowers `Op::Fused(INPLACE_AFFINE, {mul, add})` to
4307    /// `AddScalar(add)(MulScalar(mul)(x))`, which realizes on the CPU backend to
4308    /// the plain affine `mul·x + add`. Bit-exact: the affine kernel pre-casts the
4309    /// f64 scalars to f32 and computes `mul_f32 * x + add_f32`, so the two-op
4310    /// `MulScalar`→`AddScalar` composition matches a hand `(mul as f32)*x + (add
4311    /// as f32)` exactly (the intermediate `* 1.0` / `+ 0.0` are IEEE identities).
4312    #[test]
4313    fn inplace_affine_decompose_matches_affine_reference() {
4314        use fuel_graph::registry::{FusedOpParams, FusedOps};
4315        let dev = Device::cpu();
4316        let (mul, add) = (2.5f64, -1.0f64);
4317        let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4318        let shape = Shape::from_dims(&[4]);
4319        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4320        let got = lower_realize_fused(
4321            &x,
4322            fuel_graph::Op::Fused(
4323                FusedOps::INPLACE_AFFINE,
4324                FusedOpParams::InplaceAffine { mul, add },
4325            ),
4326            vec![x.inner.id()],
4327            shape,
4328        );
4329        // functional affine: out = mul·x + add (scalars pre-cast to f32).
4330        let expected: Vec<f32> = x_data
4331            .iter()
4332            .map(|&v| (mul as f32) * v + add as f32)
4333            .collect();
4334        assert_eq!(got.len(), expected.len());
4335        for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4336            assert_eq!(g, e, "inplace_affine at {i}: got {g}, expected {e}");
4337        }
4338    }
4339
4340    #[test]
4341    fn softmax_last_dim_backward_decompose_matches_reference() {
4342        use fuel_graph::registry::{FusedOpParams, FusedOps};
4343        let dev = Device::cpu();
4344        let (rows, cols) = (2usize, 3usize);
4345        // s = a real softmax over the last dim; g = arbitrary upstream.
4346        let logits = [[0.5f32, -1.0, 2.0], [1.0, 0.0, -0.5]];
4347        let mut s_data = Vec::new();
4348        for row in &logits {
4349            let m = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4350            let exps: Vec<f32> = row.iter().map(|&v| (v - m).exp()).collect();
4351            let sum: f32 = exps.iter().sum();
4352            s_data.extend(exps.iter().map(|&e| e / sum));
4353        }
4354        let g_data = vec![1.0f32, -0.5, 0.3, 0.2, 1.5, -1.0];
4355        let shape = Shape::from_dims(&[rows, cols]);
4356        let s = Tensor::from_f32(s_data.clone(), shape.clone(), &dev).unwrap();
4357        let g = s.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4358        let got = lower_realize_fused(
4359            &s,
4360            fuel_graph::Op::Fused(
4361                FusedOps::SOFTMAX_LAST_DIM_BACKWARD,
4362                FusedOpParams::SoftmaxLastDimBackward,
4363            ),
4364            vec![s.inner.id(), g.inner.id()],
4365            shape,
4366        );
4367        // grad_x = s · (g − sum(g·s, last))
4368        let mut expected = vec![0.0f32; rows * cols];
4369        for r in 0..rows {
4370            let dot: f32 = (0..cols)
4371                .map(|c| g_data[r * cols + c] * s_data[r * cols + c])
4372                .sum();
4373            for c in 0..cols {
4374                expected[r * cols + c] = s_data[r * cols + c] * (g_data[r * cols + c] - dot);
4375            }
4376        }
4377        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4378            assert!(
4379                (gv - ev).abs() < 1e-5,
4380                "softmax_bwd at {i}: got {gv}, expected {ev}"
4381            );
4382        }
4383    }
4384
4385    #[test]
4386    fn rms_norm_last_dim_backward_decompose_matches_reference() {
4387        use fuel_graph::registry::{FusedOpParams, FusedOps};
4388        let dev = Device::cpu();
4389        let (rows, cols) = (2usize, 3usize);
4390        let eps = 1e-5f64;
4391        let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4392        let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4393        let shape = Shape::from_dims(&[rows, cols]);
4394        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4395        let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4396        let got = lower_realize_fused(
4397            &x,
4398            fuel_graph::Op::Fused(
4399                FusedOps::RMS_NORM_LAST_DIM_BACKWARD,
4400                FusedOpParams::RmsNormLastDimBackward { eps },
4401            ),
4402            vec![x.inner.id(), g.inner.id()],
4403            shape,
4404        );
4405        // grad_x = r_rms · (g − x·s / (n·(mean_sq + eps)))
4406        let n = cols as f32;
4407        let mut expected = vec![0.0f32; rows * cols];
4408        for r in 0..rows {
4409            let meansq: f32 = (0..cols).map(|c| x_data[r * cols + c].powi(2)).sum::<f32>() / n;
4410            let denom = meansq + eps as f32;
4411            let rrms = 1.0 / denom.sqrt();
4412            let s: f32 = (0..cols)
4413                .map(|c| g_data[r * cols + c] * x_data[r * cols + c])
4414                .sum();
4415            for c in 0..cols {
4416                let term = x_data[r * cols + c] * s / (n * denom);
4417                expected[r * cols + c] = rrms * (g_data[r * cols + c] - term);
4418            }
4419        }
4420        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4421            assert!(
4422                (gv - ev).abs() < 1e-4,
4423                "rms_norm_bwd at {i}: got {gv}, expected {ev}"
4424            );
4425        }
4426    }
4427
4428    #[test]
4429    fn layer_norm_last_dim_backward_decompose_matches_reference() {
4430        use fuel_graph::registry::{FusedOpParams, FusedOps};
4431        let dev = Device::cpu();
4432        let (rows, cols) = (2usize, 3usize);
4433        let eps = 1e-5f64;
4434        let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4435        let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4436        let shape = Shape::from_dims(&[rows, cols]);
4437        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4438        let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4439        let got = lower_realize_fused(
4440            &x,
4441            fuel_graph::Op::Fused(
4442                FusedOps::LAYER_NORM_LAST_DIM_BACKWARD,
4443                FusedOpParams::LayerNormLastDimBackward { eps },
4444            ),
4445            vec![x.inner.id(), g.inner.id()],
4446            shape,
4447        );
4448        // grad_x = istd · (g − mean(g) − xhat·mean(g·xhat))
4449        let n = cols as f32;
4450        let mut expected = vec![0.0f32; rows * cols];
4451        for r in 0..rows {
4452            let mean_x: f32 = (0..cols).map(|c| x_data[r * cols + c]).sum::<f32>() / n;
4453            let var: f32 = (0..cols)
4454                .map(|c| (x_data[r * cols + c] - mean_x).powi(2))
4455                .sum::<f32>()
4456                / n;
4457            let istd = 1.0 / (var + eps as f32).sqrt();
4458            let xhat: Vec<f32> = (0..cols)
4459                .map(|c| (x_data[r * cols + c] - mean_x) * istd)
4460                .collect();
4461            let mean_g: f32 = (0..cols).map(|c| g_data[r * cols + c]).sum::<f32>() / n;
4462            let mean_gxh: f32 = (0..cols)
4463                .map(|c| g_data[r * cols + c] * xhat[c])
4464                .sum::<f32>()
4465                / n;
4466            for c in 0..cols {
4467                expected[r * cols + c] =
4468                    istd * (g_data[r * cols + c] - mean_g - xhat[c] * mean_gxh);
4469            }
4470        }
4471        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4472            assert!(
4473                (gv - ev).abs() < 1e-4,
4474                "layer_norm_bwd at {i}: got {gv}, expected {ev}"
4475            );
4476        }
4477    }
4478
4479    #[test]
4480    fn reduce_max_to_backward_decompose_matches_reference() {
4481        use fuel_graph::registry::{FusedOpParams, FusedOps};
4482        let dev = Device::cpu();
4483        // row0 has a tie at the max (two 3.0s → share 10/2); row1 unique max.
4484        let x_data = vec![1.0f32, 3.0, 3.0, 2.0, 0.0, 1.0]; // [2,3]
4485        let up_data = vec![10.0f32, 5.0]; // [2,1] — one per reduced row.
4486        let x_shape = Shape::from_dims(&[2, 3]);
4487        let up_shape = Shape::from_dims(&[2, 1]);
4488        let x = Tensor::from_f32(x_data.clone(), x_shape.clone(), &dev).unwrap();
4489        let up = x.const_f32_like(up_data.clone(), up_shape).unwrap();
4490        let got = lower_realize_fused(
4491            &x,
4492            fuel_graph::Op::Fused(
4493                FusedOps::REDUCE_MAX_TO_BACKWARD,
4494                FusedOpParams::ReduceMaxToBackward,
4495            ),
4496            vec![x.inner.id(), up.inner.id()],
4497            x_shape,
4498        );
4499        // row0: max 3.0 tied 2× → 10/2 each; row1: max 2.0 unique → full 5.0.
4500        let expected = vec![0.0f32, 5.0, 5.0, 5.0, 0.0, 0.0];
4501        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4502            assert!(
4503                (gv - ev).abs() < 1e-5,
4504                "reduce_max_bwd at {i}: got {gv}, expected {ev}"
4505            );
4506        }
4507    }
4508
4509    fn causal_conv1d_check(use_silu: bool) {
4510        use fuel_graph::registry::{FusedOpParams, FusedOps};
4511        let dev = Device::cpu();
4512        let (b, c, k, seq) = (1usize, 2usize, 3usize, 2usize);
4513        let x_seq = seq + (k - 1); // caller pre-pads by K-1.
4514        // x [B,C,x_seq], weight [C,1,K], bias [C] — all row-major.
4515        let x_data = vec![0.0f32, 1.0, 2.0, 3.0, 1.0, 0.0, -1.0, -2.0];
4516        let w_data = vec![0.5f32, 1.0, -0.5, 1.0, 0.0, 2.0];
4517        let bias_data = vec![0.1f32, -0.2];
4518        let x = Tensor::from_f32(x_data.clone(), Shape::from_dims(&[b, c, x_seq]), &dev).unwrap();
4519        let w = x
4520            .const_f32_like(w_data.clone(), Shape::from_dims(&[c, 1, k]))
4521            .unwrap();
4522        let bias = x
4523            .const_f32_like(bias_data.clone(), Shape::from_dims(&[c]))
4524            .unwrap();
4525        let got = lower_realize_fused(
4526            &x,
4527            fuel_graph::Op::Fused(
4528                FusedOps::CAUSAL_CONV1D,
4529                FusedOpParams::CausalConv1d { use_silu },
4530            ),
4531            vec![x.inner.id(), w.inner.id(), bias.inner.id()],
4532            Shape::from_dims(&[b, c, seq]),
4533        );
4534        // out[c,t] = Σ_k w[c,k]·x[c,t+k] + bias[c], optional SiLU.
4535        let mut expected = vec![0.0f32; b * c * seq];
4536        for ch in 0..c {
4537            for t in 0..seq {
4538                let mut acc = 0.0f32;
4539                for kk in 0..k {
4540                    acc += w_data[ch * k + kk] * x_data[ch * x_seq + t + kk];
4541                }
4542                acc += bias_data[ch];
4543                if use_silu {
4544                    acc *= 1.0 / (1.0 + (-acc).exp());
4545                }
4546                expected[ch * seq + t] = acc;
4547            }
4548        }
4549        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4550            assert!(
4551                (gv - ev).abs() < 1e-4,
4552                "causal_conv1d (silu={use_silu}) at {i}: got {gv}, expected {ev}",
4553            );
4554        }
4555    }
4556
4557    #[test]
4558    fn causal_conv1d_decompose_matches_reference() {
4559        causal_conv1d_check(false);
4560        causal_conv1d_check(true);
4561    }
4562
4563    #[test]
4564    fn flash_attn_backward_decompose_matches_reference() {
4565        use fuel_graph::registry::{FusedOpParams, FusedOps};
4566        let dev = Device::cpu();
4567        let (sq, sk, d) = (2usize, 2usize, 2usize);
4568        // 4-digit test scale (~1/sqrt(2)), deliberately NOT FRAC_1_SQRT_2 (0.70710678);
4569        // swapping the exact constant would perturb these attention-scale parity tests.
4570        #[allow(clippy::approx_constant)]
4571        let scale = 0.7071f32;
4572        let q_data = vec![0.1f32, -0.2, 0.3, 0.5]; // [Sq,D]
4573        let k_data = vec![0.4f32, 0.1, -0.3, 0.2]; // [Sk,D]
4574        let v_data = vec![1.0f32, 2.0, -1.0, 0.5]; // [Sk,D]
4575        let do_data = vec![0.5f32, -1.0, 0.2, 0.3]; // [Sq,D]
4576        let qshape = Shape::from_dims(&[1, 1, sq, d]);
4577        let kshape = Shape::from_dims(&[1, 1, sk, d]);
4578        let q = Tensor::from_f32(q_data.clone(), qshape.clone(), &dev).unwrap();
4579        let k = q.const_f32_like(k_data.clone(), kshape.clone()).unwrap();
4580        let v = q.const_f32_like(v_data.clone(), kshape.clone()).unwrap();
4581        let dout = q.const_f32_like(do_data.clone(), qshape.clone()).unwrap();
4582        let params = FusedOpParams::FlashAttnBackward {
4583            softmax_scale: scale,
4584            causal: false,
4585            window_size_left: None,
4586            window_size_right: None,
4587            softcap: None,
4588        };
4589        let inputs = vec![q.inner.id(), k.inner.id(), v.inner.id(), dout.inner.id()];
4590        let dq = lower_realize_fused(
4591            &q,
4592            fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_Q, params.clone()),
4593            inputs.clone(),
4594            qshape.clone(),
4595        );
4596        let dk = lower_realize_fused(
4597            &q,
4598            fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_K, params.clone()),
4599            inputs.clone(),
4600            kshape.clone(),
4601        );
4602        let dv = lower_realize_fused(
4603            &q,
4604            fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_V, params),
4605            inputs,
4606            kshape,
4607        );
4608
4609        // --- reference SDPA backward (B=1, H=1) ---
4610        let mut p = vec![0.0f32; sq * sk];
4611        for i in 0..sq {
4612            let mut scores = vec![0.0f32; sk];
4613            for j in 0..sk {
4614                let mut s = 0.0f32;
4615                for l in 0..d {
4616                    s += q_data[i * d + l] * k_data[j * d + l];
4617                }
4618                scores[j] = scale * s;
4619            }
4620            let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4621            let mut sum = 0.0f32;
4622            let e: Vec<f32> = scores
4623                .iter()
4624                .map(|&s| {
4625                    let x = (s - m).exp();
4626                    sum += x;
4627                    x
4628                })
4629                .collect();
4630            for j in 0..sk {
4631                p[i * sk + j] = e[j] / sum;
4632            }
4633        }
4634        // dV[j,l] = Σ_i P[i,j]·dO[i,l]
4635        let mut ref_dv = vec![0.0f32; sk * d];
4636        for j in 0..sk {
4637            for l in 0..d {
4638                ref_dv[j * d + l] = (0..sq).map(|i| p[i * sk + j] * do_data[i * d + l]).sum();
4639            }
4640        }
4641        // dP[i,j] = Σ_l dO[i,l]·v[j,l]
4642        let mut dp = vec![0.0f32; sq * sk];
4643        for i in 0..sq {
4644            for j in 0..sk {
4645                dp[i * sk + j] = (0..d).map(|l| do_data[i * d + l] * v_data[j * d + l]).sum();
4646            }
4647        }
4648        // dScores_raw[i,j] = scale · P[i,j]·(dP[i,j] − Σ_j' dP·P)
4649        let mut dsr = vec![0.0f32; sq * sk];
4650        for i in 0..sq {
4651            let rowdot: f32 = (0..sk).map(|j| dp[i * sk + j] * p[i * sk + j]).sum();
4652            for j in 0..sk {
4653                dsr[i * sk + j] = scale * p[i * sk + j] * (dp[i * sk + j] - rowdot);
4654            }
4655        }
4656        // dQ[i,l] = Σ_j dsr[i,j]·k[j,l] ; dK[j,l] = Σ_i dsr[i,j]·q[i,l]
4657        let mut ref_dq = vec![0.0f32; sq * d];
4658        for i in 0..sq {
4659            for l in 0..d {
4660                ref_dq[i * d + l] = (0..sk).map(|j| dsr[i * sk + j] * k_data[j * d + l]).sum();
4661            }
4662        }
4663        let mut ref_dk = vec![0.0f32; sk * d];
4664        for j in 0..sk {
4665            for l in 0..d {
4666                ref_dk[j * d + l] = (0..sq).map(|i| dsr[i * sk + j] * q_data[i * d + l]).sum();
4667            }
4668        }
4669        let check = |name: &str, got: &[f32], exp: &[f32]| {
4670            assert_eq!(got.len(), exp.len(), "{name} length");
4671            for (i, (&gv, &ev)) in got.iter().zip(exp).enumerate() {
4672                assert!(
4673                    (gv - ev).abs() < 1e-4,
4674                    "{name} at {i}: got {gv}, expected {ev}"
4675                );
4676            }
4677        };
4678        check("dQ", &dq, &ref_dq);
4679        check("dK", &dk, &ref_dk);
4680        check("dV", &dv, &ref_dv);
4681    }
4682
4683    #[test]
4684    fn paged_attn_decompose_matches_reference() {
4685        use fuel_graph::registry::{FusedOpParams, FusedOps};
4686        let dev = Device::cpu();
4687        // B=1, Hq=Hkv=1, Sq=1 (decode), D=2; block_size=2, max_blk=2 → kv_len=4.
4688        let block_size = 2usize;
4689        let scale = 1.0f32;
4690        // k_cache / v_cache: [num_blocks=3, block_size=2, Hkv=1, D=2]. Block 1
4691        // is unused (not in the block table) and holds sentinel 9s.
4692        let kc = vec![
4693            1.0f32, 0.0, 0.0, 1.0, 9.0, 9.0, 9.0, 9.0, 1.0, 1.0, 2.0, 0.0,
4694        ];
4695        let vc = vec![
4696            1.0f32, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0, 6.0, 7.0, 8.0,
4697        ];
4698        let q_data = vec![0.5f32, 0.5]; // [1,1,1,2]
4699        let bt = vec![0u32, 2u32]; // block_table [1,2]: sequence uses blocks 0 and 2
4700        let cl = vec![3u32]; // context_lens [1]: only the first 3 keys are valid
4701
4702        let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, 1, 1, 2]), &dev).unwrap();
4703        let kcache = q
4704            .const_f32_like(kc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4705            .unwrap();
4706        let vcache = q
4707            .const_f32_like(vc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4708            .unwrap();
4709        let block_table = q.const_u32_like(bt, Shape::from_dims(&[1, 2])).unwrap();
4710        let context_lens = q.const_u32_like(cl, Shape::from_dims(&[1])).unwrap();
4711        let params = FusedOpParams::PagedAttn {
4712            softmax_scale: scale,
4713            block_size,
4714            softcap: None,
4715        };
4716        let inputs = vec![
4717            q.inner.id(),
4718            kcache.inner.id(),
4719            vcache.inner.id(),
4720            block_table.inner.id(),
4721            context_lens.inner.id(),
4722        ];
4723        let got = lower_realize_fused(
4724            &q,
4725            fuel_graph::Op::Fused(FusedOps::PAGED_ATTN, params),
4726            inputs,
4727            Shape::from_dims(&[1, 1, 1, 2]),
4728        );
4729
4730        // reference: gather blocks [0, 2] → k_seq/v_seq[4][2]; mask j ≥ 3;
4731        // softmax; weighted sum of v.
4732        let (kv_len, ctx) = (4usize, 3usize);
4733        let mut k_seq = vec![];
4734        let mut v_seq = vec![];
4735        for &blk in &[0usize, 2usize] {
4736            for p in 0..block_size {
4737                let base = (blk * block_size + p) * 2; // Hkv=1, D=2
4738                k_seq.push([kc[base], kc[base + 1]]);
4739                v_seq.push([vc[base], vc[base + 1]]);
4740            }
4741        }
4742        let mut scores = vec![0.0f32; kv_len];
4743        for j in 0..kv_len {
4744            let s = q_data[0] * k_seq[j][0] + q_data[1] * k_seq[j][1];
4745            scores[j] = if j >= ctx {
4746                f32::NEG_INFINITY
4747            } else {
4748                scale * s
4749            };
4750        }
4751        let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4752        let mut sum = 0.0f32;
4753        let e: Vec<f32> = scores
4754            .iter()
4755            .map(|&s| {
4756                let x = (s - m).exp();
4757                sum += x;
4758                x
4759            })
4760            .collect();
4761        let mut expected = vec![0.0f32; 2];
4762        for j in 0..kv_len {
4763            for l in 0..2 {
4764                expected[l] += (e[j] / sum) * v_seq[j][l];
4765            }
4766        }
4767        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4768            assert!(
4769                (gv - ev).abs() < 1e-4,
4770                "paged_attn at {i}: got {gv}, expected {ev}"
4771            );
4772        }
4773    }
4774
4775    #[test]
4776    #[cfg(feature = "cuda")]
4777    fn cuda_executor_matches_cpu_on_add_mul() {
4778        let a =
4779            Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
4780        let b = a
4781            .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
4782            .unwrap();
4783        let c = a.add(&b).unwrap().mul(&a).unwrap();
4784        let cpu_result = c.realize_f32();
4785        let executor = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4786        let cuda_result = c.realize_f32_cuda(&executor);
4787        assert_eq!(cpu_result, cuda_result);
4788    }
4789
4790    /// END-TO-END SMOKE ONLY — NOT a CUDA-kernel pin. Verifies the lazy
4791    /// realize path produces the NaN-propagating `maximum`/`minimum`
4792    /// convention (pinned 2026-07-08,
4793    /// `docs/architecture/10-decisions-log.md`) and that a CPU realize and
4794    /// a CUDA-device realize of the same graph agree on NaN-ness.
4795    ///
4796    /// CAVEAT (orchestrator sabotage finding, 2026-07-08): cost-based
4797    /// placement may route a tiny elementwise op to CPU on BOTH legs even
4798    /// under `realize_f32_cuda`, so this test does NOT guarantee the CUDA
4799    /// kernel executed — a wrong CUDA binding can pass it. The real
4800    /// CUDA-kernel pin is the direct binding-table invocation test
4801    /// `fuel-dispatch/tests/cuda_dispatch_live.rs::cuda_maximum_minimum_propagate_nan_f32`.
4802    ///
4803    /// Gated `#[cfg(feature = "cuda")]` + `#[ignore]`; skips cleanly if no
4804    /// CUDA device is present. Run:
4805    ///   `cargo test -p fuel-core --features cuda --lib \
4806    ///    maximum_minimum_nan_convention_lazy_realize_smoke -- --ignored --nocapture`
4807    #[test]
4808    #[cfg(feature = "cuda")]
4809    #[ignore = "requires a live CUDA device"]
4810    fn maximum_minimum_nan_convention_lazy_realize_smoke() {
4811        let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4812            Ok(d) => d,
4813            Err(e) => {
4814                return fuel_test_support::hardware::skip(
4815                    fuel_test_support::hardware::Hardware::Cuda,
4816                    fuel_test_support::hardware::Missing::device(format!(
4817                        "CudaDevice::new(0): {e:?}"
4818                    )),
4819                );
4820            }
4821        };
4822        let a = Tensor::from_f32(
4823            vec![f32::NAN, -2.0, 3.0, f32::NAN],
4824            Shape::from_dims(&[4]),
4825            &Device::cpu(),
4826        )
4827        .unwrap();
4828        let b = a
4829            .const_f32_like(vec![1.0, f32::NAN, 2.0, f32::NAN], Shape::from_dims(&[4]))
4830            .unwrap();
4831
4832        let max_lazy = a.maximum(&b).unwrap();
4833        let cpu_max = max_lazy.realize_f32();
4834        let cuda_max = max_lazy.realize_f32_cuda(&cuda);
4835        for i in 0..4 {
4836            assert_eq!(
4837                cpu_max[i].is_nan(),
4838                cuda_max[i].is_nan(),
4839                "maximum[{i}] NaN-ness must match: cpu={}, cuda={}",
4840                cpu_max[i],
4841                cuda_max[i],
4842            );
4843            if !cpu_max[i].is_nan() {
4844                assert_eq!(cpu_max[i], cuda_max[i], "maximum[{i}]");
4845            }
4846        }
4847
4848        let min_lazy = a.minimum(&b).unwrap();
4849        let cpu_min = min_lazy.realize_f32();
4850        let cuda_min = min_lazy.realize_f32_cuda(&cuda);
4851        for i in 0..4 {
4852            assert_eq!(
4853                cpu_min[i].is_nan(),
4854                cuda_min[i].is_nan(),
4855                "minimum[{i}] NaN-ness must match: cpu={}, cuda={}",
4856                cpu_min[i],
4857                cuda_min[i],
4858            );
4859            if !cpu_min[i].is_nan() {
4860                assert_eq!(cpu_min[i], cuda_min[i], "minimum[{i}]");
4861            }
4862        }
4863    }
4864
4865    /// END-TO-END SMOKE ONLY — NOT a CUDA-kernel pin. Verifies the lazy
4866    /// realize path produces the NaN-propagating `relu` convention (torch
4867    /// parity, pinned 2026-07-08, `docs/architecture/10-decisions-log.md`)
4868    /// and that a CPU realize and a CUDA-device realize of the same graph
4869    /// agree on NaN-ness. Successor of the transitional-divergence pin
4870    /// `relu_cuda_still_scrubs_nan_pending_alpha76_rebind`, flipped at the
4871    /// alpha.76 `unary_relu_propagating_*` rebind.
4872    ///
4873    /// CAVEAT (orchestrator sabotage finding, 2026-07-08): cost-based
4874    /// placement may route a tiny elementwise op to CPU on BOTH legs even
4875    /// under `realize_f32_cuda`, so this test does NOT guarantee the CUDA
4876    /// kernel executed — it stayed green with the CUDA binding deliberately
4877    /// reverted to the scrubbing stem. The real CUDA-kernel pin (verified
4878    /// born-red against that sabotage) is the direct binding-table
4879    /// invocation test
4880    /// `fuel-dispatch/tests/cuda_dispatch_live.rs::cuda_relu_propagates_nan_f32`
4881    /// (+ its bf16 sibling).
4882    ///
4883    /// Gated `#[cfg(feature = "cuda")]` + `#[ignore]`; skips cleanly if no
4884    /// CUDA device is present. Run:
4885    ///   `cargo test -p fuel-core --features cuda --lib \
4886    ///    relu_nan_convention_lazy_realize_smoke -- --ignored --nocapture`
4887    #[test]
4888    #[cfg(feature = "cuda")]
4889    #[ignore = "requires a live CUDA device"]
4890    fn relu_nan_convention_lazy_realize_smoke() {
4891        let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4892            Ok(d) => d,
4893            Err(e) => {
4894                return fuel_test_support::hardware::skip(
4895                    fuel_test_support::hardware::Hardware::Cuda,
4896                    fuel_test_support::hardware::Missing::device(format!(
4897                        "CudaDevice::new(0): {e:?}"
4898                    )),
4899                );
4900            }
4901        };
4902        let a = Tensor::from_f32(
4903            vec![f32::NAN, -2.0, 3.0],
4904            Shape::from_dims(&[3]),
4905            &Device::cpu(),
4906        )
4907        .unwrap();
4908        let relu_lazy = a.relu();
4909        let cpu_result = relu_lazy.realize_f32();
4910        let cuda_result = relu_lazy.realize_f32_cuda(&cuda);
4911
4912        for i in 0..3 {
4913            assert_eq!(
4914                cpu_result[i].is_nan(),
4915                cuda_result[i].is_nan(),
4916                "relu[{i}] NaN-ness must match: cpu={}, cuda={}",
4917                cpu_result[i],
4918                cuda_result[i],
4919            );
4920            if !cpu_result[i].is_nan() {
4921                assert_eq!(cpu_result[i], cuda_result[i], "relu[{i}]");
4922            }
4923        }
4924    }
4925
4926    #[test]
4927    #[cfg(feature = "cuda")]
4928    fn cuda_executor_matches_cpu_on_matmul() {
4929        let a = Tensor::from_f32(
4930            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4931            Shape::from_dims(&[2, 3]),
4932            &Device::cpu(),
4933        )
4934        .unwrap();
4935        let b = a
4936            .const_f32_like(
4937                vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0],
4938                Shape::from_dims(&[3, 2]),
4939            )
4940            .unwrap();
4941        let c = a.matmul(&b).unwrap();
4942        let cpu = c.realize_f32();
4943        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4944        let cuda = c.realize_f32_cuda(&exe);
4945        assert_eq!(cpu.len(), cuda.len());
4946        for (i, (a, b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4947            assert!((a - b).abs() < 1e-3, "matmul[{i}]: cpu={a}, cuda={b}",);
4948        }
4949    }
4950
4951    #[test]
4952    #[cfg(feature = "cuda")]
4953    fn cuda_executor_matches_cpu_on_broadcast_matmul() {
4954        // Rank-3 × rank-2 matmul (what the transformer forward does).
4955        // The graph auto-broadcasts the rank-2 to rank-3.
4956        let x = Tensor::from_f32(
4957            (0..12).map(|i| i as f32 * 0.1).collect::<Vec<_>>(),
4958            Shape::from_dims(&[1, 3, 4]),
4959            &Device::cpu(),
4960        )
4961        .unwrap();
4962        let w = x
4963            .const_f32_like(
4964                (0..8).map(|i| i as f32 * 0.2).collect::<Vec<_>>(),
4965                Shape::from_dims(&[4, 2]),
4966            )
4967            .unwrap();
4968        let y = x.matmul(&w).unwrap();
4969        let cpu = y.realize_f32();
4970        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4971        let cuda = y.realize_f32_cuda(&exe);
4972        assert_eq!(cpu.len(), cuda.len());
4973        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4974            assert!((a - b).abs() < 1e-3, "bcast_mm[{i}]: cpu={a}, cuda={b}");
4975        }
4976    }
4977
4978    #[test]
4979    #[cfg(feature = "cuda")]
4980    fn cuda_executor_matches_cpu_on_permute() {
4981        let x = Tensor::from_f32(
4982            (0..24).map(|i| i as f32).collect::<Vec<_>>(),
4983            Shape::from_dims(&[1, 2, 3, 4]),
4984            &Device::cpu(),
4985        )
4986        .unwrap();
4987        let y = x.permute([0, 2, 1, 3_usize]).unwrap();
4988        let cpu = y.realize_f32();
4989        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4990        let cuda = y.realize_f32_cuda(&exe);
4991        assert_eq!(cpu, cuda, "permute mismatch");
4992    }
4993
4994    #[test]
4995    #[cfg(feature = "cuda")]
4996    fn cuda_executor_matches_cpu_on_softmax() {
4997        let x = Tensor::from_f32(
4998            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4999            Shape::from_dims(&[2, 3]),
5000            &Device::cpu(),
5001        )
5002        .unwrap();
5003        let y = x.softmax_last_dim().unwrap();
5004        let cpu = y.realize_f32();
5005        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5006        let cuda = y.realize_f32_cuda(&exe);
5007        assert_eq!(cpu.len(), cuda.len());
5008        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5009            assert!((a - b).abs() < 1e-4, "softmax[{i}]: cpu={a}, cuda={b}");
5010        }
5011    }
5012
5013    /// Phase 7.5 PR 3 / 3.5 live CUDA equivalence: realize a graph with
5014    /// `Op::SoftmaxLastDim` through the rule-registry pipeline using
5015    /// `RuleRegistry::lowering_only()` so the executor sees the
5016    /// 7-node lowered subgraph instead of the fused op. The composed
5017    /// CUDA execution path (ReduceMaxTo + BroadcastTo + Sub + Exp +
5018    /// ReduceSumTo + BroadcastTo + Div) must match the fused CPU
5019    /// baseline within tight epsilon.
5020    ///
5021    /// Post PR-3.5 follow-up: ReduceMaxTo / ReduceSumTo run natively
5022    /// on CUDA via the legacy executor's `Op::ReduceXxxTo` arm (which
5023    /// delegates to `backend.reduce` and relabels the result to the
5024    /// keepdim shape), so the lowered subgraph stays GPU-resident
5025    /// end-to-end. Two D2H/H2D round-trips per softmax used to be
5026    /// the cost on the prior commit — both gone now.
5027    #[test]
5028    #[cfg(feature = "cuda")]
5029    fn cuda_executor_matches_cpu_on_softmax_via_lowering() {
5030        // Use a non-trivial input shape so the broadcast paths and
5031        // the ReduceSumTo step both have actual work to do.
5032        let n = 24;
5033        let last = 5;
5034        let data: Vec<f32> = (0..n * last)
5035            .map(|i| ((i as f32) * 0.13).sin() * 2.0 - 0.7)
5036            .collect();
5037        let x = Tensor::from_f32(data, Shape::from_dims(&[n, last]), &Device::cpu()).unwrap();
5038        let y = x.softmax_last_dim().unwrap();
5039
5040        // CPU baseline: fused SoftmaxLastDim through the standard
5041        // realize_f32 path (no rule-registry pipeline involved).
5042        let cpu = y.realize_f32();
5043
5044        // CUDA via the lowered subgraph: run the lowering-only
5045        // rule registry to fixpoint first so fusion can't re-collapse
5046        // the lowered pattern back to Op::SoftmaxLastDim. Then
5047        // realize the remapped target via PipelinedExecutor.
5048        // (Phase 7.6 step 9c E.2: optimizer is caller-composed.)
5049        let graph = y.inner.graph().clone();
5050        let registry = fuel_graph::opt::RuleRegistry::lowering_only();
5051        let remapped = registry.optimize_to_fixpoint(&graph, &[y.inner.id()]);
5052        let dev = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5053        let fc_device: crate::Device = dev.clone().into();
5054        let cuda = crate::pipelined_bridge::realize_one_as::<f32>(&graph, remapped[0], &fc_device)
5055            .expect("realize lowered softmax on CUDA");
5056
5057        assert_eq!(cpu.len(), cuda.len());
5058        let mut max_abs_err = 0.0_f32;
5059        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5060            let err = (a - b).abs();
5061            if err > max_abs_err {
5062                max_abs_err = err;
5063            }
5064            assert!(
5065                err < 1e-5,
5066                "lowered softmax[{i}]: cpu={a} (fused), cuda={b} (composed), err={err}",
5067            );
5068        }
5069        eprintln!("max_abs_err over lowered-vs-fused softmax: {max_abs_err:.3e}");
5070    }
5071
5072    #[test]
5073    #[cfg(feature = "cuda")]
5074    fn cuda_executor_matches_cpu_on_concat_slice() {
5075        let a = Tensor::from_f32(
5076            vec![1.0, 2.0, 3.0, 4.0],
5077            Shape::from_dims(&[2, 2]),
5078            &Device::cpu(),
5079        )
5080        .unwrap();
5081        let b = a
5082            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], Shape::from_dims(&[2, 2]))
5083            .unwrap();
5084        let cat = a.concat(&b, 1).unwrap(); // [2, 4]
5085        let sliced = cat.slice(1, 1, 2).unwrap(); // [2, 2]
5086        let cpu = sliced.realize_f32();
5087        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5088        let cuda = sliced.realize_f32_cuda(&exe);
5089        assert_eq!(cpu, cuda, "concat+slice mismatch");
5090    }
5091
5092    #[test]
5093    #[cfg(feature = "cuda")]
5094    fn cuda_executor_matches_cpu_on_rms_norm() {
5095        let x = Tensor::from_f32(
5096            (0..8).map(|i| i as f32 * 0.5 - 1.5).collect::<Vec<_>>(),
5097            Shape::from_dims(&[2, 4]),
5098            &Device::cpu(),
5099        )
5100        .unwrap();
5101        let y = x.rms_norm_last_dim(1e-5).unwrap();
5102        let cpu = y.realize_f32();
5103        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5104        let cuda = y.realize_f32_cuda(&exe);
5105        assert_eq!(cpu.len(), cuda.len());
5106        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5107            assert!((a - b).abs() < 1e-3, "rms_norm[{i}]: cpu={a}, cuda={b}");
5108        }
5109    }
5110
5111    #[test]
5112    fn realize_f64_through_bridge() {
5113        let a =
5114            Tensor::from_f64(vec![1.5, 2.5, 3.5], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
5115        let b = a.mul(&a).unwrap();
5116        assert_eq!(b.realize_f64(), vec![2.25, 6.25, 12.25]);
5117    }
5118
5119    /// GAP-327 regression test (was the born-red probe; now LIVE — the guard has landed).
5120    ///
5121    /// The realize funnel `pipelined_bridge::extract_cpu_bytes_typed` guards the byte
5122    /// reinterpretation on the root's dtype: `realize_one_as::<T>` returns
5123    /// `Error::UnexpectedDType` on a mismatch, and the typed `realize_fNN` accessors (which
5124    /// `.expect` that result) panic. The BYTE VIEW `realize_one_bytes` is the deliberate
5125    /// UNGUARDED escape hatch and still returns raw bytes for any dtype. Measured by probe
5126    /// (GAP-327) before the fix: realize_f32()/realize_one_as::<f32> on an F64 [1.0, 2.0]
5127    /// root returned Ok([0.0, 1.875, 0.0, 2.0]) — silent reinterpretation; this asserts that
5128    /// is now rejected while the byte view is not. See docs/gaps.md GAP-327.
5129    #[test]
5130    fn gap327_realize_f32_on_f64_root_must_reject_not_reinterpret() {
5131        let t =
5132            Tensor::from_f64(vec![1.0_f64, 2.0], Shape::from_dims(&[2]), &Device::cpu()).unwrap();
5133        let graph = t.inner.graph().clone();
5134        let target = t.inner.id();
5135        let device = Device::cpu();
5136
5137        // Silence the panic hook only around the catch_unwind measurements; restore it
5138        // before the assertions so a real failure still prints.
5139        let prev_hook = std::panic::take_hook();
5140        std::panic::set_hook(Box::new(|_| {}));
5141        // Control 1 (RIGHT dtype): realize_f64 on the F64 root succeeds.
5142        let ctl_f64 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f64()));
5143        // Control 2 (guard FIRES): a MISMATCHED guarded accessor panics — proves the harness
5144        // observes guards, so the subject rejection below is real and not a swallowed nothing.
5145        let ctl_f16 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f16()));
5146        // SUBJECT A: realize_f32 on the F64 root must now panic (guard -> Err -> .expect).
5147        let subj_method =
5148            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f32()));
5149        std::panic::set_hook(prev_hook);
5150
5151        // SUBJECT B: realize_one_as::<f32> must return Err(UnexpectedDType) (matched by variant).
5152        let subj_raw = crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &device);
5153        // BYTE VIEW: the unguarded escape hatch must still return the root's RAW bytes
5154        // (2 x f64 = 16 bytes) — a dtype mismatch is NOT an error for the byte view.
5155        let bytes_view = crate::pipelined_bridge::realize_one_bytes(&graph, target, &device);
5156
5157        assert!(
5158            ctl_f64.is_ok(),
5159            "control: realize_f64 on an F64 root must succeed; got {ctl_f64:?}"
5160        );
5161        assert!(
5162            ctl_f16.is_err(),
5163            "positive control: realize_f16() on an F64 root must panic (guard fires); if it \
5164             does not, the harness is not observing panics and the subject asserts are vacuous"
5165        );
5166        assert!(
5167            subj_method.is_err(),
5168            "realize_f32() on an F64 root must REJECT (panic), not silently reinterpret bytes"
5169        );
5170        assert!(
5171            matches!(subj_raw, Err(fuel_ir::Error::UnexpectedDType { .. })),
5172            "realize_one_as::<f32> on an F64 root must return Err(UnexpectedDType), not \
5173             Ok(reinterpreted bytes); got {subj_raw:?}"
5174        );
5175        assert!(
5176            matches!(&bytes_view, Ok(b) if b.len() == 16),
5177            "byte view realize_one_bytes on an F64 [1.0, 2.0] root must return 16 raw bytes \
5178             (unguarded), not an error; got {bytes_view:?}"
5179        );
5180    }
5181
5182    #[test]
5183    fn lazy_tensor_mini_llama_block_forward() {
5184        // A minimal LLaMA-style attention-only "block" built entirely
5185        // through Tensor. No training, just the forward pass:
5186        //
5187        //   h = x + (RmsNorm(x) @ W_qkv → split Q/K/V → RoPE → attention → out proj)
5188        //
5189        // This is the sanity check that every LLaMA primitive is
5190        // reachable through the bridge's API, and that the bridge's
5191        // realize_f32 call actually runs the whole thing.
5192        let seq = 3;
5193        let d_head = 4; // must be even for RoPE
5194        let num_heads = 2;
5195        let d_model = num_heads * d_head; // 8
5196
5197        // Fake input: [1, seq, d_model]
5198        let x_data: Vec<f32> = (0..seq * d_model).map(|i| i as f32 * 0.01).collect();
5199        let x =
5200            Tensor::from_f32(x_data, Shape::from_dims(&[1, seq, d_model]), &Device::cpu()).unwrap();
5201
5202        // Fake weights (just identities for simplicity — makes the
5203        // test easy to verify output finiteness without needing to
5204        // hand-compute).
5205        let w_q = x
5206            .const_f32_like(
5207                identity_matrix(d_model),
5208                Shape::from_dims(&[d_model, d_model]),
5209            )
5210            .unwrap();
5211        let w_k = x
5212            .const_f32_like(
5213                identity_matrix(d_model),
5214                Shape::from_dims(&[d_model, d_model]),
5215            )
5216            .unwrap();
5217        let w_v = x
5218            .const_f32_like(
5219                identity_matrix(d_model),
5220                Shape::from_dims(&[d_model, d_model]),
5221            )
5222            .unwrap();
5223        let w_o = x
5224            .const_f32_like(
5225                identity_matrix(d_model),
5226                Shape::from_dims(&[d_model, d_model]),
5227            )
5228            .unwrap();
5229
5230        // RmsNorm → Q/K/V projection (auto-broadcasting matmul).
5231        let x_norm = x.rms_norm_last_dim(1e-6).unwrap();
5232        let q = x_norm.matmul(&w_q).unwrap();
5233        let k = x_norm.matmul(&w_k).unwrap();
5234        let v = x_norm.matmul(&w_v).unwrap();
5235
5236        // Split heads: [1, seq, 8] → [1, seq, 2, 4] → [1, 2, seq, 4]
5237        let q_h = q
5238            .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5239            .unwrap()
5240            .permute([0, 2, 1, 3_usize])
5241            .unwrap();
5242        let k_h = k
5243            .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5244            .unwrap()
5245            .permute([0, 2, 1, 3_usize])
5246            .unwrap();
5247        let v_h = v
5248            .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5249            .unwrap()
5250            .permute([0, 2, 1, 3_usize])
5251            .unwrap();
5252
5253        // RoPE on Q and K.
5254        let q_r = q_h.rope(10000.0, 0).unwrap();
5255        let k_r = k_h.rope(10000.0, 0).unwrap();
5256
5257        // Scaled dot-product attention.
5258        let k_t = k_r.transpose().unwrap();
5259        let scores = q_r.matmul(&k_t).unwrap();
5260        let attn = scores.softmax_last_dim().unwrap();
5261        let attn_v = attn.matmul(&v_h).unwrap();
5262
5263        // Merge heads + output projection.
5264        let merged = attn_v
5265            .permute([0, 2, 1, 3_usize])
5266            .unwrap()
5267            .reshape(Shape::from_dims(&[1, seq, d_model]))
5268            .unwrap();
5269        let attn_out = merged.matmul(&w_o).unwrap();
5270        let h = x.add(&attn_out).unwrap();
5271
5272        // Realize end-to-end through the bridge.
5273        let result = h.realize_f32();
5274        assert_eq!(result.len(), seq * d_model);
5275        for &v in &result {
5276            assert!(
5277                v.is_finite(),
5278                "bridge-based LLaMA block output non-finite: {v}"
5279            );
5280        }
5281    }
5282
5283    /// Build an identity matrix of size `n × n` in row-major layout.
5284    fn identity_matrix(n: usize) -> Vec<f32> {
5285        let mut out = vec![0.0_f32; n * n];
5286        for i in 0..n {
5287            out[i * n + i] = 1.0;
5288        }
5289        out
5290    }
5291}
5292
5293// Helper method on the wrapper that we didn't include above because the
5294// main struct's `impl` block was getting long. Kept in its own small
5295// `impl` for readability.
5296impl Tensor {
5297    /// Build a second const U32 (index) tensor on the same graph.
5298    pub fn const_u32_like(
5299        &self,
5300        data: impl Into<Arc<[u32]>>,
5301        shape: impl Into<Shape>,
5302    ) -> std::result::Result<Self, fuel_ir::Error> {
5303        Ok(Self {
5304            inner: self.inner.const_u32_like(data, shape)?,
5305        })
5306    }
5307
5308    /// Push a [`fuel_graph::Op::Const`] node on the same graph as
5309    /// `self` **without** populating the graph's storage_map. The
5310    /// caller binds the storage Arc into the realize call via
5311    /// [`InferenceContext::insert`](crate::inference_context::InferenceContext::insert).
5312    ///
5313    /// Used by the Phase E.3.3 forward path to bind pre-allocated
5314    /// KV-cache storage Arcs (`Arc<RwLock<fuel_memory::Storage>>`)
5315    /// into a per-step graph — the graph's legacy storage_map only
5316    /// holds `fuel_backend_contract::Storage`, so direct binding isn't
5317    /// possible without a type conversion.
5318    pub fn const_placeholder_like(&self, shape: impl Into<Shape>, dtype: fuel_ir::DType) -> Self {
5319        Self {
5320            inner: self.inner.const_placeholder_like(shape, dtype),
5321        }
5322    }
5323
5324    /// Append an [`fuel_graph::Op::WriteSlice`] node. Copies `source`'s
5325    /// bytes into `self` at the rectangular slab defined by `ranges`
5326    /// and returns a tensor whose Storage Arc is `self`'s — i.e. the
5327    /// post-write reference to the same underlying buffer.
5328    ///
5329    /// Destructive on `self`: after the write, downstream consumers
5330    /// must read the bytes through the returned tensor's NodeId, not
5331    /// `self`'s.
5332    ///
5333    /// **Returns `Result`**: rank/shape/range mismatches surface as a
5334    /// typed error.
5335    pub fn write_slice(&self, source: &Self, ranges: Vec<(usize, usize)>) -> crate::Result<Self> {
5336        let inner = self.inner.write_slice(&source.inner, ranges)?;
5337        Ok(Self { inner })
5338    }
5339
5340    /// Append an [`fuel_graph::Op::WriteSlice`] whose start on `dyn_axis`
5341    /// is a **runtime** value resolved through the per-pass `SymEnv` at
5342    /// realize (Phase D symbolic extents). `ranges[dyn_axis].0` is
5343    /// ignored (the start is dynamic); the slab width
5344    /// `ranges[dyn_axis].1 - ranges[dyn_axis].0` must equal `source`'s
5345    /// `dyn_axis` dim and not exceed the destination capacity. Backs the
5346    /// persistent decode KV-cache write at the per-token `cached_len`.
5347    pub fn write_slice_dyn(
5348        &self,
5349        source: &Self,
5350        ranges: Vec<(usize, usize)>,
5351        dyn_axis: usize,
5352        offset: fuel_ir::DynScalar,
5353    ) -> crate::Result<Self> {
5354        let inner = self
5355            .inner
5356            .write_slice_dyn(&source.inner, ranges, dyn_axis, offset)?;
5357        Ok(Self { inner })
5358    }
5359
5360    /// Append an [`fuel_graph::Op::WriteSliceRotating`] node — like
5361    /// [`Self::write_slice`] but the `axis` axis wraps modulo
5362    /// `modulus`. `position` is a rank-0 U32 tensor whose value (read
5363    /// at realize time) is wrapped modulo `modulus` to determine the
5364    /// dynamic write start on `axis`. `ranges[axis].0` is ignored
5365    /// (the rotating-axis start is dynamic); the slab width
5366    /// `ranges[axis].1 - ranges[axis].0` must equal `source`'s
5367    /// `axis` dim and must not exceed `modulus`.
5368    ///
5369    /// Destructive on `self`: same scheduling as `write_slice`.
5370    /// Backs sliding-window KV caches (Mistral / Phi-3 sliding-
5371    /// window). Returns `Result`: rank / dtype / axis-bound /
5372    /// modulus / range mismatches surface as typed errors at
5373    /// build time.
5374    pub fn write_slice_rotating(
5375        &self,
5376        source: &Self,
5377        position: &Self,
5378        axis: usize,
5379        modulus: usize,
5380        ranges: Vec<(usize, usize)>,
5381    ) -> crate::Result<Self> {
5382        let inner = self.inner.write_slice_rotating(
5383            &source.inner,
5384            &position.inner,
5385            axis,
5386            modulus,
5387            ranges,
5388        )?;
5389        Ok(Self { inner })
5390    }
5391
5392    /// Append a [`fuel_graph::Op::WriteSliceDoff`] node — write `source`
5393    /// into a slab of `self` whose start on `axis` is read from `offset`
5394    /// (a rank-0 `I64` tensor) **device-side** at kernel launch. No
5395    /// modulo wrap: `offset` is the raw start on `axis`.
5396    /// `ranges[axis].0` is ignored; `ranges[axis].1 - ranges[axis].0`
5397    /// is the write width on `axis` and must equal `source`'s `axis`
5398    /// dim. Bounds (`offset + width <= dest_dims[axis]`) are the
5399    /// caller's contract (device-only at build). Destructive on `self`;
5400    /// same scheduling as `write_slice`. Backs the CUDA-graph-
5401    /// capturable KV-cache append (CapturedRun / `DecodeSession`).
5402    /// Returns `Result`: rank / dtype / axis-bound / static-width /
5403    /// offset-dtype mismatches surface as typed errors at build time.
5404    pub fn write_slice_doff(
5405        &self,
5406        source: &Self,
5407        offset: &Self,
5408        axis: usize,
5409        ranges: Vec<(usize, usize)>,
5410    ) -> crate::Result<Self> {
5411        let inner = self
5412            .inner
5413            .write_slice_doff(&source.inner, &offset.inner, axis, ranges)?;
5414        Ok(Self { inner })
5415    }
5416
5417    /// Append a [`fuel_graph::Op::Fused`] node carrying
5418    /// [`fuel_graph::registry::FusedOpParams::Conv2D`]. See `fuel_graph`'s
5419    /// `Tensor::conv2d` for the full shape contract: `self` must be
5420    /// `[N, Cin, H, W]`; `weight` must be `[Cout, Cin/groups, Kh, Kw]`;
5421    /// `bias` is optional and must be `[Cout]` when provided. Returns
5422    /// a rank-4 lazy tensor `[N, Cout, Hout, Wout]`.
5423    ///
5424    /// Rank / channel / `groups` / stride mismatches surface as typed
5425    /// errors at build time rather than panicking inside the inner
5426    /// `fuel_graph` call.
5427    pub fn conv2d(
5428        &self,
5429        weight: &Self,
5430        bias: Option<&Self>,
5431        stride: (usize, usize),
5432        padding: (usize, usize),
5433        groups: usize,
5434    ) -> std::result::Result<Self, fuel_ir::Error> {
5435        if groups < 1 {
5436            return Err(
5437                fuel_ir::Error::Msg(format!("conv2d: groups must be >= 1, got {groups}",)).bt(),
5438            );
5439        }
5440        let x_shape = self.inner.shape();
5441        let x_dims = x_shape.dims();
5442        let w_shape = weight.inner.shape();
5443        let w_dims = w_shape.dims();
5444        if x_dims.len() != 4 {
5445            return Err(fuel_ir::Error::Msg(format!(
5446                "conv2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5447            ))
5448            .bt());
5449        }
5450        if w_dims.len() != 4 {
5451            return Err(fuel_ir::Error::Msg(format!(
5452                "conv2d: weight must be rank 4 [Cout, Cin/groups, Kh, Kw], got {w_dims:?}",
5453            ))
5454            .bt());
5455        }
5456        let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5457        let (cout, cin_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5458        if cin != cin_per_g * groups {
5459            return Err(fuel_ir::Error::Msg(format!(
5460                "conv2d: x has {cin} in-channels but weight expects {} ({cin_per_g}*{groups})",
5461                cin_per_g * groups,
5462            ))
5463            .bt());
5464        }
5465        if cout % groups != 0 {
5466            return Err(fuel_ir::Error::Msg(format!(
5467                "conv2d: Cout={cout} must be divisible by groups={groups}",
5468            ))
5469            .bt());
5470        }
5471        if let Some(b) = bias {
5472            let b_shape = b.inner.shape();
5473            let b_dims = b_shape.dims();
5474            if b_dims != [cout] {
5475                return Err(fuel_ir::Error::Msg(format!(
5476                    "conv2d: bias shape {b_dims:?} must match [Cout={cout}]",
5477                ))
5478                .bt());
5479            }
5480        }
5481        let (stride_h, stride_w) = stride;
5482        let (pad_h, pad_w) = padding;
5483        if stride_h < 1 || stride_w < 1 {
5484            return Err(fuel_ir::Error::Msg(format!(
5485                "conv2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5486            ))
5487            .bt());
5488        }
5489        let h_padded = h_in + 2 * pad_h;
5490        let w_padded = w_in + 2 * pad_w;
5491        if h_padded < kh || w_padded < kw {
5492            return Err(fuel_ir::Error::Msg(format!(
5493                "conv2d: padded input ({h_padded}x{w_padded}) smaller than kernel ({kh}x{kw})",
5494            ))
5495            .bt());
5496        }
5497        Ok(Self {
5498            inner: self.inner.conv2d(
5499                &weight.inner,
5500                bias.map(|b| &b.inner),
5501                stride,
5502                padding,
5503                groups,
5504            ),
5505        })
5506    }
5507
5508    /// Append a [`fuel_graph::Op::Fused`] node carrying
5509    /// [`fuel_graph::registry::FusedOpParams::FlashAttn`]. `self` is `q`
5510    /// of shape `[B, Hq, Sq, D]`; `k` and `v` are `[B, Hkv, Sk, D]`
5511    /// with `Hq` a multiple of `Hkv` (GQA). `alibi_slopes` (optional)
5512    /// is `[Hq]`. Returns the attention output, shape `[B, Hq, Sq, D]`.
5513    ///
5514    /// Rank / batch / GQA-divisibility / head-dim mismatches surface
5515    /// as typed errors at build time rather than panicking inside the
5516    /// inner `fuel_graph` call.
5517    pub fn flash_attn(
5518        &self,
5519        k: &Self,
5520        v: &Self,
5521        alibi_slopes: Option<&Self>,
5522        softmax_scale: f32,
5523        causal: bool,
5524        window_size_left: Option<usize>,
5525        window_size_right: Option<usize>,
5526        softcap: Option<f32>,
5527    ) -> std::result::Result<Self, fuel_ir::Error> {
5528        let q_shape = self.inner.shape();
5529        let q_dims = q_shape.dims();
5530        let k_shape = k.inner.shape();
5531        let k_dims = k_shape.dims();
5532        let v_shape = v.inner.shape();
5533        let v_dims = v_shape.dims();
5534        if q_dims.len() != 4 {
5535            return Err(fuel_ir::Error::Msg(format!(
5536                "flash_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5537            ))
5538            .bt());
5539        }
5540        if k_dims.len() != 4 {
5541            return Err(fuel_ir::Error::Msg(format!(
5542                "flash_attn: k must be rank 4 [B, Hkv, Sk, D], got {k_dims:?}",
5543            ))
5544            .bt());
5545        }
5546        if v_dims.len() != 4 {
5547            return Err(fuel_ir::Error::Msg(format!(
5548                "flash_attn: v must be rank 4 [B, Hkv, Sk, D], got {v_dims:?}",
5549            ))
5550            .bt());
5551        }
5552        let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5553        let (bk, hkv, sk, dk) = (k_dims[0], k_dims[1], k_dims[2], k_dims[3]);
5554        let (bv, hkv_v, sk_v, dv) = (v_dims[0], v_dims[1], v_dims[2], v_dims[3]);
5555        if b != bk || b != bv {
5556            return Err(fuel_ir::Error::Msg(
5557                format!("flash_attn: B mismatch q={b} k={bk} v={bv}",),
5558            )
5559            .bt());
5560        }
5561        if hkv != hkv_v {
5562            return Err(fuel_ir::Error::Msg(format!(
5563                "flash_attn: Hkv mismatch k={hkv} vs v={hkv_v}",
5564            ))
5565            .bt());
5566        }
5567        if sk != sk_v {
5568            return Err(fuel_ir::Error::Msg(
5569                format!("flash_attn: Sk mismatch k={sk} vs v={sk_v}",),
5570            )
5571            .bt());
5572        }
5573        if d != dk || d != dv {
5574            return Err(fuel_ir::Error::Msg(format!(
5575                "flash_attn: head_dim mismatch q={d} k={dk} v={dv}",
5576            ))
5577            .bt());
5578        }
5579        if hkv == 0 || hq % hkv != 0 {
5580            return Err(fuel_ir::Error::Msg(format!(
5581                "flash_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5582            ))
5583            .bt());
5584        }
5585        if let Some(a) = alibi_slopes {
5586            let a_shape = a.inner.shape();
5587            let a_dims = a_shape.dims();
5588            if a_dims != [hq] {
5589                return Err(fuel_ir::Error::Msg(format!(
5590                    "flash_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5591                ))
5592                .bt());
5593            }
5594        }
5595        Ok(Self {
5596            inner: self.inner.flash_attn(
5597                &k.inner,
5598                &v.inner,
5599                alibi_slopes.map(|t| &t.inner),
5600                softmax_scale,
5601                causal,
5602                window_size_left,
5603                window_size_right,
5604                softcap,
5605            ),
5606        })
5607    }
5608
5609    /// Append a [`fuel_graph::Op::Fused`] node carrying
5610    /// [`fuel_graph::registry::FusedOpParams::PagedAttn`]. `self` is the Q
5611    /// tensor `[B, Hq, Sq, D]`. `k_cache` / `v_cache` are paged caches
5612    /// `[num_blocks, block_size, Hkv, D]`. `block_table` is `[B,
5613    /// max_blocks]` u32; `context_lens` is `[B]` u32.
5614    ///
5615    /// Rank / batch / GQA-divisibility / block-size / dtype mismatches
5616    /// surface as typed errors at build time rather than panicking
5617    /// inside the inner `fuel_graph` call.
5618    pub fn paged_attn(
5619        &self,
5620        k_cache: &Self,
5621        v_cache: &Self,
5622        block_table: &Self,
5623        context_lens: &Self,
5624        alibi_slopes: Option<&Self>,
5625        softmax_scale: f32,
5626        block_size: usize,
5627        softcap: Option<f32>,
5628    ) -> std::result::Result<Self, fuel_ir::Error> {
5629        if block_size < 1 {
5630            return Err(fuel_ir::Error::Msg("paged_attn: block_size must be >= 1".into()).bt());
5631        }
5632        let q_shape = self.inner.shape();
5633        let q_dims = q_shape.dims();
5634        let kc_shape = k_cache.inner.shape();
5635        let kc_dims = kc_shape.dims();
5636        let vc_shape = v_cache.inner.shape();
5637        let vc_dims = vc_shape.dims();
5638        let bt_shape = block_table.inner.shape();
5639        let bt_dims = bt_shape.dims();
5640        let cl_shape = context_lens.inner.shape();
5641        let cl_dims = cl_shape.dims();
5642        if q_dims.len() != 4 {
5643            return Err(fuel_ir::Error::Msg(format!(
5644                "paged_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5645            ))
5646            .bt());
5647        }
5648        if kc_dims.len() != 4 {
5649            return Err(fuel_ir::Error::Msg(format!(
5650                "paged_attn: k_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {kc_dims:?}",
5651            )).bt());
5652        }
5653        if vc_dims.len() != 4 {
5654            return Err(fuel_ir::Error::Msg(format!(
5655                "paged_attn: v_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {vc_dims:?}",
5656            )).bt());
5657        }
5658        if bt_dims.len() != 2 {
5659            return Err(fuel_ir::Error::Msg(format!(
5660                "paged_attn: block_table must be rank 2 [B, max_blocks], got {bt_dims:?}",
5661            ))
5662            .bt());
5663        }
5664        if cl_dims.len() != 1 {
5665            return Err(fuel_ir::Error::Msg(format!(
5666                "paged_attn: context_lens must be rank 1 [B], got {cl_dims:?}",
5667            ))
5668            .bt());
5669        }
5670        let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5671        if kc_dims[1] != block_size {
5672            return Err(fuel_ir::Error::Msg(format!(
5673                "paged_attn: k_cache block dim {} != block_size {block_size}",
5674                kc_dims[1],
5675            ))
5676            .bt());
5677        }
5678        if vc_dims[1] != block_size {
5679            return Err(fuel_ir::Error::Msg(format!(
5680                "paged_attn: v_cache block dim {} != block_size {block_size}",
5681                vc_dims[1],
5682            ))
5683            .bt());
5684        }
5685        let hkv = kc_dims[2];
5686        if vc_dims[2] != hkv {
5687            return Err(fuel_ir::Error::Msg(format!(
5688                "paged_attn: Hkv mismatch k_cache={hkv} vs v_cache={}",
5689                vc_dims[2],
5690            ))
5691            .bt());
5692        }
5693        if kc_dims[3] != d || vc_dims[3] != d {
5694            return Err(fuel_ir::Error::Msg(format!(
5695                "paged_attn: D mismatch q={d} k={} v={}",
5696                kc_dims[3], vc_dims[3],
5697            ))
5698            .bt());
5699        }
5700        if hkv == 0 || hq % hkv != 0 {
5701            return Err(fuel_ir::Error::Msg(format!(
5702                "paged_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5703            ))
5704            .bt());
5705        }
5706        if bt_dims[0] != b {
5707            return Err(fuel_ir::Error::Msg(format!(
5708                "paged_attn: block_table batch dim {} != B={b}",
5709                bt_dims[0],
5710            ))
5711            .bt());
5712        }
5713        if cl_dims[0] != b {
5714            return Err(fuel_ir::Error::Msg(format!(
5715                "paged_attn: context_lens len {} != B={b}",
5716                cl_dims[0],
5717            ))
5718            .bt());
5719        }
5720        if block_table.inner.dtype() != fuel_ir::DType::U32 {
5721            return Err(fuel_ir::Error::Msg(format!(
5722                "paged_attn: block_table must be U32, got {:?}",
5723                block_table.inner.dtype(),
5724            ))
5725            .bt());
5726        }
5727        if context_lens.inner.dtype() != fuel_ir::DType::U32 {
5728            return Err(fuel_ir::Error::Msg(format!(
5729                "paged_attn: context_lens must be U32, got {:?}",
5730                context_lens.inner.dtype(),
5731            ))
5732            .bt());
5733        }
5734        if let Some(a) = alibi_slopes {
5735            let a_shape = a.inner.shape();
5736            let a_dims = a_shape.dims();
5737            if a_dims != [hq] {
5738                return Err(fuel_ir::Error::Msg(format!(
5739                    "paged_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5740                ))
5741                .bt());
5742            }
5743        }
5744        Ok(Self {
5745            inner: self.inner.paged_attn(
5746                &k_cache.inner,
5747                &v_cache.inner,
5748                &block_table.inner,
5749                &context_lens.inner,
5750                alibi_slopes.map(|t| &t.inner),
5751                softmax_scale,
5752                block_size,
5753                softcap,
5754            ),
5755        })
5756    }
5757
5758    /// Append a [`fuel_graph::Op::Fused`] node carrying
5759    /// [`fuel_graph::registry::FusedOpParams::ConvTranspose2D`]. `self` must
5760    /// be `[N, Cin, H, W]`; `weight` must be `[Cin, Cout/groups, Kh, Kw]`
5761    /// (note transposed channel order vs `conv2d`). Returns a rank-4
5762    /// lazy tensor `[N, Cout, Hout, Wout]`.
5763    ///
5764    /// Rank / channel / `groups` / stride / dilation mismatches surface
5765    /// as typed errors at build time rather than panicking inside the
5766    /// inner `fuel_graph` call.
5767    pub fn conv_transpose2d(
5768        &self,
5769        weight: &Self,
5770        stride: (usize, usize),
5771        padding: (usize, usize),
5772        output_padding: (usize, usize),
5773        dilation: (usize, usize),
5774        groups: usize,
5775    ) -> std::result::Result<Self, fuel_ir::Error> {
5776        if groups < 1 {
5777            return Err(fuel_ir::Error::Msg(format!(
5778                "conv_transpose2d: groups must be >= 1, got {groups}",
5779            ))
5780            .bt());
5781        }
5782        let x_shape = self.inner.shape();
5783        let x_dims = x_shape.dims();
5784        let w_shape = weight.inner.shape();
5785        let w_dims = w_shape.dims();
5786        if x_dims.len() != 4 {
5787            return Err(fuel_ir::Error::Msg(format!(
5788                "conv_transpose2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5789            ))
5790            .bt());
5791        }
5792        if w_dims.len() != 4 {
5793            return Err(fuel_ir::Error::Msg(format!(
5794                "conv_transpose2d: weight must be rank 4 [Cin, Cout/groups, Kh, Kw], got {w_dims:?}",
5795            )).bt());
5796        }
5797        let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5798        let (cin_w, cout_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5799        if cin != cin_w {
5800            return Err(fuel_ir::Error::Msg(format!(
5801                "conv_transpose2d: x has {cin} in-channels but weight has {cin_w}",
5802            ))
5803            .bt());
5804        }
5805        if cin % groups != 0 {
5806            return Err(fuel_ir::Error::Msg(format!(
5807                "conv_transpose2d: Cin={cin} must be divisible by groups={groups}",
5808            ))
5809            .bt());
5810        }
5811        let (stride_h, stride_w) = stride;
5812        let (pad_h, pad_w) = padding;
5813        let (out_pad_h, out_pad_w) = output_padding;
5814        let (dil_h, dil_w) = dilation;
5815        if stride_h < 1 || stride_w < 1 {
5816            return Err(fuel_ir::Error::Msg(format!(
5817                "conv_transpose2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5818            ))
5819            .bt());
5820        }
5821        if dil_h < 1 || dil_w < 1 {
5822            return Err(fuel_ir::Error::Msg(format!(
5823                "conv_transpose2d: dilation must be >= 1, got ({dil_h}, {dil_w})",
5824            ))
5825            .bt());
5826        }
5827        let h_out = h_in.saturating_sub(1) * stride_h + dil_h * (kh - 1) + out_pad_h + 1;
5828        let w_out = w_in.saturating_sub(1) * stride_w + dil_w * (kw - 1) + out_pad_w + 1;
5829        if h_out <= 2 * pad_h || w_out <= 2 * pad_w {
5830            return Err(fuel_ir::Error::Msg(format!(
5831                "conv_transpose2d: padding ({pad_h}x{pad_w}) is larger than the produced output dims ({h_out}x{w_out})",
5832            )).bt());
5833        }
5834        let _ = cout_per_g;
5835        Ok(Self {
5836            inner: self.inner.conv_transpose2d(
5837                &weight.inner,
5838                stride,
5839                padding,
5840                output_padding,
5841                dilation,
5842                groups,
5843            ),
5844        })
5845    }
5846
5847    /// Append a transposed 1D convolution. `self` is
5848    /// `[N, Cin, Lin]`; `weight` is `[Cin, Cout/groups, K]`
5849    /// (PyTorch channel order). Returns `[N, Cout, Lout]`.
5850    ///
5851    /// Internally lifts to rank-4 and dispatches through
5852    /// `conv_transpose2d` — there is no separate 1D op in the
5853    /// IR; the lift is transparent to the executor (which sees
5854    /// the same `Op::Fused(CONV_TRANSPOSE2D, _)` it already
5855    /// dispatches CPU kernels for).
5856    ///
5857    /// Unblocks audio codec decoders (DAC, EnCodec, SNAC, Mimi,
5858    /// Parler-TTS, MetaVoice, CSM) which all upsample quantized
5859    /// latents to waveform via strided transposed convs.
5860    pub fn conv_transpose1d(
5861        &self,
5862        weight: &Self,
5863        stride: usize,
5864        padding: usize,
5865        output_padding: usize,
5866        dilation: usize,
5867        groups: usize,
5868    ) -> std::result::Result<Self, fuel_ir::Error> {
5869        if groups < 1 {
5870            return Err(fuel_ir::Error::Msg(format!(
5871                "conv_transpose1d: groups must be >= 1, got {groups}",
5872            ))
5873            .bt());
5874        }
5875        let x_shape = self.inner.shape();
5876        let x_dims = x_shape.dims();
5877        let w_shape = weight.inner.shape();
5878        let w_dims = w_shape.dims();
5879        if x_dims.len() != 3 {
5880            return Err(fuel_ir::Error::Msg(format!(
5881                "conv_transpose1d: x must be rank 3 [N, Cin, Lin], got {x_dims:?}",
5882            ))
5883            .bt());
5884        }
5885        if w_dims.len() != 3 {
5886            return Err(fuel_ir::Error::Msg(format!(
5887                "conv_transpose1d: weight must be rank 3 [Cin, Cout/groups, K], got {w_dims:?}",
5888            ))
5889            .bt());
5890        }
5891        if stride < 1 {
5892            return Err(fuel_ir::Error::Msg(format!(
5893                "conv_transpose1d: stride must be >= 1, got {stride}",
5894            ))
5895            .bt());
5896        }
5897        if dilation < 1 {
5898            return Err(fuel_ir::Error::Msg(format!(
5899                "conv_transpose1d: dilation must be >= 1, got {dilation}",
5900            ))
5901            .bt());
5902        }
5903        let cin = x_dims[1];
5904        let cin_w = w_dims[0];
5905        if cin != cin_w {
5906            return Err(fuel_ir::Error::Msg(format!(
5907                "conv_transpose1d: x has {cin} in-channels but weight has {cin_w}",
5908            ))
5909            .bt());
5910        }
5911        if !cin.is_multiple_of(groups) {
5912            return Err(fuel_ir::Error::Msg(format!(
5913                "conv_transpose1d: Cin={cin} must be divisible by groups={groups}",
5914            ))
5915            .bt());
5916        }
5917        Ok(Self {
5918            inner: self.inner.conv_transpose1d(
5919                &weight.inner,
5920                stride,
5921                padding,
5922                output_padding,
5923                dilation,
5924                groups,
5925            ),
5926        })
5927    }
5928}
5929
5930// ============================================================================
5931// Phase A.1 — wrapper additions (eager-`Tensor` retirement program).
5932//
5933// Methods on `fuel_graph::NodeHandle` that weren't previously surfaced through
5934// `Tensor`. Pure delegation; no new graph ops. See
5935// `docs/session-prompts/eager-tensor-retirement-master-plan.md`.
5936// ============================================================================
5937
5938impl Tensor {
5939    // ---- shape ops: unsqueeze (Result + Dim) + Result-returning siblings ----
5940
5941    /// Append a size-1 dimension at position `dim`. Inverse of
5942    /// [`Self::squeeze`]. Accepts any [`Dim`] (`usize`, `D::Minus1`,
5943    /// etc.). Bad `dim` surfaces as a typed error at build time.
5944    pub fn unsqueeze<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5945        let shape = self.inner.shape();
5946        let dim = dim.to_index_plus_one(&shape, "unsqueeze")?;
5947        Ok(Self {
5948            inner: self.inner.try_unsqueeze(dim)?,
5949        })
5950    }
5951
5952    // ---- triangular masking (canonical attention masks) ----
5953
5954    /// Upper-triangular mask along the last two dims. `diagonal = 0`
5955    /// keeps the main diagonal and above; positive shifts higher.
5956    pub fn triu(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5957        Ok(Self {
5958            inner: self.inner.triu(diagonal)?,
5959        })
5960    }
5961
5962    /// Lower-triangular mask along the last two dims. `tril(0)` is the
5963    /// canonical causal-attention mask.
5964    pub fn tril(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5965        Ok(Self {
5966            inner: self.inner.tril(diagonal)?,
5967        })
5968    }
5969
5970    // ---- additional reductions / activations ----
5971
5972    /// `log(softmax(self))` along the last dim, fused into one op.
5973    pub fn log_softmax_last_dim(&self) -> std::result::Result<Self, fuel_ir::Error> {
5974        Ok(Self {
5975            inner: self.inner.log_softmax_last_dim()?,
5976        })
5977    }
5978
5979    /// Numerically-stable softmax along an arbitrary axis. Accepts any
5980    /// [`Dim`]. Decomposes into `max_keepdim` / `broadcast_sub` / `exp` /
5981    /// `sum_keepdim` / `broadcast_div`, all of which already accept
5982    /// `D: Dim`, so this is a pure composition with no new graph op.
5983    ///
5984    /// When `dim` resolves to the last axis, prefer
5985    /// [`Self::softmax_last_dim`], which dispatches to the fused
5986    /// `SoftmaxLastDim` op (single kernel rather than five graph nodes).
5987    pub fn softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5988        let shape = self.inner.shape();
5989        // Resolve once to a concrete `usize` so we can pass it to each
5990        // composed call (the `Dim` trait doesn't require `Copy`, so we
5991        // can't reuse the generic `D` across multiple calls).
5992        let axis: usize = dim.to_index(&shape, "softmax")?;
5993        let m = self.max_keepdim(axis)?;
5994        let shifted = self.broadcast_sub(&m)?;
5995        let e = shifted.exp();
5996        let s = e.sum_keepdim(axis)?;
5997        e.broadcast_div(&s)
5998    }
5999
6000    /// Numerically-stable `log(softmax(self))` along an arbitrary axis.
6001    /// Accepts any [`Dim`]. Computes `x - max - log(sum(exp(x - max)))`
6002    /// — the standard log-sum-exp form, which avoids the explicit
6003    /// `softmax`-then-`log` underflow path. Pure composition over
6004    /// existing primitives.
6005    ///
6006    /// When `dim` resolves to the last axis, prefer
6007    /// [`Self::log_softmax_last_dim`], which dispatches to the fused
6008    /// `LogSoftmaxLastDim` op.
6009    pub fn log_softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6010        let shape = self.inner.shape();
6011        let axis: usize = dim.to_index(&shape, "log_softmax")?;
6012        let m = self.max_keepdim(axis)?;
6013        let shifted = self.broadcast_sub(&m)?;
6014        let lse = shifted.exp().sum_keepdim(axis)?.log();
6015        shifted.broadcast_sub(&lse)
6016    }
6017
6018    /// Argmin along `dim`, returning a U32 tensor with the reduced dim
6019    /// removed. Non-differentiable. Bad `dim` surfaces as a typed
6020    /// error at build time. Accepts any [`Dim`].
6021    pub fn argmin_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6022        let shape = self.inner.shape();
6023        let dim = dim.to_index(&shape, "argmin_dim")?;
6024        Ok(Self {
6025            inner: self.inner.argmin_dim(dim),
6026        })
6027    }
6028
6029    // ---- masking / scatter ----
6030
6031    /// Fill every position where `mask != 0` with `value`; pass `self`
6032    /// through everywhere `mask == 0`. `mask` must be U8 with the same
6033    /// shape as `self`; `value`'s dtype must match `self`.
6034    pub fn masked_fill(
6035        &self,
6036        mask: &Self,
6037        value: fuel_ir::Scalar,
6038    ) -> std::result::Result<Self, fuel_ir::Error> {
6039        Ok(Self {
6040            inner: self.inner.masked_fill(&mask.inner, value)?,
6041        })
6042    }
6043
6044    /// `self + scatter(indices, src, dim=dim)` — accumulate `src` rows
6045    /// at positions named by `indices` along `dim`. `indices` is rank-1
6046    /// U32 with length equal to `src.dims()[dim]`. Accepts any [`Dim`].
6047    /// Dim bounds / index dtype / shape / dtype-parity mismatches
6048    /// surface as typed errors at build time.
6049    pub fn index_add<D: Dim>(
6050        &self,
6051        dim: D,
6052        indices: &Self,
6053        src: &Self,
6054    ) -> std::result::Result<Self, fuel_ir::Error> {
6055        let shape = self.inner.shape();
6056        let dim = dim.to_index(&shape, "index_add")?;
6057        if indices.inner.dtype() != fuel_ir::DType::U32 {
6058            return Err(fuel_ir::Error::Msg(format!(
6059                "index_add: index must be U32, got {:?}",
6060                indices.inner.dtype(),
6061            ))
6062            .bt());
6063        }
6064        if self.inner.dtype() != src.inner.dtype() {
6065            return Err(fuel_ir::Error::Msg(format!(
6066                "index_add: base and src dtypes must match, got {:?} vs {:?}",
6067                self.inner.dtype(),
6068                src.inner.dtype(),
6069            ))
6070            .bt());
6071        }
6072        let base_dims = shape.dims();
6073        let src_shape = src.inner.shape();
6074        let src_dims = src_shape.dims();
6075        if base_dims.len() != src_dims.len() {
6076            return Err(fuel_ir::Error::Msg(format!(
6077                "index_add: base and src must have the same rank, got {} vs {}",
6078                base_dims.len(),
6079                src_dims.len(),
6080            ))
6081            .bt());
6082        }
6083        let idx_shape = indices.inner.shape();
6084        let idx_dims = idx_shape.dims();
6085        if idx_dims.len() != 1 {
6086            return Err(fuel_ir::Error::Msg(format!(
6087                "index_add: index must be rank 1, got {idx_dims:?}",
6088            ))
6089            .bt());
6090        }
6091        if src_dims[dim] != idx_dims[0] {
6092            return Err(fuel_ir::Error::Msg(format!(
6093                "index_add: src dim {dim} ({}) must match index length ({})",
6094                src_dims[dim], idx_dims[0],
6095            ))
6096            .bt());
6097        }
6098        Ok(Self {
6099            inner: self.inner.index_add(dim, &indices.inner, &src.inner),
6100        })
6101    }
6102
6103    /// Functional inverse of [`Self::gather`]. Accumulates `src` into
6104    /// `self` at positions given by `indices` (substituted at `dim`).
6105    /// Accepts any [`Dim`]. Dim bounds / index dtype / shape / dtype-
6106    /// parity mismatches surface as typed errors at build time.
6107    pub fn scatter_add<D: Dim>(
6108        &self,
6109        dim: D,
6110        indices: &Self,
6111        src: &Self,
6112    ) -> std::result::Result<Self, fuel_ir::Error> {
6113        let shape = self.inner.shape();
6114        let dim = dim.to_index(&shape, "scatter_add")?;
6115        if indices.inner.dtype() != fuel_ir::DType::U32 {
6116            return Err(fuel_ir::Error::Msg(format!(
6117                "scatter_add: index must be U32, got {:?}",
6118                indices.inner.dtype(),
6119            ))
6120            .bt());
6121        }
6122        if self.inner.dtype() != src.inner.dtype() {
6123            return Err(fuel_ir::Error::Msg(format!(
6124                "scatter_add: base and src dtypes must match, got {:?} vs {:?}",
6125                self.inner.dtype(),
6126                src.inner.dtype(),
6127            ))
6128            .bt());
6129        }
6130        let idx_shape = indices.inner.shape();
6131        let src_shape = src.inner.shape();
6132        if idx_shape.dims() != src_shape.dims() {
6133            return Err(fuel_ir::Error::Msg(format!(
6134                "scatter_add: index and src must have the same shape, got {:?} vs {:?}",
6135                idx_shape.dims(),
6136                src_shape.dims(),
6137            ))
6138            .bt());
6139        }
6140        Ok(Self {
6141            inner: self.inner.scatter_add(dim, &indices.inner, &src.inner),
6142        })
6143    }
6144
6145    // ---- in-place activations (Phase 4-5 infrastructure, now surfaced) ----
6146    //
6147    // These mutate `self`'s storage in place. Safe to call on
6148    // tape-tracked tensors after Phase 4's view-aware ordering pass and
6149    // Phase 5's auto-copy pass. See `project_inplace_ops_complete`
6150    // memory entry.
6151
6152    /// In-place `max(0, self)`. See [`Self::relu`] for the functional
6153    /// variant.
6154    pub fn relu_inplace(&self) -> Self {
6155        Self {
6156            inner: self.inner.relu_inplace(),
6157        }
6158    }
6159
6160    /// In-place `self * sigmoid(self)`. See [`Self::silu`] for the
6161    /// functional variant.
6162    pub fn silu_inplace(&self) -> Self {
6163        Self {
6164            inner: self.inner.silu_inplace(),
6165        }
6166    }
6167
6168    /// In-place tanh-approximation GELU. See [`Self::gelu_tanh`] for the
6169    /// functional variant.
6170    pub fn gelu_tanh_inplace(&self) -> Self {
6171        Self {
6172            inner: self.inner.gelu_tanh_inplace(),
6173        }
6174    }
6175
6176    /// In-place `tanh(self)`. See [`Self::tanh`] for the functional
6177    /// variant.
6178    pub fn tanh_inplace(&self) -> Self {
6179        Self {
6180            inner: self.inner.tanh_inplace(),
6181        }
6182    }
6183
6184    /// In-place `sigmoid(self)`. See [`Self::sigmoid`] for the
6185    /// functional variant.
6186    pub fn sigmoid_inplace(&self) -> Self {
6187        Self {
6188            inner: self.inner.sigmoid_inplace(),
6189        }
6190    }
6191
6192    /// In-place `self = mul · self + add`. Single fused-op equivalent
6193    /// of `self.mul_scalar(mul).add_scalar(add)` plus reassignment.
6194    pub fn affine_inplace(&self, mul: f64, add: f64) -> Self {
6195        Self {
6196            inner: self.inner.affine_inplace(mul, add),
6197        }
6198    }
6199
6200    // ---- additional const_*_like factories ----
6201
6202    /// Build a sibling F64 `Const` on the same graph as `self`.
6203    pub fn const_f64_like(
6204        &self,
6205        data: impl Into<Arc<[f64]>>,
6206        shape: impl Into<Shape>,
6207    ) -> std::result::Result<Self, fuel_ir::Error> {
6208        Ok(Self {
6209            inner: self.inner.const_f64_like(data, shape)?,
6210        })
6211    }
6212
6213    /// Build a sibling I64 `Const` on the same graph. Used by integer-
6214    /// target ops (e.g. cross-entropy with PyTorch-convention class
6215    /// indices).
6216    pub fn const_i64_like(
6217        &self,
6218        data: impl Into<Arc<[i64]>>,
6219        shape: impl Into<Shape>,
6220    ) -> std::result::Result<Self, fuel_ir::Error> {
6221        Ok(Self {
6222            inner: self.inner.const_i64_like(data, shape)?,
6223        })
6224    }
6225
6226    // ---- device residency control ----
6227
6228    /// Pin this tensor's realized storage to `device`. Consumes `self`
6229    /// because the placement is a graph-level annotation tied to the
6230    /// node id rather than a side-effecting operation.
6231    pub fn on_device(self, device: &Device) -> Self {
6232        Self {
6233            inner: self.inner.on_device(device.location()),
6234        }
6235    }
6236
6237    /// Append an `Op::Release` node — explicitly drop this tensor's
6238    /// device-resident storage once the ordering pass has scheduled
6239    /// every reader before it.
6240    pub fn release(&self) -> Self {
6241        Self {
6242            inner: self.inner.release(),
6243        }
6244    }
6245
6246    /// Move bytes to `device`, destroying the source. Use when the
6247    /// source is genuinely dead after the transfer.
6248    pub fn move_to_device(&self, device: &Device) -> Self {
6249        Self {
6250            inner: self.inner.move_to_device(device.location()),
6251        }
6252    }
6253
6254    /// Copy bytes to `device`, leaving the source resident. Use when
6255    /// other ops still need the source.
6256    pub fn copy_to_device(&self, device: &Device) -> Self {
6257        Self {
6258            inner: self.inner.copy_to_device(device.location()),
6259        }
6260    }
6261
6262    // ---- autograd ----
6263
6264    /// Run reverse-mode autograd from this tensor as the loss, returning
6265    /// a [`fuel_graph::GradMap`] keyed by every input tensor reached.
6266    /// The gradient nodes extend the same graph; realizing a gradient
6267    /// re-executes the forward dependencies.
6268    pub fn backward(&self) -> fuel_graph::GradMap {
6269        self.inner.backward()
6270    }
6271}
6272
6273// ============================================================================
6274// Phase A.2 — composite primitives expressible from existing ops.
6275//
6276// Each method here is implemented in terms of `Tensor`'s existing
6277// surface (reshape, permute, concat, unsqueeze, etc.). No new graph ops.
6278// ============================================================================
6279
6280impl Tensor {
6281    /// Transpose the last two dims as a Result-returning convenience —
6282    /// rank < 2 surfaces as an error rather than the panic the
6283    /// no-arg [`Self::transpose`] would produce. Alias for the eager
6284    /// `transpose_last_two`.
6285    pub fn transpose_last_two(&self) -> std::result::Result<Self, fuel_ir::Error> {
6286        self.transpose()
6287    }
6288
6289    /// Eager-API alias of [`Self::transpose_last_two`]. Matches PyTorch's
6290    /// `.t()` short form and the existing eager [`Tensor::t`] method.
6291    pub fn t(&self) -> std::result::Result<Self, fuel_ir::Error> {
6292        self.transpose()
6293    }
6294
6295    /// Two-argument transpose: swap dims `dim1` and `dim2`, leaving the
6296    /// rest in place. Implemented via [`fuel_graph::NodeHandle::try_permute`]; matches the
6297    /// eager `transpose(d1, d2)` two-arg form. Accepts any [`Dim`]
6298    /// (`usize`, `D::Minus1`, etc.).
6299    pub fn transpose_dims<D1: Dim, D2: Dim>(
6300        &self,
6301        dim1: D1,
6302        dim2: D2,
6303    ) -> std::result::Result<Self, fuel_ir::Error> {
6304        let shape = self.shape();
6305        let dim1 = dim1.to_index(&shape, "transpose_dims")?;
6306        let dim2 = dim2.to_index(&shape, "transpose_dims")?;
6307        if dim1 == dim2 {
6308            return Ok(self.clone());
6309        }
6310        let rank = shape.dims().len();
6311        let mut axes: Vec<usize> = (0..rank).collect();
6312        axes.swap(dim1, dim2);
6313        self.permute(axes.as_slice())
6314    }
6315
6316    /// Collapse dims `[start_dim, end_dim]` (inclusive) into a single
6317    /// dimension. Returns `Result` so out-of-bounds surfaces as a typed
6318    /// error rather than a panic. Accepts any [`Dim`] for either arg
6319    /// (`D::Minus1` for the last axis works).
6320    pub fn flatten<D1: Dim, D2: Dim>(
6321        &self,
6322        start_dim: D1,
6323        end_dim: D2,
6324    ) -> std::result::Result<Self, fuel_ir::Error> {
6325        let shape = self.shape();
6326        let rank = shape.dims().len();
6327        if rank == 0 {
6328            return Ok(self.clone());
6329        }
6330        let start_dim = start_dim.to_index(&shape, "flatten")?;
6331        let end_dim = end_dim.to_index(&shape, "flatten")?;
6332        if start_dim > end_dim {
6333            return Err(fuel_ir::Error::Msg(format!(
6334                "flatten: start_dim={start_dim} > end_dim={end_dim}",
6335            ))
6336            .bt());
6337        }
6338        let dims = shape.dims();
6339        let merged: usize = dims[start_dim..=end_dim].iter().product();
6340        let mut new_dims: Vec<usize> = Vec::with_capacity(rank - (end_dim - start_dim));
6341        new_dims.extend_from_slice(&dims[..start_dim]);
6342        new_dims.push(merged);
6343        new_dims.extend_from_slice(&dims[end_dim + 1..]);
6344        self.reshape(new_dims)
6345    }
6346
6347    /// Flatten dims `[0, end_dim]` (inclusive) into one.
6348    pub fn flatten_to<D: Dim>(&self, end_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6349        self.flatten(0_usize, end_dim)
6350    }
6351
6352    /// Flatten dims `[start_dim, rank-1]` into one.
6353    pub fn flatten_from<D: Dim>(&self, start_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6354        let rank = self.shape().dims().len();
6355        if rank == 0 {
6356            return Ok(self.clone());
6357        }
6358        self.flatten(start_dim, rank - 1)
6359    }
6360
6361    /// Flatten the tensor to rank-1 (single dim containing every element).
6362    pub fn flatten_all(&self) -> std::result::Result<Self, fuel_ir::Error> {
6363        let rank = self.shape().dims().len();
6364        if rank == 0 {
6365            return Ok(self.clone());
6366        }
6367        self.flatten(0, rank - 1)
6368    }
6369
6370    /// Stack tensors along a new dim at position `dim`. Each input is
6371    /// `unsqueeze`d at `dim` then concatenated. All inputs must have
6372    /// identical shape; `dim` may equal `rank` (append a new trailing
6373    /// dim). Accepts any [`Dim`].
6374    pub fn stack<D: Dim>(args: &[&Self], dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6375        if args.is_empty() {
6376            return Err(fuel_ir::Error::Msg("stack: requires at least one tensor".into()).bt());
6377        }
6378        let reference_shape = args[0].shape();
6379        let reference_dims = reference_shape.dims().to_vec();
6380        let dim = dim.to_index_plus_one(&reference_shape, "stack")?;
6381        for (idx, t) in args.iter().enumerate().skip(1) {
6382            if t.shape().dims() != reference_dims.as_slice() {
6383                return Err(fuel_ir::Error::Msg(format!(
6384                    "stack: tensor {idx} shape {:?} != reference shape {:?}",
6385                    t.shape().dims(),
6386                    reference_dims,
6387                ))
6388                .bt());
6389            }
6390        }
6391        // unsqueeze every input at the new dim, then concat.
6392        let mut iter = args.iter();
6393        let first = iter.next().unwrap().unsqueeze(dim)?;
6394        let mut acc = first;
6395        for t in iter {
6396            let u = t.unsqueeze(dim)?;
6397            acc = acc.concat(&u, dim)?;
6398        }
6399        Ok(acc)
6400    }
6401
6402    // ---- keepdim reductions ----
6403    //
6404    // Each keepdim variant is the squeezed reduction post-composed with
6405    // `unsqueeze` at the same dim. The graph optimizer can fuse these
6406    // back into a single op when it's profitable; until then, the cost
6407    // is one extra view-only node.
6408
6409    /// Sum along `dim`, keeping the reduced dim as size 1. Accepts any
6410    /// [`Dim`]. Returns Result because of the cascade from [`Self::unsqueeze`].
6411    pub fn sum_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6412        let shape = self.shape();
6413        let dim = dim.to_index(&shape, "sum_keepdim")?;
6414        self.sum_dim(dim).unwrap().unsqueeze(dim)
6415    }
6416
6417    /// Mean along `dim`, keeping the reduced dim as size 1.
6418    pub fn mean_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6419        let shape = self.shape();
6420        let dim = dim.to_index(&shape, "mean_keepdim")?;
6421        self.mean_dim(dim).unwrap().unsqueeze(dim)
6422    }
6423
6424    /// Max along `dim`, keeping the reduced dim as size 1.
6425    pub fn max_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6426        let shape = self.shape();
6427        let dim = dim.to_index(&shape, "max_keepdim")?;
6428        self.max_dim(dim).unwrap().unsqueeze(dim)
6429    }
6430
6431    /// Min along `dim`, keeping the reduced dim as size 1.
6432    pub fn min_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6433        let shape = self.shape();
6434        let dim = dim.to_index(&shape, "min_keepdim")?;
6435        // sum_dim/mean_dim/max_dim/min_dim return Self today (A.8b.9 will
6436        // flip them to Result); chain through `.unsqueeze(dim)?` which now
6437        // owns the build-time dim validation.
6438        self.min_dim(dim).unwrap().unsqueeze(dim)
6439    }
6440
6441    /// Variance along `dim`, keeping the reduced dim as size 1. Divides
6442    /// squared deviations by `n - correction` (numpy/PyTorch-2.x
6443    /// `correction` convention: `0.0` is the **population** variance
6444    /// — KISS-Ops §6.13's default, and numpy's own default — `1.0` is
6445    /// Bessel's (sample/unbiased) correction, matching PyTorch's *older*
6446    /// default and the eager [`Tensor::var_keepdim`]'s prior behavior.
6447    ///
6448    /// **Not a default-argument convenience**: KISS-Ops §6.13 requires
6449    /// Bessel's correction to be an EXPLICIT attribute, not an implicit
6450    /// torch-style default silently baked into the divisor — this
6451    /// parameter exists so a caller cannot get Bessel's correction by
6452    /// omission. `correction >= n` produces a non-finite result (same
6453    /// shape as any other degenerate reduction — never a panic).
6454    pub fn var_keepdim<D: Dim>(
6455        &self,
6456        dim: D,
6457        correction: f64,
6458    ) -> std::result::Result<Self, fuel_ir::Error> {
6459        let shape = self.shape();
6460        let dim = dim.to_index(&shape, "var_keepdim")?;
6461        let dims = shape.dims();
6462        let n = dims[dim];
6463        let mean = self.mean_keepdim(dim)?;
6464        let deviation = self.broadcast_sub(&mean).unwrap();
6465        let squares = deviation.sqr();
6466        // sum_keepdim then divide by (n - correction); leaves the reduced dim as 1.
6467        let summed = squares.sum_keepdim(dim)?;
6468        let divisor = n as f64 - correction;
6469        Ok(summed.mul_scalar(1.0 / divisor))
6470    }
6471
6472    /// Variance along `dim`, squeezing the reduced dim. See
6473    /// [`Self::var_keepdim`] for the `correction` parameter's meaning.
6474    pub fn var<D: Dim>(
6475        &self,
6476        dim: D,
6477        correction: f64,
6478    ) -> std::result::Result<Self, fuel_ir::Error> {
6479        let shape = self.shape();
6480        let dim = dim.to_index(&shape, "var")?;
6481        self.var_keepdim(dim, correction)?.squeeze(dim)
6482    }
6483
6484    // ---- composite scalar / binary ops (Phase A.4) ----
6485
6486    /// `y = mul * self + add` element-wise. Two-op composite of
6487    /// [`Self::mul_scalar`] then [`Self::add_scalar`]; a fused `Op::Affine`
6488    /// can collapse this into a single op later (see the in-place
6489    /// counterpart [`Self::affine_inplace`]).
6490    pub fn affine(&self, mul: f64, add: f64) -> Self {
6491        self.mul_scalar(mul).add_scalar(add)
6492    }
6493
6494    /// `y = scale * self + shift`. Alias of [`Self::affine`] with
6495    /// descriptive parameter names; matches eager's
6496    /// `Tensor::scale_and_shift`.
6497    pub fn scale_and_shift(&self, scale: f64, shift: f64) -> Self {
6498        self.affine(scale, shift)
6499    }
6500
6501    /// Exponential Linear Unit: `self` where `self > 0`,
6502    /// `alpha * (exp(self) - 1)` otherwise. Composite of `where_cond`,
6503    /// `gt`, `exp`, `affine`.
6504    pub fn elu(&self, alpha: f64) -> Self {
6505        // Negative-branch value: alpha * (exp(self) - 1) = alpha * exp(self) - alpha
6506        let neg_branch = self.exp().affine(alpha, -alpha);
6507        // Mask: self > 0. Build a zero on the same graph.
6508        // GAP-003 carve-out. THE PROOF IS LOCAL AND ON THIS LINE: the buffer is
6509        // `vec![0.0; self.elem_count()]` and the shape is `self.shape()`, whose
6510        // elem_count IS that number. Both from `self`; no caller supplies either.
6511        let zero = self
6512            .const_f32_like(vec![0.0; self.elem_count()], self.shape())
6513            .expect("elu: vec![_; self.elem_count()] against self.shape() -- same source");
6514        let mask = self.gt(&zero).unwrap();
6515        mask.where_cond(self, &neg_branch).unwrap()
6516    }
6517
6518    /// Inner product of two rank-1 tensors. Composite of `mul` +
6519    /// `sum_all`; matches eager's [`Tensor::dot`].
6520    pub fn dot(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6521        let a = self.shape().dims().to_vec();
6522        let b = rhs.shape().dims().to_vec();
6523        if a.len() != 1 || b.len() != 1 {
6524            return Err(fuel_ir::Error::Msg(format!(
6525                "dot: requires rank-1 inputs, got lhs={a:?} rhs={b:?}",
6526            ))
6527            .bt());
6528        }
6529        if a[0] != b[0] {
6530            return Err(fuel_ir::Error::Msg(format!(
6531                "dot: length mismatch lhs={} rhs={}",
6532                a[0], b[0],
6533            ))
6534            .bt());
6535        }
6536        Ok(self.mul(rhs).unwrap().sum_all())
6537    }
6538
6539    /// Matrix × vector: `[m, n] · [n] -> [m]`. No broadcasting. Composite
6540    /// of `unsqueeze` + `matmul` + `squeeze`. Matches eager's
6541    /// [`Tensor::mv`].
6542    pub fn mv(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6543        let a = self.shape().dims().to_vec();
6544        let b = rhs.shape().dims().to_vec();
6545        if a.len() != 2 || b.len() != 1 || a[1] != b[0] {
6546            return Err(fuel_ir::Error::Msg(format!(
6547                "mv: shape mismatch lhs={a:?} rhs={b:?} (need [m,n] · [n])",
6548            ))
6549            .bt());
6550        }
6551        // unsqueeze rhs to [n,1], matmul -> [m,1], squeeze trailing dim.
6552        let rhs_col = rhs.unsqueeze(1_usize)?;
6553        let prod = self.matmul(&rhs_col).unwrap();
6554        prod.squeeze(1_usize)
6555    }
6556
6557    /// Alias of [`Self::mv`] with a more descriptive name. Matches
6558    /// eager's [`Tensor::matvec`].
6559    pub fn matvec(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6560        self.mv(rhs)
6561    }
6562
6563    /// Broadcast-aware matmul. Lazy's [`Self::matmul`] already accepts
6564    /// broadcast-compatible operands; this method is exposed for
6565    /// signature compatibility with eager's `Tensor::broadcast_matmul`.
6566    pub fn broadcast_matmul(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6567        self.matmul(rhs)
6568    }
6569
6570    // ---- Phase A.5 factory family ----
6571    //
6572    // Instance methods derive shape + dtype from `self` and place the new
6573    // tensor on the same graph via `const_*_like`. Static methods build
6574    // a fresh graph anchored on a host-allocated buffer.
6575
6576    /// New tensor with the same shape, dtype, and graph as `self`, filled
6577    /// with ones. Returns Err for unsupported dtypes (anything outside
6578    /// F32/F64/BF16/F16/U32/I64) — matches eager `Tensor::ones_like` parity.
6579    pub fn ones_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6580        let n = self.elem_count();
6581        let shape = self.shape();
6582        match self.dtype() {
6583            DType::F32 => self.const_f32_like(vec![1.0_f32; n], shape),
6584            DType::F64 => self.const_f64_like(vec![1.0_f64; n], shape),
6585            DType::BF16 => self.const_bf16_like(vec![half::bf16::ONE; n], shape),
6586            DType::F16 => self.const_f16_like(vec![half::f16::ONE; n], shape),
6587            DType::U32 => self.const_u32_like(vec![1_u32; n], shape),
6588            DType::I64 => self.const_i64_like(vec![1_i64; n], shape),
6589            other => {
6590                Err(fuel_ir::Error::Msg(format!("ones_like: unsupported dtype {other:?}",)).bt())
6591            }
6592        }
6593    }
6594
6595    /// New tensor with the same shape, dtype, and graph as `self`, filled
6596    /// with zeros. Returns Err for unsupported dtypes (anything outside
6597    /// F32/F64/BF16/F16/U32/I64) — matches eager `Tensor::zeros_like` parity.
6598    pub fn zeros_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6599        let n = self.elem_count();
6600        let shape = self.shape();
6601        match self.dtype() {
6602            DType::F32 => self.const_f32_like(vec![0.0_f32; n], shape),
6603            DType::F64 => self.const_f64_like(vec![0.0_f64; n], shape),
6604            DType::BF16 => self.const_bf16_like(vec![half::bf16::ZERO; n], shape),
6605            DType::F16 => self.const_f16_like(vec![half::f16::ZERO; n], shape),
6606            DType::U32 => self.const_u32_like(vec![0_u32; n], shape),
6607            DType::I64 => self.const_i64_like(vec![0_i64; n], shape),
6608            other => {
6609                Err(fuel_ir::Error::Msg(format!("zeros_like: unsupported dtype {other:?}",)).bt())
6610            }
6611        }
6612    }
6613
6614    /// New tensor with `shape`/`dtype`/`device`, every element set to `1`.
6615    /// Static factory equivalent of eager's `Tensor::ones`. Returns Err for
6616    /// dtypes outside F32/F64/BF16/F16/U32.
6617    pub fn ones(
6618        shape: impl Into<Shape>,
6619        dtype: DType,
6620        device: &Device,
6621    ) -> std::result::Result<Self, fuel_ir::Error> {
6622        let shape = shape.into();
6623        let n = shape.elem_count();
6624        match dtype {
6625            DType::F32 => Self::from_f32(vec![1.0_f32; n], shape, device),
6626            DType::F64 => Self::from_f64(vec![1.0_f64; n], shape, device),
6627            DType::BF16 => Self::from_bf16(vec![half::bf16::ONE; n], shape, device),
6628            DType::F16 => Self::from_f16(vec![half::f16::ONE; n], shape, device),
6629            DType::U32 => Self::from_u32(vec![1_u32; n], shape, device),
6630            other => Err(fuel_ir::Error::Msg(format!("ones: unsupported dtype {other:?}",)).bt()),
6631        }
6632    }
6633
6634    /// New tensor with `shape`/`dtype`/`device`, every element set to `0`.
6635    /// Static factory equivalent of eager's `Tensor::zeros`. Returns Err for
6636    /// dtypes outside F32/F64/BF16/F16/U32.
6637    ///
6638    /// **Mints a NEW graph** — it delegates to the `from_*` constructors, so
6639    /// the tensor it returns cannot be combined with one built elsewhere. To
6640    /// add a zero tensor to an existing graph, use
6641    /// [`zeros_like`](Self::zeros_like) or a `const_*_like` builder. See
6642    /// [graph affinity](Self#tensors-are-graph-affine--read-this-before-building-anything).
6643    pub fn zeros(
6644        shape: impl Into<Shape>,
6645        dtype: DType,
6646        device: &Device,
6647    ) -> std::result::Result<Self, fuel_ir::Error> {
6648        let shape = shape.into();
6649        let n = shape.elem_count();
6650        match dtype {
6651            DType::F32 => Self::from_f32(vec![0.0_f32; n], shape, device),
6652            DType::F64 => Self::from_f64(vec![0.0_f64; n], shape, device),
6653            DType::BF16 => Self::from_bf16(vec![half::bf16::ZERO; n], shape, device),
6654            DType::F16 => Self::from_f16(vec![half::f16::ZERO; n], shape, device),
6655            DType::U32 => Self::from_u32(vec![0_u32; n], shape, device),
6656            other => Err(fuel_ir::Error::Msg(format!("zeros: unsupported dtype {other:?}",)).bt()),
6657        }
6658    }
6659
6660    /// New tensor of `shape`/`device` filled with `value`. The scalar's
6661    /// dtype determines the tensor's dtype. Returns Err for scalar dtypes
6662    /// outside F32/F64/BF16/F16/U32.
6663    ///
6664    /// **Mints a NEW graph** — it delegates to the `from_*` constructors, so
6665    /// the tensor it returns cannot be combined with one built elsewhere. To
6666    /// add a filled tensor to an existing graph, use a `const_*_like` builder.
6667    /// See [graph affinity](Self#tensors-are-graph-affine--read-this-before-building-anything).
6668    pub fn full(
6669        shape: impl Into<Shape>,
6670        value: fuel_ir::Scalar,
6671        device: &Device,
6672    ) -> std::result::Result<Self, fuel_ir::Error> {
6673        let shape = shape.into();
6674        let n = shape.elem_count();
6675        match value {
6676            fuel_ir::Scalar::F32(v) => Self::from_f32(vec![v; n], shape, device),
6677            fuel_ir::Scalar::F64(v) => Self::from_f64(vec![v; n], shape, device),
6678            fuel_ir::Scalar::BF16(v) => Self::from_bf16(vec![v; n], shape, device),
6679            fuel_ir::Scalar::F16(v) => Self::from_f16(vec![v; n], shape, device),
6680            fuel_ir::Scalar::U32(v) => Self::from_u32(vec![v; n], shape, device),
6681            other => Err(fuel_ir::Error::Msg(format!(
6682                "full: unsupported scalar dtype {:?}",
6683                other.dtype(),
6684            ))
6685            .bt()),
6686        }
6687    }
6688
6689    /// Identity matrix `[n, n]` with the given dtype on the given device.
6690    /// Built host-side as a flat Vec; no graph-layer arange dependency.
6691    pub fn eye(n: usize, dtype: DType, device: &Device) -> Self {
6692        let mut data = vec![0.0_f32; n * n];
6693        for i in 0..n {
6694            data[i * n + i] = 1.0;
6695        }
6696        // GAP-003 carve-out: `.expect` and NOT `?`, because this cannot fail and
6697        // saying it CAN would be a false claim in the signature -- `eye` would
6698        // return a `Result` that is never `Err`, and every caller would write a
6699        // `?` for an error that does not exist.
6700        //
6701        // THE PROOF IS LOCAL, two lines up: `data` is `vec![_; n * n]`, and the
6702        // shape `[n, n]` has `elem_count() == n * n`. Both are computed from `n`
6703        // in this function; no caller supplies either, so they cannot disagree.
6704        let base = Self::from_f32(data, vec![n, n], device).expect(
6705            "eye: data is vec![_; n*n] and shape is [n, n] (elem_count n*n), \n             both computed from `n` in this function -- lengths cannot disagree",
6706        );
6707        if dtype == DType::F32 {
6708            base
6709        } else {
6710            base.to_dtype(dtype).unwrap()
6711        }
6712    }
6713
6714    /// Split a `(B, N, num_heads * head_dim)` projection into the
6715    /// multi-head attention layout `(B, num_heads, N, head_dim)`.
6716    /// Equivalent to `reshape(B, N, num_heads, head_dim).permute([0, 2, 1, 3])`
6717    /// — promoted to a method to retire the per-port reimplementations
6718    /// of this same composite.
6719    pub fn split_heads(
6720        &self,
6721        num_heads: usize,
6722        head_dim: usize,
6723    ) -> std::result::Result<Self, fuel_ir::Error> {
6724        let dims = self.inner.shape().dims().to_vec();
6725        debug_assert_eq!(
6726            dims.len(),
6727            3,
6728            "split_heads: input must be rank 3 (B, N, embed), got {dims:?}"
6729        );
6730        debug_assert_eq!(
6731            dims[2],
6732            num_heads * head_dim,
6733            "split_heads: trailing dim ({}) != num_heads * head_dim ({} * {} = {})",
6734            dims[2],
6735            num_heads,
6736            head_dim,
6737            num_heads * head_dim
6738        );
6739        let b = dims[0];
6740        let n = dims[1];
6741        self.reshape(Shape::from_dims(&[b, n, num_heads, head_dim]))?
6742            .permute([0, 2, 1, 3_usize])
6743    }
6744
6745    /// Merge a `(B, num_heads, N, head_dim)` attention result back
6746    /// into the projection layout `(B, N, num_heads * head_dim)`.
6747    /// Inverse of [`Self::split_heads`].
6748    pub fn merge_heads(&self) -> std::result::Result<Self, fuel_ir::Error> {
6749        let dims = self.inner.shape().dims().to_vec();
6750        debug_assert_eq!(
6751            dims.len(),
6752            4,
6753            "merge_heads: input must be rank 4 (B, heads, N, head_dim), got {dims:?}"
6754        );
6755        let b = dims[0];
6756        let num_heads = dims[1];
6757        let n = dims[2];
6758        let head_dim = dims[3];
6759        self.permute([0, 2, 1, 3_usize])?
6760            .reshape(Shape::from_dims(&[b, n, num_heads * head_dim]))
6761    }
6762
6763    /// Add a length-`bias.len()` bias vector to the trailing dim
6764    /// of `self`, broadcasting across all leading dims. The bias
6765    /// is materialized fresh on `self`'s graph from the supplied
6766    /// `Arc<[f32]>`.
6767    ///
6768    /// Common pattern after `WeightStorage::apply_linear` when the
6769    /// linear has a bias term but the activation tensor is on a
6770    /// different anchor than where the bias was originally
6771    /// allocated. Several lazy ports inlined this same 3-line
6772    /// helper as `bias_add` — promoted here to a method.
6773    pub fn add_trailing_bias(
6774        &self,
6775        bias: std::sync::Arc<[f32]>,
6776    ) -> std::result::Result<Self, fuel_ir::Error> {
6777        let n = bias.len();
6778        // Build the bias const in `self`'s dtype rather than hardcoding
6779        // f32: under BF16-throughout decode (Phase D increment A) the
6780        // activation stream is BF16 and `broadcast_add` asserts dtype
6781        // equality, so an f32 bias would panic. No-op for f32 activations
6782        // (every other caller today).
6783        let bias_t = self.const_like_dtype(&bias, Shape::from_dims(&[n]), self.dtype())?;
6784        self.broadcast_add(&bias_t)
6785    }
6786
6787    /// Embed `tokens` against an `[vocab_size, hidden]` embedding
6788    /// table held as `Arc<[f32]>`. Returns `(1, seq, hidden)`
6789    /// rank-3 hidden states ready to feed into a decoder backbone.
6790    ///
6791    /// Bootstraps a fresh graph anchored on a new const-f32 node.
6792    /// For composition with an already-built tensor (e.g.,
6793    /// multimodal models that need text embeddings on the audio
6794    /// graph), use [`Self::embed_tokens_anchored`] instead.
6795    ///
6796    /// Retires the 7-line `from_f32 + const_u32_like + index_select
6797    /// + reshape` ceremony every LLM port carried.
6798    pub fn embed_tokens(
6799        embed_table: std::sync::Arc<[f32]>,
6800        vocab_size: usize,
6801        hidden: usize,
6802        tokens: &[u32],
6803        device: &crate::Device,
6804    ) -> std::result::Result<Self, fuel_ir::Error> {
6805        let seq = tokens.len();
6806        if seq == 0 {
6807            return Err(fuel_ir::Error::Msg("embed_tokens: tokens must be non-empty".into()).bt());
6808        }
6809        let embed = Self::from_f32(embed_table, Shape::from_dims(&[vocab_size, hidden]), device)?;
6810        let token_ids = embed.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6811        embed
6812            .index_select(0_usize, &token_ids)?
6813            .reshape(Shape::from_dims(&[1, seq, hidden]))
6814    }
6815
6816    /// Variant of [`Self::embed_tokens`] that anchors the embedding
6817    /// table and token-id constants on the receiver's graph, so the
6818    /// resulting embeddings can compose with `self` and other
6819    /// tensors already on that graph. Used by multimodal models
6820    /// (vision + text, audio + text) where the text embeddings must
6821    /// live on the modality encoder's graph for cross-substitution
6822    /// to work.
6823    pub fn embed_tokens_anchored(
6824        &self,
6825        embed_table: std::sync::Arc<[f32]>,
6826        vocab_size: usize,
6827        hidden: usize,
6828        tokens: &[u32],
6829    ) -> std::result::Result<Self, fuel_ir::Error> {
6830        let seq = tokens.len();
6831        if seq == 0 {
6832            return Err(fuel_ir::Error::Msg(
6833                "embed_tokens_anchored: tokens must be non-empty".into(),
6834            )
6835            .bt());
6836        }
6837        let embed = self.const_f32_like(embed_table, Shape::from_dims(&[vocab_size, hidden]))?;
6838        let token_ids = self.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6839        embed
6840            .index_select(0_usize, &token_ids)?
6841            .reshape(Shape::from_dims(&[1, seq, hidden]))
6842    }
6843
6844    /// Build the standard (non-interleaved) RoPE cos/sin tables for
6845    /// `seq` positions starting at `start_pos`, anchored on the
6846    /// receiver's graph. Returns `(cos, sin)`, each with shape
6847    /// `[seq, head_dim]`.
6848    ///
6849    /// Delegates the actual `(theta, position) → (cos, sin)` host
6850    /// computation to [`fuel_graph::build_rope_tables`] (the canonical
6851    /// reference); only the const-tensor materialization is folded
6852    /// into one method to retire the per-port 4-line ceremony every
6853    /// LLM port did before calling `rope_with_tables`.
6854    pub fn rope_tables_const(
6855        &self,
6856        theta: f64,
6857        start_pos: usize,
6858        seq: usize,
6859        head_dim: usize,
6860    ) -> (Self, Self) {
6861        let (cos_data, sin_data) = fuel_graph::build_rope_tables(theta, start_pos, seq, head_dim);
6862        let rope_shape = Shape::from_dims(&[seq, head_dim]);
6863        // ⚠️ GAP-003 category 2: the proof EXISTS but is NOT LOCAL --
6864        // `build_rope_tables` owns the length and its `-> (Vec<f32>, Vec<f32>)`
6865        // signature does not carry it. The message names WHERE the guarantee
6866        // lives rather than claiming one. `-> (Self, Self)` has no error channel;
6867        // giving it one is a signature change, a different obligation. See #157.
6868        let rope_cos = self
6869            .const_f32_like(cos_data, rope_shape.clone())
6870            .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6871        let rope_sin = self
6872            .const_f32_like(sin_data, rope_shape)
6873            .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6874        (rope_cos, rope_sin)
6875    }
6876
6877    /// Constant-position RoPE cos/sin tables `[rows, head_dim]` where EVERY row
6878    /// is the *same* θ·`delta` rotation — the rung-2 delta-rotation tables. A
6879    /// cached prefix's keys, already rotated for their original positions, are
6880    /// shifted to `+delta` by applying this uniform rotation (RoPE composes
6881    /// additively per pair: `R(delta)·R(p) = R(p+delta)`).
6882    ///
6883    /// This is the position-`delta` row repeated `rows` times — NOT
6884    /// `rope_tables_const(theta, delta, rows, head_dim)`, which would give the
6885    /// *incrementing* progression `delta, delta+1, …` (the standard RoPE sweep).
6886    /// Routed through the canonical `fuel_graph::build_rope_tables` so it inherits
6887    /// the model's inv-freq/scaling exactly.
6888    pub fn rope_delta_tables_const(
6889        &self,
6890        theta: f64,
6891        delta: usize,
6892        rows: usize,
6893        head_dim: usize,
6894    ) -> (Self, Self) {
6895        let (c1, s1) = fuel_graph::build_rope_tables(theta, delta, 1, head_dim);
6896        let mut cos = Vec::with_capacity(rows * head_dim);
6897        let mut sin = Vec::with_capacity(rows * head_dim);
6898        for _ in 0..rows {
6899            cos.extend_from_slice(&c1);
6900            sin.extend_from_slice(&s1);
6901        }
6902        let shape = Shape::from_dims(&[rows, head_dim]);
6903        // ⚠️ GAP-003, HALF LOCAL. LOCAL: the loop appends `c1` exactly `rows`
6904        // times against a `[rows, head_dim]` shape. NOT LOCAL: that `c1` has
6905        // `head_dim` elements comes from `build_rope_tables`. Stated as both.
6906        (
6907            self.const_f32_like(cos, shape.clone()).expect(
6908                "rope tables: rows appends of c1 (local); c1 has head_dim elements (not local)",
6909            ),
6910            self.const_f32_like(sin, shape).expect(
6911                "rope tables: rows appends of s1 (local); s1 has head_dim elements (not local)",
6912            ),
6913        )
6914    }
6915
6916    /// Uniformly delta-rotate one cached (post-RoPE) K block by θ·`delta`,
6917    /// returning the shifted block in the SAME pool layout. Reuses the model's
6918    /// exact `rope_with_tables_decomposed` (rotate-half) rather than a hand-rolled
6919    /// rotation, so it inherits the model's RoPE convention + scaling and stays
6920    /// byte-exact with a direct rope-at-shifted-position. A single uniform
6921    /// rotation is correct for the whole block even though it holds `block_size`
6922    /// distinct original positions — the delta is the same for every position.
6923    ///
6924    /// `k_block` is the pool block layout, row-major `[block_size, n_kv_heads,
6925    /// head_dim]` (positions-major, as `DeviceKvPool::read_block` returns). RoPE
6926    /// wants `(position, dim)` as the last two axes, so heads are brought forward
6927    /// to the `project_qkv_roped` layout `[1, n_kv_heads, block_size, head_dim]`,
6928    /// rotated, and returned to `[block_size, n_kv_heads, head_dim]`. The rung-2
6929    /// numeric core, realized on `dev`.
6930    pub fn rope_delta_rotate_block_f32(
6931        dev: &crate::Device,
6932        k_block: &[f32],
6933        theta: f64,
6934        delta: usize,
6935        block_size: usize,
6936        n_kv_heads: usize,
6937        head_dim: usize,
6938    ) -> Vec<f32> {
6939        let pool_shape = Shape::from_dims(&[block_size, n_kv_heads, head_dim]);
6940        // ⚠️ GAP-003: NOT a carve-out and NOT a propagation, and the reason is
6941        // this function's SIGNATURE rather than anything about the operands.
6942        // `k_block` and the pool dims are BOTH caller-supplied, so there is no
6943        // local proof and the message below deliberately does not claim one.
6944        // `-> Vec<f32>` has no error channel, and the two `.expect`s already on
6945        // this same expression (permute, reshape) are the same pre-existing
6946        // condition -- converting one of three would be less honest, not more.
6947        //
6948        // The fix is this function returning `Result`, which is a DIFFERENT
6949        // obligation from the constructor-family ruling and is not in its scope.
6950        let k = Tensor::from_f32(k_block.to_vec(), pool_shape, dev).expect(
6951            "rope_delta_rotate_block_f32: k_block length must equal              block_size*n_kv_heads*head_dim -- both caller-supplied, so this              function cannot prove it; the -> Vec<f32> signature has no error              channel (see the sibling permute/reshape expects)",
6952        );
6953        let k4 = k
6954            .permute([1, 0, 2])
6955            .expect("rope_delta_rotate_block_f32: permute [bs,Hkv,D]->[Hkv,bs,D]")
6956            .reshape(Shape::from_dims(&[1, n_kv_heads, block_size, head_dim]))
6957            .expect("rope_delta_rotate_block_f32: reshape to [1,Hkv,bs,D]");
6958        let (cos, sin) = k4.rope_delta_tables_const(theta, delta, block_size, head_dim);
6959        k4.rope_with_tables_decomposed(&cos, &sin)
6960            .expect("rope_delta_rotate_block_f32: rope_with_tables_decomposed")
6961            .reshape(Shape::from_dims(&[n_kv_heads, block_size, head_dim]))
6962            .expect("rope_delta_rotate_block_f32: reshape back to [Hkv,bs,D]")
6963            .permute([1, 0, 2])
6964            .expect("rope_delta_rotate_block_f32: permute [Hkv,bs,D]->[bs,Hkv,D]")
6965            .realize_f32()
6966    }
6967
6968    /// `[B, 1, 1, head_dim]` RoPE cos/sin tables, row `b` at absolute position
6969    /// `positions[b]` — the per-row generalization of [`Self::rope_tables_const`]
6970    /// (which carries one shared position). Built as a **single** const from `B`
6971    /// calls to `fuel_graph::build_rope_tables` (the canonical `(theta, position)
6972    /// → (cos, sin)` math), concatenated host-side — no `Op::Concat` chain. At
6973    /// `B == 1` the emitted data is bit-identical to `rope_tables_const`'s
6974    /// `[1, head_dim]` const (same builder call), so
6975    /// [`Self::rope_batched`] reduces to the single-position decomposed RoPE.
6976    ///
6977    /// This is the mechanism that lets a decode batch carry sessions at
6978    /// *different* positions (continuous batching) rather than requiring
6979    /// equal-length rows. `anchor` (`self`) supplies the graph.
6980    pub fn rope_tables_const_batched(
6981        &self,
6982        theta: f64,
6983        positions: &[usize],
6984        head_dim: usize,
6985    ) -> (Self, Self) {
6986        let b = positions.len();
6987        let mut cos_data = Vec::with_capacity(b * head_dim);
6988        let mut sin_data = Vec::with_capacity(b * head_dim);
6989        for &pos in positions {
6990            let (c, s) = fuel_graph::build_rope_tables(theta, pos, 1, head_dim);
6991            cos_data.extend_from_slice(&c);
6992            sin_data.extend_from_slice(&s);
6993        }
6994        let shape = Shape::from_dims(&[b, 1, 1, head_dim]);
6995        // ⚠️ GAP-003, and the proof is HALF local -- worth stating as such rather
6996        // than rounding to either category. LOCAL: `b` IS `positions.len()` (six
6997        // lines up) and the loop appends once per position, so the buffer has
6998        // `positions.len() * head_dim` elements against a `[b, 1, 1, head_dim]`
6999        // shape. NOT LOCAL: that each `build_rope_tables(_, _, 1, head_dim)` call
7000        // yields exactly `head_dim` elements lives in that function.
7001        let rope_cos = self
7002            .const_f32_like(cos_data, shape.clone())
7003            .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
7004        let rope_sin = self
7005            .const_f32_like(sin_data, shape)
7006            .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
7007        (rope_cos, rope_sin)
7008    }
7009
7010    /// Rotary position embedding with **per-batch-row** tables.
7011    ///
7012    /// `self` is `[B, H, 1, D]`; `cos`/`sin` are `[B, 1, 1, D]` (row `b` holds the
7013    /// table for sequence `b`'s own position — see [`Self::rope_tables_const_batched`]).
7014    /// Broadcasting the `[B, 1, 1, D]` tables over the head axis is what lets one
7015    /// table row serve all of that sequence's heads.
7016    ///
7017    /// This is `fuel_graph::NodeHandle::rope_with_tables_decomposed`'s body
7018    /// (`fuel-graph/src/lib.rs` ~6931-6954) with the table shape generalized from
7019    /// one shared position to one-per-row — **same ops, same order, same
7020    /// associativity** (`y = x·cos + concat(-x[…,D/2..], x[…,..D/2])·sin`), so at
7021    /// `B == 1` it is bit-identical to `rope_with_tables_decomposed`. Built on the
7022    /// public `Tensor` surface (not a new `fuel-graph` op) deliberately: the
7023    /// single-position [`Self::rope_with_tables_decomposed`] hard-requires
7024    /// `cos.dims() == [seq, d]`, so it cannot carry per-row tables, and rebuilding
7025    /// the lowering here avoids re-entering the `fuel-graph` rope op.
7026    ///
7027    /// (Adopted from the Lightbulb consumer's verified `rope_batched`; the
7028    /// mechanism belongs in Fuel, not the consumer.)
7029    pub fn rope_batched(
7030        &self,
7031        cos: &Self,
7032        sin: &Self,
7033    ) -> std::result::Result<Self, fuel_ir::Error> {
7034        let dims: Vec<usize> = self.inner.shape().dims().to_vec();
7035        let rank = dims.len();
7036        if rank < 2 {
7037            return Err(fuel_ir::Error::Msg(format!(
7038                "rope_batched: expected rank >= 2, got {dims:?}",
7039            ))
7040            .bt());
7041        }
7042        let d = dims[rank - 1];
7043        if !d.is_multiple_of(2) {
7044            return Err(
7045                fuel_ir::Error::Msg(format!("rope_batched: head_dim {d} must be even",)).bt(),
7046            );
7047        }
7048        let half = d / 2;
7049        let target = Shape::from_dims(&dims);
7050
7051        let cos_b = cos.broadcast_to(target.clone())?;
7052        let sin_b = sin.broadcast_to(target)?;
7053
7054        let first = self.slice(rank - 1, 0, half)?;
7055        let second = self.slice(rank - 1, half, half)?;
7056        let rotated = second.neg().concat(&first, rank - 1)?;
7057
7058        let left = self.mul(&cos_b)?;
7059        let right = rotated.mul(&sin_b)?;
7060        left.add(&right)
7061    }
7062
7063    /// Apply RoPE to the first `rope_dim` entries of each head and
7064    /// pass the remaining `head_dim - rope_dim` features through
7065    /// unchanged. `head_dim` is derived from the receiver's last-dim
7066    /// size. When `rope_dim == head_dim` this reduces to
7067    /// [`Self::rope_with_tables`].
7068    ///
7069    /// Implements the partial-rotary convention used by StableLM,
7070    /// Phi, Persimmon, MixFormer, RecurrentGemma, and Gemma-4 text —
7071    /// all the ports that carried an identical 5-line `slice + rope +
7072    /// concat` helper before this method.
7073    pub fn rope_partial(
7074        &self,
7075        rope_cos: &Self,
7076        rope_sin: &Self,
7077        rope_dim: usize,
7078    ) -> std::result::Result<Self, fuel_ir::Error> {
7079        let dims = self.inner.shape();
7080        let dims = dims.dims();
7081        let head_dim = *dims.last().ok_or_else(|| {
7082            fuel_ir::Error::Msg("rope_partial: receiver must have at least one dimension".into())
7083                .bt()
7084        })?;
7085        if rope_dim == head_dim {
7086            return self.rope_with_tables(rope_cos, rope_sin);
7087        }
7088        if rope_dim > head_dim {
7089            return Err(fuel_ir::Error::Msg(format!(
7090                "rope_partial: rope_dim={rope_dim} exceeds head_dim={head_dim}",
7091            ))
7092            .bt());
7093        }
7094        let last = dims.len() - 1;
7095        let pass_dim = head_dim - rope_dim;
7096        let rot = self.slice(last, 0, rope_dim)?;
7097        let pass = self.slice(last, rope_dim, pass_dim)?;
7098        let rot_rotated = rot.rope_with_tables(rope_cos, rope_sin)?;
7099        rot_rotated.concat(&pass, last)
7100    }
7101
7102    /// `Option<Arc<[f32]>>` variant of [`Self::add_trailing_bias`]: if
7103    /// `bias.is_none()`, return `self` unchanged; else apply
7104    /// `add_trailing_bias`. Models the `linear_b` / `linear_no_bias`
7105    /// branch every per-port `optional_bias` / `opt_bias` helper does.
7106    pub fn add_optional_trailing_bias(
7107        &self,
7108        bias: Option<&std::sync::Arc<[f32]>>,
7109    ) -> std::result::Result<Self, fuel_ir::Error> {
7110        match bias {
7111            None => Ok(self.clone()),
7112            Some(b) => self.add_trailing_bias(std::sync::Arc::clone(b)),
7113        }
7114    }
7115
7116    /// Logit-softcap: `cap · tanh(self / cap)`. Used by the Gemma-2 /
7117    /// Gemma-3 attention-logit and final-logit softcap branches. The
7118    /// math is identical regardless of where the cap is applied;
7119    /// retired the two per-port `softcap` / `apply_softcap` helpers in
7120    /// favor of this method.
7121    pub fn softcap(&self, cap: f64) -> Self {
7122        self.mul_scalar(1.0 / cap).tanh().mul_scalar(cap)
7123    }
7124
7125    /// `Option<f64>` variant of [`Self::softcap`]: when `cap.is_none()`
7126    /// or `cap <= 0.0`, return `self` unchanged; else apply
7127    /// [`Self::softcap`]. Mirrors the optional-bias pattern.
7128    pub fn softcap_optional(&self, cap: Option<f64>) -> Self {
7129        match cap {
7130            Some(c) if c > 0.0 => self.softcap(c),
7131            _ => self.clone(),
7132        }
7133    }
7134
7135    /// Apply RMSNorm along the last dim with `(gain + offset) · x`.
7136    /// Equivalent to [`Self::rms_norm_affine`] after adding a scalar
7137    /// to every gain element — used by Gemma-family ports where
7138    /// the stored gain represents `gain - 1` and the runtime path
7139    /// must reconstruct `gain + 1`.
7140    ///
7141    /// Materializes the shifted gain on the receiver's graph; one
7142    /// allocation per call.
7143    pub fn rms_norm_affine_with_offset(
7144        &self,
7145        gain: &[f32],
7146        offset: f32,
7147        eps: f64,
7148    ) -> std::result::Result<Self, fuel_ir::Error> {
7149        let shifted: std::sync::Arc<[f32]> =
7150            std::sync::Arc::from(gain.iter().map(|g| *g + offset).collect::<Vec<_>>());
7151        self.rms_norm_affine(shifted, eps)
7152    }
7153
7154    /// Apply RMSNorm along the last dim with an affine `gain · x`
7155    /// post-step (no bias — RMSNorm has no β term). `gain` is a
7156    /// length-`gain.len()` vector materialized fresh on the
7157    /// receiver's graph and broadcast across all leading dims.
7158    pub fn rms_norm_affine(
7159        &self,
7160        gain: std::sync::Arc<[f32]>,
7161        eps: f64,
7162    ) -> std::result::Result<Self, fuel_ir::Error> {
7163        let hidden = gain.len();
7164        let normed = self.rms_norm_last_dim(eps)?;
7165        let gain_t = self.const_f32_like(gain, Shape::from_dims(&[hidden]))?;
7166        normed.broadcast_mul(&gain_t)
7167    }
7168
7169    /// Global average pool over the spatial dims of a rank-4
7170    /// `(B, C, H, W)` tensor: reduces dims 2 and 3, returning
7171    /// `(B, C)`. For the keepdim variant (`(B, C, 1, 1)`, used by
7172    /// SE blocks), follow with `.reshape(Shape::from_dims(&[B, C, 1, 1]))`.
7173    ///
7174    /// Backs the classification heads of every conv vision port
7175    /// (ResNet, EfficientNet, ConvMixer, FastViT, MobileNetV4,
7176    /// MobileOne, RepVGG, ConvNeXt, EfficientViT, etc.) plus the
7177    /// pre-projection pool inside each squeeze-excite block.
7178    pub fn global_avg_pool_2d(&self) -> std::result::Result<Self, fuel_ir::Error> {
7179        let dims = self.inner.shape().dims().to_vec();
7180        debug_assert_eq!(
7181            dims.len(),
7182            4,
7183            "global_avg_pool_2d: input must be rank 4 (B, C, H, W), got {dims:?}"
7184        );
7185        // Reduce W first (dim 3), then H (dim 2 of the H-reduced (B, C, H) tensor).
7186        self.mean_dim(3_usize)?.mean_dim(2_usize)
7187    }
7188
7189    /// Apply a per-channel affine `gain · x + bias` to a rank-4
7190    /// `(B, C, H, W)` tensor. Both `gain` and `bias` are length-`C`
7191    /// vectors materialized fresh on the receiver's graph and
7192    /// broadcast across the spatial axes.
7193    ///
7194    /// Equivalent to fused-affine BatchNorm at inference time:
7195    /// the running mean / running var / eps are absorbed at load
7196    /// time into `gain = γ / sqrt(var + eps)` and
7197    /// `bias = β - μ · γ / sqrt(var + eps)`, so the runtime forward
7198    /// is just this multiply-add. Used by inference-only conv
7199    /// vision ports (ResNet, EfficientNet, FastViT, etc.).
7200    pub fn channel_affine_4d(
7201        &self,
7202        gain: std::sync::Arc<[f32]>,
7203        bias: std::sync::Arc<[f32]>,
7204    ) -> std::result::Result<Self, fuel_ir::Error> {
7205        let dims = self.inner.shape().dims().to_vec();
7206        debug_assert_eq!(
7207            dims.len(),
7208            4,
7209            "channel_affine_4d: input must be rank 4 (B, C, H, W), got {dims:?}"
7210        );
7211        let channels = dims[1];
7212        debug_assert_eq!(
7213            gain.len(),
7214            channels,
7215            "channel_affine_4d: gain len ({}) != C ({})",
7216            gain.len(),
7217            channels
7218        );
7219        debug_assert_eq!(
7220            bias.len(),
7221            channels,
7222            "channel_affine_4d: bias len ({}) != C ({})",
7223            bias.len(),
7224            channels
7225        );
7226        let w_t = self
7227            .const_f32_like(gain, Shape::from_dims(&[channels]))?
7228            .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7229        let b_t = self
7230            .const_f32_like(bias, Shape::from_dims(&[channels]))?
7231            .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7232        self.broadcast_mul(&w_t)?.broadcast_add(&b_t)
7233    }
7234
7235    /// Build the strict additive causal mask `(seq_len, seq_len)`
7236    /// anchored on `anchor`'s graph: 0 on and below the diagonal,
7237    /// `f32::NEG_INFINITY` above it. Add to attention scores before
7238    /// softmax to enforce strict causality (position `i` cannot
7239    /// attend to position `j > i`).
7240    ///
7241    /// Equivalent to the `(T, T)` mask several ports build inline
7242    /// — promoted here so call sites stop drifting.
7243    pub fn additive_causal_mask_like(anchor: &Tensor, seq_len: usize) -> Self {
7244        let mut data = vec![0.0_f32; seq_len * seq_len];
7245        for i in 0..seq_len {
7246            for j in (i + 1)..seq_len {
7247                data[i * seq_len + j] = f32::NEG_INFINITY;
7248            }
7249        }
7250        // GAP-003 carve-out, PROOF LOCAL: `data` is `vec![_; seq_len * seq_len]`
7251        // six lines up and the loop only writes IN PLACE -- it cannot change the
7252        // length -- against a `[seq_len, seq_len]` shape. Both from `seq_len`.
7253        anchor
7254            .const_f32_like(
7255                std::sync::Arc::from(data),
7256                Shape::from_dims(&[seq_len, seq_len]),
7257            )
7258            .expect(
7259                "additive_causal_mask_like: vec![_; seq_len*seq_len] against [seq_len, seq_len]",
7260            )
7261    }
7262
7263    /// Lower-triangular ones matrix `[n, n]`. `tril2(n).to_dtype(dtype)` is
7264    /// the causal-attention-mask building block.
7265    pub fn tril2(n: usize, dtype: DType, device: &Device) -> Self {
7266        let mut data = vec![0.0_f32; n * n];
7267        for i in 0..n {
7268            for j in 0..=i {
7269                data[i * n + j] = 1.0;
7270            }
7271        }
7272        // GAP-003 carve-out: `.expect` and NOT `?`, because this cannot fail and
7273        // saying it CAN would be a false claim in the signature -- `tril2` would
7274        // return a `Result` that is never `Err`, and every caller would write a
7275        // `?` for an error that does not exist.
7276        //
7277        // THE PROOF IS LOCAL, two lines up: `data` is `vec![_; n * n]`, and the
7278        // shape `[n, n]` has `elem_count() == n * n`. Both are computed from `n`
7279        // in this function; no caller supplies either, so they cannot disagree.
7280        let base = Self::from_f32(data, vec![n, n], device).expect(
7281            "tril2: data is vec![_; n*n] and shape is [n, n] (elem_count n*n), \n             both computed from `n` in this function -- lengths cannot disagree",
7282        );
7283        if dtype == DType::F32 {
7284            base
7285        } else {
7286            base.to_dtype(dtype).unwrap()
7287        }
7288    }
7289
7290    /// Upper-triangular ones matrix `[n, n]`.
7291    pub fn triu2(n: usize, dtype: DType, device: &Device) -> Self {
7292        let mut data = vec![0.0_f32; n * n];
7293        for i in 0..n {
7294            for j in i..n {
7295                data[i * n + j] = 1.0;
7296            }
7297        }
7298        // GAP-003 carve-out: `.expect` and NOT `?`, because this cannot fail and
7299        // saying it CAN would be a false claim in the signature -- `triu2` would
7300        // return a `Result` that is never `Err`, and every caller would write a
7301        // `?` for an error that does not exist.
7302        //
7303        // THE PROOF IS LOCAL, two lines up: `data` is `vec![_; n * n]`, and the
7304        // shape `[n, n]` has `elem_count() == n * n`. Both are computed from `n`
7305        // in this function; no caller supplies either, so they cannot disagree.
7306        let base = Self::from_f32(data, vec![n, n], device).expect(
7307            "triu2: data is vec![_; n*n] and shape is [n, n] (elem_count n*n), \n             both computed from `n` in this function -- lengths cannot disagree",
7308        );
7309        if dtype == DType::F32 {
7310            base
7311        } else {
7312            base.to_dtype(dtype).unwrap()
7313        }
7314    }
7315
7316    // ---- additional deferred-Phase-A items: indexing / multi-dim / RNG ----
7317
7318    /// Eager-API alias of [`Self::slice`] (PyTorch / Candle naming).
7319    /// `narrow(dim, start, len)` is `slice(dim, start, len)` —
7320    /// produces a view of `[start, start+len)` along `dim`. Bad input
7321    /// surfaces as a typed error at build time. Accepts any [`Dim`].
7322    pub fn narrow<D: Dim>(
7323        &self,
7324        dim: D,
7325        start: usize,
7326        len: usize,
7327    ) -> std::result::Result<Self, fuel_ir::Error> {
7328        self.slice(dim, start, len)
7329    }
7330
7331    /// Split into `chunks` views along `dim`. The split distributes the
7332    /// `chunk_size = ceil(dim_size / chunks)` extra slot to the leading
7333    /// chunks so every chunk's size differs by at most 1. If `dim_size
7334    /// < chunks`, returns `dim_size` singleton chunks instead of
7335    /// `chunks` chunks (matches eager / PyTorch). Accepts any [`Dim`].
7336    pub fn chunk<D: Dim>(
7337        &self,
7338        chunks: usize,
7339        dim: D,
7340    ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
7341        let shape = self.shape();
7342        let dim = dim.to_index(&shape, "chunk")?;
7343        if chunks == 0 {
7344            return Err(fuel_ir::Error::Msg("chunk: chunks must be > 0".into()).bt());
7345        }
7346        let dims = shape.dims();
7347        let size = dims[dim];
7348        if size < chunks {
7349            return Ok((0..size).map(|i| self.slice(dim, i, 1).unwrap()).collect());
7350        }
7351        let base = size / chunks;
7352        let extra = size % chunks;
7353        let mut out = Vec::with_capacity(chunks);
7354        let mut start = 0;
7355        for i in 0..chunks {
7356            let len = if i < extra { base + 1 } else { base };
7357            out.push(self.slice(dim, start, len).unwrap());
7358            start += len;
7359        }
7360        Ok(out)
7361    }
7362
7363    /// Sub-tensor at index `i` along dim 0. Equivalent to
7364    /// `self.slice(0, i, 1).unwrap().squeeze(0)`. Matches eager's `crate::Tensor::get`.
7365    pub fn get(&self, i: usize) -> std::result::Result<Self, fuel_ir::Error> {
7366        let dims = self.shape().dims().to_vec();
7367        if dims.is_empty() {
7368            return Ok(self.clone());
7369        }
7370        self.slice(0, i, 1).unwrap().squeeze(0)
7371    }
7372
7373    /// Sub-tensor at index along an arbitrary dim. Equivalent to
7374    /// `self.slice(dim, index, 1).unwrap().squeeze(dim)`. Matches eager's
7375    /// `crate::Tensor::get_on_dim`. Accepts any [`Dim`].
7376    pub fn get_on_dim<D: Dim>(
7377        &self,
7378        dim: D,
7379        index: usize,
7380    ) -> std::result::Result<Self, fuel_ir::Error> {
7381        let shape = self.shape();
7382        let dim = dim.to_index(&shape, "get_on_dim")?;
7383        self.slice(dim, index, 1).unwrap().squeeze(dim)
7384    }
7385
7386    /// Multi-dim sum: reduce over every dim in `dims`, squeezing each.
7387    /// Reduces from the highest dim down so the lower dim indices stay
7388    /// valid throughout the reduction.
7389    pub fn sum_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7390        let shape = self.shape();
7391        let mut sorted = dims.to_indexes(&shape, "sum_dims")?;
7392        sorted.sort_by(|a, b| b.cmp(a));
7393        sorted.dedup();
7394        let mut acc = self.clone();
7395        for d in sorted {
7396            acc = acc.sum_dim(d)?;
7397        }
7398        Ok(acc)
7399    }
7400
7401    /// Multi-dim mean: reduce over every dim in `dims`, squeezing each.
7402    /// Reduces from the highest dim down. Accepts any [`Dims`].
7403    pub fn mean_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7404        let shape = self.shape();
7405        let mut sorted = dims.to_indexes(&shape, "mean_dims")?;
7406        sorted.sort_by(|a, b| b.cmp(a));
7407        sorted.dedup();
7408        let mut acc = self.clone();
7409        for d in sorted {
7410            acc = acc.mean_dim(d)?;
7411        }
7412        Ok(acc)
7413    }
7414
7415    /// Multi-dim sum with keepdim: every named dim becomes size 1
7416    /// instead of being squeezed out. Reduce-order-invariant (every
7417    /// keepdim preserves indices). Returns Result because of cascade
7418    /// from [`Self::sum_keepdim`].
7419    pub fn sum_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7420        let mut sorted: Vec<usize> = dims.to_vec();
7421        sorted.sort();
7422        sorted.dedup();
7423        let mut acc = self.clone();
7424        for d in sorted {
7425            acc = acc.sum_keepdim(d)?;
7426        }
7427        Ok(acc)
7428    }
7429
7430    /// Multi-dim mean with keepdim.
7431    pub fn mean_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7432        let mut sorted: Vec<usize> = dims.to_vec();
7433        sorted.sort();
7434        sorted.dedup();
7435        let mut acc = self.clone();
7436        for d in sorted {
7437            acc = acc.mean_keepdim(d)?;
7438        }
7439        Ok(acc)
7440    }
7441
7442    /// Uniform random tensor in `[lo, up)` with shape/dtype/device matching `self`.
7443    /// Backed by [`rand::rng`]. Returns Err for unsupported dtypes.
7444    pub fn rand_like(&self, lo: f64, up: f64) -> std::result::Result<Self, fuel_ir::Error> {
7445        Self::rand(self.shape(), lo, up, self.dtype(), &Device::cpu())
7446    }
7447
7448    /// Normal random tensor with shape/dtype/device matching `self`.
7449    /// Returns Err for unsupported dtypes or invalid stdev.
7450    pub fn randn_like(&self, mean: f64, stdev: f64) -> std::result::Result<Self, fuel_ir::Error> {
7451        Self::randn(self.shape(), mean, stdev, self.dtype(), &Device::cpu())
7452    }
7453
7454    /// Uniform random tensor in `[lo, up)`. Static factory.
7455    /// Supported dtypes: F32, F64, BF16, F16. F32 is the typical
7456    /// initialization target. Returns Err for any other dtype.
7457    pub fn rand(
7458        shape: impl Into<Shape>,
7459        lo: f64,
7460        up: f64,
7461        dtype: DType,
7462        device: &Device,
7463    ) -> std::result::Result<Self, fuel_ir::Error> {
7464        use rand::RngExt;
7465        let shape = shape.into();
7466        let n = shape.elem_count();
7467        let mut rng = rand::rng();
7468        match dtype {
7469            DType::F32 => {
7470                let data: Vec<f32> = (0..n).map(|_| rng.random_range(lo..up) as f32).collect();
7471                Self::from_f32(data, shape, device)
7472            }
7473            DType::F64 => {
7474                let data: Vec<f64> = (0..n).map(|_| rng.random_range(lo..up)).collect();
7475                Self::from_f64(data, shape, device)
7476            }
7477            DType::BF16 => {
7478                let data: Vec<half::bf16> = (0..n)
7479                    .map(|_| half::bf16::from_f64(rng.random_range(lo..up)))
7480                    .collect();
7481                Self::from_bf16(data, shape, device)
7482            }
7483            DType::F16 => {
7484                let data: Vec<half::f16> = (0..n)
7485                    .map(|_| half::f16::from_f64(rng.random_range(lo..up)))
7486                    .collect();
7487                Self::from_f16(data, shape, device)
7488            }
7489            other => {
7490                Err(fuel_ir::Error::Msg(format!("Tensor::rand: unsupported dtype {other:?}",)).bt())
7491            }
7492        }
7493    }
7494
7495    /// Normal random tensor with given `mean` and `stdev`. Static factory.
7496    /// Supported dtypes: F32, F64, BF16, F16. Returns Err on any other
7497    /// dtype, or if `stdev` is not finite / not positive.
7498    pub fn randn(
7499        shape: impl Into<Shape>,
7500        mean: f64,
7501        stdev: f64,
7502        dtype: DType,
7503        device: &Device,
7504    ) -> std::result::Result<Self, fuel_ir::Error> {
7505        use rand_distr::{Distribution, Normal};
7506        let shape = shape.into();
7507        let n = shape.elem_count();
7508        let normal = Normal::new(mean, stdev).map_err(|e| {
7509            fuel_ir::Error::Msg(format!("Tensor::randn: invalid stdev={stdev}: {e}",)).bt()
7510        })?;
7511        let mut rng = rand::rng();
7512        match dtype {
7513            DType::F32 => {
7514                let data: Vec<f32> = (0..n).map(|_| normal.sample(&mut rng) as f32).collect();
7515                Self::from_f32(data, shape, device)
7516            }
7517            DType::F64 => {
7518                let data: Vec<f64> = (0..n).map(|_| normal.sample(&mut rng)).collect();
7519                Self::from_f64(data, shape, device)
7520            }
7521            DType::BF16 => {
7522                let data: Vec<half::bf16> = (0..n)
7523                    .map(|_| half::bf16::from_f64(normal.sample(&mut rng)))
7524                    .collect();
7525                Self::from_bf16(data, shape, device)
7526            }
7527            DType::F16 => {
7528                let data: Vec<half::f16> = (0..n)
7529                    .map(|_| half::f16::from_f64(normal.sample(&mut rng)))
7530                    .collect();
7531                Self::from_f16(data, shape, device)
7532            }
7533            other => Err(fuel_ir::Error::Msg(format!(
7534                "Tensor::randn: unsupported dtype {other:?}",
7535            ))
7536            .bt()),
7537        }
7538    }
7539
7540    /// `arange(start, end, device)`: a rank-1 tensor of `[start, end)` in
7541    /// step 1, dtype F32. Matches NumPy / PyTorch convention.
7542    pub fn arange(start: f32, end: f32, device: &Device) -> Self {
7543        Self::arange_step(start, end, 1.0, device)
7544    }
7545
7546    /// `arange_step(start, end, step, device)`: a rank-1 tensor of
7547    /// `[start, end)` with constant step. F32 only for the static
7548    /// factory; cast for other dtypes. Errors at runtime if `step ==
7549    /// 0`.
7550    pub fn arange_step(start: f32, end: f32, step: f32, device: &Device) -> Self {
7551        assert!(step != 0.0, "arange_step: step must be non-zero");
7552        let mut data = Vec::new();
7553        let mut current = start;
7554        if step > 0.0 {
7555            while current < end {
7556                data.push(current);
7557                current += step;
7558            }
7559        } else {
7560            while current > end {
7561                data.push(current);
7562                current += step;
7563            }
7564        }
7565        let n = data.len();
7566        // GAP-003 carve-out. `.expect`, NOT `?`: this cannot fail, and a `Result`
7567        // here would be one that is never `Err` -- a false claim in the signature
7568        // that every caller would then write a `?` for.
7569        //
7570        // THE PROOF IS LOCAL AND ONE LINE UP: `n` IS `data.len()`, and the shape
7571        // is `[n]`. The shape is derived FROM the buffer, so they cannot disagree.
7572        Self::from_f32(data, vec![n], device).expect(
7573            "arange_step: shape is [n] where n IS data.len(), computed one line              above from that same buffer -- the lengths cannot disagree",
7574        )
7575    }
7576
7577    /// Linearly-spaced 1D tensor with `n` points from `start` to `end`
7578    /// (inclusive on both ends). Matches NumPy's `linspace`.
7579    pub fn linspace(start: f32, end: f32, n: usize, device: &Device) -> Self {
7580        assert!(n >= 1, "linspace: n must be >= 1");
7581        if n == 1 {
7582            // GAP-003 carve-out: a one-element vec with shape [1], on one line.
7583            return Self::from_f32(vec![start], vec![1], device)
7584                .expect("linspace: vec![start] has exactly 1 element and the shape is [1]");
7585        }
7586        let step = (end - start) / ((n - 1) as f32);
7587        let data: Vec<f32> = (0..n).map(|i| start + step * (i as f32)).collect();
7588        // GAP-003 carve-out: `data` is a `(0..n)` map -- exactly `n` elements --
7589        // and the shape is `[n]`. Both from the same `n`, two lines apart.
7590        Self::from_f32(data, vec![n], device).expect(
7591            "linspace: data is (0..n).map(..).collect() so it has exactly n              elements, and the shape is [n] -- both from the same `n`",
7592        )
7593    }
7594
7595    /// Frobenius norm: `sqrt(sum(self * self))`. Returns a scalar tensor.
7596    pub fn norm(&self) -> Self {
7597        self.sqr().sum_all().sqrt()
7598    }
7599
7600    /// General 1-D cross-correlation. Shapes:
7601    /// - `self`: `[N, Cin, T]`
7602    /// - `weight`: `[Cout, Cin/groups, K]`
7603    /// - `bias` (optional): `[Cout]`
7604    /// - returns: `[N, Cout, Tout]` where `Tout = (T + 2·padding - K) /
7605    ///   stride + 1`
7606    ///
7607    /// Implemented as a composite via Conv2D: unsqueeze the spatial dim
7608    /// to make a unit `H = 1`, run `Conv2D` with `Kh = 1, stride.0 = 1,
7609    /// padding.0 = 0`, then squeeze the dim back out. Works through
7610    /// every backend's Conv2D dispatch (CPU, CUDA via baracuda,
7611    /// Vulkan, AOCL, MKL). The future fused `Op::Conv1D` will collapse
7612    /// the unsqueeze/squeeze pair when a high-volume Conv1D consumer
7613    /// materializes.
7614    pub fn conv1d(
7615        &self,
7616        weight: &Self,
7617        bias: Option<&Self>,
7618        stride: usize,
7619        padding: usize,
7620        groups: usize,
7621    ) -> std::result::Result<Self, fuel_ir::Error> {
7622        let x_dims = self.shape().dims().to_vec();
7623        let w_dims = weight.shape().dims().to_vec();
7624        if x_dims.len() != 3 {
7625            return Err(fuel_ir::Error::Msg(format!(
7626                "conv1d: x must be rank 3 [N, Cin, T], got {x_dims:?}",
7627            ))
7628            .bt());
7629        }
7630        if w_dims.len() != 3 {
7631            return Err(fuel_ir::Error::Msg(format!(
7632                "conv1d: weight must be rank 3 [Cout, Cin/groups, K], got {w_dims:?}",
7633            ))
7634            .bt());
7635        }
7636        if groups < 1 {
7637            return Err(fuel_ir::Error::Msg("conv1d: groups must be ≥ 1".into()).bt());
7638        }
7639        if stride < 1 {
7640            return Err(fuel_ir::Error::Msg("conv1d: stride must be ≥ 1".into()).bt());
7641        }
7642        // Add a unit H dim at index 2 → [N, Cin, 1, T] and [Cout, Cin/g, 1, K].
7643        let x_4d = self.unsqueeze(2_usize)?;
7644        let w_4d = weight.unsqueeze(2_usize)?;
7645        let out_4d = x_4d.conv2d(&w_4d, bias, (1, stride), (0, padding), groups)?;
7646        out_4d.squeeze(2)
7647    }
7648
7649    /// Eager-API parity for `conv1d_with_algo`. The `_algo` selector is
7650    /// ignored on the lazy path — algorithm selection happens at
7651    /// backend dispatch time, not at graph construction. Reduces to
7652    /// [`Self::conv1d`].
7653    pub fn conv1d_with_algo<A>(
7654        &self,
7655        weight: &Self,
7656        bias: Option<&Self>,
7657        stride: usize,
7658        padding: usize,
7659        groups: usize,
7660        _algo: A,
7661    ) -> std::result::Result<Self, fuel_ir::Error> {
7662        self.conv1d(weight, bias, stride, padding, groups)
7663    }
7664
7665    /// 2-D average pooling. Input `[N, C, H, W]`, output
7666    /// `[N, C, H_out, W_out]` where
7667    /// `H_out = (H + 2·padding.0 - kernel.0) / stride.0 + 1`.
7668    ///
7669    /// Implemented as a **depthwise Conv2D** with a constant
7670    /// `1/(kh·kw)` kernel: one graph node + the kernel const. Works
7671    /// through every backend's Conv2D dispatch and inherits Conv2D's
7672    /// gradient. Composite supports arbitrary kernel / stride /
7673    /// padding.
7674    pub fn avg_pool2d(
7675        &self,
7676        kernel: (usize, usize),
7677        stride: (usize, usize),
7678        padding: (usize, usize),
7679        count_include_pad: bool,
7680    ) -> std::result::Result<Self, fuel_ir::Error> {
7681        let dims = self.shape().dims().to_vec();
7682        if dims.len() != 4 {
7683            return Err(fuel_ir::Error::Msg(format!(
7684                "avg_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7685            ))
7686            .bt());
7687        }
7688        let (c, h, w) = (dims[1], dims[2], dims[3]);
7689        let (kh, kw) = kernel;
7690        if kh == 0 || kw == 0 {
7691            return Err(
7692                fuel_ir::Error::Msg("avg_pool2d: kernel sizes must be positive".into()).bt(),
7693            );
7694        }
7695
7696        // KISS-Ops §6.13: count_include_pad must be an EXPLICIT choice, not
7697        // an implicit torch-style default silently baked into the divisor.
7698        // With no padding the two modes coincide (every window is full),
7699        // so the uniform-divisor path below is exact either way.
7700        if count_include_pad || padding == (0, 0) {
7701            let inv = 1.0_f32 / ((kh * kw) as f32);
7702            // Depthwise kernel: one filter per input channel, each filter
7703            // is a constant 1/(kh·kw). Shape [C, 1, kh, kw] with groups=C
7704            // makes Conv2D compute one independent kernel per channel.
7705            let weight =
7706                self.const_f32_like(vec![inv; c * kh * kw], Shape::from_dims(&[c, 1, kh, kw]))?;
7707            return self.conv2d(&weight, None, stride, padding, c);
7708        }
7709
7710        // count_include_pad = false with real padding: a uniform divisor is
7711        // WRONG at every border window (it silently averages in the padded
7712        // zeros as if they were real taps, undercounting the true mean by
7713        // exactly the fraction of the window that's padding). The correct
7714        // divisor is the per-position COUNT of valid (non-padded) taps, which
7715        // varies by output position — sum-pool (an all-ones depthwise kernel)
7716        // then divide by that count map instead of a constant.
7717        //
7718        // A window can have ZERO valid taps (e.g. a 1x1 input with padding
7719        // >= kernel size): every tap it covers is padding. The count map
7720        // would then be zero at that position and `broadcast_div` would
7721        // silently emit NaN/inf instead of a defined average. Refuse up
7722        // front rather than let that propagate -- computed host-side from
7723        // (H, W, kernel, stride, padding) alone, no graph ops needed.
7724        if let Some((axis, out_idx)) = first_zero_tap_output_index(h, kh, stride.0, padding.0)
7725            .map(|i| ("H", i))
7726            .or_else(|| first_zero_tap_output_index(w, kw, stride.1, padding.1).map(|i| ("W", i)))
7727        {
7728            return Err(fuel_ir::Error::Msg(format!(
7729                "avg_pool2d: count_include_pad=false has no valid (non-padded) \
7730                 taps for output position {out_idx} on the {axis} axis \
7731                 (kernel={kernel:?}, stride={stride:?}, padding={padding:?}, \
7732                 input H×W={h}×{w}) — the average would divide by zero",
7733            ))
7734            .bt());
7735        }
7736
7737        let sum_weight = self.const_f32_like(
7738            vec![1.0_f32; c * kh * kw],
7739            Shape::from_dims(&[c, 1, kh, kw]),
7740        )?;
7741        let summed = self.conv2d(&sum_weight, None, stride, padding, c)?;
7742
7743        // The valid-tap count depends only on (H, W, kernel, stride,
7744        // padding) -- not on N or C -- so it's computed once at minimal
7745        // [1, 1, H, W] size and broadcast-divided into every (n, c) plane.
7746        let ones_hw = self.const_f32_like(vec![1.0_f32; h * w], Shape::from_dims(&[1, 1, h, w]))?;
7747        let count_weight =
7748            self.const_f32_like(vec![1.0_f32; kh * kw], Shape::from_dims(&[1, 1, kh, kw]))?;
7749        let count = ones_hw.conv2d(&count_weight, None, stride, padding, 1)?;
7750
7751        summed.broadcast_div(&count)
7752    }
7753
7754    /// Eager-API parity for `avg_pool2d_with_stride`. Same shape as
7755    /// [`Self::avg_pool2d`] but the stride is passed explicitly rather
7756    /// than inferred from kernel.
7757    pub fn avg_pool2d_with_stride(
7758        &self,
7759        kernel: (usize, usize),
7760        stride: (usize, usize),
7761    ) -> std::result::Result<Self, fuel_ir::Error> {
7762        // No padding, so count_include_pad is a don't-care (both modes
7763        // coincide — see avg_pool2d's own short-circuit for this case).
7764        self.avg_pool2d(kernel, stride, (0, 0), true)
7765    }
7766
7767    /// 2-D max pooling. Input `[N, C, H, W]`, output
7768    /// `[N, C, H_out, W_out]` where
7769    /// `H_out = (H + 2·padding.0 - kernel.0) / stride.0 + 1`.
7770    ///
7771    /// Composite via slice + maximum: pad the input, then for every
7772    /// `(ky, kx)` in `[0..kh, 0..kw]` slice the strided grid of taps
7773    /// (one tap per output position) and take the elementwise max.
7774    /// Produces `kh·kw` nodes per call plus padding — cheap for the
7775    /// common 2×2 / 3×3 cases.
7776    ///
7777    /// Strided-slice trick: for stride `sh`, reshape the padded H from
7778    /// `(H_out · sh)` to `(H_out, sh)`, then slice the inner `sh`-dim
7779    /// at the tap index. Requires `H_padded == H_out · sh` exactly.
7780    /// Inputs that don't divide cleanly will be padded so they do.
7781    pub fn max_pool2d(
7782        &self,
7783        kernel: (usize, usize),
7784        stride: (usize, usize),
7785        padding: (usize, usize),
7786    ) -> std::result::Result<Self, fuel_ir::Error> {
7787        // KISS-Ops §6.13 / PyTorch-correct semantics: a padded slot must
7788        // never win the max — zero padding silently wins every all-negative
7789        // window (e.g. post-activation feature maps dominated by negative
7790        // values at the border), which is a real correctness bug, not a
7791        // style choice. `f32::NEG_INFINITY` padding is the fix; use
7792        // [`Self::max_pool2d_with_pad_value`] directly for any OTHER pad
7793        // value (there is no implicit torch-style default here anymore).
7794        self.max_pool2d_with_pad_value(kernel, stride, padding, f32::NEG_INFINITY)
7795    }
7796
7797    /// `max_pool2d` variant where the boundary padding is filled with an
7798    /// explicit `pad_value` instead of `0.0`. Pass `f32::NEG_INFINITY`
7799    /// for PyTorch-correct semantics (padded slots can never win the
7800    /// max). All other constraints match [`Self::max_pool2d`]; the only
7801    /// difference is the constant value the implicit boundary pad uses.
7802    pub fn max_pool2d_with_pad_value(
7803        &self,
7804        kernel: (usize, usize),
7805        stride: (usize, usize),
7806        padding: (usize, usize),
7807        pad_value: f32,
7808    ) -> std::result::Result<Self, fuel_ir::Error> {
7809        let dims = self.shape().dims().to_vec();
7810        if dims.len() != 4 {
7811            return Err(fuel_ir::Error::Msg(format!(
7812                "max_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7813            ))
7814            .bt());
7815        }
7816        let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7817        let (kh, kw) = kernel;
7818        let (sh, sw) = stride;
7819        let (ph, pw) = padding;
7820        if kh == 0 || kw == 0 {
7821            return Err(
7822                fuel_ir::Error::Msg("max_pool2d: kernel sizes must be positive".into()).bt(),
7823            );
7824        }
7825        if sh == 0 || sw == 0 {
7826            return Err(fuel_ir::Error::Msg("max_pool2d: strides must be positive".into()).bt());
7827        }
7828        let h_padded_min = h + 2 * ph;
7829        let w_padded_min = w + 2 * pw;
7830        if h_padded_min < kh || w_padded_min < kw {
7831            return Err(fuel_ir::Error::Msg(format!(
7832                "max_pool2d: padded input ({h_padded_min}×{w_padded_min}) smaller than kernel ({kh}×{kw})",
7833            )).bt());
7834        }
7835        let h_out = (h_padded_min - kh) / sh + 1;
7836        let w_out = (w_padded_min - kw) / sw + 1;
7837        // Pad H/W to be exactly (h_out · sh + (kh - sh)) and (w_out · sw + (kw - sw)),
7838        // i.e., enough to contain every (ky, kx) tap for every output.
7839        let h_total = h_out * sh + (kh - 1);
7840        let w_total = w_out * sw + (kw - 1);
7841        let extra_h = h_total.saturating_sub(h_padded_min);
7842        let extra_w = w_total.saturating_sub(w_padded_min);
7843        let padded = self
7844            .pad_with_value(2, ph, ph + extra_h, pad_value)?
7845            .pad_with_value(3, pw, pw + extra_w, pad_value)?;
7846        // For each (ky, kx) collect the strided tap.
7847        let mut acc: Option<Tensor> = None;
7848        for ky in 0..kh {
7849            // Slice H starting at ky, length h_out · sh, then reshape
7850            // to [N, C, h_out, sh, w_total] and slice the sh-dim at 0
7851            // (we'll handle stride > 1 by reshape).
7852            let row_slice = padded.slice(2, ky, h_out * sh).unwrap();
7853            // Reshape H dim of length `h_out · sh` into (h_out, sh),
7854            // then take dim 3 at offset 0 (the tap on the sh axis).
7855            let row_reshaped = row_slice.reshape(vec![n, c, h_out, sh, w_total])?;
7856            let row_tap = row_reshaped.slice(3, 0, 1).unwrap().squeeze(3)?;
7857            for kx in 0..kw {
7858                let col_slice = row_tap.slice(3, kx, w_out * sw).unwrap();
7859                let col_reshaped = col_slice.reshape(vec![n, c, h_out, w_out, sw])?;
7860                let win = col_reshaped.slice(4, 0, 1).unwrap().squeeze(4)?;
7861                acc = Some(match acc {
7862                    None => win,
7863                    Some(a) => a.maximum(&win).unwrap(),
7864                });
7865            }
7866        }
7867        acc.ok_or_else(|| fuel_ir::Error::Msg("max_pool2d: empty kernel".into()).bt())
7868    }
7869
7870    /// Eager-API parity for `max_pool2d_with_stride`.
7871    pub fn max_pool2d_with_stride(
7872        &self,
7873        kernel: (usize, usize),
7874        stride: (usize, usize),
7875    ) -> std::result::Result<Self, fuel_ir::Error> {
7876        self.max_pool2d(kernel, stride, (0, 0))
7877    }
7878
7879    /// Nearest-neighbor upsample by integer factor `scale` along the
7880    /// last two spatial dims. Input `[N, C, H, W]` → output
7881    /// `[N, C, H·scale, W·scale]`.
7882    ///
7883    /// Composite via reshape + concat + reshape: insert a unit dim
7884    /// after each spatial dim, concat `scale` copies of the tensor on
7885    /// each new dim, then collapse the inflated dims back. Same shape
7886    /// as the `upsample_nearest_2x` helper in `fuel_transformers::models::lazy_yolov8`
7887    /// and `fuel_transformers::models::lazy_sd_unet`, generalized to arbitrary scale.
7888    pub fn upsample_nearest2d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7889        if scale == 0 {
7890            return Err(
7891                fuel_ir::Error::Msg("upsample_nearest2d: scale must be positive".into()).bt(),
7892            );
7893        }
7894        let dims = self.shape().dims().to_vec();
7895        if dims.len() != 4 {
7896            return Err(fuel_ir::Error::Msg(format!(
7897                "upsample_nearest2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7898            ))
7899            .bt());
7900        }
7901        if scale == 1 {
7902            return Ok(self.clone());
7903        }
7904        let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7905        // [N, C, H, 1, W, 1]
7906        let expanded = self.reshape(vec![n, c, h, 1, w, 1])?;
7907        // Replicate along the new unit dims by concatenating scale copies.
7908        let h_expanded = (0..scale)
7909            .fold(None, |acc: Option<Tensor>, _| {
7910                Some(match acc {
7911                    None => expanded.clone(),
7912                    Some(a) => a.concat(&expanded, 3).unwrap(),
7913                })
7914            })
7915            .unwrap();
7916        let h_then_w = (0..scale)
7917            .fold(None, |acc: Option<Tensor>, _| {
7918                Some(match acc {
7919                    None => h_expanded.clone(),
7920                    Some(a) => a.concat(&h_expanded, 5).unwrap(),
7921                })
7922            })
7923            .unwrap();
7924        h_then_w.reshape(vec![n, c, h * scale, w * scale])
7925    }
7926
7927    /// Nearest-neighbor upsample for 1-D signals `[N, C, T]` by integer
7928    /// `scale`. Reshape to insert a unit dim, concat scale copies,
7929    /// reshape back.
7930    pub fn upsample_nearest1d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7931        if scale == 0 {
7932            return Err(
7933                fuel_ir::Error::Msg("upsample_nearest1d: scale must be positive".into()).bt(),
7934            );
7935        }
7936        let dims = self.shape().dims().to_vec();
7937        if dims.len() != 3 {
7938            return Err(fuel_ir::Error::Msg(format!(
7939                "upsample_nearest1d: input must be rank 3 [N, C, T], got {dims:?}",
7940            ))
7941            .bt());
7942        }
7943        if scale == 1 {
7944            return Ok(self.clone());
7945        }
7946        let (n, c, t) = (dims[0], dims[1], dims[2]);
7947        let expanded = self.reshape(vec![n, c, t, 1])?;
7948        let replicated = (0..scale)
7949            .fold(None, |acc: Option<Tensor>, _| {
7950                Some(match acc {
7951                    None => expanded.clone(),
7952                    Some(a) => a.concat(&expanded, 3).unwrap(),
7953                })
7954            })
7955            .unwrap();
7956        replicated.reshape(vec![n, c, t * scale])
7957    }
7958
7959    /// 2-D nearest interpolation to an explicit target size.
7960    /// Arbitrary ratios (non-integer, non-uniform between H and
7961    /// W) supported via an `index_select`-based composite. The
7962    /// indexing convention matches PyTorch / the eager kernel:
7963    /// `src_h[oi] = min(H - 1, floor(oi * H / H_out))`.
7964    ///
7965    /// Used by DepthAnythingV2's DPT head and similar dense
7966    /// prediction heads that resize feature maps to arbitrary
7967    /// targets.
7968    pub fn interpolate2d(
7969        &self,
7970        target_h: usize,
7971        target_w: usize,
7972    ) -> std::result::Result<Self, fuel_ir::Error> {
7973        let dims = self.shape().dims().to_vec();
7974        if dims.len() != 4 {
7975            return Err(fuel_ir::Error::Msg(format!(
7976                "interpolate2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7977            ))
7978            .bt());
7979        }
7980        let h = dims[2];
7981        let w = dims[3];
7982        if h == 0 || w == 0 || target_h == 0 || target_w == 0 {
7983            return Err(fuel_ir::Error::Msg(
7984                "interpolate2d: input + target spatial dims must be positive".into(),
7985            )
7986            .bt());
7987        }
7988        // Fast-path: identity.
7989        if target_h == h && target_w == w {
7990            return Ok(self.clone());
7991        }
7992        // Fast-path: integer-multiple uniform scale → existing
7993        // `upsample_nearest2d` (more cache-friendly than the
7994        // index_select composite for the common 2× / 4× case).
7995        if target_h.is_multiple_of(h) && target_w.is_multiple_of(w) && target_h / h == target_w / w
7996        {
7997            return self.upsample_nearest2d(target_h / h);
7998        }
7999        // General case: build per-axis source-index tensors and
8000        // index_select. Matches the eager UpsampleNearest2D
8001        // kernel's convention: src_idx = min(src - 1, floor(out * src / target)).
8002        let h_idx: Vec<u32> = (0..target_h)
8003            .map(|oi| ((oi * h) / target_h).min(h - 1) as u32)
8004            .collect();
8005        let w_idx: Vec<u32> = (0..target_w)
8006            .map(|oj| ((oj * w) / target_w).min(w - 1) as u32)
8007            .collect();
8008        let h_idx_tensor = self.const_u32_like(h_idx, fuel_ir::Shape::from_dims(&[target_h]))?;
8009        let w_idx_tensor = self.const_u32_like(w_idx, fuel_ir::Shape::from_dims(&[target_w]))?;
8010        let after_h = self.index_select(2_usize, &h_idx_tensor)?;
8011        after_h.index_select(3_usize, &w_idx_tensor)
8012    }
8013
8014    /// 1-D nearest interpolation to an explicit target size. Same
8015    /// constraints as [`Self::interpolate2d`]: integer-multiple targets
8016    /// only.
8017    pub fn interpolate1d(&self, target_t: usize) -> std::result::Result<Self, fuel_ir::Error> {
8018        let dims = self.shape().dims().to_vec();
8019        if dims.len() != 3 {
8020            return Err(fuel_ir::Error::Msg(format!(
8021                "interpolate1d: input must be rank 3 [N, C, T], got {dims:?}",
8022            ))
8023            .bt());
8024        }
8025        let t = dims[2];
8026        if t == 0 {
8027            return Err(
8028                fuel_ir::Error::Msg("interpolate1d: input length must be positive".into()).bt(),
8029            );
8030        }
8031        if !target_t.is_multiple_of(t) {
8032            return Err(fuel_ir::Error::Msg(format!(
8033                "interpolate1d: target {target_t} must be integer multiple of input {t}; non-integer ratios are future work",
8034            )).bt());
8035        }
8036        self.upsample_nearest1d(target_t / t)
8037    }
8038
8039    /// Pad with zeros along `dim`: `left` zeros before, `right` zeros
8040    /// after. Thin wrapper over [`Self::pad_with_value`] with `value = 0.0`.
8041    /// Composite — no new graph op. Accepts any [`Dim`].
8042    pub fn pad_with_zeros<D: Dim>(
8043        &self,
8044        dim: D,
8045        left: usize,
8046        right: usize,
8047    ) -> std::result::Result<Self, fuel_ir::Error> {
8048        self.pad_with_value(dim, left, right, 0.0)
8049    }
8050
8051    /// Pad with a constant `value` along `dim`: `before` slots before,
8052    /// `after` slots after. Wraps [`Self::pad`] with `PadMode::Constant`
8053    /// for the named dim (other dims get `(0, 0)`); the `f32` value is
8054    /// widened to the graph op's `f64` param. Useful for `-inf` padding
8055    /// around max-reductions (e.g. PyTorch-style `max_pool2d`, where
8056    /// padded positions must never win the max). Accepts any [`Dim`].
8057    pub fn pad_with_value<D: Dim>(
8058        &self,
8059        dim: D,
8060        before: usize,
8061        after: usize,
8062        value: f32,
8063    ) -> std::result::Result<Self, fuel_ir::Error> {
8064        let shape = self.shape();
8065        let dim = dim.to_index(&shape, "pad_with_value")?;
8066        let rank = shape.dims().len();
8067        if before == 0 && after == 0 {
8068            return Ok(self.clone());
8069        }
8070        let mut padding: Vec<(usize, usize)> = vec![(0, 0); rank];
8071        padding[dim] = (before, after);
8072        self.pad(padding, fuel_graph::PadMode::Constant, value as f64)
8073    }
8074
8075    /// Coordinate grids from rank-1 inputs. Matches PyTorch's
8076    /// `torch.meshgrid` and eager's `crate::Tensor::meshgrid`:
8077    ///
8078    /// - `xy_indexing = true` (Cartesian, NumPy default): the first
8079    ///   two inputs are swapped before broadcasting and the resulting
8080    ///   grids reversed back, so for `(x, y)` the grids carry shape
8081    ///   `[len(y), len(x)]` (image-coordinate convention).
8082    /// - `xy_indexing = false` (matrix / ij): grids carry the input
8083    ///   cardinalities in input order.
8084    ///
8085    /// Implemented as `reshape` + `repeat`. All inputs must share the
8086    /// same dtype and live on the same graph.
8087    pub fn meshgrid(
8088        args: &[&Self],
8089        xy_indexing: bool,
8090    ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
8091        if args.len() < 2 {
8092            return Err(fuel_ir::Error::Msg(
8093                "meshgrid: requires at least two rank-1 tensors".into(),
8094            )
8095            .bt());
8096        }
8097        let ordered: Vec<&Self> = if xy_indexing {
8098            args.iter().rev().copied().collect()
8099        } else {
8100            args.to_vec()
8101        };
8102        let mut lens = Vec::with_capacity(ordered.len());
8103        for (i, t) in ordered.iter().enumerate() {
8104            let dims = t.shape().dims().to_vec();
8105            if dims.len() != 1 {
8106                return Err(fuel_ir::Error::Msg(format!(
8107                    "meshgrid: input {i} must be rank 1, got shape {dims:?}",
8108                ))
8109                .bt());
8110            }
8111            lens.push(dims[0]);
8112        }
8113        let mut grids = Vec::with_capacity(ordered.len());
8114        for (idx, t) in ordered.iter().enumerate() {
8115            let mut shape = vec![1_usize; ordered.len()];
8116            shape[idx] = lens[idx];
8117            let placed = t.reshape(shape)?;
8118            let mut repeats = lens.clone();
8119            repeats[idx] = 1;
8120            let grid = placed.repeat(repeats)?;
8121            grids.push(grid);
8122        }
8123        if xy_indexing {
8124            grids.reverse();
8125        }
8126        Ok(grids)
8127    }
8128
8129    /// Repeat the tensor along each dim `repeats[i]` times. If `repeats`
8130    /// has more dims than `self`, `self` is implicitly left-padded with
8131    /// size-1 dims to match. Matches PyTorch's `Tensor.repeat`.
8132    pub fn repeat(&self, repeats: impl Into<Shape>) -> std::result::Result<Self, fuel_ir::Error> {
8133        let repeats = repeats.into();
8134        let repeats: Vec<usize> = repeats.dims().to_vec();
8135        let self_rank = self.shape().dims().len();
8136        let target_rank = repeats.len();
8137        let mut work = if self_rank < target_rank {
8138            let pad_count = target_rank - self_rank;
8139            let mut new_shape: Vec<usize> = vec![1; pad_count];
8140            new_shape.extend_from_slice(self.shape().dims());
8141            self.reshape(new_shape)?
8142        } else if self_rank > target_rank {
8143            return Err(fuel_ir::Error::Msg(format!(
8144                "repeat: repeats rank {target_rank} smaller than tensor rank {self_rank}",
8145            ))
8146            .bt());
8147        } else {
8148            self.clone()
8149        };
8150        for (axis, &n) in repeats.iter().enumerate() {
8151            if n == 0 {
8152                return Err(fuel_ir::Error::Msg(format!(
8153                    "repeat: zero repeat count at axis {axis} not supported",
8154                ))
8155                .bt());
8156            }
8157            if n == 1 {
8158                continue;
8159            }
8160            // n copies concatenated along `axis`.
8161            let base = work.clone();
8162            for _ in 1..n {
8163                work = work.concat(&base, axis)?;
8164            }
8165        }
8166        Ok(work)
8167    }
8168}
8169
8170/// For a single pooling axis, the first output index (if any) whose window
8171/// has zero valid (non-padded) taps — every tap it covers lies outside
8172/// `[0, dim)`. Used by [`Tensor::avg_pool2d`]'s `count_include_pad = false`
8173/// path to refuse a divide-by-zero divisor instead of emitting NaN/inf.
8174///
8175/// Pure host-side arithmetic over the pooling shape parameters; computes no
8176/// tensor, builds no graph. `out_dim` matches the formula in
8177/// [`Tensor::avg_pool2d`]'s doc comment: `(dim + 2*pad - kernel) / stride + 1`.
8178fn first_zero_tap_output_index(
8179    dim: usize,
8180    kernel: usize,
8181    stride: usize,
8182    pad: usize,
8183) -> Option<usize> {
8184    let out_dim = (dim + 2 * pad).saturating_sub(kernel) / stride + 1;
8185    (0..out_dim).find(|&o| {
8186        let start = o as isize * stride as isize - pad as isize;
8187        let end = start + kernel as isize;
8188        let valid_start = start.max(0);
8189        let valid_end = end.min(dim as isize);
8190        valid_end <= valid_start
8191    })
8192}
8193
8194// ---- safetensors integration -----------------------------------------------
8195
8196impl Tensor {
8197    /// Build a `Tensor` from raw little-endian bytes as they appear
8198    /// in a safetensors file, plus a dtype and shape. Row-major layout
8199    /// is assumed. The byte count must match `shape.elem_count() *
8200    /// dtype_bytes`.
8201    ///
8202    /// This is the low-level loader. Prefer [`Self::from_safetensors_view`]
8203    /// if you already have a `safetensors::TensorView` in hand.
8204    ///
8205    /// Supported dtypes today: `F32`, `F64`, `BF16`, `F16`, `U32`.
8206    /// Integer types other than `U32` are rejected to keep the
8207    /// surface small; add them when a real model needs them.
8208    pub fn from_safetensors_bytes(
8209        bytes: &[u8],
8210        dtype: safetensors::Dtype,
8211        shape: &[usize],
8212        device: &Device,
8213    ) -> crate::Result<Self> {
8214        use safetensors::Dtype;
8215        let shape_obj = Shape::from_dims(shape);
8216        let elem_count = shape_obj.elem_count();
8217
8218        let check_len = |expected: usize| -> crate::Result<()> {
8219            if bytes.len() != expected {
8220                crate::bail!(
8221                    "from_safetensors_bytes: expected {expected} bytes for dtype {dtype:?} \
8222                     and shape {shape:?}, got {}",
8223                    bytes.len(),
8224                );
8225            }
8226            Ok(())
8227        };
8228
8229        match dtype {
8230            Dtype::F32 => {
8231                check_len(elem_count * 4)?;
8232                let mut data = Vec::with_capacity(elem_count);
8233                for chunk in bytes.as_chunks::<4>().0.iter() {
8234                    data.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8235                }
8236                Self::from_f32(data, shape_obj, device)
8237            }
8238            Dtype::F64 => {
8239                check_len(elem_count * 8)?;
8240                let mut data = Vec::with_capacity(elem_count);
8241                for chunk in bytes.as_chunks::<8>().0.iter() {
8242                    let arr: [u8; 8] = *chunk;
8243                    data.push(f64::from_le_bytes(arr));
8244                }
8245                Self::from_f64(data, shape_obj, device)
8246            }
8247            Dtype::BF16 => {
8248                check_len(elem_count * 2)?;
8249                let mut data = Vec::with_capacity(elem_count);
8250                for chunk in bytes.as_chunks::<2>().0.iter() {
8251                    let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8252                    data.push(half::bf16::from_bits(raw));
8253                }
8254                Self::from_bf16(data, shape_obj, device)
8255            }
8256            Dtype::F16 => {
8257                check_len(elem_count * 2)?;
8258                let mut data = Vec::with_capacity(elem_count);
8259                for chunk in bytes.as_chunks::<2>().0.iter() {
8260                    let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8261                    data.push(half::f16::from_bits(raw));
8262                }
8263                Self::from_f16(data, shape_obj, device)
8264            }
8265            Dtype::U32 => {
8266                check_len(elem_count * 4)?;
8267                let mut data = Vec::with_capacity(elem_count);
8268                for chunk in bytes.as_chunks::<4>().0.iter() {
8269                    data.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8270                }
8271                Self::from_u32(data, shape_obj, device)
8272            }
8273            other => crate::bail!(
8274                "from_safetensors_bytes: unsupported dtype {other:?} — extend Tensor's \
8275                 safetensors loader to handle it",
8276            ),
8277        }
8278    }
8279
8280    /// Build a `Tensor` from a `safetensors::TensorView`. This is
8281    /// the most natural entry point when iterating over a
8282    /// [`fuel_loaders::safetensors::MmapedSafetensors`] or similar.
8283    pub fn from_safetensors_view(
8284        view: &safetensors::tensor::TensorView<'_>,
8285        device: &Device,
8286    ) -> crate::Result<Self> {
8287        Self::from_safetensors_bytes(view.data(), view.dtype(), view.shape(), device)
8288    }
8289}
8290
8291/// A LayerNorm's learned `(gain, bias)`, both length `[dim]`. Wrapped in
8292/// `Option` at sites where the norm is conditional — present only on layer 0
8293/// (RWKV `pre_ln`), or only when a config flag is set (Persimmon QK-LN).
8294pub type LayerNormPair = (Arc<[f32]>, Arc<[f32]>);
8295
8296/// A convolution's `(weight, bias)`: the flattened `[out, in, kh, kw]` kernel
8297/// and its `[out]` bias. Distinct from [`LayerNormPair`] despite the identical
8298/// shape — the two are not interchangeable and naming them apart is the point.
8299pub type ConvWeightBias = (Arc<[f32]>, Arc<[f32]>);
8300
8301/// Weight tensor storage that preserves source precision.
8302///
8303/// Projection weights (Q/K/V/O for attention, gate/up/down for FFN,
8304/// and the output `lm_head` matrix) stay in whatever dtype the
8305/// source checkpoint used — f32 when that's how it was saved, bf16
8306/// for modern HF checkpoints that ship bf16 to halve weight memory.
8307/// Activations are usually f32, but LlamaModel's cached decode path
8308/// (Phase D increment A, "BF16-throughout decode") threads the KV
8309/// cache's dtype through the whole activation stream — when that's
8310/// BF16, weights must ALSO be BF16 (`matmul`'s dtype gate only allows
8311/// same-dtype homogeneous or `(lhs=F32, rhs=BF16)`; a BF16-activation ×
8312/// F32-weight matmul is rejected, the opposite direction from the
8313/// classic f32-activation-over-bf16-weight quantized-serving case).
8314/// F32 activations still route the mixed `(A:F32, B:BF16) → F32`
8315/// precision via `VulkanBackend::matmul` unchanged.
8316///
8317/// Norm gains and biases are NOT covered by this enum — they're
8318/// small and precision-sensitive, so they stay `Arc<[f32]>`.
8319///
8320/// Cloning is cheap (Arc bump) for both variants. Use
8321/// [`WeightStorage::const_like`] to emit a [`Tensor`] `Const`
8322/// node with the right dtype.
8323#[derive(Debug, Clone)]
8324pub enum WeightStorage {
8325    F32(Arc<[f32]>),
8326    BF16(Arc<[half::bf16]>),
8327    /// GGML Q4_0 blocks (raw byte stream), laid out as `[out_features,
8328    /// in_features / 32]` blocks (18 bytes each, llama.cpp convention).
8329    /// Stored as `Arc<[u32]>` — the byte stream reinterpreted as u32
8330    /// words so subsequent forward passes just Arc-clone (cheap) rather
8331    /// than recopying the bytes. The graph sees this directly as a U32
8332    /// tensor; matmul dispatch goes through `Op::QMatMul`.
8333    ///
8334    /// `bytes_len` is the original byte count (u32_len * 4) so the
8335    /// const_like shape computation doesn't accidentally round up.
8336    Q4_0 {
8337        words: Arc<[u32]>,
8338        bytes_len: usize,
8339        in_features: usize,
8340        out_features: usize,
8341    },
8342    /// Base weight wrapped with a trainable LoRA (Low-Rank Adaptation)
8343    /// update: effective weight `W_eff = base + (alpha / rank) · A · B`
8344    /// where `A` has shape `[in_features, rank]` and `B` has shape
8345    /// `[rank, out_features]` (both stored in the same layout
8346    /// convention as F32 weights — `[in, out]`).
8347    ///
8348    /// Used for PEFT-style inference with frozen base weights (which
8349    /// can be F32, BF16, or Q4_0) plus small trainable adapter matrices.
8350    /// The adapter is cheap to apply — for a 2560×2560 projection at
8351    /// rank 8 the LoRA path is ~0.5% of the base matmul cost.
8352    WithLoRA {
8353        base: Box<WeightStorage>,
8354        /// `[in_features, rank]` adapter A (HF's `lora_A` transposed).
8355        lora_a: Arc<[f32]>,
8356        /// `[rank, out_features]` adapter B (HF's `lora_B` transposed).
8357        lora_b: Arc<[f32]>,
8358        rank: usize,
8359        /// LoRA scaling factor; effective scale is `alpha / rank`.
8360        alpha: f32,
8361        in_features: usize,
8362        out_features: usize,
8363    },
8364}
8365
8366impl WeightStorage {
8367    pub fn elem_count(&self) -> usize {
8368        match self {
8369            Self::F32(a) => a.len(),
8370            Self::BF16(a) => a.len(),
8371            // Logical element count for a Q4_0 weight matrix is n*k.
8372            Self::Q4_0 {
8373                in_features,
8374                out_features,
8375                ..
8376            } => *in_features * *out_features,
8377            Self::WithLoRA {
8378                in_features,
8379                out_features,
8380                ..
8381            } => *in_features * *out_features,
8382        }
8383    }
8384
8385    pub fn dtype(&self) -> fuel_ir::DType {
8386        match self {
8387            Self::F32(_) => fuel_ir::DType::F32,
8388            Self::BF16(_) => fuel_ir::DType::BF16,
8389            // Q4_0 surfaces as U32 at the graph level (raw bytes
8390            // reinterpreted). Callers that care about the "actual"
8391            // quantization type should match on the variant directly.
8392            Self::Q4_0 { .. } => fuel_ir::DType::U32,
8393            // WithLoRA exposes the base's dtype (the LoRA adapter is
8394            // always F32 but activations are typed by the base weight).
8395            Self::WithLoRA { base, .. } => base.dtype(),
8396        }
8397    }
8398
8399    /// Emit a `Const` node on `anchor`'s graph matching this
8400    /// storage's dtype. Used everywhere the forward pass wraps a
8401    /// weight into a `Tensor`.
8402    ///
8403    /// For `Q4_0`, the emitted tensor is a 1-D `U32` const of length
8404    /// `bytes.len() / 4` holding the raw block byte stream. Callers
8405    /// must pair this with `Tensor::qmatmul` rather than `matmul`.
8406    ///
8407    /// Returns Err for `WithLoRA` — the base + LoRA update can only be
8408    /// applied via `apply_linear` so the right graph structure is built.
8409    pub fn const_like(
8410        &self,
8411        anchor: &Tensor,
8412        shape: Shape,
8413    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8414        match self {
8415            Self::F32(a) => anchor.const_f32_like(a.clone(), shape),
8416            Self::BF16(a) => anchor.const_bf16_like(a.clone(), shape),
8417            Self::Q4_0 { words, .. } => {
8418                let _ = shape; // shape arg unused — Q4_0 const is 1-D U32
8419                // Arc-clone the precomputed u32 view; no byte copy.
8420                anchor.const_u32_like(Arc::clone(words), Shape::from_dims(&[words.len()]))
8421            }
8422            Self::WithLoRA { .. } => Err(fuel_ir::Error::Msg(
8423                "WeightStorage::WithLoRA::const_like is not supported \
8424                 — the base + LoRA update must be applied via \
8425                 apply_linear to produce the right graph structure."
8426                    .into(),
8427            )
8428            .bt()),
8429        }
8430    }
8431
8432    /// Produce `X @ W + bias` for this weight storage. Bias is a
8433    /// length-`out_features` Arc<[f32]> materialized fresh on the
8434    /// receiver's graph and broadcast across the leading dims of
8435    /// the output.
8436    ///
8437    /// Equivalent to the per-port `apply_linear_with_bias` helpers
8438    /// that several ports inlined — promoted here so call sites
8439    /// stop drifting.
8440    pub fn apply_linear_with_bias(
8441        &self,
8442        x: &Tensor,
8443        in_features: usize,
8444        out_features: usize,
8445        bias: std::sync::Arc<[f32]>,
8446    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8447        if bias.len() != out_features {
8448            // Was a `debug_assert_eq!` — i.e. compiled OUT of release builds,
8449            // so a release binary silently built a broadcast against a
8450            // wrong-length bias. A length mismatch is a checkpoint condition;
8451            // it errors in every profile now.
8452            return Err(fuel_ir::Error::Msg(format!(
8453                "apply_linear_with_bias: bias len ({}) != out_features ({out_features})",
8454                bias.len(),
8455            ))
8456            .bt());
8457        }
8458        let projected = self.apply_linear(x, in_features, out_features)?;
8459        let bias_t = projected.const_f32_like(bias, Shape::from_dims(&[out_features]))?;
8460        projected.broadcast_add(&bias_t)
8461    }
8462
8463    /// Produce `X @ W` (with optional bias) for this weight storage.
8464    /// Dispatches to `matmul` for F32/BF16 weights and to `qmatmul`
8465    /// for Q4_0. `x`'s dtype must be compatible with this weight's
8466    /// dtype per `matmul`'s gate: same-dtype homogeneous (e.g. BF16
8467    /// activations × BF16 weights — LlamaModel's BF16-throughout
8468    /// decode path, Phase D increment A) or `(x=F32, weight=BF16)`
8469    /// (f32-activation quantized-weight serving). `(x=BF16,
8470    /// weight=F32)` is NOT supported — cast the weight or the
8471    /// activation to match before calling.
8472    /// # Errors
8473    ///
8474    /// Returns a typed error rather than panicking on a dimension or dtype
8475    /// mismatch. This used to `.unwrap()` the inner `matmul`/`qmatmul` and
8476    /// `assert_eq!` the stored-vs-requested dimensions, which made a
8477    /// **production** path panic — a mismatched checkpoint (a GGUF whose
8478    /// stored shape disagrees with the model config) is a data condition, not
8479    /// a programming invariant, so it must surface as `Err`.
8480    pub fn apply_linear(
8481        &self,
8482        x: &Tensor,
8483        in_features: usize,
8484        out_features: usize,
8485    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8486        // Build-time contract check, shared by every arm: a "linear with
8487        // `in_features`" requires x's trailing dim to BE `in_features`.
8488        // Checked here rather than at the ~620 call sites, and phrased so the
8489        // message names the projection instead of surfacing a bare matmul
8490        // shape error two layers down.
8491        let x_dims = x.shape().dims().to_vec();
8492        match x_dims.last() {
8493            Some(&k) if k == in_features => {}
8494            _ => {
8495                return Err(fuel_ir::Error::Msg(format!(
8496                    "apply_linear({in_features} -> {out_features}): input shape {x_dims:?} \
8497                     must have trailing dim {in_features}",
8498                ))
8499                .bt());
8500            }
8501        }
8502        match self {
8503            Self::F32(_) | Self::BF16(_) => {
8504                let w = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8505                x.matmul(&w)
8506            }
8507            Self::Q4_0 {
8508                in_features: expected_in,
8509                out_features: expected_out,
8510                ..
8511            } => {
8512                if *expected_in != in_features || *expected_out != out_features {
8513                    return Err(fuel_ir::Error::Msg(format!(
8514                        "WeightStorage::Q4_0 shape mismatch: stored \
8515                         [{expected_in}, {expected_out}], requested \
8516                         [{in_features}, {out_features}]",
8517                    ))
8518                    .bt());
8519                }
8520                let w_bytes = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8521                x.qmatmul(
8522                    &w_bytes,
8523                    fuel_graph::QuantType::Q4_0,
8524                    in_features,
8525                    out_features,
8526                )
8527            }
8528            Self::WithLoRA {
8529                base,
8530                lora_a,
8531                lora_b,
8532                rank,
8533                alpha,
8534                in_features: expected_in,
8535                out_features: expected_out,
8536            } => {
8537                if *expected_in != in_features || *expected_out != out_features {
8538                    return Err(fuel_ir::Error::Msg(format!(
8539                        "WeightStorage::WithLoRA shape mismatch: stored \
8540                         [{expected_in}, {expected_out}], requested \
8541                         [{in_features}, {out_features}]",
8542                    ))
8543                    .bt());
8544                }
8545                // Base forward (F32, BF16, or Q4_0).
8546                let base_out = base.apply_linear(x, in_features, out_features)?;
8547                // Low-rank update: y += (alpha/rank) · x @ A @ B.
8548                let a_t =
8549                    x.const_f32_like(Arc::clone(lora_a), Shape::from_dims(&[in_features, *rank]))?;
8550                let b_t =
8551                    x.const_f32_like(Arc::clone(lora_b), Shape::from_dims(&[*rank, out_features]))?;
8552                let scale = *alpha as f64 / *rank as f64;
8553                // x: [*, in] → @A [*, rank] → @B [*, out] → scale → add base.
8554                let lora_path = Tensor {
8555                    inner: x.matmul(&a_t)?.matmul(&b_t)?.inner.mul_scalar(scale),
8556                };
8557                base_out.add(&lora_path)
8558            }
8559        }
8560    }
8561
8562    /// Data-determined-M variant of [`Self::apply_linear`]: computes only
8563    /// `row_count` rows of the capacity-buffer projection `x @ W`, the rest
8564    /// left zeroed — the per-expert sparse-MoE FFN path. `x` is a
8565    /// `[capacity, in_features]` buffer whose first `row_count` rows are the
8566    /// routed tokens; the returned `[capacity, out_features]` buffer's tail
8567    /// (rows `row_count..capacity`) stays exactly zero (see
8568    /// [`Tensor::matmul_dyn_m`]).
8569    ///
8570    /// F32-only (mirrors `matmul_dyn_m`); other weight encodings surface a
8571    /// typed build-time error. No bias is applied — a bias would fill the
8572    /// un-computed tail, which the sparse-MoE caller relies on being zero
8573    /// for a correct `index_add` scatter-back.
8574    pub fn apply_linear_dyn_m(
8575        &self,
8576        x: &Tensor,
8577        in_features: usize,
8578        out_features: usize,
8579        row_count: fuel_ir::DynScalar,
8580    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8581        match self {
8582            Self::F32(_) => {
8583                // const_like only errors on WithLoRA; the F32 arm is infallible.
8584                let w = self
8585                    .const_like(x, Shape::from_dims(&[in_features, out_features]))
8586                    .expect("apply_linear_dyn_m F32 arm: const_like cannot fail for F32");
8587                x.matmul_dyn_m(&w, row_count)
8588            }
8589            other => Err(fuel_ir::Error::Msg(format!(
8590                "apply_linear_dyn_m: sparse-MoE dispatch is F32-only today, got {:?} \
8591                 weight (cast the expert weights to F32)",
8592                other.dtype(),
8593            ))
8594            .bt()),
8595        }
8596    }
8597
8598    /// Wrap this weight storage with a LoRA adapter. Asserts that the
8599    /// adapter shapes match `in_features`/`out_features`. Panics if the
8600    /// base is already a `WithLoRA` (nested adapters aren't supported;
8601    /// merge them explicitly if needed).
8602    pub fn with_lora(
8603        self,
8604        lora_a: Arc<[f32]>,
8605        lora_b: Arc<[f32]>,
8606        rank: usize,
8607        alpha: f32,
8608        in_features: usize,
8609        out_features: usize,
8610    ) -> Self {
8611        assert_eq!(
8612            lora_a.len(),
8613            in_features * rank,
8614            "lora_a length {} does not match in_features ({in_features}) × rank ({rank}) = {}",
8615            lora_a.len(),
8616            in_features * rank,
8617        );
8618        assert_eq!(
8619            lora_b.len(),
8620            rank * out_features,
8621            "lora_b length {} does not match rank ({rank}) × out_features ({out_features}) = {}",
8622            lora_b.len(),
8623            rank * out_features,
8624        );
8625        assert!(
8626            !matches!(self, Self::WithLoRA { .. }),
8627            "with_lora: base is already WithLoRA (nested adapters unsupported)",
8628        );
8629        Self::WithLoRA {
8630            base: Box::new(self),
8631            lora_a,
8632            lora_b,
8633            rank,
8634            alpha,
8635            in_features,
8636            out_features,
8637        }
8638    }
8639}
8640
8641// Auto-conversions so code that was storing `Arc<[f32]>` keeps
8642// compiling through the refactor — the LayerWeights field type
8643// widened to WeightStorage but ergonomics don't regress.
8644impl From<Arc<[f32]>> for WeightStorage {
8645    fn from(a: Arc<[f32]>) -> Self {
8646        Self::F32(a)
8647    }
8648}
8649impl From<Vec<f32>> for WeightStorage {
8650    fn from(v: Vec<f32>) -> Self {
8651        Self::F32(Arc::from(v))
8652    }
8653}
8654impl From<Arc<[half::bf16]>> for WeightStorage {
8655    fn from(a: Arc<[half::bf16]>) -> Self {
8656        Self::BF16(a)
8657    }
8658}
8659impl From<Vec<half::bf16>> for WeightStorage {
8660    fn from(v: Vec<half::bf16>) -> Self {
8661        Self::BF16(Arc::from(v))
8662    }
8663}
8664
8665/// Per-layer weights of a LLaMA transformer block. All tensors are
8666/// stored as `Arc<[f32]>` so they can be loaded once and shared across
8667/// every forward pass with zero copy — each call to
8668/// `LlamaModel::forward` clones the `Arc` (a refcount bump) when it
8669/// builds fresh const nodes for this layer.
8670///
8671/// LLaMA proper has no biases anywhere in the attention block, so the
8672/// `*_bias` fields are `None` for LLaMA family models. Qwen2 and a few
8673/// related architectures do add biases on Q/K/V (but not on the output
8674/// projection), so the loader stores them here when the safetensors
8675/// file contains them.
8676#[derive(Debug, Clone)]
8677pub struct LayerWeights {
8678    /// `[dim, dim]` query projection. Supports bf16 or f32.
8679    pub attn_q: WeightStorage,
8680    /// `[dim]` query projection bias (Qwen2-style; LLaMA has none).
8681    pub attn_q_bias: Option<Arc<[f32]>>,
8682    /// `[dim, dim]` key projection.
8683    pub attn_k: WeightStorage,
8684    /// `[kv_dim]` key projection bias.
8685    pub attn_k_bias: Option<Arc<[f32]>>,
8686    /// `[dim, dim]` value projection.
8687    pub attn_v: WeightStorage,
8688    /// `[kv_dim]` value projection bias.
8689    pub attn_v_bias: Option<Arc<[f32]>>,
8690    /// `[dim, dim]` output projection.
8691    pub attn_o: WeightStorage,
8692    /// `[dim, ffn_dim]` gate projection for SwiGLU.
8693    pub ffn_gate: WeightStorage,
8694    /// `[dim, ffn_dim]` up projection for SwiGLU.
8695    pub ffn_up: WeightStorage,
8696    /// `[ffn_dim, dim]` down projection for SwiGLU.
8697    pub ffn_down: WeightStorage,
8698    /// `[dim]` RmsNorm gain for the pre-attention norm. Stays f32
8699    /// — norm gains are small and precision-sensitive.
8700    pub attn_norm_gain: Arc<[f32]>,
8701    /// `[dim]` RmsNorm gain for the pre-FFN norm.
8702    pub ffn_norm_gain: Arc<[f32]>,
8703}
8704
8705pub fn invalidate_decode_pair_if_stale<C>(
8706    session: &mut Option<crate::inference_context::DecodeSession>,
8707    captured: &mut Option<C>,
8708    ctx: &mut InferenceContext,
8709    seq: usize,
8710    max_seq_len: Option<usize>,
8711    cache_dtype: DType,
8712    n_layers: usize,
8713    shape_key: u64,
8714    kv: &dyn crate::inference_context::KvRebindSource,
8715    drop_session: impl FnOnce(
8716        &mut Option<crate::inference_context::DecodeSession>,
8717        &mut InferenceContext,
8718    ),
8719) -> SessionDisposition {
8720    refresh_decode_session(
8721        session,
8722        ctx,
8723        seq,
8724        max_seq_len,
8725        cache_dtype,
8726        n_layers,
8727        shape_key,
8728        kv,
8729        // Reader before owner — see the doc comment above. This fires for a
8730        // re-bind as well as a drop: a capture records FIXED device addresses
8731        // drawn from `base_cache`, and `rebind_kv` replaces some of them.
8732        || *captured = None,
8733        drop_session,
8734    )
8735}
8736
8737/// What [`refresh_decode_session`] did to the held plan.
8738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8739pub enum SessionDisposition {
8740    /// There was no held plan; nothing to do.
8741    Absent,
8742    /// The plan is valid for this step and was left alone.
8743    Kept,
8744    /// The KV allocation changed and the plan was re-bound onto it (GAP-028).
8745    /// Any reader of the plan's device addresses has been retired.
8746    Rebound,
8747    /// The plan was retired. `Some(refusal)` means a re-bind was attempted and
8748    /// declined — carried so a caller (or a test) can distinguish "refused for
8749    /// the right reason" from "refused because nothing was tried".
8750    Dropped(Option<crate::inference_context::RebindRefusal>),
8751}
8752
8753/// **The single decision point for what happens to a held decode plan at the
8754/// top of a decode step.** Every model routes through here, so they cannot
8755/// drift on what "stale" means — the reason this was a shared free function
8756/// before GAP-028, and more so now that the answer has three outcomes.
8757///
8758/// Order of business, and why it is this order:
8759///
8760/// 1. Ask [`crate::inference_context::DecodeSession::validity_for`] — ONE comparison list, so "is it
8761///    valid" and "is only the allocation different" cannot disagree.
8762/// 2. On anything but `Valid`, retire readers FIRST via `on_invalidate`. A
8763///    `CapturedDecodeSession` replays a recorded CUDA graph over FIXED device
8764///    addresses drawn from the session's `base_cache`; both a drop and a
8765///    re-bind make some of those addresses stale, and a replay against stale
8766///    addresses is silently wrong logits at full speed. Retiring before
8767///    touching the owner means no replay can observe a half-updated plan.
8768///    Note this fires *before* the re-bind is attempted, not after it
8769///    succeeds: a capture is worthless either way, and clearing it early keeps
8770///    the ordering true without depending on the outcome.
8771/// 3. Try the guarded re-bind. It may refuse; a refusal is not an error.
8772/// 4. Otherwise drop, so the caller's first-decode-token arm rebuilds.
8773///
8774/// Generic in nothing — the capture rides in the `on_invalidate` closure,
8775/// which is what lets this compile (and therefore be CPU-tested) without the
8776/// `cuda` feature.
8777pub fn refresh_decode_session(
8778    session: &mut Option<crate::inference_context::DecodeSession>,
8779    ctx: &mut InferenceContext,
8780    seq: usize,
8781    max_seq_len: Option<usize>,
8782    cache_dtype: DType,
8783    n_layers: usize,
8784    shape_key: u64,
8785    kv: &dyn crate::inference_context::KvRebindSource,
8786    on_invalidate: impl FnOnce(),
8787    drop_session: impl FnOnce(
8788        &mut Option<crate::inference_context::DecodeSession>,
8789        &mut InferenceContext,
8790    ),
8791) -> SessionDisposition {
8792    use crate::inference_context::PlanValidity;
8793
8794    let verdict = match (session.as_ref(), max_seq_len) {
8795        (None, _) => return SessionDisposition::Absent,
8796        (Some(s), Some(msl)) => {
8797            s.validity_for(seq, msl, n_layers, cache_dtype, shape_key, kv.alloc_id())
8798        }
8799        // A cache with no pre-allocated capacity cannot back a held plan at
8800        // all (the graph bakes `max_seq_len` into its KV Const shapes).
8801        (Some(_), None) => PlanValidity::Stale,
8802    };
8803    if verdict == PlanValidity::Valid {
8804        return SessionDisposition::Kept;
8805    }
8806
8807    on_invalidate();
8808
8809    let refusal = if verdict == PlanValidity::AllocationChanged {
8810        match session.as_mut().map(|s| s.rebind_kv(kv)) {
8811            Some(Ok(())) => return SessionDisposition::Rebound,
8812            Some(Err(r)) => Some(r),
8813            // Unreachable: `verdict` was computed from a `Some` session and
8814            // nothing above takes it. Handled rather than `expect`ed because
8815            // this is a production path.
8816            None => None,
8817        }
8818    } else {
8819        None
8820    };
8821
8822    drop_session(session, ctx);
8823    SessionDisposition::Dropped(refusal)
8824}
8825
8826pub fn build_decode_causal_mask(cached_len: usize, seq: usize, max_seq_len: usize) -> Vec<f32> {
8827    let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8828    for q_idx in 0..seq {
8829        let abs_q = cached_len + q_idx;
8830        for k_idx in (abs_q + 1)..max_seq_len {
8831            mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8832        }
8833    }
8834    mask_data
8835}
8836
8837/// Sliding-window sibling of [`build_decode_causal_mask`] — the decode-time
8838/// mask for a layer that attends only to the most recent `window` positions.
8839///
8840/// # Why this exists (GAP-029 increment 3)
8841///
8842/// The decode path has always built **one** mask per model, because `LlamaModel`
8843/// and `PhiModel` — the only two carriers — have no per-layer mask variation
8844/// (measured: **zero** `sliding_window` hits in this file, against a positive
8845/// control of hits in five sibling model files). **Four of the six families
8846/// GAP-029 increment 3 ports do vary per layer**, three of them via a sliding
8847/// window: `Qwen2` (`lazy_qwen2.rs:249-251`), `Qwen3` (`lazy_qwen3.rs:172`) and
8848/// `Qwen3Moe` (`lazy_qwen3_moe.rs:192`) all select per layer on
8849/// `use_sliding_window && layer_idx < max_window_layers`.
8850///
8851/// # The predicate, and why it is stated against the prefill path
8852///
8853/// The shipped **prefill** mask for these families is `j > i || j + window <= i`
8854/// over absolute positions (`lazy_qwen2.rs:271`). Decode must agree with it
8855/// position-for-position, or a model silently changes behaviour at the
8856/// prefill→decode boundary. Here the query's absolute position is
8857/// `cached_len + q_idx` and the key's is `k_idx`, giving the same two clauses:
8858/// causal (`k_idx > abs_q`) and window (`k_idx + window <= abs_q`).
8859///
8860/// `window >= max_seq_len` makes the window clause unsatisfiable, so the result
8861/// is exactly [`build_decode_causal_mask`] — the property that lets a family
8862/// with the window disabled share the dense path rather than a near-copy of it.
8863///
8864/// # Status: LIVE. `#[allow(dead_code)]` removed 2026-08-13 — this has a caller.
8865///
8866/// It landed ahead of its consumer with its correctness established
8867/// independently (see the born-red record in `decode_mask_tests`), under an
8868/// architect-owned checkpoint: *land family 1 or the function is deleted*.
8869/// GAP-029 increment 3 landed `Qwen2Model`'s decode path, and
8870/// [`crate::persistent_decode::build_decode_mask_variants`] calls this for every
8871/// windowed mask variant — so the attribute is gone rather than renewed.
8872///
8873/// It is now on the live decode path of the three windowed families as they
8874/// arrive (`Qwen2Model` today; `Qwen3Model`, `Qwen3MoeModel` next), reached
8875/// through `MaskPlan::split_window`.
8876pub fn build_decode_causal_mask_windowed(
8877    cached_len: usize,
8878    seq: usize,
8879    max_seq_len: usize,
8880    window: usize,
8881) -> Vec<f32> {
8882    let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8883    for q_idx in 0..seq {
8884        let abs_q = cached_len + q_idx;
8885        for k_idx in 0..max_seq_len {
8886            // Causal: no future keys. Window: nothing older than `window` back.
8887            if k_idx > abs_q || k_idx + window <= abs_q {
8888                mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8889            }
8890        }
8891    }
8892    mask_data
8893}
8894
8895#[cfg(test)]
8896mod decode_mask_tests {
8897    use super::{build_decode_causal_mask, build_decode_causal_mask_windowed};
8898
8899    /// Render a mask row as `1` for attend / `0` for masked, so a failure
8900    /// prints the shape of the disagreement rather than a wall of `-inf`.
8901    fn attendable(row: &[f32]) -> Vec<u8> {
8902        row.iter().map(|&v| u8::from(v == 0.0)).collect()
8903    }
8904
8905    /// **The discriminating test.** Hand-computed ground truth, NOT a
8906    /// re-derivation of the implementation's own formula: at `cached_len = 3`,
8907    /// `seq = 1`, `max_seq_len = 6`, `window = 2` the single query sits at
8908    /// absolute position 3 and may attend to positions `{2, 3}` — 3 itself
8909    /// (causal boundary) and 2 (one step back, inside a window of 2). Positions
8910    /// 0 and 1 fall out of the window; 4 and 5 are the unwritten future tail.
8911    #[test]
8912    fn windowed_mask_excludes_positions_older_than_the_window() {
8913        let m = build_decode_causal_mask_windowed(3, 1, 6, 2);
8914        assert_eq!(
8915            attendable(&m),
8916            vec![0, 0, 1, 1, 0, 0],
8917            "window=2 at absolute position 3 must attend to exactly {{2, 3}}",
8918        );
8919    }
8920
8921    /// Multi-row (prefill-shaped) ground truth, also by hand: `cached_len = 1`,
8922    /// `seq = 2`, `window = 2` puts the queries at absolute positions 1 and 2,
8923    /// attending to `{0, 1}` and `{1, 2}` respectively. Guards the `q_idx`
8924    /// → `abs_q` offset, which a `seq == 1` test cannot see at all.
8925    #[test]
8926    fn windowed_mask_offsets_each_row_by_cached_len() {
8927        let m = build_decode_causal_mask_windowed(1, 2, 5, 2);
8928        assert_eq!(
8929            attendable(&m[0..5]),
8930            vec![1, 1, 0, 0, 0],
8931            "row 0 = abs pos 1"
8932        );
8933        assert_eq!(
8934            attendable(&m[5..10]),
8935            vec![0, 1, 1, 0, 0],
8936            "row 1 = abs pos 2"
8937        );
8938    }
8939
8940    /// **Non-discrimination control, and it is what keeps the two tests above
8941    /// meaningful.** A window at least as wide as the capacity cannot mask
8942    /// anything the causal clause does not already mask, so the windowed builder
8943    /// must reproduce the dense one byte-for-byte. This passes under BOTH a
8944    /// correct implementation and one that ignores `window` entirely — so it
8945    /// certifies that the suite is not simply broken, and it must NOT be read as
8946    /// evidence that the window works. (Same role as the positive control that
8947    /// stayed `ok` in increment 2a's sabotage record.)
8948    #[test]
8949    fn window_wider_than_capacity_is_byte_identical_to_the_dense_mask() {
8950        for (cached_len, seq, max_seq_len) in [(0, 1, 8), (3, 1, 8), (0, 4, 8), (2, 3, 8)] {
8951            let dense = build_decode_causal_mask(cached_len, seq, max_seq_len);
8952            let windowed =
8953                build_decode_causal_mask_windowed(cached_len, seq, max_seq_len, max_seq_len);
8954            assert_eq!(
8955                dense, windowed,
8956                "window == max_seq_len must equal dense at \
8957                 (cached_len={cached_len}, seq={seq}, max_seq_len={max_seq_len})",
8958            );
8959        }
8960    }
8961
8962    /// Decode must agree with the **shipped prefill** mask position-for-position,
8963    /// or a windowed model changes behaviour at the prefill→decode boundary.
8964    /// This replays `lazy_qwen2.rs:265-277`'s predicate over a full prefill of
8965    /// length `n`, then asserts that decoding position `p` with `cached_len = p`
8966    /// reproduces prefill row `p`. The prefill mask is `[seq, seq]` and the
8967    /// decode mask `[1, max_seq_len]`, so only the first `n` keys are comparable
8968    /// — the tail beyond `n` is the unwritten capacity the prefill mask has no
8969    /// opinion about.
8970    #[test]
8971    fn decode_row_matches_the_prefill_mask_row_at_the_same_position() {
8972        let (n, window, max_seq_len) = (6_usize, 3_usize, 10_usize);
8973
8974        // Qwen2's prefill predicate, transcribed from `build_layer_mask`.
8975        let mut prefill = vec![0.0_f32; n * n];
8976        for i in 0..n {
8977            for j in 0..n {
8978                if j > i || j + window <= i {
8979                    prefill[i * n + j] = f32::NEG_INFINITY;
8980                }
8981            }
8982        }
8983
8984        for p in 0..n {
8985            let decode = build_decode_causal_mask_windowed(p, 1, max_seq_len, window);
8986            assert_eq!(
8987                attendable(&decode[0..n]),
8988                attendable(&prefill[p * n..(p + 1) * n]),
8989                "decode at cached_len={p} must match prefill row {p}",
8990            );
8991            assert!(
8992                decode[n..].iter().all(|&v| v == f32::NEG_INFINITY),
8993                "the unwritten tail beyond position {n} must stay masked",
8994            );
8995        }
8996    }
8997
8998    // =======================================================================
8999    // BORN-RED RECORD (2026-08-13, GAP-029 increment 3)
9000    //
9001    // These tests were run FIRST against a deliberately wrong body — the
9002    // "old fabrication in the new shape": `build_decode_causal_mask_windowed`
9003    // returning `build_decode_causal_mask(..)` and ignoring `window`, which is
9004    // precisely what a single-mask decode port computes today. That makes the
9005    // red state prove *windowing vs dense* rather than merely proving the tests
9006    // execute.
9007    //
9008    //   windowed_mask_excludes_positions_older_than_the_window   FAILED
9009    //       left [1, 1, 1, 1, 0, 0]  right [0, 0, 1, 1, 0, 0]
9010    //   windowed_mask_offsets_each_row_by_cached_len             FAILED
9011    //       left [1, 1, 1, 0, 0]     right [0, 1, 1, 0, 0]
9012    //   decode_row_matches_the_prefill_mask_row_at_the_same_position FAILED
9013    //       left [1, 1, 1, 1, 0, 0]  right [0, 1, 1, 1, 0, 0]
9014    //   window_wider_than_capacity_is_byte_identical_to_the_dense_mask  ok
9015    //   test result: FAILED. 1 passed; 3 failed
9016    //
9017    // The `ok` line is load-bearing in BOTH directions and is the reason the
9018    // control exists: it holds under the correct body AND under the dense
9019    // fabrication, so (a) the suite is demonstrably not simply broken, and
9020    // (b) it must never be cited as evidence that windowing works. Only the
9021    // three that moved carry that claim.
9022    //
9023    // After the flip: `4 passed; 0 failed`, on a run reporting a 53.44s
9024    // compile — i.e. a rebuilt binary, not a cached one. A passing run without
9025    // confirmed recompilation would have certified nothing.
9026    //
9027    // WHAT THIS DOES **NOT** ESTABLISH, stated because the next reader will be
9028    // wiring this into a decode path: these are tests of the MASK BYTES ONLY.
9029    // No decode has yet been run with a windowed mask, so "a single-mask decode
9030    // port is silently wrong for Qwen2/Qwen3/Qwen3Moe" remains an INFERENCE
9031    // from the mask's meaning — argued from the fact that one
9032    // `[1, 1, seq, max_seq_len]` Const cannot express two layer groups. The
9033    // logits-level born-red against Qwen2's live mixed config
9034    // (`use_sliding_window: true, max_window_layers: 1` over 2 layers,
9035    // `lazy_qwen2.rs:441-443`) is the measurement that would settle it, and it
9036    // is NOT in this change.
9037    // =======================================================================
9038}
9039
9040/// Build a [`fuel_dispatch::decode_flash::DecodeFlashSpec`] from a decode
9041/// attention region's tensor handles and offer the optimizer-owned CUDA
9042/// flash-decode arm on the shared graph.
9043///
9044/// This is the model-layer WIRING for [`fuel_dispatch::decode_flash::offer_decode_flash_arm`]: it supplies
9045/// the region's tensor handles + the live attended-prefix `k_len` (as
9046/// `Sym(attended_len_sym)`, resolved per-token via the `SymEnv`) — data the
9047/// model alone knows — while every strategic decision (the shape/dtype/config
9048/// gate, the capability gate, the CUDA pin, the `Op::Branch` construction)
9049/// stays in the dispatch layer. The region is always **causal** with no
9050/// window / softcap / ALiBi (the LlamaModel decode shape).
9051///
9052/// - `q` — the RoPE'd query (`[B, Hq, 1, D]` in decode), also the branch
9053///   diverge point;
9054/// - `k` / `v` — the post-`WriteSlice` capacity KV buffers (`[B, Hkv,
9055///   capacity, D]`);
9056/// - `decomposed_out` — the region's attention output (arm 0 / the oracle);
9057/// - `reconverge` — the sole consumer of `decomposed_out` (the merge).
9058///
9059/// Returns `Ok(None)` (graph untouched, byte-identical to today) whenever the
9060/// emitter's gate declines — f32/f64 dtype, `seq_q != 1` (prefill),
9061/// `head_dim > 128`, or a non-CUDA / kernel-absent topology. Never panics.
9062/// Translate a Fuel sliding-window width into the FA-v2 `window_size_*`
9063/// convention the CUDA flash path speaks.
9064///
9065/// Fuel's mask predicate is `j > i || j + window <= i`, i.e. query `i` attends
9066/// keys `j ∈ [i - window + 1, i]` — **`window - 1` positions LEFT and `0`
9067/// RIGHT**. FA-v2 reads `is_causal` as `window_size_right == 0 &&
9068/// window_size_left < 0`, and treats *both* bounds `>= 0` as opting into local
9069/// attention (`fuel-cuda-backend/src/flash_attn.rs::translate_window`).
9070///
9071/// `None` (a dense layer) stays `(None, None)` — plain causal, which is what
9072/// every non-windowed family wants and what the decode kernel implements.
9073///
9074/// The `w - 1` is saturating: a nonsensical `window == 0` yields `Some(0)`,
9075/// which the admissibility gate rejects along with every other window. It never
9076/// silently becomes "no window".
9077pub fn flash_window_bounds(window: Option<usize>) -> (Option<usize>, Option<usize>) {
9078    match window {
9079        None => (None, None),
9080        Some(w) => (Some(w.saturating_sub(1)), Some(0)),
9081    }
9082}
9083
9084// 11 independent graph handles (q/k/v/decomposed/reconverge node ids) plus the
9085// scalar rewrite parameters (scale, attended-len sym, window, softcap, backend
9086// capability) for one flash-decode-arm offer. Each is a distinct graph node or
9087// scalar, not a bundle-able struct. Exceeds even the raised (10) threshold.
9088pub fn offer_flash_decode_arm_for_region(
9089    graph: &fuel_graph::SharedGraph,
9090    q: fuel_graph::NodeId,
9091    k: fuel_graph::NodeId,
9092    v: fuel_graph::NodeId,
9093    decomposed_out: fuel_graph::NodeId,
9094    reconverge: fuel_graph::NodeId,
9095    softmax_scale: f32,
9096    attended_len_sym: fuel_ir::SymId,
9097    attn_window: Option<usize>,
9098    softcap: Option<f32>,
9099    cap: fuel_dispatch::decode_flash::FlashArmCapability,
9100) -> crate::Result<Option<fuel_graph::NodeId>> {
9101    use fuel_dispatch::decode_flash::{DecodeFlashSpec, offer_decode_flash_arm};
9102    let (window_size_left, window_size_right) = flash_window_bounds(attn_window);
9103    let spec = DecodeFlashSpec {
9104        q,
9105        k,
9106        v,
9107        alibi: None,
9108        softmax_scale,
9109        causal: true,
9110        // ⚠️ GAP-194: these three were hardcoded `None`. That is TRUE for
9111        // `LlamaModel` — no window, no softcap — and would be a LIE for a
9112        // windowed or softcapped family, whose arm would then attend the whole
9113        // prefix and skip the cap, silently. A guard must not encode a claim
9114        // that is false: the caller states them, and the admissibility gate
9115        // declines what `flash_decoding` cannot implement.
9116        window_size_left,
9117        window_size_right,
9118        softcap,
9119        k_len: fuel_ir::DynScalar::Sym(attended_len_sym),
9120        decomposed_out,
9121        reconverge,
9122    };
9123    let mut g = graph.write().map_err(|_| {
9124        fuel_ir::Error::Msg("graph lock poisoned during flash-arm offer".into()).bt()
9125    })?;
9126    offer_decode_flash_arm(&mut g, &spec, cap)
9127}
9128
9129pub fn apply_affine_rms_norm(x: &Tensor, gain: &Arc<[f32]>, dim: usize, eps: f64) -> Tensor {
9130    assert_eq!(
9131        gain.len(),
9132        dim,
9133        "apply_affine_rms_norm: gain length must equal dim"
9134    );
9135    let normalized = x.rms_norm_last_dim(eps).unwrap();
9136    // The gain is always stored f32 (norm gains are precision-sensitive —
9137    // see `LayerWeights::attn_norm_gain`'s doc) but must be materialized
9138    // in `x`'s dtype: under BF16-throughout decode (Phase D increment A)
9139    // `x` is BF16 and `broadcast_mul` asserts dtype equality, so an f32
9140    // gain would panic. No-op conversion for f32 activations.
9141    let gain_t = x
9142        .const_like_dtype(gain, Shape::from_dims(&[dim]), x.dtype())
9143        .expect("apply_affine_rms_norm: activation dtype must be F32 or BF16");
9144    normalized.broadcast_mul(&gain_t).unwrap()
9145}
9146
9147// ---- HuggingFace Hub and safetensors weight loading ----------------------
9148
9149/// Load a tensor by name from a `MmapedSafetensors` as a flat
9150/// `Vec<f32>`, converting from whatever dtype the file stores it in.
9151/// Handles `F32`, `F64`, `BF16`, and `F16` — the dtypes real LLaMA
9152/// weights use on disk. Returns an error for unsupported dtypes.
9153pub fn load_tensor_as_f32(
9154    st: &fuel_loaders::safetensors::MmapedSafetensors,
9155    name: &str,
9156) -> crate::Result<Vec<f32>> {
9157    use safetensors::Dtype;
9158    let view = st.get(name)?;
9159    let bytes = view.data();
9160    match view.dtype() {
9161        Dtype::F32 => {
9162            let mut out = Vec::with_capacity(bytes.len() / 4);
9163            for chunk in bytes.as_chunks::<4>().0.iter() {
9164                out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
9165            }
9166            Ok(out)
9167        }
9168        Dtype::F64 => {
9169            let mut out = Vec::with_capacity(bytes.len() / 8);
9170            for chunk in bytes.as_chunks::<8>().0.iter() {
9171                let arr: [u8; 8] = *chunk;
9172                out.push(f64::from_le_bytes(arr) as f32);
9173            }
9174            Ok(out)
9175        }
9176        Dtype::BF16 => {
9177            let mut out = Vec::with_capacity(bytes.len() / 2);
9178            for chunk in bytes.as_chunks::<2>().0.iter() {
9179                let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9180                out.push(half::bf16::from_bits(raw).to_f32());
9181            }
9182            Ok(out)
9183        }
9184        Dtype::F16 => {
9185            let mut out = Vec::with_capacity(bytes.len() / 2);
9186            for chunk in bytes.as_chunks::<2>().0.iter() {
9187                let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9188                out.push(half::f16::from_bits(raw).to_f32());
9189            }
9190            Ok(out)
9191        }
9192        other => {
9193            crate::bail!("load_tensor_as_f32: unsupported dtype {other:?} for tensor {name:?}",)
9194        }
9195    }
9196}
9197
9198/// Load a tensor by name and physically transpose it from `[out, in]`
9199/// (HuggingFace layout) to `[in, out]` (fuel-graph's layout for
9200/// `x @ W` where `W` is `[in, out]`). Linear-layer weights in HF
9201/// transformers are stored as `[out_features, in_features]`, so every
9202/// call to this function is effectively "give me that matrix as I'd
9203/// use it in `matmul`."
9204pub fn load_transposed_matrix(
9205    st: &fuel_loaders::safetensors::MmapedSafetensors,
9206    name: &str,
9207    out_features: usize,
9208    in_features: usize,
9209) -> crate::Result<Vec<f32>> {
9210    let flat = load_tensor_as_f32(st, name)?;
9211    if flat.len() != out_features * in_features {
9212        crate::bail!(
9213            "load_transposed_matrix: tensor {name:?} has {} elements, expected {} ({out_features} × {in_features})",
9214            flat.len(),
9215            out_features * in_features,
9216        );
9217    }
9218    // HF layout: flat[i * in_features + j] is W[i, j] for (out i, in j).
9219    // Target layout: out[j * out_features + i] so that indexing `[j, i]`
9220    // in row-major gives the same W[i, j] — i.e. out has shape [in, out].
9221    let mut out = vec![0.0_f32; out_features * in_features];
9222    for i in 0..out_features {
9223        for j in 0..in_features {
9224            out[j * out_features + i] = flat[i * in_features + j];
9225        }
9226    }
9227    Ok(out)
9228}
9229
9230/// Transposed-matrix loader that preserves source dtype. For
9231/// safetensors files saved with bf16 weights, returns
9232/// `WeightStorage::BF16` and never materializes an f32 copy — the
9233/// 2× memory saving vs `load_transposed_matrix` comes from here.
9234/// f32 and other source dtypes still go through the f32 upcast path
9235/// for safety; extending this to preserve f16 is a one-line change
9236/// when a consumer wants it.
9237///
9238/// The transpose itself is done in whatever dtype we're keeping:
9239/// read bf16 elements from the file, place them in the transposed
9240/// target buffer, no conversion.
9241pub fn load_transposed_matrix_preserve_dtype(
9242    st: &fuel_loaders::safetensors::MmapedSafetensors,
9243    name: &str,
9244    out_features: usize,
9245    in_features: usize,
9246) -> crate::Result<WeightStorage> {
9247    use safetensors::Dtype;
9248    let view = st.get(name)?;
9249    let bytes = view.data();
9250    let expected = out_features * in_features;
9251    match view.dtype() {
9252        Dtype::BF16 => {
9253            if bytes.len() != expected * 2 {
9254                crate::bail!(
9255                    "load_transposed_matrix_preserve_dtype: bf16 tensor {name:?} has {} bytes, expected {}",
9256                    bytes.len(),
9257                    expected * 2,
9258                );
9259            }
9260            // Reinterpret input as [out_features, in_features] of
9261            // bf16; write transposed layout.
9262            let mut out = vec![half::bf16::ZERO; expected];
9263            for i in 0..out_features {
9264                for j in 0..in_features {
9265                    let src_off = (i * in_features + j) * 2;
9266                    let bits = u16::from_le_bytes([bytes[src_off], bytes[src_off + 1]]);
9267                    out[j * out_features + i] = half::bf16::from_bits(bits);
9268                }
9269            }
9270            Ok(WeightStorage::BF16(Arc::from(out)))
9271        }
9272        _ => {
9273            // F32, F64, F16 all fall through to the f32 upcast path.
9274            // Non-f32 source types still benefit from being readable;
9275            // they just lose the "weights stay compact" win.
9276            let flat = load_transposed_matrix(st, name, out_features, in_features)?;
9277            Ok(WeightStorage::F32(Arc::from(flat)))
9278        }
9279    }
9280}
9281
9282/// Sampling strategy for decode loops.
9283#[derive(Debug, Clone, Copy, Default)]
9284pub enum SamplingStrategy {
9285    /// Greedy: always pick the highest-probability token.
9286    #[default]
9287    Greedy,
9288    /// Temperature-scaled sampling with a deterministic seed. `temp`
9289    /// is the softmax temperature (`1.0` is unscaled, `0.0` is
9290    /// effectively greedy, higher values spread probability mass).
9291    /// The seed makes sampling reproducible.
9292    Temperature { temp: f32, seed: u64 },
9293}
9294
9295/// Pick the next token from a logits vector using the configured
9296/// sampling strategy. Pulled out of `generate` so both the cached and
9297/// future non-cached callers can share it.
9298pub fn sample_logits(logits: &[f32], strategy: SamplingStrategy, rng_state: &mut u64) -> u32 {
9299    match strategy {
9300        SamplingStrategy::Greedy => {
9301            let (i, _) = logits
9302                .iter()
9303                .enumerate()
9304                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
9305                .expect("sample_logits: empty logits");
9306            i as u32
9307        }
9308        SamplingStrategy::Temperature { temp, .. } => {
9309            // Stable softmax over optionally temperature-scaled logits,
9310            // then a deterministic multinomial draw.
9311            let inv_temp = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9312            let scaled: Vec<f32> = logits.iter().map(|&x| x * inv_temp).collect();
9313            let max = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9314            let exp: Vec<f32> = scaled.iter().map(|&x| (x - max).exp()).collect();
9315            let sum: f32 = exp.iter().sum();
9316            let probs: Vec<f32> = exp.iter().map(|&x| x / sum).collect();
9317            sample_multinomial(&probs, rng_state)
9318        }
9319    }
9320}
9321
9322// ---- Speculative-decoding helpers ---------------------------------------
9323//
9324// Shared by `generate_streaming_spec_with_kv_context`'s draft / accept
9325// phases. All host-side: spec decode's accept rule operates on logits
9326// vectors already downloaded from the device.
9327
9328/// Greedy argmax over a logits row.
9329pub fn spec_argmax(logits: &[f32]) -> u32 {
9330    let mut best = 0;
9331    let mut best_v = logits[0];
9332    for (i, &v) in logits.iter().enumerate().skip(1) {
9333        if v > best_v {
9334            best_v = v;
9335            best = i;
9336        }
9337    }
9338    best as u32
9339}
9340
9341/// Temperature-scaled softmax. Returns normalized probabilities.
9342pub fn spec_softmax_temp(logits: &[f32], temp: f32) -> Vec<f32> {
9343    let inv_t = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9344    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9345    let exp: Vec<f32> = logits.iter().map(|&x| ((x - max) * inv_t).exp()).collect();
9346    let sum: f32 = exp.iter().sum();
9347    exp.iter().map(|&x| x / sum).collect()
9348}
9349
9350/// Advance a deterministic LCG and return a u01 uniform.
9351pub fn spec_next_u01(state: &mut u64) -> f32 {
9352    *state = state
9353        .wrapping_mul(6364136223846793005)
9354        .wrapping_add(1442695040888963407);
9355    (*state >> 32) as f32 / u32::MAX as f32
9356}
9357
9358/// Sample a category from a distribution summing to ~1.
9359pub fn spec_sample_cat(probs: &[f32], state: &mut u64) -> u32 {
9360    let u = spec_next_u01(state);
9361    let mut cum = 0.0_f32;
9362    for (i, &p) in probs.iter().enumerate() {
9363        cum += p;
9364        if u <= cum {
9365            return i as u32;
9366        }
9367    }
9368    (probs.len() - 1) as u32
9369}
9370
9371/// Sample a categorical distribution using a small deterministic LCG.
9372/// Takes `probs` (assumed to sum to ~1) and a mutable RNG state,
9373/// returns a sampled index.
9374pub fn sample_multinomial(probs: &[f32], rng_state: &mut u64) -> u32 {
9375    // Advance the LCG and turn it into a u01 uniform.
9376    *rng_state = rng_state
9377        .wrapping_mul(6364136223846793005)
9378        .wrapping_add(1442695040888963407);
9379    let u = (*rng_state >> 32) as f32 / u32::MAX as f32;
9380    let mut cumulative = 0.0_f32;
9381    for (i, &p) in probs.iter().enumerate() {
9382        cumulative += p;
9383        if u <= cumulative {
9384            return i as u32;
9385        }
9386    }
9387    // Floating-point slop: fall through to the last index.
9388    (probs.len() - 1) as u32
9389}
9390
9391#[cfg(test)]
9392mod lora_tests {
9393    use super::*;
9394    use crate::Shape;
9395
9396    #[test]
9397    fn with_lora_matches_manual_base_plus_lora() {
9398        // Anchor graph.
9399        let in_f = 4;
9400        let out_f = 3;
9401        let rank = 2;
9402        let alpha = 8.0_f32;
9403
9404        let anchor =
9405            Tensor::from_f32(vec![0.0_f32; 1], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
9406        // Base weight [in, out].
9407        let base_vec: Vec<f32> = (0..in_f * out_f).map(|i| (i as f32) * 0.1).collect();
9408        let lora_a_vec: Vec<f32> = (0..in_f * rank).map(|i| (i as f32) * 0.05).collect();
9409        let lora_b_vec: Vec<f32> = (0..rank * out_f).map(|i| (i as f32) * 0.02).collect();
9410
9411        let ws = WeightStorage::F32(Arc::from(base_vec.clone())).with_lora(
9412            Arc::from(lora_a_vec.clone()),
9413            Arc::from(lora_b_vec.clone()),
9414            rank,
9415            alpha,
9416            in_f,
9417            out_f,
9418        );
9419
9420        // Activations x [2, in_f].
9421        let batch = 2;
9422        let x_data: Vec<f32> = (0..batch * in_f).map(|i| (i as f32) * 0.1 + 0.5).collect();
9423        let x = anchor
9424            .const_f32_like(x_data.clone(), Shape::from_dims(&[batch, in_f]))
9425            .unwrap();
9426        let y = ws.apply_linear(&x, in_f, out_f).unwrap();
9427        let got = y.realize_f32().to_vec();
9428
9429        // Reference: base + (alpha/rank) * x @ A @ B, all f32, on CPU.
9430        let mut expected = vec![0.0_f32; batch * out_f];
9431        for b in 0..batch {
9432            for j in 0..out_f {
9433                let mut acc = 0.0_f32;
9434                // Base path: sum_k x[b,k] * W[k,j].
9435                for k in 0..in_f {
9436                    acc += x_data[b * in_f + k] * base_vec[k * out_f + j];
9437                }
9438                // LoRA path: sum_r (sum_k x[b,k] * A[k,r]) * B[r,j] * (alpha/rank).
9439                let scale = alpha as f64 / rank as f64;
9440                for r in 0..rank {
9441                    let mut xar = 0.0_f32;
9442                    for k in 0..in_f {
9443                        xar += x_data[b * in_f + k] * lora_a_vec[k * rank + r];
9444                    }
9445                    acc += (xar * lora_b_vec[r * out_f + j]) * scale as f32;
9446                }
9447                expected[b * out_f + j] = acc;
9448            }
9449        }
9450
9451        for (i, (&e, &g)) in expected.iter().zip(got.iter()).enumerate() {
9452            let diff = (e - g).abs();
9453            assert!(
9454                diff <= 1e-4,
9455                "LoRA mismatch at {i}: expected {e}, got {g} (diff {diff})",
9456            );
9457        }
9458    }
9459
9460    #[test]
9461    #[should_panic(expected = "lora_a length")]
9462    fn with_lora_rejects_mismatched_a_shape() {
9463        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9464        let bad_a = Arc::from(vec![0.0_f32; 3]); // wrong
9465        let b = Arc::from(vec![0.0_f32; 6]); // 2 x 3
9466        let _ = ws.with_lora(bad_a, b, 2, 8.0, 4, 3);
9467    }
9468
9469    // ---- never-panic conversion (`apply_linear` -> `Result`) ----
9470    //
9471    // These three inputs each used to abort the process on a PRODUCTION path:
9472    // a wrong trailing dim reached `x.matmul(&w).unwrap()`, a stored/requested
9473    // shape disagreement hit `assert_eq!`, and a wrong-length bias hit a
9474    // `debug_assert_eq!` that is compiled OUT of release builds — so release
9475    // binaries broadcast a mis-sized bias with no complaint at all. All three
9476    // are checkpoint/data conditions rather than programming invariants (a
9477    // GGUF whose stored shape disagrees with the model config produces exactly
9478    // this), so all three must be `Err`.
9479
9480    #[test]
9481    fn apply_linear_errors_on_trailing_dim_mismatch() {
9482        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9483        let x = Tensor::from_f32(
9484            vec![0.0_f32; 10],
9485            Shape::from_dims(&[2, 5]), // trailing dim 5 != in_features 4
9486            &Device::cpu(),
9487        )
9488        .unwrap();
9489        let err = ws
9490            .apply_linear(&x, 4, 3)
9491            .expect_err("must not panic, must Err");
9492        let msg = err.to_string();
9493        assert!(
9494            msg.contains("trailing dim 4"),
9495            "error should name the expected trailing dim, got: {msg}",
9496        );
9497    }
9498
9499    #[test]
9500    fn apply_linear_errors_on_q4_0_stored_shape_mismatch() {
9501        // 4 x 4 of Q4_0: one 32-element block needs 18 bytes; keep the word
9502        // count self-consistent since only the shape check is under test.
9503        let ws = WeightStorage::Q4_0 {
9504            words: Arc::from(vec![0_u32; 8]),
9505            bytes_len: 18, // one Q4_0 block: 2-byte scale + 16 packed nibble bytes
9506            in_features: 4,
9507            out_features: 4,
9508        };
9509        let x =
9510            Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9511        // Trailing dim matches in_features, so this reaches the stored-shape
9512        // check rather than the contract check above.
9513        let err = ws
9514            .apply_linear(&x, 4, 7)
9515            .expect_err("must not panic, must Err");
9516        let msg = err.to_string();
9517        assert!(
9518            msg.contains("Q4_0 shape mismatch"),
9519            "error should name the stored/requested disagreement, got: {msg}",
9520        );
9521    }
9522
9523    #[test]
9524    fn apply_linear_with_bias_errors_on_wrong_bias_length() {
9525        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9526        let x =
9527            Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9528        let bias: Arc<[f32]> = Arc::from(vec![0.0_f32; 2]); // wrong: out_features is 3
9529        let err = ws
9530            .apply_linear_with_bias(&x, 4, 3, bias)
9531            .expect_err("wrong-length bias must Err in EVERY profile, not just debug");
9532        assert!(
9533            err.to_string().contains("bias len"),
9534            "error should name the bias length, got: {err}",
9535        );
9536    }
9537}
9538
9539#[cfg(test)]
9540mod safetensors_bridge_tests {
9541    use super::*;
9542
9543    #[test]
9544    fn from_safetensors_bytes_round_trip_f32() {
9545        // Build a tensor, serialize it as little-endian f32 bytes,
9546        // then deserialize via the bridge. Should get back the same
9547        // values.
9548        let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
9549        let mut bytes = Vec::with_capacity(original.len() * 4);
9550        for &v in &original {
9551            bytes.extend_from_slice(&v.to_le_bytes());
9552        }
9553        let t =
9554            Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::F32, &[4], &Device::cpu())
9555                .unwrap();
9556        assert_eq!(t.shape().dims(), &[4]);
9557        assert_eq!(t.dtype(), DType::F32);
9558        assert_eq!(t.realize_f32(), original);
9559    }
9560
9561    #[test]
9562    fn from_safetensors_bytes_round_trip_bf16() {
9563        let original_f32: Vec<f32> = vec![0.5, -1.0, 2.0, 4.0];
9564        let bf16_vec: Vec<half::bf16> = original_f32
9565            .iter()
9566            .map(|&v| half::bf16::from_f32(v))
9567            .collect();
9568        let mut bytes = Vec::with_capacity(bf16_vec.len() * 2);
9569        for b in &bf16_vec {
9570            bytes.extend_from_slice(&b.to_bits().to_le_bytes());
9571        }
9572        let t =
9573            Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::BF16, &[4], &Device::cpu())
9574                .unwrap();
9575        assert_eq!(t.dtype(), DType::BF16);
9576        // Values that round-trip exactly through bf16 should come back
9577        // unchanged.
9578        let realized = t.realize_bf16();
9579        assert_eq!(realized, bf16_vec);
9580    }
9581
9582    #[test]
9583    fn from_safetensors_bytes_rejects_wrong_byte_count() {
9584        // Shape says 3 elements, but we pass 4 bytes (1 f32 = 4 bytes
9585        // so 3 elements would need 12 bytes).
9586        let bad_bytes = vec![0_u8; 4];
9587        let result = Tensor::from_safetensors_bytes(
9588            &bad_bytes,
9589            safetensors::Dtype::F32,
9590            &[3],
9591            &Device::cpu(),
9592        );
9593        assert!(result.is_err());
9594    }
9595}
9596
9597// ============================================================================
9598// Phase A.1 wrapper smoke tests.
9599//
9600// Pure pass-through tests: realize and assert the returned tensor has the
9601// expected shape / dtype / values. The graph-level ops are tested in
9602// `fuel-graph`; here we only verify that the Tensor wrappers don't
9603// drop information or mis-thread arguments.
9604// ============================================================================
9605#[cfg(test)]
9606mod phase_a1_wrapper_tests {
9607    use super::*;
9608
9609    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
9610        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
9611    }
9612
9613    #[test]
9614    fn unsqueeze_adds_size_one_dim() {
9615        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9616        let out = t.unsqueeze(0_usize).unwrap();
9617        assert_eq!(out.shape().dims(), &[1, 2, 2]);
9618        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
9619    }
9620
9621    #[test]
9622    fn unsqueeze_errors_out_of_bounds() {
9623        let t = cpu_f32(vec![1.0, 2.0], &[2]);
9624        // rank=1, so dim<=1 is valid; dim=2 must error.
9625        assert!(t.unsqueeze(0_usize).is_ok());
9626        assert!(t.unsqueeze(1_usize).is_ok());
9627        assert!(t.unsqueeze(2_usize).is_err());
9628    }
9629
9630    #[test]
9631    fn try_reshape_errors_on_size_mismatch() {
9632        let t = cpu_f32(vec![1.0; 6], &[2, 3]);
9633        assert!(t.reshape(vec![3, 2]).is_ok());
9634        assert!(t.reshape(vec![2, 2]).is_err());
9635    }
9636
9637    #[test]
9638    fn try_permute_validates_axes() {
9639        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
9640        // The Dims trait accepts tuples, owned arrays, and slices.
9641        assert!(t.permute((2_usize, 0_usize, 1_usize)).is_ok());
9642        assert!(t.permute([2_usize, 0, 1]).is_ok());
9643        assert!(t.permute([0_usize, 1]).is_err()); // wrong rank
9644        assert!(t.permute([0_usize, 0, 1]).is_err()); // dup axis
9645    }
9646
9647    #[test]
9648    fn try_transpose_requires_rank_two_plus() {
9649        let scalar = cpu_f32(vec![1.0], &[1]);
9650        // rank-1: transpose surfaces a typed error at build time.
9651        let _ = scalar.transpose();
9652    }
9653
9654    #[test]
9655    fn triu_tril_shape_preserved() {
9656        let t = cpu_f32(vec![1.0; 9], &[3, 3]);
9657        let upper = t.triu(0).unwrap();
9658        let lower = t.tril(0).unwrap();
9659        assert_eq!(upper.shape().dims(), &[3, 3]);
9660        assert_eq!(lower.shape().dims(), &[3, 3]);
9661        // tril(0) of all-ones: 1s on/below diagonal, 0s above
9662        assert_eq!(
9663            lower.realize_f32(),
9664            vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0]
9665        );
9666    }
9667
9668    #[test]
9669    fn triu_rejects_rank_one() {
9670        let t = cpu_f32(vec![1.0, 2.0], &[2]);
9671        assert!(t.triu(0).is_err());
9672    }
9673
9674    #[test]
9675    fn log_softmax_last_dim_shape_preserved() {
9676        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9677        let out = t.log_softmax_last_dim().unwrap();
9678        assert_eq!(out.shape().dims(), &[2, 2]);
9679        // log_softmax values must be <= 0.
9680        for v in out.realize_f32() {
9681            assert!(v <= 0.0 + 1e-6, "log_softmax produced positive value: {v}");
9682        }
9683    }
9684
9685    #[test]
9686    fn softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9687        // shape [2, 3, 2] — softmax along axis=1 (the size-3 axis).
9688        // Layout (row-major): element (b, r, c) lives at b*6 + r*2 + c.
9689        // We hand-pick values so each (b, c) lane's max is exactly 0 to
9690        // make the reference closed-form: probs = exp(x) / sum_r exp(x).
9691        // Lane (b=0, c=0): values [-1, 0, -2]  → exp = [e^-1, 1, e^-2]
9692        // Lane (b=0, c=1): values [ 0,-3, -1]  → exp = [1, e^-3, e^-1]
9693        // Lane (b=1, c=0): values [-2,-1,  0]  → exp = [e^-2, e^-1, 1]
9694        // Lane (b=1, c=1): values [-1,-1,  0]  → exp = [e^-1, e^-1, 1]
9695        let data: Vec<f32> = vec![
9696            // b=0
9697            -1.0, 0.0, // r=0, c=0..1
9698            0.0, -3.0, // r=1
9699            -2.0, -1.0, // r=2
9700            // b=1
9701            -2.0, -1.0, // r=0
9702            -1.0, -1.0, // r=1
9703            0.0, 0.0, // r=2
9704        ];
9705        let t = cpu_f32(data, &[2, 3, 2]);
9706        let out = t.softmax(1_usize).unwrap();
9707        assert_eq!(out.shape().dims(), &[2, 3, 2]);
9708        let v = out.realize_f32();
9709
9710        // Reference: closed-form softmax per (b, c) lane.
9711        let lane_softmax = |xs: [f32; 3]| -> [f32; 3] {
9712            let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9713            let exps = [(xs[0] - m).exp(), (xs[1] - m).exp(), (xs[2] - m).exp()];
9714            let s = exps[0] + exps[1] + exps[2];
9715            [exps[0] / s, exps[1] / s, exps[2] / s]
9716        };
9717        // Re-extract source lanes
9718        let lanes: [[f32; 3]; 4] = [
9719            [-1.0, 0.0, -2.0], // (b=0, c=0)
9720            [0.0, -3.0, -1.0], // (b=0, c=1)
9721            [-2.0, -1.0, 0.0], // (b=1, c=0)
9722            [-1.0, -1.0, 0.0], // (b=1, c=1)
9723        ];
9724        let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_softmax(*l)).collect();
9725
9726        // out[b, r, c] at index b*6 + r*2 + c — verify each (b, c) lane.
9727        for b in 0..2 {
9728            for c in 0..2 {
9729                let lane_ix = b * 2 + c;
9730                for r in 0..3 {
9731                    let got = v[b * 6 + r * 2 + c];
9732                    let want = refs[lane_ix][r];
9733                    assert!(
9734                        (got - want).abs() < 1e-6,
9735                        "softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9736                    );
9737                }
9738                // sanity: lane sums to 1
9739                let sum: f32 = (0..3).map(|r| v[b * 6 + r * 2 + c]).sum();
9740                assert!((sum - 1.0).abs() < 1e-6, "lane (b={b},c={c}) sums to {sum}");
9741            }
9742        }
9743    }
9744
9745    #[test]
9746    fn softmax_last_axis_matches_softmax_last_dim() {
9747        let data: Vec<f32> = vec![
9748            1.0, 2.0, -1.0, 0.5, 0.0, -2.0, 3.0, 1.5, // batch dim 2
9749            4.0, -1.0, 2.0, 0.0, -3.0, 0.25, 0.75, 1.0,
9750        ];
9751        let t = cpu_f32(data, &[2, 2, 4]);
9752        let via_general = t.softmax(2_usize).unwrap();
9753        let via_fused = t.softmax_last_dim().unwrap();
9754        assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9755        let g = via_general.realize_f32();
9756        let f = via_fused.realize_f32();
9757        assert_eq!(g.len(), f.len());
9758        for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9759            assert!(
9760                (a - b).abs() < 1e-6,
9761                "softmax (general axis=2) vs softmax_last_dim diverge at {i}: {a} vs {b}",
9762            );
9763        }
9764    }
9765
9766    #[test]
9767    fn log_softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9768        // Same construction as the softmax test, but compare against
9769        // closed-form log_softmax per (b, c) lane.
9770        let data: Vec<f32> = vec![
9771            -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0,
9772        ];
9773        let t = cpu_f32(data, &[2, 3, 2]);
9774        let out = t.log_softmax(1_usize).unwrap();
9775        assert_eq!(out.shape().dims(), &[2, 3, 2]);
9776        let v = out.realize_f32();
9777
9778        let lane_log_softmax = |xs: [f32; 3]| -> [f32; 3] {
9779            let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9780            let shifted = [xs[0] - m, xs[1] - m, xs[2] - m];
9781            let lse = (shifted[0].exp() + shifted[1].exp() + shifted[2].exp()).ln();
9782            [shifted[0] - lse, shifted[1] - lse, shifted[2] - lse]
9783        };
9784        let lanes: [[f32; 3]; 4] = [
9785            [-1.0, 0.0, -2.0],
9786            [0.0, -3.0, -1.0],
9787            [-2.0, -1.0, 0.0],
9788            [-1.0, -1.0, 0.0],
9789        ];
9790        let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_log_softmax(*l)).collect();
9791
9792        for b in 0..2 {
9793            for c in 0..2 {
9794                let lane_ix = b * 2 + c;
9795                for r in 0..3 {
9796                    let got = v[b * 6 + r * 2 + c];
9797                    let want = refs[lane_ix][r];
9798                    assert!(
9799                        (got - want).abs() < 1e-6,
9800                        "log_softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9801                    );
9802                    // log_softmax values must be <= 0
9803                    assert!(got <= 1e-6, "log_softmax produced positive value: {got}");
9804                }
9805            }
9806        }
9807    }
9808
9809    #[test]
9810    fn log_softmax_last_axis_matches_log_softmax_last_dim() {
9811        let data: Vec<f32> = vec![
9812            1.0, 2.0, -1.0, 0.5, 0.0, -2.0, 3.0, 1.5, 4.0, -1.0, 2.0, 0.0, -3.0, 0.25, 0.75, 1.0,
9813        ];
9814        let t = cpu_f32(data, &[2, 2, 4]);
9815        let via_general = t.log_softmax(2_usize).unwrap();
9816        let via_fused = t.log_softmax_last_dim().unwrap();
9817        assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9818        let g = via_general.realize_f32();
9819        let f = via_fused.realize_f32();
9820        assert_eq!(g.len(), f.len());
9821        for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9822            assert!(
9823                (a - b).abs() < 1e-6,
9824                "log_softmax (general axis=2) vs log_softmax_last_dim diverge at {i}: {a} vs {b}",
9825            );
9826        }
9827    }
9828
9829    #[test]
9830    fn argmin_dim_drops_reduced_axis() {
9831        let t = cpu_f32(vec![3.0, 1.0, 2.0, 0.5, 5.0, 4.0], &[2, 3]);
9832        let out = t.argmin_dim(1_usize).unwrap();
9833        assert_eq!(out.shape().dims(), &[2]);
9834        assert_eq!(out.dtype(), DType::U32);
9835        assert_eq!(out.realize_u32(), vec![1, 0]);
9836    }
9837
9838    #[test]
9839    fn masked_fill_smoke() {
9840        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9841        // Comparison ops produce Bool masks directly (GAP-168(c)) — masked_fill
9842        // now accepts Bool, no F32→mask cast needed.
9843        let probe = t
9844            .const_f32_like(vec![0.0, 1.0, 1.0, 0.0], vec![2, 2])
9845            .unwrap();
9846        let threshold = t.const_f32_like(vec![0.5; 4], vec![2, 2]).unwrap();
9847        let mask = probe.gt(&threshold).unwrap(); // [0, 1, 1, 0] as Bool
9848        let out = t.masked_fill(&mask, fuel_ir::Scalar::F32(-9.0)).unwrap();
9849        assert_eq!(out.realize_f32(), vec![1.0, -9.0, -9.0, 4.0]);
9850    }
9851
9852    /// GAP-168(c) DISCRIMINATION: `Bool` and `U8` are byte-identical in storage,
9853    /// which is exactly the condition under which a wrong wiring is *silently
9854    /// correct*. This test proves they are NOT interchangeable end-to-end on CPU:
9855    /// a comparison yields a `Bool` (not `U8`) tensor, and `to_dtype(Bool)` on a
9856    /// `U8` tensor is a real `!= 0` CONVERSION — never a byte reinterpret.
9857    #[test]
9858    fn bool_is_distinguishable_from_u8_end_to_end() {
9859        // A comparison realizes end-to-end as a Bool tensor (NOT U8) whose bytes
9860        // are 0/1 — the byte-identity hazard's most likely hiding spot, closed:
9861        // dtype tag is Bool, the CPU kernel dispatches on [F32,F32,Bool] (its FKC
9862        // contract declares fixed(BOOL)), and Copy[Bool] materializes it to host.
9863        let t = cpu_f32(vec![0.0, 5.0, 3.0, 0.0], &[4]);
9864        let thr = t.const_f32_like(vec![0.5; 4], vec![4]).unwrap();
9865        let mask = t.gt(&thr).unwrap();
9866        assert_eq!(mask.dtype(), DType::Bool, "gt yields Bool, not U8");
9867        assert_eq!(mask.realize_u8(), vec![0, 1, 1, 0], "mask bytes are 0/1");
9868
9869        // ---- The cast discrimination, now that the 22 Bool pairs are wired ----
9870        //
9871        // NEGATIVE CONTROL FIRST, or the assertion below is vacuous: build a U8
9872        // tensor that genuinely holds 5. If this line ever came back as 0/1, the
9873        // real test would pass for the wrong reason — it would be comparing 0/1
9874        // against 0/1 and could not tell a conversion from a reinterpret.
9875        let u8_vals = t
9876            .const_f32_like(vec![0.0, 5.0, 3.0, 0.0], vec![4])
9877            .unwrap()
9878            .to_dtype(DType::U8)
9879            .unwrap();
9880        assert_eq!(u8_vals.dtype(), DType::U8);
9881        assert_eq!(
9882            u8_vals.realize_u8(),
9883            vec![0, 5, 3, 0],
9884            "negative control: the U8 source must really hold 5 and 3, else the \
9885             conversion assertion below cannot discriminate"
9886        );
9887
9888        // THE DISCRIMINATION: U8 -> Bool is a real `!= 0` conversion. A byte
9889        // reinterpret would yield [0, 5, 3, 0] — byte-identical storage makes
9890        // that the silently-correct-looking failure. A conversion yields 0/1.
9891        let as_bool = u8_vals.to_dtype(DType::Bool).unwrap();
9892        assert_eq!(as_bool.dtype(), DType::Bool);
9893        assert_eq!(
9894            as_bool.realize_u8(),
9895            vec![0, 1, 1, 0],
9896            "U8->Bool must CONVERT (5 -> 1), not reinterpret (5 -> 5)"
9897        );
9898
9899        // And back out: Bool -> F32 is exactly 0.0/1.0.
9900        assert_eq!(
9901            as_bool.to_dtype(DType::F32).unwrap().realize_f32(),
9902            vec![0.0, 1.0, 1.0, 0.0],
9903            "Bool->F32 yields 0.0/1.0"
9904        );
9905        // Bool -> U8 keeps the canonical 0/1 (this direction IS byte-preserving,
9906        // which is fine — it is the OTHER direction that must not be).
9907        assert_eq!(
9908            mask.to_dtype(DType::U8).unwrap().realize_u8(),
9909            vec![0, 1, 1, 0]
9910        );
9911
9912        // FINALLY, the two are not INTERCHANGEABLE at the API, which is what
9913        // stops a numeric mask being coerced silently: `masked_fill` requires a
9914        // Bool mask and rejects U8 at GRAPH-BUILD time, so the cast is explicit
9915        // at the call site rather than implied.
9916        let err = t.masked_fill(&u8_vals, fuel_ir::Scalar::F32(-9.0));
9917        assert!(
9918            err.is_err(),
9919            "a U8 mask must be REJECTED by masked_fill — Bool and U8 are \
9920             byte-identical, so accepting it would 'work' and erase the distinction"
9921        );
9922    }
9923
9924    /// GAP-183: `float -> Bool` is lowered in the optimizer to `Ne(x, x*0)` so it
9925    /// reaches CUDA's shipped `ne_{f32,f64,f16,bf16}` kernels, which a
9926    /// storage-level cast cannot compose. This pins the VALUES end-to-end, and
9927    /// the three interesting inputs are the whole point of the test — a lowering
9928    /// that got only the ordinary cases right would still be wrong:
9929    ///
9930    ///   -0.0  -> FALSE   (IEEE `-0.0 == 0.0`)
9931    ///   NaN   -> TRUE    (matches PyTorch `.bool()`)
9932    ///   ±inf  -> TRUE
9933    ///
9934    /// The zeros operand is itself NaN for non-finite x; that is harmless because
9935    /// `x != NaN` and `x != 0` agree for every non-finite x. This test is what
9936    /// would catch it if that ever stopped being true.
9937    #[test]
9938    fn float_to_bool_lowering_preserves_ieee_semantics() {
9939        let t = cpu_f32(
9940            vec![
9941                0.0,
9942                -0.0,
9943                5.0,
9944                -3.0,
9945                f32::NAN,
9946                f32::INFINITY,
9947                f32::NEG_INFINITY,
9948            ],
9949            &[7],
9950        );
9951        let b = t.to_dtype(DType::Bool).unwrap();
9952        assert_eq!(b.dtype(), DType::Bool, "to_dtype(Bool) still yields Bool");
9953        assert_eq!(
9954            b.realize_u8(),
9955            vec![0, 0, 1, 1, 1, 1, 1],
9956            "0 and -0.0 are false; every other value including NaN and both \
9957             infinities is true",
9958        );
9959    }
9960
9961    #[test]
9962    fn index_add_smoke() {
9963        let base = cpu_f32(vec![1.0, 1.0, 1.0, 1.0], &[2, 2]);
9964        let src = base
9965            .const_f32_like(vec![10.0, 20.0, 30.0, 40.0], vec![2, 2])
9966            .unwrap();
9967        let indices = base.const_u32_like(vec![0_u32, 0_u32], vec![2]).unwrap();
9968        let out = base.index_add(0, &indices, &src).unwrap();
9969        assert_eq!(out.shape().dims(), &[2, 2]);
9970        // both src rows added to row 0; row 1 unchanged
9971        let v = out.realize_f32();
9972        assert_eq!(v[0], 41.0); // 1 + 10 + 30
9973        assert_eq!(v[1], 61.0); // 1 + 20 + 40
9974        assert_eq!(v[2], 1.0);
9975        assert_eq!(v[3], 1.0);
9976    }
9977
9978    #[test]
9979    fn scatter_add_smoke() {
9980        let base = cpu_f32(vec![0.0, 0.0, 0.0, 0.0], &[2, 2]);
9981        let src = base
9982            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
9983            .unwrap();
9984        let indices = base
9985            .const_u32_like(vec![0_u32, 1_u32, 1_u32, 0_u32], vec![2, 2])
9986            .unwrap();
9987        let out = base.scatter_add(0, &indices, &src).unwrap();
9988        assert_eq!(out.shape().dims(), &[2, 2]);
9989    }
9990
9991    #[test]
9992    fn inplace_activations_compile_and_run() {
9993        let t = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]);
9994        // Each in-place op is destructive, so chain through fresh tensors.
9995        let r = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).relu_inplace();
9996        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).silu_inplace();
9997        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).gelu_tanh_inplace();
9998        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).tanh_inplace();
9999        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).sigmoid_inplace();
10000        let _ = t.affine_inplace(2.0, 1.0);
10001        // Spot-check the relu output.
10002        let v = r.realize_f32();
10003        assert_eq!(v, vec![0.0, 0.5, 0.0, 2.0]);
10004    }
10005
10006    #[test]
10007    fn const_f64_like_round_trips() {
10008        let anchor = cpu_f32(vec![0.0], &[1]);
10009        let t = anchor.const_f64_like(vec![1.5, 2.5, 3.5], vec![3]).unwrap();
10010        assert_eq!(t.shape().dims(), &[3]);
10011        assert_eq!(t.dtype(), DType::F64);
10012        assert_eq!(t.realize_f64(), vec![1.5, 2.5, 3.5]);
10013    }
10014
10015    #[test]
10016    fn const_i64_like_round_trips() {
10017        let anchor = cpu_f32(vec![0.0], &[1]);
10018        let t = anchor.const_i64_like(vec![-1_i64, 2, -3], vec![3]).unwrap();
10019        assert_eq!(t.shape().dims(), &[3]);
10020        assert_eq!(t.dtype(), DType::I64);
10021    }
10022
10023    #[test]
10024    fn on_device_smoke_cpu() {
10025        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10026        let pinned = t.on_device(&Device::cpu());
10027        assert_eq!(pinned.realize_f32(), vec![1.0, 2.0]);
10028    }
10029
10030    #[test]
10031    fn copy_to_device_same_device_round_trips() {
10032        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10033        let copied = t.copy_to_device(&Device::cpu());
10034        assert_eq!(copied.realize_f32(), vec![1.0, 2.0, 3.0]);
10035    }
10036}
10037
10038// ============================================================================
10039// Phase A.2 composite primitives tests.
10040// ============================================================================
10041#[cfg(test)]
10042mod phase_a2_composite_tests {
10043    use super::*;
10044
10045    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10046        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10047    }
10048
10049    #[test]
10050    fn transpose_last_two_swaps_last_two_dims() {
10051        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10052        let out = t.transpose_last_two().unwrap();
10053        assert_eq!(out.shape().dims(), &[3, 2]);
10054        assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10055    }
10056
10057    #[test]
10058    fn t_is_alias_of_transpose_last_two() {
10059        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10060        assert_eq!(
10061            t.t().unwrap().realize_f32(),
10062            t.transpose_last_two().unwrap().realize_f32()
10063        );
10064    }
10065
10066    #[test]
10067    fn transpose_dims_swaps_arbitrary_axes() {
10068        let t = cpu_f32((0..24).map(|i| i as f32).collect(), &[2, 3, 4]);
10069        let out = t.transpose_dims(0, 2).unwrap();
10070        assert_eq!(out.shape().dims(), &[4, 3, 2]);
10071    }
10072
10073    #[test]
10074    fn transpose_dims_identity_when_same_axis() {
10075        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10076        let out = t.transpose_dims(0, 0).unwrap();
10077        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
10078    }
10079
10080    #[test]
10081    fn transpose_dims_errors_out_of_bounds() {
10082        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10083        assert!(t.transpose_dims(0, 2).is_err());
10084        assert!(t.transpose_dims(5, 0).is_err());
10085    }
10086
10087    #[test]
10088    fn flatten_merges_middle_dims() {
10089        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10090        let out = t.flatten(0, 1).unwrap();
10091        assert_eq!(out.shape().dims(), &[6, 4]);
10092    }
10093
10094    #[test]
10095    fn flatten_to_merges_leading_dims() {
10096        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10097        let out = t.flatten_to(1).unwrap();
10098        assert_eq!(out.shape().dims(), &[6, 4]);
10099    }
10100
10101    #[test]
10102    fn flatten_from_merges_trailing_dims() {
10103        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10104        let out = t.flatten_from(1).unwrap();
10105        assert_eq!(out.shape().dims(), &[2, 12]);
10106    }
10107
10108    #[test]
10109    fn flatten_all_produces_rank_one() {
10110        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10111        let out = t.flatten_all().unwrap();
10112        assert_eq!(out.shape().dims(), &[6]);
10113        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10114    }
10115
10116    /// `rope_batched` at `B == 1` must be **bit-identical** to
10117    /// `rope_with_tables_decomposed`, and each row at `B == 3` must equal the
10118    /// single-position result at that row's own position — the exactness proof
10119    /// for the per-row RoPE that unblocks ragged (non-uniform-position) batched
10120    /// decode. A subtly wrong rotate-half (wrong half split, wrong sign, wrong
10121    /// concat order) still produces plausible tokens, so this asserts
10122    /// bit-equality; the CONTROL proves the per-row table is load-bearing.
10123    /// (Adopted from the Lightbulb consumer's verified `rope_batched` oracle;
10124    /// the mechanism belongs in Fuel.)
10125    #[test]
10126    fn rope_batched_matches_single_position_rope_row_by_row() {
10127        let lcg = |n: usize, seed: u32| -> Vec<f32> {
10128            let mut s = seed;
10129            (0..n)
10130                .map(|_| {
10131                    s = s.wrapping_mul(1103515245).wrapping_add(12345);
10132                    ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
10133                })
10134                .collect()
10135        };
10136        let (b, heads, hd) = (3usize, 4usize, 8usize);
10137        let base = 10000.0f64;
10138        let positions = [0usize, 7, 3];
10139
10140        let x_data = lcg(b * heads * hd, 11);
10141        let x = cpu_f32(x_data.clone(), &[b, heads, 1, hd]);
10142        let (cos, sin) = x.rope_tables_const_batched(base, &positions, hd);
10143        let got = x.rope_batched(&cos, &sin).unwrap().realize_f32();
10144
10145        let per_row = heads * hd;
10146        for (row, &pos) in positions.iter().enumerate() {
10147            let row_data = x_data[row * per_row..(row + 1) * per_row].to_vec();
10148            let xr = cpu_f32(row_data, &[1, heads, 1, hd]);
10149            let (c, s) = xr.rope_tables_const(base, pos, 1, hd);
10150            let want = xr
10151                .rope_with_tables_decomposed(&c, &s)
10152                .unwrap()
10153                .realize_f32();
10154            assert_eq!(
10155                &got[row * per_row..(row + 1) * per_row],
10156                &want[..],
10157                "row {row} (position {pos}) != decomposed RoPE at that position",
10158            );
10159        }
10160
10161        // CONTROL: prove the per-row table is load-bearing. Give EVERY row
10162        // position 0 (row 0's position); rows at other positions must now
10163        // DISAGREE with their own-position result — else the comparison above is
10164        // blind to the whole per-row-position mechanism.
10165        let shared = [positions[0]; 3];
10166        let (c0, s0) = x.rope_tables_const_batched(base, &shared, hd);
10167        let wrong = x.rope_batched(&c0, &s0).unwrap().realize_f32();
10168        for (row, &pos) in positions.iter().enumerate().skip(1) {
10169            assert_ne!(
10170                &wrong[row * per_row..(row + 1) * per_row],
10171                &got[row * per_row..(row + 1) * per_row],
10172                "CONTROL: row {row} at shared position 0 matched its own position \
10173                 {pos} — per-row RoPE not applied, or comparison blind to it",
10174            );
10175        }
10176    }
10177
10178    #[test]
10179    fn flatten_errors_on_bad_range() {
10180        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10181        assert!(t.flatten(0, 5).is_err());
10182        assert!(t.flatten(2, 1).is_err()); // start > end
10183    }
10184
10185    #[test]
10186    fn split_heads_then_merge_heads_round_trip() {
10187        // (B=1, N=2, embed=6) — split into 2 heads of head_dim=3.
10188        let x = cpu_f32((0..12).map(|i| i as f32).collect(), &[1, 2, 6]);
10189        let split = x.split_heads(2, 3).unwrap();
10190        assert_eq!(split.shape().dims(), &[1, 2, 2, 3]);
10191        let merged = split.merge_heads().unwrap();
10192        assert_eq!(merged.shape().dims(), &[1, 2, 6]);
10193        let m = merged.realize_f32();
10194        let original = x.realize_f32();
10195        for (a, b) in m.iter().zip(original.iter()) {
10196            assert!((a - b).abs() < 1e-7, "{a} vs {b}");
10197        }
10198    }
10199
10200    #[test]
10201    fn rope_tables_const_shapes_and_anchoring() {
10202        let anchor = cpu_f32(vec![0.0_f32; 8], &[1, 4, 2]);
10203        let (cos, sin) = anchor.rope_tables_const(10_000.0, 0, 4, 8);
10204        assert_eq!(cos.shape().dims(), &[4, 8]);
10205        assert_eq!(sin.shape().dims(), &[4, 8]);
10206        // First row at position 0: cos = 1.0, sin = 0.0 for every pair.
10207        let cv = cos.realize_f32();
10208        let sv = sin.realize_f32();
10209        // cos[0,...] should all be 1.0 (RoPE at position 0).
10210        for i in 0..8 {
10211            assert!((cv[i] - 1.0).abs() < 1e-5, "cos[0,{i}] = {}", cv[i]);
10212            assert!(sv[i].abs() < 1e-5, "sin[0,{i}] = {}", sv[i]);
10213        }
10214    }
10215
10216    #[test]
10217    fn embed_tokens_shape_and_lookup() {
10218        // 5-token vocab, 3-dim hidden. Vocab embedding table contains
10219        // row i = (i, i+0.5, i+1) so the lookup result is verifiable.
10220        let vocab_size = 5;
10221        let hidden = 3;
10222        let table: Vec<f32> = (0..vocab_size)
10223            .flat_map(|i| vec![i as f32, i as f32 + 0.5, i as f32 + 1.0])
10224            .collect();
10225        let tokens = vec![1_u32, 3, 0];
10226        let out = Tensor::embed_tokens(
10227            std::sync::Arc::from(table),
10228            vocab_size,
10229            hidden,
10230            &tokens,
10231            &crate::Device::cpu(),
10232        )
10233        .unwrap();
10234        assert_eq!(out.shape().dims(), &[1, 3, hidden]);
10235        let v = out.realize_f32();
10236        let want = [
10237            1.0_f32, 1.5, 2.0, // token 1
10238            3.0, 3.5, 4.0, // token 3
10239            0.0, 0.5, 1.0, // token 0
10240        ];
10241        for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10242            assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10243        }
10244    }
10245
10246    #[test]
10247    fn embed_tokens_anchored_lives_on_receiver_graph() {
10248        // Two paths: one bootstrapped via embed_tokens, one anchored
10249        // on a pre-existing tensor. Both produce identical values,
10250        // but only the anchored one can compose with the anchor.
10251        let vocab_size = 4;
10252        let hidden = 2;
10253        let table: Vec<f32> = (0..vocab_size)
10254            .flat_map(|i| vec![i as f32, i as f32 * 2.0])
10255            .collect();
10256        let table_arc: std::sync::Arc<[f32]> = std::sync::Arc::from(table);
10257        let tokens = vec![2_u32, 1];
10258
10259        let anchor = cpu_f32(vec![0.0_f32], &[1]);
10260        let embedded = anchor
10261            .embed_tokens_anchored(
10262                std::sync::Arc::clone(&table_arc),
10263                vocab_size,
10264                hidden,
10265                &tokens,
10266            )
10267            .unwrap();
10268        assert_eq!(embedded.shape().dims(), &[1, 2, hidden]);
10269
10270        // Anchored: composes with the anchor.
10271        let one_scaled = anchor
10272            .const_f32_like(std::sync::Arc::from(vec![1.0_f32]), Shape::from_dims(&[1]))
10273            .unwrap();
10274        let _ = embedded
10275            .add(
10276                &one_scaled
10277                    .reshape(Shape::from_dims(&[1, 1, 1]))
10278                    .unwrap()
10279                    .broadcast_to(Shape::from_dims(&[1, 2, hidden]))
10280                    .unwrap(),
10281            )
10282            .unwrap();
10283        let v = embedded.realize_f32();
10284        let want = [
10285            2.0_f32, 4.0, // token 2
10286            1.0, 2.0, // token 1
10287        ];
10288        for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10289            assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10290        }
10291    }
10292
10293    #[test]
10294    fn embed_tokens_empty_returns_error() {
10295        let r = Tensor::embed_tokens(
10296            std::sync::Arc::from(vec![0.0_f32]),
10297            1,
10298            1,
10299            &[],
10300            &crate::Device::cpu(),
10301        );
10302        assert!(r.is_err());
10303    }
10304
10305    #[test]
10306    fn softcap_matches_tanh_form() {
10307        // Tiny input; verify cap·tanh(x/cap) at known points.
10308        let x = cpu_f32(vec![0.0_f32, 5.0, -5.0, 30.0], &[1, 4]);
10309        let capped = x.softcap(10.0).realize_f32();
10310        // tanh(0)=0; tanh(0.5)≈0.4621; tanh(-0.5)≈-0.4621; tanh(3)≈0.9951.
10311        let expect = [0.0_f32, 4.6212, -4.6212, 9.9505];
10312        for (i, (&got, &want)) in capped.iter().zip(expect.iter()).enumerate() {
10313            assert!(
10314                (got - want).abs() < 1e-3,
10315                "softcap[{i}] got={got} want={want}"
10316            );
10317        }
10318    }
10319
10320    #[test]
10321    fn softcap_optional_none_returns_input_unchanged() {
10322        let x = cpu_f32(vec![1.0_f32, -2.0, 30.0], &[1, 3]);
10323        let out = x.softcap_optional(None).realize_f32();
10324        let expect = [1.0_f32, -2.0, 30.0];
10325        for (g, w) in out.iter().zip(expect.iter()) {
10326            assert!((g - w).abs() < 1e-6);
10327        }
10328        // Some(0.0) or negative cap also returns unchanged (guard).
10329        let out = x.softcap_optional(Some(0.0)).realize_f32();
10330        for (g, w) in out.iter().zip(expect.iter()) {
10331            assert!((g - w).abs() < 1e-6);
10332        }
10333    }
10334
10335    #[test]
10336    fn rope_partial_full_dim_matches_rope_with_tables() {
10337        // head_dim == rope_dim ⇒ should degenerate to full rope.
10338        let qk = cpu_f32(vec![1.0_f32; 1 * 1 * 2 * 4], &[1, 1, 2, 4]);
10339        let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 2, 4);
10340        let via_partial = qk.rope_partial(&cos, &sin, 4).unwrap();
10341        let via_full = qk.rope_with_tables(&cos, &sin).unwrap();
10342        let a = via_partial.realize_f32();
10343        let b = via_full.realize_f32();
10344        for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
10345            assert!((av - bv).abs() < 1e-6, "{i}: {av} vs {bv}");
10346        }
10347    }
10348
10349    #[test]
10350    fn rope_partial_pass_through_suffix_unchanged() {
10351        // rope_dim=2, head_dim=4 ⇒ first 2 features rotated, last 2
10352        // unchanged. At position 0 the rotation is identity, so all 4
10353        // features should equal the input.
10354        let qk = cpu_f32(
10355            vec![
10356                // shape [1, 1, 1, 4] — one position, one head
10357                1.0_f32, 2.0, 3.0, 4.0,
10358            ],
10359            &[1, 1, 1, 4],
10360        );
10361        let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 1, 2);
10362        let out = qk.rope_partial(&cos, &sin, 2).unwrap().realize_f32();
10363        for (i, &v) in out.iter().enumerate() {
10364            let expect = [1.0_f32, 2.0, 3.0, 4.0][i];
10365            assert!((v - expect).abs() < 1e-6, "{i}: {v} != {expect}");
10366        }
10367    }
10368
10369    #[test]
10370    fn add_optional_trailing_bias_none_returns_input_unchanged() {
10371        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10372        let original = a.realize_f32();
10373        let out = a.add_optional_trailing_bias(None).unwrap();
10374        assert_eq!(out.realize_f32(), original);
10375    }
10376
10377    #[test]
10378    fn add_optional_trailing_bias_some_applies_add() {
10379        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10380        let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, 20.0, 30.0]);
10381        let out = a.add_optional_trailing_bias(Some(&bias)).unwrap();
10382        assert_eq!(out.realize_f32(), vec![11.0, 22.0, 33.0]);
10383    }
10384
10385    #[test]
10386    fn add_trailing_bias_broadcasts_across_leading_dims() {
10387        // (2, 3) input + length-3 bias should add per-column.
10388        let x = cpu_f32(vec![1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0], &[2, 3]);
10389        let bias = std::sync::Arc::<[f32]>::from(vec![100.0_f32, 200.0, 300.0]);
10390        let out = x.add_trailing_bias(bias).unwrap();
10391        assert_eq!(out.shape().dims(), &[2, 3]);
10392        let v = out.realize_f32();
10393        assert_eq!(v, vec![101.0, 202.0, 303.0, 110.0, 220.0, 330.0]);
10394    }
10395
10396    #[test]
10397    fn rms_norm_affine_with_offset_adds_offset_to_each_gain() {
10398        let x = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10399        let gain_raw: [f32; 3] = [-0.5, 0.0, 0.5];
10400        let via_offset = x.rms_norm_affine_with_offset(&gain_raw, 1.0, 1e-6).unwrap();
10401        let gain_shifted = std::sync::Arc::<[f32]>::from(vec![0.5_f32, 1.0, 1.5]);
10402        let via_plain = x.rms_norm_affine(gain_shifted, 1e-6).unwrap();
10403        let a = via_offset.realize_f32();
10404        let b = via_plain.realize_f32();
10405        for (x, y) in a.iter().zip(b.iter()) {
10406            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
10407        }
10408    }
10409
10410    #[test]
10411    fn global_avg_pool_2d_averages_spatial_dims() {
10412        // (1, 2, 2, 3) — two channels, 2×3 spatial.
10413        // Channel 0: 1..=6, mean = 3.5.
10414        // Channel 1: 10, 20, 30, 40, 50, 60, mean = 35.
10415        let x = cpu_f32(
10416            vec![
10417                1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0,
10418            ],
10419            &[1, 2, 2, 3],
10420        );
10421        let out = x.global_avg_pool_2d().unwrap();
10422        assert_eq!(out.shape().dims(), &[1, 2]);
10423        let v = out.realize_f32();
10424        assert!((v[0] - 3.5).abs() < 1e-5);
10425        assert!((v[1] - 35.0).abs() < 1e-4);
10426    }
10427
10428    #[test]
10429    fn channel_affine_4d_applies_per_channel_gain_and_bias() {
10430        // (1, 2, 2, 2) — two channels, each a 2×2 spatial map.
10431        let x = cpu_f32(
10432            vec![
10433                1.0_f32, 2.0, 3.0, 4.0, // channel 0
10434                10.0, 20.0, 30.0, 40.0, // channel 1
10435            ],
10436            &[1, 2, 2, 2],
10437        );
10438        let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5]);
10439        let bias = std::sync::Arc::<[f32]>::from(vec![1.0_f32, -10.0]);
10440        let out = x.channel_affine_4d(gain, bias).unwrap();
10441        let v = out.realize_f32();
10442        // Channel 0: gain=2, bias=1 → 2x+1
10443        assert_eq!(&v[0..4], &[3.0, 5.0, 7.0, 9.0]);
10444        // Channel 1: gain=0.5, bias=-10 → 0.5x-10
10445        assert_eq!(&v[4..8], &[-5.0, 0.0, 5.0, 10.0]);
10446    }
10447
10448    #[test]
10449    fn additive_causal_mask_has_strict_lower_triangle() {
10450        let anchor = cpu_f32(vec![0.0_f32], &[1]);
10451        let mask = Tensor::additive_causal_mask_like(&anchor, 4);
10452        assert_eq!(mask.shape().dims(), &[4, 4]);
10453        let v = mask.realize_f32();
10454        // Expected (-inf shown as 'x'):
10455        //   0 x x x
10456        //   0 0 x x
10457        //   0 0 0 x
10458        //   0 0 0 0
10459        for i in 0..4 {
10460            for j in 0..4 {
10461                let got = v[i * 4 + j];
10462                if j > i {
10463                    assert!(
10464                        got.is_infinite() && got.is_sign_negative(),
10465                        "above-diag (i={i}, j={j}) should be -inf, got {got}"
10466                    );
10467                } else {
10468                    assert_eq!(
10469                        got, 0.0,
10470                        "on/below-diag (i={i}, j={j}) should be 0, got {got}"
10471                    );
10472                }
10473            }
10474        }
10475    }
10476
10477    #[test]
10478    fn layer_norm_affine_unit_gain_zero_bias_matches_layer_norm_last_dim() {
10479        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10480        let gain = std::sync::Arc::<[f32]>::from(vec![1.0_f32; 3]);
10481        let bias = std::sync::Arc::<[f32]>::from(vec![0.0_f32; 3]);
10482        let out_affine = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10483        let out_plain = a.layer_norm_last_dim(1e-5).unwrap();
10484        let va = out_affine.realize_f32();
10485        let vp = out_plain.realize_f32();
10486        for (a, b) in va.iter().zip(vp.iter()) {
10487            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
10488        }
10489    }
10490
10491    #[test]
10492    fn layer_norm_affine_applies_gain_and_bias() {
10493        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10494        let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5, 1.5]);
10495        let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, -5.0, 0.0]);
10496        let out = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10497        let v = out.realize_f32();
10498        // Manual: mean=2, var=2/3; normed = (x-2)/sqrt(2/3+1e-5).
10499        let mean = 2.0_f32;
10500        let var = ((1.0 - mean).powi(2) + (2.0 - mean).powi(2) + (3.0 - mean).powi(2)) / 3.0;
10501        let den = (var + 1e-5_f32).sqrt();
10502        let expected = [
10503            ((1.0 - mean) / den) * 2.0 + 10.0,
10504            ((2.0 - mean) / den) * 0.5 + (-5.0),
10505            ((3.0 - mean) / den) * 1.5 + 0.0,
10506        ];
10507        for (got, want) in v.iter().zip(expected.iter()) {
10508            assert!((got - want).abs() < 1e-4, "{got} vs {want}");
10509        }
10510    }
10511
10512    #[test]
10513    fn l2_normalize_last_dim_unit_norm_per_row() {
10514        // (2, 3): rows [3,4,0] (norm 5) and [1,2,2] (norm 3).
10515        let a = cpu_f32(vec![3.0, 4.0, 0.0, 1.0, 2.0, 2.0], &[2, 3]);
10516        let out = a.l2_normalize(1_usize, 1e-12).unwrap();
10517        assert_eq!(out.shape().dims(), &[2, 3]);
10518        let v = out.realize_f32();
10519        let row0_norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
10520        let row1_norm = (v[3] * v[3] + v[4] * v[4] + v[5] * v[5]).sqrt();
10521        assert!((row0_norm - 1.0).abs() < 1e-5, "row 0 norm = {row0_norm}");
10522        assert!((row1_norm - 1.0).abs() < 1e-5, "row 1 norm = {row1_norm}");
10523        // Row 0: [3,4,0]/5 → [0.6, 0.8, 0.0].
10524        assert!((v[0] - 0.6).abs() < 1e-5);
10525        assert!((v[1] - 0.8).abs() < 1e-5);
10526        assert!(v[2].abs() < 1e-5);
10527    }
10528
10529    #[test]
10530    fn l2_normalize_eps_zero_works_when_nonzero() {
10531        let a = cpu_f32(vec![1.0_f32, 0.0], &[2]);
10532        let out = a.l2_normalize(0_usize, 0.0).unwrap();
10533        let v = out.realize_f32();
10534        assert!((v[0] - 1.0).abs() < 1e-6);
10535        assert!(v[1].abs() < 1e-6);
10536    }
10537
10538    #[test]
10539    fn repeat_interleave_last_dim_matches_torch_semantics() {
10540        // (2, 3) input: rows [1,2,3] and [4,5,6]. dim=1, repeats=2
10541        // → each element becomes two consecutive copies:
10542        // (2, 6): [1,1,2,2,3,3] and [4,4,5,5,6,6].
10543        let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10544        let out = a.repeat_interleave(1_usize, 2).unwrap();
10545        assert_eq!(out.shape().dims(), &[2, 6]);
10546        assert_eq!(
10547            out.realize_f32(),
10548            vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0]
10549        );
10550    }
10551
10552    #[test]
10553    fn repeat_interleave_middle_dim() {
10554        // (2, 2, 2) input. dim=1, repeats=3 → (2, 6, 2).
10555        let a = cpu_f32((0..8).map(|i| i as f32).collect(), &[2, 2, 2]);
10556        let out = a.repeat_interleave(1_usize, 3).unwrap();
10557        assert_eq!(out.shape().dims(), &[2, 6, 2]);
10558        // First sample's elements: (0,1) repeated 3× then (2,3) repeated 3×.
10559        let v = out.realize_f32();
10560        assert_eq!(&v[0..6], &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
10561        assert_eq!(&v[6..12], &[2.0, 3.0, 2.0, 3.0, 2.0, 3.0]);
10562    }
10563
10564    #[test]
10565    fn repeat_interleave_repeats_one_is_noop() {
10566        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10567        let out = a.repeat_interleave(0_usize, 1).unwrap();
10568        assert_eq!(out.shape().dims(), &[3]);
10569        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10570    }
10571
10572    #[test]
10573    fn repeat_interleave_repeats_zero_errors() {
10574        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10575        assert!(a.repeat_interleave(0_usize, 0).is_err());
10576    }
10577
10578    #[test]
10579    fn stack_adds_leading_dim() {
10580        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10581        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10582        let out = Tensor::stack(&[&a, &b], 0).unwrap();
10583        assert_eq!(out.shape().dims(), &[2, 3]);
10584        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10585    }
10586
10587    #[test]
10588    fn stack_adds_trailing_dim() {
10589        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10590        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10591        let out = Tensor::stack(&[&a, &b], 1).unwrap();
10592        assert_eq!(out.shape().dims(), &[3, 2]);
10593        assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10594    }
10595
10596    #[test]
10597    fn stack_rejects_mismatched_shapes() {
10598        let a = cpu_f32(vec![1.0, 2.0], &[2]);
10599        let b = a.const_f32_like(vec![3.0, 4.0, 5.0], vec![3]).unwrap();
10600        assert!(Tensor::stack(&[&a, &b], 0).is_err());
10601    }
10602
10603    #[test]
10604    fn stack_rejects_empty_input() {
10605        let result = Tensor::stack(&[], 0);
10606        assert!(result.is_err());
10607    }
10608
10609    #[test]
10610    fn repeat_tiles_along_each_dim() {
10611        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10612        let out = t.repeat(vec![3]).unwrap();
10613        assert_eq!(out.shape().dims(), &[6]);
10614        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
10615    }
10616
10617    #[test]
10618    fn repeat_extends_rank_when_needed() {
10619        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10620        // repeat with shape [3, 2] left-pads tensor to [1, 2] then tiles to [3, 4]
10621        let out = t.repeat(vec![3, 2]).unwrap();
10622        assert_eq!(out.shape().dims(), &[3, 4]);
10623    }
10624
10625    #[test]
10626    fn repeat_identity_with_all_ones() {
10627        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10628        let out = t.repeat(vec![1]).unwrap();
10629        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10630    }
10631}
10632
10633// ============================================================================
10634// Phase A.3 keepdim reduction tests.
10635// ============================================================================
10636#[cfg(test)]
10637mod phase_a3_keepdim_tests {
10638    use super::*;
10639
10640    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10641        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10642    }
10643
10644    #[test]
10645    fn sum_keepdim_preserves_dim_as_one() {
10646        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10647        let out = t.sum_keepdim(1_usize).unwrap();
10648        assert_eq!(out.shape().dims(), &[2, 1]);
10649        assert_eq!(out.realize_f32(), vec![3.0, 7.0]);
10650    }
10651
10652    #[test]
10653    fn mean_keepdim_preserves_dim_as_one() {
10654        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10655        let out = t.mean_keepdim(0_usize).unwrap();
10656        assert_eq!(out.shape().dims(), &[1, 2]);
10657        assert_eq!(out.realize_f32(), vec![2.0, 3.0]);
10658    }
10659
10660    #[test]
10661    fn max_keepdim_preserves_dim_as_one() {
10662        let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10663        let out = t.max_keepdim(1_usize).unwrap();
10664        assert_eq!(out.shape().dims(), &[2, 1]);
10665        assert_eq!(out.realize_f32(), vec![3.0, 4.0]);
10666    }
10667
10668    #[test]
10669    fn min_keepdim_preserves_dim_as_one() {
10670        let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10671        let out = t.min_keepdim(1_usize).unwrap();
10672        assert_eq!(out.shape().dims(), &[2, 1]);
10673        assert_eq!(out.realize_f32(), vec![1.0, 2.0]);
10674    }
10675
10676    #[test]
10677    fn var_matches_unbiased_formula_with_correction_1() {
10678        // [[1,2,3],[4,5,6]] -> var along axis 1: each row has mean=mid, dev=[-1,0,1], sq sum=2, /(3-1) = 1
10679        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10680        let out = t.var(1, 1.0).unwrap();
10681        assert_eq!(out.shape().dims(), &[2]);
10682        let v = out.realize_f32();
10683        assert!((v[0] - 1.0).abs() < 1e-5, "var row 0 = {} != 1.0", v[0]);
10684        assert!((v[1] - 1.0).abs() < 1e-5, "var row 1 = {} != 1.0", v[1]);
10685    }
10686
10687    /// KISS-Ops §6.13 S14: `correction = 0.0` (the spec default, numpy's
10688    /// default) divides by `n` instead of `n - 1` -- same fixture as the
10689    /// Bessel test above, different divisor, so the two tests pin BOTH
10690    /// halves of the explicit-attribute contract against the same data.
10691    #[test]
10692    fn var_population_with_correction_0() {
10693        // Same fixture: dev=[-1,0,1], sq sum=2, /3 = 0.666...
10694        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10695        let out = t.var(1, 0.0).unwrap();
10696        let v = out.realize_f32();
10697        let want = 2.0 / 3.0;
10698        assert!(
10699            (v[0] - want).abs() < 1e-5,
10700            "population var row 0 = {} != {want}",
10701            v[0]
10702        );
10703        assert!(
10704            (v[1] - want).abs() < 1e-5,
10705            "population var row 1 = {} != {want}",
10706            v[1]
10707        );
10708    }
10709
10710    #[test]
10711    fn var_keepdim_preserves_dim() {
10712        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10713        let out = t.var_keepdim(1, 1.0).unwrap();
10714        assert_eq!(out.shape().dims(), &[2, 1]);
10715    }
10716
10717    #[test]
10718    fn var_errors_out_of_bounds() {
10719        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10720        assert!(t.var(3, 1.0).is_err());
10721        assert!(t.var_keepdim(3, 1.0).is_err());
10722    }
10723}
10724
10725// ============================================================================
10726// Phase A.4 scalar/binary composite tests.
10727// ============================================================================
10728#[cfg(test)]
10729mod phase_a4_composite_tests {
10730    use super::*;
10731
10732    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10733        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10734    }
10735
10736    #[test]
10737    fn affine_applies_mul_then_add() {
10738        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10739        let out = t.affine(2.0, 10.0);
10740        assert_eq!(out.realize_f32(), vec![12.0, 14.0, 16.0]);
10741    }
10742
10743    #[test]
10744    fn scale_and_shift_alias_of_affine() {
10745        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10746        assert_eq!(
10747            t.scale_and_shift(2.0, 10.0).realize_f32(),
10748            t.affine(2.0, 10.0).realize_f32(),
10749        );
10750    }
10751
10752    #[test]
10753    fn elu_matches_reference_values() {
10754        let t = cpu_f32(vec![1.0, 0.0, -1.0, -2.0], &[4]);
10755        let out = t.elu(1.0);
10756        let v = out.realize_f32();
10757        // x > 0: identity. x == 0: 0 (boundary; gt returns 0 → neg branch which is alpha*(1-1)=0).
10758        // x < 0: alpha * (exp(x) - 1).
10759        assert!((v[0] - 1.0).abs() < 1e-5);
10760        assert!(v[1].abs() < 1e-5);
10761        assert!((v[2] - ((-1.0_f32).exp() - 1.0)).abs() < 1e-5);
10762        assert!((v[3] - ((-2.0_f32).exp() - 1.0)).abs() < 1e-5);
10763    }
10764
10765    #[test]
10766    fn dot_of_rank_one_vectors() {
10767        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10768        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10769        let out = a.dot(&b).unwrap();
10770        assert_eq!(out.shape().elem_count(), 1);
10771        let v = out.realize_f32();
10772        assert_eq!(v[0], 32.0); // 1*4 + 2*5 + 3*6
10773    }
10774
10775    #[test]
10776    fn dot_rejects_non_rank_one() {
10777        let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10778        let b = a
10779            .const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
10780            .unwrap();
10781        assert!(a.dot(&b).is_err());
10782    }
10783
10784    #[test]
10785    fn dot_rejects_length_mismatch() {
10786        let a = cpu_f32(vec![1.0, 2.0], &[2]);
10787        let b = a.const_f32_like(vec![1.0, 2.0, 3.0], vec![3]).unwrap();
10788        assert!(a.dot(&b).is_err());
10789    }
10790
10791    #[test]
10792    fn mv_matrix_times_vector() {
10793        let m = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10794        let v = m.const_f32_like(vec![1.0, 1.0, 1.0], vec![3]).unwrap();
10795        let out = m.mv(&v).unwrap();
10796        assert_eq!(out.shape().dims(), &[2]);
10797        assert_eq!(out.realize_f32(), vec![6.0, 15.0]);
10798    }
10799
10800    #[test]
10801    fn matvec_is_mv_alias() {
10802        let m = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10803        let v = m.const_f32_like(vec![3.0, 4.0], vec![2]).unwrap();
10804        let a = m.mv(&v).unwrap().realize_f32();
10805        let b = m.matvec(&v).unwrap().realize_f32();
10806        assert_eq!(a, b);
10807    }
10808
10809    #[test]
10810    fn mv_rejects_shape_mismatch() {
10811        let m = cpu_f32(vec![1.0; 6], &[2, 3]);
10812        let v = m.const_f32_like(vec![1.0, 1.0], vec![2]).unwrap();
10813        assert!(m.mv(&v).is_err());
10814    }
10815
10816    #[test]
10817    fn broadcast_matmul_passes_through_to_matmul() {
10818        let a = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10819        let b = a
10820            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10821            .unwrap();
10822        let out = a.broadcast_matmul(&b).unwrap();
10823        assert_eq!(out.realize_f32(), vec![5.0, 6.0, 7.0, 8.0]);
10824    }
10825}
10826
10827// ============================================================================
10828// Phase A.5 factory family tests.
10829// ============================================================================
10830#[cfg(test)]
10831mod phase_a5_factory_tests {
10832    use super::*;
10833
10834    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10835        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10836    }
10837
10838    #[test]
10839    fn ones_like_matches_shape_dtype_graph() {
10840        let t = cpu_f32(vec![5.0, 6.0, 7.0, 8.0], &[2, 2]);
10841        let ones = t.ones_like().unwrap();
10842        assert_eq!(ones.shape().dims(), t.shape().dims());
10843        assert_eq!(ones.dtype(), t.dtype());
10844        assert_eq!(ones.realize_f32(), vec![1.0; 4]);
10845    }
10846
10847    #[test]
10848    fn zeros_like_matches_shape_dtype_graph() {
10849        let t = cpu_f32(vec![5.0, 6.0, 7.0], &[3]);
10850        let zeros = t.zeros_like().unwrap();
10851        assert_eq!(zeros.realize_f32(), vec![0.0; 3]);
10852    }
10853
10854    #[test]
10855    fn static_ones_f32() {
10856        let t = Tensor::ones(vec![2, 3], DType::F32, &Device::cpu()).unwrap();
10857        assert_eq!(t.shape().dims(), &[2, 3]);
10858        assert_eq!(t.realize_f32(), vec![1.0; 6]);
10859    }
10860
10861    #[test]
10862    fn static_zeros_f64() {
10863        let t = Tensor::zeros(vec![4], DType::F64, &Device::cpu()).unwrap();
10864        assert_eq!(t.dtype(), DType::F64);
10865        assert_eq!(t.realize_f64(), vec![0.0; 4]);
10866    }
10867
10868    #[test]
10869    fn full_with_f32_scalar() {
10870        let t = Tensor::full(vec![5], fuel_ir::Scalar::F32(2.5), &Device::cpu()).unwrap();
10871        assert_eq!(t.realize_f32(), vec![2.5; 5]);
10872    }
10873
10874    #[test]
10875    fn eye_identity_matrix() {
10876        let t = Tensor::eye(3, DType::F32, &Device::cpu());
10877        assert_eq!(t.shape().dims(), &[3, 3]);
10878        assert_eq!(
10879            t.realize_f32(),
10880            vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
10881        );
10882    }
10883
10884    #[test]
10885    fn tril2_lower_triangular_ones() {
10886        let t = Tensor::tril2(3, DType::F32, &Device::cpu());
10887        assert_eq!(
10888            t.realize_f32(),
10889            vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0],
10890        );
10891    }
10892
10893    #[test]
10894    fn triu2_upper_triangular_ones() {
10895        let t = Tensor::triu2(3, DType::F32, &Device::cpu());
10896        assert_eq!(
10897            t.realize_f32(),
10898            vec![1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0],
10899        );
10900    }
10901
10902    #[test]
10903    fn meshgrid_ij_indexing_two_inputs() {
10904        let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10905        let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10906        let grids = Tensor::meshgrid(&[&x, &y], false).unwrap();
10907        assert_eq!(grids.len(), 2);
10908        // ij: shapes are [len(x), len(y)] = [3, 2].
10909        assert_eq!(grids[0].shape().dims(), &[3, 2]);
10910        assert_eq!(grids[1].shape().dims(), &[3, 2]);
10911        // X grid: each row repeats x's value, so each row is identical along axis 1.
10912        assert_eq!(grids[0].realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
10913        // Y grid: each column repeats y's value, so each column is identical along axis 0.
10914        assert_eq!(grids[1].realize_f32(), vec![4.0, 5.0, 4.0, 5.0, 4.0, 5.0]);
10915    }
10916
10917    #[test]
10918    fn meshgrid_xy_indexing_swaps_first_two() {
10919        let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10920        let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10921        let grids = Tensor::meshgrid(&[&x, &y], true).unwrap();
10922        // xy: shapes flip to [len(y), len(x)] = [2, 3].
10923        assert_eq!(grids[0].shape().dims(), &[2, 3]);
10924        assert_eq!(grids[1].shape().dims(), &[2, 3]);
10925        // X grid: same row twice, each row is x.
10926        assert_eq!(grids[0].realize_f32(), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
10927        // Y grid: each row repeats one y element.
10928        assert_eq!(grids[1].realize_f32(), vec![4.0, 4.0, 4.0, 5.0, 5.0, 5.0]);
10929    }
10930
10931    #[test]
10932    fn meshgrid_rejects_single_input() {
10933        let x = Tensor::from_f32(vec![1.0_f32, 2.0], vec![2], &Device::cpu()).unwrap();
10934        assert!(Tensor::meshgrid(&[&x], false).is_err());
10935    }
10936
10937    #[test]
10938    fn meshgrid_rejects_non_rank_one() {
10939        let x = Tensor::from_f32(vec![1.0; 4], vec![2, 2], &Device::cpu()).unwrap();
10940        let y = x.const_f32_like(vec![1.0, 2.0], vec![2]).unwrap();
10941        assert!(Tensor::meshgrid(&[&x, &y], false).is_err());
10942    }
10943
10944    // ---- additional deferred-Phase-A item tests ----
10945
10946    #[test]
10947    fn narrow_is_slice_alias() {
10948        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5]);
10949        let a = t.narrow(0, 1, 3).unwrap().realize_f32();
10950        let b = t.slice(0, 1, 3).unwrap().realize_f32();
10951        assert_eq!(a, b);
10952        assert_eq!(a, vec![2.0, 3.0, 4.0]);
10953    }
10954
10955    #[test]
10956    fn chunk_splits_evenly() {
10957        let t = cpu_f32((1..=6).map(|i| i as f32).collect(), &[6]);
10958        let parts = t.chunk(3, 0).unwrap();
10959        assert_eq!(parts.len(), 3);
10960        assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0]);
10961        assert_eq!(parts[1].realize_f32(), vec![3.0, 4.0]);
10962        assert_eq!(parts[2].realize_f32(), vec![5.0, 6.0]);
10963    }
10964
10965    #[test]
10966    fn chunk_distributes_remainder_to_leading() {
10967        // size 7, 3 chunks → first 7%3=1 chunk gets the extra: sizes 3, 2, 2
10968        let t = cpu_f32((1..=7).map(|i| i as f32).collect(), &[7]);
10969        let parts = t.chunk(3, 0).unwrap();
10970        assert_eq!(parts.len(), 3);
10971        assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0, 3.0]);
10972        assert_eq!(parts[1].realize_f32(), vec![4.0, 5.0]);
10973        assert_eq!(parts[2].realize_f32(), vec![6.0, 7.0]);
10974    }
10975
10976    #[test]
10977    fn chunk_returns_singletons_when_size_less_than_chunks() {
10978        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10979        let parts = t.chunk(5, 0).unwrap();
10980        assert_eq!(parts.len(), 2);
10981        assert_eq!(parts[0].realize_f32(), vec![1.0]);
10982        assert_eq!(parts[1].realize_f32(), vec![2.0]);
10983    }
10984
10985    #[test]
10986    fn get_at_first_dim() {
10987        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
10988        let row1 = t.get(1).unwrap();
10989        assert_eq!(row1.shape().dims(), &[2]);
10990        assert_eq!(row1.realize_f32(), vec![3.0, 4.0]);
10991    }
10992
10993    #[test]
10994    fn get_on_dim_arbitrary_axis() {
10995        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
10996        let col0 = t.get_on_dim(1, 0).unwrap();
10997        assert_eq!(col0.shape().dims(), &[3]);
10998        assert_eq!(col0.realize_f32(), vec![1.0, 3.0, 5.0]);
10999    }
11000
11001    #[test]
11002    fn sum_dims_multi_dim_reduces_to_smaller() {
11003        // [2,3,4] sum over (0, 2) → [3]
11004        let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11005        let s = t.sum_dims([0, 2_usize]).unwrap();
11006        assert_eq!(s.shape().dims(), &[3]);
11007        // each element is 2*4 = 8 (sum across dim 0 = 2 elements, dim 2 = 4 elements)
11008        assert_eq!(s.realize_f32(), vec![8.0; 3]);
11009    }
11010
11011    #[test]
11012    fn mean_dims_multi_dim() {
11013        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11014        let m = t.mean_dims([0, 1_usize]).unwrap();
11015        assert_eq!(m.shape().dims(), &[] as &[usize]);
11016        assert_eq!(m.realize_f32(), vec![2.5]);
11017    }
11018
11019    #[test]
11020    fn sum_dims_keepdim_preserves_rank() {
11021        let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11022        let s = t.sum_dims_keepdim(&[0, 2]).unwrap();
11023        assert_eq!(s.shape().dims(), &[1, 3, 1]);
11024    }
11025
11026    #[test]
11027    fn mean_dims_keepdim_preserves_rank() {
11028        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11029        let m = t.mean_dims_keepdim(&[0, 1]).unwrap();
11030        assert_eq!(m.shape().dims(), &[1, 1]);
11031        assert_eq!(m.realize_f32(), vec![2.5]);
11032    }
11033
11034    #[test]
11035    fn rand_like_matches_shape_dtype() {
11036        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
11037        let r = t.rand_like(-1.0, 1.0).unwrap();
11038        assert_eq!(r.shape().dims(), t.shape().dims());
11039        assert_eq!(r.dtype(), t.dtype());
11040        // Every sample must be in [-1, 1).
11041        for v in r.realize_f32() {
11042            assert!((-1.0..1.0).contains(&v), "sample {v} out of [-1, 1)");
11043        }
11044    }
11045
11046    #[test]
11047    fn randn_like_matches_shape_dtype() {
11048        let t = cpu_f32(vec![0.0; 4], &[4]);
11049        let r = t.randn_like(0.0, 1.0).unwrap();
11050        assert_eq!(r.shape().dims(), &[4]);
11051        assert_eq!(r.dtype(), DType::F32);
11052        // Samples are random — just sanity-check they're finite.
11053        for v in r.realize_f32() {
11054            assert!(v.is_finite());
11055        }
11056    }
11057
11058    #[test]
11059    fn static_rand_f32() {
11060        let t = Tensor::rand(vec![100], 0.0, 1.0, DType::F32, &Device::cpu()).unwrap();
11061        let v = t.realize_f32();
11062        // Mean of uniform [0,1) should be ~0.5; tolerate sample noise.
11063        let mean: f32 = v.iter().sum::<f32>() / v.len() as f32;
11064        assert!((0.3..0.7).contains(&mean), "mean {mean} too far from 0.5");
11065    }
11066
11067    #[test]
11068    fn static_randn_f64() {
11069        let t = Tensor::randn(vec![1000], 0.0, 1.0, DType::F64, &Device::cpu()).unwrap();
11070        let v = t.realize_f64();
11071        let mean: f64 = v.iter().sum::<f64>() / v.len() as f64;
11072        // Normal(0,1) mean should be near 0; n=1000 gives stderr ~0.03.
11073        assert!(mean.abs() < 0.2, "mean {mean} too far from 0");
11074    }
11075
11076    #[test]
11077    fn arange_int_step() {
11078        let t = Tensor::arange(0.0, 5.0, &Device::cpu());
11079        assert_eq!(t.shape().dims(), &[5]);
11080        assert_eq!(t.realize_f32(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
11081    }
11082
11083    #[test]
11084    fn arange_step_fractional() {
11085        let t = Tensor::arange_step(2.0, 4.0, 0.5, &Device::cpu());
11086        assert_eq!(t.realize_f32(), vec![2.0, 2.5, 3.0, 3.5]);
11087    }
11088
11089    #[test]
11090    fn arange_step_negative_descends() {
11091        let t = Tensor::arange_step(5.0, 0.0, -1.0, &Device::cpu());
11092        assert_eq!(t.realize_f32(), vec![5.0, 4.0, 3.0, 2.0, 1.0]);
11093    }
11094
11095    #[test]
11096    fn linspace_includes_endpoints() {
11097        let t = Tensor::linspace(0.0, 1.0, 5, &Device::cpu());
11098        assert_eq!(t.shape().dims(), &[5]);
11099        let v = t.realize_f32();
11100        assert!((v[0] - 0.0).abs() < 1e-6);
11101        assert!((v[4] - 1.0).abs() < 1e-6);
11102        assert!((v[2] - 0.5).abs() < 1e-6);
11103    }
11104
11105    #[test]
11106    fn linspace_n_one_returns_start() {
11107        let t = Tensor::linspace(7.0, 99.0, 1, &Device::cpu());
11108        assert_eq!(t.realize_f32(), vec![7.0]);
11109    }
11110
11111    #[test]
11112    fn norm_is_sqrt_sum_sq() {
11113        let t = Tensor::from_f32(vec![3.0_f32, 4.0], vec![2], &Device::cpu()).unwrap();
11114        let n = t.norm();
11115        assert!((n.realize_f32()[0] - 5.0).abs() < 1e-6);
11116    }
11117
11118    #[test]
11119    fn pad_with_zeros_left_and_right() {
11120        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11121        let p = t.pad_with_zeros(0, 2, 1).unwrap();
11122        assert_eq!(p.shape().dims(), &[6]);
11123        assert_eq!(p.realize_f32(), vec![0.0, 0.0, 1.0, 2.0, 3.0, 0.0]);
11124    }
11125
11126    #[test]
11127    fn pad_with_zeros_identity_when_both_zero() {
11128        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11129        let p = t.pad_with_zeros(0, 0, 0).unwrap();
11130        assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11131    }
11132
11133    #[test]
11134    fn pad_with_zeros_rejects_bad_dim() {
11135        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11136        assert!(t.pad_with_zeros(3, 1, 1).is_err());
11137    }
11138
11139    #[test]
11140    fn pad_with_value_zero_matches_pad_with_zeros() {
11141        // pad_with_value(_, _, _, 0.0) must be observationally identical
11142        // to pad_with_zeros — the latter is now a wrapper for the former.
11143        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11144        let via_zeros = t.pad_with_zeros(0, 1, 2).unwrap();
11145        let via_value = t.pad_with_value(0, 1, 2, 0.0).unwrap();
11146        assert_eq!(via_zeros.shape().dims(), via_value.shape().dims());
11147        assert_eq!(via_zeros.shape().dims(), &[5, 2]);
11148        assert_eq!(via_zeros.realize_f32(), via_value.realize_f32());
11149        assert_eq!(
11150            via_value.realize_f32(),
11151            vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0],
11152        );
11153    }
11154
11155    #[test]
11156    fn pad_with_value_neg_inf_then_max_reduce_ignores_pad() {
11157        // Negative interior values: -1, -2, -3. Pad with -inf on both
11158        // sides. Max along dim 0 must be the interior max (-1.0), not
11159        // -inf. This is the load-bearing property for max_pool2d.
11160        let t = cpu_f32(vec![-1.0, -2.0, -3.0], &[3]);
11161        let padded = t.pad_with_value(0, 2, 2, f32::NEG_INFINITY).unwrap();
11162        assert_eq!(padded.shape().dims(), &[7]);
11163        let v = padded.realize_f32();
11164        // Layout: [-inf, -inf, -1, -2, -3, -inf, -inf]
11165        assert!(v[0].is_infinite() && v[0].is_sign_negative());
11166        assert!(v[1].is_infinite() && v[1].is_sign_negative());
11167        assert_eq!(v[2], -1.0);
11168        assert_eq!(v[3], -2.0);
11169        assert_eq!(v[4], -3.0);
11170        assert!(v[5].is_infinite() && v[5].is_sign_negative());
11171        assert!(v[6].is_infinite() && v[6].is_sign_negative());
11172        // max along the only dim drops the pad and returns the interior max.
11173        let m = padded.max_all().realize_f32();
11174        assert_eq!(m, vec![-1.0]);
11175    }
11176
11177    #[test]
11178    fn pad_with_value_identity_when_both_zero() {
11179        // The early-out path must fire regardless of value (no spurious
11180        // graph node when there's nothing to pad).
11181        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11182        let p = t.pad_with_value(0, 0, 0, f32::NEG_INFINITY).unwrap();
11183        assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11184    }
11185
11186    #[test]
11187    fn pad_with_value_rejects_bad_dim() {
11188        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11189        assert!(t.pad_with_value(3, 1, 1, 0.0).is_err());
11190    }
11191
11192    #[test]
11193    fn max_pool2d_with_pad_value_neg_inf_on_negative_interior() {
11194        // Negative-only interior values: a zero-padded max_pool2d would
11195        // incorrectly return 0 in boundary windows. -inf padding gives
11196        // the PyTorch-correct answer (the interior max).
11197        //
11198        // 1x1x3x3 tensor, all values negative:
11199        //   [ -1, -2, -3 ]
11200        //   [ -4, -5, -6 ]
11201        //   [ -7, -8, -9 ]
11202        // With kernel=3, stride=1, padding=1, output is 3x3 where the
11203        // (1,1) center sees the full grid → max = -1.
11204        let x = cpu_f32(
11205            vec![-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0],
11206            &[1, 1, 3, 3],
11207        );
11208        let out = x
11209            .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), f32::NEG_INFINITY)
11210            .unwrap();
11211        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11212        let v = out.realize_f32();
11213        // Top-left corner (0,0): window sees only [(0,0),(0,1),(1,0),(1,1)]
11214        // = [-1,-2,-4,-5]; padded slots are -inf → max = -1.
11215        assert_eq!(v[0], -1.0);
11216        // Center (1,1): no padded slots in window → max of all 9 = -1.
11217        assert_eq!(v[4], -1.0);
11218        // Bottom-right (2,2): window sees [(1,1),(1,2),(2,1),(2,2)]
11219        // = [-5,-6,-8,-9]; padded slots are -inf → max = -5.
11220        assert_eq!(v[8], -5.0);
11221
11222        // KISS-Ops S14: `max_pool2d`'s own default pad value was 0.0
11223        // ("legacy behavior") -- the exact bug this test file demonstrates
11224        // above -- and is now `f32::NEG_INFINITY`. So `max_pool2d` (no
11225        // explicit pad value) must now agree with the NEG_INFINITY variant
11226        // above, not with a zero-padded one; the OLD zero-padded answer is
11227        // only reachable by asking for it explicitly via
11228        // `max_pool2d_with_pad_value(..., 0.0)`, which this block also
11229        // checks, to keep BOTH halves of the fix (new default AND old
11230        // explicit escape hatch) under one assertion.
11231        let default_pad = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11232        let vd = default_pad.realize_f32();
11233        assert_eq!(
11234            vd[0], -1.0,
11235            "max_pool2d's default must now match NEG_INFINITY padding, not 0.0"
11236        );
11237        assert_eq!(vd[8], -5.0);
11238
11239        let explicit_zero_pad = x
11240            .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), 0.0)
11241            .unwrap();
11242        let vz = explicit_zero_pad.realize_f32();
11243        assert_eq!(
11244            vz[0], 0.0,
11245            "explicit 0.0 padding must still be reachable via max_pool2d_with_pad_value"
11246        );
11247        assert_eq!(vz[8], 0.0);
11248    }
11249
11250    #[test]
11251    fn max_pool2d_with_pad_value_zero_matches_max_pool2d() {
11252        // With pad_value = 0.0, the new variant must agree with the
11253        // legacy max_pool2d byte-for-byte.
11254        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11255        let x = cpu_f32(data, &[1, 1, 4, 4]);
11256        let a = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11257        let b = x
11258            .max_pool2d_with_pad_value((2, 2), (2, 2), (0, 0), 0.0)
11259            .unwrap();
11260        assert_eq!(a.shape().dims(), b.shape().dims());
11261        assert_eq!(a.realize_f32(), b.realize_f32());
11262    }
11263
11264    // ---- Phase A.6 conv1d composite tests ----
11265
11266    #[test]
11267    fn conv1d_identity_kernel_passes_input_through() {
11268        // Single-batch, single-channel, kernel-1 identity → output equals input.
11269        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[1, 1, 5]);
11270        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11271        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11272        assert_eq!(out.shape().dims(), &[1, 1, 5]);
11273        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
11274    }
11275
11276    #[test]
11277    fn conv1d_sum_kernel_two_wide() {
11278        // Sum kernel of size 2: out[t] = x[t] + x[t+1].
11279        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11280        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11281        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11282        assert_eq!(out.shape().dims(), &[1, 1, 3]);
11283        assert_eq!(out.realize_f32(), vec![3.0, 5.0, 7.0]);
11284    }
11285
11286    #[test]
11287    fn conv1d_with_bias_applies_correctly() {
11288        let x = cpu_f32(vec![1.0, 1.0, 1.0], &[1, 1, 3]);
11289        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11290        let bias = x.const_f32_like(vec![10.0], vec![1]).unwrap();
11291        let out = x.conv1d(&w, Some(&bias), 1, 0, 1).unwrap();
11292        assert_eq!(out.realize_f32(), vec![11.0, 11.0, 11.0]);
11293    }
11294
11295    #[test]
11296    fn conv1d_stride_two_halves_output() {
11297        // Input length 6, kernel 2, stride 2 → output length (6-2)/2+1 = 3.
11298        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1, 1, 6]);
11299        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11300        let out = x.conv1d(&w, None, 2, 0, 1).unwrap();
11301        assert_eq!(out.shape().dims(), &[1, 1, 3]);
11302        assert_eq!(out.realize_f32(), vec![3.0, 7.0, 11.0]);
11303    }
11304
11305    #[test]
11306    fn conv1d_padding_one_preserves_length() {
11307        // Input length 4, kernel 3, padding 1, stride 1 → output length 4.
11308        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11309        let w = x
11310            .const_f32_like(vec![1.0, 1.0, 1.0], vec![1, 1, 3])
11311            .unwrap();
11312        let out = x.conv1d(&w, None, 1, 1, 1).unwrap();
11313        assert_eq!(out.shape().dims(), &[1, 1, 4]);
11314        // out[0] = 0+x[0]+x[1] = 3; out[1] = x[0]+x[1]+x[2] = 6;
11315        // out[2] = x[1]+x[2]+x[3] = 9; out[3] = x[2]+x[3]+0 = 7
11316        assert_eq!(out.realize_f32(), vec![3.0, 6.0, 9.0, 7.0]);
11317    }
11318
11319    #[test]
11320    fn conv1d_multi_channel_output() {
11321        // 1 batch, 1 in-channel, 3 timesteps; 2 out-channels with kernel 1.
11322        let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11323        // Weight [Cout=2, Cin=1, K=1]: filter 0 = 2.0, filter 1 = -1.0.
11324        let w = x.const_f32_like(vec![2.0, -1.0], vec![2, 1, 1]).unwrap();
11325        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11326        assert_eq!(out.shape().dims(), &[1, 2, 3]);
11327        // Channel 0: 2.0 × input. Channel 1: -1.0 × input.
11328        assert_eq!(out.realize_f32(), vec![2.0, 4.0, 6.0, -1.0, -2.0, -3.0]);
11329    }
11330
11331    #[test]
11332    fn conv1d_rejects_rank_two_input() {
11333        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11334        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11335        assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11336    }
11337
11338    #[test]
11339    fn conv1d_rejects_rank_two_weight() {
11340        let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11341        let w = x.const_f32_like(vec![1.0], vec![1, 1]).unwrap();
11342        assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11343    }
11344
11345    #[test]
11346    fn conv1d_rejects_zero_groups_or_stride() {
11347        let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11348        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11349        assert!(x.conv1d(&w, None, 0, 0, 1).is_err());
11350        assert!(x.conv1d(&w, None, 1, 0, 0).is_err());
11351    }
11352
11353    #[test]
11354    fn conv1d_with_algo_ignores_algo_param() {
11355        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11356        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11357        // Pass a dummy algo (the parameter is ignored on the lazy path).
11358        let a = x.conv1d_with_algo(&w, None, 1, 0, 1, "unused").unwrap();
11359        let b = x.conv1d(&w, None, 1, 0, 1).unwrap();
11360        assert_eq!(a.realize_f32(), b.realize_f32());
11361    }
11362
11363    // ---- Phase A.7 pooling / interpolation composite tests ----
11364
11365    #[test]
11366    fn avg_pool2d_2x2_stride2() {
11367        // 1×1×4×4 input with values 0..15.
11368        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11369        let x = cpu_f32(data, &[1, 1, 4, 4]);
11370        let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11371        assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11372        // Each 2x2 block average: top-left = (0+1+4+5)/4 = 2.5,
11373        // top-right = (2+3+6+7)/4 = 4.5, bottom-left = (8+9+12+13)/4 = 10.5,
11374        // bottom-right = (10+11+14+15)/4 = 12.5.
11375        let v = out.realize_f32();
11376        assert!((v[0] - 2.5).abs() < 1e-5);
11377        assert!((v[1] - 4.5).abs() < 1e-5);
11378        assert!((v[2] - 10.5).abs() < 1e-5);
11379        assert!((v[3] - 12.5).abs() < 1e-5);
11380    }
11381
11382    #[test]
11383    fn avg_pool2d_3x3_stride1_padding1_preserves_size() {
11384        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11385        let out = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11386        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11387    }
11388
11389    /// KISS-Ops §6.13 S14: `count_include_pad = false` must divide each
11390    /// border window by its TRUE valid-tap count, not the uniform `kh·kw`.
11391    /// Fixture: a 1x1x4x4 all-ones input, 3x3 kernel, padding 1.
11392    ///
11393    /// `excluded` (count_include_pad=false) must be EXACTLY 1.0 at every
11394    /// output position, border included: averaging any positive count of
11395    /// real taps that are all 1.0 is always 1.0, regardless of how many
11396    /// padded slots surround them — this is the invariant the fix exists
11397    /// to restore. `included` (count_include_pad=true, today's old
11398    /// behavior) must NOT share that invariant: it divides by the uniform
11399    /// 9 regardless of how many taps were real, so a corner window (only
11400    /// 4 of 9 taps real) computes `4/9 ≈ 0.444`, well below 1.0 — the two
11401    /// modes genuinely disagree at the border, which is the point of the
11402    /// fix. (On fully-interior positions, where every tap is real either
11403    /// way, both modes necessarily agree — not asserted here since it
11404    /// proves nothing about the fix; see the non-uniform fixture below for
11405    /// a sharper per-position contrast.)
11406    #[test]
11407    fn avg_pool2d_excludes_padding_keeps_uniform_data_exactly_at_its_true_value() {
11408        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11409        let included = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11410        let excluded = x.avg_pool2d((3, 3), (1, 1), (1, 1), false).unwrap();
11411        assert_eq!(included.shape().dims(), excluded.shape().dims());
11412
11413        let ve = excluded.realize_f32();
11414        for (i, &v) in ve.iter().enumerate() {
11415            assert!(
11416                (v - 1.0).abs() < 1e-5,
11417                "count_include_pad=false must be exactly 1.0 at every position \
11418                 (all real taps are 1.0) — position {i} got {v}",
11419            );
11420        }
11421
11422        let vi = included.realize_f32();
11423        assert!(
11424            (vi[0] - 1.0).abs() > 0.1,
11425            "count_include_pad=true must NOT match at a corner (4 of 9 taps \
11426             real → 4/9, far from 1.0) — got {}, modes should disagree here",
11427            vi[0]
11428        );
11429    }
11430
11431    /// The actual discriminating case: non-uniform data at a border window.
11432    /// A 1x1x2x2 input `[[10, 10], [10, 10]]`, kernel 2x2, padding 1 (so
11433    /// the TOP-LEFT output position's window is `[[pad,pad],[pad,10]]` --
11434    /// only ONE of four taps is real). count_include_pad=true (today's old
11435    /// behavior) divides by the uniform 4: `10/4 = 2.5`. count_include_pad
11436    /// =false (ONNX/PyTorch default) divides by the TRUE valid count, 1:
11437    /// `10/1 = 10.0` -- the actual average of the real values in the
11438    /// window, which is what "average pooling, excluding padding" means.
11439    #[test]
11440    fn avg_pool2d_excludes_padding_from_divisor_discriminating() {
11441        let x = cpu_f32(vec![10.0, 10.0, 10.0, 10.0], &[1, 1, 2, 2]);
11442        let included = x.avg_pool2d((2, 2), (1, 1), (1, 1), true).unwrap();
11443        let excluded = x.avg_pool2d((2, 2), (1, 1), (1, 1), false).unwrap();
11444        let vi = included.realize_f32();
11445        let ve = excluded.realize_f32();
11446        assert!(
11447            (vi[0] - 2.5).abs() < 1e-5,
11448            "count_include_pad=true: top-left = 10/4 = 2.5, got {}",
11449            vi[0]
11450        );
11451        assert!(
11452            (ve[0] - 10.0).abs() < 1e-5,
11453            "count_include_pad=false: top-left = 10/1 (ONE real tap) = 10.0, got {}",
11454            ve[0]
11455        );
11456    }
11457
11458    /// A 1x1 input with padding >= kernel size: EVERY output window is
11459    /// entirely padding, so the count-include-pad=false divisor would be
11460    /// zero everywhere. Must refuse with a descriptive error rather than
11461    /// let `broadcast_div` emit NaN/inf.
11462    #[test]
11463    fn avg_pool2d_rejects_zero_valid_tap_window() {
11464        let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11465        let err = x
11466            .avg_pool2d((1, 1), (1, 1), (2, 2), false)
11467            .expect_err("a window with zero real taps must be rejected, not silently NaN");
11468        let msg = err.to_string();
11469        assert!(
11470            msg.contains("divide by zero") || msg.contains("no valid"),
11471            "error should explain the zero-valid-tap cause, got: {msg}"
11472        );
11473
11474        // Positive control: count_include_pad=true has no notion of "valid
11475        // taps" (it always divides by kh*kw), so the SAME shape must still
11476        // succeed under that mode -- proving the rejection is specific to
11477        // the zero-valid-tap case, not a blanket refusal of this shape.
11478        let ok = x.avg_pool2d((1, 1), (1, 1), (2, 2), true);
11479        assert!(
11480            ok.is_ok(),
11481            "count_include_pad=true must still accept this shape (uniform divisor, never zero)"
11482        );
11483    }
11484
11485    #[test]
11486    fn avg_pool2d_multi_channel() {
11487        // 1×2×2×2: each channel is filled with its index.
11488        let x = cpu_f32(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], &[1, 2, 2, 2]);
11489        let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11490        assert_eq!(out.shape().dims(), &[1, 2, 1, 1]);
11491        assert_eq!(out.realize_f32(), vec![0.0, 1.0]);
11492    }
11493
11494    #[test]
11495    fn max_pool2d_2x2_stride2() {
11496        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11497        let x = cpu_f32(data, &[1, 1, 4, 4]);
11498        let out = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11499        assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11500        // Each 2x2 block max: 5, 7, 13, 15.
11501        assert_eq!(out.realize_f32(), vec![5.0, 7.0, 13.0, 15.0]);
11502    }
11503
11504    #[test]
11505    fn max_pool2d_3x3_stride1_padding1() {
11506        let x = cpu_f32(
11507            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
11508            &[1, 1, 3, 3],
11509        );
11510        let out = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11511        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11512        // Center should be the global max 9; corners should be the max of their 2×2 window.
11513        let v = out.realize_f32();
11514        // (1,1) center: max of all 9 = 9
11515        assert!((v[4] - 9.0).abs() < 1e-5);
11516    }
11517
11518    #[test]
11519    fn upsample_nearest2d_2x() {
11520        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11521        let out = x.upsample_nearest2d(2).unwrap();
11522        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11523        // Each cell replicated 2×2: rows are [1,1,2,2; 1,1,2,2; 3,3,4,4; 3,3,4,4].
11524        assert_eq!(
11525            out.realize_f32(),
11526            vec![
11527                1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 3.0, 3.0, 4.0, 4.0,
11528            ],
11529        );
11530    }
11531
11532    #[test]
11533    fn upsample_nearest2d_3x() {
11534        let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11535        let out = x.upsample_nearest2d(3).unwrap();
11536        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11537        assert_eq!(out.realize_f32(), vec![5.0; 9]);
11538    }
11539
11540    #[test]
11541    fn upsample_nearest2d_identity_scale_one() {
11542        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11543        let out = x.upsample_nearest2d(1).unwrap();
11544        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
11545    }
11546
11547    #[test]
11548    fn upsample_nearest1d_2x() {
11549        let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11550        let out = x.upsample_nearest1d(2).unwrap();
11551        assert_eq!(out.shape().dims(), &[1, 1, 6]);
11552        assert_eq!(out.realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
11553    }
11554
11555    #[test]
11556    fn interpolate2d_integer_multiple() {
11557        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11558        let out = x.interpolate2d(4, 4).unwrap();
11559        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11560    }
11561
11562    #[test]
11563    fn interpolate2d_accepts_non_integer_ratio() {
11564        // Lifted from "rejects non-integer ratio" — arbitrary
11565        // ratios are now supported via the index_select composite
11566        // (matching the eager UpsampleNearest2D convention). See
11567        // tests/lazy_interpolate2d_oracle.rs for parity checks.
11568        let x = cpu_f32(vec![1.0; 4], &[1, 1, 2, 2]);
11569        let out = x.interpolate2d(3, 4).unwrap();
11570        assert_eq!(out.shape().dims(), &[1, 1, 3, 4]);
11571    }
11572
11573    #[test]
11574    fn interpolate1d_integer_multiple() {
11575        let x = cpu_f32(vec![1.0, 2.0], &[1, 1, 2]);
11576        let out = x.interpolate1d(6).unwrap();
11577        assert_eq!(out.shape().dims(), &[1, 1, 6]);
11578        assert_eq!(out.realize_f32(), vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
11579    }
11580
11581    #[test]
11582    fn pool_rejects_bad_rank() {
11583        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11584        assert!(x.avg_pool2d((2, 2), (2, 2), (0, 0), true).is_err());
11585        assert!(x.max_pool2d((2, 2), (2, 2), (0, 0)).is_err());
11586    }
11587
11588    #[test]
11589    fn pool_rejects_zero_kernel() {
11590        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11591        assert!(x.avg_pool2d((0, 2), (1, 1), (0, 0), true).is_err());
11592        assert!(x.max_pool2d((2, 0), (1, 1), (0, 0)).is_err());
11593    }
11594
11595    // ---- Phase A.8 scope-limited harmonization aliases ----
11596
11597    #[test]
11598    fn numel_matches_elem_count() {
11599        let t = cpu_f32(vec![1.0; 12], &[3, 4]);
11600        assert_eq!(t.numel(), t.elem_count());
11601        assert_eq!(t.numel(), 12);
11602    }
11603
11604    #[test]
11605    fn dim_returns_specific_axis_size() {
11606        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11607        assert_eq!(t.dim(0).unwrap(), 2);
11608        assert_eq!(t.dim(1).unwrap(), 3);
11609        assert_eq!(t.dim(2).unwrap(), 4);
11610        assert!(t.dim(3).is_err());
11611    }
11612
11613    #[test]
11614    fn to_dtype_switches_dtype() {
11615        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11616        let b = t.to_dtype(DType::F64).unwrap();
11617        assert_eq!(b.dtype(), DType::F64);
11618        assert_eq!(b.realize_f64(), vec![1.0, 2.0]);
11619    }
11620
11621    #[test]
11622    fn to_dtype_same_dtype_is_noop() {
11623        let t = cpu_f32(vec![1.0_f32], &[1]);
11624        let b = t.to_dtype(DType::F32).unwrap();
11625        assert_eq!(b.dtype(), DType::F32);
11626    }
11627
11628    #[test]
11629    fn detach_is_identity_on_lazy() {
11630        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11631        let d = t.detach();
11632        assert_eq!(d.realize_f32(), t.realize_f32());
11633    }
11634
11635    #[test]
11636    fn track_op_is_true_on_lazy() {
11637        let t = cpu_f32(vec![0.0], &[1]);
11638        assert!(t.track_op());
11639    }
11640
11641    // ---- Phase A.8a Dim/Dims trait port ergonomics tests ----
11642
11643    #[test]
11644    fn try_permute_accepts_tuple_syntax() {
11645        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11646        // Eager-style tuple permute now works on lazy.
11647        let out = t.permute((2_usize, 0_usize, 1_usize)).unwrap();
11648        assert_eq!(out.shape().dims(), &[4, 2, 3]);
11649    }
11650
11651    #[test]
11652    fn dim_arg_methods_accept_negative_indexing() {
11653        use fuel_ir::D;
11654        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
11655        // squeeze, sum_dim, mean_dim, etc. all accept D::Minus1 now.
11656        let sum_last = t.shape().dims().to_vec(); // just demonstrate compile
11657        assert_eq!(sum_last, vec![2, 3]);
11658        // sum_dim is still Self-returning (Phase A.8b will flip), so test
11659        // a method that already returns Result + dim arg.
11660        let cumsum_last = t.cumsum(D::Minus1).unwrap();
11661        assert_eq!(cumsum_last.shape().dims(), &[2, 3]);
11662    }
11663
11664    #[test]
11665    fn unsqueeze_accepts_dim_trait() {
11666        use fuel_ir::D;
11667        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11668        // Append a new last dim via D::Minus1 (rank-aware negative indexing).
11669        let out = t.unsqueeze(D::Minus1).unwrap();
11670        // The position D::Minus1 in to_index_plus_one is "the very end"
11671        // → output rank 2 with the new dim trailing.
11672        assert_eq!(out.shape().dims().len(), 2);
11673    }
11674}
11675pub struct TokenDataHost {
11676    pub token_ids: fuel_ir::HostBuffer,
11677    pub rope_cos: fuel_ir::HostBuffer,
11678    pub rope_sin: fuel_ir::HostBuffer,
11679    pub mask: fuel_ir::HostBuffer,
11680    pub offset: Option<fuel_ir::HostBuffer>,
11681}
11682
11683/// Same per-token data as [`crate::inference_context::DecodeTokenData`],
11684/// as raw host bytes instead of freshly-uploaded device Arcs — for
11685/// [`fuel_dispatch::pipelined::CapturedDecodeSession::replay_token`]'s
11686/// in-place H2D overwrite of fixed buffers. `LlamaModel`-private.
11687#[cfg(feature = "cuda")]
11688pub struct TokenDataBytes {
11689    pub token_ids: Vec<u8>,
11690    pub rope_cos: Vec<u8>,
11691    pub rope_sin: Vec<u8>,
11692    pub mask: Vec<u8>,
11693    pub offset: Option<Vec<u8>>,
11694}
11695
11696/// D2H a [`fuel_dispatch::pipelined::CapturedDecodeSession::replay_token`]
11697/// result's device-resident output Arc to a host `Vec<f32>`.
11698///
11699/// The capture mechanism deliberately keeps the output device-resident
11700/// INSIDE the capture (a `Copy`/`Move` node is capture-unsafe — see
11701/// `LlamaModel::forward_with_kv_context_captured`'s design note); this
11702/// reads it back OUTSIDE the capture, dispatching on the same
11703/// `BackendStorage` variants every other pipelined D2H site in this
11704/// codebase does (`CudaStorageBytes::to_cpu_bytes` / `CpuStorageBytes::
11705/// bytes`) rather than hand-rolling a new D2H mechanism. Capture is
11706/// f32-only today (project constraint), so the bytes are reinterpreted
11707/// as f32 directly.
11708#[cfg(feature = "cuda")]
11709pub fn captured_output_to_f32(
11710    output: &Arc<std::sync::RwLock<fuel_memory::Storage>>,
11711) -> fuel_ir::error::Result<Vec<f32>> {
11712    use fuel_memory::BackendStorage;
11713    let guard = output.read().map_err(|_| {
11714        fuel_ir::Error::Msg("captured decode output storage lock poisoned".into()).bt()
11715    })?;
11716    let bytes: Vec<u8> = match &guard.inner {
11717        BackendStorage::Cpu(c) => c.bytes().to_vec(),
11718        BackendStorage::Cuda(c) => c.to_cpu_bytes()?,
11719        #[allow(unreachable_patterns)]
11720        other => {
11721            return Err(fuel_ir::Error::Msg(format!(
11722                "captured decode output is BackendStorage::{:?}, expected Cpu or Cuda",
11723                std::mem::discriminant(other),
11724            ))
11725            .bt());
11726        }
11727    };
11728    Ok(bytemuck::cast_slice::<u8, f32>(&bytes).to_vec())
11729}