use crate::{KernelBody, KernelDescriptor, KernelOpKind};
use rustc_hash::FxHashMap;
#[must_use]
pub fn load_forwarding(desc: &KernelDescriptor) -> KernelDescriptor {
let mut out = desc.clone();
out.body = load_forwarding_body(out.body);
out
}
fn load_forwarding_body(mut body: KernelBody) -> KernelBody {
let mut cache: FxHashMap<u32, FxHashMap<u32, u32>> = FxHashMap::default();
let mut id_remap: FxHashMap<u32, u32> = FxHashMap::default();
for op in &body.ops {
match &op.kind {
KernelOpKind::StoreGlobal | KernelOpKind::StoreShared => {
if op.operands.len() < 3 {
continue;
}
let slot = op.operands[0];
let idx = resolve(op.operands[1], &id_remap);
let val = resolve(op.operands[2], &id_remap);
let entry = cache.entry(slot).or_default();
entry.clear();
entry.insert(idx, val);
}
KernelOpKind::LoadGlobal | KernelOpKind::LoadShared => {
if op.operands.len() < 2 {
continue;
}
let slot = op.operands[0];
let idx = resolve(op.operands[1], &id_remap);
let Some(load_result) = op.result else {
continue;
};
let entry = cache.entry(slot).or_default();
if let Some(&forwarded) = entry.get(&idx) {
id_remap.insert(load_result, forwarded);
} else {
entry.insert(idx, load_result);
}
}
KernelOpKind::LoadConstant => {
if op.operands.len() < 2 {
continue;
}
let slot = op.operands[0];
let idx = resolve(op.operands[1], &id_remap);
let Some(load_result) = op.result else {
continue;
};
let entry = cache.entry(slot).or_default();
if let Some(&forwarded) = entry.get(&idx) {
id_remap.insert(load_result, forwarded);
} else {
entry.insert(idx, load_result);
}
}
KernelOpKind::Atomic { .. } => {
if let Some(&slot) = op.operands.first() {
cache.remove(&slot);
}
}
KernelOpKind::Barrier { .. } => {
cache.clear();
}
KernelOpKind::AsyncLoad { .. }
| KernelOpKind::AsyncStore { .. }
| KernelOpKind::AsyncWait { .. } => {
cache.clear();
}
KernelOpKind::StructuredIfThen
| KernelOpKind::StructuredIfThenElse
| KernelOpKind::StructuredForLoop { .. }
| KernelOpKind::StructuredBlock
| KernelOpKind::Region { .. } => {
cache.clear();
}
KernelOpKind::Trap { .. } | KernelOpKind::Resume { .. } | KernelOpKind::Return => {
cache.clear();
}
KernelOpKind::Call { .. }
| KernelOpKind::OpaqueExpr { .. }
| KernelOpKind::OpaqueNode { .. } => {
cache.clear();
}
_ => {}
}
}
if !id_remap.is_empty() {
for op in &mut body.ops {
for pos in 0..op.operands.len() {
if operand_is_result_reference(&op.kind, pos) {
if let Some(&new) = id_remap.get(&op.operands[pos]) {
op.operands[pos] = new;
}
}
}
}
}
body.child_bodies = body
.child_bodies
.into_iter()
.map(load_forwarding_body)
.collect();
body
}
fn resolve(id: u32, remap: &FxHashMap<u32, u32>) -> u32 {
let mut cur = id;
let mut hops = 0usize;
while let Some(&next) = remap.get(&cur) {
if next == cur {
break;
}
cur = next;
hops += 1;
if hops > remap.len() + 1 {
break;
}
}
cur
}
fn operand_is_result_reference(kind: &KernelOpKind, pos: usize) -> bool {
use KernelOpKind::*;
match kind {
Literal => false,
LocalInvocationId | GlobalInvocationId | WorkgroupId => false,
SubgroupLocalId | SubgroupSize | LoopIndex { .. } => false,
LoopCarrier { .. } | LoopCarrierEnd { .. } => pos == 0,
LoopCarrierFinal { .. } => false,
LoadGlobal | LoadShared | LoadConstant => pos != 0,
BufferLength => false,
StoreGlobal | StoreShared => pos != 0,
BinOpKind(_) | UnOpKind(_) | Fma | MatrixMma { .. } | Select | Cast { .. } => true,
Atomic { .. } => pos != 0,
SubgroupBallot | SubgroupShuffle | SubgroupAdd => true,
StructuredIfThen | StructuredIfThenElse => pos == 0,
StructuredForLoop { .. } => pos != 2,
StructuredBlock | Region { .. } => false,
Return | Barrier { .. } => false,
AsyncLoad { .. } | AsyncStore { .. } => pos >= 2,
AsyncWait { .. } => false,
Trap { .. } => pos == 0,
Resume { .. } => false,
IndirectDispatch { .. } => false,
Call { .. } => true,
OpaqueExpr { .. } | OpaqueNode { .. } => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BindingLayout, BindingSlot, BindingVisibility, Dispatch, KernelBody, KernelDescriptor,
KernelOp, KernelOpKind, LiteralValue, MemoryClass,
};
use vyre_foundation::ir::DataType;
fn rw_slot() -> BindingSlot {
BindingSlot {
slot: 0,
element_type: DataType::U32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadWrite,
name: "buf".into(),
}
}
#[test]
fn store_then_load_forwards_value() {
let desc = KernelDescriptor {
id: "stl".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
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,
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 2], result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[4].operands, vec![0, 0, 1]);
}
#[test]
fn load_then_load_same_idx_forwards() {
let desc = KernelDescriptor {
id: "ll".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 2],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[3].operands, vec![0, 0, 1]);
}
#[test]
fn cache_keys_resolve_through_prior_load_forwarding() {
let desc = KernelDescriptor {
id: "resolved_idx".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
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(3),
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 1, 3],
result: None,
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 2],
result: Some(4),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 1, 4],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(9)],
},
};
let out = load_forwarding(&desc);
assert_eq!(
out.body.ops[6].operands,
vec![0, 1, 3],
"load at a remapped-equivalent index should forward from the earlier store"
);
}
#[test]
fn intervening_store_to_different_idx_invalidates_cache() {
let desc = KernelDescriptor {
id: "intervening".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
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::Literal,
operands: vec![2],
result: Some(2),
}, KernelOp {
kind: KernelOpKind::Literal,
operands: vec![3],
result: Some(3),
}, KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 1], result: None,
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 2, 3], result: None,
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0], result: Some(4),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 4],
result: None,
},
],
child_bodies: vec![],
literals: vec![
LiteralValue::U32(0),
LiteralValue::U32(7),
LiteralValue::U32(99),
LiteralValue::U32(8),
],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[7].operands, vec![0, 0, 4]);
}
#[test]
fn barrier_invalidates_cache() {
let desc = KernelDescriptor {
id: "barrier".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
dispatch: Dispatch::new(64, 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,
},
KernelOp {
kind: KernelOpKind::Barrier {
ordering:
vyre_foundation::runtime::memory_model::MemoryOrdering::SeqCst,
},
operands: vec![],
result: None,
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 2],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[5].operands, vec![0, 0, 2]);
}
#[test]
fn different_slots_dont_interfere() {
let mut s1 = rw_slot();
s1.slot = 1;
s1.name = "buf2".into();
let desc = KernelDescriptor {
id: "twoslots".into(),
bindings: BindingLayout {
slots: vec![rw_slot(), s1],
},
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::Literal,
operands: vec![2],
result: Some(2),
}, KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 1], result: None,
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![1, 0, 2], result: None,
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0], result: Some(3),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 3],
result: None,
},
],
child_bodies: vec![],
literals: vec![
LiteralValue::U32(0),
LiteralValue::U32(7),
LiteralValue::U32(99),
],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[6].operands, vec![0, 0, 1]);
}
#[test]
fn atomic_invalidates_only_target_slot() {
let desc = KernelDescriptor {
id: "atomic_slot".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
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,
},
KernelOp {
kind: KernelOpKind::Atomic {
op: vyre_foundation::ir::AtomicOp::Add,
ordering:
vyre_foundation::runtime::memory_model::MemoryOrdering::SeqCst,
},
operands: vec![0, 0, 1], result: Some(2),
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(3),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 3],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[5].operands, vec![0, 0, 3]);
}
#[test]
fn structured_if_invalidates_cache() {
let desc = KernelDescriptor {
id: "if_invalidates".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
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,
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::StructuredIfThen,
operands: vec![2, 0], result: None,
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(3),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 3],
result: None,
},
],
child_bodies: vec![KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
}],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[6].operands, vec![0, 0, 3]);
}
#[test]
fn nothing_to_forward_is_noop() {
let desc = KernelDescriptor {
id: "noop".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
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 out = load_forwarding(&desc);
assert_eq!(out.body.ops.len(), 3);
assert_eq!(out.body.ops[2].operands, vec![0, 0, 1]);
}
#[test]
fn loadconstant_is_forwardable_across_arbitrary_ops() {
let desc = KernelDescriptor {
id: "lc".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::LoadConstant,
operands: vec![0, 0],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::LoadConstant,
operands: vec![0, 0], result: Some(2),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 2],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let out = load_forwarding(&desc);
assert_eq!(out.body.ops[3].operands, vec![0, 0, 1]);
}
#[test]
fn idempotent() {
let desc = KernelDescriptor {
id: "idemp".into(),
bindings: BindingLayout {
slots: vec![rw_slot()],
},
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,
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![0, 0],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 2],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let once = load_forwarding(&desc);
let twice = load_forwarding(&once);
assert_eq!(once.body.ops, twice.body.ops);
}
#[test]
fn empty_kernel_is_noop() {
let desc = KernelDescriptor {
id: "empty".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
},
};
let out = load_forwarding(&desc);
assert!(out.body.ops.is_empty());
}
}