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), so a
3616    /// fixed set of words exercises the decompose's dequantize-then-matmul
3617    /// path under autograd. Kernel layout: `K = 64`, `group_size = 64`
3618    /// (`zeros_w = 2`), `N = 8`.
3619    #[test]
3620    fn awq_matmul_activation_gradient_matches_finite_difference() {
3621        let x_vals: Vec<f32> = (0..64).map(|k| ((k * 7) % 13) as f32 * 0.1 - 0.6).collect();
3622        let (g, fd) = grad_vs_fd_lowered(&x_vals, &[1, 64], |act| {
3623            let words: Vec<u32> = (0..64u32)
3624                .map(|i| i.wrapping_mul(0x9E37_79B1) ^ 0x1234_5678)
3625                .collect();
3626            let weight = act
3627                .graph_tensor()
3628                .const_u32_like(words, Shape::from_dims(&[8, 8]))
3629                .unwrap();
3630            let zeros = act
3631                .graph_tensor()
3632                .const_u32_like(vec![0x7777_7777; 16], Shape::from_dims(&[8, 2]))
3633                .unwrap();
3634            let scales: Vec<half::f16> = (0..128)
3635                .map(|i| half::f16::from_f32(0.05 + 0.01 * (i % 7) as f32))
3636                .collect();
3637            let scales = act
3638                .const_f16_like(scales, Shape::from_dims(&[8, 16]))
3639                .unwrap();
3640            Tensor::from_graph_tensor(act.graph_tensor().awq_matmul(
3641                &weight,
3642                &zeros,
3643                scales.graph_tensor(),
3644                64,
3645                8,
3646            ))
3647        });
3648        assert_grad_close("awq_matmul d/dact", &g, &fd);
3649    }
3650
3651    #[test]
3652    fn causal_conv1d_weight_and_bias_gradients_match_finite_difference() {
3653        let x_data = vec![0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7];
3654        for use_silu in [false, true] {
3655            // d/dweight: weight is the differentiated leaf.
3656            let (g, fd) = grad_vs_fd(&[0.5, -0.25, 0.75, 0.1, 0.9, -0.4], &[2, 1, 3], |w| {
3657                let x = w
3658                    .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3659                    .unwrap();
3660                let b = w
3661                    .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3662                    .unwrap();
3663                x.causal_conv1d(w, &b, use_silu)
3664            });
3665            assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dw"), &g, &fd);
3666            // d/dbias.
3667            let (g, fd) = grad_vs_fd(&[0.05, -0.05], &[2], |b| {
3668                let x = b
3669                    .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3670                    .unwrap();
3671                let w = b
3672                    .const_f32_like(
3673                        vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3674                        Shape::from_dims(&[2, 1, 3]),
3675                    )
3676                    .unwrap();
3677                x.causal_conv1d(&w, b, use_silu)
3678            });
3679            assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dbias"), &g, &fd);
3680        }
3681    }
3682
3683    #[test]
3684    fn causal_conv1d_input_gradient_matches_finite_difference() {
3685        // x [1, 2, 4] (channels 2, seq 2, kernel 3 -> left-padded length 4).
3686        for use_silu in [false, true] {
3687            let (g, fd) = grad_vs_fd(
3688                &[0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7],
3689                &[1, 2, 4],
3690                |x| {
3691                    let w = x
3692                        .const_f32_like(
3693                            vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3694                            Shape::from_dims(&[2, 1, 3]),
3695                        )
3696                        .unwrap();
3697                    let b = x
3698                        .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3699                        .unwrap();
3700                    x.causal_conv1d(&w, &b, use_silu)
3701                },
3702            );
3703            assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dx"), &g, &fd);
3704        }
3705    }
3706
3707    // Review Fix 2: the seqlen=1 SSM gates above multiply exp(dt*a) by a zero
3708    // initial carry, so `y` is linear in the input and the multi-step recurrent
3709    // gate is never FD-checked for the SSM ops. These seqlen>=2 gates carry a
3710    // NON-ZERO h across steps (a < 0), so d(sum y)/d(early input) flows THROUGH
3711    // the gate exp(dt*a) — the multi-step BPTT the headline SSM deliverable needs.
3712
3713    #[test]
3714    fn selective_scan_seqlen2_bptt_matches_finite_difference() {
3715        let dev = Device::cpu();
3716        // u/delta [batch,seqlen,dim]=[1,2,1]; a [dim,dstate]=[1,1] (< 0, stable gate);
3717        // b/c [batch,seqlen,dstate]=[1,2,1]. loss = sum(y) (ones-seed over [1,2,1]).
3718        let fwd = |u_vals: &[f32]| -> f32 {
3719            let u = Tensor::from_f32(u_vals.to_vec(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3720            let delta = u
3721                .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3722                .unwrap();
3723            let a = u
3724                .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3725                .unwrap();
3726            let b = u
3727                .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3728                .unwrap();
3729            let c = u
3730                .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3731                .unwrap();
3732            u.selective_scan(&delta, &a, &b, &c, false)
3733                .realize_f32()
3734                .iter()
3735                .sum()
3736        };
3737        let u0 = vec![1.0f32, 2.0];
3738        let u = Tensor::from_f32(u0.clone(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3739        let delta = u
3740            .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3741            .unwrap();
3742        let a = u
3743            .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3744            .unwrap();
3745        let b = u
3746            .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3747            .unwrap();
3748        let c = u
3749            .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3750            .unwrap();
3751        let y = u.selective_scan(&delta, &a, &b, &c, false);
3752        let grads = y.inner.backward(); // ones-seed over [1,2,1] == grad of sum(y)
3753        let g_u_id = grads.get(u.graph_tensor()).expect("grad u").id();
3754        let g_u =
3755            crate::pipelined_bridge::realize_one_as::<f32>(&u.inner.graph().clone(), g_u_id, &dev)
3756                .expect("realize");
3757        // FD over u_1 (index 0): d(sum y)/d u_1 = dB + c*exp(dt*a)*dB — the second
3758        // term is the recurrent gate carrying u_1 into y_2.
3759        let h = 1e-3f32;
3760        let mut up = u0.clone();
3761        up[0] += h;
3762        let mut um = u0.clone();
3763        um[0] -= h;
3764        let fd0 = (fwd(&up) - fwd(&um)) / (2.0 * h);
3765        assert!(
3766            (g_u[0] - fd0).abs() < 5e-2,
3767            "selective_scan seqlen2 dL/du_1: autograd {} vs FD {fd0}",
3768            g_u[0]
3769        );
3770        // Non-triviality: the gate contribution makes this strictly > the pure-dB
3771        // seqlen=1 grad — a real, non-vacuous multi-step gradient (~1.19 here).
3772        assert!(
3773            g_u[0].abs() > 0.5,
3774            "recurrent-gate grad must be non-trivial, got {g_u:?}"
3775        );
3776    }
3777
3778    #[test]
3779    fn ssd_chunk_scan_seqlen4_chunk2_bptt_matches_finite_difference() {
3780        let dev = Device::cpu();
3781        // seqlen=4, chunk=2 -> a 2-CHUNK Op::Scan (bound=2): the inter-chunk state
3782        // carry passes exp(dt*a) across chunks. x [b,s,h,hd]=[1,4,1,1]; dt [b,s,h]=[1,4,1];
3783        // a [heads]=[1] (< 0); b/c [b,s,h,state]=[1,4,1,1].
3784        let fwd = |x_vals: &[f32]| -> f32 {
3785            let x =
3786                Tensor::from_f32(x_vals.to_vec(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3787            let dt = x
3788                .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3789                .unwrap();
3790            let a = x
3791                .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3792                .unwrap();
3793            let b = x
3794                .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3795                .unwrap();
3796            let c = x
3797                .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3798                .unwrap();
3799            x.ssd_chunk_scan(&dt, &a, &b, &c, 2)
3800                .realize_f32()
3801                .iter()
3802                .sum()
3803        };
3804        let x0 = vec![1.0f32, 2.0, 3.0, 4.0];
3805        let x = Tensor::from_f32(x0.clone(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3806        let dt = x
3807            .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3808            .unwrap();
3809        let a = x
3810            .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3811            .unwrap();
3812        let b = x
3813            .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3814            .unwrap();
3815        let c = x
3816            .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3817            .unwrap();
3818        let y = x.ssd_chunk_scan(&dt, &a, &b, &c, 2);
3819        let grads = y.inner.backward();
3820        let g_x_id = grads.get(x.graph_tensor()).expect("grad x").id();
3821        let g_x =
3822            crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_x_id, &dev)
3823                .expect("realize");
3824        // FD over x_0 (a chunk-0 input): its contribution to y_3/y_4 (chunk 1)
3825        // rides the cross-chunk gate exp(dt*a) — multi-step BPTT.
3826        let h = 1e-3f32;
3827        let mut xp = x0.clone();
3828        xp[0] += h;
3829        let mut xm = x0.clone();
3830        xm[0] -= h;
3831        let fd0 = (fwd(&xp) - fwd(&xm)) / (2.0 * h);
3832        assert!(
3833            (g_x[0] - fd0).abs() < 5e-2,
3834            "ssd_chunk_scan seqlen4 dL/dx_0: autograd {} vs FD {fd0}",
3835            g_x[0]
3836        );
3837        assert!(
3838            g_x[0].abs() > 0.5,
3839            "cross-chunk-gate grad must be non-trivial, got {g_x:?}"
3840        );
3841    }
3842
3843    // ---- Task 8: general-dimension SSM parity gate (C7 non-regression) ------
3844    //
3845    // The all-1s gap-test fixtures above (Task 6/7) share a single flat order
3846    // across every axis, so they cannot catch an axis transposition in the
3847    // decompose. The tests below run each SSM op at DISTINGUISHING multi-dim
3848    // sizes and assert the `unroll_scan` oracle (over the lowered `Op::Scan`)
3849    // matches the fused kernel element-wise, which is only possible if every
3850    // per-step slice / broadcast / reduce axis in the Task-6/7 decompose lines
3851    // up with the kernel's layout.
3852
3853    /// Walk the lowered graph from `root`, assert NO `Op::Fused(fused_id)`
3854    /// survives the `lowering_only()` pass, and return the `Op::Scan` terminal
3855    /// the decompose emitted (there must be exactly one on the SSM path).
3856    fn find_scan_terminal(
3857        graph: &std::sync::Arc<std::sync::RwLock<fuel_graph::Graph>>,
3858        root: fuel_graph::NodeId,
3859        fused_id: fuel_graph::registry::FusedOpId,
3860    ) -> fuel_graph::NodeId {
3861        use fuel_graph::Op;
3862        let g = graph.read().unwrap();
3863        let mut stack = vec![root];
3864        let mut seen = std::collections::HashSet::new();
3865        let mut scan_id = None;
3866        while let Some(nid) = stack.pop() {
3867            if !seen.insert(nid) {
3868                continue;
3869            }
3870            let node = g.node(nid);
3871            assert!(
3872                !matches!(node.op, Op::Fused(fid, _) if fid == fused_id),
3873                "SSM op must lower to Op::Scan, not remain fused",
3874            );
3875            if matches!(node.op, Op::Scan { .. }) {
3876                scan_id = Some(nid);
3877            }
3878            for &inp in &node.inputs {
3879                stack.push(inp);
3880            }
3881        }
3882        scan_id.expect("an Op::Scan terminal must be present after lowering")
3883    }
3884
3885    /// Reindex a flat row-major `[seqlen, batch, tail]` buffer — the layout
3886    /// `unroll_scan` stacks its per-step ys into (scan axis 0) — to
3887    /// `[batch, seqlen, tail]`, the fused kernel's `y` layout, so the two align
3888    /// element-wise. `tail` is the product of the trailing per-token dims (`dim`
3889    /// for selective_scan; `heads*head_dim` for ssd) — a contiguous block whose
3890    /// internal order is preserved by the batch/seqlen swap.
3891    ///
3892    /// This is done in host code deliberately: the executor treats `Op::Permute`
3893    /// as a metadata-only view (shared bytes + strided layout), so realizing a
3894    /// `Permute` *root* can hand back byte-shared storage still in seqlen-major
3895    /// order — an unreliable comparand. Realizing the contiguous unroll `Concat`
3896    /// (`ys`) and permuting the flat host bytes here is unambiguous.
3897    fn scan_ys_to_fused_layout(ys: &[f32], seqlen: usize, batch: usize, tail: usize) -> Vec<f32> {
3898        assert_eq!(ys.len(), seqlen * batch * tail, "ys layout mismatch");
3899        let mut out = vec![0f32; ys.len()];
3900        for s in 0..seqlen {
3901            for bb in 0..batch {
3902                for t in 0..tail {
3903                    out[(bb * seqlen + s) * tail + t] = ys[(s * batch + bb) * tail + t];
3904                }
3905            }
3906        }
3907        out
3908    }
3909
3910    /// Task 8 (part 1) — `selective_scan` general-dimension parity.
3911    ///
3912    /// **C7 non-regression:** `PipelinedExecutor::realize` dispatches
3913    /// `Op::Fused(SELECTIVE_SCAN)` **directly** to its kernel and never runs a
3914    /// `RuleRegistry` lowering pass, so `decompose`→`Op::Scan` is reached ONLY by
3915    /// the `lowering_only()` verification below — the fused kernel is the
3916    /// executed path *by construction*. The `realize_f32()` leg (a) is the proof
3917    /// the fused kernel still runs (no `O(seqlen)` unroll on the hot path).
3918    ///
3919    /// **Distinguishing dims** batch=2, seqlen=3, dim=2, dstate=2 with EVERY
3920    /// element distinct and asymmetric: `u`/`delta` index `dim`, `b`/`c` index
3921    /// `dstate`, and `a` is a full `[dim,dstate]` gate — so a dim↔dstate
3922    /// broadcast swap multiplies wrong pairs, and because `h` threads across the
3923    /// (order-sensitive) seqlen recurrence, a batch↔seqlen transpose changes the
3924    /// result. The all-1s fixture cannot see either; this can.
3925    #[test]
3926    fn selective_scan_multidim_unroll_matches_fused_kernel() {
3927        use fuel_graph::registry::FusedOps;
3928        let dev = Device::cpu();
3929        let (batch, seqlen, dim, dstate) = (2usize, 3usize, 2usize, 2usize);
3930        // [batch, seqlen, dim] — all distinct, mixed signs.
3931        let u = Tensor::from_f32(
3932            vec![
3933                0.5, -0.3, 1.0, 0.2, -0.5, 0.7, 0.25, 0.9, -0.8, 0.4, 0.6, -0.1,
3934            ],
3935            Shape::from_dims(&[batch, seqlen, dim]),
3936            &dev,
3937        )
3938        .unwrap();
3939        // [batch, seqlen, dim] — positive (delta is a rate); all distinct.
3940        let delta = u
3941            .const_f32_like(
3942                vec![
3943                    0.1, 0.2, 0.3, 0.15, 0.25, 0.4, 0.35, 0.05, 0.2, 0.3, 0.12, 0.28,
3944                ],
3945                Shape::from_dims(&[batch, seqlen, dim]),
3946            )
3947            .unwrap();
3948        // [dim, dstate] — negative (Mamba a<0 → stable gate in (0,1)); distinct.
3949        let a = u
3950            .const_f32_like(
3951                vec![-0.7, -0.3, -0.5, -0.9],
3952                Shape::from_dims(&[dim, dstate]),
3953            )
3954            .unwrap();
3955        // [batch, seqlen, dstate] — distinct.
3956        let b = u
3957            .const_f32_like(
3958                vec![
3959                    1.0, 0.5, 0.25, 0.75, 0.6, 0.2, 0.8, 0.4, 0.3, 0.9, 0.55, 0.15,
3960                ],
3961                Shape::from_dims(&[batch, seqlen, dstate]),
3962            )
3963            .unwrap();
3964        // [batch, seqlen, dstate] — distinct.
3965        let c = u
3966            .const_f32_like(
3967                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],
3968                Shape::from_dims(&[batch, seqlen, dstate]),
3969            )
3970            .unwrap();
3971        let y = u.selective_scan(&delta, &a, &b, &c, /* delta_softplus */ false);
3972
3973        // (a) Fused kernel (executed production path) runs and is well-shaped.
3974        let fused = y.realize_f32();
3975        assert_eq!(fused.len(), batch * seqlen * dim);
3976
3977        // (b) Unroll oracle over the lowered Op::Scan matches the fused kernel.
3978        let graph = y.inner.graph().clone();
3979        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
3980            .optimize_to_fixpoint(&graph, &[y.inner.id()]);
3981        assert_eq!(roots.len(), 1);
3982        let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
3983        let ys = {
3984            let mut g = graph.write().unwrap();
3985            fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
3986                .expect("unroll")
3987                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
3988        };
3989        let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
3990            .expect("realize selective_scan unroll oracle on CPU");
3991        // ys is [seqlen,batch,dim]; fused y is [batch,seqlen,dim].
3992        let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
3993        assert_eq!(oracle.len(), fused.len());
3994        let mut max_drift = 0f32;
3995        for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
3996            let d = (o - f).abs();
3997            max_drift = max_drift.max(d);
3998            // 1e-3 sabotage-calibrated: native-F32 unroll vs F64-accumulate
3999            // kernel. Honest drift here is ~1e-6 (printed); a batch/seqlen or
4000            // dim/dstate transpose diverges by O(0.1..10) — far above 1e-3.
4001            assert!(
4002                d < 1e-3,
4003                "selective_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4004            );
4005        }
4006        eprintln!("selective_scan multidim: max honest drift = {max_drift:e}");
4007    }
4008
4009    /// Task 8 (part 1, softplus) — `selective_scan` with `delta_softplus=true`,
4010    /// the branch unexercised at T=1. `delta` carries mixed-sign values so the
4011    /// stable softplus form `Relu(x)+Log(1+Exp(Neg(Abs(x))))` does real work on
4012    /// both the `x>0` and `x<0` sides before the scan. Same fused-vs-oracle
4013    /// parity as the non-softplus case.
4014    #[test]
4015    fn selective_scan_softplus_multitoken_unroll_matches_fused_kernel() {
4016        use fuel_graph::registry::FusedOps;
4017        let dev = Device::cpu();
4018        let (batch, seqlen, dim, dstate) = (1usize, 3usize, 2usize, 2usize);
4019        let u = Tensor::from_f32(
4020            vec![0.5, -0.3, 1.0, 0.2, -0.5, 0.7],
4021            Shape::from_dims(&[batch, seqlen, dim]),
4022            &dev,
4023        )
4024        .unwrap();
4025        // Mixed signs → softplus exercises max(x,0) AND ln(1+exp(-|x|)).
4026        let delta = u
4027            .const_f32_like(
4028                vec![-0.5, 0.2, 0.8, -1.0, 0.3, -0.2],
4029                Shape::from_dims(&[batch, seqlen, dim]),
4030            )
4031            .unwrap();
4032        let a = u
4033            .const_f32_like(
4034                vec![-0.7, -0.3, -0.5, -0.9],
4035                Shape::from_dims(&[dim, dstate]),
4036            )
4037            .unwrap();
4038        let b = u
4039            .const_f32_like(
4040                vec![1.0, 0.5, 0.25, 0.75, 0.6, 0.2],
4041                Shape::from_dims(&[batch, seqlen, dstate]),
4042            )
4043            .unwrap();
4044        let c = u
4045            .const_f32_like(
4046                vec![2.0, 1.0, 0.5, 1.5, 0.8, 0.3],
4047                Shape::from_dims(&[batch, seqlen, dstate]),
4048            )
4049            .unwrap();
4050        let y = u.selective_scan(&delta, &a, &b, &c, /* delta_softplus */ true);
4051
4052        let fused = y.realize_f32();
4053        assert_eq!(fused.len(), batch * seqlen * dim);
4054
4055        let graph = y.inner.graph().clone();
4056        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4057            .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4058        let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
4059        let ys = {
4060            let mut g = graph.write().unwrap();
4061            fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4062                .expect("unroll")
4063                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
4064        };
4065        let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4066            .expect("realize selective_scan softplus oracle on CPU");
4067        let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
4068        let mut max_drift = 0f32;
4069        for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4070            let d = (o - f).abs();
4071            max_drift = max_drift.max(d);
4072            assert!(
4073                d < 1e-3,
4074                "selective_scan softplus mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4075            );
4076        }
4077        eprintln!("selective_scan softplus: max honest drift = {max_drift:e}");
4078    }
4079
4080    /// Task 8 (part 2) — `ssd_chunk_scan` general-dimension parity.
4081    ///
4082    /// **C7 non-regression** (same construction as part 1): the executor
4083    /// dispatches `Op::Fused(SSD_CHUNK_SCAN)` straight to its kernel; the
4084    /// `decompose`→`Op::Scan` recipe is reached only by `lowering_only()` here.
4085    ///
4086    /// **Distinguishing dims** batch=2, seqlen=4, heads=2, head_dim=2,
4087    /// state_dim=2, chunk_size=2. Per-head `a = [-0.6, -1.0]` (head 0's gate
4088    /// leaking into head 1 flips the decay); `x` (indexes head_dim), `b`/`c`
4089    /// (index state_dim) use three DIFFERENT value ramps, so a head_dim↔state_dim
4090    /// broadcast swap (`bc_x` vs `bc_state`) mixes the wrong axes, and the
4091    /// seqlen recurrence + batch↔seqlen permute are exercised because batch>1.
4092    /// Every element is distinct.
4093    #[test]
4094    fn ssd_chunk_scan_multidim_unroll_matches_fused_kernel() {
4095        use fuel_graph::registry::FusedOps;
4096        let dev = Device::cpu();
4097        let (batch, seqlen, heads, head_dim, state_dim, chunk) =
4098            (2usize, 4usize, 2usize, 2usize, 2usize, 2usize);
4099        // Distinct value ramps (different base + slope per tensor) guarantee no
4100        // accidental symmetry; dt stays positive, a stays negative (stable gate).
4101        let x_data: Vec<f32> = (0..batch * seqlen * heads * head_dim)
4102            .map(|i| 0.20 + 0.05 * i as f32)
4103            .collect();
4104        let dt_data: Vec<f32> = (0..batch * seqlen * heads)
4105            .map(|i| 0.10 + 0.04 * i as f32)
4106            .collect();
4107        let b_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4108            .map(|i| 0.15 + 0.03 * i as f32)
4109            .collect();
4110        let c_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4111            .map(|i| 0.25 + 0.035 * i as f32)
4112            .collect();
4113        let x = Tensor::from_f32(
4114            x_data,
4115            Shape::from_dims(&[batch, seqlen, heads, head_dim]),
4116            &dev,
4117        )
4118        .unwrap();
4119        let dt = x
4120            .const_f32_like(dt_data, Shape::from_dims(&[batch, seqlen, heads]))
4121            .unwrap();
4122        let a = x
4123            .const_f32_like(vec![-0.6f32, -1.0], Shape::from_dims(&[heads]))
4124            .unwrap();
4125        let b = x
4126            .const_f32_like(b_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4127            .unwrap();
4128        let c = x
4129            .const_f32_like(c_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4130            .unwrap();
4131        let y = x.ssd_chunk_scan(&dt, &a, &b, &c, chunk);
4132
4133        // (a) Fused kernel (executed production path) runs and is well-shaped.
4134        let fused = y.realize_f32();
4135        assert_eq!(fused.len(), batch * seqlen * heads * head_dim);
4136
4137        // (b) Unroll oracle over the lowered Op::Scan matches the fused kernel.
4138        let graph = y.inner.graph().clone();
4139        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4140            .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4141        assert_eq!(roots.len(), 1);
4142        let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SSD_CHUNK_SCAN);
4143        let ys = {
4144            let mut g = graph.write().unwrap();
4145            fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4146                .expect("unroll")
4147                .0[0] // GAP-303: selected side is a Vec; emit=All => one stacked ys
4148        };
4149        let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4150            .expect("realize ssd_chunk_scan unroll oracle on CPU");
4151        // ys is [seqlen,batch,heads,head_dim]; fused y is
4152        // [batch,seqlen,heads,head_dim]. tail = heads*head_dim (contiguous).
4153        let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, heads * head_dim);
4154        assert_eq!(oracle.len(), fused.len());
4155        let mut max_drift = 0f32;
4156        for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4157            let d = (o - f).abs();
4158            max_drift = max_drift.max(d);
4159            assert!(
4160                d < 1e-3,
4161                "ssd_chunk_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4162            );
4163        }
4164        eprintln!("ssd_chunk_scan multidim: max honest drift = {max_drift:e}");
4165    }
4166
4167    /// Recipe principle (G2): Nf4Matmul must decompose to a **fused-free**
4168    /// primitive subgraph whose realize matches the fused kernel. RED before
4169    /// the total decompose landed (the self-return left an
4170    /// `Op::Fused(NF4_MATMUL)` opaque island in the base map); GREEN after.
4171    /// Uses the hand-computed two-outputs / two-blocks case (expected `[10,
4172    /// 50]`) shared with `fuel_cpu_backend`'s byte-kernel test, so the
4173    /// indicator-sum codebook + nibble unpack + per-block scale are checked
4174    /// against the same numbers the fused CPU kernel produces.
4175    #[test]
4176    fn nf4_matmul_decompose_matches_kernel() {
4177        use fuel_graph::Op;
4178        use fuel_graph::registry::FusedOps;
4179        let dev = Device::cpu();
4180        // n=2, k=4, block_size=2; w_packed [2, 2] U8, absmax [2, 2] F32.
4181        let weight = crate::nf4::nf4_from_bytes(
4182            vec![247_u8, 247, 127, 127],
4183            vec![1.0_f32, 2.0, 10.0, 20.0],
4184            2,
4185            4,
4186            2,
4187            &dev,
4188        )
4189        .expect("nf4_from_bytes");
4190        let act = Tensor::from_graph_tensor(
4191            weight
4192                .w_packed
4193                .graph_tensor()
4194                .const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
4195                .unwrap(),
4196        );
4197        let y = weight.matmul(&act);
4198
4199        // Decompose explicitly, then realize the primitive subgraph.
4200        let graph = y.inner.graph().clone();
4201        let id = y.inner.id();
4202        let roots =
4203            fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
4204        assert_eq!(roots.len(), 1, "lowering should keep a single root");
4205
4206        // Born-red discriminator: no Op::Fused(NF4_MATMUL) reachable from the
4207        // realized root — a self-returning decompose fails exactly here.
4208        {
4209            let g = graph.read().unwrap();
4210            let mut stack = vec![roots[0]];
4211            let mut seen = std::collections::HashSet::new();
4212            while let Some(nid) = stack.pop() {
4213                if !seen.insert(nid) {
4214                    continue;
4215                }
4216                let node = g.node(nid);
4217                assert!(
4218                    !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::NF4_MATMUL),
4219                    "decomposed graph still contains an Op::Fused(NF4_MATMUL) island",
4220                );
4221                for &inp in &node.inputs {
4222                    stack.push(inp);
4223                }
4224            }
4225        }
4226
4227        let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
4228            .expect("realize decomposed Nf4Matmul on CPU");
4229        assert_eq!(got.len(), 2);
4230        assert!((got[0] - 10.0).abs() < 1e-4, "out 0: {}", got[0]);
4231        assert!((got[1] - 50.0).abs() < 1e-4, "out 1: {}", got[1]);
4232    }
4233
4234    /// Build a one-output fused-op node directly on `anchor`'s graph, lower it
4235    /// to primitives, realize on CPU, and return the F32 values. The decompose
4236    /// parity tests for backward helpers use this — those ops have no public
4237    /// builder (autograd creates them), so the test constructs the
4238    /// `Op::Fused` node by hand.
4239    fn lower_realize_fused(
4240        anchor: &Tensor,
4241        op: fuel_graph::Op,
4242        inputs: Vec<fuel_graph::NodeId>,
4243        shape: Shape,
4244    ) -> Vec<f32> {
4245        let graph = anchor.inner.graph().clone();
4246        let fused_id = {
4247            let mut g = graph.write().unwrap();
4248            g.push(fuel_graph::Node {
4249                op,
4250                inputs,
4251                shape,
4252                dtype: DType::F32,
4253            })
4254        };
4255        let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4256            .optimize_to_fixpoint(&graph, &[fused_id]);
4257        assert_eq!(roots.len(), 1, "lowering keeps a single root");
4258        crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &Device::cpu())
4259            .expect("realize decomposed fused op on CPU")
4260    }
4261
4262    #[test]
4263    fn powi_backward_decompose_matches_reference() {
4264        use fuel_graph::registry::{FusedOpParams, FusedOps};
4265        let dev = Device::cpu();
4266        let exp = 3i32;
4267        let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4268        let up_data = vec![1.0f32, 0.5, 2.0, -1.0];
4269        let shape = Shape::from_dims(&[4]);
4270        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4271        let up = x.const_f32_like(up_data.clone(), shape.clone()).unwrap();
4272        let got = lower_realize_fused(
4273            &x,
4274            fuel_graph::Op::Fused(FusedOps::POWI_BACKWARD, FusedOpParams::PowIBackward { exp }),
4275            vec![x.inner.id(), up.inner.id()],
4276            shape,
4277        );
4278        // grad_x = exp · x^(exp-1) · upstream
4279        let expected: Vec<f32> = x_data
4280            .iter()
4281            .zip(&up_data)
4282            .map(|(&x, &u)| exp as f32 * x.powi(exp - 1) * u)
4283            .collect();
4284        assert_eq!(got.len(), expected.len());
4285        for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4286            assert!(
4287                (g - e).abs() < 1e-4,
4288                "powi_backward at {i}: got {g}, expected {e}"
4289            );
4290        }
4291    }
4292
4293    /// Increment C — `inplace_affine`'s migrated functional value recipe:
4294    /// `decompose` lowers `Op::Fused(INPLACE_AFFINE, {mul, add})` to
4295    /// `AddScalar(add)(MulScalar(mul)(x))`, which realizes on the CPU backend to
4296    /// the plain affine `mul·x + add`. Bit-exact: the affine kernel pre-casts the
4297    /// f64 scalars to f32 and computes `mul_f32 * x + add_f32`, so the two-op
4298    /// `MulScalar`→`AddScalar` composition matches a hand `(mul as f32)*x + (add
4299    /// as f32)` exactly (the intermediate `* 1.0` / `+ 0.0` are IEEE identities).
4300    #[test]
4301    fn inplace_affine_decompose_matches_affine_reference() {
4302        use fuel_graph::registry::{FusedOpParams, FusedOps};
4303        let dev = Device::cpu();
4304        let (mul, add) = (2.5f64, -1.0f64);
4305        let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4306        let shape = Shape::from_dims(&[4]);
4307        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4308        let got = lower_realize_fused(
4309            &x,
4310            fuel_graph::Op::Fused(
4311                FusedOps::INPLACE_AFFINE,
4312                FusedOpParams::InplaceAffine { mul, add },
4313            ),
4314            vec![x.inner.id()],
4315            shape,
4316        );
4317        // functional affine: out = mul·x + add (scalars pre-cast to f32).
4318        let expected: Vec<f32> = x_data
4319            .iter()
4320            .map(|&v| (mul as f32) * v + add as f32)
4321            .collect();
4322        assert_eq!(got.len(), expected.len());
4323        for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4324            assert_eq!(g, e, "inplace_affine at {i}: got {g}, expected {e}");
4325        }
4326    }
4327
4328    #[test]
4329    fn softmax_last_dim_backward_decompose_matches_reference() {
4330        use fuel_graph::registry::{FusedOpParams, FusedOps};
4331        let dev = Device::cpu();
4332        let (rows, cols) = (2usize, 3usize);
4333        // s = a real softmax over the last dim; g = arbitrary upstream.
4334        let logits = [[0.5f32, -1.0, 2.0], [1.0, 0.0, -0.5]];
4335        let mut s_data = Vec::new();
4336        for row in &logits {
4337            let m = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4338            let exps: Vec<f32> = row.iter().map(|&v| (v - m).exp()).collect();
4339            let sum: f32 = exps.iter().sum();
4340            s_data.extend(exps.iter().map(|&e| e / sum));
4341        }
4342        let g_data = vec![1.0f32, -0.5, 0.3, 0.2, 1.5, -1.0];
4343        let shape = Shape::from_dims(&[rows, cols]);
4344        let s = Tensor::from_f32(s_data.clone(), shape.clone(), &dev).unwrap();
4345        let g = s.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4346        let got = lower_realize_fused(
4347            &s,
4348            fuel_graph::Op::Fused(
4349                FusedOps::SOFTMAX_LAST_DIM_BACKWARD,
4350                FusedOpParams::SoftmaxLastDimBackward,
4351            ),
4352            vec![s.inner.id(), g.inner.id()],
4353            shape,
4354        );
4355        // grad_x = s · (g − sum(g·s, last))
4356        let mut expected = vec![0.0f32; rows * cols];
4357        for r in 0..rows {
4358            let dot: f32 = (0..cols)
4359                .map(|c| g_data[r * cols + c] * s_data[r * cols + c])
4360                .sum();
4361            for c in 0..cols {
4362                expected[r * cols + c] = s_data[r * cols + c] * (g_data[r * cols + c] - dot);
4363            }
4364        }
4365        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4366            assert!(
4367                (gv - ev).abs() < 1e-5,
4368                "softmax_bwd at {i}: got {gv}, expected {ev}"
4369            );
4370        }
4371    }
4372
4373    #[test]
4374    fn rms_norm_last_dim_backward_decompose_matches_reference() {
4375        use fuel_graph::registry::{FusedOpParams, FusedOps};
4376        let dev = Device::cpu();
4377        let (rows, cols) = (2usize, 3usize);
4378        let eps = 1e-5f64;
4379        let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4380        let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4381        let shape = Shape::from_dims(&[rows, cols]);
4382        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4383        let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4384        let got = lower_realize_fused(
4385            &x,
4386            fuel_graph::Op::Fused(
4387                FusedOps::RMS_NORM_LAST_DIM_BACKWARD,
4388                FusedOpParams::RmsNormLastDimBackward { eps },
4389            ),
4390            vec![x.inner.id(), g.inner.id()],
4391            shape,
4392        );
4393        // grad_x = r_rms · (g − x·s / (n·(mean_sq + eps)))
4394        let n = cols as f32;
4395        let mut expected = vec![0.0f32; rows * cols];
4396        for r in 0..rows {
4397            let meansq: f32 = (0..cols).map(|c| x_data[r * cols + c].powi(2)).sum::<f32>() / n;
4398            let denom = meansq + eps as f32;
4399            let rrms = 1.0 / denom.sqrt();
4400            let s: f32 = (0..cols)
4401                .map(|c| g_data[r * cols + c] * x_data[r * cols + c])
4402                .sum();
4403            for c in 0..cols {
4404                let term = x_data[r * cols + c] * s / (n * denom);
4405                expected[r * cols + c] = rrms * (g_data[r * cols + c] - term);
4406            }
4407        }
4408        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4409            assert!(
4410                (gv - ev).abs() < 1e-4,
4411                "rms_norm_bwd at {i}: got {gv}, expected {ev}"
4412            );
4413        }
4414    }
4415
4416    #[test]
4417    fn layer_norm_last_dim_backward_decompose_matches_reference() {
4418        use fuel_graph::registry::{FusedOpParams, FusedOps};
4419        let dev = Device::cpu();
4420        let (rows, cols) = (2usize, 3usize);
4421        let eps = 1e-5f64;
4422        let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4423        let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4424        let shape = Shape::from_dims(&[rows, cols]);
4425        let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4426        let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4427        let got = lower_realize_fused(
4428            &x,
4429            fuel_graph::Op::Fused(
4430                FusedOps::LAYER_NORM_LAST_DIM_BACKWARD,
4431                FusedOpParams::LayerNormLastDimBackward { eps },
4432            ),
4433            vec![x.inner.id(), g.inner.id()],
4434            shape,
4435        );
4436        // grad_x = istd · (g − mean(g) − xhat·mean(g·xhat))
4437        let n = cols as f32;
4438        let mut expected = vec![0.0f32; rows * cols];
4439        for r in 0..rows {
4440            let mean_x: f32 = (0..cols).map(|c| x_data[r * cols + c]).sum::<f32>() / n;
4441            let var: f32 = (0..cols)
4442                .map(|c| (x_data[r * cols + c] - mean_x).powi(2))
4443                .sum::<f32>()
4444                / n;
4445            let istd = 1.0 / (var + eps as f32).sqrt();
4446            let xhat: Vec<f32> = (0..cols)
4447                .map(|c| (x_data[r * cols + c] - mean_x) * istd)
4448                .collect();
4449            let mean_g: f32 = (0..cols).map(|c| g_data[r * cols + c]).sum::<f32>() / n;
4450            let mean_gxh: f32 = (0..cols)
4451                .map(|c| g_data[r * cols + c] * xhat[c])
4452                .sum::<f32>()
4453                / n;
4454            for c in 0..cols {
4455                expected[r * cols + c] =
4456                    istd * (g_data[r * cols + c] - mean_g - xhat[c] * mean_gxh);
4457            }
4458        }
4459        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4460            assert!(
4461                (gv - ev).abs() < 1e-4,
4462                "layer_norm_bwd at {i}: got {gv}, expected {ev}"
4463            );
4464        }
4465    }
4466
4467    #[test]
4468    fn reduce_max_to_backward_decompose_matches_reference() {
4469        use fuel_graph::registry::{FusedOpParams, FusedOps};
4470        let dev = Device::cpu();
4471        // row0 has a tie at the max (two 3.0s → share 10/2); row1 unique max.
4472        let x_data = vec![1.0f32, 3.0, 3.0, 2.0, 0.0, 1.0]; // [2,3]
4473        let up_data = vec![10.0f32, 5.0]; // [2,1] — one per reduced row.
4474        let x_shape = Shape::from_dims(&[2, 3]);
4475        let up_shape = Shape::from_dims(&[2, 1]);
4476        let x = Tensor::from_f32(x_data.clone(), x_shape.clone(), &dev).unwrap();
4477        let up = x.const_f32_like(up_data.clone(), up_shape).unwrap();
4478        let got = lower_realize_fused(
4479            &x,
4480            fuel_graph::Op::Fused(
4481                FusedOps::REDUCE_MAX_TO_BACKWARD,
4482                FusedOpParams::ReduceMaxToBackward,
4483            ),
4484            vec![x.inner.id(), up.inner.id()],
4485            x_shape,
4486        );
4487        // row0: max 3.0 tied 2× → 10/2 each; row1: max 2.0 unique → full 5.0.
4488        let expected = vec![0.0f32, 5.0, 5.0, 5.0, 0.0, 0.0];
4489        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4490            assert!(
4491                (gv - ev).abs() < 1e-5,
4492                "reduce_max_bwd at {i}: got {gv}, expected {ev}"
4493            );
4494        }
4495    }
4496
4497    fn causal_conv1d_check(use_silu: bool) {
4498        use fuel_graph::registry::{FusedOpParams, FusedOps};
4499        let dev = Device::cpu();
4500        let (b, c, k, seq) = (1usize, 2usize, 3usize, 2usize);
4501        let x_seq = seq + (k - 1); // caller pre-pads by K-1.
4502        // x [B,C,x_seq], weight [C,1,K], bias [C] — all row-major.
4503        let x_data = vec![0.0f32, 1.0, 2.0, 3.0, 1.0, 0.0, -1.0, -2.0];
4504        let w_data = vec![0.5f32, 1.0, -0.5, 1.0, 0.0, 2.0];
4505        let bias_data = vec![0.1f32, -0.2];
4506        let x = Tensor::from_f32(x_data.clone(), Shape::from_dims(&[b, c, x_seq]), &dev).unwrap();
4507        let w = x
4508            .const_f32_like(w_data.clone(), Shape::from_dims(&[c, 1, k]))
4509            .unwrap();
4510        let bias = x
4511            .const_f32_like(bias_data.clone(), Shape::from_dims(&[c]))
4512            .unwrap();
4513        let got = lower_realize_fused(
4514            &x,
4515            fuel_graph::Op::Fused(
4516                FusedOps::CAUSAL_CONV1D,
4517                FusedOpParams::CausalConv1d { use_silu },
4518            ),
4519            vec![x.inner.id(), w.inner.id(), bias.inner.id()],
4520            Shape::from_dims(&[b, c, seq]),
4521        );
4522        // out[c,t] = Σ_k w[c,k]·x[c,t+k] + bias[c], optional SiLU.
4523        let mut expected = vec![0.0f32; b * c * seq];
4524        for ch in 0..c {
4525            for t in 0..seq {
4526                let mut acc = 0.0f32;
4527                for kk in 0..k {
4528                    acc += w_data[ch * k + kk] * x_data[ch * x_seq + t + kk];
4529                }
4530                acc += bias_data[ch];
4531                if use_silu {
4532                    acc *= 1.0 / (1.0 + (-acc).exp());
4533                }
4534                expected[ch * seq + t] = acc;
4535            }
4536        }
4537        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4538            assert!(
4539                (gv - ev).abs() < 1e-4,
4540                "causal_conv1d (silu={use_silu}) at {i}: got {gv}, expected {ev}",
4541            );
4542        }
4543    }
4544
4545    #[test]
4546    fn causal_conv1d_decompose_matches_reference() {
4547        causal_conv1d_check(false);
4548        causal_conv1d_check(true);
4549    }
4550
4551    #[test]
4552    fn flash_attn_backward_decompose_matches_reference() {
4553        use fuel_graph::registry::{FusedOpParams, FusedOps};
4554        let dev = Device::cpu();
4555        let (sq, sk, d) = (2usize, 2usize, 2usize);
4556        // 4-digit test scale (~1/sqrt(2)), deliberately NOT FRAC_1_SQRT_2 (0.70710678);
4557        // swapping the exact constant would perturb these attention-scale parity tests.
4558        #[allow(clippy::approx_constant)]
4559        let scale = 0.7071f32;
4560        let q_data = vec![0.1f32, -0.2, 0.3, 0.5]; // [Sq,D]
4561        let k_data = vec![0.4f32, 0.1, -0.3, 0.2]; // [Sk,D]
4562        let v_data = vec![1.0f32, 2.0, -1.0, 0.5]; // [Sk,D]
4563        let do_data = vec![0.5f32, -1.0, 0.2, 0.3]; // [Sq,D]
4564        let qshape = Shape::from_dims(&[1, 1, sq, d]);
4565        let kshape = Shape::from_dims(&[1, 1, sk, d]);
4566        let q = Tensor::from_f32(q_data.clone(), qshape.clone(), &dev).unwrap();
4567        let k = q.const_f32_like(k_data.clone(), kshape.clone()).unwrap();
4568        let v = q.const_f32_like(v_data.clone(), kshape.clone()).unwrap();
4569        let dout = q.const_f32_like(do_data.clone(), qshape.clone()).unwrap();
4570        let params = FusedOpParams::FlashAttnBackward {
4571            softmax_scale: scale,
4572            causal: false,
4573            window_size_left: None,
4574            window_size_right: None,
4575            softcap: None,
4576        };
4577        let inputs = vec![q.inner.id(), k.inner.id(), v.inner.id(), dout.inner.id()];
4578        let dq = lower_realize_fused(
4579            &q,
4580            fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_Q, params.clone()),
4581            inputs.clone(),
4582            qshape.clone(),
4583        );
4584        let dk = lower_realize_fused(
4585            &q,
4586            fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_K, params.clone()),
4587            inputs.clone(),
4588            kshape.clone(),
4589        );
4590        let dv = lower_realize_fused(
4591            &q,
4592            fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_V, params),
4593            inputs,
4594            kshape,
4595        );
4596
4597        // --- reference SDPA backward (B=1, H=1) ---
4598        let mut p = vec![0.0f32; sq * sk];
4599        for i in 0..sq {
4600            let mut scores = vec![0.0f32; sk];
4601            for j in 0..sk {
4602                let mut s = 0.0f32;
4603                for l in 0..d {
4604                    s += q_data[i * d + l] * k_data[j * d + l];
4605                }
4606                scores[j] = scale * s;
4607            }
4608            let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4609            let mut sum = 0.0f32;
4610            let e: Vec<f32> = scores
4611                .iter()
4612                .map(|&s| {
4613                    let x = (s - m).exp();
4614                    sum += x;
4615                    x
4616                })
4617                .collect();
4618            for j in 0..sk {
4619                p[i * sk + j] = e[j] / sum;
4620            }
4621        }
4622        // dV[j,l] = Σ_i P[i,j]·dO[i,l]
4623        let mut ref_dv = vec![0.0f32; sk * d];
4624        for j in 0..sk {
4625            for l in 0..d {
4626                ref_dv[j * d + l] = (0..sq).map(|i| p[i * sk + j] * do_data[i * d + l]).sum();
4627            }
4628        }
4629        // dP[i,j] = Σ_l dO[i,l]·v[j,l]
4630        let mut dp = vec![0.0f32; sq * sk];
4631        for i in 0..sq {
4632            for j in 0..sk {
4633                dp[i * sk + j] = (0..d).map(|l| do_data[i * d + l] * v_data[j * d + l]).sum();
4634            }
4635        }
4636        // dScores_raw[i,j] = scale · P[i,j]·(dP[i,j] − Σ_j' dP·P)
4637        let mut dsr = vec![0.0f32; sq * sk];
4638        for i in 0..sq {
4639            let rowdot: f32 = (0..sk).map(|j| dp[i * sk + j] * p[i * sk + j]).sum();
4640            for j in 0..sk {
4641                dsr[i * sk + j] = scale * p[i * sk + j] * (dp[i * sk + j] - rowdot);
4642            }
4643        }
4644        // dQ[i,l] = Σ_j dsr[i,j]·k[j,l] ; dK[j,l] = Σ_i dsr[i,j]·q[i,l]
4645        let mut ref_dq = vec![0.0f32; sq * d];
4646        for i in 0..sq {
4647            for l in 0..d {
4648                ref_dq[i * d + l] = (0..sk).map(|j| dsr[i * sk + j] * k_data[j * d + l]).sum();
4649            }
4650        }
4651        let mut ref_dk = vec![0.0f32; sk * d];
4652        for j in 0..sk {
4653            for l in 0..d {
4654                ref_dk[j * d + l] = (0..sq).map(|i| dsr[i * sk + j] * q_data[i * d + l]).sum();
4655            }
4656        }
4657        let check = |name: &str, got: &[f32], exp: &[f32]| {
4658            assert_eq!(got.len(), exp.len(), "{name} length");
4659            for (i, (&gv, &ev)) in got.iter().zip(exp).enumerate() {
4660                assert!(
4661                    (gv - ev).abs() < 1e-4,
4662                    "{name} at {i}: got {gv}, expected {ev}"
4663                );
4664            }
4665        };
4666        check("dQ", &dq, &ref_dq);
4667        check("dK", &dk, &ref_dk);
4668        check("dV", &dv, &ref_dv);
4669    }
4670
4671    #[test]
4672    fn paged_attn_decompose_matches_reference() {
4673        use fuel_graph::registry::{FusedOpParams, FusedOps};
4674        let dev = Device::cpu();
4675        // B=1, Hq=Hkv=1, Sq=1 (decode), D=2; block_size=2, max_blk=2 → kv_len=4.
4676        let block_size = 2usize;
4677        let scale = 1.0f32;
4678        // k_cache / v_cache: [num_blocks=3, block_size=2, Hkv=1, D=2]. Block 1
4679        // is unused (not in the block table) and holds sentinel 9s.
4680        let kc = vec![
4681            1.0f32, 0.0, 0.0, 1.0, 9.0, 9.0, 9.0, 9.0, 1.0, 1.0, 2.0, 0.0,
4682        ];
4683        let vc = vec![
4684            1.0f32, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0, 6.0, 7.0, 8.0,
4685        ];
4686        let q_data = vec![0.5f32, 0.5]; // [1,1,1,2]
4687        let bt = vec![0u32, 2u32]; // block_table [1,2]: sequence uses blocks 0 and 2
4688        let cl = vec![3u32]; // context_lens [1]: only the first 3 keys are valid
4689
4690        let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, 1, 1, 2]), &dev).unwrap();
4691        let kcache = q
4692            .const_f32_like(kc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4693            .unwrap();
4694        let vcache = q
4695            .const_f32_like(vc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4696            .unwrap();
4697        let block_table = q.const_u32_like(bt, Shape::from_dims(&[1, 2])).unwrap();
4698        let context_lens = q.const_u32_like(cl, Shape::from_dims(&[1])).unwrap();
4699        let params = FusedOpParams::PagedAttn {
4700            softmax_scale: scale,
4701            block_size,
4702            softcap: None,
4703        };
4704        let inputs = vec![
4705            q.inner.id(),
4706            kcache.inner.id(),
4707            vcache.inner.id(),
4708            block_table.inner.id(),
4709            context_lens.inner.id(),
4710        ];
4711        let got = lower_realize_fused(
4712            &q,
4713            fuel_graph::Op::Fused(FusedOps::PAGED_ATTN, params),
4714            inputs,
4715            Shape::from_dims(&[1, 1, 1, 2]),
4716        );
4717
4718        // reference: gather blocks [0, 2] → k_seq/v_seq[4][2]; mask j ≥ 3;
4719        // softmax; weighted sum of v.
4720        let (kv_len, ctx) = (4usize, 3usize);
4721        let mut k_seq = vec![];
4722        let mut v_seq = vec![];
4723        for &blk in &[0usize, 2usize] {
4724            for p in 0..block_size {
4725                let base = (blk * block_size + p) * 2; // Hkv=1, D=2
4726                k_seq.push([kc[base], kc[base + 1]]);
4727                v_seq.push([vc[base], vc[base + 1]]);
4728            }
4729        }
4730        let mut scores = vec![0.0f32; kv_len];
4731        for j in 0..kv_len {
4732            let s = q_data[0] * k_seq[j][0] + q_data[1] * k_seq[j][1];
4733            scores[j] = if j >= ctx {
4734                f32::NEG_INFINITY
4735            } else {
4736                scale * s
4737            };
4738        }
4739        let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4740        let mut sum = 0.0f32;
4741        let e: Vec<f32> = scores
4742            .iter()
4743            .map(|&s| {
4744                let x = (s - m).exp();
4745                sum += x;
4746                x
4747            })
4748            .collect();
4749        let mut expected = vec![0.0f32; 2];
4750        for j in 0..kv_len {
4751            for l in 0..2 {
4752                expected[l] += (e[j] / sum) * v_seq[j][l];
4753            }
4754        }
4755        for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4756            assert!(
4757                (gv - ev).abs() < 1e-4,
4758                "paged_attn at {i}: got {gv}, expected {ev}"
4759            );
4760        }
4761    }
4762
4763    #[test]
4764    #[cfg(feature = "cuda")]
4765    fn cuda_executor_matches_cpu_on_add_mul() {
4766        let a =
4767            Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
4768        let b = a
4769            .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
4770            .unwrap();
4771        let c = a.add(&b).unwrap().mul(&a).unwrap();
4772        let cpu_result = c.realize_f32();
4773        let executor = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4774        let cuda_result = c.realize_f32_cuda(&executor);
4775        assert_eq!(cpu_result, cuda_result);
4776    }
4777
4778    /// END-TO-END SMOKE ONLY — NOT a CUDA-kernel pin. Verifies the lazy
4779    /// realize path produces the NaN-propagating `maximum`/`minimum`
4780    /// convention (pinned 2026-07-08,
4781    /// `docs/architecture/10-decisions-log.md`) and that a CPU realize and
4782    /// a CUDA-device realize of the same graph agree on NaN-ness.
4783    ///
4784    /// CAVEAT (orchestrator sabotage finding, 2026-07-08): cost-based
4785    /// placement may route a tiny elementwise op to CPU on BOTH legs even
4786    /// under `realize_f32_cuda`, so this test does NOT guarantee the CUDA
4787    /// kernel executed — a wrong CUDA binding can pass it. The real
4788    /// CUDA-kernel pin is the direct binding-table invocation test
4789    /// `fuel-dispatch/tests/cuda_dispatch_live.rs::cuda_maximum_minimum_propagate_nan_f32`.
4790    ///
4791    /// Gated `#[cfg(feature = "cuda")]` + `#[ignore]`; skips cleanly if no
4792    /// CUDA device is present. Run:
4793    ///   `cargo test -p fuel-core --features cuda --lib \
4794    ///    maximum_minimum_nan_convention_lazy_realize_smoke -- --ignored --nocapture`
4795    #[test]
4796    #[cfg(feature = "cuda")]
4797    #[ignore = "requires a live CUDA device"]
4798    fn maximum_minimum_nan_convention_lazy_realize_smoke() {
4799        let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4800            Ok(d) => d,
4801            Err(e) => {
4802                return fuel_test_support::hardware::skip(
4803                    fuel_test_support::hardware::Hardware::Cuda,
4804                    fuel_test_support::hardware::Missing::device(format!(
4805                        "CudaDevice::new(0): {e:?}"
4806                    )),
4807                );
4808            }
4809        };
4810        let a = Tensor::from_f32(
4811            vec![f32::NAN, -2.0, 3.0, f32::NAN],
4812            Shape::from_dims(&[4]),
4813            &Device::cpu(),
4814        )
4815        .unwrap();
4816        let b = a
4817            .const_f32_like(vec![1.0, f32::NAN, 2.0, f32::NAN], Shape::from_dims(&[4]))
4818            .unwrap();
4819
4820        let max_lazy = a.maximum(&b).unwrap();
4821        let cpu_max = max_lazy.realize_f32();
4822        let cuda_max = max_lazy.realize_f32_cuda(&cuda);
4823        for i in 0..4 {
4824            assert_eq!(
4825                cpu_max[i].is_nan(),
4826                cuda_max[i].is_nan(),
4827                "maximum[{i}] NaN-ness must match: cpu={}, cuda={}",
4828                cpu_max[i],
4829                cuda_max[i],
4830            );
4831            if !cpu_max[i].is_nan() {
4832                assert_eq!(cpu_max[i], cuda_max[i], "maximum[{i}]");
4833            }
4834        }
4835
4836        let min_lazy = a.minimum(&b).unwrap();
4837        let cpu_min = min_lazy.realize_f32();
4838        let cuda_min = min_lazy.realize_f32_cuda(&cuda);
4839        for i in 0..4 {
4840            assert_eq!(
4841                cpu_min[i].is_nan(),
4842                cuda_min[i].is_nan(),
4843                "minimum[{i}] NaN-ness must match: cpu={}, cuda={}",
4844                cpu_min[i],
4845                cuda_min[i],
4846            );
4847            if !cpu_min[i].is_nan() {
4848                assert_eq!(cpu_min[i], cuda_min[i], "minimum[{i}]");
4849            }
4850        }
4851    }
4852
4853    /// END-TO-END SMOKE ONLY — NOT a CUDA-kernel pin. Verifies the lazy
4854    /// realize path produces the NaN-propagating `relu` convention (torch
4855    /// parity, pinned 2026-07-08, `docs/architecture/10-decisions-log.md`)
4856    /// and that a CPU realize and a CUDA-device realize of the same graph
4857    /// agree on NaN-ness. Successor of the transitional-divergence pin
4858    /// `relu_cuda_still_scrubs_nan_pending_alpha76_rebind`, flipped at the
4859    /// alpha.76 `unary_relu_propagating_*` rebind.
4860    ///
4861    /// CAVEAT (orchestrator sabotage finding, 2026-07-08): cost-based
4862    /// placement may route a tiny elementwise op to CPU on BOTH legs even
4863    /// under `realize_f32_cuda`, so this test does NOT guarantee the CUDA
4864    /// kernel executed — it stayed green with the CUDA binding deliberately
4865    /// reverted to the scrubbing stem. The real CUDA-kernel pin (verified
4866    /// born-red against that sabotage) is the direct binding-table
4867    /// invocation test
4868    /// `fuel-dispatch/tests/cuda_dispatch_live.rs::cuda_relu_propagates_nan_f32`
4869    /// (+ its bf16 sibling).
4870    ///
4871    /// Gated `#[cfg(feature = "cuda")]` + `#[ignore]`; skips cleanly if no
4872    /// CUDA device is present. Run:
4873    ///   `cargo test -p fuel-core --features cuda --lib \
4874    ///    relu_nan_convention_lazy_realize_smoke -- --ignored --nocapture`
4875    #[test]
4876    #[cfg(feature = "cuda")]
4877    #[ignore = "requires a live CUDA device"]
4878    fn relu_nan_convention_lazy_realize_smoke() {
4879        let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4880            Ok(d) => d,
4881            Err(e) => {
4882                return fuel_test_support::hardware::skip(
4883                    fuel_test_support::hardware::Hardware::Cuda,
4884                    fuel_test_support::hardware::Missing::device(format!(
4885                        "CudaDevice::new(0): {e:?}"
4886                    )),
4887                );
4888            }
4889        };
4890        let a = Tensor::from_f32(
4891            vec![f32::NAN, -2.0, 3.0],
4892            Shape::from_dims(&[3]),
4893            &Device::cpu(),
4894        )
4895        .unwrap();
4896        let relu_lazy = a.relu();
4897        let cpu_result = relu_lazy.realize_f32();
4898        let cuda_result = relu_lazy.realize_f32_cuda(&cuda);
4899
4900        for i in 0..3 {
4901            assert_eq!(
4902                cpu_result[i].is_nan(),
4903                cuda_result[i].is_nan(),
4904                "relu[{i}] NaN-ness must match: cpu={}, cuda={}",
4905                cpu_result[i],
4906                cuda_result[i],
4907            );
4908            if !cpu_result[i].is_nan() {
4909                assert_eq!(cpu_result[i], cuda_result[i], "relu[{i}]");
4910            }
4911        }
4912    }
4913
4914    #[test]
4915    #[cfg(feature = "cuda")]
4916    fn cuda_executor_matches_cpu_on_matmul() {
4917        let a = Tensor::from_f32(
4918            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4919            Shape::from_dims(&[2, 3]),
4920            &Device::cpu(),
4921        )
4922        .unwrap();
4923        let b = a
4924            .const_f32_like(
4925                vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0],
4926                Shape::from_dims(&[3, 2]),
4927            )
4928            .unwrap();
4929        let c = a.matmul(&b).unwrap();
4930        let cpu = c.realize_f32();
4931        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4932        let cuda = c.realize_f32_cuda(&exe);
4933        assert_eq!(cpu.len(), cuda.len());
4934        for (i, (a, b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4935            assert!((a - b).abs() < 1e-3, "matmul[{i}]: cpu={a}, cuda={b}",);
4936        }
4937    }
4938
4939    #[test]
4940    #[cfg(feature = "cuda")]
4941    fn cuda_executor_matches_cpu_on_broadcast_matmul() {
4942        // Rank-3 × rank-2 matmul (what the transformer forward does).
4943        // The graph auto-broadcasts the rank-2 to rank-3.
4944        let x = Tensor::from_f32(
4945            (0..12).map(|i| i as f32 * 0.1).collect::<Vec<_>>(),
4946            Shape::from_dims(&[1, 3, 4]),
4947            &Device::cpu(),
4948        )
4949        .unwrap();
4950        let w = x
4951            .const_f32_like(
4952                (0..8).map(|i| i as f32 * 0.2).collect::<Vec<_>>(),
4953                Shape::from_dims(&[4, 2]),
4954            )
4955            .unwrap();
4956        let y = x.matmul(&w).unwrap();
4957        let cpu = y.realize_f32();
4958        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4959        let cuda = y.realize_f32_cuda(&exe);
4960        assert_eq!(cpu.len(), cuda.len());
4961        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4962            assert!((a - b).abs() < 1e-3, "bcast_mm[{i}]: cpu={a}, cuda={b}");
4963        }
4964    }
4965
4966    #[test]
4967    #[cfg(feature = "cuda")]
4968    fn cuda_executor_matches_cpu_on_permute() {
4969        let x = Tensor::from_f32(
4970            (0..24).map(|i| i as f32).collect::<Vec<_>>(),
4971            Shape::from_dims(&[1, 2, 3, 4]),
4972            &Device::cpu(),
4973        )
4974        .unwrap();
4975        let y = x.permute([0, 2, 1, 3_usize]).unwrap();
4976        let cpu = y.realize_f32();
4977        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4978        let cuda = y.realize_f32_cuda(&exe);
4979        assert_eq!(cpu, cuda, "permute mismatch");
4980    }
4981
4982    #[test]
4983    #[cfg(feature = "cuda")]
4984    fn cuda_executor_matches_cpu_on_softmax() {
4985        let x = Tensor::from_f32(
4986            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4987            Shape::from_dims(&[2, 3]),
4988            &Device::cpu(),
4989        )
4990        .unwrap();
4991        let y = x.softmax_last_dim().unwrap();
4992        let cpu = y.realize_f32();
4993        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4994        let cuda = y.realize_f32_cuda(&exe);
4995        assert_eq!(cpu.len(), cuda.len());
4996        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4997            assert!((a - b).abs() < 1e-4, "softmax[{i}]: cpu={a}, cuda={b}");
4998        }
4999    }
5000
5001    /// Phase 7.5 PR 3 / 3.5 live CUDA equivalence: realize a graph with
5002    /// `Op::SoftmaxLastDim` through the rule-registry pipeline using
5003    /// `RuleRegistry::lowering_only()` so the executor sees the
5004    /// 7-node lowered subgraph instead of the fused op. The composed
5005    /// CUDA execution path (ReduceMaxTo + BroadcastTo + Sub + Exp +
5006    /// ReduceSumTo + BroadcastTo + Div) must match the fused CPU
5007    /// baseline within tight epsilon.
5008    ///
5009    /// Post PR-3.5 follow-up: ReduceMaxTo / ReduceSumTo run natively
5010    /// on CUDA via the legacy executor's `Op::ReduceXxxTo` arm (which
5011    /// delegates to `backend.reduce` and relabels the result to the
5012    /// keepdim shape), so the lowered subgraph stays GPU-resident
5013    /// end-to-end. Two D2H/H2D round-trips per softmax used to be
5014    /// the cost on the prior commit — both gone now.
5015    #[test]
5016    #[cfg(feature = "cuda")]
5017    fn cuda_executor_matches_cpu_on_softmax_via_lowering() {
5018        // Use a non-trivial input shape so the broadcast paths and
5019        // the ReduceSumTo step both have actual work to do.
5020        let n = 24;
5021        let last = 5;
5022        let data: Vec<f32> = (0..n * last)
5023            .map(|i| ((i as f32) * 0.13).sin() * 2.0 - 0.7)
5024            .collect();
5025        let x = Tensor::from_f32(data, Shape::from_dims(&[n, last]), &Device::cpu()).unwrap();
5026        let y = x.softmax_last_dim().unwrap();
5027
5028        // CPU baseline: fused SoftmaxLastDim through the standard
5029        // realize_f32 path (no rule-registry pipeline involved).
5030        let cpu = y.realize_f32();
5031
5032        // CUDA via the lowered subgraph: run the lowering-only
5033        // rule registry to fixpoint first so fusion can't re-collapse
5034        // the lowered pattern back to Op::SoftmaxLastDim. Then
5035        // realize the remapped target via PipelinedExecutor.
5036        // (Phase 7.6 step 9c E.2: optimizer is caller-composed.)
5037        let graph = y.inner.graph().clone();
5038        let registry = fuel_graph::opt::RuleRegistry::lowering_only();
5039        let remapped = registry.optimize_to_fixpoint(&graph, &[y.inner.id()]);
5040        let dev = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5041        let fc_device: crate::Device = dev.clone().into();
5042        let cuda = crate::pipelined_bridge::realize_one_as::<f32>(&graph, remapped[0], &fc_device)
5043            .expect("realize lowered softmax on CUDA");
5044
5045        assert_eq!(cpu.len(), cuda.len());
5046        let mut max_abs_err = 0.0_f32;
5047        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5048            let err = (a - b).abs();
5049            if err > max_abs_err {
5050                max_abs_err = err;
5051            }
5052            assert!(
5053                err < 1e-5,
5054                "lowered softmax[{i}]: cpu={a} (fused), cuda={b} (composed), err={err}",
5055            );
5056        }
5057        eprintln!("max_abs_err over lowered-vs-fused softmax: {max_abs_err:.3e}");
5058    }
5059
5060    #[test]
5061    #[cfg(feature = "cuda")]
5062    fn cuda_executor_matches_cpu_on_concat_slice() {
5063        let a = Tensor::from_f32(
5064            vec![1.0, 2.0, 3.0, 4.0],
5065            Shape::from_dims(&[2, 2]),
5066            &Device::cpu(),
5067        )
5068        .unwrap();
5069        let b = a
5070            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], Shape::from_dims(&[2, 2]))
5071            .unwrap();
5072        let cat = a.concat(&b, 1).unwrap(); // [2, 4]
5073        let sliced = cat.slice(1, 1, 2).unwrap(); // [2, 2]
5074        let cpu = sliced.realize_f32();
5075        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5076        let cuda = sliced.realize_f32_cuda(&exe);
5077        assert_eq!(cpu, cuda, "concat+slice mismatch");
5078    }
5079
5080    #[test]
5081    #[cfg(feature = "cuda")]
5082    fn cuda_executor_matches_cpu_on_rms_norm() {
5083        let x = Tensor::from_f32(
5084            (0..8).map(|i| i as f32 * 0.5 - 1.5).collect::<Vec<_>>(),
5085            Shape::from_dims(&[2, 4]),
5086            &Device::cpu(),
5087        )
5088        .unwrap();
5089        let y = x.rms_norm_last_dim(1e-5).unwrap();
5090        let cpu = y.realize_f32();
5091        let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5092        let cuda = y.realize_f32_cuda(&exe);
5093        assert_eq!(cpu.len(), cuda.len());
5094        for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5095            assert!((a - b).abs() < 1e-3, "rms_norm[{i}]: cpu={a}, cuda={b}");
5096        }
5097    }
5098
5099    #[test]
5100    fn realize_f64_through_bridge() {
5101        let a =
5102            Tensor::from_f64(vec![1.5, 2.5, 3.5], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
5103        let b = a.mul(&a).unwrap();
5104        assert_eq!(b.realize_f64(), vec![2.25, 6.25, 12.25]);
5105    }
5106
5107    /// GAP-327 regression test (was the born-red probe; now LIVE — the guard has landed).
5108    ///
5109    /// The realize funnel `pipelined_bridge::extract_cpu_bytes_typed` guards the byte
5110    /// reinterpretation on the root's dtype: `realize_one_as::<T>` returns
5111    /// `Error::UnexpectedDType` on a mismatch, and the typed `realize_fNN` accessors (which
5112    /// `.expect` that result) panic. The BYTE VIEW `realize_one_bytes` is the deliberate
5113    /// UNGUARDED escape hatch and still returns raw bytes for any dtype. Measured by probe
5114    /// (GAP-327) before the fix: realize_f32()/realize_one_as::<f32> on an F64 [1.0, 2.0]
5115    /// root returned Ok([0.0, 1.875, 0.0, 2.0]) — silent reinterpretation; this asserts that
5116    /// is now rejected while the byte view is not. See docs/gaps.md GAP-327.
5117    #[test]
5118    fn gap327_realize_f32_on_f64_root_must_reject_not_reinterpret() {
5119        let t =
5120            Tensor::from_f64(vec![1.0_f64, 2.0], Shape::from_dims(&[2]), &Device::cpu()).unwrap();
5121        let graph = t.inner.graph().clone();
5122        let target = t.inner.id();
5123        let device = Device::cpu();
5124
5125        // Silence the panic hook only around the catch_unwind measurements; restore it
5126        // before the assertions so a real failure still prints.
5127        let prev_hook = std::panic::take_hook();
5128        std::panic::set_hook(Box::new(|_| {}));
5129        // Control 1 (RIGHT dtype): realize_f64 on the F64 root succeeds.
5130        let ctl_f64 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f64()));
5131        // Control 2 (guard FIRES): a MISMATCHED guarded accessor panics — proves the harness
5132        // observes guards, so the subject rejection below is real and not a swallowed nothing.
5133        let ctl_f16 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f16()));
5134        // SUBJECT A: realize_f32 on the F64 root must now panic (guard -> Err -> .expect).
5135        let subj_method =
5136            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f32()));
5137        std::panic::set_hook(prev_hook);
5138
5139        // SUBJECT B: realize_one_as::<f32> must return Err(UnexpectedDType) (matched by variant).
5140        let subj_raw = crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &device);
5141        // BYTE VIEW: the unguarded escape hatch must still return the root's RAW bytes
5142        // (2 x f64 = 16 bytes) — a dtype mismatch is NOT an error for the byte view.
5143        let bytes_view = crate::pipelined_bridge::realize_one_bytes(&graph, target, &device);
5144
5145        assert!(
5146            ctl_f64.is_ok(),
5147            "control: realize_f64 on an F64 root must succeed; got {ctl_f64:?}"
5148        );
5149        assert!(
5150            ctl_f16.is_err(),
5151            "positive control: realize_f16() on an F64 root must panic (guard fires); if it \
5152             does not, the harness is not observing panics and the subject asserts are vacuous"
5153        );
5154        assert!(
5155            subj_method.is_err(),
5156            "realize_f32() on an F64 root must REJECT (panic), not silently reinterpret bytes"
5157        );
5158        assert!(
5159            matches!(subj_raw, Err(fuel_ir::Error::UnexpectedDType { .. })),
5160            "realize_one_as::<f32> on an F64 root must return Err(UnexpectedDType), not \
5161             Ok(reinterpreted bytes); got {subj_raw:?}"
5162        );
5163        assert!(
5164            matches!(&bytes_view, Ok(b) if b.len() == 16),
5165            "byte view realize_one_bytes on an F64 [1.0, 2.0] root must return 16 raw bytes \
5166             (unguarded), not an error; got {bytes_view:?}"
5167        );
5168    }
5169
5170    #[test]
5171    fn lazy_tensor_mini_llama_block_forward() {
5172        // A minimal LLaMA-style attention-only "block" built entirely
5173        // through Tensor. No training, just the forward pass:
5174        //
5175        //   h = x + (RmsNorm(x) @ W_qkv → split Q/K/V → RoPE → attention → out proj)
5176        //
5177        // This is the sanity check that every LLaMA primitive is
5178        // reachable through the bridge's API, and that the bridge's
5179        // realize_f32 call actually runs the whole thing.
5180        let seq = 3;
5181        let d_head = 4; // must be even for RoPE
5182        let num_heads = 2;
5183        let d_model = num_heads * d_head; // 8
5184
5185        // Fake input: [1, seq, d_model]
5186        let x_data: Vec<f32> = (0..seq * d_model).map(|i| i as f32 * 0.01).collect();
5187        let x =
5188            Tensor::from_f32(x_data, Shape::from_dims(&[1, seq, d_model]), &Device::cpu()).unwrap();
5189
5190        // Fake weights (just identities for simplicity — makes the
5191        // test easy to verify output finiteness without needing to
5192        // hand-compute).
5193        let w_q = x
5194            .const_f32_like(
5195                identity_matrix(d_model),
5196                Shape::from_dims(&[d_model, d_model]),
5197            )
5198            .unwrap();
5199        let w_k = x
5200            .const_f32_like(
5201                identity_matrix(d_model),
5202                Shape::from_dims(&[d_model, d_model]),
5203            )
5204            .unwrap();
5205        let w_v = x
5206            .const_f32_like(
5207                identity_matrix(d_model),
5208                Shape::from_dims(&[d_model, d_model]),
5209            )
5210            .unwrap();
5211        let w_o = x
5212            .const_f32_like(
5213                identity_matrix(d_model),
5214                Shape::from_dims(&[d_model, d_model]),
5215            )
5216            .unwrap();
5217
5218        // RmsNorm → Q/K/V projection (auto-broadcasting matmul).
5219        let x_norm = x.rms_norm_last_dim(1e-6).unwrap();
5220        let q = x_norm.matmul(&w_q).unwrap();
5221        let k = x_norm.matmul(&w_k).unwrap();
5222        let v = x_norm.matmul(&w_v).unwrap();
5223
5224        // Split heads: [1, seq, 8] → [1, seq, 2, 4] → [1, 2, seq, 4]
5225        let q_h = q
5226            .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5227            .unwrap()
5228            .permute([0, 2, 1, 3_usize])
5229            .unwrap();
5230        let k_h = k
5231            .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5232            .unwrap()
5233            .permute([0, 2, 1, 3_usize])
5234            .unwrap();
5235        let v_h = v
5236            .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5237            .unwrap()
5238            .permute([0, 2, 1, 3_usize])
5239            .unwrap();
5240
5241        // RoPE on Q and K.
5242        let q_r = q_h.rope(10000.0, 0).unwrap();
5243        let k_r = k_h.rope(10000.0, 0).unwrap();
5244
5245        // Scaled dot-product attention.
5246        let k_t = k_r.transpose().unwrap();
5247        let scores = q_r.matmul(&k_t).unwrap();
5248        let attn = scores.softmax_last_dim().unwrap();
5249        let attn_v = attn.matmul(&v_h).unwrap();
5250
5251        // Merge heads + output projection.
5252        let merged = attn_v
5253            .permute([0, 2, 1, 3_usize])
5254            .unwrap()
5255            .reshape(Shape::from_dims(&[1, seq, d_model]))
5256            .unwrap();
5257        let attn_out = merged.matmul(&w_o).unwrap();
5258        let h = x.add(&attn_out).unwrap();
5259
5260        // Realize end-to-end through the bridge.
5261        let result = h.realize_f32();
5262        assert_eq!(result.len(), seq * d_model);
5263        for &v in &result {
5264            assert!(
5265                v.is_finite(),
5266                "bridge-based LLaMA block output non-finite: {v}"
5267            );
5268        }
5269    }
5270
5271    /// Build an identity matrix of size `n × n` in row-major layout.
5272    fn identity_matrix(n: usize) -> Vec<f32> {
5273        let mut out = vec![0.0_f32; n * n];
5274        for i in 0..n {
5275            out[i * n + i] = 1.0;
5276        }
5277        out
5278    }
5279}
5280
5281// Helper method on the wrapper that we didn't include above because the
5282// main struct's `impl` block was getting long. Kept in its own small
5283// `impl` for readability.
5284impl Tensor {
5285    /// Build a second const U32 (index) tensor on the same graph.
5286    pub fn const_u32_like(
5287        &self,
5288        data: impl Into<Arc<[u32]>>,
5289        shape: impl Into<Shape>,
5290    ) -> std::result::Result<Self, fuel_ir::Error> {
5291        Ok(Self {
5292            inner: self.inner.const_u32_like(data, shape)?,
5293        })
5294    }
5295
5296    /// Push a [`fuel_graph::Op::Const`] node on the same graph as
5297    /// `self` **without** populating the graph's storage_map. The
5298    /// caller binds the storage Arc into the realize call via
5299    /// [`InferenceContext::insert`](crate::inference_context::InferenceContext::insert).
5300    ///
5301    /// Used by the Phase E.3.3 forward path to bind pre-allocated
5302    /// KV-cache storage Arcs (`Arc<RwLock<fuel_memory::Storage>>`)
5303    /// into a per-step graph — the graph's legacy storage_map only
5304    /// holds `fuel_backend_contract::Storage`, so direct binding isn't
5305    /// possible without a type conversion.
5306    pub fn const_placeholder_like(&self, shape: impl Into<Shape>, dtype: fuel_ir::DType) -> Self {
5307        Self {
5308            inner: self.inner.const_placeholder_like(shape, dtype),
5309        }
5310    }
5311
5312    /// Append an [`fuel_graph::Op::WriteSlice`] node. Copies `source`'s
5313    /// bytes into `self` at the rectangular slab defined by `ranges`
5314    /// and returns a tensor whose Storage Arc is `self`'s — i.e. the
5315    /// post-write reference to the same underlying buffer.
5316    ///
5317    /// Destructive on `self`: after the write, downstream consumers
5318    /// must read the bytes through the returned tensor's NodeId, not
5319    /// `self`'s.
5320    ///
5321    /// **Returns `Result`**: rank/shape/range mismatches surface as a
5322    /// typed error.
5323    pub fn write_slice(&self, source: &Self, ranges: Vec<(usize, usize)>) -> crate::Result<Self> {
5324        let inner = self.inner.write_slice(&source.inner, ranges)?;
5325        Ok(Self { inner })
5326    }
5327
5328    /// Append an [`fuel_graph::Op::WriteSlice`] whose start on `dyn_axis`
5329    /// is a **runtime** value resolved through the per-pass `SymEnv` at
5330    /// realize (Phase D symbolic extents). `ranges[dyn_axis].0` is
5331    /// ignored (the start is dynamic); the slab width
5332    /// `ranges[dyn_axis].1 - ranges[dyn_axis].0` must equal `source`'s
5333    /// `dyn_axis` dim and not exceed the destination capacity. Backs the
5334    /// persistent decode KV-cache write at the per-token `cached_len`.
5335    pub fn write_slice_dyn(
5336        &self,
5337        source: &Self,
5338        ranges: Vec<(usize, usize)>,
5339        dyn_axis: usize,
5340        offset: fuel_ir::DynScalar,
5341    ) -> crate::Result<Self> {
5342        let inner = self
5343            .inner
5344            .write_slice_dyn(&source.inner, ranges, dyn_axis, offset)?;
5345        Ok(Self { inner })
5346    }
5347
5348    /// Append an [`fuel_graph::Op::WriteSliceRotating`] node — like
5349    /// [`Self::write_slice`] but the `axis` axis wraps modulo
5350    /// `modulus`. `position` is a rank-0 U32 tensor whose value (read
5351    /// at realize time) is wrapped modulo `modulus` to determine the
5352    /// dynamic write start on `axis`. `ranges[axis].0` is ignored
5353    /// (the rotating-axis start is dynamic); the slab width
5354    /// `ranges[axis].1 - ranges[axis].0` must equal `source`'s
5355    /// `axis` dim and must not exceed `modulus`.
5356    ///
5357    /// Destructive on `self`: same scheduling as `write_slice`.
5358    /// Backs sliding-window KV caches (Mistral / Phi-3 sliding-
5359    /// window). Returns `Result`: rank / dtype / axis-bound /
5360    /// modulus / range mismatches surface as typed errors at
5361    /// build time.
5362    pub fn write_slice_rotating(
5363        &self,
5364        source: &Self,
5365        position: &Self,
5366        axis: usize,
5367        modulus: usize,
5368        ranges: Vec<(usize, usize)>,
5369    ) -> crate::Result<Self> {
5370        let inner = self.inner.write_slice_rotating(
5371            &source.inner,
5372            &position.inner,
5373            axis,
5374            modulus,
5375            ranges,
5376        )?;
5377        Ok(Self { inner })
5378    }
5379
5380    /// Append a [`fuel_graph::Op::WriteSliceDoff`] node — write `source`
5381    /// into a slab of `self` whose start on `axis` is read from `offset`
5382    /// (a rank-0 `I64` tensor) **device-side** at kernel launch. No
5383    /// modulo wrap: `offset` is the raw start on `axis`.
5384    /// `ranges[axis].0` is ignored; `ranges[axis].1 - ranges[axis].0`
5385    /// is the write width on `axis` and must equal `source`'s `axis`
5386    /// dim. Bounds (`offset + width <= dest_dims[axis]`) are the
5387    /// caller's contract (device-only at build). Destructive on `self`;
5388    /// same scheduling as `write_slice`. Backs the CUDA-graph-
5389    /// capturable KV-cache append (CapturedRun / `DecodeSession`).
5390    /// Returns `Result`: rank / dtype / axis-bound / static-width /
5391    /// offset-dtype mismatches surface as typed errors at build time.
5392    pub fn write_slice_doff(
5393        &self,
5394        source: &Self,
5395        offset: &Self,
5396        axis: usize,
5397        ranges: Vec<(usize, usize)>,
5398    ) -> crate::Result<Self> {
5399        let inner = self
5400            .inner
5401            .write_slice_doff(&source.inner, &offset.inner, axis, ranges)?;
5402        Ok(Self { inner })
5403    }
5404
5405    /// Append a [`fuel_graph::Op::Fused`] node carrying
5406    /// [`fuel_graph::registry::FusedOpParams::Conv2D`]. See `fuel_graph`'s
5407    /// `Tensor::conv2d` for the full shape contract: `self` must be
5408    /// `[N, Cin, H, W]`; `weight` must be `[Cout, Cin/groups, Kh, Kw]`;
5409    /// `bias` is optional and must be `[Cout]` when provided. Returns
5410    /// a rank-4 lazy tensor `[N, Cout, Hout, Wout]`.
5411    ///
5412    /// Rank / channel / `groups` / stride mismatches surface as typed
5413    /// errors at build time rather than panicking inside the inner
5414    /// `fuel_graph` call.
5415    pub fn conv2d(
5416        &self,
5417        weight: &Self,
5418        bias: Option<&Self>,
5419        stride: (usize, usize),
5420        padding: (usize, usize),
5421        groups: usize,
5422    ) -> std::result::Result<Self, fuel_ir::Error> {
5423        if groups < 1 {
5424            return Err(
5425                fuel_ir::Error::Msg(format!("conv2d: groups must be >= 1, got {groups}",)).bt(),
5426            );
5427        }
5428        let x_shape = self.inner.shape();
5429        let x_dims = x_shape.dims();
5430        let w_shape = weight.inner.shape();
5431        let w_dims = w_shape.dims();
5432        if x_dims.len() != 4 {
5433            return Err(fuel_ir::Error::Msg(format!(
5434                "conv2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5435            ))
5436            .bt());
5437        }
5438        if w_dims.len() != 4 {
5439            return Err(fuel_ir::Error::Msg(format!(
5440                "conv2d: weight must be rank 4 [Cout, Cin/groups, Kh, Kw], got {w_dims:?}",
5441            ))
5442            .bt());
5443        }
5444        let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5445        let (cout, cin_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5446        if cin != cin_per_g * groups {
5447            return Err(fuel_ir::Error::Msg(format!(
5448                "conv2d: x has {cin} in-channels but weight expects {} ({cin_per_g}*{groups})",
5449                cin_per_g * groups,
5450            ))
5451            .bt());
5452        }
5453        if cout % groups != 0 {
5454            return Err(fuel_ir::Error::Msg(format!(
5455                "conv2d: Cout={cout} must be divisible by groups={groups}",
5456            ))
5457            .bt());
5458        }
5459        if let Some(b) = bias {
5460            let b_shape = b.inner.shape();
5461            let b_dims = b_shape.dims();
5462            if b_dims != [cout] {
5463                return Err(fuel_ir::Error::Msg(format!(
5464                    "conv2d: bias shape {b_dims:?} must match [Cout={cout}]",
5465                ))
5466                .bt());
5467            }
5468        }
5469        let (stride_h, stride_w) = stride;
5470        let (pad_h, pad_w) = padding;
5471        if stride_h < 1 || stride_w < 1 {
5472            return Err(fuel_ir::Error::Msg(format!(
5473                "conv2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5474            ))
5475            .bt());
5476        }
5477        let h_padded = h_in + 2 * pad_h;
5478        let w_padded = w_in + 2 * pad_w;
5479        if h_padded < kh || w_padded < kw {
5480            return Err(fuel_ir::Error::Msg(format!(
5481                "conv2d: padded input ({h_padded}x{w_padded}) smaller than kernel ({kh}x{kw})",
5482            ))
5483            .bt());
5484        }
5485        Ok(Self {
5486            inner: self.inner.conv2d(
5487                &weight.inner,
5488                bias.map(|b| &b.inner),
5489                stride,
5490                padding,
5491                groups,
5492            ),
5493        })
5494    }
5495
5496    /// Append a [`fuel_graph::Op::Fused`] node carrying
5497    /// [`fuel_graph::registry::FusedOpParams::FlashAttn`]. `self` is `q`
5498    /// of shape `[B, Hq, Sq, D]`; `k` and `v` are `[B, Hkv, Sk, D]`
5499    /// with `Hq` a multiple of `Hkv` (GQA). `alibi_slopes` (optional)
5500    /// is `[Hq]`. Returns the attention output, shape `[B, Hq, Sq, D]`.
5501    ///
5502    /// Rank / batch / GQA-divisibility / head-dim mismatches surface
5503    /// as typed errors at build time rather than panicking inside the
5504    /// inner `fuel_graph` call.
5505    pub fn flash_attn(
5506        &self,
5507        k: &Self,
5508        v: &Self,
5509        alibi_slopes: Option<&Self>,
5510        softmax_scale: f32,
5511        causal: bool,
5512        window_size_left: Option<usize>,
5513        window_size_right: Option<usize>,
5514        softcap: Option<f32>,
5515    ) -> std::result::Result<Self, fuel_ir::Error> {
5516        let q_shape = self.inner.shape();
5517        let q_dims = q_shape.dims();
5518        let k_shape = k.inner.shape();
5519        let k_dims = k_shape.dims();
5520        let v_shape = v.inner.shape();
5521        let v_dims = v_shape.dims();
5522        if q_dims.len() != 4 {
5523            return Err(fuel_ir::Error::Msg(format!(
5524                "flash_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5525            ))
5526            .bt());
5527        }
5528        if k_dims.len() != 4 {
5529            return Err(fuel_ir::Error::Msg(format!(
5530                "flash_attn: k must be rank 4 [B, Hkv, Sk, D], got {k_dims:?}",
5531            ))
5532            .bt());
5533        }
5534        if v_dims.len() != 4 {
5535            return Err(fuel_ir::Error::Msg(format!(
5536                "flash_attn: v must be rank 4 [B, Hkv, Sk, D], got {v_dims:?}",
5537            ))
5538            .bt());
5539        }
5540        let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5541        let (bk, hkv, sk, dk) = (k_dims[0], k_dims[1], k_dims[2], k_dims[3]);
5542        let (bv, hkv_v, sk_v, dv) = (v_dims[0], v_dims[1], v_dims[2], v_dims[3]);
5543        if b != bk || b != bv {
5544            return Err(fuel_ir::Error::Msg(
5545                format!("flash_attn: B mismatch q={b} k={bk} v={bv}",),
5546            )
5547            .bt());
5548        }
5549        if hkv != hkv_v {
5550            return Err(fuel_ir::Error::Msg(format!(
5551                "flash_attn: Hkv mismatch k={hkv} vs v={hkv_v}",
5552            ))
5553            .bt());
5554        }
5555        if sk != sk_v {
5556            return Err(fuel_ir::Error::Msg(
5557                format!("flash_attn: Sk mismatch k={sk} vs v={sk_v}",),
5558            )
5559            .bt());
5560        }
5561        if d != dk || d != dv {
5562            return Err(fuel_ir::Error::Msg(format!(
5563                "flash_attn: head_dim mismatch q={d} k={dk} v={dv}",
5564            ))
5565            .bt());
5566        }
5567        if hkv == 0 || hq % hkv != 0 {
5568            return Err(fuel_ir::Error::Msg(format!(
5569                "flash_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5570            ))
5571            .bt());
5572        }
5573        if let Some(a) = alibi_slopes {
5574            let a_shape = a.inner.shape();
5575            let a_dims = a_shape.dims();
5576            if a_dims != [hq] {
5577                return Err(fuel_ir::Error::Msg(format!(
5578                    "flash_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5579                ))
5580                .bt());
5581            }
5582        }
5583        Ok(Self {
5584            inner: self.inner.flash_attn(
5585                &k.inner,
5586                &v.inner,
5587                alibi_slopes.map(|t| &t.inner),
5588                softmax_scale,
5589                causal,
5590                window_size_left,
5591                window_size_right,
5592                softcap,
5593            ),
5594        })
5595    }
5596
5597    /// Append a [`fuel_graph::Op::Fused`] node carrying
5598    /// [`fuel_graph::registry::FusedOpParams::PagedAttn`]. `self` is the Q
5599    /// tensor `[B, Hq, Sq, D]`. `k_cache` / `v_cache` are paged caches
5600    /// `[num_blocks, block_size, Hkv, D]`. `block_table` is `[B,
5601    /// max_blocks]` u32; `context_lens` is `[B]` u32.
5602    ///
5603    /// Rank / batch / GQA-divisibility / block-size / dtype mismatches
5604    /// surface as typed errors at build time rather than panicking
5605    /// inside the inner `fuel_graph` call.
5606    pub fn paged_attn(
5607        &self,
5608        k_cache: &Self,
5609        v_cache: &Self,
5610        block_table: &Self,
5611        context_lens: &Self,
5612        alibi_slopes: Option<&Self>,
5613        softmax_scale: f32,
5614        block_size: usize,
5615        softcap: Option<f32>,
5616    ) -> std::result::Result<Self, fuel_ir::Error> {
5617        if block_size < 1 {
5618            return Err(fuel_ir::Error::Msg("paged_attn: block_size must be >= 1".into()).bt());
5619        }
5620        let q_shape = self.inner.shape();
5621        let q_dims = q_shape.dims();
5622        let kc_shape = k_cache.inner.shape();
5623        let kc_dims = kc_shape.dims();
5624        let vc_shape = v_cache.inner.shape();
5625        let vc_dims = vc_shape.dims();
5626        let bt_shape = block_table.inner.shape();
5627        let bt_dims = bt_shape.dims();
5628        let cl_shape = context_lens.inner.shape();
5629        let cl_dims = cl_shape.dims();
5630        if q_dims.len() != 4 {
5631            return Err(fuel_ir::Error::Msg(format!(
5632                "paged_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5633            ))
5634            .bt());
5635        }
5636        if kc_dims.len() != 4 {
5637            return Err(fuel_ir::Error::Msg(format!(
5638                "paged_attn: k_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {kc_dims:?}",
5639            )).bt());
5640        }
5641        if vc_dims.len() != 4 {
5642            return Err(fuel_ir::Error::Msg(format!(
5643                "paged_attn: v_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {vc_dims:?}",
5644            )).bt());
5645        }
5646        if bt_dims.len() != 2 {
5647            return Err(fuel_ir::Error::Msg(format!(
5648                "paged_attn: block_table must be rank 2 [B, max_blocks], got {bt_dims:?}",
5649            ))
5650            .bt());
5651        }
5652        if cl_dims.len() != 1 {
5653            return Err(fuel_ir::Error::Msg(format!(
5654                "paged_attn: context_lens must be rank 1 [B], got {cl_dims:?}",
5655            ))
5656            .bt());
5657        }
5658        let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5659        if kc_dims[1] != block_size {
5660            return Err(fuel_ir::Error::Msg(format!(
5661                "paged_attn: k_cache block dim {} != block_size {block_size}",
5662                kc_dims[1],
5663            ))
5664            .bt());
5665        }
5666        if vc_dims[1] != block_size {
5667            return Err(fuel_ir::Error::Msg(format!(
5668                "paged_attn: v_cache block dim {} != block_size {block_size}",
5669                vc_dims[1],
5670            ))
5671            .bt());
5672        }
5673        let hkv = kc_dims[2];
5674        if vc_dims[2] != hkv {
5675            return Err(fuel_ir::Error::Msg(format!(
5676                "paged_attn: Hkv mismatch k_cache={hkv} vs v_cache={}",
5677                vc_dims[2],
5678            ))
5679            .bt());
5680        }
5681        if kc_dims[3] != d || vc_dims[3] != d {
5682            return Err(fuel_ir::Error::Msg(format!(
5683                "paged_attn: D mismatch q={d} k={} v={}",
5684                kc_dims[3], vc_dims[3],
5685            ))
5686            .bt());
5687        }
5688        if hkv == 0 || hq % hkv != 0 {
5689            return Err(fuel_ir::Error::Msg(format!(
5690                "paged_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5691            ))
5692            .bt());
5693        }
5694        if bt_dims[0] != b {
5695            return Err(fuel_ir::Error::Msg(format!(
5696                "paged_attn: block_table batch dim {} != B={b}",
5697                bt_dims[0],
5698            ))
5699            .bt());
5700        }
5701        if cl_dims[0] != b {
5702            return Err(fuel_ir::Error::Msg(format!(
5703                "paged_attn: context_lens len {} != B={b}",
5704                cl_dims[0],
5705            ))
5706            .bt());
5707        }
5708        if block_table.inner.dtype() != fuel_ir::DType::U32 {
5709            return Err(fuel_ir::Error::Msg(format!(
5710                "paged_attn: block_table must be U32, got {:?}",
5711                block_table.inner.dtype(),
5712            ))
5713            .bt());
5714        }
5715        if context_lens.inner.dtype() != fuel_ir::DType::U32 {
5716            return Err(fuel_ir::Error::Msg(format!(
5717                "paged_attn: context_lens must be U32, got {:?}",
5718                context_lens.inner.dtype(),
5719            ))
5720            .bt());
5721        }
5722        if let Some(a) = alibi_slopes {
5723            let a_shape = a.inner.shape();
5724            let a_dims = a_shape.dims();
5725            if a_dims != [hq] {
5726                return Err(fuel_ir::Error::Msg(format!(
5727                    "paged_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5728                ))
5729                .bt());
5730            }
5731        }
5732        Ok(Self {
5733            inner: self.inner.paged_attn(
5734                &k_cache.inner,
5735                &v_cache.inner,
5736                &block_table.inner,
5737                &context_lens.inner,
5738                alibi_slopes.map(|t| &t.inner),
5739                softmax_scale,
5740                block_size,
5741                softcap,
5742            ),
5743        })
5744    }
5745
5746    /// Append a [`fuel_graph::Op::Fused`] node carrying
5747    /// [`fuel_graph::registry::FusedOpParams::ConvTranspose2D`]. `self` must
5748    /// be `[N, Cin, H, W]`; `weight` must be `[Cin, Cout/groups, Kh, Kw]`
5749    /// (note transposed channel order vs `conv2d`). Returns a rank-4
5750    /// lazy tensor `[N, Cout, Hout, Wout]`.
5751    ///
5752    /// Rank / channel / `groups` / stride / dilation mismatches surface
5753    /// as typed errors at build time rather than panicking inside the
5754    /// inner `fuel_graph` call.
5755    pub fn conv_transpose2d(
5756        &self,
5757        weight: &Self,
5758        stride: (usize, usize),
5759        padding: (usize, usize),
5760        output_padding: (usize, usize),
5761        dilation: (usize, usize),
5762        groups: usize,
5763    ) -> std::result::Result<Self, fuel_ir::Error> {
5764        if groups < 1 {
5765            return Err(fuel_ir::Error::Msg(format!(
5766                "conv_transpose2d: groups must be >= 1, got {groups}",
5767            ))
5768            .bt());
5769        }
5770        let x_shape = self.inner.shape();
5771        let x_dims = x_shape.dims();
5772        let w_shape = weight.inner.shape();
5773        let w_dims = w_shape.dims();
5774        if x_dims.len() != 4 {
5775            return Err(fuel_ir::Error::Msg(format!(
5776                "conv_transpose2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5777            ))
5778            .bt());
5779        }
5780        if w_dims.len() != 4 {
5781            return Err(fuel_ir::Error::Msg(format!(
5782                "conv_transpose2d: weight must be rank 4 [Cin, Cout/groups, Kh, Kw], got {w_dims:?}",
5783            )).bt());
5784        }
5785        let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5786        let (cin_w, cout_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5787        if cin != cin_w {
5788            return Err(fuel_ir::Error::Msg(format!(
5789                "conv_transpose2d: x has {cin} in-channels but weight has {cin_w}",
5790            ))
5791            .bt());
5792        }
5793        if cin % groups != 0 {
5794            return Err(fuel_ir::Error::Msg(format!(
5795                "conv_transpose2d: Cin={cin} must be divisible by groups={groups}",
5796            ))
5797            .bt());
5798        }
5799        let (stride_h, stride_w) = stride;
5800        let (pad_h, pad_w) = padding;
5801        let (out_pad_h, out_pad_w) = output_padding;
5802        let (dil_h, dil_w) = dilation;
5803        if stride_h < 1 || stride_w < 1 {
5804            return Err(fuel_ir::Error::Msg(format!(
5805                "conv_transpose2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5806            ))
5807            .bt());
5808        }
5809        if dil_h < 1 || dil_w < 1 {
5810            return Err(fuel_ir::Error::Msg(format!(
5811                "conv_transpose2d: dilation must be >= 1, got ({dil_h}, {dil_w})",
5812            ))
5813            .bt());
5814        }
5815        let h_out = h_in.saturating_sub(1) * stride_h + dil_h * (kh - 1) + out_pad_h + 1;
5816        let w_out = w_in.saturating_sub(1) * stride_w + dil_w * (kw - 1) + out_pad_w + 1;
5817        if h_out <= 2 * pad_h || w_out <= 2 * pad_w {
5818            return Err(fuel_ir::Error::Msg(format!(
5819                "conv_transpose2d: padding ({pad_h}x{pad_w}) is larger than the produced output dims ({h_out}x{w_out})",
5820            )).bt());
5821        }
5822        let _ = cout_per_g;
5823        Ok(Self {
5824            inner: self.inner.conv_transpose2d(
5825                &weight.inner,
5826                stride,
5827                padding,
5828                output_padding,
5829                dilation,
5830                groups,
5831            ),
5832        })
5833    }
5834
5835    /// Append a transposed 1D convolution. `self` is
5836    /// `[N, Cin, Lin]`; `weight` is `[Cin, Cout/groups, K]`
5837    /// (PyTorch channel order). Returns `[N, Cout, Lout]`.
5838    ///
5839    /// Internally lifts to rank-4 and dispatches through
5840    /// `conv_transpose2d` — there is no separate 1D op in the
5841    /// IR; the lift is transparent to the executor (which sees
5842    /// the same `Op::Fused(CONV_TRANSPOSE2D, _)` it already
5843    /// dispatches CPU kernels for).
5844    ///
5845    /// Unblocks audio codec decoders (DAC, EnCodec, SNAC, Mimi,
5846    /// Parler-TTS, MetaVoice, CSM) which all upsample quantized
5847    /// latents to waveform via strided transposed convs.
5848    pub fn conv_transpose1d(
5849        &self,
5850        weight: &Self,
5851        stride: usize,
5852        padding: usize,
5853        output_padding: usize,
5854        dilation: usize,
5855        groups: usize,
5856    ) -> std::result::Result<Self, fuel_ir::Error> {
5857        if groups < 1 {
5858            return Err(fuel_ir::Error::Msg(format!(
5859                "conv_transpose1d: groups must be >= 1, got {groups}",
5860            ))
5861            .bt());
5862        }
5863        let x_shape = self.inner.shape();
5864        let x_dims = x_shape.dims();
5865        let w_shape = weight.inner.shape();
5866        let w_dims = w_shape.dims();
5867        if x_dims.len() != 3 {
5868            return Err(fuel_ir::Error::Msg(format!(
5869                "conv_transpose1d: x must be rank 3 [N, Cin, Lin], got {x_dims:?}",
5870            ))
5871            .bt());
5872        }
5873        if w_dims.len() != 3 {
5874            return Err(fuel_ir::Error::Msg(format!(
5875                "conv_transpose1d: weight must be rank 3 [Cin, Cout/groups, K], got {w_dims:?}",
5876            ))
5877            .bt());
5878        }
5879        if stride < 1 {
5880            return Err(fuel_ir::Error::Msg(format!(
5881                "conv_transpose1d: stride must be >= 1, got {stride}",
5882            ))
5883            .bt());
5884        }
5885        if dilation < 1 {
5886            return Err(fuel_ir::Error::Msg(format!(
5887                "conv_transpose1d: dilation must be >= 1, got {dilation}",
5888            ))
5889            .bt());
5890        }
5891        let cin = x_dims[1];
5892        let cin_w = w_dims[0];
5893        if cin != cin_w {
5894            return Err(fuel_ir::Error::Msg(format!(
5895                "conv_transpose1d: x has {cin} in-channels but weight has {cin_w}",
5896            ))
5897            .bt());
5898        }
5899        if !cin.is_multiple_of(groups) {
5900            return Err(fuel_ir::Error::Msg(format!(
5901                "conv_transpose1d: Cin={cin} must be divisible by groups={groups}",
5902            ))
5903            .bt());
5904        }
5905        Ok(Self {
5906            inner: self.inner.conv_transpose1d(
5907                &weight.inner,
5908                stride,
5909                padding,
5910                output_padding,
5911                dilation,
5912                groups,
5913            ),
5914        })
5915    }
5916}
5917
5918// ============================================================================
5919// Phase A.1 — wrapper additions (eager-`Tensor` retirement program).
5920//
5921// Methods on `fuel_graph::NodeHandle` that weren't previously surfaced through
5922// `Tensor`. Pure delegation; no new graph ops. See
5923// `docs/session-prompts/eager-tensor-retirement-master-plan.md`.
5924// ============================================================================
5925
5926impl Tensor {
5927    // ---- shape ops: unsqueeze (Result + Dim) + Result-returning siblings ----
5928
5929    /// Append a size-1 dimension at position `dim`. Inverse of
5930    /// [`Self::squeeze`]. Accepts any [`Dim`] (`usize`, `D::Minus1`,
5931    /// etc.). Bad `dim` surfaces as a typed error at build time.
5932    pub fn unsqueeze<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5933        let shape = self.inner.shape();
5934        let dim = dim.to_index_plus_one(&shape, "unsqueeze")?;
5935        Ok(Self {
5936            inner: self.inner.try_unsqueeze(dim)?,
5937        })
5938    }
5939
5940    // ---- triangular masking (canonical attention masks) ----
5941
5942    /// Upper-triangular mask along the last two dims. `diagonal = 0`
5943    /// keeps the main diagonal and above; positive shifts higher.
5944    pub fn triu(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5945        Ok(Self {
5946            inner: self.inner.triu(diagonal)?,
5947        })
5948    }
5949
5950    /// Lower-triangular mask along the last two dims. `tril(0)` is the
5951    /// canonical causal-attention mask.
5952    pub fn tril(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5953        Ok(Self {
5954            inner: self.inner.tril(diagonal)?,
5955        })
5956    }
5957
5958    // ---- additional reductions / activations ----
5959
5960    /// `log(softmax(self))` along the last dim, fused into one op.
5961    pub fn log_softmax_last_dim(&self) -> std::result::Result<Self, fuel_ir::Error> {
5962        Ok(Self {
5963            inner: self.inner.log_softmax_last_dim()?,
5964        })
5965    }
5966
5967    /// Numerically-stable softmax along an arbitrary axis. Accepts any
5968    /// [`Dim`]. Decomposes into `max_keepdim` / `broadcast_sub` / `exp` /
5969    /// `sum_keepdim` / `broadcast_div`, all of which already accept
5970    /// `D: Dim`, so this is a pure composition with no new graph op.
5971    ///
5972    /// When `dim` resolves to the last axis, prefer
5973    /// [`Self::softmax_last_dim`], which dispatches to the fused
5974    /// `SoftmaxLastDim` op (single kernel rather than five graph nodes).
5975    pub fn softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5976        let shape = self.inner.shape();
5977        // Resolve once to a concrete `usize` so we can pass it to each
5978        // composed call (the `Dim` trait doesn't require `Copy`, so we
5979        // can't reuse the generic `D` across multiple calls).
5980        let axis: usize = dim.to_index(&shape, "softmax")?;
5981        let m = self.max_keepdim(axis)?;
5982        let shifted = self.broadcast_sub(&m)?;
5983        let e = shifted.exp();
5984        let s = e.sum_keepdim(axis)?;
5985        e.broadcast_div(&s)
5986    }
5987
5988    /// Numerically-stable `log(softmax(self))` along an arbitrary axis.
5989    /// Accepts any [`Dim`]. Computes `x - max - log(sum(exp(x - max)))`
5990    /// — the standard log-sum-exp form, which avoids the explicit
5991    /// `softmax`-then-`log` underflow path. Pure composition over
5992    /// existing primitives.
5993    ///
5994    /// When `dim` resolves to the last axis, prefer
5995    /// [`Self::log_softmax_last_dim`], which dispatches to the fused
5996    /// `LogSoftmaxLastDim` op.
5997    pub fn log_softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5998        let shape = self.inner.shape();
5999        let axis: usize = dim.to_index(&shape, "log_softmax")?;
6000        let m = self.max_keepdim(axis)?;
6001        let shifted = self.broadcast_sub(&m)?;
6002        let lse = shifted.exp().sum_keepdim(axis)?.log();
6003        shifted.broadcast_sub(&lse)
6004    }
6005
6006    /// Argmin along `dim`, returning a U32 tensor with the reduced dim
6007    /// removed. Non-differentiable. Bad `dim` surfaces as a typed
6008    /// error at build time. Accepts any [`Dim`].
6009    pub fn argmin_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6010        let shape = self.inner.shape();
6011        let dim = dim.to_index(&shape, "argmin_dim")?;
6012        Ok(Self {
6013            inner: self.inner.argmin_dim(dim),
6014        })
6015    }
6016
6017    // ---- masking / scatter ----
6018
6019    /// Fill every position where `mask != 0` with `value`; pass `self`
6020    /// through everywhere `mask == 0`. `mask` must be U8 with the same
6021    /// shape as `self`; `value`'s dtype must match `self`.
6022    pub fn masked_fill(
6023        &self,
6024        mask: &Self,
6025        value: fuel_ir::Scalar,
6026    ) -> std::result::Result<Self, fuel_ir::Error> {
6027        Ok(Self {
6028            inner: self.inner.masked_fill(&mask.inner, value)?,
6029        })
6030    }
6031
6032    /// `self + scatter(indices, src, dim=dim)` — accumulate `src` rows
6033    /// at positions named by `indices` along `dim`. `indices` is rank-1
6034    /// U32 with length equal to `src.dims()[dim]`. Accepts any [`Dim`].
6035    /// Dim bounds / index dtype / shape / dtype-parity mismatches
6036    /// surface as typed errors at build time.
6037    pub fn index_add<D: Dim>(
6038        &self,
6039        dim: D,
6040        indices: &Self,
6041        src: &Self,
6042    ) -> std::result::Result<Self, fuel_ir::Error> {
6043        let shape = self.inner.shape();
6044        let dim = dim.to_index(&shape, "index_add")?;
6045        if indices.inner.dtype() != fuel_ir::DType::U32 {
6046            return Err(fuel_ir::Error::Msg(format!(
6047                "index_add: index must be U32, got {:?}",
6048                indices.inner.dtype(),
6049            ))
6050            .bt());
6051        }
6052        if self.inner.dtype() != src.inner.dtype() {
6053            return Err(fuel_ir::Error::Msg(format!(
6054                "index_add: base and src dtypes must match, got {:?} vs {:?}",
6055                self.inner.dtype(),
6056                src.inner.dtype(),
6057            ))
6058            .bt());
6059        }
6060        let base_dims = shape.dims();
6061        let src_shape = src.inner.shape();
6062        let src_dims = src_shape.dims();
6063        if base_dims.len() != src_dims.len() {
6064            return Err(fuel_ir::Error::Msg(format!(
6065                "index_add: base and src must have the same rank, got {} vs {}",
6066                base_dims.len(),
6067                src_dims.len(),
6068            ))
6069            .bt());
6070        }
6071        let idx_shape = indices.inner.shape();
6072        let idx_dims = idx_shape.dims();
6073        if idx_dims.len() != 1 {
6074            return Err(fuel_ir::Error::Msg(format!(
6075                "index_add: index must be rank 1, got {idx_dims:?}",
6076            ))
6077            .bt());
6078        }
6079        if src_dims[dim] != idx_dims[0] {
6080            return Err(fuel_ir::Error::Msg(format!(
6081                "index_add: src dim {dim} ({}) must match index length ({})",
6082                src_dims[dim], idx_dims[0],
6083            ))
6084            .bt());
6085        }
6086        Ok(Self {
6087            inner: self.inner.index_add(dim, &indices.inner, &src.inner),
6088        })
6089    }
6090
6091    /// Functional inverse of [`Self::gather`]. Accumulates `src` into
6092    /// `self` at positions given by `indices` (substituted at `dim`).
6093    /// Accepts any [`Dim`]. Dim bounds / index dtype / shape / dtype-
6094    /// parity mismatches surface as typed errors at build time.
6095    pub fn scatter_add<D: Dim>(
6096        &self,
6097        dim: D,
6098        indices: &Self,
6099        src: &Self,
6100    ) -> std::result::Result<Self, fuel_ir::Error> {
6101        let shape = self.inner.shape();
6102        let dim = dim.to_index(&shape, "scatter_add")?;
6103        if indices.inner.dtype() != fuel_ir::DType::U32 {
6104            return Err(fuel_ir::Error::Msg(format!(
6105                "scatter_add: index must be U32, got {:?}",
6106                indices.inner.dtype(),
6107            ))
6108            .bt());
6109        }
6110        if self.inner.dtype() != src.inner.dtype() {
6111            return Err(fuel_ir::Error::Msg(format!(
6112                "scatter_add: base and src dtypes must match, got {:?} vs {:?}",
6113                self.inner.dtype(),
6114                src.inner.dtype(),
6115            ))
6116            .bt());
6117        }
6118        let idx_shape = indices.inner.shape();
6119        let src_shape = src.inner.shape();
6120        if idx_shape.dims() != src_shape.dims() {
6121            return Err(fuel_ir::Error::Msg(format!(
6122                "scatter_add: index and src must have the same shape, got {:?} vs {:?}",
6123                idx_shape.dims(),
6124                src_shape.dims(),
6125            ))
6126            .bt());
6127        }
6128        Ok(Self {
6129            inner: self.inner.scatter_add(dim, &indices.inner, &src.inner),
6130        })
6131    }
6132
6133    // ---- in-place activations (Phase 4-5 infrastructure, now surfaced) ----
6134    //
6135    // These mutate `self`'s storage in place. Safe to call on
6136    // tape-tracked tensors after Phase 4's view-aware ordering pass and
6137    // Phase 5's auto-copy pass. See `project_inplace_ops_complete`
6138    // memory entry.
6139
6140    /// In-place `max(0, self)`. See [`Self::relu`] for the functional
6141    /// variant.
6142    pub fn relu_inplace(&self) -> Self {
6143        Self {
6144            inner: self.inner.relu_inplace(),
6145        }
6146    }
6147
6148    /// In-place `self * sigmoid(self)`. See [`Self::silu`] for the
6149    /// functional variant.
6150    pub fn silu_inplace(&self) -> Self {
6151        Self {
6152            inner: self.inner.silu_inplace(),
6153        }
6154    }
6155
6156    /// In-place tanh-approximation GELU. See [`Self::gelu_tanh`] for the
6157    /// functional variant.
6158    pub fn gelu_tanh_inplace(&self) -> Self {
6159        Self {
6160            inner: self.inner.gelu_tanh_inplace(),
6161        }
6162    }
6163
6164    /// In-place `tanh(self)`. See [`Self::tanh`] for the functional
6165    /// variant.
6166    pub fn tanh_inplace(&self) -> Self {
6167        Self {
6168            inner: self.inner.tanh_inplace(),
6169        }
6170    }
6171
6172    /// In-place `sigmoid(self)`. See [`Self::sigmoid`] for the
6173    /// functional variant.
6174    pub fn sigmoid_inplace(&self) -> Self {
6175        Self {
6176            inner: self.inner.sigmoid_inplace(),
6177        }
6178    }
6179
6180    /// In-place `self = mul · self + add`. Single fused-op equivalent
6181    /// of `self.mul_scalar(mul).add_scalar(add)` plus reassignment.
6182    pub fn affine_inplace(&self, mul: f64, add: f64) -> Self {
6183        Self {
6184            inner: self.inner.affine_inplace(mul, add),
6185        }
6186    }
6187
6188    // ---- additional const_*_like factories ----
6189
6190    /// Build a sibling F64 `Const` on the same graph as `self`.
6191    pub fn const_f64_like(
6192        &self,
6193        data: impl Into<Arc<[f64]>>,
6194        shape: impl Into<Shape>,
6195    ) -> std::result::Result<Self, fuel_ir::Error> {
6196        Ok(Self {
6197            inner: self.inner.const_f64_like(data, shape)?,
6198        })
6199    }
6200
6201    /// Build a sibling I64 `Const` on the same graph. Used by integer-
6202    /// target ops (e.g. cross-entropy with PyTorch-convention class
6203    /// indices).
6204    pub fn const_i64_like(
6205        &self,
6206        data: impl Into<Arc<[i64]>>,
6207        shape: impl Into<Shape>,
6208    ) -> std::result::Result<Self, fuel_ir::Error> {
6209        Ok(Self {
6210            inner: self.inner.const_i64_like(data, shape)?,
6211        })
6212    }
6213
6214    // ---- device residency control ----
6215
6216    /// Pin this tensor's realized storage to `device`. Consumes `self`
6217    /// because the placement is a graph-level annotation tied to the
6218    /// node id rather than a side-effecting operation.
6219    pub fn on_device(self, device: &Device) -> Self {
6220        Self {
6221            inner: self.inner.on_device(device.location()),
6222        }
6223    }
6224
6225    /// Append an `Op::Release` node — explicitly drop this tensor's
6226    /// device-resident storage once the ordering pass has scheduled
6227    /// every reader before it.
6228    pub fn release(&self) -> Self {
6229        Self {
6230            inner: self.inner.release(),
6231        }
6232    }
6233
6234    /// Move bytes to `device`, destroying the source. Use when the
6235    /// source is genuinely dead after the transfer.
6236    pub fn move_to_device(&self, device: &Device) -> Self {
6237        Self {
6238            inner: self.inner.move_to_device(device.location()),
6239        }
6240    }
6241
6242    /// Copy bytes to `device`, leaving the source resident. Use when
6243    /// other ops still need the source.
6244    pub fn copy_to_device(&self, device: &Device) -> Self {
6245        Self {
6246            inner: self.inner.copy_to_device(device.location()),
6247        }
6248    }
6249
6250    // ---- autograd ----
6251
6252    /// Run reverse-mode autograd from this tensor as the loss, returning
6253    /// a [`fuel_graph::GradMap`] keyed by every input tensor reached.
6254    /// The gradient nodes extend the same graph; realizing a gradient
6255    /// re-executes the forward dependencies.
6256    pub fn backward(&self) -> fuel_graph::GradMap {
6257        self.inner.backward()
6258    }
6259}
6260
6261// ============================================================================
6262// Phase A.2 — composite primitives expressible from existing ops.
6263//
6264// Each method here is implemented in terms of `Tensor`'s existing
6265// surface (reshape, permute, concat, unsqueeze, etc.). No new graph ops.
6266// ============================================================================
6267
6268impl Tensor {
6269    /// Transpose the last two dims as a Result-returning convenience —
6270    /// rank < 2 surfaces as an error rather than the panic the
6271    /// no-arg [`Self::transpose`] would produce. Alias for the eager
6272    /// `transpose_last_two`.
6273    pub fn transpose_last_two(&self) -> std::result::Result<Self, fuel_ir::Error> {
6274        self.transpose()
6275    }
6276
6277    /// Eager-API alias of [`Self::transpose_last_two`]. Matches PyTorch's
6278    /// `.t()` short form and the existing eager [`Tensor::t`] method.
6279    pub fn t(&self) -> std::result::Result<Self, fuel_ir::Error> {
6280        self.transpose()
6281    }
6282
6283    /// Two-argument transpose: swap dims `dim1` and `dim2`, leaving the
6284    /// rest in place. Implemented via [`fuel_graph::NodeHandle::try_permute`]; matches the
6285    /// eager `transpose(d1, d2)` two-arg form. Accepts any [`Dim`]
6286    /// (`usize`, `D::Minus1`, etc.).
6287    pub fn transpose_dims<D1: Dim, D2: Dim>(
6288        &self,
6289        dim1: D1,
6290        dim2: D2,
6291    ) -> std::result::Result<Self, fuel_ir::Error> {
6292        let shape = self.shape();
6293        let dim1 = dim1.to_index(&shape, "transpose_dims")?;
6294        let dim2 = dim2.to_index(&shape, "transpose_dims")?;
6295        if dim1 == dim2 {
6296            return Ok(self.clone());
6297        }
6298        let rank = shape.dims().len();
6299        let mut axes: Vec<usize> = (0..rank).collect();
6300        axes.swap(dim1, dim2);
6301        self.permute(axes.as_slice())
6302    }
6303
6304    /// Collapse dims `[start_dim, end_dim]` (inclusive) into a single
6305    /// dimension. Returns `Result` so out-of-bounds surfaces as a typed
6306    /// error rather than a panic. Accepts any [`Dim`] for either arg
6307    /// (`D::Minus1` for the last axis works).
6308    pub fn flatten<D1: Dim, D2: Dim>(
6309        &self,
6310        start_dim: D1,
6311        end_dim: D2,
6312    ) -> std::result::Result<Self, fuel_ir::Error> {
6313        let shape = self.shape();
6314        let rank = shape.dims().len();
6315        if rank == 0 {
6316            return Ok(self.clone());
6317        }
6318        let start_dim = start_dim.to_index(&shape, "flatten")?;
6319        let end_dim = end_dim.to_index(&shape, "flatten")?;
6320        if start_dim > end_dim {
6321            return Err(fuel_ir::Error::Msg(format!(
6322                "flatten: start_dim={start_dim} > end_dim={end_dim}",
6323            ))
6324            .bt());
6325        }
6326        let dims = shape.dims();
6327        let merged: usize = dims[start_dim..=end_dim].iter().product();
6328        let mut new_dims: Vec<usize> = Vec::with_capacity(rank - (end_dim - start_dim));
6329        new_dims.extend_from_slice(&dims[..start_dim]);
6330        new_dims.push(merged);
6331        new_dims.extend_from_slice(&dims[end_dim + 1..]);
6332        self.reshape(new_dims)
6333    }
6334
6335    /// Flatten dims `[0, end_dim]` (inclusive) into one.
6336    pub fn flatten_to<D: Dim>(&self, end_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6337        self.flatten(0_usize, end_dim)
6338    }
6339
6340    /// Flatten dims `[start_dim, rank-1]` into one.
6341    pub fn flatten_from<D: Dim>(&self, start_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6342        let rank = self.shape().dims().len();
6343        if rank == 0 {
6344            return Ok(self.clone());
6345        }
6346        self.flatten(start_dim, rank - 1)
6347    }
6348
6349    /// Flatten the tensor to rank-1 (single dim containing every element).
6350    pub fn flatten_all(&self) -> std::result::Result<Self, fuel_ir::Error> {
6351        let rank = self.shape().dims().len();
6352        if rank == 0 {
6353            return Ok(self.clone());
6354        }
6355        self.flatten(0, rank - 1)
6356    }
6357
6358    /// Stack tensors along a new dim at position `dim`. Each input is
6359    /// `unsqueeze`d at `dim` then concatenated. All inputs must have
6360    /// identical shape; `dim` may equal `rank` (append a new trailing
6361    /// dim). Accepts any [`Dim`].
6362    pub fn stack<D: Dim>(args: &[&Self], dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6363        if args.is_empty() {
6364            return Err(fuel_ir::Error::Msg("stack: requires at least one tensor".into()).bt());
6365        }
6366        let reference_shape = args[0].shape();
6367        let reference_dims = reference_shape.dims().to_vec();
6368        let dim = dim.to_index_plus_one(&reference_shape, "stack")?;
6369        for (idx, t) in args.iter().enumerate().skip(1) {
6370            if t.shape().dims() != reference_dims.as_slice() {
6371                return Err(fuel_ir::Error::Msg(format!(
6372                    "stack: tensor {idx} shape {:?} != reference shape {:?}",
6373                    t.shape().dims(),
6374                    reference_dims,
6375                ))
6376                .bt());
6377            }
6378        }
6379        // unsqueeze every input at the new dim, then concat.
6380        let mut iter = args.iter();
6381        let first = iter.next().unwrap().unsqueeze(dim)?;
6382        let mut acc = first;
6383        for t in iter {
6384            let u = t.unsqueeze(dim)?;
6385            acc = acc.concat(&u, dim)?;
6386        }
6387        Ok(acc)
6388    }
6389
6390    // ---- keepdim reductions ----
6391    //
6392    // Each keepdim variant is the squeezed reduction post-composed with
6393    // `unsqueeze` at the same dim. The graph optimizer can fuse these
6394    // back into a single op when it's profitable; until then, the cost
6395    // is one extra view-only node.
6396
6397    /// Sum along `dim`, keeping the reduced dim as size 1. Accepts any
6398    /// [`Dim`]. Returns Result because of the cascade from [`Self::unsqueeze`].
6399    pub fn sum_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6400        let shape = self.shape();
6401        let dim = dim.to_index(&shape, "sum_keepdim")?;
6402        self.sum_dim(dim).unwrap().unsqueeze(dim)
6403    }
6404
6405    /// Mean along `dim`, keeping the reduced dim as size 1.
6406    pub fn mean_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6407        let shape = self.shape();
6408        let dim = dim.to_index(&shape, "mean_keepdim")?;
6409        self.mean_dim(dim).unwrap().unsqueeze(dim)
6410    }
6411
6412    /// Max along `dim`, keeping the reduced dim as size 1.
6413    pub fn max_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6414        let shape = self.shape();
6415        let dim = dim.to_index(&shape, "max_keepdim")?;
6416        self.max_dim(dim).unwrap().unsqueeze(dim)
6417    }
6418
6419    /// Min along `dim`, keeping the reduced dim as size 1.
6420    pub fn min_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6421        let shape = self.shape();
6422        let dim = dim.to_index(&shape, "min_keepdim")?;
6423        // sum_dim/mean_dim/max_dim/min_dim return Self today (A.8b.9 will
6424        // flip them to Result); chain through `.unsqueeze(dim)?` which now
6425        // owns the build-time dim validation.
6426        self.min_dim(dim).unwrap().unsqueeze(dim)
6427    }
6428
6429    /// Variance along `dim`, keeping the reduced dim as size 1. Divides
6430    /// squared deviations by `n - correction` (numpy/PyTorch-2.x
6431    /// `correction` convention: `0.0` is the **population** variance
6432    /// — KISS-Ops §6.13's default, and numpy's own default — `1.0` is
6433    /// Bessel's (sample/unbiased) correction, matching PyTorch's *older*
6434    /// default and the eager [`Tensor::var_keepdim`]'s prior behavior.
6435    ///
6436    /// **Not a default-argument convenience**: KISS-Ops §6.13 requires
6437    /// Bessel's correction to be an EXPLICIT attribute, not an implicit
6438    /// torch-style default silently baked into the divisor — this
6439    /// parameter exists so a caller cannot get Bessel's correction by
6440    /// omission. `correction >= n` produces a non-finite result (same
6441    /// shape as any other degenerate reduction — never a panic).
6442    pub fn var_keepdim<D: Dim>(
6443        &self,
6444        dim: D,
6445        correction: f64,
6446    ) -> std::result::Result<Self, fuel_ir::Error> {
6447        let shape = self.shape();
6448        let dim = dim.to_index(&shape, "var_keepdim")?;
6449        let dims = shape.dims();
6450        let n = dims[dim];
6451        let mean = self.mean_keepdim(dim)?;
6452        let deviation = self.broadcast_sub(&mean).unwrap();
6453        let squares = deviation.sqr();
6454        // sum_keepdim then divide by (n - correction); leaves the reduced dim as 1.
6455        let summed = squares.sum_keepdim(dim)?;
6456        let divisor = n as f64 - correction;
6457        Ok(summed.mul_scalar(1.0 / divisor))
6458    }
6459
6460    /// Variance along `dim`, squeezing the reduced dim. See
6461    /// [`Self::var_keepdim`] for the `correction` parameter's meaning.
6462    pub fn var<D: Dim>(
6463        &self,
6464        dim: D,
6465        correction: f64,
6466    ) -> std::result::Result<Self, fuel_ir::Error> {
6467        let shape = self.shape();
6468        let dim = dim.to_index(&shape, "var")?;
6469        self.var_keepdim(dim, correction)?.squeeze(dim)
6470    }
6471
6472    // ---- composite scalar / binary ops (Phase A.4) ----
6473
6474    /// `y = mul * self + add` element-wise. Two-op composite of
6475    /// [`Self::mul_scalar`] then [`Self::add_scalar`]; a fused `Op::Affine`
6476    /// can collapse this into a single op later (see the in-place
6477    /// counterpart [`Self::affine_inplace`]).
6478    pub fn affine(&self, mul: f64, add: f64) -> Self {
6479        self.mul_scalar(mul).add_scalar(add)
6480    }
6481
6482    /// `y = scale * self + shift`. Alias of [`Self::affine`] with
6483    /// descriptive parameter names; matches eager's
6484    /// `Tensor::scale_and_shift`.
6485    pub fn scale_and_shift(&self, scale: f64, shift: f64) -> Self {
6486        self.affine(scale, shift)
6487    }
6488
6489    /// Exponential Linear Unit: `self` where `self > 0`,
6490    /// `alpha * (exp(self) - 1)` otherwise. Composite of `where_cond`,
6491    /// `gt`, `exp`, `affine`.
6492    pub fn elu(&self, alpha: f64) -> Self {
6493        // Negative-branch value: alpha * (exp(self) - 1) = alpha * exp(self) - alpha
6494        let neg_branch = self.exp().affine(alpha, -alpha);
6495        // Mask: self > 0. Build a zero on the same graph.
6496        // GAP-003 carve-out. THE PROOF IS LOCAL AND ON THIS LINE: the buffer is
6497        // `vec![0.0; self.elem_count()]` and the shape is `self.shape()`, whose
6498        // elem_count IS that number. Both from `self`; no caller supplies either.
6499        let zero = self
6500            .const_f32_like(vec![0.0; self.elem_count()], self.shape())
6501            .expect("elu: vec![_; self.elem_count()] against self.shape() -- same source");
6502        let mask = self.gt(&zero).unwrap();
6503        mask.where_cond(self, &neg_branch).unwrap()
6504    }
6505
6506    /// Inner product of two rank-1 tensors. Composite of `mul` +
6507    /// `sum_all`; matches eager's [`Tensor::dot`].
6508    pub fn dot(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6509        let a = self.shape().dims().to_vec();
6510        let b = rhs.shape().dims().to_vec();
6511        if a.len() != 1 || b.len() != 1 {
6512            return Err(fuel_ir::Error::Msg(format!(
6513                "dot: requires rank-1 inputs, got lhs={a:?} rhs={b:?}",
6514            ))
6515            .bt());
6516        }
6517        if a[0] != b[0] {
6518            return Err(fuel_ir::Error::Msg(format!(
6519                "dot: length mismatch lhs={} rhs={}",
6520                a[0], b[0],
6521            ))
6522            .bt());
6523        }
6524        Ok(self.mul(rhs).unwrap().sum_all())
6525    }
6526
6527    /// Matrix × vector: `[m, n] · [n] -> [m]`. No broadcasting. Composite
6528    /// of `unsqueeze` + `matmul` + `squeeze`. Matches eager's
6529    /// [`Tensor::mv`].
6530    pub fn mv(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6531        let a = self.shape().dims().to_vec();
6532        let b = rhs.shape().dims().to_vec();
6533        if a.len() != 2 || b.len() != 1 || a[1] != b[0] {
6534            return Err(fuel_ir::Error::Msg(format!(
6535                "mv: shape mismatch lhs={a:?} rhs={b:?} (need [m,n] · [n])",
6536            ))
6537            .bt());
6538        }
6539        // unsqueeze rhs to [n,1], matmul -> [m,1], squeeze trailing dim.
6540        let rhs_col = rhs.unsqueeze(1_usize)?;
6541        let prod = self.matmul(&rhs_col).unwrap();
6542        prod.squeeze(1_usize)
6543    }
6544
6545    /// Alias of [`Self::mv`] with a more descriptive name. Matches
6546    /// eager's [`Tensor::matvec`].
6547    pub fn matvec(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6548        self.mv(rhs)
6549    }
6550
6551    /// Broadcast-aware matmul. Lazy's [`Self::matmul`] already accepts
6552    /// broadcast-compatible operands; this method is exposed for
6553    /// signature compatibility with eager's `Tensor::broadcast_matmul`.
6554    pub fn broadcast_matmul(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6555        self.matmul(rhs)
6556    }
6557
6558    // ---- Phase A.5 factory family ----
6559    //
6560    // Instance methods derive shape + dtype from `self` and place the new
6561    // tensor on the same graph via `const_*_like`. Static methods build
6562    // a fresh graph anchored on a host-allocated buffer.
6563
6564    /// New tensor with the same shape, dtype, and graph as `self`, filled
6565    /// with ones. Returns Err for unsupported dtypes (anything outside
6566    /// F32/F64/BF16/F16/U32/I64) — matches eager `Tensor::ones_like` parity.
6567    pub fn ones_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6568        let n = self.elem_count();
6569        let shape = self.shape();
6570        match self.dtype() {
6571            DType::F32 => self.const_f32_like(vec![1.0_f32; n], shape),
6572            DType::F64 => self.const_f64_like(vec![1.0_f64; n], shape),
6573            DType::BF16 => self.const_bf16_like(vec![half::bf16::ONE; n], shape),
6574            DType::F16 => self.const_f16_like(vec![half::f16::ONE; n], shape),
6575            DType::U32 => self.const_u32_like(vec![1_u32; n], shape),
6576            DType::I64 => self.const_i64_like(vec![1_i64; n], shape),
6577            other => {
6578                Err(fuel_ir::Error::Msg(format!("ones_like: unsupported dtype {other:?}",)).bt())
6579            }
6580        }
6581    }
6582
6583    /// New tensor with the same shape, dtype, and graph as `self`, filled
6584    /// with zeros. Returns Err for unsupported dtypes (anything outside
6585    /// F32/F64/BF16/F16/U32/I64) — matches eager `Tensor::zeros_like` parity.
6586    pub fn zeros_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6587        let n = self.elem_count();
6588        let shape = self.shape();
6589        match self.dtype() {
6590            DType::F32 => self.const_f32_like(vec![0.0_f32; n], shape),
6591            DType::F64 => self.const_f64_like(vec![0.0_f64; n], shape),
6592            DType::BF16 => self.const_bf16_like(vec![half::bf16::ZERO; n], shape),
6593            DType::F16 => self.const_f16_like(vec![half::f16::ZERO; n], shape),
6594            DType::U32 => self.const_u32_like(vec![0_u32; n], shape),
6595            DType::I64 => self.const_i64_like(vec![0_i64; n], shape),
6596            other => {
6597                Err(fuel_ir::Error::Msg(format!("zeros_like: unsupported dtype {other:?}",)).bt())
6598            }
6599        }
6600    }
6601
6602    /// New tensor with `shape`/`dtype`/`device`, every element set to `1`.
6603    /// Static factory equivalent of eager's `Tensor::ones`. Returns Err for
6604    /// dtypes outside F32/F64/BF16/F16/U32.
6605    pub fn ones(
6606        shape: impl Into<Shape>,
6607        dtype: DType,
6608        device: &Device,
6609    ) -> std::result::Result<Self, fuel_ir::Error> {
6610        let shape = shape.into();
6611        let n = shape.elem_count();
6612        match dtype {
6613            DType::F32 => Self::from_f32(vec![1.0_f32; n], shape, device),
6614            DType::F64 => Self::from_f64(vec![1.0_f64; n], shape, device),
6615            DType::BF16 => Self::from_bf16(vec![half::bf16::ONE; n], shape, device),
6616            DType::F16 => Self::from_f16(vec![half::f16::ONE; n], shape, device),
6617            DType::U32 => Self::from_u32(vec![1_u32; n], shape, device),
6618            other => Err(fuel_ir::Error::Msg(format!("ones: unsupported dtype {other:?}",)).bt()),
6619        }
6620    }
6621
6622    /// New tensor with `shape`/`dtype`/`device`, every element set to `0`.
6623    /// Static factory equivalent of eager's `Tensor::zeros`. Returns Err for
6624    /// dtypes outside F32/F64/BF16/F16/U32.
6625    ///
6626    /// **Mints a NEW graph** — it delegates to the `from_*` constructors, so
6627    /// the tensor it returns cannot be combined with one built elsewhere. To
6628    /// add a zero tensor to an existing graph, use
6629    /// [`zeros_like`](Self::zeros_like) or a `const_*_like` builder. See
6630    /// [graph affinity](Self#tensors-are-graph-affine--read-this-before-building-anything).
6631    pub fn zeros(
6632        shape: impl Into<Shape>,
6633        dtype: DType,
6634        device: &Device,
6635    ) -> std::result::Result<Self, fuel_ir::Error> {
6636        let shape = shape.into();
6637        let n = shape.elem_count();
6638        match dtype {
6639            DType::F32 => Self::from_f32(vec![0.0_f32; n], shape, device),
6640            DType::F64 => Self::from_f64(vec![0.0_f64; n], shape, device),
6641            DType::BF16 => Self::from_bf16(vec![half::bf16::ZERO; n], shape, device),
6642            DType::F16 => Self::from_f16(vec![half::f16::ZERO; n], shape, device),
6643            DType::U32 => Self::from_u32(vec![0_u32; n], shape, device),
6644            other => Err(fuel_ir::Error::Msg(format!("zeros: unsupported dtype {other:?}",)).bt()),
6645        }
6646    }
6647
6648    /// New tensor of `shape`/`device` filled with `value`. The scalar's
6649    /// dtype determines the tensor's dtype. Returns Err for scalar dtypes
6650    /// outside F32/F64/BF16/F16/U32.
6651    ///
6652    /// **Mints a NEW graph** — it delegates to the `from_*` constructors, so
6653    /// the tensor it returns cannot be combined with one built elsewhere. To
6654    /// add a filled tensor to an existing graph, use a `const_*_like` builder.
6655    /// See [graph affinity](Self#tensors-are-graph-affine--read-this-before-building-anything).
6656    pub fn full(
6657        shape: impl Into<Shape>,
6658        value: fuel_ir::Scalar,
6659        device: &Device,
6660    ) -> std::result::Result<Self, fuel_ir::Error> {
6661        let shape = shape.into();
6662        let n = shape.elem_count();
6663        match value {
6664            fuel_ir::Scalar::F32(v) => Self::from_f32(vec![v; n], shape, device),
6665            fuel_ir::Scalar::F64(v) => Self::from_f64(vec![v; n], shape, device),
6666            fuel_ir::Scalar::BF16(v) => Self::from_bf16(vec![v; n], shape, device),
6667            fuel_ir::Scalar::F16(v) => Self::from_f16(vec![v; n], shape, device),
6668            fuel_ir::Scalar::U32(v) => Self::from_u32(vec![v; n], shape, device),
6669            other => Err(fuel_ir::Error::Msg(format!(
6670                "full: unsupported scalar dtype {:?}",
6671                other.dtype(),
6672            ))
6673            .bt()),
6674        }
6675    }
6676
6677    /// Identity matrix `[n, n]` with the given dtype on the given device.
6678    /// Built host-side as a flat Vec; no graph-layer arange dependency.
6679    pub fn eye(n: usize, dtype: DType, device: &Device) -> Self {
6680        let mut data = vec![0.0_f32; n * n];
6681        for i in 0..n {
6682            data[i * n + i] = 1.0;
6683        }
6684        // GAP-003 carve-out: `.expect` and NOT `?`, because this cannot fail and
6685        // saying it CAN would be a false claim in the signature -- `eye` would
6686        // return a `Result` that is never `Err`, and every caller would write a
6687        // `?` for an error that does not exist.
6688        //
6689        // THE PROOF IS LOCAL, two lines up: `data` is `vec![_; n * n]`, and the
6690        // shape `[n, n]` has `elem_count() == n * n`. Both are computed from `n`
6691        // in this function; no caller supplies either, so they cannot disagree.
6692        let base = Self::from_f32(data, vec![n, n], device).expect(
6693            "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",
6694        );
6695        if dtype == DType::F32 {
6696            base
6697        } else {
6698            base.to_dtype(dtype).unwrap()
6699        }
6700    }
6701
6702    /// Split a `(B, N, num_heads * head_dim)` projection into the
6703    /// multi-head attention layout `(B, num_heads, N, head_dim)`.
6704    /// Equivalent to `reshape(B, N, num_heads, head_dim).permute([0, 2, 1, 3])`
6705    /// — promoted to a method to retire the per-port reimplementations
6706    /// of this same composite.
6707    pub fn split_heads(
6708        &self,
6709        num_heads: usize,
6710        head_dim: usize,
6711    ) -> std::result::Result<Self, fuel_ir::Error> {
6712        let dims = self.inner.shape().dims().to_vec();
6713        debug_assert_eq!(
6714            dims.len(),
6715            3,
6716            "split_heads: input must be rank 3 (B, N, embed), got {dims:?}"
6717        );
6718        debug_assert_eq!(
6719            dims[2],
6720            num_heads * head_dim,
6721            "split_heads: trailing dim ({}) != num_heads * head_dim ({} * {} = {})",
6722            dims[2],
6723            num_heads,
6724            head_dim,
6725            num_heads * head_dim
6726        );
6727        let b = dims[0];
6728        let n = dims[1];
6729        self.reshape(Shape::from_dims(&[b, n, num_heads, head_dim]))?
6730            .permute([0, 2, 1, 3_usize])
6731    }
6732
6733    /// Merge a `(B, num_heads, N, head_dim)` attention result back
6734    /// into the projection layout `(B, N, num_heads * head_dim)`.
6735    /// Inverse of [`Self::split_heads`].
6736    pub fn merge_heads(&self) -> std::result::Result<Self, fuel_ir::Error> {
6737        let dims = self.inner.shape().dims().to_vec();
6738        debug_assert_eq!(
6739            dims.len(),
6740            4,
6741            "merge_heads: input must be rank 4 (B, heads, N, head_dim), got {dims:?}"
6742        );
6743        let b = dims[0];
6744        let num_heads = dims[1];
6745        let n = dims[2];
6746        let head_dim = dims[3];
6747        self.permute([0, 2, 1, 3_usize])?
6748            .reshape(Shape::from_dims(&[b, n, num_heads * head_dim]))
6749    }
6750
6751    /// Add a length-`bias.len()` bias vector to the trailing dim
6752    /// of `self`, broadcasting across all leading dims. The bias
6753    /// is materialized fresh on `self`'s graph from the supplied
6754    /// `Arc<[f32]>`.
6755    ///
6756    /// Common pattern after `WeightStorage::apply_linear` when the
6757    /// linear has a bias term but the activation tensor is on a
6758    /// different anchor than where the bias was originally
6759    /// allocated. Several lazy ports inlined this same 3-line
6760    /// helper as `bias_add` — promoted here to a method.
6761    pub fn add_trailing_bias(
6762        &self,
6763        bias: std::sync::Arc<[f32]>,
6764    ) -> std::result::Result<Self, fuel_ir::Error> {
6765        let n = bias.len();
6766        // Build the bias const in `self`'s dtype rather than hardcoding
6767        // f32: under BF16-throughout decode (Phase D increment A) the
6768        // activation stream is BF16 and `broadcast_add` asserts dtype
6769        // equality, so an f32 bias would panic. No-op for f32 activations
6770        // (every other caller today).
6771        let bias_t = self.const_like_dtype(&bias, Shape::from_dims(&[n]), self.dtype())?;
6772        self.broadcast_add(&bias_t)
6773    }
6774
6775    /// Embed `tokens` against an `[vocab_size, hidden]` embedding
6776    /// table held as `Arc<[f32]>`. Returns `(1, seq, hidden)`
6777    /// rank-3 hidden states ready to feed into a decoder backbone.
6778    ///
6779    /// Bootstraps a fresh graph anchored on a new const-f32 node.
6780    /// For composition with an already-built tensor (e.g.,
6781    /// multimodal models that need text embeddings on the audio
6782    /// graph), use [`Self::embed_tokens_anchored`] instead.
6783    ///
6784    /// Retires the 7-line `from_f32 + const_u32_like + index_select
6785    /// + reshape` ceremony every LLM port carried.
6786    pub fn embed_tokens(
6787        embed_table: std::sync::Arc<[f32]>,
6788        vocab_size: usize,
6789        hidden: usize,
6790        tokens: &[u32],
6791        device: &crate::Device,
6792    ) -> std::result::Result<Self, fuel_ir::Error> {
6793        let seq = tokens.len();
6794        if seq == 0 {
6795            return Err(fuel_ir::Error::Msg("embed_tokens: tokens must be non-empty".into()).bt());
6796        }
6797        let embed = Self::from_f32(embed_table, Shape::from_dims(&[vocab_size, hidden]), device)?;
6798        let token_ids = embed.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6799        embed
6800            .index_select(0_usize, &token_ids)?
6801            .reshape(Shape::from_dims(&[1, seq, hidden]))
6802    }
6803
6804    /// Variant of [`Self::embed_tokens`] that anchors the embedding
6805    /// table and token-id constants on the receiver's graph, so the
6806    /// resulting embeddings can compose with `self` and other
6807    /// tensors already on that graph. Used by multimodal models
6808    /// (vision + text, audio + text) where the text embeddings must
6809    /// live on the modality encoder's graph for cross-substitution
6810    /// to work.
6811    pub fn embed_tokens_anchored(
6812        &self,
6813        embed_table: std::sync::Arc<[f32]>,
6814        vocab_size: usize,
6815        hidden: usize,
6816        tokens: &[u32],
6817    ) -> std::result::Result<Self, fuel_ir::Error> {
6818        let seq = tokens.len();
6819        if seq == 0 {
6820            return Err(fuel_ir::Error::Msg(
6821                "embed_tokens_anchored: tokens must be non-empty".into(),
6822            )
6823            .bt());
6824        }
6825        let embed = self.const_f32_like(embed_table, Shape::from_dims(&[vocab_size, hidden]))?;
6826        let token_ids = self.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6827        embed
6828            .index_select(0_usize, &token_ids)?
6829            .reshape(Shape::from_dims(&[1, seq, hidden]))
6830    }
6831
6832    /// Build the standard (non-interleaved) RoPE cos/sin tables for
6833    /// `seq` positions starting at `start_pos`, anchored on the
6834    /// receiver's graph. Returns `(cos, sin)`, each with shape
6835    /// `[seq, head_dim]`.
6836    ///
6837    /// Delegates the actual `(theta, position) → (cos, sin)` host
6838    /// computation to [`fuel_graph::build_rope_tables`] (the canonical
6839    /// reference); only the const-tensor materialization is folded
6840    /// into one method to retire the per-port 4-line ceremony every
6841    /// LLM port did before calling `rope_with_tables`.
6842    pub fn rope_tables_const(
6843        &self,
6844        theta: f64,
6845        start_pos: usize,
6846        seq: usize,
6847        head_dim: usize,
6848    ) -> (Self, Self) {
6849        let (cos_data, sin_data) = fuel_graph::build_rope_tables(theta, start_pos, seq, head_dim);
6850        let rope_shape = Shape::from_dims(&[seq, head_dim]);
6851        // ⚠️ GAP-003 category 2: the proof EXISTS but is NOT LOCAL --
6852        // `build_rope_tables` owns the length and its `-> (Vec<f32>, Vec<f32>)`
6853        // signature does not carry it. The message names WHERE the guarantee
6854        // lives rather than claiming one. `-> (Self, Self)` has no error channel;
6855        // giving it one is a signature change, a different obligation. See #157.
6856        let rope_cos = self
6857            .const_f32_like(cos_data, rope_shape.clone())
6858            .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6859        let rope_sin = self
6860            .const_f32_like(sin_data, rope_shape)
6861            .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6862        (rope_cos, rope_sin)
6863    }
6864
6865    /// Constant-position RoPE cos/sin tables `[rows, head_dim]` where EVERY row
6866    /// is the *same* θ·`delta` rotation — the rung-2 delta-rotation tables. A
6867    /// cached prefix's keys, already rotated for their original positions, are
6868    /// shifted to `+delta` by applying this uniform rotation (RoPE composes
6869    /// additively per pair: `R(delta)·R(p) = R(p+delta)`).
6870    ///
6871    /// This is the position-`delta` row repeated `rows` times — NOT
6872    /// `rope_tables_const(theta, delta, rows, head_dim)`, which would give the
6873    /// *incrementing* progression `delta, delta+1, …` (the standard RoPE sweep).
6874    /// Routed through the canonical `fuel_graph::build_rope_tables` so it inherits
6875    /// the model's inv-freq/scaling exactly.
6876    pub fn rope_delta_tables_const(
6877        &self,
6878        theta: f64,
6879        delta: usize,
6880        rows: usize,
6881        head_dim: usize,
6882    ) -> (Self, Self) {
6883        let (c1, s1) = fuel_graph::build_rope_tables(theta, delta, 1, head_dim);
6884        let mut cos = Vec::with_capacity(rows * head_dim);
6885        let mut sin = Vec::with_capacity(rows * head_dim);
6886        for _ in 0..rows {
6887            cos.extend_from_slice(&c1);
6888            sin.extend_from_slice(&s1);
6889        }
6890        let shape = Shape::from_dims(&[rows, head_dim]);
6891        // ⚠️ GAP-003, HALF LOCAL. LOCAL: the loop appends `c1` exactly `rows`
6892        // times against a `[rows, head_dim]` shape. NOT LOCAL: that `c1` has
6893        // `head_dim` elements comes from `build_rope_tables`. Stated as both.
6894        (
6895            self.const_f32_like(cos, shape.clone()).expect(
6896                "rope tables: rows appends of c1 (local); c1 has head_dim elements (not local)",
6897            ),
6898            self.const_f32_like(sin, shape).expect(
6899                "rope tables: rows appends of s1 (local); s1 has head_dim elements (not local)",
6900            ),
6901        )
6902    }
6903
6904    /// Uniformly delta-rotate one cached (post-RoPE) K block by θ·`delta`,
6905    /// returning the shifted block in the SAME pool layout. Reuses the model's
6906    /// exact `rope_with_tables_decomposed` (rotate-half) rather than a hand-rolled
6907    /// rotation, so it inherits the model's RoPE convention + scaling and stays
6908    /// byte-exact with a direct rope-at-shifted-position. A single uniform
6909    /// rotation is correct for the whole block even though it holds `block_size`
6910    /// distinct original positions — the delta is the same for every position.
6911    ///
6912    /// `k_block` is the pool block layout, row-major `[block_size, n_kv_heads,
6913    /// head_dim]` (positions-major, as `DeviceKvPool::read_block` returns). RoPE
6914    /// wants `(position, dim)` as the last two axes, so heads are brought forward
6915    /// to the `project_qkv_roped` layout `[1, n_kv_heads, block_size, head_dim]`,
6916    /// rotated, and returned to `[block_size, n_kv_heads, head_dim]`. The rung-2
6917    /// numeric core, realized on `dev`.
6918    pub fn rope_delta_rotate_block_f32(
6919        dev: &crate::Device,
6920        k_block: &[f32],
6921        theta: f64,
6922        delta: usize,
6923        block_size: usize,
6924        n_kv_heads: usize,
6925        head_dim: usize,
6926    ) -> Vec<f32> {
6927        let pool_shape = Shape::from_dims(&[block_size, n_kv_heads, head_dim]);
6928        // ⚠️ GAP-003: NOT a carve-out and NOT a propagation, and the reason is
6929        // this function's SIGNATURE rather than anything about the operands.
6930        // `k_block` and the pool dims are BOTH caller-supplied, so there is no
6931        // local proof and the message below deliberately does not claim one.
6932        // `-> Vec<f32>` has no error channel, and the two `.expect`s already on
6933        // this same expression (permute, reshape) are the same pre-existing
6934        // condition -- converting one of three would be less honest, not more.
6935        //
6936        // The fix is this function returning `Result`, which is a DIFFERENT
6937        // obligation from the constructor-family ruling and is not in its scope.
6938        let k = Tensor::from_f32(k_block.to_vec(), pool_shape, dev).expect(
6939            "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)",
6940        );
6941        let k4 = k
6942            .permute([1, 0, 2])
6943            .expect("rope_delta_rotate_block_f32: permute [bs,Hkv,D]->[Hkv,bs,D]")
6944            .reshape(Shape::from_dims(&[1, n_kv_heads, block_size, head_dim]))
6945            .expect("rope_delta_rotate_block_f32: reshape to [1,Hkv,bs,D]");
6946        let (cos, sin) = k4.rope_delta_tables_const(theta, delta, block_size, head_dim);
6947        k4.rope_with_tables_decomposed(&cos, &sin)
6948            .expect("rope_delta_rotate_block_f32: rope_with_tables_decomposed")
6949            .reshape(Shape::from_dims(&[n_kv_heads, block_size, head_dim]))
6950            .expect("rope_delta_rotate_block_f32: reshape back to [Hkv,bs,D]")
6951            .permute([1, 0, 2])
6952            .expect("rope_delta_rotate_block_f32: permute [Hkv,bs,D]->[bs,Hkv,D]")
6953            .realize_f32()
6954    }
6955
6956    /// `[B, 1, 1, head_dim]` RoPE cos/sin tables, row `b` at absolute position
6957    /// `positions[b]` — the per-row generalization of [`Self::rope_tables_const`]
6958    /// (which carries one shared position). Built as a **single** const from `B`
6959    /// calls to `fuel_graph::build_rope_tables` (the canonical `(theta, position)
6960    /// → (cos, sin)` math), concatenated host-side — no `Op::Concat` chain. At
6961    /// `B == 1` the emitted data is bit-identical to `rope_tables_const`'s
6962    /// `[1, head_dim]` const (same builder call), so
6963    /// [`Self::rope_batched`] reduces to the single-position decomposed RoPE.
6964    ///
6965    /// This is the mechanism that lets a decode batch carry sessions at
6966    /// *different* positions (continuous batching) rather than requiring
6967    /// equal-length rows. `anchor` (`self`) supplies the graph.
6968    pub fn rope_tables_const_batched(
6969        &self,
6970        theta: f64,
6971        positions: &[usize],
6972        head_dim: usize,
6973    ) -> (Self, Self) {
6974        let b = positions.len();
6975        let mut cos_data = Vec::with_capacity(b * head_dim);
6976        let mut sin_data = Vec::with_capacity(b * head_dim);
6977        for &pos in positions {
6978            let (c, s) = fuel_graph::build_rope_tables(theta, pos, 1, head_dim);
6979            cos_data.extend_from_slice(&c);
6980            sin_data.extend_from_slice(&s);
6981        }
6982        let shape = Shape::from_dims(&[b, 1, 1, head_dim]);
6983        // ⚠️ GAP-003, and the proof is HALF local -- worth stating as such rather
6984        // than rounding to either category. LOCAL: `b` IS `positions.len()` (six
6985        // lines up) and the loop appends once per position, so the buffer has
6986        // `positions.len() * head_dim` elements against a `[b, 1, 1, head_dim]`
6987        // shape. NOT LOCAL: that each `build_rope_tables(_, _, 1, head_dim)` call
6988        // yields exactly `head_dim` elements lives in that function.
6989        let rope_cos = self
6990            .const_f32_like(cos_data, shape.clone())
6991            .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
6992        let rope_sin = self
6993            .const_f32_like(sin_data, shape)
6994            .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
6995        (rope_cos, rope_sin)
6996    }
6997
6998    /// [`Self::rope_tables_const`] with caller-supplied inverse frequencies
6999    /// (`inv_freq.len() == head_dim / 2`) in place of the ones derived from
7000    /// `theta` -- the scaled-RoPE (llama3 / yarn / linear) entry. Delegates the
7001    /// host math to [`fuel_graph::build_rope_tables_with_inv_freq`].
7002    pub fn rope_tables_const_with_inv_freq(
7003        &self,
7004        inv_freq: &[f64],
7005        start_pos: usize,
7006        seq: usize,
7007        head_dim: usize,
7008    ) -> (Self, Self) {
7009        let (cos_data, sin_data) =
7010            fuel_graph::build_rope_tables_with_inv_freq(inv_freq, start_pos, seq, head_dim);
7011        let rope_shape = Shape::from_dims(&[seq, head_dim]);
7012        // GAP-003: same non-local proof as `rope_tables_const` -- the length
7013        // lives in `build_rope_tables_with_inv_freq`, which has no error channel.
7014        let rope_cos = self.const_f32_like(cos_data, rope_shape.clone()).expect(
7015            "rope tables: build_rope_tables_with_inv_freq must return seq*head_dim elements",
7016        );
7017        let rope_sin = self.const_f32_like(sin_data, rope_shape).expect(
7018            "rope tables: build_rope_tables_with_inv_freq must return seq*head_dim elements",
7019        );
7020        (rope_cos, rope_sin)
7021    }
7022
7023    /// [`Self::rope_tables_const_batched`] with caller-supplied inverse
7024    /// frequencies (`inv_freq.len() == head_dim / 2`).
7025    pub fn rope_tables_const_batched_with_inv_freq(
7026        &self,
7027        inv_freq: &[f64],
7028        positions: &[usize],
7029        head_dim: usize,
7030    ) -> (Self, Self) {
7031        let b = positions.len();
7032        let mut cos_data = Vec::with_capacity(b * head_dim);
7033        let mut sin_data = Vec::with_capacity(b * head_dim);
7034        for &pos in positions {
7035            let (c, s) = fuel_graph::build_rope_tables_with_inv_freq(inv_freq, pos, 1, head_dim);
7036            cos_data.extend_from_slice(&c);
7037            sin_data.extend_from_slice(&s);
7038        }
7039        let shape = Shape::from_dims(&[b, 1, 1, head_dim]);
7040        // GAP-003: `b` IS `positions.len()` (local); the per-position length
7041        // lives in `build_rope_tables_with_inv_freq` (not local).
7042        let rope_cos = self
7043            .const_f32_like(cos_data, shape.clone())
7044            .expect("batched rope tables: b IS positions.len() (local); build_rope_tables_with_inv_freq must yield head_dim per position (not local)");
7045        let rope_sin = self
7046            .const_f32_like(sin_data, shape)
7047            .expect("batched rope tables: b IS positions.len() (local); build_rope_tables_with_inv_freq must yield head_dim per position (not local)");
7048        (rope_cos, rope_sin)
7049    }
7050
7051    /// Rotary position embedding with **per-batch-row** tables.
7052    ///
7053    /// `self` is `[B, H, 1, D]`; `cos`/`sin` are `[B, 1, 1, D]` (row `b` holds the
7054    /// table for sequence `b`'s own position — see [`Self::rope_tables_const_batched`]).
7055    /// Broadcasting the `[B, 1, 1, D]` tables over the head axis is what lets one
7056    /// table row serve all of that sequence's heads.
7057    ///
7058    /// This is `fuel_graph::NodeHandle::rope_with_tables_decomposed`'s body
7059    /// (`fuel-graph/src/lib.rs` ~6931-6954) with the table shape generalized from
7060    /// one shared position to one-per-row — **same ops, same order, same
7061    /// associativity** (`y = x·cos + concat(-x[…,D/2..], x[…,..D/2])·sin`), so at
7062    /// `B == 1` it is bit-identical to `rope_with_tables_decomposed`. Built on the
7063    /// public `Tensor` surface (not a new `fuel-graph` op) deliberately: the
7064    /// single-position [`Self::rope_with_tables_decomposed`] hard-requires
7065    /// `cos.dims() == [seq, d]`, so it cannot carry per-row tables, and rebuilding
7066    /// the lowering here avoids re-entering the `fuel-graph` rope op.
7067    ///
7068    /// (Adopted from the Lightbulb consumer's verified `rope_batched`; the
7069    /// mechanism belongs in Fuel, not the consumer.)
7070    pub fn rope_batched(
7071        &self,
7072        cos: &Self,
7073        sin: &Self,
7074    ) -> std::result::Result<Self, fuel_ir::Error> {
7075        let dims: Vec<usize> = self.inner.shape().dims().to_vec();
7076        let rank = dims.len();
7077        if rank < 2 {
7078            return Err(fuel_ir::Error::Msg(format!(
7079                "rope_batched: expected rank >= 2, got {dims:?}",
7080            ))
7081            .bt());
7082        }
7083        let d = dims[rank - 1];
7084        if !d.is_multiple_of(2) {
7085            return Err(
7086                fuel_ir::Error::Msg(format!("rope_batched: head_dim {d} must be even",)).bt(),
7087            );
7088        }
7089        let half = d / 2;
7090        let target = Shape::from_dims(&dims);
7091
7092        let cos_b = cos.broadcast_to(target.clone())?;
7093        let sin_b = sin.broadcast_to(target)?;
7094
7095        let first = self.slice(rank - 1, 0, half)?;
7096        let second = self.slice(rank - 1, half, half)?;
7097        let rotated = second.neg().concat(&first, rank - 1)?;
7098
7099        let left = self.mul(&cos_b)?;
7100        let right = rotated.mul(&sin_b)?;
7101        left.add(&right)
7102    }
7103
7104    /// Apply RoPE to the first `rope_dim` entries of each head and
7105    /// pass the remaining `head_dim - rope_dim` features through
7106    /// unchanged. `head_dim` is derived from the receiver's last-dim
7107    /// size. When `rope_dim == head_dim` this reduces to
7108    /// [`Self::rope_with_tables`].
7109    ///
7110    /// Implements the partial-rotary convention used by StableLM,
7111    /// Phi, Persimmon, MixFormer, RecurrentGemma, and Gemma-4 text —
7112    /// all the ports that carried an identical 5-line `slice + rope +
7113    /// concat` helper before this method.
7114    pub fn rope_partial(
7115        &self,
7116        rope_cos: &Self,
7117        rope_sin: &Self,
7118        rope_dim: usize,
7119    ) -> std::result::Result<Self, fuel_ir::Error> {
7120        let dims = self.inner.shape();
7121        let dims = dims.dims();
7122        let head_dim = *dims.last().ok_or_else(|| {
7123            fuel_ir::Error::Msg("rope_partial: receiver must have at least one dimension".into())
7124                .bt()
7125        })?;
7126        if rope_dim == head_dim {
7127            return self.rope_with_tables(rope_cos, rope_sin);
7128        }
7129        if rope_dim > head_dim {
7130            return Err(fuel_ir::Error::Msg(format!(
7131                "rope_partial: rope_dim={rope_dim} exceeds head_dim={head_dim}",
7132            ))
7133            .bt());
7134        }
7135        let last = dims.len() - 1;
7136        let pass_dim = head_dim - rope_dim;
7137        let rot = self.slice(last, 0, rope_dim)?;
7138        let pass = self.slice(last, rope_dim, pass_dim)?;
7139        let rot_rotated = rot.rope_with_tables(rope_cos, rope_sin)?;
7140        rot_rotated.concat(&pass, last)
7141    }
7142
7143    /// `Option<Arc<[f32]>>` variant of [`Self::add_trailing_bias`]: if
7144    /// `bias.is_none()`, return `self` unchanged; else apply
7145    /// `add_trailing_bias`. Models the `linear_b` / `linear_no_bias`
7146    /// branch every per-port `optional_bias` / `opt_bias` helper does.
7147    pub fn add_optional_trailing_bias(
7148        &self,
7149        bias: Option<&std::sync::Arc<[f32]>>,
7150    ) -> std::result::Result<Self, fuel_ir::Error> {
7151        match bias {
7152            None => Ok(self.clone()),
7153            Some(b) => self.add_trailing_bias(std::sync::Arc::clone(b)),
7154        }
7155    }
7156
7157    /// Logit-softcap: `cap · tanh(self / cap)`. Used by the Gemma-2 /
7158    /// Gemma-3 attention-logit and final-logit softcap branches. The
7159    /// math is identical regardless of where the cap is applied;
7160    /// retired the two per-port `softcap` / `apply_softcap` helpers in
7161    /// favor of this method.
7162    pub fn softcap(&self, cap: f64) -> Self {
7163        self.mul_scalar(1.0 / cap).tanh().mul_scalar(cap)
7164    }
7165
7166    /// `Option<f64>` variant of [`Self::softcap`]: when `cap.is_none()`
7167    /// or `cap <= 0.0`, return `self` unchanged; else apply
7168    /// [`Self::softcap`]. Mirrors the optional-bias pattern.
7169    pub fn softcap_optional(&self, cap: Option<f64>) -> Self {
7170        match cap {
7171            Some(c) if c > 0.0 => self.softcap(c),
7172            _ => self.clone(),
7173        }
7174    }
7175
7176    /// Apply RMSNorm along the last dim with `(gain + offset) · x`.
7177    /// Equivalent to [`Self::rms_norm_affine`] after adding a scalar
7178    /// to every gain element — used by Gemma-family ports where
7179    /// the stored gain represents `gain - 1` and the runtime path
7180    /// must reconstruct `gain + 1`.
7181    ///
7182    /// Materializes the shifted gain on the receiver's graph; one
7183    /// allocation per call.
7184    pub fn rms_norm_affine_with_offset(
7185        &self,
7186        gain: &[f32],
7187        offset: f32,
7188        eps: f64,
7189    ) -> std::result::Result<Self, fuel_ir::Error> {
7190        let shifted: std::sync::Arc<[f32]> =
7191            std::sync::Arc::from(gain.iter().map(|g| *g + offset).collect::<Vec<_>>());
7192        self.rms_norm_affine(shifted, eps)
7193    }
7194
7195    /// Apply RMSNorm along the last dim with an affine `gain · x`
7196    /// post-step (no bias — RMSNorm has no β term). `gain` is a
7197    /// length-`gain.len()` vector materialized fresh on the
7198    /// receiver's graph and broadcast across all leading dims.
7199    pub fn rms_norm_affine(
7200        &self,
7201        gain: std::sync::Arc<[f32]>,
7202        eps: f64,
7203    ) -> std::result::Result<Self, fuel_ir::Error> {
7204        let hidden = gain.len();
7205        let normed = self.rms_norm_last_dim(eps)?;
7206        let gain_t = self.const_f32_like(gain, Shape::from_dims(&[hidden]))?;
7207        normed.broadcast_mul(&gain_t)
7208    }
7209
7210    /// Global average pool over the spatial dims of a rank-4
7211    /// `(B, C, H, W)` tensor: reduces dims 2 and 3, returning
7212    /// `(B, C)`. For the keepdim variant (`(B, C, 1, 1)`, used by
7213    /// SE blocks), follow with `.reshape(Shape::from_dims(&[B, C, 1, 1]))`.
7214    ///
7215    /// Backs the classification heads of every conv vision port
7216    /// (ResNet, EfficientNet, ConvMixer, FastViT, MobileNetV4,
7217    /// MobileOne, RepVGG, ConvNeXt, EfficientViT, etc.) plus the
7218    /// pre-projection pool inside each squeeze-excite block.
7219    pub fn global_avg_pool_2d(&self) -> std::result::Result<Self, fuel_ir::Error> {
7220        let dims = self.inner.shape().dims().to_vec();
7221        debug_assert_eq!(
7222            dims.len(),
7223            4,
7224            "global_avg_pool_2d: input must be rank 4 (B, C, H, W), got {dims:?}"
7225        );
7226        // Reduce W first (dim 3), then H (dim 2 of the H-reduced (B, C, H) tensor).
7227        self.mean_dim(3_usize)?.mean_dim(2_usize)
7228    }
7229
7230    /// Apply a per-channel affine `gain · x + bias` to a rank-4
7231    /// `(B, C, H, W)` tensor. Both `gain` and `bias` are length-`C`
7232    /// vectors materialized fresh on the receiver's graph and
7233    /// broadcast across the spatial axes.
7234    ///
7235    /// Equivalent to fused-affine BatchNorm at inference time:
7236    /// the running mean / running var / eps are absorbed at load
7237    /// time into `gain = γ / sqrt(var + eps)` and
7238    /// `bias = β - μ · γ / sqrt(var + eps)`, so the runtime forward
7239    /// is just this multiply-add. Used by inference-only conv
7240    /// vision ports (ResNet, EfficientNet, FastViT, etc.).
7241    pub fn channel_affine_4d(
7242        &self,
7243        gain: std::sync::Arc<[f32]>,
7244        bias: std::sync::Arc<[f32]>,
7245    ) -> std::result::Result<Self, fuel_ir::Error> {
7246        let dims = self.inner.shape().dims().to_vec();
7247        debug_assert_eq!(
7248            dims.len(),
7249            4,
7250            "channel_affine_4d: input must be rank 4 (B, C, H, W), got {dims:?}"
7251        );
7252        let channels = dims[1];
7253        debug_assert_eq!(
7254            gain.len(),
7255            channels,
7256            "channel_affine_4d: gain len ({}) != C ({})",
7257            gain.len(),
7258            channels
7259        );
7260        debug_assert_eq!(
7261            bias.len(),
7262            channels,
7263            "channel_affine_4d: bias len ({}) != C ({})",
7264            bias.len(),
7265            channels
7266        );
7267        let w_t = self
7268            .const_f32_like(gain, Shape::from_dims(&[channels]))?
7269            .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7270        let b_t = self
7271            .const_f32_like(bias, Shape::from_dims(&[channels]))?
7272            .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7273        self.broadcast_mul(&w_t)?.broadcast_add(&b_t)
7274    }
7275
7276    /// Build the strict additive causal mask `(seq_len, seq_len)`
7277    /// anchored on `anchor`'s graph: 0 on and below the diagonal,
7278    /// `f32::NEG_INFINITY` above it. Add to attention scores before
7279    /// softmax to enforce strict causality (position `i` cannot
7280    /// attend to position `j > i`).
7281    ///
7282    /// Equivalent to the `(T, T)` mask several ports build inline
7283    /// — promoted here so call sites stop drifting.
7284    pub fn additive_causal_mask_like(anchor: &Tensor, seq_len: usize) -> Self {
7285        let mut data = vec![0.0_f32; seq_len * seq_len];
7286        for i in 0..seq_len {
7287            for j in (i + 1)..seq_len {
7288                data[i * seq_len + j] = f32::NEG_INFINITY;
7289            }
7290        }
7291        // GAP-003 carve-out, PROOF LOCAL: `data` is `vec![_; seq_len * seq_len]`
7292        // six lines up and the loop only writes IN PLACE -- it cannot change the
7293        // length -- against a `[seq_len, seq_len]` shape. Both from `seq_len`.
7294        anchor
7295            .const_f32_like(
7296                std::sync::Arc::from(data),
7297                Shape::from_dims(&[seq_len, seq_len]),
7298            )
7299            .expect(
7300                "additive_causal_mask_like: vec![_; seq_len*seq_len] against [seq_len, seq_len]",
7301            )
7302    }
7303
7304    /// Lower-triangular ones matrix `[n, n]`. `tril2(n).to_dtype(dtype)` is
7305    /// the causal-attention-mask building block.
7306    pub fn tril2(n: usize, dtype: DType, device: &Device) -> Self {
7307        let mut data = vec![0.0_f32; n * n];
7308        for i in 0..n {
7309            for j in 0..=i {
7310                data[i * n + j] = 1.0;
7311            }
7312        }
7313        // GAP-003 carve-out: `.expect` and NOT `?`, because this cannot fail and
7314        // saying it CAN would be a false claim in the signature -- `tril2` would
7315        // return a `Result` that is never `Err`, and every caller would write a
7316        // `?` for an error that does not exist.
7317        //
7318        // THE PROOF IS LOCAL, two lines up: `data` is `vec![_; n * n]`, and the
7319        // shape `[n, n]` has `elem_count() == n * n`. Both are computed from `n`
7320        // in this function; no caller supplies either, so they cannot disagree.
7321        let base = Self::from_f32(data, vec![n, n], device).expect(
7322            "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",
7323        );
7324        if dtype == DType::F32 {
7325            base
7326        } else {
7327            base.to_dtype(dtype).unwrap()
7328        }
7329    }
7330
7331    /// Upper-triangular ones matrix `[n, n]`.
7332    pub fn triu2(n: usize, dtype: DType, device: &Device) -> Self {
7333        let mut data = vec![0.0_f32; n * n];
7334        for i in 0..n {
7335            for j in i..n {
7336                data[i * n + j] = 1.0;
7337            }
7338        }
7339        // GAP-003 carve-out: `.expect` and NOT `?`, because this cannot fail and
7340        // saying it CAN would be a false claim in the signature -- `triu2` would
7341        // return a `Result` that is never `Err`, and every caller would write a
7342        // `?` for an error that does not exist.
7343        //
7344        // THE PROOF IS LOCAL, two lines up: `data` is `vec![_; n * n]`, and the
7345        // shape `[n, n]` has `elem_count() == n * n`. Both are computed from `n`
7346        // in this function; no caller supplies either, so they cannot disagree.
7347        let base = Self::from_f32(data, vec![n, n], device).expect(
7348            "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",
7349        );
7350        if dtype == DType::F32 {
7351            base
7352        } else {
7353            base.to_dtype(dtype).unwrap()
7354        }
7355    }
7356
7357    // ---- additional deferred-Phase-A items: indexing / multi-dim / RNG ----
7358
7359    /// Eager-API alias of [`Self::slice`] (PyTorch / Candle naming).
7360    /// `narrow(dim, start, len)` is `slice(dim, start, len)` —
7361    /// produces a view of `[start, start+len)` along `dim`. Bad input
7362    /// surfaces as a typed error at build time. Accepts any [`Dim`].
7363    pub fn narrow<D: Dim>(
7364        &self,
7365        dim: D,
7366        start: usize,
7367        len: usize,
7368    ) -> std::result::Result<Self, fuel_ir::Error> {
7369        self.slice(dim, start, len)
7370    }
7371
7372    /// Split into `chunks` views along `dim`. The split distributes the
7373    /// `chunk_size = ceil(dim_size / chunks)` extra slot to the leading
7374    /// chunks so every chunk's size differs by at most 1. If `dim_size
7375    /// < chunks`, returns `dim_size` singleton chunks instead of
7376    /// `chunks` chunks (matches eager / PyTorch). Accepts any [`Dim`].
7377    pub fn chunk<D: Dim>(
7378        &self,
7379        chunks: usize,
7380        dim: D,
7381    ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
7382        let shape = self.shape();
7383        let dim = dim.to_index(&shape, "chunk")?;
7384        if chunks == 0 {
7385            return Err(fuel_ir::Error::Msg("chunk: chunks must be > 0".into()).bt());
7386        }
7387        let dims = shape.dims();
7388        let size = dims[dim];
7389        if size < chunks {
7390            return Ok((0..size).map(|i| self.slice(dim, i, 1).unwrap()).collect());
7391        }
7392        let base = size / chunks;
7393        let extra = size % chunks;
7394        let mut out = Vec::with_capacity(chunks);
7395        let mut start = 0;
7396        for i in 0..chunks {
7397            let len = if i < extra { base + 1 } else { base };
7398            out.push(self.slice(dim, start, len).unwrap());
7399            start += len;
7400        }
7401        Ok(out)
7402    }
7403
7404    /// Sub-tensor at index `i` along dim 0. Equivalent to
7405    /// `self.slice(0, i, 1).unwrap().squeeze(0)`. Matches eager's `crate::Tensor::get`.
7406    pub fn get(&self, i: usize) -> std::result::Result<Self, fuel_ir::Error> {
7407        let dims = self.shape().dims().to_vec();
7408        if dims.is_empty() {
7409            return Ok(self.clone());
7410        }
7411        self.slice(0, i, 1).unwrap().squeeze(0)
7412    }
7413
7414    /// Sub-tensor at index along an arbitrary dim. Equivalent to
7415    /// `self.slice(dim, index, 1).unwrap().squeeze(dim)`. Matches eager's
7416    /// `crate::Tensor::get_on_dim`. Accepts any [`Dim`].
7417    pub fn get_on_dim<D: Dim>(
7418        &self,
7419        dim: D,
7420        index: usize,
7421    ) -> std::result::Result<Self, fuel_ir::Error> {
7422        let shape = self.shape();
7423        let dim = dim.to_index(&shape, "get_on_dim")?;
7424        self.slice(dim, index, 1).unwrap().squeeze(dim)
7425    }
7426
7427    /// Multi-dim sum: reduce over every dim in `dims`, squeezing each.
7428    /// Reduces from the highest dim down so the lower dim indices stay
7429    /// valid throughout the reduction.
7430    pub fn sum_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7431        let shape = self.shape();
7432        let mut sorted = dims.to_indexes(&shape, "sum_dims")?;
7433        sorted.sort_by(|a, b| b.cmp(a));
7434        sorted.dedup();
7435        let mut acc = self.clone();
7436        for d in sorted {
7437            acc = acc.sum_dim(d)?;
7438        }
7439        Ok(acc)
7440    }
7441
7442    /// Multi-dim mean: reduce over every dim in `dims`, squeezing each.
7443    /// Reduces from the highest dim down. Accepts any [`Dims`].
7444    pub fn mean_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7445        let shape = self.shape();
7446        let mut sorted = dims.to_indexes(&shape, "mean_dims")?;
7447        sorted.sort_by(|a, b| b.cmp(a));
7448        sorted.dedup();
7449        let mut acc = self.clone();
7450        for d in sorted {
7451            acc = acc.mean_dim(d)?;
7452        }
7453        Ok(acc)
7454    }
7455
7456    /// Multi-dim sum with keepdim: every named dim becomes size 1
7457    /// instead of being squeezed out. Reduce-order-invariant (every
7458    /// keepdim preserves indices). Returns Result because of cascade
7459    /// from [`Self::sum_keepdim`].
7460    pub fn sum_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7461        let mut sorted: Vec<usize> = dims.to_vec();
7462        sorted.sort();
7463        sorted.dedup();
7464        let mut acc = self.clone();
7465        for d in sorted {
7466            acc = acc.sum_keepdim(d)?;
7467        }
7468        Ok(acc)
7469    }
7470
7471    /// Multi-dim mean with keepdim.
7472    pub fn mean_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7473        let mut sorted: Vec<usize> = dims.to_vec();
7474        sorted.sort();
7475        sorted.dedup();
7476        let mut acc = self.clone();
7477        for d in sorted {
7478            acc = acc.mean_keepdim(d)?;
7479        }
7480        Ok(acc)
7481    }
7482
7483    /// Uniform random tensor in `[lo, up)` with shape/dtype/device matching `self`.
7484    /// Backed by [`rand::rng`]. Returns Err for unsupported dtypes.
7485    pub fn rand_like(&self, lo: f64, up: f64) -> std::result::Result<Self, fuel_ir::Error> {
7486        Self::rand(self.shape(), lo, up, self.dtype(), &Device::cpu())
7487    }
7488
7489    /// Normal random tensor with shape/dtype/device matching `self`.
7490    /// Returns Err for unsupported dtypes or invalid stdev.
7491    pub fn randn_like(&self, mean: f64, stdev: f64) -> std::result::Result<Self, fuel_ir::Error> {
7492        Self::randn(self.shape(), mean, stdev, self.dtype(), &Device::cpu())
7493    }
7494
7495    /// Uniform random tensor in `[lo, up)`. Static factory.
7496    /// Supported dtypes: F32, F64, BF16, F16. F32 is the typical
7497    /// initialization target. Returns Err for any other dtype.
7498    pub fn rand(
7499        shape: impl Into<Shape>,
7500        lo: f64,
7501        up: f64,
7502        dtype: DType,
7503        device: &Device,
7504    ) -> std::result::Result<Self, fuel_ir::Error> {
7505        use rand::RngExt;
7506        let shape = shape.into();
7507        let n = shape.elem_count();
7508        let mut rng = rand::rng();
7509        match dtype {
7510            DType::F32 => {
7511                let data: Vec<f32> = (0..n).map(|_| rng.random_range(lo..up) as f32).collect();
7512                Self::from_f32(data, shape, device)
7513            }
7514            DType::F64 => {
7515                let data: Vec<f64> = (0..n).map(|_| rng.random_range(lo..up)).collect();
7516                Self::from_f64(data, shape, device)
7517            }
7518            DType::BF16 => {
7519                let data: Vec<half::bf16> = (0..n)
7520                    .map(|_| half::bf16::from_f64(rng.random_range(lo..up)))
7521                    .collect();
7522                Self::from_bf16(data, shape, device)
7523            }
7524            DType::F16 => {
7525                let data: Vec<half::f16> = (0..n)
7526                    .map(|_| half::f16::from_f64(rng.random_range(lo..up)))
7527                    .collect();
7528                Self::from_f16(data, shape, device)
7529            }
7530            other => {
7531                Err(fuel_ir::Error::Msg(format!("Tensor::rand: unsupported dtype {other:?}",)).bt())
7532            }
7533        }
7534    }
7535
7536    /// Normal random tensor with given `mean` and `stdev`. Static factory.
7537    /// Supported dtypes: F32, F64, BF16, F16. Returns Err on any other
7538    /// dtype, or if `stdev` is not finite / not positive.
7539    pub fn randn(
7540        shape: impl Into<Shape>,
7541        mean: f64,
7542        stdev: f64,
7543        dtype: DType,
7544        device: &Device,
7545    ) -> std::result::Result<Self, fuel_ir::Error> {
7546        use rand_distr::{Distribution, Normal};
7547        let shape = shape.into();
7548        let n = shape.elem_count();
7549        let normal = Normal::new(mean, stdev).map_err(|e| {
7550            fuel_ir::Error::Msg(format!("Tensor::randn: invalid stdev={stdev}: {e}",)).bt()
7551        })?;
7552        let mut rng = rand::rng();
7553        match dtype {
7554            DType::F32 => {
7555                let data: Vec<f32> = (0..n).map(|_| normal.sample(&mut rng) as f32).collect();
7556                Self::from_f32(data, shape, device)
7557            }
7558            DType::F64 => {
7559                let data: Vec<f64> = (0..n).map(|_| normal.sample(&mut rng)).collect();
7560                Self::from_f64(data, shape, device)
7561            }
7562            DType::BF16 => {
7563                let data: Vec<half::bf16> = (0..n)
7564                    .map(|_| half::bf16::from_f64(normal.sample(&mut rng)))
7565                    .collect();
7566                Self::from_bf16(data, shape, device)
7567            }
7568            DType::F16 => {
7569                let data: Vec<half::f16> = (0..n)
7570                    .map(|_| half::f16::from_f64(normal.sample(&mut rng)))
7571                    .collect();
7572                Self::from_f16(data, shape, device)
7573            }
7574            other => Err(fuel_ir::Error::Msg(format!(
7575                "Tensor::randn: unsupported dtype {other:?}",
7576            ))
7577            .bt()),
7578        }
7579    }
7580
7581    /// `arange(start, end, device)`: a rank-1 tensor of `[start, end)` in
7582    /// step 1, dtype F32. Matches NumPy / PyTorch convention.
7583    pub fn arange(start: f32, end: f32, device: &Device) -> Self {
7584        Self::arange_step(start, end, 1.0, device)
7585    }
7586
7587    /// `arange_step(start, end, step, device)`: a rank-1 tensor of
7588    /// `[start, end)` with constant step. F32 only for the static
7589    /// factory; cast for other dtypes. Errors at runtime if `step ==
7590    /// 0`.
7591    pub fn arange_step(start: f32, end: f32, step: f32, device: &Device) -> Self {
7592        assert!(step != 0.0, "arange_step: step must be non-zero");
7593        let mut data = Vec::new();
7594        let mut current = start;
7595        if step > 0.0 {
7596            while current < end {
7597                data.push(current);
7598                current += step;
7599            }
7600        } else {
7601            while current > end {
7602                data.push(current);
7603                current += step;
7604            }
7605        }
7606        let n = data.len();
7607        // GAP-003 carve-out. `.expect`, NOT `?`: this cannot fail, and a `Result`
7608        // here would be one that is never `Err` -- a false claim in the signature
7609        // that every caller would then write a `?` for.
7610        //
7611        // THE PROOF IS LOCAL AND ONE LINE UP: `n` IS `data.len()`, and the shape
7612        // is `[n]`. The shape is derived FROM the buffer, so they cannot disagree.
7613        Self::from_f32(data, vec![n], device).expect(
7614            "arange_step: shape is [n] where n IS data.len(), computed one line              above from that same buffer -- the lengths cannot disagree",
7615        )
7616    }
7617
7618    /// Linearly-spaced 1D tensor with `n` points from `start` to `end`
7619    /// (inclusive on both ends). Matches NumPy's `linspace`.
7620    pub fn linspace(start: f32, end: f32, n: usize, device: &Device) -> Self {
7621        assert!(n >= 1, "linspace: n must be >= 1");
7622        if n == 1 {
7623            // GAP-003 carve-out: a one-element vec with shape [1], on one line.
7624            return Self::from_f32(vec![start], vec![1], device)
7625                .expect("linspace: vec![start] has exactly 1 element and the shape is [1]");
7626        }
7627        let step = (end - start) / ((n - 1) as f32);
7628        let data: Vec<f32> = (0..n).map(|i| start + step * (i as f32)).collect();
7629        // GAP-003 carve-out: `data` is a `(0..n)` map -- exactly `n` elements --
7630        // and the shape is `[n]`. Both from the same `n`, two lines apart.
7631        Self::from_f32(data, vec![n], device).expect(
7632            "linspace: data is (0..n).map(..).collect() so it has exactly n              elements, and the shape is [n] -- both from the same `n`",
7633        )
7634    }
7635
7636    /// Frobenius norm: `sqrt(sum(self * self))`. Returns a scalar tensor.
7637    pub fn norm(&self) -> Self {
7638        self.sqr().sum_all().sqrt()
7639    }
7640
7641    /// General 1-D cross-correlation. Shapes:
7642    /// - `self`: `[N, Cin, T]`
7643    /// - `weight`: `[Cout, Cin/groups, K]`
7644    /// - `bias` (optional): `[Cout]`
7645    /// - returns: `[N, Cout, Tout]` where `Tout = (T + 2·padding - K) /
7646    ///   stride + 1`
7647    ///
7648    /// Implemented as a composite via Conv2D: unsqueeze the spatial dim
7649    /// to make a unit `H = 1`, run `Conv2D` with `Kh = 1, stride.0 = 1,
7650    /// padding.0 = 0`, then squeeze the dim back out. Works through
7651    /// every backend's Conv2D dispatch (CPU, CUDA via baracuda,
7652    /// Vulkan, AOCL, MKL). The future fused `Op::Conv1D` will collapse
7653    /// the unsqueeze/squeeze pair when a high-volume Conv1D consumer
7654    /// materializes.
7655    pub fn conv1d(
7656        &self,
7657        weight: &Self,
7658        bias: Option<&Self>,
7659        stride: usize,
7660        padding: usize,
7661        groups: usize,
7662    ) -> std::result::Result<Self, fuel_ir::Error> {
7663        let x_dims = self.shape().dims().to_vec();
7664        let w_dims = weight.shape().dims().to_vec();
7665        if x_dims.len() != 3 {
7666            return Err(fuel_ir::Error::Msg(format!(
7667                "conv1d: x must be rank 3 [N, Cin, T], got {x_dims:?}",
7668            ))
7669            .bt());
7670        }
7671        if w_dims.len() != 3 {
7672            return Err(fuel_ir::Error::Msg(format!(
7673                "conv1d: weight must be rank 3 [Cout, Cin/groups, K], got {w_dims:?}",
7674            ))
7675            .bt());
7676        }
7677        if groups < 1 {
7678            return Err(fuel_ir::Error::Msg("conv1d: groups must be ≥ 1".into()).bt());
7679        }
7680        if stride < 1 {
7681            return Err(fuel_ir::Error::Msg("conv1d: stride must be ≥ 1".into()).bt());
7682        }
7683        // Add a unit H dim at index 2 → [N, Cin, 1, T] and [Cout, Cin/g, 1, K].
7684        let x_4d = self.unsqueeze(2_usize)?;
7685        let w_4d = weight.unsqueeze(2_usize)?;
7686        let out_4d = x_4d.conv2d(&w_4d, bias, (1, stride), (0, padding), groups)?;
7687        out_4d.squeeze(2)
7688    }
7689
7690    /// Eager-API parity for `conv1d_with_algo`. The `_algo` selector is
7691    /// ignored on the lazy path — algorithm selection happens at
7692    /// backend dispatch time, not at graph construction. Reduces to
7693    /// [`Self::conv1d`].
7694    pub fn conv1d_with_algo<A>(
7695        &self,
7696        weight: &Self,
7697        bias: Option<&Self>,
7698        stride: usize,
7699        padding: usize,
7700        groups: usize,
7701        _algo: A,
7702    ) -> std::result::Result<Self, fuel_ir::Error> {
7703        self.conv1d(weight, bias, stride, padding, groups)
7704    }
7705
7706    /// 2-D average pooling. Input `[N, C, H, W]`, output
7707    /// `[N, C, H_out, W_out]` where
7708    /// `H_out = (H + 2·padding.0 - kernel.0) / stride.0 + 1`.
7709    ///
7710    /// Implemented as a **depthwise Conv2D** with a constant
7711    /// `1/(kh·kw)` kernel: one graph node + the kernel const. Works
7712    /// through every backend's Conv2D dispatch and inherits Conv2D's
7713    /// gradient. Composite supports arbitrary kernel / stride /
7714    /// padding.
7715    pub fn avg_pool2d(
7716        &self,
7717        kernel: (usize, usize),
7718        stride: (usize, usize),
7719        padding: (usize, usize),
7720        count_include_pad: bool,
7721    ) -> std::result::Result<Self, fuel_ir::Error> {
7722        let dims = self.shape().dims().to_vec();
7723        if dims.len() != 4 {
7724            return Err(fuel_ir::Error::Msg(format!(
7725                "avg_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7726            ))
7727            .bt());
7728        }
7729        let (c, h, w) = (dims[1], dims[2], dims[3]);
7730        let (kh, kw) = kernel;
7731        if kh == 0 || kw == 0 {
7732            return Err(
7733                fuel_ir::Error::Msg("avg_pool2d: kernel sizes must be positive".into()).bt(),
7734            );
7735        }
7736
7737        // KISS-Ops §6.13: count_include_pad must be an EXPLICIT choice, not
7738        // an implicit torch-style default silently baked into the divisor.
7739        // With no padding the two modes coincide (every window is full),
7740        // so the uniform-divisor path below is exact either way.
7741        if count_include_pad || padding == (0, 0) {
7742            let inv = 1.0_f32 / ((kh * kw) as f32);
7743            // Depthwise kernel: one filter per input channel, each filter
7744            // is a constant 1/(kh·kw). Shape [C, 1, kh, kw] with groups=C
7745            // makes Conv2D compute one independent kernel per channel.
7746            let weight =
7747                self.const_f32_like(vec![inv; c * kh * kw], Shape::from_dims(&[c, 1, kh, kw]))?;
7748            return self.conv2d(&weight, None, stride, padding, c);
7749        }
7750
7751        // count_include_pad = false with real padding: a uniform divisor is
7752        // WRONG at every border window (it silently averages in the padded
7753        // zeros as if they were real taps, undercounting the true mean by
7754        // exactly the fraction of the window that's padding). The correct
7755        // divisor is the per-position COUNT of valid (non-padded) taps, which
7756        // varies by output position — sum-pool (an all-ones depthwise kernel)
7757        // then divide by that count map instead of a constant.
7758        //
7759        // A window can have ZERO valid taps (e.g. a 1x1 input with padding
7760        // >= kernel size): every tap it covers is padding. The count map
7761        // would then be zero at that position and `broadcast_div` would
7762        // silently emit NaN/inf instead of a defined average. Refuse up
7763        // front rather than let that propagate -- computed host-side from
7764        // (H, W, kernel, stride, padding) alone, no graph ops needed.
7765        if let Some((axis, out_idx)) = first_zero_tap_output_index(h, kh, stride.0, padding.0)
7766            .map(|i| ("H", i))
7767            .or_else(|| first_zero_tap_output_index(w, kw, stride.1, padding.1).map(|i| ("W", i)))
7768        {
7769            return Err(fuel_ir::Error::Msg(format!(
7770                "avg_pool2d: count_include_pad=false has no valid (non-padded) \
7771                 taps for output position {out_idx} on the {axis} axis \
7772                 (kernel={kernel:?}, stride={stride:?}, padding={padding:?}, \
7773                 input H×W={h}×{w}) — the average would divide by zero",
7774            ))
7775            .bt());
7776        }
7777
7778        let sum_weight = self.const_f32_like(
7779            vec![1.0_f32; c * kh * kw],
7780            Shape::from_dims(&[c, 1, kh, kw]),
7781        )?;
7782        let summed = self.conv2d(&sum_weight, None, stride, padding, c)?;
7783
7784        // The valid-tap count depends only on (H, W, kernel, stride,
7785        // padding) -- not on N or C -- so it's computed once at minimal
7786        // [1, 1, H, W] size and broadcast-divided into every (n, c) plane.
7787        let ones_hw = self.const_f32_like(vec![1.0_f32; h * w], Shape::from_dims(&[1, 1, h, w]))?;
7788        let count_weight =
7789            self.const_f32_like(vec![1.0_f32; kh * kw], Shape::from_dims(&[1, 1, kh, kw]))?;
7790        let count = ones_hw.conv2d(&count_weight, None, stride, padding, 1)?;
7791
7792        summed.broadcast_div(&count)
7793    }
7794
7795    /// Eager-API parity for `avg_pool2d_with_stride`. Same shape as
7796    /// [`Self::avg_pool2d`] but the stride is passed explicitly rather
7797    /// than inferred from kernel.
7798    pub fn avg_pool2d_with_stride(
7799        &self,
7800        kernel: (usize, usize),
7801        stride: (usize, usize),
7802    ) -> std::result::Result<Self, fuel_ir::Error> {
7803        // No padding, so count_include_pad is a don't-care (both modes
7804        // coincide — see avg_pool2d's own short-circuit for this case).
7805        self.avg_pool2d(kernel, stride, (0, 0), true)
7806    }
7807
7808    /// 2-D max pooling. Input `[N, C, H, W]`, output
7809    /// `[N, C, H_out, W_out]` where
7810    /// `H_out = (H + 2·padding.0 - kernel.0) / stride.0 + 1`.
7811    ///
7812    /// Composite via slice + maximum: pad the input, then for every
7813    /// `(ky, kx)` in `[0..kh, 0..kw]` slice the strided grid of taps
7814    /// (one tap per output position) and take the elementwise max.
7815    /// Produces `kh·kw` nodes per call plus padding — cheap for the
7816    /// common 2×2 / 3×3 cases.
7817    ///
7818    /// Strided-slice trick: for stride `sh`, reshape the padded H from
7819    /// `(H_out · sh)` to `(H_out, sh)`, then slice the inner `sh`-dim
7820    /// at the tap index. Requires `H_padded == H_out · sh` exactly.
7821    /// Inputs that don't divide cleanly will be padded so they do.
7822    pub fn max_pool2d(
7823        &self,
7824        kernel: (usize, usize),
7825        stride: (usize, usize),
7826        padding: (usize, usize),
7827    ) -> std::result::Result<Self, fuel_ir::Error> {
7828        // KISS-Ops §6.13 / PyTorch-correct semantics: a padded slot must
7829        // never win the max — zero padding silently wins every all-negative
7830        // window (e.g. post-activation feature maps dominated by negative
7831        // values at the border), which is a real correctness bug, not a
7832        // style choice. `f32::NEG_INFINITY` padding is the fix; use
7833        // [`Self::max_pool2d_with_pad_value`] directly for any OTHER pad
7834        // value (there is no implicit torch-style default here anymore).
7835        self.max_pool2d_with_pad_value(kernel, stride, padding, f32::NEG_INFINITY)
7836    }
7837
7838    /// `max_pool2d` variant where the boundary padding is filled with an
7839    /// explicit `pad_value` instead of `0.0`. Pass `f32::NEG_INFINITY`
7840    /// for PyTorch-correct semantics (padded slots can never win the
7841    /// max). All other constraints match [`Self::max_pool2d`]; the only
7842    /// difference is the constant value the implicit boundary pad uses.
7843    pub fn max_pool2d_with_pad_value(
7844        &self,
7845        kernel: (usize, usize),
7846        stride: (usize, usize),
7847        padding: (usize, usize),
7848        pad_value: f32,
7849    ) -> std::result::Result<Self, fuel_ir::Error> {
7850        let dims = self.shape().dims().to_vec();
7851        if dims.len() != 4 {
7852            return Err(fuel_ir::Error::Msg(format!(
7853                "max_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7854            ))
7855            .bt());
7856        }
7857        let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7858        let (kh, kw) = kernel;
7859        let (sh, sw) = stride;
7860        let (ph, pw) = padding;
7861        if kh == 0 || kw == 0 {
7862            return Err(
7863                fuel_ir::Error::Msg("max_pool2d: kernel sizes must be positive".into()).bt(),
7864            );
7865        }
7866        if sh == 0 || sw == 0 {
7867            return Err(fuel_ir::Error::Msg("max_pool2d: strides must be positive".into()).bt());
7868        }
7869        let h_padded_min = h + 2 * ph;
7870        let w_padded_min = w + 2 * pw;
7871        if h_padded_min < kh || w_padded_min < kw {
7872            return Err(fuel_ir::Error::Msg(format!(
7873                "max_pool2d: padded input ({h_padded_min}×{w_padded_min}) smaller than kernel ({kh}×{kw})",
7874            )).bt());
7875        }
7876        let h_out = (h_padded_min - kh) / sh + 1;
7877        let w_out = (w_padded_min - kw) / sw + 1;
7878        // Pad H/W to be exactly (h_out · sh + (kh - sh)) and (w_out · sw + (kw - sw)),
7879        // i.e., enough to contain every (ky, kx) tap for every output.
7880        let h_total = h_out * sh + (kh - 1);
7881        let w_total = w_out * sw + (kw - 1);
7882        let extra_h = h_total.saturating_sub(h_padded_min);
7883        let extra_w = w_total.saturating_sub(w_padded_min);
7884        let padded = self
7885            .pad_with_value(2, ph, ph + extra_h, pad_value)?
7886            .pad_with_value(3, pw, pw + extra_w, pad_value)?;
7887        // For each (ky, kx) collect the strided tap.
7888        let mut acc: Option<Tensor> = None;
7889        for ky in 0..kh {
7890            // Slice H starting at ky, length h_out · sh, then reshape
7891            // to [N, C, h_out, sh, w_total] and slice the sh-dim at 0
7892            // (we'll handle stride > 1 by reshape).
7893            let row_slice = padded.slice(2, ky, h_out * sh).unwrap();
7894            // Reshape H dim of length `h_out · sh` into (h_out, sh),
7895            // then take dim 3 at offset 0 (the tap on the sh axis).
7896            let row_reshaped = row_slice.reshape(vec![n, c, h_out, sh, w_total])?;
7897            let row_tap = row_reshaped.slice(3, 0, 1).unwrap().squeeze(3)?;
7898            for kx in 0..kw {
7899                let col_slice = row_tap.slice(3, kx, w_out * sw).unwrap();
7900                let col_reshaped = col_slice.reshape(vec![n, c, h_out, w_out, sw])?;
7901                let win = col_reshaped.slice(4, 0, 1).unwrap().squeeze(4)?;
7902                acc = Some(match acc {
7903                    None => win,
7904                    Some(a) => a.maximum(&win).unwrap(),
7905                });
7906            }
7907        }
7908        acc.ok_or_else(|| fuel_ir::Error::Msg("max_pool2d: empty kernel".into()).bt())
7909    }
7910
7911    /// Eager-API parity for `max_pool2d_with_stride`.
7912    pub fn max_pool2d_with_stride(
7913        &self,
7914        kernel: (usize, usize),
7915        stride: (usize, usize),
7916    ) -> std::result::Result<Self, fuel_ir::Error> {
7917        self.max_pool2d(kernel, stride, (0, 0))
7918    }
7919
7920    /// Nearest-neighbor upsample by integer factor `scale` along the
7921    /// last two spatial dims. Input `[N, C, H, W]` → output
7922    /// `[N, C, H·scale, W·scale]`.
7923    ///
7924    /// Composite via reshape + concat + reshape: insert a unit dim
7925    /// after each spatial dim, concat `scale` copies of the tensor on
7926    /// each new dim, then collapse the inflated dims back. Same shape
7927    /// as the `upsample_nearest_2x` helper in `fuel_transformers::models::lazy_yolov8`
7928    /// and `fuel_transformers::models::lazy_sd_unet`, generalized to arbitrary scale.
7929    pub fn upsample_nearest2d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7930        if scale == 0 {
7931            return Err(
7932                fuel_ir::Error::Msg("upsample_nearest2d: scale must be positive".into()).bt(),
7933            );
7934        }
7935        let dims = self.shape().dims().to_vec();
7936        if dims.len() != 4 {
7937            return Err(fuel_ir::Error::Msg(format!(
7938                "upsample_nearest2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7939            ))
7940            .bt());
7941        }
7942        if scale == 1 {
7943            return Ok(self.clone());
7944        }
7945        let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7946        // [N, C, H, 1, W, 1]
7947        let expanded = self.reshape(vec![n, c, h, 1, w, 1])?;
7948        // Replicate along the new unit dims by concatenating scale copies.
7949        let h_expanded = (0..scale)
7950            .fold(None, |acc: Option<Tensor>, _| {
7951                Some(match acc {
7952                    None => expanded.clone(),
7953                    Some(a) => a.concat(&expanded, 3).unwrap(),
7954                })
7955            })
7956            .unwrap();
7957        let h_then_w = (0..scale)
7958            .fold(None, |acc: Option<Tensor>, _| {
7959                Some(match acc {
7960                    None => h_expanded.clone(),
7961                    Some(a) => a.concat(&h_expanded, 5).unwrap(),
7962                })
7963            })
7964            .unwrap();
7965        h_then_w.reshape(vec![n, c, h * scale, w * scale])
7966    }
7967
7968    /// Nearest-neighbor upsample for 1-D signals `[N, C, T]` by integer
7969    /// `scale`. Reshape to insert a unit dim, concat scale copies,
7970    /// reshape back.
7971    pub fn upsample_nearest1d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7972        if scale == 0 {
7973            return Err(
7974                fuel_ir::Error::Msg("upsample_nearest1d: scale must be positive".into()).bt(),
7975            );
7976        }
7977        let dims = self.shape().dims().to_vec();
7978        if dims.len() != 3 {
7979            return Err(fuel_ir::Error::Msg(format!(
7980                "upsample_nearest1d: input must be rank 3 [N, C, T], got {dims:?}",
7981            ))
7982            .bt());
7983        }
7984        if scale == 1 {
7985            return Ok(self.clone());
7986        }
7987        let (n, c, t) = (dims[0], dims[1], dims[2]);
7988        let expanded = self.reshape(vec![n, c, t, 1])?;
7989        let replicated = (0..scale)
7990            .fold(None, |acc: Option<Tensor>, _| {
7991                Some(match acc {
7992                    None => expanded.clone(),
7993                    Some(a) => a.concat(&expanded, 3).unwrap(),
7994                })
7995            })
7996            .unwrap();
7997        replicated.reshape(vec![n, c, t * scale])
7998    }
7999
8000    /// 2-D nearest interpolation to an explicit target size.
8001    /// Arbitrary ratios (non-integer, non-uniform between H and
8002    /// W) supported via an `index_select`-based composite. The
8003    /// indexing convention matches PyTorch / the eager kernel:
8004    /// `src_h[oi] = min(H - 1, floor(oi * H / H_out))`.
8005    ///
8006    /// Used by DepthAnythingV2's DPT head and similar dense
8007    /// prediction heads that resize feature maps to arbitrary
8008    /// targets.
8009    pub fn interpolate2d(
8010        &self,
8011        target_h: usize,
8012        target_w: usize,
8013    ) -> std::result::Result<Self, fuel_ir::Error> {
8014        let dims = self.shape().dims().to_vec();
8015        if dims.len() != 4 {
8016            return Err(fuel_ir::Error::Msg(format!(
8017                "interpolate2d: input must be rank 4 [N, C, H, W], got {dims:?}",
8018            ))
8019            .bt());
8020        }
8021        let h = dims[2];
8022        let w = dims[3];
8023        if h == 0 || w == 0 || target_h == 0 || target_w == 0 {
8024            return Err(fuel_ir::Error::Msg(
8025                "interpolate2d: input + target spatial dims must be positive".into(),
8026            )
8027            .bt());
8028        }
8029        // Fast-path: identity.
8030        if target_h == h && target_w == w {
8031            return Ok(self.clone());
8032        }
8033        // Fast-path: integer-multiple uniform scale → existing
8034        // `upsample_nearest2d` (more cache-friendly than the
8035        // index_select composite for the common 2× / 4× case).
8036        if target_h.is_multiple_of(h) && target_w.is_multiple_of(w) && target_h / h == target_w / w
8037        {
8038            return self.upsample_nearest2d(target_h / h);
8039        }
8040        // General case: build per-axis source-index tensors and
8041        // index_select. Matches the eager UpsampleNearest2D
8042        // kernel's convention: src_idx = min(src - 1, floor(out * src / target)).
8043        let h_idx: Vec<u32> = (0..target_h)
8044            .map(|oi| ((oi * h) / target_h).min(h - 1) as u32)
8045            .collect();
8046        let w_idx: Vec<u32> = (0..target_w)
8047            .map(|oj| ((oj * w) / target_w).min(w - 1) as u32)
8048            .collect();
8049        let h_idx_tensor = self.const_u32_like(h_idx, fuel_ir::Shape::from_dims(&[target_h]))?;
8050        let w_idx_tensor = self.const_u32_like(w_idx, fuel_ir::Shape::from_dims(&[target_w]))?;
8051        let after_h = self.index_select(2_usize, &h_idx_tensor)?;
8052        after_h.index_select(3_usize, &w_idx_tensor)
8053    }
8054
8055    /// 1-D nearest interpolation to an explicit target size. Same
8056    /// constraints as [`Self::interpolate2d`]: integer-multiple targets
8057    /// only.
8058    pub fn interpolate1d(&self, target_t: usize) -> std::result::Result<Self, fuel_ir::Error> {
8059        let dims = self.shape().dims().to_vec();
8060        if dims.len() != 3 {
8061            return Err(fuel_ir::Error::Msg(format!(
8062                "interpolate1d: input must be rank 3 [N, C, T], got {dims:?}",
8063            ))
8064            .bt());
8065        }
8066        let t = dims[2];
8067        if t == 0 {
8068            return Err(
8069                fuel_ir::Error::Msg("interpolate1d: input length must be positive".into()).bt(),
8070            );
8071        }
8072        if !target_t.is_multiple_of(t) {
8073            return Err(fuel_ir::Error::Msg(format!(
8074                "interpolate1d: target {target_t} must be integer multiple of input {t}; non-integer ratios are future work",
8075            )).bt());
8076        }
8077        self.upsample_nearest1d(target_t / t)
8078    }
8079
8080    /// Pad with zeros along `dim`: `left` zeros before, `right` zeros
8081    /// after. Thin wrapper over [`Self::pad_with_value`] with `value = 0.0`.
8082    /// Composite — no new graph op. Accepts any [`Dim`].
8083    pub fn pad_with_zeros<D: Dim>(
8084        &self,
8085        dim: D,
8086        left: usize,
8087        right: usize,
8088    ) -> std::result::Result<Self, fuel_ir::Error> {
8089        self.pad_with_value(dim, left, right, 0.0)
8090    }
8091
8092    /// Pad with a constant `value` along `dim`: `before` slots before,
8093    /// `after` slots after. Wraps [`Self::pad`] with `PadMode::Constant`
8094    /// for the named dim (other dims get `(0, 0)`); the `f32` value is
8095    /// widened to the graph op's `f64` param. Useful for `-inf` padding
8096    /// around max-reductions (e.g. PyTorch-style `max_pool2d`, where
8097    /// padded positions must never win the max). Accepts any [`Dim`].
8098    pub fn pad_with_value<D: Dim>(
8099        &self,
8100        dim: D,
8101        before: usize,
8102        after: usize,
8103        value: f32,
8104    ) -> std::result::Result<Self, fuel_ir::Error> {
8105        let shape = self.shape();
8106        let dim = dim.to_index(&shape, "pad_with_value")?;
8107        let rank = shape.dims().len();
8108        if before == 0 && after == 0 {
8109            return Ok(self.clone());
8110        }
8111        let mut padding: Vec<(usize, usize)> = vec![(0, 0); rank];
8112        padding[dim] = (before, after);
8113        self.pad(padding, fuel_graph::PadMode::Constant, value as f64)
8114    }
8115
8116    /// Coordinate grids from rank-1 inputs. Matches PyTorch's
8117    /// `torch.meshgrid` and eager's `crate::Tensor::meshgrid`:
8118    ///
8119    /// - `xy_indexing = true` (Cartesian, NumPy default): the first
8120    ///   two inputs are swapped before broadcasting and the resulting
8121    ///   grids reversed back, so for `(x, y)` the grids carry shape
8122    ///   `[len(y), len(x)]` (image-coordinate convention).
8123    /// - `xy_indexing = false` (matrix / ij): grids carry the input
8124    ///   cardinalities in input order.
8125    ///
8126    /// Implemented as `reshape` + `repeat`. All inputs must share the
8127    /// same dtype and live on the same graph.
8128    pub fn meshgrid(
8129        args: &[&Self],
8130        xy_indexing: bool,
8131    ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
8132        if args.len() < 2 {
8133            return Err(fuel_ir::Error::Msg(
8134                "meshgrid: requires at least two rank-1 tensors".into(),
8135            )
8136            .bt());
8137        }
8138        let ordered: Vec<&Self> = if xy_indexing {
8139            args.iter().rev().copied().collect()
8140        } else {
8141            args.to_vec()
8142        };
8143        let mut lens = Vec::with_capacity(ordered.len());
8144        for (i, t) in ordered.iter().enumerate() {
8145            let dims = t.shape().dims().to_vec();
8146            if dims.len() != 1 {
8147                return Err(fuel_ir::Error::Msg(format!(
8148                    "meshgrid: input {i} must be rank 1, got shape {dims:?}",
8149                ))
8150                .bt());
8151            }
8152            lens.push(dims[0]);
8153        }
8154        let mut grids = Vec::with_capacity(ordered.len());
8155        for (idx, t) in ordered.iter().enumerate() {
8156            let mut shape = vec![1_usize; ordered.len()];
8157            shape[idx] = lens[idx];
8158            let placed = t.reshape(shape)?;
8159            let mut repeats = lens.clone();
8160            repeats[idx] = 1;
8161            let grid = placed.repeat(repeats)?;
8162            grids.push(grid);
8163        }
8164        if xy_indexing {
8165            grids.reverse();
8166        }
8167        Ok(grids)
8168    }
8169
8170    /// Repeat the tensor along each dim `repeats[i]` times. If `repeats`
8171    /// has more dims than `self`, `self` is implicitly left-padded with
8172    /// size-1 dims to match. Matches PyTorch's `Tensor.repeat`.
8173    pub fn repeat(&self, repeats: impl Into<Shape>) -> std::result::Result<Self, fuel_ir::Error> {
8174        let repeats = repeats.into();
8175        let repeats: Vec<usize> = repeats.dims().to_vec();
8176        let self_rank = self.shape().dims().len();
8177        let target_rank = repeats.len();
8178        let mut work = if self_rank < target_rank {
8179            let pad_count = target_rank - self_rank;
8180            let mut new_shape: Vec<usize> = vec![1; pad_count];
8181            new_shape.extend_from_slice(self.shape().dims());
8182            self.reshape(new_shape)?
8183        } else if self_rank > target_rank {
8184            return Err(fuel_ir::Error::Msg(format!(
8185                "repeat: repeats rank {target_rank} smaller than tensor rank {self_rank}",
8186            ))
8187            .bt());
8188        } else {
8189            self.clone()
8190        };
8191        for (axis, &n) in repeats.iter().enumerate() {
8192            if n == 0 {
8193                return Err(fuel_ir::Error::Msg(format!(
8194                    "repeat: zero repeat count at axis {axis} not supported",
8195                ))
8196                .bt());
8197            }
8198            if n == 1 {
8199                continue;
8200            }
8201            // n copies concatenated along `axis`.
8202            let base = work.clone();
8203            for _ in 1..n {
8204                work = work.concat(&base, axis)?;
8205            }
8206        }
8207        Ok(work)
8208    }
8209}
8210
8211/// For a single pooling axis, the first output index (if any) whose window
8212/// has zero valid (non-padded) taps — every tap it covers lies outside
8213/// `[0, dim)`. Used by [`Tensor::avg_pool2d`]'s `count_include_pad = false`
8214/// path to refuse a divide-by-zero divisor instead of emitting NaN/inf.
8215///
8216/// Pure host-side arithmetic over the pooling shape parameters; computes no
8217/// tensor, builds no graph. `out_dim` matches the formula in
8218/// [`Tensor::avg_pool2d`]'s doc comment: `(dim + 2*pad - kernel) / stride + 1`.
8219fn first_zero_tap_output_index(
8220    dim: usize,
8221    kernel: usize,
8222    stride: usize,
8223    pad: usize,
8224) -> Option<usize> {
8225    let out_dim = (dim + 2 * pad).saturating_sub(kernel) / stride + 1;
8226    (0..out_dim).find(|&o| {
8227        let start = o as isize * stride as isize - pad as isize;
8228        let end = start + kernel as isize;
8229        let valid_start = start.max(0);
8230        let valid_end = end.min(dim as isize);
8231        valid_end <= valid_start
8232    })
8233}
8234
8235// ---- safetensors integration -----------------------------------------------
8236
8237impl Tensor {
8238    /// Build a `Tensor` from raw little-endian bytes as they appear
8239    /// in a safetensors file, plus a dtype and shape. Row-major layout
8240    /// is assumed. The byte count must match `shape.elem_count() *
8241    /// dtype_bytes`.
8242    ///
8243    /// This is the low-level loader. Prefer [`Self::from_safetensors_view`]
8244    /// if you already have a `safetensors::TensorView` in hand.
8245    ///
8246    /// Supported dtypes today: `F32`, `F64`, `BF16`, `F16`, `U32`.
8247    /// Integer types other than `U32` are rejected to keep the
8248    /// surface small; add them when a real model needs them.
8249    pub fn from_safetensors_bytes(
8250        bytes: &[u8],
8251        dtype: safetensors::Dtype,
8252        shape: &[usize],
8253        device: &Device,
8254    ) -> crate::Result<Self> {
8255        use safetensors::Dtype;
8256        let shape_obj = Shape::from_dims(shape);
8257        let elem_count = shape_obj.elem_count();
8258
8259        let check_len = |expected: usize| -> crate::Result<()> {
8260            if bytes.len() != expected {
8261                crate::bail!(
8262                    "from_safetensors_bytes: expected {expected} bytes for dtype {dtype:?} \
8263                     and shape {shape:?}, got {}",
8264                    bytes.len(),
8265                );
8266            }
8267            Ok(())
8268        };
8269
8270        match dtype {
8271            Dtype::F32 => {
8272                check_len(elem_count * 4)?;
8273                let mut data = Vec::with_capacity(elem_count);
8274                for chunk in bytes.as_chunks::<4>().0.iter() {
8275                    data.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8276                }
8277                Self::from_f32(data, shape_obj, device)
8278            }
8279            Dtype::F64 => {
8280                check_len(elem_count * 8)?;
8281                let mut data = Vec::with_capacity(elem_count);
8282                for chunk in bytes.as_chunks::<8>().0.iter() {
8283                    let arr: [u8; 8] = *chunk;
8284                    data.push(f64::from_le_bytes(arr));
8285                }
8286                Self::from_f64(data, shape_obj, device)
8287            }
8288            Dtype::BF16 => {
8289                check_len(elem_count * 2)?;
8290                let mut data = Vec::with_capacity(elem_count);
8291                for chunk in bytes.as_chunks::<2>().0.iter() {
8292                    let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8293                    data.push(half::bf16::from_bits(raw));
8294                }
8295                Self::from_bf16(data, shape_obj, device)
8296            }
8297            Dtype::F16 => {
8298                check_len(elem_count * 2)?;
8299                let mut data = Vec::with_capacity(elem_count);
8300                for chunk in bytes.as_chunks::<2>().0.iter() {
8301                    let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8302                    data.push(half::f16::from_bits(raw));
8303                }
8304                Self::from_f16(data, shape_obj, device)
8305            }
8306            Dtype::U32 => {
8307                check_len(elem_count * 4)?;
8308                let mut data = Vec::with_capacity(elem_count);
8309                for chunk in bytes.as_chunks::<4>().0.iter() {
8310                    data.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8311                }
8312                Self::from_u32(data, shape_obj, device)
8313            }
8314            other => crate::bail!(
8315                "from_safetensors_bytes: unsupported dtype {other:?} — extend Tensor's \
8316                 safetensors loader to handle it",
8317            ),
8318        }
8319    }
8320
8321    /// Build a `Tensor` from a `safetensors::TensorView`. This is
8322    /// the most natural entry point when iterating over a
8323    /// [`fuel_loaders::safetensors::MmapedSafetensors`] or similar.
8324    pub fn from_safetensors_view(
8325        view: &safetensors::tensor::TensorView<'_>,
8326        device: &Device,
8327    ) -> crate::Result<Self> {
8328        Self::from_safetensors_bytes(view.data(), view.dtype(), view.shape(), device)
8329    }
8330}
8331
8332/// A LayerNorm's learned `(gain, bias)`, both length `[dim]`. Wrapped in
8333/// `Option` at sites where the norm is conditional — present only on layer 0
8334/// (RWKV `pre_ln`), or only when a config flag is set (Persimmon QK-LN).
8335pub type LayerNormPair = (Arc<[f32]>, Arc<[f32]>);
8336
8337/// A convolution's `(weight, bias)`: the flattened `[out, in, kh, kw]` kernel
8338/// and its `[out]` bias. Distinct from [`LayerNormPair`] despite the identical
8339/// shape — the two are not interchangeable and naming them apart is the point.
8340pub type ConvWeightBias = (Arc<[f32]>, Arc<[f32]>);
8341
8342/// Weight tensor storage that preserves source precision.
8343///
8344/// Projection weights (Q/K/V/O for attention, gate/up/down for FFN,
8345/// and the output `lm_head` matrix) stay in whatever dtype the
8346/// source checkpoint used — f32 when that's how it was saved, bf16
8347/// for modern HF checkpoints that ship bf16 to halve weight memory.
8348/// Activations are usually f32, but LlamaModel's cached decode path
8349/// (Phase D increment A, "BF16-throughout decode") threads the KV
8350/// cache's dtype through the whole activation stream — when that's
8351/// BF16, weights must ALSO be BF16 (`matmul`'s dtype gate only allows
8352/// same-dtype homogeneous or `(lhs=F32, rhs=BF16)`; a BF16-activation ×
8353/// F32-weight matmul is rejected, the opposite direction from the
8354/// classic f32-activation-over-bf16-weight quantized-serving case).
8355/// F32 activations still route the mixed `(A:F32, B:BF16) → F32`
8356/// precision via `VulkanBackend::matmul` unchanged.
8357///
8358/// Norm gains and biases are NOT covered by this enum — they're
8359/// small and precision-sensitive, so they stay `Arc<[f32]>`.
8360///
8361/// Cloning is cheap (Arc bump) for both variants. Use
8362/// [`WeightStorage::const_like`] to emit a [`Tensor`] `Const`
8363/// node with the right dtype.
8364#[derive(Debug, Clone)]
8365pub enum WeightStorage {
8366    F32(Arc<[f32]>),
8367    BF16(Arc<[half::bf16]>),
8368    /// GGML Q4_0 blocks (raw byte stream), laid out as `[out_features,
8369    /// in_features / 32]` blocks (18 bytes each, llama.cpp convention).
8370    /// Stored as `Arc<[u32]>` — the byte stream reinterpreted as u32
8371    /// words so subsequent forward passes just Arc-clone (cheap) rather
8372    /// than recopying the bytes. The graph sees this directly as a U32
8373    /// tensor; matmul dispatch goes through `Op::QMatMul`.
8374    ///
8375    /// `bytes_len` is the original byte count (u32_len * 4) so the
8376    /// const_like shape computation doesn't accidentally round up.
8377    Q4_0 {
8378        words: Arc<[u32]>,
8379        bytes_len: usize,
8380        in_features: usize,
8381        out_features: usize,
8382    },
8383    /// Base weight wrapped with a trainable LoRA (Low-Rank Adaptation)
8384    /// update: effective weight `W_eff = base + (alpha / rank) · A · B`
8385    /// where `A` has shape `[in_features, rank]` and `B` has shape
8386    /// `[rank, out_features]` (both stored in the same layout
8387    /// convention as F32 weights — `[in, out]`).
8388    ///
8389    /// Used for PEFT-style inference with frozen base weights (which
8390    /// can be F32, BF16, or Q4_0) plus small trainable adapter matrices.
8391    /// The adapter is cheap to apply — for a 2560×2560 projection at
8392    /// rank 8 the LoRA path is ~0.5% of the base matmul cost.
8393    WithLoRA {
8394        base: Box<WeightStorage>,
8395        /// `[in_features, rank]` adapter A (HF's `lora_A` transposed).
8396        lora_a: Arc<[f32]>,
8397        /// `[rank, out_features]` adapter B (HF's `lora_B` transposed).
8398        lora_b: Arc<[f32]>,
8399        rank: usize,
8400        /// LoRA scaling factor; effective scale is `alpha / rank`.
8401        alpha: f32,
8402        in_features: usize,
8403        out_features: usize,
8404    },
8405}
8406
8407impl WeightStorage {
8408    pub fn elem_count(&self) -> usize {
8409        match self {
8410            Self::F32(a) => a.len(),
8411            Self::BF16(a) => a.len(),
8412            // Logical element count for a Q4_0 weight matrix is n*k.
8413            Self::Q4_0 {
8414                in_features,
8415                out_features,
8416                ..
8417            } => *in_features * *out_features,
8418            Self::WithLoRA {
8419                in_features,
8420                out_features,
8421                ..
8422            } => *in_features * *out_features,
8423        }
8424    }
8425
8426    pub fn dtype(&self) -> fuel_ir::DType {
8427        match self {
8428            Self::F32(_) => fuel_ir::DType::F32,
8429            Self::BF16(_) => fuel_ir::DType::BF16,
8430            // Q4_0 surfaces as U32 at the graph level (raw bytes
8431            // reinterpreted). Callers that care about the "actual"
8432            // quantization type should match on the variant directly.
8433            Self::Q4_0 { .. } => fuel_ir::DType::U32,
8434            // WithLoRA exposes the base's dtype (the LoRA adapter is
8435            // always F32 but activations are typed by the base weight).
8436            Self::WithLoRA { base, .. } => base.dtype(),
8437        }
8438    }
8439
8440    /// Emit a `Const` node on `anchor`'s graph matching this
8441    /// storage's dtype. Used everywhere the forward pass wraps a
8442    /// weight into a `Tensor`.
8443    ///
8444    /// For `Q4_0`, the emitted tensor is a 1-D `U32` const of length
8445    /// `bytes.len() / 4` holding the raw block byte stream. Callers
8446    /// must pair this with `Tensor::qmatmul` rather than `matmul`.
8447    ///
8448    /// Returns Err for `WithLoRA` — the base + LoRA update can only be
8449    /// applied via `apply_linear` so the right graph structure is built.
8450    pub fn const_like(
8451        &self,
8452        anchor: &Tensor,
8453        shape: Shape,
8454    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8455        match self {
8456            Self::F32(a) => anchor.const_f32_like(a.clone(), shape),
8457            Self::BF16(a) => anchor.const_bf16_like(a.clone(), shape),
8458            Self::Q4_0 { words, .. } => {
8459                let _ = shape; // shape arg unused — Q4_0 const is 1-D U32
8460                // Arc-clone the precomputed u32 view; no byte copy.
8461                anchor.const_u32_like(Arc::clone(words), Shape::from_dims(&[words.len()]))
8462            }
8463            Self::WithLoRA { .. } => Err(fuel_ir::Error::Msg(
8464                "WeightStorage::WithLoRA::const_like is not supported \
8465                 — the base + LoRA update must be applied via \
8466                 apply_linear to produce the right graph structure."
8467                    .into(),
8468            )
8469            .bt()),
8470        }
8471    }
8472
8473    /// Produce `X @ W + bias` for this weight storage. Bias is a
8474    /// length-`out_features` Arc<[f32]> materialized fresh on the
8475    /// receiver's graph and broadcast across the leading dims of
8476    /// the output.
8477    ///
8478    /// Equivalent to the per-port `apply_linear_with_bias` helpers
8479    /// that several ports inlined — promoted here so call sites
8480    /// stop drifting.
8481    pub fn apply_linear_with_bias(
8482        &self,
8483        x: &Tensor,
8484        in_features: usize,
8485        out_features: usize,
8486        bias: std::sync::Arc<[f32]>,
8487    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8488        if bias.len() != out_features {
8489            // Was a `debug_assert_eq!` — i.e. compiled OUT of release builds,
8490            // so a release binary silently built a broadcast against a
8491            // wrong-length bias. A length mismatch is a checkpoint condition;
8492            // it errors in every profile now.
8493            return Err(fuel_ir::Error::Msg(format!(
8494                "apply_linear_with_bias: bias len ({}) != out_features ({out_features})",
8495                bias.len(),
8496            ))
8497            .bt());
8498        }
8499        let projected = self.apply_linear(x, in_features, out_features)?;
8500        let bias_t = projected.const_f32_like(bias, Shape::from_dims(&[out_features]))?;
8501        projected.broadcast_add(&bias_t)
8502    }
8503
8504    /// Produce `X @ W` (with optional bias) for this weight storage.
8505    /// Dispatches to `matmul` for F32/BF16 weights and to `qmatmul`
8506    /// for Q4_0. `x`'s dtype must be compatible with this weight's
8507    /// dtype per `matmul`'s gate: same-dtype homogeneous (e.g. BF16
8508    /// activations × BF16 weights — LlamaModel's BF16-throughout
8509    /// decode path, Phase D increment A) or `(x=F32, weight=BF16)`
8510    /// (f32-activation quantized-weight serving). `(x=BF16,
8511    /// weight=F32)` is NOT supported — cast the weight or the
8512    /// activation to match before calling.
8513    /// # Errors
8514    ///
8515    /// Returns a typed error rather than panicking on a dimension or dtype
8516    /// mismatch. This used to `.unwrap()` the inner `matmul`/`qmatmul` and
8517    /// `assert_eq!` the stored-vs-requested dimensions, which made a
8518    /// **production** path panic — a mismatched checkpoint (a GGUF whose
8519    /// stored shape disagrees with the model config) is a data condition, not
8520    /// a programming invariant, so it must surface as `Err`.
8521    pub fn apply_linear(
8522        &self,
8523        x: &Tensor,
8524        in_features: usize,
8525        out_features: usize,
8526    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8527        // Build-time contract check, shared by every arm: a "linear with
8528        // `in_features`" requires x's trailing dim to BE `in_features`.
8529        // Checked here rather than at the ~620 call sites, and phrased so the
8530        // message names the projection instead of surfacing a bare matmul
8531        // shape error two layers down.
8532        let x_dims = x.shape().dims().to_vec();
8533        match x_dims.last() {
8534            Some(&k) if k == in_features => {}
8535            _ => {
8536                return Err(fuel_ir::Error::Msg(format!(
8537                    "apply_linear({in_features} -> {out_features}): input shape {x_dims:?} \
8538                     must have trailing dim {in_features}",
8539                ))
8540                .bt());
8541            }
8542        }
8543        match self {
8544            Self::F32(_) | Self::BF16(_) => {
8545                let w = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8546                x.matmul(&w)
8547            }
8548            Self::Q4_0 {
8549                in_features: expected_in,
8550                out_features: expected_out,
8551                ..
8552            } => {
8553                if *expected_in != in_features || *expected_out != out_features {
8554                    return Err(fuel_ir::Error::Msg(format!(
8555                        "WeightStorage::Q4_0 shape mismatch: stored \
8556                         [{expected_in}, {expected_out}], requested \
8557                         [{in_features}, {out_features}]",
8558                    ))
8559                    .bt());
8560                }
8561                let w_bytes = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8562                x.qmatmul(
8563                    &w_bytes,
8564                    fuel_graph::QuantType::Q4_0,
8565                    in_features,
8566                    out_features,
8567                )
8568            }
8569            Self::WithLoRA {
8570                base,
8571                lora_a,
8572                lora_b,
8573                rank,
8574                alpha,
8575                in_features: expected_in,
8576                out_features: expected_out,
8577            } => {
8578                if *expected_in != in_features || *expected_out != out_features {
8579                    return Err(fuel_ir::Error::Msg(format!(
8580                        "WeightStorage::WithLoRA shape mismatch: stored \
8581                         [{expected_in}, {expected_out}], requested \
8582                         [{in_features}, {out_features}]",
8583                    ))
8584                    .bt());
8585                }
8586                // Base forward (F32, BF16, or Q4_0).
8587                let base_out = base.apply_linear(x, in_features, out_features)?;
8588                // Low-rank update: y += (alpha/rank) · x @ A @ B.
8589                let a_t =
8590                    x.const_f32_like(Arc::clone(lora_a), Shape::from_dims(&[in_features, *rank]))?;
8591                let b_t =
8592                    x.const_f32_like(Arc::clone(lora_b), Shape::from_dims(&[*rank, out_features]))?;
8593                let scale = *alpha as f64 / *rank as f64;
8594                // x: [*, in] → @A [*, rank] → @B [*, out] → scale → add base.
8595                let lora_path = Tensor {
8596                    inner: x.matmul(&a_t)?.matmul(&b_t)?.inner.mul_scalar(scale),
8597                };
8598                base_out.add(&lora_path)
8599            }
8600        }
8601    }
8602
8603    /// Data-determined-M variant of [`Self::apply_linear`]: computes only
8604    /// `row_count` rows of the capacity-buffer projection `x @ W`, the rest
8605    /// left zeroed — the per-expert sparse-MoE FFN path. `x` is a
8606    /// `[capacity, in_features]` buffer whose first `row_count` rows are the
8607    /// routed tokens; the returned `[capacity, out_features]` buffer's tail
8608    /// (rows `row_count..capacity`) stays exactly zero (see
8609    /// [`Tensor::matmul_dyn_m`]).
8610    ///
8611    /// F32-only (mirrors `matmul_dyn_m`); other weight encodings surface a
8612    /// typed build-time error. No bias is applied — a bias would fill the
8613    /// un-computed tail, which the sparse-MoE caller relies on being zero
8614    /// for a correct `index_add` scatter-back.
8615    pub fn apply_linear_dyn_m(
8616        &self,
8617        x: &Tensor,
8618        in_features: usize,
8619        out_features: usize,
8620        row_count: fuel_ir::DynScalar,
8621    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8622        match self {
8623            Self::F32(_) => {
8624                // const_like only errors on WithLoRA; the F32 arm is infallible.
8625                let w = self
8626                    .const_like(x, Shape::from_dims(&[in_features, out_features]))
8627                    .expect("apply_linear_dyn_m F32 arm: const_like cannot fail for F32");
8628                x.matmul_dyn_m(&w, row_count)
8629            }
8630            other => Err(fuel_ir::Error::Msg(format!(
8631                "apply_linear_dyn_m: sparse-MoE dispatch is F32-only today, got {:?} \
8632                 weight (cast the expert weights to F32)",
8633                other.dtype(),
8634            ))
8635            .bt()),
8636        }
8637    }
8638
8639    /// Wrap this weight storage with a LoRA adapter. Asserts that the
8640    /// adapter shapes match `in_features`/`out_features`. Panics if the
8641    /// base is already a `WithLoRA` (nested adapters aren't supported;
8642    /// merge them explicitly if needed).
8643    pub fn with_lora(
8644        self,
8645        lora_a: Arc<[f32]>,
8646        lora_b: Arc<[f32]>,
8647        rank: usize,
8648        alpha: f32,
8649        in_features: usize,
8650        out_features: usize,
8651    ) -> Self {
8652        assert_eq!(
8653            lora_a.len(),
8654            in_features * rank,
8655            "lora_a length {} does not match in_features ({in_features}) × rank ({rank}) = {}",
8656            lora_a.len(),
8657            in_features * rank,
8658        );
8659        assert_eq!(
8660            lora_b.len(),
8661            rank * out_features,
8662            "lora_b length {} does not match rank ({rank}) × out_features ({out_features}) = {}",
8663            lora_b.len(),
8664            rank * out_features,
8665        );
8666        assert!(
8667            !matches!(self, Self::WithLoRA { .. }),
8668            "with_lora: base is already WithLoRA (nested adapters unsupported)",
8669        );
8670        Self::WithLoRA {
8671            base: Box::new(self),
8672            lora_a,
8673            lora_b,
8674            rank,
8675            alpha,
8676            in_features,
8677            out_features,
8678        }
8679    }
8680}
8681
8682// Auto-conversions so code that was storing `Arc<[f32]>` keeps
8683// compiling through the refactor — the LayerWeights field type
8684// widened to WeightStorage but ergonomics don't regress.
8685impl From<Arc<[f32]>> for WeightStorage {
8686    fn from(a: Arc<[f32]>) -> Self {
8687        Self::F32(a)
8688    }
8689}
8690impl From<Vec<f32>> for WeightStorage {
8691    fn from(v: Vec<f32>) -> Self {
8692        Self::F32(Arc::from(v))
8693    }
8694}
8695impl From<Arc<[half::bf16]>> for WeightStorage {
8696    fn from(a: Arc<[half::bf16]>) -> Self {
8697        Self::BF16(a)
8698    }
8699}
8700impl From<Vec<half::bf16>> for WeightStorage {
8701    fn from(v: Vec<half::bf16>) -> Self {
8702        Self::BF16(Arc::from(v))
8703    }
8704}
8705
8706/// Per-layer weights of a LLaMA transformer block. All tensors are
8707/// stored as `Arc<[f32]>` so they can be loaded once and shared across
8708/// every forward pass with zero copy — each call to
8709/// `LlamaModel::forward` clones the `Arc` (a refcount bump) when it
8710/// builds fresh const nodes for this layer.
8711///
8712/// LLaMA proper has no biases anywhere in the attention block, so the
8713/// `*_bias` fields are `None` for LLaMA family models. Qwen2 and a few
8714/// related architectures do add biases on Q/K/V (but not on the output
8715/// projection), so the loader stores them here when the safetensors
8716/// file contains them.
8717#[derive(Debug, Clone)]
8718pub struct LayerWeights {
8719    /// `[dim, dim]` query projection. Supports bf16 or f32.
8720    pub attn_q: WeightStorage,
8721    /// `[dim]` query projection bias (Qwen2-style; LLaMA has none).
8722    pub attn_q_bias: Option<Arc<[f32]>>,
8723    /// `[dim, dim]` key projection.
8724    pub attn_k: WeightStorage,
8725    /// `[kv_dim]` key projection bias.
8726    pub attn_k_bias: Option<Arc<[f32]>>,
8727    /// `[dim, dim]` value projection.
8728    pub attn_v: WeightStorage,
8729    /// `[kv_dim]` value projection bias.
8730    pub attn_v_bias: Option<Arc<[f32]>>,
8731    /// `[dim, dim]` output projection.
8732    pub attn_o: WeightStorage,
8733    /// `[dim, ffn_dim]` gate projection for SwiGLU.
8734    pub ffn_gate: WeightStorage,
8735    /// `[dim, ffn_dim]` up projection for SwiGLU.
8736    pub ffn_up: WeightStorage,
8737    /// `[ffn_dim, dim]` down projection for SwiGLU.
8738    pub ffn_down: WeightStorage,
8739    /// `[dim]` RmsNorm gain for the pre-attention norm. Stays f32
8740    /// — norm gains are small and precision-sensitive.
8741    pub attn_norm_gain: Arc<[f32]>,
8742    /// `[dim]` RmsNorm gain for the pre-FFN norm.
8743    pub ffn_norm_gain: Arc<[f32]>,
8744}
8745
8746pub fn invalidate_decode_pair_if_stale<C>(
8747    session: &mut Option<crate::inference_context::DecodeSession>,
8748    captured: &mut Option<C>,
8749    ctx: &mut InferenceContext,
8750    seq: usize,
8751    max_seq_len: Option<usize>,
8752    cache_dtype: DType,
8753    n_layers: usize,
8754    shape_key: u64,
8755    kv: &dyn crate::inference_context::KvRebindSource,
8756    drop_session: impl FnOnce(
8757        &mut Option<crate::inference_context::DecodeSession>,
8758        &mut InferenceContext,
8759    ),
8760) -> SessionDisposition {
8761    refresh_decode_session(
8762        session,
8763        ctx,
8764        seq,
8765        max_seq_len,
8766        cache_dtype,
8767        n_layers,
8768        shape_key,
8769        kv,
8770        // Reader before owner — see the doc comment above. This fires for a
8771        // re-bind as well as a drop: a capture records FIXED device addresses
8772        // drawn from `base_cache`, and `rebind_kv` replaces some of them.
8773        || *captured = None,
8774        drop_session,
8775    )
8776}
8777
8778/// What [`refresh_decode_session`] did to the held plan.
8779#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8780pub enum SessionDisposition {
8781    /// There was no held plan; nothing to do.
8782    Absent,
8783    /// The plan is valid for this step and was left alone.
8784    Kept,
8785    /// The KV allocation changed and the plan was re-bound onto it (GAP-028).
8786    /// Any reader of the plan's device addresses has been retired.
8787    Rebound,
8788    /// The plan was retired. `Some(refusal)` means a re-bind was attempted and
8789    /// declined — carried so a caller (or a test) can distinguish "refused for
8790    /// the right reason" from "refused because nothing was tried".
8791    Dropped(Option<crate::inference_context::RebindRefusal>),
8792}
8793
8794/// **The single decision point for what happens to a held decode plan at the
8795/// top of a decode step.** Every model routes through here, so they cannot
8796/// drift on what "stale" means — the reason this was a shared free function
8797/// before GAP-028, and more so now that the answer has three outcomes.
8798///
8799/// Order of business, and why it is this order:
8800///
8801/// 1. Ask [`crate::inference_context::DecodeSession::validity_for`] — ONE comparison list, so "is it
8802///    valid" and "is only the allocation different" cannot disagree.
8803/// 2. On anything but `Valid`, retire readers FIRST via `on_invalidate`. A
8804///    `CapturedDecodeSession` replays a recorded CUDA graph over FIXED device
8805///    addresses drawn from the session's `base_cache`; both a drop and a
8806///    re-bind make some of those addresses stale, and a replay against stale
8807///    addresses is silently wrong logits at full speed. Retiring before
8808///    touching the owner means no replay can observe a half-updated plan.
8809///    Note this fires *before* the re-bind is attempted, not after it
8810///    succeeds: a capture is worthless either way, and clearing it early keeps
8811///    the ordering true without depending on the outcome.
8812/// 3. Try the guarded re-bind. It may refuse; a refusal is not an error.
8813/// 4. Otherwise drop, so the caller's first-decode-token arm rebuilds.
8814///
8815/// Generic in nothing — the capture rides in the `on_invalidate` closure,
8816/// which is what lets this compile (and therefore be CPU-tested) without the
8817/// `cuda` feature.
8818pub fn refresh_decode_session(
8819    session: &mut Option<crate::inference_context::DecodeSession>,
8820    ctx: &mut InferenceContext,
8821    seq: usize,
8822    max_seq_len: Option<usize>,
8823    cache_dtype: DType,
8824    n_layers: usize,
8825    shape_key: u64,
8826    kv: &dyn crate::inference_context::KvRebindSource,
8827    on_invalidate: impl FnOnce(),
8828    drop_session: impl FnOnce(
8829        &mut Option<crate::inference_context::DecodeSession>,
8830        &mut InferenceContext,
8831    ),
8832) -> SessionDisposition {
8833    use crate::inference_context::PlanValidity;
8834
8835    let verdict = match (session.as_ref(), max_seq_len) {
8836        (None, _) => return SessionDisposition::Absent,
8837        (Some(s), Some(msl)) => {
8838            s.validity_for(seq, msl, n_layers, cache_dtype, shape_key, kv.alloc_id())
8839        }
8840        // A cache with no pre-allocated capacity cannot back a held plan at
8841        // all (the graph bakes `max_seq_len` into its KV Const shapes).
8842        (Some(_), None) => PlanValidity::Stale,
8843    };
8844    if verdict == PlanValidity::Valid {
8845        return SessionDisposition::Kept;
8846    }
8847
8848    on_invalidate();
8849
8850    let refusal = if verdict == PlanValidity::AllocationChanged {
8851        match session.as_mut().map(|s| s.rebind_kv(kv)) {
8852            Some(Ok(())) => return SessionDisposition::Rebound,
8853            Some(Err(r)) => Some(r),
8854            // Unreachable: `verdict` was computed from a `Some` session and
8855            // nothing above takes it. Handled rather than `expect`ed because
8856            // this is a production path.
8857            None => None,
8858        }
8859    } else {
8860        None
8861    };
8862
8863    drop_session(session, ctx);
8864    SessionDisposition::Dropped(refusal)
8865}
8866
8867pub fn build_decode_causal_mask(cached_len: usize, seq: usize, max_seq_len: usize) -> Vec<f32> {
8868    let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8869    for q_idx in 0..seq {
8870        let abs_q = cached_len + q_idx;
8871        for k_idx in (abs_q + 1)..max_seq_len {
8872            mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8873        }
8874    }
8875    mask_data
8876}
8877
8878/// Sliding-window sibling of [`build_decode_causal_mask`] — the decode-time
8879/// mask for a layer that attends only to the most recent `window` positions.
8880///
8881/// # Why this exists (GAP-029 increment 3)
8882///
8883/// The decode path has always built **one** mask per model, because `LlamaModel`
8884/// and `PhiModel` — the only two carriers — have no per-layer mask variation
8885/// (measured: **zero** `sliding_window` hits in this file, against a positive
8886/// control of hits in five sibling model files). **Four of the six families
8887/// GAP-029 increment 3 ports do vary per layer**, three of them via a sliding
8888/// window: `Qwen2` (`lazy_qwen2.rs:249-251`), `Qwen3` (`lazy_qwen3.rs:172`) and
8889/// `Qwen3Moe` (`lazy_qwen3_moe.rs:192`) all select per layer on
8890/// `use_sliding_window && layer_idx < max_window_layers`.
8891///
8892/// # The predicate, and why it is stated against the prefill path
8893///
8894/// The shipped **prefill** mask for these families is `j > i || j + window <= i`
8895/// over absolute positions (`lazy_qwen2.rs:271`). Decode must agree with it
8896/// position-for-position, or a model silently changes behaviour at the
8897/// prefill→decode boundary. Here the query's absolute position is
8898/// `cached_len + q_idx` and the key's is `k_idx`, giving the same two clauses:
8899/// causal (`k_idx > abs_q`) and window (`k_idx + window <= abs_q`).
8900///
8901/// `window >= max_seq_len` makes the window clause unsatisfiable, so the result
8902/// is exactly [`build_decode_causal_mask`] — the property that lets a family
8903/// with the window disabled share the dense path rather than a near-copy of it.
8904///
8905/// # Status: LIVE. `#[allow(dead_code)]` removed 2026-08-13 — this has a caller.
8906///
8907/// It landed ahead of its consumer with its correctness established
8908/// independently (see the born-red record in `decode_mask_tests`), under an
8909/// architect-owned checkpoint: *land family 1 or the function is deleted*.
8910/// GAP-029 increment 3 landed `Qwen2Model`'s decode path, and
8911/// [`crate::persistent_decode::build_decode_mask_variants`] calls this for every
8912/// windowed mask variant — so the attribute is gone rather than renewed.
8913///
8914/// It is now on the live decode path of the three windowed families as they
8915/// arrive (`Qwen2Model` today; `Qwen3Model`, `Qwen3MoeModel` next), reached
8916/// through `MaskPlan::split_window`.
8917pub fn build_decode_causal_mask_windowed(
8918    cached_len: usize,
8919    seq: usize,
8920    max_seq_len: usize,
8921    window: usize,
8922) -> Vec<f32> {
8923    let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8924    for q_idx in 0..seq {
8925        let abs_q = cached_len + q_idx;
8926        for k_idx in 0..max_seq_len {
8927            // Causal: no future keys. Window: nothing older than `window` back.
8928            if k_idx > abs_q || k_idx + window <= abs_q {
8929                mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8930            }
8931        }
8932    }
8933    mask_data
8934}
8935
8936#[cfg(test)]
8937mod decode_mask_tests {
8938    use super::{build_decode_causal_mask, build_decode_causal_mask_windowed};
8939
8940    /// Render a mask row as `1` for attend / `0` for masked, so a failure
8941    /// prints the shape of the disagreement rather than a wall of `-inf`.
8942    fn attendable(row: &[f32]) -> Vec<u8> {
8943        row.iter().map(|&v| u8::from(v == 0.0)).collect()
8944    }
8945
8946    /// **The discriminating test.** Hand-computed ground truth, NOT a
8947    /// re-derivation of the implementation's own formula: at `cached_len = 3`,
8948    /// `seq = 1`, `max_seq_len = 6`, `window = 2` the single query sits at
8949    /// absolute position 3 and may attend to positions `{2, 3}` — 3 itself
8950    /// (causal boundary) and 2 (one step back, inside a window of 2). Positions
8951    /// 0 and 1 fall out of the window; 4 and 5 are the unwritten future tail.
8952    #[test]
8953    fn windowed_mask_excludes_positions_older_than_the_window() {
8954        let m = build_decode_causal_mask_windowed(3, 1, 6, 2);
8955        assert_eq!(
8956            attendable(&m),
8957            vec![0, 0, 1, 1, 0, 0],
8958            "window=2 at absolute position 3 must attend to exactly {{2, 3}}",
8959        );
8960    }
8961
8962    /// Multi-row (prefill-shaped) ground truth, also by hand: `cached_len = 1`,
8963    /// `seq = 2`, `window = 2` puts the queries at absolute positions 1 and 2,
8964    /// attending to `{0, 1}` and `{1, 2}` respectively. Guards the `q_idx`
8965    /// → `abs_q` offset, which a `seq == 1` test cannot see at all.
8966    #[test]
8967    fn windowed_mask_offsets_each_row_by_cached_len() {
8968        let m = build_decode_causal_mask_windowed(1, 2, 5, 2);
8969        assert_eq!(
8970            attendable(&m[0..5]),
8971            vec![1, 1, 0, 0, 0],
8972            "row 0 = abs pos 1"
8973        );
8974        assert_eq!(
8975            attendable(&m[5..10]),
8976            vec![0, 1, 1, 0, 0],
8977            "row 1 = abs pos 2"
8978        );
8979    }
8980
8981    /// **Non-discrimination control, and it is what keeps the two tests above
8982    /// meaningful.** A window at least as wide as the capacity cannot mask
8983    /// anything the causal clause does not already mask, so the windowed builder
8984    /// must reproduce the dense one byte-for-byte. This passes under BOTH a
8985    /// correct implementation and one that ignores `window` entirely — so it
8986    /// certifies that the suite is not simply broken, and it must NOT be read as
8987    /// evidence that the window works. (Same role as the positive control that
8988    /// stayed `ok` in increment 2a's sabotage record.)
8989    #[test]
8990    fn window_wider_than_capacity_is_byte_identical_to_the_dense_mask() {
8991        for (cached_len, seq, max_seq_len) in [(0, 1, 8), (3, 1, 8), (0, 4, 8), (2, 3, 8)] {
8992            let dense = build_decode_causal_mask(cached_len, seq, max_seq_len);
8993            let windowed =
8994                build_decode_causal_mask_windowed(cached_len, seq, max_seq_len, max_seq_len);
8995            assert_eq!(
8996                dense, windowed,
8997                "window == max_seq_len must equal dense at \
8998                 (cached_len={cached_len}, seq={seq}, max_seq_len={max_seq_len})",
8999            );
9000        }
9001    }
9002
9003    /// Decode must agree with the **shipped prefill** mask position-for-position,
9004    /// or a windowed model changes behaviour at the prefill→decode boundary.
9005    /// This replays `lazy_qwen2.rs:265-277`'s predicate over a full prefill of
9006    /// length `n`, then asserts that decoding position `p` with `cached_len = p`
9007    /// reproduces prefill row `p`. The prefill mask is `[seq, seq]` and the
9008    /// decode mask `[1, max_seq_len]`, so only the first `n` keys are comparable
9009    /// — the tail beyond `n` is the unwritten capacity the prefill mask has no
9010    /// opinion about.
9011    #[test]
9012    fn decode_row_matches_the_prefill_mask_row_at_the_same_position() {
9013        let (n, window, max_seq_len) = (6_usize, 3_usize, 10_usize);
9014
9015        // Qwen2's prefill predicate, transcribed from `build_layer_mask`.
9016        let mut prefill = vec![0.0_f32; n * n];
9017        for i in 0..n {
9018            for j in 0..n {
9019                if j > i || j + window <= i {
9020                    prefill[i * n + j] = f32::NEG_INFINITY;
9021                }
9022            }
9023        }
9024
9025        for p in 0..n {
9026            let decode = build_decode_causal_mask_windowed(p, 1, max_seq_len, window);
9027            assert_eq!(
9028                attendable(&decode[0..n]),
9029                attendable(&prefill[p * n..(p + 1) * n]),
9030                "decode at cached_len={p} must match prefill row {p}",
9031            );
9032            assert!(
9033                decode[n..].iter().all(|&v| v == f32::NEG_INFINITY),
9034                "the unwritten tail beyond position {n} must stay masked",
9035            );
9036        }
9037    }
9038
9039    // =======================================================================
9040    // BORN-RED RECORD (2026-08-13, GAP-029 increment 3)
9041    //
9042    // These tests were run FIRST against a deliberately wrong body — the
9043    // "old fabrication in the new shape": `build_decode_causal_mask_windowed`
9044    // returning `build_decode_causal_mask(..)` and ignoring `window`, which is
9045    // precisely what a single-mask decode port computes today. That makes the
9046    // red state prove *windowing vs dense* rather than merely proving the tests
9047    // execute.
9048    //
9049    //   windowed_mask_excludes_positions_older_than_the_window   FAILED
9050    //       left [1, 1, 1, 1, 0, 0]  right [0, 0, 1, 1, 0, 0]
9051    //   windowed_mask_offsets_each_row_by_cached_len             FAILED
9052    //       left [1, 1, 1, 0, 0]     right [0, 1, 1, 0, 0]
9053    //   decode_row_matches_the_prefill_mask_row_at_the_same_position FAILED
9054    //       left [1, 1, 1, 1, 0, 0]  right [0, 1, 1, 1, 0, 0]
9055    //   window_wider_than_capacity_is_byte_identical_to_the_dense_mask  ok
9056    //   test result: FAILED. 1 passed; 3 failed
9057    //
9058    // The `ok` line is load-bearing in BOTH directions and is the reason the
9059    // control exists: it holds under the correct body AND under the dense
9060    // fabrication, so (a) the suite is demonstrably not simply broken, and
9061    // (b) it must never be cited as evidence that windowing works. Only the
9062    // three that moved carry that claim.
9063    //
9064    // After the flip: `4 passed; 0 failed`, on a run reporting a 53.44s
9065    // compile — i.e. a rebuilt binary, not a cached one. A passing run without
9066    // confirmed recompilation would have certified nothing.
9067    //
9068    // WHAT THIS DOES **NOT** ESTABLISH, stated because the next reader will be
9069    // wiring this into a decode path: these are tests of the MASK BYTES ONLY.
9070    // No decode has yet been run with a windowed mask, so "a single-mask decode
9071    // port is silently wrong for Qwen2/Qwen3/Qwen3Moe" remains an INFERENCE
9072    // from the mask's meaning — argued from the fact that one
9073    // `[1, 1, seq, max_seq_len]` Const cannot express two layer groups. The
9074    // logits-level born-red against Qwen2's live mixed config
9075    // (`use_sliding_window: true, max_window_layers: 1` over 2 layers,
9076    // `lazy_qwen2.rs:441-443`) is the measurement that would settle it, and it
9077    // is NOT in this change.
9078    // =======================================================================
9079}
9080
9081/// Build a [`fuel_dispatch::decode_flash::DecodeFlashSpec`] from a decode
9082/// attention region's tensor handles and offer the optimizer-owned CUDA
9083/// flash-decode arm on the shared graph.
9084///
9085/// This is the model-layer WIRING for [`fuel_dispatch::decode_flash::offer_decode_flash_arm`]: it supplies
9086/// the region's tensor handles + the live attended-prefix `k_len` (as
9087/// `Sym(attended_len_sym)`, resolved per-token via the `SymEnv`) — data the
9088/// model alone knows — while every strategic decision (the shape/dtype/config
9089/// gate, the capability gate, the CUDA pin, the `Op::Branch` construction)
9090/// stays in the dispatch layer. The region is always **causal** with no
9091/// window / softcap / ALiBi (the LlamaModel decode shape).
9092///
9093/// - `q` — the RoPE'd query (`[B, Hq, 1, D]` in decode), also the branch
9094///   diverge point;
9095/// - `k` / `v` — the post-`WriteSlice` capacity KV buffers (`[B, Hkv,
9096///   capacity, D]`);
9097/// - `decomposed_out` — the region's attention output (arm 0 / the oracle);
9098/// - `reconverge` — the sole consumer of `decomposed_out` (the merge).
9099///
9100/// Returns `Ok(None)` (graph untouched, byte-identical to today) whenever the
9101/// emitter's gate declines — f32/f64 dtype, `seq_q != 1` (prefill),
9102/// `head_dim > 128`, or a non-CUDA / kernel-absent topology. Never panics.
9103/// Translate a Fuel sliding-window width into the FA-v2 `window_size_*`
9104/// convention the CUDA flash path speaks.
9105///
9106/// Fuel's mask predicate is `j > i || j + window <= i`, i.e. query `i` attends
9107/// keys `j ∈ [i - window + 1, i]` — **`window - 1` positions LEFT and `0`
9108/// RIGHT**. FA-v2 reads `is_causal` as `window_size_right == 0 &&
9109/// window_size_left < 0`, and treats *both* bounds `>= 0` as opting into local
9110/// attention (`fuel-cuda-backend/src/flash_attn.rs::translate_window`).
9111///
9112/// `None` (a dense layer) stays `(None, None)` — plain causal, which is what
9113/// every non-windowed family wants and what the decode kernel implements.
9114///
9115/// The `w - 1` is saturating: a nonsensical `window == 0` yields `Some(0)`,
9116/// which the admissibility gate rejects along with every other window. It never
9117/// silently becomes "no window".
9118pub fn flash_window_bounds(window: Option<usize>) -> (Option<usize>, Option<usize>) {
9119    match window {
9120        None => (None, None),
9121        Some(w) => (Some(w.saturating_sub(1)), Some(0)),
9122    }
9123}
9124
9125// 11 independent graph handles (q/k/v/decomposed/reconverge node ids) plus the
9126// scalar rewrite parameters (scale, attended-len sym, window, softcap, backend
9127// capability) for one flash-decode-arm offer. Each is a distinct graph node or
9128// scalar, not a bundle-able struct. Exceeds even the raised (10) threshold.
9129pub fn offer_flash_decode_arm_for_region(
9130    graph: &fuel_graph::SharedGraph,
9131    q: fuel_graph::NodeId,
9132    k: fuel_graph::NodeId,
9133    v: fuel_graph::NodeId,
9134    decomposed_out: fuel_graph::NodeId,
9135    reconverge: fuel_graph::NodeId,
9136    softmax_scale: f32,
9137    attended_len_sym: fuel_ir::SymId,
9138    attn_window: Option<usize>,
9139    softcap: Option<f32>,
9140    cap: fuel_dispatch::decode_flash::FlashArmCapability,
9141) -> crate::Result<Option<fuel_graph::NodeId>> {
9142    use fuel_dispatch::decode_flash::{DecodeFlashSpec, offer_decode_flash_arm};
9143    let (window_size_left, window_size_right) = flash_window_bounds(attn_window);
9144    let spec = DecodeFlashSpec {
9145        q,
9146        k,
9147        v,
9148        alibi: None,
9149        softmax_scale,
9150        causal: true,
9151        // ⚠️ GAP-194: these three were hardcoded `None`. That is TRUE for
9152        // `LlamaModel` — no window, no softcap — and would be a LIE for a
9153        // windowed or softcapped family, whose arm would then attend the whole
9154        // prefix and skip the cap, silently. A guard must not encode a claim
9155        // that is false: the caller states them, and the admissibility gate
9156        // declines what `flash_decoding` cannot implement.
9157        window_size_left,
9158        window_size_right,
9159        softcap,
9160        k_len: fuel_ir::DynScalar::Sym(attended_len_sym),
9161        decomposed_out,
9162        reconverge,
9163    };
9164    let mut g = graph.write().map_err(|_| {
9165        fuel_ir::Error::Msg("graph lock poisoned during flash-arm offer".into()).bt()
9166    })?;
9167    offer_decode_flash_arm(&mut g, &spec, cap)
9168}
9169
9170pub fn apply_affine_rms_norm(x: &Tensor, gain: &Arc<[f32]>, dim: usize, eps: f64) -> Tensor {
9171    assert_eq!(
9172        gain.len(),
9173        dim,
9174        "apply_affine_rms_norm: gain length must equal dim"
9175    );
9176    let normalized = x.rms_norm_last_dim(eps).unwrap();
9177    // The gain is always stored f32 (norm gains are precision-sensitive —
9178    // see `LayerWeights::attn_norm_gain`'s doc) but must be materialized
9179    // in `x`'s dtype: under BF16-throughout decode (Phase D increment A)
9180    // `x` is BF16 and `broadcast_mul` asserts dtype equality, so an f32
9181    // gain would panic. No-op conversion for f32 activations.
9182    let gain_t = x
9183        .const_like_dtype(gain, Shape::from_dims(&[dim]), x.dtype())
9184        .expect("apply_affine_rms_norm: activation dtype must be F32 or BF16");
9185    normalized.broadcast_mul(&gain_t).unwrap()
9186}
9187
9188// ---- HuggingFace Hub and safetensors weight loading ----------------------
9189
9190/// Load a tensor by name from a `MmapedSafetensors` as a flat
9191/// `Vec<f32>`, converting from whatever dtype the file stores it in.
9192/// Handles `F32`, `F64`, `BF16`, and `F16` — the dtypes real LLaMA
9193/// weights use on disk. Returns an error for unsupported dtypes.
9194pub fn load_tensor_as_f32(
9195    st: &fuel_loaders::safetensors::MmapedSafetensors,
9196    name: &str,
9197) -> crate::Result<Vec<f32>> {
9198    use safetensors::Dtype;
9199    let view = st.get(name)?;
9200    let bytes = view.data();
9201    match view.dtype() {
9202        Dtype::F32 => {
9203            let mut out = Vec::with_capacity(bytes.len() / 4);
9204            for chunk in bytes.as_chunks::<4>().0.iter() {
9205                out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
9206            }
9207            Ok(out)
9208        }
9209        Dtype::F64 => {
9210            let mut out = Vec::with_capacity(bytes.len() / 8);
9211            for chunk in bytes.as_chunks::<8>().0.iter() {
9212                let arr: [u8; 8] = *chunk;
9213                out.push(f64::from_le_bytes(arr) as f32);
9214            }
9215            Ok(out)
9216        }
9217        Dtype::BF16 => {
9218            let mut out = Vec::with_capacity(bytes.len() / 2);
9219            for chunk in bytes.as_chunks::<2>().0.iter() {
9220                let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9221                out.push(half::bf16::from_bits(raw).to_f32());
9222            }
9223            Ok(out)
9224        }
9225        Dtype::F16 => {
9226            let mut out = Vec::with_capacity(bytes.len() / 2);
9227            for chunk in bytes.as_chunks::<2>().0.iter() {
9228                let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9229                out.push(half::f16::from_bits(raw).to_f32());
9230            }
9231            Ok(out)
9232        }
9233        other => {
9234            crate::bail!("load_tensor_as_f32: unsupported dtype {other:?} for tensor {name:?}",)
9235        }
9236    }
9237}
9238
9239/// Load a tensor by name and physically transpose it from `[out, in]`
9240/// (HuggingFace layout) to `[in, out]` (fuel-graph's layout for
9241/// `x @ W` where `W` is `[in, out]`). Linear-layer weights in HF
9242/// transformers are stored as `[out_features, in_features]`, so every
9243/// call to this function is effectively "give me that matrix as I'd
9244/// use it in `matmul`."
9245pub fn load_transposed_matrix(
9246    st: &fuel_loaders::safetensors::MmapedSafetensors,
9247    name: &str,
9248    out_features: usize,
9249    in_features: usize,
9250) -> crate::Result<Vec<f32>> {
9251    let flat = load_tensor_as_f32(st, name)?;
9252    if flat.len() != out_features * in_features {
9253        crate::bail!(
9254            "load_transposed_matrix: tensor {name:?} has {} elements, expected {} ({out_features} × {in_features})",
9255            flat.len(),
9256            out_features * in_features,
9257        );
9258    }
9259    // HF layout: flat[i * in_features + j] is W[i, j] for (out i, in j).
9260    // Target layout: out[j * out_features + i] so that indexing `[j, i]`
9261    // in row-major gives the same W[i, j] — i.e. out has shape [in, out].
9262    let mut out = vec![0.0_f32; out_features * in_features];
9263    for i in 0..out_features {
9264        for j in 0..in_features {
9265            out[j * out_features + i] = flat[i * in_features + j];
9266        }
9267    }
9268    Ok(out)
9269}
9270
9271/// Transposed-matrix loader that preserves source dtype. For
9272/// safetensors files saved with bf16 weights, returns
9273/// `WeightStorage::BF16` and never materializes an f32 copy — the
9274/// 2× memory saving vs `load_transposed_matrix` comes from here.
9275/// f32 and other source dtypes still go through the f32 upcast path
9276/// for safety; extending this to preserve f16 is a one-line change
9277/// when a consumer wants it.
9278///
9279/// The transpose itself is done in whatever dtype we're keeping:
9280/// read bf16 elements from the file, place them in the transposed
9281/// target buffer, no conversion.
9282pub fn load_transposed_matrix_preserve_dtype(
9283    st: &fuel_loaders::safetensors::MmapedSafetensors,
9284    name: &str,
9285    out_features: usize,
9286    in_features: usize,
9287) -> crate::Result<WeightStorage> {
9288    use safetensors::Dtype;
9289    let view = st.get(name)?;
9290    let bytes = view.data();
9291    let expected = out_features * in_features;
9292    match view.dtype() {
9293        Dtype::BF16 => {
9294            if bytes.len() != expected * 2 {
9295                crate::bail!(
9296                    "load_transposed_matrix_preserve_dtype: bf16 tensor {name:?} has {} bytes, expected {}",
9297                    bytes.len(),
9298                    expected * 2,
9299                );
9300            }
9301            // Reinterpret input as [out_features, in_features] of
9302            // bf16; write transposed layout.
9303            let mut out = vec![half::bf16::ZERO; expected];
9304            for i in 0..out_features {
9305                for j in 0..in_features {
9306                    let src_off = (i * in_features + j) * 2;
9307                    let bits = u16::from_le_bytes([bytes[src_off], bytes[src_off + 1]]);
9308                    out[j * out_features + i] = half::bf16::from_bits(bits);
9309                }
9310            }
9311            Ok(WeightStorage::BF16(Arc::from(out)))
9312        }
9313        _ => {
9314            // F32, F64, F16 all fall through to the f32 upcast path.
9315            // Non-f32 source types still benefit from being readable;
9316            // they just lose the "weights stay compact" win.
9317            let flat = load_transposed_matrix(st, name, out_features, in_features)?;
9318            Ok(WeightStorage::F32(Arc::from(flat)))
9319        }
9320    }
9321}
9322
9323/// Sampling strategy for decode loops.
9324#[derive(Debug, Clone, Copy, Default)]
9325pub enum SamplingStrategy {
9326    /// Greedy: always pick the highest-probability token.
9327    #[default]
9328    Greedy,
9329    /// Temperature-scaled sampling with a deterministic seed. `temp`
9330    /// is the softmax temperature (`1.0` is unscaled, `0.0` is
9331    /// effectively greedy, higher values spread probability mass).
9332    /// The seed makes sampling reproducible.
9333    Temperature { temp: f32, seed: u64 },
9334}
9335
9336/// Pick the next token from a logits vector using the configured
9337/// sampling strategy. Pulled out of `generate` so both the cached and
9338/// future non-cached callers can share it.
9339pub fn sample_logits(logits: &[f32], strategy: SamplingStrategy, rng_state: &mut u64) -> u32 {
9340    match strategy {
9341        SamplingStrategy::Greedy => {
9342            let (i, _) = logits
9343                .iter()
9344                .enumerate()
9345                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
9346                .expect("sample_logits: empty logits");
9347            i as u32
9348        }
9349        SamplingStrategy::Temperature { temp, .. } => {
9350            // Stable softmax over optionally temperature-scaled logits,
9351            // then a deterministic multinomial draw.
9352            let inv_temp = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9353            let scaled: Vec<f32> = logits.iter().map(|&x| x * inv_temp).collect();
9354            let max = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9355            let exp: Vec<f32> = scaled.iter().map(|&x| (x - max).exp()).collect();
9356            let sum: f32 = exp.iter().sum();
9357            let probs: Vec<f32> = exp.iter().map(|&x| x / sum).collect();
9358            sample_multinomial(&probs, rng_state)
9359        }
9360    }
9361}
9362
9363// ---- Speculative-decoding helpers ---------------------------------------
9364//
9365// Shared by `generate_streaming_spec_with_kv_context`'s draft / accept
9366// phases. All host-side: spec decode's accept rule operates on logits
9367// vectors already downloaded from the device.
9368
9369/// Greedy argmax over a logits row.
9370pub fn spec_argmax(logits: &[f32]) -> u32 {
9371    let mut best = 0;
9372    let mut best_v = logits[0];
9373    for (i, &v) in logits.iter().enumerate().skip(1) {
9374        if v > best_v {
9375            best_v = v;
9376            best = i;
9377        }
9378    }
9379    best as u32
9380}
9381
9382/// Temperature-scaled softmax. Returns normalized probabilities.
9383pub fn spec_softmax_temp(logits: &[f32], temp: f32) -> Vec<f32> {
9384    let inv_t = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9385    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9386    let exp: Vec<f32> = logits.iter().map(|&x| ((x - max) * inv_t).exp()).collect();
9387    let sum: f32 = exp.iter().sum();
9388    exp.iter().map(|&x| x / sum).collect()
9389}
9390
9391/// Advance a deterministic LCG and return a u01 uniform.
9392pub fn spec_next_u01(state: &mut u64) -> f32 {
9393    *state = state
9394        .wrapping_mul(6364136223846793005)
9395        .wrapping_add(1442695040888963407);
9396    (*state >> 32) as f32 / u32::MAX as f32
9397}
9398
9399/// Sample a category from a distribution summing to ~1.
9400pub fn spec_sample_cat(probs: &[f32], state: &mut u64) -> u32 {
9401    let u = spec_next_u01(state);
9402    let mut cum = 0.0_f32;
9403    for (i, &p) in probs.iter().enumerate() {
9404        cum += p;
9405        if u <= cum {
9406            return i as u32;
9407        }
9408    }
9409    (probs.len() - 1) as u32
9410}
9411
9412/// Sample a categorical distribution using a small deterministic LCG.
9413/// Takes `probs` (assumed to sum to ~1) and a mutable RNG state,
9414/// returns a sampled index.
9415pub fn sample_multinomial(probs: &[f32], rng_state: &mut u64) -> u32 {
9416    // Advance the LCG and turn it into a u01 uniform.
9417    *rng_state = rng_state
9418        .wrapping_mul(6364136223846793005)
9419        .wrapping_add(1442695040888963407);
9420    let u = (*rng_state >> 32) as f32 / u32::MAX as f32;
9421    let mut cumulative = 0.0_f32;
9422    for (i, &p) in probs.iter().enumerate() {
9423        cumulative += p;
9424        if u <= cumulative {
9425            return i as u32;
9426        }
9427    }
9428    // Floating-point slop: fall through to the last index.
9429    (probs.len() - 1) as u32
9430}
9431
9432#[cfg(test)]
9433mod lora_tests {
9434    use super::*;
9435    use crate::Shape;
9436
9437    #[test]
9438    fn with_lora_matches_manual_base_plus_lora() {
9439        // Anchor graph.
9440        let in_f = 4;
9441        let out_f = 3;
9442        let rank = 2;
9443        let alpha = 8.0_f32;
9444
9445        let anchor =
9446            Tensor::from_f32(vec![0.0_f32; 1], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
9447        // Base weight [in, out].
9448        let base_vec: Vec<f32> = (0..in_f * out_f).map(|i| (i as f32) * 0.1).collect();
9449        let lora_a_vec: Vec<f32> = (0..in_f * rank).map(|i| (i as f32) * 0.05).collect();
9450        let lora_b_vec: Vec<f32> = (0..rank * out_f).map(|i| (i as f32) * 0.02).collect();
9451
9452        let ws = WeightStorage::F32(Arc::from(base_vec.clone())).with_lora(
9453            Arc::from(lora_a_vec.clone()),
9454            Arc::from(lora_b_vec.clone()),
9455            rank,
9456            alpha,
9457            in_f,
9458            out_f,
9459        );
9460
9461        // Activations x [2, in_f].
9462        let batch = 2;
9463        let x_data: Vec<f32> = (0..batch * in_f).map(|i| (i as f32) * 0.1 + 0.5).collect();
9464        let x = anchor
9465            .const_f32_like(x_data.clone(), Shape::from_dims(&[batch, in_f]))
9466            .unwrap();
9467        let y = ws.apply_linear(&x, in_f, out_f).unwrap();
9468        let got = y.realize_f32().to_vec();
9469
9470        // Reference: base + (alpha/rank) * x @ A @ B, all f32, on CPU.
9471        let mut expected = vec![0.0_f32; batch * out_f];
9472        for b in 0..batch {
9473            for j in 0..out_f {
9474                let mut acc = 0.0_f32;
9475                // Base path: sum_k x[b,k] * W[k,j].
9476                for k in 0..in_f {
9477                    acc += x_data[b * in_f + k] * base_vec[k * out_f + j];
9478                }
9479                // LoRA path: sum_r (sum_k x[b,k] * A[k,r]) * B[r,j] * (alpha/rank).
9480                let scale = alpha as f64 / rank as f64;
9481                for r in 0..rank {
9482                    let mut xar = 0.0_f32;
9483                    for k in 0..in_f {
9484                        xar += x_data[b * in_f + k] * lora_a_vec[k * rank + r];
9485                    }
9486                    acc += (xar * lora_b_vec[r * out_f + j]) * scale as f32;
9487                }
9488                expected[b * out_f + j] = acc;
9489            }
9490        }
9491
9492        for (i, (&e, &g)) in expected.iter().zip(got.iter()).enumerate() {
9493            let diff = (e - g).abs();
9494            assert!(
9495                diff <= 1e-4,
9496                "LoRA mismatch at {i}: expected {e}, got {g} (diff {diff})",
9497            );
9498        }
9499    }
9500
9501    #[test]
9502    #[should_panic(expected = "lora_a length")]
9503    fn with_lora_rejects_mismatched_a_shape() {
9504        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9505        let bad_a = Arc::from(vec![0.0_f32; 3]); // wrong
9506        let b = Arc::from(vec![0.0_f32; 6]); // 2 x 3
9507        let _ = ws.with_lora(bad_a, b, 2, 8.0, 4, 3);
9508    }
9509
9510    // ---- never-panic conversion (`apply_linear` -> `Result`) ----
9511    //
9512    // These three inputs each used to abort the process on a PRODUCTION path:
9513    // a wrong trailing dim reached `x.matmul(&w).unwrap()`, a stored/requested
9514    // shape disagreement hit `assert_eq!`, and a wrong-length bias hit a
9515    // `debug_assert_eq!` that is compiled OUT of release builds — so release
9516    // binaries broadcast a mis-sized bias with no complaint at all. All three
9517    // are checkpoint/data conditions rather than programming invariants (a
9518    // GGUF whose stored shape disagrees with the model config produces exactly
9519    // this), so all three must be `Err`.
9520
9521    #[test]
9522    fn apply_linear_errors_on_trailing_dim_mismatch() {
9523        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9524        let x = Tensor::from_f32(
9525            vec![0.0_f32; 10],
9526            Shape::from_dims(&[2, 5]), // trailing dim 5 != in_features 4
9527            &Device::cpu(),
9528        )
9529        .unwrap();
9530        let err = ws
9531            .apply_linear(&x, 4, 3)
9532            .expect_err("must not panic, must Err");
9533        let msg = err.to_string();
9534        assert!(
9535            msg.contains("trailing dim 4"),
9536            "error should name the expected trailing dim, got: {msg}",
9537        );
9538    }
9539
9540    #[test]
9541    fn apply_linear_errors_on_q4_0_stored_shape_mismatch() {
9542        // 4 x 4 of Q4_0: one 32-element block needs 18 bytes; keep the word
9543        // count self-consistent since only the shape check is under test.
9544        let ws = WeightStorage::Q4_0 {
9545            words: Arc::from(vec![0_u32; 8]),
9546            bytes_len: 18, // one Q4_0 block: 2-byte scale + 16 packed nibble bytes
9547            in_features: 4,
9548            out_features: 4,
9549        };
9550        let x =
9551            Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9552        // Trailing dim matches in_features, so this reaches the stored-shape
9553        // check rather than the contract check above.
9554        let err = ws
9555            .apply_linear(&x, 4, 7)
9556            .expect_err("must not panic, must Err");
9557        let msg = err.to_string();
9558        assert!(
9559            msg.contains("Q4_0 shape mismatch"),
9560            "error should name the stored/requested disagreement, got: {msg}",
9561        );
9562    }
9563
9564    #[test]
9565    fn apply_linear_with_bias_errors_on_wrong_bias_length() {
9566        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9567        let x =
9568            Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9569        let bias: Arc<[f32]> = Arc::from(vec![0.0_f32; 2]); // wrong: out_features is 3
9570        let err = ws
9571            .apply_linear_with_bias(&x, 4, 3, bias)
9572            .expect_err("wrong-length bias must Err in EVERY profile, not just debug");
9573        assert!(
9574            err.to_string().contains("bias len"),
9575            "error should name the bias length, got: {err}",
9576        );
9577    }
9578}
9579
9580#[cfg(test)]
9581mod safetensors_bridge_tests {
9582    use super::*;
9583
9584    #[test]
9585    fn from_safetensors_bytes_round_trip_f32() {
9586        // Build a tensor, serialize it as little-endian f32 bytes,
9587        // then deserialize via the bridge. Should get back the same
9588        // values.
9589        let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
9590        let mut bytes = Vec::with_capacity(original.len() * 4);
9591        for &v in &original {
9592            bytes.extend_from_slice(&v.to_le_bytes());
9593        }
9594        let t =
9595            Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::F32, &[4], &Device::cpu())
9596                .unwrap();
9597        assert_eq!(t.shape().dims(), &[4]);
9598        assert_eq!(t.dtype(), DType::F32);
9599        assert_eq!(t.realize_f32(), original);
9600    }
9601
9602    #[test]
9603    fn from_safetensors_bytes_round_trip_bf16() {
9604        let original_f32: Vec<f32> = vec![0.5, -1.0, 2.0, 4.0];
9605        let bf16_vec: Vec<half::bf16> = original_f32
9606            .iter()
9607            .map(|&v| half::bf16::from_f32(v))
9608            .collect();
9609        let mut bytes = Vec::with_capacity(bf16_vec.len() * 2);
9610        for b in &bf16_vec {
9611            bytes.extend_from_slice(&b.to_bits().to_le_bytes());
9612        }
9613        let t =
9614            Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::BF16, &[4], &Device::cpu())
9615                .unwrap();
9616        assert_eq!(t.dtype(), DType::BF16);
9617        // Values that round-trip exactly through bf16 should come back
9618        // unchanged.
9619        let realized = t.realize_bf16();
9620        assert_eq!(realized, bf16_vec);
9621    }
9622
9623    #[test]
9624    fn from_safetensors_bytes_rejects_wrong_byte_count() {
9625        // Shape says 3 elements, but we pass 4 bytes (1 f32 = 4 bytes
9626        // so 3 elements would need 12 bytes).
9627        let bad_bytes = vec![0_u8; 4];
9628        let result = Tensor::from_safetensors_bytes(
9629            &bad_bytes,
9630            safetensors::Dtype::F32,
9631            &[3],
9632            &Device::cpu(),
9633        );
9634        assert!(result.is_err());
9635    }
9636}
9637
9638// ============================================================================
9639// Phase A.1 wrapper smoke tests.
9640//
9641// Pure pass-through tests: realize and assert the returned tensor has the
9642// expected shape / dtype / values. The graph-level ops are tested in
9643// `fuel-graph`; here we only verify that the Tensor wrappers don't
9644// drop information or mis-thread arguments.
9645// ============================================================================
9646#[cfg(test)]
9647mod phase_a1_wrapper_tests {
9648    use super::*;
9649
9650    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
9651        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
9652    }
9653
9654    #[test]
9655    fn unsqueeze_adds_size_one_dim() {
9656        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9657        let out = t.unsqueeze(0_usize).unwrap();
9658        assert_eq!(out.shape().dims(), &[1, 2, 2]);
9659        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
9660    }
9661
9662    #[test]
9663    fn unsqueeze_errors_out_of_bounds() {
9664        let t = cpu_f32(vec![1.0, 2.0], &[2]);
9665        // rank=1, so dim<=1 is valid; dim=2 must error.
9666        assert!(t.unsqueeze(0_usize).is_ok());
9667        assert!(t.unsqueeze(1_usize).is_ok());
9668        assert!(t.unsqueeze(2_usize).is_err());
9669    }
9670
9671    #[test]
9672    fn try_reshape_errors_on_size_mismatch() {
9673        let t = cpu_f32(vec![1.0; 6], &[2, 3]);
9674        assert!(t.reshape(vec![3, 2]).is_ok());
9675        assert!(t.reshape(vec![2, 2]).is_err());
9676    }
9677
9678    #[test]
9679    fn try_permute_validates_axes() {
9680        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
9681        // The Dims trait accepts tuples, owned arrays, and slices.
9682        assert!(t.permute((2_usize, 0_usize, 1_usize)).is_ok());
9683        assert!(t.permute([2_usize, 0, 1]).is_ok());
9684        assert!(t.permute([0_usize, 1]).is_err()); // wrong rank
9685        assert!(t.permute([0_usize, 0, 1]).is_err()); // dup axis
9686    }
9687
9688    #[test]
9689    fn try_transpose_requires_rank_two_plus() {
9690        let scalar = cpu_f32(vec![1.0], &[1]);
9691        // rank-1: transpose surfaces a typed error at build time.
9692        let _ = scalar.transpose();
9693    }
9694
9695    #[test]
9696    fn triu_tril_shape_preserved() {
9697        let t = cpu_f32(vec![1.0; 9], &[3, 3]);
9698        let upper = t.triu(0).unwrap();
9699        let lower = t.tril(0).unwrap();
9700        assert_eq!(upper.shape().dims(), &[3, 3]);
9701        assert_eq!(lower.shape().dims(), &[3, 3]);
9702        // tril(0) of all-ones: 1s on/below diagonal, 0s above
9703        assert_eq!(
9704            lower.realize_f32(),
9705            vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0]
9706        );
9707    }
9708
9709    #[test]
9710    fn triu_rejects_rank_one() {
9711        let t = cpu_f32(vec![1.0, 2.0], &[2]);
9712        assert!(t.triu(0).is_err());
9713    }
9714
9715    #[test]
9716    fn log_softmax_last_dim_shape_preserved() {
9717        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9718        let out = t.log_softmax_last_dim().unwrap();
9719        assert_eq!(out.shape().dims(), &[2, 2]);
9720        // log_softmax values must be <= 0.
9721        for v in out.realize_f32() {
9722            assert!(v <= 0.0 + 1e-6, "log_softmax produced positive value: {v}");
9723        }
9724    }
9725
9726    #[test]
9727    fn softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9728        // shape [2, 3, 2] — softmax along axis=1 (the size-3 axis).
9729        // Layout (row-major): element (b, r, c) lives at b*6 + r*2 + c.
9730        // We hand-pick values so each (b, c) lane's max is exactly 0 to
9731        // make the reference closed-form: probs = exp(x) / sum_r exp(x).
9732        // Lane (b=0, c=0): values [-1, 0, -2]  → exp = [e^-1, 1, e^-2]
9733        // Lane (b=0, c=1): values [ 0,-3, -1]  → exp = [1, e^-3, e^-1]
9734        // Lane (b=1, c=0): values [-2,-1,  0]  → exp = [e^-2, e^-1, 1]
9735        // Lane (b=1, c=1): values [-1,-1,  0]  → exp = [e^-1, e^-1, 1]
9736        let data: Vec<f32> = vec![
9737            // b=0
9738            -1.0, 0.0, // r=0, c=0..1
9739            0.0, -3.0, // r=1
9740            -2.0, -1.0, // r=2
9741            // b=1
9742            -2.0, -1.0, // r=0
9743            -1.0, -1.0, // r=1
9744            0.0, 0.0, // r=2
9745        ];
9746        let t = cpu_f32(data, &[2, 3, 2]);
9747        let out = t.softmax(1_usize).unwrap();
9748        assert_eq!(out.shape().dims(), &[2, 3, 2]);
9749        let v = out.realize_f32();
9750
9751        // Reference: closed-form softmax per (b, c) lane.
9752        let lane_softmax = |xs: [f32; 3]| -> [f32; 3] {
9753            let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9754            let exps = [(xs[0] - m).exp(), (xs[1] - m).exp(), (xs[2] - m).exp()];
9755            let s = exps[0] + exps[1] + exps[2];
9756            [exps[0] / s, exps[1] / s, exps[2] / s]
9757        };
9758        // Re-extract source lanes
9759        let lanes: [[f32; 3]; 4] = [
9760            [-1.0, 0.0, -2.0], // (b=0, c=0)
9761            [0.0, -3.0, -1.0], // (b=0, c=1)
9762            [-2.0, -1.0, 0.0], // (b=1, c=0)
9763            [-1.0, -1.0, 0.0], // (b=1, c=1)
9764        ];
9765        let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_softmax(*l)).collect();
9766
9767        // out[b, r, c] at index b*6 + r*2 + c — verify each (b, c) lane.
9768        for b in 0..2 {
9769            for c in 0..2 {
9770                let lane_ix = b * 2 + c;
9771                for r in 0..3 {
9772                    let got = v[b * 6 + r * 2 + c];
9773                    let want = refs[lane_ix][r];
9774                    assert!(
9775                        (got - want).abs() < 1e-6,
9776                        "softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9777                    );
9778                }
9779                // sanity: lane sums to 1
9780                let sum: f32 = (0..3).map(|r| v[b * 6 + r * 2 + c]).sum();
9781                assert!((sum - 1.0).abs() < 1e-6, "lane (b={b},c={c}) sums to {sum}");
9782            }
9783        }
9784    }
9785
9786    #[test]
9787    fn softmax_last_axis_matches_softmax_last_dim() {
9788        let data: Vec<f32> = vec![
9789            1.0, 2.0, -1.0, 0.5, 0.0, -2.0, 3.0, 1.5, // batch dim 2
9790            4.0, -1.0, 2.0, 0.0, -3.0, 0.25, 0.75, 1.0,
9791        ];
9792        let t = cpu_f32(data, &[2, 2, 4]);
9793        let via_general = t.softmax(2_usize).unwrap();
9794        let via_fused = t.softmax_last_dim().unwrap();
9795        assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9796        let g = via_general.realize_f32();
9797        let f = via_fused.realize_f32();
9798        assert_eq!(g.len(), f.len());
9799        for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9800            assert!(
9801                (a - b).abs() < 1e-6,
9802                "softmax (general axis=2) vs softmax_last_dim diverge at {i}: {a} vs {b}",
9803            );
9804        }
9805    }
9806
9807    #[test]
9808    fn log_softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9809        // Same construction as the softmax test, but compare against
9810        // closed-form log_softmax per (b, c) lane.
9811        let data: Vec<f32> = vec![
9812            -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0,
9813        ];
9814        let t = cpu_f32(data, &[2, 3, 2]);
9815        let out = t.log_softmax(1_usize).unwrap();
9816        assert_eq!(out.shape().dims(), &[2, 3, 2]);
9817        let v = out.realize_f32();
9818
9819        let lane_log_softmax = |xs: [f32; 3]| -> [f32; 3] {
9820            let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9821            let shifted = [xs[0] - m, xs[1] - m, xs[2] - m];
9822            let lse = (shifted[0].exp() + shifted[1].exp() + shifted[2].exp()).ln();
9823            [shifted[0] - lse, shifted[1] - lse, shifted[2] - lse]
9824        };
9825        let lanes: [[f32; 3]; 4] = [
9826            [-1.0, 0.0, -2.0],
9827            [0.0, -3.0, -1.0],
9828            [-2.0, -1.0, 0.0],
9829            [-1.0, -1.0, 0.0],
9830        ];
9831        let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_log_softmax(*l)).collect();
9832
9833        for b in 0..2 {
9834            for c in 0..2 {
9835                let lane_ix = b * 2 + c;
9836                for r in 0..3 {
9837                    let got = v[b * 6 + r * 2 + c];
9838                    let want = refs[lane_ix][r];
9839                    assert!(
9840                        (got - want).abs() < 1e-6,
9841                        "log_softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9842                    );
9843                    // log_softmax values must be <= 0
9844                    assert!(got <= 1e-6, "log_softmax produced positive value: {got}");
9845                }
9846            }
9847        }
9848    }
9849
9850    #[test]
9851    fn log_softmax_last_axis_matches_log_softmax_last_dim() {
9852        let data: Vec<f32> = vec![
9853            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,
9854        ];
9855        let t = cpu_f32(data, &[2, 2, 4]);
9856        let via_general = t.log_softmax(2_usize).unwrap();
9857        let via_fused = t.log_softmax_last_dim().unwrap();
9858        assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9859        let g = via_general.realize_f32();
9860        let f = via_fused.realize_f32();
9861        assert_eq!(g.len(), f.len());
9862        for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9863            assert!(
9864                (a - b).abs() < 1e-6,
9865                "log_softmax (general axis=2) vs log_softmax_last_dim diverge at {i}: {a} vs {b}",
9866            );
9867        }
9868    }
9869
9870    #[test]
9871    fn argmin_dim_drops_reduced_axis() {
9872        let t = cpu_f32(vec![3.0, 1.0, 2.0, 0.5, 5.0, 4.0], &[2, 3]);
9873        let out = t.argmin_dim(1_usize).unwrap();
9874        assert_eq!(out.shape().dims(), &[2]);
9875        assert_eq!(out.dtype(), DType::U32);
9876        assert_eq!(out.realize_u32(), vec![1, 0]);
9877    }
9878
9879    #[test]
9880    fn masked_fill_smoke() {
9881        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9882        // Comparison ops produce Bool masks directly (GAP-168(c)) — masked_fill
9883        // now accepts Bool, no F32→mask cast needed.
9884        let probe = t
9885            .const_f32_like(vec![0.0, 1.0, 1.0, 0.0], vec![2, 2])
9886            .unwrap();
9887        let threshold = t.const_f32_like(vec![0.5; 4], vec![2, 2]).unwrap();
9888        let mask = probe.gt(&threshold).unwrap(); // [0, 1, 1, 0] as Bool
9889        let out = t.masked_fill(&mask, fuel_ir::Scalar::F32(-9.0)).unwrap();
9890        assert_eq!(out.realize_f32(), vec![1.0, -9.0, -9.0, 4.0]);
9891    }
9892
9893    /// GAP-168(c) DISCRIMINATION: `Bool` and `U8` are byte-identical in storage,
9894    /// which is exactly the condition under which a wrong wiring is *silently
9895    /// correct*. This test proves they are NOT interchangeable end-to-end on CPU:
9896    /// a comparison yields a `Bool` (not `U8`) tensor, and `to_dtype(Bool)` on a
9897    /// `U8` tensor is a real `!= 0` CONVERSION — never a byte reinterpret.
9898    #[test]
9899    fn bool_is_distinguishable_from_u8_end_to_end() {
9900        // A comparison realizes end-to-end as a Bool tensor (NOT U8) whose bytes
9901        // are 0/1 — the byte-identity hazard's most likely hiding spot, closed:
9902        // dtype tag is Bool, the CPU kernel dispatches on [F32,F32,Bool] (its FKC
9903        // contract declares fixed(BOOL)), and Copy[Bool] materializes it to host.
9904        let t = cpu_f32(vec![0.0, 5.0, 3.0, 0.0], &[4]);
9905        let thr = t.const_f32_like(vec![0.5; 4], vec![4]).unwrap();
9906        let mask = t.gt(&thr).unwrap();
9907        assert_eq!(mask.dtype(), DType::Bool, "gt yields Bool, not U8");
9908        assert_eq!(mask.realize_u8(), vec![0, 1, 1, 0], "mask bytes are 0/1");
9909
9910        // ---- The cast discrimination, now that the 22 Bool pairs are wired ----
9911        //
9912        // NEGATIVE CONTROL FIRST, or the assertion below is vacuous: build a U8
9913        // tensor that genuinely holds 5. If this line ever came back as 0/1, the
9914        // real test would pass for the wrong reason — it would be comparing 0/1
9915        // against 0/1 and could not tell a conversion from a reinterpret.
9916        let u8_vals = t
9917            .const_f32_like(vec![0.0, 5.0, 3.0, 0.0], vec![4])
9918            .unwrap()
9919            .to_dtype(DType::U8)
9920            .unwrap();
9921        assert_eq!(u8_vals.dtype(), DType::U8);
9922        assert_eq!(
9923            u8_vals.realize_u8(),
9924            vec![0, 5, 3, 0],
9925            "negative control: the U8 source must really hold 5 and 3, else the \
9926             conversion assertion below cannot discriminate"
9927        );
9928
9929        // THE DISCRIMINATION: U8 -> Bool is a real `!= 0` conversion. A byte
9930        // reinterpret would yield [0, 5, 3, 0] — byte-identical storage makes
9931        // that the silently-correct-looking failure. A conversion yields 0/1.
9932        let as_bool = u8_vals.to_dtype(DType::Bool).unwrap();
9933        assert_eq!(as_bool.dtype(), DType::Bool);
9934        assert_eq!(
9935            as_bool.realize_u8(),
9936            vec![0, 1, 1, 0],
9937            "U8->Bool must CONVERT (5 -> 1), not reinterpret (5 -> 5)"
9938        );
9939
9940        // And back out: Bool -> F32 is exactly 0.0/1.0.
9941        assert_eq!(
9942            as_bool.to_dtype(DType::F32).unwrap().realize_f32(),
9943            vec![0.0, 1.0, 1.0, 0.0],
9944            "Bool->F32 yields 0.0/1.0"
9945        );
9946        // Bool -> U8 keeps the canonical 0/1 (this direction IS byte-preserving,
9947        // which is fine — it is the OTHER direction that must not be).
9948        assert_eq!(
9949            mask.to_dtype(DType::U8).unwrap().realize_u8(),
9950            vec![0, 1, 1, 0]
9951        );
9952
9953        // FINALLY, the two are not INTERCHANGEABLE at the API, which is what
9954        // stops a numeric mask being coerced silently: `masked_fill` requires a
9955        // Bool mask and rejects U8 at GRAPH-BUILD time, so the cast is explicit
9956        // at the call site rather than implied.
9957        let err = t.masked_fill(&u8_vals, fuel_ir::Scalar::F32(-9.0));
9958        assert!(
9959            err.is_err(),
9960            "a U8 mask must be REJECTED by masked_fill — Bool and U8 are \
9961             byte-identical, so accepting it would 'work' and erase the distinction"
9962        );
9963    }
9964
9965    /// GAP-183: `float -> Bool` is lowered in the optimizer to `Ne(x, x*0)` so it
9966    /// reaches CUDA's shipped `ne_{f32,f64,f16,bf16}` kernels, which a
9967    /// storage-level cast cannot compose. This pins the VALUES end-to-end, and
9968    /// the three interesting inputs are the whole point of the test — a lowering
9969    /// that got only the ordinary cases right would still be wrong:
9970    ///
9971    ///   -0.0  -> FALSE   (IEEE `-0.0 == 0.0`)
9972    ///   NaN   -> TRUE    (matches PyTorch `.bool()`)
9973    ///   ±inf  -> TRUE
9974    ///
9975    /// The zeros operand is itself NaN for non-finite x; that is harmless because
9976    /// `x != NaN` and `x != 0` agree for every non-finite x. This test is what
9977    /// would catch it if that ever stopped being true.
9978    #[test]
9979    fn float_to_bool_lowering_preserves_ieee_semantics() {
9980        let t = cpu_f32(
9981            vec![
9982                0.0,
9983                -0.0,
9984                5.0,
9985                -3.0,
9986                f32::NAN,
9987                f32::INFINITY,
9988                f32::NEG_INFINITY,
9989            ],
9990            &[7],
9991        );
9992        let b = t.to_dtype(DType::Bool).unwrap();
9993        assert_eq!(b.dtype(), DType::Bool, "to_dtype(Bool) still yields Bool");
9994        assert_eq!(
9995            b.realize_u8(),
9996            vec![0, 0, 1, 1, 1, 1, 1],
9997            "0 and -0.0 are false; every other value including NaN and both \
9998             infinities is true",
9999        );
10000    }
10001
10002    #[test]
10003    fn index_add_smoke() {
10004        let base = cpu_f32(vec![1.0, 1.0, 1.0, 1.0], &[2, 2]);
10005        let src = base
10006            .const_f32_like(vec![10.0, 20.0, 30.0, 40.0], vec![2, 2])
10007            .unwrap();
10008        let indices = base.const_u32_like(vec![0_u32, 0_u32], vec![2]).unwrap();
10009        let out = base.index_add(0, &indices, &src).unwrap();
10010        assert_eq!(out.shape().dims(), &[2, 2]);
10011        // both src rows added to row 0; row 1 unchanged
10012        let v = out.realize_f32();
10013        assert_eq!(v[0], 41.0); // 1 + 10 + 30
10014        assert_eq!(v[1], 61.0); // 1 + 20 + 40
10015        assert_eq!(v[2], 1.0);
10016        assert_eq!(v[3], 1.0);
10017    }
10018
10019    #[test]
10020    fn scatter_add_smoke() {
10021        let base = cpu_f32(vec![0.0, 0.0, 0.0, 0.0], &[2, 2]);
10022        let src = base
10023            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10024            .unwrap();
10025        let indices = base
10026            .const_u32_like(vec![0_u32, 1_u32, 1_u32, 0_u32], vec![2, 2])
10027            .unwrap();
10028        let out = base.scatter_add(0, &indices, &src).unwrap();
10029        assert_eq!(out.shape().dims(), &[2, 2]);
10030    }
10031
10032    #[test]
10033    fn inplace_activations_compile_and_run() {
10034        let t = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]);
10035        // Each in-place op is destructive, so chain through fresh tensors.
10036        let r = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).relu_inplace();
10037        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).silu_inplace();
10038        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).gelu_tanh_inplace();
10039        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).tanh_inplace();
10040        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).sigmoid_inplace();
10041        let _ = t.affine_inplace(2.0, 1.0);
10042        // Spot-check the relu output.
10043        let v = r.realize_f32();
10044        assert_eq!(v, vec![0.0, 0.5, 0.0, 2.0]);
10045    }
10046
10047    #[test]
10048    fn const_f64_like_round_trips() {
10049        let anchor = cpu_f32(vec![0.0], &[1]);
10050        let t = anchor.const_f64_like(vec![1.5, 2.5, 3.5], vec![3]).unwrap();
10051        assert_eq!(t.shape().dims(), &[3]);
10052        assert_eq!(t.dtype(), DType::F64);
10053        assert_eq!(t.realize_f64(), vec![1.5, 2.5, 3.5]);
10054    }
10055
10056    #[test]
10057    fn const_i64_like_round_trips() {
10058        let anchor = cpu_f32(vec![0.0], &[1]);
10059        let t = anchor.const_i64_like(vec![-1_i64, 2, -3], vec![3]).unwrap();
10060        assert_eq!(t.shape().dims(), &[3]);
10061        assert_eq!(t.dtype(), DType::I64);
10062    }
10063
10064    #[test]
10065    fn on_device_smoke_cpu() {
10066        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10067        let pinned = t.on_device(&Device::cpu());
10068        assert_eq!(pinned.realize_f32(), vec![1.0, 2.0]);
10069    }
10070
10071    #[test]
10072    fn copy_to_device_same_device_round_trips() {
10073        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10074        let copied = t.copy_to_device(&Device::cpu());
10075        assert_eq!(copied.realize_f32(), vec![1.0, 2.0, 3.0]);
10076    }
10077}
10078
10079// ============================================================================
10080// Phase A.2 composite primitives tests.
10081// ============================================================================
10082#[cfg(test)]
10083mod phase_a2_composite_tests {
10084    use super::*;
10085
10086    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10087        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10088    }
10089
10090    #[test]
10091    fn transpose_last_two_swaps_last_two_dims() {
10092        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10093        let out = t.transpose_last_two().unwrap();
10094        assert_eq!(out.shape().dims(), &[3, 2]);
10095        assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10096    }
10097
10098    #[test]
10099    fn t_is_alias_of_transpose_last_two() {
10100        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10101        assert_eq!(
10102            t.t().unwrap().realize_f32(),
10103            t.transpose_last_two().unwrap().realize_f32()
10104        );
10105    }
10106
10107    #[test]
10108    fn transpose_dims_swaps_arbitrary_axes() {
10109        let t = cpu_f32((0..24).map(|i| i as f32).collect(), &[2, 3, 4]);
10110        let out = t.transpose_dims(0, 2).unwrap();
10111        assert_eq!(out.shape().dims(), &[4, 3, 2]);
10112    }
10113
10114    #[test]
10115    fn transpose_dims_identity_when_same_axis() {
10116        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10117        let out = t.transpose_dims(0, 0).unwrap();
10118        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
10119    }
10120
10121    #[test]
10122    fn transpose_dims_errors_out_of_bounds() {
10123        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10124        assert!(t.transpose_dims(0, 2).is_err());
10125        assert!(t.transpose_dims(5, 0).is_err());
10126    }
10127
10128    #[test]
10129    fn flatten_merges_middle_dims() {
10130        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10131        let out = t.flatten(0, 1).unwrap();
10132        assert_eq!(out.shape().dims(), &[6, 4]);
10133    }
10134
10135    #[test]
10136    fn flatten_to_merges_leading_dims() {
10137        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10138        let out = t.flatten_to(1).unwrap();
10139        assert_eq!(out.shape().dims(), &[6, 4]);
10140    }
10141
10142    #[test]
10143    fn flatten_from_merges_trailing_dims() {
10144        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10145        let out = t.flatten_from(1).unwrap();
10146        assert_eq!(out.shape().dims(), &[2, 12]);
10147    }
10148
10149    #[test]
10150    fn flatten_all_produces_rank_one() {
10151        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10152        let out = t.flatten_all().unwrap();
10153        assert_eq!(out.shape().dims(), &[6]);
10154        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10155    }
10156
10157    /// `rope_batched` at `B == 1` must be **bit-identical** to
10158    /// `rope_with_tables_decomposed`, and each row at `B == 3` must equal the
10159    /// single-position result at that row's own position — the exactness proof
10160    /// for the per-row RoPE that unblocks ragged (non-uniform-position) batched
10161    /// decode. A subtly wrong rotate-half (wrong half split, wrong sign, wrong
10162    /// concat order) still produces plausible tokens, so this asserts
10163    /// bit-equality; the CONTROL proves the per-row table is load-bearing.
10164    /// (Adopted from the Lightbulb consumer's verified `rope_batched` oracle;
10165    /// the mechanism belongs in Fuel.)
10166    #[test]
10167    fn rope_batched_matches_single_position_rope_row_by_row() {
10168        let lcg = |n: usize, seed: u32| -> Vec<f32> {
10169            let mut s = seed;
10170            (0..n)
10171                .map(|_| {
10172                    s = s.wrapping_mul(1103515245).wrapping_add(12345);
10173                    ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
10174                })
10175                .collect()
10176        };
10177        let (b, heads, hd) = (3usize, 4usize, 8usize);
10178        let base = 10000.0f64;
10179        let positions = [0usize, 7, 3];
10180
10181        let x_data = lcg(b * heads * hd, 11);
10182        let x = cpu_f32(x_data.clone(), &[b, heads, 1, hd]);
10183        let (cos, sin) = x.rope_tables_const_batched(base, &positions, hd);
10184        let got = x.rope_batched(&cos, &sin).unwrap().realize_f32();
10185
10186        let per_row = heads * hd;
10187        for (row, &pos) in positions.iter().enumerate() {
10188            let row_data = x_data[row * per_row..(row + 1) * per_row].to_vec();
10189            let xr = cpu_f32(row_data, &[1, heads, 1, hd]);
10190            let (c, s) = xr.rope_tables_const(base, pos, 1, hd);
10191            let want = xr
10192                .rope_with_tables_decomposed(&c, &s)
10193                .unwrap()
10194                .realize_f32();
10195            assert_eq!(
10196                &got[row * per_row..(row + 1) * per_row],
10197                &want[..],
10198                "row {row} (position {pos}) != decomposed RoPE at that position",
10199            );
10200        }
10201
10202        // CONTROL: prove the per-row table is load-bearing. Give EVERY row
10203        // position 0 (row 0's position); rows at other positions must now
10204        // DISAGREE with their own-position result — else the comparison above is
10205        // blind to the whole per-row-position mechanism.
10206        let shared = [positions[0]; 3];
10207        let (c0, s0) = x.rope_tables_const_batched(base, &shared, hd);
10208        let wrong = x.rope_batched(&c0, &s0).unwrap().realize_f32();
10209        for (row, &pos) in positions.iter().enumerate().skip(1) {
10210            assert_ne!(
10211                &wrong[row * per_row..(row + 1) * per_row],
10212                &got[row * per_row..(row + 1) * per_row],
10213                "CONTROL: row {row} at shared position 0 matched its own position \
10214                 {pos} — per-row RoPE not applied, or comparison blind to it",
10215            );
10216        }
10217    }
10218
10219    #[test]
10220    fn flatten_errors_on_bad_range() {
10221        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10222        assert!(t.flatten(0, 5).is_err());
10223        assert!(t.flatten(2, 1).is_err()); // start > end
10224    }
10225
10226    #[test]
10227    fn split_heads_then_merge_heads_round_trip() {
10228        // (B=1, N=2, embed=6) — split into 2 heads of head_dim=3.
10229        let x = cpu_f32((0..12).map(|i| i as f32).collect(), &[1, 2, 6]);
10230        let split = x.split_heads(2, 3).unwrap();
10231        assert_eq!(split.shape().dims(), &[1, 2, 2, 3]);
10232        let merged = split.merge_heads().unwrap();
10233        assert_eq!(merged.shape().dims(), &[1, 2, 6]);
10234        let m = merged.realize_f32();
10235        let original = x.realize_f32();
10236        for (a, b) in m.iter().zip(original.iter()) {
10237            assert!((a - b).abs() < 1e-7, "{a} vs {b}");
10238        }
10239    }
10240
10241    #[test]
10242    fn rope_tables_const_shapes_and_anchoring() {
10243        let anchor = cpu_f32(vec![0.0_f32; 8], &[1, 4, 2]);
10244        let (cos, sin) = anchor.rope_tables_const(10_000.0, 0, 4, 8);
10245        assert_eq!(cos.shape().dims(), &[4, 8]);
10246        assert_eq!(sin.shape().dims(), &[4, 8]);
10247        // First row at position 0: cos = 1.0, sin = 0.0 for every pair.
10248        let cv = cos.realize_f32();
10249        let sv = sin.realize_f32();
10250        // cos[0,...] should all be 1.0 (RoPE at position 0).
10251        for i in 0..8 {
10252            assert!((cv[i] - 1.0).abs() < 1e-5, "cos[0,{i}] = {}", cv[i]);
10253            assert!(sv[i].abs() < 1e-5, "sin[0,{i}] = {}", sv[i]);
10254        }
10255    }
10256
10257    #[test]
10258    fn embed_tokens_shape_and_lookup() {
10259        // 5-token vocab, 3-dim hidden. Vocab embedding table contains
10260        // row i = (i, i+0.5, i+1) so the lookup result is verifiable.
10261        let vocab_size = 5;
10262        let hidden = 3;
10263        let table: Vec<f32> = (0..vocab_size)
10264            .flat_map(|i| vec![i as f32, i as f32 + 0.5, i as f32 + 1.0])
10265            .collect();
10266        let tokens = vec![1_u32, 3, 0];
10267        let out = Tensor::embed_tokens(
10268            std::sync::Arc::from(table),
10269            vocab_size,
10270            hidden,
10271            &tokens,
10272            &crate::Device::cpu(),
10273        )
10274        .unwrap();
10275        assert_eq!(out.shape().dims(), &[1, 3, hidden]);
10276        let v = out.realize_f32();
10277        let want = [
10278            1.0_f32, 1.5, 2.0, // token 1
10279            3.0, 3.5, 4.0, // token 3
10280            0.0, 0.5, 1.0, // token 0
10281        ];
10282        for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10283            assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10284        }
10285    }
10286
10287    #[test]
10288    fn embed_tokens_anchored_lives_on_receiver_graph() {
10289        // Two paths: one bootstrapped via embed_tokens, one anchored
10290        // on a pre-existing tensor. Both produce identical values,
10291        // but only the anchored one can compose with the anchor.
10292        let vocab_size = 4;
10293        let hidden = 2;
10294        let table: Vec<f32> = (0..vocab_size)
10295            .flat_map(|i| vec![i as f32, i as f32 * 2.0])
10296            .collect();
10297        let table_arc: std::sync::Arc<[f32]> = std::sync::Arc::from(table);
10298        let tokens = vec![2_u32, 1];
10299
10300        let anchor = cpu_f32(vec![0.0_f32], &[1]);
10301        let embedded = anchor
10302            .embed_tokens_anchored(
10303                std::sync::Arc::clone(&table_arc),
10304                vocab_size,
10305                hidden,
10306                &tokens,
10307            )
10308            .unwrap();
10309        assert_eq!(embedded.shape().dims(), &[1, 2, hidden]);
10310
10311        // Anchored: composes with the anchor.
10312        let one_scaled = anchor
10313            .const_f32_like(std::sync::Arc::from(vec![1.0_f32]), Shape::from_dims(&[1]))
10314            .unwrap();
10315        let _ = embedded
10316            .add(
10317                &one_scaled
10318                    .reshape(Shape::from_dims(&[1, 1, 1]))
10319                    .unwrap()
10320                    .broadcast_to(Shape::from_dims(&[1, 2, hidden]))
10321                    .unwrap(),
10322            )
10323            .unwrap();
10324        let v = embedded.realize_f32();
10325        let want = [
10326            2.0_f32, 4.0, // token 2
10327            1.0, 2.0, // token 1
10328        ];
10329        for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10330            assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10331        }
10332    }
10333
10334    #[test]
10335    fn embed_tokens_empty_returns_error() {
10336        let r = Tensor::embed_tokens(
10337            std::sync::Arc::from(vec![0.0_f32]),
10338            1,
10339            1,
10340            &[],
10341            &crate::Device::cpu(),
10342        );
10343        assert!(r.is_err());
10344    }
10345
10346    #[test]
10347    fn softcap_matches_tanh_form() {
10348        // Tiny input; verify cap·tanh(x/cap) at known points.
10349        let x = cpu_f32(vec![0.0_f32, 5.0, -5.0, 30.0], &[1, 4]);
10350        let capped = x.softcap(10.0).realize_f32();
10351        // tanh(0)=0; tanh(0.5)≈0.4621; tanh(-0.5)≈-0.4621; tanh(3)≈0.9951.
10352        let expect = [0.0_f32, 4.6212, -4.6212, 9.9505];
10353        for (i, (&got, &want)) in capped.iter().zip(expect.iter()).enumerate() {
10354            assert!(
10355                (got - want).abs() < 1e-3,
10356                "softcap[{i}] got={got} want={want}"
10357            );
10358        }
10359    }
10360
10361    #[test]
10362    fn softcap_optional_none_returns_input_unchanged() {
10363        let x = cpu_f32(vec![1.0_f32, -2.0, 30.0], &[1, 3]);
10364        let out = x.softcap_optional(None).realize_f32();
10365        let expect = [1.0_f32, -2.0, 30.0];
10366        for (g, w) in out.iter().zip(expect.iter()) {
10367            assert!((g - w).abs() < 1e-6);
10368        }
10369        // Some(0.0) or negative cap also returns unchanged (guard).
10370        let out = x.softcap_optional(Some(0.0)).realize_f32();
10371        for (g, w) in out.iter().zip(expect.iter()) {
10372            assert!((g - w).abs() < 1e-6);
10373        }
10374    }
10375
10376    #[test]
10377    fn rope_partial_full_dim_matches_rope_with_tables() {
10378        // head_dim == rope_dim ⇒ should degenerate to full rope.
10379        let qk = cpu_f32(vec![1.0_f32; 1 * 1 * 2 * 4], &[1, 1, 2, 4]);
10380        let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 2, 4);
10381        let via_partial = qk.rope_partial(&cos, &sin, 4).unwrap();
10382        let via_full = qk.rope_with_tables(&cos, &sin).unwrap();
10383        let a = via_partial.realize_f32();
10384        let b = via_full.realize_f32();
10385        for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
10386            assert!((av - bv).abs() < 1e-6, "{i}: {av} vs {bv}");
10387        }
10388    }
10389
10390    #[test]
10391    fn rope_partial_pass_through_suffix_unchanged() {
10392        // rope_dim=2, head_dim=4 ⇒ first 2 features rotated, last 2
10393        // unchanged. At position 0 the rotation is identity, so all 4
10394        // features should equal the input.
10395        let qk = cpu_f32(
10396            vec![
10397                // shape [1, 1, 1, 4] — one position, one head
10398                1.0_f32, 2.0, 3.0, 4.0,
10399            ],
10400            &[1, 1, 1, 4],
10401        );
10402        let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 1, 2);
10403        let out = qk.rope_partial(&cos, &sin, 2).unwrap().realize_f32();
10404        for (i, &v) in out.iter().enumerate() {
10405            let expect = [1.0_f32, 2.0, 3.0, 4.0][i];
10406            assert!((v - expect).abs() < 1e-6, "{i}: {v} != {expect}");
10407        }
10408    }
10409
10410    #[test]
10411    fn add_optional_trailing_bias_none_returns_input_unchanged() {
10412        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10413        let original = a.realize_f32();
10414        let out = a.add_optional_trailing_bias(None).unwrap();
10415        assert_eq!(out.realize_f32(), original);
10416    }
10417
10418    #[test]
10419    fn add_optional_trailing_bias_some_applies_add() {
10420        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10421        let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, 20.0, 30.0]);
10422        let out = a.add_optional_trailing_bias(Some(&bias)).unwrap();
10423        assert_eq!(out.realize_f32(), vec![11.0, 22.0, 33.0]);
10424    }
10425
10426    #[test]
10427    fn add_trailing_bias_broadcasts_across_leading_dims() {
10428        // (2, 3) input + length-3 bias should add per-column.
10429        let x = cpu_f32(vec![1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0], &[2, 3]);
10430        let bias = std::sync::Arc::<[f32]>::from(vec![100.0_f32, 200.0, 300.0]);
10431        let out = x.add_trailing_bias(bias).unwrap();
10432        assert_eq!(out.shape().dims(), &[2, 3]);
10433        let v = out.realize_f32();
10434        assert_eq!(v, vec![101.0, 202.0, 303.0, 110.0, 220.0, 330.0]);
10435    }
10436
10437    #[test]
10438    fn rms_norm_affine_with_offset_adds_offset_to_each_gain() {
10439        let x = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10440        let gain_raw: [f32; 3] = [-0.5, 0.0, 0.5];
10441        let via_offset = x.rms_norm_affine_with_offset(&gain_raw, 1.0, 1e-6).unwrap();
10442        let gain_shifted = std::sync::Arc::<[f32]>::from(vec![0.5_f32, 1.0, 1.5]);
10443        let via_plain = x.rms_norm_affine(gain_shifted, 1e-6).unwrap();
10444        let a = via_offset.realize_f32();
10445        let b = via_plain.realize_f32();
10446        for (x, y) in a.iter().zip(b.iter()) {
10447            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
10448        }
10449    }
10450
10451    #[test]
10452    fn global_avg_pool_2d_averages_spatial_dims() {
10453        // (1, 2, 2, 3) — two channels, 2×3 spatial.
10454        // Channel 0: 1..=6, mean = 3.5.
10455        // Channel 1: 10, 20, 30, 40, 50, 60, mean = 35.
10456        let x = cpu_f32(
10457            vec![
10458                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,
10459            ],
10460            &[1, 2, 2, 3],
10461        );
10462        let out = x.global_avg_pool_2d().unwrap();
10463        assert_eq!(out.shape().dims(), &[1, 2]);
10464        let v = out.realize_f32();
10465        assert!((v[0] - 3.5).abs() < 1e-5);
10466        assert!((v[1] - 35.0).abs() < 1e-4);
10467    }
10468
10469    #[test]
10470    fn channel_affine_4d_applies_per_channel_gain_and_bias() {
10471        // (1, 2, 2, 2) — two channels, each a 2×2 spatial map.
10472        let x = cpu_f32(
10473            vec![
10474                1.0_f32, 2.0, 3.0, 4.0, // channel 0
10475                10.0, 20.0, 30.0, 40.0, // channel 1
10476            ],
10477            &[1, 2, 2, 2],
10478        );
10479        let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5]);
10480        let bias = std::sync::Arc::<[f32]>::from(vec![1.0_f32, -10.0]);
10481        let out = x.channel_affine_4d(gain, bias).unwrap();
10482        let v = out.realize_f32();
10483        // Channel 0: gain=2, bias=1 → 2x+1
10484        assert_eq!(&v[0..4], &[3.0, 5.0, 7.0, 9.0]);
10485        // Channel 1: gain=0.5, bias=-10 → 0.5x-10
10486        assert_eq!(&v[4..8], &[-5.0, 0.0, 5.0, 10.0]);
10487    }
10488
10489    #[test]
10490    fn additive_causal_mask_has_strict_lower_triangle() {
10491        let anchor = cpu_f32(vec![0.0_f32], &[1]);
10492        let mask = Tensor::additive_causal_mask_like(&anchor, 4);
10493        assert_eq!(mask.shape().dims(), &[4, 4]);
10494        let v = mask.realize_f32();
10495        // Expected (-inf shown as 'x'):
10496        //   0 x x x
10497        //   0 0 x x
10498        //   0 0 0 x
10499        //   0 0 0 0
10500        for i in 0..4 {
10501            for j in 0..4 {
10502                let got = v[i * 4 + j];
10503                if j > i {
10504                    assert!(
10505                        got.is_infinite() && got.is_sign_negative(),
10506                        "above-diag (i={i}, j={j}) should be -inf, got {got}"
10507                    );
10508                } else {
10509                    assert_eq!(
10510                        got, 0.0,
10511                        "on/below-diag (i={i}, j={j}) should be 0, got {got}"
10512                    );
10513                }
10514            }
10515        }
10516    }
10517
10518    #[test]
10519    fn layer_norm_affine_unit_gain_zero_bias_matches_layer_norm_last_dim() {
10520        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10521        let gain = std::sync::Arc::<[f32]>::from(vec![1.0_f32; 3]);
10522        let bias = std::sync::Arc::<[f32]>::from(vec![0.0_f32; 3]);
10523        let out_affine = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10524        let out_plain = a.layer_norm_last_dim(1e-5).unwrap();
10525        let va = out_affine.realize_f32();
10526        let vp = out_plain.realize_f32();
10527        for (a, b) in va.iter().zip(vp.iter()) {
10528            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
10529        }
10530    }
10531
10532    #[test]
10533    fn layer_norm_affine_applies_gain_and_bias() {
10534        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10535        let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5, 1.5]);
10536        let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, -5.0, 0.0]);
10537        let out = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10538        let v = out.realize_f32();
10539        // Manual: mean=2, var=2/3; normed = (x-2)/sqrt(2/3+1e-5).
10540        let mean = 2.0_f32;
10541        let var = ((1.0 - mean).powi(2) + (2.0 - mean).powi(2) + (3.0 - mean).powi(2)) / 3.0;
10542        let den = (var + 1e-5_f32).sqrt();
10543        let expected = [
10544            ((1.0 - mean) / den) * 2.0 + 10.0,
10545            ((2.0 - mean) / den) * 0.5 + (-5.0),
10546            ((3.0 - mean) / den) * 1.5 + 0.0,
10547        ];
10548        for (got, want) in v.iter().zip(expected.iter()) {
10549            assert!((got - want).abs() < 1e-4, "{got} vs {want}");
10550        }
10551    }
10552
10553    #[test]
10554    fn l2_normalize_last_dim_unit_norm_per_row() {
10555        // (2, 3): rows [3,4,0] (norm 5) and [1,2,2] (norm 3).
10556        let a = cpu_f32(vec![3.0, 4.0, 0.0, 1.0, 2.0, 2.0], &[2, 3]);
10557        let out = a.l2_normalize(1_usize, 1e-12).unwrap();
10558        assert_eq!(out.shape().dims(), &[2, 3]);
10559        let v = out.realize_f32();
10560        let row0_norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
10561        let row1_norm = (v[3] * v[3] + v[4] * v[4] + v[5] * v[5]).sqrt();
10562        assert!((row0_norm - 1.0).abs() < 1e-5, "row 0 norm = {row0_norm}");
10563        assert!((row1_norm - 1.0).abs() < 1e-5, "row 1 norm = {row1_norm}");
10564        // Row 0: [3,4,0]/5 → [0.6, 0.8, 0.0].
10565        assert!((v[0] - 0.6).abs() < 1e-5);
10566        assert!((v[1] - 0.8).abs() < 1e-5);
10567        assert!(v[2].abs() < 1e-5);
10568    }
10569
10570    #[test]
10571    fn l2_normalize_eps_zero_works_when_nonzero() {
10572        let a = cpu_f32(vec![1.0_f32, 0.0], &[2]);
10573        let out = a.l2_normalize(0_usize, 0.0).unwrap();
10574        let v = out.realize_f32();
10575        assert!((v[0] - 1.0).abs() < 1e-6);
10576        assert!(v[1].abs() < 1e-6);
10577    }
10578
10579    #[test]
10580    fn repeat_interleave_last_dim_matches_torch_semantics() {
10581        // (2, 3) input: rows [1,2,3] and [4,5,6]. dim=1, repeats=2
10582        // → each element becomes two consecutive copies:
10583        // (2, 6): [1,1,2,2,3,3] and [4,4,5,5,6,6].
10584        let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10585        let out = a.repeat_interleave(1_usize, 2).unwrap();
10586        assert_eq!(out.shape().dims(), &[2, 6]);
10587        assert_eq!(
10588            out.realize_f32(),
10589            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]
10590        );
10591    }
10592
10593    #[test]
10594    fn repeat_interleave_middle_dim() {
10595        // (2, 2, 2) input. dim=1, repeats=3 → (2, 6, 2).
10596        let a = cpu_f32((0..8).map(|i| i as f32).collect(), &[2, 2, 2]);
10597        let out = a.repeat_interleave(1_usize, 3).unwrap();
10598        assert_eq!(out.shape().dims(), &[2, 6, 2]);
10599        // First sample's elements: (0,1) repeated 3× then (2,3) repeated 3×.
10600        let v = out.realize_f32();
10601        assert_eq!(&v[0..6], &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
10602        assert_eq!(&v[6..12], &[2.0, 3.0, 2.0, 3.0, 2.0, 3.0]);
10603    }
10604
10605    #[test]
10606    fn repeat_interleave_repeats_one_is_noop() {
10607        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10608        let out = a.repeat_interleave(0_usize, 1).unwrap();
10609        assert_eq!(out.shape().dims(), &[3]);
10610        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10611    }
10612
10613    #[test]
10614    fn repeat_interleave_repeats_zero_errors() {
10615        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10616        assert!(a.repeat_interleave(0_usize, 0).is_err());
10617    }
10618
10619    #[test]
10620    fn stack_adds_leading_dim() {
10621        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10622        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10623        let out = Tensor::stack(&[&a, &b], 0).unwrap();
10624        assert_eq!(out.shape().dims(), &[2, 3]);
10625        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10626    }
10627
10628    #[test]
10629    fn stack_adds_trailing_dim() {
10630        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10631        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10632        let out = Tensor::stack(&[&a, &b], 1).unwrap();
10633        assert_eq!(out.shape().dims(), &[3, 2]);
10634        assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10635    }
10636
10637    #[test]
10638    fn stack_rejects_mismatched_shapes() {
10639        let a = cpu_f32(vec![1.0, 2.0], &[2]);
10640        let b = a.const_f32_like(vec![3.0, 4.0, 5.0], vec![3]).unwrap();
10641        assert!(Tensor::stack(&[&a, &b], 0).is_err());
10642    }
10643
10644    #[test]
10645    fn stack_rejects_empty_input() {
10646        let result = Tensor::stack(&[], 0);
10647        assert!(result.is_err());
10648    }
10649
10650    #[test]
10651    fn repeat_tiles_along_each_dim() {
10652        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10653        let out = t.repeat(vec![3]).unwrap();
10654        assert_eq!(out.shape().dims(), &[6]);
10655        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
10656    }
10657
10658    #[test]
10659    fn repeat_extends_rank_when_needed() {
10660        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10661        // repeat with shape [3, 2] left-pads tensor to [1, 2] then tiles to [3, 4]
10662        let out = t.repeat(vec![3, 2]).unwrap();
10663        assert_eq!(out.shape().dims(), &[3, 4]);
10664    }
10665
10666    #[test]
10667    fn repeat_identity_with_all_ones() {
10668        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10669        let out = t.repeat(vec![1]).unwrap();
10670        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10671    }
10672}
10673
10674// ============================================================================
10675// Phase A.3 keepdim reduction tests.
10676// ============================================================================
10677#[cfg(test)]
10678mod phase_a3_keepdim_tests {
10679    use super::*;
10680
10681    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10682        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10683    }
10684
10685    #[test]
10686    fn sum_keepdim_preserves_dim_as_one() {
10687        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10688        let out = t.sum_keepdim(1_usize).unwrap();
10689        assert_eq!(out.shape().dims(), &[2, 1]);
10690        assert_eq!(out.realize_f32(), vec![3.0, 7.0]);
10691    }
10692
10693    #[test]
10694    fn mean_keepdim_preserves_dim_as_one() {
10695        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10696        let out = t.mean_keepdim(0_usize).unwrap();
10697        assert_eq!(out.shape().dims(), &[1, 2]);
10698        assert_eq!(out.realize_f32(), vec![2.0, 3.0]);
10699    }
10700
10701    #[test]
10702    fn max_keepdim_preserves_dim_as_one() {
10703        let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10704        let out = t.max_keepdim(1_usize).unwrap();
10705        assert_eq!(out.shape().dims(), &[2, 1]);
10706        assert_eq!(out.realize_f32(), vec![3.0, 4.0]);
10707    }
10708
10709    #[test]
10710    fn min_keepdim_preserves_dim_as_one() {
10711        let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10712        let out = t.min_keepdim(1_usize).unwrap();
10713        assert_eq!(out.shape().dims(), &[2, 1]);
10714        assert_eq!(out.realize_f32(), vec![1.0, 2.0]);
10715    }
10716
10717    #[test]
10718    fn var_matches_unbiased_formula_with_correction_1() {
10719        // [[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
10720        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10721        let out = t.var(1, 1.0).unwrap();
10722        assert_eq!(out.shape().dims(), &[2]);
10723        let v = out.realize_f32();
10724        assert!((v[0] - 1.0).abs() < 1e-5, "var row 0 = {} != 1.0", v[0]);
10725        assert!((v[1] - 1.0).abs() < 1e-5, "var row 1 = {} != 1.0", v[1]);
10726    }
10727
10728    /// KISS-Ops §6.13 S14: `correction = 0.0` (the spec default, numpy's
10729    /// default) divides by `n` instead of `n - 1` -- same fixture as the
10730    /// Bessel test above, different divisor, so the two tests pin BOTH
10731    /// halves of the explicit-attribute contract against the same data.
10732    #[test]
10733    fn var_population_with_correction_0() {
10734        // Same fixture: dev=[-1,0,1], sq sum=2, /3 = 0.666...
10735        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10736        let out = t.var(1, 0.0).unwrap();
10737        let v = out.realize_f32();
10738        let want = 2.0 / 3.0;
10739        assert!(
10740            (v[0] - want).abs() < 1e-5,
10741            "population var row 0 = {} != {want}",
10742            v[0]
10743        );
10744        assert!(
10745            (v[1] - want).abs() < 1e-5,
10746            "population var row 1 = {} != {want}",
10747            v[1]
10748        );
10749    }
10750
10751    #[test]
10752    fn var_keepdim_preserves_dim() {
10753        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10754        let out = t.var_keepdim(1, 1.0).unwrap();
10755        assert_eq!(out.shape().dims(), &[2, 1]);
10756    }
10757
10758    #[test]
10759    fn var_errors_out_of_bounds() {
10760        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10761        assert!(t.var(3, 1.0).is_err());
10762        assert!(t.var_keepdim(3, 1.0).is_err());
10763    }
10764}
10765
10766// ============================================================================
10767// Phase A.4 scalar/binary composite tests.
10768// ============================================================================
10769#[cfg(test)]
10770mod phase_a4_composite_tests {
10771    use super::*;
10772
10773    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10774        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10775    }
10776
10777    #[test]
10778    fn affine_applies_mul_then_add() {
10779        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10780        let out = t.affine(2.0, 10.0);
10781        assert_eq!(out.realize_f32(), vec![12.0, 14.0, 16.0]);
10782    }
10783
10784    #[test]
10785    fn scale_and_shift_alias_of_affine() {
10786        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10787        assert_eq!(
10788            t.scale_and_shift(2.0, 10.0).realize_f32(),
10789            t.affine(2.0, 10.0).realize_f32(),
10790        );
10791    }
10792
10793    #[test]
10794    fn elu_matches_reference_values() {
10795        let t = cpu_f32(vec![1.0, 0.0, -1.0, -2.0], &[4]);
10796        let out = t.elu(1.0);
10797        let v = out.realize_f32();
10798        // x > 0: identity. x == 0: 0 (boundary; gt returns 0 → neg branch which is alpha*(1-1)=0).
10799        // x < 0: alpha * (exp(x) - 1).
10800        assert!((v[0] - 1.0).abs() < 1e-5);
10801        assert!(v[1].abs() < 1e-5);
10802        assert!((v[2] - ((-1.0_f32).exp() - 1.0)).abs() < 1e-5);
10803        assert!((v[3] - ((-2.0_f32).exp() - 1.0)).abs() < 1e-5);
10804    }
10805
10806    #[test]
10807    fn dot_of_rank_one_vectors() {
10808        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10809        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10810        let out = a.dot(&b).unwrap();
10811        assert_eq!(out.shape().elem_count(), 1);
10812        let v = out.realize_f32();
10813        assert_eq!(v[0], 32.0); // 1*4 + 2*5 + 3*6
10814    }
10815
10816    #[test]
10817    fn dot_rejects_non_rank_one() {
10818        let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10819        let b = a
10820            .const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
10821            .unwrap();
10822        assert!(a.dot(&b).is_err());
10823    }
10824
10825    #[test]
10826    fn dot_rejects_length_mismatch() {
10827        let a = cpu_f32(vec![1.0, 2.0], &[2]);
10828        let b = a.const_f32_like(vec![1.0, 2.0, 3.0], vec![3]).unwrap();
10829        assert!(a.dot(&b).is_err());
10830    }
10831
10832    #[test]
10833    fn mv_matrix_times_vector() {
10834        let m = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10835        let v = m.const_f32_like(vec![1.0, 1.0, 1.0], vec![3]).unwrap();
10836        let out = m.mv(&v).unwrap();
10837        assert_eq!(out.shape().dims(), &[2]);
10838        assert_eq!(out.realize_f32(), vec![6.0, 15.0]);
10839    }
10840
10841    #[test]
10842    fn matvec_is_mv_alias() {
10843        let m = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10844        let v = m.const_f32_like(vec![3.0, 4.0], vec![2]).unwrap();
10845        let a = m.mv(&v).unwrap().realize_f32();
10846        let b = m.matvec(&v).unwrap().realize_f32();
10847        assert_eq!(a, b);
10848    }
10849
10850    #[test]
10851    fn mv_rejects_shape_mismatch() {
10852        let m = cpu_f32(vec![1.0; 6], &[2, 3]);
10853        let v = m.const_f32_like(vec![1.0, 1.0], vec![2]).unwrap();
10854        assert!(m.mv(&v).is_err());
10855    }
10856
10857    #[test]
10858    fn broadcast_matmul_passes_through_to_matmul() {
10859        let a = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10860        let b = a
10861            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10862            .unwrap();
10863        let out = a.broadcast_matmul(&b).unwrap();
10864        assert_eq!(out.realize_f32(), vec![5.0, 6.0, 7.0, 8.0]);
10865    }
10866}
10867
10868// ============================================================================
10869// Phase A.5 factory family tests.
10870// ============================================================================
10871#[cfg(test)]
10872mod phase_a5_factory_tests {
10873    use super::*;
10874
10875    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10876        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10877    }
10878
10879    #[test]
10880    fn ones_like_matches_shape_dtype_graph() {
10881        let t = cpu_f32(vec![5.0, 6.0, 7.0, 8.0], &[2, 2]);
10882        let ones = t.ones_like().unwrap();
10883        assert_eq!(ones.shape().dims(), t.shape().dims());
10884        assert_eq!(ones.dtype(), t.dtype());
10885        assert_eq!(ones.realize_f32(), vec![1.0; 4]);
10886    }
10887
10888    #[test]
10889    fn zeros_like_matches_shape_dtype_graph() {
10890        let t = cpu_f32(vec![5.0, 6.0, 7.0], &[3]);
10891        let zeros = t.zeros_like().unwrap();
10892        assert_eq!(zeros.realize_f32(), vec![0.0; 3]);
10893    }
10894
10895    #[test]
10896    fn static_ones_f32() {
10897        let t = Tensor::ones(vec![2, 3], DType::F32, &Device::cpu()).unwrap();
10898        assert_eq!(t.shape().dims(), &[2, 3]);
10899        assert_eq!(t.realize_f32(), vec![1.0; 6]);
10900    }
10901
10902    #[test]
10903    fn static_zeros_f64() {
10904        let t = Tensor::zeros(vec![4], DType::F64, &Device::cpu()).unwrap();
10905        assert_eq!(t.dtype(), DType::F64);
10906        assert_eq!(t.realize_f64(), vec![0.0; 4]);
10907    }
10908
10909    #[test]
10910    fn full_with_f32_scalar() {
10911        let t = Tensor::full(vec![5], fuel_ir::Scalar::F32(2.5), &Device::cpu()).unwrap();
10912        assert_eq!(t.realize_f32(), vec![2.5; 5]);
10913    }
10914
10915    #[test]
10916    fn eye_identity_matrix() {
10917        let t = Tensor::eye(3, DType::F32, &Device::cpu());
10918        assert_eq!(t.shape().dims(), &[3, 3]);
10919        assert_eq!(
10920            t.realize_f32(),
10921            vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
10922        );
10923    }
10924
10925    #[test]
10926    fn tril2_lower_triangular_ones() {
10927        let t = Tensor::tril2(3, DType::F32, &Device::cpu());
10928        assert_eq!(
10929            t.realize_f32(),
10930            vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0],
10931        );
10932    }
10933
10934    #[test]
10935    fn triu2_upper_triangular_ones() {
10936        let t = Tensor::triu2(3, DType::F32, &Device::cpu());
10937        assert_eq!(
10938            t.realize_f32(),
10939            vec![1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0],
10940        );
10941    }
10942
10943    #[test]
10944    fn meshgrid_ij_indexing_two_inputs() {
10945        let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10946        let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10947        let grids = Tensor::meshgrid(&[&x, &y], false).unwrap();
10948        assert_eq!(grids.len(), 2);
10949        // ij: shapes are [len(x), len(y)] = [3, 2].
10950        assert_eq!(grids[0].shape().dims(), &[3, 2]);
10951        assert_eq!(grids[1].shape().dims(), &[3, 2]);
10952        // X grid: each row repeats x's value, so each row is identical along axis 1.
10953        assert_eq!(grids[0].realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
10954        // Y grid: each column repeats y's value, so each column is identical along axis 0.
10955        assert_eq!(grids[1].realize_f32(), vec![4.0, 5.0, 4.0, 5.0, 4.0, 5.0]);
10956    }
10957
10958    #[test]
10959    fn meshgrid_xy_indexing_swaps_first_two() {
10960        let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10961        let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10962        let grids = Tensor::meshgrid(&[&x, &y], true).unwrap();
10963        // xy: shapes flip to [len(y), len(x)] = [2, 3].
10964        assert_eq!(grids[0].shape().dims(), &[2, 3]);
10965        assert_eq!(grids[1].shape().dims(), &[2, 3]);
10966        // X grid: same row twice, each row is x.
10967        assert_eq!(grids[0].realize_f32(), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
10968        // Y grid: each row repeats one y element.
10969        assert_eq!(grids[1].realize_f32(), vec![4.0, 4.0, 4.0, 5.0, 5.0, 5.0]);
10970    }
10971
10972    #[test]
10973    fn meshgrid_rejects_single_input() {
10974        let x = Tensor::from_f32(vec![1.0_f32, 2.0], vec![2], &Device::cpu()).unwrap();
10975        assert!(Tensor::meshgrid(&[&x], false).is_err());
10976    }
10977
10978    #[test]
10979    fn meshgrid_rejects_non_rank_one() {
10980        let x = Tensor::from_f32(vec![1.0; 4], vec![2, 2], &Device::cpu()).unwrap();
10981        let y = x.const_f32_like(vec![1.0, 2.0], vec![2]).unwrap();
10982        assert!(Tensor::meshgrid(&[&x, &y], false).is_err());
10983    }
10984
10985    // ---- additional deferred-Phase-A item tests ----
10986
10987    #[test]
10988    fn narrow_is_slice_alias() {
10989        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5]);
10990        let a = t.narrow(0, 1, 3).unwrap().realize_f32();
10991        let b = t.slice(0, 1, 3).unwrap().realize_f32();
10992        assert_eq!(a, b);
10993        assert_eq!(a, vec![2.0, 3.0, 4.0]);
10994    }
10995
10996    #[test]
10997    fn chunk_splits_evenly() {
10998        let t = cpu_f32((1..=6).map(|i| i as f32).collect(), &[6]);
10999        let parts = t.chunk(3, 0).unwrap();
11000        assert_eq!(parts.len(), 3);
11001        assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0]);
11002        assert_eq!(parts[1].realize_f32(), vec![3.0, 4.0]);
11003        assert_eq!(parts[2].realize_f32(), vec![5.0, 6.0]);
11004    }
11005
11006    #[test]
11007    fn chunk_distributes_remainder_to_leading() {
11008        // size 7, 3 chunks → first 7%3=1 chunk gets the extra: sizes 3, 2, 2
11009        let t = cpu_f32((1..=7).map(|i| i as f32).collect(), &[7]);
11010        let parts = t.chunk(3, 0).unwrap();
11011        assert_eq!(parts.len(), 3);
11012        assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0, 3.0]);
11013        assert_eq!(parts[1].realize_f32(), vec![4.0, 5.0]);
11014        assert_eq!(parts[2].realize_f32(), vec![6.0, 7.0]);
11015    }
11016
11017    #[test]
11018    fn chunk_returns_singletons_when_size_less_than_chunks() {
11019        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11020        let parts = t.chunk(5, 0).unwrap();
11021        assert_eq!(parts.len(), 2);
11022        assert_eq!(parts[0].realize_f32(), vec![1.0]);
11023        assert_eq!(parts[1].realize_f32(), vec![2.0]);
11024    }
11025
11026    #[test]
11027    fn get_at_first_dim() {
11028        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11029        let row1 = t.get(1).unwrap();
11030        assert_eq!(row1.shape().dims(), &[2]);
11031        assert_eq!(row1.realize_f32(), vec![3.0, 4.0]);
11032    }
11033
11034    #[test]
11035    fn get_on_dim_arbitrary_axis() {
11036        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11037        let col0 = t.get_on_dim(1, 0).unwrap();
11038        assert_eq!(col0.shape().dims(), &[3]);
11039        assert_eq!(col0.realize_f32(), vec![1.0, 3.0, 5.0]);
11040    }
11041
11042    #[test]
11043    fn sum_dims_multi_dim_reduces_to_smaller() {
11044        // [2,3,4] sum over (0, 2) → [3]
11045        let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11046        let s = t.sum_dims([0, 2_usize]).unwrap();
11047        assert_eq!(s.shape().dims(), &[3]);
11048        // each element is 2*4 = 8 (sum across dim 0 = 2 elements, dim 2 = 4 elements)
11049        assert_eq!(s.realize_f32(), vec![8.0; 3]);
11050    }
11051
11052    #[test]
11053    fn mean_dims_multi_dim() {
11054        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11055        let m = t.mean_dims([0, 1_usize]).unwrap();
11056        assert_eq!(m.shape().dims(), &[] as &[usize]);
11057        assert_eq!(m.realize_f32(), vec![2.5]);
11058    }
11059
11060    #[test]
11061    fn sum_dims_keepdim_preserves_rank() {
11062        let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11063        let s = t.sum_dims_keepdim(&[0, 2]).unwrap();
11064        assert_eq!(s.shape().dims(), &[1, 3, 1]);
11065    }
11066
11067    #[test]
11068    fn mean_dims_keepdim_preserves_rank() {
11069        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11070        let m = t.mean_dims_keepdim(&[0, 1]).unwrap();
11071        assert_eq!(m.shape().dims(), &[1, 1]);
11072        assert_eq!(m.realize_f32(), vec![2.5]);
11073    }
11074
11075    #[test]
11076    fn rand_like_matches_shape_dtype() {
11077        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
11078        let r = t.rand_like(-1.0, 1.0).unwrap();
11079        assert_eq!(r.shape().dims(), t.shape().dims());
11080        assert_eq!(r.dtype(), t.dtype());
11081        // Every sample must be in [-1, 1).
11082        for v in r.realize_f32() {
11083            assert!((-1.0..1.0).contains(&v), "sample {v} out of [-1, 1)");
11084        }
11085    }
11086
11087    #[test]
11088    fn randn_like_matches_shape_dtype() {
11089        let t = cpu_f32(vec![0.0; 4], &[4]);
11090        let r = t.randn_like(0.0, 1.0).unwrap();
11091        assert_eq!(r.shape().dims(), &[4]);
11092        assert_eq!(r.dtype(), DType::F32);
11093        // Samples are random — just sanity-check they're finite.
11094        for v in r.realize_f32() {
11095            assert!(v.is_finite());
11096        }
11097    }
11098
11099    #[test]
11100    fn static_rand_f32() {
11101        let t = Tensor::rand(vec![100], 0.0, 1.0, DType::F32, &Device::cpu()).unwrap();
11102        let v = t.realize_f32();
11103        // Mean of uniform [0,1) should be ~0.5; tolerate sample noise.
11104        let mean: f32 = v.iter().sum::<f32>() / v.len() as f32;
11105        assert!((0.3..0.7).contains(&mean), "mean {mean} too far from 0.5");
11106    }
11107
11108    #[test]
11109    fn static_randn_f64() {
11110        let t = Tensor::randn(vec![1000], 0.0, 1.0, DType::F64, &Device::cpu()).unwrap();
11111        let v = t.realize_f64();
11112        let mean: f64 = v.iter().sum::<f64>() / v.len() as f64;
11113        // Normal(0,1) mean should be near 0; n=1000 gives stderr ~0.03.
11114        assert!(mean.abs() < 0.2, "mean {mean} too far from 0");
11115    }
11116
11117    #[test]
11118    fn arange_int_step() {
11119        let t = Tensor::arange(0.0, 5.0, &Device::cpu());
11120        assert_eq!(t.shape().dims(), &[5]);
11121        assert_eq!(t.realize_f32(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
11122    }
11123
11124    #[test]
11125    fn arange_step_fractional() {
11126        let t = Tensor::arange_step(2.0, 4.0, 0.5, &Device::cpu());
11127        assert_eq!(t.realize_f32(), vec![2.0, 2.5, 3.0, 3.5]);
11128    }
11129
11130    #[test]
11131    fn arange_step_negative_descends() {
11132        let t = Tensor::arange_step(5.0, 0.0, -1.0, &Device::cpu());
11133        assert_eq!(t.realize_f32(), vec![5.0, 4.0, 3.0, 2.0, 1.0]);
11134    }
11135
11136    #[test]
11137    fn linspace_includes_endpoints() {
11138        let t = Tensor::linspace(0.0, 1.0, 5, &Device::cpu());
11139        assert_eq!(t.shape().dims(), &[5]);
11140        let v = t.realize_f32();
11141        assert!((v[0] - 0.0).abs() < 1e-6);
11142        assert!((v[4] - 1.0).abs() < 1e-6);
11143        assert!((v[2] - 0.5).abs() < 1e-6);
11144    }
11145
11146    #[test]
11147    fn linspace_n_one_returns_start() {
11148        let t = Tensor::linspace(7.0, 99.0, 1, &Device::cpu());
11149        assert_eq!(t.realize_f32(), vec![7.0]);
11150    }
11151
11152    #[test]
11153    fn norm_is_sqrt_sum_sq() {
11154        let t = Tensor::from_f32(vec![3.0_f32, 4.0], vec![2], &Device::cpu()).unwrap();
11155        let n = t.norm();
11156        assert!((n.realize_f32()[0] - 5.0).abs() < 1e-6);
11157    }
11158
11159    #[test]
11160    fn pad_with_zeros_left_and_right() {
11161        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11162        let p = t.pad_with_zeros(0, 2, 1).unwrap();
11163        assert_eq!(p.shape().dims(), &[6]);
11164        assert_eq!(p.realize_f32(), vec![0.0, 0.0, 1.0, 2.0, 3.0, 0.0]);
11165    }
11166
11167    #[test]
11168    fn pad_with_zeros_identity_when_both_zero() {
11169        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11170        let p = t.pad_with_zeros(0, 0, 0).unwrap();
11171        assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11172    }
11173
11174    #[test]
11175    fn pad_with_zeros_rejects_bad_dim() {
11176        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11177        assert!(t.pad_with_zeros(3, 1, 1).is_err());
11178    }
11179
11180    #[test]
11181    fn pad_with_value_zero_matches_pad_with_zeros() {
11182        // pad_with_value(_, _, _, 0.0) must be observationally identical
11183        // to pad_with_zeros — the latter is now a wrapper for the former.
11184        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11185        let via_zeros = t.pad_with_zeros(0, 1, 2).unwrap();
11186        let via_value = t.pad_with_value(0, 1, 2, 0.0).unwrap();
11187        assert_eq!(via_zeros.shape().dims(), via_value.shape().dims());
11188        assert_eq!(via_zeros.shape().dims(), &[5, 2]);
11189        assert_eq!(via_zeros.realize_f32(), via_value.realize_f32());
11190        assert_eq!(
11191            via_value.realize_f32(),
11192            vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0],
11193        );
11194    }
11195
11196    #[test]
11197    fn pad_with_value_neg_inf_then_max_reduce_ignores_pad() {
11198        // Negative interior values: -1, -2, -3. Pad with -inf on both
11199        // sides. Max along dim 0 must be the interior max (-1.0), not
11200        // -inf. This is the load-bearing property for max_pool2d.
11201        let t = cpu_f32(vec![-1.0, -2.0, -3.0], &[3]);
11202        let padded = t.pad_with_value(0, 2, 2, f32::NEG_INFINITY).unwrap();
11203        assert_eq!(padded.shape().dims(), &[7]);
11204        let v = padded.realize_f32();
11205        // Layout: [-inf, -inf, -1, -2, -3, -inf, -inf]
11206        assert!(v[0].is_infinite() && v[0].is_sign_negative());
11207        assert!(v[1].is_infinite() && v[1].is_sign_negative());
11208        assert_eq!(v[2], -1.0);
11209        assert_eq!(v[3], -2.0);
11210        assert_eq!(v[4], -3.0);
11211        assert!(v[5].is_infinite() && v[5].is_sign_negative());
11212        assert!(v[6].is_infinite() && v[6].is_sign_negative());
11213        // max along the only dim drops the pad and returns the interior max.
11214        let m = padded.max_all().realize_f32();
11215        assert_eq!(m, vec![-1.0]);
11216    }
11217
11218    #[test]
11219    fn pad_with_value_identity_when_both_zero() {
11220        // The early-out path must fire regardless of value (no spurious
11221        // graph node when there's nothing to pad).
11222        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11223        let p = t.pad_with_value(0, 0, 0, f32::NEG_INFINITY).unwrap();
11224        assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11225    }
11226
11227    #[test]
11228    fn pad_with_value_rejects_bad_dim() {
11229        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11230        assert!(t.pad_with_value(3, 1, 1, 0.0).is_err());
11231    }
11232
11233    #[test]
11234    fn max_pool2d_with_pad_value_neg_inf_on_negative_interior() {
11235        // Negative-only interior values: a zero-padded max_pool2d would
11236        // incorrectly return 0 in boundary windows. -inf padding gives
11237        // the PyTorch-correct answer (the interior max).
11238        //
11239        // 1x1x3x3 tensor, all values negative:
11240        //   [ -1, -2, -3 ]
11241        //   [ -4, -5, -6 ]
11242        //   [ -7, -8, -9 ]
11243        // With kernel=3, stride=1, padding=1, output is 3x3 where the
11244        // (1,1) center sees the full grid → max = -1.
11245        let x = cpu_f32(
11246            vec![-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0],
11247            &[1, 1, 3, 3],
11248        );
11249        let out = x
11250            .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), f32::NEG_INFINITY)
11251            .unwrap();
11252        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11253        let v = out.realize_f32();
11254        // Top-left corner (0,0): window sees only [(0,0),(0,1),(1,0),(1,1)]
11255        // = [-1,-2,-4,-5]; padded slots are -inf → max = -1.
11256        assert_eq!(v[0], -1.0);
11257        // Center (1,1): no padded slots in window → max of all 9 = -1.
11258        assert_eq!(v[4], -1.0);
11259        // Bottom-right (2,2): window sees [(1,1),(1,2),(2,1),(2,2)]
11260        // = [-5,-6,-8,-9]; padded slots are -inf → max = -5.
11261        assert_eq!(v[8], -5.0);
11262
11263        // KISS-Ops S14: `max_pool2d`'s own default pad value was 0.0
11264        // ("legacy behavior") -- the exact bug this test file demonstrates
11265        // above -- and is now `f32::NEG_INFINITY`. So `max_pool2d` (no
11266        // explicit pad value) must now agree with the NEG_INFINITY variant
11267        // above, not with a zero-padded one; the OLD zero-padded answer is
11268        // only reachable by asking for it explicitly via
11269        // `max_pool2d_with_pad_value(..., 0.0)`, which this block also
11270        // checks, to keep BOTH halves of the fix (new default AND old
11271        // explicit escape hatch) under one assertion.
11272        let default_pad = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11273        let vd = default_pad.realize_f32();
11274        assert_eq!(
11275            vd[0], -1.0,
11276            "max_pool2d's default must now match NEG_INFINITY padding, not 0.0"
11277        );
11278        assert_eq!(vd[8], -5.0);
11279
11280        let explicit_zero_pad = x
11281            .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), 0.0)
11282            .unwrap();
11283        let vz = explicit_zero_pad.realize_f32();
11284        assert_eq!(
11285            vz[0], 0.0,
11286            "explicit 0.0 padding must still be reachable via max_pool2d_with_pad_value"
11287        );
11288        assert_eq!(vz[8], 0.0);
11289    }
11290
11291    #[test]
11292    fn max_pool2d_with_pad_value_zero_matches_max_pool2d() {
11293        // With pad_value = 0.0, the new variant must agree with the
11294        // legacy max_pool2d byte-for-byte.
11295        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11296        let x = cpu_f32(data, &[1, 1, 4, 4]);
11297        let a = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11298        let b = x
11299            .max_pool2d_with_pad_value((2, 2), (2, 2), (0, 0), 0.0)
11300            .unwrap();
11301        assert_eq!(a.shape().dims(), b.shape().dims());
11302        assert_eq!(a.realize_f32(), b.realize_f32());
11303    }
11304
11305    // ---- Phase A.6 conv1d composite tests ----
11306
11307    #[test]
11308    fn conv1d_identity_kernel_passes_input_through() {
11309        // Single-batch, single-channel, kernel-1 identity → output equals input.
11310        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[1, 1, 5]);
11311        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11312        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11313        assert_eq!(out.shape().dims(), &[1, 1, 5]);
11314        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
11315    }
11316
11317    #[test]
11318    fn conv1d_sum_kernel_two_wide() {
11319        // Sum kernel of size 2: out[t] = x[t] + x[t+1].
11320        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11321        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11322        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11323        assert_eq!(out.shape().dims(), &[1, 1, 3]);
11324        assert_eq!(out.realize_f32(), vec![3.0, 5.0, 7.0]);
11325    }
11326
11327    #[test]
11328    fn conv1d_with_bias_applies_correctly() {
11329        let x = cpu_f32(vec![1.0, 1.0, 1.0], &[1, 1, 3]);
11330        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11331        let bias = x.const_f32_like(vec![10.0], vec![1]).unwrap();
11332        let out = x.conv1d(&w, Some(&bias), 1, 0, 1).unwrap();
11333        assert_eq!(out.realize_f32(), vec![11.0, 11.0, 11.0]);
11334    }
11335
11336    #[test]
11337    fn conv1d_stride_two_halves_output() {
11338        // Input length 6, kernel 2, stride 2 → output length (6-2)/2+1 = 3.
11339        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1, 1, 6]);
11340        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11341        let out = x.conv1d(&w, None, 2, 0, 1).unwrap();
11342        assert_eq!(out.shape().dims(), &[1, 1, 3]);
11343        assert_eq!(out.realize_f32(), vec![3.0, 7.0, 11.0]);
11344    }
11345
11346    #[test]
11347    fn conv1d_padding_one_preserves_length() {
11348        // Input length 4, kernel 3, padding 1, stride 1 → output length 4.
11349        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11350        let w = x
11351            .const_f32_like(vec![1.0, 1.0, 1.0], vec![1, 1, 3])
11352            .unwrap();
11353        let out = x.conv1d(&w, None, 1, 1, 1).unwrap();
11354        assert_eq!(out.shape().dims(), &[1, 1, 4]);
11355        // out[0] = 0+x[0]+x[1] = 3; out[1] = x[0]+x[1]+x[2] = 6;
11356        // out[2] = x[1]+x[2]+x[3] = 9; out[3] = x[2]+x[3]+0 = 7
11357        assert_eq!(out.realize_f32(), vec![3.0, 6.0, 9.0, 7.0]);
11358    }
11359
11360    #[test]
11361    fn conv1d_multi_channel_output() {
11362        // 1 batch, 1 in-channel, 3 timesteps; 2 out-channels with kernel 1.
11363        let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11364        // Weight [Cout=2, Cin=1, K=1]: filter 0 = 2.0, filter 1 = -1.0.
11365        let w = x.const_f32_like(vec![2.0, -1.0], vec![2, 1, 1]).unwrap();
11366        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11367        assert_eq!(out.shape().dims(), &[1, 2, 3]);
11368        // Channel 0: 2.0 × input. Channel 1: -1.0 × input.
11369        assert_eq!(out.realize_f32(), vec![2.0, 4.0, 6.0, -1.0, -2.0, -3.0]);
11370    }
11371
11372    #[test]
11373    fn conv1d_rejects_rank_two_input() {
11374        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11375        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11376        assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11377    }
11378
11379    #[test]
11380    fn conv1d_rejects_rank_two_weight() {
11381        let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11382        let w = x.const_f32_like(vec![1.0], vec![1, 1]).unwrap();
11383        assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11384    }
11385
11386    #[test]
11387    fn conv1d_rejects_zero_groups_or_stride() {
11388        let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11389        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11390        assert!(x.conv1d(&w, None, 0, 0, 1).is_err());
11391        assert!(x.conv1d(&w, None, 1, 0, 0).is_err());
11392    }
11393
11394    #[test]
11395    fn conv1d_with_algo_ignores_algo_param() {
11396        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11397        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11398        // Pass a dummy algo (the parameter is ignored on the lazy path).
11399        let a = x.conv1d_with_algo(&w, None, 1, 0, 1, "unused").unwrap();
11400        let b = x.conv1d(&w, None, 1, 0, 1).unwrap();
11401        assert_eq!(a.realize_f32(), b.realize_f32());
11402    }
11403
11404    // ---- Phase A.7 pooling / interpolation composite tests ----
11405
11406    #[test]
11407    fn avg_pool2d_2x2_stride2() {
11408        // 1×1×4×4 input with values 0..15.
11409        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11410        let x = cpu_f32(data, &[1, 1, 4, 4]);
11411        let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11412        assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11413        // Each 2x2 block average: top-left = (0+1+4+5)/4 = 2.5,
11414        // top-right = (2+3+6+7)/4 = 4.5, bottom-left = (8+9+12+13)/4 = 10.5,
11415        // bottom-right = (10+11+14+15)/4 = 12.5.
11416        let v = out.realize_f32();
11417        assert!((v[0] - 2.5).abs() < 1e-5);
11418        assert!((v[1] - 4.5).abs() < 1e-5);
11419        assert!((v[2] - 10.5).abs() < 1e-5);
11420        assert!((v[3] - 12.5).abs() < 1e-5);
11421    }
11422
11423    #[test]
11424    fn avg_pool2d_3x3_stride1_padding1_preserves_size() {
11425        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11426        let out = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11427        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11428    }
11429
11430    /// KISS-Ops §6.13 S14: `count_include_pad = false` must divide each
11431    /// border window by its TRUE valid-tap count, not the uniform `kh·kw`.
11432    /// Fixture: a 1x1x4x4 all-ones input, 3x3 kernel, padding 1.
11433    ///
11434    /// `excluded` (count_include_pad=false) must be EXACTLY 1.0 at every
11435    /// output position, border included: averaging any positive count of
11436    /// real taps that are all 1.0 is always 1.0, regardless of how many
11437    /// padded slots surround them — this is the invariant the fix exists
11438    /// to restore. `included` (count_include_pad=true, today's old
11439    /// behavior) must NOT share that invariant: it divides by the uniform
11440    /// 9 regardless of how many taps were real, so a corner window (only
11441    /// 4 of 9 taps real) computes `4/9 ≈ 0.444`, well below 1.0 — the two
11442    /// modes genuinely disagree at the border, which is the point of the
11443    /// fix. (On fully-interior positions, where every tap is real either
11444    /// way, both modes necessarily agree — not asserted here since it
11445    /// proves nothing about the fix; see the non-uniform fixture below for
11446    /// a sharper per-position contrast.)
11447    #[test]
11448    fn avg_pool2d_excludes_padding_keeps_uniform_data_exactly_at_its_true_value() {
11449        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11450        let included = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11451        let excluded = x.avg_pool2d((3, 3), (1, 1), (1, 1), false).unwrap();
11452        assert_eq!(included.shape().dims(), excluded.shape().dims());
11453
11454        let ve = excluded.realize_f32();
11455        for (i, &v) in ve.iter().enumerate() {
11456            assert!(
11457                (v - 1.0).abs() < 1e-5,
11458                "count_include_pad=false must be exactly 1.0 at every position \
11459                 (all real taps are 1.0) — position {i} got {v}",
11460            );
11461        }
11462
11463        let vi = included.realize_f32();
11464        assert!(
11465            (vi[0] - 1.0).abs() > 0.1,
11466            "count_include_pad=true must NOT match at a corner (4 of 9 taps \
11467             real → 4/9, far from 1.0) — got {}, modes should disagree here",
11468            vi[0]
11469        );
11470    }
11471
11472    /// The actual discriminating case: non-uniform data at a border window.
11473    /// A 1x1x2x2 input `[[10, 10], [10, 10]]`, kernel 2x2, padding 1 (so
11474    /// the TOP-LEFT output position's window is `[[pad,pad],[pad,10]]` --
11475    /// only ONE of four taps is real). count_include_pad=true (today's old
11476    /// behavior) divides by the uniform 4: `10/4 = 2.5`. count_include_pad
11477    /// =false (ONNX/PyTorch default) divides by the TRUE valid count, 1:
11478    /// `10/1 = 10.0` -- the actual average of the real values in the
11479    /// window, which is what "average pooling, excluding padding" means.
11480    #[test]
11481    fn avg_pool2d_excludes_padding_from_divisor_discriminating() {
11482        let x = cpu_f32(vec![10.0, 10.0, 10.0, 10.0], &[1, 1, 2, 2]);
11483        let included = x.avg_pool2d((2, 2), (1, 1), (1, 1), true).unwrap();
11484        let excluded = x.avg_pool2d((2, 2), (1, 1), (1, 1), false).unwrap();
11485        let vi = included.realize_f32();
11486        let ve = excluded.realize_f32();
11487        assert!(
11488            (vi[0] - 2.5).abs() < 1e-5,
11489            "count_include_pad=true: top-left = 10/4 = 2.5, got {}",
11490            vi[0]
11491        );
11492        assert!(
11493            (ve[0] - 10.0).abs() < 1e-5,
11494            "count_include_pad=false: top-left = 10/1 (ONE real tap) = 10.0, got {}",
11495            ve[0]
11496        );
11497    }
11498
11499    /// A 1x1 input with padding >= kernel size: EVERY output window is
11500    /// entirely padding, so the count-include-pad=false divisor would be
11501    /// zero everywhere. Must refuse with a descriptive error rather than
11502    /// let `broadcast_div` emit NaN/inf.
11503    #[test]
11504    fn avg_pool2d_rejects_zero_valid_tap_window() {
11505        let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11506        let err = x
11507            .avg_pool2d((1, 1), (1, 1), (2, 2), false)
11508            .expect_err("a window with zero real taps must be rejected, not silently NaN");
11509        let msg = err.to_string();
11510        assert!(
11511            msg.contains("divide by zero") || msg.contains("no valid"),
11512            "error should explain the zero-valid-tap cause, got: {msg}"
11513        );
11514
11515        // Positive control: count_include_pad=true has no notion of "valid
11516        // taps" (it always divides by kh*kw), so the SAME shape must still
11517        // succeed under that mode -- proving the rejection is specific to
11518        // the zero-valid-tap case, not a blanket refusal of this shape.
11519        let ok = x.avg_pool2d((1, 1), (1, 1), (2, 2), true);
11520        assert!(
11521            ok.is_ok(),
11522            "count_include_pad=true must still accept this shape (uniform divisor, never zero)"
11523        );
11524    }
11525
11526    #[test]
11527    fn avg_pool2d_multi_channel() {
11528        // 1×2×2×2: each channel is filled with its index.
11529        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]);
11530        let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11531        assert_eq!(out.shape().dims(), &[1, 2, 1, 1]);
11532        assert_eq!(out.realize_f32(), vec![0.0, 1.0]);
11533    }
11534
11535    #[test]
11536    fn max_pool2d_2x2_stride2() {
11537        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11538        let x = cpu_f32(data, &[1, 1, 4, 4]);
11539        let out = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11540        assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11541        // Each 2x2 block max: 5, 7, 13, 15.
11542        assert_eq!(out.realize_f32(), vec![5.0, 7.0, 13.0, 15.0]);
11543    }
11544
11545    #[test]
11546    fn max_pool2d_3x3_stride1_padding1() {
11547        let x = cpu_f32(
11548            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
11549            &[1, 1, 3, 3],
11550        );
11551        let out = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11552        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11553        // Center should be the global max 9; corners should be the max of their 2×2 window.
11554        let v = out.realize_f32();
11555        // (1,1) center: max of all 9 = 9
11556        assert!((v[4] - 9.0).abs() < 1e-5);
11557    }
11558
11559    #[test]
11560    fn upsample_nearest2d_2x() {
11561        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11562        let out = x.upsample_nearest2d(2).unwrap();
11563        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11564        // Each cell replicated 2×2: rows are [1,1,2,2; 1,1,2,2; 3,3,4,4; 3,3,4,4].
11565        assert_eq!(
11566            out.realize_f32(),
11567            vec![
11568                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,
11569            ],
11570        );
11571    }
11572
11573    #[test]
11574    fn upsample_nearest2d_3x() {
11575        let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11576        let out = x.upsample_nearest2d(3).unwrap();
11577        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11578        assert_eq!(out.realize_f32(), vec![5.0; 9]);
11579    }
11580
11581    #[test]
11582    fn upsample_nearest2d_identity_scale_one() {
11583        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11584        let out = x.upsample_nearest2d(1).unwrap();
11585        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
11586    }
11587
11588    #[test]
11589    fn upsample_nearest1d_2x() {
11590        let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11591        let out = x.upsample_nearest1d(2).unwrap();
11592        assert_eq!(out.shape().dims(), &[1, 1, 6]);
11593        assert_eq!(out.realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
11594    }
11595
11596    #[test]
11597    fn interpolate2d_integer_multiple() {
11598        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11599        let out = x.interpolate2d(4, 4).unwrap();
11600        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11601    }
11602
11603    #[test]
11604    fn interpolate2d_accepts_non_integer_ratio() {
11605        // Lifted from "rejects non-integer ratio" — arbitrary
11606        // ratios are now supported via the index_select composite
11607        // (matching the eager UpsampleNearest2D convention). See
11608        // tests/lazy_interpolate2d_oracle.rs for parity checks.
11609        let x = cpu_f32(vec![1.0; 4], &[1, 1, 2, 2]);
11610        let out = x.interpolate2d(3, 4).unwrap();
11611        assert_eq!(out.shape().dims(), &[1, 1, 3, 4]);
11612    }
11613
11614    #[test]
11615    fn interpolate1d_integer_multiple() {
11616        let x = cpu_f32(vec![1.0, 2.0], &[1, 1, 2]);
11617        let out = x.interpolate1d(6).unwrap();
11618        assert_eq!(out.shape().dims(), &[1, 1, 6]);
11619        assert_eq!(out.realize_f32(), vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
11620    }
11621
11622    #[test]
11623    fn pool_rejects_bad_rank() {
11624        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11625        assert!(x.avg_pool2d((2, 2), (2, 2), (0, 0), true).is_err());
11626        assert!(x.max_pool2d((2, 2), (2, 2), (0, 0)).is_err());
11627    }
11628
11629    #[test]
11630    fn pool_rejects_zero_kernel() {
11631        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11632        assert!(x.avg_pool2d((0, 2), (1, 1), (0, 0), true).is_err());
11633        assert!(x.max_pool2d((2, 0), (1, 1), (0, 0)).is_err());
11634    }
11635
11636    // ---- Phase A.8 scope-limited harmonization aliases ----
11637
11638    #[test]
11639    fn numel_matches_elem_count() {
11640        let t = cpu_f32(vec![1.0; 12], &[3, 4]);
11641        assert_eq!(t.numel(), t.elem_count());
11642        assert_eq!(t.numel(), 12);
11643    }
11644
11645    #[test]
11646    fn dim_returns_specific_axis_size() {
11647        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11648        assert_eq!(t.dim(0).unwrap(), 2);
11649        assert_eq!(t.dim(1).unwrap(), 3);
11650        assert_eq!(t.dim(2).unwrap(), 4);
11651        assert!(t.dim(3).is_err());
11652    }
11653
11654    #[test]
11655    fn to_dtype_switches_dtype() {
11656        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11657        let b = t.to_dtype(DType::F64).unwrap();
11658        assert_eq!(b.dtype(), DType::F64);
11659        assert_eq!(b.realize_f64(), vec![1.0, 2.0]);
11660    }
11661
11662    #[test]
11663    fn to_dtype_same_dtype_is_noop() {
11664        let t = cpu_f32(vec![1.0_f32], &[1]);
11665        let b = t.to_dtype(DType::F32).unwrap();
11666        assert_eq!(b.dtype(), DType::F32);
11667    }
11668
11669    #[test]
11670    fn detach_is_identity_on_lazy() {
11671        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11672        let d = t.detach();
11673        assert_eq!(d.realize_f32(), t.realize_f32());
11674    }
11675
11676    #[test]
11677    fn track_op_is_true_on_lazy() {
11678        let t = cpu_f32(vec![0.0], &[1]);
11679        assert!(t.track_op());
11680    }
11681
11682    // ---- Phase A.8a Dim/Dims trait port ergonomics tests ----
11683
11684    #[test]
11685    fn try_permute_accepts_tuple_syntax() {
11686        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11687        // Eager-style tuple permute now works on lazy.
11688        let out = t.permute((2_usize, 0_usize, 1_usize)).unwrap();
11689        assert_eq!(out.shape().dims(), &[4, 2, 3]);
11690    }
11691
11692    #[test]
11693    fn dim_arg_methods_accept_negative_indexing() {
11694        use fuel_ir::D;
11695        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
11696        // squeeze, sum_dim, mean_dim, etc. all accept D::Minus1 now.
11697        let sum_last = t.shape().dims().to_vec(); // just demonstrate compile
11698        assert_eq!(sum_last, vec![2, 3]);
11699        // sum_dim is still Self-returning (Phase A.8b will flip), so test
11700        // a method that already returns Result + dim arg.
11701        let cumsum_last = t.cumsum(D::Minus1).unwrap();
11702        assert_eq!(cumsum_last.shape().dims(), &[2, 3]);
11703    }
11704
11705    #[test]
11706    fn unsqueeze_accepts_dim_trait() {
11707        use fuel_ir::D;
11708        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11709        // Append a new last dim via D::Minus1 (rank-aware negative indexing).
11710        let out = t.unsqueeze(D::Minus1).unwrap();
11711        // The position D::Minus1 in to_index_plus_one is "the very end"
11712        // → output rank 2 with the new dim trailing.
11713        assert_eq!(out.shape().dims().len(), 2);
11714    }
11715}
11716pub struct TokenDataHost {
11717    pub token_ids: fuel_ir::HostBuffer,
11718    pub rope_cos: fuel_ir::HostBuffer,
11719    pub rope_sin: fuel_ir::HostBuffer,
11720    pub mask: fuel_ir::HostBuffer,
11721    pub offset: Option<fuel_ir::HostBuffer>,
11722}
11723
11724/// Same per-token data as [`crate::inference_context::DecodeTokenData`],
11725/// as raw host bytes instead of freshly-uploaded device Arcs — for
11726/// [`fuel_dispatch::pipelined::CapturedDecodeSession::replay_token`]'s
11727/// in-place H2D overwrite of fixed buffers. `LlamaModel`-private.
11728#[cfg(feature = "cuda")]
11729pub struct TokenDataBytes {
11730    pub token_ids: Vec<u8>,
11731    pub rope_cos: Vec<u8>,
11732    pub rope_sin: Vec<u8>,
11733    pub mask: Vec<u8>,
11734    pub offset: Option<Vec<u8>>,
11735}
11736
11737/// D2H a [`fuel_dispatch::pipelined::CapturedDecodeSession::replay_token`]
11738/// result's device-resident output Arc to a host `Vec<f32>`.
11739///
11740/// The capture mechanism deliberately keeps the output device-resident
11741/// INSIDE the capture (a `Copy`/`Move` node is capture-unsafe — see
11742/// `LlamaModel::forward_with_kv_context_captured`'s design note); this
11743/// reads it back OUTSIDE the capture, dispatching on the same
11744/// `BackendStorage` variants every other pipelined D2H site in this
11745/// codebase does (`CudaStorageBytes::to_cpu_bytes` / `CpuStorageBytes::
11746/// bytes`) rather than hand-rolling a new D2H mechanism. Capture is
11747/// f32-only today (project constraint), so the bytes are reinterpreted
11748/// as f32 directly.
11749#[cfg(feature = "cuda")]
11750pub fn captured_output_to_f32(
11751    output: &Arc<std::sync::RwLock<fuel_memory::Storage>>,
11752) -> fuel_ir::error::Result<Vec<f32>> {
11753    use fuel_memory::BackendStorage;
11754    let guard = output.read().map_err(|_| {
11755        fuel_ir::Error::Msg("captured decode output storage lock poisoned".into()).bt()
11756    })?;
11757    let bytes: Vec<u8> = match &guard.inner {
11758        BackendStorage::Cpu(c) => c.bytes().to_vec(),
11759        BackendStorage::Cuda(c) => c.to_cpu_bytes()?,
11760        #[allow(unreachable_patterns)]
11761        other => {
11762            return Err(fuel_ir::Error::Msg(format!(
11763                "captured decode output is BackendStorage::{:?}, expected Cpu or Cuda",
11764                std::mem::discriminant(other),
11765            ))
11766            .bt());
11767        }
11768    };
11769    Ok(bytemuck::cast_slice::<u8, f32>(&bytes).to_vec())
11770}