use std::fmt::{self, Debug, Display, Formatter};
use furiosa_mapping::{Ident, Index, M, MappingExt};
use furiosa_opt_macro::primitive;
use smart_default::SmartDefault;
use crate::tensor::Tensor;
use super::scalar::VeScalar;
pub use furiosa_opt_common_ir::{BitReq, ExecutionId, GroupId, TagGuard};
use furiosa_opt_common_ir::BranchedOperand;
#[primitive(ve::TagMode)]
#[derive(Debug, Clone, SmartDefault)]
pub enum TagMode<D: VeScalar> {
#[default]
Zero,
AxisToggle {
axis: Ident,
},
Comparison([Cmp<D>; 4]),
}
impl<D: VeScalar + Display> Display for TagMode<D> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Zero => write!(f, "TagMode::Zero"),
Self::AxisToggle { axis } => write!(f, "TagMode::AxisToggle {{ axis: {axis} }}"),
Self::Comparison(input_cmps) => {
write!(f, "TagMode::Comparison(")?;
for (i, cmp) in input_cmps.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{cmp}")?;
}
write!(f, ")")
}
}
}
}
#[primitive(ve::Cmp)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Cmp<D: VeScalar> {
Equal(D),
Less(D),
Greater(D),
LessUnsigned(D),
GreaterUnsigned(D),
True,
False,
}
impl<D: VeScalar> Cmp<D> {
#[inline]
pub(crate) fn matches(self, x: D) -> bool {
match self {
Cmp::Equal(boundary) => x == boundary,
Cmp::Less(boundary) => x.lt_scalar(boundary),
Cmp::Greater(boundary) => boundary.lt_scalar(x),
Cmp::LessUnsigned(boundary) => x.to_raw_bits() < boundary.to_raw_bits(),
Cmp::GreaterUnsigned(boundary) => x.to_raw_bits() > boundary.to_raw_bits(),
Cmp::True => true,
Cmp::False => false,
}
}
}
impl<D: VeScalar + Display> Display for Cmp<D> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Equal(boundary) => write!(f, "={boundary}"),
Self::Less(boundary) => write!(f, "<{boundary}"),
Self::Greater(boundary) => write!(f, ">{boundary}"),
Self::LessUnsigned(boundary) => write!(f, "<u{boundary}"),
Self::GreaterUnsigned(boundary) => write!(f, ">u{boundary}"),
Self::True => write!(f, "true"),
Self::False => write!(f, "false"),
}
}
}
#[derive(Debug)]
pub enum RfPort<D: VeScalar, Mapping: M> {
External(Tensor<D, Mapping>),
Stash,
}
pub type BinaryBranchedOperand<D, Mapping> = BranchedOperand<D, RfPort<D, Mapping>>;
pub type TernaryBranchedOperand<D, Mapping> = BranchedOperand<(D, D), (RfPort<D, Mapping>, D)>;
pub trait VeOperandLayout<D: VeScalar, Mapping: M> {
type Operand1: Copy + Sync;
fn regs(&self) -> [Option<(&TagGuard, D, Self::Operand1)>; 3];
fn port(&self) -> Option<(&TagGuard, &RfPort<D, Mapping>, Self::Operand1)>;
fn reads_stash(&self) -> bool {
matches!(self.port(), Some((_, RfPort::Stash, _)))
}
}
impl<D: VeScalar, Mapping: M> VeOperandLayout<D, Mapping> for BinaryBranchedOperand<D, Mapping> {
type Operand1 = ();
fn regs(&self) -> [Option<(&TagGuard, D, ())>; 3] {
self.reg_slots()
.map(|slot| slot.as_ref().map(|(guard, operand0)| (guard, *operand0, ())))
}
fn port(&self) -> Option<(&TagGuard, &RfPort<D, Mapping>, ())> {
self.rf_slot().as_ref().map(|(guard, port)| (guard, port, ()))
}
}
impl<D: VeScalar, Mapping: M> VeOperandLayout<D, Mapping> for TernaryBranchedOperand<D, Mapping> {
type Operand1 = D;
fn regs(&self) -> [Option<(&TagGuard, D, D)>; 3] {
self.reg_slots().map(|slot| {
slot.as_ref()
.map(|(guard, (operand0, operand1))| (guard, *operand0, *operand1))
})
}
fn port(&self) -> Option<(&TagGuard, &RfPort<D, Mapping>, D)> {
self.rf_slot()
.as_ref()
.map(|(guard, (port, operand1))| (guard, port, *operand1))
}
}
pub(crate) fn apply_branch_config<D: VeScalar, Mapping: M>(
data: &Tensor<D, Mapping>,
config: &TagMode<D>,
) -> Tensor<u8, Mapping> {
match config {
TagMode::Zero => data.map(|_| 0u8),
TagMode::AxisToggle { axis } => Tensor::<u8, Mapping>::from_vec(axis_toggle_pattern::<Mapping>(axis)),
TagMode::Comparison(cmps) => data.map(|x| {
let mut exec_id: u8 = 0;
for (bit_pos, cmp) in cmps.iter().enumerate() {
let bit = if cmp.matches(x) { 0x1 } else { 0x0 };
exec_id |= bit << bit_pos;
}
exec_id
}),
}
}
fn axis_toggle_pattern<Mapping: M>(axis: &Ident) -> Vec<u8> {
let mapping = Mapping::to_value();
let axes = mapping.axes();
let Some(pos) = crate::storage::axis_position(&axes, axis) else {
return vec![0u8; mapping.size()]; };
let weight: usize = axes[pos + 1..].iter().map(|a| a.modulo).product();
let modulo = axes[pos].modulo;
mapping
.iter(&axes, &Index::new(), true)
.map(|offset| offset.map_or(0u8, |o| (((o / weight) % modulo % 2) as u8) << 3))
.collect()
}
#[cfg(test)]
mod tests {
use furiosa_mapping::*;
use super::{BitReq, Cmp, ExecutionId, GroupId, TagGuard, TagMode, apply_branch_config, axis_toggle_pattern};
use crate::tensor::Tensor;
#[test]
fn axis_toggle_pattern_decodes_axis_parity() {
axes![A = 4, B = 2];
assert_eq!(axis_toggle_pattern::<m![A, B]>(&Ident::A), vec![0, 0, 8, 8, 0, 0, 8, 8]);
assert_eq!(axis_toggle_pattern::<m![A, B]>(&Ident::B), vec![0, 8, 0, 8, 0, 8, 0, 8]);
}
#[test]
fn comparison_bits_land_in_cmp_order() {
axes![A = 3];
let cmps = [Cmp::Equal(0.0f32), Cmp::Greater(0.0), Cmp::Less(0.0), Cmp::True];
let data = Tensor::<f32, m![A]>::from_vec([0.0, 1.5, -2.5]);
let tags = apply_branch_config(&data, &TagMode::Comparison(cmps));
assert_eq!(
tags.into_vec(),
vec![
0b1001, 0b1010, 0b1100, ]
);
}
#[test]
fn a_guard_selects_the_cells_its_comparison_matched() {
axes![A = 4];
let cmps = [Cmp::Equal(0.0f32), Cmp::Greater(0.0), Cmp::Less(0.0), Cmp::True];
let data = Tensor::<f32, m![A]>::from_vec([0.0, 1.5, -2.5, -0.0]);
let tags = apply_branch_config(&data, &TagMode::Comparison(cmps)).into_vec();
let selects = |guard: TagGuard| {
tags.iter()
.map(|id| guard.admits(ExecutionId::try_new(*id).expect("the tag unit writes four bits")))
.collect::<Vec<_>>()
};
let negative = TagGuard::matches([BitReq::Ignore, BitReq::Ignore, BitReq::One, BitReq::Ignore]);
assert_eq!(selects(negative), vec![false, false, true, false], "the `Less` cell");
let zero = TagGuard::matches([BitReq::One, BitReq::Ignore, BitReq::Ignore, BitReq::Ignore]);
assert_eq!(
selects(zero),
vec![true, false, false, true],
"both zeros, `Equal` being `==`"
);
assert_eq!(
selects(TagGuard::group(GroupId::One)),
vec![true; 4],
"`True` sets bit 3 for all"
);
assert_eq!(selects(TagGuard::group(GroupId::Zero)), vec![false; 4]);
}
#[test]
fn equal_on_f32_treats_negative_zero_as_zero() {
assert!(Cmp::Equal(0.0f32).matches(-0.0));
assert!(Cmp::Equal(-0.0f32).matches(0.0));
}
#[test]
fn unsigned_compares_read_the_bit_pattern_not_the_value() {
assert!(Cmp::GreaterUnsigned(1.0f32).matches(-1.0));
assert!(!Cmp::Greater(1.0f32).matches(-1.0));
assert!(Cmp::LessUnsigned(-1.0f32).matches(1.0));
assert!(Cmp::GreaterUnsigned(1i32).matches(-1));
assert!(!Cmp::Greater(1i32).matches(-1));
assert!(!Cmp::LessUnsigned(0i32).matches(-1), "nothing is below zero unsigned");
assert!(Cmp::Less(0i32).matches(-1), "but -1 is below zero signed");
}
}