use bytemuck::Pod;
use crate::byte_storage::CpuStorageBytes;
use fuel_ir::{Error, Result};
pub trait ReduceOp<T: Copy> {
type Acc: Copy;
fn init() -> Self::Acc;
fn fold(acc: Self::Acc, x: T) -> Self::Acc;
fn finalize(acc: Self::Acc, count: usize) -> T;
fn validate_count(_name: &str, _count: usize) -> Result<()> {
Ok(())
}
}
pub struct Sum;
pub struct Max;
pub struct Min;
pub struct Mean;
impl ReduceOp<f32> for Sum {
type Acc = f32;
fn init() -> f32 {
0.0
}
fn fold(acc: f32, x: f32) -> f32 {
acc + x
}
fn finalize(acc: f32, _: usize) -> f32 {
acc
}
}
impl ReduceOp<f64> for Sum {
type Acc = f64;
fn init() -> f64 {
0.0
}
fn fold(acc: f64, x: f64) -> f64 {
acc + x
}
fn finalize(acc: f64, _: usize) -> f64 {
acc
}
}
impl ReduceOp<half::bf16> for Sum {
type Acc = f32; fn init() -> f32 {
0.0
}
fn fold(acc: f32, x: half::bf16) -> f32 {
acc + x.to_f32()
}
fn finalize(acc: f32, _: usize) -> half::bf16 {
half::bf16::from_f32(acc)
}
}
impl ReduceOp<half::f16> for Sum {
type Acc = f32; fn init() -> f32 {
0.0
}
fn fold(acc: f32, x: half::f16) -> f32 {
acc + x.to_f32()
}
fn finalize(acc: f32, _: usize) -> half::f16 {
half::f16::from_f32(acc)
}
}
impl ReduceOp<f32> for Max {
type Acc = f32;
fn init() -> f32 {
f32::NEG_INFINITY
}
fn fold(acc: f32, x: f32) -> f32 {
<super::binary::Maximum as super::binary::BinaryOpCore>::f32(acc, x)
}
fn finalize(acc: f32, _: usize) -> f32 {
acc
}
}
impl ReduceOp<f64> for Max {
type Acc = f64;
fn init() -> f64 {
f64::NEG_INFINITY
}
fn fold(acc: f64, x: f64) -> f64 {
<super::binary::Maximum as super::binary::BinaryOpCore>::f64(acc, x)
}
fn finalize(acc: f64, _: usize) -> f64 {
acc
}
}
impl ReduceOp<half::bf16> for Max {
type Acc = f32; fn init() -> f32 {
f32::NEG_INFINITY
}
fn fold(acc: f32, x: half::bf16) -> f32 {
<super::binary::Maximum as super::binary::BinaryOpCore>::f32(acc, x.to_f32())
}
fn finalize(acc: f32, _: usize) -> half::bf16 {
half::bf16::from_f32(acc)
}
}
impl ReduceOp<half::f16> for Max {
type Acc = f32;
fn init() -> f32 {
f32::NEG_INFINITY
}
fn fold(acc: f32, x: half::f16) -> f32 {
<super::binary::Maximum as super::binary::BinaryOpCore>::f32(acc, x.to_f32())
}
fn finalize(acc: f32, _: usize) -> half::f16 {
half::f16::from_f32(acc)
}
}
impl ReduceOp<f32> for Min {
type Acc = f32;
fn init() -> f32 {
f32::INFINITY
}
fn fold(acc: f32, x: f32) -> f32 {
<super::binary::Minimum as super::binary::BinaryOpCore>::f32(acc, x)
}
fn finalize(acc: f32, _: usize) -> f32 {
acc
}
}
impl ReduceOp<f64> for Min {
type Acc = f64;
fn init() -> f64 {
f64::INFINITY
}
fn fold(acc: f64, x: f64) -> f64 {
<super::binary::Minimum as super::binary::BinaryOpCore>::f64(acc, x)
}
fn finalize(acc: f64, _: usize) -> f64 {
acc
}
}
impl ReduceOp<half::bf16> for Min {
type Acc = f32;
fn init() -> f32 {
f32::INFINITY
}
fn fold(acc: f32, x: half::bf16) -> f32 {
<super::binary::Minimum as super::binary::BinaryOpCore>::f32(acc, x.to_f32())
}
fn finalize(acc: f32, _: usize) -> half::bf16 {
half::bf16::from_f32(acc)
}
}
impl ReduceOp<half::f16> for Min {
type Acc = f32;
fn init() -> f32 {
f32::INFINITY
}
fn fold(acc: f32, x: half::f16) -> f32 {
<super::binary::Minimum as super::binary::BinaryOpCore>::f32(acc, x.to_f32())
}
fn finalize(acc: f32, _: usize) -> half::f16 {
half::f16::from_f32(acc)
}
}
fn mean_divisor_check(name: &str, count: usize) -> Result<()> {
if count == 0 {
return Err(Error::Msg(format!("{name}: divisor zero (reduced dim has size 0)",)).bt());
}
Ok(())
}
impl ReduceOp<f32> for Mean {
type Acc = f32;
fn init() -> f32 {
0.0
}
fn fold(acc: f32, x: f32) -> f32 {
acc + x
}
fn finalize(acc: f32, count: usize) -> f32 {
acc / count as f32
}
fn validate_count(name: &str, count: usize) -> Result<()> {
mean_divisor_check(name, count)
}
}
impl ReduceOp<f64> for Mean {
type Acc = f64;
fn init() -> f64 {
0.0
}
fn fold(acc: f64, x: f64) -> f64 {
acc + x
}
fn finalize(acc: f64, count: usize) -> f64 {
acc / count as f64
}
fn validate_count(name: &str, count: usize) -> Result<()> {
mean_divisor_check(name, count)
}
}
impl ReduceOp<half::bf16> for Mean {
type Acc = f32;
fn init() -> f32 {
0.0
}
fn fold(acc: f32, x: half::bf16) -> f32 {
acc + x.to_f32()
}
fn finalize(acc: f32, count: usize) -> half::bf16 {
half::bf16::from_f32(acc / count as f32)
}
fn validate_count(name: &str, count: usize) -> Result<()> {
mean_divisor_check(name, count)
}
}
impl ReduceOp<half::f16> for Mean {
type Acc = f32;
fn init() -> f32 {
0.0
}
fn fold(acc: f32, x: half::f16) -> f32 {
acc + x.to_f32()
}
fn finalize(acc: f32, count: usize) -> half::f16 {
half::f16::from_f32(acc / count as f32)
}
fn validate_count(name: &str, count: usize) -> Result<()> {
mean_divisor_check(name, count)
}
}
pub fn reduce<T, R>(
name: &str,
input: &CpuStorageBytes,
output: &mut CpuStorageBytes,
input_shape: &[usize],
reduce_dims: &[usize],
) -> Result<()>
where
T: Copy + Pod,
R: ReduceOp<T>,
{
let (total_input, kept) = check_reduce_shape(
name,
input,
output,
input_shape,
reduce_dims,
std::mem::size_of::<T>(),
)?;
let count: usize = reduce_dims.iter().map(|&d| input_shape[d]).product();
R::validate_count(name, count)?;
let in_view: &[T] = input.as_slice()?;
let out_view: &mut [T] = output.as_slice_mut()?;
let total_output = out_view.len();
let mut acc: Vec<R::Acc> = (0..total_output).map(|_| R::init()).collect();
let mut mi = vec![0usize; input_shape.len()];
#[expect(
clippy::needless_range_loop,
reason = "flat index is decoded to a multi-index (decode_multi_index) and scattered into acc[oi], not a sequential scan of in_view; enumerate would obscure the mapping and risk an iteration-count mismatch"
)]
for flat in 0..total_input {
decode_multi_index(flat, input_shape, &mut mi);
let oi = output_index(input_shape, &kept, &mi);
acc[oi] = R::fold(acc[oi], in_view[flat]);
}
for (slot, &a) in out_view.iter_mut().zip(&acc) {
*slot = R::finalize(a, count);
}
Ok(())
}
pub fn reduce_to<T, R>(
name: &str,
input: &CpuStorageBytes,
output: &mut CpuStorageBytes,
input_shape: &[usize],
output_shape: &[usize],
) -> Result<()>
where
T: Copy + Pod,
R: ReduceOp<T>,
{
let padded = align_reduce_to(name, input_shape, output_shape)?;
let elem = std::mem::size_of::<T>();
let in_elems: usize = input_shape.iter().product();
let out_elems: usize = output_shape.iter().product();
if input.len_bytes() != in_elems.saturating_mul(elem)
|| output.len_bytes() != out_elems.saturating_mul(elem)
{
return Err(Error::Msg(format!(
"{name}: bytes mismatch (in {in_elems} elems, out {out_elems} elems)",
))
.bt());
}
let mut count: usize = 1;
for (axis, &p) in padded.iter().enumerate() {
if p == 1 {
count = count.saturating_mul(input_shape[axis]);
}
}
R::validate_count(name, count)?;
let in_view: &[T] = input.as_slice()?;
let out_view: &mut [T] = output.as_slice_mut()?;
let mut acc: Vec<R::Acc> = (0..out_elems).map(|_| R::init()).collect();
let rank = input_shape.len();
let mut in_strides = vec![1_usize; rank];
for i in (0..rank.saturating_sub(1)).rev() {
in_strides[i] = in_strides[i + 1] * input_shape[i + 1];
}
let mut out_strides_padded = vec![1_usize; rank];
for i in (0..rank.saturating_sub(1)).rev() {
out_strides_padded[i] = out_strides_padded[i + 1] * padded[i + 1];
}
#[expect(
clippy::needless_range_loop,
reason = "in_flat drives explicit multi-index reconstruction (rem = in_flat) and strided scatter, not a sequential scan; enumerate would obscure the mapping and risk an iteration-count mismatch"
)]
for in_flat in 0..in_elems {
let mut out_flat = 0_usize;
let mut rem = in_flat;
for axis in 0..rank {
let coord = rem / in_strides[axis];
rem %= in_strides[axis];
let out_coord = if padded[axis] == 1 { 0 } else { coord };
out_flat += out_coord * out_strides_padded[axis];
}
acc[out_flat] = R::fold(acc[out_flat], in_view[in_flat]);
}
for (slot, &a) in out_view.iter_mut().zip(&acc) {
*slot = R::finalize(a, count);
}
Ok(())
}
fn align_reduce_to(
name: &str,
input_shape: &[usize],
output_shape: &[usize],
) -> Result<Vec<usize>> {
if output_shape.len() > input_shape.len() {
return Err(Error::Msg(format!(
"{name}: output rank {} exceeds input rank {}",
output_shape.len(),
input_shape.len(),
))
.bt());
}
let pad = input_shape.len() - output_shape.len();
let mut padded = vec![1_usize; pad];
padded.extend_from_slice(output_shape);
for (i, (&s, &t)) in input_shape.iter().zip(padded.iter()).enumerate() {
if t != 1 && t != s {
return Err(Error::Msg(format!(
"{name}: axis {i} target {t} must be 1 or input {s}",
))
.bt());
}
}
Ok(padded)
}
fn check_reduce_shape(
name: &str,
input: &CpuStorageBytes,
output: &CpuStorageBytes,
input_shape: &[usize],
reduce_dims: &[usize],
elem_size: usize,
) -> Result<(usize, Vec<usize>)> {
let total_input: usize = input_shape.iter().product();
if input.len_bytes() != total_input.saturating_mul(elem_size) {
return Err(Error::Msg(format!(
"{name}: input bytes={} doesn't match shape {:?}",
input.len_bytes(),
input_shape,
))
.bt());
}
let rank = input_shape.len();
for &d in reduce_dims {
if d >= rank {
return Err(Error::Msg(format!(
"{name}: reduce dim {d} out of range for rank {rank}",
))
.bt());
}
}
if reduce_dims.windows(2).any(|w| w[0] >= w[1]) {
return Err(Error::Msg(format!(
"{name}: reduce dims {reduce_dims:?} must be sorted ascending + unique",
))
.bt());
}
let kept: Vec<usize> = (0..rank).filter(|d| !reduce_dims.contains(d)).collect();
let output_count: usize = kept.iter().map(|&d| input_shape[d]).product();
if output.len_bytes() != output_count.saturating_mul(elem_size) {
return Err(Error::Msg(format!(
"{name}: output bytes={} doesn't match reduced shape (kept dims {:?}, count {output_count})",
output.len_bytes(),
kept,
))
.bt());
}
Ok((total_input, kept))
}
fn output_index(input_shape: &[usize], kept: &[usize], multi_index: &[usize]) -> usize {
let mut out_flat = 0;
for &d in kept {
out_flat = out_flat * input_shape[d] + multi_index[d];
}
out_flat
}
fn decode_multi_index(flat: usize, input_shape: &[usize], multi_index: &mut [usize]) {
let mut rem = flat;
for d in (0..input_shape.len()).rev() {
let s = input_shape[d];
multi_index[d] = rem % s;
rem /= s;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reduce_op_sum_f32_zero_init_fold_matches_iter_sum() {
let xs = [1.0_f32, 2.5, -3.0, 4.0];
let mut acc = <Sum as ReduceOp<f32>>::init();
for &x in &xs {
acc = <Sum as ReduceOp<f32>>::fold(acc, x);
}
let out = <Sum as ReduceOp<f32>>::finalize(acc, xs.len());
assert_eq!(out, xs.iter().sum::<f32>());
}
#[test]
fn reduce_op_max_f32_neg_inf_init_finds_largest() {
let xs = [1.0_f32, -5.0, 3.0, 2.0];
let mut acc = <Max as ReduceOp<f32>>::init();
for &x in &xs {
acc = <Max as ReduceOp<f32>>::fold(acc, x);
}
let out = <Max as ReduceOp<f32>>::finalize(acc, xs.len());
assert_eq!(out, 3.0);
}
#[test]
fn reduce_op_min_f32_pos_inf_init_finds_smallest() {
let xs = [1.0_f32, -5.0, 3.0, 2.0];
let mut acc = <Min as ReduceOp<f32>>::init();
for &x in &xs {
acc = <Min as ReduceOp<f32>>::fold(acc, x);
}
let out = <Min as ReduceOp<f32>>::finalize(acc, xs.len());
assert_eq!(out, -5.0);
}
#[test]
fn reduce_op_max_min_f32_propagate_nan() {
let xs = [1.0_f32, f32::NAN, 2.0];
let mut max_acc = <Max as ReduceOp<f32>>::init();
let mut min_acc = <Min as ReduceOp<f32>>::init();
for &x in &xs {
max_acc = <Max as ReduceOp<f32>>::fold(max_acc, x);
min_acc = <Min as ReduceOp<f32>>::fold(min_acc, x);
}
let max_out = <Max as ReduceOp<f32>>::finalize(max_acc, xs.len());
let min_out = <Min as ReduceOp<f32>>::finalize(min_acc, xs.len());
assert!(
max_out.is_nan(),
"Max must propagate a NaN input, got {max_out}"
);
assert!(
min_out.is_nan(),
"Min must propagate a NaN input, got {min_out}"
);
let xs64 = [1.0_f64, f64::NAN, 2.0];
let mut max64 = <Max as ReduceOp<f64>>::init();
for &x in &xs64 {
max64 = <Max as ReduceOp<f64>>::fold(max64, x);
}
assert!(
<Max as ReduceOp<f64>>::finalize(max64, xs64.len()).is_nan(),
"f64 Max must propagate NaN"
);
let xs16 = [
half::bf16::from_f32(1.0),
half::bf16::NAN,
half::bf16::from_f32(2.0),
];
let mut max16 = <Max as ReduceOp<half::bf16>>::init();
for &x in &xs16 {
max16 = <Max as ReduceOp<half::bf16>>::fold(max16, x);
}
let out16: half::bf16 = <Max as ReduceOp<half::bf16>>::finalize(max16, xs16.len());
assert!(out16.is_nan(), "bf16 Max must propagate NaN");
let xs_clean = [1.0_f32, -5.0, 3.0, 2.0];
let mut acc = <Max as ReduceOp<f32>>::init();
for &x in &xs_clean {
acc = <Max as ReduceOp<f32>>::fold(acc, x);
}
assert_eq!(
<Max as ReduceOp<f32>>::finalize(acc, xs_clean.len()),
3.0,
"positive control: Max still finds the correct extremum without NaN"
);
}
#[test]
fn reduce_op_mean_f32_divides_by_count_in_finalize() {
let xs = [2.0_f32, 4.0, 6.0, 8.0];
let mut acc = <Mean as ReduceOp<f32>>::init();
for &x in &xs {
acc = <Mean as ReduceOp<f32>>::fold(acc, x);
}
let out = <Mean as ReduceOp<f32>>::finalize(acc, xs.len());
assert_eq!(out, 5.0);
}
#[test]
fn reduce_op_mean_rejects_zero_count() {
let r = <Mean as ReduceOp<f32>>::validate_count("test", 0);
assert!(r.is_err());
let r = <Mean as ReduceOp<f32>>::validate_count("test", 5);
assert!(r.is_ok());
}
#[test]
fn reduce_to_sum_f32_collapses_inner_axis() {
let input = CpuStorageBytes::from_slice(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut output = CpuStorageBytes::from_zero_bytes(2 * 4);
reduce_to::<f32, Sum>("test_sum_to", &input, &mut output, &[2, 3], &[2, 1])
.expect("reduce_to sum_f32");
let r: &[f32] = output.as_slice().unwrap();
assert_eq!(r, &[6.0, 15.0]);
}
#[test]
fn reduce_to_max_f32_collapses_all_to_scalar() {
let input = CpuStorageBytes::from_slice(&[1.0_f32, -5.0, 3.0, 2.0, 0.0, -1.0]);
let mut output = CpuStorageBytes::from_zero_bytes(4);
reduce_to::<f32, Max>("test_max_to", &input, &mut output, &[2, 3], &[1, 1])
.expect("reduce_to max_f32");
let r: &[f32] = output.as_slice().unwrap();
assert_eq!(r, &[3.0]);
}
#[test]
fn reduce_to_pad_rank_with_leading_ones() {
let input = CpuStorageBytes::from_slice(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut output = CpuStorageBytes::from_zero_bytes(3 * 4);
reduce_to::<f32, Sum>("test_pad", &input, &mut output, &[2, 3], &[3])
.expect("reduce_to sum_f32 padded");
let r: &[f32] = output.as_slice().unwrap();
assert_eq!(r, &[5.0, 7.0, 9.0]);
}
#[test]
fn reduce_to_rejects_incompatible_target_axis() {
let input = CpuStorageBytes::from_slice(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut output = CpuStorageBytes::from_zero_bytes(2 * 2 * 4);
let r = reduce_to::<f32, Sum>("test_bad", &input, &mut output, &[2, 3], &[2, 2]);
assert!(r.is_err());
}
#[test]
fn reduce_op_sum_bf16_accumulator_is_f32() {
let xs: Vec<half::bf16> = (0..1000).map(|_| half::bf16::from_f32(0.1)).collect();
let mut acc = <Sum as ReduceOp<half::bf16>>::init();
for &x in &xs {
acc = <Sum as ReduceOp<half::bf16>>::fold(acc, x);
}
let out = <Sum as ReduceOp<half::bf16>>::finalize(acc, xs.len()).to_f32();
assert!(out > 95.0 && out < 105.0, "got {out}");
}
}