use super::body_index::BodyIndex;
use crate::{KernelBody, KernelDescriptor, KernelOpKind, LiteralValue};
use vyre_foundation::ir::BinOp;
use vyre_foundation::ir::DataType;
#[must_use]
pub fn mul_add_to_fma(desc: &KernelDescriptor) -> KernelDescriptor {
let mut out = desc.clone();
out.body = mul_add_to_fma_body(out.body);
out
}
fn mul_add_to_fma_body(mut body: KernelBody) -> KernelBody {
let index = BodyIndex::new(&body);
let mut promotions: Vec<(usize, u32, u32, u32)> = Vec::new(); for (idx, op) in body.ops.iter().enumerate() {
let bin = match &op.kind {
KernelOpKind::BinOpKind(BinOp::Add) => BinOp::Add,
_ => continue,
};
if op.operands.len() != 2 {
continue;
}
let _ = bin;
let lhs = op.operands[0];
let rhs = op.operands[1];
let mul_side = candidate_mul(&body, &index, lhs)
.map(|(a, b)| (a, b, rhs))
.or_else(|| candidate_mul(&body, &index, rhs).map(|(a, b)| (a, b, lhs)));
let Some((a_id, b_id, c_id)) = mul_side else {
continue;
};
if !is_float_producing(&body, &index, a_id, 0)
|| !is_float_producing(&body, &index, b_id, 0)
|| !is_float_producing(&body, &index, c_id, 0)
{
continue;
}
promotions.push((idx, a_id, b_id, c_id));
}
for (add_idx, a_id, b_id, c_id) in promotions {
body.ops[add_idx].kind = KernelOpKind::Fma;
body.ops[add_idx].operands = vec![a_id, b_id, c_id];
}
body.child_bodies = body
.child_bodies
.into_iter()
.map(mul_add_to_fma_body)
.collect();
body
}
fn candidate_mul(body: &KernelBody, index: &BodyIndex, result_id: u32) -> Option<(u32, u32)> {
let producer = index.producer(body, result_id)?;
if !matches!(producer.kind, KernelOpKind::BinOpKind(BinOp::Mul)) {
return None;
}
if producer.operands.len() != 2 {
return None;
}
if !index.has_single_consumer(result_id) {
return None;
}
Some((producer.operands[0], producer.operands[1]))
}
const FLOAT_TRACE_DEPTH_LIMIT: usize = 8;
fn is_float_producing(body: &KernelBody, index: &BodyIndex, result_id: u32, depth: usize) -> bool {
if depth >= FLOAT_TRACE_DEPTH_LIMIT {
return false;
}
let Some(op) = index.producer(body, result_id) else {
return false;
};
match &op.kind {
KernelOpKind::Literal => {
let pool_idx = match op.operands.first() {
Some(p) => *p as usize,
None => return false,
};
matches!(body.literals.get(pool_idx), Some(LiteralValue::F32(_)))
}
KernelOpKind::Cast { target } => matches!(
target,
DataType::F32 | DataType::F16 | DataType::BF16 | DataType::F64
),
KernelOpKind::Fma => true,
KernelOpKind::BinOpKind(_) => {
op.operands
.iter()
.any(|operand| is_float_producing(body, index, *operand, depth + 1))
}
KernelOpKind::UnOpKind(_) | KernelOpKind::Select => op
.operands
.iter()
.any(|operand| is_float_producing(body, index, *operand, depth + 1)),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BindingLayout, Dispatch, KernelOp};
use vyre_foundation::ir::BinOp;
fn empty_body() -> KernelBody {
KernelBody {
ops: Vec::new(),
child_bodies: Vec::new(),
literals: Vec::new(),
}
}
fn lit_f32(body: &mut KernelBody, value: f32, result: u32) {
let pool_idx = body.literals.len() as u32;
body.literals.push(LiteralValue::F32(value));
body.ops.push(KernelOp {
kind: KernelOpKind::Literal,
operands: vec![pool_idx],
result: Some(result),
});
}
fn lit_u32(body: &mut KernelBody, value: u32, result: u32) {
let pool_idx = body.literals.len() as u32;
body.literals.push(LiteralValue::U32(value));
body.ops.push(KernelOp {
kind: KernelOpKind::Literal,
operands: vec![pool_idx],
result: Some(result),
});
}
fn mul(body: &mut KernelBody, a: u32, b: u32, result: u32) {
body.ops.push(KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Mul),
operands: vec![a, b],
result: Some(result),
});
}
fn add(body: &mut KernelBody, a: u32, b: u32, result: u32) {
body.ops.push(KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Add),
operands: vec![a, b],
result: Some(result),
});
}
fn descriptor_with(body: KernelBody) -> KernelDescriptor {
KernelDescriptor {
id: "fma_test_kernel".into(),
bindings: BindingLayout { slots: Vec::new() },
dispatch: Dispatch::new(1, 1, 1),
body,
}
}
#[test]
fn fma_replaces_add_of_mul_lhs_when_floats() {
let mut body = empty_body();
lit_f32(&mut body, 1.0, 0);
lit_f32(&mut body, 2.0, 1);
lit_f32(&mut body, 3.0, 2);
mul(&mut body, 0, 1, 3); add(&mut body, 3, 2, 4); let desc = descriptor_with(body);
let out = mul_add_to_fma(&desc);
let add_op = out
.body
.ops
.iter()
.find(|op| op.result == Some(4))
.expect("Fix: result-4 op must survive the rewrite (now as Fma).");
assert!(
matches!(add_op.kind, KernelOpKind::Fma),
"Fix: Add(Mul(a,b), c) over floats must become Fma(a, b, c) - got {:?}",
add_op.kind
);
assert_eq!(
add_op.operands,
vec![0, 1, 2],
"Fix: Fma operands must be [a_id, b_id, c_id] = [0, 1, 2]."
);
}
#[test]
fn fma_replaces_add_of_mul_rhs_when_floats() {
let mut body = empty_body();
lit_f32(&mut body, 1.0, 0);
lit_f32(&mut body, 2.0, 1);
lit_f32(&mut body, 3.0, 2);
mul(&mut body, 0, 1, 3);
add(&mut body, 2, 3, 4); let desc = descriptor_with(body);
let out = mul_add_to_fma(&desc);
let add_op = out
.body
.ops
.iter()
.find(|op| op.result == Some(4))
.expect("Fix: result-4 op must survive the rewrite.");
assert!(matches!(add_op.kind, KernelOpKind::Fma));
assert_eq!(add_op.operands, vec![0, 1, 2]);
}
#[test]
fn no_fma_when_mul_has_multiple_consumers() {
let mut body = empty_body();
lit_f32(&mut body, 1.0, 0);
lit_f32(&mut body, 2.0, 1);
lit_f32(&mut body, 3.0, 2);
mul(&mut body, 0, 1, 3);
add(&mut body, 3, 2, 4);
add(&mut body, 3, 0, 5); let desc = descriptor_with(body);
let out = mul_add_to_fma(&desc);
for op in &out.body.ops {
assert!(
!matches!(op.kind, KernelOpKind::Fma),
"Fix: must not promote when the Mul has more than one consumer."
);
}
}
#[test]
fn no_fma_when_operands_are_integer_literals() {
let mut body = empty_body();
lit_u32(&mut body, 1, 0);
lit_u32(&mut body, 2, 1);
lit_u32(&mut body, 3, 2);
mul(&mut body, 0, 1, 3);
add(&mut body, 3, 2, 4);
let desc = descriptor_with(body);
let out = mul_add_to_fma(&desc);
for op in &out.body.ops {
assert!(
!matches!(op.kind, KernelOpKind::Fma),
"Fix: integer Add(Mul(...), ...) must not promote - Fma is FP-only at every backend."
);
}
}
#[test]
fn rewrite_is_idempotent() {
let mut body = empty_body();
lit_f32(&mut body, 1.0, 0);
lit_f32(&mut body, 2.0, 1);
lit_f32(&mut body, 3.0, 2);
mul(&mut body, 0, 1, 3);
add(&mut body, 3, 2, 4);
let desc = descriptor_with(body);
let once = mul_add_to_fma(&desc);
let twice = mul_add_to_fma(&once);
assert_eq!(
once, twice,
"Fix: rewrite must reach a fixpoint after one application."
);
}
#[test]
fn recurses_into_child_bodies() {
let mut child = empty_body();
lit_f32(&mut child, 1.0, 10);
lit_f32(&mut child, 2.0, 11);
lit_f32(&mut child, 3.0, 12);
mul(&mut child, 10, 11, 13);
add(&mut child, 13, 12, 14);
let mut body = empty_body();
body.child_bodies = vec![child];
let desc = descriptor_with(body);
let out = mul_add_to_fma(&desc);
let child_out = &out.body.child_bodies[0];
let promoted = child_out
.ops
.iter()
.find(|op| op.result == Some(14))
.expect("Fix: child-body promotion target must survive the rewrite.");
assert!(
matches!(promoted.kind, KernelOpKind::Fma),
"Fix: rewrite must recurse into child bodies. Got {:?}.",
promoted.kind
);
}
}