use crate::{KernelBody, KernelDescriptor, KernelOpKind, LiteralValue};
use rustc_hash::FxHashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VecPackChain {
pub slot: u32,
pub op_indices: Vec<usize>,
pub start_index: u32,
}
impl VecPackChain {
#[must_use]
pub fn pack_width(&self) -> u32 {
let len = self.op_indices.len() as u32;
len.min(4)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VecPackReport {
pub chains: Vec<VecPackChain>,
pub total_ops_eliminated: u32,
}
impl VecPackReport {
#[must_use]
pub fn has_chains(&self) -> bool {
!self.chains.is_empty()
}
}
#[must_use]
pub fn analyze(desc: &KernelDescriptor) -> VecPackReport {
let mut report = VecPackReport::default();
walk_body(&desc.body, &mut report);
report.chains.sort_by_key(|c| (c.slot, c.start_index));
report.total_ops_eliminated = report
.chains
.iter()
.map(|c| (c.op_indices.len() as u32).saturating_sub(1))
.sum();
report
}
fn walk_body(body: &KernelBody, report: &mut VecPackReport) {
detect_chains_in_body(body, report);
for child in &body.child_bodies {
walk_body(child, report);
}
}
fn detect_chains_in_body(body: &KernelBody, report: &mut VecPackReport) {
let literal_indices = literal_index_by_result(body);
let mut by_slot: FxHashMap<u32, Vec<(u32, usize)>> =
FxHashMap::with_capacity_and_hasher(body.ops.len(), Default::default());
for (op_idx, op) in body.ops.iter().enumerate() {
let Some((slot, literal_index)) = load_with_literal_index(op, &literal_indices) else {
continue;
};
by_slot
.entry(slot)
.or_default()
.push((literal_index, op_idx));
}
for (slot, mut candidates) in by_slot {
candidates.sort_unstable_by_key(|(literal_index, op_idx)| (*literal_index, *op_idx));
let mut run_start = 0usize;
while run_start < candidates.len() {
let mut run_end = run_start + 1;
while run_end < candidates.len()
&& candidates[run_end].0 == candidates[run_end - 1].0.saturating_add(1)
{
run_end += 1;
}
if run_end - run_start >= 2 {
report.chains.push(VecPackChain {
slot,
op_indices: candidates[run_start..run_end]
.iter()
.map(|(_, op_idx)| *op_idx)
.collect(),
start_index: candidates[run_start].0,
});
}
run_start = run_end;
}
}
}
fn literal_index_by_result(body: &KernelBody) -> FxHashMap<u32, u32> {
let mut out = FxHashMap::with_capacity_and_hasher(body.ops.len(), Default::default());
for op in &body.ops {
if !matches!(op.kind, KernelOpKind::Literal) {
continue;
}
let Some(result) = op.result else {
continue;
};
let Some(pool_idx) = op.operands.first() else {
continue;
};
let Some(LiteralValue::U32(value)) = body.literals.get(*pool_idx as usize) else {
continue;
};
out.insert(result, *value);
}
out
}
fn load_with_literal_index(
op: &crate::KernelOp,
literal_indices: &FxHashMap<u32, u32>,
) -> Option<(u32, u32)> {
if !matches!(op.kind, KernelOpKind::LoadGlobal) {
return None;
}
let slot = *op.operands.first()?;
let index_op_id = *op.operands.get(1)?;
literal_indices
.get(&index_op_id)
.map(|literal_index| (slot, *literal_index))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BindingLayout, BindingSlot, BindingVisibility, Dispatch, KernelBody, KernelDescriptor,
KernelOp, KernelOpKind, MemoryClass,
};
use vyre_foundation::ir::DataType;
fn input_slot(id: u32, name: &str) -> BindingSlot {
BindingSlot {
slot: id,
element_type: DataType::U32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadOnly,
name: name.into(),
}
}
fn linear_load_body(slot: u32, n: u32) -> KernelBody {
let mut ops = Vec::new();
let mut literals = Vec::new();
for i in 0..n {
literals.push(LiteralValue::U32(i));
ops.push(KernelOp {
kind: KernelOpKind::Literal,
operands: vec![i],
result: Some(i),
});
}
for i in 0..n {
ops.push(KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![slot, i],
result: Some(n + i),
});
}
KernelBody {
ops,
child_bodies: vec![],
literals,
}
}
fn desc_with_body(body: KernelBody) -> KernelDescriptor {
KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![input_slot(0, "in")],
},
dispatch: Dispatch::new(1, 1, 1),
body,
}
}
#[test]
fn empty_body_has_no_chains() {
let desc = desc_with_body(KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
});
let report = analyze(&desc);
assert!(!report.has_chains());
assert_eq!(report.total_ops_eliminated, 0);
}
#[test]
fn single_load_is_not_a_chain() {
let desc = desc_with_body(linear_load_body(0, 1));
assert!(!analyze(&desc).has_chains());
}
#[test]
fn two_adjacent_loads_form_a_chain() {
let desc = desc_with_body(linear_load_body(0, 2));
let report = analyze(&desc);
assert_eq!(report.chains.len(), 1);
assert_eq!(report.chains[0].op_indices.len(), 2);
assert_eq!(report.chains[0].slot, 0);
assert_eq!(report.chains[0].start_index, 0);
assert_eq!(report.chains[0].pack_width(), 2);
assert_eq!(report.total_ops_eliminated, 1);
}
#[test]
fn four_adjacent_loads_form_one_chain_at_pack_width_4() {
let desc = desc_with_body(linear_load_body(0, 4));
let report = analyze(&desc);
assert_eq!(report.chains.len(), 1);
assert_eq!(report.chains[0].pack_width(), 4);
assert_eq!(report.total_ops_eliminated, 3);
}
#[test]
fn five_adjacent_loads_pack_width_capped_at_4() {
let desc = desc_with_body(linear_load_body(0, 5));
let report = analyze(&desc);
assert_eq!(report.chains.len(), 1);
assert_eq!(report.chains[0].op_indices.len(), 5);
assert_eq!(report.chains[0].pack_width(), 4);
assert_eq!(report.total_ops_eliminated, 4);
}
#[test]
fn loads_on_different_slots_form_separate_chains() {
let mut body = linear_load_body(0, 3);
body.literals.push(LiteralValue::U32(0));
body.literals.push(LiteralValue::U32(1));
let lit_a = body.literals.len() as u32 - 2;
let lit_b = body.literals.len() as u32 - 1;
let result_a = body.ops.len() as u32 + 100;
let result_b = result_a + 1;
body.ops.push(KernelOp {
kind: KernelOpKind::Literal,
operands: vec![lit_a],
result: Some(result_a),
});
body.ops.push(KernelOp {
kind: KernelOpKind::Literal,
operands: vec![lit_b],
result: Some(result_b),
});
body.ops.push(KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![1, result_a],
result: Some(200),
});
body.ops.push(KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![1, result_b],
result: Some(201),
});
let mut desc = desc_with_body(body);
desc.bindings.slots.push(input_slot(1, "in2"));
let report = analyze(&desc);
assert_eq!(report.chains.len(), 2);
assert_eq!(report.chains[0].slot, 0);
assert_eq!(report.chains[0].op_indices.len(), 3);
assert_eq!(report.chains[1].slot, 1);
assert_eq!(report.chains[1].op_indices.len(), 2);
}
#[test]
fn non_consecutive_indices_break_the_chain() {
let mut body = KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![
LiteralValue::U32(0),
LiteralValue::U32(1),
LiteralValue::U32(3),
],
};
for (i, _) in [0, 1, 3].iter().enumerate() {
body.ops.push(KernelOp {
kind: KernelOpKind::Literal,
operands: vec![i as u32],
result: Some(i as u32),
});
}
for (offset, lit_id) in [0, 1, 2].iter().enumerate() {
body.ops.push(KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, *lit_id as u32],
result: Some(10 + offset as u32),
});
}
let report = analyze(&desc_with_body(body));
assert_eq!(report.chains.len(), 1);
assert_eq!(report.chains[0].op_indices.len(), 2);
assert_eq!(report.total_ops_eliminated, 1);
}
#[test]
fn non_literal_index_is_not_chainable() {
let body = KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::LocalInvocationId,
operands: vec![0],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Add),
operands: vec![0, 1],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 2],
result: Some(3),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
};
let report = analyze(&desc_with_body(body));
assert!(!report.has_chains());
}
#[test]
fn chains_in_child_bodies_are_detected_too() {
let child = linear_load_body(0, 3);
let parent = KernelBody {
ops: vec![KernelOp {
kind: KernelOpKind::StructuredBlock,
operands: vec![0],
result: None,
}],
child_bodies: vec![child],
literals: vec![],
};
let report = analyze(&desc_with_body(parent));
assert_eq!(report.chains.len(), 1);
assert_eq!(report.chains[0].op_indices.len(), 3);
}
}