use serde::{Deserialize, Serialize};
use std::sync::Arc;
use vyre_foundation::ir::{AtomicOp, BinOp, DataType, UnOp};
use vyre_foundation::runtime::memory_model::MemoryOrdering;
pub const TRAP_SIDECAR_NAME: &str = "__vyre_descriptor_trap_sidecar";
pub const TRAP_SIDECAR_WORDS: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Dispatch {
pub workgroup_size: [u32; 3],
}
impl Dispatch {
pub const fn new(x: u32, y: u32, z: u32) -> Self {
Self {
workgroup_size: [x, y, z],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MemoryClass {
Global,
Shared,
Constant,
Uniform,
Scratch,
}
impl MemoryClass {
#[must_use]
pub fn is_global_visibility(self) -> bool {
matches!(self, Self::Global | Self::Constant)
}
#[must_use]
pub fn is_writable(self) -> bool {
!matches!(self, Self::Constant)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BindingVisibility {
ReadOnly,
WriteOnly,
ReadWrite,
}
impl BindingVisibility {
#[must_use]
pub fn is_readable(self) -> bool {
matches!(self, Self::ReadOnly | Self::ReadWrite)
}
#[must_use]
pub fn is_writable(self) -> bool {
matches!(self, Self::WriteOnly | Self::ReadWrite)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BindingSlot {
pub slot: u32,
pub element_type: DataType,
pub element_count: Option<u32>,
pub memory_class: MemoryClass,
pub visibility: BindingVisibility,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BindingLayout {
pub slots: Vec<BindingSlot>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LiteralValue {
U32(u32),
I32(i32),
F32(f32),
Bool(bool),
}
impl Eq for LiteralValue {}
impl std::hash::Hash for LiteralValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Self::U32(v) => {
0u8.hash(state);
v.hash(state);
}
Self::I32(v) => {
1u8.hash(state);
v.hash(state);
}
Self::F32(v) => {
2u8.hash(state);
v.to_bits().hash(state);
}
Self::Bool(v) => {
3u8.hash(state);
v.hash(state);
}
}
}
}
pub type Name = Arc<str>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MatrixMmaShape {
M16N8K16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MatrixMmaElement {
F16,
BF16,
TF32,
F32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MatrixMmaLayout {
RowMajor,
ColMajor,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KernelOp {
pub kind: KernelOpKind,
pub operands: Vec<u32>,
pub result: Option<u32>,
}
impl Eq for KernelOp {}
impl std::hash::Hash for KernelOp {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.kind.hash(state);
self.operands.hash(state);
self.result.hash(state);
}
}
impl KernelOp {
#[must_use]
pub fn result_id_count(&self) -> u32 {
match self.kind {
KernelOpKind::MatrixMma { .. } => 4,
_ => u32::from(self.result.is_some()),
}
}
pub fn result_ids(&self) -> impl Iterator<Item = u32> + '_ {
let base = self.result;
(0..self.result_id_count())
.filter_map(move |offset| base.and_then(|id| id.checked_add(offset)))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum KernelOpKind {
Literal,
Copy,
LocalInvocationId,
GlobalInvocationId,
WorkgroupId,
SubgroupLocalId,
SubgroupSize,
LoopIndex { loop_var: Name },
LoopCarrierInit { name: Name },
LoopCarrier { name: Name },
LoopCarrierEnd { name: Name },
LoadGlobal,
LoadShared,
LoadConstant,
BufferLength,
StoreGlobal,
StoreShared,
BinOpKind(BinOp),
UnOpKind(UnOp),
Fma,
MatrixMma {
shape: MatrixMmaShape,
a_layout: MatrixMmaLayout,
b_layout: MatrixMmaLayout,
a_type: MatrixMmaElement,
b_type: MatrixMmaElement,
accum_type: MatrixMmaElement,
},
Select,
Cast { target: DataType },
Atomic {
op: AtomicOp,
ordering: MemoryOrdering,
},
SubgroupBallot,
SubgroupShuffle,
SubgroupAdd,
StructuredIfThen,
StructuredIfThenElse,
StructuredForLoop { loop_var: Name },
StructuredBlock,
Return,
Barrier { ordering: MemoryOrdering },
Region { generator: Name },
AsyncLoad { tag: Name },
AsyncStore { tag: Name },
AsyncWait { tag: Name },
Trap { tag: Name },
Resume { tag: Name },
IndirectDispatch { count_offset: u64 },
Call { op_id: Name },
OpaqueExpr(Box<OpaqueExprData>),
OpaqueNode(Box<OpaqueNodeData>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OpaqueExprData {
pub extension_id: u32,
pub extension_kind: String,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OpaqueNodeData {
pub extension_kind: String,
pub payload: Vec<u8>,
}
impl Eq for KernelOpKind {}
impl std::hash::Hash for KernelOpKind {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::BinOpKind(op) => op.hash(state),
Self::UnOpKind(op) => op.hash(state),
Self::MatrixMma {
shape,
a_layout,
b_layout,
a_type,
b_type,
accum_type,
} => {
shape.hash(state);
a_layout.hash(state);
b_layout.hash(state);
a_type.hash(state);
b_type.hash(state);
accum_type.hash(state);
}
Self::Cast { target } => target.hash(state),
Self::Atomic { op, ordering } => {
op.hash(state);
ordering.hash(state);
}
Self::StructuredForLoop { loop_var } => loop_var.hash(state),
Self::LoopIndex { loop_var } => loop_var.hash(state),
Self::Barrier { ordering } => ordering.hash(state),
Self::Region { generator } => generator.hash(state),
Self::AsyncLoad { tag }
| Self::AsyncStore { tag }
| Self::AsyncWait { tag }
| Self::Trap { tag }
| Self::Resume { tag } => tag.hash(state),
Self::LoopCarrierInit { name }
| Self::LoopCarrier { name }
| Self::LoopCarrierEnd { name } => name.hash(state),
Self::IndirectDispatch { count_offset } => count_offset.hash(state),
Self::Call { op_id } => op_id.hash(state),
Self::OpaqueExpr(data) => {
data.extension_id.hash(state);
data.extension_kind.hash(state);
data.payload.hash(state);
}
Self::OpaqueNode(data) => {
data.extension_kind.hash(state);
data.payload.hash(state);
}
_ => {}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KernelBody {
pub ops: Vec<KernelOp>,
pub child_bodies: Vec<KernelBody>,
pub literals: Vec<LiteralValue>,
}
impl Eq for KernelBody {}
impl std::hash::Hash for KernelBody {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.ops.hash(state);
self.child_bodies.hash(state);
for lit in &self.literals {
lit.hash(state);
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KernelDescriptor {
pub id: String,
pub bindings: BindingLayout,
pub dispatch: Dispatch,
pub body: KernelBody,
}
impl Eq for KernelDescriptor {}
impl std::hash::Hash for KernelDescriptor {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.bindings.hash(state);
self.dispatch.hash(state);
self.body.hash(state);
}
}
impl KernelDescriptor {
#[must_use]
pub fn summary(&self) -> String {
format!(
"{}: {} ops, {} bindings, {} child bodies, {} literals, dispatch {:?}",
if self.id.is_empty() {
"<unnamed>"
} else {
&self.id
},
self.body.ops.len(),
self.bindings.slots.len(),
self.body.child_bodies.len(),
self.body.literals.len(),
self.dispatch.workgroup_size,
)
}
#[must_use]
pub fn summary_compact(&self) -> String {
format!(
"{}({} ops, {} bindings)",
if self.id.is_empty() {
"<unnamed>"
} else {
&self.id
},
self.body.ops.len(),
self.bindings.slots.len(),
)
}
#[must_use]
pub fn total_ops(&self) -> usize {
fn walk(b: &KernelBody) -> usize {
b.ops.len() + b.child_bodies.iter().map(walk).sum::<usize>()
}
walk(&self.body)
}
#[must_use]
pub fn body_count(&self) -> usize {
fn walk(b: &KernelBody) -> usize {
1 + b.child_bodies.iter().map(walk).sum::<usize>()
}
walk(&self.body)
}
#[must_use]
pub fn max_body_depth(&self) -> usize {
fn walk(b: &KernelBody) -> usize {
b.child_bodies
.iter()
.map(|c| 1 + walk(c))
.max()
.unwrap_or(0)
}
walk(&self.body)
}
#[must_use]
pub fn body_at(&self, path: &[usize]) -> Option<&KernelBody> {
let mut current = &self.body;
for &idx in path {
current = current.child_bodies.get(idx)?;
}
Some(current)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.total_ops() == 0
}
#[must_use]
pub fn is_pure(&self) -> bool {
!self.has_side_effects()
}
pub fn ops_iter(&self) -> KernelOpsIter<'_> {
KernelOpsIter {
stack: vec![(&self.body, 0)],
}
}
#[must_use]
pub fn find_op_by_id(&self, id: u32) -> Option<&KernelOp> {
self.ops_iter().find(|op| op.result == Some(id))
}
#[must_use]
pub fn dispatch_total_threads(&self) -> u32 {
let wg = self.dispatch.workgroup_size;
wg[0].saturating_mul(wg[1]).saturating_mul(wg[2])
}
#[must_use]
pub fn with_id(&self, id: impl Into<String>) -> Self {
let mut clone = self.clone();
clone.id = id.into();
clone
}
#[must_use]
pub fn has_side_effects(&self) -> bool {
fn walk(b: &KernelBody) -> bool {
for op in &b.ops {
use KernelOpKind::*;
if matches!(
op.kind,
StoreGlobal
| StoreShared
| LoopCarrierInit { .. }
| LoopCarrierEnd { .. }
| Atomic { .. }
| AsyncStore { .. }
| Barrier { .. }
| Trap { .. }
| Resume { .. }
| Return
| Call { .. }
| OpaqueExpr(..)
| OpaqueNode(..)
) {
return true;
}
}
b.child_bodies.iter().any(walk)
}
walk(&self.body)
}
}
pub struct KernelOpsIter<'a> {
stack: Vec<(&'a KernelBody, usize)>,
}
impl<'a> Iterator for KernelOpsIter<'a> {
type Item = &'a KernelOp;
fn next(&mut self) -> Option<Self::Item> {
loop {
let (body, idx) = self.stack.last_mut()?;
if let Some(op) = body.ops.get(*idx) {
*idx += 1;
return Some(op);
}
let body = *body;
self.stack.pop();
for child in body.child_bodies.iter().rev() {
self.stack.push((child, 0));
}
}
}
}
#[cfg(test)]
mod desc_helper_tests {
use super::*;
use vyre_foundation::ir::DataType;
fn build(ops: Vec<KernelOp>, child_bodies: Vec<KernelBody>) -> KernelDescriptor {
KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![BindingSlot {
slot: 0,
element_type: DataType::U32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadWrite,
name: "buf".into(),
}],
},
dispatch: Dispatch::new(64, 1, 1),
body: KernelBody {
ops,
child_bodies,
literals: vec![LiteralValue::U32(7)],
},
}
}
#[test]
fn summary_includes_all_counts() {
let d = build(vec![], vec![]);
let s = d.summary();
assert!(s.contains("k:"));
assert!(s.contains("0 ops"));
assert!(s.contains("1 bindings"));
assert!(s.contains("0 child bodies"));
assert!(s.contains("1 literals"));
assert!(s.contains("[64, 1, 1]"));
}
#[test]
fn summary_compact_terser_form() {
let d = build(
vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
}],
vec![],
);
let s = d.summary_compact();
assert_eq!(s, "k(1 ops, 1 bindings)");
}
#[test]
fn unnamed_descriptor_uses_unnamed_label() {
let mut d = build(vec![], vec![]);
d.id = String::new();
let s = d.summary();
assert!(s.contains("<unnamed>"));
}
#[test]
fn total_ops_recurses_into_child_bodies() {
let child = KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(1),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(5)],
};
let parent_ops = vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
}];
let d = build(parent_ops, vec![child]);
assert_eq!(d.body.ops.len(), 1); assert_eq!(d.total_ops(), 3); }
#[test]
fn body_at_empty_path_returns_parent() {
let d = build(
vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(7),
}],
vec![],
);
let body = d.body_at(&[]).unwrap();
assert_eq!(body.ops.len(), 1);
assert_eq!(body.ops[0].result, Some(7));
}
#[test]
fn body_at_descends_into_children() {
let grandchild = KernelBody {
ops: vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(99),
}],
child_bodies: vec![],
literals: vec![LiteralValue::U32(7)],
};
let child = KernelBody {
ops: vec![],
child_bodies: vec![grandchild],
literals: vec![],
};
let d = build(vec![], vec![child]);
let b = d.body_at(&[0]).unwrap();
assert!(b.ops.is_empty());
let b = d.body_at(&[0, 0]).unwrap();
assert_eq!(b.ops[0].result, Some(99));
}
#[test]
fn body_at_out_of_range_returns_none() {
let d = build(vec![], vec![]);
assert!(d.body_at(&[5]).is_none());
assert!(d.body_at(&[0, 0]).is_none());
}
#[test]
fn body_count_includes_parent_plus_recursive_children() {
let nested = KernelBody {
ops: vec![],
child_bodies: vec![KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
}],
literals: vec![],
};
let d = build(vec![], vec![nested]);
assert_eq!(d.body_count(), 3);
}
#[test]
fn body_count_flat_kernel_is_one() {
let d = build(vec![], vec![]);
assert_eq!(d.body_count(), 1);
}
#[test]
fn max_body_depth_flat_is_zero() {
let d = build(vec![], vec![]);
assert_eq!(d.max_body_depth(), 0);
}
#[test]
fn max_body_depth_one_if_is_one() {
let child = KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
};
let d = build(vec![], vec![child]);
assert_eq!(d.max_body_depth(), 1);
}
#[test]
fn max_body_depth_two_levels() {
let grandchild = KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
};
let child = KernelBody {
ops: vec![],
child_bodies: vec![grandchild],
literals: vec![],
};
let d = build(vec![], vec![child]);
assert_eq!(d.max_body_depth(), 2);
}
#[test]
fn total_ops_zero_for_empty_kernel() {
let d = build(vec![], vec![]);
assert_eq!(d.total_ops(), 0);
}
#[test]
fn is_empty_true_when_no_ops() {
let d = build(vec![], vec![]);
assert!(d.is_empty());
}
#[test]
fn is_empty_false_when_parent_has_ops() {
let d = build(
vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
}],
vec![],
);
assert!(!d.is_empty());
assert_eq!(d.total_ops(), 1);
}
#[test]
fn is_empty_false_when_child_has_ops() {
let child = KernelBody {
ops: vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
}],
child_bodies: vec![],
literals: vec![LiteralValue::U32(1)],
};
let d = build(vec![], vec![child]);
assert!(!d.is_empty());
assert_eq!(d.total_ops(), 1);
}
#[test]
fn has_side_effects_true_with_store() {
let d = build(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 0],
result: None,
},
],
vec![],
);
assert!(d.has_side_effects());
}
#[test]
fn has_side_effects_false_with_only_arithmetic() {
let d = build(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Add),
operands: vec![0, 0],
result: Some(1),
},
],
vec![],
);
assert!(!d.has_side_effects());
}
#[test]
fn ops_iter_visits_parent_then_children_in_order() {
let child0 = KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(10),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(11),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(1)],
};
let child1 = KernelBody {
ops: vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(20),
}],
child_bodies: vec![],
literals: vec![LiteralValue::U32(2)],
};
let d = build(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(1),
},
],
vec![child0, child1],
);
let visited: Vec<u32> = d.ops_iter().map(|o| o.result.unwrap()).collect();
assert_eq!(visited, vec![0, 1, 10, 11, 20]);
}
#[test]
fn ops_iter_count_matches_total_ops() {
let child = KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(1),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(7)],
};
let d = build(
vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
}],
vec![child],
);
assert_eq!(d.ops_iter().count(), d.total_ops());
}
#[test]
fn memory_class_predicates() {
assert!(MemoryClass::Global.is_global_visibility());
assert!(MemoryClass::Constant.is_global_visibility());
assert!(!MemoryClass::Shared.is_global_visibility());
assert!(!MemoryClass::Scratch.is_global_visibility());
assert!(MemoryClass::Global.is_writable());
assert!(MemoryClass::Shared.is_writable());
assert!(MemoryClass::Scratch.is_writable());
assert!(!MemoryClass::Constant.is_writable());
}
#[test]
fn binding_visibility_readable_writable() {
assert!(BindingVisibility::ReadOnly.is_readable());
assert!(!BindingVisibility::ReadOnly.is_writable());
assert!(!BindingVisibility::WriteOnly.is_readable());
assert!(BindingVisibility::WriteOnly.is_writable());
assert!(BindingVisibility::ReadWrite.is_readable());
assert!(BindingVisibility::ReadWrite.is_writable());
}
#[test]
fn dispatch_total_threads_multiplies_dims() {
let d = build(vec![], vec![]);
assert_eq!(d.dispatch_total_threads(), 64);
let mut d2 = build(vec![], vec![]);
d2.dispatch = Dispatch::new(8, 8, 4);
assert_eq!(d2.dispatch_total_threads(), 256);
}
#[test]
fn with_id_preserves_everything_else() {
let d = build(
vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
}],
vec![],
);
let renamed = d.with_id("renamed");
assert_eq!(renamed.id, "renamed");
assert_eq!(d.id, "k"); assert_eq!(renamed.body.ops.len(), d.body.ops.len());
assert_eq!(renamed.bindings, d.bindings);
assert_eq!(renamed.dispatch, d.dispatch);
}
#[test]
fn dispatch_total_threads_saturates_on_overflow() {
let mut d = build(vec![], vec![]);
d.dispatch = Dispatch::new(u32::MAX, u32::MAX, u32::MAX);
assert_eq!(d.dispatch_total_threads(), u32::MAX);
}
#[test]
fn find_op_by_id_in_parent() {
let d = build(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(7),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(42),
},
],
vec![],
);
let op = d.find_op_by_id(42).expect("Fix: found");
assert_eq!(op.result, Some(42));
assert!(d.find_op_by_id(99).is_none());
}
#[test]
fn find_op_by_id_finds_in_child() {
let child = KernelBody {
ops: vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(100),
}],
child_bodies: vec![],
literals: vec![LiteralValue::U32(7)],
};
let d = build(vec![], vec![child]);
assert!(d.find_op_by_id(100).is_some());
}
#[test]
fn ops_iter_empty_descriptor_yields_none() {
let d = build(vec![], vec![]);
assert!(d.ops_iter().next().is_none());
}
#[test]
fn is_pure_inverse_of_has_side_effects() {
let pure_kernel = build(
vec![KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
}],
vec![],
);
assert!(pure_kernel.is_pure());
assert!(!pure_kernel.has_side_effects());
let impure = build(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 0],
result: None,
},
],
vec![],
);
assert!(!impure.is_pure());
assert!(impure.has_side_effects());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn binding(slot: u32, element: DataType, mc: MemoryClass) -> BindingSlot {
BindingSlot {
slot,
element_type: element,
element_count: None,
memory_class: mc,
visibility: BindingVisibility::ReadWrite,
name: format!("b{slot}"),
}
}
#[test]
fn empty_descriptor_round_trips_serde_byte_stable() {
let k = KernelDescriptor {
id: "test".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
},
};
let json1 = serde_json::to_string(&k).unwrap();
let parsed: KernelDescriptor = serde_json::from_str(&json1).unwrap();
let json2 = serde_json::to_string(&parsed).unwrap();
assert_eq!(json1, json2);
assert_eq!(k, parsed);
}
#[test]
fn one_store_kernel_round_trips_byte_stable() {
let k = KernelDescriptor {
id: "store_one".into(),
bindings: BindingLayout {
slots: vec![binding(0, DataType::U32, MemoryClass::Global)],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 1],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let json1 = serde_json::to_string(&k).unwrap();
let parsed: KernelDescriptor = serde_json::from_str(&json1).unwrap();
let json2 = serde_json::to_string(&parsed).unwrap();
assert_eq!(json1, json2);
}
#[test]
fn binop_kind_carries_full_vyre_spec_op() {
let op = KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::SaturatingAdd),
operands: vec![0, 1],
result: Some(2),
};
let json = serde_json::to_string(&op).unwrap();
let parsed: KernelOp = serde_json::from_str(&json).unwrap();
assert_eq!(op, parsed);
match parsed.kind {
KernelOpKind::BinOpKind(BinOp::SaturatingAdd) => {}
other => panic!("lost BinOp variant: {other:?}"),
}
}
#[test]
fn unop_kind_carries_full_vyre_spec_op() {
let op = KernelOp {
kind: KernelOpKind::UnOpKind(UnOp::InverseSqrt),
operands: vec![5],
result: Some(6),
};
let json = serde_json::to_string(&op).unwrap();
let parsed: KernelOp = serde_json::from_str(&json).unwrap();
assert_eq!(op, parsed);
match parsed.kind {
KernelOpKind::UnOpKind(UnOp::InverseSqrt) => {}
other => panic!("lost UnOp variant: {other:?}"),
}
}
#[test]
fn atomic_carries_op_and_ordering() {
let op = KernelOp {
kind: KernelOpKind::Atomic {
op: AtomicOp::CompareExchange,
ordering: MemoryOrdering::AcqRel,
},
operands: vec![0, 1, 2, 3],
result: Some(4),
};
let json = serde_json::to_string(&op).unwrap();
let parsed: KernelOp = serde_json::from_str(&json).unwrap();
assert_eq!(op, parsed);
}
#[test]
fn nested_if_then_body_round_trips() {
let inner = KernelBody {
ops: vec![KernelOp {
kind: KernelOpKind::Barrier {
ordering: MemoryOrdering::SeqCst,
},
operands: vec![],
result: None,
}],
child_bodies: vec![],
literals: vec![],
};
let outer = KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::StructuredIfThen,
operands: vec![0, 0],
result: None,
},
],
child_bodies: vec![inner],
literals: vec![LiteralValue::Bool(true)],
};
let k = KernelDescriptor {
id: "if_then".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: outer,
};
let json1 = serde_json::to_string(&k).unwrap();
let parsed: KernelDescriptor = serde_json::from_str(&json1).unwrap();
let json2 = serde_json::to_string(&parsed).unwrap();
assert_eq!(json1, json2);
}
#[test]
fn for_loop_with_var_name_round_trips() {
let body = KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
};
let outer = KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::StructuredForLoop {
loop_var: "i".into(),
},
operands: vec![0, 1, 0],
result: None,
},
],
child_bodies: vec![body],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(64)],
};
let k = KernelDescriptor {
id: "for_i".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(64, 1, 1),
body: outer,
};
let json = serde_json::to_string(&k).unwrap();
let parsed: KernelDescriptor = serde_json::from_str(&json).unwrap();
assert_eq!(k, parsed);
}
#[test]
fn async_load_wait_carry_tag() {
let body = KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::AsyncLoad {
tag: "chunk-0".into(),
},
operands: vec![0, 1, 0, 1],
result: None,
},
KernelOp {
kind: KernelOpKind::AsyncWait {
tag: "chunk-0".into(),
},
operands: vec![],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(16)],
};
let k = KernelDescriptor {
id: "async".into(),
bindings: BindingLayout {
slots: vec![
binding(0, DataType::U32, MemoryClass::Global),
binding(1, DataType::U32, MemoryClass::Shared),
],
},
dispatch: Dispatch::new(64, 1, 1),
body,
};
let json = serde_json::to_string(&k).unwrap();
let parsed: KernelDescriptor = serde_json::from_str(&json).unwrap();
assert_eq!(k, parsed);
}
#[test]
fn cast_op_preserves_target_dtype() {
let op = KernelOp {
kind: KernelOpKind::Cast {
target: DataType::F16,
},
operands: vec![3],
result: Some(4),
};
let json = serde_json::to_string(&op).unwrap();
let parsed: KernelOp = serde_json::from_str(&json).unwrap();
match parsed.kind {
KernelOpKind::Cast {
target: DataType::F16,
} => {}
other => panic!("lost cast target: {other:?}"),
}
}
#[test]
fn binding_carries_full_data_type() {
let b = BindingSlot {
slot: 5,
element_type: DataType::Vec {
element: Box::new(DataType::F32),
count: 4,
},
element_count: Some(64),
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadWrite,
name: "v4f32".into(),
};
let json = serde_json::to_string(&b).unwrap();
let parsed: BindingSlot = serde_json::from_str(&json).unwrap();
assert_eq!(b, parsed);
}
#[test]
fn literal_value_eq_treats_nan_as_distinct_via_bits() {
let nan1 = LiteralValue::F32(f32::NAN);
let nan2 = LiteralValue::F32(f32::NAN);
assert_ne!(nan1, nan2);
}
#[test]
fn region_op_round_trips_with_generator_name() {
let op = KernelOp {
kind: KernelOpKind::Region {
generator: "vyre.libs.nn.gqa_attention".into(),
},
operands: vec![0],
result: None,
};
let json = serde_json::to_string(&op).unwrap();
let parsed: KernelOp = serde_json::from_str(&json).unwrap();
assert_eq!(op, parsed);
}
#[test]
fn dispatch_constructor_preserves_axes() {
let d = Dispatch::new(64, 4, 2);
assert_eq!(d.workgroup_size, [64, 4, 2]);
}
}