use crate::analyses::layout_aos_to_soa;
use crate::{BindingSlot, KernelDescriptor};
use vyre_foundation::ir::DataType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LayoutHint {
Aos,
Soa,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SoaBindingSplit {
pub source_slot: u32,
pub first_component_slot: u32,
pub component_bindings: Vec<BindingSlot>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SoaBindingSplitPlan {
pub splits: Vec<SoaBindingSplit>,
}
impl SoaBindingSplitPlan {
#[must_use]
pub fn is_empty(&self) -> bool {
self.splits.is_empty()
}
#[must_use]
pub fn component_binding_count(&self) -> usize {
self.splits
.iter()
.map(|split| split.component_bindings.len())
.sum()
}
}
#[must_use]
pub fn promote(desc: &KernelDescriptor) -> Vec<(u32, LayoutHint)> {
let plan = layout_aos_to_soa::analyze(desc);
let mut hints: Vec<(u32, LayoutHint)> = desc
.bindings
.slots
.iter()
.map(|s| (s.slot, LayoutHint::Aos))
.collect();
for cand in &plan.candidates {
if let Some(entry) = hints
.iter_mut()
.find(|(slot, _)| *slot == cand.binding_slot)
{
entry.1 = LayoutHint::Soa;
}
}
hints.sort_unstable_by_key(|(slot, _)| *slot);
hints
}
#[must_use]
pub fn plan_binding_splits(desc: &KernelDescriptor) -> SoaBindingSplitPlan {
let analysis = layout_aos_to_soa::analyze(desc);
let mut next_slot = desc
.bindings
.slots
.iter()
.filter(|slot| {
!matches!(
slot.memory_class,
crate::MemoryClass::Shared | crate::MemoryClass::Scratch,
)
})
.map(|slot| slot.slot)
.max()
.unwrap_or(0)
.saturating_add(1);
let mut splits = Vec::with_capacity(analysis.candidates.len());
for candidate in analysis.candidates {
let Some(source) = desc
.bindings
.slots
.iter()
.find(|slot| slot.slot == candidate.binding_slot)
else {
continue;
};
let Some(component_type) = component_type(&source.element_type) else {
continue;
};
let first_component_slot = next_slot;
let mut component_bindings = Vec::with_capacity(candidate.component_count as usize);
for lane in 0..candidate.component_count {
let mut binding = source.clone();
binding.slot = next_slot;
binding.element_type = component_type.clone();
binding.name = format!("{}_soa{lane}", source.name);
component_bindings.push(binding);
next_slot = next_slot.saturating_add(1);
}
splits.push(SoaBindingSplit {
source_slot: source.slot,
first_component_slot,
component_bindings,
});
}
SoaBindingSplitPlan { splits }
}
fn component_type(dtype: &DataType) -> Option<DataType> {
match dtype {
DataType::Vec { element, .. } => Some((**element).clone()),
DataType::Vec2U32 | DataType::Vec4U32 => Some(DataType::U32),
DataType::TensorShaped { element, .. } => Some((**element).clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BindingLayout, BindingSlot, BindingVisibility, Dispatch, KernelBody, KernelDescriptor,
KernelOp, KernelOpKind, LiteralValue, MemoryClass,
};
use vyre_foundation::ir::DataType;
fn vec4_binding(slot: u32) -> BindingSlot {
BindingSlot {
slot,
element_type: DataType::Vec {
element: Box::new(DataType::F32),
count: 4,
},
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadOnly,
name: format!("v{slot}"),
}
}
fn scalar_binding(slot: u32) -> BindingSlot {
BindingSlot {
slot,
element_type: DataType::F32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadOnly,
name: format!("s{slot}"),
}
}
fn load_op(slot: u32, idx_id: u32, result: u32) -> KernelOp {
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![slot, idx_id],
result: Some(result),
}
}
#[test]
fn empty_descriptor_produces_empty_hints() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
},
};
assert!(promote(&desc).is_empty());
}
#[test]
fn scalar_binding_stays_aos() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![scalar_binding(0)],
},
dispatch: Dispatch::new(64, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
load_op(0, 0, 1),
load_op(0, 0, 2),
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let hints = promote(&desc);
assert_eq!(hints, vec![(0, LayoutHint::Aos)]);
}
#[test]
fn vec4_with_multiple_loads_promotes_to_soa() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![vec4_binding(0)],
},
dispatch: Dispatch::new(32, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
load_op(0, 0, 1),
load_op(0, 0, 2),
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let hints = promote(&desc);
assert_eq!(hints, vec![(0, LayoutHint::Soa)]);
}
#[test]
fn mixed_bindings_get_per_slot_hints() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![scalar_binding(0), vec4_binding(1)],
},
dispatch: Dispatch::new(32, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
load_op(0, 0, 1),
load_op(1, 0, 2),
load_op(1, 0, 3),
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let hints = promote(&desc);
assert_eq!(hints, vec![(0, LayoutHint::Aos), (1, LayoutHint::Soa)]);
}
#[test]
fn promote_is_idempotent() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![vec4_binding(0)],
},
dispatch: Dispatch::new(32, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
load_op(0, 0, 1),
load_op(0, 0, 2),
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let once = promote(&desc);
let twice = promote(&desc);
assert_eq!(once, twice);
}
#[test]
fn split_plan_allocates_scalar_component_bindings() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![vec4_binding(7)],
},
dispatch: Dispatch::new(32, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
load_op(7, 0, 1),
load_op(7, 0, 2),
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let plan = plan_binding_splits(&desc);
assert_eq!(plan.component_binding_count(), 4);
assert_eq!(plan.splits[0].source_slot, 7);
assert_eq!(plan.splits[0].first_component_slot, 8);
assert_eq!(
plan.splits[0]
.component_bindings
.iter()
.map(|binding| binding.slot)
.collect::<Vec<_>>(),
vec![8, 9, 10, 11]
);
assert!(plan.splits[0]
.component_bindings
.iter()
.all(|binding| matches!(binding.element_type, DataType::F32)));
}
}