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    /// An AWQ 4-bit linear (HuggingFace `*-AWQ`, GEMM version) in the
8384    /// `AWQ_MATMUL` kernel layout, as graph-independent host buffers. The
8385    /// graph sees it through `NodeHandle::awq_matmul` operands
8386    /// built in `apply_linear`; it has no plain `const_like` form.
8387    Awq(Arc<crate::awq::AwqHostWeight>),
8388    /// Base weight wrapped with a trainable LoRA (Low-Rank Adaptation)
8389    /// update: effective weight `W_eff = base + (alpha / rank) · A · B`
8390    /// where `A` has shape `[in_features, rank]` and `B` has shape
8391    /// `[rank, out_features]` (both stored in the same layout
8392    /// convention as F32 weights — `[in, out]`).
8393    ///
8394    /// Used for PEFT-style inference with frozen base weights (which
8395    /// can be F32, BF16, or Q4_0) plus small trainable adapter matrices.
8396    /// The adapter is cheap to apply — for a 2560×2560 projection at
8397    /// rank 8 the LoRA path is ~0.5% of the base matmul cost.
8398    WithLoRA {
8399        base: Box<WeightStorage>,
8400        /// `[in_features, rank]` adapter A (HF's `lora_A` transposed).
8401        lora_a: Arc<[f32]>,
8402        /// `[rank, out_features]` adapter B (HF's `lora_B` transposed).
8403        lora_b: Arc<[f32]>,
8404        rank: usize,
8405        /// LoRA scaling factor; effective scale is `alpha / rank`.
8406        alpha: f32,
8407        in_features: usize,
8408        out_features: usize,
8409    },
8410}
8411
8412impl WeightStorage {
8413    pub fn elem_count(&self) -> usize {
8414        match self {
8415            Self::F32(a) => a.len(),
8416            Self::BF16(a) => a.len(),
8417            // Logical element count for a Q4_0 weight matrix is n*k.
8418            Self::Q4_0 {
8419                in_features,
8420                out_features,
8421                ..
8422            } => *in_features * *out_features,
8423            Self::Awq(w) => w.ic * w.oc,
8424            Self::WithLoRA {
8425                in_features,
8426                out_features,
8427                ..
8428            } => *in_features * *out_features,
8429        }
8430    }
8431
8432    pub fn dtype(&self) -> fuel_ir::DType {
8433        match self {
8434            Self::F32(_) => fuel_ir::DType::F32,
8435            Self::BF16(_) => fuel_ir::DType::BF16,
8436            // Q4_0 surfaces as U32 at the graph level (raw bytes
8437            // reinterpreted). Callers that care about the "actual"
8438            // quantization type should match on the variant directly.
8439            Self::Q4_0 { .. } => fuel_ir::DType::U32,
8440            // AWQ surfaces as U32 for the same reason (packed nibbles).
8441            Self::Awq(_) => fuel_ir::DType::U32,
8442            // WithLoRA exposes the base's dtype (the LoRA adapter is
8443            // always F32 but activations are typed by the base weight).
8444            Self::WithLoRA { base, .. } => base.dtype(),
8445        }
8446    }
8447
8448    /// Emit a `Const` node on `anchor`'s graph matching this
8449    /// storage's dtype. Used everywhere the forward pass wraps a
8450    /// weight into a `Tensor`.
8451    ///
8452    /// For `Q4_0`, the emitted tensor is a 1-D `U32` const of length
8453    /// `bytes.len() / 4` holding the raw block byte stream. Callers
8454    /// must pair this with `Tensor::qmatmul` rather than `matmul`.
8455    ///
8456    /// Returns Err for `WithLoRA` — the base + LoRA update can only be
8457    /// applied via `apply_linear` so the right graph structure is built.
8458    pub fn const_like(
8459        &self,
8460        anchor: &Tensor,
8461        shape: Shape,
8462    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8463        match self {
8464            Self::F32(a) => anchor.const_f32_like(a.clone(), shape),
8465            Self::BF16(a) => anchor.const_bf16_like(a.clone(), shape),
8466            Self::Q4_0 { words, .. } => {
8467                let _ = shape; // shape arg unused — Q4_0 const is 1-D U32
8468                // Arc-clone the precomputed u32 view; no byte copy.
8469                anchor.const_u32_like(Arc::clone(words), Shape::from_dims(&[words.len()]))
8470            }
8471            Self::Awq(_) => Err(fuel_ir::Error::Msg(
8472                "WeightStorage::Awq::const_like is not supported — an AWQ weight is three operands \
8473                 (weight, zeros, scales), applied via apply_linear."
8474                    .into(),
8475            )
8476            .bt()),
8477            Self::WithLoRA { .. } => Err(fuel_ir::Error::Msg(
8478                "WeightStorage::WithLoRA::const_like is not supported \
8479                 — the base + LoRA update must be applied via \
8480                 apply_linear to produce the right graph structure."
8481                    .into(),
8482            )
8483            .bt()),
8484        }
8485    }
8486
8487    /// Produce `X @ W + bias` for this weight storage. Bias is a
8488    /// length-`out_features` Arc<[f32]> materialized fresh on the
8489    /// receiver's graph and broadcast across the leading dims of
8490    /// the output.
8491    ///
8492    /// Equivalent to the per-port `apply_linear_with_bias` helpers
8493    /// that several ports inlined — promoted here so call sites
8494    /// stop drifting.
8495    pub fn apply_linear_with_bias(
8496        &self,
8497        x: &Tensor,
8498        in_features: usize,
8499        out_features: usize,
8500        bias: std::sync::Arc<[f32]>,
8501    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8502        if bias.len() != out_features {
8503            // Was a `debug_assert_eq!` — i.e. compiled OUT of release builds,
8504            // so a release binary silently built a broadcast against a
8505            // wrong-length bias. A length mismatch is a checkpoint condition;
8506            // it errors in every profile now.
8507            return Err(fuel_ir::Error::Msg(format!(
8508                "apply_linear_with_bias: bias len ({}) != out_features ({out_features})",
8509                bias.len(),
8510            ))
8511            .bt());
8512        }
8513        let projected = self.apply_linear(x, in_features, out_features)?;
8514        let bias_t = projected.const_f32_like(bias, Shape::from_dims(&[out_features]))?;
8515        projected.broadcast_add(&bias_t)
8516    }
8517
8518    /// Produce `X @ W` (with optional bias) for this weight storage.
8519    /// Dispatches to `matmul` for F32/BF16 weights and to `qmatmul`
8520    /// for Q4_0. `x`'s dtype must be compatible with this weight's
8521    /// dtype per `matmul`'s gate: same-dtype homogeneous (e.g. BF16
8522    /// activations × BF16 weights — LlamaModel's BF16-throughout
8523    /// decode path, Phase D increment A) or `(x=F32, weight=BF16)`
8524    /// (f32-activation quantized-weight serving). `(x=BF16,
8525    /// weight=F32)` is NOT supported — cast the weight or the
8526    /// activation to match before calling.
8527    /// # Errors
8528    ///
8529    /// Returns a typed error rather than panicking on a dimension or dtype
8530    /// mismatch. This used to `.unwrap()` the inner `matmul`/`qmatmul` and
8531    /// `assert_eq!` the stored-vs-requested dimensions, which made a
8532    /// **production** path panic — a mismatched checkpoint (a GGUF whose
8533    /// stored shape disagrees with the model config) is a data condition, not
8534    /// a programming invariant, so it must surface as `Err`.
8535    pub fn apply_linear(
8536        &self,
8537        x: &Tensor,
8538        in_features: usize,
8539        out_features: usize,
8540    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8541        // Build-time contract check, shared by every arm: a "linear with
8542        // `in_features`" requires x's trailing dim to BE `in_features`.
8543        // Checked here rather than at the ~620 call sites, and phrased so the
8544        // message names the projection instead of surfacing a bare matmul
8545        // shape error two layers down.
8546        let x_dims = x.shape().dims().to_vec();
8547        match x_dims.last() {
8548            Some(&k) if k == in_features => {}
8549            _ => {
8550                return Err(fuel_ir::Error::Msg(format!(
8551                    "apply_linear({in_features} -> {out_features}): input shape {x_dims:?} \
8552                     must have trailing dim {in_features}",
8553                ))
8554                .bt());
8555            }
8556        }
8557        match self {
8558            Self::F32(_) | Self::BF16(_) => {
8559                let w = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8560                x.matmul(&w)
8561            }
8562            Self::Q4_0 {
8563                in_features: expected_in,
8564                out_features: expected_out,
8565                ..
8566            } => {
8567                if *expected_in != in_features || *expected_out != out_features {
8568                    return Err(fuel_ir::Error::Msg(format!(
8569                        "WeightStorage::Q4_0 shape mismatch: stored \
8570                         [{expected_in}, {expected_out}], requested \
8571                         [{in_features}, {out_features}]",
8572                    ))
8573                    .bt());
8574                }
8575                let w_bytes = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8576                x.qmatmul(
8577                    &w_bytes,
8578                    fuel_graph::QuantType::Q4_0,
8579                    in_features,
8580                    out_features,
8581                )
8582            }
8583            Self::Awq(w) => {
8584                if w.ic != in_features || w.oc != out_features {
8585                    return Err(fuel_ir::Error::Msg(format!(
8586                        "WeightStorage::Awq shape mismatch: stored [{}, {}], requested [{in_features}, {out_features}]",
8587                        w.ic, w.oc,
8588                    ))
8589                    .bt());
8590                }
8591                // 8 is AutoAWQ's default launch tuning; the decompose path
8592                // ignores it.
8593                w.matmul(x, 8)
8594            }
8595            Self::WithLoRA {
8596                base,
8597                lora_a,
8598                lora_b,
8599                rank,
8600                alpha,
8601                in_features: expected_in,
8602                out_features: expected_out,
8603            } => {
8604                if *expected_in != in_features || *expected_out != out_features {
8605                    return Err(fuel_ir::Error::Msg(format!(
8606                        "WeightStorage::WithLoRA shape mismatch: stored \
8607                         [{expected_in}, {expected_out}], requested \
8608                         [{in_features}, {out_features}]",
8609                    ))
8610                    .bt());
8611                }
8612                // Base forward (F32, BF16, or Q4_0).
8613                let base_out = base.apply_linear(x, in_features, out_features)?;
8614                // Low-rank update: y += (alpha/rank) · x @ A @ B.
8615                let a_t =
8616                    x.const_f32_like(Arc::clone(lora_a), Shape::from_dims(&[in_features, *rank]))?;
8617                let b_t =
8618                    x.const_f32_like(Arc::clone(lora_b), Shape::from_dims(&[*rank, out_features]))?;
8619                let scale = *alpha as f64 / *rank as f64;
8620                // x: [*, in] → @A [*, rank] → @B [*, out] → scale → add base.
8621                let lora_path = Tensor {
8622                    inner: x.matmul(&a_t)?.matmul(&b_t)?.inner.mul_scalar(scale),
8623                };
8624                base_out.add(&lora_path)
8625            }
8626        }
8627    }
8628
8629    /// Data-determined-M variant of [`Self::apply_linear`]: computes only
8630    /// `row_count` rows of the capacity-buffer projection `x @ W`, the rest
8631    /// left zeroed — the per-expert sparse-MoE FFN path. `x` is a
8632    /// `[capacity, in_features]` buffer whose first `row_count` rows are the
8633    /// routed tokens; the returned `[capacity, out_features]` buffer's tail
8634    /// (rows `row_count..capacity`) stays exactly zero (see
8635    /// [`Tensor::matmul_dyn_m`]).
8636    ///
8637    /// F32-only (mirrors `matmul_dyn_m`); other weight encodings surface a
8638    /// typed build-time error. No bias is applied — a bias would fill the
8639    /// un-computed tail, which the sparse-MoE caller relies on being zero
8640    /// for a correct `index_add` scatter-back.
8641    pub fn apply_linear_dyn_m(
8642        &self,
8643        x: &Tensor,
8644        in_features: usize,
8645        out_features: usize,
8646        row_count: fuel_ir::DynScalar,
8647    ) -> std::result::Result<Tensor, fuel_ir::Error> {
8648        match self {
8649            Self::F32(_) => {
8650                // const_like only errors on WithLoRA; the F32 arm is infallible.
8651                let w = self
8652                    .const_like(x, Shape::from_dims(&[in_features, out_features]))
8653                    .expect("apply_linear_dyn_m F32 arm: const_like cannot fail for F32");
8654                x.matmul_dyn_m(&w, row_count)
8655            }
8656            other => Err(fuel_ir::Error::Msg(format!(
8657                "apply_linear_dyn_m: sparse-MoE dispatch is F32-only today, got {:?} \
8658                 weight (cast the expert weights to F32)",
8659                other.dtype(),
8660            ))
8661            .bt()),
8662        }
8663    }
8664
8665    /// Wrap this weight storage with a LoRA adapter. Asserts that the
8666    /// adapter shapes match `in_features`/`out_features`. Panics if the
8667    /// base is already a `WithLoRA` (nested adapters aren't supported;
8668    /// merge them explicitly if needed).
8669    pub fn with_lora(
8670        self,
8671        lora_a: Arc<[f32]>,
8672        lora_b: Arc<[f32]>,
8673        rank: usize,
8674        alpha: f32,
8675        in_features: usize,
8676        out_features: usize,
8677    ) -> Self {
8678        assert_eq!(
8679            lora_a.len(),
8680            in_features * rank,
8681            "lora_a length {} does not match in_features ({in_features}) × rank ({rank}) = {}",
8682            lora_a.len(),
8683            in_features * rank,
8684        );
8685        assert_eq!(
8686            lora_b.len(),
8687            rank * out_features,
8688            "lora_b length {} does not match rank ({rank}) × out_features ({out_features}) = {}",
8689            lora_b.len(),
8690            rank * out_features,
8691        );
8692        assert!(
8693            !matches!(self, Self::WithLoRA { .. }),
8694            "with_lora: base is already WithLoRA (nested adapters unsupported)",
8695        );
8696        Self::WithLoRA {
8697            base: Box::new(self),
8698            lora_a,
8699            lora_b,
8700            rank,
8701            alpha,
8702            in_features,
8703            out_features,
8704        }
8705    }
8706}
8707
8708// Auto-conversions so code that was storing `Arc<[f32]>` keeps
8709// compiling through the refactor — the LayerWeights field type
8710// widened to WeightStorage but ergonomics don't regress.
8711impl From<Arc<[f32]>> for WeightStorage {
8712    fn from(a: Arc<[f32]>) -> Self {
8713        Self::F32(a)
8714    }
8715}
8716impl From<Vec<f32>> for WeightStorage {
8717    fn from(v: Vec<f32>) -> Self {
8718        Self::F32(Arc::from(v))
8719    }
8720}
8721impl From<Arc<[half::bf16]>> for WeightStorage {
8722    fn from(a: Arc<[half::bf16]>) -> Self {
8723        Self::BF16(a)
8724    }
8725}
8726impl From<Vec<half::bf16>> for WeightStorage {
8727    fn from(v: Vec<half::bf16>) -> Self {
8728        Self::BF16(Arc::from(v))
8729    }
8730}
8731
8732/// Per-layer weights of a LLaMA transformer block. All tensors are
8733/// stored as `Arc<[f32]>` so they can be loaded once and shared across
8734/// every forward pass with zero copy — each call to
8735/// `LlamaModel::forward` clones the `Arc` (a refcount bump) when it
8736/// builds fresh const nodes for this layer.
8737///
8738/// LLaMA proper has no biases anywhere in the attention block, so the
8739/// `*_bias` fields are `None` for LLaMA family models. Qwen2 and a few
8740/// related architectures do add biases on Q/K/V (but not on the output
8741/// projection), so the loader stores them here when the safetensors
8742/// file contains them.
8743#[derive(Debug, Clone)]
8744pub struct LayerWeights {
8745    /// `[dim, dim]` query projection. Supports bf16 or f32.
8746    pub attn_q: WeightStorage,
8747    /// `[dim]` query projection bias (Qwen2-style; LLaMA has none).
8748    pub attn_q_bias: Option<Arc<[f32]>>,
8749    /// `[dim, dim]` key projection.
8750    pub attn_k: WeightStorage,
8751    /// `[kv_dim]` key projection bias.
8752    pub attn_k_bias: Option<Arc<[f32]>>,
8753    /// `[dim, dim]` value projection.
8754    pub attn_v: WeightStorage,
8755    /// `[kv_dim]` value projection bias.
8756    pub attn_v_bias: Option<Arc<[f32]>>,
8757    /// `[dim, dim]` output projection.
8758    pub attn_o: WeightStorage,
8759    /// `[dim, ffn_dim]` gate projection for SwiGLU.
8760    pub ffn_gate: WeightStorage,
8761    /// `[dim, ffn_dim]` up projection for SwiGLU.
8762    pub ffn_up: WeightStorage,
8763    /// `[ffn_dim, dim]` down projection for SwiGLU.
8764    pub ffn_down: WeightStorage,
8765    /// `[dim]` RmsNorm gain for the pre-attention norm. Stays f32
8766    /// — norm gains are small and precision-sensitive.
8767    pub attn_norm_gain: Arc<[f32]>,
8768    /// `[dim]` RmsNorm gain for the pre-FFN norm.
8769    pub ffn_norm_gain: Arc<[f32]>,
8770}
8771
8772pub fn invalidate_decode_pair_if_stale<C>(
8773    session: &mut Option<crate::inference_context::DecodeSession>,
8774    captured: &mut Option<C>,
8775    ctx: &mut InferenceContext,
8776    seq: usize,
8777    max_seq_len: Option<usize>,
8778    cache_dtype: DType,
8779    n_layers: usize,
8780    shape_key: u64,
8781    kv: &dyn crate::inference_context::KvRebindSource,
8782    drop_session: impl FnOnce(
8783        &mut Option<crate::inference_context::DecodeSession>,
8784        &mut InferenceContext,
8785    ),
8786) -> SessionDisposition {
8787    refresh_decode_session(
8788        session,
8789        ctx,
8790        seq,
8791        max_seq_len,
8792        cache_dtype,
8793        n_layers,
8794        shape_key,
8795        kv,
8796        // Reader before owner — see the doc comment above. This fires for a
8797        // re-bind as well as a drop: a capture records FIXED device addresses
8798        // drawn from `base_cache`, and `rebind_kv` replaces some of them.
8799        || *captured = None,
8800        drop_session,
8801    )
8802}
8803
8804/// What [`refresh_decode_session`] did to the held plan.
8805#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8806pub enum SessionDisposition {
8807    /// There was no held plan; nothing to do.
8808    Absent,
8809    /// The plan is valid for this step and was left alone.
8810    Kept,
8811    /// The KV allocation changed and the plan was re-bound onto it (GAP-028).
8812    /// Any reader of the plan's device addresses has been retired.
8813    Rebound,
8814    /// The plan was retired. `Some(refusal)` means a re-bind was attempted and
8815    /// declined — carried so a caller (or a test) can distinguish "refused for
8816    /// the right reason" from "refused because nothing was tried".
8817    Dropped(Option<crate::inference_context::RebindRefusal>),
8818}
8819
8820/// **The single decision point for what happens to a held decode plan at the
8821/// top of a decode step.** Every model routes through here, so they cannot
8822/// drift on what "stale" means — the reason this was a shared free function
8823/// before GAP-028, and more so now that the answer has three outcomes.
8824///
8825/// Order of business, and why it is this order:
8826///
8827/// 1. Ask [`crate::inference_context::DecodeSession::validity_for`] — ONE comparison list, so "is it
8828///    valid" and "is only the allocation different" cannot disagree.
8829/// 2. On anything but `Valid`, retire readers FIRST via `on_invalidate`. A
8830///    `CapturedDecodeSession` replays a recorded CUDA graph over FIXED device
8831///    addresses drawn from the session's `base_cache`; both a drop and a
8832///    re-bind make some of those addresses stale, and a replay against stale
8833///    addresses is silently wrong logits at full speed. Retiring before
8834///    touching the owner means no replay can observe a half-updated plan.
8835///    Note this fires *before* the re-bind is attempted, not after it
8836///    succeeds: a capture is worthless either way, and clearing it early keeps
8837///    the ordering true without depending on the outcome.
8838/// 3. Try the guarded re-bind. It may refuse; a refusal is not an error.
8839/// 4. Otherwise drop, so the caller's first-decode-token arm rebuilds.
8840///
8841/// Generic in nothing — the capture rides in the `on_invalidate` closure,
8842/// which is what lets this compile (and therefore be CPU-tested) without the
8843/// `cuda` feature.
8844pub fn refresh_decode_session(
8845    session: &mut Option<crate::inference_context::DecodeSession>,
8846    ctx: &mut InferenceContext,
8847    seq: usize,
8848    max_seq_len: Option<usize>,
8849    cache_dtype: DType,
8850    n_layers: usize,
8851    shape_key: u64,
8852    kv: &dyn crate::inference_context::KvRebindSource,
8853    on_invalidate: impl FnOnce(),
8854    drop_session: impl FnOnce(
8855        &mut Option<crate::inference_context::DecodeSession>,
8856        &mut InferenceContext,
8857    ),
8858) -> SessionDisposition {
8859    use crate::inference_context::PlanValidity;
8860
8861    let verdict = match (session.as_ref(), max_seq_len) {
8862        (None, _) => return SessionDisposition::Absent,
8863        (Some(s), Some(msl)) => {
8864            s.validity_for(seq, msl, n_layers, cache_dtype, shape_key, kv.alloc_id())
8865        }
8866        // A cache with no pre-allocated capacity cannot back a held plan at
8867        // all (the graph bakes `max_seq_len` into its KV Const shapes).
8868        (Some(_), None) => PlanValidity::Stale,
8869    };
8870    if verdict == PlanValidity::Valid {
8871        return SessionDisposition::Kept;
8872    }
8873
8874    on_invalidate();
8875
8876    let refusal = if verdict == PlanValidity::AllocationChanged {
8877        match session.as_mut().map(|s| s.rebind_kv(kv)) {
8878            Some(Ok(())) => return SessionDisposition::Rebound,
8879            Some(Err(r)) => Some(r),
8880            // Unreachable: `verdict` was computed from a `Some` session and
8881            // nothing above takes it. Handled rather than `expect`ed because
8882            // this is a production path.
8883            None => None,
8884        }
8885    } else {
8886        None
8887    };
8888
8889    drop_session(session, ctx);
8890    SessionDisposition::Dropped(refusal)
8891}
8892
8893pub fn build_decode_causal_mask(cached_len: usize, seq: usize, max_seq_len: usize) -> Vec<f32> {
8894    let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8895    for q_idx in 0..seq {
8896        let abs_q = cached_len + q_idx;
8897        for k_idx in (abs_q + 1)..max_seq_len {
8898            mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8899        }
8900    }
8901    mask_data
8902}
8903
8904/// Sliding-window sibling of [`build_decode_causal_mask`] — the decode-time
8905/// mask for a layer that attends only to the most recent `window` positions.
8906///
8907/// # Why this exists (GAP-029 increment 3)
8908///
8909/// The decode path has always built **one** mask per model, because `LlamaModel`
8910/// and `PhiModel` — the only two carriers — have no per-layer mask variation
8911/// (measured: **zero** `sliding_window` hits in this file, against a positive
8912/// control of hits in five sibling model files). **Four of the six families
8913/// GAP-029 increment 3 ports do vary per layer**, three of them via a sliding
8914/// window: `Qwen2` (`lazy_qwen2.rs:249-251`), `Qwen3` (`lazy_qwen3.rs:172`) and
8915/// `Qwen3Moe` (`lazy_qwen3_moe.rs:192`) all select per layer on
8916/// `use_sliding_window && layer_idx < max_window_layers`.
8917///
8918/// # The predicate, and why it is stated against the prefill path
8919///
8920/// The shipped **prefill** mask for these families is `j > i || j + window <= i`
8921/// over absolute positions (`lazy_qwen2.rs:271`). Decode must agree with it
8922/// position-for-position, or a model silently changes behaviour at the
8923/// prefill→decode boundary. Here the query's absolute position is
8924/// `cached_len + q_idx` and the key's is `k_idx`, giving the same two clauses:
8925/// causal (`k_idx > abs_q`) and window (`k_idx + window <= abs_q`).
8926///
8927/// `window >= max_seq_len` makes the window clause unsatisfiable, so the result
8928/// is exactly [`build_decode_causal_mask`] — the property that lets a family
8929/// with the window disabled share the dense path rather than a near-copy of it.
8930///
8931/// # Status: LIVE. `#[allow(dead_code)]` removed 2026-08-13 — this has a caller.
8932///
8933/// It landed ahead of its consumer with its correctness established
8934/// independently (see the born-red record in `decode_mask_tests`), under an
8935/// architect-owned checkpoint: *land family 1 or the function is deleted*.
8936/// GAP-029 increment 3 landed `Qwen2Model`'s decode path, and
8937/// [`crate::persistent_decode::build_decode_mask_variants`] calls this for every
8938/// windowed mask variant — so the attribute is gone rather than renewed.
8939///
8940/// It is now on the live decode path of the three windowed families as they
8941/// arrive (`Qwen2Model` today; `Qwen3Model`, `Qwen3MoeModel` next), reached
8942/// through `MaskPlan::split_window`.
8943pub fn build_decode_causal_mask_windowed(
8944    cached_len: usize,
8945    seq: usize,
8946    max_seq_len: usize,
8947    window: usize,
8948) -> Vec<f32> {
8949    let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8950    for q_idx in 0..seq {
8951        let abs_q = cached_len + q_idx;
8952        for k_idx in 0..max_seq_len {
8953            // Causal: no future keys. Window: nothing older than `window` back.
8954            if k_idx > abs_q || k_idx + window <= abs_q {
8955                mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8956            }
8957        }
8958    }
8959    mask_data
8960}
8961
8962#[cfg(test)]
8963mod decode_mask_tests {
8964    use super::{build_decode_causal_mask, build_decode_causal_mask_windowed};
8965
8966    /// Render a mask row as `1` for attend / `0` for masked, so a failure
8967    /// prints the shape of the disagreement rather than a wall of `-inf`.
8968    fn attendable(row: &[f32]) -> Vec<u8> {
8969        row.iter().map(|&v| u8::from(v == 0.0)).collect()
8970    }
8971
8972    /// **The discriminating test.** Hand-computed ground truth, NOT a
8973    /// re-derivation of the implementation's own formula: at `cached_len = 3`,
8974    /// `seq = 1`, `max_seq_len = 6`, `window = 2` the single query sits at
8975    /// absolute position 3 and may attend to positions `{2, 3}` — 3 itself
8976    /// (causal boundary) and 2 (one step back, inside a window of 2). Positions
8977    /// 0 and 1 fall out of the window; 4 and 5 are the unwritten future tail.
8978    #[test]
8979    fn windowed_mask_excludes_positions_older_than_the_window() {
8980        let m = build_decode_causal_mask_windowed(3, 1, 6, 2);
8981        assert_eq!(
8982            attendable(&m),
8983            vec![0, 0, 1, 1, 0, 0],
8984            "window=2 at absolute position 3 must attend to exactly {{2, 3}}",
8985        );
8986    }
8987
8988    /// Multi-row (prefill-shaped) ground truth, also by hand: `cached_len = 1`,
8989    /// `seq = 2`, `window = 2` puts the queries at absolute positions 1 and 2,
8990    /// attending to `{0, 1}` and `{1, 2}` respectively. Guards the `q_idx`
8991    /// → `abs_q` offset, which a `seq == 1` test cannot see at all.
8992    #[test]
8993    fn windowed_mask_offsets_each_row_by_cached_len() {
8994        let m = build_decode_causal_mask_windowed(1, 2, 5, 2);
8995        assert_eq!(
8996            attendable(&m[0..5]),
8997            vec![1, 1, 0, 0, 0],
8998            "row 0 = abs pos 1"
8999        );
9000        assert_eq!(
9001            attendable(&m[5..10]),
9002            vec![0, 1, 1, 0, 0],
9003            "row 1 = abs pos 2"
9004        );
9005    }
9006
9007    /// **Non-discrimination control, and it is what keeps the two tests above
9008    /// meaningful.** A window at least as wide as the capacity cannot mask
9009    /// anything the causal clause does not already mask, so the windowed builder
9010    /// must reproduce the dense one byte-for-byte. This passes under BOTH a
9011    /// correct implementation and one that ignores `window` entirely — so it
9012    /// certifies that the suite is not simply broken, and it must NOT be read as
9013    /// evidence that the window works. (Same role as the positive control that
9014    /// stayed `ok` in increment 2a's sabotage record.)
9015    #[test]
9016    fn window_wider_than_capacity_is_byte_identical_to_the_dense_mask() {
9017        for (cached_len, seq, max_seq_len) in [(0, 1, 8), (3, 1, 8), (0, 4, 8), (2, 3, 8)] {
9018            let dense = build_decode_causal_mask(cached_len, seq, max_seq_len);
9019            let windowed =
9020                build_decode_causal_mask_windowed(cached_len, seq, max_seq_len, max_seq_len);
9021            assert_eq!(
9022                dense, windowed,
9023                "window == max_seq_len must equal dense at \
9024                 (cached_len={cached_len}, seq={seq}, max_seq_len={max_seq_len})",
9025            );
9026        }
9027    }
9028
9029    /// Decode must agree with the **shipped prefill** mask position-for-position,
9030    /// or a windowed model changes behaviour at the prefill→decode boundary.
9031    /// This replays `lazy_qwen2.rs:265-277`'s predicate over a full prefill of
9032    /// length `n`, then asserts that decoding position `p` with `cached_len = p`
9033    /// reproduces prefill row `p`. The prefill mask is `[seq, seq]` and the
9034    /// decode mask `[1, max_seq_len]`, so only the first `n` keys are comparable
9035    /// — the tail beyond `n` is the unwritten capacity the prefill mask has no
9036    /// opinion about.
9037    #[test]
9038    fn decode_row_matches_the_prefill_mask_row_at_the_same_position() {
9039        let (n, window, max_seq_len) = (6_usize, 3_usize, 10_usize);
9040
9041        // Qwen2's prefill predicate, transcribed from `build_layer_mask`.
9042        let mut prefill = vec![0.0_f32; n * n];
9043        for i in 0..n {
9044            for j in 0..n {
9045                if j > i || j + window <= i {
9046                    prefill[i * n + j] = f32::NEG_INFINITY;
9047                }
9048            }
9049        }
9050
9051        for p in 0..n {
9052            let decode = build_decode_causal_mask_windowed(p, 1, max_seq_len, window);
9053            assert_eq!(
9054                attendable(&decode[0..n]),
9055                attendable(&prefill[p * n..(p + 1) * n]),
9056                "decode at cached_len={p} must match prefill row {p}",
9057            );
9058            assert!(
9059                decode[n..].iter().all(|&v| v == f32::NEG_INFINITY),
9060                "the unwritten tail beyond position {n} must stay masked",
9061            );
9062        }
9063    }
9064
9065    // =======================================================================
9066    // BORN-RED RECORD (2026-08-13, GAP-029 increment 3)
9067    //
9068    // These tests were run FIRST against a deliberately wrong body — the
9069    // "old fabrication in the new shape": `build_decode_causal_mask_windowed`
9070    // returning `build_decode_causal_mask(..)` and ignoring `window`, which is
9071    // precisely what a single-mask decode port computes today. That makes the
9072    // red state prove *windowing vs dense* rather than merely proving the tests
9073    // execute.
9074    //
9075    //   windowed_mask_excludes_positions_older_than_the_window   FAILED
9076    //       left [1, 1, 1, 1, 0, 0]  right [0, 0, 1, 1, 0, 0]
9077    //   windowed_mask_offsets_each_row_by_cached_len             FAILED
9078    //       left [1, 1, 1, 0, 0]     right [0, 1, 1, 0, 0]
9079    //   decode_row_matches_the_prefill_mask_row_at_the_same_position FAILED
9080    //       left [1, 1, 1, 1, 0, 0]  right [0, 1, 1, 1, 0, 0]
9081    //   window_wider_than_capacity_is_byte_identical_to_the_dense_mask  ok
9082    //   test result: FAILED. 1 passed; 3 failed
9083    //
9084    // The `ok` line is load-bearing in BOTH directions and is the reason the
9085    // control exists: it holds under the correct body AND under the dense
9086    // fabrication, so (a) the suite is demonstrably not simply broken, and
9087    // (b) it must never be cited as evidence that windowing works. Only the
9088    // three that moved carry that claim.
9089    //
9090    // After the flip: `4 passed; 0 failed`, on a run reporting a 53.44s
9091    // compile — i.e. a rebuilt binary, not a cached one. A passing run without
9092    // confirmed recompilation would have certified nothing.
9093    //
9094    // WHAT THIS DOES **NOT** ESTABLISH, stated because the next reader will be
9095    // wiring this into a decode path: these are tests of the MASK BYTES ONLY.
9096    // No decode has yet been run with a windowed mask, so "a single-mask decode
9097    // port is silently wrong for Qwen2/Qwen3/Qwen3Moe" remains an INFERENCE
9098    // from the mask's meaning — argued from the fact that one
9099    // `[1, 1, seq, max_seq_len]` Const cannot express two layer groups. The
9100    // logits-level born-red against Qwen2's live mixed config
9101    // (`use_sliding_window: true, max_window_layers: 1` over 2 layers,
9102    // `lazy_qwen2.rs:441-443`) is the measurement that would settle it, and it
9103    // is NOT in this change.
9104    // =======================================================================
9105}
9106
9107/// Build a [`fuel_dispatch::decode_flash::DecodeFlashSpec`] from a decode
9108/// attention region's tensor handles and offer the optimizer-owned CUDA
9109/// flash-decode arm on the shared graph.
9110///
9111/// This is the model-layer WIRING for [`fuel_dispatch::decode_flash::offer_decode_flash_arm`]: it supplies
9112/// the region's tensor handles + the live attended-prefix `k_len` (as
9113/// `Sym(attended_len_sym)`, resolved per-token via the `SymEnv`) — data the
9114/// model alone knows — while every strategic decision (the shape/dtype/config
9115/// gate, the capability gate, the CUDA pin, the `Op::Branch` construction)
9116/// stays in the dispatch layer. The region is always **causal** with no
9117/// window / softcap / ALiBi (the LlamaModel decode shape).
9118///
9119/// - `q` — the RoPE'd query (`[B, Hq, 1, D]` in decode), also the branch
9120///   diverge point;
9121/// - `k` / `v` — the post-`WriteSlice` capacity KV buffers (`[B, Hkv,
9122///   capacity, D]`);
9123/// - `decomposed_out` — the region's attention output (arm 0 / the oracle);
9124/// - `reconverge` — the sole consumer of `decomposed_out` (the merge).
9125///
9126/// Returns `Ok(None)` (graph untouched, byte-identical to today) whenever the
9127/// emitter's gate declines — f32/f64 dtype, `seq_q != 1` (prefill),
9128/// `head_dim > 128`, or a non-CUDA / kernel-absent topology. Never panics.
9129/// Translate a Fuel sliding-window width into the FA-v2 `window_size_*`
9130/// convention the CUDA flash path speaks.
9131///
9132/// Fuel's mask predicate is `j > i || j + window <= i`, i.e. query `i` attends
9133/// keys `j ∈ [i - window + 1, i]` — **`window - 1` positions LEFT and `0`
9134/// RIGHT**. FA-v2 reads `is_causal` as `window_size_right == 0 &&
9135/// window_size_left < 0`, and treats *both* bounds `>= 0` as opting into local
9136/// attention (`fuel-cuda-backend/src/flash_attn.rs::translate_window`).
9137///
9138/// `None` (a dense layer) stays `(None, None)` — plain causal, which is what
9139/// every non-windowed family wants and what the decode kernel implements.
9140///
9141/// The `w - 1` is saturating: a nonsensical `window == 0` yields `Some(0)`,
9142/// which the admissibility gate rejects along with every other window. It never
9143/// silently becomes "no window".
9144pub fn flash_window_bounds(window: Option<usize>) -> (Option<usize>, Option<usize>) {
9145    match window {
9146        None => (None, None),
9147        Some(w) => (Some(w.saturating_sub(1)), Some(0)),
9148    }
9149}
9150
9151// 11 independent graph handles (q/k/v/decomposed/reconverge node ids) plus the
9152// scalar rewrite parameters (scale, attended-len sym, window, softcap, backend
9153// capability) for one flash-decode-arm offer. Each is a distinct graph node or
9154// scalar, not a bundle-able struct. Exceeds even the raised (10) threshold.
9155pub fn offer_flash_decode_arm_for_region(
9156    graph: &fuel_graph::SharedGraph,
9157    q: fuel_graph::NodeId,
9158    k: fuel_graph::NodeId,
9159    v: fuel_graph::NodeId,
9160    decomposed_out: fuel_graph::NodeId,
9161    reconverge: fuel_graph::NodeId,
9162    softmax_scale: f32,
9163    attended_len_sym: fuel_ir::SymId,
9164    attn_window: Option<usize>,
9165    softcap: Option<f32>,
9166    cap: fuel_dispatch::decode_flash::FlashArmCapability,
9167) -> crate::Result<Option<fuel_graph::NodeId>> {
9168    use fuel_dispatch::decode_flash::{DecodeFlashSpec, offer_decode_flash_arm};
9169    let (window_size_left, window_size_right) = flash_window_bounds(attn_window);
9170    let spec = DecodeFlashSpec {
9171        q,
9172        k,
9173        v,
9174        alibi: None,
9175        softmax_scale,
9176        causal: true,
9177        // ⚠️ GAP-194: these three were hardcoded `None`. That is TRUE for
9178        // `LlamaModel` — no window, no softcap — and would be a LIE for a
9179        // windowed or softcapped family, whose arm would then attend the whole
9180        // prefix and skip the cap, silently. A guard must not encode a claim
9181        // that is false: the caller states them, and the admissibility gate
9182        // declines what `flash_decoding` cannot implement.
9183        window_size_left,
9184        window_size_right,
9185        softcap,
9186        k_len: fuel_ir::DynScalar::Sym(attended_len_sym),
9187        decomposed_out,
9188        reconverge,
9189    };
9190    let mut g = graph.write().map_err(|_| {
9191        fuel_ir::Error::Msg("graph lock poisoned during flash-arm offer".into()).bt()
9192    })?;
9193    offer_decode_flash_arm(&mut g, &spec, cap)
9194}
9195
9196pub fn apply_affine_rms_norm(x: &Tensor, gain: &Arc<[f32]>, dim: usize, eps: f64) -> Tensor {
9197    assert_eq!(
9198        gain.len(),
9199        dim,
9200        "apply_affine_rms_norm: gain length must equal dim"
9201    );
9202    let normalized = x.rms_norm_last_dim(eps).unwrap();
9203    // The gain is always stored f32 (norm gains are precision-sensitive —
9204    // see `LayerWeights::attn_norm_gain`'s doc) but must be materialized
9205    // in `x`'s dtype: under BF16-throughout decode (Phase D increment A)
9206    // `x` is BF16 and `broadcast_mul` asserts dtype equality, so an f32
9207    // gain would panic. No-op conversion for f32 activations.
9208    let gain_t = x
9209        .const_like_dtype(gain, Shape::from_dims(&[dim]), x.dtype())
9210        .expect("apply_affine_rms_norm: activation dtype must be F32 or BF16");
9211    normalized.broadcast_mul(&gain_t).unwrap()
9212}
9213
9214// ---- HuggingFace Hub and safetensors weight loading ----------------------
9215
9216/// Load a tensor by name from a `MmapedSafetensors` as a flat
9217/// `Vec<f32>`, converting from whatever dtype the file stores it in.
9218/// Handles `F32`, `F64`, `BF16`, and `F16` — the dtypes real LLaMA
9219/// weights use on disk. Returns an error for unsupported dtypes.
9220pub fn load_tensor_as_f32(
9221    st: &fuel_loaders::safetensors::MmapedSafetensors,
9222    name: &str,
9223) -> crate::Result<Vec<f32>> {
9224    use safetensors::Dtype;
9225    let view = st.get(name)?;
9226    let bytes = view.data();
9227    match view.dtype() {
9228        Dtype::F32 => {
9229            let mut out = Vec::with_capacity(bytes.len() / 4);
9230            for chunk in bytes.as_chunks::<4>().0.iter() {
9231                out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
9232            }
9233            Ok(out)
9234        }
9235        Dtype::F64 => {
9236            let mut out = Vec::with_capacity(bytes.len() / 8);
9237            for chunk in bytes.as_chunks::<8>().0.iter() {
9238                let arr: [u8; 8] = *chunk;
9239                out.push(f64::from_le_bytes(arr) as f32);
9240            }
9241            Ok(out)
9242        }
9243        Dtype::BF16 => {
9244            let mut out = Vec::with_capacity(bytes.len() / 2);
9245            for chunk in bytes.as_chunks::<2>().0.iter() {
9246                let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9247                out.push(half::bf16::from_bits(raw).to_f32());
9248            }
9249            Ok(out)
9250        }
9251        Dtype::F16 => {
9252            let mut out = Vec::with_capacity(bytes.len() / 2);
9253            for chunk in bytes.as_chunks::<2>().0.iter() {
9254                let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9255                out.push(half::f16::from_bits(raw).to_f32());
9256            }
9257            Ok(out)
9258        }
9259        other => {
9260            crate::bail!("load_tensor_as_f32: unsupported dtype {other:?} for tensor {name:?}",)
9261        }
9262    }
9263}
9264
9265/// Load a tensor by name and physically transpose it from `[out, in]`
9266/// (HuggingFace layout) to `[in, out]` (fuel-graph's layout for
9267/// `x @ W` where `W` is `[in, out]`). Linear-layer weights in HF
9268/// transformers are stored as `[out_features, in_features]`, so every
9269/// call to this function is effectively "give me that matrix as I'd
9270/// use it in `matmul`."
9271pub fn load_transposed_matrix(
9272    st: &fuel_loaders::safetensors::MmapedSafetensors,
9273    name: &str,
9274    out_features: usize,
9275    in_features: usize,
9276) -> crate::Result<Vec<f32>> {
9277    let flat = load_tensor_as_f32(st, name)?;
9278    if flat.len() != out_features * in_features {
9279        crate::bail!(
9280            "load_transposed_matrix: tensor {name:?} has {} elements, expected {} ({out_features} × {in_features})",
9281            flat.len(),
9282            out_features * in_features,
9283        );
9284    }
9285    // HF layout: flat[i * in_features + j] is W[i, j] for (out i, in j).
9286    // Target layout: out[j * out_features + i] so that indexing `[j, i]`
9287    // in row-major gives the same W[i, j] — i.e. out has shape [in, out].
9288    let mut out = vec![0.0_f32; out_features * in_features];
9289    for i in 0..out_features {
9290        for j in 0..in_features {
9291            out[j * out_features + i] = flat[i * in_features + j];
9292        }
9293    }
9294    Ok(out)
9295}
9296
9297/// Transposed-matrix loader that preserves source dtype. For
9298/// safetensors files saved with bf16 weights, returns
9299/// `WeightStorage::BF16` and never materializes an f32 copy — the
9300/// 2× memory saving vs `load_transposed_matrix` comes from here.
9301/// f32 and other source dtypes still go through the f32 upcast path
9302/// for safety; extending this to preserve f16 is a one-line change
9303/// when a consumer wants it.
9304///
9305/// The transpose itself is done in whatever dtype we're keeping:
9306/// read bf16 elements from the file, place them in the transposed
9307/// target buffer, no conversion.
9308pub fn load_transposed_matrix_preserve_dtype(
9309    st: &fuel_loaders::safetensors::MmapedSafetensors,
9310    name: &str,
9311    out_features: usize,
9312    in_features: usize,
9313) -> crate::Result<WeightStorage> {
9314    use safetensors::Dtype;
9315    let view = st.get(name)?;
9316    let bytes = view.data();
9317    let expected = out_features * in_features;
9318    match view.dtype() {
9319        Dtype::BF16 => {
9320            if bytes.len() != expected * 2 {
9321                crate::bail!(
9322                    "load_transposed_matrix_preserve_dtype: bf16 tensor {name:?} has {} bytes, expected {}",
9323                    bytes.len(),
9324                    expected * 2,
9325                );
9326            }
9327            // Reinterpret input as [out_features, in_features] of
9328            // bf16; write transposed layout.
9329            let mut out = vec![half::bf16::ZERO; expected];
9330            for i in 0..out_features {
9331                for j in 0..in_features {
9332                    let src_off = (i * in_features + j) * 2;
9333                    let bits = u16::from_le_bytes([bytes[src_off], bytes[src_off + 1]]);
9334                    out[j * out_features + i] = half::bf16::from_bits(bits);
9335                }
9336            }
9337            Ok(WeightStorage::BF16(Arc::from(out)))
9338        }
9339        _ => {
9340            // F32, F64, F16 all fall through to the f32 upcast path.
9341            // Non-f32 source types still benefit from being readable;
9342            // they just lose the "weights stay compact" win.
9343            let flat = load_transposed_matrix(st, name, out_features, in_features)?;
9344            Ok(WeightStorage::F32(Arc::from(flat)))
9345        }
9346    }
9347}
9348
9349/// Sampling strategy for decode loops.
9350#[derive(Debug, Clone, Copy, Default)]
9351pub enum SamplingStrategy {
9352    /// Greedy: always pick the highest-probability token.
9353    #[default]
9354    Greedy,
9355    /// Temperature-scaled sampling with a deterministic seed. `temp`
9356    /// is the softmax temperature (`1.0` is unscaled, `0.0` is
9357    /// effectively greedy, higher values spread probability mass).
9358    /// The seed makes sampling reproducible.
9359    Temperature { temp: f32, seed: u64 },
9360}
9361
9362/// Pick the next token from a logits vector using the configured
9363/// sampling strategy. Pulled out of `generate` so both the cached and
9364/// future non-cached callers can share it.
9365pub fn sample_logits(logits: &[f32], strategy: SamplingStrategy, rng_state: &mut u64) -> u32 {
9366    match strategy {
9367        SamplingStrategy::Greedy => {
9368            let (i, _) = logits
9369                .iter()
9370                .enumerate()
9371                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
9372                .expect("sample_logits: empty logits");
9373            i as u32
9374        }
9375        SamplingStrategy::Temperature { temp, .. } => {
9376            // Stable softmax over optionally temperature-scaled logits,
9377            // then a deterministic multinomial draw.
9378            let inv_temp = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9379            let scaled: Vec<f32> = logits.iter().map(|&x| x * inv_temp).collect();
9380            let max = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9381            let exp: Vec<f32> = scaled.iter().map(|&x| (x - max).exp()).collect();
9382            let sum: f32 = exp.iter().sum();
9383            let probs: Vec<f32> = exp.iter().map(|&x| x / sum).collect();
9384            sample_multinomial(&probs, rng_state)
9385        }
9386    }
9387}
9388
9389// ---- Speculative-decoding helpers ---------------------------------------
9390//
9391// Shared by `generate_streaming_spec_with_kv_context`'s draft / accept
9392// phases. All host-side: spec decode's accept rule operates on logits
9393// vectors already downloaded from the device.
9394
9395/// Greedy argmax over a logits row.
9396pub fn spec_argmax(logits: &[f32]) -> u32 {
9397    let mut best = 0;
9398    let mut best_v = logits[0];
9399    for (i, &v) in logits.iter().enumerate().skip(1) {
9400        if v > best_v {
9401            best_v = v;
9402            best = i;
9403        }
9404    }
9405    best as u32
9406}
9407
9408/// Temperature-scaled softmax. Returns normalized probabilities.
9409pub fn spec_softmax_temp(logits: &[f32], temp: f32) -> Vec<f32> {
9410    let inv_t = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9411    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9412    let exp: Vec<f32> = logits.iter().map(|&x| ((x - max) * inv_t).exp()).collect();
9413    let sum: f32 = exp.iter().sum();
9414    exp.iter().map(|&x| x / sum).collect()
9415}
9416
9417/// Advance a deterministic LCG and return a u01 uniform.
9418pub fn spec_next_u01(state: &mut u64) -> f32 {
9419    *state = state
9420        .wrapping_mul(6364136223846793005)
9421        .wrapping_add(1442695040888963407);
9422    (*state >> 32) as f32 / u32::MAX as f32
9423}
9424
9425/// Sample a category from a distribution summing to ~1.
9426pub fn spec_sample_cat(probs: &[f32], state: &mut u64) -> u32 {
9427    let u = spec_next_u01(state);
9428    let mut cum = 0.0_f32;
9429    for (i, &p) in probs.iter().enumerate() {
9430        cum += p;
9431        if u <= cum {
9432            return i as u32;
9433        }
9434    }
9435    (probs.len() - 1) as u32
9436}
9437
9438/// Sample a categorical distribution using a small deterministic LCG.
9439/// Takes `probs` (assumed to sum to ~1) and a mutable RNG state,
9440/// returns a sampled index.
9441pub fn sample_multinomial(probs: &[f32], rng_state: &mut u64) -> u32 {
9442    // Advance the LCG and turn it into a u01 uniform.
9443    *rng_state = rng_state
9444        .wrapping_mul(6364136223846793005)
9445        .wrapping_add(1442695040888963407);
9446    let u = (*rng_state >> 32) as f32 / u32::MAX as f32;
9447    let mut cumulative = 0.0_f32;
9448    for (i, &p) in probs.iter().enumerate() {
9449        cumulative += p;
9450        if u <= cumulative {
9451            return i as u32;
9452        }
9453    }
9454    // Floating-point slop: fall through to the last index.
9455    (probs.len() - 1) as u32
9456}
9457
9458#[cfg(test)]
9459mod lora_tests {
9460    use super::*;
9461    use crate::Shape;
9462
9463    #[test]
9464    fn with_lora_matches_manual_base_plus_lora() {
9465        // Anchor graph.
9466        let in_f = 4;
9467        let out_f = 3;
9468        let rank = 2;
9469        let alpha = 8.0_f32;
9470
9471        let anchor =
9472            Tensor::from_f32(vec![0.0_f32; 1], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
9473        // Base weight [in, out].
9474        let base_vec: Vec<f32> = (0..in_f * out_f).map(|i| (i as f32) * 0.1).collect();
9475        let lora_a_vec: Vec<f32> = (0..in_f * rank).map(|i| (i as f32) * 0.05).collect();
9476        let lora_b_vec: Vec<f32> = (0..rank * out_f).map(|i| (i as f32) * 0.02).collect();
9477
9478        let ws = WeightStorage::F32(Arc::from(base_vec.clone())).with_lora(
9479            Arc::from(lora_a_vec.clone()),
9480            Arc::from(lora_b_vec.clone()),
9481            rank,
9482            alpha,
9483            in_f,
9484            out_f,
9485        );
9486
9487        // Activations x [2, in_f].
9488        let batch = 2;
9489        let x_data: Vec<f32> = (0..batch * in_f).map(|i| (i as f32) * 0.1 + 0.5).collect();
9490        let x = anchor
9491            .const_f32_like(x_data.clone(), Shape::from_dims(&[batch, in_f]))
9492            .unwrap();
9493        let y = ws.apply_linear(&x, in_f, out_f).unwrap();
9494        let got = y.realize_f32().to_vec();
9495
9496        // Reference: base + (alpha/rank) * x @ A @ B, all f32, on CPU.
9497        let mut expected = vec![0.0_f32; batch * out_f];
9498        for b in 0..batch {
9499            for j in 0..out_f {
9500                let mut acc = 0.0_f32;
9501                // Base path: sum_k x[b,k] * W[k,j].
9502                for k in 0..in_f {
9503                    acc += x_data[b * in_f + k] * base_vec[k * out_f + j];
9504                }
9505                // LoRA path: sum_r (sum_k x[b,k] * A[k,r]) * B[r,j] * (alpha/rank).
9506                let scale = alpha as f64 / rank as f64;
9507                for r in 0..rank {
9508                    let mut xar = 0.0_f32;
9509                    for k in 0..in_f {
9510                        xar += x_data[b * in_f + k] * lora_a_vec[k * rank + r];
9511                    }
9512                    acc += (xar * lora_b_vec[r * out_f + j]) * scale as f32;
9513                }
9514                expected[b * out_f + j] = acc;
9515            }
9516        }
9517
9518        for (i, (&e, &g)) in expected.iter().zip(got.iter()).enumerate() {
9519            let diff = (e - g).abs();
9520            assert!(
9521                diff <= 1e-4,
9522                "LoRA mismatch at {i}: expected {e}, got {g} (diff {diff})",
9523            );
9524        }
9525    }
9526
9527    #[test]
9528    #[should_panic(expected = "lora_a length")]
9529    fn with_lora_rejects_mismatched_a_shape() {
9530        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9531        let bad_a = Arc::from(vec![0.0_f32; 3]); // wrong
9532        let b = Arc::from(vec![0.0_f32; 6]); // 2 x 3
9533        let _ = ws.with_lora(bad_a, b, 2, 8.0, 4, 3);
9534    }
9535
9536    // ---- never-panic conversion (`apply_linear` -> `Result`) ----
9537    //
9538    // These three inputs each used to abort the process on a PRODUCTION path:
9539    // a wrong trailing dim reached `x.matmul(&w).unwrap()`, a stored/requested
9540    // shape disagreement hit `assert_eq!`, and a wrong-length bias hit a
9541    // `debug_assert_eq!` that is compiled OUT of release builds — so release
9542    // binaries broadcast a mis-sized bias with no complaint at all. All three
9543    // are checkpoint/data conditions rather than programming invariants (a
9544    // GGUF whose stored shape disagrees with the model config produces exactly
9545    // this), so all three must be `Err`.
9546
9547    #[test]
9548    fn apply_linear_errors_on_trailing_dim_mismatch() {
9549        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9550        let x = Tensor::from_f32(
9551            vec![0.0_f32; 10],
9552            Shape::from_dims(&[2, 5]), // trailing dim 5 != in_features 4
9553            &Device::cpu(),
9554        )
9555        .unwrap();
9556        let err = ws
9557            .apply_linear(&x, 4, 3)
9558            .expect_err("must not panic, must Err");
9559        let msg = err.to_string();
9560        assert!(
9561            msg.contains("trailing dim 4"),
9562            "error should name the expected trailing dim, got: {msg}",
9563        );
9564    }
9565
9566    #[test]
9567    fn apply_linear_errors_on_q4_0_stored_shape_mismatch() {
9568        // 4 x 4 of Q4_0: one 32-element block needs 18 bytes; keep the word
9569        // count self-consistent since only the shape check is under test.
9570        let ws = WeightStorage::Q4_0 {
9571            words: Arc::from(vec![0_u32; 8]),
9572            bytes_len: 18, // one Q4_0 block: 2-byte scale + 16 packed nibble bytes
9573            in_features: 4,
9574            out_features: 4,
9575        };
9576        let x =
9577            Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9578        // Trailing dim matches in_features, so this reaches the stored-shape
9579        // check rather than the contract check above.
9580        let err = ws
9581            .apply_linear(&x, 4, 7)
9582            .expect_err("must not panic, must Err");
9583        let msg = err.to_string();
9584        assert!(
9585            msg.contains("Q4_0 shape mismatch"),
9586            "error should name the stored/requested disagreement, got: {msg}",
9587        );
9588    }
9589
9590    #[test]
9591    fn apply_linear_with_bias_errors_on_wrong_bias_length() {
9592        let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); // 4 x 3
9593        let x =
9594            Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9595        let bias: Arc<[f32]> = Arc::from(vec![0.0_f32; 2]); // wrong: out_features is 3
9596        let err = ws
9597            .apply_linear_with_bias(&x, 4, 3, bias)
9598            .expect_err("wrong-length bias must Err in EVERY profile, not just debug");
9599        assert!(
9600            err.to_string().contains("bias len"),
9601            "error should name the bias length, got: {err}",
9602        );
9603    }
9604}
9605
9606#[cfg(test)]
9607mod safetensors_bridge_tests {
9608    use super::*;
9609
9610    #[test]
9611    fn from_safetensors_bytes_round_trip_f32() {
9612        // Build a tensor, serialize it as little-endian f32 bytes,
9613        // then deserialize via the bridge. Should get back the same
9614        // values.
9615        let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
9616        let mut bytes = Vec::with_capacity(original.len() * 4);
9617        for &v in &original {
9618            bytes.extend_from_slice(&v.to_le_bytes());
9619        }
9620        let t =
9621            Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::F32, &[4], &Device::cpu())
9622                .unwrap();
9623        assert_eq!(t.shape().dims(), &[4]);
9624        assert_eq!(t.dtype(), DType::F32);
9625        assert_eq!(t.realize_f32(), original);
9626    }
9627
9628    #[test]
9629    fn from_safetensors_bytes_round_trip_bf16() {
9630        let original_f32: Vec<f32> = vec![0.5, -1.0, 2.0, 4.0];
9631        let bf16_vec: Vec<half::bf16> = original_f32
9632            .iter()
9633            .map(|&v| half::bf16::from_f32(v))
9634            .collect();
9635        let mut bytes = Vec::with_capacity(bf16_vec.len() * 2);
9636        for b in &bf16_vec {
9637            bytes.extend_from_slice(&b.to_bits().to_le_bytes());
9638        }
9639        let t =
9640            Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::BF16, &[4], &Device::cpu())
9641                .unwrap();
9642        assert_eq!(t.dtype(), DType::BF16);
9643        // Values that round-trip exactly through bf16 should come back
9644        // unchanged.
9645        let realized = t.realize_bf16();
9646        assert_eq!(realized, bf16_vec);
9647    }
9648
9649    #[test]
9650    fn from_safetensors_bytes_rejects_wrong_byte_count() {
9651        // Shape says 3 elements, but we pass 4 bytes (1 f32 = 4 bytes
9652        // so 3 elements would need 12 bytes).
9653        let bad_bytes = vec![0_u8; 4];
9654        let result = Tensor::from_safetensors_bytes(
9655            &bad_bytes,
9656            safetensors::Dtype::F32,
9657            &[3],
9658            &Device::cpu(),
9659        );
9660        assert!(result.is_err());
9661    }
9662}
9663
9664// ============================================================================
9665// Phase A.1 wrapper smoke tests.
9666//
9667// Pure pass-through tests: realize and assert the returned tensor has the
9668// expected shape / dtype / values. The graph-level ops are tested in
9669// `fuel-graph`; here we only verify that the Tensor wrappers don't
9670// drop information or mis-thread arguments.
9671// ============================================================================
9672#[cfg(test)]
9673mod phase_a1_wrapper_tests {
9674    use super::*;
9675
9676    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
9677        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
9678    }
9679
9680    #[test]
9681    fn unsqueeze_adds_size_one_dim() {
9682        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9683        let out = t.unsqueeze(0_usize).unwrap();
9684        assert_eq!(out.shape().dims(), &[1, 2, 2]);
9685        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
9686    }
9687
9688    #[test]
9689    fn unsqueeze_errors_out_of_bounds() {
9690        let t = cpu_f32(vec![1.0, 2.0], &[2]);
9691        // rank=1, so dim<=1 is valid; dim=2 must error.
9692        assert!(t.unsqueeze(0_usize).is_ok());
9693        assert!(t.unsqueeze(1_usize).is_ok());
9694        assert!(t.unsqueeze(2_usize).is_err());
9695    }
9696
9697    #[test]
9698    fn try_reshape_errors_on_size_mismatch() {
9699        let t = cpu_f32(vec![1.0; 6], &[2, 3]);
9700        assert!(t.reshape(vec![3, 2]).is_ok());
9701        assert!(t.reshape(vec![2, 2]).is_err());
9702    }
9703
9704    #[test]
9705    fn try_permute_validates_axes() {
9706        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
9707        // The Dims trait accepts tuples, owned arrays, and slices.
9708        assert!(t.permute((2_usize, 0_usize, 1_usize)).is_ok());
9709        assert!(t.permute([2_usize, 0, 1]).is_ok());
9710        assert!(t.permute([0_usize, 1]).is_err()); // wrong rank
9711        assert!(t.permute([0_usize, 0, 1]).is_err()); // dup axis
9712    }
9713
9714    #[test]
9715    fn try_transpose_requires_rank_two_plus() {
9716        let scalar = cpu_f32(vec![1.0], &[1]);
9717        // rank-1: transpose surfaces a typed error at build time.
9718        let _ = scalar.transpose();
9719    }
9720
9721    #[test]
9722    fn triu_tril_shape_preserved() {
9723        let t = cpu_f32(vec![1.0; 9], &[3, 3]);
9724        let upper = t.triu(0).unwrap();
9725        let lower = t.tril(0).unwrap();
9726        assert_eq!(upper.shape().dims(), &[3, 3]);
9727        assert_eq!(lower.shape().dims(), &[3, 3]);
9728        // tril(0) of all-ones: 1s on/below diagonal, 0s above
9729        assert_eq!(
9730            lower.realize_f32(),
9731            vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0]
9732        );
9733    }
9734
9735    #[test]
9736    fn triu_rejects_rank_one() {
9737        let t = cpu_f32(vec![1.0, 2.0], &[2]);
9738        assert!(t.triu(0).is_err());
9739    }
9740
9741    #[test]
9742    fn log_softmax_last_dim_shape_preserved() {
9743        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9744        let out = t.log_softmax_last_dim().unwrap();
9745        assert_eq!(out.shape().dims(), &[2, 2]);
9746        // log_softmax values must be <= 0.
9747        for v in out.realize_f32() {
9748            assert!(v <= 0.0 + 1e-6, "log_softmax produced positive value: {v}");
9749        }
9750    }
9751
9752    #[test]
9753    fn softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9754        // shape [2, 3, 2] — softmax along axis=1 (the size-3 axis).
9755        // Layout (row-major): element (b, r, c) lives at b*6 + r*2 + c.
9756        // We hand-pick values so each (b, c) lane's max is exactly 0 to
9757        // make the reference closed-form: probs = exp(x) / sum_r exp(x).
9758        // Lane (b=0, c=0): values [-1, 0, -2]  → exp = [e^-1, 1, e^-2]
9759        // Lane (b=0, c=1): values [ 0,-3, -1]  → exp = [1, e^-3, e^-1]
9760        // Lane (b=1, c=0): values [-2,-1,  0]  → exp = [e^-2, e^-1, 1]
9761        // Lane (b=1, c=1): values [-1,-1,  0]  → exp = [e^-1, e^-1, 1]
9762        let data: Vec<f32> = vec![
9763            // b=0
9764            -1.0, 0.0, // r=0, c=0..1
9765            0.0, -3.0, // r=1
9766            -2.0, -1.0, // r=2
9767            // b=1
9768            -2.0, -1.0, // r=0
9769            -1.0, -1.0, // r=1
9770            0.0, 0.0, // r=2
9771        ];
9772        let t = cpu_f32(data, &[2, 3, 2]);
9773        let out = t.softmax(1_usize).unwrap();
9774        assert_eq!(out.shape().dims(), &[2, 3, 2]);
9775        let v = out.realize_f32();
9776
9777        // Reference: closed-form softmax per (b, c) lane.
9778        let lane_softmax = |xs: [f32; 3]| -> [f32; 3] {
9779            let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9780            let exps = [(xs[0] - m).exp(), (xs[1] - m).exp(), (xs[2] - m).exp()];
9781            let s = exps[0] + exps[1] + exps[2];
9782            [exps[0] / s, exps[1] / s, exps[2] / s]
9783        };
9784        // Re-extract source lanes
9785        let lanes: [[f32; 3]; 4] = [
9786            [-1.0, 0.0, -2.0], // (b=0, c=0)
9787            [0.0, -3.0, -1.0], // (b=0, c=1)
9788            [-2.0, -1.0, 0.0], // (b=1, c=0)
9789            [-1.0, -1.0, 0.0], // (b=1, c=1)
9790        ];
9791        let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_softmax(*l)).collect();
9792
9793        // out[b, r, c] at index b*6 + r*2 + c — verify each (b, c) lane.
9794        for b in 0..2 {
9795            for c in 0..2 {
9796                let lane_ix = b * 2 + c;
9797                for r in 0..3 {
9798                    let got = v[b * 6 + r * 2 + c];
9799                    let want = refs[lane_ix][r];
9800                    assert!(
9801                        (got - want).abs() < 1e-6,
9802                        "softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9803                    );
9804                }
9805                // sanity: lane sums to 1
9806                let sum: f32 = (0..3).map(|r| v[b * 6 + r * 2 + c]).sum();
9807                assert!((sum - 1.0).abs() < 1e-6, "lane (b={b},c={c}) sums to {sum}");
9808            }
9809        }
9810    }
9811
9812    #[test]
9813    fn softmax_last_axis_matches_softmax_last_dim() {
9814        let data: Vec<f32> = vec![
9815            1.0, 2.0, -1.0, 0.5, 0.0, -2.0, 3.0, 1.5, // batch dim 2
9816            4.0, -1.0, 2.0, 0.0, -3.0, 0.25, 0.75, 1.0,
9817        ];
9818        let t = cpu_f32(data, &[2, 2, 4]);
9819        let via_general = t.softmax(2_usize).unwrap();
9820        let via_fused = t.softmax_last_dim().unwrap();
9821        assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9822        let g = via_general.realize_f32();
9823        let f = via_fused.realize_f32();
9824        assert_eq!(g.len(), f.len());
9825        for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9826            assert!(
9827                (a - b).abs() < 1e-6,
9828                "softmax (general axis=2) vs softmax_last_dim diverge at {i}: {a} vs {b}",
9829            );
9830        }
9831    }
9832
9833    #[test]
9834    fn log_softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9835        // Same construction as the softmax test, but compare against
9836        // closed-form log_softmax per (b, c) lane.
9837        let data: Vec<f32> = vec![
9838            -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0,
9839        ];
9840        let t = cpu_f32(data, &[2, 3, 2]);
9841        let out = t.log_softmax(1_usize).unwrap();
9842        assert_eq!(out.shape().dims(), &[2, 3, 2]);
9843        let v = out.realize_f32();
9844
9845        let lane_log_softmax = |xs: [f32; 3]| -> [f32; 3] {
9846            let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9847            let shifted = [xs[0] - m, xs[1] - m, xs[2] - m];
9848            let lse = (shifted[0].exp() + shifted[1].exp() + shifted[2].exp()).ln();
9849            [shifted[0] - lse, shifted[1] - lse, shifted[2] - lse]
9850        };
9851        let lanes: [[f32; 3]; 4] = [
9852            [-1.0, 0.0, -2.0],
9853            [0.0, -3.0, -1.0],
9854            [-2.0, -1.0, 0.0],
9855            [-1.0, -1.0, 0.0],
9856        ];
9857        let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_log_softmax(*l)).collect();
9858
9859        for b in 0..2 {
9860            for c in 0..2 {
9861                let lane_ix = b * 2 + c;
9862                for r in 0..3 {
9863                    let got = v[b * 6 + r * 2 + c];
9864                    let want = refs[lane_ix][r];
9865                    assert!(
9866                        (got - want).abs() < 1e-6,
9867                        "log_softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9868                    );
9869                    // log_softmax values must be <= 0
9870                    assert!(got <= 1e-6, "log_softmax produced positive value: {got}");
9871                }
9872            }
9873        }
9874    }
9875
9876    #[test]
9877    fn log_softmax_last_axis_matches_log_softmax_last_dim() {
9878        let data: Vec<f32> = vec![
9879            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,
9880        ];
9881        let t = cpu_f32(data, &[2, 2, 4]);
9882        let via_general = t.log_softmax(2_usize).unwrap();
9883        let via_fused = t.log_softmax_last_dim().unwrap();
9884        assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9885        let g = via_general.realize_f32();
9886        let f = via_fused.realize_f32();
9887        assert_eq!(g.len(), f.len());
9888        for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9889            assert!(
9890                (a - b).abs() < 1e-6,
9891                "log_softmax (general axis=2) vs log_softmax_last_dim diverge at {i}: {a} vs {b}",
9892            );
9893        }
9894    }
9895
9896    #[test]
9897    fn argmin_dim_drops_reduced_axis() {
9898        let t = cpu_f32(vec![3.0, 1.0, 2.0, 0.5, 5.0, 4.0], &[2, 3]);
9899        let out = t.argmin_dim(1_usize).unwrap();
9900        assert_eq!(out.shape().dims(), &[2]);
9901        assert_eq!(out.dtype(), DType::U32);
9902        assert_eq!(out.realize_u32(), vec![1, 0]);
9903    }
9904
9905    #[test]
9906    fn masked_fill_smoke() {
9907        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9908        // Comparison ops produce Bool masks directly (GAP-168(c)) — masked_fill
9909        // now accepts Bool, no F32→mask cast needed.
9910        let probe = t
9911            .const_f32_like(vec![0.0, 1.0, 1.0, 0.0], vec![2, 2])
9912            .unwrap();
9913        let threshold = t.const_f32_like(vec![0.5; 4], vec![2, 2]).unwrap();
9914        let mask = probe.gt(&threshold).unwrap(); // [0, 1, 1, 0] as Bool
9915        let out = t.masked_fill(&mask, fuel_ir::Scalar::F32(-9.0)).unwrap();
9916        assert_eq!(out.realize_f32(), vec![1.0, -9.0, -9.0, 4.0]);
9917    }
9918
9919    /// GAP-168(c) DISCRIMINATION: `Bool` and `U8` are byte-identical in storage,
9920    /// which is exactly the condition under which a wrong wiring is *silently
9921    /// correct*. This test proves they are NOT interchangeable end-to-end on CPU:
9922    /// a comparison yields a `Bool` (not `U8`) tensor, and `to_dtype(Bool)` on a
9923    /// `U8` tensor is a real `!= 0` CONVERSION — never a byte reinterpret.
9924    #[test]
9925    fn bool_is_distinguishable_from_u8_end_to_end() {
9926        // A comparison realizes end-to-end as a Bool tensor (NOT U8) whose bytes
9927        // are 0/1 — the byte-identity hazard's most likely hiding spot, closed:
9928        // dtype tag is Bool, the CPU kernel dispatches on [F32,F32,Bool] (its FKC
9929        // contract declares fixed(BOOL)), and Copy[Bool] materializes it to host.
9930        let t = cpu_f32(vec![0.0, 5.0, 3.0, 0.0], &[4]);
9931        let thr = t.const_f32_like(vec![0.5; 4], vec![4]).unwrap();
9932        let mask = t.gt(&thr).unwrap();
9933        assert_eq!(mask.dtype(), DType::Bool, "gt yields Bool, not U8");
9934        assert_eq!(mask.realize_u8(), vec![0, 1, 1, 0], "mask bytes are 0/1");
9935
9936        // ---- The cast discrimination, now that the 22 Bool pairs are wired ----
9937        //
9938        // NEGATIVE CONTROL FIRST, or the assertion below is vacuous: build a U8
9939        // tensor that genuinely holds 5. If this line ever came back as 0/1, the
9940        // real test would pass for the wrong reason — it would be comparing 0/1
9941        // against 0/1 and could not tell a conversion from a reinterpret.
9942        let u8_vals = t
9943            .const_f32_like(vec![0.0, 5.0, 3.0, 0.0], vec![4])
9944            .unwrap()
9945            .to_dtype(DType::U8)
9946            .unwrap();
9947        assert_eq!(u8_vals.dtype(), DType::U8);
9948        assert_eq!(
9949            u8_vals.realize_u8(),
9950            vec![0, 5, 3, 0],
9951            "negative control: the U8 source must really hold 5 and 3, else the \
9952             conversion assertion below cannot discriminate"
9953        );
9954
9955        // THE DISCRIMINATION: U8 -> Bool is a real `!= 0` conversion. A byte
9956        // reinterpret would yield [0, 5, 3, 0] — byte-identical storage makes
9957        // that the silently-correct-looking failure. A conversion yields 0/1.
9958        let as_bool = u8_vals.to_dtype(DType::Bool).unwrap();
9959        assert_eq!(as_bool.dtype(), DType::Bool);
9960        assert_eq!(
9961            as_bool.realize_u8(),
9962            vec![0, 1, 1, 0],
9963            "U8->Bool must CONVERT (5 -> 1), not reinterpret (5 -> 5)"
9964        );
9965
9966        // And back out: Bool -> F32 is exactly 0.0/1.0.
9967        assert_eq!(
9968            as_bool.to_dtype(DType::F32).unwrap().realize_f32(),
9969            vec![0.0, 1.0, 1.0, 0.0],
9970            "Bool->F32 yields 0.0/1.0"
9971        );
9972        // Bool -> U8 keeps the canonical 0/1 (this direction IS byte-preserving,
9973        // which is fine — it is the OTHER direction that must not be).
9974        assert_eq!(
9975            mask.to_dtype(DType::U8).unwrap().realize_u8(),
9976            vec![0, 1, 1, 0]
9977        );
9978
9979        // FINALLY, the two are not INTERCHANGEABLE at the API, which is what
9980        // stops a numeric mask being coerced silently: `masked_fill` requires a
9981        // Bool mask and rejects U8 at GRAPH-BUILD time, so the cast is explicit
9982        // at the call site rather than implied.
9983        let err = t.masked_fill(&u8_vals, fuel_ir::Scalar::F32(-9.0));
9984        assert!(
9985            err.is_err(),
9986            "a U8 mask must be REJECTED by masked_fill — Bool and U8 are \
9987             byte-identical, so accepting it would 'work' and erase the distinction"
9988        );
9989    }
9990
9991    /// GAP-183: `float -> Bool` is lowered in the optimizer to `Ne(x, x*0)` so it
9992    /// reaches CUDA's shipped `ne_{f32,f64,f16,bf16}` kernels, which a
9993    /// storage-level cast cannot compose. This pins the VALUES end-to-end, and
9994    /// the three interesting inputs are the whole point of the test — a lowering
9995    /// that got only the ordinary cases right would still be wrong:
9996    ///
9997    ///   -0.0  -> FALSE   (IEEE `-0.0 == 0.0`)
9998    ///   NaN   -> TRUE    (matches PyTorch `.bool()`)
9999    ///   ±inf  -> TRUE
10000    ///
10001    /// The zeros operand is itself NaN for non-finite x; that is harmless because
10002    /// `x != NaN` and `x != 0` agree for every non-finite x. This test is what
10003    /// would catch it if that ever stopped being true.
10004    #[test]
10005    fn float_to_bool_lowering_preserves_ieee_semantics() {
10006        let t = cpu_f32(
10007            vec![
10008                0.0,
10009                -0.0,
10010                5.0,
10011                -3.0,
10012                f32::NAN,
10013                f32::INFINITY,
10014                f32::NEG_INFINITY,
10015            ],
10016            &[7],
10017        );
10018        let b = t.to_dtype(DType::Bool).unwrap();
10019        assert_eq!(b.dtype(), DType::Bool, "to_dtype(Bool) still yields Bool");
10020        assert_eq!(
10021            b.realize_u8(),
10022            vec![0, 0, 1, 1, 1, 1, 1],
10023            "0 and -0.0 are false; every other value including NaN and both \
10024             infinities is true",
10025        );
10026    }
10027
10028    #[test]
10029    fn index_add_smoke() {
10030        let base = cpu_f32(vec![1.0, 1.0, 1.0, 1.0], &[2, 2]);
10031        let src = base
10032            .const_f32_like(vec![10.0, 20.0, 30.0, 40.0], vec![2, 2])
10033            .unwrap();
10034        let indices = base.const_u32_like(vec![0_u32, 0_u32], vec![2]).unwrap();
10035        let out = base.index_add(0, &indices, &src).unwrap();
10036        assert_eq!(out.shape().dims(), &[2, 2]);
10037        // both src rows added to row 0; row 1 unchanged
10038        let v = out.realize_f32();
10039        assert_eq!(v[0], 41.0); // 1 + 10 + 30
10040        assert_eq!(v[1], 61.0); // 1 + 20 + 40
10041        assert_eq!(v[2], 1.0);
10042        assert_eq!(v[3], 1.0);
10043    }
10044
10045    #[test]
10046    fn scatter_add_smoke() {
10047        let base = cpu_f32(vec![0.0, 0.0, 0.0, 0.0], &[2, 2]);
10048        let src = base
10049            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10050            .unwrap();
10051        let indices = base
10052            .const_u32_like(vec![0_u32, 1_u32, 1_u32, 0_u32], vec![2, 2])
10053            .unwrap();
10054        let out = base.scatter_add(0, &indices, &src).unwrap();
10055        assert_eq!(out.shape().dims(), &[2, 2]);
10056    }
10057
10058    #[test]
10059    fn inplace_activations_compile_and_run() {
10060        let t = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]);
10061        // Each in-place op is destructive, so chain through fresh tensors.
10062        let r = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).relu_inplace();
10063        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).silu_inplace();
10064        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).gelu_tanh_inplace();
10065        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).tanh_inplace();
10066        let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).sigmoid_inplace();
10067        let _ = t.affine_inplace(2.0, 1.0);
10068        // Spot-check the relu output.
10069        let v = r.realize_f32();
10070        assert_eq!(v, vec![0.0, 0.5, 0.0, 2.0]);
10071    }
10072
10073    #[test]
10074    fn const_f64_like_round_trips() {
10075        let anchor = cpu_f32(vec![0.0], &[1]);
10076        let t = anchor.const_f64_like(vec![1.5, 2.5, 3.5], vec![3]).unwrap();
10077        assert_eq!(t.shape().dims(), &[3]);
10078        assert_eq!(t.dtype(), DType::F64);
10079        assert_eq!(t.realize_f64(), vec![1.5, 2.5, 3.5]);
10080    }
10081
10082    #[test]
10083    fn const_i64_like_round_trips() {
10084        let anchor = cpu_f32(vec![0.0], &[1]);
10085        let t = anchor.const_i64_like(vec![-1_i64, 2, -3], vec![3]).unwrap();
10086        assert_eq!(t.shape().dims(), &[3]);
10087        assert_eq!(t.dtype(), DType::I64);
10088    }
10089
10090    #[test]
10091    fn on_device_smoke_cpu() {
10092        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10093        let pinned = t.on_device(&Device::cpu());
10094        assert_eq!(pinned.realize_f32(), vec![1.0, 2.0]);
10095    }
10096
10097    #[test]
10098    fn copy_to_device_same_device_round_trips() {
10099        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10100        let copied = t.copy_to_device(&Device::cpu());
10101        assert_eq!(copied.realize_f32(), vec![1.0, 2.0, 3.0]);
10102    }
10103}
10104
10105// ============================================================================
10106// Phase A.2 composite primitives tests.
10107// ============================================================================
10108#[cfg(test)]
10109mod phase_a2_composite_tests {
10110    use super::*;
10111
10112    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10113        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10114    }
10115
10116    #[test]
10117    fn transpose_last_two_swaps_last_two_dims() {
10118        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10119        let out = t.transpose_last_two().unwrap();
10120        assert_eq!(out.shape().dims(), &[3, 2]);
10121        assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10122    }
10123
10124    #[test]
10125    fn t_is_alias_of_transpose_last_two() {
10126        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10127        assert_eq!(
10128            t.t().unwrap().realize_f32(),
10129            t.transpose_last_two().unwrap().realize_f32()
10130        );
10131    }
10132
10133    #[test]
10134    fn transpose_dims_swaps_arbitrary_axes() {
10135        let t = cpu_f32((0..24).map(|i| i as f32).collect(), &[2, 3, 4]);
10136        let out = t.transpose_dims(0, 2).unwrap();
10137        assert_eq!(out.shape().dims(), &[4, 3, 2]);
10138    }
10139
10140    #[test]
10141    fn transpose_dims_identity_when_same_axis() {
10142        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10143        let out = t.transpose_dims(0, 0).unwrap();
10144        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
10145    }
10146
10147    #[test]
10148    fn transpose_dims_errors_out_of_bounds() {
10149        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10150        assert!(t.transpose_dims(0, 2).is_err());
10151        assert!(t.transpose_dims(5, 0).is_err());
10152    }
10153
10154    #[test]
10155    fn flatten_merges_middle_dims() {
10156        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10157        let out = t.flatten(0, 1).unwrap();
10158        assert_eq!(out.shape().dims(), &[6, 4]);
10159    }
10160
10161    #[test]
10162    fn flatten_to_merges_leading_dims() {
10163        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10164        let out = t.flatten_to(1).unwrap();
10165        assert_eq!(out.shape().dims(), &[6, 4]);
10166    }
10167
10168    #[test]
10169    fn flatten_from_merges_trailing_dims() {
10170        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10171        let out = t.flatten_from(1).unwrap();
10172        assert_eq!(out.shape().dims(), &[2, 12]);
10173    }
10174
10175    #[test]
10176    fn flatten_all_produces_rank_one() {
10177        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10178        let out = t.flatten_all().unwrap();
10179        assert_eq!(out.shape().dims(), &[6]);
10180        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10181    }
10182
10183    /// `rope_batched` at `B == 1` must be **bit-identical** to
10184    /// `rope_with_tables_decomposed`, and each row at `B == 3` must equal the
10185    /// single-position result at that row's own position — the exactness proof
10186    /// for the per-row RoPE that unblocks ragged (non-uniform-position) batched
10187    /// decode. A subtly wrong rotate-half (wrong half split, wrong sign, wrong
10188    /// concat order) still produces plausible tokens, so this asserts
10189    /// bit-equality; the CONTROL proves the per-row table is load-bearing.
10190    /// (Adopted from the Lightbulb consumer's verified `rope_batched` oracle;
10191    /// the mechanism belongs in Fuel.)
10192    #[test]
10193    fn rope_batched_matches_single_position_rope_row_by_row() {
10194        let lcg = |n: usize, seed: u32| -> Vec<f32> {
10195            let mut s = seed;
10196            (0..n)
10197                .map(|_| {
10198                    s = s.wrapping_mul(1103515245).wrapping_add(12345);
10199                    ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
10200                })
10201                .collect()
10202        };
10203        let (b, heads, hd) = (3usize, 4usize, 8usize);
10204        let base = 10000.0f64;
10205        let positions = [0usize, 7, 3];
10206
10207        let x_data = lcg(b * heads * hd, 11);
10208        let x = cpu_f32(x_data.clone(), &[b, heads, 1, hd]);
10209        let (cos, sin) = x.rope_tables_const_batched(base, &positions, hd);
10210        let got = x.rope_batched(&cos, &sin).unwrap().realize_f32();
10211
10212        let per_row = heads * hd;
10213        for (row, &pos) in positions.iter().enumerate() {
10214            let row_data = x_data[row * per_row..(row + 1) * per_row].to_vec();
10215            let xr = cpu_f32(row_data, &[1, heads, 1, hd]);
10216            let (c, s) = xr.rope_tables_const(base, pos, 1, hd);
10217            let want = xr
10218                .rope_with_tables_decomposed(&c, &s)
10219                .unwrap()
10220                .realize_f32();
10221            assert_eq!(
10222                &got[row * per_row..(row + 1) * per_row],
10223                &want[..],
10224                "row {row} (position {pos}) != decomposed RoPE at that position",
10225            );
10226        }
10227
10228        // CONTROL: prove the per-row table is load-bearing. Give EVERY row
10229        // position 0 (row 0's position); rows at other positions must now
10230        // DISAGREE with their own-position result — else the comparison above is
10231        // blind to the whole per-row-position mechanism.
10232        let shared = [positions[0]; 3];
10233        let (c0, s0) = x.rope_tables_const_batched(base, &shared, hd);
10234        let wrong = x.rope_batched(&c0, &s0).unwrap().realize_f32();
10235        for (row, &pos) in positions.iter().enumerate().skip(1) {
10236            assert_ne!(
10237                &wrong[row * per_row..(row + 1) * per_row],
10238                &got[row * per_row..(row + 1) * per_row],
10239                "CONTROL: row {row} at shared position 0 matched its own position \
10240                 {pos} — per-row RoPE not applied, or comparison blind to it",
10241            );
10242        }
10243    }
10244
10245    #[test]
10246    fn flatten_errors_on_bad_range() {
10247        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10248        assert!(t.flatten(0, 5).is_err());
10249        assert!(t.flatten(2, 1).is_err()); // start > end
10250    }
10251
10252    #[test]
10253    fn split_heads_then_merge_heads_round_trip() {
10254        // (B=1, N=2, embed=6) — split into 2 heads of head_dim=3.
10255        let x = cpu_f32((0..12).map(|i| i as f32).collect(), &[1, 2, 6]);
10256        let split = x.split_heads(2, 3).unwrap();
10257        assert_eq!(split.shape().dims(), &[1, 2, 2, 3]);
10258        let merged = split.merge_heads().unwrap();
10259        assert_eq!(merged.shape().dims(), &[1, 2, 6]);
10260        let m = merged.realize_f32();
10261        let original = x.realize_f32();
10262        for (a, b) in m.iter().zip(original.iter()) {
10263            assert!((a - b).abs() < 1e-7, "{a} vs {b}");
10264        }
10265    }
10266
10267    #[test]
10268    fn rope_tables_const_shapes_and_anchoring() {
10269        let anchor = cpu_f32(vec![0.0_f32; 8], &[1, 4, 2]);
10270        let (cos, sin) = anchor.rope_tables_const(10_000.0, 0, 4, 8);
10271        assert_eq!(cos.shape().dims(), &[4, 8]);
10272        assert_eq!(sin.shape().dims(), &[4, 8]);
10273        // First row at position 0: cos = 1.0, sin = 0.0 for every pair.
10274        let cv = cos.realize_f32();
10275        let sv = sin.realize_f32();
10276        // cos[0,...] should all be 1.0 (RoPE at position 0).
10277        for i in 0..8 {
10278            assert!((cv[i] - 1.0).abs() < 1e-5, "cos[0,{i}] = {}", cv[i]);
10279            assert!(sv[i].abs() < 1e-5, "sin[0,{i}] = {}", sv[i]);
10280        }
10281    }
10282
10283    #[test]
10284    fn embed_tokens_shape_and_lookup() {
10285        // 5-token vocab, 3-dim hidden. Vocab embedding table contains
10286        // row i = (i, i+0.5, i+1) so the lookup result is verifiable.
10287        let vocab_size = 5;
10288        let hidden = 3;
10289        let table: Vec<f32> = (0..vocab_size)
10290            .flat_map(|i| vec![i as f32, i as f32 + 0.5, i as f32 + 1.0])
10291            .collect();
10292        let tokens = vec![1_u32, 3, 0];
10293        let out = Tensor::embed_tokens(
10294            std::sync::Arc::from(table),
10295            vocab_size,
10296            hidden,
10297            &tokens,
10298            &crate::Device::cpu(),
10299        )
10300        .unwrap();
10301        assert_eq!(out.shape().dims(), &[1, 3, hidden]);
10302        let v = out.realize_f32();
10303        let want = [
10304            1.0_f32, 1.5, 2.0, // token 1
10305            3.0, 3.5, 4.0, // token 3
10306            0.0, 0.5, 1.0, // token 0
10307        ];
10308        for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10309            assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10310        }
10311    }
10312
10313    #[test]
10314    fn embed_tokens_anchored_lives_on_receiver_graph() {
10315        // Two paths: one bootstrapped via embed_tokens, one anchored
10316        // on a pre-existing tensor. Both produce identical values,
10317        // but only the anchored one can compose with the anchor.
10318        let vocab_size = 4;
10319        let hidden = 2;
10320        let table: Vec<f32> = (0..vocab_size)
10321            .flat_map(|i| vec![i as f32, i as f32 * 2.0])
10322            .collect();
10323        let table_arc: std::sync::Arc<[f32]> = std::sync::Arc::from(table);
10324        let tokens = vec![2_u32, 1];
10325
10326        let anchor = cpu_f32(vec![0.0_f32], &[1]);
10327        let embedded = anchor
10328            .embed_tokens_anchored(
10329                std::sync::Arc::clone(&table_arc),
10330                vocab_size,
10331                hidden,
10332                &tokens,
10333            )
10334            .unwrap();
10335        assert_eq!(embedded.shape().dims(), &[1, 2, hidden]);
10336
10337        // Anchored: composes with the anchor.
10338        let one_scaled = anchor
10339            .const_f32_like(std::sync::Arc::from(vec![1.0_f32]), Shape::from_dims(&[1]))
10340            .unwrap();
10341        let _ = embedded
10342            .add(
10343                &one_scaled
10344                    .reshape(Shape::from_dims(&[1, 1, 1]))
10345                    .unwrap()
10346                    .broadcast_to(Shape::from_dims(&[1, 2, hidden]))
10347                    .unwrap(),
10348            )
10349            .unwrap();
10350        let v = embedded.realize_f32();
10351        let want = [
10352            2.0_f32, 4.0, // token 2
10353            1.0, 2.0, // token 1
10354        ];
10355        for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10356            assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10357        }
10358    }
10359
10360    #[test]
10361    fn embed_tokens_empty_returns_error() {
10362        let r = Tensor::embed_tokens(
10363            std::sync::Arc::from(vec![0.0_f32]),
10364            1,
10365            1,
10366            &[],
10367            &crate::Device::cpu(),
10368        );
10369        assert!(r.is_err());
10370    }
10371
10372    #[test]
10373    fn softcap_matches_tanh_form() {
10374        // Tiny input; verify cap·tanh(x/cap) at known points.
10375        let x = cpu_f32(vec![0.0_f32, 5.0, -5.0, 30.0], &[1, 4]);
10376        let capped = x.softcap(10.0).realize_f32();
10377        // tanh(0)=0; tanh(0.5)≈0.4621; tanh(-0.5)≈-0.4621; tanh(3)≈0.9951.
10378        let expect = [0.0_f32, 4.6212, -4.6212, 9.9505];
10379        for (i, (&got, &want)) in capped.iter().zip(expect.iter()).enumerate() {
10380            assert!(
10381                (got - want).abs() < 1e-3,
10382                "softcap[{i}] got={got} want={want}"
10383            );
10384        }
10385    }
10386
10387    #[test]
10388    fn softcap_optional_none_returns_input_unchanged() {
10389        let x = cpu_f32(vec![1.0_f32, -2.0, 30.0], &[1, 3]);
10390        let out = x.softcap_optional(None).realize_f32();
10391        let expect = [1.0_f32, -2.0, 30.0];
10392        for (g, w) in out.iter().zip(expect.iter()) {
10393            assert!((g - w).abs() < 1e-6);
10394        }
10395        // Some(0.0) or negative cap also returns unchanged (guard).
10396        let out = x.softcap_optional(Some(0.0)).realize_f32();
10397        for (g, w) in out.iter().zip(expect.iter()) {
10398            assert!((g - w).abs() < 1e-6);
10399        }
10400    }
10401
10402    #[test]
10403    fn rope_partial_full_dim_matches_rope_with_tables() {
10404        // head_dim == rope_dim ⇒ should degenerate to full rope.
10405        let qk = cpu_f32(vec![1.0_f32; 1 * 1 * 2 * 4], &[1, 1, 2, 4]);
10406        let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 2, 4);
10407        let via_partial = qk.rope_partial(&cos, &sin, 4).unwrap();
10408        let via_full = qk.rope_with_tables(&cos, &sin).unwrap();
10409        let a = via_partial.realize_f32();
10410        let b = via_full.realize_f32();
10411        for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
10412            assert!((av - bv).abs() < 1e-6, "{i}: {av} vs {bv}");
10413        }
10414    }
10415
10416    #[test]
10417    fn rope_partial_pass_through_suffix_unchanged() {
10418        // rope_dim=2, head_dim=4 ⇒ first 2 features rotated, last 2
10419        // unchanged. At position 0 the rotation is identity, so all 4
10420        // features should equal the input.
10421        let qk = cpu_f32(
10422            vec![
10423                // shape [1, 1, 1, 4] — one position, one head
10424                1.0_f32, 2.0, 3.0, 4.0,
10425            ],
10426            &[1, 1, 1, 4],
10427        );
10428        let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 1, 2);
10429        let out = qk.rope_partial(&cos, &sin, 2).unwrap().realize_f32();
10430        for (i, &v) in out.iter().enumerate() {
10431            let expect = [1.0_f32, 2.0, 3.0, 4.0][i];
10432            assert!((v - expect).abs() < 1e-6, "{i}: {v} != {expect}");
10433        }
10434    }
10435
10436    #[test]
10437    fn add_optional_trailing_bias_none_returns_input_unchanged() {
10438        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10439        let original = a.realize_f32();
10440        let out = a.add_optional_trailing_bias(None).unwrap();
10441        assert_eq!(out.realize_f32(), original);
10442    }
10443
10444    #[test]
10445    fn add_optional_trailing_bias_some_applies_add() {
10446        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10447        let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, 20.0, 30.0]);
10448        let out = a.add_optional_trailing_bias(Some(&bias)).unwrap();
10449        assert_eq!(out.realize_f32(), vec![11.0, 22.0, 33.0]);
10450    }
10451
10452    #[test]
10453    fn add_trailing_bias_broadcasts_across_leading_dims() {
10454        // (2, 3) input + length-3 bias should add per-column.
10455        let x = cpu_f32(vec![1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0], &[2, 3]);
10456        let bias = std::sync::Arc::<[f32]>::from(vec![100.0_f32, 200.0, 300.0]);
10457        let out = x.add_trailing_bias(bias).unwrap();
10458        assert_eq!(out.shape().dims(), &[2, 3]);
10459        let v = out.realize_f32();
10460        assert_eq!(v, vec![101.0, 202.0, 303.0, 110.0, 220.0, 330.0]);
10461    }
10462
10463    #[test]
10464    fn rms_norm_affine_with_offset_adds_offset_to_each_gain() {
10465        let x = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10466        let gain_raw: [f32; 3] = [-0.5, 0.0, 0.5];
10467        let via_offset = x.rms_norm_affine_with_offset(&gain_raw, 1.0, 1e-6).unwrap();
10468        let gain_shifted = std::sync::Arc::<[f32]>::from(vec![0.5_f32, 1.0, 1.5]);
10469        let via_plain = x.rms_norm_affine(gain_shifted, 1e-6).unwrap();
10470        let a = via_offset.realize_f32();
10471        let b = via_plain.realize_f32();
10472        for (x, y) in a.iter().zip(b.iter()) {
10473            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
10474        }
10475    }
10476
10477    #[test]
10478    fn global_avg_pool_2d_averages_spatial_dims() {
10479        // (1, 2, 2, 3) — two channels, 2×3 spatial.
10480        // Channel 0: 1..=6, mean = 3.5.
10481        // Channel 1: 10, 20, 30, 40, 50, 60, mean = 35.
10482        let x = cpu_f32(
10483            vec![
10484                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,
10485            ],
10486            &[1, 2, 2, 3],
10487        );
10488        let out = x.global_avg_pool_2d().unwrap();
10489        assert_eq!(out.shape().dims(), &[1, 2]);
10490        let v = out.realize_f32();
10491        assert!((v[0] - 3.5).abs() < 1e-5);
10492        assert!((v[1] - 35.0).abs() < 1e-4);
10493    }
10494
10495    #[test]
10496    fn channel_affine_4d_applies_per_channel_gain_and_bias() {
10497        // (1, 2, 2, 2) — two channels, each a 2×2 spatial map.
10498        let x = cpu_f32(
10499            vec![
10500                1.0_f32, 2.0, 3.0, 4.0, // channel 0
10501                10.0, 20.0, 30.0, 40.0, // channel 1
10502            ],
10503            &[1, 2, 2, 2],
10504        );
10505        let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5]);
10506        let bias = std::sync::Arc::<[f32]>::from(vec![1.0_f32, -10.0]);
10507        let out = x.channel_affine_4d(gain, bias).unwrap();
10508        let v = out.realize_f32();
10509        // Channel 0: gain=2, bias=1 → 2x+1
10510        assert_eq!(&v[0..4], &[3.0, 5.0, 7.0, 9.0]);
10511        // Channel 1: gain=0.5, bias=-10 → 0.5x-10
10512        assert_eq!(&v[4..8], &[-5.0, 0.0, 5.0, 10.0]);
10513    }
10514
10515    #[test]
10516    fn additive_causal_mask_has_strict_lower_triangle() {
10517        let anchor = cpu_f32(vec![0.0_f32], &[1]);
10518        let mask = Tensor::additive_causal_mask_like(&anchor, 4);
10519        assert_eq!(mask.shape().dims(), &[4, 4]);
10520        let v = mask.realize_f32();
10521        // Expected (-inf shown as 'x'):
10522        //   0 x x x
10523        //   0 0 x x
10524        //   0 0 0 x
10525        //   0 0 0 0
10526        for i in 0..4 {
10527            for j in 0..4 {
10528                let got = v[i * 4 + j];
10529                if j > i {
10530                    assert!(
10531                        got.is_infinite() && got.is_sign_negative(),
10532                        "above-diag (i={i}, j={j}) should be -inf, got {got}"
10533                    );
10534                } else {
10535                    assert_eq!(
10536                        got, 0.0,
10537                        "on/below-diag (i={i}, j={j}) should be 0, got {got}"
10538                    );
10539                }
10540            }
10541        }
10542    }
10543
10544    #[test]
10545    fn layer_norm_affine_unit_gain_zero_bias_matches_layer_norm_last_dim() {
10546        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10547        let gain = std::sync::Arc::<[f32]>::from(vec![1.0_f32; 3]);
10548        let bias = std::sync::Arc::<[f32]>::from(vec![0.0_f32; 3]);
10549        let out_affine = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10550        let out_plain = a.layer_norm_last_dim(1e-5).unwrap();
10551        let va = out_affine.realize_f32();
10552        let vp = out_plain.realize_f32();
10553        for (a, b) in va.iter().zip(vp.iter()) {
10554            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
10555        }
10556    }
10557
10558    #[test]
10559    fn layer_norm_affine_applies_gain_and_bias() {
10560        let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10561        let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5, 1.5]);
10562        let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, -5.0, 0.0]);
10563        let out = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10564        let v = out.realize_f32();
10565        // Manual: mean=2, var=2/3; normed = (x-2)/sqrt(2/3+1e-5).
10566        let mean = 2.0_f32;
10567        let var = ((1.0 - mean).powi(2) + (2.0 - mean).powi(2) + (3.0 - mean).powi(2)) / 3.0;
10568        let den = (var + 1e-5_f32).sqrt();
10569        let expected = [
10570            ((1.0 - mean) / den) * 2.0 + 10.0,
10571            ((2.0 - mean) / den) * 0.5 + (-5.0),
10572            ((3.0 - mean) / den) * 1.5 + 0.0,
10573        ];
10574        for (got, want) in v.iter().zip(expected.iter()) {
10575            assert!((got - want).abs() < 1e-4, "{got} vs {want}");
10576        }
10577    }
10578
10579    #[test]
10580    fn l2_normalize_last_dim_unit_norm_per_row() {
10581        // (2, 3): rows [3,4,0] (norm 5) and [1,2,2] (norm 3).
10582        let a = cpu_f32(vec![3.0, 4.0, 0.0, 1.0, 2.0, 2.0], &[2, 3]);
10583        let out = a.l2_normalize(1_usize, 1e-12).unwrap();
10584        assert_eq!(out.shape().dims(), &[2, 3]);
10585        let v = out.realize_f32();
10586        let row0_norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
10587        let row1_norm = (v[3] * v[3] + v[4] * v[4] + v[5] * v[5]).sqrt();
10588        assert!((row0_norm - 1.0).abs() < 1e-5, "row 0 norm = {row0_norm}");
10589        assert!((row1_norm - 1.0).abs() < 1e-5, "row 1 norm = {row1_norm}");
10590        // Row 0: [3,4,0]/5 → [0.6, 0.8, 0.0].
10591        assert!((v[0] - 0.6).abs() < 1e-5);
10592        assert!((v[1] - 0.8).abs() < 1e-5);
10593        assert!(v[2].abs() < 1e-5);
10594    }
10595
10596    #[test]
10597    fn l2_normalize_eps_zero_works_when_nonzero() {
10598        let a = cpu_f32(vec![1.0_f32, 0.0], &[2]);
10599        let out = a.l2_normalize(0_usize, 0.0).unwrap();
10600        let v = out.realize_f32();
10601        assert!((v[0] - 1.0).abs() < 1e-6);
10602        assert!(v[1].abs() < 1e-6);
10603    }
10604
10605    #[test]
10606    fn repeat_interleave_last_dim_matches_torch_semantics() {
10607        // (2, 3) input: rows [1,2,3] and [4,5,6]. dim=1, repeats=2
10608        // → each element becomes two consecutive copies:
10609        // (2, 6): [1,1,2,2,3,3] and [4,4,5,5,6,6].
10610        let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10611        let out = a.repeat_interleave(1_usize, 2).unwrap();
10612        assert_eq!(out.shape().dims(), &[2, 6]);
10613        assert_eq!(
10614            out.realize_f32(),
10615            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]
10616        );
10617    }
10618
10619    #[test]
10620    fn repeat_interleave_middle_dim() {
10621        // (2, 2, 2) input. dim=1, repeats=3 → (2, 6, 2).
10622        let a = cpu_f32((0..8).map(|i| i as f32).collect(), &[2, 2, 2]);
10623        let out = a.repeat_interleave(1_usize, 3).unwrap();
10624        assert_eq!(out.shape().dims(), &[2, 6, 2]);
10625        // First sample's elements: (0,1) repeated 3× then (2,3) repeated 3×.
10626        let v = out.realize_f32();
10627        assert_eq!(&v[0..6], &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
10628        assert_eq!(&v[6..12], &[2.0, 3.0, 2.0, 3.0, 2.0, 3.0]);
10629    }
10630
10631    #[test]
10632    fn repeat_interleave_repeats_one_is_noop() {
10633        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10634        let out = a.repeat_interleave(0_usize, 1).unwrap();
10635        assert_eq!(out.shape().dims(), &[3]);
10636        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10637    }
10638
10639    #[test]
10640    fn repeat_interleave_repeats_zero_errors() {
10641        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10642        assert!(a.repeat_interleave(0_usize, 0).is_err());
10643    }
10644
10645    #[test]
10646    fn stack_adds_leading_dim() {
10647        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10648        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10649        let out = Tensor::stack(&[&a, &b], 0).unwrap();
10650        assert_eq!(out.shape().dims(), &[2, 3]);
10651        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10652    }
10653
10654    #[test]
10655    fn stack_adds_trailing_dim() {
10656        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10657        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10658        let out = Tensor::stack(&[&a, &b], 1).unwrap();
10659        assert_eq!(out.shape().dims(), &[3, 2]);
10660        assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10661    }
10662
10663    #[test]
10664    fn stack_rejects_mismatched_shapes() {
10665        let a = cpu_f32(vec![1.0, 2.0], &[2]);
10666        let b = a.const_f32_like(vec![3.0, 4.0, 5.0], vec![3]).unwrap();
10667        assert!(Tensor::stack(&[&a, &b], 0).is_err());
10668    }
10669
10670    #[test]
10671    fn stack_rejects_empty_input() {
10672        let result = Tensor::stack(&[], 0);
10673        assert!(result.is_err());
10674    }
10675
10676    #[test]
10677    fn repeat_tiles_along_each_dim() {
10678        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10679        let out = t.repeat(vec![3]).unwrap();
10680        assert_eq!(out.shape().dims(), &[6]);
10681        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
10682    }
10683
10684    #[test]
10685    fn repeat_extends_rank_when_needed() {
10686        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10687        // repeat with shape [3, 2] left-pads tensor to [1, 2] then tiles to [3, 4]
10688        let out = t.repeat(vec![3, 2]).unwrap();
10689        assert_eq!(out.shape().dims(), &[3, 4]);
10690    }
10691
10692    #[test]
10693    fn repeat_identity_with_all_ones() {
10694        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10695        let out = t.repeat(vec![1]).unwrap();
10696        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10697    }
10698}
10699
10700// ============================================================================
10701// Phase A.3 keepdim reduction tests.
10702// ============================================================================
10703#[cfg(test)]
10704mod phase_a3_keepdim_tests {
10705    use super::*;
10706
10707    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10708        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10709    }
10710
10711    #[test]
10712    fn sum_keepdim_preserves_dim_as_one() {
10713        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10714        let out = t.sum_keepdim(1_usize).unwrap();
10715        assert_eq!(out.shape().dims(), &[2, 1]);
10716        assert_eq!(out.realize_f32(), vec![3.0, 7.0]);
10717    }
10718
10719    #[test]
10720    fn mean_keepdim_preserves_dim_as_one() {
10721        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10722        let out = t.mean_keepdim(0_usize).unwrap();
10723        assert_eq!(out.shape().dims(), &[1, 2]);
10724        assert_eq!(out.realize_f32(), vec![2.0, 3.0]);
10725    }
10726
10727    #[test]
10728    fn max_keepdim_preserves_dim_as_one() {
10729        let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10730        let out = t.max_keepdim(1_usize).unwrap();
10731        assert_eq!(out.shape().dims(), &[2, 1]);
10732        assert_eq!(out.realize_f32(), vec![3.0, 4.0]);
10733    }
10734
10735    #[test]
10736    fn min_keepdim_preserves_dim_as_one() {
10737        let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10738        let out = t.min_keepdim(1_usize).unwrap();
10739        assert_eq!(out.shape().dims(), &[2, 1]);
10740        assert_eq!(out.realize_f32(), vec![1.0, 2.0]);
10741    }
10742
10743    #[test]
10744    fn var_matches_unbiased_formula_with_correction_1() {
10745        // [[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
10746        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10747        let out = t.var(1, 1.0).unwrap();
10748        assert_eq!(out.shape().dims(), &[2]);
10749        let v = out.realize_f32();
10750        assert!((v[0] - 1.0).abs() < 1e-5, "var row 0 = {} != 1.0", v[0]);
10751        assert!((v[1] - 1.0).abs() < 1e-5, "var row 1 = {} != 1.0", v[1]);
10752    }
10753
10754    /// KISS-Ops §6.13 S14: `correction = 0.0` (the spec default, numpy's
10755    /// default) divides by `n` instead of `n - 1` -- same fixture as the
10756    /// Bessel test above, different divisor, so the two tests pin BOTH
10757    /// halves of the explicit-attribute contract against the same data.
10758    #[test]
10759    fn var_population_with_correction_0() {
10760        // Same fixture: dev=[-1,0,1], sq sum=2, /3 = 0.666...
10761        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10762        let out = t.var(1, 0.0).unwrap();
10763        let v = out.realize_f32();
10764        let want = 2.0 / 3.0;
10765        assert!(
10766            (v[0] - want).abs() < 1e-5,
10767            "population var row 0 = {} != {want}",
10768            v[0]
10769        );
10770        assert!(
10771            (v[1] - want).abs() < 1e-5,
10772            "population var row 1 = {} != {want}",
10773            v[1]
10774        );
10775    }
10776
10777    #[test]
10778    fn var_keepdim_preserves_dim() {
10779        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10780        let out = t.var_keepdim(1, 1.0).unwrap();
10781        assert_eq!(out.shape().dims(), &[2, 1]);
10782    }
10783
10784    #[test]
10785    fn var_errors_out_of_bounds() {
10786        let t = cpu_f32(vec![1.0, 2.0], &[2]);
10787        assert!(t.var(3, 1.0).is_err());
10788        assert!(t.var_keepdim(3, 1.0).is_err());
10789    }
10790}
10791
10792// ============================================================================
10793// Phase A.4 scalar/binary composite tests.
10794// ============================================================================
10795#[cfg(test)]
10796mod phase_a4_composite_tests {
10797    use super::*;
10798
10799    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10800        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10801    }
10802
10803    #[test]
10804    fn affine_applies_mul_then_add() {
10805        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10806        let out = t.affine(2.0, 10.0);
10807        assert_eq!(out.realize_f32(), vec![12.0, 14.0, 16.0]);
10808    }
10809
10810    #[test]
10811    fn scale_and_shift_alias_of_affine() {
10812        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10813        assert_eq!(
10814            t.scale_and_shift(2.0, 10.0).realize_f32(),
10815            t.affine(2.0, 10.0).realize_f32(),
10816        );
10817    }
10818
10819    #[test]
10820    fn elu_matches_reference_values() {
10821        let t = cpu_f32(vec![1.0, 0.0, -1.0, -2.0], &[4]);
10822        let out = t.elu(1.0);
10823        let v = out.realize_f32();
10824        // x > 0: identity. x == 0: 0 (boundary; gt returns 0 → neg branch which is alpha*(1-1)=0).
10825        // x < 0: alpha * (exp(x) - 1).
10826        assert!((v[0] - 1.0).abs() < 1e-5);
10827        assert!(v[1].abs() < 1e-5);
10828        assert!((v[2] - ((-1.0_f32).exp() - 1.0)).abs() < 1e-5);
10829        assert!((v[3] - ((-2.0_f32).exp() - 1.0)).abs() < 1e-5);
10830    }
10831
10832    #[test]
10833    fn dot_of_rank_one_vectors() {
10834        let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10835        let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10836        let out = a.dot(&b).unwrap();
10837        assert_eq!(out.shape().elem_count(), 1);
10838        let v = out.realize_f32();
10839        assert_eq!(v[0], 32.0); // 1*4 + 2*5 + 3*6
10840    }
10841
10842    #[test]
10843    fn dot_rejects_non_rank_one() {
10844        let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10845        let b = a
10846            .const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
10847            .unwrap();
10848        assert!(a.dot(&b).is_err());
10849    }
10850
10851    #[test]
10852    fn dot_rejects_length_mismatch() {
10853        let a = cpu_f32(vec![1.0, 2.0], &[2]);
10854        let b = a.const_f32_like(vec![1.0, 2.0, 3.0], vec![3]).unwrap();
10855        assert!(a.dot(&b).is_err());
10856    }
10857
10858    #[test]
10859    fn mv_matrix_times_vector() {
10860        let m = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10861        let v = m.const_f32_like(vec![1.0, 1.0, 1.0], vec![3]).unwrap();
10862        let out = m.mv(&v).unwrap();
10863        assert_eq!(out.shape().dims(), &[2]);
10864        assert_eq!(out.realize_f32(), vec![6.0, 15.0]);
10865    }
10866
10867    #[test]
10868    fn matvec_is_mv_alias() {
10869        let m = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10870        let v = m.const_f32_like(vec![3.0, 4.0], vec![2]).unwrap();
10871        let a = m.mv(&v).unwrap().realize_f32();
10872        let b = m.matvec(&v).unwrap().realize_f32();
10873        assert_eq!(a, b);
10874    }
10875
10876    #[test]
10877    fn mv_rejects_shape_mismatch() {
10878        let m = cpu_f32(vec![1.0; 6], &[2, 3]);
10879        let v = m.const_f32_like(vec![1.0, 1.0], vec![2]).unwrap();
10880        assert!(m.mv(&v).is_err());
10881    }
10882
10883    #[test]
10884    fn broadcast_matmul_passes_through_to_matmul() {
10885        let a = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10886        let b = a
10887            .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10888            .unwrap();
10889        let out = a.broadcast_matmul(&b).unwrap();
10890        assert_eq!(out.realize_f32(), vec![5.0, 6.0, 7.0, 8.0]);
10891    }
10892}
10893
10894// ============================================================================
10895// Phase A.5 factory family tests.
10896// ============================================================================
10897#[cfg(test)]
10898mod phase_a5_factory_tests {
10899    use super::*;
10900
10901    fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10902        Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10903    }
10904
10905    #[test]
10906    fn ones_like_matches_shape_dtype_graph() {
10907        let t = cpu_f32(vec![5.0, 6.0, 7.0, 8.0], &[2, 2]);
10908        let ones = t.ones_like().unwrap();
10909        assert_eq!(ones.shape().dims(), t.shape().dims());
10910        assert_eq!(ones.dtype(), t.dtype());
10911        assert_eq!(ones.realize_f32(), vec![1.0; 4]);
10912    }
10913
10914    #[test]
10915    fn zeros_like_matches_shape_dtype_graph() {
10916        let t = cpu_f32(vec![5.0, 6.0, 7.0], &[3]);
10917        let zeros = t.zeros_like().unwrap();
10918        assert_eq!(zeros.realize_f32(), vec![0.0; 3]);
10919    }
10920
10921    #[test]
10922    fn static_ones_f32() {
10923        let t = Tensor::ones(vec![2, 3], DType::F32, &Device::cpu()).unwrap();
10924        assert_eq!(t.shape().dims(), &[2, 3]);
10925        assert_eq!(t.realize_f32(), vec![1.0; 6]);
10926    }
10927
10928    #[test]
10929    fn static_zeros_f64() {
10930        let t = Tensor::zeros(vec![4], DType::F64, &Device::cpu()).unwrap();
10931        assert_eq!(t.dtype(), DType::F64);
10932        assert_eq!(t.realize_f64(), vec![0.0; 4]);
10933    }
10934
10935    #[test]
10936    fn full_with_f32_scalar() {
10937        let t = Tensor::full(vec![5], fuel_ir::Scalar::F32(2.5), &Device::cpu()).unwrap();
10938        assert_eq!(t.realize_f32(), vec![2.5; 5]);
10939    }
10940
10941    #[test]
10942    fn eye_identity_matrix() {
10943        let t = Tensor::eye(3, DType::F32, &Device::cpu());
10944        assert_eq!(t.shape().dims(), &[3, 3]);
10945        assert_eq!(
10946            t.realize_f32(),
10947            vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
10948        );
10949    }
10950
10951    #[test]
10952    fn tril2_lower_triangular_ones() {
10953        let t = Tensor::tril2(3, DType::F32, &Device::cpu());
10954        assert_eq!(
10955            t.realize_f32(),
10956            vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0],
10957        );
10958    }
10959
10960    #[test]
10961    fn triu2_upper_triangular_ones() {
10962        let t = Tensor::triu2(3, DType::F32, &Device::cpu());
10963        assert_eq!(
10964            t.realize_f32(),
10965            vec![1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0],
10966        );
10967    }
10968
10969    #[test]
10970    fn meshgrid_ij_indexing_two_inputs() {
10971        let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10972        let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10973        let grids = Tensor::meshgrid(&[&x, &y], false).unwrap();
10974        assert_eq!(grids.len(), 2);
10975        // ij: shapes are [len(x), len(y)] = [3, 2].
10976        assert_eq!(grids[0].shape().dims(), &[3, 2]);
10977        assert_eq!(grids[1].shape().dims(), &[3, 2]);
10978        // X grid: each row repeats x's value, so each row is identical along axis 1.
10979        assert_eq!(grids[0].realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
10980        // Y grid: each column repeats y's value, so each column is identical along axis 0.
10981        assert_eq!(grids[1].realize_f32(), vec![4.0, 5.0, 4.0, 5.0, 4.0, 5.0]);
10982    }
10983
10984    #[test]
10985    fn meshgrid_xy_indexing_swaps_first_two() {
10986        let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10987        let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10988        let grids = Tensor::meshgrid(&[&x, &y], true).unwrap();
10989        // xy: shapes flip to [len(y), len(x)] = [2, 3].
10990        assert_eq!(grids[0].shape().dims(), &[2, 3]);
10991        assert_eq!(grids[1].shape().dims(), &[2, 3]);
10992        // X grid: same row twice, each row is x.
10993        assert_eq!(grids[0].realize_f32(), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
10994        // Y grid: each row repeats one y element.
10995        assert_eq!(grids[1].realize_f32(), vec![4.0, 4.0, 4.0, 5.0, 5.0, 5.0]);
10996    }
10997
10998    #[test]
10999    fn meshgrid_rejects_single_input() {
11000        let x = Tensor::from_f32(vec![1.0_f32, 2.0], vec![2], &Device::cpu()).unwrap();
11001        assert!(Tensor::meshgrid(&[&x], false).is_err());
11002    }
11003
11004    #[test]
11005    fn meshgrid_rejects_non_rank_one() {
11006        let x = Tensor::from_f32(vec![1.0; 4], vec![2, 2], &Device::cpu()).unwrap();
11007        let y = x.const_f32_like(vec![1.0, 2.0], vec![2]).unwrap();
11008        assert!(Tensor::meshgrid(&[&x, &y], false).is_err());
11009    }
11010
11011    // ---- additional deferred-Phase-A item tests ----
11012
11013    #[test]
11014    fn narrow_is_slice_alias() {
11015        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5]);
11016        let a = t.narrow(0, 1, 3).unwrap().realize_f32();
11017        let b = t.slice(0, 1, 3).unwrap().realize_f32();
11018        assert_eq!(a, b);
11019        assert_eq!(a, vec![2.0, 3.0, 4.0]);
11020    }
11021
11022    #[test]
11023    fn chunk_splits_evenly() {
11024        let t = cpu_f32((1..=6).map(|i| i as f32).collect(), &[6]);
11025        let parts = t.chunk(3, 0).unwrap();
11026        assert_eq!(parts.len(), 3);
11027        assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0]);
11028        assert_eq!(parts[1].realize_f32(), vec![3.0, 4.0]);
11029        assert_eq!(parts[2].realize_f32(), vec![5.0, 6.0]);
11030    }
11031
11032    #[test]
11033    fn chunk_distributes_remainder_to_leading() {
11034        // size 7, 3 chunks → first 7%3=1 chunk gets the extra: sizes 3, 2, 2
11035        let t = cpu_f32((1..=7).map(|i| i as f32).collect(), &[7]);
11036        let parts = t.chunk(3, 0).unwrap();
11037        assert_eq!(parts.len(), 3);
11038        assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0, 3.0]);
11039        assert_eq!(parts[1].realize_f32(), vec![4.0, 5.0]);
11040        assert_eq!(parts[2].realize_f32(), vec![6.0, 7.0]);
11041    }
11042
11043    #[test]
11044    fn chunk_returns_singletons_when_size_less_than_chunks() {
11045        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11046        let parts = t.chunk(5, 0).unwrap();
11047        assert_eq!(parts.len(), 2);
11048        assert_eq!(parts[0].realize_f32(), vec![1.0]);
11049        assert_eq!(parts[1].realize_f32(), vec![2.0]);
11050    }
11051
11052    #[test]
11053    fn get_at_first_dim() {
11054        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11055        let row1 = t.get(1).unwrap();
11056        assert_eq!(row1.shape().dims(), &[2]);
11057        assert_eq!(row1.realize_f32(), vec![3.0, 4.0]);
11058    }
11059
11060    #[test]
11061    fn get_on_dim_arbitrary_axis() {
11062        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11063        let col0 = t.get_on_dim(1, 0).unwrap();
11064        assert_eq!(col0.shape().dims(), &[3]);
11065        assert_eq!(col0.realize_f32(), vec![1.0, 3.0, 5.0]);
11066    }
11067
11068    #[test]
11069    fn sum_dims_multi_dim_reduces_to_smaller() {
11070        // [2,3,4] sum over (0, 2) → [3]
11071        let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11072        let s = t.sum_dims([0, 2_usize]).unwrap();
11073        assert_eq!(s.shape().dims(), &[3]);
11074        // each element is 2*4 = 8 (sum across dim 0 = 2 elements, dim 2 = 4 elements)
11075        assert_eq!(s.realize_f32(), vec![8.0; 3]);
11076    }
11077
11078    #[test]
11079    fn mean_dims_multi_dim() {
11080        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11081        let m = t.mean_dims([0, 1_usize]).unwrap();
11082        assert_eq!(m.shape().dims(), &[] as &[usize]);
11083        assert_eq!(m.realize_f32(), vec![2.5]);
11084    }
11085
11086    #[test]
11087    fn sum_dims_keepdim_preserves_rank() {
11088        let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11089        let s = t.sum_dims_keepdim(&[0, 2]).unwrap();
11090        assert_eq!(s.shape().dims(), &[1, 3, 1]);
11091    }
11092
11093    #[test]
11094    fn mean_dims_keepdim_preserves_rank() {
11095        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11096        let m = t.mean_dims_keepdim(&[0, 1]).unwrap();
11097        assert_eq!(m.shape().dims(), &[1, 1]);
11098        assert_eq!(m.realize_f32(), vec![2.5]);
11099    }
11100
11101    #[test]
11102    fn rand_like_matches_shape_dtype() {
11103        let t = cpu_f32(vec![0.0; 6], &[2, 3]);
11104        let r = t.rand_like(-1.0, 1.0).unwrap();
11105        assert_eq!(r.shape().dims(), t.shape().dims());
11106        assert_eq!(r.dtype(), t.dtype());
11107        // Every sample must be in [-1, 1).
11108        for v in r.realize_f32() {
11109            assert!((-1.0..1.0).contains(&v), "sample {v} out of [-1, 1)");
11110        }
11111    }
11112
11113    #[test]
11114    fn randn_like_matches_shape_dtype() {
11115        let t = cpu_f32(vec![0.0; 4], &[4]);
11116        let r = t.randn_like(0.0, 1.0).unwrap();
11117        assert_eq!(r.shape().dims(), &[4]);
11118        assert_eq!(r.dtype(), DType::F32);
11119        // Samples are random — just sanity-check they're finite.
11120        for v in r.realize_f32() {
11121            assert!(v.is_finite());
11122        }
11123    }
11124
11125    #[test]
11126    fn static_rand_f32() {
11127        let t = Tensor::rand(vec![100], 0.0, 1.0, DType::F32, &Device::cpu()).unwrap();
11128        let v = t.realize_f32();
11129        // Mean of uniform [0,1) should be ~0.5; tolerate sample noise.
11130        let mean: f32 = v.iter().sum::<f32>() / v.len() as f32;
11131        assert!((0.3..0.7).contains(&mean), "mean {mean} too far from 0.5");
11132    }
11133
11134    #[test]
11135    fn static_randn_f64() {
11136        let t = Tensor::randn(vec![1000], 0.0, 1.0, DType::F64, &Device::cpu()).unwrap();
11137        let v = t.realize_f64();
11138        let mean: f64 = v.iter().sum::<f64>() / v.len() as f64;
11139        // Normal(0,1) mean should be near 0; n=1000 gives stderr ~0.03.
11140        assert!(mean.abs() < 0.2, "mean {mean} too far from 0");
11141    }
11142
11143    #[test]
11144    fn arange_int_step() {
11145        let t = Tensor::arange(0.0, 5.0, &Device::cpu());
11146        assert_eq!(t.shape().dims(), &[5]);
11147        assert_eq!(t.realize_f32(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
11148    }
11149
11150    #[test]
11151    fn arange_step_fractional() {
11152        let t = Tensor::arange_step(2.0, 4.0, 0.5, &Device::cpu());
11153        assert_eq!(t.realize_f32(), vec![2.0, 2.5, 3.0, 3.5]);
11154    }
11155
11156    #[test]
11157    fn arange_step_negative_descends() {
11158        let t = Tensor::arange_step(5.0, 0.0, -1.0, &Device::cpu());
11159        assert_eq!(t.realize_f32(), vec![5.0, 4.0, 3.0, 2.0, 1.0]);
11160    }
11161
11162    #[test]
11163    fn linspace_includes_endpoints() {
11164        let t = Tensor::linspace(0.0, 1.0, 5, &Device::cpu());
11165        assert_eq!(t.shape().dims(), &[5]);
11166        let v = t.realize_f32();
11167        assert!((v[0] - 0.0).abs() < 1e-6);
11168        assert!((v[4] - 1.0).abs() < 1e-6);
11169        assert!((v[2] - 0.5).abs() < 1e-6);
11170    }
11171
11172    #[test]
11173    fn linspace_n_one_returns_start() {
11174        let t = Tensor::linspace(7.0, 99.0, 1, &Device::cpu());
11175        assert_eq!(t.realize_f32(), vec![7.0]);
11176    }
11177
11178    #[test]
11179    fn norm_is_sqrt_sum_sq() {
11180        let t = Tensor::from_f32(vec![3.0_f32, 4.0], vec![2], &Device::cpu()).unwrap();
11181        let n = t.norm();
11182        assert!((n.realize_f32()[0] - 5.0).abs() < 1e-6);
11183    }
11184
11185    #[test]
11186    fn pad_with_zeros_left_and_right() {
11187        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11188        let p = t.pad_with_zeros(0, 2, 1).unwrap();
11189        assert_eq!(p.shape().dims(), &[6]);
11190        assert_eq!(p.realize_f32(), vec![0.0, 0.0, 1.0, 2.0, 3.0, 0.0]);
11191    }
11192
11193    #[test]
11194    fn pad_with_zeros_identity_when_both_zero() {
11195        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11196        let p = t.pad_with_zeros(0, 0, 0).unwrap();
11197        assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11198    }
11199
11200    #[test]
11201    fn pad_with_zeros_rejects_bad_dim() {
11202        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11203        assert!(t.pad_with_zeros(3, 1, 1).is_err());
11204    }
11205
11206    #[test]
11207    fn pad_with_value_zero_matches_pad_with_zeros() {
11208        // pad_with_value(_, _, _, 0.0) must be observationally identical
11209        // to pad_with_zeros — the latter is now a wrapper for the former.
11210        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11211        let via_zeros = t.pad_with_zeros(0, 1, 2).unwrap();
11212        let via_value = t.pad_with_value(0, 1, 2, 0.0).unwrap();
11213        assert_eq!(via_zeros.shape().dims(), via_value.shape().dims());
11214        assert_eq!(via_zeros.shape().dims(), &[5, 2]);
11215        assert_eq!(via_zeros.realize_f32(), via_value.realize_f32());
11216        assert_eq!(
11217            via_value.realize_f32(),
11218            vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0],
11219        );
11220    }
11221
11222    #[test]
11223    fn pad_with_value_neg_inf_then_max_reduce_ignores_pad() {
11224        // Negative interior values: -1, -2, -3. Pad with -inf on both
11225        // sides. Max along dim 0 must be the interior max (-1.0), not
11226        // -inf. This is the load-bearing property for max_pool2d.
11227        let t = cpu_f32(vec![-1.0, -2.0, -3.0], &[3]);
11228        let padded = t.pad_with_value(0, 2, 2, f32::NEG_INFINITY).unwrap();
11229        assert_eq!(padded.shape().dims(), &[7]);
11230        let v = padded.realize_f32();
11231        // Layout: [-inf, -inf, -1, -2, -3, -inf, -inf]
11232        assert!(v[0].is_infinite() && v[0].is_sign_negative());
11233        assert!(v[1].is_infinite() && v[1].is_sign_negative());
11234        assert_eq!(v[2], -1.0);
11235        assert_eq!(v[3], -2.0);
11236        assert_eq!(v[4], -3.0);
11237        assert!(v[5].is_infinite() && v[5].is_sign_negative());
11238        assert!(v[6].is_infinite() && v[6].is_sign_negative());
11239        // max along the only dim drops the pad and returns the interior max.
11240        let m = padded.max_all().realize_f32();
11241        assert_eq!(m, vec![-1.0]);
11242    }
11243
11244    #[test]
11245    fn pad_with_value_identity_when_both_zero() {
11246        // The early-out path must fire regardless of value (no spurious
11247        // graph node when there's nothing to pad).
11248        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11249        let p = t.pad_with_value(0, 0, 0, f32::NEG_INFINITY).unwrap();
11250        assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11251    }
11252
11253    #[test]
11254    fn pad_with_value_rejects_bad_dim() {
11255        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11256        assert!(t.pad_with_value(3, 1, 1, 0.0).is_err());
11257    }
11258
11259    #[test]
11260    fn max_pool2d_with_pad_value_neg_inf_on_negative_interior() {
11261        // Negative-only interior values: a zero-padded max_pool2d would
11262        // incorrectly return 0 in boundary windows. -inf padding gives
11263        // the PyTorch-correct answer (the interior max).
11264        //
11265        // 1x1x3x3 tensor, all values negative:
11266        //   [ -1, -2, -3 ]
11267        //   [ -4, -5, -6 ]
11268        //   [ -7, -8, -9 ]
11269        // With kernel=3, stride=1, padding=1, output is 3x3 where the
11270        // (1,1) center sees the full grid → max = -1.
11271        let x = cpu_f32(
11272            vec![-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0],
11273            &[1, 1, 3, 3],
11274        );
11275        let out = x
11276            .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), f32::NEG_INFINITY)
11277            .unwrap();
11278        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11279        let v = out.realize_f32();
11280        // Top-left corner (0,0): window sees only [(0,0),(0,1),(1,0),(1,1)]
11281        // = [-1,-2,-4,-5]; padded slots are -inf → max = -1.
11282        assert_eq!(v[0], -1.0);
11283        // Center (1,1): no padded slots in window → max of all 9 = -1.
11284        assert_eq!(v[4], -1.0);
11285        // Bottom-right (2,2): window sees [(1,1),(1,2),(2,1),(2,2)]
11286        // = [-5,-6,-8,-9]; padded slots are -inf → max = -5.
11287        assert_eq!(v[8], -5.0);
11288
11289        // KISS-Ops S14: `max_pool2d`'s own default pad value was 0.0
11290        // ("legacy behavior") -- the exact bug this test file demonstrates
11291        // above -- and is now `f32::NEG_INFINITY`. So `max_pool2d` (no
11292        // explicit pad value) must now agree with the NEG_INFINITY variant
11293        // above, not with a zero-padded one; the OLD zero-padded answer is
11294        // only reachable by asking for it explicitly via
11295        // `max_pool2d_with_pad_value(..., 0.0)`, which this block also
11296        // checks, to keep BOTH halves of the fix (new default AND old
11297        // explicit escape hatch) under one assertion.
11298        let default_pad = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11299        let vd = default_pad.realize_f32();
11300        assert_eq!(
11301            vd[0], -1.0,
11302            "max_pool2d's default must now match NEG_INFINITY padding, not 0.0"
11303        );
11304        assert_eq!(vd[8], -5.0);
11305
11306        let explicit_zero_pad = x
11307            .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), 0.0)
11308            .unwrap();
11309        let vz = explicit_zero_pad.realize_f32();
11310        assert_eq!(
11311            vz[0], 0.0,
11312            "explicit 0.0 padding must still be reachable via max_pool2d_with_pad_value"
11313        );
11314        assert_eq!(vz[8], 0.0);
11315    }
11316
11317    #[test]
11318    fn max_pool2d_with_pad_value_zero_matches_max_pool2d() {
11319        // With pad_value = 0.0, the new variant must agree with the
11320        // legacy max_pool2d byte-for-byte.
11321        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11322        let x = cpu_f32(data, &[1, 1, 4, 4]);
11323        let a = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11324        let b = x
11325            .max_pool2d_with_pad_value((2, 2), (2, 2), (0, 0), 0.0)
11326            .unwrap();
11327        assert_eq!(a.shape().dims(), b.shape().dims());
11328        assert_eq!(a.realize_f32(), b.realize_f32());
11329    }
11330
11331    // ---- Phase A.6 conv1d composite tests ----
11332
11333    #[test]
11334    fn conv1d_identity_kernel_passes_input_through() {
11335        // Single-batch, single-channel, kernel-1 identity → output equals input.
11336        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[1, 1, 5]);
11337        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11338        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11339        assert_eq!(out.shape().dims(), &[1, 1, 5]);
11340        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
11341    }
11342
11343    #[test]
11344    fn conv1d_sum_kernel_two_wide() {
11345        // Sum kernel of size 2: out[t] = x[t] + x[t+1].
11346        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11347        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11348        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11349        assert_eq!(out.shape().dims(), &[1, 1, 3]);
11350        assert_eq!(out.realize_f32(), vec![3.0, 5.0, 7.0]);
11351    }
11352
11353    #[test]
11354    fn conv1d_with_bias_applies_correctly() {
11355        let x = cpu_f32(vec![1.0, 1.0, 1.0], &[1, 1, 3]);
11356        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11357        let bias = x.const_f32_like(vec![10.0], vec![1]).unwrap();
11358        let out = x.conv1d(&w, Some(&bias), 1, 0, 1).unwrap();
11359        assert_eq!(out.realize_f32(), vec![11.0, 11.0, 11.0]);
11360    }
11361
11362    #[test]
11363    fn conv1d_stride_two_halves_output() {
11364        // Input length 6, kernel 2, stride 2 → output length (6-2)/2+1 = 3.
11365        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1, 1, 6]);
11366        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11367        let out = x.conv1d(&w, None, 2, 0, 1).unwrap();
11368        assert_eq!(out.shape().dims(), &[1, 1, 3]);
11369        assert_eq!(out.realize_f32(), vec![3.0, 7.0, 11.0]);
11370    }
11371
11372    #[test]
11373    fn conv1d_padding_one_preserves_length() {
11374        // Input length 4, kernel 3, padding 1, stride 1 → output length 4.
11375        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11376        let w = x
11377            .const_f32_like(vec![1.0, 1.0, 1.0], vec![1, 1, 3])
11378            .unwrap();
11379        let out = x.conv1d(&w, None, 1, 1, 1).unwrap();
11380        assert_eq!(out.shape().dims(), &[1, 1, 4]);
11381        // out[0] = 0+x[0]+x[1] = 3; out[1] = x[0]+x[1]+x[2] = 6;
11382        // out[2] = x[1]+x[2]+x[3] = 9; out[3] = x[2]+x[3]+0 = 7
11383        assert_eq!(out.realize_f32(), vec![3.0, 6.0, 9.0, 7.0]);
11384    }
11385
11386    #[test]
11387    fn conv1d_multi_channel_output() {
11388        // 1 batch, 1 in-channel, 3 timesteps; 2 out-channels with kernel 1.
11389        let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11390        // Weight [Cout=2, Cin=1, K=1]: filter 0 = 2.0, filter 1 = -1.0.
11391        let w = x.const_f32_like(vec![2.0, -1.0], vec![2, 1, 1]).unwrap();
11392        let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11393        assert_eq!(out.shape().dims(), &[1, 2, 3]);
11394        // Channel 0: 2.0 × input. Channel 1: -1.0 × input.
11395        assert_eq!(out.realize_f32(), vec![2.0, 4.0, 6.0, -1.0, -2.0, -3.0]);
11396    }
11397
11398    #[test]
11399    fn conv1d_rejects_rank_two_input() {
11400        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11401        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11402        assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11403    }
11404
11405    #[test]
11406    fn conv1d_rejects_rank_two_weight() {
11407        let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11408        let w = x.const_f32_like(vec![1.0], vec![1, 1]).unwrap();
11409        assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11410    }
11411
11412    #[test]
11413    fn conv1d_rejects_zero_groups_or_stride() {
11414        let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11415        let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11416        assert!(x.conv1d(&w, None, 0, 0, 1).is_err());
11417        assert!(x.conv1d(&w, None, 1, 0, 0).is_err());
11418    }
11419
11420    #[test]
11421    fn conv1d_with_algo_ignores_algo_param() {
11422        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11423        let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11424        // Pass a dummy algo (the parameter is ignored on the lazy path).
11425        let a = x.conv1d_with_algo(&w, None, 1, 0, 1, "unused").unwrap();
11426        let b = x.conv1d(&w, None, 1, 0, 1).unwrap();
11427        assert_eq!(a.realize_f32(), b.realize_f32());
11428    }
11429
11430    // ---- Phase A.7 pooling / interpolation composite tests ----
11431
11432    #[test]
11433    fn avg_pool2d_2x2_stride2() {
11434        // 1×1×4×4 input with values 0..15.
11435        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11436        let x = cpu_f32(data, &[1, 1, 4, 4]);
11437        let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11438        assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11439        // Each 2x2 block average: top-left = (0+1+4+5)/4 = 2.5,
11440        // top-right = (2+3+6+7)/4 = 4.5, bottom-left = (8+9+12+13)/4 = 10.5,
11441        // bottom-right = (10+11+14+15)/4 = 12.5.
11442        let v = out.realize_f32();
11443        assert!((v[0] - 2.5).abs() < 1e-5);
11444        assert!((v[1] - 4.5).abs() < 1e-5);
11445        assert!((v[2] - 10.5).abs() < 1e-5);
11446        assert!((v[3] - 12.5).abs() < 1e-5);
11447    }
11448
11449    #[test]
11450    fn avg_pool2d_3x3_stride1_padding1_preserves_size() {
11451        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11452        let out = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11453        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11454    }
11455
11456    /// KISS-Ops §6.13 S14: `count_include_pad = false` must divide each
11457    /// border window by its TRUE valid-tap count, not the uniform `kh·kw`.
11458    /// Fixture: a 1x1x4x4 all-ones input, 3x3 kernel, padding 1.
11459    ///
11460    /// `excluded` (count_include_pad=false) must be EXACTLY 1.0 at every
11461    /// output position, border included: averaging any positive count of
11462    /// real taps that are all 1.0 is always 1.0, regardless of how many
11463    /// padded slots surround them — this is the invariant the fix exists
11464    /// to restore. `included` (count_include_pad=true, today's old
11465    /// behavior) must NOT share that invariant: it divides by the uniform
11466    /// 9 regardless of how many taps were real, so a corner window (only
11467    /// 4 of 9 taps real) computes `4/9 ≈ 0.444`, well below 1.0 — the two
11468    /// modes genuinely disagree at the border, which is the point of the
11469    /// fix. (On fully-interior positions, where every tap is real either
11470    /// way, both modes necessarily agree — not asserted here since it
11471    /// proves nothing about the fix; see the non-uniform fixture below for
11472    /// a sharper per-position contrast.)
11473    #[test]
11474    fn avg_pool2d_excludes_padding_keeps_uniform_data_exactly_at_its_true_value() {
11475        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11476        let included = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11477        let excluded = x.avg_pool2d((3, 3), (1, 1), (1, 1), false).unwrap();
11478        assert_eq!(included.shape().dims(), excluded.shape().dims());
11479
11480        let ve = excluded.realize_f32();
11481        for (i, &v) in ve.iter().enumerate() {
11482            assert!(
11483                (v - 1.0).abs() < 1e-5,
11484                "count_include_pad=false must be exactly 1.0 at every position \
11485                 (all real taps are 1.0) — position {i} got {v}",
11486            );
11487        }
11488
11489        let vi = included.realize_f32();
11490        assert!(
11491            (vi[0] - 1.0).abs() > 0.1,
11492            "count_include_pad=true must NOT match at a corner (4 of 9 taps \
11493             real → 4/9, far from 1.0) — got {}, modes should disagree here",
11494            vi[0]
11495        );
11496    }
11497
11498    /// The actual discriminating case: non-uniform data at a border window.
11499    /// A 1x1x2x2 input `[[10, 10], [10, 10]]`, kernel 2x2, padding 1 (so
11500    /// the TOP-LEFT output position's window is `[[pad,pad],[pad,10]]` --
11501    /// only ONE of four taps is real). count_include_pad=true (today's old
11502    /// behavior) divides by the uniform 4: `10/4 = 2.5`. count_include_pad
11503    /// =false (ONNX/PyTorch default) divides by the TRUE valid count, 1:
11504    /// `10/1 = 10.0` -- the actual average of the real values in the
11505    /// window, which is what "average pooling, excluding padding" means.
11506    #[test]
11507    fn avg_pool2d_excludes_padding_from_divisor_discriminating() {
11508        let x = cpu_f32(vec![10.0, 10.0, 10.0, 10.0], &[1, 1, 2, 2]);
11509        let included = x.avg_pool2d((2, 2), (1, 1), (1, 1), true).unwrap();
11510        let excluded = x.avg_pool2d((2, 2), (1, 1), (1, 1), false).unwrap();
11511        let vi = included.realize_f32();
11512        let ve = excluded.realize_f32();
11513        assert!(
11514            (vi[0] - 2.5).abs() < 1e-5,
11515            "count_include_pad=true: top-left = 10/4 = 2.5, got {}",
11516            vi[0]
11517        );
11518        assert!(
11519            (ve[0] - 10.0).abs() < 1e-5,
11520            "count_include_pad=false: top-left = 10/1 (ONE real tap) = 10.0, got {}",
11521            ve[0]
11522        );
11523    }
11524
11525    /// A 1x1 input with padding >= kernel size: EVERY output window is
11526    /// entirely padding, so the count-include-pad=false divisor would be
11527    /// zero everywhere. Must refuse with a descriptive error rather than
11528    /// let `broadcast_div` emit NaN/inf.
11529    #[test]
11530    fn avg_pool2d_rejects_zero_valid_tap_window() {
11531        let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11532        let err = x
11533            .avg_pool2d((1, 1), (1, 1), (2, 2), false)
11534            .expect_err("a window with zero real taps must be rejected, not silently NaN");
11535        let msg = err.to_string();
11536        assert!(
11537            msg.contains("divide by zero") || msg.contains("no valid"),
11538            "error should explain the zero-valid-tap cause, got: {msg}"
11539        );
11540
11541        // Positive control: count_include_pad=true has no notion of "valid
11542        // taps" (it always divides by kh*kw), so the SAME shape must still
11543        // succeed under that mode -- proving the rejection is specific to
11544        // the zero-valid-tap case, not a blanket refusal of this shape.
11545        let ok = x.avg_pool2d((1, 1), (1, 1), (2, 2), true);
11546        assert!(
11547            ok.is_ok(),
11548            "count_include_pad=true must still accept this shape (uniform divisor, never zero)"
11549        );
11550    }
11551
11552    #[test]
11553    fn avg_pool2d_multi_channel() {
11554        // 1×2×2×2: each channel is filled with its index.
11555        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]);
11556        let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11557        assert_eq!(out.shape().dims(), &[1, 2, 1, 1]);
11558        assert_eq!(out.realize_f32(), vec![0.0, 1.0]);
11559    }
11560
11561    #[test]
11562    fn max_pool2d_2x2_stride2() {
11563        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11564        let x = cpu_f32(data, &[1, 1, 4, 4]);
11565        let out = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11566        assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11567        // Each 2x2 block max: 5, 7, 13, 15.
11568        assert_eq!(out.realize_f32(), vec![5.0, 7.0, 13.0, 15.0]);
11569    }
11570
11571    #[test]
11572    fn max_pool2d_3x3_stride1_padding1() {
11573        let x = cpu_f32(
11574            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
11575            &[1, 1, 3, 3],
11576        );
11577        let out = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11578        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11579        // Center should be the global max 9; corners should be the max of their 2×2 window.
11580        let v = out.realize_f32();
11581        // (1,1) center: max of all 9 = 9
11582        assert!((v[4] - 9.0).abs() < 1e-5);
11583    }
11584
11585    #[test]
11586    fn upsample_nearest2d_2x() {
11587        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11588        let out = x.upsample_nearest2d(2).unwrap();
11589        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11590        // Each cell replicated 2×2: rows are [1,1,2,2; 1,1,2,2; 3,3,4,4; 3,3,4,4].
11591        assert_eq!(
11592            out.realize_f32(),
11593            vec![
11594                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,
11595            ],
11596        );
11597    }
11598
11599    #[test]
11600    fn upsample_nearest2d_3x() {
11601        let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11602        let out = x.upsample_nearest2d(3).unwrap();
11603        assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11604        assert_eq!(out.realize_f32(), vec![5.0; 9]);
11605    }
11606
11607    #[test]
11608    fn upsample_nearest2d_identity_scale_one() {
11609        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11610        let out = x.upsample_nearest2d(1).unwrap();
11611        assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
11612    }
11613
11614    #[test]
11615    fn upsample_nearest1d_2x() {
11616        let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11617        let out = x.upsample_nearest1d(2).unwrap();
11618        assert_eq!(out.shape().dims(), &[1, 1, 6]);
11619        assert_eq!(out.realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
11620    }
11621
11622    #[test]
11623    fn interpolate2d_integer_multiple() {
11624        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11625        let out = x.interpolate2d(4, 4).unwrap();
11626        assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11627    }
11628
11629    #[test]
11630    fn interpolate2d_accepts_non_integer_ratio() {
11631        // Lifted from "rejects non-integer ratio" — arbitrary
11632        // ratios are now supported via the index_select composite
11633        // (matching the eager UpsampleNearest2D convention). See
11634        // tests/lazy_interpolate2d_oracle.rs for parity checks.
11635        let x = cpu_f32(vec![1.0; 4], &[1, 1, 2, 2]);
11636        let out = x.interpolate2d(3, 4).unwrap();
11637        assert_eq!(out.shape().dims(), &[1, 1, 3, 4]);
11638    }
11639
11640    #[test]
11641    fn interpolate1d_integer_multiple() {
11642        let x = cpu_f32(vec![1.0, 2.0], &[1, 1, 2]);
11643        let out = x.interpolate1d(6).unwrap();
11644        assert_eq!(out.shape().dims(), &[1, 1, 6]);
11645        assert_eq!(out.realize_f32(), vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
11646    }
11647
11648    #[test]
11649    fn pool_rejects_bad_rank() {
11650        let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11651        assert!(x.avg_pool2d((2, 2), (2, 2), (0, 0), true).is_err());
11652        assert!(x.max_pool2d((2, 2), (2, 2), (0, 0)).is_err());
11653    }
11654
11655    #[test]
11656    fn pool_rejects_zero_kernel() {
11657        let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11658        assert!(x.avg_pool2d((0, 2), (1, 1), (0, 0), true).is_err());
11659        assert!(x.max_pool2d((2, 0), (1, 1), (0, 0)).is_err());
11660    }
11661
11662    // ---- Phase A.8 scope-limited harmonization aliases ----
11663
11664    #[test]
11665    fn numel_matches_elem_count() {
11666        let t = cpu_f32(vec![1.0; 12], &[3, 4]);
11667        assert_eq!(t.numel(), t.elem_count());
11668        assert_eq!(t.numel(), 12);
11669    }
11670
11671    #[test]
11672    fn dim_returns_specific_axis_size() {
11673        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11674        assert_eq!(t.dim(0).unwrap(), 2);
11675        assert_eq!(t.dim(1).unwrap(), 3);
11676        assert_eq!(t.dim(2).unwrap(), 4);
11677        assert!(t.dim(3).is_err());
11678    }
11679
11680    #[test]
11681    fn to_dtype_switches_dtype() {
11682        let t = cpu_f32(vec![1.0, 2.0], &[2]);
11683        let b = t.to_dtype(DType::F64).unwrap();
11684        assert_eq!(b.dtype(), DType::F64);
11685        assert_eq!(b.realize_f64(), vec![1.0, 2.0]);
11686    }
11687
11688    #[test]
11689    fn to_dtype_same_dtype_is_noop() {
11690        let t = cpu_f32(vec![1.0_f32], &[1]);
11691        let b = t.to_dtype(DType::F32).unwrap();
11692        assert_eq!(b.dtype(), DType::F32);
11693    }
11694
11695    #[test]
11696    fn detach_is_identity_on_lazy() {
11697        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11698        let d = t.detach();
11699        assert_eq!(d.realize_f32(), t.realize_f32());
11700    }
11701
11702    #[test]
11703    fn track_op_is_true_on_lazy() {
11704        let t = cpu_f32(vec![0.0], &[1]);
11705        assert!(t.track_op());
11706    }
11707
11708    // ---- Phase A.8a Dim/Dims trait port ergonomics tests ----
11709
11710    #[test]
11711    fn try_permute_accepts_tuple_syntax() {
11712        let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11713        // Eager-style tuple permute now works on lazy.
11714        let out = t.permute((2_usize, 0_usize, 1_usize)).unwrap();
11715        assert_eq!(out.shape().dims(), &[4, 2, 3]);
11716    }
11717
11718    #[test]
11719    fn dim_arg_methods_accept_negative_indexing() {
11720        use fuel_ir::D;
11721        let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
11722        // squeeze, sum_dim, mean_dim, etc. all accept D::Minus1 now.
11723        let sum_last = t.shape().dims().to_vec(); // just demonstrate compile
11724        assert_eq!(sum_last, vec![2, 3]);
11725        // sum_dim is still Self-returning (Phase A.8b will flip), so test
11726        // a method that already returns Result + dim arg.
11727        let cumsum_last = t.cumsum(D::Minus1).unwrap();
11728        assert_eq!(cumsum_last.shape().dims(), &[2, 3]);
11729    }
11730
11731    #[test]
11732    fn unsqueeze_accepts_dim_trait() {
11733        use fuel_ir::D;
11734        let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11735        // Append a new last dim via D::Minus1 (rank-aware negative indexing).
11736        let out = t.unsqueeze(D::Minus1).unwrap();
11737        // The position D::Minus1 in to_index_plus_one is "the very end"
11738        // → output rank 2 with the new dim trailing.
11739        assert_eq!(out.shape().dims().len(), 2);
11740    }
11741}
11742pub struct TokenDataHost {
11743    pub token_ids: fuel_ir::HostBuffer,
11744    pub rope_cos: fuel_ir::HostBuffer,
11745    pub rope_sin: fuel_ir::HostBuffer,
11746    pub mask: fuel_ir::HostBuffer,
11747    pub offset: Option<fuel_ir::HostBuffer>,
11748}
11749
11750/// Same per-token data as [`crate::inference_context::DecodeTokenData`],
11751/// as raw host bytes instead of freshly-uploaded device Arcs — for
11752/// [`fuel_dispatch::pipelined::CapturedDecodeSession::replay_token`]'s
11753/// in-place H2D overwrite of fixed buffers. `LlamaModel`-private.
11754#[cfg(feature = "cuda")]
11755pub struct TokenDataBytes {
11756    pub token_ids: Vec<u8>,
11757    pub rope_cos: Vec<u8>,
11758    pub rope_sin: Vec<u8>,
11759    pub mask: Vec<u8>,
11760    pub offset: Option<Vec<u8>>,
11761}
11762
11763/// D2H a [`fuel_dispatch::pipelined::CapturedDecodeSession::replay_token`]
11764/// result's device-resident output Arc to a host `Vec<f32>`.
11765///
11766/// The capture mechanism deliberately keeps the output device-resident
11767/// INSIDE the capture (a `Copy`/`Move` node is capture-unsafe — see
11768/// `LlamaModel::forward_with_kv_context_captured`'s design note); this
11769/// reads it back OUTSIDE the capture, dispatching on the same
11770/// `BackendStorage` variants every other pipelined D2H site in this
11771/// codebase does (`CudaStorageBytes::to_cpu_bytes` / `CpuStorageBytes::
11772/// bytes`) rather than hand-rolling a new D2H mechanism. Capture is
11773/// f32-only today (project constraint), so the bytes are reinterpreted
11774/// as f32 directly.
11775#[cfg(feature = "cuda")]
11776pub fn captured_output_to_f32(
11777    output: &Arc<std::sync::RwLock<fuel_memory::Storage>>,
11778) -> fuel_ir::error::Result<Vec<f32>> {
11779    use fuel_memory::BackendStorage;
11780    let guard = output.read().map_err(|_| {
11781        fuel_ir::Error::Msg("captured decode output storage lock poisoned".into()).bt()
11782    })?;
11783    let bytes: Vec<u8> = match &guard.inner {
11784        BackendStorage::Cpu(c) => c.bytes().to_vec(),
11785        BackendStorage::Cuda(c) => c.to_cpu_bytes()?,
11786        #[allow(unreachable_patterns)]
11787        other => {
11788            return Err(fuel_ir::Error::Msg(format!(
11789                "captured decode output is BackendStorage::{:?}, expected Cpu or Cuda",
11790                std::mem::discriminant(other),
11791            ))
11792            .bt());
11793        }
11794    };
11795    Ok(bytemuck::cast_slice::<u8, f32>(&bytes).to_vec())
11796}