#![cfg(feature = "cuda-oxide-backend")]
use crate::ir::{ElemType, GridHint, TensorWasmKernelBlueprint, TensorWasmOp};
use crate::lowered_ir::{LoweredFunction, LoweredOp, LoweredType};
const DEFAULT_LANES: u32 = 4;
fn lowered_type_to_elem(ty: &LoweredType) -> Option<ElemType> {
match ty {
LoweredType::I8 => Some(ElemType::I8),
LoweredType::I16 => Some(ElemType::I16),
LoweredType::I32 => Some(ElemType::I32),
LoweredType::I64 => Some(ElemType::I64),
LoweredType::F32 => Some(ElemType::F32),
LoweredType::F64 => Some(ElemType::F64),
_ => None,
}
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum AdapterError {
#[error("blueprint_adapter: function does not match a wave-1 blueprint pattern ({reason})")]
UnmatchedPattern {
reason: String,
},
#[error("blueprint_adapter: multi-block LoweredFunction not supported in wave 1 (got {block_count} blocks)")]
MultiBlockNotSupported {
block_count: usize,
},
#[error("blueprint_adapter: malformed LoweredFunction")]
Malformed,
}
pub fn lowered_function_to_blueprint(
func: &LoweredFunction,
) -> Result<TensorWasmKernelBlueprint, AdapterError> {
if func.blocks.is_empty() {
return Err(AdapterError::Malformed);
}
if func.blocks.len() > 1 {
return Err(AdapterError::MultiBlockNotSupported {
block_count: func.blocks.len(),
});
}
let block = &func.blocks[0];
let last = block.ops.last().ok_or(AdapterError::Malformed)?;
if !last.is_terminator() {
return Err(AdapterError::Malformed);
}
if !matches!(last, LoweredOp::Return { .. }) {
return Err(AdapterError::UnmatchedPattern {
reason: format!(
"terminator is not Return ({} ops in block)",
block.ops.len()
),
});
}
if block.ops.len() != 4 {
return Err(AdapterError::UnmatchedPattern {
reason: format!(
"expected exactly 4 ops (Load, arith, Store, Return), got {}",
block.ops.len()
),
});
}
let is_load = matches!(&block.ops[0], LoweredOp::Load { .. });
if !is_load {
return Err(AdapterError::UnmatchedPattern {
reason: "no Load before arith op".to_string(),
});
}
let is_store = matches!(&block.ops[2], LoweredOp::Store { .. });
if !is_store {
return Err(AdapterError::UnmatchedPattern {
reason: "no Store after arith op".to_string(),
});
}
let unsupported_elem = |ty: &LoweredType| AdapterError::UnmatchedPattern {
reason: format!("arith op element type `{ty}` has no blueprint representation"),
};
let (kernel_op, elem) = match &block.ops[1] {
LoweredOp::AddF { ty, .. } | LoweredOp::AddI { ty, .. } => {
let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
(
TensorWasmOp::VecAdd {
elem,
lanes: DEFAULT_LANES,
},
elem,
)
}
LoweredOp::MulF { ty, .. } | LoweredOp::MulI { ty, .. } => {
let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
(
TensorWasmOp::VecMul {
elem,
lanes: DEFAULT_LANES,
},
elem,
)
}
LoweredOp::Fma { ty, .. } => {
let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
(
TensorWasmOp::VecFma {
elem,
lanes: DEFAULT_LANES,
},
elem,
)
}
other => {
return Err(AdapterError::UnmatchedPattern {
reason: format!(
"unsupported middle op: {} (wave 1 recognises Add/Mul/Fma only; \
MatMul deferred to wave 3)",
debug_op_name(other)
),
});
}
};
let blueprint = TensorWasmKernelBlueprint {
entry: func.name.clone(),
ops: vec![
TensorWasmOp::LoadUnified {
elem,
lanes: DEFAULT_LANES,
},
kernel_op,
TensorWasmOp::StoreUnified {
elem,
lanes: DEFAULT_LANES,
},
],
grid_hint: GridHint::default(),
shared_mem_bytes: 0,
};
Ok(blueprint)
}
fn debug_op_name(op: &LoweredOp) -> &'static str {
match op {
LoweredOp::AddI { .. } => "AddI",
LoweredOp::SubI { .. } => "SubI",
LoweredOp::MulI { .. } => "MulI",
LoweredOp::DivS { .. } => "DivS",
LoweredOp::DivU { .. } => "DivU",
LoweredOp::RemS { .. } => "RemS",
LoweredOp::RemU { .. } => "RemU",
LoweredOp::AddF { .. } => "AddF",
LoweredOp::SubF { .. } => "SubF",
LoweredOp::MulF { .. } => "MulF",
LoweredOp::DivF { .. } => "DivF",
LoweredOp::Fma { .. } => "Fma",
LoweredOp::FNeg { .. } => "FNeg",
LoweredOp::FAbs { .. } => "FAbs",
LoweredOp::Load { .. } => "Load",
LoweredOp::Store { .. } => "Store",
LoweredOp::StackAlloc { .. } => "StackAlloc",
LoweredOp::Br { .. } => "Br",
LoweredOp::CondBr { .. } => "CondBr",
LoweredOp::Switch { .. } => "Switch",
LoweredOp::Return { .. } => "Return",
LoweredOp::VMin { .. } => "VMin",
LoweredOp::VMax { .. } => "VMax",
LoweredOp::VSplat { .. } => "VSplat",
LoweredOp::VSelect { .. } => "VSelect",
LoweredOp::VAllTrue { .. } => "VAllTrue",
LoweredOp::VAnyTrue { .. } => "VAnyTrue",
LoweredOp::Select { .. } => "Select",
LoweredOp::Bitcast { .. } => "Bitcast",
LoweredOp::TruncI { .. } => "TruncI",
LoweredOp::ExtendU { .. } => "ExtendU",
LoweredOp::ExtendS { .. } => "ExtendS",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lowered_ir::{
LoweredBlock, LoweredFunction, LoweredOp, LoweredSignature, LoweredType,
};
fn straight_line_fn(name: &str, mid: LoweredOp) -> LoweredFunction {
let mut func = LoweredFunction::new(
name,
LoweredSignature {
params: vec![LoweredType::Ptr, LoweredType::Ptr],
returns: vec![],
},
);
let mut block = LoweredBlock::new(0);
block.params = vec![(1, LoweredType::Ptr), (2, LoweredType::Ptr)];
block.ops.push(LoweredOp::Load {
ty: LoweredType::F32,
base: 1,
offset: 0,
result: 10,
});
block.ops.push(mid);
block.ops.push(LoweredOp::Store {
ty: LoweredType::F32,
value: 11,
base: 2,
offset: 0,
});
block.ops.push(LoweredOp::Return { values: vec![] });
func.blocks.push(block);
func
}
#[test]
fn load_addf_store_becomes_vec_add() {
let func = straight_line_fn(
"vector_add",
LoweredOp::AddF {
ty: LoweredType::F32,
lhs: 10,
rhs: 10,
result: 11,
},
);
let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
assert_eq!(bp.entry, "vector_add");
assert_eq!(
bp.ops,
vec![
TensorWasmOp::LoadUnified {
elem: ElemType::F32,
lanes: 4
},
TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4
},
TensorWasmOp::StoreUnified {
elem: ElemType::F32,
lanes: 4
},
]
);
assert_eq!(bp.shared_mem_bytes, 0);
assert_eq!(bp.grid_hint, GridHint::default());
}
#[test]
fn load_addi_store_also_becomes_vec_add() {
let func = straight_line_fn(
"int_add",
LoweredOp::AddI {
ty: LoweredType::I32,
lhs: 10,
rhs: 10,
result: 11,
},
);
let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
assert!(matches!(
bp.ops[1],
TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4
}
));
}
#[test]
fn load_mulf_store_becomes_vec_mul() {
let func = straight_line_fn(
"vector_mul",
LoweredOp::MulF {
ty: LoweredType::F32,
lhs: 10,
rhs: 10,
result: 11,
},
);
let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
assert_eq!(bp.entry, "vector_mul");
assert!(matches!(
bp.ops[1],
TensorWasmOp::VecMul {
elem: ElemType::F32,
lanes: 4
}
));
}
#[test]
fn load_muli_store_also_becomes_vec_mul() {
let func = straight_line_fn(
"int_mul",
LoweredOp::MulI {
ty: LoweredType::I32,
lhs: 10,
rhs: 10,
result: 11,
},
);
let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
assert!(matches!(
bp.ops[1],
TensorWasmOp::VecMul {
elem: ElemType::F32,
lanes: 4
}
));
}
#[test]
fn load_fma_store_becomes_vec_fma() {
let func = straight_line_fn(
"vector_fma",
LoweredOp::Fma {
ty: LoweredType::F32,
a: 10,
b: 10,
c: 10,
result: 11,
},
);
let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
assert_eq!(bp.entry, "vector_fma");
assert!(matches!(
bp.ops[1],
TensorWasmOp::VecFma {
elem: ElemType::F32,
lanes: 4
}
));
}
#[test]
fn multi_block_rejected() {
let mut func = LoweredFunction::new(
"multi",
LoweredSignature {
params: vec![],
returns: vec![],
},
);
let mut b0 = LoweredBlock::new(0);
b0.ops.push(LoweredOp::Br {
target: 1,
args: vec![],
});
let mut b1 = LoweredBlock::new(1);
b1.ops.push(LoweredOp::Return { values: vec![] });
func.blocks.push(b0);
func.blocks.push(b1);
let err = lowered_function_to_blueprint(&func).unwrap_err();
assert_eq!(err, AdapterError::MultiBlockNotSupported { block_count: 2 });
}
#[test]
fn unsupported_pattern_rejected_with_descriptive_reason() {
let func = straight_line_fn(
"weird",
LoweredOp::Bitcast {
from_ty: LoweredType::F32,
to_ty: LoweredType::I32,
src: 10,
result: 11,
},
);
let err = lowered_function_to_blueprint(&func).unwrap_err();
match err {
AdapterError::UnmatchedPattern { reason } => {
assert!(
reason.contains("Bitcast"),
"reason should name the offending op, got: {reason}"
);
}
other => panic!("expected UnmatchedPattern, got {other:?}"),
}
}
#[test]
fn empty_function_is_malformed() {
let func = LoweredFunction::new("empty", LoweredSignature::default());
let err = lowered_function_to_blueprint(&func).unwrap_err();
assert_eq!(err, AdapterError::Malformed);
}
#[test]
fn block_without_terminator_is_malformed() {
let mut func = LoweredFunction::new("bad", LoweredSignature::default());
func.blocks.push(LoweredBlock::new(0));
let err = lowered_function_to_blueprint(&func).unwrap_err();
assert_eq!(err, AdapterError::Malformed);
}
#[test]
fn extra_intermediate_op_rejected() {
let mut func = LoweredFunction::new(
"extra",
LoweredSignature {
params: vec![LoweredType::Ptr],
returns: vec![],
},
);
let mut block = LoweredBlock::new(0);
block.ops.push(LoweredOp::Load {
ty: LoweredType::F32,
base: 1,
offset: 0,
result: 10,
});
block.ops.push(LoweredOp::AddF {
ty: LoweredType::F32,
lhs: 10,
rhs: 10,
result: 11,
});
block.ops.push(LoweredOp::Bitcast {
from_ty: LoweredType::F32,
to_ty: LoweredType::I32,
src: 11,
result: 12,
});
block.ops.push(LoweredOp::Store {
ty: LoweredType::I32,
value: 12,
base: 1,
offset: 0,
});
block.ops.push(LoweredOp::Return { values: vec![] });
func.blocks.push(block);
let err = lowered_function_to_blueprint(&func).unwrap_err();
assert!(matches!(err, AdapterError::UnmatchedPattern { .. }));
}
#[test]
fn adapter_error_displays_useful_messages() {
let e = AdapterError::MultiBlockNotSupported { block_count: 3 };
let s = e.to_string();
assert!(s.contains("multi-block"));
assert!(s.contains('3'));
let e = AdapterError::UnmatchedPattern {
reason: "no Load before arith op".to_string(),
};
let s = e.to_string();
assert!(s.contains("no Load before arith op"));
}
}