#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
#[non_exhaustive]
pub enum SubgroupReduceOp {
Add,
Mul,
Min,
Max,
And,
Or,
Xor,
}
impl SubgroupReduceOp {
pub const ALL: [Self; 7] = [
Self::Add,
Self::Mul,
Self::Min,
Self::Max,
Self::And,
Self::Or,
Self::Xor,
];
#[must_use]
pub const fn builtin_wire_tag(self) -> u8 {
match self {
Self::Add => 0x01,
Self::Mul => 0x02,
Self::Min => 0x03,
Self::Max => 0x04,
Self::And => 0x05,
Self::Or => 0x06,
Self::Xor => 0x07,
}
}
pub fn from_wire_tag(tag: u8) -> Result<Self, String> {
match tag {
0x01 => Ok(Self::Add),
0x02 => Ok(Self::Mul),
0x03 => Ok(Self::Min),
0x04 => Ok(Self::Max),
0x05 => Ok(Self::And),
0x06 => Ok(Self::Or),
0x07 => Ok(Self::Xor),
value => Err(format!(
"Fix: unknown SubgroupReduceOp tag {value}; use a Program serializer compatible with this vyre version."
)),
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Add => "add",
Self::Mul => "mul",
Self::Min => "min",
Self::Max => "max",
Self::And => "and",
Self::Or => "or",
Self::Xor => "xor",
}
}
#[must_use]
pub const fn is_bitwise(self) -> bool {
matches!(self, Self::And | Self::Or | Self::Xor)
}
#[must_use]
pub fn reduce_u32(self, lanes: impl IntoIterator<Item = u32>) -> u32 {
let lanes = lanes.into_iter();
match self {
Self::Add => lanes.fold(0u32, u32::wrapping_add),
Self::Mul => lanes.fold(1u32, u32::wrapping_mul),
Self::Min => lanes.fold(u32::MAX, u32::min),
Self::Max => lanes.fold(0u32, u32::max),
Self::And => lanes.fold(u32::MAX, |acc, lane| acc & lane),
Self::Or => lanes.fold(0u32, |acc, lane| acc | lane),
Self::Xor => lanes.fold(0u32, |acc, lane| acc ^ lane),
}
}
#[must_use]
pub fn f32_identity(self) -> Option<f32> {
match self {
Self::Add => Some(0.0),
Self::Mul => Some(1.0),
Self::Min => Some(f32::INFINITY),
Self::Max => Some(f32::NEG_INFINITY),
Self::And | Self::Or | Self::Xor => None,
}
}
#[must_use]
pub fn combine_f32(self, acc: f32, lane: f32) -> Option<f32> {
match self {
Self::Add => Some(acc + lane),
Self::Mul => Some(acc * lane),
Self::Min => Some(acc.min(lane)),
Self::Max => Some(acc.max(lane)),
Self::And | Self::Or | Self::Xor => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_tag_roundtrips_every_op() {
for op in SubgroupReduceOp::ALL {
let tag = op.builtin_wire_tag();
assert_eq!(
SubgroupReduceOp::from_wire_tag(tag).unwrap(),
op,
"Fix: SubgroupReduceOp wire tag {tag:#04x} must round-trip"
);
}
}
#[test]
fn wire_tags_are_distinct_and_dense() {
let tags: Vec<u8> = SubgroupReduceOp::ALL
.iter()
.map(|op| op.builtin_wire_tag())
.collect();
assert_eq!(tags, vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
}
#[test]
fn unknown_tag_is_rejected_loudly() {
let err = SubgroupReduceOp::from_wire_tag(0x42).unwrap_err();
assert!(err.contains("unknown SubgroupReduceOp tag 66"), "{err}");
}
#[test]
fn reduce_u32_computes_exact_values() {
let lanes = [3u32, 1, 4, 1, 5];
assert_eq!(SubgroupReduceOp::Add.reduce_u32(lanes), 14);
assert_eq!(SubgroupReduceOp::Mul.reduce_u32(lanes), 60);
assert_eq!(SubgroupReduceOp::Min.reduce_u32(lanes), 1);
assert_eq!(SubgroupReduceOp::Max.reduce_u32(lanes), 5);
assert_eq!(SubgroupReduceOp::And.reduce_u32([0b1100u32, 0b1010]), 0b1000);
assert_eq!(SubgroupReduceOp::Or.reduce_u32([0b1100u32, 0b1010]), 0b1110);
assert_eq!(SubgroupReduceOp::Xor.reduce_u32([0b1100u32, 0b1010]), 0b0110);
}
#[test]
fn reduce_u32_add_wraps_like_the_isa() {
assert_eq!(SubgroupReduceOp::Add.reduce_u32([u32::MAX, 1]), 0);
assert_eq!(SubgroupReduceOp::Mul.reduce_u32([u32::MAX, 2]), u32::MAX - 1);
}
#[test]
fn reduce_u32_empty_yields_neutral() {
assert_eq!(SubgroupReduceOp::Add.reduce_u32([]), 0);
assert_eq!(SubgroupReduceOp::Mul.reduce_u32([]), 1);
assert_eq!(SubgroupReduceOp::Min.reduce_u32([]), u32::MAX);
assert_eq!(SubgroupReduceOp::Max.reduce_u32([]), 0);
assert_eq!(SubgroupReduceOp::And.reduce_u32([]), u32::MAX);
}
#[test]
fn f32_reduce_helpers_match_op_and_reject_bitwise() {
let lanes = [3.0f32, 1.0, 4.0];
let fold = |op: SubgroupReduceOp| {
op.f32_identity()
.map(|id| lanes.iter().fold(id, |acc, &l| op.combine_f32(acc, l).unwrap()))
};
assert_eq!(fold(SubgroupReduceOp::Add), Some(8.0));
assert_eq!(fold(SubgroupReduceOp::Mul), Some(12.0));
assert_eq!(fold(SubgroupReduceOp::Min), Some(1.0));
assert_eq!(fold(SubgroupReduceOp::Max), Some(4.0));
assert_eq!(SubgroupReduceOp::And.f32_identity(), None);
assert_eq!(SubgroupReduceOp::Or.combine_f32(1.0, 2.0), None);
}
#[test]
fn only_bitwise_ops_are_bitwise() {
assert!(SubgroupReduceOp::And.is_bitwise());
assert!(SubgroupReduceOp::Or.is_bitwise());
assert!(SubgroupReduceOp::Xor.is_bitwise());
assert!(!SubgroupReduceOp::Add.is_bitwise());
assert!(!SubgroupReduceOp::Mul.is_bitwise());
assert!(!SubgroupReduceOp::Min.is_bitwise());
assert!(!SubgroupReduceOp::Max.is_bitwise());
}
}