Skip to main content

fuel_tensor/judge/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! The Judge — Phase 6b's empirical profiler.
3//!
4//! For every `(backend, device)` pair the probe report knows about,
5//! the Judge walks a matrix of `(op_kind, dtype, size_class)` and
6//! measures two things: wall-clock latency and numerical precision
7//! relative to the reference backend. Output is a persistable
8//! [`ProfileReport`] that the (future) ranked dispatch table indexes
9//! at realize time.
10//!
11//! ## Submodules
12//!
13//! - [`cache`] — process-wide [`DispatchTable`] cache. Hosts
14//!   `cached()`, `populate_dispatch_table()`, `invalidate()`. Was
15//!   `fuel_core::dispatch` before the 2026-05-31 dispatch-move; the
16//!   name was a misnomer (it caches the Judge's output, not the
17//!   binding-table dispatch path), so it now lives under the Judge
18//!   module that owns its lifecycle. The cache surface is re-exported
19//!   at this module's top level so callers can write
20//!   `fuel_tensor::judge::cached()` directly.
21//!
22//! # Scope of this revision
23//!
24//! Ships the types, the runner skeleton, and per-family probe
25//! coverage spanning the inference op surface in F32:
26//!
27//! - **Ops** — the canonical list, currently:
28//!   - dense linear algebra: [`OpKind::MatMul`]
29//!   - elementwise binary: Add / Sub / Mul / Div / Maximum / Minimum
30//!   - elementwise unary: Neg / Sqr / Sqrt / Exp / Log / Sin / Cos /
31//!     Tanh / Sigmoid / Silu / Gelu / Relu / Step
32//!   - per-axis reductions: SumReduce / MaxReduce / MinReduce /
33//!     MeanReduce (probed as last-dim reduction over `[rows, 64]`)
34//!   - reduce-to-broadcast-target: ReduceSumTo / ReduceMaxTo
35//!   - parametric one-input: Affine / Clamp / PowI
36//!   - 28 OpKind variants total, 84 (op, size) cells per backend
37//!     class.
38//! - **Dtypes**: `{f32, f16, bf16}`. The
39//!   dispatch key always carried a `dtype` axis
40//!   ([`ProfileEntry::dtype`]); the 2026-07-04 dtype slice made the
41//!   *runner* iterate it rather than hard-coding f32. Each profiled
42//!   op/size is now measured once per dtype, so the report carries
43//!   `3 ×` the cells it did in the f32-only era. Inputs are generated
44//!   as deterministic f32 and down-converted to the cell's dtype at
45//!   graph-build time; the realizer times the native-dtype kernel and
46//!   reads the output back at its native width, converting to f32
47//!   only for the cross-backend correctness verdict
48//!   ([`crate::factories::Realizer::realize_capture_f32`]). A cell
49//!   whose backend has no kernel for the requested dtype is skipped
50//!   cleanly (logged), never fatal — CPU currently carries
51//!   f16/bf16 kernels for the elementwise/matmul/reduction families
52//!   (FKC `[F32,F64,BF16,F16]` fan-out). f64 stays OUT of the matrix:
53//!   decode/inference is f16/bf16, and f64's only consumer is
54//!   correctness reference paths that don't route through the ranked
55//!   dispatch table.
56//! - **Backends**: every backend in the [`crate::factories`] registry.
57//!   A backend that doesn't implement an op is skipped per cell with
58//!   a stderr note (the realize call is wrapped in `catch_unwind`),
59//!   not fatal to the run — what makes the Judge safe to expand
60//!   ahead of every backend's coverage.
61//!
62//! Op kinds *not yet profiled* (build_input_graph would `panic!` if
63//! the size_plan returned a non-empty ladder for them; the size_plan's
64//! catch-all `_ => Vec::new()` arm silently skips):
65//!
66//! - composition / fused ops: SoftmaxLastDim / RmsNormLastDim /
67//!   LayerNormLastDim / Rope / FlashAttn / PagedAttn / FusedLinear.
68//!   These are slated for Phase 7.6 fused-op profiling (per ROADMAP),
69//!   not the primitive-OpKind sweep this module covers.
70//! - shape-rearranging: Cast / IndexSelect / Gather / Concat /
71//!   IndexAdd / ScatterAdd / ArgMaxDim / ArgMinDim.
72//! - convolutions: Conv2D / ConvTranspose2D.
73//! - quantized: QMatMul.
74//!
75//! Each of those is a separable add: extend `PROFILED_OPS`, add a
76//! `size_plan` arm with a representative size ladder, add a
77//! `build_input_graph` arm that constructs the input graph.
78//!
79//! # Decode-shaped ladders (Layer-2 coverage arc, slice 2)
80//!
81//! Alongside the square/general ladder, `Judge::size_plan` appends
82//! **decode-representative** cells on the decode-attention path so the
83//! Judge measures the ops at the SKINNY shapes autoregressive decode
84//! (seq_q=1) actually runs:
85//!
86//! - **MatMul** — a `[1,H]×[H,H]` hidden/attention projection GEMV
87//!   (bandwidth-bound), distinct in cost regime from the square
88//!   (compute-bound) cells.
89//! - **MaxReduce / SumReduce** — the row-max and denominator-sum of the
90//!   decomposed softmax, reducing a wide `[Hq, k_len]` score row.
91//! - **Sub / Exp / Div** — the elementwise softmax components over the
92//!   `[Hq, k_len]` score tensor.
93//!
94//! These feed slice-4's flash-vs-decomposed ranking: arm-0 (the
95//! decomposed attention region) is costed from the MEASURED decode
96//! latencies of its primitives, so the CUDA flash arm only wins on a
97//! real comparison.
98//!
99//! ## Decode ladder — SizeClass aspect key (reconciled, v4)
100//!
101//! For **non-matmul** ops [`SizeClass`] is still `log2(total_elements)`,
102//! so decode reduction/elementwise cells are placed in buckets that are
103//! FREE within their op family (reduce/elementwise sc12 + sc17) — a
104//! collision would let the oracle keep the MIN latency across the shared
105//! key and poison the cell.
106//!
107//! For **matmul** the aspect-blindness that once blocked the non-square
108//! decode cells is FIXED (SizeClass v4, 2026-07-04). The Judge keyed a
109//! matmul cell on `total_elements = m*n` (output) while the ranker's
110//! realize-time lookup ([`fuel_dispatch::ranker::compute_static_costs`])
111//! keyed on `shapes[0].elem_count() = m*k` (the LHS input) — so every
112//! *non-square* matmul lookup missed the profiled cell (square matmuls
113//! agreed by accident, `m*k == m*n`). Both sides now derive the key from
114//! the operand shapes through the shared [`SizeClass::matmul`] /
115//! [`SizeClass::for_op`] helper — packing `(log2(m·n), log2(m),
116//! log2(k))` — so the same shape maps to one identical key on producer
117//! and consumer, and a GEMV never collides with a same-output-size
118//! square (their `log2(m)` bytes differ). The previously DEFERRED decode
119//! matmuls — the FFN-width GEMV (`[1,2048]×[2048,5632]`), the QKᵀ score
120//! GEMV, the attention-output GEMV — are un-deferred into `size_plan`
121//! now that they key correctly. This bumped `PROFILE_REPORT_VERSION`
122//! v3 → v4 (fuel-ir + fuel-core + fuel-dispatch reconciliation).
123//!
124//! # Equivalence-class discipline
125//!
126//! The Judge profiles one device per equivalence class. A rig with
127//! four identical RTX 4090s runs one CUDA profile and the dispatch
128//! table shares the result across all four ordinals.
129//!
130//! # Why latency *and* precision
131//!
132//! Two ops with identical latency can produce different answers — a
133//! fast backend that drifts 1e-3 rel vs reference is fine for most
134//! ML workloads but catastrophic for a correctness-critical path
135//! like a cross-device equivalence test. Carrying both axes lets a
136//! dispatch table serve both "fastest" and "most accurate"
137//! criteria without re-profiling.
138
139pub mod cache;
140pub use cache::*;
141// `oracle` moved to `fuel_dispatch::ranker::profile_oracle` (fuel-core
142// dissolution, plan item 8) — it had zero coupling to this module's
143// `Tensor`/`Device`-constructing profiling code, only to `fuel-dispatch`'s
144// own `ranker::judge` types. Re-exported here so `crate::judge::
145// ProfileJudgeOracle` / `fuel_tensor::judge::ProfileJudgeOracle` callers
146// (this module's own tests, `cache.rs`) are unchanged.
147pub use fuel_dispatch::ranker::profile_oracle::ProfileJudgeOracle;
148
149use fuel_correctness_fixtures::{
150    CorrectnessDrift, CorrectnessFixture, FIXTURE_FILE_VERSION, FixtureFile,
151    validate_against_fixture,
152};
153use fuel_hardware::probe::ProbeReport;
154use fuel_ir::probe::{BackendId, DeviceDescriptor};
155use fuel_ir::{DType, Result, Shape};
156use std::collections::HashMap;
157use std::panic::AssertUnwindSafe;
158use std::path::Path;
159use std::sync::{Arc, RwLock};
160use std::time::Instant;
161
162// Re-export the dispatch types (moved to fuel-core-types so
163// `fuel-graph-router`'s Router can consume them without depending
164// on `fuel-core`). All existing callers `use fuel_tensor::judge::*`
165// keep working — `ProfileReport::save` / `ProfileReport::load` are
166// inherent methods on the moved type.
167pub use fuel_ir::dispatch::{
168    OpKind, PROFILE_REPORT_VERSION, ProfileEntry, ProfileReport, SizeClass,
169};
170
171// `PROFILE_REPORT_FILENAME` + `default_report_path` moved to
172// `fuel_dispatch::judge_report_path` (fuel-core dissolution, plan item 8) —
173// pure path-joining logic, zero coupling to this module's `Tensor`/`Device`
174// profiling code. Re-exported below so `crate::judge::PROFILE_REPORT_FILENAME`
175// / `crate::judge::default_report_path` callers (`cache.rs`, `scheduling.rs`,
176// and this module's own tests) are unchanged.
177pub use fuel_dispatch::judge_report_path::{PROFILE_REPORT_FILENAME, default_report_path};
178
179/// A synthetic device identity for fixtures, stamped with the given
180/// `backend` so `ProfileEntry::device.backend` agrees with the entry's
181/// own `backend` axis (the `from_report` `debug_assert` that keeps the
182/// co-encoding honest). Fixtures never load through the hardware gate,
183/// so the vendor/device ids are irrelevant here — only the backend is.
184#[cfg(test)]
185pub(crate) fn test_equiv_key(backend: BackendId) -> fuel_ir::probe::EquivalenceKey {
186    fuel_ir::probe::EquivalenceKey {
187        backend,
188        vendor_id: 0,
189        device_id: 0,
190        compute_capability: None,
191        driver_version: String::new(),
192    }
193}
194
195/// Op kinds the Judge currently profiles. Each entry must have a
196/// matching `size_plan` arm (returning a non-empty ladder) and a
197/// matching `build_input_graph` arm. New families land here as they
198/// ship; the order here is the order entries appear in the persisted
199/// report (deterministic for diff-friendly output).
200const PROFILED_OPS: &[OpKind] = &[
201    OpKind::MatMul,
202    OpKind::AddElementwise,
203    // --- elementwise unary fanout ---
204    OpKind::NegElementwise,
205    OpKind::SqrElementwise,
206    OpKind::SqrtElementwise,
207    OpKind::ExpElementwise,
208    OpKind::LogElementwise,
209    OpKind::SinElementwise,
210    OpKind::CosElementwise,
211    OpKind::TanhElementwise,
212    OpKind::SigmoidElementwise,
213    OpKind::SiluElementwise,
214    OpKind::GeluTanhElementwise,
215    OpKind::ReluElementwise,
216    OpKind::StepElementwise,
217    OpKind::RecipElementwise,
218    OpKind::AbsElementwise,
219    OpKind::FloorElementwise,
220    OpKind::CeilElementwise,
221    OpKind::RoundElementwise,
222    OpKind::SignElementwise,
223    OpKind::ErfElementwise,
224    OpKind::GeluElementwise,
225    OpKind::RsqrtElementwise,
226    // --- elementwise binary additions ---
227    OpKind::PowElementwise,
228    OpKind::RemElementwise,
229    // --- elementwise binary fanout ---
230    OpKind::SubElementwise,
231    OpKind::MulElementwise,
232    OpKind::DivElementwise,
233    OpKind::MaximumElementwise,
234    OpKind::MinimumElementwise,
235    // --- reductions along one dim ---
236    OpKind::SumReduce,
237    OpKind::MaxReduce,
238    OpKind::MinReduce,
239    OpKind::MeanReduce,
240    // --- reduce-to-broadcast-target ---
241    OpKind::ReduceSumTo,
242    OpKind::ReduceMaxTo,
243    // --- scalar / clamp / powi ---
244    OpKind::Affine,
245    OpKind::ClampElementwise,
246    OpKind::PowIElementwise,
247    // --- fused decode attention (arm-1 of slice-4's flash-vs-decomposed
248    // ranking; the decomposed arm-0 primitives are already covered at
249    // decode shapes by the elementwise/reduction/matmul families above) ---
250    OpKind::FlashAttn,
251];
252
253/// Dtypes the Judge profiles for every op in [`PROFILED_OPS`]. The
254/// dispatch key ([`ProfileEntry::dtype`]) always carried this axis;
255/// this list is what the *runner* iterates (2026-07-04 dtype slice).
256///
257/// - **F32** — the correctness/reference baseline and the widest
258///   kernel coverage.
259/// - **F16 / BF16** — the decode/inference dtypes. Flash-attention,
260///   skinny GQA matmuls, and the elementwise glue in a transformer
261///   forward all run in half precision, so the ranked dispatch table
262///   needs their measured per-dtype/per-backend latencies (the coarse
263///   Layer-1 model can't tell an f16 kernel from an f32 one).
264///
265/// **F64 is intentionally excluded**: it has no inference consumer and
266/// its only routing path is the correctness-reference backend, which
267/// the ranked table never picks. Adding it would triple-count cold
268/// cells for no ranking benefit. A cell whose backend lacks a kernel
269/// for one of these dtypes is skipped cleanly, so listing a dtype here
270/// is safe even on backends with partial coverage.
271const PROFILED_DTYPES: &[DType] = &[DType::F32, DType::F16, DType::BF16];
272
273// =============================================================
274// Decode-shaped size-ladder constants (Layer-2 coverage arc,
275// slice 2, 2026-07-04)
276// =============================================================
277//
278// The general ladders are square/large — a 1024×1024×1024 matmul,
279// a 2²⁰-element reduction. Decode (seq_q=1 autoregressive) runs
280// SKINNY shapes that live in a different cost regime: a
281// `[1,K]×[K,N]` GEMV is bandwidth-bound, not compute-bound, and the
282// decomposed decode-attention softmax reduces/maps over a `[Hq,
283// k_len]` score row. These constants shape the decode-representative
284// cells the Judge appends so it measures the ops at the shapes decode
285// actually runs (slice-4's flash-vs-decomposed comparison ranks on
286// arm-0's MEASURED decode primitives, not extrapolated square costs).
287//
288// **SizeClass discipline.** For non-matmul ops [`SizeClass`] is
289// `log2(total_elements)`, aspect-blind — so each decode reduction/
290// elementwise cell below lands in a bucket that is FREE within its op
291// family (no collision with an existing ladder cell — the oracle keeps
292// the MIN latency across a colliding key, which would poison the cell).
293// For matmul the v4 aspect key `matmul(m,n,k)` is derived identically
294// on the producer (Judge) and consumer (ranker) sides, so the non-square
295// FFN/QKᵀ/attn-output GEMVs are un-deferred (no longer square-consistency
296// constrained). See the module-level "Decode ladder — SizeClass aspect
297// key" note.
298
299/// TinyLlama-class decode attention head count (Hq). The decomposed
300/// decode softmax runs one score row per query head at seq_q=1, so the
301/// decode reduction/elementwise cells are shaped `[DECODE_HEADS,
302/// k_len]`.
303const DECODE_HEADS: usize = 32;
304
305/// Small decode key-length (early decode / short context). Wide enough
306/// (≥128) to be a genuine row-reduction, distinct from the cols=64
307/// general reduction ladder.
308const DECODE_KLEN_SMALL: usize = 128;
309
310/// Capacity-ish decode key-length (long context / near KV-cache cap) —
311/// the bandwidth-bound end of the decode reduction regime.
312const DECODE_KLEN_CAP: usize = 4096;
313
314/// Hidden dim of the decode hidden/attention projection GEMV
315/// (`[1,H]×[H,H]`). With the v4 aspect [`SizeClass`] this cell keys as
316/// `matmul(1, H, H)` — distinct from every square cell by the `log2(m)`
317/// byte (`m=1` vs `m=n`). (Pre-v4 this constant was pinned to keep the
318/// old scalar `m·n == m·k` keys in agreement; the aspect key removes
319/// that constraint.)
320const DECODE_HIDDEN: usize = 2048;
321
322/// TinyLlama-class attention head dim (D). The decode QKᵀ score GEMV
323/// contracts over D (`[1,D]×[D,k_len]`), the attention-output GEMV
324/// contracts over k_len back down to D (`[1,k_len]×[k_len,D]`).
325const DECODE_HEAD_DIM: usize = 64;
326
327/// TinyLlama-class FFN intermediate width. The decode up-projection is a
328/// wide GEMV `[1,H]×[H,FFN]` (H=2048 → FFN=5632). Under the *old* scalar
329/// key its output `m·n = 5632` bucketed to sc12 — colliding with the
330/// 64³ square (`m·n = 4096` → sc12) — which is exactly why slice 2
331/// DEFERRED it. The v4 aspect key `matmul(1, 5632, 2048)` no longer
332/// collides (the `log2(m)=0` byte separates it from the square's
333/// `log2(m)=6`), so it is un-deferred here.
334const DECODE_FFN: usize = 5632;
335
336/// Element count of the `[Hq, k_len]` softmax score tensor at the small
337/// decode key-length (`Hq * k_len` = 4096 → sc12, free in the
338/// elementwise family whose ladder occupies sc {10,16,20}).
339const DECODE_SCORE_SMALL_ELEMS: usize = DECODE_HEADS * DECODE_KLEN_SMALL;
340
341/// Element count of the `[Hq, k_len]` softmax score tensor at the
342/// capacity decode key-length (`Hq * k_len` = 131072 → sc17, free).
343const DECODE_SCORE_CAP_ELEMS: usize = DECODE_HEADS * DECODE_KLEN_CAP;
344
345/// TinyLlama-class decode KV head count (Hkv). GQA folds the 32 query
346/// heads onto 4 KV heads (`Hq / Hkv = 8`). Shapes the fused FlashAttn
347/// op's `k`/`v` operands `[b, DECODE_KV_HEADS, k_len, d]` — the arm-1
348/// half of slice-4's flash-vs-decomposed comparison (arm-0's decomposed
349/// QKᵀ/softmax/PV primitives are already covered by slices 2/2.5 at the
350/// same decode shapes).
351const DECODE_KV_HEADS: usize = 4;
352
353/// How many measurement iterations per (op, dtype, size, backend)
354/// cell. Median of this many runs is recorded.
355pub const DEFAULT_ITERATIONS: u32 = 7;
356
357/// How many warmup iterations before the timed run. Discards cold-
358/// cache and JIT-compile latency; matters most on GPU backends
359/// where the first launch includes kernel-cache population.
360pub const DEFAULT_WARMUP: u32 = 3;
361
362/// The runner. Consumes a [`ProbeReport`] (so it knows which devices
363/// exist and which equivalence classes to share across), builds the
364/// full measurement matrix, and produces a [`ProfileReport`].
365pub struct Judge {
366    pub iterations: u32,
367    pub warmup: u32,
368    /// Optional shrunk size ladder. `None` = use the full default
369    /// ladder: three square/general sizes per op (up to 1024×1024
370    /// matmul / 2²⁰ elementwise), PLUS decode-shaped cells on the
371    /// decode-attention path (seq_q=1 matmul GEMV; wide-`k_len`
372    /// MaxReduce/SumReduce; decode-score-row Sub/Exp/Div). Tests supply
373    /// a shrunk ladder to stay fast.
374    pub size_plan_override: Option<Vec<(OpKind, OpSize)>>,
375    /// Optional pre-validated correctness fixtures keyed by
376    /// `(op, dtype, size_class)`. When a fixture covers a profiling
377    /// cell, the Judge validates every CellRun's output against it
378    /// via [`validate_against_fixture`] and assigns each
379    /// `ProfileEntry::max_rel_error` from the verdict, bypassing the
380    /// inline pairwise-consensus path for that cell. Cells without a
381    /// matching fixture fall back to the consensus path (today's
382    /// default behavior on multi-backend systems).
383    ///
384    /// One key may map to multiple fixtures when capture used
385    /// different `input_seed`s; the Judge applies the first whose
386    /// fixture-shape and the cell's regenerated input agree. If no
387    /// fixture matches the cell's input seed (mismatch logged), the
388    /// fallback consensus path is used for the cell.
389    pub fixtures: Option<HashMap<(OpKind, DType, SizeClass), Vec<CorrectnessFixture>>>,
390}
391
392impl Default for Judge {
393    fn default() -> Self {
394        Self {
395            iterations: DEFAULT_ITERATIONS,
396            warmup: DEFAULT_WARMUP,
397            size_plan_override: None,
398            fixtures: None,
399        }
400    }
401}
402
403impl Judge {
404    /// Construct a Judge with the default measurement parameters and
405    /// the fixture set loaded from `path`. The path may point at a
406    /// single `*.json` file or a directory; in the directory case
407    /// every `*.json` under the root is loaded recursively (the
408    /// `fuel-correctness-fixtures` capture binary writes
409    /// `v1/<dtype>/<op>.json`, so a single root covers an entire
410    /// fixture distribution).
411    ///
412    /// Returns an `io::Error` wrapped as a `fuel-core` `Error` if the
413    /// path can't be read or any file fails to parse as a
414    /// [`FixtureFile`].
415    ///
416    /// **Use**: pass a fixture root once at Judge construction time;
417    /// every cell the Judge profiles thereafter validates against the
418    /// fixture (if one exists for that cell) instead of inline
419    /// consensus. Cells with no matching fixture fall back to the
420    /// consensus path.
421    pub fn with_fixtures_from(path: &Path) -> Result<Self> {
422        let fixtures = load_fixtures_recursive(path)?;
423        Ok(Self {
424            fixtures: Some(fixtures),
425            ..Self::default()
426        })
427    }
428
429    /// Look up a fixture matching `(op, dtype, size_class)` and the
430    /// expected element count of the cell's output. Returns `None`
431    /// when no fixture set is configured, when no fixture is keyed
432    /// for the cell, when no fixture matches the cell's output
433    /// shape, when the fixture's `input_seed` doesn't match the
434    /// Judge-derived per-cell seed, or when the fixture's
435    /// `input_hash` doesn't match the regenerated input bytes.
436    ///
437    /// **Triple-agreement contract (2026-06-08)**: a fixture is
438    /// accepted iff
439    ///
440    ///   1. `(op, dtype, size_class)` matches the bucket key, AND
441    ///   2. `output_element_count` matches the cell's output shape, AND
442    ///   3. `input_seed` matches
443    ///      [`fuel_correctness_fixtures::capture::derive_seed`]'s
444    ///      output for this cell, AND
445    ///   4. `input_hash` matches
446    ///      [`fuel_correctness_fixtures::capture::hash_f32_input`]
447    ///      applied to the regenerated input via
448    ///      [`fuel_correctness_fixtures::capture::deterministic_f32_input`].
449    ///
450    /// Failure of any check is logged and the cell falls back to
451    /// inline pairwise consensus (the caller's
452    /// `compute_pairwise_consensus` path runs).
453    ///
454    /// `input_elem_count` is the number of f32 elements the capture
455    /// tool's `deterministic_f32_input` would emit for this cell —
456    /// the caller derives it from the live `OpSize` via
457    /// [`OpSize::input_elements`]. It is decoupled from
458    /// `output_element_count` because binary ops carry `[a, b]`
459    /// concatenated (input = 2 × N) while their output is a single
460    /// `[n]` buffer.
461    fn lookup_fixture(
462        &self,
463        op: OpKind,
464        dtype: DType,
465        size_class: SizeClass,
466        output_elem_count: usize,
467        input_elem_count: usize,
468    ) -> Option<&CorrectnessFixture> {
469        let map = self.fixtures.as_ref()?;
470        let bucket = map.get(&(op, dtype, size_class))?;
471
472        // Derive the per-cell seed once — capture's `derive_seed`
473        // is a pure function of (op, dtype, size_class).
474        let expected_seed = fuel_correctness_fixtures::capture::derive_seed(op, dtype, size_class);
475
476        // Lazily regenerate and hash the input only when a fixture's
477        // shape + seed agree (the hash is the expensive check).
478        let mut regenerated_hash: Option<u64> = None;
479        let mut shape_matches = false;
480        for f in bucket {
481            if f.output_element_count != output_elem_count {
482                continue;
483            }
484            shape_matches = true;
485            if f.input_seed != expected_seed {
486                eprintln!(
487                    "judge: fixture for ({op:?}, {dtype:?}, size_class={}) seed mismatch \
488                     (fixture seed={}, expected={}); falling back to consensus",
489                    size_class.0, f.input_seed, expected_seed,
490                );
491                continue;
492            }
493            let actual_hash = *regenerated_hash.get_or_insert_with(|| {
494                let input = fuel_correctness_fixtures::capture::deterministic_f32_input(
495                    op,
496                    input_elem_count,
497                );
498                fuel_correctness_fixtures::capture::hash_f32_input(&input)
499            });
500            if f.input_hash != actual_hash {
501                eprintln!(
502                    "judge: fixture input drifted for ({op:?}, {dtype:?}, \
503                     size_class={}, seed={}); fixture hash={}, regenerated hash={}; \
504                     falling back to consensus",
505                    size_class.0, f.input_seed, f.input_hash, actual_hash,
506                );
507                continue;
508            }
509            return Some(f);
510        }
511        if !shape_matches {
512            // The bucket exists but no fixture matches the cell's shape.
513            // The capture used a different input convention; skip + fall
514            // back to consensus rather than emit a misleading rel_err.
515            eprintln!(
516                "judge: fixture bucket for ({op:?}, {dtype:?}, size_class={}) has no entry \
517                 matching output_element_count={output_elem_count}; falling back to consensus",
518                size_class.0,
519            );
520        }
521        None
522    }
523
524    /// Representative size classes for the Judge's op matrix. Each
525    /// entry is (op, [input element counts]) — the Judge picks the
526    /// first entry whose op matches and iterates the sizes.
527    fn size_plan(&self, op: OpKind) -> Vec<OpSize> {
528        if let Some(over) = &self.size_plan_override {
529            return over
530                .iter()
531                .filter(|(k, _)| *k == op)
532                .map(|(_, s)| *s)
533                .collect();
534        }
535        match op {
536            OpKind::MatMul => vec![
537                // General square ladder (compute-bound regime).
538                OpSize::MatMul {
539                    m: 64,
540                    n: 64,
541                    k: 64,
542                },
543                OpSize::MatMul {
544                    m: 256,
545                    n: 256,
546                    k: 256,
547                },
548                OpSize::MatMul {
549                    m: 1024,
550                    n: 1024,
551                    k: 1024,
552                },
553                // ---- Decode GEMVs (seq_q=1, bandwidth-bound). ----
554                // With the v4 aspect SizeClass every cell below keys as
555                // `matmul(m,n,k)` and the same operand dims yield the
556                // same key on the ranker's realize-time lookup, so these
557                // are found (no longer square-consistency-constrained).
558                //
559                // Hidden / attention projection `[1,H]×[H,H]`.
560                OpSize::MatMul {
561                    m: 1,
562                    n: DECODE_HIDDEN,
563                    k: DECODE_HIDDEN,
564                },
565                // FFN up-projection `[1,H]×[H,FFN]` (wide GEMV). Un-
566                // deferred from slice 2: under the old scalar key its
567                // output `m·n=5632` collided with the 64³ square (both
568                // sc12); the aspect key `matmul(1,5632,2048)` separates
569                // them by the `log2(m)=0` byte.
570                OpSize::MatMul {
571                    m: 1,
572                    n: DECODE_FFN,
573                    k: DECODE_HIDDEN,
574                },
575                // QKᵀ score GEMV, per query head `[1,D]×[D,k_len]` (the
576                // leading Hq batch dim doesn't affect the aspect key —
577                // `for_op` reads the trailing two dims). Contracts over
578                // the head dim D; output width is k_len.
579                OpSize::MatMul {
580                    m: 1,
581                    n: DECODE_KLEN_SMALL,
582                    k: DECODE_HEAD_DIM,
583                },
584                // Attention-output GEMV, per head `[1,k_len]×[k_len,D]`.
585                // Contracts over k_len back down to D — the (n,k)-swapped
586                // mirror of QKᵀ, and the aspect key keeps them distinct.
587                OpSize::MatMul {
588                    m: 1,
589                    n: DECODE_HEAD_DIM,
590                    k: DECODE_KLEN_SMALL,
591                },
592            ],
593            // Element-wise binary + unary share one ladder. Three sizes
594            // span the regime where launch overhead dominates (1 KiB),
595            // L2 still fits the working set (256 KiB), and DRAM
596            // bandwidth dominates (4 MiB).
597            OpKind::AddElementwise
598            | OpKind::SubElementwise
599            | OpKind::MulElementwise
600            | OpKind::DivElementwise
601            | OpKind::MaximumElementwise
602            | OpKind::MinimumElementwise
603            | OpKind::PowElementwise
604            | OpKind::RemElementwise
605            | OpKind::NegElementwise
606            | OpKind::SqrElementwise
607            | OpKind::SqrtElementwise
608            | OpKind::ExpElementwise
609            | OpKind::LogElementwise
610            | OpKind::SinElementwise
611            | OpKind::CosElementwise
612            | OpKind::TanhElementwise
613            | OpKind::SigmoidElementwise
614            | OpKind::SiluElementwise
615            | OpKind::GeluTanhElementwise
616            | OpKind::ReluElementwise
617            | OpKind::StepElementwise
618            | OpKind::RecipElementwise
619            | OpKind::AbsElementwise
620            | OpKind::FloorElementwise
621            | OpKind::CeilElementwise
622            | OpKind::RoundElementwise
623            | OpKind::SignElementwise
624            | OpKind::ErfElementwise
625            | OpKind::GeluElementwise
626            | OpKind::RsqrtElementwise
627            | OpKind::Affine
628            | OpKind::ClampElementwise
629            | OpKind::PowIElementwise => {
630                let mut ladder = vec![
631                    OpSize::Elementwise(1 << 10),
632                    OpSize::Elementwise(1 << 16),
633                    OpSize::Elementwise(1 << 20),
634                ];
635                // Decode-score-row cells for the softmax elementwise
636                // components (sub/exp/div over the `[Hq, k_len]` score
637                // tensor). Only these three ops appear on the decomposed
638                // decode-attention softmax path — the rest of the
639                // fanout (sin/cos/tanh/gelu_tanh/…) keeps the general ladder.
640                // Element counts 4096 (sc12) / 131072 (sc17) are both
641                // free in this family's occupied buckets {10,16,20}.
642                if is_decode_softmax_elementwise(op) {
643                    ladder.push(OpSize::Elementwise(DECODE_SCORE_SMALL_ELEMS));
644                    ladder.push(OpSize::Elementwise(DECODE_SCORE_CAP_ELEMS));
645                }
646                ladder
647            }
648            // Per-axis reductions: probe last-dim reductions over a
649            // `[rows, cols]` shape with cols=64 (typical hidden-dim
650            // chunk). Total elements 1 KiB / 64 KiB / 1 MiB to align
651            // with the elementwise size ladder for cross-family
652            // size_class comparison in the dispatch table.
653            OpKind::SumReduce | OpKind::MaxReduce | OpKind::MinReduce | OpKind::MeanReduce => {
654                let mut ladder = vec![
655                    OpSize::Reduce {
656                        rows: 1 << 4,
657                        cols: 64,
658                    },
659                    OpSize::Reduce {
660                        rows: 1 << 10,
661                        cols: 64,
662                    },
663                    OpSize::Reduce {
664                        rows: 1 << 14,
665                        cols: 64,
666                    },
667                ];
668                // Decode-row cells for the softmax reductions: MaxReduce
669                // (row max) + SumReduce (denominator). The reduced dim
670                // is the last dim (cols = k_len), so these reduce a WIDE
671                // `[Hq, k_len]` row — a different regime from the
672                // narrow cols=64 general ladder. Total elements
673                // 32*128=4096 (sc12) / 32*4096=131072 (sc17) are both
674                // free in this family's occupied buckets {10,16,20}.
675                // Min/Mean are NOT on the softmax path — no decode cell.
676                if is_decode_softmax_reduction(op) {
677                    ladder.push(OpSize::Reduce {
678                        rows: DECODE_HEADS,
679                        cols: DECODE_KLEN_SMALL,
680                    });
681                    ladder.push(OpSize::Reduce {
682                        rows: DECODE_HEADS,
683                        cols: DECODE_KLEN_CAP,
684                    });
685                }
686                ladder
687            }
688            // Reduce-to-broadcast-target — reduces a `[rows, cols]`
689            // input to a `[1, cols]` output (sum/max along leading
690            // dim). The canonical autograd-backward shape; broader
691            // patterns (multi-axis, mid-rank insertion) follow once
692            // the dispatch tables can carry them.
693            OpKind::ReduceSumTo | OpKind::ReduceMaxTo => vec![
694                OpSize::ReduceTo {
695                    rows: 1 << 4,
696                    cols: 64,
697                },
698                OpSize::ReduceTo {
699                    rows: 1 << 10,
700                    cols: 64,
701                },
702                OpSize::ReduceTo {
703                    rows: 1 << 14,
704                    cols: 64,
705                },
706            ],
707            // Fused decode attention (arm-1 of slice-4's flash-vs-
708            // decomposed ranking). Two decode-representative GQA cells at
709            // seq_q=1: a short-context and a capacity-ish k_len. Both share
710            // TinyLlama geometry (Hq=32, Hkv=4, D=64) and key to DISTINCT
711            // `SizeClass::attention` buckets (the k_len byte differs), so
712            // the oracle costs the short-context and long-context flash
713            // kernel separately. Measurement-bounded: at k_len=4096 the
714            // naive CPU SDPA is ~Hq·k_len·D ≈ 8.4M MACs per QKᵀ/PV pass.
715            OpKind::FlashAttn => vec![
716                OpSize::Attention {
717                    b: 1,
718                    hq: DECODE_HEADS,
719                    hkv: DECODE_KV_HEADS,
720                    d: DECODE_HEAD_DIM,
721                    k_len: DECODE_KLEN_SMALL,
722                },
723                OpSize::Attention {
724                    b: 1,
725                    hq: DECODE_HEADS,
726                    hkv: DECODE_KV_HEADS,
727                    d: DECODE_HEAD_DIM,
728                    k_len: DECODE_KLEN_CAP,
729                },
730            ],
731            // OpKind is `#[non_exhaustive]` — future variants land
732            // here until the Judge gets a measurement strategy for
733            // them. Empty plan = "Judge skips this op silently."
734            _ => Vec::new(),
735        }
736    }
737
738    /// Run the full profile matrix for every equivalence class in
739    /// `probe`. Skips backends that aren't compiled in; logs (stderr)
740    /// any backend that's present but hasn't been wired into the
741    /// Judge yet (e.g. Vulkan in this revision).
742    ///
743    /// **Reference-retirement note (2026-06-07)**: the loop order is
744    /// inverted relative to the pre-retirement shape — outer over
745    /// `(op, size)`, inner over equivalence classes. This lets Judge
746    /// collect all backends' outputs at a given cell, compute pairwise
747    /// consensus across them, and assign each entry's `max_rel_error`
748    /// relative to the consensus cluster (no privileged "reference
749    /// output" required). When only one backend is available at a
750    /// cell, consensus is trivial and `max_rel_error = 0.0` (no peers
751    /// to compare against — the caller treats the lone backend as
752    /// authoritative).
753    ///
754    /// **Per-alternative measurement (2026-06-08)**: for each
755    /// equivalence-class representative the Judge now walks
756    /// `KernelBindingTable::lookup_alternatives(op_kind, dtypes,
757    /// backend)` and emits one `CellRun` per registered alternative.
758    /// One alternative is timed via the standard realizer path; its
759    /// CellRun is attributed to the sibling the picker ACTUALLY
760    /// dispatched (the bridge's post-realize report — Session 3
761    /// rider, 2026-06-11 — with first-registered fallback when the
762    /// plan had no entry for the measured root). The remaining
763    /// alternatives at the same `(op, dtypes, backend)` key are
764    /// timed via a direct kernel-pointer call so AOCL/MKL siblings
765    /// get distinct latency numbers.
766    pub fn run(&self, probe: &ProbeReport) -> ProfileReport {
767        let mut entries = Vec::new();
768        let classes = probe.equivalence_classes();
769        // Stable iteration order over equivalence classes — sort
770        // class keys so the profile report is deterministic across
771        // runs (HashMap iteration order is not guaranteed across
772        // process invocations).
773        let mut class_keys: Vec<_> = classes.keys().collect();
774        class_keys.sort_by_key(|k| (k.backend as u32, k.device_id, k.vendor_id));
775
776        for &op in PROFILED_OPS {
777            for sz in self.size_plan(op) {
778                // MatMul keys on shape aspect `(m,n,k)` via the shared
779                // `SizeClass::matmul` helper the `fuel-dispatch` ranker
780                // also reaches (through `SizeClass::for_op`), so a
781                // non-square matmul this producer profiles is found by
782                // the consumer at realize time — the same operand dims
783                // map to one identical key on both sides. Every other op
784                // keys on total element count. (SizeClass v4 producer/
785                // consumer reconciliation, 2026-07-04.)
786                let size_class = match sz {
787                    OpSize::MatMul { m, n, k } => SizeClass::matmul(m, n, k),
788                    // FlashAttn keys on the attention aspect `(hq, k_len, d)`
789                    // via the shared `SizeClass::attention` helper that
790                    // slice-4's bake also reaches (through `SizeClass::for_op`
791                    // from the flash node's operand shapes), so a decode
792                    // attention cell this producer profiles is found by the
793                    // consumer at bake time. Every other op keys on total
794                    // element count.
795                    OpSize::Attention { hq, k_len, d, .. } => SizeClass::attention(hq, k_len, d),
796                    _ => SizeClass::from_elem_count(sz.total_elements()),
797                };
798
799                // Dtype axis (2026-07-04): measure each (op, size) cell
800                // once per profiled dtype. The size_class is dtype-
801                // independent (element count, not byte count), so it's
802                // computed once outside this loop. Consensus is per
803                // (op, dtype, size) — f16 outputs cluster against f16
804                // peers, never against the f32 measurement of the same
805                // op — because `cell_runs` is rebuilt inside the loop.
806                for &dtype in PROFILED_DTYPES {
807                    // First pass: per equivalence-class representative,
808                    // measure latency + capture the kernel's output for
809                    // consensus comparison. The realizer path measures
810                    // whichever alternative the picker dispatches; its
811                    // CellRun already carries that sibling's
812                    // `kernel_source` (the bridge's post-realize report,
813                    // filled in by `time_op_capturing` — Session 3 rider).
814                    let mut cell_runs: Vec<CellRun> = Vec::with_capacity(class_keys.len());
815                    for key in &class_keys {
816                        let devs = &classes[key];
817                        let rep = devs[0];
818                        if let Some(run) = self.measure_on_device_capturing(op, dtype, &sz, rep) {
819                            let dispatched_source = run.kernel_source.clone();
820                            // The dispatched kernel's FULL identity — the tag
821                            // alone no longer picks a sibling out (see
822                            // `direct_call_alternatives`). Captured before the
823                            // move below.
824                            let dispatched_revision = run.kernel_revision_hash;
825                            cell_runs.push(run);
826
827                            // Per-alternative measurement: walk the
828                            // REMAINING alternatives at the same
829                            // `(op_kind, dtypes, backend)` binding-table
830                            // cell and time each via a direct
831                            // kernel-pointer call. The dispatched
832                            // alternative is already covered by the
833                            // realizer measurement above.
834                            if let Some(extra_runs) = self.measure_extra_alternatives(
835                                op,
836                                dtype,
837                                &sz,
838                                rep,
839                                &dispatched_source,
840                                dispatched_revision,
841                            ) {
842                                for extra in extra_runs {
843                                    cell_runs.push(extra);
844                                }
845                            }
846                        }
847                    }
848
849                    // Second pass: pick a correctness verdict for each
850                    // CellRun. The fixture fast-path is preferred when a
851                    // fixture exists for the cell — that's the
852                    // pre-validated multi-backend output captured on a
853                    // reference rig, so single-backend systems get the
854                    // same correctness signal without needing peers
855                    // locally. When no fixture is present, fall back to
856                    // pairwise consensus across the CellRuns at this
857                    // cell.
858                    let expected_elem_count =
859                        cell_runs.first().map(|r| r.output.len()).unwrap_or(0);
860                    let input_elem_count = sz.input_elements(op);
861                    let fixture = self.lookup_fixture(
862                        op,
863                        dtype,
864                        size_class,
865                        expected_elem_count,
866                        input_elem_count,
867                    );
868                    let consensus = if fixture.is_none() {
869                        compute_pairwise_consensus(&cell_runs)
870                    } else {
871                        Vec::new() // unused on the fixture path
872                    };
873
874                    // Third pass: emit one ProfileEntry per (run, device).
875                    // The first alternative's CellRun replicates across
876                    // each equivalence-class device; per-alternative
877                    // CellRuns from the binding-table direct path
878                    // record on the representative's device_index only
879                    // (the cross-device sharing convention applies to
880                    // backend-level equivalence, not within-cell kernel
881                    // sibling differentiation — siblings are
882                    // hardware-agnostic at the binding-table layer).
883                    for (i, run) in cell_runs.iter().enumerate() {
884                        let rel_err = if let Some(f) = fixture {
885                            max_rel_err_vs_fixture(&run.output, f)
886                        } else {
887                            max_rel_err_vs_consensus(&cell_runs, &consensus, i)
888                        };
889                        // Find the equivalence class this run belongs to
890                        // (its representative's backend matches `run.backend`).
891                        let class_devs = classes.iter().find_map(|(k, devs)| {
892                            if k.backend == run.backend && devs[0].device_index == run.device_index
893                            {
894                                Some(devs)
895                            } else {
896                                None
897                            }
898                        });
899                        if let Some(devs) = class_devs {
900                            for d in devs {
901                                entries.push(ProfileEntry {
902                                    op,
903                                    dtype,
904                                    size_class,
905                                    backend: run.backend,
906                                    device_index: d.device_index,
907                                    latency_ns: run.latency_ns,
908                                    iterations: run.iterations,
909                                    max_rel_error: rel_err,
910                                    kernel_source: run.kernel_source.clone(),
911                                    kernel_revision_hash: run.kernel_revision_hash,
912                                    // The device this row was measured on. `d`
913                                    // is a member of the equivalence class whose
914                                    // key `k.backend == run.backend`, so this
915                                    // identity's backend agrees with `backend`
916                                    // above by construction (from_report asserts
917                                    // it). The load gate refuses the whole report
918                                    // on any box that lacks this device.
919                                    device: d.equivalence_key(),
920                                });
921                            }
922                        }
923                    }
924                }
925            }
926        }
927
928        ProfileReport {
929            version: PROFILE_REPORT_VERSION,
930            entries,
931        }
932    }
933
934    /// Walk binding-table alternatives at `(op_kind, dtypes, backend)`
935    /// beyond the one the realizer already timed (the picker's
936    /// dispatched sibling, `dispatched_kernel_source`) and run a
937    /// direct kernel-pointer measurement on each. Returns `None` when
938    /// only one alternative is registered (no extra work) or when the
939    /// op family is not yet wired into the direct-call path.
940    ///
941    /// **Why direct call instead of realizer**: the realizer dispatches
942    /// through the picker, which selects ONE alternative per realize.
943    /// To measure the siblings the picker didn't choose (e.g. AOCL
944    /// when MKL won the rank), we have to bypass the picker entirely
945    /// and invoke each specific `BindingEntry::kernel` function
946    /// pointer with hand-built inputs.
947    ///
948    /// **Scope**: v1 supports the subset of [`PROFILED_OPS`] where
949    /// input/output Storage + Layout + OpParams can be built without
950    /// going through the lazy-tensor graph. Today: matmul, elementwise
951    /// unary/binary, reductions, reduce-to, affine/clamp/powi. Other
952    /// op families return `None` and only the realizer-measured
953    /// dispatched alternative is recorded for the cell.
954    fn measure_extra_alternatives(
955        &self,
956        op: OpKind,
957        dtype: DType,
958        size: &OpSize,
959        device: &DeviceDescriptor,
960        dispatched_kernel_source: &str,
961        dispatched_kernel_revision: u64,
962    ) -> Option<Vec<CellRun>> {
963        // Direct kernel-pointer calls are only meaningful on the CPU
964        // backend today — CUDA / Vulkan storage handles are backend-
965        // specific and the realizer-internal allocator hierarchy
966        // doesn't expose a stand-alone "build CUDA storage from f32
967        // slice" entry point at the binding-table layer.
968        if device.backend != BackendId::Cpu {
969            return None;
970        }
971
972        // Dtype axis (2026-07-04): the direct-call input builder
973        // (`prepare_direct_call_inputs`) materializes f32 CPU storage
974        // only, and `read_output_f32` reads it back as f32. For F16/
975        // BF16 cells the realizer-measured primary alternative is the
976        // sole entry until the direct-call path grows a typed input
977        // builder. This is a coverage gap, not a correctness hazard:
978        // AOCL/MKL half-precision siblings (the direct-call path's
979        // reason to exist) are feature-gated off in the born-red CPU
980        // build anyway, so today no sibling is silently dropped.
981        if dtype != DType::F32 {
982            return None;
983        }
984
985        let alternatives =
986            direct_call_alternatives(op, dispatched_kernel_source, dispatched_kernel_revision);
987        if alternatives.is_empty() {
988            return None;
989        }
990
991        // Build the inputs once — every alternative consumes the same
992        // input data, so this is shared across the per-alt timing loop.
993        let prepared = prepare_direct_call_inputs(op, size)?;
994
995        let mut extra: Vec<CellRun> = Vec::with_capacity(alternatives.len());
996        for alt in alternatives {
997            if let Some(run) = self.time_alternative_direct(op, size, device, &prepared, &alt) {
998                extra.push(run);
999            }
1000        }
1001        if extra.is_empty() { None } else { Some(extra) }
1002    }
1003
1004    /// Direct-call timing path. Bypasses the realizer; calls the
1005    /// supplied `BindingEntry::kernel` function pointer directly with
1006    /// the prepared inputs. `iterations` median + `warmup` discard
1007    /// applied identically to the realizer path.
1008    fn time_alternative_direct(
1009        &self,
1010        op: OpKind,
1011        size: &OpSize,
1012        device: &DeviceDescriptor,
1013        prepared: &PreparedDirectCall,
1014        alt: &DirectCallAlternative,
1015    ) -> Option<CellRun> {
1016        // Warmup — discard timings. A backend whose kernel panics
1017        // mid-warmup is treated as if the alternative didn't exist
1018        // (skip this CellRun); the primary realizer-measured run
1019        // still represents the cell.
1020        let kernel = alt.kernel;
1021        for _ in 0..self.warmup {
1022            let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
1023                let mut outs = vec![Arc::clone(&prepared.output)];
1024                kernel(
1025                    &prepared.inputs,
1026                    &mut outs,
1027                    &prepared.layouts,
1028                    &prepared.op_params,
1029                )
1030            }));
1031            if r.is_err() {
1032                eprintln!(
1033                    "judge: skipping {op}@{size:?} on {}:{} kernel_source={:?} — kernel panicked in warmup",
1034                    device.backend, device.device_index, alt.kernel_source,
1035                );
1036                return None;
1037            }
1038            // Match `time_op_capturing`'s call-twice tolerance for
1039            // panic-vs-return-Err: an alternative that returns Err is
1040            // a real signal (the kernel rejected the inputs) — skip
1041            // rather than abort.
1042            if let Ok(Err(_)) = r {
1043                eprintln!(
1044                    "judge: skipping {op}@{size:?} on {}:{} kernel_source={:?} — kernel returned Err in warmup",
1045                    device.backend, device.device_index, alt.kernel_source,
1046                );
1047                return None;
1048            }
1049        }
1050
1051        let mut timings_ns = Vec::with_capacity(self.iterations as usize);
1052        let mut captured_output: Option<Vec<f32>> = None;
1053        for _ in 0..self.iterations {
1054            let t0 = Instant::now();
1055            let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
1056                let mut outs = vec![Arc::clone(&prepared.output)];
1057                kernel(
1058                    &prepared.inputs,
1059                    &mut outs,
1060                    &prepared.layouts,
1061                    &prepared.op_params,
1062                )
1063            }));
1064            let elapsed_ns = t0.elapsed().as_nanos() as u64;
1065            match res {
1066                Ok(Ok(())) => {}
1067                _ => {
1068                    eprintln!(
1069                        "judge: skipping {op}@{size:?} on {}:{} kernel_source={:?} — direct call failed mid-run",
1070                        device.backend, device.device_index, alt.kernel_source,
1071                    );
1072                    return None;
1073                }
1074            }
1075            timings_ns.push(elapsed_ns);
1076            if captured_output.is_none() {
1077                captured_output = Some(read_output_f32(&prepared.output));
1078            }
1079        }
1080        timings_ns.sort_unstable();
1081        let median = timings_ns[timings_ns.len() / 2];
1082        let output = captured_output?;
1083
1084        Some(CellRun {
1085            backend: device.backend,
1086            device_index: device.device_index,
1087            output,
1088            latency_ns: median,
1089            iterations: self.iterations,
1090            kernel_source: alt.kernel_source.to_string(),
1091            // Direct-call sibling: a real, attributed revision from the
1092            // alternative's own binding — never a guess.
1093            kernel_revision_hash: alt.kernel_revision_hash,
1094        })
1095    }
1096
1097    /// Measure one (op, dtype, size) cell on one representative
1098    /// device, capturing the kernel's output alongside the timing
1099    /// data so the caller can compute pairwise consensus across
1100    /// backends. Returns `None` if the backend isn't compiled in,
1101    /// its constructor errors out, or the realize itself fails.
1102    ///
1103    /// **Reference-retirement note (2026-06-07)**: replaces the
1104    /// pre-retirement `measure_on_device` which compared against
1105    /// `tensor.realize_f32_reference()` per-cell. The new flow
1106    /// defers correctness comparison to [`Self::run`]'s consensus
1107    /// pass.
1108    fn measure_on_device_capturing(
1109        &self,
1110        op: OpKind,
1111        dtype: DType,
1112        size: &OpSize,
1113        device: &DeviceDescriptor,
1114    ) -> Option<CellRun> {
1115        // Native-implementation gate (executor-unification Session 2):
1116        // the bridge realizer dispatches through the picker, whose
1117        // off-device fallback (picker-arc step 4b) would silently run
1118        // a missing GPU op on the CPU — and this cell would record a
1119        // CPU+transfer latency mislabeled as `device.backend`. The
1120        // legacy executor skipped such cells (its trait method
1121        // panicked); preserve that contract by requiring a binding-
1122        // table alternative at the cell before measuring. CPU is
1123        // exempt: it's the fallback root (nowhere to fall back to —
1124        // a genuinely missing CPU kernel surfaces as a realize Err
1125        // below and the cell is skipped through that path).
1126        //
1127        // NB: existence-checked via `lookup_alternatives`, NOT via
1128        // `primary_kernel_source() == ""` — `kernel_source` is a
1129        // diagnostic tag that legitimately defaults to `""` (the
1130        // baracuda CUDA registrations are untagged), so an empty tag
1131        // does not mean an empty cell.
1132        if device.backend != BackendId::Cpu && !has_binding_alternative(op, dtype, device.backend) {
1133            eprintln!(
1134                "judge: skipping {op}@{size:?} ({dtype:?}) on {}:{} — no binding-table \
1135                 alternative registered for this backend/dtype",
1136                device.backend, device.device_index,
1137            );
1138            return None;
1139        }
1140
1141        // Walk the factory registry instead of naming each backend.
1142        // A backend that isn't compiled in simply doesn't appear in
1143        // the registry; one whose constructor fails is logged and
1144        // skipped per-device.
1145        let factory = match crate::factories::factory_for(device.backend) {
1146            Some(f) => f,
1147            None => {
1148                eprintln!(
1149                    "judge: skipping {}:{} — backend not compiled in",
1150                    device.backend, device.device_index,
1151                );
1152                return None;
1153            }
1154        };
1155        let mut realizer = match factory.try_make_realizer(device.device_index) {
1156            Ok(r) => r,
1157            Err(e) => {
1158                eprintln!(
1159                    "judge: skipping {}:{} — factory failed: {e}",
1160                    device.backend, device.device_index,
1161                );
1162                return None;
1163            }
1164        };
1165
1166        let cell = self.time_op_capturing(op, dtype, size, device, realizer.as_mut());
1167        drop(realizer);
1168        cell
1169    }
1170
1171    /// Build the op's input graph, realize it for warmup + timed
1172    /// runs, measure latency, capture the first iteration's output.
1173    /// Returns the captured output alongside the timing data; the
1174    /// caller computes correctness via consensus across backends
1175    /// (see [`Self::run`]).
1176    ///
1177    /// All realize calls are wrapped in `catch_unwind` — a kernel
1178    /// that panics on a corner-case input is logged and skipped, not
1179    /// fatal to the run. Post-Session-2 the realizer also returns
1180    /// typed `Err`s (the bridge's no-panics contract); an erroring
1181    /// backend is skipped through the same per-cell path. This is
1182    /// what makes the Judge safe to expand across the full op
1183    /// surface ahead of every backend's coverage.
1184    fn time_op_capturing(
1185        &self,
1186        op: OpKind,
1187        dtype: DType,
1188        size: &OpSize,
1189        device: &DeviceDescriptor,
1190        realizer: &mut dyn crate::factories::Realizer,
1191    ) -> Option<CellRun> {
1192        let tensor =
1193            match std::panic::catch_unwind(AssertUnwindSafe(|| build_input_graph(op, dtype, size)))
1194            {
1195                Ok(t) => t,
1196                Err(_) => {
1197                    eprintln!("judge: skipping {op}@{size:?} — build_input_graph panicked",);
1198                    return None;
1199                }
1200            };
1201
1202        // Warmup — discard timings; stabilize kernel caches, warm
1203        // up any BLAS internal state, fault-in heap, upload Consts
1204        // into the realizer's persistent cache. If any warmup call
1205        // panics or errors (e.g. backend doesn't support this op),
1206        // skip the entire (op, backend) cell.
1207        for _ in 0..self.warmup {
1208            match std::panic::catch_unwind(AssertUnwindSafe(|| {
1209                realizer.realize_capture_f32(&tensor)
1210            })) {
1211                Ok(Ok(_)) => {}
1212                Ok(Err(e)) => {
1213                    eprintln!(
1214                        "judge: skipping {op}@{size:?} on {}:{} — backend realize errored: {e}",
1215                        device.backend, device.device_index,
1216                    );
1217                    return None;
1218                }
1219                Err(_) => {
1220                    eprintln!(
1221                        "judge: skipping {op}@{size:?} on {}:{} — backend realize panicked",
1222                        device.backend, device.device_index,
1223                    );
1224                    return None;
1225                }
1226            }
1227        }
1228
1229        // Timed iterations — record each and take the median so
1230        // one pre-empted scheduler tick doesn't skew the number.
1231        // Capture the first iteration's output for consensus
1232        // comparison; subsequent iterations discard the output
1233        // (correctness is deterministic for a given backend + input).
1234        let mut timings_ns = Vec::with_capacity(self.iterations as usize);
1235        let mut captured_output: Option<Vec<f32>> = None;
1236        for _ in 0..self.iterations {
1237            let t0 = Instant::now();
1238            let out = match std::panic::catch_unwind(AssertUnwindSafe(|| {
1239                realizer.realize_capture_f32(&tensor)
1240            })) {
1241                Ok(Ok(v)) => v,
1242                Ok(Err(e)) => {
1243                    eprintln!(
1244                        "judge: skipping {op}@{size:?} on {}:{} — backend realize errored mid-run: {e}",
1245                        device.backend, device.device_index,
1246                    );
1247                    return None;
1248                }
1249                Err(_) => {
1250                    eprintln!(
1251                        "judge: skipping {op}@{size:?} on {}:{} — backend realize panicked mid-run",
1252                        device.backend, device.device_index,
1253                    );
1254                    return None;
1255                }
1256            };
1257            let elapsed_ns = t0.elapsed().as_nanos() as u64;
1258            timings_ns.push(elapsed_ns);
1259            if captured_output.is_none() {
1260                captured_output = Some(out);
1261            }
1262        }
1263        timings_ns.sort_unstable();
1264        let median = timings_ns[timings_ns.len() / 2];
1265        let output = captured_output?;
1266
1267        // Attribution (executor-unification Session 3 rider): the
1268        // bridge realizer reports the `kernel_source` of the
1269        // alternative the picker actually dispatched for the measured
1270        // root. At multi-sibling cells (portable/AOCL/MKL at one CPU
1271        // key) the dispatched sibling can differ from the
1272        // first-registered one, so the realizer's report is the only
1273        // truthful tag. `None` (no plan entry for the root, or a
1274        // realizer impl without a picker) falls back to the
1275        // first-registered tag — exactly the alternative the
1276        // executor's `compile_node` fallback dispatches in that case.
1277        // The tag AND its revision come from the SAME picker result
1278        // (`last_kernel_source` / `last_kernel_revision`, set together in the
1279        // bridge realizer), so they are both `Some` or both `None`.
1280        // Conservative cut (Judge item 7, Option A): when the picker did not
1281        // attribute this dispatch — no plan `AlternativeSet` for the root, or
1282        // a best-effort lock/lookup miss — write NO row. `alts.first()`'s
1283        // revision would be a plausible GUESS about a real sibling, filed
1284        // against a measurement we could not attribute; a `0` would claim
1285        // "untracked" about a kernel that actually ran. Both are rows that
1286        // LIE about kernel identity — the identity `kernel_revision_hash`
1287        // exists to make trustworthy — so an absent row is the honest outcome
1288        // (an absent cell just misses in the oracle: Layer-1 static costs
1289        // stand). The dropped-row count is reported in the re-profile commit.
1290        let (kernel_source, kernel_revision_hash) = match (
1291            realizer.last_kernel_source(),
1292            realizer.last_kernel_revision(),
1293        ) {
1294            (Some(src), Some(rev)) => (src.to_string(), rev),
1295            _ => return None,
1296        };
1297
1298        Some(CellRun {
1299            backend: device.backend,
1300            device_index: device.device_index,
1301            output,
1302            latency_ns: median,
1303            iterations: self.iterations,
1304            kernel_source,
1305            kernel_revision_hash,
1306        })
1307    }
1308}
1309
1310// =============================================================
1311// Direct-kernel-call infrastructure for per-alternative timing
1312// (Phase 1.1 of the backend-extensions refactor, 2026-06-08)
1313// =============================================================
1314
1315/// One alternative the direct-call path will time. Holds the
1316/// kernel function pointer and the `kernel_source` tag the resulting
1317/// CellRun carries.
1318struct DirectCallAlternative {
1319    kernel: fuel_dispatch::kernel::KernelRef,
1320    kernel_source: &'static str,
1321    /// Revision of this alternative's binding, from the same `BindingEntry`
1322    /// as `kernel_source` (so tag and revision agree by construction).
1323    kernel_revision_hash: u64,
1324}
1325
1326/// Shared inputs/outputs/layouts/op_params for one (op, size) cell.
1327/// Built once per cell and reused across every alternative's
1328/// direct-call measurement so the kernels compare on identical
1329/// inputs (a prerequisite for cross-alternative consensus to mean
1330/// anything).
1331struct PreparedDirectCall {
1332    inputs: Vec<Arc<RwLock<fuel_memory::Storage>>>,
1333    output: Arc<RwLock<fuel_memory::Storage>>,
1334    layouts: Vec<fuel_ir::Layout>,
1335    op_params: fuel_dispatch::kernel::OpParams,
1336}
1337
1338/// Does the binding table hold ANY alternative at the cell's
1339/// canonical F32 `(op, dtypes, backend)` key? Existence check for
1340/// the Judge's native-implementation gate.
1341///
1342/// Judge item 7 removed the first-registered `kernel_source` fallback
1343/// that used to attribute a dispatched measurement when the realizer
1344/// reported no picker sibling: the tag alone was already lossy (an empty
1345/// tag conflated "no alternative" with an untagged registration — every
1346/// baracuda CUDA binding is untagged), and it carries no revision at all.
1347/// The dispatched-path attribution now comes solely from the realizer's
1348/// picker report (`last_kernel_source` + `last_kernel_revision`), so a
1349/// cell with no attribution produces no measurement row rather than an
1350/// untagged, zero-revision guess.
1351///
1352/// Ops without a canonical dtype mapping return `false` — the gate
1353/// then skips the (non-CPU) cell rather than risk recording an
1354/// off-device-fallback measurement under the wrong backend label.
1355/// Unreachable for today's [`PROFILED_OPS`] (all are mapped).
1356fn has_binding_alternative(op: OpKind, dtype: DType, backend: BackendId) -> bool {
1357    let dtypes = match canonical_binding_dtypes_for(op, dtype) {
1358        Some(d) => d,
1359        None => return false,
1360    };
1361    let table = fuel_dispatch::dispatch::global_bindings();
1362    !table.lookup_alternatives(op, &dtypes, backend).is_empty()
1363}
1364
1365/// Whether `alt` (identified by its `kernel_source` tag and
1366/// `kernel_revision_hash`) IS the dispatched kernel. Matched on the FULL
1367/// identity — BOTH fields — because variant siblings of one provider share a
1368/// tag and differ only by revision, so the tag alone no longer picks a sibling
1369/// out. Keying the skip on the tag would skip the wrong sibling; this is the
1370/// one place that identity is compared, so it cannot drift from the key
1371/// `ProfileEntry`/`Candidate`/`Pick` carry.
1372fn is_dispatched_alt(
1373    alt_source: &str,
1374    alt_revision: u64,
1375    dispatched_source: &str,
1376    dispatched_revision: u64,
1377) -> bool {
1378    alt_source == dispatched_source && alt_revision == dispatched_revision
1379}
1380
1381/// Collect every binding-table alternative at the cell EXCEPT the
1382/// dispatched one (already timed via the realizer). Returns a fresh
1383/// `Vec` of [`DirectCallAlternative`] — empty when the cell has only
1384/// one alternative or the op isn't direct-call-eligible yet.
1385fn direct_call_alternatives(
1386    op: OpKind,
1387    dispatched_kernel_source: &str,
1388    dispatched_kernel_revision: u64,
1389) -> Vec<DirectCallAlternative> {
1390    // Direct-call path is F32-only (see `measure_extra_alternatives`'s
1391    // dtype gate + `prepare_direct_call_inputs`), so the binding key is
1392    // always the F32 tuple here.
1393    let dtypes = match canonical_binding_dtypes_for(op, DType::F32) {
1394        Some(d) => d,
1395        None => return Vec::new(),
1396    };
1397    let table = fuel_dispatch::dispatch::global_bindings();
1398    let alts = table.lookup_alternatives(op, &dtypes, BackendId::Cpu);
1399    if alts.len() < 2 {
1400        return Vec::new();
1401    }
1402    // Skip the alternative the realizer already measured. Its identity is
1403    // (kernel_source, kernel_revision_hash) — the tag ALONE no longer picks a
1404    // sibling out, now that variant siblings of one provider share a tag and
1405    // differ only by revision, so keying the skip on the tag could skip the
1406    // WRONG sibling (leaving the dispatched one to be re-measured as an extra
1407    // AND dropping a genuine sibling). Skip the FIRST entry matching BOTH
1408    // fields, wherever it sits in registration order (the picker is free to
1409    // dispatch a non-first sibling). If nothing matches — off-cell dispatch, or
1410    // a revision the current binding table no longer holds — nothing is skipped:
1411    // every CPU sibling at the cell is then unmeasured work. That is a redundant
1412    // measurement (the oracle min-collapses the duplicate at the identical
1413    // (source, revision) key), never a wrong one.
1414    let mut extras = Vec::with_capacity(alts.len().saturating_sub(1));
1415    let mut skipped_dispatched = false;
1416    for alt in alts.iter() {
1417        if !skipped_dispatched
1418            && is_dispatched_alt(
1419                alt.kernel_source,
1420                alt.kernel_revision_hash,
1421                dispatched_kernel_source,
1422                dispatched_kernel_revision,
1423            )
1424        {
1425            skipped_dispatched = true;
1426            continue;
1427        }
1428        extras.push(DirectCallAlternative {
1429            kernel: alt.kernel,
1430            kernel_source: alt.kernel_source,
1431            kernel_revision_hash: alt.kernel_revision_hash,
1432        });
1433    }
1434    extras
1435}
1436
1437/// Map an `OpKind` to its binding-table dtype list (inputs + outputs)
1438/// at the profiled `dtype`. Every profiled family is uniform-dtype, so
1439/// the list is just `dtype` repeated per operand slot. Returns `None`
1440/// for op families the native-implementation gate / direct-call path
1441/// don't yet support — caller falls back to first-alternative-only
1442/// measurement at the cell (or skips the non-CPU cell).
1443fn canonical_binding_dtypes_for(op: OpKind, dtype: DType) -> Option<Vec<DType>> {
1444    // Most elementwise ops follow `[input..., output]`. Reductions
1445    // and reduce-to follow `[input, output]`. MatMul is 3 inputs
1446    // (no — 2 inputs + 1 output): `[lhs, rhs, out] = [T, T, T]`.
1447    //
1448    // Dtype axis (2026-07-04): every profiled family in this map is
1449    // uniform-dtype (all operands share `dtype`), so substituting the
1450    // profiled dtype `T` for the former hard-coded F32 gives the right
1451    // binding-table key for F16/BF16 cells too. The mixed-dtype ops
1452    // (Cast, QMatMul, comparisons with a U8 mask) are not in
1453    // PROFILED_OPS and fall through to the `None` arm.
1454    let t = dtype;
1455    Some(match op {
1456        OpKind::MatMul => vec![t, t, t],
1457        // Binary elementwise: 2 inputs + 1 output.
1458        OpKind::AddElementwise
1459        | OpKind::SubElementwise
1460        | OpKind::MulElementwise
1461        | OpKind::DivElementwise
1462        | OpKind::MaximumElementwise
1463        | OpKind::MinimumElementwise
1464        | OpKind::PowElementwise
1465        | OpKind::RemElementwise => vec![t, t, t],
1466        // Unary elementwise: 1 input + 1 output.
1467        OpKind::NegElementwise
1468        | OpKind::SqrElementwise
1469        | OpKind::SqrtElementwise
1470        | OpKind::ExpElementwise
1471        | OpKind::LogElementwise
1472        | OpKind::SinElementwise
1473        | OpKind::CosElementwise
1474        | OpKind::TanhElementwise
1475        | OpKind::SigmoidElementwise
1476        | OpKind::SiluElementwise
1477        | OpKind::GeluTanhElementwise
1478        | OpKind::ReluElementwise
1479        | OpKind::StepElementwise
1480        | OpKind::RecipElementwise
1481        | OpKind::AbsElementwise
1482        | OpKind::FloorElementwise
1483        | OpKind::CeilElementwise
1484        | OpKind::RoundElementwise
1485        | OpKind::SignElementwise
1486        | OpKind::ErfElementwise
1487        | OpKind::GeluElementwise
1488        | OpKind::RsqrtElementwise
1489        | OpKind::Affine
1490        | OpKind::ClampElementwise
1491        | OpKind::PowIElementwise => vec![t, t],
1492        // Reductions: 1 input + 1 output.
1493        OpKind::SumReduce
1494        | OpKind::MaxReduce
1495        | OpKind::MinReduce
1496        | OpKind::MeanReduce
1497        | OpKind::ReduceSumTo
1498        | OpKind::ReduceMaxTo => vec![t, t],
1499        // Fused decode attention, NO-alibi key `[q, k, v, out]` (the
1500        // Judge builds q/k/v without alibi_slopes). This drives the
1501        // non-CPU native-implementation gate ([`has_binding_alternative`])
1502        // so a live CUDA/Vulkan run finds the flash binding and populates
1503        // the GPU arm-1 rows (CUDA flash is f16/bf16-only, so its f32
1504        // cell has no binding and is skipped cleanly). The direct-call
1505        // path stays disabled for FlashAttn — `prepare_direct_call_inputs`
1506        // has no FlashAttn arm, so `measure_extra_alternatives` returns
1507        // None regardless; this mapping only powers the existence gate +
1508        // the first-registered `kernel_source` fallback.
1509        OpKind::FlashAttn => vec![t, t, t, t],
1510        // All others: not yet wired into the direct-call path. The
1511        // realizer-measured first alternative is the only entry for
1512        // the cell until this list expands.
1513        _ => return None,
1514    })
1515}
1516
1517/// Build the inputs/outputs/layouts/op_params for direct-call timing
1518/// of one (op, size) cell. Returns `None` for ops not yet supported.
1519/// Mirrors the input domains used by `build_input_graph` so the
1520/// direct-call CellRuns produce numerically-comparable output to the
1521/// realizer-measured primary alternative (consensus needs aligned
1522/// inputs across CellRuns).
1523fn prepare_direct_call_inputs(op: OpKind, size: &OpSize) -> Option<PreparedDirectCall> {
1524    use fuel_cpu_backend::CpuStorageBytes;
1525    use fuel_dispatch::kernel::OpParams;
1526    use fuel_ir::Layout;
1527    use fuel_memory::{BackendStorage, Storage};
1528
1529    let make_storage = |bytes: CpuStorageBytes| {
1530        Arc::new(RwLock::new(Storage::new(
1531            BackendStorage::Cpu(bytes),
1532            DType::F32,
1533        )))
1534    };
1535
1536    match (op, *size) {
1537        (OpKind::MatMul, OpSize::MatMul { m, n, k }) => {
1538            let a_data: Vec<f32> = (0..(m * k)).map(|i| ((i as f32) * 1.3e-3).sin()).collect();
1539            let b_data: Vec<f32> = (0..(k * n)).map(|i| ((i as f32) * 1.7e-3).cos()).collect();
1540            let lhs = make_storage(CpuStorageBytes::from_slice(&a_data));
1541            let rhs = make_storage(CpuStorageBytes::from_slice(&b_data));
1542            let out = make_storage(CpuStorageBytes::from_zero_bytes(
1543                m * n * std::mem::size_of::<f32>(),
1544            ));
1545            let layouts = vec![
1546                Layout::contiguous(Shape::from_dims(&[m, k])),
1547                Layout::contiguous(Shape::from_dims(&[k, n])),
1548                Layout::contiguous(Shape::from_dims(&[m, n])),
1549            ];
1550            let op_params = OpParams::Matmul {
1551                lhs_batch_dims: Vec::new(),
1552                rhs_batch_dims: Vec::new(),
1553                m,
1554                n,
1555                k,
1556                // Judge profiles dense static matmuls.
1557                m_compute: fuel_dispatch::kernel::MatmulM::All,
1558            };
1559            Some(PreparedDirectCall {
1560                inputs: vec![lhs, rhs],
1561                output: out,
1562                layouts,
1563                op_params,
1564            })
1565        }
1566        (op, OpSize::Elementwise(n)) if is_binary_elementwise(op) => {
1567            let (a_data, b_data) = binary_inputs(op, n);
1568            let lhs = make_storage(CpuStorageBytes::from_slice(&a_data));
1569            let rhs = make_storage(CpuStorageBytes::from_slice(&b_data));
1570            let out = make_storage(CpuStorageBytes::from_zero_bytes(
1571                n * std::mem::size_of::<f32>(),
1572            ));
1573            let shape = Shape::from_dims(&[n]);
1574            let layouts = vec![
1575                Layout::contiguous(shape.clone()),
1576                Layout::contiguous(shape.clone()),
1577                Layout::contiguous(shape),
1578            ];
1579            Some(PreparedDirectCall {
1580                inputs: vec![lhs, rhs],
1581                output: out,
1582                layouts,
1583                op_params: OpParams::None,
1584            })
1585        }
1586        (op, OpSize::Elementwise(n)) if is_unary_elementwise(op) => {
1587            let raw = unary_input(n);
1588            let needs_nonzero = matches!(
1589                op,
1590                OpKind::SqrtElementwise
1591                    | OpKind::LogElementwise
1592                    | OpKind::RecipElementwise
1593                    | OpKind::RsqrtElementwise,
1594            );
1595            let data: Vec<f32> = if needs_nonzero {
1596                raw.into_iter().map(|x| x + 1.5).collect()
1597            } else {
1598                raw
1599            };
1600            let inp = make_storage(CpuStorageBytes::from_slice(&data));
1601            let out = make_storage(CpuStorageBytes::from_zero_bytes(
1602                n * std::mem::size_of::<f32>(),
1603            ));
1604            let shape = Shape::from_dims(&[n]);
1605            let layouts = vec![Layout::contiguous(shape.clone()), Layout::contiguous(shape)];
1606            Some(PreparedDirectCall {
1607                inputs: vec![inp],
1608                output: out,
1609                layouts,
1610                op_params: OpParams::None,
1611            })
1612        }
1613        (op, OpSize::Elementwise(n)) if is_scalar_op(op) => {
1614            let data = unary_input(n);
1615            let inp = make_storage(CpuStorageBytes::from_slice(&data));
1616            let out = make_storage(CpuStorageBytes::from_zero_bytes(
1617                n * std::mem::size_of::<f32>(),
1618            ));
1619            let shape = Shape::from_dims(&[n]);
1620            let layouts = vec![Layout::contiguous(shape.clone()), Layout::contiguous(shape)];
1621            let op_params = match op {
1622                OpKind::Affine => OpParams::Affine { mul: 2.0, add: 0.0 },
1623                OpKind::ClampElementwise => OpParams::Clamp {
1624                    min: -0.5,
1625                    max: 0.5,
1626                },
1627                OpKind::PowIElementwise => OpParams::PowI { exp: 3 },
1628                _ => unreachable!(),
1629            };
1630            Some(PreparedDirectCall {
1631                inputs: vec![inp],
1632                output: out,
1633                layouts,
1634                op_params,
1635            })
1636        }
1637        // Reductions + ReduceTo not yet wired into the direct-call
1638        // path: the OpParams shape needs the input layout, and the
1639        // output shape is op-dependent. Falls back to first-alt-only
1640        // measurement until the binding-table key conventions are
1641        // double-checked end-to-end here.
1642        _ => None,
1643    }
1644}
1645
1646/// Read an output `Storage`'s F32 bytes back out as a `Vec<f32>` for
1647/// consensus comparison.
1648fn read_output_f32(out: &Arc<RwLock<fuel_memory::Storage>>) -> Vec<f32> {
1649    use fuel_memory::BackendStorage;
1650    let g = out.read().expect("output storage lock");
1651    match &g.inner {
1652        BackendStorage::Cpu(c) => c
1653            .as_slice::<f32>()
1654            .expect("cpu storage as f32 slice")
1655            .to_vec(),
1656        // Direct-call path is CPU-only today; other backends are
1657        // guarded by `measure_extra_alternatives` returning None
1658        // before we reach this point. A non-CPU storage here is a
1659        // bug, but we'd rather skip the entry than panic in the
1660        // Judge.
1661        #[allow(unreachable_patterns)]
1662        _ => Vec::new(),
1663    }
1664}
1665
1666/// Per-op size descriptor. Exposed (not private) so tests can supply
1667/// a shrunk `size_plan_override`; not in the persisted profile
1668/// report shape — the report uses the bucketed [`SizeClass`] instead.
1669#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1670pub enum OpSize {
1671    /// Dense matmul `[m, k] @ [k, n] -> [m, n]`.
1672    MatMul { m: usize, n: usize, k: usize },
1673    /// Flat 1-D shape used by unary, binary, scalar/affine, clamp, powi
1674    /// kernels. Probe builds inputs of shape `[n]`.
1675    Elementwise(usize),
1676    /// Reduction along the last dim of a 2-D shape `[rows, cols]`.
1677    /// Total element count = rows × cols. Used for `SumReduce` /
1678    /// `MaxReduce` / `MinReduce` / `MeanReduce`.
1679    Reduce { rows: usize, cols: usize },
1680    /// Reduce-to-broadcast-target from `[rows, cols]` → `[1, cols]`
1681    /// (sum-reduce-along-rows). Used for `ReduceSumTo` / `ReduceMaxTo`.
1682    ReduceTo { rows: usize, cols: usize },
1683    /// Decode-shaped GQA attention for [`OpKind::FlashAttn`]. seq_q is
1684    /// fixed at 1 (autoregressive decode — the regime slice-4's
1685    /// flash-vs-decomposed ranking cares about). Builds
1686    /// `q = [b, hq, 1, d]`, `k = v = [b, hkv, k_len, d]` (`hq % hkv == 0`,
1687    /// GQA), attending the full `k_len` prefix. Keyed via
1688    /// [`SizeClass::attention`]`(hq, k_len, d)`.
1689    Attention {
1690        b: usize,
1691        hq: usize,
1692        hkv: usize,
1693        d: usize,
1694        k_len: usize,
1695    },
1696}
1697
1698impl OpSize {
1699    fn total_elements(&self) -> usize {
1700        match *self {
1701            OpSize::MatMul { m, n, k: _ } => m * n,
1702            OpSize::Elementwise(n) => n,
1703            OpSize::Reduce { rows, cols } => rows * cols,
1704            OpSize::ReduceTo { rows, cols } => rows * cols,
1705            // Output element count of decode attention `[b, hq, 1, d]`.
1706            // Attention keys via `SizeClass::attention`, not this — this
1707            // is only the Total-key fallback for a non-attention consumer.
1708            OpSize::Attention { b, hq, d, .. } => b * hq * d,
1709        }
1710    }
1711
1712    /// Element count of the input buffer the capture tool's
1713    /// [`fuel_correctness_fixtures::capture::deterministic_f32_input`]
1714    /// would emit for an op of this size. Used by the Judge's
1715    /// fixture-lookup path to regenerate-and-hash the input.
1716    ///
1717    /// Per-op shape (mirrors capture's `deterministic_f32_input`):
1718    /// - MatMul: `m*k + k*n` — `a_data` followed by `b_data`.
1719    /// - Elementwise (binary): `2 * n` — `[a, b]` concatenated.
1720    /// - Elementwise (unary / scalar / clamp / powi): `n`.
1721    /// - Reduce / ReduceTo: `rows * cols`.
1722    ///
1723    /// `op` is needed to disambiguate binary vs unary on the
1724    /// `Elementwise(n)` shape.
1725    fn input_elements(&self, op: OpKind) -> usize {
1726        match *self {
1727            OpSize::MatMul { m, n, k } => m * k + k * n,
1728            OpSize::Elementwise(n) => {
1729                if is_binary_elementwise(op) {
1730                    2 * n
1731                } else {
1732                    n
1733                }
1734            }
1735            OpSize::Reduce { rows, cols } => rows * cols,
1736            OpSize::ReduceTo { rows, cols } => rows * cols,
1737            // q + k + v elements. FlashAttn has no capture-tool fixture
1738            // (not in the fixture distribution), so this feeds no live
1739            // fixture-hash path today; kept consistent for completeness.
1740            OpSize::Attention {
1741                b,
1742                hq,
1743                hkv,
1744                d,
1745                k_len,
1746            } => b * hq * 1 * d + 2 * (b * hkv * k_len * d),
1747        }
1748    }
1749}
1750
1751/// Build a leaf [`Tensor`] at `dtype` from f32 source `data`. F16/
1752/// BF16 down-convert the deterministic f32 domain element-wise
1753/// (`half::f16/bf16::from_f32`) so the same input distribution is
1754/// exercised at every precision — the cross-dtype comparison stays
1755/// apples-to-apples modulo the dtype's own rounding. Panics only on a
1756/// dtype outside the profiled `{F32, F16, BF16}` set (never reached:
1757/// [`PROFILED_DTYPES`] is the sole caller of `build_input_graph`).
1758fn make_leaf(dtype: DType, data: Vec<f32>, shape: Shape) -> crate::lazy::Tensor {
1759    use crate::lazy::Tensor;
1760    let dev = crate::Device::cpu();
1761    // ⚠️ GAP-003, and this is NEITHER of the two usual dispositions.
1762    //
1763    // `make_leaf` cannot prove its operands agree -- `data` and `shape` are BOTH
1764    // parameters, so there is no local proof and an `.expect` claiming one would
1765    // be false. But `Result` is wrong here too, and for a reason that is a
1766    // property of the CALLER, not of this function: the sole caller wraps
1767    // `build_input_graph` in `std::panic::catch_unwind` (see `Judge::run`), so
1768    // this path ALREADY has a working error channel and a panic here is
1769    // CAUGHT and reported as a profile failure, not an abort.
1770    //
1771    // Adding `Result` would give the judge two error channels for one failure
1772    // and force every caller to handle both. So the message states the DESIGN
1773    // BOUNDARY rather than a proof it cannot make -- which is the honest third
1774    // form when a proof exists neither locally nor at all.
1775    let leaf = |t: std::result::Result<Tensor, fuel_ir::Error>| {
1776        t.expect(
1777            "make_leaf: data length must match shape element count. Both are              caller-supplied, so this function cannot prove it -- the panic is              the judge's error channel and is caught by the catch_unwind in              Judge::run",
1778        )
1779    };
1780    match dtype {
1781        DType::F32 => leaf(Tensor::from_f32(data, shape, &dev)),
1782        DType::F16 => leaf(Tensor::from_f16(
1783            data.iter()
1784                .map(|&x| half::f16::from_f32(x))
1785                .collect::<Vec<_>>(),
1786            shape,
1787            &dev,
1788        )),
1789        DType::BF16 => leaf(Tensor::from_bf16(
1790            data.iter()
1791                .map(|&x| half::bf16::from_f32(x))
1792                .collect::<Vec<_>>(),
1793            shape,
1794            &dev,
1795        )),
1796        other => panic!("build_input_graph: unsupported profiled dtype {other:?}"),
1797    }
1798}
1799
1800/// Build a same-graph const sibling of `a` at `dtype` from f32 source
1801/// `data`. Mirrors [`make_leaf`] but through the `const_*_like`
1802/// constructors so the second operand shares `a`'s graph.
1803fn make_const_like(
1804    a: &crate::lazy::Tensor,
1805    dtype: DType,
1806    data: Vec<f32>,
1807    shape: Shape,
1808) -> crate::lazy::Tensor {
1809    // GAP-003, same DESIGN-BOUNDARY form as `make_leaf` directly above: `data`
1810    // and `shape` are BOTH parameters, so there is no local proof -- but the sole
1811    // caller wraps `build_input_graph` in `std::panic::catch_unwind`, so this
1812    // path already HAS an error channel and a `Result` would be a second one.
1813    let cl = |t: std::result::Result<crate::lazy::Tensor, fuel_ir::Error>| {
1814        t.expect(
1815            "make_const_like: data length must match shape element count. Both are              caller-supplied, so this function cannot prove it -- the panic is the              judge's error channel and is caught by the catch_unwind in Judge::run",
1816        )
1817    };
1818    match dtype {
1819        DType::F32 => cl(a.const_f32_like(data, shape)),
1820        DType::F16 => cl(a.const_f16_like(
1821            data.iter()
1822                .map(|&x| half::f16::from_f32(x))
1823                .collect::<Vec<_>>(),
1824            shape,
1825        )),
1826        DType::BF16 => cl(a.const_bf16_like(
1827            data.iter()
1828                .map(|&x| half::bf16::from_f32(x))
1829                .collect::<Vec<_>>(),
1830            shape,
1831        )),
1832        other => panic!("build_input_graph: unsupported profiled dtype {other:?}"),
1833    }
1834}
1835
1836/// Build a 1-node graph for the given (op, dtype, size) that takes
1837/// constant inputs. The inputs are deterministic (generated in f32 and
1838/// down-converted to `dtype`) so precision comparisons across backends
1839/// are meaningful.
1840fn build_input_graph(op: OpKind, dtype: DType, size: &OpSize) -> crate::lazy::Tensor {
1841    match (op, *size) {
1842        (OpKind::MatMul, OpSize::MatMul { m, n, k }) => {
1843            let a_data: Vec<f32> = (0..(m * k)).map(|i| ((i as f32) * 1.3e-3).sin()).collect();
1844            let b_data: Vec<f32> = (0..(k * n)).map(|i| ((i as f32) * 1.7e-3).cos()).collect();
1845            let a = make_leaf(dtype, a_data, Shape::from_dims(&[m, k]));
1846            let b = make_const_like(&a, dtype, b_data, Shape::from_dims(&[k, n]));
1847            a.matmul(&b).unwrap()
1848        }
1849        (op, OpSize::Elementwise(n)) if is_binary_elementwise(op) => {
1850            let (a_data, b_data) = binary_inputs(op, n);
1851            let a = make_leaf(dtype, a_data, Shape::from_dims(&[n]));
1852            let b = make_const_like(&a, dtype, b_data, Shape::from_dims(&[n]));
1853            apply_binary(op, &a, &b)
1854        }
1855        // -------- elementwise unary fanout --------
1856        //
1857        // Domain notes:
1858        // - sqrt/log require strictly positive inputs; inputs are in
1859        //   `[0.5, 2.5]` after the +1.5 offset.
1860        // - exp/sigmoid saturate quickly; inputs are bounded
1861        //   `[-1, 1]` (sin output) so exp stays in `[0.36, 2.72]` and
1862        //   sigmoid stays away from saturation (more measurable
1863        //   precision differences across backends).
1864        // - silu/gelu_tanh/relu/tanh/neg/sqr/step/sin/cos all accept any f32.
1865        // -------- per-axis reductions --------
1866        //
1867        // Reduce along the last dim of `[rows, cols]`, producing
1868        // `[rows]`. Bucketing on total element count means the
1869        // dispatch table groups `Reduce(rows=1024, cols=64)` and
1870        // `Elementwise(65536)` in the same size class — fine, the
1871        // size_class is the per-op axis, not a cross-op identity.
1872        (op, OpSize::Reduce { rows, cols }) if is_reduction(op) => {
1873            let n = rows * cols;
1874            let data: Vec<f32> = (0..n).map(|i| ((i as f32) * 1.7e-3).sin()).collect();
1875            let a = make_leaf(dtype, data, Shape::from_dims(&[rows, cols]));
1876            apply_reduction(op, &a)
1877        }
1878        // -------- reduce-to-broadcast-target --------
1879        //
1880        // Reduce `[rows, cols]` to `[1, cols]`.
1881        (op, OpSize::ReduceTo { rows, cols }) if is_reduce_to(op) => {
1882            let n = rows * cols;
1883            let data: Vec<f32> = (0..n).map(|i| ((i as f32) * 1.7e-3).sin()).collect();
1884            let a = make_leaf(dtype, data, Shape::from_dims(&[rows, cols]));
1885            let target = Shape::from_dims(&[1, cols]);
1886            match op {
1887                OpKind::ReduceSumTo => a.reduce_sum_to(target),
1888                OpKind::ReduceMaxTo => a.reduce_max_to(target),
1889                _ => unreachable!(),
1890            }
1891        }
1892        // -------- scalar / clamp / powi (one-input non-unary) --------
1893        //
1894        // Affine here is the canonical MulScalar form (mul=2.0); the
1895        // dispatch key OpKind::Affine covers AddScalar too. Clamp uses
1896        // bounds [-0.5, 0.5] so roughly half the [-1, 1] sin input
1897        // gets clipped, exercising both branches of the kernel. PowI
1898        // uses exp=3 so the kernel iterates rather than degenerating
1899        // to sqr/identity.
1900        (op, OpSize::Elementwise(n)) if is_scalar_op(op) => {
1901            let data: Vec<f32> = unary_input(n);
1902            let a = make_leaf(dtype, data, Shape::from_dims(&[n]));
1903            match op {
1904                OpKind::Affine => a.mul_scalar(2.0),
1905                OpKind::ClampElementwise => a.clamp(-0.5, 0.5),
1906                OpKind::PowIElementwise => a.powi(3),
1907                _ => unreachable!(),
1908            }
1909        }
1910        (op, OpSize::Elementwise(n)) if is_unary_elementwise(op) => {
1911            let raw = unary_input(n);
1912            // Sqrt/Log require strictly positive inputs; Recip can't be
1913            // measured against zero (1/0 = inf saturates `max_rel_err`).
1914            // The +1.5 shift puts unary_input's [-1, 1] range into
1915            // [0.5, 2.5] — safe for all three.
1916            let needs_nonzero = matches!(
1917                op,
1918                OpKind::SqrtElementwise
1919                    | OpKind::LogElementwise
1920                    | OpKind::RecipElementwise
1921                    | OpKind::RsqrtElementwise,
1922            );
1923            let data: Vec<f32> = if needs_nonzero {
1924                raw.into_iter().map(|x| x + 1.5).collect()
1925            } else {
1926                raw
1927            };
1928            let a = make_leaf(dtype, data, Shape::from_dims(&[n]));
1929            apply_unary(op, &a)
1930        }
1931        // -------- fused decode attention (FlashAttn) --------
1932        //
1933        // Builds the GQA decode operands q=[b,hq,1,d], k=v=[b,hkv,k_len,d]
1934        // and emits `Op::Fused(FLASH_ATTN, FlashAttn{..})` via the
1935        // Tensor `flash_attn` builder (k_len == the K length axis, so
1936        // the whole prefix is attended — the concrete-k_len decode).
1937        // seq_q=1 with `causal=true` is a no-op mask (the single query at
1938        // the last position attends every key), matching decode. On CPU
1939        // the realize path dispatches this to the registered fused CPU
1940        // FlashAttn kernel (naive SDPA, `flash_attn_{f32,f16,bf16}` in
1941        // fuel-cpu-backend::byte_kernels); f16/bf16 inputs down-convert
1942        // the deterministic f32 domain, mirroring the other families.
1943        (
1944            OpKind::FlashAttn,
1945            OpSize::Attention {
1946                b,
1947                hq,
1948                hkv,
1949                d,
1950                k_len,
1951            },
1952        ) => {
1953            let scale = 1.0 / (d as f32).sqrt();
1954            let q_elems = b * hq * 1 * d;
1955            let kv_elems = b * hkv * k_len * d;
1956            let q_data: Vec<f32> = (0..q_elems).map(|i| ((i as f32) * 1.3e-3).sin()).collect();
1957            let k_data: Vec<f32> = (0..kv_elems).map(|i| ((i as f32) * 1.7e-3).cos()).collect();
1958            let v_data: Vec<f32> = (0..kv_elems)
1959                .map(|i| ((i as f32) * 0.9e-3).sin() + 1.0)
1960                .collect();
1961            let q = make_leaf(dtype, q_data, Shape::from_dims(&[b, hq, 1, d]));
1962            let k = make_const_like(&q, dtype, k_data, Shape::from_dims(&[b, hkv, k_len, d]));
1963            let v = make_const_like(&q, dtype, v_data, Shape::from_dims(&[b, hkv, k_len, d]));
1964            q.flash_attn(&k, &v, None, scale, /*causal=*/ true, None, None, None)
1965                .expect("judge: flash_attn shape/GQA invariant")
1966        }
1967        (op, sz) => panic!("build_input_graph: op {op:?} size {sz:?} not implemented"),
1968    }
1969}
1970
1971/// Whether `op` is a one-input scalar / clamp / powi op the Judge
1972/// profiles using the `Elementwise(n)` size ladder. These differ
1973/// from the unary ops only in carrying static parameters on the op
1974/// itself (scalar coefficient, clamp bounds, integer exponent).
1975fn is_scalar_op(op: OpKind) -> bool {
1976    matches!(
1977        op,
1978        OpKind::Affine | OpKind::ClampElementwise | OpKind::PowIElementwise,
1979    )
1980}
1981
1982/// Whether `op` is a reduce-to-broadcast-target op the Judge profiles
1983/// using the `ReduceTo { rows, cols }` size ladder.
1984fn is_reduce_to(op: OpKind) -> bool {
1985    matches!(op, OpKind::ReduceSumTo | OpKind::ReduceMaxTo)
1986}
1987
1988/// Elementwise ops on the decomposed decode-attention softmax path
1989/// (`exp(x - max) / sum`): Sub, Exp, Div. These get decode-score-row
1990/// [`OpSize::Elementwise`] cells appended to their base ladder so the
1991/// Judge costs the decomposed softmax at the element counts decode
1992/// runs (slice-4's arm-0 flash-vs-decomposed comparison needs the
1993/// decomposed primitives' MEASURED decode latencies). Every other
1994/// elementwise op is off the decode softmax path and keeps only its
1995/// general ladder.
1996fn is_decode_softmax_elementwise(op: OpKind) -> bool {
1997    matches!(
1998        op,
1999        OpKind::SubElementwise | OpKind::ExpElementwise | OpKind::DivElementwise,
2000    )
2001}
2002
2003/// Reductions on the decomposed decode-attention softmax path:
2004/// MaxReduce (row max) and SumReduce (denominator). Min/Mean don't
2005/// appear there and keep only their general ladder.
2006fn is_decode_softmax_reduction(op: OpKind) -> bool {
2007    matches!(op, OpKind::MaxReduce | OpKind::SumReduce)
2008}
2009
2010/// Whether `op` is a per-axis reduction the Judge profiles using the
2011/// `Reduce { rows, cols }` size ladder.
2012fn is_reduction(op: OpKind) -> bool {
2013    matches!(
2014        op,
2015        OpKind::SumReduce | OpKind::MaxReduce | OpKind::MinReduce | OpKind::MeanReduce,
2016    )
2017}
2018
2019/// Dispatch one per-axis reduction along the last dim. The reduced
2020/// dim is removed from the output shape.
2021fn apply_reduction(op: OpKind, a: &crate::lazy::Tensor) -> crate::lazy::Tensor {
2022    // Reduce the last dim. Input is rank-2 `[rows, cols]`; output is
2023    // rank-1 `[rows]`.
2024    let last_dim = a.rank() - 1;
2025    match op {
2026        OpKind::SumReduce => a.sum_dim(last_dim).unwrap(),
2027        OpKind::MaxReduce => a.max_dim(last_dim).unwrap(),
2028        OpKind::MinReduce => a.min_dim(last_dim).unwrap(),
2029        OpKind::MeanReduce => a.mean_dim(last_dim).unwrap(),
2030        _ => unreachable!("apply_reduction called on non-reduction OpKind {op:?}"),
2031    }
2032}
2033
2034/// Whether `op` is one of the elementwise binary ops the Judge profiles
2035/// using the `Elementwise(n)` size ladder.
2036fn is_binary_elementwise(op: OpKind) -> bool {
2037    matches!(
2038        op,
2039        OpKind::AddElementwise
2040            | OpKind::SubElementwise
2041            | OpKind::MulElementwise
2042            | OpKind::DivElementwise
2043            | OpKind::MaximumElementwise
2044            | OpKind::MinimumElementwise
2045            | OpKind::PowElementwise
2046            | OpKind::RemElementwise,
2047    )
2048}
2049
2050/// Deterministic two-input data for binary ops. `a` is `sin(i*2.1e-3)`
2051/// and `b` is `cos(i*1.9e-3)` for most ops; for Div, `b` is shifted
2052/// away from zero (`+ 1.5`, range `[0.5, 2.5]`) to avoid division by
2053/// near-zero values that would saturate `max_rel_err` to infinity on
2054/// any tiny precision difference.
2055fn binary_inputs(op: OpKind, n: usize) -> (Vec<f32>, Vec<f32>) {
2056    let mut a: Vec<f32> = (0..n).map(|i| ((i as f32) * 2.1e-3).sin()).collect();
2057    let mut b: Vec<f32> = (0..n).map(|i| ((i as f32) * 1.9e-3).cos()).collect();
2058    if matches!(op, OpKind::DivElementwise) {
2059        for x in &mut b {
2060            *x += 1.5;
2061        }
2062    }
2063    if matches!(op, OpKind::PowElementwise) {
2064        // Both inputs must be positive: pow(neg, non-int) = NaN under
2065        // IEEE-754 and saturates max_rel_err. Shift both to [0.5, 2.5].
2066        for x in &mut a {
2067            *x += 1.5;
2068        }
2069        for x in &mut b {
2070            *x += 1.5;
2071        }
2072    }
2073    if matches!(op, OpKind::RemElementwise) {
2074        // Divisor must be away from zero (`a / b` blows up otherwise).
2075        for x in &mut b {
2076            *x += 1.5;
2077        }
2078    }
2079    (a, b)
2080}
2081
2082/// Dispatch one elementwise binary op against `(a, b)`.
2083fn apply_binary(
2084    op: OpKind,
2085    a: &crate::lazy::Tensor,
2086    b: &crate::lazy::Tensor,
2087) -> crate::lazy::Tensor {
2088    match op {
2089        OpKind::AddElementwise => a.add(b).unwrap(),
2090        OpKind::SubElementwise => a.sub(b).unwrap(),
2091        OpKind::MulElementwise => a.mul(b).unwrap(),
2092        OpKind::DivElementwise => a.div(b).unwrap(),
2093        OpKind::MaximumElementwise => a.maximum(b).unwrap(),
2094        OpKind::MinimumElementwise => a.minimum(b).unwrap(),
2095        // Pow/Rem return Result; expect() in Judge's measurement
2096        // path is fine — inputs are constructed locally here, so a
2097        // shape/dtype mismatch is a programming bug in the test
2098        // harness, not a runtime user input.
2099        OpKind::PowElementwise => a.pow(b).expect("judge: pow shape/dtype invariant"),
2100        OpKind::RemElementwise => a.rem(b).expect("judge: rem shape/dtype invariant"),
2101        _ => unreachable!("apply_binary called on non-binary OpKind {op:?}"),
2102    }
2103}
2104
2105/// Whether `op` is one of the elementwise unary ops the Judge profiles
2106/// using the `Elementwise(n)` size ladder.
2107fn is_unary_elementwise(op: OpKind) -> bool {
2108    matches!(
2109        op,
2110        OpKind::NegElementwise
2111            | OpKind::SqrElementwise
2112            | OpKind::SqrtElementwise
2113            | OpKind::ExpElementwise
2114            | OpKind::LogElementwise
2115            | OpKind::SinElementwise
2116            | OpKind::CosElementwise
2117            | OpKind::TanhElementwise
2118            | OpKind::SigmoidElementwise
2119            | OpKind::SiluElementwise
2120            | OpKind::GeluTanhElementwise
2121            | OpKind::ReluElementwise
2122            | OpKind::StepElementwise
2123            | OpKind::RecipElementwise
2124            | OpKind::AbsElementwise
2125            | OpKind::FloorElementwise
2126            | OpKind::CeilElementwise
2127            | OpKind::RoundElementwise
2128            | OpKind::SignElementwise
2129            | OpKind::ErfElementwise
2130            | OpKind::GeluElementwise
2131            | OpKind::RsqrtElementwise,
2132    )
2133}
2134
2135/// Deterministic unary-op input. `sin(i * 2.1e-3)` produces values in
2136/// `[-1, 1]`, varied enough that argmax/argmin/relu/step all see a
2137/// realistic mix of positive and negative values.
2138fn unary_input(n: usize) -> Vec<f32> {
2139    (0..n).map(|i| ((i as f32) * 2.1e-3).sin()).collect()
2140}
2141
2142/// Dispatch one elementwise unary op against `a`.
2143fn apply_unary(op: OpKind, a: &crate::lazy::Tensor) -> crate::lazy::Tensor {
2144    match op {
2145        OpKind::NegElementwise => a.neg(),
2146        OpKind::SqrElementwise => a.sqr(),
2147        OpKind::SqrtElementwise => a.sqrt(),
2148        OpKind::ExpElementwise => a.exp(),
2149        OpKind::LogElementwise => a.log(),
2150        OpKind::TanhElementwise => a.tanh(),
2151        OpKind::SigmoidElementwise => a.sigmoid(),
2152        OpKind::SiluElementwise => a.silu(),
2153        OpKind::GeluTanhElementwise => a.gelu_tanh(),
2154        OpKind::ReluElementwise => a.relu(),
2155        OpKind::SinElementwise => a.sin(),
2156        OpKind::CosElementwise => a.cos(),
2157        OpKind::StepElementwise => a.step(),
2158        OpKind::RecipElementwise => a.recip(),
2159        OpKind::AbsElementwise => a.abs(),
2160        OpKind::FloorElementwise => a.floor(),
2161        OpKind::CeilElementwise => a.ceil(),
2162        OpKind::RoundElementwise => a.round(),
2163        OpKind::SignElementwise => a.sign(),
2164        OpKind::ErfElementwise => a.erf(),
2165        OpKind::GeluElementwise => a.gelu(),
2166        OpKind::RsqrtElementwise => a.rsqrt(),
2167        _ => unreachable!("apply_unary called on non-unary OpKind {op:?}"),
2168    }
2169}
2170
2171/// Max element-wise relative error between two same-length f32
2172/// vectors. Denominator guards against divide-by-zero on mutually-
2173/// zero pairs. Returns 0.0 when the lengths don't match (caller is
2174/// responsible for length sanity — this function is in the hot
2175/// path and avoids panics).
2176fn max_rel_err(a: &[f32], b: &[f32]) -> f32 {
2177    if a.len() != b.len() {
2178        return f32::INFINITY;
2179    }
2180    let mut worst = 0.0_f32;
2181    for (&x, &y) in a.iter().zip(b.iter()) {
2182        if !x.is_finite() || !y.is_finite() {
2183            return f32::INFINITY;
2184        }
2185        let denom = x.abs().max(y.abs()).max(f32::MIN_POSITIVE);
2186        let rel = (x - y).abs() / denom;
2187        if rel > worst {
2188            worst = rel;
2189        }
2190    }
2191    worst
2192}
2193
2194// =============================================================
2195// Pairwise consensus — Reference-retirement replacement (2026-06-07)
2196// =============================================================
2197//
2198// Pre-retirement, Judge compared every backend's output against
2199// `tensor.realize_f32_reference()` to populate `ProfileEntry::max_rel_error`.
2200// That privileged the `fuel-reference-backend` crate as THE oracle —
2201// the architectural decision in v0.2 of `05-backend-contract.md`
2202// demoted Reference but the code lagged.
2203//
2204// Post-retirement, Judge runs every available backend's kernel for
2205// each (op, dtype, size) cell, clusters the outputs by mutual
2206// `rel_err < CONSENSUS_EPSILON`, and computes each backend's
2207// `max_rel_error` against the largest cluster (the "consensus
2208// group"). A backend in the consensus group reports its max
2209// within-cluster rel_err (typically near 0); a backend outside
2210// the consensus reports its rel_err against the cluster — that's
2211// the outlier signal callers want.
2212
2213/// Generous epsilon for cross-backend consensus clustering.
2214/// Different backends produce numerically-different bits for the
2215/// same logical operation (IEEE rounding rules + accumulation
2216/// order differ across BLAS impls, GPU shader cores, etc.); the
2217/// cluster definition has to accommodate this. `1e-3` is loose
2218/// enough that floating-point accumulation reordering won't push
2219/// honest implementations out of consensus while still catching
2220/// kernels that are silently wrong.
2221///
2222/// Per-op tightening can land later via per-op
2223/// `PrecisionGuarantee::max_relative` if a particular op family
2224/// warrants tighter clustering. The 1e-3 default matches the
2225/// `assert!(e.max_rel_error < 5e-3)` thresholds used in Judge's
2226/// own tests below — both numbers reflect "this is the noise
2227/// floor for honest cross-backend implementations of f32 math."
2228const CONSENSUS_EPSILON: f32 = 1e-3;
2229
2230/// One backend's measurement for one (op, dtype, size, kernel_source)
2231/// cell — kernel output captured alongside latency for downstream
2232/// consensus comparison.
2233///
2234/// **Per-alternative measurement (2026-06-08)**: `kernel_source`
2235/// distinguishes sibling kernels at one `(op, dtypes, backend)`
2236/// binding-table key (e.g. AOCL vs MKL vs portable-cpu at
2237/// `(MatMul, [F32×3], BackendId::Cpu)`). Each alternative produces
2238/// its own CellRun; the pairwise consensus then sees the full
2239/// cross-kernel-source population, treating AOCL/MKL/portable as
2240/// peers alongside CUDA/Vulkan.
2241#[derive(Debug)]
2242struct CellRun {
2243    backend: BackendId,
2244    device_index: u32,
2245    output: Vec<f32>,
2246    latency_ns: u64,
2247    iterations: u32,
2248    /// Diagnostic tag identifying the kernel sibling at the
2249    /// binding-table cell. `""` for cells with a single alternative
2250    /// or when the kernel-source tag is unknown.
2251    kernel_source: String,
2252    /// Content-derived revision of the kernel that produced this run,
2253    /// paired with `kernel_source` from a single attribution so the two
2254    /// never describe different kernels. Always a real, attributed value:
2255    /// a run whose revision could not be attributed is not recorded (see
2256    /// the dispatched-path conservative cut) rather than stamped `0`.
2257    kernel_revision_hash: u64,
2258}
2259
2260/// Compute the largest mutually-close cluster across `runs`. Returns
2261/// indices into `runs` (sorted ascending) of the consensus members.
2262///
2263/// Algorithm: for each run i, build the set of runs that pairwise
2264/// agree with i (and with each other). The largest such set is the
2265/// consensus. Ties broken by lowest first-index — deterministic.
2266///
2267/// Edge cases:
2268/// - Empty `runs`: empty consensus.
2269/// - Single run: trivial consensus of [0]. The lone backend is
2270///   authoritative by absence of peers.
2271/// - Two runs agreeing within `CONSENSUS_EPSILON`: consensus is
2272///   both; each reports its rel_err against the other (small).
2273/// - Two runs disagreeing: consensus is [0] (first wins ties by
2274///   index); the other gets the disagreement as its rel_err.
2275///   Callers should treat 2-backend cells with disagreement as
2276///   "human review needed" — neither answer is independently
2277///   validated.
2278/// - Three runs with one outlier: consensus is the two that agree;
2279///   the outlier reports its rel_err against the cluster.
2280// needless_range_loop here: the index is used as a value
2281// (assert/message/push/compare), not only to index.
2282#[allow(clippy::needless_range_loop)]
2283fn compute_pairwise_consensus(runs: &[CellRun]) -> Vec<usize> {
2284    let n = runs.len();
2285    if n == 0 {
2286        return Vec::new();
2287    }
2288    if n == 1 {
2289        return vec![0];
2290    }
2291
2292    // Pairwise rel_err matrix: agree[i][j] = (max_rel_err < CONSENSUS_EPSILON).
2293    let mut agree = vec![vec![false; n]; n];
2294    for i in 0..n {
2295        agree[i][i] = true;
2296        for j in (i + 1)..n {
2297            let close = max_rel_err(&runs[i].output, &runs[j].output) < CONSENSUS_EPSILON;
2298            agree[i][j] = close;
2299            agree[j][i] = close;
2300        }
2301    }
2302
2303    // For each i, expand a cluster greedily: start with {i}, add
2304    // each j ∈ neighbors that pairwise-agrees with all current
2305    // members. Greedy — but for small N (typical: 2-4 backends),
2306    // exhaustive search would also be cheap. Greedy is correct
2307    // when the threshold defines a true equivalence-like relation;
2308    // for f32 cross-backend agreement that's a reasonable assumption.
2309    let mut best: Vec<usize> = vec![0];
2310    for i in 0..n {
2311        let mut cluster = vec![i];
2312        for j in 0..n {
2313            if j == i {
2314                continue;
2315            }
2316            // j joins iff agrees with every existing member.
2317            if cluster.iter().all(|&k| agree[j][k]) {
2318                cluster.push(j);
2319            }
2320        }
2321        cluster.sort_unstable();
2322        // Larger cluster wins; ties broken by lower starting index.
2323        if cluster.len() > best.len() {
2324            best = cluster;
2325        }
2326    }
2327    best
2328}
2329
2330/// Reinterpret an f32 vector as the little-endian bytes that
2331/// [`validate_against_fixture`] expects, run validation, and lift
2332/// the verdict into a single rel_err number for the ProfileEntry.
2333///
2334/// - `Ok(())` → `0.0` (output matches fixture within tolerance).
2335/// - `Err(OutOfTolerance { rel_err, .. })` → that `rel_err`.
2336/// - `Err(LengthMismatch | NonFinite | UnvalidatableDType)` →
2337///   `f32::INFINITY` — these are categorical failures (wrong shape,
2338///   NaN/Inf where finite was expected, or a dtype this crate has no
2339///   element-wise validator for). The dispatch picker will rank such
2340///   an alternative behind any finite-rel_err peer; the kernel-source
2341///   tag on the ProfileEntry tells us which alternative blew up.
2342///   `UnvalidatableDType` must NOT map to `0.0`: that would report an
2343///   unvalidated kernel as *perfectly* correct, out-ranking every
2344///   validated peer while the validator certified nothing (GAP-075).
2345fn max_rel_err_vs_fixture(out: &[f32], fixture: &CorrectnessFixture) -> f32 {
2346    let bytes: Vec<u8> = out.iter().flat_map(|x| x.to_le_bytes()).collect();
2347    match validate_against_fixture(fixture, &bytes) {
2348        Ok(()) => 0.0,
2349        Err(CorrectnessDrift::OutOfTolerance { rel_err, .. }) => rel_err as f32,
2350        Err(CorrectnessDrift::LengthMismatch { .. }) => f32::INFINITY,
2351        Err(CorrectnessDrift::NonFinite { .. }) => f32::INFINITY,
2352        // Correctness UNKNOWN (no element-wise validator for this dtype)
2353        // ranks as a categorical failure, never as 0.0 "perfect" (GAP-075).
2354        Err(CorrectnessDrift::UnvalidatableDType { .. }) => f32::INFINITY,
2355    }
2356}
2357
2358/// Load every `*.json` file under `root` and flatten the contained
2359/// [`FixtureFile`] entries into a map keyed by
2360/// `(op, dtype, size_class)`. `root` may itself be a file (one
2361/// fixture file loaded) or a directory (recursive walk).
2362///
2363/// Errors propagate the first I/O failure or deserialization error
2364/// encountered. A directory with no `*.json` files yields an empty
2365/// map (the Judge then behaves as if no fixtures were configured).
2366///
2367/// Files whose `FixtureFile.version` does not match the current
2368/// [`FIXTURE_FILE_VERSION`] are skipped with a stderr warning. The
2369/// load itself is non-fatal — a single stale file in the fixture
2370/// distribution shouldn't strand the rest of the run.
2371fn load_fixtures_recursive(
2372    root: &Path,
2373) -> Result<HashMap<(OpKind, DType, SizeClass), Vec<CorrectnessFixture>>> {
2374    let mut map: HashMap<(OpKind, DType, SizeClass), Vec<CorrectnessFixture>> = HashMap::new();
2375    let mut visit = |p: &Path| -> Result<()> {
2376        let raw = std::fs::read_to_string(p).map_err(|e| {
2377            fuel_ir::error::Error::Msg(format!(
2378                "judge: failed to read fixture file {}: {e}",
2379                p.display(),
2380            ))
2381        })?;
2382        let file: FixtureFile = serde_json::from_str(&raw).map_err(|e| {
2383            fuel_ir::error::Error::Msg(format!(
2384                "judge: failed to parse fixture file {} as FixtureFile: {e}",
2385                p.display(),
2386            ))
2387        })?;
2388        if file.version != FIXTURE_FILE_VERSION {
2389            eprintln!(
2390                "judge: skipping fixture file {} — version {} does not match \
2391                 current FIXTURE_FILE_VERSION ({}); regenerate via fuel-capture-fixtures",
2392                p.display(),
2393                file.version,
2394                FIXTURE_FILE_VERSION,
2395            );
2396            return Ok(());
2397        }
2398        for f in file.fixtures {
2399            let key = (f.op, f.dtype, f.size_class);
2400            map.entry(key).or_default().push(f);
2401        }
2402        Ok(())
2403    };
2404
2405    fn walk(dir: &Path, visit: &mut dyn FnMut(&Path) -> Result<()>) -> Result<()> {
2406        let entries = std::fs::read_dir(dir).map_err(|e| {
2407            fuel_ir::error::Error::Msg(format!(
2408                "judge: failed to read fixture dir {}: {e}",
2409                dir.display(),
2410            ))
2411        })?;
2412        for entry in entries {
2413            let entry = entry.map_err(|e| {
2414                fuel_ir::error::Error::Msg(format!(
2415                    "judge: failed reading dir entry in {}: {e}",
2416                    dir.display(),
2417                ))
2418            })?;
2419            let p = entry.path();
2420            if p.is_dir() {
2421                walk(&p, visit)?;
2422            } else if p.extension().and_then(|s| s.to_str()) == Some("json") {
2423                visit(&p)?;
2424            }
2425        }
2426        Ok(())
2427    }
2428
2429    if root.is_file() {
2430        visit(root)?;
2431    } else if root.is_dir() {
2432        walk(root, &mut visit)?;
2433    } else {
2434        return Err(fuel_ir::error::Error::Msg(format!(
2435            "judge: fixture path {} does not exist",
2436            root.display(),
2437        )));
2438    }
2439    Ok(map)
2440}
2441
2442/// Compute `runs[self_idx]`'s max relative error against the
2443/// consensus cluster. When the consensus contains only `self_idx`
2444/// (single backend or every backend disagreed), returns `0.0` —
2445/// there's no peer to measure drift against.
2446fn max_rel_err_vs_consensus(runs: &[CellRun], consensus: &[usize], self_idx: usize) -> f32 {
2447    let peers: Vec<usize> = consensus
2448        .iter()
2449        .copied()
2450        .filter(|&i| i != self_idx)
2451        .collect();
2452    if peers.is_empty() {
2453        return 0.0;
2454    }
2455    let self_out = &runs[self_idx].output;
2456    let mut worst = 0.0_f32;
2457    for i in peers {
2458        let e = max_rel_err(self_out, &runs[i].output);
2459        if e > worst {
2460            worst = e;
2461        }
2462    }
2463    worst
2464}
2465
2466#[cfg(test)]
2467mod tests {
2468    use super::*;
2469
2470    #[test]
2471    fn size_class_matches_log2_floor() {
2472        assert_eq!(SizeClass::from_elem_count(1).0, 0);
2473        assert_eq!(SizeClass::from_elem_count(2).0, 1);
2474        assert_eq!(SizeClass::from_elem_count(3).0, 1);
2475        assert_eq!(SizeClass::from_elem_count(4).0, 2);
2476        assert_eq!(SizeClass::from_elem_count(1 << 16).0, 16);
2477        assert_eq!(SizeClass::from_elem_count(65_537).0, 16);
2478    }
2479
2480    #[test]
2481    fn size_class_zero_input_clamps_to_one() {
2482        assert_eq!(SizeClass::from_elem_count(0).0, 0);
2483    }
2484
2485    #[test]
2486    fn op_kind_display_is_stable() {
2487        assert_eq!(OpKind::MatMul.to_string(), "matmul");
2488        assert_eq!(OpKind::AddElementwise.to_string(), "add");
2489    }
2490
2491    /// GAP-075: a fixture whose dtype has no element-wise validator
2492    /// wired must rank as a categorical FAILURE (`INFINITY`), never as
2493    /// `0.0` ("perfect"). The old `_ => Ok(())` in
2494    /// `validate_against_fixture` mapped here to `0.0`, so an
2495    /// unvalidated kernel out-ranked every validated peer while the
2496    /// validator had certified nothing. This locks the verdict at the
2497    /// Judge boundary, where the false-green would otherwise surface as
2498    /// a green test suite with no traceable symptom.
2499    #[test]
2500    fn unvalidatable_dtype_fixture_ranks_as_failure_not_perfect() {
2501        use fuel_correctness_fixtures::ToleranceBand;
2502        // Byte-lengths must match so we reach the dtype-decline arm, not
2503        // LengthMismatch: `out` is f32 (4 B each) → 2 elems = 8 B; the
2504        // F16 fixture (2 B each) with 4 elems = 8 B. Equal ⇒ past the
2505        // length gate, into the unwired-dtype decline.
2506        let out = vec![0.0f32, 0.0f32];
2507        let fixture = CorrectnessFixture {
2508            op: OpKind::AddElementwise,
2509            dtype: DType::F16,
2510            size_class: SizeClass(2),
2511            input_seed: 1,
2512            input_hash: 0,
2513            expected_output: vec![0u8; 8],
2514            output_element_count: 4,
2515            tolerance: ToleranceBand::F32_DEFAULT,
2516        };
2517        let verdict = max_rel_err_vs_fixture(&out, &fixture);
2518        assert_eq!(
2519            verdict,
2520            f32::INFINITY,
2521            "unvalidatable-dtype fixture must rank as failure (INFINITY), \
2522             not 0.0 — a false 'perfect' certifies an unchecked kernel",
2523        );
2524    }
2525
2526    /// Session 3 rider + Judge item 7 (device+kernel identity): the
2527    /// realizer-measured CellRun records the `(kernel_source,
2528    /// kernel_revision_hash)` the realizer REPORTS — the picker's dispatched
2529    /// sibling — both fields from ONE attribution so they cannot disagree
2530    /// about which kernel ran.
2531    ///
2532    /// When the realizer has NO attribution (`last_kernel_source` /
2533    /// `last_kernel_revision` both `None` — the plan had no entry for the
2534    /// root, or a best-effort lock/lookup miss), NO CellRun is produced. This
2535    /// is the conservative cut: `alts.first()`'s revision would be a plausible
2536    /// GUESS about a real sibling, and `0` would claim "untracked" about a
2537    /// kernel that actually ran — both are rows that LIE about the identity
2538    /// `kernel_revision_hash` exists to make trustworthy. An absent row is the
2539    /// honest outcome (the cell then simply misses in the oracle; Layer-1
2540    /// static costs stand). Born-red: before the cut, the `None` case wrote a
2541    /// row stamped with the first-registered fallback tag and a `0` revision.
2542    #[test]
2543    fn cell_run_carries_attribution_or_is_dropped() {
2544        struct StubRealizer {
2545            src: Option<&'static str>,
2546            rev: Option<u64>,
2547        }
2548        impl crate::factories::Realizer for StubRealizer {
2549            fn realize_f32(&mut self, _tensor: &crate::lazy::Tensor) -> Result<Vec<f32>> {
2550                Ok(vec![0.0; 8])
2551            }
2552            fn last_kernel_source(&self) -> Option<&'static str> {
2553                self.src
2554            }
2555            fn last_kernel_revision(&self) -> Option<u64> {
2556                self.rev
2557            }
2558        }
2559
2560        let judge = Judge {
2561            iterations: 1,
2562            warmup: 0,
2563            size_plan_override: None,
2564            fixtures: None,
2565        };
2566        let device = DeviceDescriptor {
2567            backend: BackendId::Cpu,
2568            device_index: 0,
2569            hardware_sku: "test-cpu".into(),
2570            vendor_id: 0,
2571            device_id: 0,
2572            compute_capability: None,
2573            subgroup_width: None,
2574            driver_version: String::new(),
2575            total_memory_bytes: 0,
2576            location: fuel_ir::DeviceLocation::Cpu,
2577        };
2578        let op = OpKind::AddElementwise;
2579        let size = OpSize::Elementwise(8);
2580
2581        // Attributed: source AND revision from one picker result → the CellRun
2582        // carries both verbatim, regardless of registration order at the cell.
2583        let mut reporting = StubRealizer {
2584            src: Some("stub-sibling"),
2585            rev: Some(0x00AB_CDEF),
2586        };
2587        let run = judge
2588            .time_op_capturing(op, DType::F32, &size, &device, &mut reporting)
2589            .expect("attributed dispatch produces a CellRun");
2590        assert_eq!(run.kernel_source, "stub-sibling");
2591        assert_eq!(run.kernel_revision_hash, 0x00AB_CDEF);
2592
2593        // No attribution (both None) → NO CellRun. The conservative cut writes
2594        // nothing rather than a guessed or fabricated revision.
2595        let mut silent = StubRealizer {
2596            src: None,
2597            rev: None,
2598        };
2599        assert!(
2600            judge
2601                .time_op_capturing(op, DType::F32, &size, &device, &mut silent)
2602                .is_none(),
2603            "unattributed dispatch must produce NO row, not a guessed/fabricated one",
2604        );
2605    }
2606
2607    #[test]
2608    fn judge_profiles_cpu_on_small_matmul() {
2609        // Post-Reference-retirement (2026-06-07): with Reference
2610        // gone, the local single-backend (CPU) is alone in its
2611        // (op, dtype, size) cell. Consensus is trivially `[0]`;
2612        // `max_rel_error` is `0.0` (no peers to measure drift
2613        // against). The test verifies the Judge still produces
2614        // entries with sane timing data when only one backend is
2615        // available — the single-backend edge case in the
2616        // consensus algorithm.
2617        let probe = ProbeReport::probe_all();
2618        let judge = Judge {
2619            iterations: 3,
2620            warmup: 1,
2621            size_plan_override: Some(vec![
2622                (
2623                    OpKind::MatMul,
2624                    OpSize::MatMul {
2625                        m: 32,
2626                        n: 32,
2627                        k: 32,
2628                    },
2629                ),
2630                (OpKind::AddElementwise, OpSize::Elementwise(1 << 10)),
2631            ]),
2632            fixtures: None,
2633        };
2634        let report = judge.run(&probe);
2635        assert_eq!(report.version, PROFILE_REPORT_VERSION);
2636        assert!(report.entries.iter().any(|e| e.backend == BackendId::Cpu));
2637        // Post-v2 (per-alternative measurement): one CPU cell may
2638        // produce multiple entries (one per kernel_source — AOCL,
2639        // MKL, portable-cpu). When peers exist the rel_err can be
2640        // non-zero but should stay below the bit-stable floor;
2641        // when only one alternative is registered rel_err is 0.0
2642        // (no peers → no drift reference).
2643        for e in report
2644            .entries
2645            .iter()
2646            .filter(|e| e.backend == BackendId::Cpu)
2647        {
2648            assert!(
2649                e.max_rel_error < 1e-3,
2650                "cpu cell rel_err should stay tight ({e:?})"
2651            );
2652            assert!(e.latency_ns > 0);
2653        }
2654    }
2655
2656    #[test]
2657    fn judge_profiles_all_unary_elementwise_ops() {
2658        // Exercise every elementwise-unary OpKind on cpu + reference at
2659        // a tiny size. Confirms the size_plan / build_input_graph /
2660        // apply_unary wiring is complete and that no unary kernel
2661        // diverges wildly from reference on the cpu backend.
2662        let probe = ProbeReport::probe_all();
2663        let unary = [
2664            OpKind::NegElementwise,
2665            OpKind::SqrElementwise,
2666            OpKind::SqrtElementwise,
2667            OpKind::ExpElementwise,
2668            OpKind::LogElementwise,
2669            OpKind::SinElementwise,
2670            OpKind::CosElementwise,
2671            OpKind::TanhElementwise,
2672            OpKind::SigmoidElementwise,
2673            OpKind::SiluElementwise,
2674            OpKind::GeluTanhElementwise,
2675            OpKind::ReluElementwise,
2676            OpKind::StepElementwise,
2677            OpKind::RecipElementwise,
2678            OpKind::AbsElementwise,
2679            OpKind::FloorElementwise,
2680            OpKind::CeilElementwise,
2681            OpKind::RoundElementwise,
2682            OpKind::SignElementwise,
2683            OpKind::ErfElementwise,
2684            OpKind::GeluElementwise,
2685            OpKind::RsqrtElementwise,
2686        ];
2687        let plan: Vec<_> = unary
2688            .iter()
2689            .map(|&op| (op, OpSize::Elementwise(1 << 8)))
2690            .collect();
2691        let judge = Judge {
2692            iterations: 3,
2693            warmup: 1,
2694            size_plan_override: Some(plan),
2695            fixtures: None,
2696        };
2697        let report = judge.run(&probe);
2698        for &op in &unary {
2699            let cpu_entries: Vec<_> = report
2700                .entries
2701                .iter()
2702                .filter(|e| e.op == op && e.backend == BackendId::Cpu)
2703                .collect();
2704            // Post-v2 (per-alternative measurement): ≥1 — when only the
2705            // portable CPU kernel is registered the count is 1; with
2706            // AOCL/MKL feature-gated alternatives it grows. Pick the
2707            // first entry (the realizer-primary measurement) for the
2708            // numerical-divergence check.
2709            assert!(
2710                !cpu_entries.is_empty(),
2711                "expected ≥1 cpu entry for {op}, got 0"
2712            );
2713            let e = cpu_entries[0];
2714            // Elementwise unary ops are bit-stable on cpu vs reference
2715            // (no accumulation order to worry about). Allow a
2716            // generous bound so transcendental approximations
2717            // (tanh/sigmoid/silu/gelu_tanh) on different math libs still
2718            // pass — but a runaway divergence (>1e-3) flags a bug.
2719            assert!(
2720                e.max_rel_error < 1e-3,
2721                "cpu vs reference disagreement on {op}: rel_err={}",
2722                e.max_rel_error
2723            );
2724            assert!(e.latency_ns > 0);
2725        }
2726    }
2727
2728    #[test]
2729    fn judge_profiles_all_binary_elementwise_ops() {
2730        let probe = ProbeReport::probe_all();
2731        let binary = [
2732            OpKind::AddElementwise,
2733            OpKind::SubElementwise,
2734            OpKind::MulElementwise,
2735            OpKind::DivElementwise,
2736            OpKind::MaximumElementwise,
2737            OpKind::MinimumElementwise,
2738            OpKind::PowElementwise,
2739            OpKind::RemElementwise,
2740        ];
2741        let plan: Vec<_> = binary
2742            .iter()
2743            .map(|&op| (op, OpSize::Elementwise(1 << 8)))
2744            .collect();
2745        let judge = Judge {
2746            iterations: 3,
2747            warmup: 1,
2748            size_plan_override: Some(plan),
2749            fixtures: None,
2750        };
2751        let report = judge.run(&probe);
2752        for &op in &binary {
2753            let cpu_entries: Vec<_> = report
2754                .entries
2755                .iter()
2756                .filter(|e| e.op == op && e.backend == BackendId::Cpu)
2757                .collect();
2758            assert!(
2759                !cpu_entries.is_empty(),
2760                "expected >=1 cpu entry for {op}, got 0"
2761            );
2762            let e = cpu_entries[0];
2763            assert!(
2764                e.max_rel_error < 1e-3,
2765                "cpu vs reference disagreement on {op}: rel_err={}",
2766                e.max_rel_error
2767            );
2768            assert!(e.latency_ns > 0);
2769        }
2770    }
2771
2772    #[test]
2773    fn judge_profiles_all_reductions() {
2774        let probe = ProbeReport::probe_all();
2775        let reduce = [
2776            OpKind::SumReduce,
2777            OpKind::MaxReduce,
2778            OpKind::MinReduce,
2779            OpKind::MeanReduce,
2780        ];
2781        let plan: Vec<_> = reduce
2782            .iter()
2783            .map(|&op| (op, OpSize::Reduce { rows: 16, cols: 16 }))
2784            .collect();
2785        let judge = Judge {
2786            iterations: 3,
2787            warmup: 1,
2788            size_plan_override: Some(plan),
2789            fixtures: None,
2790        };
2791        let report = judge.run(&probe);
2792        for &op in &reduce {
2793            let cpu_entries: Vec<_> = report
2794                .entries
2795                .iter()
2796                .filter(|e| e.op == op && e.backend == BackendId::Cpu)
2797                .collect();
2798            assert!(
2799                !cpu_entries.is_empty(),
2800                "expected >=1 cpu entry for {op}, got 0"
2801            );
2802            let e = cpu_entries[0];
2803            // Sum-style reductions accumulate in different order on
2804            // backend vs reference; allow up to 5e-3.
2805            assert!(
2806                e.max_rel_error < 5e-3,
2807                "cpu vs reference disagreement on {op}: rel_err={}",
2808                e.max_rel_error
2809            );
2810            assert!(e.latency_ns > 0);
2811        }
2812    }
2813
2814    #[test]
2815    fn judge_profiles_all_reduce_to() {
2816        let probe = ProbeReport::probe_all();
2817        let reduce_to = [OpKind::ReduceSumTo, OpKind::ReduceMaxTo];
2818        let plan: Vec<_> = reduce_to
2819            .iter()
2820            .map(|&op| (op, OpSize::ReduceTo { rows: 16, cols: 16 }))
2821            .collect();
2822        let judge = Judge {
2823            iterations: 3,
2824            warmup: 1,
2825            size_plan_override: Some(plan),
2826            fixtures: None,
2827        };
2828        let report = judge.run(&probe);
2829        for &op in &reduce_to {
2830            let cpu_entries: Vec<_> = report
2831                .entries
2832                .iter()
2833                .filter(|e| e.op == op && e.backend == BackendId::Cpu)
2834                .collect();
2835            assert!(
2836                !cpu_entries.is_empty(),
2837                "expected >=1 cpu entry for {op}, got 0"
2838            );
2839            let e = cpu_entries[0];
2840            assert!(
2841                e.max_rel_error < 5e-3,
2842                "cpu vs reference disagreement on {op}: rel_err={}",
2843                e.max_rel_error
2844            );
2845            assert!(e.latency_ns > 0);
2846        }
2847    }
2848
2849    #[test]
2850    fn judge_profiles_all_scalar_ops() {
2851        let probe = ProbeReport::probe_all();
2852        let scalar = [
2853            OpKind::Affine,
2854            OpKind::ClampElementwise,
2855            OpKind::PowIElementwise,
2856        ];
2857        let plan: Vec<_> = scalar
2858            .iter()
2859            .map(|&op| (op, OpSize::Elementwise(1 << 8)))
2860            .collect();
2861        let judge = Judge {
2862            iterations: 3,
2863            warmup: 1,
2864            size_plan_override: Some(plan),
2865            fixtures: None,
2866        };
2867        let report = judge.run(&probe);
2868        for &op in &scalar {
2869            let cpu_entries: Vec<_> = report
2870                .entries
2871                .iter()
2872                .filter(|e| e.op == op && e.backend == BackendId::Cpu)
2873                .collect();
2874            assert!(
2875                !cpu_entries.is_empty(),
2876                "expected >=1 cpu entry for {op}, got 0"
2877            );
2878            let e = cpu_entries[0];
2879            assert!(
2880                e.max_rel_error < 1e-3,
2881                "cpu vs reference disagreement on {op}: rel_err={}",
2882                e.max_rel_error
2883            );
2884            assert!(e.latency_ns > 0);
2885        }
2886    }
2887
2888    /// Decode-shaped-ladder slice (Judge Layer-2 coverage arc,
2889    /// 2026-07-04, slice 2).
2890    ///
2891    /// **Born-red**: before this slice the `size_plan` ladders were all
2892    /// SQUARE/large — no seq_q=1 matmul, no wide-`k_len` reduction, no
2893    /// decode-score-row elementwise cell. The decomposed decode-
2894    /// attention softmax (`exp(x-max)/sum`) and the projection/score
2895    /// GEMVs run at SKINNY shapes that behave nothing like the square
2896    /// ladder (bandwidth-bound vs compute-bound). This asserts the real
2897    /// default `size_plan` now carries the decode cells and that the
2898    /// decode matmul cell keys to a DISTINCT [`SizeClass`] from every
2899    /// square cell (slice-4's arm-0 flash-vs-decomposed comparison
2900    /// needs the decomposed primitives measured at decode shapes, keyed
2901    /// distinctly so the oracle doesn't collapse them onto a square
2902    /// cell). Exercises the production `size_plan`, not an override.
2903    #[test]
2904    fn size_plan_carries_decode_shaped_cells() {
2905        let judge = Judge::default(); // no override → real size_plan
2906
2907        // -- MatMul: a seq_q=1 (m==1) decode GEMV must be present, and
2908        //    its aspect SizeClass must differ from every square cell's.
2909        //    Keys are derived via `SizeClass::matmul(m,n,k)` — the SAME
2910        //    helper the producer (`run`) and the ranker consumer reach
2911        //    (through `for_op`), so this asserts the real dispatch key. --
2912        let mm = judge.size_plan(OpKind::MatMul);
2913        let sc_of = |s: &OpSize| match *s {
2914            OpSize::MatMul { m, n, k } => SizeClass::matmul(m, n, k),
2915            _ => unreachable!("size_plan(MatMul) yields only MatMul sizes"),
2916        };
2917        let square_scs: Vec<SizeClass> = mm
2918            .iter()
2919            .filter(|s| matches!(s, OpSize::MatMul { m, n, .. } if m == n))
2920            .map(sc_of)
2921            .collect();
2922        assert!(
2923            !square_scs.is_empty(),
2924            "the general square matmul ladder must be preserved",
2925        );
2926        let decode_mm: Vec<&OpSize> = mm
2927            .iter()
2928            .filter(|s| matches!(s, OpSize::MatMul { m, .. } if *m == 1))
2929            .collect();
2930        assert!(
2931            !decode_mm.is_empty(),
2932            "size_plan(MatMul) must include a seq_q=1 decode GEMV cell",
2933        );
2934        for d in &decode_mm {
2935            let sc = sc_of(d);
2936            assert!(
2937                !square_scs.contains(&sc),
2938                "decode matmul cell {d:?} SizeClass {sc:?} collides with a \
2939                 square cell — arm-0 costing would read the square latency",
2940            );
2941        }
2942
2943        // -- Decode-row reductions (softmax row-max + denominator-sum):
2944        //    a WIDE reduced dim (cols >= 128 = k_len), distinct from the
2945        //    cols=64 general ladder. Min/Mean are NOT on the softmax
2946        //    path and must keep only their general ladder. --
2947        for op in [OpKind::MaxReduce, OpKind::SumReduce] {
2948            let plan = judge.size_plan(op);
2949            assert!(
2950                plan.iter()
2951                    .any(|s| matches!(s, OpSize::Reduce { cols, .. } if *cols >= 128)),
2952                "size_plan({op}) must include a decode-row (wide-cols) reduction cell",
2953            );
2954        }
2955        for op in [OpKind::MinReduce, OpKind::MeanReduce] {
2956            let plan = judge.size_plan(op);
2957            assert!(
2958                plan.iter()
2959                    .all(|s| matches!(s, OpSize::Reduce { cols, .. } if *cols == 64)),
2960                "size_plan({op}) is off the decode softmax path — no decode cell expected",
2961            );
2962        }
2963
2964        // -- Decode-score-row elementwise (softmax sub/exp/div over the
2965        //    [Hq, k_len] score tensor). These three get decode cells
2966        //    appended; an off-path elementwise op (e.g. Sin) does not. --
2967        for op in [
2968            OpKind::SubElementwise,
2969            OpKind::ExpElementwise,
2970            OpKind::DivElementwise,
2971        ] {
2972            let plan = judge.size_plan(op);
2973            assert!(
2974                plan.len() > 3,
2975                "size_plan({op}) must append decode cells beyond the 3-size base ladder",
2976            );
2977        }
2978        assert_eq!(
2979            judge.size_plan(OpKind::SinElementwise).len(),
2980            3,
2981            "an off-softmax-path elementwise op must keep only its 3-size base ladder",
2982        );
2983    }
2984
2985    /// Slice-2 born-red companion: a fast CPU run over a square + a
2986    /// decode matmul cell produces `ProfileEntry`s at TWO distinct
2987    /// [`SizeClass`] keys, each with a positive latency. This is the
2988    /// end-to-end proof that the decode GEMV is measurable AND
2989    /// distinguishable from the square regime through the full run
2990    /// path (deliverable 3's "distinguishable from the square cell").
2991    #[test]
2992    fn judge_measures_decode_matmul_distinct_from_square() {
2993        let probe = ProbeReport::probe_all();
2994        let judge = Judge {
2995            iterations: 3,
2996            warmup: 1,
2997            size_plan_override: Some(vec![
2998                // Square (compute-bound) — small enough to stay fast.
2999                (
3000                    OpKind::MatMul,
3001                    OpSize::MatMul {
3002                        m: 256,
3003                        n: 256,
3004                        k: 256,
3005                    },
3006                ),
3007                // Decode GEMV (seq_q=1, bandwidth-bound).
3008                (
3009                    OpKind::MatMul,
3010                    OpSize::MatMul {
3011                        m: 1,
3012                        n: 2048,
3013                        k: 2048,
3014                    },
3015                ),
3016            ]),
3017            fixtures: None,
3018        };
3019        let report = judge.run(&probe);
3020        // v4 aspect keys — the same `matmul(m,n,k)` the producer's
3021        // `run` stamps on each entry.
3022        let square_sc = SizeClass::matmul(256, 256, 256);
3023        let decode_sc = SizeClass::matmul(1, 2048, 2048);
3024        assert_ne!(
3025            square_sc, decode_sc,
3026            "decode + square matmul must key to different SizeClass",
3027        );
3028        let measured = |sc: SizeClass| {
3029            report.entries.iter().any(|e| {
3030                e.op == OpKind::MatMul
3031                    && e.backend == BackendId::Cpu
3032                    && e.size_class == sc
3033                    && e.latency_ns > 0
3034            })
3035        };
3036        assert!(measured(square_sc), "square matmul cell must be measured");
3037        assert!(
3038            measured(decode_sc),
3039            "decode matmul cell must be measured with latency > 0 at its own SizeClass",
3040        );
3041    }
3042
3043    /// SizeClass v4 producer/consumer round-trip (Judge Layer-2 coverage
3044    /// arc, slice 2.5, 2026-07-04).
3045    ///
3046    /// **Born-red**: this is the round-trip the aspect-blind key broke.
3047    /// The Judge (producer) profiled the FFN-width decode GEMV
3048    /// `[1,2048]×[2048,5632]` keyed on `m·n = 5632`; the `fuel-dispatch`
3049    /// ranker (consumer) keyed its realize-time lookup on
3050    /// `shapes[0].elem_count() = m·k = 2048` — a DIFFERENT bucket — so
3051    /// the lookup never found the produced cell. This drives the real
3052    /// producer path (`run`) to emit the cell, then performs the exact
3053    /// consumer key derivation (`SizeClass::for_op` from the operand
3054    /// shapes, as `compute_static_costs` does) and asserts the produced
3055    /// entry is HIT through the actual `ProfileJudgeOracle`. Pre-v4 this
3056    /// asserted-`is_some` lookup returned `None` (miss).
3057    #[test]
3058    fn ranker_lookup_hits_producer_decode_gemv_cell() {
3059        use fuel_dispatch::ranker::JudgeOracle;
3060
3061        let probe = ProbeReport::probe_all();
3062        let judge = Judge {
3063            iterations: 3,
3064            warmup: 1,
3065            // The genuinely non-square FFN-width decode GEMV — the cell
3066            // slice 2 had to DEFER because its old m·n key collided with
3067            // the 64³ square.
3068            size_plan_override: Some(vec![(
3069                OpKind::MatMul,
3070                OpSize::MatMul {
3071                    m: 1,
3072                    n: 5632,
3073                    k: 2048,
3074                },
3075            )]),
3076            fixtures: None,
3077        };
3078        let report = judge.run(&probe);
3079
3080        // Consumer-side key: derived from the operand shapes
3081        // lhs=[m,k]/rhs=[k,n] exactly the way the ranker's
3082        // `compute_static_costs` does (`SizeClass::for_op`).
3083        let lhs = Shape::from_dims(&[1, 2048]);
3084        let rhs = Shape::from_dims(&[2048, 5632]);
3085        let ranker_key = SizeClass::for_op(OpKind::MatMul, &[lhs, rhs]);
3086
3087        // Old-bug witness: for this non-square shape the pre-v4 scalar
3088        // keys the two sides used (consumer m·k vs producer m·n) landed
3089        // in different buckets — the disagreement this slice fixes.
3090        assert_ne!(
3091            SizeClass::from_elem_count(1 * 2048), // old consumer key (m·k)
3092            SizeClass::from_elem_count(1 * 5632), // old producer key (m·n)
3093            "sanity: non-square shape → the OLD scalar keys disagreed",
3094        );
3095
3096        // The produced entry the ranker must now find.
3097        let cpu_entry = report
3098            .entries
3099            .iter()
3100            .find(|e| {
3101                e.op == OpKind::MatMul && e.backend == BackendId::Cpu && e.dtype == DType::F32
3102            })
3103            .expect("Judge produced a CPU f32 matmul entry for the GEMV");
3104        assert_eq!(
3105            cpu_entry.size_class, ranker_key,
3106            "producer key must EQUAL the consumer's for_op key for the \
3107             non-square GEMV (the reconciliation)",
3108        );
3109
3110        // And the real consumer oracle resolves it — the round trip.
3111        let oracle = ProfileJudgeOracle::from_report(&report);
3112        let hit = oracle.measured_latency_ns(
3113            OpKind::MatMul,
3114            DType::F32,
3115            ranker_key,
3116            BackendId::Cpu,
3117            cpu_entry.kernel_source.as_str(),
3118            // The round trip must key on the SAME revision the producer wrote,
3119            // not 0 — the produced entry now carries a real attributed
3120            // revision, so a 0 query would miss.
3121            cpu_entry.kernel_revision_hash,
3122        );
3123        assert!(
3124            hit.is_some(),
3125            "synthetic ranker lookup at the GEMV shape must HIT the \
3126             produced cell (pre-v4 this missed)",
3127        );
3128        assert!(hit.unwrap() > 0, "measured latency must be positive");
3129    }
3130
3131    /// Dtype-axis slice (Judge Layer-2 coverage arc, 2026-07-04).
3132    ///
3133    /// **Born-red history**: before this slice the Judge hard-coded
3134    /// `DType::F32` in every measurement call, so `run()` emitted ONLY
3135    /// f32 `ProfileEntry`s. This test asserts the measurement matrix
3136    /// now covers f16 AND bf16 for the profiled families, that each
3137    /// entry is keyed by the dtype it was measured at, and that the
3138    /// measured latencies are positive. CPU-only shrunk ladder — no
3139    /// live GPU needed (CPU carries f16/bf16 kernels for the
3140    /// elementwise + matmul families via the FKC `[F32,F64,BF16,F16]`
3141    /// fan-out). A live CUDA/Vulkan profile would ADD the per-dtype
3142    /// GPU rows the ranker ultimately compares flash-vs-decomposed
3143    /// against, but the axis mechanics are fully exercised on CPU.
3144    #[test]
3145    fn judge_profiles_f16_and_bf16_cells() {
3146        let probe = ProbeReport::probe_all();
3147        let judge = Judge {
3148            iterations: 3,
3149            warmup: 1,
3150            size_plan_override: Some(vec![
3151                (OpKind::AddElementwise, OpSize::Elementwise(1 << 8)),
3152                (OpKind::MulElementwise, OpSize::Elementwise(1 << 8)),
3153                (
3154                    OpKind::MatMul,
3155                    OpSize::MatMul {
3156                        m: 16,
3157                        n: 16,
3158                        k: 16,
3159                    },
3160                ),
3161            ]),
3162            fixtures: None,
3163        };
3164        let report = judge.run(&probe);
3165
3166        // The half-precision dtypes must each produce ≥1 CPU entry per
3167        // profiled op, keyed by that dtype, with a positive latency.
3168        for dtype in [DType::F16, DType::BF16] {
3169            for op in [
3170                OpKind::AddElementwise,
3171                OpKind::MulElementwise,
3172                OpKind::MatMul,
3173            ] {
3174                let cells: Vec<_> = report
3175                    .entries
3176                    .iter()
3177                    .filter(|e| e.op == op && e.dtype == dtype && e.backend == BackendId::Cpu)
3178                    .collect();
3179                assert!(
3180                    !cells.is_empty(),
3181                    "dtype-axis: expected ≥1 CPU {dtype:?} entry for {op}, got 0 \
3182                     (is the {dtype:?} kernel registered for this op?)",
3183                );
3184                for e in &cells {
3185                    assert_eq!(
3186                        e.dtype, dtype,
3187                        "entry must be keyed by the dtype it was measured at",
3188                    );
3189                    assert!(
3190                        e.latency_ns > 0,
3191                        "{op} {dtype:?} latency must be > 0, got {e:?}",
3192                    );
3193                    // Single-backend CPU cell → trivial consensus → the
3194                    // rel_err is 0.0 (no peers). Half-precision rounding
3195                    // shows up only against a differing peer, which the
3196                    // CPU-only born-red doesn't have.
3197                    assert!(
3198                        e.max_rel_error.is_finite(),
3199                        "{op} {dtype:?} rel_err must be finite, got {}",
3200                        e.max_rel_error,
3201                    );
3202                }
3203            }
3204        }
3205
3206        // Regression guard: the f32 rows the pre-slice Judge produced
3207        // are still present — the dtype loop ADDS f16/bf16, it doesn't
3208        // displace f32.
3209        assert!(
3210            report.entries.iter().any(|e| e.dtype == DType::F32
3211                && e.op == OpKind::AddElementwise
3212                && e.backend == BackendId::Cpu),
3213            "dtype-axis: f32 coverage regressed — expected f32 AddElementwise entry",
3214        );
3215
3216        // Cross-check: exactly the three profiled dtypes appear, none
3217        // else (no f64 or stray dtype leaked into the matrix).
3218        let mut dtypes: Vec<DType> = report.entries.iter().map(|e| e.dtype).collect();
3219        dtypes.sort_by_key(|d| format!("{d:?}"));
3220        dtypes.dedup();
3221        assert_eq!(
3222            dtypes.len(),
3223            PROFILED_DTYPES.len(),
3224            "expected exactly the profiled dtype set {PROFILED_DTYPES:?}, saw {dtypes:?}",
3225        );
3226        for d in PROFILED_DTYPES {
3227            assert!(
3228                dtypes.contains(d),
3229                "profiled dtype {d:?} missing from the report",
3230            );
3231        }
3232    }
3233
3234    /// FlashAttn-as-profiled-op slice (Judge Layer-2 coverage arc,
3235    /// slice 3, 2026-07-04).
3236    ///
3237    /// **Born-red**: before this slice `OpKind::FlashAttn` was absent from
3238    /// [`PROFILED_OPS`], so `run()`'s outer `for &op in PROFILED_OPS` loop
3239    /// never processed it — a `size_plan_override` carrying a FlashAttn
3240    /// cell was filtered out and the report held ZERO FlashAttn entries.
3241    /// This asserts the fused decode-attention op is now MEASURED on CPU:
3242    /// the Judge builds q=[b,hq,1,d] / k=v=[b,hkv,k_len,d], emits the
3243    /// `Op::Fused(FLASH_ATTN)` node, realizes it (the registered fused CPU
3244    /// SDPA kernel), and produces `ProfileEntry`s keyed by the flash
3245    /// node's `SizeClass::attention` — arm-1 of slice-4's flash-vs-
3246    /// decomposed comparison, whose latency is what THIS slice measures.
3247    ///
3248    /// Two decode GQA cells (short + capacity k_len) must each land at a
3249    /// DISTINCT attention SizeClass with a positive latency; the same
3250    /// `attention(hq, k_len, d)` key slice-4's bake derives via `for_op`.
3251    /// CPU-only born-red; f32 is asserted (the reference-width kernel),
3252    /// f16/bf16 are checked opportunistically (the half kernels exist,
3253    /// but a missing dtype cell is skipped cleanly, never fatal).
3254    #[test]
3255    fn judge_profiles_flash_attn_at_decode_shapes() {
3256        let probe = ProbeReport::probe_all();
3257        // TinyLlama-ish decode GQA: Hq=32, Hkv=4, D=64. Two k_len cells
3258        // (short + capacity) — kept small enough to stay fast.
3259        let small = OpSize::Attention {
3260            b: 1,
3261            hq: 32,
3262            hkv: 4,
3263            d: 64,
3264            k_len: 128,
3265        };
3266        let cap = OpSize::Attention {
3267            b: 1,
3268            hq: 32,
3269            hkv: 4,
3270            d: 64,
3271            k_len: 512,
3272        };
3273        let judge = Judge {
3274            iterations: 3,
3275            warmup: 1,
3276            size_plan_override: Some(vec![(OpKind::FlashAttn, small), (OpKind::FlashAttn, cap)]),
3277            fixtures: None,
3278        };
3279        let report = judge.run(&probe);
3280
3281        // The two decode cells key to DISTINCT attention SizeClasses —
3282        // the same helper the producer stamps and slice-4's `for_op`
3283        // consumer derives.
3284        let sc_small = SizeClass::attention(32, 128, 64);
3285        let sc_cap = SizeClass::attention(32, 512, 64);
3286        assert_ne!(
3287            sc_small, sc_cap,
3288            "short + capacity decode attention must key to different SizeClass",
3289        );
3290
3291        // f32 (reference-width) MUST be measured at BOTH decode cells.
3292        for (label, sc) in [("short", sc_small), ("capacity", sc_cap)] {
3293            let hit = report.entries.iter().any(|e| {
3294                e.op == OpKind::FlashAttn
3295                    && e.backend == BackendId::Cpu
3296                    && e.dtype == DType::F32
3297                    && e.size_class == sc
3298                    && e.latency_ns > 0
3299            });
3300            assert!(
3301                hit,
3302                "FlashAttn f32 {label} decode cell (SizeClass {sc:?}) must be \
3303                 measured with latency > 0 — arm-1 of the flash-vs-decomposed \
3304                 comparison",
3305            );
3306        }
3307
3308        // Consumer round-trip: the key slice-4's bake derives from the
3309        // flash node's operand shapes via `for_op` must EQUAL the
3310        // producer's stamped key, so the bake's lookup HITS the cell.
3311        let consumer_key = SizeClass::for_op(
3312            OpKind::FlashAttn,
3313            &[
3314                Shape::from_dims(&[1, 32, 1, 64]),
3315                Shape::from_dims(&[1, 4, 128, 64]),
3316                Shape::from_dims(&[1, 4, 128, 64]),
3317            ],
3318        );
3319        assert_eq!(
3320            consumer_key, sc_small,
3321            "the flash node's for_op key must equal the producer's attention key",
3322        );
3323
3324        // Half-precision cells exist opportunistically (the CPU flash
3325        // kernel carries f16/bf16 variants). Not fatal if a backend
3326        // skips one, but on this CPU build they should all measure.
3327        for dtype in [DType::F16, DType::BF16] {
3328            let hit = report.entries.iter().any(|e| {
3329                e.op == OpKind::FlashAttn
3330                    && e.backend == BackendId::Cpu
3331                    && e.dtype == dtype
3332                    && e.size_class == sc_small
3333                    && e.latency_ns > 0
3334            });
3335            assert!(
3336                hit,
3337                "FlashAttn {dtype:?} short decode cell must be measured (the CPU \
3338                 flash kernel has an {dtype:?} variant)",
3339            );
3340        }
3341    }
3342
3343    #[test]
3344    fn dispatch_table_built_from_expanded_report_serves_multiple_kinds() {
3345        // Confirms the DispatchTable's O(1) lookup path handles the
3346        // expanded OpKind coverage. The route picker can now pick
3347        // among many more (op, size_class) cells than the original
3348        // matmul + add report supported.
3349        use fuel_ir::dispatch::{Criterion, DispatchTable};
3350
3351        let probe = ProbeReport::probe_all();
3352        let judge = Judge {
3353            iterations: 3,
3354            warmup: 1,
3355            size_plan_override: Some(vec![
3356                (
3357                    OpKind::MatMul,
3358                    OpSize::MatMul {
3359                        m: 32,
3360                        n: 32,
3361                        k: 32,
3362                    },
3363                ),
3364                (OpKind::AddElementwise, OpSize::Elementwise(1 << 8)),
3365                (OpKind::ReluElementwise, OpSize::Elementwise(1 << 8)),
3366                (OpKind::SumReduce, OpSize::Reduce { rows: 16, cols: 16 }),
3367                (OpKind::ReduceSumTo, OpSize::ReduceTo { rows: 16, cols: 16 }),
3368                (OpKind::Affine, OpSize::Elementwise(1 << 8)),
3369            ]),
3370            fixtures: None,
3371        };
3372        let report = judge.run(&probe);
3373        let table = DispatchTable::build(&report);
3374
3375        // Every kind in the plan should yield a Pick at every
3376        // criterion. SizeClass(8) covers the elementwise plan
3377        // (256 elems = 2^8) and the reductions (16*16 = 2^8); the
3378        // matmul cell keys on its v4 aspect key `matmul(32,32,32)`
3379        // (the producer no longer keys it on the scalar 32*32=2^10).
3380        let elementwise_kinds = [
3381            OpKind::AddElementwise,
3382            OpKind::ReluElementwise,
3383            OpKind::SumReduce,
3384            OpKind::ReduceSumTo,
3385            OpKind::Affine,
3386        ];
3387        for &op in &elementwise_kinds {
3388            for &crit in &[
3389                Criterion::Fastest,
3390                Criterion::MostAccurate,
3391                Criterion::Balanced,
3392            ] {
3393                let pick = table.pick(op, DType::F32, SizeClass(8), crit);
3394                assert!(
3395                    pick.is_some(),
3396                    "DispatchTable missing pick for {op}@2^8 / {crit:?}"
3397                );
3398            }
3399        }
3400        let matmul_pick = table.pick(
3401            OpKind::MatMul,
3402            DType::F32,
3403            SizeClass::matmul(32, 32, 32),
3404            Criterion::Fastest,
3405        );
3406        assert!(matmul_pick.is_some(), "DispatchTable missing matmul pick");
3407
3408        // The table's keys() should include every (op, dtype,
3409        // size_class) tuple exercised by the plan.
3410        let keys = table.keys();
3411        assert!(
3412            keys.iter()
3413                .any(|(op, _, sc)| *op == OpKind::ReluElementwise && sc.0 == 8)
3414        );
3415        assert!(
3416            keys.iter()
3417                .any(|(op, _, sc)| *op == OpKind::SumReduce && sc.0 == 8)
3418        );
3419        assert!(
3420            keys.iter()
3421                .any(|(op, _, sc)| *op == OpKind::ReduceSumTo && sc.0 == 8)
3422        );
3423        assert!(
3424            keys.iter()
3425                .any(|(op, _, sc)| *op == OpKind::Affine && sc.0 == 8)
3426        );
3427    }
3428
3429    #[test]
3430    fn profile_report_save_load_roundtrip() {
3431        let report = ProfileReport {
3432            version: PROFILE_REPORT_VERSION,
3433            entries: vec![ProfileEntry {
3434                op: OpKind::MatMul,
3435                dtype: DType::F32,
3436                size_class: SizeClass(8),
3437                backend: BackendId::Cpu,
3438                device_index: 0,
3439                latency_ns: 12_345,
3440                iterations: 7,
3441                max_rel_error: 1e-7,
3442                kernel_source: String::new(),
3443                kernel_revision_hash: 0,
3444                device: test_equiv_key(BackendId::Cpu),
3445            }],
3446        };
3447        let tmp = std::env::temp_dir().join(format!("fuel-judge-test-{}.json", std::process::id()));
3448        report.save(&tmp).expect("save");
3449        // Positive control: the loading machine declares it HAS the CPU device
3450        // the report was stamped with, so the hardware gate admits it.
3451        let loaded = ProfileReport::load(&tmp, &[test_equiv_key(BackendId::Cpu)])
3452            .expect("load")
3453            .expect("file exists");
3454        assert_eq!(loaded, report);
3455        let _ = std::fs::remove_file(&tmp);
3456    }
3457
3458    /// (a) device-identity gate. A `ProfileReport` is the output of ONE Judge
3459    /// run on ONE machine, so a report stamped with device X must be REFUSED
3460    /// WHOLE when loaded on a machine whose hardware is Y — the `device_index`
3461    /// ordinal it was keyed by rebinds across machines, so a coincidental
3462    /// overlap is a foreign report, not a valid subset. The paired positive
3463    /// control (`current = [X]`) proves the `None` is the hardware gate firing,
3464    /// not `load()` failing for a parse/version reason that would `None` either
3465    /// way. X and Y share a backend and differ only in vendor/device id, so the
3466    /// gate must discriminate on the FULL key, not merely the backend axis.
3467    #[test]
3468    fn load_refuses_a_report_measured_on_foreign_hardware() {
3469        use fuel_ir::probe::EquivalenceKey;
3470        let device_x = EquivalenceKey {
3471            backend: BackendId::Cuda,
3472            vendor_id: 0x10de, // NVIDIA
3473            device_id: 0x2820,
3474            compute_capability: Some((8, 9)),
3475            driver_version: "560.0".to_string(),
3476        };
3477        let device_y = EquivalenceKey {
3478            backend: BackendId::Cuda,
3479            vendor_id: 0x1002, // AMD
3480            device_id: 0x1234,
3481            compute_capability: None,
3482            driver_version: "24.0".to_string(),
3483        };
3484        assert_ne!(device_x, device_y);
3485
3486        let report = ProfileReport {
3487            version: PROFILE_REPORT_VERSION,
3488            entries: vec![ProfileEntry {
3489                op: OpKind::MatMul,
3490                dtype: DType::F32,
3491                size_class: SizeClass(8),
3492                backend: BackendId::Cuda,
3493                device_index: 0,
3494                latency_ns: 5_000,
3495                iterations: 7,
3496                max_rel_error: 1e-7,
3497                kernel_source: String::new(),
3498                kernel_revision_hash: 0,
3499                device: device_x.clone(),
3500            }],
3501        };
3502        let tmp =
3503            std::env::temp_dir().join(format!("fuel-judge-foreign-{}.json", std::process::id()));
3504        report.save(&tmp).expect("save");
3505
3506        // Loaded on a machine that has ONLY device Y: the report is foreign, so
3507        // the whole thing is refused -> Ok(None).
3508        let refused = ProfileReport::load(&tmp, std::slice::from_ref(&device_y)).expect("load ok");
3509        assert!(
3510            refused.is_none(),
3511            "a report stamped on device X must be refused on a Y-only machine"
3512        );
3513
3514        // Positive control: loaded on a machine that HAS device X, the SAME
3515        // bytes load as Some — so the None above is the gate, not a parse or
3516        // version failure that would fail regardless of hardware.
3517        let admitted = ProfileReport::load(&tmp, std::slice::from_ref(&device_x)).expect("load ok");
3518        assert_eq!(
3519            admitted.expect("positive control: device X is present, report must load"),
3520            report
3521        );
3522
3523        let _ = std::fs::remove_file(&tmp);
3524    }
3525
3526    /// `load()` deserializes BEFORE it checks the version, so a report from an
3527    /// OLDER schema (missing today's fields) must still DESERIALIZE — otherwise
3528    /// a stale report `Err`s where it should be a clean cache miss. This pins
3529    /// that every field added since v4 carries `#[serde(default)]`: a v4-shaped
3530    /// JSON (no `device`, no `kernel_revision_hash`) loads as `Ok(None)` (the
3531    /// version gate), never `Err` (a parse failure). If a future field drops
3532    /// its default, this goes red.
3533    #[test]
3534    fn v4_report_yields_no_oracle_entries() {
3535        let report = ProfileReport {
3536            version: PROFILE_REPORT_VERSION,
3537            entries: vec![ProfileEntry {
3538                op: OpKind::MatMul,
3539                dtype: DType::F32,
3540                size_class: SizeClass(8),
3541                backend: BackendId::Cpu,
3542                device_index: 0,
3543                latency_ns: 12_345,
3544                iterations: 7,
3545                max_rel_error: 1e-7,
3546                kernel_source: String::new(),
3547                kernel_revision_hash: 0,
3548                device: test_equiv_key(BackendId::Cpu),
3549            }],
3550        };
3551        // Strip the current report back to the v4 wire shape: version 4, and
3552        // drop the two fields v4 never had.
3553        let mut v: serde_json::Value = serde_json::to_value(&report).expect("to_value");
3554        v["version"] = serde_json::json!(4);
3555        for e in v["entries"].as_array_mut().unwrap() {
3556            let obj = e.as_object_mut().unwrap();
3557            obj.remove("device");
3558            obj.remove("kernel_revision_hash");
3559        }
3560        let tmp = std::env::temp_dir().join(format!("fuel-judge-v4-{}.json", std::process::id()));
3561        std::fs::write(&tmp, serde_json::to_vec(&v).unwrap()).expect("write");
3562
3563        // Ok(None): the missing fields default in (deserialization succeeds —
3564        // proving the `#[serde(default)]`s), then the version gate rejects the
3565        // stale schema. An `Err` here would mean a new field lost its default
3566        // and a stale report now crashes the load path instead of missing.
3567        let loaded = ProfileReport::load(&tmp, &[test_equiv_key(BackendId::Cpu)]);
3568        assert!(
3569            matches!(loaded, Ok(None)),
3570            "a v4-shaped report must load as Ok(None) (version gate), got {loaded:?}"
3571        );
3572        let _ = std::fs::remove_file(&tmp);
3573    }
3574
3575    /// F2 born-red: the dispatched-alternative skip keys on the FULL identity
3576    /// (kernel_source AND kernel_revision_hash), not the tag alone. Now that
3577    /// variant siblings of one provider share a tag and differ only by revision,
3578    /// a tag-only match would skip the WRONG sibling — leaving the dispatched one
3579    /// re-measured and dropping a real sibling. This pins `is_dispatched_alt`,
3580    /// the single place the skip identity is compared. Sabotage: drop the
3581    /// revision check (tag-only) and the first assertion flips true → red.
3582    #[test]
3583    fn dispatched_alt_is_matched_on_source_and_revision() {
3584        // A sibling sharing the tag but a DIFFERENT revision is NOT dispatched.
3585        assert!(!is_dispatched_alt(
3586            "portable-cpu",
3587            0xAAAA,
3588            "portable-cpu",
3589            0xBBBB
3590        ));
3591        // The exact (source, revision) IS the dispatched kernel.
3592        assert!(is_dispatched_alt(
3593            "portable-cpu",
3594            0xBBBB,
3595            "portable-cpu",
3596            0xBBBB
3597        ));
3598        // A different provider at the same revision is NOT dispatched.
3599        assert!(!is_dispatched_alt("aocl", 0xBBBB, "portable-cpu", 0xBBBB));
3600        // Untracked revision (0) still requires the tag to agree.
3601        assert!(is_dispatched_alt("", 0, "", 0));
3602        assert!(!is_dispatched_alt("mkl", 0, "", 0));
3603    }
3604
3605    #[test]
3606    fn max_rel_err_matches_known_values() {
3607        assert_eq!(max_rel_err(&[1.0, 2.0], &[1.0, 2.0]), 0.0);
3608        // f32 rounding in both `2.001` literals and the subtraction
3609        // means the exact numeric answer is not `0.001 / 2.001`. Pin
3610        // the answer loosely: should be within a few ULP of that
3611        // ratio and close to 5e-4.
3612        let got = max_rel_err(&[1.0, 2.0], &[1.0, 2.001]);
3613        assert!((got - 5.0e-4).abs() < 1e-6, "expected ~0.0005, got {got}",);
3614        // Length mismatch → infinity.
3615        assert!(max_rel_err(&[1.0], &[1.0, 1.0]).is_infinite());
3616        // NaN in either operand → infinity.
3617        assert!(max_rel_err(&[f32::NAN], &[0.0]).is_infinite());
3618    }
3619
3620    /// Fixture-fast-path integration: when a fixture exists for the
3621    /// cell, the Judge derives `max_rel_error` from
3622    /// `validate_against_fixture` instead of running inline pairwise
3623    /// consensus. This test constructs an intentionally-divergent
3624    /// fixture for the AddElementwise cell and asserts the divergence
3625    /// shows up as the entries' `max_rel_error`.
3626    ///
3627    /// We deliberately *don't* use a fixture matching the actual cpu
3628    /// kernel's output here — the assertion is on the wire-up, not on
3629    /// any specific numerical value the cpu produces. Real fixtures
3630    /// land via the capture pipeline; this test verifies the path.
3631    #[test]
3632    fn judge_uses_fixture_when_provided() {
3633        use fuel_correctness_fixtures::{CorrectnessFixture, ToleranceBand};
3634
3635        let probe = ProbeReport::probe_all();
3636        let elem_count = 1 << 10;
3637        let sc = SizeClass::from_elem_count(elem_count);
3638        // Plant an obviously-wrong fixture: all zeros expected. The
3639        // cpu add kernel produces sin+cos = non-zero, so
3640        // validate_against_fixture's OutOfTolerance.rel_err lands in
3641        // every entry's max_rel_error (well above any plausible
3642        // cross-backend drift floor).
3643        //
3644        // The fixture must satisfy the triple-agreement contract
3645        // (seed + hash + shape) for `lookup_fixture` to return it.
3646        // We use the same `derive_seed` / `deterministic_f32_input` /
3647        // `hash_f32_input` the capture tool uses, so the lookup path
3648        // accepts the (deliberately-wrong-expected-output) fixture.
3649        let zeros: Vec<u8> = vec![0u8; elem_count * 4];
3650        let seed =
3651            fuel_correctness_fixtures::capture::derive_seed(OpKind::AddElementwise, DType::F32, sc);
3652        // AddElementwise is binary: input is the concatenated `[a, b]`
3653        // buffer of length `2 * n`.
3654        let input = fuel_correctness_fixtures::capture::deterministic_f32_input(
3655            OpKind::AddElementwise,
3656            2 * elem_count,
3657        );
3658        let input_hash = fuel_correctness_fixtures::capture::hash_f32_input(&input);
3659        let bogus_fixture = CorrectnessFixture {
3660            op: OpKind::AddElementwise,
3661            dtype: DType::F32,
3662            size_class: sc,
3663            input_seed: seed,
3664            input_hash,
3665            expected_output: zeros,
3666            output_element_count: elem_count,
3667            // Tight tolerance — guarantees the all-zeros fixture
3668            // disagrees with any honest add(sin, cos) output.
3669            tolerance: ToleranceBand {
3670                max_relative: 1e-9,
3671                max_absolute: 1e-12,
3672            },
3673        };
3674        let mut map: HashMap<(OpKind, DType, SizeClass), Vec<CorrectnessFixture>> = HashMap::new();
3675        map.insert(
3676            (
3677                OpKind::AddElementwise,
3678                DType::F32,
3679                SizeClass::from_elem_count(elem_count),
3680            ),
3681            vec![bogus_fixture],
3682        );
3683
3684        let judge = Judge {
3685            iterations: 3,
3686            warmup: 1,
3687            size_plan_override: Some(vec![(
3688                OpKind::AddElementwise,
3689                OpSize::Elementwise(elem_count),
3690            )]),
3691            fixtures: Some(map),
3692        };
3693        let report = judge.run(&probe);
3694        // Dtype-axis note (2026-07-04): the planted fixture is keyed
3695        // `(AddElementwise, F32, sc)`, so only the F32 cells hit the
3696        // fixture fast-path. The f16/bf16 cells the dtype loop now also
3697        // emits have NO fixture → they take the consensus path → lone
3698        // CPU backend → rel_err 0.0. Scope this assertion to the F32
3699        // entries the fixture actually governs.
3700        let cpu_entries: Vec<_> = report
3701            .entries
3702            .iter()
3703            .filter(|e| {
3704                e.op == OpKind::AddElementwise
3705                    && e.dtype == DType::F32
3706                    && e.backend == BackendId::Cpu
3707            })
3708            .collect();
3709        assert!(
3710            !cpu_entries.is_empty(),
3711            "expected ≥1 cpu f32 entry for AddElementwise, got 0"
3712        );
3713        // The bogus all-zeros fixture is wildly wrong vs the honest
3714        // sin+cos output, so every cpu entry's max_rel_error should
3715        // be very large (≥ 1.0 — relative error vs ~zero expected is
3716        // unbounded; floor at 1.0 is conservative). Without the
3717        // fixture fast-path this value would be ~0.0 (lone CPU
3718        // backend → trivial consensus → no peers → 0.0).
3719        for e in &cpu_entries {
3720            assert!(
3721                e.max_rel_error >= 1.0,
3722                "expected fixture-derived rel_err ≥1.0 (vs all-zeros fixture), got {} for entry {e:?}",
3723                e.max_rel_error
3724            );
3725        }
3726    }
3727
3728    /// Loader: `with_fixtures_from(file)` reads a JSON fixture file
3729    /// and produces a Judge whose `fixtures` map has the entry keyed
3730    /// correctly.
3731    #[test]
3732    fn judge_with_fixtures_from_loads_json_file() {
3733        use fuel_correctness_fixtures::{
3734            CorrectnessFixture, FIXTURE_FILE_VERSION, FixtureFile, ToleranceBand,
3735        };
3736
3737        let fixture = CorrectnessFixture {
3738            op: OpKind::MatMul,
3739            dtype: DType::F32,
3740            size_class: SizeClass(10),
3741            input_seed: 42,
3742            input_hash: 0xdeadbeef,
3743            expected_output: vec![0u8; 16],
3744            output_element_count: 4,
3745            tolerance: ToleranceBand::F32_DEFAULT,
3746        };
3747        let file = FixtureFile {
3748            version: FIXTURE_FILE_VERSION,
3749            fixtures: vec![fixture],
3750        };
3751        let dir =
3752            std::env::temp_dir().join(format!("fuel-judge-fixture-load-{}", std::process::id(),));
3753        let _ = std::fs::create_dir_all(&dir);
3754        let path = dir.join("matmul_f32.json");
3755        std::fs::write(&path, serde_json::to_string(&file).unwrap()).unwrap();
3756
3757        let judge = Judge::with_fixtures_from(&dir).expect("load");
3758        let map = judge.fixtures.as_ref().expect("fixtures populated");
3759        let key = (OpKind::MatMul, DType::F32, SizeClass(10));
3760        let bucket = map.get(&key).expect("matmul fixture in bucket");
3761        assert_eq!(bucket.len(), 1);
3762        assert_eq!(bucket[0].input_seed, 42);
3763
3764        // Loading from a single file path works too.
3765        let judge2 = Judge::with_fixtures_from(&path).expect("load file");
3766        let map2 = judge2.fixtures.as_ref().expect("fixtures populated");
3767        assert!(map2.contains_key(&key));
3768
3769        let _ = std::fs::remove_dir_all(&dir);
3770    }
3771
3772    /// Loader: fixture files whose `version` doesn't match the
3773    /// current `FIXTURE_FILE_VERSION` are skipped with a stderr
3774    /// warning. The load itself is non-fatal — a single stale file
3775    /// in the distribution shouldn't strand the rest of the run.
3776    #[test]
3777    fn judge_load_skips_mismatched_version() {
3778        use fuel_correctness_fixtures::{CorrectnessFixture, FixtureFile, ToleranceBand};
3779
3780        let good_fixture = CorrectnessFixture {
3781            op: OpKind::MatMul,
3782            dtype: DType::F32,
3783            size_class: SizeClass(10),
3784            input_seed: 1,
3785            input_hash: 2,
3786            expected_output: vec![0u8; 16],
3787            output_element_count: 4,
3788            tolerance: ToleranceBand::F32_DEFAULT,
3789        };
3790        let bad_version_file = FixtureFile {
3791            version: FIXTURE_FILE_VERSION + 999,
3792            fixtures: vec![good_fixture.clone()],
3793        };
3794        let good_version_file = FixtureFile {
3795            version: FIXTURE_FILE_VERSION,
3796            fixtures: vec![CorrectnessFixture {
3797                op: OpKind::AddElementwise,
3798                ..good_fixture
3799            }],
3800        };
3801        let dir = std::env::temp_dir()
3802            .join(format!("fuel-judge-fixture-version-{}", std::process::id(),));
3803        let _ = std::fs::create_dir_all(&dir);
3804        let bad_path = dir.join("matmul_f32.json");
3805        let good_path = dir.join("add_f32.json");
3806        std::fs::write(&bad_path, serde_json::to_string(&bad_version_file).unwrap()).unwrap();
3807        std::fs::write(
3808            &good_path,
3809            serde_json::to_string(&good_version_file).unwrap(),
3810        )
3811        .unwrap();
3812
3813        let judge = Judge::with_fixtures_from(&dir).expect("load");
3814        let map = judge.fixtures.as_ref().expect("fixtures populated");
3815        // The bad-version file is silently dropped; the good-version
3816        // file's bucket is still present.
3817        assert!(
3818            !map.contains_key(&(OpKind::MatMul, DType::F32, SizeClass(10))),
3819            "expected mismatched-version file to be skipped",
3820        );
3821        assert!(
3822            map.contains_key(&(OpKind::AddElementwise, DType::F32, SizeClass(10))),
3823            "expected good-version file to be loaded",
3824        );
3825
3826        let _ = std::fs::remove_dir_all(&dir);
3827    }
3828
3829    /// `lookup_fixture` returns `None` for a fixture whose `input_seed`
3830    /// doesn't match the Judge-derived per-cell seed. The caller (run)
3831    /// then falls back to inline consensus.
3832    #[test]
3833    fn lookup_fixture_skips_seed_mismatch() {
3834        use fuel_correctness_fixtures::{CorrectnessFixture, ToleranceBand};
3835
3836        let op = OpKind::AddElementwise;
3837        let dtype = DType::F32;
3838        let elem_count = 1 << 10;
3839        let sc = SizeClass::from_elem_count(elem_count);
3840        let input = fuel_correctness_fixtures::capture::deterministic_f32_input(op, 2 * elem_count);
3841        let input_hash = fuel_correctness_fixtures::capture::hash_f32_input(&input);
3842        // Plant the right hash but the wrong seed — the lookup must
3843        // skip the fixture even though the hash would have matched.
3844        let real_seed = fuel_correctness_fixtures::capture::derive_seed(op, dtype, sc);
3845        let wrong_seed = real_seed.wrapping_add(1);
3846        let fixture = CorrectnessFixture {
3847            op,
3848            dtype,
3849            size_class: sc,
3850            input_seed: wrong_seed,
3851            input_hash,
3852            expected_output: vec![0u8; elem_count * 4],
3853            output_element_count: elem_count,
3854            tolerance: ToleranceBand::F32_DEFAULT,
3855        };
3856        let mut map: HashMap<(OpKind, DType, SizeClass), Vec<CorrectnessFixture>> = HashMap::new();
3857        map.insert((op, dtype, sc), vec![fixture]);
3858        let judge = Judge {
3859            fixtures: Some(map),
3860            ..Judge::default()
3861        };
3862        let got = judge.lookup_fixture(op, dtype, sc, elem_count, 2 * elem_count);
3863        assert!(
3864            got.is_none(),
3865            "fixture with mismatched seed should be skipped (got {got:?})",
3866        );
3867    }
3868
3869    /// `lookup_fixture` returns `None` for a fixture whose `input_hash`
3870    /// doesn't match the regenerated input. The caller falls back to
3871    /// inline consensus.
3872    #[test]
3873    fn lookup_fixture_skips_hash_mismatch() {
3874        use fuel_correctness_fixtures::{CorrectnessFixture, ToleranceBand};
3875
3876        let op = OpKind::AddElementwise;
3877        let dtype = DType::F32;
3878        let elem_count = 1 << 10;
3879        let sc = SizeClass::from_elem_count(elem_count);
3880        let real_seed = fuel_correctness_fixtures::capture::derive_seed(op, dtype, sc);
3881        let input = fuel_correctness_fixtures::capture::deterministic_f32_input(op, 2 * elem_count);
3882        let real_hash = fuel_correctness_fixtures::capture::hash_f32_input(&input);
3883        // Plant the right seed but a wrong hash. The lookup must skip.
3884        let wrong_hash = real_hash.wrapping_add(0xfeed_face);
3885        let fixture = CorrectnessFixture {
3886            op,
3887            dtype,
3888            size_class: sc,
3889            input_seed: real_seed,
3890            input_hash: wrong_hash,
3891            expected_output: vec![0u8; elem_count * 4],
3892            output_element_count: elem_count,
3893            tolerance: ToleranceBand::F32_DEFAULT,
3894        };
3895        let mut map: HashMap<(OpKind, DType, SizeClass), Vec<CorrectnessFixture>> = HashMap::new();
3896        map.insert((op, dtype, sc), vec![fixture]);
3897        let judge = Judge {
3898            fixtures: Some(map),
3899            ..Judge::default()
3900        };
3901        let got = judge.lookup_fixture(op, dtype, sc, elem_count, 2 * elem_count);
3902        assert!(
3903            got.is_none(),
3904            "fixture with drifted input_hash should be skipped (got {got:?})",
3905        );
3906    }
3907
3908    /// `lookup_fixture` accepts a fixture when (op, dtype, size_class) +
3909    /// output shape + seed + input_hash all agree. Triple-agreement
3910    /// contract.
3911    #[test]
3912    fn lookup_fixture_accepts_triple_agreement() {
3913        use fuel_correctness_fixtures::{CorrectnessFixture, ToleranceBand};
3914
3915        let op = OpKind::AddElementwise;
3916        let dtype = DType::F32;
3917        let elem_count = 1 << 10;
3918        let sc = SizeClass::from_elem_count(elem_count);
3919        let seed = fuel_correctness_fixtures::capture::derive_seed(op, dtype, sc);
3920        let input = fuel_correctness_fixtures::capture::deterministic_f32_input(op, 2 * elem_count);
3921        let hash = fuel_correctness_fixtures::capture::hash_f32_input(&input);
3922        let fixture = CorrectnessFixture {
3923            op,
3924            dtype,
3925            size_class: sc,
3926            input_seed: seed,
3927            input_hash: hash,
3928            expected_output: vec![0u8; elem_count * 4],
3929            output_element_count: elem_count,
3930            tolerance: ToleranceBand::F32_DEFAULT,
3931        };
3932        let mut map: HashMap<(OpKind, DType, SizeClass), Vec<CorrectnessFixture>> = HashMap::new();
3933        map.insert((op, dtype, sc), vec![fixture]);
3934        let judge = Judge {
3935            fixtures: Some(map),
3936            ..Judge::default()
3937        };
3938        let got = judge.lookup_fixture(op, dtype, sc, elem_count, 2 * elem_count);
3939        assert!(got.is_some(), "triple-agreement fixture should be returned");
3940    }
3941}