Skip to main content

fuel_dispatch/
fused.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! FusedOpRegistry — kernel-side payload. Phase 7.6 step 1 (skeleton).
3//!
4//! Architecture v1.0 splits the fused-op registry across two crates:
5//! - graph-side metadata in `fuel-graph::registry` (id, name, family,
6//!   pattern, decompose, backward, shape/dtype rules);
7//! - kernel-side payload here in `fuel-storage::fused` ([`BackendImpl`],
8//!   [`CostEstimate`], [`PrecisionGuarantee`], [`KernelRevisionHash`]).
9//!
10//! The split exists because [`KernelRef`] lives in fuel-storage and
11//! fuel-graph cannot depend on fuel-storage (the dependency arrow goes
12//! the other way). Joining the two halves is by [`fuel_graph::registry::FusedOpId`]
13//! at runtime: the optimizer reads the metadata-side entry to reason
14//! about decomposition and shape, then asks the kernel-side
15//! [`FusedKernelRegistry`] for the per-backend [`BackendImpl`] when it
16//! needs to pre-resolve a `KernelRef`.
17//!
18//! ## Status (step 1)
19//!
20//! Types only. No callers; no behavior change. Subsequent steps:
21//! - Step 3: register the SoftmaxLastDim CPU `BackendImpl` and dispatch
22//!   `Op::Fused(SOFTMAX_LAST_DIM, _)` through it from the executor.
23//! - Step 6-9: extend per-backend coverage, populate `PrecisionGuarantee`
24//!   and `cost`, and migrate the binding-table lookup off the executor's
25//!   hot path.
26
27use crate::kernel::{KernelCaps, KernelRef};
28use fuel_graph::registry::{FusedOpId, FusedOpParams};
29use fuel_ir::{DType, Shape, backend::BackendCapabilities, probe::BackendId};
30use smallvec::SmallVec;
31use std::collections::HashMap;
32
33/// Per-backend kernel implementation for one fused op. The optimizer
34/// reads this to (1) pre-resolve [`KernelRef`] for nodes it places on
35/// this backend, (2) score routes against [`CostEstimate`], and
36/// (3) admit candidates against the per-route tolerance budget via
37/// [`PrecisionGuarantee`].
38///
39/// Function-pointer composition (no trait-object indirection) — the
40/// registry stores [`BackendImpl`] values inline, the executor calls
41/// the function pointer directly.
42///
43/// A single `(FusedOpId, BackendId)` decision point may have multiple
44/// registered `BackendImpl`s — one per dtype tuple, and potentially
45/// multiple alternatives at the same dtype (e.g. cuBLAS bias-epilogue
46/// vs CUTLASS bias-epilogue for `(FUSED_LINEAR, [BF16,BF16,BF16,BF16],
47/// Cuda)`). The route picker filters by [`Self::dtypes`] and ranks the
48/// remaining alternatives by cost + precision.
49#[derive(Copy, Clone)]
50pub struct BackendImpl {
51    /// Dispatch wrapper for this backend's kernel for this fused op.
52    /// Same `KernelRef` signature as primitive-op kernels.
53    pub kernel: KernelRef,
54    /// Dtype tuple this kernel is registered for. Convention mirrors
55    /// the binding-table key shape: typically `[input1, input2, ...,
56    /// output]` per the existing `register(table, op, &[dtype; N],
57    /// backend, kernel)` call sites. Using `&'static [DType]` keeps
58    /// `BackendImpl` `Copy` and lets registrations declare dtypes
59    /// inline as `&[F32, F32, F32, F32]` literals.
60    pub dtypes: &'static [DType],
61    /// Cost-estimate function. Given the input shapes, the
62    /// per-instance fused-op params, and the backend's capabilities,
63    /// returns a [`CostEstimate`] used for placement and route ranking.
64    pub cost: fn(&[Shape], &FusedOpParams, &BackendCapabilities) -> CostEstimate,
65    /// Numerical precision properties of this kernel.
66    pub precision: PrecisionGuarantee,
67    /// Layout / capability flags (e.g. strided_input).
68    pub caps: KernelCaps,
69    /// Revision hash — opaque identifier for the source-version of this
70    /// kernel. Persisted optimization caches use this to detect kernel
71    /// drift and invalidate stale cache entries (see
72    /// `docs/architecture/11-persistence.md`).
73    pub revision: KernelRevisionHash,
74    /// Declared cost AST compiled from the contract's `cost:` field
75    /// (FKC gap-closure §2.3, Task 2.4). `None` when the contract
76    /// declared no cost expression, or for hand-written (non-FKC)
77    /// registrations. When present, [`crate::fused_cost::fused_layer1_cost`]
78    /// prefers it over the [`is_fused_cost_sentinel`](crate::fused_cost::is_fused_cost_sentinel)
79    /// decompose-derived fallback.
80    pub cost_expr: Option<&'static crate::fkc::CompiledCostExpr>,
81}
82
83/// Coarse cost model for one kernel invocation. Layer-1 of the
84/// architecture's cost-model tower (FLOP counts + bandwidth + launch
85/// overhead). Layer-2 — empirical refinement from per-deployment
86/// telemetry — composes on top of these without changing this shape.
87#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
88pub struct CostEstimate {
89    /// Compute pressure — number of floating-point operations.
90    pub flops: u64,
91    /// Bandwidth pressure — bytes moved through device memory hierarchy
92    /// for this kernel's inputs + outputs (excluding cache hits).
93    pub bytes_moved: u64,
94    /// Fixed launch overhead. CPU kernels measure this in tens of ns;
95    /// GPU launches in low microseconds.
96    pub kernel_overhead_ns: u32,
97}
98
99/// What this kernel guarantees about its numerical behavior.
100///
101/// Replaces the binary-flag OracleGrade concept that pre-architecture
102/// drafts used; per architecture v1.0 every kernel registration carries
103/// a structured precision statement so the optimizer can reason about
104/// tolerance budgets and pick comparators for calibration.
105///
106/// `bit_stable_on_same_hardware` is the strongest property. The
107/// always-built backend (fuel-cpu-backend by convention) commits to
108/// providing at least one `bit_stable_on_same_hardware: true` kernel
109/// per primitive op as the architecture v1.0 coverage commitment;
110/// step 7 enforces this via a CI lint.
111///
112/// The Optional fields encode the bound's flavor (ULP / relative /
113/// absolute). Multiple may be present; the optimizer takes the
114/// intersection (a budget is admissible only if every populated bound
115/// satisfies it). Absent bounds mean "this kernel makes no claim
116/// about that flavor."
117#[derive(Copy, Clone, Debug)]
118pub struct PrecisionGuarantee {
119    /// True iff this kernel produces bit-identical output for
120    /// bit-identical inputs on the same hardware (no nondeterminism
121    /// from kernel scheduling, atomic ordering, etc.).
122    pub bit_stable_on_same_hardware: bool,
123    /// Maximum unit-in-last-place error vs the IEEE-754 correctly-
124    /// rounded result. Tighter than max_relative for low-magnitude
125    /// values; many vendor math libraries quote ULPs.
126    pub max_ulp: Option<u32>,
127    /// Maximum relative error: `|out - ref| / max(|ref|, eps)`.
128    pub max_relative: Option<f64>,
129    /// Maximum absolute error: `|out - ref|`.
130    pub max_absolute: Option<f64>,
131    /// Free-text qualifier — for audited claims, this carries the
132    /// audit reason (vendor citation, scheduler-dependence note,
133    /// implementation hint). For the [`UNAUDITED`] sentinel, this
134    /// is a fixed placeholder string the coverage lint matches
135    /// against to distinguish unaudited placeholders from audited
136    /// claims that happen to have no static bound. **If you change
137    /// `UNAUDITED.notes`, the lint's detector picks up the new value
138    /// automatically — it reads `PrecisionGuarantee::UNAUDITED.notes`
139    /// at test time, not a hardcoded string.**
140    ///
141    /// [`UNAUDITED`]: PrecisionGuarantee::UNAUDITED
142    pub notes: &'static str,
143}
144
145impl PrecisionGuarantee {
146    /// "Reference" guarantee: bit-stable on same hardware, no error
147    /// claim above that. Used by reference-grade CPU kernels that
148    /// commit to deterministic IEEE-754 evaluation.
149    pub const REFERENCE: PrecisionGuarantee = PrecisionGuarantee {
150        bit_stable_on_same_hardware: true,
151        max_ulp: Some(0),
152        max_relative: Some(0.0),
153        max_absolute: Some(0.0),
154        notes: "Reference IEEE-754 evaluation; bit-identical re-run.",
155    };
156
157    /// Phase 7.6 step 7b: shared precision guarantee for the
158    /// always-built CPU backend's **primitive** kernels — elementwise
159    /// unary/binary, reductions, matmul, casts, comparisons, indexing,
160    /// scalar ops, transcendentals (Exp/Log/Sin/Cos/Tanh/Sigmoid/
161    /// Gelu/Silu), etc. All claim:
162    ///
163    /// - `bit_stable_on_same_hardware: true` (deterministic iteration
164    ///   order; no atomic FP adds; sequential nested loops)
165    /// - F32 accumulator for BF16/F16 inputs, native F32 / F64 / U32
166    ///   for matching dtypes
167    /// - No specific ULP / relative / absolute bound claimed at this
168    ///   layer — the empirical calibration framework in step 8
169    ///   populates per-op-per-shape bounds via reference comparisons
170    ///
171    /// This is the default value bulk-applied by
172    /// [`crate::kernel::KernelBindingTable::fill_unset_cpu_precision`]
173    /// at the end of `register_cpu_kernels`. Sites that need a
174    /// different claim must call
175    /// [`crate::kernel::KernelBindingTable::register_with_precision`]
176    /// explicitly with the weaker guarantee — those don't get
177    /// bulk-overwritten because the fill only touches UNAUDITED
178    /// entries.
179    pub const PRIMITIVE_DETERMINISTIC_CPU: PrecisionGuarantee = PrecisionGuarantee {
180        bit_stable_on_same_hardware: true,
181        max_ulp: None,
182        max_relative: None,
183        max_absolute: None,
184        notes: "fuel-cpu-backend primitive kernel family: deterministic \
185                nested-loop iteration order; F32 accumulator for \
186                half-precision inputs; bit-identical same-hardware \
187                re-run. Per-op-per-shape ULP / relative bounds land \
188                with the step-8 empirical calibration framework.",
189    };
190
191    /// Default placeholder for kernels whose precision hasn't been
192    /// audited yet. Identified by the coverage lint via notes-
193    /// equality against `PrecisionGuarantee::UNAUDITED.notes` —
194    /// the only way a registration ends up with this specific
195    /// notes string is by using the literal `UNAUDITED` const
196    /// (the default in unannotated `register(...)` / `register_with_caps(...)`
197    /// calls).
198    ///
199    /// **For kernels audited and concluded "no static bound
200    /// applies"** (e.g. Vulkan subgroup reductions where FADD
201    /// order is scheduler-determined per dispatch), use
202    /// [`none`] instead. It returns a structurally-similar
203    /// PrecisionGuarantee but with the audit reason in `notes`,
204    /// captured at the registration site. The lint accepts any
205    /// notes value other than UNAUDITED's specific text.
206    ///
207    /// [`none`]: PrecisionGuarantee::none
208    pub const UNAUDITED: PrecisionGuarantee = PrecisionGuarantee {
209        bit_stable_on_same_hardware: false,
210        max_ulp: None,
211        max_relative: None,
212        max_absolute: None,
213        notes: "PrecisionGuarantee::UNAUDITED — default placeholder for \
214                kernels whose precision hasn't been audited yet. Coverage \
215                lints flag these. Sites must either annotate via \
216                `register_with_precision` with a real claim or use \
217                `PrecisionGuarantee::none(reason)` if the audit \
218                concluded no static bound applies.",
219    };
220
221    /// Constructor for the "audited, no static guarantee could be
222    /// established" conclusion. Used by Vulkan subgroup reductions
223    /// (where FADD order is scheduler-determined per dispatch) and
224    /// similar kernels whose numerical character can't be
225    /// summarized by static ULP / relative / absolute bounds.
226    ///
227    /// The `reason` is captured in `notes`, so the audit reasoning
228    /// lives at the registration site — visible in code review,
229    /// not buried in a separate lint allowlist file. Any `reason`
230    /// other than `UNAUDITED.notes` makes the resulting value pass
231    /// the coverage lint.
232    ///
233    /// Distinguished from [`UNAUDITED`] by `notes` content alone
234    /// (the value-field shape is identical). The lint uses
235    /// `precision.notes == PrecisionGuarantee::UNAUDITED.notes` as
236    /// the unaudited detector — robust to UNAUDITED's notes text
237    /// drifting because it reads from the const itself.
238    ///
239    /// [`UNAUDITED`]: PrecisionGuarantee::UNAUDITED
240    pub const fn none(reason: &'static str) -> PrecisionGuarantee {
241        PrecisionGuarantee {
242            bit_stable_on_same_hardware: false,
243            max_ulp: None,
244            max_relative: None,
245            max_absolute: None,
246            notes: reason,
247        }
248    }
249
250    /// Does this guarantee assert nothing machine-checkable — no
251    /// bit-stability, no ULP / relative / absolute bound?
252    ///
253    /// True for BOTH [`UNAUDITED`] and [`none`]: they are field-identical,
254    /// because they make the same (empty) set of claims. What separates them
255    /// is *provenance*, not claim content — see [`Self::is_audited_no_bound`].
256    ///
257    /// [`UNAUDITED`]: PrecisionGuarantee::UNAUDITED
258    /// [`none`]: PrecisionGuarantee::none
259    pub fn makes_no_static_claim(&self) -> bool {
260        !self.bit_stable_on_same_hardware
261            && self.max_ulp.is_none()
262            && self.max_relative.is_none()
263            && self.max_absolute.is_none()
264    }
265
266    /// Is this an **audited conclusion that no static bound applies**
267    /// ([`none(reason)`]) rather than the never-audited placeholder
268    /// ([`UNAUDITED`])?
269    ///
270    /// # Why this is a method and not an open-coded check (GAP-095)
271    ///
272    /// These two states are **field-identical apart from `notes`**, so any
273    /// consumer that must tell them apart is forced to key on a string. Two
274    /// consumers previously invented their own predicate and *disagreed*: the
275    /// bit-stable coverage lint compared `notes` against `UNAUDITED.notes`,
276    /// while [`KernelBindingTable::fill_unset_cpu_precision`] looked only at
277    /// the four value fields — and therefore silently rewrote an audited
278    /// "no static bound applies" conclusion into a
279    /// `bit_stable_on_same_hardware: true` claim the audit had declined to
280    /// make.
281    ///
282    /// One definition, so the next consumer inherits the distinction instead
283    /// of re-deriving it. **This is a seam, not a cure:** a `&'static str`
284    /// remains the load-bearing discriminator, and only making the two states
285    /// structurally distinct would make conflation *unrepresentable*. That
286    /// change is priced at ~64 `PrecisionGuarantee` literals across three
287    /// crates and is tracked separately; when it lands, this method's body
288    /// changes and its callers do not.
289    ///
290    /// [`none(reason)`]: PrecisionGuarantee::none
291    /// [`UNAUDITED`]: PrecisionGuarantee::UNAUDITED
292    /// [`KernelBindingTable::fill_unset_cpu_precision`]: crate::kernel::KernelBindingTable::fill_unset_cpu_precision
293    pub fn is_audited_no_bound(&self) -> bool {
294        self.makes_no_static_claim()
295            && !self.notes.is_empty()
296            && self.notes != Self::UNAUDITED.notes
297    }
298}
299
300/// Opaque revision hash of a registered kernel. Persisted optimization
301/// caches read this to detect kernel drift between cache build and
302/// cache load (see `docs/architecture/11-persistence.md`). Computed
303/// from kernel source + version metadata at registration time; step 9
304/// fills in the actual hashing function alongside the binding-table
305/// planning-time refactor.
306#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
307pub struct KernelRevisionHash(pub u64);
308
309impl KernelRevisionHash {
310    /// Sentinel meaning "no revision tracked yet." Used by step-1-shipped
311    /// `BackendImpl` registrations until step 9 wires real hashing.
312    pub const UNTRACKED: KernelRevisionHash = KernelRevisionHash(0);
313}
314
315/// Inline capacity for per-fused-op backend lists. SmallVec at 4 fits
316/// CPU + CUDA + Vulkan + Metal without spilling to heap; the typical
317/// fused op has 1-3 backends with kernels.
318type BackendImplList = SmallVec<[(BackendId, BackendImpl); 4]>;
319
320/// Kernel-side registry: `FusedOpId` → list of per-backend
321/// [`BackendImpl`]s. Joined to `fuel-graph`'s metadata-side
322/// [`fuel_graph::registry::FusedOpRegistry`] by id at runtime.
323///
324/// Built at process startup, frozen thereafter (architecture v1.0:
325/// no runtime extensibility).
326#[derive(Default)]
327pub struct FusedKernelRegistry {
328    by_id: HashMap<FusedOpId, BackendImplList>,
329    /// Finding 5.4 (FKC side, Task 3.6) side-table: preserves a fused
330    /// contract's `return.bundle` per-slot NAMES, keyed by the same
331    /// `(FusedOpId, BackendId, dtypes)` tuple the kernel itself was
332    /// registered under. Before this table existed the names were parsed
333    /// (`return_check::bundle_slot_names`) then discarded — a later
334    /// consumer (e.g. FDX output-view labeling) had no artifact to read
335    /// them from. `Vec<DType>` (not `&'static [DType]`) because the key is
336    /// built from a caller-owned slice at lookup time.
337    bundle_slot_names: HashMap<(FusedOpId, BackendId, Vec<DType>), Vec<String>>,
338}
339
340impl FusedKernelRegistry {
341    pub fn new() -> Self {
342        Self::default()
343    }
344
345    /// Register a [`BackendImpl`] for this `(id, backend)` pair. Always
346    /// appends — multiple impls per `(id, backend)` are allowed (per-
347    /// dtype registrations, alternative algorithms at the same dtype,
348    /// etc.). The route picker filters by [`BackendImpl::dtypes`] and
349    /// ranks remaining alternatives at lookup time.
350    pub fn register(&mut self, id: FusedOpId, backend: BackendId, impl_: BackendImpl) {
351        self.by_id.entry(id).or_default().push((backend, impl_));
352    }
353
354    /// Look up the first [`BackendImpl`] registered for `(id, backend)`
355    /// regardless of dtypes. Convenience for callers that already know
356    /// a single impl exists for the pair. Prefer
357    /// [`Self::lookup_by_dtypes`] when multiple per-dtype impls may
358    /// share a backend.
359    pub fn lookup(&self, id: FusedOpId, backend: BackendId) -> Option<BackendImpl> {
360        self.by_id
361            .get(&id)
362            .and_then(|impls| impls.iter().find(|(b, _)| *b == backend))
363            .map(|(_, impl_)| *impl_)
364    }
365
366    /// Look up the [`BackendImpl`] registered for `(id, backend,
367    /// dtypes)`. Returns `None` when no kernel matches all three; the
368    /// optimizer's fallback in that case is to lower the fused op via
369    /// the metadata-side `decompose` and run it as primitives on the
370    /// backend.
371    ///
372    /// When multiple impls match (alternatives at the same decision
373    /// point — e.g. cuBLAS vs CUTLASS bf16 matmul), the first
374    /// registration wins. Step 9's route picker replaces this with a
375    /// cost-aware selector.
376    pub fn lookup_by_dtypes(
377        &self,
378        id: FusedOpId,
379        backend: BackendId,
380        dtypes: &[DType],
381    ) -> Option<BackendImpl> {
382        self.by_id
383            .get(&id)
384            .and_then(|impls| {
385                impls
386                    .iter()
387                    .find(|(b, i)| *b == backend && i.dtypes == dtypes)
388            })
389            .map(|(_, impl_)| *impl_)
390    }
391
392    /// All `(BackendId, BackendImpl)` pairs registered for this id.
393    /// The optimizer reads this when deciding placement: a fused op
394    /// with a CUDA-only kernel limits its placement candidates.
395    pub fn impls_for(&self, id: FusedOpId) -> &[(BackendId, BackendImpl)] {
396        self.by_id.get(&id).map(|v| v.as_slice()).unwrap_or(&[])
397    }
398
399    /// Number of distinct `FusedOpId`s with at least one registered
400    /// backend impl.
401    pub fn len(&self) -> usize {
402        self.by_id.len()
403    }
404
405    /// Whether the registry has any registered impls.
406    pub fn is_empty(&self) -> bool {
407        self.by_id.is_empty()
408    }
409
410    /// Finding 5.4 (FKC side, Task 3.6): record a fused contract's
411    /// `return.bundle` per-slot names against the `(id, backend, dtypes)`
412    /// key it was registered under. Overwrites any prior entry at the same
413    /// key (re-import is idempotent, mirroring `register`'s append
414    /// semantics at the registration level — the side-table itself has no
415    /// duplicate-detection gate).
416    pub fn record_bundle_slot_names(
417        &mut self,
418        id: FusedOpId,
419        backend: BackendId,
420        dtypes: &[DType],
421        names: &[String],
422    ) {
423        self.bundle_slot_names
424            .insert((id, backend, dtypes.to_vec()), names.to_vec());
425    }
426
427    /// Look up the per-slot names recorded by [`Self::record_bundle_slot_names`]
428    /// for `(id, backend, dtypes)`. `None` when no bundle names were ever
429    /// recorded for that exact key (including a single-output section,
430    /// which never calls `record_bundle_slot_names` in the first place).
431    pub fn bundle_slot_names(
432        &self,
433        id: FusedOpId,
434        backend: BackendId,
435        dtypes: &[DType],
436    ) -> Option<&[String]> {
437        self.bundle_slot_names
438            .get(&(id, backend, dtypes.to_vec()))
439            .map(|v| v.as_slice())
440    }
441}
442
443/// Register a [`BackendImpl`] into a [`FusedKernelRegistry`] via the
444/// step-6 macro shape. Hides the boilerplate of constructing the
445/// `BackendImpl` struct so registration sites read as a flat list of
446/// `(id, backend, dtypes, kernel, cost, precision)` tuples.
447///
448/// Caps default to [`KernelCaps::empty()`] and revision to
449/// [`KernelRevisionHash::UNTRACKED`]; pass `caps = ...` and
450/// `revision = ...` to override.
451///
452/// # Examples
453///
454/// ```ignore
455/// register_fused!(
456///     registry,
457///     FusedOps::FUSED_LINEAR,
458///     BackendId::Cpu,
459///     &[F32, F32, F32, F32],
460///     fused_linear_f32_cpu_wrapper,
461///     cost = cost_fused_linear_cpu,
462///     precision = PrecisionGuarantee::REFERENCE,
463/// );
464/// ```
465#[macro_export]
466macro_rules! register_fused {
467    (
468        $registry:expr,
469        $id:expr,
470        $backend:expr,
471        $dtypes:expr,
472        $kernel:expr,
473        cost = $cost:expr,
474        precision = $precision:expr
475        $(, caps = $caps:expr)?
476        $(, revision = $revision:expr)?
477        $(,)?
478    ) => {{
479        #[allow(unused_mut, unused_assignments)]
480        let mut caps = $crate::kernel::KernelCaps::empty();
481        $( caps = $caps; )?
482        #[allow(unused_mut, unused_assignments)]
483        let mut revision = $crate::fused::KernelRevisionHash::UNTRACKED;
484        $( revision = $revision; )?
485        $registry.register(
486            $id,
487            $backend,
488            $crate::fused::BackendImpl {
489                kernel: $kernel,
490                dtypes: $dtypes,
491                cost: $cost,
492                precision: $precision,
493                caps,
494                revision,
495                cost_expr: None,
496            },
497        );
498    }};
499}
500
501/// Process-wide default kernel registry: every backend's fused-op
502/// `BackendImpl`s registered at startup. Built once on first access
503/// via [`std::sync::OnceLock`]; immutable thereafter (architecture
504/// v1.0: no runtime extensibility).
505///
506/// Today's coverage:
507/// - `FUSED_LINEAR` × `Cpu` × `{F32, F64, BF16, F16}` — four
508///   bit-stable per-dtype impls registered via [`register_default_kernels`].
509///
510/// Backend-side crates (fuel-cuda-backend, fuel-vulkan-backend) extend
511/// this set by either (a) registering during their own startup, or
512/// (b) the step-9 binding-table refactor where the route picker pulls
513/// from this registry. Today's executors continue to lookup via the
514/// per-dtype [`KernelBindingTable`](crate::kernel::KernelBindingTable); this
515/// registry is
516/// the architecture-target shape for CUTLASS / cuBLAS alternative
517/// registrations and step-9's pre-resolved `KernelRef` pipeline.
518pub fn default_kernel_registry() -> &'static FusedKernelRegistry {
519    use std::sync::OnceLock;
520    static REGISTRY: OnceLock<FusedKernelRegistry> = OnceLock::new();
521    REGISTRY.get_or_init(|| {
522        let mut r = FusedKernelRegistry::new();
523        register_default_kernels(&mut r);
524        r
525    })
526}
527
528/// Populate a [`FusedKernelRegistry`] with the always-built kernels
529/// (today: `fuel-cpu-backend` FusedLinear F32/F64/BF16/F16). Called
530/// from [`default_kernel_registry`]'s OnceLock initializer; exposed as
531/// a free function so backend crates can compose against a custom
532/// registry in tests.
533pub fn register_default_kernels(r: &mut FusedKernelRegistry) {
534    crate::dispatch::register_default_fused_kernels(r);
535}
536
537/// Static cost model for `(FUSED_LINEAR, Cpu)` kernels — the conservative
538/// FLOP + bandwidth model per architecture v1.0 §"Layer-1 cost model."
539/// Identical for F32/F64/BF16/F16 (the kernels accumulate in F32
540/// regardless of input dtype); the empirical Layer-2 refinement
541/// framework will tighten per-dtype later.
542///
543/// Shapes: `[a, b, bias]` where `a = [..., M, K]`, `b = [..., K, N]`,
544/// `bias = [N]`.
545pub fn cost_fused_linear_cpu(
546    shapes: &[Shape],
547    _params: &FusedOpParams,
548    _caps: &BackendCapabilities,
549) -> CostEstimate {
550    debug_assert_eq!(shapes.len(), 3, "FusedLinear cost: expected 3 input shapes");
551    let a_dims = shapes[0].dims();
552    let b_dims = shapes[1].dims();
553    let rank = a_dims.len();
554    if rank < 2 || b_dims.len() < 2 {
555        return CostEstimate::default();
556    }
557    let m = a_dims[rank - 2] as u64;
558    let k = a_dims[rank - 1] as u64;
559    let n = b_dims[b_dims.len() - 1] as u64;
560    let batch: u64 = a_dims[..rank - 2]
561        .iter()
562        .map(|d| *d as u64)
563        .product::<u64>()
564        .max(1);
565    // FMA counts as 2 FLOPs. Matmul: 2·M·N·K per batch. Bias-add: M·N per batch.
566    let mm_flops = 2u64 * m * n * k * batch;
567    let bias_flops = m * n * batch;
568    // Conservative bandwidth: assume 4 B/elem (F32-equivalent). Empirical
569    // refinement adjusts per-dtype; the static layer is intentionally coarse.
570    let elems = batch * (m * k + k * n + m * n) + n;
571    let bytes_moved = elems * 4;
572    CostEstimate {
573        flops: mm_flops + bias_flops,
574        bytes_moved,
575        kernel_overhead_ns: 50, // CPU launch overhead, ballpark
576    }
577}
578
579/// Precision guarantee shared by every `(FUSED_LINEAR, Cpu, *)` impl
580/// in fuel-cpu-backend: deterministic per-element accumulation in F32
581/// (BF16/F16 inputs upcast and narrow at the end), bit-identical on
582/// same hardware. Bounded error claims (max_ulp / max_relative) depend
583/// on the contracting dim K and so stay `None` at this layer — the
584/// empirical calibration framework derives them per-shape.
585pub const FUSED_LINEAR_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
586    bit_stable_on_same_hardware: true,
587    max_ulp: None,
588    max_relative: None,
589    max_absolute: None,
590    notes: "fuel-cpu-backend FusedLinear: deterministic matmul + bias-add; \
591            BF16/F16 accumulate in F32 and narrow at end; same-hardware re-run \
592            bit-identical.",
593};
594
595/// Static cost model for `(FUSED_SOFTMAX_CROSS_ENTROPY, Cpu)` — row-by-row
596/// stable log-softmax + gather. FLOPs scale with `n_rows × vocab`
597/// (two passes per row: one for max+sum_exp, one for the loss
598/// accumulator); memory is `n_rows × vocab` reads plus the targets and
599/// the scalar/vector output.
600///
601/// Shapes: `[logits, targets]` where `logits = [..., V]` (F32) and
602/// `targets = [...]` (I64); the per-row reduction count comes from
603/// the logits shape's leading dims.
604pub fn cost_fused_softmax_cross_entropy_cpu(
605    shapes: &[Shape],
606    _params: &FusedOpParams,
607    _caps: &BackendCapabilities,
608) -> CostEstimate {
609    debug_assert_eq!(
610        shapes.len(),
611        2,
612        "FusedSoftmaxCrossEntropy cost: expected 2 input shapes (logits, targets)",
613    );
614    let logits_dims = shapes[0].dims();
615    if logits_dims.is_empty() {
616        return CostEstimate::default();
617    }
618    let vocab = *logits_dims.last().unwrap() as u64;
619    let n_rows: u64 = logits_dims[..logits_dims.len() - 1]
620        .iter()
621        .map(|d| *d as u64)
622        .product::<u64>()
623        .max(1);
624    // Two passes over vocab per row: pass 1 finds max (V compares),
625    // pass 2 sums exp(x - max) (V FMAs ≈ 2V FLOPs). Plus one log
626    // call. The transcendental `exp` counts as ~10 FLOPs by convention.
627    let per_row_flops = vocab + 2 * vocab + 10 * vocab + 10;
628    let total_flops = n_rows * per_row_flops;
629    // Bandwidth: logits (n_rows × vocab × 4) + targets (n_rows × 8) +
630    // output (4 bytes for Mean/Sum, ~n_rows × 4 for None — average to
631    // the worst case here for the conservative layer-1 estimate).
632    let bytes_moved = n_rows * vocab * 4 + n_rows * 8 + n_rows * 4;
633    CostEstimate {
634        flops: total_flops,
635        bytes_moved,
636        kernel_overhead_ns: 50,
637    }
638}
639
640/// Precision guarantee for `(FUSED_SOFTMAX_CROSS_ENTROPY, Cpu, *)` —
641/// stable log-sum-exp accumulated in F64, gather + NLL in F64,
642/// narrow to F32 at end. Bit-identical re-run on same hardware. The
643/// kernel's iteration order is fixed, so the only sources of
644/// non-determinism (reduction-order changes, denormal flushing) are
645/// excluded by construction.
646pub const FUSED_SOFTMAX_CROSS_ENTROPY_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
647    bit_stable_on_same_hardware: true,
648    max_ulp: None,
649    max_relative: None,
650    max_absolute: None,
651    notes: "fuel-cpu-backend FusedSoftmaxCrossEntropy: stable log-sum-exp \
652            in F64 accumulator; gather + NLL in F64; narrow to F32 at end; \
653            same-hardware re-run bit-identical.",
654};
655
656/// Static cost model for `(CAUSAL_CONV1D, Cpu)` — depthwise 1-D conv.
657/// FLOPs scale with `batch × channels × seq_out × kernel`; bandwidth
658/// is `batch × channels × seq_in` (x reads) + `channels × kernel`
659/// (weight reads) + `channels` (bias) + `batch × channels × seq_out`
660/// (output writes). SiLU adds one transcendental per output element.
661///
662/// Shapes: `[x, weight, bias]` where `x = [batch, channels, seq_in]`,
663/// `weight = [channels, 1, kernel]`, `bias = [channels]`.
664pub fn cost_causal_conv1d_cpu(
665    shapes: &[Shape],
666    params: &FusedOpParams,
667    _caps: &BackendCapabilities,
668) -> CostEstimate {
669    debug_assert_eq!(
670        shapes.len(),
671        3,
672        "CausalConv1d cost: expected 3 input shapes (x, weight, bias)",
673    );
674    let use_silu = match params {
675        FusedOpParams::CausalConv1d { use_silu } => *use_silu,
676        _ => return CostEstimate::default(),
677    };
678    let x_dims = shapes[0].dims();
679    let w_dims = shapes[1].dims();
680    if x_dims.len() != 3 || w_dims.len() != 3 {
681        return CostEstimate::default();
682    }
683    let batch = x_dims[0] as u64;
684    let channels = x_dims[1] as u64;
685    let seq_in = x_dims[2] as u64;
686    let kernel = w_dims[2] as u64;
687    if seq_in < kernel - 1 {
688        return CostEstimate::default();
689    }
690    let seq_out = seq_in - (kernel - 1);
691    // 2·kernel FLOPs per output element (FMA = 2). SiLU adds ~10
692    // FLOPs per element (transcendental convention).
693    let per_out_flops = 2 * kernel + if use_silu { 10 } else { 0 };
694    let flops = batch * channels * seq_out * per_out_flops;
695    let bytes_moved = batch * channels * seq_in * 4
696        + channels * kernel * 4
697        + channels * 4
698        + batch * channels * seq_out * 4;
699    CostEstimate {
700        flops,
701        bytes_moved,
702        kernel_overhead_ns: 50,
703    }
704}
705
706/// Precision guarantee for `(CAUSAL_CONV1D, Cpu, *)` — textbook
707/// nested-loop depthwise conv with F32 accumulator; SiLU computed
708/// element-wise via `x / (1 + exp(-x))`. Iteration order fixed;
709/// bit-identical re-run on same hardware.
710pub const CAUSAL_CONV1D_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
711    bit_stable_on_same_hardware: true,
712    max_ulp: None,
713    max_relative: None,
714    max_absolute: None,
715    notes: "fuel-cpu-backend CausalConv1d: textbook depthwise nested-loop \
716            with F32 accumulator; optional SiLU via x/(1+exp(-x)); \
717            same-hardware re-run bit-identical.",
718};
719
720/// Static cost model for `(SELECTIVE_SCAN, Cpu)` — Mamba's selective
721/// state-space scan. FLOPs scale with `batch · seqlen · dim · dstate`
722/// (the inner triple-nested loop). Bandwidth is dominated by reading
723/// u/delta/b/c per timestep + writing y.
724///
725/// Shapes: `[u, delta, a, b, c]`. Per-step cost: ~4·dstate FMAs (exp +
726/// mul + add for h update; mul + accumulate for y).
727pub fn cost_selective_scan_cpu(
728    shapes: &[Shape],
729    _params: &FusedOpParams,
730    _caps: &BackendCapabilities,
731) -> CostEstimate {
732    debug_assert_eq!(
733        shapes.len(),
734        5,
735        "SelectiveScan cost: expected 5 input shapes (u, delta, a, b, c)",
736    );
737    let u_dims = shapes[0].dims();
738    let a_dims = shapes[2].dims();
739    if u_dims.len() != 3 || a_dims.len() != 2 {
740        return CostEstimate::default();
741    }
742    let batch = u_dims[0] as u64;
743    let seqlen = u_dims[1] as u64;
744    let dim = u_dims[2] as u64;
745    let dstate = a_dims[1] as u64;
746    // ~8 FLOPs per (b, t, i, j) iteration: exp (10), mul (1), mul (1),
747    // add (1), mul (1), add (1). Conservative average ~16/iter once
748    // exp is amortized.
749    let per_iter_flops = 16;
750    let flops = batch * seqlen * dim * dstate * per_iter_flops;
751    // u + delta + b + c (read per step) + a (read once) + out (written once).
752    let bytes_moved = 2 * batch * seqlen * dim * 4
753        + 2 * batch * seqlen * dstate * 4
754        + dim * dstate * 4
755        + batch * seqlen * dim * 4;
756    CostEstimate {
757        flops,
758        bytes_moved,
759        kernel_overhead_ns: 50,
760    }
761}
762
763/// Precision guarantee for `(SELECTIVE_SCAN, Cpu, *)` — recurrence
764/// accumulated in F64 (h is `Vec<f64>` internally), final y narrowed
765/// to F32. Iteration order fixed; bit-identical re-run on same
766/// hardware. The F64 accumulator buys ~7 extra decimal digits of
767/// headroom vs naive F32 over the per-step state update.
768pub const SELECTIVE_SCAN_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
769    bit_stable_on_same_hardware: true,
770    max_ulp: None,
771    max_relative: None,
772    max_absolute: None,
773    notes: "fuel-cpu-backend SelectiveScan: state recurrence in F64 \
774            accumulator; narrow to F32 on y store; deterministic \
775            iteration order; same-hardware re-run bit-identical.",
776};
777
778/// Static cost model for `(SSD_CHUNK_SCAN, Cpu)` — Mamba-2's chunked
779/// SSD scan. FLOPs scale with
780/// `batch · seqlen · heads · head_dim · state_dim` (the inner
781/// quadruple-nested loop). Bandwidth reads x/dt/b/c per timestep +
782/// a once + writes y.
783///
784/// Shapes: `[x, dt, a, b, c]`. v1 single-chunk only.
785pub fn cost_ssd_chunk_scan_cpu(
786    shapes: &[Shape],
787    _params: &FusedOpParams,
788    _caps: &BackendCapabilities,
789) -> CostEstimate {
790    debug_assert_eq!(
791        shapes.len(),
792        5,
793        "SsdChunkScan cost: expected 5 input shapes (x, dt, a, b, c)",
794    );
795    let x_dims = shapes[0].dims();
796    let b_dims = shapes[3].dims();
797    if x_dims.len() != 4 || b_dims.len() != 4 {
798        return CostEstimate::default();
799    }
800    let batch = x_dims[0] as u64;
801    let seqlen = x_dims[1] as u64;
802    let heads = x_dims[2] as u64;
803    let head_dim = x_dims[3] as u64;
804    let state_dim = b_dims[3] as u64;
805    // ~8 FLOPs per (b, t, h, i, j) inner iteration: mul-mul-add for
806    // h-update + mul-add for y-accumulate, plus exp amortized per
807    // outer (b, t, h).
808    let per_iter_flops = 8;
809    let flops = batch * seqlen * heads * head_dim * state_dim * per_iter_flops;
810    let bytes_moved = 2 * batch * seqlen * heads * head_dim * 4
811        + batch * seqlen * heads * 4
812        + 2 * batch * seqlen * heads * state_dim * 4
813        + heads * 4;
814    CostEstimate {
815        flops,
816        bytes_moved,
817        kernel_overhead_ns: 50,
818    }
819}
820
821/// Precision guarantee for `(SSD_CHUNK_SCAN, Cpu, *)` — same shape
822/// as `SELECTIVE_SCAN_CPU_PRECISION`.
823pub const SSD_CHUNK_SCAN_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
824    bit_stable_on_same_hardware: true,
825    max_ulp: None,
826    max_relative: None,
827    max_absolute: None,
828    notes: "fuel-cpu-backend SsdChunkScan: per-head state recurrence \
829            in F64 accumulator; narrow to F32 on y store; \
830            deterministic iteration order; same-hardware re-run \
831            bit-identical. chunk_size is a GPU-parallelism knob; \
832            CPU runs sequential, any positive chunk_size dividing \
833            seqlen produces the same answer.",
834};
835
836/// Static cost model for `(NF4_MATMUL, Cpu)` — bitsandbytes 4-bit
837/// quantized matmul. FLOPs scale with `batch · m · n · k`
838/// (one FMA per k step, after dequant). Bandwidth reads
839/// `batch·m·k` activations + `n·k/2` packed weight bytes + `n·k/block_size`
840/// absmax scales + writes `batch·m·n` outputs.
841pub fn cost_nf4_matmul_cpu(
842    shapes: &[Shape],
843    params: &FusedOpParams,
844    _caps: &BackendCapabilities,
845) -> CostEstimate {
846    debug_assert_eq!(
847        shapes.len(),
848        3,
849        "Nf4Matmul cost: expected 3 input shapes (activations, w_packed, absmax)",
850    );
851    let block_size = match params {
852        FusedOpParams::Nf4Matmul { block_size } => *block_size as u64,
853        _ => return CostEstimate::default(),
854    };
855    let a_dims = shapes[0].dims();
856    let w_dims = shapes[1].dims();
857    if a_dims.len() < 2 || w_dims.len() != 2 {
858        return CostEstimate::default();
859    }
860    let m = a_dims[a_dims.len() - 2] as u64;
861    let k = a_dims[a_dims.len() - 1] as u64;
862    let n = w_dims[0] as u64;
863    let batch: u64 = a_dims[..a_dims.len() - 2]
864        .iter()
865        .map(|d| *d as u64)
866        .product::<u64>()
867        .max(1);
868    if block_size == 0 || !k.is_multiple_of(block_size) {
869        return CostEstimate::default();
870    }
871    // 2 FLOPs per FMA (mul + add). Dequant lookup is one mul per k step.
872    let flops = batch * m * n * k * 3;
873    // Bandwidth: 4 B/elem default (F32-ish), 1 B/elem for w_packed.
874    let elem_bytes = 4;
875    let bytes_moved = batch * m * k * elem_bytes
876        + n * (k / 2)
877        + n * (k / block_size) * 4
878        + batch * m * n * elem_bytes;
879    CostEstimate {
880        flops,
881        bytes_moved,
882        kernel_overhead_ns: 50,
883    }
884}
885
886/// Precision guarantee for `(NF4_MATMUL, Cpu, *)` — inner product
887/// accumulated in F32 regardless of activation/output dtype (F16 +
888/// BF16 variants up-cast on load and narrow on store, matching
889/// bitsandbytes' fp16/bf16 GEMV behavior). Iteration order fixed;
890/// bit-identical re-run on same hardware. The 16-entry NF4 LUT is
891/// baked in as a const so quantization roundtrips are reproducible.
892pub const NF4_MATMUL_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
893    bit_stable_on_same_hardware: true,
894    max_ulp: None,
895    max_relative: None,
896    max_absolute: None,
897    notes: "fuel-cpu-backend Nf4Matmul: dequant via baked NF4_LUT × \
898            per-block absmax; F32 accumulator for the inner product \
899            (F16/BF16 up-cast on load, narrow on store); \
900            deterministic iteration order; same-hardware re-run \
901            bit-identical.",
902};
903
904/// Static cost model for `(CONV2D, Cpu)` kernels — FLOP + bandwidth
905/// per architecture v1.0 §"Layer-1 cost model."
906///
907/// Shapes: `[x, weight, (bias)]` where `x = [N, Cin, H, W]`,
908/// `weight = [Cout, Cin/groups, Kh, Kw]`, optional `bias = [Cout]`.
909/// The per-instance `FusedOpParams::Conv2D { stride, padding, groups }`
910/// supply the rest; output spatial dims are recomputed from input
911/// shape + params via the standard `(H + 2·pad - Kh) / stride + 1`.
912pub fn cost_conv2d_cpu(
913    shapes: &[Shape],
914    params: &FusedOpParams,
915    _caps: &BackendCapabilities,
916) -> CostEstimate {
917    debug_assert!(
918        shapes.len() == 2 || shapes.len() == 3,
919        "Conv2D cost: expected 2 or 3 input shapes",
920    );
921    let (stride, padding, groups) = match params {
922        FusedOpParams::Conv2D {
923            stride,
924            padding,
925            groups,
926        } => (*stride, *padding, *groups),
927        _ => return CostEstimate::default(),
928    };
929    let x_dims = shapes[0].dims();
930    let w_dims = shapes[1].dims();
931    if x_dims.len() != 4 || w_dims.len() != 4 || groups == 0 {
932        return CostEstimate::default();
933    }
934    let (n, cin, h_in, w_in) = (
935        x_dims[0] as u64,
936        x_dims[1] as u64,
937        x_dims[2] as u64,
938        x_dims[3] as u64,
939    );
940    let (cout, cin_per_g, kh, kw) = (
941        w_dims[0] as u64,
942        w_dims[1] as u64,
943        w_dims[2] as u64,
944        w_dims[3] as u64,
945    );
946    let (sh, sw) = (stride.0 as u64, stride.1 as u64);
947    let (ph, pw) = (padding.0 as u64, padding.1 as u64);
948    if sh == 0 || sw == 0 || kh > h_in + 2 * ph || kw > w_in + 2 * pw {
949        return CostEstimate::default();
950    }
951    let h_out = (h_in + 2 * ph - kh) / sh + 1;
952    let w_out = (w_in + 2 * pw - kw) / sw + 1;
953    // FMA counts as 2 FLOPs per (Cin/g · Kh · Kw) inner-product step,
954    // summed across N · Cout · Hout · Wout output positions. Bias-add
955    // contributes one FLOP per output element.
956    let conv_flops = 2u64 * n * cout * h_out * w_out * cin_per_g * kh * kw;
957    let bias_flops = if shapes.len() == 3 {
958        n * cout * h_out * w_out
959    } else {
960        0
961    };
962    let elems_in = n * cin * h_in * w_in;
963    let elems_w = cout * cin_per_g * kh * kw;
964    let elems_out = n * cout * h_out * w_out;
965    let elems_bias = if shapes.len() == 3 { cout } else { 0 };
966    let bytes_moved = (elems_in + elems_w + elems_out + elems_bias) * 4;
967    CostEstimate {
968        flops: conv_flops + bias_flops,
969        bytes_moved,
970        kernel_overhead_ns: 50,
971    }
972}
973
974/// Precision guarantee shared by every `(CONV2D, Cpu, *)` impl in
975/// fuel-cpu-backend: textbook nested-loop accumulation in F32 (F64
976/// kernels accumulate in F64; BF16/F16 upcast each multiply to F32
977/// and narrow at end), bit-identical re-run on same hardware.
978/// Same shape as [`FUSED_LINEAR_CPU_PRECISION`] — the conv kernel's
979/// numerical character is governed by the same "F32 accumulator,
980/// deterministic iteration order" properties.
981pub const CONV2D_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
982    bit_stable_on_same_hardware: true,
983    max_ulp: None,
984    max_relative: None,
985    max_absolute: None,
986    notes: "fuel-cpu-backend Conv2D: textbook nested-loop \
987            cross-correlation; F32 accumulator (F64 for F64 input); \
988            BF16/F16 multiply in F32 and narrow at end; same-hardware \
989            re-run bit-identical.",
990};
991
992// =============================================================================
993// Phase 7.6 step 6 — cost functions + PrecisionGuarantees for the
994// remaining 8 ops that have CPU byte-level wrappers in
995// `fuel-storage::dispatch`. The four backward helpers
996// (SoftmaxLastDimBackward / LayerNormLastDimBackward /
997// RmsNormLastDimBackward / ReduceMaxToBackward) are not covered
998// here — their CPU dispatch flows through the `GraphBackend` trait
999// methods in `fuel-graph-executor`, not the byte-level binding
1000// table. Wrapper conversion + step-6 registration for those is a
1001// follow-up.
1002// =============================================================================
1003
1004/// Shared precision guarantee for the norm/softmax family — softmax,
1005/// rms_norm, layer_norm. All three CPU kernels use deterministic
1006/// elementwise + scalar-reduction patterns with F32 accumulator
1007/// (F64 for F64 input; BF16/F16 multiply in F32 and narrow at end).
1008/// Same shape as [`FUSED_LINEAR_CPU_PRECISION`] — the numerical
1009/// character is governed by the same "F32 accumulator, deterministic
1010/// iteration order" properties.
1011pub const NORM_FAMILY_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1012    bit_stable_on_same_hardware: true,
1013    max_ulp: None,
1014    max_relative: None,
1015    max_absolute: None,
1016    notes: "fuel-cpu-backend Softmax/RmsNorm/LayerNorm: deterministic \
1017            elementwise + per-row reduction; F32 accumulator (F64 for \
1018            F64 input); BF16/F16 multiply in F32 and narrow at end; \
1019            same-hardware re-run bit-identical.",
1020};
1021
1022/// Cost model for `(SoftmaxLastDim | RmsNormLastDim | LayerNormLastDim,
1023/// Cpu)` kernels — outer-product structure: `outer × last_dim`
1024/// elementwise pass + scalar reduction. FLOPs scale linearly with
1025/// element count, bandwidth dominates for typical token-dim sizes.
1026///
1027/// Phase 7.6 step 8 — branches on `FusedOpParams` to use a per-op
1028/// FLOP/element count: softmax ≈ 5, rms_norm ≈ 4, layer_norm ≈ 7
1029/// (plus their backwards, which do comparable per-element work).
1030/// Replaces the step-6 midpoint-of-5 estimate. Both forward and
1031/// backward variants of each norm use the same FLOP/element count
1032/// because they touch the data the same way per row.
1033///
1034/// Shapes: `[x]` (forward, 1 input) or `[x_or_y, g]` (backward,
1035/// 2 inputs). Total element count comes from `input[0]` either way.
1036pub fn cost_norm_family_cpu(
1037    shapes: &[Shape],
1038    params: &FusedOpParams,
1039    _caps: &BackendCapabilities,
1040) -> CostEstimate {
1041    debug_assert!(
1042        !shapes.is_empty(),
1043        "Norm-family cost: expected ≥1 input shape",
1044    );
1045    let dims = shapes[0].dims();
1046    if dims.is_empty() {
1047        return CostEstimate::default();
1048    }
1049    let elems: u64 = dims.iter().map(|&d| d as u64).product();
1050    let flops_per_elem: u64 = match params {
1051        FusedOpParams::SoftmaxLastDim | FusedOpParams::SoftmaxLastDimBackward => 5, // max-sub + exp + sum + divide
1052        FusedOpParams::RmsNormLastDim { .. } | FusedOpParams::RmsNormLastDimBackward { .. } => 4, // sqr + sum + sqrt + divide
1053        FusedOpParams::LayerNormLastDim { .. } | FusedOpParams::LayerNormLastDimBackward { .. } => {
1054            7
1055        } // mean-sub + sqr + sum + sqrt + divide + center
1056        _ => 5, // fallback if a future caller mis-dispatches
1057    };
1058    // Backwards have 2 inputs + 1 output (3 element-count touches);
1059    // forwards have 1 input + 1 output (2 element-count touches).
1060    let bandwidth_factor: u64 = if shapes.len() >= 2 { 3 } else { 2 };
1061    CostEstimate {
1062        flops: flops_per_elem * elems,
1063        bytes_moved: bandwidth_factor * elems * 4,
1064        kernel_overhead_ns: 50,
1065    }
1066}
1067
1068/// Precision guarantee for `(ROPE, Cpu, *)` impls. Rotary position
1069/// embedding is a rotation in 2D subspaces — multiplies + adds with
1070/// the cos/sin tables; F32 accumulator semantics match the norm
1071/// family.
1072pub const ROPE_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1073    bit_stable_on_same_hardware: true,
1074    max_ulp: None,
1075    max_relative: None,
1076    max_absolute: None,
1077    notes: "fuel-cpu-backend Rope: per-head rotation `out = x·cos + \
1078            rotated(x)·sin` with deterministic iteration order. \
1079            BF16/F16 multiply in F32 and narrow at end; same-hardware \
1080            re-run bit-identical.",
1081};
1082
1083/// Cost model for `(ROPE, Cpu)` — per-element rotation costs 4 FMAs
1084/// (2 multiplies + 2 adds in two 2D planes).
1085///
1086/// Shapes: `[x, cos, sin]` where `x = [..., seq, head_dim]`,
1087/// `cos = sin = [seq, head_dim]`.
1088pub fn cost_rope_cpu(
1089    shapes: &[Shape],
1090    _params: &FusedOpParams,
1091    _caps: &BackendCapabilities,
1092) -> CostEstimate {
1093    debug_assert_eq!(
1094        shapes.len(),
1095        3,
1096        "Rope cost: expected 3 input shapes (x, cos, sin)"
1097    );
1098    let dims = shapes[0].dims();
1099    if dims.is_empty() {
1100        return CostEstimate::default();
1101    }
1102    let elems: u64 = dims.iter().map(|&d| d as u64).product();
1103    // 4 FLOPs per element (2 FMA pairs across the two rotation planes).
1104    let flops = 4 * elems;
1105    // Read x + cos/sin tables (small), write x_out.
1106    let cs_elems: u64 = shapes[1].dims().iter().map(|&d| d as u64).product::<u64>() * 2;
1107    let bytes_moved = (2 * elems + cs_elems) * 4;
1108    CostEstimate {
1109        flops,
1110        bytes_moved,
1111        kernel_overhead_ns: 50,
1112    }
1113}
1114
1115/// Precision guarantee for `(CONV_TRANSPOSE2D, Cpu, *)` impls.
1116/// Same structure as Conv2D — textbook nested-loop accumulation in
1117/// F32 (F64 for F64 input).
1118pub const CONV_TRANSPOSE2D_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1119    bit_stable_on_same_hardware: true,
1120    max_ulp: None,
1121    max_relative: None,
1122    max_absolute: None,
1123    notes: "fuel-cpu-backend ConvTranspose2D: textbook scatter-with-stride \
1124            + nested loops; F32 accumulator (F64 for F64); BF16/F16 \
1125            multiply in F32 and narrow at end; same-hardware bit-identical.",
1126};
1127
1128/// Cost model for `(CONV_TRANSPOSE2D, Cpu)` — FLOP count parallels
1129/// Conv2D: `2·N·Cout·Hout·Wout·(Cin/g)·Kh·Kw` (each output position
1130/// accumulates contributions from `Cin/g · Kh · Kw` input positions,
1131/// reading transposed).
1132///
1133/// Shapes: `[x, weight]` where `x = [N, Cin, H, W]`,
1134/// `weight = [Cin, Cout/groups, Kh, Kw]` (note transposed channel
1135/// order vs Conv2D).
1136pub fn cost_conv_transpose2d_cpu(
1137    shapes: &[Shape],
1138    params: &FusedOpParams,
1139    _caps: &BackendCapabilities,
1140) -> CostEstimate {
1141    debug_assert_eq!(
1142        shapes.len(),
1143        2,
1144        "ConvTranspose2D cost: expected 2 input shapes"
1145    );
1146    let (stride, padding, output_padding, dilation, groups) = match params {
1147        FusedOpParams::ConvTranspose2D {
1148            stride,
1149            padding,
1150            output_padding,
1151            dilation,
1152            groups,
1153        } => (*stride, *padding, *output_padding, *dilation, *groups),
1154        _ => return CostEstimate::default(),
1155    };
1156    let x_dims = shapes[0].dims();
1157    let w_dims = shapes[1].dims();
1158    if x_dims.len() != 4 || w_dims.len() != 4 || groups == 0 {
1159        return CostEstimate::default();
1160    }
1161    let (n, cin, h_in, w_in) = (
1162        x_dims[0] as u64,
1163        x_dims[1] as u64,
1164        x_dims[2] as u64,
1165        x_dims[3] as u64,
1166    );
1167    let (_, cout_per_g, kh, kw) = (
1168        w_dims[0] as u64,
1169        w_dims[1] as u64,
1170        w_dims[2] as u64,
1171        w_dims[3] as u64,
1172    );
1173    let cout = cout_per_g * groups as u64;
1174    // Output spatial dims: `(H-1)·stride - 2·pad + dilation·(K-1) + out_pad + 1`.
1175    let h_out = h_in.saturating_sub(1) * stride.0 as u64
1176        + dilation.0 as u64 * kh.saturating_sub(1)
1177        + output_padding.0 as u64
1178        + 1;
1179    let h_out = h_out.saturating_sub(2 * padding.0 as u64);
1180    let w_out = w_in.saturating_sub(1) * stride.1 as u64
1181        + dilation.1 as u64 * kw.saturating_sub(1)
1182        + output_padding.1 as u64
1183        + 1;
1184    let w_out = w_out.saturating_sub(2 * padding.1 as u64);
1185    let cin_per_g = cin / groups as u64;
1186    let flops = 2u64 * n * cout * h_out * w_out * cin_per_g * kh * kw;
1187    let elems_in = n * cin * h_in * w_in;
1188    let elems_w = cin * cout_per_g * kh * kw;
1189    let elems_out = n * cout * h_out * w_out;
1190    let bytes_moved = (elems_in + elems_w + elems_out) * 4;
1191    CostEstimate {
1192        flops,
1193        bytes_moved,
1194        kernel_overhead_ns: 50,
1195    }
1196}
1197
1198/// Precision guarantee for `(FLASH_ATTN | PAGED_ATTN, Cpu, *)` impls.
1199/// The CPU naive-attention reference is bit-stable per-hardware
1200/// because both kernels iterate in fixed order. However the tiled
1201/// flash-attention GPU kernel produces different numerics than the
1202/// naive reference (different reduction tree); that's a GPU-side
1203/// PrecisionGuarantee concern — the CPU side stays bit-stable.
1204pub const ATTN_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1205    bit_stable_on_same_hardware: true,
1206    max_ulp: None,
1207    max_relative: None,
1208    max_absolute: None,
1209    notes: "fuel-cpu-backend FlashAttn/PagedAttn: naive scaled-dot- \
1210            product reference; F32 accumulator; deterministic \
1211            iteration order; bit-identical re-run on same hardware. \
1212            GPU kernels produce different numerics (tiled softmax) — \
1213            their PrecisionGuarantee is declared separately when they \
1214            register.",
1215};
1216
1217/// Cost model for `(FLASH_ATTN, Cpu)` and `(PAGED_ATTN, Cpu)` kernels
1218/// — scaled-dot-product attention is `O(B·Hq·Sq·Sk·D)` multiplies
1219/// for the QK matmul + same for the PV matmul, plus softmax (small).
1220///
1221/// For FlashAttn shapes are `[q, k, v, (alibi)]` with
1222/// `q = [B, Hq, Sq, D]`, `k = v = [B, Hkv, Sk, D]`.
1223/// For PagedAttn the cache shapes complicate the calculation; we
1224/// approximate using q's `Sq` × the maximum context length implied
1225/// by k_cache's `num_blocks · block_size`. Both share this function
1226/// because the dominant FLOP term has the same shape.
1227pub fn cost_attn_cpu(
1228    shapes: &[Shape],
1229    _params: &FusedOpParams,
1230    _caps: &BackendCapabilities,
1231) -> CostEstimate {
1232    if shapes.is_empty() {
1233        return CostEstimate::default();
1234    }
1235    let q_dims = shapes[0].dims();
1236    if q_dims.len() != 4 {
1237        return CostEstimate::default();
1238    }
1239    let (b, hq, sq, d) = (
1240        q_dims[0] as u64,
1241        q_dims[1] as u64,
1242        q_dims[2] as u64,
1243        q_dims[3] as u64,
1244    );
1245    // Approximate K-len from input[1] (k or k_cache). For FlashAttn
1246    // k.shape[-2] is Sk; for PagedAttn k_cache.shape is `[num_blocks,
1247    // block_size, Hkv, D]` so the effective Sk per query is bounded
1248    // by `num_blocks · block_size`.
1249    let sk: u64 = if shapes.len() >= 2 {
1250        let k_dims = shapes[1].dims();
1251        if k_dims.len() == 4 {
1252            // FlashAttn shape: [B, Hkv, Sk, D] → Sk at index 2.
1253            // PagedAttn shape: [num_blocks, block_size, Hkv, D] →
1254            // total slots = dim[0] · dim[1]; conservative upper bound.
1255            (k_dims[2] as u64).max(k_dims[0] as u64 * k_dims[1] as u64)
1256        } else {
1257            sq
1258        }
1259    } else {
1260        sq
1261    };
1262    // QK matmul: 2·B·Hq·Sq·Sk·D FLOPs.
1263    // PV matmul: 2·B·Hq·Sq·Sk·D FLOPs.
1264    // Softmax: ~5·B·Hq·Sq·Sk FLOPs (small relative to the matmuls).
1265    let mm_flops = 4u64 * b * hq * sq * sk * d;
1266    let sm_flops = 5u64 * b * hq * sq * sk;
1267    // Bandwidth: q, k, v reads + output write; approximate.
1268    let elems_qkv = b * hq * sq * d + 2 * b * hq * sk * d;
1269    let elems_out = b * hq * sq * d;
1270    let bytes_moved = (elems_qkv + elems_out) * 4;
1271    CostEstimate {
1272        flops: mm_flops + sm_flops,
1273        bytes_moved,
1274        // Attention has higher launch overhead than elementwise.
1275        kernel_overhead_ns: 200,
1276    }
1277}
1278
1279/// Precision guarantee for `(FLASH_ATTN_BACKWARD_{Q,K,V}, Cpu, *)`
1280/// impls. The CPU backward is the math-definition gradient: it
1281/// recomputes the forward softmax probabilities row-by-row and
1282/// accumulates dQ/dK/dV with sequential nested loops — fixed
1283/// iteration order, no atomics, no parallel reductions. F32/F64 run
1284/// natively; BF16/F16 accumulate in F32 and narrow on store.
1285/// Bit-identical re-run on same hardware.
1286pub const ATTN_BACKWARD_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1287    bit_stable_on_same_hardware: true,
1288    max_ulp: None,
1289    max_relative: None,
1290    max_absolute: None,
1291    notes: "fuel-cpu-backend FlashAttnBackward{Q,K,V}: math-definition \
1292            gradient with recomputed softmax; sequential nested-loop \
1293            accumulation (F32 accumulator for half types); \
1294            deterministic iteration order; bit-identical re-run on \
1295            same hardware. GPU backward kernels (tiled, atomics) \
1296            declare their own PrecisionGuarantee when they register.",
1297};
1298
1299/// Cost model for `(FLASH_ATTN_BACKWARD_{Q,K,V}, Cpu)` kernels. The
1300/// math-definition backward recomputes the attention probabilities
1301/// (one QKᵀ matmul) and then forms dV, dP, dQ and dK — five
1302/// `O(B·Hq·Sq·Sk·D)` matmul-shaped passes vs the forward's two.
1303/// The CPU byte wrapper additionally computes ALL THREE gradients
1304/// per call and persists only the requested one (the Q/K/V OpKind
1305/// split has one output each), so the per-call cost does not shrink
1306/// with the selected gradient.
1307///
1308/// Shapes are `[q, k, v, do, (alibi)]` with `q = do = [B, Hq, Sq, D]`,
1309/// `k = v = [B, Hkv, Sk, D]`.
1310pub fn cost_attn_backward_cpu(
1311    shapes: &[Shape],
1312    _params: &FusedOpParams,
1313    _caps: &BackendCapabilities,
1314) -> CostEstimate {
1315    if shapes.is_empty() {
1316        return CostEstimate::default();
1317    }
1318    let q_dims = shapes[0].dims();
1319    if q_dims.len() != 4 {
1320        return CostEstimate::default();
1321    }
1322    let (b, hq, sq, d) = (
1323        q_dims[0] as u64,
1324        q_dims[1] as u64,
1325        q_dims[2] as u64,
1326        q_dims[3] as u64,
1327    );
1328    let sk: u64 = if shapes.len() >= 2 {
1329        let k_dims = shapes[1].dims();
1330        if k_dims.len() == 4 {
1331            k_dims[2] as u64
1332        } else {
1333            sq
1334        }
1335    } else {
1336        sq
1337    };
1338    // Five matmul-shaped passes (P recompute, dV, dP, dQ, dK):
1339    // 2·B·Hq·Sq·Sk·D FLOPs each.
1340    let mm_flops = 10u64 * b * hq * sq * sk * d;
1341    // Softmax recompute + dS row reductions: ~8·B·Hq·Sq·Sk.
1342    let sm_flops = 8u64 * b * hq * sq * sk;
1343    // Bandwidth: q + do reads, k + v reads (Hkv ≈ Hq approximation,
1344    // mirroring cost_attn_cpu), plus all three gradient buffers
1345    // written (the wrapper materializes dQ, dK and dV internally
1346    // even though only one persists).
1347    let elems_read = 2 * b * hq * sq * d + 2 * b * hq * sk * d;
1348    let elems_written = b * hq * sq * d + 2 * b * hq * sk * d;
1349    let bytes_moved = (elems_read + elems_written) * 4;
1350    CostEstimate {
1351        flops: mm_flops + sm_flops,
1352        bytes_moved,
1353        // Same launch-overhead class as the attention forward.
1354        kernel_overhead_ns: 200,
1355    }
1356}
1357
1358/// Precision guarantee for `(QMATMUL, Cpu, *)` impls. Quantized
1359/// matmul dequantizes inline; the dequant arithmetic is exact for
1360/// the quantization scheme's block format, and the F32 matmul
1361/// accumulator stays deterministic. Bit-identical on same hardware.
1362pub const QMATMUL_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1363    bit_stable_on_same_hardware: true,
1364    max_ulp: None,
1365    max_relative: None,
1366    max_absolute: None,
1367    notes: "fuel-cpu-backend QMatMul: per-block dequant + F32 matmul \
1368            accumulate; deterministic iteration order; bit-identical \
1369            re-run on same hardware. Inherent precision loss from \
1370            the quantization itself is a property of the QuantType, \
1371            not the kernel — captured separately when the calibration \
1372            framework lands.",
1373};
1374
1375/// Precision guarantee for `(REDUCE_MAX_TO_BACKWARD, Cpu, *)` impls.
1376/// The kernel recomputes the forward max, builds a tie-mask,
1377/// counts ties via reduce_sum, divides upstream by the count, then
1378/// broadcasts back and gates by the mask. All deterministic on
1379/// same hardware (no atomics, no parallel reductions); F32
1380/// accumulator for half-precision dtypes via the shared
1381/// `reduce_max_to_backward_impl` adapter.
1382pub const REDUCE_MAX_TO_BACKWARD_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1383    bit_stable_on_same_hardware: true,
1384    max_ulp: None,
1385    max_relative: None,
1386    max_absolute: None,
1387    notes: "fuel-cpu-backend ReduceMaxTo backward: recomputed-max + \
1388            tie-mask + scale + gate; deterministic iteration order; \
1389            F32 accumulator for half-precision; bit-identical same-\
1390            hardware re-run.",
1391};
1392
1393/// Precision guarantee for `(INPLACE_AFFINE, Cpu)`. Single-pass
1394/// `x = mul · x + add` per element with no parallel accumulation;
1395/// deterministic iteration order; bit-identical same-hardware re-run.
1396/// The half-precision dtypes (bf16/f16) pivot through f32 for the
1397/// arithmetic, identical to the non-inplace Affine family.
1398pub const INPLACE_AFFINE_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1399    bit_stable_on_same_hardware: true,
1400    max_ulp: None,
1401    max_relative: None,
1402    max_absolute: None,
1403    notes: "fuel-cpu-backend in-place affine: single-pass `mul · x + \
1404            add` per element; deterministic iteration order; F32 pivot \
1405            for half-precision dtypes; bit-identical same-hardware re-run.",
1406};
1407
1408/// Cost model for `(INPLACE_AFFINE, Cpu)`. Single pass: 1 FMA per
1409/// element. Bandwidth is one read + one write per element (in place,
1410/// so the buffer is touched once for read + once for write through
1411/// the same cache line).
1412pub fn cost_inplace_affine_cpu(
1413    shapes: &[Shape],
1414    _params: &FusedOpParams,
1415    _caps: &BackendCapabilities,
1416) -> CostEstimate {
1417    debug_assert_eq!(
1418        shapes.len(),
1419        1,
1420        "InplaceAffine cost: expected 1 input shape"
1421    );
1422    let n: u64 = shapes[0].dims().iter().map(|&d| d as u64).product();
1423    // mul + add per element = 1 FMA. Treat as 1 FLOP for simplicity
1424    // (matches Affine's accounting; the fused mul-add is one tier-2
1425    // float op).
1426    let flops = n;
1427    // In-place: the buffer is read once + written once. Use
1428    // dtype-agnostic 4 bytes/elem default — the cost model's caller
1429    // scales by the actual dtype size if it cares.
1430    let bytes_moved = n * 4 * 2;
1431    CostEstimate {
1432        flops,
1433        bytes_moved,
1434        kernel_overhead_ns: 50,
1435    }
1436}
1437
1438/// Precision guarantee for `(POWI_BACKWARD, Cpu)`. Per-element
1439/// `grad_x = exp · x^(exp-1) · upstream` with the same floating-point
1440/// determinism guarantees as the forward PowI primitive: deterministic
1441/// iteration order, no parallel reductions, F32 compute for half-
1442/// precision dtypes via the shared `powi_backward_half_kernel`.
1443pub const POWI_BACKWARD_CPU_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1444    bit_stable_on_same_hardware: true,
1445    max_ulp: None,
1446    max_relative: None,
1447    max_absolute: None,
1448    notes: "fuel-cpu-backend PowI backward: single-pass `exp · \
1449            x.powi(exp-1) · upstream` per element; deterministic \
1450            iteration order; F32 compute for half-precision; bit-\
1451            identical same-hardware re-run.",
1452};
1453
1454/// Cost model for `(POWI_BACKWARD, Cpu)` — single pass over the input:
1455/// per-element `powi(exp-1)` (≈ log2(|exp|) FMAs) + 2 multiplies. Bandwidth
1456/// dominated by reading `x` + `upstream` and writing `grad_x`.
1457pub fn cost_powi_backward_cpu(
1458    shapes: &[Shape],
1459    params: &FusedOpParams,
1460    _caps: &BackendCapabilities,
1461) -> CostEstimate {
1462    debug_assert_eq!(
1463        shapes.len(),
1464        2,
1465        "PowIBackward cost: expected 2 input shapes"
1466    );
1467    let in_count: u64 = shapes[0].dims().iter().map(|&d| d as u64).product();
1468    let exp_abs = match params {
1469        FusedOpParams::PowIBackward { exp } => (*exp).unsigned_abs().max(1) as u64,
1470        _ => 1,
1471    };
1472    // power-by-squaring: ceil(log2(|exp|)) multiplies for the powi,
1473    // +2 multiplies for the coefficient and upstream factors.
1474    let powi_muls = 64 - exp_abs.leading_zeros() as u64;
1475    let flops = in_count * (powi_muls + 2);
1476    let bytes_moved = in_count * 4 /* x */ + in_count * 4 /* upstream */ + in_count * 4 /* grad_x */;
1477    CostEstimate {
1478        flops,
1479        bytes_moved,
1480        kernel_overhead_ns: 60,
1481    }
1482}
1483
1484/// Cost model for `(REDUCE_MAX_TO_BACKWARD, Cpu)` — five passes over
1485/// the input: forward max, mask build, count, scale, broadcast +
1486/// gate. Roughly `5 · in_count` FLOPs, with bandwidth dominated by
1487/// 5× the input element count.
1488pub fn cost_reduce_max_to_backward_cpu(
1489    shapes: &[Shape],
1490    _params: &FusedOpParams,
1491    _caps: &BackendCapabilities,
1492) -> CostEstimate {
1493    debug_assert_eq!(
1494        shapes.len(),
1495        2,
1496        "ReduceMaxToBackward cost: expected 2 input shapes"
1497    );
1498    let in_count: u64 = shapes[0].dims().iter().map(|&d| d as u64).product();
1499    let out_count: u64 = shapes[1].dims().iter().map(|&d| d as u64).product();
1500    let flops = 5 * in_count + 2 * out_count;
1501    let bytes_moved = (5 * in_count + 3 * out_count) * 4;
1502    CostEstimate {
1503        flops,
1504        bytes_moved,
1505        kernel_overhead_ns: 80,
1506    }
1507}
1508
1509/// Cost model for `(QMATMUL, Cpu)` — same FLOP count as a regular
1510/// matmul (the dequant adds a small per-block overhead that's
1511/// dwarfed by the FMA count for any useful K).
1512///
1513/// Shapes: `[a, w_q_bytes]` where `a = [..., M, K]`. `N` comes from
1514/// the FusedOpParams::QMatMul payload.
1515pub fn cost_qmatmul_cpu(
1516    shapes: &[Shape],
1517    params: &FusedOpParams,
1518    _caps: &BackendCapabilities,
1519) -> CostEstimate {
1520    debug_assert_eq!(shapes.len(), 2, "QMatMul cost: expected 2 input shapes");
1521    let (k, n) = match params {
1522        FusedOpParams::QMatMul { k, n, .. } => (*k as u64, *n as u64),
1523        _ => return CostEstimate::default(),
1524    };
1525    let a_dims = shapes[0].dims();
1526    let rank = a_dims.len();
1527    if rank < 2 {
1528        return CostEstimate::default();
1529    }
1530    let m = a_dims[rank - 2] as u64;
1531    let batch: u64 = a_dims[..rank - 2]
1532        .iter()
1533        .map(|&d| d as u64)
1534        .product::<u64>()
1535        .max(1);
1536    // 2·M·N·K FLOPs per batch (FMA).
1537    let flops = 2 * batch * m * n * k;
1538    // Bandwidth: read A + W_Q (counted as bytes via shapes[1]
1539    // elem_count of U32 = 4 bytes), write C.
1540    let a_elems = batch * m * k;
1541    let w_u32_elems: u64 = shapes[1].dims().iter().map(|&d| d as u64).product();
1542    let c_elems = batch * m * n;
1543    let bytes_moved = (a_elems + c_elems) * 4 + w_u32_elems * 4;
1544    CostEstimate {
1545        flops,
1546        bytes_moved,
1547        kernel_overhead_ns: 50,
1548    }
1549}
1550
1551// =============================================================================
1552// Vulkan PrecisionGuarantee constants (Phase 7.6 step 9c follow-up,
1553// 2026-05-23). Per-kernel precision claims so the optimizer's
1554// tolerance-budget pass can admit Vulkan alternatives. The constants
1555// group Vulkan kernels by their numerical character: pointwise IEEE-
1556// 754 ops (bit-stable, ~1 ULP), half-precision pointwise (bit-stable
1557// at lower mantissa), transcendentals (vendor lib ~2-4 ULP),
1558// reductions (NOT bit-stable due to subgroup composition), matmul
1559// (deterministic accumulation), tensor-core matmul (FMA chains),
1560// byte-level (memcpy, bit-identical), cast, and qmatmul.
1561// =============================================================================
1562
1563/// Vulkan f32 / f64 pointwise ops — Add, Sub, Mul, Div, Maximum,
1564/// Minimum, Neg, Sqr, Sqrt, Relu, Step, Affine, Clamp.
1565///
1566/// IEEE-754 conformant per the Vulkan SPIR-V spec: direct hardware
1567/// FADD / FMUL / FMA per thread, no atomic accumulators, no
1568/// cross-thread communication. Bit-stable on same hardware (kernel
1569/// scheduling doesn't affect per-thread outputs). ULP is the
1570/// standard IEEE-754 round-to-nearest tolerance (≤0.5 ULP for FADD/
1571/// FMUL/FDIV, ≤1 ULP for FMA chains).
1572///
1573/// Not REFERENCE-grade because we don't claim ULP=0 across all
1574/// implementations — Vulkan compilers may reorder commutative ops
1575/// or fuse multiply-adds in ways that differ from a strict
1576/// IEEE-754-strict reference. Within those bounds, every conforming
1577/// Vulkan implementation produces results within 1 ULP.
1578pub const VULKAN_FLOAT_POINTWISE_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1579    bit_stable_on_same_hardware: true,
1580    max_ulp: Some(1),
1581    max_relative: None,
1582    max_absolute: None,
1583    notes: "fuel-vulkan-backend f32/f64 pointwise: direct hardware FADD/\
1584            FMUL/FMA per thread; no atomics; bit-stable on same hardware. \
1585            Within 1 ULP of IEEE-754 round-to-nearest; small variation \
1586            across implementations due to commutative-reorder + FMA fusion.",
1587};
1588
1589/// Vulkan f16 / bf16 pointwise ops — same op surface as
1590/// [`VULKAN_FLOAT_POINTWISE_PRECISION`] at half precision. f16 uses
1591/// shaderFloat16 native ops (where available); bf16 packs two values
1592/// per u32 with software upcast/downcast through f32 for the arithmetic.
1593///
1594/// Bit-stable on same hardware (no atomics, deterministic per-thread).
1595/// ULP claim is 1 in the dtype's mantissa space — meaningful for f16
1596/// (10-bit mantissa) and bf16 (7-bit mantissa) precision budgeting.
1597/// Absolute error scales with magnitude × 2^(-mantissa_bits).
1598pub const VULKAN_HALF_POINTWISE_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1599    bit_stable_on_same_hardware: true,
1600    max_ulp: Some(1),
1601    max_relative: None,
1602    max_absolute: None,
1603    notes: "fuel-vulkan-backend f16/bf16 pointwise: shaderFloat16 native or \
1604            u32-packed bf16; bit-stable on same hardware; within 1 ULP in \
1605            the dtype's mantissa space (10-bit f16, 7-bit bf16).",
1606};
1607
1608/// Vulkan transcendentals — Exp, Log, Sin, Cos, Tanh, Sigmoid, Silu,
1609/// Gelu (f32/f16/bf16/f64). f32/f16 go through GLSL.std.450's vendor-
1610/// library implementations (variable ULP per GPU/driver); f64 uses
1611/// the Horner-polynomial approximations in `unary_f64.slang`
1612/// (target ~1e-12 relative error).
1613///
1614/// Bit-stable on same hardware (driver-deterministic; the
1615/// transcendental implementation is fixed per build). ULP bound is
1616/// loose: Vulkan spec allows 3-4 ULP for GLSL.std.450 transcendentals;
1617/// real implementations are typically tighter (~1-2 ULP on NVIDIA's
1618/// libdevice).
1619pub const VULKAN_TRANSCENDENTAL_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1620    bit_stable_on_same_hardware: true,
1621    max_ulp: Some(4),
1622    max_relative: None,
1623    max_absolute: None,
1624    notes: "fuel-vulkan-backend transcendentals: GLSL.std.450 vendor lib \
1625            (f32/f16/bf16) or Horner polynomial (f64); bit-stable on same \
1626            hardware; Vulkan spec allows ≤4 ULP, real GPUs typically tighter.",
1627};
1628
1629// Vulkan reductions — SumReduce, MaxReduce, MinReduce, MeanReduce,
1630// SoftmaxLastDim, RmsNormLastDim, LayerNormLastDim, and any kernel
1631// that uses subgroup tree reductions — use `PrecisionGuarantee::
1632// UNAUDITED` (audited result of "no static bound to claim"). The
1633// reasoning: subgroup composition is scheduler-determined per
1634// dispatch, so reordering FADDs across subgroups produces different
1635// floating-point results; no static ULP / relative / absolute bound
1636// applies. Their (op, dtypes, Vulkan) tuples are listed in the
1637// `vulkan_dispatch_per_kernel_precision_and_cost_coverage` lint's
1638// `KNOWN_GAPS` allowlist so the lint accepts UNAUDITED for these
1639// specific kernels; new Vulkan reduction kernels must be added to
1640// KNOWN_GAPS at registration time. The architectural decision is
1641// thus visible at review time rather than buried in a per-kernel
1642// constant.
1643
1644/// Vulkan matmul (f32 standard, no tensor cores) — tiled / reg-tile /
1645/// matvec kernels. Accumulation order is deterministic per kernel
1646/// dispatch (each output element's reduction runs in a single thread
1647/// with no cross-thread atomic adds); the kernel is bit-stable on
1648/// same hardware.
1649///
1650/// ULP claim left unspecified; the per-element error is bounded by
1651/// `K · ULP(2 · max_abs_product)` from the K-deep FMA chain.
1652pub const VULKAN_MATMUL_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1653    bit_stable_on_same_hardware: true,
1654    max_ulp: None,
1655    max_relative: None,
1656    max_absolute: None,
1657    notes: "fuel-vulkan-backend f32 matmul: tiled / reg-tile / matvec \
1658            kernels with per-thread sequential FMA accumulation; bit-stable \
1659            on same hardware; error bounded by O(K · ULP) from K-deep \
1660            reduction.",
1661};
1662
1663/// Vulkan tensor-core mixed-precision matmul (f32 × bf16 → f32, via
1664/// `matmul_coop` cooperative kernels on Ampere+ tensor cores).
1665///
1666/// Tensor-core FMAs accumulate in extended precision (f32 internal,
1667/// IEEE-754 conformant FMA chain); the cross-tile reduction happens
1668/// with full f32 precision. Bit-stable on same hardware per the
1669/// Ampere/Ada spec (the tensor cores are deterministic given the
1670/// same inputs + scheduling). ULP claim is wider than pure f32
1671/// because the bf16 inputs lose 16 mantissa bits before the FMA.
1672pub const VULKAN_MATMUL_TENSORCORE_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1673    bit_stable_on_same_hardware: true,
1674    max_ulp: None,
1675    max_relative: None,
1676    max_absolute: None,
1677    notes: "fuel-vulkan-backend tensor-core matmul (f32 × bf16 → f32): \
1678            cooperative-matrix FMAs with f32 internal accumulator; bit-stable \
1679            on same hardware; per-tile error bounded by O(K · ULP_bf16 + \
1680            tile_K · ULP_f32).",
1681};
1682
1683/// Vulkan byte-level (memcpy-style) ops — Triu, Tril, Flip, Roll,
1684/// WriteSlice, IndexSelect, Concat, Copy (D2H/H2D). Pure data movement
1685/// with no FP arithmetic; output bytes are bit-identical to input
1686/// bytes selected/permuted according to the op.
1687///
1688/// Bit-stable on same hardware, ULP/relative/absolute = 0 (byte-level
1689/// equivalence). The strongest precision claim, equivalent to
1690/// REFERENCE but earned through "no FP math" rather than "explicit
1691/// IEEE-754 evaluation."
1692pub const VULKAN_BYTE_LEVEL_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1693    bit_stable_on_same_hardware: true,
1694    max_ulp: Some(0),
1695    max_relative: Some(0.0),
1696    max_absolute: Some(0.0),
1697    notes: "fuel-vulkan-backend byte-level ops (Triu/Tril/Flip/Roll/\
1698            WriteSlice/IndexSelect/Concat/Copy): pure data movement, no FP \
1699            math; bit-identical to input bytes.",
1700};
1701
1702/// Vulkan dtype cast — f32↔f16, f32↔bf16, F8E4M3↔{f32,f16,bf16}.
1703/// Pure conversion with no accumulation; output is the IEEE-754
1704/// round-to-nearest representation of the input in the target dtype.
1705///
1706/// Bit-stable on same hardware (no atomics, no cross-thread comm).
1707/// ULP=0 vs the IEEE-754 round-to-nearest reference for the target
1708/// dtype (the cast itself is exact within the target's representable
1709/// range; any precision loss is inherent to the dtype, not the
1710/// implementation).
1711pub const VULKAN_CAST_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1712    bit_stable_on_same_hardware: true,
1713    max_ulp: Some(0),
1714    max_relative: None,
1715    max_absolute: None,
1716    notes: "fuel-vulkan-backend Cast: pure dtype conversion via SPIR-V \
1717            OpFConvert / OpConvertFToS / OpConvertSToF; bit-stable on same \
1718            hardware; ULP=0 vs IEEE-754 round-to-nearest in target dtype \
1719            (loss is inherent to the target dtype, not the implementation).",
1720};
1721
1722/// Vulkan QMatMul (Q4_0 / Q4_K_M / Q8_0 weights × F32 activation).
1723/// Per-block dequant + per-output-element FMA accumulation; same
1724/// deterministic structure as the CPU QMatMul kernel (bit-stable on
1725/// same hardware).
1726///
1727/// The inherent precision loss from quantization is a property of
1728/// the QuantType (captured separately when the calibration framework
1729/// lands); the kernel itself contributes only the matmul accumulation
1730/// error.
1731pub const VULKAN_QMATMUL_PRECISION: PrecisionGuarantee = PrecisionGuarantee {
1732    bit_stable_on_same_hardware: true,
1733    max_ulp: None,
1734    max_relative: None,
1735    max_absolute: None,
1736    notes: "fuel-vulkan-backend QMatMul (Q4_0/Q4_K_M/Q8_0 × F32): per-block \
1737            dequant + per-output-element FMA; bit-stable on same hardware. \
1738            Quantization precision loss is a QuantType property, not the \
1739            kernel's contribution.",
1740};
1741
1742#[cfg(test)]
1743mod tests {
1744    use super::*;
1745    use fuel_ir::{DType, Layout, Result};
1746    use std::sync::Arc;
1747    use std::sync::RwLock;
1748
1749    fn dummy_kernel(
1750        _inputs: &[Arc<RwLock<fuel_memory::Storage>>],
1751        _outputs: &mut [Arc<RwLock<fuel_memory::Storage>>],
1752        _layouts: &[Layout],
1753        _params: &crate::kernel::OpParams,
1754    ) -> Result<()> {
1755        Ok(())
1756    }
1757
1758    fn dummy_cost(_s: &[Shape], _p: &FusedOpParams, _c: &BackendCapabilities) -> CostEstimate {
1759        CostEstimate::default()
1760    }
1761
1762    const DUMMY_DTYPES: &[DType] = &[DType::F32];
1763
1764    fn make_impl() -> BackendImpl {
1765        BackendImpl {
1766            kernel: dummy_kernel,
1767            dtypes: DUMMY_DTYPES,
1768            cost: dummy_cost,
1769            precision: PrecisionGuarantee::UNAUDITED,
1770            caps: KernelCaps::empty(),
1771            revision: KernelRevisionHash::UNTRACKED,
1772            cost_expr: None,
1773        }
1774    }
1775
1776    /// Smoke: empty registry has no impls.
1777    #[test]
1778    fn fused_kernel_registry_empty() {
1779        let r = FusedKernelRegistry::new();
1780        assert!(r.is_empty());
1781        assert_eq!(r.len(), 0);
1782        assert!(r.lookup(FusedOpId(1), BackendId::Cpu).is_none());
1783        assert!(r.impls_for(FusedOpId(1)).is_empty());
1784        // Suppress unused-warning for DType import on no-feature builds.
1785        let _ = DType::F32;
1786    }
1787
1788    /// Smoke: register and look up a single impl.
1789    #[test]
1790    fn fused_kernel_registry_register_and_lookup() {
1791        let mut r = FusedKernelRegistry::new();
1792        r.register(FusedOpId(1), BackendId::Cpu, make_impl());
1793        assert!(!r.is_empty());
1794        assert_eq!(r.len(), 1);
1795        let got = r.lookup(FusedOpId(1), BackendId::Cpu);
1796        assert!(got.is_some());
1797        let impls = r.impls_for(FusedOpId(1));
1798        assert_eq!(impls.len(), 1);
1799        assert_eq!(impls[0].0, BackendId::Cpu);
1800    }
1801
1802    /// Re-registering at the same `(id, backend)` appends — multiple
1803    /// alternatives at the same decision point are a feature (e.g.
1804    /// cuBLAS vs CUTLASS bf16 matmul at the same dtype). The route
1805    /// picker filters by dtypes via `lookup_by_dtypes` and ranks
1806    /// remaining alternatives.
1807    #[test]
1808    fn fused_kernel_registry_register_appends_alternatives() {
1809        let mut r = FusedKernelRegistry::new();
1810        r.register(FusedOpId(1), BackendId::Cpu, make_impl());
1811        r.register(FusedOpId(1), BackendId::Cpu, make_impl());
1812        assert_eq!(r.len(), 1, "one id");
1813        assert_eq!(
1814            r.impls_for(FusedOpId(1)).len(),
1815            2,
1816            "two alternatives registered",
1817        );
1818    }
1819
1820    /// PrecisionGuarantee::REFERENCE has the strongest properties.
1821    #[test]
1822    fn precision_guarantee_reference_is_strict() {
1823        let p = PrecisionGuarantee::REFERENCE;
1824        assert!(p.bit_stable_on_same_hardware);
1825        assert_eq!(p.max_ulp, Some(0));
1826    }
1827
1828    /// CostEstimate::default is all-zero.
1829    #[test]
1830    fn cost_estimate_default_zero() {
1831        let c = CostEstimate::default();
1832        assert_eq!(c.flops, 0);
1833        assert_eq!(c.bytes_moved, 0);
1834        assert_eq!(c.kernel_overhead_ns, 0);
1835    }
1836
1837    /// Step 6 — `default_kernel_registry` populates the four CPU
1838    /// FusedLinear impls under `FUSED_LINEAR × Cpu × (F32|F64|BF16|F16)`.
1839    #[test]
1840    fn default_kernel_registry_has_fused_linear_cpu_quartet() {
1841        use fuel_graph::registry::FusedOps;
1842
1843        let r = default_kernel_registry();
1844        let impls = r.impls_for(FusedOps::FUSED_LINEAR);
1845        assert_eq!(
1846            impls.len(),
1847            4,
1848            "expected 4 FusedLinear × Cpu × dtype registrations, got {}",
1849            impls.len(),
1850        );
1851        for (backend, _) in impls {
1852            assert_eq!(*backend, BackendId::Cpu);
1853        }
1854        // Per-dtype lookup hits each registration.
1855        for dtype in [DType::F32, DType::F64, DType::BF16, DType::F16] {
1856            let want = [dtype; 4];
1857            let got = r.lookup_by_dtypes(FusedOps::FUSED_LINEAR, BackendId::Cpu, &want);
1858            assert!(
1859                got.is_some(),
1860                "lookup_by_dtypes missed FusedLinear × Cpu × {dtype:?}",
1861            );
1862            let impl_ = got.unwrap();
1863            assert!(
1864                impl_.precision.bit_stable_on_same_hardware,
1865                "CPU FusedLinear should be bit-stable on same hardware",
1866            );
1867        }
1868    }
1869
1870    /// Step 6 (Conv2D extension) — `default_kernel_registry` populates
1871    /// eight CPU Conv2D impls: four dtypes × {no-bias, with-bias}.
1872    /// The route picker filters by the dtype tuple length to dispatch
1873    /// the right variant per call site.
1874    #[test]
1875    fn default_kernel_registry_has_conv2d_cpu_octet() {
1876        use fuel_graph::registry::FusedOps;
1877
1878        let r = default_kernel_registry();
1879        let impls = r.impls_for(FusedOps::CONV2D);
1880        assert_eq!(
1881            impls.len(),
1882            8,
1883            "expected 8 Conv2D × Cpu × dtype × (no-bias|with-bias) \
1884             registrations, got {}",
1885            impls.len(),
1886        );
1887        for (backend, _) in impls {
1888            assert_eq!(*backend, BackendId::Cpu);
1889        }
1890        // No-bias (rank-3) and with-bias (rank-4) lookups both hit per dtype.
1891        for dtype in [DType::F32, DType::F64, DType::BF16, DType::F16] {
1892            let no_bias = [dtype; 3];
1893            let with_bias = [dtype; 4];
1894            assert!(
1895                r.lookup_by_dtypes(FusedOps::CONV2D, BackendId::Cpu, &no_bias)
1896                    .is_some(),
1897                "lookup_by_dtypes missed Conv2D × Cpu × {dtype:?} (no-bias)",
1898            );
1899            let with_bias_impl = r.lookup_by_dtypes(FusedOps::CONV2D, BackendId::Cpu, &with_bias);
1900            assert!(
1901                with_bias_impl.is_some(),
1902                "lookup_by_dtypes missed Conv2D × Cpu × {dtype:?} (with-bias)",
1903            );
1904            let impl_ = with_bias_impl.unwrap();
1905            assert!(
1906                impl_.precision.bit_stable_on_same_hardware,
1907                "CPU Conv2D should be bit-stable on same hardware",
1908            );
1909        }
1910    }
1911
1912    /// Phase 7.6 step 7 — the architecture v1.0 §05 "always-built
1913    /// backend bit-stable coverage commitment" lint.
1914    ///
1915    /// Every fused op registered in
1916    /// `fuel_graph::registry::default_registry()` MUST have at least
1917    /// one CPU `BackendImpl` in
1918    /// `fuel_memory::fused::default_kernel_registry()` with
1919    /// `precision.bit_stable_on_same_hardware == true`. This is the
1920    /// architecture's correctness anchor: the always-built backend
1921    /// gives every downstream consumer a deterministic
1922    /// implementation to fall back on, so cross-backend equivalence
1923    /// tests have a fixed reference.
1924    ///
1925    /// The lint runs as a unit test so violations surface in CI
1926    /// rather than at runtime. Adding a new `FusedOpEntry` without
1927    /// a matching CPU registration fails this test immediately.
1928    ///
1929    /// As of step-6 follow-up (2026-05-11), the previously-empty-
1930    /// covered backward helpers (SoftmaxLastDimBackward,
1931    /// LayerNormLastDimBackward, RmsNormLastDimBackward,
1932    /// ReduceMaxToBackward) gained byte-level CPU wrappers +
1933    /// binding-table registrations + BackendImpls. The `KNOWN_GAPS`
1934    /// allowlist is therefore **empty** — every registered fused
1935    /// op has bit-stable CPU coverage.
1936    ///
1937    /// Note on primitive Ops: this lint covers the **fused-op**
1938    /// registry only. Primitive ops (Op::Add, Op::MatMul, etc.)
1939    /// dispatch through `KernelBindingTable` and don't carry a
1940    /// `PrecisionGuarantee` field today. Extending the architecture
1941    /// commitment to primitives is step 7b — pending a binding-table
1942    /// refactor or a parallel PrecisionGuarantee side-table per
1943    /// OpKind.
1944    #[test]
1945    fn precision_guarantee_lint_bit_stable_cpu_coverage() {
1946        use fuel_graph::registry::{FusedOpId, FusedOps, default_registry};
1947
1948        // If a future commit introduces a new entry without immediate
1949        // CPU coverage, add it here with a documented reason AND file
1950        // a follow-up to remove the line. This check already detects
1951        // its own expiry: if a CPU registration later appears for an
1952        // allowlisted id, the `has_cpu` check below fails loudly
1953        // rather than silently granting double coverage.
1954        //
1955        // AwqMatmul (fuel-graph's AWQ weight-only GEMM op) ships
1956        // decompose-only in this increment: its dequant-then-matmul
1957        // decompose already provides correctness on every backend via
1958        // existing primitive ops, which independently carry their own
1959        // CPU coverage -- but this lint checks for a DIRECT
1960        // binding-table CPU registration of the fused op itself,
1961        // which AwqMatmul deliberately does not have yet. Reaching
1962        // the real, measured CUDA kernel
1963        // (fuel-cuda-backend/src/baracuda/quant_w4a16.rs::awq_gemm_f16)
1964        // is separate follow-on work named in the op's own module doc;
1965        // a CPU binding-table wrapper is the same class of follow-up.
1966        const KNOWN_GAPS: &[(FusedOpId, &str)] = &[(
1967            FusedOps::AWQ_MATMUL,
1968            "decompose-only in this increment; no direct CPU binding-table \
1969             registration yet -- see fuel-graph/src/registry/awq_matmul.rs's \
1970             module doc for the follow-on dispatch-wiring work",
1971        )];
1972
1973        let meta = default_registry();
1974        let kernels = default_kernel_registry();
1975
1976        let mut failures: Vec<String> = Vec::new();
1977        let mut covered = 0usize;
1978        let mut allowlisted = 0usize;
1979
1980        for entry in meta.entries_iter() {
1981            let id = entry.id;
1982            let name = entry.name;
1983
1984            if let Some((_, reason)) = KNOWN_GAPS.iter().find(|(g, _)| *g == id) {
1985                allowlisted += 1;
1986                // Sanity: a gap entry should have no CPU coverage.
1987                // If a CPU registration shows up for an
1988                // allowlisted id, the gap entry should be removed.
1989                let has_cpu = kernels
1990                    .impls_for(id)
1991                    .iter()
1992                    .any(|(b, _)| *b == BackendId::Cpu);
1993                if has_cpu {
1994                    failures.push(format!(
1995                        "FusedOpId {id:?} ({name}) is on the KNOWN_GAPS allowlist \
1996                         but DOES have a CPU registration now. Reason given was: \
1997                         {reason:?}. Remove the allowlist entry to enable the \
1998                         bit-stable lint.",
1999                    ));
2000                }
2001                continue;
2002            }
2003
2004            let impls = kernels.impls_for(id);
2005            let has_bit_stable_cpu = impls.iter().any(|(backend, impl_)| {
2006                *backend == BackendId::Cpu && impl_.precision.bit_stable_on_same_hardware
2007            });
2008            if has_bit_stable_cpu {
2009                covered += 1;
2010            } else {
2011                failures.push(format!(
2012                    "FusedOpId {id:?} ({name}) has no bit-stable CPU BackendImpl. \
2013                     Architecture v1.0 §05 requires the always-built backend \
2014                     (fuel-cpu-backend) to provide at least one \
2015                     `bit_stable_on_same_hardware: true` kernel per registered \
2016                     fused op. Either add a CPU registration in \
2017                     fuel-storage::dispatch::register_default_fused_kernels with \
2018                     a matching PrecisionGuarantee, or add a line to KNOWN_GAPS \
2019                     above with a documented reason.",
2020                ));
2021            }
2022        }
2023
2024        assert!(
2025            failures.is_empty(),
2026            "Architecture v1.0 bit-stable CPU coverage lint failed:\n{}",
2027            failures.join("\n"),
2028        );
2029        // Sanity: the lint should be exercising real ops. If both
2030        // counts are zero, something is wrong with the registry
2031        // population.
2032        assert!(
2033            covered > 0 || allowlisted > 0,
2034            "lint covered 0 ops — registry appears empty"
2035        );
2036        // Stronger commitment: exactly the one documented gap above,
2037        // no silent additions.
2038        assert_eq!(
2039            allowlisted, 1,
2040            "KNOWN_GAPS allowlist has exactly 1 documented entry (AwqMatmul); \
2041             saw {allowlisted} allowlisted",
2042        );
2043        // Sanity: we should see all 24 registered fused ops covered.
2044        // 14 from the original Phase 7.6 lineup + PowIBackward
2045        // (autograd switched from emitting the primitive decomposition
2046        // to emitting Op::Fused(POWI_BACKWARD, _)) + INPLACE_AFFINE
2047        // (Phase 3 of the in-place ops infrastructure) +
2048        // FUSED_SOFTMAX_CROSS_ENTROPY + CAUSAL_CONV1D + SELECTIVE_SCAN
2049        // + SSD_CHUNK_SCAN + NF4_MATMUL (the full CPU OpKind coverage
2050        // plan: FSCE + Mamba-adjacent trio + NF4) +
2051        // FLASH_ATTN_BACKWARD_{Q,K,V} (reusing the binding-table CPU
2052        // dispatch wrappers). AWQ_MATMUL is the registry's 25th entry
2053        // and is the one documented KNOWN_GAPS allowlist line above,
2054        // not counted here.
2055        assert_eq!(
2056            covered, 24,
2057            "expected 24 fused ops covered by bit-stable CPU impls, got {covered}",
2058        );
2059    }
2060
2061    /// Negative-path check for the bit-stable lint logic: a
2062    /// `FusedKernelRegistry` with no CPU impl for a registered id
2063    /// must fail the bit-stable check. Verifies the lint's failure
2064    /// detection works rather than just relying on the positive
2065    /// path being green.
2066    ///
2067    /// Doesn't touch `default_kernel_registry()` — runs the check
2068    /// against a freshly-constructed empty registry so the failure
2069    /// is deterministic regardless of global state.
2070    #[test]
2071    fn precision_guarantee_lint_detects_missing_cpu() {
2072        use fuel_graph::registry::FusedOpId;
2073        let empty = FusedKernelRegistry::new();
2074        let id = FusedOpId(1);
2075        // Same predicate the production lint uses.
2076        let has_bit_stable_cpu = empty.impls_for(id).iter().any(|(backend, impl_)| {
2077            *backend == BackendId::Cpu && impl_.precision.bit_stable_on_same_hardware
2078        });
2079        assert!(
2080            !has_bit_stable_cpu,
2081            "empty registry must not report bit-stable CPU coverage"
2082        );
2083    }
2084
2085    /// Negative-path check: a CPU registration without
2086    /// `bit_stable_on_same_hardware: true` must NOT satisfy the
2087    /// lint. Captures the difference between "any CPU impl" and "a
2088    /// bit-stable CPU impl" — the architecture commits to the
2089    /// stronger property.
2090    #[test]
2091    fn precision_guarantee_lint_rejects_non_bit_stable_cpu() {
2092        use fuel_graph::registry::FusedOpId;
2093        let mut r = FusedKernelRegistry::new();
2094        let id = FusedOpId(42);
2095        let weak = BackendImpl {
2096            kernel: dummy_kernel,
2097            dtypes: DUMMY_DTYPES,
2098            cost: dummy_cost,
2099            // UNAUDITED has bit_stable_on_same_hardware: false.
2100            precision: PrecisionGuarantee::UNAUDITED,
2101            caps: KernelCaps::empty(),
2102            revision: KernelRevisionHash::UNTRACKED,
2103            cost_expr: None,
2104        };
2105        r.register(id, BackendId::Cpu, weak);
2106        let has_bit_stable_cpu = r.impls_for(id).iter().any(|(backend, impl_)| {
2107            *backend == BackendId::Cpu && impl_.precision.bit_stable_on_same_hardware
2108        });
2109        assert!(
2110            !has_bit_stable_cpu,
2111            "an UNAUDITED-precision CPU impl must not satisfy the bit-stable lint"
2112        );
2113    }
2114
2115    /// Step 6 + backward-helper follow-up — coverage assertion for
2116    /// the 12 ops that gained BackendImpls (8 forwards from step 6
2117    /// + 4 backward helpers from the follow-up), plus the
2118    ///
2119    /// FlashAttnBackward{Q,K,V} trio (4 dtypes × 2 alibi shapes
2120    /// each, reusing the binding-table CPU wrappers). Each entry
2121    /// should have at least the expected CPU impl count after
2122    /// `default_kernel_registry()` populates.
2123    #[test]
2124    fn default_kernel_registry_step6_coverage() {
2125        use fuel_graph::registry::FusedOps;
2126
2127        let r = default_kernel_registry();
2128        // (id, expected_impl_count) — derived from the dtype-tuple
2129        // shapes registered. SoftmaxLastDim/RmsNormLastDim/
2130        // LayerNormLastDim/Rope = 4 dtypes each. ConvTranspose2D /
2131        // FlashAttn / PagedAttn = 4 dtypes × 2 shapes = 8 each.
2132        // QMatMul = 1 (F32 only). The 4 backward helpers = 4 dtypes
2133        // each.
2134        for (id, want) in [
2135            (FusedOps::SOFTMAX_LAST_DIM, 4usize),
2136            (FusedOps::RMS_NORM_LAST_DIM, 4),
2137            (FusedOps::LAYER_NORM_LAST_DIM, 4),
2138            (FusedOps::ROPE, 4),
2139            (FusedOps::CONV_TRANSPOSE2D, 8),
2140            (FusedOps::FLASH_ATTN, 8),
2141            (FusedOps::FLASH_ATTN_BACKWARD_Q, 8),
2142            (FusedOps::FLASH_ATTN_BACKWARD_K, 8),
2143            (FusedOps::FLASH_ATTN_BACKWARD_V, 8),
2144            (FusedOps::PAGED_ATTN, 8),
2145            (FusedOps::QMATMUL, 1),
2146            (FusedOps::SOFTMAX_LAST_DIM_BACKWARD, 4),
2147            (FusedOps::LAYER_NORM_LAST_DIM_BACKWARD, 4),
2148            (FusedOps::RMS_NORM_LAST_DIM_BACKWARD, 4),
2149            (FusedOps::REDUCE_MAX_TO_BACKWARD, 4),
2150        ] {
2151            let impls = r.impls_for(id);
2152            assert_eq!(
2153                impls.len(),
2154                want,
2155                "expected {want} CPU impls for FusedOpId {id:?}, got {}",
2156                impls.len(),
2157            );
2158            for (backend, impl_) in impls {
2159                assert_eq!(*backend, BackendId::Cpu);
2160                assert!(
2161                    impl_.precision.bit_stable_on_same_hardware,
2162                    "CPU impl for {id:?} should be bit-stable on same hardware",
2163                );
2164            }
2165        }
2166    }
2167
2168    /// Conv2D cost — FLOPs scale with `2·N·Cout·Hout·Wave·(Cin/g)·Kh·Kw`
2169    /// + bias-add FLOPs (when bias is present).
2170    #[test]
2171    fn cost_conv2d_cpu_flops_scale() {
2172        use crate::fused::cost_conv2d_cpu;
2173        use fuel_ir::DeviceLocation;
2174        use std::collections::HashSet;
2175
2176        // x: [N=2, Cin=4, H=8, W=8], w: [Cout=6, Cin/g=4, Kh=3, Kw=3],
2177        // stride=(1,1), padding=(1,1), groups=1.
2178        // Hout = Wout = (8 + 2 - 3) + 1 = 8.
2179        // Conv FLOPs (FMA = 2): 2·N·Cout·Hout·Wout·Cin_per_g·Kh·Kw
2180        //                    = 2·2·6·8·8·4·3·3 = 55296.
2181        // Bias FLOPs: N·Cout·Hout·Wout = 2·6·8·8 = 768.
2182        let x = Shape::from_dims(&[2, 4, 8, 8]);
2183        let w = Shape::from_dims(&[6, 4, 3, 3]);
2184        let bias = Shape::from_dims(&[6]);
2185        let caps = BackendCapabilities {
2186            backend_id: BackendId::Cpu,
2187            device_location: DeviceLocation::Cpu,
2188            op_dtype_support: HashSet::new(),
2189            required_alignment: 1,
2190            access_granularity_bits: 8,
2191            transfer_paths: vec![],
2192            storage_substrate: fuel_ir::backend::SubstrateClass::HostBytes,
2193            compute_throughput_flops_per_ns: 1.0,
2194            mem_bandwidth_bytes_per_ns: 4.0,
2195        };
2196        let params = FusedOpParams::Conv2D {
2197            stride: (1, 1),
2198            padding: (1, 1),
2199            groups: 1,
2200        };
2201        let c = cost_conv2d_cpu(&[x, w, bias], &params, &caps);
2202        assert_eq!(c.flops, 55296 + 768);
2203        assert!(c.bytes_moved > 0);
2204    }
2205
2206    /// Cost model — FLOPs scale with 2·M·N·K + M·N, batch multiplies.
2207    #[test]
2208    fn cost_fused_linear_cpu_flops_scale() {
2209        use fuel_ir::DeviceLocation;
2210        use std::collections::HashSet;
2211
2212        let a = Shape::from_dims(&[2, 4, 8]); // batch=2, M=4, K=8
2213        let b = Shape::from_dims(&[2, 8, 16]); // batch=2, K=8, N=16
2214        let bias = Shape::from_dims(&[16]);
2215        let caps = BackendCapabilities {
2216            backend_id: BackendId::Cpu,
2217            device_location: DeviceLocation::Cpu,
2218            op_dtype_support: HashSet::new(),
2219            required_alignment: 1,
2220            access_granularity_bits: 8,
2221            transfer_paths: vec![],
2222            storage_substrate: fuel_ir::backend::SubstrateClass::HostBytes,
2223            compute_throughput_flops_per_ns: 1.0,
2224            mem_bandwidth_bytes_per_ns: 4.0,
2225        };
2226        let c = cost_fused_linear_cpu(&[a, b, bias], &FusedOpParams::FusedLinear, &caps);
2227        // 2 · 2 · 4 · 16 · 8 = 2048 matmul FLOPs + 2 · 4 · 16 = 128 bias FLOPs
2228        assert_eq!(c.flops, 2048 + 128);
2229        assert!(c.bytes_moved > 0);
2230    }
2231
2232    /// Finding 5.4 (FKC side, Task 3.6) — a `return.bundle`'s per-slot NAMES
2233    /// round-trip through a side-table keyed on `(FusedOpId, BackendId,
2234    /// dtypes)`. Before this task the names were parsed then discarded;
2235    /// `record_bundle_slot_names` / `bundle_slot_names` preserve them so a
2236    /// later consumer (e.g. FDX output-view labeling) can look them up by
2237    /// the same key the kernel itself was registered under. A different
2238    /// dtype key must miss (`None`), never fall back to an unrelated entry.
2239    #[test]
2240    fn bundle_slot_names_round_trip_through_the_fused_registry() {
2241        use fuel_graph::registry::FusedOps;
2242        use fuel_ir::{DType, probe::BackendId};
2243        let mut reg = FusedKernelRegistry::new();
2244        let dtypes = &[DType::F32, DType::F32][..];
2245        reg.record_bundle_slot_names(
2246            FusedOps::SELECTIVE_SCAN,
2247            BackendId::Cpu,
2248            dtypes,
2249            &["y".to_string(), "last_state".to_string()],
2250        );
2251        assert_eq!(
2252            reg.bundle_slot_names(FusedOps::SELECTIVE_SCAN, BackendId::Cpu, dtypes),
2253            Some(&["y".to_string(), "last_state".to_string()][..])
2254        );
2255        assert_eq!(
2256            reg.bundle_slot_names(FusedOps::SELECTIVE_SCAN, BackendId::Cpu, &[DType::F16][..]),
2257            None
2258        );
2259    }
2260}