Skip to main content

fuel_cpu_backend/
dyn_impl.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! `DynBackendStorage` and `DynBackendDevice` implementations for the CPU backend.
3//!
4//! `CpuStorage` (defined here) owns raw tensor data as a typed `HostBuffer` and
5//! implements `DynBackendStorage` directly. `CpuBackendDevice` is the stateless
6//! device handle. `CpuBackendStorage` is a backward-compat alias for `CpuStorage`.
7
8use float8::F8E4M3;
9use fuel_backend_contract::dyn_backend::{DynBackendDevice, DynBackendStorage};
10use fuel_cpu_kernels::erf;
11use fuel_ir::conv::{ParamsConv1D, ParamsConv2D, ParamsConvTranspose1D, ParamsConvTranspose2D};
12use fuel_ir::op::{BinaryOp, CmpOp, ReduceOp, UnaryOp};
13use fuel_ir::{
14    CpuStorage as HostBuffer, DType, DeviceLocation, Error, Layout, Result, Scalar, Shape,
15};
16use half::{bf16, f16};
17use num_traits::Float as _;
18use std::any::Any;
19use std::sync::Arc;
20
21use crate::chassis::unary::{quiet_f32, quiet_f64};
22use crate::utils::{Map1, Map1Any, Map2, Map2InPlace, Map2U8, binary_map, unary_map};
23
24// ---------------------------------------------------------------------------
25// CpuStorage — newtype wrapping HostBuffer
26// ---------------------------------------------------------------------------
27
28/// CPU backend storage: owns raw tensor data as a typed `HostBuffer`.
29///
30/// Defined in `fuel-cpu-backend` so the orphan rule allows implementing
31/// `DynBackendStorage` here with full access to CPU kernels.
32#[derive(Debug, Clone)]
33pub struct CpuStorage(pub HostBuffer);
34
35/// Backward-compat alias.
36pub type CpuBackendStorage = CpuStorage;
37
38impl CpuStorage {
39    /// Unwrap the inner `HostBuffer`.
40    pub fn into_inner(self) -> HostBuffer {
41        self.0
42    }
43
44    /// Borrow the inner `HostBuffer`.
45    pub fn inner(&self) -> &HostBuffer {
46        &self.0
47    }
48}
49
50impl From<HostBuffer> for CpuStorage {
51    fn from(s: HostBuffer) -> Self {
52        Self(s)
53    }
54}
55
56impl From<CpuStorage> for HostBuffer {
57    fn from(s: CpuStorage) -> Self {
58        s.0
59    }
60}
61
62impl fuel_backend_contract::backend::HostStorage for CpuStorage {
63    fn as_host_buffer_ref(&self) -> fuel_ir::Result<fuel_ir::HostBufferRef<'_>> {
64        Ok(self.0.as_ref())
65    }
66
67    // Override the default to hand out the existing `Vec<T>` without
68    // a copy — we own the buffer outright.
69    fn into_host_buffer(self) -> fuel_ir::Result<fuel_ir::HostBuffer> {
70        Ok(self.0)
71    }
72}
73
74// ---------------------------------------------------------------------------
75// CpuBackendDevice — newtype wrapper
76// ---------------------------------------------------------------------------
77
78/// CPU device handle (stateless) implementing [`DynBackendDevice`].
79#[derive(Debug, Clone, Copy)]
80pub struct CpuBackendDevice;
81
82impl fuel_backend_contract::backend::BackendRuntime for CpuBackendDevice {
83    /// System-wide available RAM via the OS query in
84    /// [`crate::system_memory`]. `None` on platforms without an
85    /// implemented query (macOS + unknown). The signal reflects
86    /// total OS state, not Fuel's per-process usage — selectors
87    /// should weight it accordingly.
88    fn available_bytes(&self) -> Option<u64> {
89        crate::system_memory::available_bytes()
90    }
91
92    /// Total physical RAM via the OS query in
93    /// [`crate::system_memory`]. `None` on platforms without an
94    /// implemented query.
95    fn total_bytes(&self) -> Option<u64> {
96        crate::system_memory::total_bytes()
97    }
98}
99
100// ---------------------------------------------------------------------------
101// Helpers
102// ---------------------------------------------------------------------------
103
104/// Downcast a `&dyn DynBackendStorage` to `&CpuStorage`.
105fn downcast(s: &dyn DynBackendStorage) -> Result<&CpuStorage> {
106    s.as_any().downcast_ref::<CpuStorage>().ok_or_else(|| {
107        Error::DeviceMismatchBinaryOp {
108            lhs: DeviceLocation::Cpu,
109            rhs: s.device_dyn().location_dyn(),
110            op: "dyn_backend",
111        }
112        .bt()
113    })
114}
115
116/// Downcast a `&mut dyn DynBackendStorage` to `&mut CpuStorage`.
117fn downcast_mut(s: &mut dyn DynBackendStorage) -> Result<&mut CpuStorage> {
118    let loc = s.device_dyn().location_dyn();
119    s.as_any_mut().downcast_mut::<CpuStorage>().ok_or_else(|| {
120        Error::DeviceMismatchBinaryOp {
121            lhs: DeviceLocation::Cpu,
122            rhs: loc,
123            op: "dyn_backend",
124        }
125        .bt()
126    })
127}
128
129// ---------------------------------------------------------------------------
130// Unary / binary dispatch helpers
131// ---------------------------------------------------------------------------
132
133/// Helper: apply a per-element unary operation selected by [`UnaryOp`].
134fn cpu_unary_op(s: &HostBuffer, layout: &Layout, op: UnaryOp) -> Result<HostBuffer> {
135    use UnaryOp::*;
136
137    // Most unary ops only make sense on floating types; for integer types they
138    // either return the identity or should error.  We first handle the full
139    // float-path ops generically, then special-case per variant when needed.
140
141    match op {
142        Exp => float_unary(
143            s,
144            layout,
145            |v: f32| quiet_f32(v.exp()),
146            |v: f64| quiet_f64(v.exp()),
147        ),
148        Log => float_unary(
149            s,
150            layout,
151            |v: f32| quiet_f32(v.ln()),
152            |v: f64| quiet_f64(v.ln()),
153        ),
154        Sin => float_unary(
155            s,
156            layout,
157            |v: f32| quiet_f32(v.sin()),
158            |v: f64| quiet_f64(v.sin()),
159        ),
160        Cos => float_unary(
161            s,
162            layout,
163            |v: f32| quiet_f32(v.cos()),
164            |v: f64| quiet_f64(v.cos()),
165        ),
166        Tanh => float_unary(
167            s,
168            layout,
169            |v: f32| quiet_f32(v.tanh()),
170            |v: f64| quiet_f64(v.tanh()),
171        ),
172        Neg => all_unary_neg(s, layout),
173        Recip => float_unary(s, layout, |v: f32| v.recip(), |v: f64| v.recip()),
174        Sqr => all_unary_sqr(s, layout),
175        Sqrt => float_unary(
176            s,
177            layout,
178            |v: f32| quiet_f32(v.sqrt()),
179            |v: f64| quiet_f64(v.sqrt()),
180        ),
181        Abs => all_unary_abs(s, layout),
182        Relu => all_unary_relu(s, layout),
183        Floor => float_unary_identity_int(
184            s,
185            layout,
186            |v: f32| quiet_f32(v.floor()),
187            |v: f64| quiet_f64(v.floor()),
188        ),
189        Ceil => float_unary_identity_int(
190            s,
191            layout,
192            |v: f32| quiet_f32(v.ceil()),
193            |v: f64| quiet_f64(v.ceil()),
194        ),
195        Round => float_unary_identity_int(
196            s,
197            layout,
198            |v: f32| quiet_f32(v.round()),
199            |v: f64| quiet_f64(v.round()),
200        ),
201        Sign => all_unary_sign(s, layout),
202        GeluTanh => float_unary(s, layout, gelu_f32, gelu_f64),
203        Gelu => float_unary(s, layout, gelu_erf_f32, gelu_erf_f64),
204        Erf => float_unary(s, layout, erf::erf_f32, erf::erf_f64),
205        Silu => float_unary(s, layout, silu_f32, silu_f64),
206    }
207}
208
209#[allow(clippy::excessive_precision)]
210fn gelu_f32(v: f32) -> f32 {
211    0.5 * v
212        * (1.0 + f32::tanh(0.79788456080286535587989211986876373 * v * (1.0 + 0.044715 * v * v)))
213}
214
215#[allow(clippy::excessive_precision)]
216fn gelu_f64(v: f64) -> f64 {
217    0.5 * v
218        * (1.0 + f64::tanh(0.79788456080286535587989211986876373 * v * (1.0 + 0.044715 * v * v)))
219}
220
221fn gelu_erf_f32(v: f32) -> f32 {
222    (erf::erf_f32(v * std::f32::consts::FRAC_1_SQRT_2) + 1.) * 0.5 * v
223}
224
225fn gelu_erf_f64(v: f64) -> f64 {
226    (erf::erf_f64(v * std::f64::consts::FRAC_1_SQRT_2) + 1.) * 0.5 * v
227}
228
229fn silu_f32(v: f32) -> f32 {
230    v / (1.0 + (-v).exp())
231}
232
233fn silu_f64(v: f64) -> f64 {
234    v / (1.0 + (-v).exp())
235}
236
237/// Apply a float-only unary op.  For half/f8 types, promote to f32/f64 then demote.
238/// Integer types return an error.
239fn float_unary(
240    s: &HostBuffer,
241    layout: &Layout,
242    f32_fn: impl Fn(f32) -> f32 + Copy,
243    f64_fn: impl Fn(f64) -> f64 + Copy,
244) -> Result<HostBuffer> {
245    match s {
246        HostBuffer::BF16(data) => {
247            let out = unary_map(data, layout, |v: bf16| bf16::from_f32(f32_fn(v.to_f32())));
248            Ok(HostBuffer::BF16(out))
249        }
250        HostBuffer::F16(data) => {
251            let out = unary_map(data, layout, |v: f16| f16::from_f32(f32_fn(v.to_f32())));
252            Ok(HostBuffer::F16(out))
253        }
254        HostBuffer::F32(data) => {
255            let out = unary_map(data, layout, f32_fn);
256            Ok(HostBuffer::F32(out))
257        }
258        HostBuffer::F64(data) => {
259            let out = unary_map(data, layout, f64_fn);
260            Ok(HostBuffer::F64(out))
261        }
262        HostBuffer::F8E4M3(data) => {
263            let out = unary_map(data, layout, |v: F8E4M3| {
264                F8E4M3::from_f32(f32_fn(v.to_f32()))
265            });
266            Ok(HostBuffer::F8E4M3(out))
267        }
268        other => Err(Error::UnsupportedDTypeForOp(other.dtype(), "unary_op").bt()),
269    }
270}
271
272/// Apply a float unary op; integer types pass through as identity.
273fn float_unary_identity_int(
274    s: &HostBuffer,
275    layout: &Layout,
276    f32_fn: impl Fn(f32) -> f32 + Copy,
277    f64_fn: impl Fn(f64) -> f64 + Copy,
278) -> Result<HostBuffer> {
279    match s {
280        HostBuffer::U8(d) => Ok(HostBuffer::U8(unary_map(d, layout, |v| v))),
281        HostBuffer::U32(d) => Ok(HostBuffer::U32(unary_map(d, layout, |v| v))),
282        HostBuffer::I16(d) => Ok(HostBuffer::I16(unary_map(d, layout, |v| v))),
283        HostBuffer::I32(d) => Ok(HostBuffer::I32(unary_map(d, layout, |v| v))),
284        HostBuffer::I64(d) => Ok(HostBuffer::I64(unary_map(d, layout, |v| v))),
285        _ => float_unary(s, layout, f32_fn, f64_fn),
286    }
287}
288
289fn all_unary_neg(s: &HostBuffer, layout: &Layout) -> Result<HostBuffer> {
290    match s {
291        HostBuffer::BF16(d) => Ok(HostBuffer::BF16(unary_map(d, layout, |v: bf16| -v))),
292        HostBuffer::F16(d) => Ok(HostBuffer::F16(unary_map(d, layout, |v: f16| -v))),
293        HostBuffer::F32(d) => Ok(HostBuffer::F32(unary_map(d, layout, |v: f32| -v))),
294        HostBuffer::F64(d) => Ok(HostBuffer::F64(unary_map(d, layout, |v: f64| -v))),
295        // KISS-Ops §6.4-0005: negating the signed minimum wraps to itself
296        // (INT_MIN has no positive counterpart in two's complement). `-v`
297        // panics under overflow-checks and is UB-adjacent without them;
298        // `wrapping_neg` gives the spec-mandated INT_MIN result unconditionally.
299        HostBuffer::I16(d) => Ok(HostBuffer::I16(unary_map(d, layout, |v: i16| {
300            v.wrapping_neg()
301        }))),
302        HostBuffer::I32(d) => Ok(HostBuffer::I32(unary_map(d, layout, |v: i32| {
303            v.wrapping_neg()
304        }))),
305        HostBuffer::I64(d) => Ok(HostBuffer::I64(unary_map(d, layout, |v: i64| {
306            v.wrapping_neg()
307        }))),
308        HostBuffer::F8E4M3(d) => Ok(HostBuffer::F8E4M3(unary_map(d, layout, |v: F8E4M3| -v))),
309        other => Err(Error::UnsupportedDTypeForOp(other.dtype(), "neg").bt()),
310    }
311}
312
313fn all_unary_sqr(s: &HostBuffer, layout: &Layout) -> Result<HostBuffer> {
314    match s {
315        HostBuffer::BF16(d) => Ok(HostBuffer::BF16(unary_map(d, layout, |v: bf16| v * v))),
316        HostBuffer::F16(d) => Ok(HostBuffer::F16(unary_map(d, layout, |v: f16| v * v))),
317        HostBuffer::F32(d) => Ok(HostBuffer::F32(unary_map(d, layout, |v: f32| v * v))),
318        HostBuffer::F64(d) => Ok(HostBuffer::F64(unary_map(d, layout, |v: f64| v * v))),
319        HostBuffer::U8(d) => Ok(HostBuffer::U8(unary_map(d, layout, |v: u8| v * v))),
320        HostBuffer::U32(d) => Ok(HostBuffer::U32(unary_map(d, layout, |v: u32| v * v))),
321        HostBuffer::I16(d) => Ok(HostBuffer::I16(unary_map(d, layout, |v: i16| v * v))),
322        HostBuffer::I32(d) => Ok(HostBuffer::I32(unary_map(d, layout, |v: i32| v * v))),
323        HostBuffer::I64(d) => Ok(HostBuffer::I64(unary_map(d, layout, |v: i64| v * v))),
324        HostBuffer::F8E4M3(d) => Ok(HostBuffer::F8E4M3(unary_map(d, layout, |v: F8E4M3| v * v))),
325        other => Err(Error::UnsupportedDTypeForOp(other.dtype(), "sqr").bt()),
326    }
327}
328
329fn all_unary_abs(s: &HostBuffer, layout: &Layout) -> Result<HostBuffer> {
330    match s {
331        // ⚠️ RAW BITS, NOT `v.abs()` -- and the reason is subtle enough to state.
332        // `half` ships TWO `abs` for each narrow type under the SAME NAME, in two
333        // traits, with OPPOSITE conformance:
334        //     num_traits::FloatCore::abs  ->  from_bits(to_bits() & 0x7FFF)   raw-bit
335        //     num_traits::Float::abs      ->  from_f32(to_f32().abs())        PROMOTES
336        // There is no inherent `abs` on `bf16`/`f16`, so `v.abs()` resolves purely
337        // by WHICH TRAIT IS IN SCOPE. This file has `use num_traits::Float as _`
338        // (line 17) and NOT `FloatCore`, so `v.abs()` bound to the PROMOTING one --
339        // which quiets a signalling NaN on both conversion legs and violates
340        // KISS-OPS-6.16-0009 (a moved operand keeps its bits exactly).
341        //
342        // Writing the bits is the only form that cannot be silently re-resolved by
343        // someone changing an import at the top of the file.
344        HostBuffer::BF16(d) => Ok(HostBuffer::BF16(unary_map(d, layout, |v: bf16| {
345            bf16::from_bits(v.to_bits() & 0x7FFF)
346        }))),
347        HostBuffer::F16(d) => Ok(HostBuffer::F16(unary_map(d, layout, |v: f16| {
348            f16::from_bits(v.to_bits() & 0x7FFF)
349        }))),
350        HostBuffer::F32(d) => Ok(HostBuffer::F32(unary_map(d, layout, |v: f32| v.abs()))),
351        HostBuffer::F64(d) => Ok(HostBuffer::F64(unary_map(d, layout, |v: f64| v.abs()))),
352        HostBuffer::U8(d) => Ok(HostBuffer::U8(unary_map(d, layout, |v: u8| v))),
353        HostBuffer::U32(d) => Ok(HostBuffer::U32(unary_map(d, layout, |v: u32| v))),
354        // KISS-Ops §6.4-0005: same INT_MIN rule as `all_unary_neg` above --
355        // `v.abs()` panics under overflow-checks on INT_MIN; `wrapping_abs`
356        // gives the spec-mandated INT_MIN result unconditionally.
357        HostBuffer::I16(d) => Ok(HostBuffer::I16(unary_map(d, layout, |v: i16| {
358            v.wrapping_abs()
359        }))),
360        HostBuffer::I32(d) => Ok(HostBuffer::I32(unary_map(d, layout, |v: i32| {
361            v.wrapping_abs()
362        }))),
363        HostBuffer::I64(d) => Ok(HostBuffer::I64(unary_map(d, layout, |v: i64| {
364            v.wrapping_abs()
365        }))),
366        // `float8` has the IDENTICAL two-trait split (`num_traits.rs` :960 raw-bit
367        // vs :1125 promoting), so this call also binds to the promoting one -- but
368        // it is NOT the same defect and is deliberately left alone. E4M3 encodes
369        // exactly TWO NaN bit patterns (`is_nan` is `self.0 == 0x7F || == 0xFF`):
370        // no quiet bit, no payload. Both forms therefore yield `0x7F` and there is
371        // nothing to lose. ⚠️ That reasoning is about E4M3's ENCODING, not about
372        // this call being safe -- a narrow type WITH a quiet bit added to this
373        // match would need the raw-bit form like the two arms above. GAP(GAP-274)
374        HostBuffer::F8E4M3(d) => Ok(HostBuffer::F8E4M3(unary_map(d, layout, |v: F8E4M3| {
375            v.abs()
376        }))),
377        other => Err(Error::UnsupportedDTypeForOp(other.dtype(), "abs").bt()),
378    }
379}
380
381/// NaN-propagating (torch parity), pinned 2026-07-08
382/// (`docs/architecture/10-decisions-log.md`). Float branches check
383/// `is_nan()` before scrubbing to 0 so a NaN input passes through
384/// unchanged (payload-preserving); integer branches are untouched (no
385/// NaN concept).
386fn all_unary_relu(s: &HostBuffer, layout: &Layout) -> Result<HostBuffer> {
387    match s {
388        HostBuffer::BF16(d) => Ok(HostBuffer::BF16(unary_map(d, layout, |v: bf16| {
389            if v.is_nan() { v } else { v.max(bf16::ZERO) }
390        }))),
391        HostBuffer::F16(d) => Ok(HostBuffer::F16(unary_map(d, layout, |v: f16| {
392            if v.is_nan() { v } else { v.max(f16::ZERO) }
393        }))),
394        HostBuffer::F32(d) => Ok(HostBuffer::F32(unary_map(d, layout, |v: f32| {
395            if v.is_nan() { v } else { v.max(0.0) }
396        }))),
397        HostBuffer::F64(d) => Ok(HostBuffer::F64(unary_map(d, layout, |v: f64| {
398            if v.is_nan() { v } else { v.max(0.0) }
399        }))),
400        HostBuffer::U8(d) => Ok(HostBuffer::U8(unary_map(d, layout, |v: u8| v))),
401        HostBuffer::U32(d) => Ok(HostBuffer::U32(unary_map(d, layout, |v: u32| v))),
402        HostBuffer::I16(d) => Ok(HostBuffer::I16(unary_map(d, layout, |v: i16| v.max(0)))),
403        HostBuffer::I32(d) => Ok(HostBuffer::I32(unary_map(d, layout, |v: i32| v.max(0)))),
404        HostBuffer::I64(d) => Ok(HostBuffer::I64(unary_map(d, layout, |v: i64| v.max(0)))),
405        HostBuffer::F8E4M3(d) => Ok(HostBuffer::F8E4M3(unary_map(d, layout, |v: F8E4M3| {
406            if v.is_nan() { v } else { v.max(F8E4M3::ZERO) }
407        }))),
408        other => Err(Error::UnsupportedDTypeForOp(other.dtype(), "relu").bt()),
409    }
410}
411
412fn all_unary_sign(s: &HostBuffer, layout: &Layout) -> Result<HostBuffer> {
413    match s {
414        HostBuffer::BF16(d) => Ok(HostBuffer::BF16(unary_map(d, layout, |v: bf16| {
415            bf16::from((v > bf16::ZERO) as i8) - bf16::from((v < bf16::ZERO) as i8)
416        }))),
417        HostBuffer::F16(d) => Ok(HostBuffer::F16(unary_map(d, layout, |v: f16| {
418            f16::from((v > f16::ZERO) as i8) - f16::from((v < f16::ZERO) as i8)
419        }))),
420        HostBuffer::F32(d) => Ok(HostBuffer::F32(unary_map(d, layout, |v: f32| {
421            f32::from(v > 0.) - f32::from(v < 0.)
422        }))),
423        HostBuffer::F64(d) => Ok(HostBuffer::F64(unary_map(d, layout, |v: f64| {
424            f64::from(v > 0.) - f64::from(v < 0.)
425        }))),
426        HostBuffer::I16(d) => Ok(HostBuffer::I16(unary_map(d, layout, |v: i16| v.signum()))),
427        HostBuffer::I32(d) => Ok(HostBuffer::I32(unary_map(d, layout, |v: i32| v.signum()))),
428        HostBuffer::I64(d) => Ok(HostBuffer::I64(unary_map(d, layout, |v: i64| v.signum()))),
429        HostBuffer::F8E4M3(d) => Ok(HostBuffer::F8E4M3(unary_map(d, layout, |v: F8E4M3| {
430            let f = v.to_f32();
431            F8E4M3::from_f32(f32::from(f > 0.) - f32::from(f < 0.))
432        }))),
433        other => Err(Error::UnsupportedDTypeForOp(other.dtype(), "sign").bt()),
434    }
435}
436
437/// Apply a binary op selected by [`BinaryOp`].
438fn cpu_binary_op(
439    lhs: &HostBuffer,
440    rhs: &HostBuffer,
441    lhs_l: &Layout,
442    rhs_l: &Layout,
443    op: BinaryOp,
444) -> Result<HostBuffer> {
445    use BinaryOp::*;
446    match op {
447        // KISS-OPS-6.2-0002: integer Add/Sub/Mul wrap mod 2^n. Each gets its
448        // own `wrapping_*` i64 closure (`fi`) alongside the existing float
449        // closure (`f`) -- see `all_binary`'s `G` bound for why a shared
450        // float-only closure cannot express this.
451        Add => all_binary(
452            lhs,
453            rhs,
454            lhs_l,
455            rhs_l,
456            |a, b| a + b,
457            |a: i64, b: i64| a.wrapping_add(b),
458            "add",
459        ),
460        Sub => all_binary(
461            lhs,
462            rhs,
463            lhs_l,
464            rhs_l,
465            |a, b| a - b,
466            |a: i64, b: i64| a.wrapping_sub(b),
467            "sub",
468        ),
469        Mul => all_binary(
470            lhs,
471            rhs,
472            lhs_l,
473            rhs_l,
474            |a, b| a * b,
475            |a: i64, b: i64| a.wrapping_mul(b),
476            "mul",
477        ),
478        // Div is unchanged by this fix (KISS's finding was scoped to
479        // Add/Sub/Mul) -- the integer closure reproduces exactly the prior
480        // f64-mediated division (including its float->int saturating
481        // behavior on divide-by-zero), not wrapping div, since Rust's own
482        // integer division panics on divide-by-zero and changing that
483        // behavior is a separate, unasked-for decision.
484        Div => all_binary(
485            lhs,
486            rhs,
487            lhs_l,
488            rhs_l,
489            |a, b| a / b,
490            |a: i64, b: i64| (a as f64 / b as f64) as i64,
491            "div",
492        ),
493        // NaN-propagating (torch parity), pinned 2026-07-08
494        // (`docs/architecture/10-decisions-log.md`). `all_binary` computes
495        // in f64 for every dtype (including integers, which never produce
496        // NaN), so `.is_nan()` guards are safe generically here — replaces
497        // the prior asymmetric scrub (`a < b` is false whenever either
498        // operand is NaN, so the old code silently propagated only when
499        // `a` was NaN and scrubbed when only `b` was).
500        Maximum => all_binary(
501            lhs,
502            rhs,
503            lhs_l,
504            rhs_l,
505            |a, b| {
506                // NaN-propagating maximum (torch semantics): a NaN operand
507                // wins; otherwise the larger. `b` is returned both when it is
508                // NaN and when it exceeds `a` -- distinct reasons, same result.
509                if a.is_nan() {
510                    a
511                } else if b.is_nan() || a < b {
512                    b
513                } else {
514                    a
515                }
516            },
517            // Integers never produce NaN, so the native i64 comparison is
518            // exactly the float path's non-NaN branch, with no overflow
519            // concern (max/min never leaves the input range).
520            |a: i64, b: i64| a.max(b),
521            "maximum",
522        ),
523        Minimum => all_binary(
524            lhs,
525            rhs,
526            lhs_l,
527            rhs_l,
528            |a, b| {
529                // NaN-propagating minimum (torch semantics): a NaN operand
530                // wins; otherwise the smaller. `b` is returned both when it is
531                // NaN and when it is below `a` -- distinct reasons, same result.
532                if a.is_nan() {
533                    a
534                } else if b.is_nan() || a > b {
535                    b
536                } else {
537                    a
538                }
539            },
540            |a: i64, b: i64| a.min(b),
541            "minimum",
542        ),
543    }
544}
545
546fn all_binary<F, G>(
547    lhs: &HostBuffer,
548    rhs: &HostBuffer,
549    lhs_l: &Layout,
550    rhs_l: &Layout,
551    f: F,
552    fi: G,
553    op_name: &'static str,
554) -> Result<HostBuffer>
555where
556    F: Fn(f64, f64) -> f64 + Copy,
557    // KISS-OPS-6.2-0002: integer arithmetic wraps mod 2^n. The float path
558    // `f` cannot express this (a float intermediate saturates on the final
559    // float->int cast rather than wrapping, and loses precision above 2^53
560    // for i64), so every integer dtype routes through its OWN i64-native
561    // closure instead. `fi` computes in i64 (wide enough to hold any U8/
562    // U32/I16/I32 value exactly, and is I64's own native type), and the
563    // final `as <narrower>` is an INT-to-int cast, which Rust truncates/
564    // wraps rather than saturates -- unlike the float-to-int cast this
565    // replaces.
566    G: Fn(i64, i64) -> i64 + Copy,
567{
568    // Dispatch on matching dtype pairs.  Uses binary_map from utils.
569    macro_rules! dispatch_pair {
570        ($lhs_v:ident, $rhs_v:ident, $variant:ident, $conv_fn:expr, $result_fn:expr) => {{
571            let out = binary_map(lhs_l, rhs_l, $lhs_v, $rhs_v, |a, b| {
572                let result = f($conv_fn(a), $conv_fn(b));
573                $result_fn(result)
574            });
575            Ok(HostBuffer::$variant(out))
576        }};
577    }
578
579    match (lhs, rhs) {
580        (HostBuffer::BF16(a), HostBuffer::BF16(b)) => {
581            dispatch_pair!(a, b, BF16, |v: bf16| v.to_f64(), |v: f64| bf16::from_f64(v))
582        }
583        (HostBuffer::F16(a), HostBuffer::F16(b)) => {
584            dispatch_pair!(a, b, F16, |v: f16| v.to_f64(), |v: f64| f16::from_f64(v))
585        }
586        (HostBuffer::F32(a), HostBuffer::F32(b)) => {
587            let out = binary_map(lhs_l, rhs_l, a, b, |a, b| f(a as f64, b as f64) as f32);
588            Ok(HostBuffer::F32(out))
589        }
590        (HostBuffer::F64(a), HostBuffer::F64(b)) => {
591            let out = binary_map(lhs_l, rhs_l, a, b, &f);
592            Ok(HostBuffer::F64(out))
593        }
594        (HostBuffer::U8(a), HostBuffer::U8(b)) => {
595            let out = binary_map(lhs_l, rhs_l, a, b, |a, b| fi(a as i64, b as i64) as u8);
596            Ok(HostBuffer::U8(out))
597        }
598        (HostBuffer::U32(a), HostBuffer::U32(b)) => {
599            let out = binary_map(lhs_l, rhs_l, a, b, |a, b| fi(a as i64, b as i64) as u32);
600            Ok(HostBuffer::U32(out))
601        }
602        (HostBuffer::I16(a), HostBuffer::I16(b)) => {
603            let out = binary_map(lhs_l, rhs_l, a, b, |a, b| fi(a as i64, b as i64) as i16);
604            Ok(HostBuffer::I16(out))
605        }
606        (HostBuffer::I32(a), HostBuffer::I32(b)) => {
607            let out = binary_map(lhs_l, rhs_l, a, b, |a, b| fi(a as i64, b as i64) as i32);
608            Ok(HostBuffer::I32(out))
609        }
610        (HostBuffer::I64(a), HostBuffer::I64(b)) => {
611            let out = binary_map(lhs_l, rhs_l, a, b, fi);
612            Ok(HostBuffer::I64(out))
613        }
614        (HostBuffer::F8E4M3(a), HostBuffer::F8E4M3(b)) => {
615            dispatch_pair!(a, b, F8E4M3, |v: F8E4M3| v.to_f64(), |v: f64| {
616                F8E4M3::from_f64(v)
617            })
618        }
619        _ => Err(Error::DTypeMismatchBinaryOp {
620            lhs: lhs.dtype(),
621            rhs: rhs.dtype(),
622            op: op_name,
623        }
624        .bt()),
625    }
626}
627
628// ---------------------------------------------------------------------------
629// Copy helpers (same as in fuel-core/src/cpu_backend/mod.rs)
630// ---------------------------------------------------------------------------
631
632fn copy2d_<T: Copy>(
633    src: &[T],
634    dst: &mut [T],
635    d1: usize,
636    d2: usize,
637    src_stride1: usize,
638    dst_stride1: usize,
639    src_offset: usize,
640    dst_offset: usize,
641) {
642    for i1 in 0..d1 {
643        let dst_idx = i1 * dst_stride1 + dst_offset;
644        let src_idx = i1 * src_stride1 + src_offset;
645        dst[dst_idx..dst_idx + d2].copy_from_slice(&src[src_idx..src_idx + d2]);
646    }
647}
648
649fn copy_strided_src_<T: Copy>(src: &[T], dst: &mut [T], dst_offset: usize, src_l: &Layout) {
650    match src_l.strided_blocks() {
651        fuel_ir::StridedBlocks::SingleBlock { start_offset, len } => {
652            let to_copy = (dst.len() - dst_offset).min(len);
653            dst[dst_offset..dst_offset + to_copy]
654                .copy_from_slice(&src[start_offset..start_offset + to_copy])
655        }
656        fuel_ir::StridedBlocks::MultipleBlocks {
657            block_start_index,
658            block_len: 1,
659        } => {
660            for (dst_index, src_index) in block_start_index.enumerate() {
661                let dst_index = dst_index + dst_offset;
662                if dst_index >= dst.len() {
663                    break;
664                }
665                dst[dst_index] = src[src_index];
666            }
667        }
668        fuel_ir::StridedBlocks::MultipleBlocks {
669            block_start_index,
670            block_len,
671        } => {
672            let mut dst_index = dst_offset;
673            for src_index in block_start_index {
674                let next_dst_index = dst_index + block_len;
675                if dst_index >= dst.len() {
676                    break;
677                }
678                let to_copy = usize::min(block_len, dst.len() - dst_index);
679                dst[dst_index..dst_index + to_copy]
680                    .copy_from_slice(&src[src_index..src_index + to_copy]);
681                dst_index = next_dst_index;
682            }
683        }
684    }
685}
686
687fn elu<T: num_traits::Float>(v: T, alpha: T) -> T {
688    if v.is_sign_positive() {
689        v
690    } else {
691        (v.exp() - T::one()) * alpha
692    }
693}
694
695// ---------------------------------------------------------------------------
696// impl DynBackendStorage for CpuStorage
697// ---------------------------------------------------------------------------
698
699impl DynBackendStorage for CpuStorage {
700    fn try_clone_dyn(&self, _layout: &Layout) -> Result<Box<dyn DynBackendStorage>> {
701        Ok(Box::new(self.clone()))
702    }
703
704    fn dtype_dyn(&self) -> DType {
705        self.0.dtype()
706    }
707
708    fn device_dyn(&self) -> &dyn DynBackendDevice {
709        // We can return a static reference because CpuBackendDevice is stateless.
710        &CpuBackendDevice
711    }
712
713    fn device_arc_dyn(&self) -> Arc<dyn DynBackendDevice> {
714        Arc::new(CpuBackendDevice)
715    }
716
717    fn to_host_buffer_dyn(&self) -> Result<HostBuffer> {
718        Ok(self.0.clone())
719    }
720
721    fn affine_dyn(
722        &self,
723        layout: &Layout,
724        mul: f64,
725        add: f64,
726    ) -> Result<Box<dyn DynBackendStorage>> {
727        let result = Map1::map(&crate::ops::Affine(mul, add), &self.0, layout)?;
728        Ok(Box::new(CpuStorage(result)))
729    }
730
731    fn powf_dyn(&self, layout: &Layout, e: f64) -> Result<Box<dyn DynBackendStorage>> {
732        use num_traits::Float;
733        let result = match &self.0 {
734            HostBuffer::BF16(d) => {
735                HostBuffer::BF16(unary_map(d, layout, |v: bf16| v.powf(bf16::from_f64(e))))
736            }
737            HostBuffer::F16(d) => {
738                HostBuffer::F16(unary_map(d, layout, |v: f16| v.powf(f16::from_f64(e))))
739            }
740            HostBuffer::F32(d) => {
741                HostBuffer::F32(unary_map(d, layout, |v: f32| quiet_f32(v.powf(e as f32))))
742            }
743            HostBuffer::F64(d) => {
744                HostBuffer::F64(unary_map(d, layout, |v: f64| quiet_f64(v.powf(e))))
745            }
746            HostBuffer::F8E4M3(d) => HostBuffer::F8E4M3(unary_map(d, layout, |v: F8E4M3| {
747                v.powf(F8E4M3::from_f64(e))
748            })),
749            other => return Err(Error::UnsupportedDTypeForOp(other.dtype(), "powf").bt()),
750        };
751        Ok(Box::new(CpuStorage(result)))
752    }
753
754    fn elu_dyn(&self, layout: &Layout, alpha: f64) -> Result<Box<dyn DynBackendStorage>> {
755        let result = match &self.0 {
756            HostBuffer::BF16(d) => {
757                HostBuffer::BF16(unary_map(d, layout, |v| elu(v, bf16::from_f64(alpha))))
758            }
759            HostBuffer::F16(d) => {
760                HostBuffer::F16(unary_map(d, layout, |v| elu(v, f16::from_f64(alpha))))
761            }
762            HostBuffer::F32(d) => HostBuffer::F32(unary_map(d, layout, |v| elu(v, alpha as f32))),
763            HostBuffer::F64(d) => HostBuffer::F64(unary_map(d, layout, |v| elu(v, alpha))),
764            HostBuffer::F8E4M3(d) => {
765                HostBuffer::F8E4M3(unary_map(d, layout, |v| elu(v, F8E4M3::from_f64(alpha))))
766            }
767            other => return Err(Error::UnsupportedDTypeForOp(other.dtype(), "elu").bt()),
768        };
769        Ok(Box::new(CpuStorage(result)))
770    }
771
772    fn reduce_op_dyn(
773        &self,
774        op: ReduceOp,
775        layout: &Layout,
776        axes: &[usize],
777    ) -> Result<Box<dyn DynBackendStorage>> {
778        let result = match op {
779            ReduceOp::Sum => {
780                let src_dims = layout.dims();
781                let mut dst_dims = src_dims.to_vec();
782                for &dim in axes.iter() {
783                    dst_dims[dim] = 1;
784                }
785                let dst_shape = Shape::from(dst_dims);
786                let mut reduce_dims = axes.to_vec();
787                reduce_dims.sort();
788                let reduce_dims_and_stride: Vec<_> = reduce_dims
789                    .iter()
790                    .map(|&d| (src_dims[d], src_dims[d + 1..].iter().product::<usize>()))
791                    .collect();
792                Map1::map(
793                    &crate::ops::ReduceSum {
794                        dst_shape: &dst_shape,
795                        reduce_dims: &reduce_dims,
796                        reduce_dims_and_stride,
797                    },
798                    &self.0,
799                    layout,
800                )?
801            }
802            ReduceOp::Min | ReduceOp::ArgMin | ReduceOp::Max | ReduceOp::ArgMax => {
803                let reduce_dim_index = match axes {
804                    [dim] => *dim,
805                    _ => {
806                        let op_name = match op {
807                            ReduceOp::Min => "min",
808                            ReduceOp::ArgMin => "argmin",
809                            ReduceOp::Max => "max",
810                            ReduceOp::ArgMax => "argmax",
811                            _ => unreachable!(),
812                        };
813                        return Err(Error::OnlySingleDimension {
814                            op: op_name,
815                            dims: axes.to_vec(),
816                        }
817                        .bt());
818                    }
819                };
820                let (use_min, return_index) = match op {
821                    ReduceOp::Min => (true, false),
822                    ReduceOp::ArgMin => (true, true),
823                    ReduceOp::Max => (false, false),
824                    ReduceOp::ArgMax => (false, true),
825                    _ => unreachable!(),
826                };
827                Map1Any::map(
828                    &crate::ops::ReduceIndex {
829                        reduce_dim_index,
830                        use_min,
831                        return_index,
832                    },
833                    &self.0,
834                    layout,
835                )?
836            }
837        };
838        Ok(Box::new(CpuStorage(result)))
839    }
840
841    fn cmp_dyn(
842        &self,
843        op: CmpOp,
844        rhs: &dyn DynBackendStorage,
845        lhs_layout: &Layout,
846        rhs_layout: &Layout,
847    ) -> Result<Box<dyn DynBackendStorage>> {
848        let rhs = downcast(rhs)?;
849        let result = Map2U8::map(
850            &crate::ops::Cmp(op),
851            &self.0,
852            lhs_layout,
853            &rhs.0,
854            rhs_layout,
855        )?;
856        Ok(Box::new(CpuStorage(result)))
857    }
858
859    fn to_dtype_dyn(&self, layout: &Layout, dtype: DType) -> Result<Box<dyn DynBackendStorage>> {
860        // Delegate to the existing to_dtype machinery.
861        // This is one of the complex methods with O(dtypes^2) match arms.
862        // For now, go through CPU storage → convert via unary_map.
863        //
864        // We replicate the approach from fuel-core's BackendStorage::to_dtype:
865        // match (self.dtype(), target) and use unary_map for each conversion.
866        let result = cpu_to_dtype(&self.0, layout, dtype)?;
867        Ok(Box::new(CpuStorage(result)))
868    }
869
870    fn unary_op_dyn(&self, layout: &Layout, op: UnaryOp) -> Result<Box<dyn DynBackendStorage>> {
871        let result = cpu_unary_op(&self.0, layout, op)?;
872        Ok(Box::new(CpuStorage(result)))
873    }
874
875    fn binary_op_dyn(
876        &self,
877        rhs: &dyn DynBackendStorage,
878        lhs_layout: &Layout,
879        rhs_layout: &Layout,
880        op: BinaryOp,
881    ) -> Result<Box<dyn DynBackendStorage>> {
882        let rhs = downcast(rhs)?;
883        let result = cpu_binary_op(&self.0, &rhs.0, lhs_layout, rhs_layout, op)?;
884        Ok(Box::new(CpuStorage(result)))
885    }
886
887    fn where_cond_dyn(
888        &self,
889        cond_layout: &Layout,
890        on_true: &dyn DynBackendStorage,
891        on_true_layout: &Layout,
892        on_false: &dyn DynBackendStorage,
893        on_false_layout: &Layout,
894    ) -> Result<Box<dyn DynBackendStorage>> {
895        let t = downcast(on_true)?;
896        let f = downcast(on_false)?;
897        let result = match &self.0 {
898            HostBuffer::U8(pred) => Map2::map(
899                &crate::ops::WCond(pred, cond_layout),
900                &t.0,
901                on_true_layout,
902                &f.0,
903                on_false_layout,
904            )?,
905            HostBuffer::U32(pred) => Map2::map(
906                &crate::ops::WCond(pred, cond_layout),
907                &t.0,
908                on_true_layout,
909                &f.0,
910                on_false_layout,
911            )?,
912            HostBuffer::I16(pred) => Map2::map(
913                &crate::ops::WCond(pred, cond_layout),
914                &t.0,
915                on_true_layout,
916                &f.0,
917                on_false_layout,
918            )?,
919            HostBuffer::I32(pred) => Map2::map(
920                &crate::ops::WCond(pred, cond_layout),
921                &t.0,
922                on_true_layout,
923                &f.0,
924                on_false_layout,
925            )?,
926            HostBuffer::I64(pred) => Map2::map(
927                &crate::ops::WCond(pred, cond_layout),
928                &t.0,
929                on_true_layout,
930                &f.0,
931                on_false_layout,
932            )?,
933            // GAP-168(c): Bool is the canonical `where` cond — the mask bytes
934            // (0/1) are the predicate, byte-identical to the U8 case.
935            HostBuffer::Bool(pred) => Map2::map(
936                &crate::ops::WCond(pred, cond_layout),
937                &t.0,
938                on_true_layout,
939                &f.0,
940                on_false_layout,
941            )?,
942            _ => return Err(Error::UnsupportedDTypeForOp(self.0.dtype(), "where-cond").bt()),
943        };
944        Ok(Box::new(CpuStorage(result)))
945    }
946
947    fn conv1d_dyn(
948        &self,
949        l: &Layout,
950        kernel: &dyn DynBackendStorage,
951        kernel_l: &Layout,
952        params: &ParamsConv1D,
953    ) -> Result<Box<dyn DynBackendStorage>> {
954        let kernel = downcast(kernel)?;
955        // Use the im2col approach for conv1d (same as BackendStorage impl).
956        let l_k = params.k_size;
957        let col = Map1::map(
958            &crate::ops::Im2Col1D {
959                l_k,
960                padding: params.padding,
961                stride: params.stride,
962                dilation: params.dilation,
963            },
964            &self.0,
965            l,
966        )?;
967        let b = params.b_size;
968        let n = params.c_out;
969        let l_out = params.l_out();
970        let k = l_k * params.c_in;
971        let m = l_out;
972        let col_l = Layout::contiguous((b, m, k));
973        let kernel_l_c = if kernel_l.is_contiguous() {
974            Layout::contiguous_with_offset((1, n, k), kernel_l.start_offset())
975                .transpose(1, 2)?
976                .broadcast_as((b, k, n))?
977        } else {
978            // Make kernel contiguous
979            let mut kernel_c = unsafe { cpu_alloc_uninit(kernel_l.shape(), kernel.0.dtype())? };
980            cpu_copy_strided_src(&kernel.0, &mut kernel_c, 0, kernel_l)?;
981            let new_l = Layout::contiguous_with_offset((1, n, k), kernel_l.start_offset())
982                .transpose(1, 2)?
983                .broadcast_as((b, k, n))?;
984            // Also update the kernel ref
985            return {
986                let res = Map2::map(
987                    &crate::ops::MatMul((b, m, n, k)),
988                    &col,
989                    &col_l,
990                    &kernel_c,
991                    &new_l,
992                )?;
993                let res_l = Layout::contiguous((b, l_out, params.c_out)).transpose(1, 2)?;
994                let mut res_t = unsafe { cpu_alloc_uninit(res_l.shape(), res.dtype())? };
995                cpu_copy_strided_src(&res, &mut res_t, 0, &res_l)?;
996                Ok(Box::new(CpuStorage(res_t)))
997            };
998        };
999        let res = Map2::map(
1000            &crate::ops::MatMul((b, m, n, k)),
1001            &col,
1002            &col_l,
1003            &kernel.0,
1004            &kernel_l_c,
1005        )?;
1006        let res_l = Layout::contiguous((b, l_out, params.c_out)).transpose(1, 2)?;
1007        let mut res_t = unsafe { cpu_alloc_uninit(res_l.shape(), res.dtype())? };
1008        cpu_copy_strided_src(&res, &mut res_t, 0, &res_l)?;
1009        Ok(Box::new(CpuStorage(res_t)))
1010    }
1011
1012    fn conv_transpose1d_dyn(
1013        &self,
1014        l: &Layout,
1015        kernel: &dyn DynBackendStorage,
1016        kernel_l: &Layout,
1017        params: &ParamsConvTranspose1D,
1018    ) -> Result<Box<dyn DynBackendStorage>> {
1019        let kernel = downcast(kernel)?;
1020        let result = Map2::map(
1021            &crate::ops::ConvTranspose1D(params),
1022            &self.0,
1023            l,
1024            &kernel.0,
1025            kernel_l,
1026        )?;
1027        Ok(Box::new(CpuStorage(result)))
1028    }
1029
1030    fn conv2d_dyn(
1031        &self,
1032        l: &Layout,
1033        kernel: &dyn DynBackendStorage,
1034        kernel_l: &Layout,
1035        params: &ParamsConv2D,
1036    ) -> Result<Box<dyn DynBackendStorage>> {
1037        let kernel = downcast(kernel)?;
1038        let result = Map2::map(
1039            &crate::conv2d::Conv2D(params),
1040            &self.0,
1041            l,
1042            &kernel.0,
1043            kernel_l,
1044        )?;
1045        Ok(Box::new(CpuStorage(result)))
1046    }
1047
1048    fn conv_transpose2d_dyn(
1049        &self,
1050        l: &Layout,
1051        kernel: &dyn DynBackendStorage,
1052        kernel_l: &Layout,
1053        params: &ParamsConvTranspose2D,
1054    ) -> Result<Box<dyn DynBackendStorage>> {
1055        let kernel = downcast(kernel)?;
1056        let result = Map2::map(
1057            &crate::ops::ConvTranspose2D(params),
1058            &self.0,
1059            l,
1060            &kernel.0,
1061            kernel_l,
1062        )?;
1063        Ok(Box::new(CpuStorage(result)))
1064    }
1065
1066    fn avg_pool2d_dyn(
1067        &self,
1068        layout: &Layout,
1069        kernel: (usize, usize),
1070        stride: (usize, usize),
1071    ) -> Result<Box<dyn DynBackendStorage>> {
1072        let result = Map1::map(&crate::ops::AvgPool2D(kernel, stride), &self.0, layout)?;
1073        Ok(Box::new(CpuStorage(result)))
1074    }
1075
1076    fn max_pool2d_dyn(
1077        &self,
1078        layout: &Layout,
1079        kernel: (usize, usize),
1080        stride: (usize, usize),
1081    ) -> Result<Box<dyn DynBackendStorage>> {
1082        let result = Map1::map(&crate::ops::MaxPool2D(kernel, stride), &self.0, layout)?;
1083        Ok(Box::new(CpuStorage(result)))
1084    }
1085
1086    fn upsample_nearest1d_dyn(
1087        &self,
1088        layout: &Layout,
1089        target_size: usize,
1090    ) -> Result<Box<dyn DynBackendStorage>> {
1091        let result = Map1::map(&crate::ops::UpsampleNearest1D(target_size), &self.0, layout)?;
1092        Ok(Box::new(CpuStorage(result)))
1093    }
1094
1095    fn upsample_nearest2d_dyn(
1096        &self,
1097        layout: &Layout,
1098        target_h: usize,
1099        target_w: usize,
1100    ) -> Result<Box<dyn DynBackendStorage>> {
1101        let result = Map1::map(
1102            &crate::ops::UpsampleNearest2D(target_h, target_w),
1103            &self.0,
1104            layout,
1105        )?;
1106        Ok(Box::new(CpuStorage(result)))
1107    }
1108
1109    fn upsample_bilinear2d_dyn(
1110        &self,
1111        layout: &Layout,
1112        target_h: usize,
1113        target_w: usize,
1114        align_corners: bool,
1115        scale_h: Option<f64>,
1116        scale_w: Option<f64>,
1117    ) -> Result<Box<dyn DynBackendStorage>> {
1118        let result = Map1::map(
1119            &crate::ops::UpsampleBilinear2D {
1120                target_h,
1121                target_w,
1122                align_corners,
1123                scale_h_factor: scale_h,
1124                scale_w_factor: scale_w,
1125            },
1126            &self.0,
1127            layout,
1128        )?;
1129        Ok(Box::new(CpuStorage(result)))
1130    }
1131
1132    fn gather_dyn(
1133        &self,
1134        src_layout: &Layout,
1135        ids: &dyn DynBackendStorage,
1136        ids_layout: &Layout,
1137        dim: usize,
1138    ) -> Result<Box<dyn DynBackendStorage>> {
1139        let ids_s = downcast(ids)?;
1140        let result = match &ids_s.0 {
1141            HostBuffer::U8(ids) => Map1::map(
1142                &crate::ops::Gather {
1143                    ids,
1144                    ids_l: ids_layout,
1145                    dim,
1146                },
1147                &self.0,
1148                src_layout,
1149            )?,
1150            HostBuffer::U32(ids) => Map1::map(
1151                &crate::ops::Gather {
1152                    ids,
1153                    ids_l: ids_layout,
1154                    dim,
1155                },
1156                &self.0,
1157                src_layout,
1158            )?,
1159            HostBuffer::I64(ids) => Map1::map(
1160                &crate::ops::Gather {
1161                    ids,
1162                    ids_l: ids_layout,
1163                    dim,
1164                },
1165                &self.0,
1166                src_layout,
1167            )?,
1168            _ => return Err(Error::UnsupportedDTypeForOp(ids_s.0.dtype(), "gather").bt()),
1169        };
1170        Ok(Box::new(CpuStorage(result)))
1171    }
1172
1173    fn scatter_set_dyn(
1174        &mut self,
1175        self_layout: &Layout,
1176        src: &dyn DynBackendStorage,
1177        src_layout: &Layout,
1178        ids: &dyn DynBackendStorage,
1179        ids_layout: &Layout,
1180        dim: usize,
1181    ) -> Result<()> {
1182        let ids_s = downcast(ids)?;
1183        let src_s = downcast(src)?;
1184        match &ids_s.0 {
1185            HostBuffer::U8(ids) => Map2InPlace::map(
1186                &crate::ops::Scatter::<_, crate::ops::Set>::new(ids, ids_layout, dim),
1187                &mut self.0,
1188                self_layout,
1189                &src_s.0,
1190                src_layout,
1191            )?,
1192            HostBuffer::U32(ids) => Map2InPlace::map(
1193                &crate::ops::Scatter::<_, crate::ops::Set>::new(ids, ids_layout, dim),
1194                &mut self.0,
1195                self_layout,
1196                &src_s.0,
1197                src_layout,
1198            )?,
1199            HostBuffer::I64(ids) => Map2InPlace::map(
1200                &crate::ops::Scatter::<_, crate::ops::Set>::new(ids, ids_layout, dim),
1201                &mut self.0,
1202                self_layout,
1203                &src_s.0,
1204                src_layout,
1205            )?,
1206            _ => return Err(Error::UnsupportedDTypeForOp(ids_s.0.dtype(), "scatter").bt()),
1207        };
1208        Ok(())
1209    }
1210
1211    fn scatter_add_set_dyn(
1212        &mut self,
1213        self_layout: &Layout,
1214        src: &dyn DynBackendStorage,
1215        src_layout: &Layout,
1216        ids: &dyn DynBackendStorage,
1217        ids_layout: &Layout,
1218        dim: usize,
1219    ) -> Result<()> {
1220        let ids_s = downcast(ids)?;
1221        let src_s = downcast(src)?;
1222        match &ids_s.0 {
1223            HostBuffer::U8(ids) => Map2InPlace::map(
1224                &crate::ops::Scatter::<_, crate::ops::Add>::new(ids, ids_layout, dim),
1225                &mut self.0,
1226                self_layout,
1227                &src_s.0,
1228                src_layout,
1229            )?,
1230            HostBuffer::U32(ids) => Map2InPlace::map(
1231                &crate::ops::Scatter::<_, crate::ops::Add>::new(ids, ids_layout, dim),
1232                &mut self.0,
1233                self_layout,
1234                &src_s.0,
1235                src_layout,
1236            )?,
1237            HostBuffer::I16(ids) => Map2InPlace::map(
1238                &crate::ops::Scatter::<_, crate::ops::Add>::new(ids, ids_layout, dim),
1239                &mut self.0,
1240                self_layout,
1241                &src_s.0,
1242                src_layout,
1243            )?,
1244            HostBuffer::I32(ids) => Map2InPlace::map(
1245                &crate::ops::Scatter::<_, crate::ops::Add>::new(ids, ids_layout, dim),
1246                &mut self.0,
1247                self_layout,
1248                &src_s.0,
1249                src_layout,
1250            )?,
1251            HostBuffer::I64(ids) => Map2InPlace::map(
1252                &crate::ops::Scatter::<_, crate::ops::Add>::new(ids, ids_layout, dim),
1253                &mut self.0,
1254                self_layout,
1255                &src_s.0,
1256                src_layout,
1257            )?,
1258            _ => return Err(Error::UnsupportedDTypeForOp(ids_s.0.dtype(), "scatter-add").bt()),
1259        };
1260        Ok(())
1261    }
1262
1263    fn index_select_dyn(
1264        &self,
1265        ids: &dyn DynBackendStorage,
1266        src_layout: &Layout,
1267        ids_layout: &Layout,
1268        dim: usize,
1269    ) -> Result<Box<dyn DynBackendStorage>> {
1270        let ids_s = downcast(ids)?;
1271        let result = match &ids_s.0 {
1272            HostBuffer::U8(ids) => Map1::map(
1273                &crate::ops::IndexSelect {
1274                    ids,
1275                    ids_l: ids_layout,
1276                    dim,
1277                },
1278                &self.0,
1279                src_layout,
1280            )?,
1281            HostBuffer::U32(ids) => Map1::map(
1282                &crate::ops::IndexSelect {
1283                    ids,
1284                    ids_l: ids_layout,
1285                    dim,
1286                },
1287                &self.0,
1288                src_layout,
1289            )?,
1290            HostBuffer::I64(ids) => Map1::map(
1291                &crate::ops::IndexSelect {
1292                    ids,
1293                    ids_l: ids_layout,
1294                    dim,
1295                },
1296                &self.0,
1297                src_layout,
1298            )?,
1299            _ => return Err(Error::UnsupportedDTypeForOp(ids_s.0.dtype(), "index-select").bt()),
1300        };
1301        Ok(Box::new(CpuStorage(result)))
1302    }
1303
1304    fn index_add_dyn(
1305        &self,
1306        self_layout: &Layout,
1307        ids: &dyn DynBackendStorage,
1308        ids_layout: &Layout,
1309        src: &dyn DynBackendStorage,
1310        src_layout: &Layout,
1311        dim: usize,
1312    ) -> Result<Box<dyn DynBackendStorage>> {
1313        let ids_s = downcast(ids)?;
1314        let src_s = downcast(src)?;
1315        let result = match &ids_s.0 {
1316            HostBuffer::U8(ids) => {
1317                let ids = match ids_layout.contiguous_offsets() {
1318                    Some((a, b)) => &ids[a..b],
1319                    None => return Err(Error::RequiresContiguous { op: "index-add" }.bt()),
1320                };
1321                Map2::map(
1322                    &crate::ops::IndexAdd { ids, dim },
1323                    &self.0,
1324                    self_layout,
1325                    &src_s.0,
1326                    src_layout,
1327                )?
1328            }
1329            HostBuffer::U32(ids) => {
1330                let ids = match ids_layout.contiguous_offsets() {
1331                    Some((a, b)) => &ids[a..b],
1332                    None => return Err(Error::RequiresContiguous { op: "index-add" }.bt()),
1333                };
1334                Map2::map(
1335                    &crate::ops::IndexAdd { ids, dim },
1336                    &self.0,
1337                    self_layout,
1338                    &src_s.0,
1339                    src_layout,
1340                )?
1341            }
1342            HostBuffer::I16(ids) => {
1343                let ids = match ids_layout.contiguous_offsets() {
1344                    Some((a, b)) => &ids[a..b],
1345                    None => return Err(Error::RequiresContiguous { op: "index-add" }.bt()),
1346                };
1347                Map2::map(
1348                    &crate::ops::IndexAdd { ids, dim },
1349                    &self.0,
1350                    self_layout,
1351                    &src_s.0,
1352                    src_layout,
1353                )?
1354            }
1355            HostBuffer::I32(ids) => {
1356                let ids = match ids_layout.contiguous_offsets() {
1357                    Some((a, b)) => &ids[a..b],
1358                    None => return Err(Error::RequiresContiguous { op: "index-add" }.bt()),
1359                };
1360                Map2::map(
1361                    &crate::ops::IndexAdd { ids, dim },
1362                    &self.0,
1363                    self_layout,
1364                    &src_s.0,
1365                    src_layout,
1366                )?
1367            }
1368            HostBuffer::I64(ids) => {
1369                let ids = match ids_layout.contiguous_offsets() {
1370                    Some((a, b)) => &ids[a..b],
1371                    None => return Err(Error::RequiresContiguous { op: "index-add" }.bt()),
1372                };
1373                Map2::map(
1374                    &crate::ops::IndexAdd { ids, dim },
1375                    &self.0,
1376                    self_layout,
1377                    &src_s.0,
1378                    src_layout,
1379                )?
1380            }
1381            _ => return Err(Error::UnsupportedDTypeForOp(ids_s.0.dtype(), "index-add").bt()),
1382        };
1383        Ok(Box::new(CpuStorage(result)))
1384    }
1385
1386    fn matmul_dyn(
1387        &self,
1388        rhs: &dyn DynBackendStorage,
1389        bmnk: (usize, usize, usize, usize),
1390        lhs_layout: &Layout,
1391        rhs_layout: &Layout,
1392    ) -> Result<Box<dyn DynBackendStorage>> {
1393        let rhs = downcast(rhs)?;
1394        let result = Map2::map(
1395            &crate::ops::MatMul(bmnk),
1396            &self.0,
1397            lhs_layout,
1398            &rhs.0,
1399            rhs_layout,
1400        )?;
1401        Ok(Box::new(CpuStorage(result)))
1402    }
1403
1404    fn copy_strided_src_dyn(
1405        &self,
1406        dst: &mut dyn DynBackendStorage,
1407        dst_offset: usize,
1408        src_layout: &Layout,
1409    ) -> Result<()> {
1410        let dst = downcast_mut(dst)?;
1411        cpu_copy_strided_src(&self.0, &mut dst.0, dst_offset, src_layout)
1412    }
1413
1414    fn copy2d_dyn(
1415        &self,
1416        dst: &mut dyn DynBackendStorage,
1417        d1: usize,
1418        d2: usize,
1419        src_stride1: usize,
1420        dst_stride1: usize,
1421        src_offset: usize,
1422        dst_offset: usize,
1423    ) -> Result<()> {
1424        let dst = downcast_mut(dst)?;
1425        cpu_copy2d(
1426            &self.0,
1427            &mut dst.0,
1428            d1,
1429            d2,
1430            src_stride1,
1431            dst_stride1,
1432            src_offset,
1433            dst_offset,
1434        )
1435    }
1436
1437    fn const_set_dyn(&mut self, value: Scalar, layout: &Layout) -> Result<()> {
1438        cpu_const_set(&mut self.0, value, layout)
1439    }
1440
1441    fn as_any(&self) -> &dyn Any {
1442        self
1443    }
1444
1445    fn as_any_mut(&mut self) -> &mut dyn Any {
1446        self
1447    }
1448}
1449
1450// ---------------------------------------------------------------------------
1451// impl DynBackendDevice for CpuBackendDevice
1452// ---------------------------------------------------------------------------
1453
1454impl DynBackendDevice for CpuBackendDevice {
1455    fn location_dyn(&self) -> DeviceLocation {
1456        DeviceLocation::Cpu
1457    }
1458
1459    fn as_backend_runtime(&self) -> Option<&dyn fuel_backend_contract::backend::BackendRuntime> {
1460        Some(self)
1461    }
1462
1463    fn same_device_dyn(&self, other: &dyn DynBackendDevice) -> bool {
1464        other.as_any().downcast_ref::<CpuBackendDevice>().is_some()
1465    }
1466
1467    fn zeros_impl_dyn(&self, shape: &Shape, dtype: DType) -> Result<Box<dyn DynBackendStorage>> {
1468        let storage = cpu_zeros(shape, dtype)?;
1469        Ok(Box::new(CpuStorage(storage)))
1470    }
1471
1472    unsafe fn alloc_uninit_dyn(
1473        &self,
1474        shape: &Shape,
1475        dtype: DType,
1476    ) -> Result<Box<dyn DynBackendStorage>> {
1477        let storage = unsafe { cpu_alloc_uninit(shape, dtype)? };
1478        Ok(Box::new(CpuStorage(storage)))
1479    }
1480
1481    fn storage_from_host_buffer_dyn(&self, buf: &HostBuffer) -> Result<Box<dyn DynBackendStorage>> {
1482        Ok(Box::new(CpuStorage(buf.clone())))
1483    }
1484
1485    fn storage_from_host_buffer_owned_dyn(
1486        &self,
1487        buf: HostBuffer,
1488    ) -> Result<Box<dyn DynBackendStorage>> {
1489        Ok(Box::new(CpuStorage(buf)))
1490    }
1491
1492    fn rand_uniform_dyn(
1493        &self,
1494        shape: &Shape,
1495        dtype: DType,
1496        lo: f64,
1497        hi: f64,
1498    ) -> Result<Box<dyn DynBackendStorage>> {
1499        let storage = cpu_rand_uniform(shape, dtype, lo, hi)?;
1500        Ok(Box::new(CpuStorage(storage)))
1501    }
1502
1503    fn rand_normal_dyn(
1504        &self,
1505        shape: &Shape,
1506        dtype: DType,
1507        mean: f64,
1508        std: f64,
1509    ) -> Result<Box<dyn DynBackendStorage>> {
1510        let storage = cpu_rand_normal(shape, dtype, mean, std)?;
1511        Ok(Box::new(CpuStorage(storage)))
1512    }
1513
1514    fn set_seed_dyn(&self, _seed: u64) -> Result<()> {
1515        Err(Error::Msg("cannot seed the CPU rng with set_seed".into()).bt())
1516    }
1517
1518    fn get_current_seed_dyn(&self) -> Result<u64> {
1519        Err(Error::Msg("cannot get the CPU rng seed with get_current_seed".into()).bt())
1520    }
1521
1522    fn synchronize_dyn(&self) -> Result<()> {
1523        Ok(())
1524    }
1525
1526    fn as_any(&self) -> &dyn Any {
1527        self
1528    }
1529
1530    fn as_quantized_kernels(
1531        &self,
1532    ) -> Option<&dyn fuel_backend_contract::quantized::QuantizedDeviceKernels> {
1533        Some(self)
1534    }
1535}
1536
1537// ---------------------------------------------------------------------------
1538// Device helper implementations
1539// ---------------------------------------------------------------------------
1540
1541fn cpu_zeros(shape: &Shape, dtype: DType) -> Result<HostBuffer> {
1542    let elem_count = shape.elem_count();
1543    let storage = match dtype {
1544        DType::U8 => HostBuffer::U8(vec![0u8; elem_count]),
1545        DType::I8 => HostBuffer::I8(vec![0i8; elem_count]),
1546        DType::U32 => HostBuffer::U32(vec![0u32; elem_count]),
1547        DType::I16 => HostBuffer::I16(vec![0i16; elem_count]),
1548        DType::I32 => HostBuffer::I32(vec![0i32; elem_count]),
1549        DType::I64 => HostBuffer::I64(vec![0i64; elem_count]),
1550        DType::BF16 => HostBuffer::BF16(vec![bf16::ZERO; elem_count]),
1551        DType::F16 => HostBuffer::F16(vec![f16::ZERO; elem_count]),
1552        DType::F32 => HostBuffer::F32(vec![0f32; elem_count]),
1553        DType::F64 => HostBuffer::F64(vec![0f64; elem_count]),
1554        DType::F8E4M3 => HostBuffer::F8E4M3(vec![F8E4M3::ZERO; elem_count]),
1555        // F8E5M2 declines for a DIFFERENT reason than F8E6M2 below, and the
1556        // difference matters: E5M2 is OCP-standard, `float8::F8E5M2` exists,
1557        // and it has a real DLPack representation. It declines only because no
1558        // `HostBuffer::F8E5M2` variant has been added yet. This is a
1559        // DELIBERATE UNDER-SHIPMENT (the fix is to add the buffer variant),
1560        // not an absent representation (F8E6M2, whose encoding is unauthored).
1561        DType::F8E5M2 => return Err(Error::UnsupportedDTypeForOp(dtype, "zeros").bt()),
1562        // GAP-168(c): Bool zeros = all `false` (0 bytes), byte-backed like u8.
1563        DType::Bool => HostBuffer::Bool(vec![0u8; elem_count]),
1564        DType::F6E2M3 | DType::F6E3M2 | DType::F4 | DType::F8E8M0 | DType::F8E6M2 => {
1565            return Err(Error::UnsupportedDTypeForOp(dtype, "zeros").bt());
1566        }
1567    };
1568    Ok(storage)
1569}
1570
1571#[allow(clippy::uninit_vec)]
1572unsafe fn cpu_alloc_uninit(shape: &Shape, dtype: DType) -> Result<HostBuffer> {
1573    let elem_count = shape.elem_count();
1574    let storage = match dtype {
1575        DType::U8 => {
1576            let mut v = Vec::with_capacity(elem_count);
1577            unsafe { v.set_len(elem_count) };
1578            HostBuffer::U8(v)
1579        }
1580        DType::I8 => {
1581            let mut v = Vec::with_capacity(elem_count);
1582            unsafe { v.set_len(elem_count) };
1583            HostBuffer::I8(v)
1584        }
1585        DType::U32 => {
1586            let mut v = Vec::with_capacity(elem_count);
1587            unsafe { v.set_len(elem_count) };
1588            HostBuffer::U32(v)
1589        }
1590        DType::I16 => {
1591            let mut v = Vec::with_capacity(elem_count);
1592            unsafe { v.set_len(elem_count) };
1593            HostBuffer::I16(v)
1594        }
1595        DType::I32 => {
1596            let mut v = Vec::with_capacity(elem_count);
1597            unsafe { v.set_len(elem_count) };
1598            HostBuffer::I32(v)
1599        }
1600        DType::I64 => {
1601            let mut v = Vec::with_capacity(elem_count);
1602            unsafe { v.set_len(elem_count) };
1603            HostBuffer::I64(v)
1604        }
1605        DType::BF16 => {
1606            let mut v = Vec::with_capacity(elem_count);
1607            unsafe { v.set_len(elem_count) };
1608            HostBuffer::BF16(v)
1609        }
1610        DType::F16 => {
1611            let mut v = Vec::with_capacity(elem_count);
1612            unsafe { v.set_len(elem_count) };
1613            HostBuffer::F16(v)
1614        }
1615        DType::F32 => {
1616            let mut v = Vec::with_capacity(elem_count);
1617            unsafe { v.set_len(elem_count) };
1618            HostBuffer::F32(v)
1619        }
1620        DType::F64 => {
1621            let mut v = Vec::with_capacity(elem_count);
1622            unsafe { v.set_len(elem_count) };
1623            HostBuffer::F64(v)
1624        }
1625        DType::F8E4M3 => {
1626            let mut v = Vec::with_capacity(elem_count);
1627            unsafe { v.set_len(elem_count) };
1628            HostBuffer::F8E4M3(v)
1629        }
1630        // F8E5M2 declines for a DIFFERENT reason than F8E6M2 below, and the
1631        // difference matters: E5M2 is OCP-standard, `float8::F8E5M2` exists,
1632        // and it has a real DLPack representation. It declines only because no
1633        // `HostBuffer::F8E5M2` variant has been added yet. This is a
1634        // DELIBERATE UNDER-SHIPMENT (the fix is to add the buffer variant),
1635        // not an absent representation (F8E6M2, whose encoding is unauthored).
1636        DType::F8E5M2 => return Err(Error::UnsupportedDTypeForOp(dtype, "alloc_uninit").bt()),
1637        // GAP-168(c): Bool uninit = a byte buffer (byte-backed like u8).
1638        DType::Bool => {
1639            let mut v = Vec::with_capacity(elem_count);
1640            unsafe { v.set_len(elem_count) };
1641            HostBuffer::Bool(v)
1642        }
1643        DType::F6E2M3 | DType::F6E3M2 | DType::F4 | DType::F8E8M0 | DType::F8E6M2 => {
1644            return Err(Error::UnsupportedDTypeForOp(dtype, "alloc_uninit").bt());
1645        }
1646    };
1647    Ok(storage)
1648}
1649
1650/// Draw one `f32` in `[min, max)` and narrow it to a low-precision dtype,
1651/// redrawing when rounding carries the value outside the interval (an `f32`
1652/// just below `max` can round up to `max`). If the dtype has no representable
1653/// value inside a very narrow interval, the bounded retry falls back to the
1654/// narrowed lower bound rather than looping forever.
1655fn sample_narrow_in_range<T: Copy>(
1656    rng: &mut impl rand::Rng,
1657    uniform: rand::distr::Uniform<f32>,
1658    min: f32,
1659    max: f32,
1660    narrow: impl Fn(f32) -> T,
1661    widen: impl Fn(T) -> f32,
1662) -> T {
1663    use rand::RngExt;
1664    for _ in 0..64 {
1665        let v = narrow(rng.sample::<f32, _>(uniform));
1666        let w = widen(v);
1667        if w >= min && w < max {
1668            return v;
1669        }
1670    }
1671    narrow(min)
1672}
1673
1674fn cpu_rand_uniform(shape: &Shape, dtype: DType, min: f64, max: f64) -> Result<HostBuffer> {
1675    use rand::prelude::*;
1676    let elem_count = shape.elem_count();
1677    let mut rng = rand::rng();
1678    match dtype {
1679        DType::BF16 => {
1680            let uniform =
1681                rand::distr::Uniform::new(min as f32, max as f32).map_err(fuel_ir::Error::wrap)?;
1682            let data: Vec<_> = (0..elem_count)
1683                .map(|_| {
1684                    sample_narrow_in_range(
1685                        &mut rng,
1686                        uniform,
1687                        min as f32,
1688                        max as f32,
1689                        bf16::from_f32,
1690                        |x| x.to_f32(),
1691                    )
1692                })
1693                .collect();
1694            Ok(HostBuffer::BF16(data))
1695        }
1696        DType::F16 => {
1697            let uniform =
1698                rand::distr::Uniform::new(min as f32, max as f32).map_err(fuel_ir::Error::wrap)?;
1699            let data: Vec<_> = (0..elem_count)
1700                .map(|_| {
1701                    sample_narrow_in_range(
1702                        &mut rng,
1703                        uniform,
1704                        min as f32,
1705                        max as f32,
1706                        f16::from_f32,
1707                        |x| x.to_f32(),
1708                    )
1709                })
1710                .collect();
1711            Ok(HostBuffer::F16(data))
1712        }
1713        DType::F8E4M3 => {
1714            let uniform =
1715                rand::distr::Uniform::new(min as f32, max as f32).map_err(fuel_ir::Error::wrap)?;
1716            let data: Vec<_> = (0..elem_count)
1717                .map(|_| {
1718                    sample_narrow_in_range(
1719                        &mut rng,
1720                        uniform,
1721                        min as f32,
1722                        max as f32,
1723                        F8E4M3::from_f32,
1724                        |x| x.to_f32(),
1725                    )
1726                })
1727                .collect();
1728            Ok(HostBuffer::F8E4M3(data))
1729        }
1730        DType::F32 => {
1731            let uniform =
1732                rand::distr::Uniform::new(min as f32, max as f32).map_err(fuel_ir::Error::wrap)?;
1733            let data: Vec<_> = (0..elem_count)
1734                .map(|_| rng.sample::<f32, _>(uniform))
1735                .collect();
1736            Ok(HostBuffer::F32(data))
1737        }
1738        DType::F64 => {
1739            let uniform = rand::distr::Uniform::new(min, max).map_err(fuel_ir::Error::wrap)?;
1740            let data: Vec<_> = (0..elem_count)
1741                .map(|_| rng.sample::<f64, _>(uniform))
1742                .collect();
1743            Ok(HostBuffer::F64(data))
1744        }
1745        _ => Err(Error::UnsupportedDTypeForOp(dtype, "rand_uniform").bt()),
1746    }
1747}
1748
1749fn cpu_rand_normal(shape: &Shape, dtype: DType, mean: f64, std: f64) -> Result<HostBuffer> {
1750    use rand::prelude::*;
1751    let elem_count = shape.elem_count();
1752    let mut rng = rand::rng();
1753    match dtype {
1754        DType::BF16 => {
1755            let normal =
1756                rand_distr::Normal::new(mean as f32, std as f32).map_err(fuel_ir::Error::wrap)?;
1757            let data: Vec<_> = (0..elem_count)
1758                .map(|_| bf16::from_f32(normal.sample(&mut rng)))
1759                .collect();
1760            Ok(HostBuffer::BF16(data))
1761        }
1762        DType::F16 => {
1763            let normal =
1764                rand_distr::Normal::new(mean as f32, std as f32).map_err(fuel_ir::Error::wrap)?;
1765            let data: Vec<_> = (0..elem_count)
1766                .map(|_| f16::from_f32(normal.sample(&mut rng)))
1767                .collect();
1768            Ok(HostBuffer::F16(data))
1769        }
1770        DType::F8E4M3 => {
1771            let normal =
1772                rand_distr::Normal::new(mean as f32, std as f32).map_err(fuel_ir::Error::wrap)?;
1773            let data: Vec<_> = (0..elem_count)
1774                .map(|_| F8E4M3::from_f32(normal.sample(&mut rng)))
1775                .collect();
1776            Ok(HostBuffer::F8E4M3(data))
1777        }
1778        DType::F32 => {
1779            let normal =
1780                rand_distr::Normal::new(mean as f32, std as f32).map_err(fuel_ir::Error::wrap)?;
1781            let data: Vec<_> = (0..elem_count).map(|_| normal.sample(&mut rng)).collect();
1782            Ok(HostBuffer::F32(data))
1783        }
1784        DType::F64 => {
1785            let normal = rand_distr::Normal::new(mean, std).map_err(fuel_ir::Error::wrap)?;
1786            let data: Vec<_> = (0..elem_count).map(|_| normal.sample(&mut rng)).collect();
1787            Ok(HostBuffer::F64(data))
1788        }
1789        _ => Err(Error::UnsupportedDTypeForOp(dtype, "rand_normal").bt()),
1790    }
1791}
1792
1793// ---------------------------------------------------------------------------
1794// Storage helper functions (copy, const_set, to_dtype)
1795// ---------------------------------------------------------------------------
1796
1797fn cpu_copy_strided_src(
1798    src: &HostBuffer,
1799    dst: &mut HostBuffer,
1800    dst_offset: usize,
1801    src_l: &Layout,
1802) -> Result<()> {
1803    match (src, dst) {
1804        (HostBuffer::U8(s), HostBuffer::U8(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1805        (HostBuffer::U32(s), HostBuffer::U32(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1806        (HostBuffer::I16(s), HostBuffer::I16(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1807        (HostBuffer::I32(s), HostBuffer::I32(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1808        (HostBuffer::I64(s), HostBuffer::I64(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1809        (HostBuffer::BF16(s), HostBuffer::BF16(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1810        (HostBuffer::F16(s), HostBuffer::F16(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1811        (HostBuffer::F32(s), HostBuffer::F32(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1812        (HostBuffer::F64(s), HostBuffer::F64(d)) => copy_strided_src_(s, d, dst_offset, src_l),
1813        (HostBuffer::F8E4M3(s), HostBuffer::F8E4M3(d)) => {
1814            copy_strided_src_(s, d, dst_offset, src_l)
1815        }
1816        (_, d) => {
1817            return Err(Error::DTypeMismatchBinaryOp {
1818                lhs: src.dtype(),
1819                rhs: d.dtype(),
1820                op: "copy_strided",
1821            }
1822            .bt());
1823        }
1824    }
1825    Ok(())
1826}
1827
1828fn cpu_copy2d(
1829    src: &HostBuffer,
1830    dst: &mut HostBuffer,
1831    d1: usize,
1832    d2: usize,
1833    src_s: usize,
1834    dst_s: usize,
1835    src_o: usize,
1836    dst_o: usize,
1837) -> Result<()> {
1838    match (src, dst) {
1839        (HostBuffer::U8(s), HostBuffer::U8(d)) => copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o),
1840        (HostBuffer::U32(s), HostBuffer::U32(d)) => {
1841            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1842        }
1843        (HostBuffer::I16(s), HostBuffer::I16(d)) => {
1844            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1845        }
1846        (HostBuffer::I32(s), HostBuffer::I32(d)) => {
1847            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1848        }
1849        (HostBuffer::I64(s), HostBuffer::I64(d)) => {
1850            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1851        }
1852        (HostBuffer::BF16(s), HostBuffer::BF16(d)) => {
1853            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1854        }
1855        (HostBuffer::F16(s), HostBuffer::F16(d)) => {
1856            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1857        }
1858        (HostBuffer::F32(s), HostBuffer::F32(d)) => {
1859            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1860        }
1861        (HostBuffer::F64(s), HostBuffer::F64(d)) => {
1862            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1863        }
1864        (HostBuffer::F8E4M3(s), HostBuffer::F8E4M3(d)) => {
1865            copy2d_(s, d, d1, d2, src_s, dst_s, src_o, dst_o)
1866        }
1867        (_, d) => {
1868            return Err(Error::DTypeMismatchBinaryOp {
1869                lhs: src.dtype(),
1870                rhs: d.dtype(),
1871                op: "copy2d",
1872            }
1873            .bt());
1874        }
1875    }
1876    Ok(())
1877}
1878
1879fn cpu_const_set(storage: &mut HostBuffer, s: Scalar, l: &Layout) -> Result<()> {
1880    use fuel_ir::Scalar as S;
1881    fn set<T: fuel_ir::WithDType + Copy>(src: &mut [T], l: &Layout, s: T) {
1882        match l.strided_blocks() {
1883            fuel_ir::StridedBlocks::SingleBlock { start_offset, len } => {
1884                src[start_offset..start_offset + len].fill(s)
1885            }
1886            fuel_ir::StridedBlocks::MultipleBlocks {
1887                block_start_index,
1888                block_len: 1,
1889            } => {
1890                for src_index in block_start_index {
1891                    src[src_index] = s;
1892                }
1893            }
1894            fuel_ir::StridedBlocks::MultipleBlocks {
1895                block_start_index,
1896                block_len,
1897            } => {
1898                for src_index in block_start_index {
1899                    src[src_index..src_index + block_len].fill(s);
1900                }
1901            }
1902        }
1903    }
1904    match (storage, s) {
1905        (HostBuffer::BF16(d), S::BF16(v)) => set(d, l, v),
1906        (HostBuffer::F16(d), S::F16(v)) => set(d, l, v),
1907        (HostBuffer::F32(d), S::F32(v)) => set(d, l, v),
1908        (HostBuffer::F64(d), S::F64(v)) => set(d, l, v),
1909        (HostBuffer::U8(d), S::U8(v)) => set(d, l, v),
1910        (HostBuffer::U32(d), S::U32(v)) => set(d, l, v),
1911        (HostBuffer::I16(d), S::I16(v)) => set(d, l, v),
1912        (HostBuffer::I32(d), S::I32(v)) => set(d, l, v),
1913        (HostBuffer::I64(d), S::I64(v)) => set(d, l, v),
1914        (HostBuffer::F8E4M3(d), S::F8E4M3(v)) => set(d, l, v),
1915        (st, s) => {
1916            return Err(Error::Msg(format!(
1917                "const_set dtype mismatch, expected {:?} but got {:?}",
1918                st.dtype(),
1919                s
1920            ))
1921            .bt());
1922        }
1923    }
1924    Ok(())
1925}
1926
1927/// Dtype conversion.  This has O(dtypes²) match arms — mirrors
1928/// `BackendStorage::to_dtype` from fuel-core's cpu_backend.
1929fn cpu_to_dtype(src: &HostBuffer, layout: &Layout, dtype: DType) -> Result<HostBuffer> {
1930    // Short-circuit: if source dtype matches target, just copy via layout.
1931    if src.dtype() == dtype {
1932        // Clone elements described by layout.
1933        return match src {
1934            HostBuffer::U8(d) => Ok(HostBuffer::U8(unary_map(d, layout, |v: u8| v))),
1935            HostBuffer::U32(d) => Ok(HostBuffer::U32(unary_map(d, layout, |v: u32| v))),
1936            HostBuffer::I16(d) => Ok(HostBuffer::I16(unary_map(d, layout, |v: i16| v))),
1937            HostBuffer::I32(d) => Ok(HostBuffer::I32(unary_map(d, layout, |v: i32| v))),
1938            HostBuffer::I64(d) => Ok(HostBuffer::I64(unary_map(d, layout, |v: i64| v))),
1939            HostBuffer::BF16(d) => Ok(HostBuffer::BF16(unary_map(d, layout, |v: bf16| v))),
1940            HostBuffer::F16(d) => Ok(HostBuffer::F16(unary_map(d, layout, |v: f16| v))),
1941            HostBuffer::F32(d) => Ok(HostBuffer::F32(unary_map(d, layout, |v: f32| v))),
1942            HostBuffer::F64(d) => Ok(HostBuffer::F64(unary_map(d, layout, |v: f64| v))),
1943            HostBuffer::F8E4M3(d) => Ok(HostBuffer::F8E4M3(unary_map(d, layout, |v: F8E4M3| v))),
1944            // GAP-168(c): Bool identity clone (byte copy).
1945            HostBuffer::Bool(d) => Ok(HostBuffer::Bool(unary_map(d, layout, |v: u8| v))),
1946            _ => Err(Error::UnsupportedDTypeForOp(src.dtype(), "to_dtype").bt()),
1947        };
1948    }
1949
1950    // Generic conversion: go through f64 as intermediate.
1951    // This handles all non-dummy dtype pairs.
1952    let as_f64: Vec<f64> = match src {
1953        HostBuffer::U8(d) => unary_map(d, layout, |v: u8| v as f64),
1954        HostBuffer::U32(d) => unary_map(d, layout, |v: u32| v as f64),
1955        HostBuffer::I16(d) => unary_map(d, layout, |v: i16| v as f64),
1956        HostBuffer::I32(d) => unary_map(d, layout, |v: i32| v as f64),
1957        HostBuffer::I64(d) => unary_map(d, layout, |v: i64| v as f64),
1958        HostBuffer::BF16(d) => unary_map(d, layout, |v: bf16| v.to_f64()),
1959        HostBuffer::F16(d) => unary_map(d, layout, |v: f16| v.to_f64()),
1960        HostBuffer::F32(d) => unary_map(d, layout, |v: f32| v as f64),
1961        HostBuffer::F64(d) => unary_map(d, layout, |v: f64| v),
1962        HostBuffer::F8E4M3(d) => unary_map(d, layout, |v: F8E4M3| v.to_f64()),
1963        // GAP-168(c): Bool as cast SOURCE — a mask byte (0/1) is 0.0/1.0, so a
1964        // numeric value can be extracted (constraint #1's explicit `.cast(U8)`).
1965        HostBuffer::Bool(d) => unary_map(d, layout, |v: u8| v as f64),
1966        _ => return Err(Error::UnsupportedDTypeForOp(src.dtype(), "to_dtype").bt()),
1967    };
1968
1969    match dtype {
1970        DType::U8 => Ok(HostBuffer::U8(
1971            as_f64.into_iter().map(|v| v as u8).collect(),
1972        )),
1973        DType::U32 => Ok(HostBuffer::U32(
1974            as_f64.into_iter().map(|v| v as u32).collect(),
1975        )),
1976        DType::I16 => Ok(HostBuffer::I16(
1977            as_f64.into_iter().map(|v| v as i16).collect(),
1978        )),
1979        DType::I32 => Ok(HostBuffer::I32(
1980            as_f64.into_iter().map(|v| v as i32).collect(),
1981        )),
1982        DType::I64 => Ok(HostBuffer::I64(
1983            as_f64.into_iter().map(|v| v as i64).collect(),
1984        )),
1985        DType::BF16 => Ok(HostBuffer::BF16(
1986            as_f64.into_iter().map(bf16::from_f64).collect(),
1987        )),
1988        DType::F16 => Ok(HostBuffer::F16(
1989            as_f64.into_iter().map(f16::from_f64).collect(),
1990        )),
1991        DType::F32 => Ok(HostBuffer::F32(
1992            as_f64.into_iter().map(|v| v as f32).collect(),
1993        )),
1994        DType::F64 => Ok(HostBuffer::F64(as_f64)),
1995        DType::F8E4M3 => Ok(HostBuffer::F8E4M3(
1996            as_f64.into_iter().map(F8E4M3::from_f64).collect(),
1997        )),
1998        // GAP-168(c): Bool as cast TARGET — this is the TRUTHINESS coercion
1999        // (`!= 0`), which belongs in a CAST (unlike `Scalar::from_f64`, the
2000        // constructor, which declines a non-truth-value). A real conversion, NOT
2001        // a byte reinterpret: `U8(5) -> 5.0 -> true`, so Bool ≠ U8.
2002        DType::Bool => Ok(HostBuffer::Bool(
2003            as_f64
2004                .into_iter()
2005                .map(|v| if v != 0.0 { 1u8 } else { 0u8 })
2006                .collect(),
2007        )),
2008        _ => Err(Error::UnsupportedDTypeForOp(dtype, "to_dtype").bt()),
2009    }
2010}
2011
2012/// GAP-275 tripwire: E5M2 storage is not wired, and the moment it is, this
2013/// test fails and says what to do about it.
2014#[cfg(test)]
2015mod gap_275_e5m2_tripwire {
2016    use super::*;
2017
2018    /// **This test exists to FAIL, and its failure is the point.**
2019    ///
2020    /// `DType::F8E5M2` is OCP-standard, `float8::F8E5M2` exists, and it has a
2021    /// real DLPack representation. It declines only because no
2022    /// `HostBuffer::F8E5M2` variant has been wired yet -- a deliberate
2023    /// under-shipment, not an absent encoding (contrast `F8E6M2`).
2024    ///
2025    /// Adding the variant is ALREADY a compile error: `HostBuffer::as_ref`
2026    /// matches all 16 variants with no wildcard, so the enum cannot grow
2027    /// silently. **What that compile error does not say is WHY you should
2028    /// care, and that is what this test carries.** It also fires one step
2029    /// later than the compiler -- when the capability is actually WIRED,
2030    /// rather than when the enum merely grows.
2031    ///
2032    /// **If this failed, you wired E5M2. Read this before deleting it:**
2033    ///
2034    /// 1. `float8` 0.7.0's `is_infinite` is BROKEN for both f8 types: it
2035    ///    returns `true` for `0x7E`, which is the FINITE 448, and
2036    ///    `F8E5M2::INFINITY` is likewise a finite normal. Fuel calls neither
2037    ///    predicate today, so exposure is currently zero -- wiring E5M2 is
2038    ///    what creates it.
2039    /// 2. E5M2 conversions therefore need BOUNDARY coverage before they are
2040    ///    trusted: overflow, saturation, infinity, NaN, subnormals, signed
2041    ///    zero. The existing f8 tests sit in the exactly-representable
2042    ///    interior and would pass over every one of those.
2043    /// 3. The template is `cast_f32_f8e4m3_boundary_saturates_and_preserves_nan`
2044    ///    in `byte_kernels.rs`: measure first, assert second, keep a positive
2045    ///    control, retain the vacuity guard.
2046    ///
2047    /// **Then delete this test.** It is a tripwire, not a prohibition, and
2048    /// removing it is the correct final step once the coverage exists.
2049    #[test]
2050    fn f8e5m2_zeros_declines_until_someone_wires_it() {
2051        let shape = Shape::from_dims(&[4]);
2052
2053        // POSITIVE CONTROL, load-bearing: a narrow float that IS wired must
2054        // succeed. Without it, "F8E5M2 declined" is equally satisfied by a
2055        // `cpu_zeros` broken for every dtype, and the tripwire would pass
2056        // while asserting nothing about E5M2 at all.
2057        assert!(
2058            cpu_zeros(&shape, DType::F8E4M3).is_ok(),
2059            "CONTROL FAILED: cpu_zeros declined F8E4M3, which IS wired. The F8E5M2 assertion below is therefore vacuous and this test proves nothing -- fix the control before reading the tripwire."
2060        );
2061
2062        assert!(
2063            cpu_zeros(&shape, DType::F8E5M2).is_err(),
2064            "GAP-275 TRIPWIRE FIRED: cpu_zeros now ACCEPTS DType::F8E5M2, so the E5M2 storage path has been wired. E5M2 conversions have NO boundary coverage, and float8 0.7.0 is_infinite returns true for 0x7E (the FINITE 448) on both f8 types. Add boundary tests modelled on cast_f32_f8e4m3_boundary_saturates_and_preserves_nan in byte_kernels.rs, then delete this test. See GAP-275."
2065        );
2066    }
2067}
2068
2069#[cfg(test)]
2070mod backend_runtime_tests {
2071    use super::*;
2072    use fuel_backend_contract::backend::BackendRuntime;
2073    use fuel_ir::backend::FitStatus;
2074
2075    /// CpuBackendDevice implements BackendRuntime. On a supported
2076    /// platform (Linux/Windows) reports `Some(_)`; on an unsupported
2077    /// platform reports `None`. Both are valid honest outcomes per
2078    /// the trait contract.
2079    #[test]
2080    fn cpu_backend_device_implements_backend_runtime() {
2081        let dev = CpuBackendDevice;
2082        let avail = dev.available_bytes();
2083        let total = dev.total_bytes();
2084        // Either both `Some` or both `None` — system_memory's
2085        // capture_platform never mixes them.
2086        match (avail, total) {
2087            (Some(a), Some(t)) => {
2088                assert!(t > 0);
2089                assert!(a <= t);
2090            }
2091            (None, None) => {
2092                // Unsupported platform (macOS / BSD / etc.) — valid.
2093            }
2094            mixed => panic!("inconsistent availability report: {mixed:?}"),
2095        }
2096    }
2097
2098    /// would_fit defers to the default trait method which derives
2099    /// from available + total. On supported platforms, asking for
2100    /// a 1-byte allocation should be Comfortable; on unsupported,
2101    /// Unknown.
2102    #[test]
2103    fn cpu_backend_device_would_fit_smoke() {
2104        let dev = CpuBackendDevice;
2105        let status = dev.would_fit(1);
2106        match (dev.available_bytes(), status) {
2107            (Some(_), FitStatus::Comfortable | FitStatus::Tight) => {
2108                // Either is fine — depends on host load.
2109            }
2110            (None, FitStatus::Unknown) => {
2111                // Unsupported platform — also fine.
2112            }
2113            other => panic!("unexpected (signal, status) pair: {other:?}"),
2114        }
2115    }
2116}
2117
2118#[cfg(test)]
2119mod binary_op_nan_tests {
2120    //! NaN-propagation for `cpu_binary_op`'s Maximum/Minimum on the typed
2121    //! `HostBuffer` path. The ratified `byte_kernels::tests::
2122    //! maximum_minimum_*_propagates_nan` tests exercise the BYTE path, not this
2123    //! one -- confirmed by sabotage (dropping the `b.is_nan()` term here leaves
2124    //! them green). This closes a pre-existing coverage hole in a pinned
2125    //! convention (Relu/Maximum/Minimum are NaN-propagating, torch semantics).
2126    use super::*;
2127
2128    fn f32_buf(v: &[f32]) -> HostBuffer {
2129        HostBuffer::F32(v.to_vec())
2130    }
2131
2132    /// KISS-OPS-6.16-0009 on the DYN path, which the chassis tests cannot reach.
2133    ///
2134    /// ⚠️ THIS FILE HAS ITS OWN HAND-WRITTEN PER-DTYPE ARMS, so a fix in
2135    /// `chassis/unary.rs` does NOT cover it — the same two-path split this
2136    /// module already documents for `Relu`. `abs` was the live half: `half`
2137    /// ships TWO `abs` per narrow type under one name (`FloatCore::abs` raw-bit,
2138    /// `Float::abs` promoting), there is no inherent `abs`, and this file has
2139    /// `use num_traits::Float as _` at the top — so `v.abs()` bound to the
2140    /// PROMOTING one and quieted a signalling NaN.
2141    ///
2142    /// The POSITIVE CONTROL is required: it asserts the promoting form still
2143    /// quiets in this build, so "still signalling" is evidence rather than an
2144    /// accident of the fixture or of a `half` release that stopped quieting.
2145    #[test]
2146    fn cpu_unary_abs_moves_a_signalling_nan_without_quieting_it() {
2147        const QUIET: u16 = 0x0040;
2148        let s = bf16::from_bits(0x7F81);
2149        assert_eq!(s.to_bits() & QUIET, 0, "fixture must be SIGNALLING");
2150
2151        // CONTROL: the promoting form quiets. If this stops being true the
2152        // assertion below proves nothing.
2153        assert_ne!(
2154            bf16::from_f32(s.to_f32().abs()).to_bits() & QUIET,
2155            0,
2156            "control failed: the promoting form no longer quiets, so the dyn-path              assertion below is vacuous"
2157        );
2158
2159        let buf = HostBuffer::BF16(vec![s]);
2160        let l = Layout::contiguous(Shape::from_dims(&[1]));
2161        let out = cpu_unary_op(&buf, &l, UnaryOp::Abs).expect("abs");
2162        let HostBuffer::BF16(got) = out else {
2163            panic!("expected BF16 output");
2164        };
2165        assert_eq!(
2166            got[0].to_bits(),
2167            s.to_bits() & 0x7FFF,
2168            "dyn-path abs must clear the sign bit and PRESERVE the rest              (0x{:04X} -> 0x{:04X}); it bound to the promoting `Float::abs` and              quieted the NaN. See KISS-OPS-6.16-0009.",
2169            s.to_bits(),
2170            got[0].to_bits()
2171        );
2172        assert_eq!(
2173            got[0].to_bits() & QUIET,
2174            0,
2175            "dyn-path abs QUIETED a signalling NaN"
2176        );
2177    }
2178
2179    #[test]
2180    fn cpu_binary_op_maximum_minimum_propagate_nan() {
2181        let n = f32::NAN;
2182        // Every position has exactly one NaN operand, alternating sides, so a
2183        // NaN dropped from either the a- or b- side would surface here.
2184        let a = f32_buf(&[1.0, n, 3.0, n]);
2185        let b = f32_buf(&[n, 2.0, n, 4.0]);
2186        let l = Layout::contiguous(Shape::from_dims(&[4]));
2187
2188        for op in [BinaryOp::Maximum, BinaryOp::Minimum] {
2189            let out = cpu_binary_op(&a, &b, &l, &l, op).expect("binary op");
2190            let HostBuffer::F32(got) = out else {
2191                panic!("expected F32 output for {op:?}");
2192            };
2193            for (i, &g) in got.iter().enumerate() {
2194                assert!(
2195                    g.is_nan(),
2196                    "{op:?} at {i}: NaN must propagate (torch convention), got {g}"
2197                );
2198            }
2199        }
2200    }
2201
2202    #[test]
2203    fn cpu_binary_op_maximum_minimum_basic() {
2204        // Non-NaN magnitude behaviour on the same path: max picks the larger,
2205        // min the smaller.
2206        let a = f32_buf(&[1.0, 5.0]);
2207        let b = f32_buf(&[2.0, 3.0]);
2208        let l = Layout::contiguous(Shape::from_dims(&[2]));
2209
2210        let HostBuffer::F32(max) = cpu_binary_op(&a, &b, &l, &l, BinaryOp::Maximum).expect("max")
2211        else {
2212            panic!("expected F32");
2213        };
2214        assert_eq!(max, vec![2.0, 5.0]);
2215
2216        let HostBuffer::F32(min) = cpu_binary_op(&a, &b, &l, &l, BinaryOp::Minimum).expect("min")
2217        else {
2218            panic!("expected F32");
2219        };
2220        assert_eq!(min, vec![1.0, 3.0]);
2221    }
2222
2223    /// KISS-OPS-6.2-0002 finding #3: integer Add/Sub/Mul used to compute in
2224    /// f64 then cast back with a float->int cast, which SATURATES on overflow
2225    /// rather than wrapping mod 2^n. `200u8 + 100u8` is the textbook case:
2226    /// the true wrapped result is `44` (`300 mod 256`); the old code produced
2227    /// `255` (`300.0 as u8` saturates).
2228    #[test]
2229    fn cpu_binary_op_integer_add_sub_mul_wrap_mod_2n() {
2230        let l = Layout::contiguous(Shape::from_dims(&[1]));
2231
2232        let a = HostBuffer::U8(vec![200]);
2233        let b = HostBuffer::U8(vec![100]);
2234        let HostBuffer::U8(sum) = cpu_binary_op(&a, &b, &l, &l, BinaryOp::Add).expect("add") else {
2235            panic!("expected U8");
2236        };
2237        assert_eq!(
2238            sum,
2239            vec![44],
2240            "200u8 + 100u8 must wrap to 44 (300 mod 256), not saturate"
2241        );
2242
2243        let a = HostBuffer::U8(vec![10]);
2244        let b = HostBuffer::U8(vec![20]);
2245        let HostBuffer::U8(diff) = cpu_binary_op(&a, &b, &l, &l, BinaryOp::Sub).expect("sub")
2246        else {
2247            panic!("expected U8");
2248        };
2249        assert_eq!(
2250            diff,
2251            vec![246],
2252            "10u8 - 20u8 must wrap to 246, not saturate to 0"
2253        );
2254
2255        let a = HostBuffer::I32(vec![i32::MAX]);
2256        let b = HostBuffer::I32(vec![1]);
2257        let HostBuffer::I32(sum) = cpu_binary_op(&a, &b, &l, &l, BinaryOp::Add).expect("add")
2258        else {
2259            panic!("expected I32");
2260        };
2261        assert_eq!(
2262            sum,
2263            vec![i32::MIN],
2264            "i32::MAX + 1 must wrap to i32::MIN, not saturate to i32::MAX"
2265        );
2266
2267        let a = HostBuffer::I32(vec![100_000]);
2268        let b = HostBuffer::I32(vec![100_000]);
2269        let HostBuffer::I32(prod) = cpu_binary_op(&a, &b, &l, &l, BinaryOp::Mul).expect("mul")
2270        else {
2271            panic!("expected I32");
2272        };
2273        assert_eq!(
2274            prod,
2275            vec![100_000i32.wrapping_mul(100_000)],
2276            "100_000 * 100_000 (exceeds i32 range) must wrap, not saturate"
2277        );
2278
2279        // Positive control: Maximum/Minimum (unchanged by this fix) still
2280        // behave correctly on integers after the signature change.
2281        let a = HostBuffer::I32(vec![5]);
2282        let b = HostBuffer::I32(vec![9]);
2283        let HostBuffer::I32(max) = cpu_binary_op(&a, &b, &l, &l, BinaryOp::Maximum).expect("max")
2284        else {
2285            panic!("expected I32");
2286        };
2287        assert_eq!(
2288            max,
2289            vec![9],
2290            "positive control: Maximum still correct on integers"
2291        );
2292    }
2293
2294    /// KISS-Ops §6.4-0005 finding #4: negating or taking the absolute value
2295    /// of the signed minimum has no positive counterpart in two's complement
2296    /// (`-i32::MIN` doesn't fit in `i32`) -- the spec-mandated result is
2297    /// `INT_MIN` itself, unconditionally. The un-fixed code used plain `-v`/
2298    /// `v.abs()`, which panics under overflow-checks (debug builds) and is a
2299    /// silent trap otherwise.
2300    #[test]
2301    fn cpu_unary_neg_abs_on_int_min_wraps_to_int_min_without_panicking() {
2302        let l = Layout::contiguous(Shape::from_dims(&[1]));
2303
2304        for dtype_buf in [
2305            HostBuffer::I16(vec![i16::MIN]),
2306            HostBuffer::I32(vec![i32::MIN]),
2307            HostBuffer::I64(vec![i64::MIN]),
2308        ] {
2309            let neg = cpu_unary_op(&dtype_buf, &l, UnaryOp::Neg).expect("neg must not panic");
2310            let abs = cpu_unary_op(&dtype_buf, &l, UnaryOp::Abs).expect("abs must not panic");
2311            match (&dtype_buf, &neg, &abs) {
2312                (HostBuffer::I16(_), HostBuffer::I16(n), HostBuffer::I16(a)) => {
2313                    assert_eq!(n[0], i16::MIN, "neg(i16::MIN) must wrap to i16::MIN");
2314                    assert_eq!(a[0], i16::MIN, "abs(i16::MIN) must wrap to i16::MIN");
2315                }
2316                (HostBuffer::I32(_), HostBuffer::I32(n), HostBuffer::I32(a)) => {
2317                    assert_eq!(n[0], i32::MIN, "neg(i32::MIN) must wrap to i32::MIN");
2318                    assert_eq!(a[0], i32::MIN, "abs(i32::MIN) must wrap to i32::MIN");
2319                }
2320                (HostBuffer::I64(_), HostBuffer::I64(n), HostBuffer::I64(a)) => {
2321                    assert_eq!(n[0], i64::MIN, "neg(i64::MIN) must wrap to i64::MIN");
2322                    assert_eq!(a[0], i64::MIN, "abs(i64::MIN) must wrap to i64::MIN");
2323                }
2324                _ => panic!("dtype mismatch in neg/abs output"),
2325            }
2326        }
2327
2328        // Positive control: a normal negative value still negates correctly,
2329        // so the wrapping behavior above is specific to the MIN edge case.
2330        let buf = HostBuffer::I32(vec![-5]);
2331        let HostBuffer::I32(n) = cpu_unary_op(&buf, &l, UnaryOp::Neg).expect("neg") else {
2332            panic!("expected I32");
2333        };
2334        assert_eq!(n, vec![5], "positive control: neg(-5) is still 5");
2335    }
2336}
2337
2338#[cfg(test)]
2339mod rand_bounds_tests {
2340    use super::*;
2341
2342    /// An `f32` draw just below `max` can round UP to `max` in a narrower
2343    /// dtype. 200k draws over `[0, 1)` hit that ~2^-9 band hundreds of times
2344    /// in bf16 (and ~2^-12 in f16), so any escape is all but certain.
2345    #[test]
2346    fn low_precision_uniform_stays_inside_half_open_range() {
2347        let shape = Shape::from((200_000,));
2348        match cpu_rand_uniform(&shape, DType::BF16, 0.0, 1.0).unwrap() {
2349            HostBuffer::BF16(v) => assert!(v.iter().all(|x| (0.0..1.0).contains(&x.to_f32()))),
2350            _ => panic!("wrong buffer"),
2351        }
2352        match cpu_rand_uniform(&shape, DType::F16, 0.0, 1.0).unwrap() {
2353            HostBuffer::F16(v) => assert!(v.iter().all(|x| (0.0..1.0).contains(&x.to_f32()))),
2354            _ => panic!("wrong buffer"),
2355        }
2356    }
2357
2358    /// Sabotage sibling: the naive narrowing the finding describes DOES escape
2359    /// the range on the same draw count, so the test above can fail.
2360    #[test]
2361    fn naive_narrowing_escapes_the_range() {
2362        use rand::RngExt;
2363        let mut rng = rand::rng();
2364        let u = rand::distr::Uniform::new(0.0f32, 1.0).unwrap();
2365        let escaped = (0..200_000)
2366            .filter(|_| bf16::from_f32(rng.sample::<f32, _>(u)).to_f32() >= 1.0)
2367            .count();
2368        assert!(escaped > 0, "control found no escape in 200k draws");
2369    }
2370}