use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ElemType {
I8,
I16,
I32,
I64,
F32,
F64,
}
impl ElemType {
pub fn byte_width(self) -> u32 {
match self {
ElemType::I8 => 1,
ElemType::I16 => 2,
ElemType::I32 | ElemType::F32 => 4,
ElemType::I64 | ElemType::F64 => 8,
}
}
pub fn is_float(self) -> bool {
matches!(self, ElemType::F32 | ElemType::F64)
}
fn fingerprint_tag(self) -> u8 {
match self {
ElemType::I8 => 0,
ElemType::I16 => 1,
ElemType::I32 => 2,
ElemType::I64 => 3,
ElemType::F32 => 4,
ElemType::F64 => 5,
}
}
}
impl fmt::Display for ElemType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ElemType::I8 => "i8",
ElemType::I16 => "i16",
ElemType::I32 => "i32",
ElemType::I64 => "i64",
ElemType::F32 => "f32",
ElemType::F64 => "f64",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TensorWasmOp {
VecAdd {
elem: ElemType,
lanes: u32,
},
VecMul {
elem: ElemType,
lanes: u32,
},
VecFma {
elem: ElemType,
lanes: u32,
},
MatMul {
m: u32,
n: u32,
k: u32,
},
LoadUnified {
elem: ElemType,
lanes: u32,
},
StoreUnified {
elem: ElemType,
lanes: u32,
},
Barrier,
}
impl fmt::Display for TensorWasmOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TensorWasmOp::VecAdd { elem, lanes } => write!(f, "vec_add.{elem}[{lanes}]"),
TensorWasmOp::VecMul { elem, lanes } => write!(f, "vec_mul.{elem}[{lanes}]"),
TensorWasmOp::VecFma { elem, lanes } => write!(f, "vec_fma.{elem}[{lanes}]"),
TensorWasmOp::MatMul { m, n, k } => write!(f, "matmul[{m}x{n}x{k}]"),
TensorWasmOp::LoadUnified { elem, lanes } => write!(f, "load_unified.{elem}[{lanes}]"),
TensorWasmOp::StoreUnified { elem, lanes } => {
write!(f, "store_unified.{elem}[{lanes}]")
}
TensorWasmOp::Barrier => f.write_str("barrier"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GridHint {
pub total_threads: u32,
pub preferred_block_size: u32,
}
impl Default for GridHint {
fn default() -> Self {
Self {
total_threads: 256,
preferred_block_size: 128,
}
}
}
impl GridHint {
pub fn launch_geometry(&self) -> (u32, u32) {
let block = self.preferred_block_size.max(1);
let grid = self.total_threads.div_ceil(block);
(grid.max(1), block)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TensorWasmKernelBlueprint {
pub entry: String,
pub ops: Vec<TensorWasmOp>,
pub grid_hint: GridHint,
pub shared_mem_bytes: u32,
}
impl TensorWasmKernelBlueprint {
pub fn new(entry: impl Into<String>) -> Self {
Self {
entry: entry.into(),
ops: Vec::new(),
grid_hint: GridHint::default(),
shared_mem_bytes: 0,
}
}
pub fn push(mut self, op: TensorWasmOp) -> Self {
self.ops.push(op);
self
}
pub fn with_grid(mut self, hint: GridHint) -> Self {
self.grid_hint = hint;
self
}
pub fn with_shared_mem(mut self, bytes: u32) -> Self {
self.shared_mem_bytes = bytes;
self
}
pub fn is_empty(&self) -> bool {
self.ops.is_empty()
}
fn digest(&self) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"tensor-wasm-jit::ir::v2\0");
hasher.update(&(self.entry.len() as u64).to_le_bytes());
hasher.update(self.entry.as_bytes());
hasher.update(&(self.ops.len() as u64).to_le_bytes());
for op in &self.ops {
match op {
TensorWasmOp::VecAdd { elem, lanes } => {
hasher.update(&[0u8]);
hasher.update(&[elem.fingerprint_tag()]);
hasher.update(&lanes.to_le_bytes());
}
TensorWasmOp::VecMul { elem, lanes } => {
hasher.update(&[1u8]);
hasher.update(&[elem.fingerprint_tag()]);
hasher.update(&lanes.to_le_bytes());
}
TensorWasmOp::VecFma { elem, lanes } => {
hasher.update(&[2u8]);
hasher.update(&[elem.fingerprint_tag()]);
hasher.update(&lanes.to_le_bytes());
}
TensorWasmOp::MatMul { m, n, k } => {
hasher.update(&[3u8]);
hasher.update(&m.to_le_bytes());
hasher.update(&n.to_le_bytes());
hasher.update(&k.to_le_bytes());
}
TensorWasmOp::LoadUnified { elem, lanes } => {
hasher.update(&[4u8]);
hasher.update(&[elem.fingerprint_tag()]);
hasher.update(&lanes.to_le_bytes());
}
TensorWasmOp::StoreUnified { elem, lanes } => {
hasher.update(&[5u8]);
hasher.update(&[elem.fingerprint_tag()]);
hasher.update(&lanes.to_le_bytes());
}
TensorWasmOp::Barrier => {
hasher.update(&[6u8]);
}
}
}
hasher.update(&self.grid_hint.total_threads.to_le_bytes());
hasher.update(&self.grid_hint.preferred_block_size.to_le_bytes());
hasher.update(&self.shared_mem_bytes.to_le_bytes());
*hasher.finalize().as_bytes()
}
pub fn fingerprint(&self) -> u64 {
let bytes = self.digest();
u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
}
pub fn fingerprint128(&self) -> u128 {
let bytes = self.digest();
let mut buf = [0u8; 16];
buf.copy_from_slice(&bytes[0..16]);
u128::from_le_bytes(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blueprint_builder() {
let bp = TensorWasmKernelBlueprint::new("vector_add")
.push(TensorWasmOp::LoadUnified {
elem: ElemType::F32,
lanes: 4,
})
.push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4,
})
.push(TensorWasmOp::StoreUnified {
elem: ElemType::F32,
lanes: 4,
})
.with_grid(GridHint {
total_threads: 1024,
preferred_block_size: 128,
})
.with_shared_mem(0);
assert_eq!(bp.entry, "vector_add");
assert_eq!(bp.ops.len(), 3);
assert_eq!(bp.grid_hint.total_threads, 1024);
}
#[test]
fn grid_geometry_rounds_up() {
let g = GridHint {
total_threads: 130,
preferred_block_size: 64,
};
let (grid, block) = g.launch_geometry();
assert_eq!(block, 64);
assert_eq!(grid, 3); }
#[test]
fn fingerprint_is_deterministic() {
let a = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4,
});
let b = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4,
});
assert_eq!(a.fingerprint(), b.fingerprint());
assert_eq!(a.fingerprint128(), b.fingerprint128());
assert_eq!(
a.fingerprint() as u128,
a.fingerprint128() & u64::MAX as u128
);
}
#[test]
fn fingerprint_changes_with_lanes() {
let a = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4,
});
let b = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 8,
});
assert_ne!(a.fingerprint(), b.fingerprint());
}
#[test]
fn fingerprint_distinguishes_element_type() {
let f32_add = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4,
});
let i32_add = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::I32,
lanes: 4,
});
assert_ne!(
f32_add.fingerprint(),
i32_add.fingerprint(),
"f32x4.add and i32x4.add must not share a 64-bit cache key"
);
assert_ne!(
f32_add.fingerprint128(),
i32_add.fingerprint128(),
"f32x4.add and i32x4.add must not share a 128-bit fingerprint"
);
let f32_load = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::LoadUnified {
elem: ElemType::F32,
lanes: 4,
});
let i32_load = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::LoadUnified {
elem: ElemType::I32,
lanes: 4,
});
assert_ne!(f32_load.fingerprint(), i32_load.fingerprint());
}
#[test]
fn fingerprint_is_blake3_based_and_pinned() {
let bp = TensorWasmKernelBlueprint::new("vector_add")
.push(TensorWasmOp::LoadUnified {
elem: ElemType::F32,
lanes: 4,
})
.push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4,
})
.push(TensorWasmOp::StoreUnified {
elem: ElemType::F32,
lanes: 4,
});
let fp = bp.fingerprint();
assert_ne!(fp, 0);
let other = TensorWasmKernelBlueprint::new("vector_add")
.push(TensorWasmOp::LoadUnified {
elem: ElemType::F32,
lanes: 4,
})
.push(TensorWasmOp::VecMul {
elem: ElemType::F32,
lanes: 4,
})
.push(TensorWasmOp::StoreUnified {
elem: ElemType::F32,
lanes: 4,
});
assert_ne!(fp, other.fingerprint());
}
#[test]
fn fingerprint_distinguishes_matmul_dims() {
let a = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::MatMul {
m: 16,
n: 16,
k: 16,
});
let b = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::MatMul {
m: 32,
n: 32,
k: 32,
});
assert_ne!(a.fingerprint(), b.fingerprint());
}
#[test]
fn empty_blueprint_detectable() {
let bp = TensorWasmKernelBlueprint::new("empty");
assert!(bp.is_empty());
}
#[test]
fn op_display() {
assert_eq!(
TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4
}
.to_string(),
"vec_add.f32[4]"
);
assert_eq!(
TensorWasmOp::VecAdd {
elem: ElemType::I32,
lanes: 4
}
.to_string(),
"vec_add.i32[4]"
);
assert_eq!(
TensorWasmOp::MatMul {
m: 16,
n: 16,
k: 16
}
.to_string(),
"matmul[16x16x16]"
);
assert_eq!(TensorWasmOp::Barrier.to_string(), "barrier");
}
#[test]
fn elem_type_byte_width() {
assert_eq!(ElemType::I8.byte_width(), 1);
assert_eq!(ElemType::I16.byte_width(), 2);
assert_eq!(ElemType::I32.byte_width(), 4);
assert_eq!(ElemType::F32.byte_width(), 4);
assert_eq!(ElemType::I64.byte_width(), 8);
assert_eq!(ElemType::F64.byte_width(), 8);
assert!(ElemType::F32.is_float());
assert!(!ElemType::I32.is_float());
}
}