use std::collections::HashSet;
use std::fmt;
use crate::analysis::fusion_cost_model::{FusionCostModel, FusionVerdict};
use crate::arch::SmVersion;
use crate::ir::{Instruction, PtxFunction, PtxType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FusionType {
Elementwise,
ProducerConsumer,
Horizontal,
Vertical,
}
impl fmt::Display for FusionType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Elementwise => write!(f, "elementwise"),
Self::ProducerConsumer => write!(f, "producer-consumer"),
Self::Horizontal => write!(f, "horizontal"),
Self::Vertical => write!(f, "vertical"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccessPattern {
Streaming,
Random,
Strided(u32),
Unknown,
}
impl fmt::Display for AccessPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Streaming => write!(f, "streaming"),
Self::Random => write!(f, "random"),
Self::Strided(s) => write!(f, "strided({s})"),
Self::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FusionConstraint {
SameGridDimensions,
NoSharedMemoryConflict,
NoBarrierConflict,
RegisterBudget(u32),
}
#[derive(Debug, Clone)]
pub struct FusionCandidate {
pub producer_index: usize,
pub consumer_index: usize,
pub shared_buffer: String,
pub fusion_type: FusionType,
pub estimated_speedup: f64,
pub producer_shared_bytes: usize,
pub consumer_shared_bytes: usize,
pub estimated_registers: u32,
pub producer_global_bytes: usize,
pub consumer_global_bytes: usize,
pub eliminated_global_bytes: usize,
pub producer_flops: u64,
pub consumer_flops: u64,
pub kernel_count: usize,
}
#[derive(Debug, Clone)]
pub struct DataDependency {
pub producer: usize,
pub consumer: usize,
pub buffer_name: String,
pub access_pattern: AccessPattern,
}
#[derive(Debug, Clone)]
pub struct FusionPlan {
pub candidates: Vec<FusionCandidate>,
pub fused_groups: Vec<Vec<usize>>,
pub original_kernel_count: usize,
pub fused_kernel_count: usize,
pub estimated_total_speedup: f64,
}
impl fmt::Display for FusionPlan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Fusion Plan")?;
writeln!(f, " Original kernels: {}", self.original_kernel_count)?;
writeln!(f, " Fused kernels: {}", self.fused_kernel_count)?;
writeln!(
f,
" Estimated speedup: {:.2}x",
self.estimated_total_speedup
)?;
writeln!(f, " Groups:")?;
for (i, group) in self.fused_groups.iter().enumerate() {
writeln!(f, " [{i}]: {group:?}")?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FusionReport {
pub plan: FusionPlan,
pub rejected: Vec<(FusionCandidate, String)>,
}
impl fmt::Display for FusionReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.plan)?;
if !self.rejected.is_empty() {
writeln!(f, " Rejected candidates:")?;
for (cand, reason) in &self.rejected {
writeln!(
f,
" kernel[{}] -> kernel[{}] ({}): {}",
cand.producer_index, cand.consumer_index, cand.fusion_type, reason
)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FusionAnalysis;
impl FusionAnalysis {
pub const fn new() -> Self {
Self
}
pub fn analyze_pair(producer: &PtxFunction, consumer: &PtxFunction) -> Option<FusionCandidate> {
let prod_shared = shared_mem_bytes(producer);
let cons_shared = shared_mem_bytes(consumer);
let est_regs = estimate_register_count(producer) + estimate_register_count(consumer);
let shared_buf = find_shared_buffer(producer, consumer);
let prod_ew = Self::is_elementwise(producer);
let cons_ew = Self::is_elementwise(consumer);
let fusion_type = if prod_ew && cons_ew {
FusionType::Elementwise
} else if shared_buf.is_some() {
if has_reduction(producer) {
FusionType::Vertical
} else {
FusionType::ProducerConsumer
}
} else if compatible_grid_hints(producer, consumer) {
FusionType::Horizontal
} else {
return None;
};
let buffer_name = shared_buf.unwrap_or_default();
let producer_global_bytes = global_traffic_bytes(producer);
let consumer_global_bytes = global_traffic_bytes(consumer);
let producer_flops = arithmetic_op_count(producer);
let consumer_flops = arithmetic_op_count(consumer);
let eliminated_global_bytes =
intermediate_roundtrip_bytes(producer, consumer, fusion_type, !buffer_name.is_empty());
let candidate = FusionCandidate {
producer_index: 0,
consumer_index: 1,
shared_buffer: buffer_name,
fusion_type,
estimated_speedup: 1.0, producer_shared_bytes: prod_shared,
consumer_shared_bytes: cons_shared,
estimated_registers: est_regs,
producer_global_bytes,
consumer_global_bytes,
eliminated_global_bytes,
producer_flops,
consumer_flops,
kernel_count: 2,
};
let speedup = Self::estimate_fusion_speedup(&candidate);
Some(FusionCandidate {
estimated_speedup: speedup,
..candidate
})
}
pub fn analyze_sequence(kernels: &[PtxFunction]) -> Vec<FusionCandidate> {
let mut candidates = Vec::new();
if kernels.len() < 2 {
return candidates;
}
for i in 0..kernels.len() {
for j in (i + 1)..kernels.len() {
if let Some(mut cand) = Self::analyze_pair(&kernels[i], &kernels[j]) {
cand.producer_index = i;
cand.consumer_index = j;
cand.estimated_speedup = Self::estimate_fusion_speedup(&cand);
candidates.push(cand);
}
}
}
candidates
}
pub fn check_constraints(
candidate: &FusionCandidate,
constraints: &[FusionConstraint],
) -> bool {
for constraint in constraints {
match constraint {
FusionConstraint::SameGridDimensions => {
}
FusionConstraint::NoSharedMemoryConflict => {
let combined =
candidate.producer_shared_bytes + candidate.consumer_shared_bytes;
if combined > 49152 {
return false;
}
}
FusionConstraint::NoBarrierConflict => {
if candidate.producer_shared_bytes > 0
&& candidate.consumer_shared_bytes > 0
&& candidate.fusion_type != FusionType::Elementwise
{
return false;
}
}
FusionConstraint::RegisterBudget(max_regs) => {
if candidate.estimated_registers > *max_regs {
return false;
}
}
}
}
true
}
pub fn is_elementwise(func: &PtxFunction) -> bool {
if !func.shared_mem.is_empty() {
return false;
}
for inst in &func.body {
if is_non_elementwise_instruction(inst) {
return false;
}
}
true
}
pub fn find_data_dependencies(funcs: &[PtxFunction]) -> Vec<DataDependency> {
let mut deps = Vec::new();
for (i, prod_func) in funcs.iter().enumerate() {
let producer_outputs = output_params(prod_func);
for (j, cons_func) in funcs.iter().enumerate().skip(i + 1) {
let consumer_inputs = input_params(cons_func);
for out_name in &producer_outputs {
for in_name in &consumer_inputs {
if out_name == in_name {
let pattern = infer_access_pattern(cons_func);
deps.push(DataDependency {
producer: i,
consumer: j,
buffer_name: out_name.clone(),
access_pattern: pattern,
});
}
}
}
}
}
deps
}
#[allow(clippy::cast_precision_loss)]
pub fn estimate_fusion_speedup(candidate: &FusionCandidate) -> f64 {
if candidate.kernel_count < 2 {
return 1.0;
}
let n = candidate.kernel_count as f64;
let prod_bytes = candidate.producer_global_bytes as f64;
let cons_bytes = candidate.consumer_global_bytes as f64;
let prod_flops = candidate.producer_flops as f64;
let cons_flops = candidate.consumer_flops as f64;
let prod_cost = LAUNCH_OVERHEAD_NS + kernel_runtime_ns(prod_bytes, prod_flops);
let cons_cost = LAUNCH_OVERHEAD_NS + kernel_runtime_ns(cons_bytes, cons_flops);
let unfused_cost = prod_cost + cons_cost;
let total_bytes = prod_bytes + cons_bytes;
let eliminated = (candidate.eliminated_global_bytes as f64).min(total_bytes);
let fused_bytes = (total_bytes - eliminated).max(0.0);
let fused_flops = prod_flops + cons_flops;
let efficiency = fused_occupancy_efficiency(candidate);
let fused_cost =
LAUNCH_OVERHEAD_NS + kernel_runtime_ns(fused_bytes, fused_flops) / efficiency;
let costs_valid = fused_cost.is_finite() && fused_cost > 0.0 && unfused_cost.is_finite();
if !costs_valid {
return 1.0;
}
let raw = unfused_cost / fused_cost;
if !raw.is_finite() {
return 1.0;
}
raw.clamp(1.0, MAX_FUSION_SPEEDUP.min(n))
}
}
const LAUNCH_OVERHEAD_NS: f64 = 5_000.0;
const MEM_BANDWIDTH_B_PER_NS: f64 = 1_500.0;
const COMPUTE_THROUGHPUT_OPS_PER_NS: f64 = 20_000.0;
const MAX_FUSION_SPEEDUP: f64 = 4.0;
fn kernel_runtime_ns(bytes: f64, flops: f64) -> f64 {
let mem_ns = bytes / MEM_BANDWIDTH_B_PER_NS;
let compute_ns = flops / COMPUTE_THROUGHPUT_OPS_PER_NS;
mem_ns.max(compute_ns)
}
#[allow(clippy::cast_precision_loss)]
fn fused_occupancy_efficiency(candidate: &FusionCandidate) -> f64 {
let reg_efficiency = if candidate.estimated_registers > 128 {
0.90
} else if candidate.estimated_registers > 64 {
0.95
} else {
1.0
};
let smem_total = candidate.producer_shared_bytes + candidate.consumer_shared_bytes;
let smem_efficiency = if smem_total > 32_768 { 0.90 } else { 1.0 };
reg_efficiency * smem_efficiency
}
pub fn plan_fusion(
kernels: &[PtxFunction],
max_registers: u32,
max_shared_mem: u32,
) -> FusionReport {
let cost_model = FusionCostModel::default()
.with_register_file(max_registers)
.with_register_pressure_threshold((max_registers / 2).clamp(32, max_registers.max(32)))
.with_shared_mem_budget(max_shared_mem as usize);
plan_fusion_with_model(kernels, max_registers, max_shared_mem, &cost_model)
}
pub fn plan_fusion_for_target(kernels: &[PtxFunction], target: SmVersion) -> FusionReport {
let cost_model = FusionCostModel::for_target(target);
let max_registers = cost_model.register_file;
let max_shared_mem = u32::try_from(cost_model.shared_mem_budget).unwrap_or(u32::MAX);
plan_fusion_with_model(kernels, max_registers, max_shared_mem, &cost_model)
}
fn plan_fusion_with_model(
kernels: &[PtxFunction],
max_registers: u32,
max_shared_mem: u32,
cost_model: &FusionCostModel,
) -> FusionReport {
let candidates = FusionAnalysis::analyze_sequence(kernels);
let constraints = vec![
FusionConstraint::SameGridDimensions,
FusionConstraint::NoSharedMemoryConflict,
FusionConstraint::NoBarrierConflict,
FusionConstraint::RegisterBudget(max_registers),
];
let mut accepted = Vec::new();
let mut rejected = Vec::new();
for cand in candidates {
if !FusionAnalysis::check_constraints(&cand, &constraints) {
let reason = rejection_reason(&cand, &constraints, max_shared_mem);
rejected.push((cand, reason));
continue;
}
let decision = cost_model.decide(&cand);
if decision.should_fuse {
accepted.push(cand);
} else {
let reason = cost_model_rejection_reason(decision.verdict);
rejected.push((cand, reason));
}
}
let groups = build_fusion_groups(&accepted, kernels.len());
let fused_kernel_count = groups.len();
let total_speedup = if accepted.is_empty() {
1.0
} else {
let product: f64 = accepted.iter().map(|c| c.estimated_speedup).product();
#[allow(clippy::cast_precision_loss)]
let n = accepted.len() as f64;
product.powf(1.0 / n)
};
FusionReport {
plan: FusionPlan {
candidates: accepted,
fused_groups: groups,
original_kernel_count: kernels.len(),
fused_kernel_count,
estimated_total_speedup: total_speedup,
},
rejected,
}
}
fn shared_mem_bytes(func: &PtxFunction) -> usize {
func.shared_mem
.iter()
.map(|(_, ty, count)| ty.size_bytes() * count)
.sum()
}
fn global_traffic_bytes(func: &PtxFunction) -> usize {
let mut bytes = 0usize;
for inst in &func.body {
bytes = bytes.saturating_add(instruction_global_bytes(inst));
}
bytes.max(1)
}
fn instruction_global_bytes(inst: &Instruction) -> usize {
match inst {
Instruction::Load { space, vec, ty, .. } | Instruction::Store { space, vec, ty, .. }
if *space == crate::ir::MemorySpace::Global =>
{
ty.size_bytes() * vector_lanes(*vec)
}
Instruction::CpAsync { bytes, .. } => *bytes as usize,
Instruction::Tex1d { ty, .. }
| Instruction::Tex2d { ty, .. }
| Instruction::Tex3d { ty, .. }
| Instruction::SurfLoad { ty, .. }
| Instruction::SurfStore { ty, .. } => ty.size_bytes() * 4,
Instruction::AtomGlobalAddFloat { .. } => 4,
_ => 0,
}
}
const fn vector_lanes(vec: crate::ir::VectorWidth) -> usize {
match vec {
crate::ir::VectorWidth::V1 => 1,
crate::ir::VectorWidth::V2 => 2,
crate::ir::VectorWidth::V4 => 4,
}
}
fn arithmetic_op_count(func: &PtxFunction) -> u64 {
let mut ops = 0u64;
for inst in &func.body {
ops = ops.saturating_add(instruction_op_cost(inst));
}
ops
}
const fn instruction_op_cost(inst: &Instruction) -> u64 {
match inst {
Instruction::Add { .. }
| Instruction::Sub { .. }
| Instruction::Mul { .. }
| Instruction::Neg { .. }
| Instruction::Abs { .. }
| Instruction::Min { .. }
| Instruction::Max { .. }
| Instruction::Addc { .. }
| Instruction::Selp { .. }
| Instruction::Shl { .. }
| Instruction::Shr { .. }
| Instruction::And { .. }
| Instruction::Or { .. }
| Instruction::Xor { .. }
| Instruction::Brev { .. }
| Instruction::Clz { .. }
| Instruction::Popc { .. }
| Instruction::Bfind { .. }
| Instruction::Bfe { .. }
| Instruction::Bfi { .. }
| Instruction::SetP { .. }
| Instruction::Cvt { .. } => 1,
Instruction::Mad { .. }
| Instruction::MadLo { .. }
| Instruction::MadHi { .. }
| Instruction::MadWide { .. }
| Instruction::Fma { .. }
| Instruction::Dp2a { .. } => 2,
Instruction::Rcp { .. }
| Instruction::Rsqrt { .. }
| Instruction::Sqrt { .. }
| Instruction::Div { .. }
| Instruction::Rem { .. } => 4,
Instruction::Dp4a { .. }
| Instruction::Ex2 { .. }
| Instruction::Lg2 { .. }
| Instruction::Sin { .. }
| Instruction::Cos { .. } => 8,
_ => 0,
}
}
fn intermediate_roundtrip_bytes(
producer: &PtxFunction,
consumer: &PtxFunction,
fusion_type: FusionType,
has_named_buffer: bool,
) -> usize {
if fusion_type == FusionType::Horizontal {
return 0;
}
if fusion_type != FusionType::Elementwise && !has_named_buffer {
return 0;
}
let elem = producer_output_store_bytes(producer)
.or_else(|| consumer_input_load_bytes(consumer))
.unwrap_or(PtxType::F32.size_bytes());
elem.saturating_mul(2)
}
fn producer_output_store_bytes(func: &PtxFunction) -> Option<usize> {
func.body.iter().find_map(|inst| match inst {
Instruction::Store { space, vec, ty, .. } if *space == crate::ir::MemorySpace::Global => {
Some(ty.size_bytes() * vector_lanes(*vec))
}
_ => None,
})
}
fn consumer_input_load_bytes(func: &PtxFunction) -> Option<usize> {
func.body.iter().find_map(|inst| match inst {
Instruction::Load { space, vec, ty, .. } if *space == crate::ir::MemorySpace::Global => {
Some(ty.size_bytes() * vector_lanes(*vec))
}
_ => None,
})
}
fn estimate_register_count(func: &PtxFunction) -> u32 {
let mut reg_names: HashSet<&str> = HashSet::new();
for inst in &func.body {
if let Some(name) = destination_register_name(inst) {
reg_names.insert(name);
}
}
let count = reg_names.len().max(1);
u32::try_from(count).unwrap_or(u32::MAX)
}
fn destination_register_name(inst: &Instruction) -> Option<&str> {
match inst {
Instruction::Add { dst, .. }
| Instruction::Sub { dst, .. }
| Instruction::Mul { dst, .. }
| Instruction::Mad { dst, .. }
| Instruction::MadLo { dst, .. }
| Instruction::MadHi { dst, .. }
| Instruction::MadWide { dst, .. }
| Instruction::Fma { dst, .. }
| Instruction::Neg { dst, .. }
| Instruction::Abs { dst, .. }
| Instruction::Min { dst, .. }
| Instruction::Max { dst, .. }
| Instruction::Brev { dst, .. }
| Instruction::Clz { dst, .. }
| Instruction::Popc { dst, .. }
| Instruction::Bfind { dst, .. }
| Instruction::Bfe { dst, .. }
| Instruction::Bfi { dst, .. }
| Instruction::Rcp { dst, .. }
| Instruction::Rsqrt { dst, .. }
| Instruction::Sqrt { dst, .. }
| Instruction::Ex2 { dst, .. }
| Instruction::Lg2 { dst, .. }
| Instruction::Sin { dst, .. }
| Instruction::Cos { dst, .. }
| Instruction::Shl { dst, .. }
| Instruction::Shr { dst, .. }
| Instruction::Div { dst, .. }
| Instruction::Rem { dst, .. }
| Instruction::And { dst, .. }
| Instruction::Or { dst, .. }
| Instruction::Xor { dst, .. }
| Instruction::SetP { dst, .. }
| Instruction::Load { dst, .. }
| Instruction::Cvt { dst, .. }
| Instruction::MovSpecial { dst, .. }
| Instruction::LoadParam { dst, .. }
| Instruction::Atom { dst, .. }
| Instruction::AtomCas { dst, .. }
| Instruction::Dp4a { dst, .. }
| Instruction::Dp2a { dst, .. }
| Instruction::Tex1d { dst, .. }
| Instruction::Tex2d { dst, .. }
| Instruction::Tex3d { dst, .. }
| Instruction::SurfLoad { dst, .. }
| Instruction::Redux { dst, .. }
| Instruction::ElectSync { dst, .. } => Some(&dst.name),
_ => None,
}
}
fn is_non_elementwise_instruction(inst: &Instruction) -> bool {
matches!(
inst,
Instruction::BarSync { .. }
| Instruction::BarArrive { .. }
| Instruction::FenceAcqRel { .. }
| Instruction::Mma { .. }
| Instruction::Wgmma { .. }
| Instruction::TmaLoad { .. }
| Instruction::CpAsync { .. }
| Instruction::CpAsyncCommit
| Instruction::CpAsyncWait { .. }
| Instruction::Redux { .. }
| Instruction::Stmatrix { .. }
| Instruction::MbarrierInit { .. }
| Instruction::MbarrierArrive { .. }
| Instruction::MbarrierWait { .. }
| Instruction::FenceProxy { .. }
) || matches!(inst, Instruction::Wmma { .. })
|| is_shared_mem_access(inst)
}
fn is_shared_mem_access(inst: &Instruction) -> bool {
match inst {
Instruction::Load { space, .. } | Instruction::Store { space, .. } => {
*space == crate::ir::MemorySpace::Shared
}
_ => false,
}
}
fn has_reduction(func: &PtxFunction) -> bool {
func.body
.iter()
.any(|inst| matches!(inst, Instruction::Redux { .. }))
}
fn find_shared_buffer(producer: &PtxFunction, consumer: &PtxFunction) -> Option<String> {
let producer_outputs = output_params(producer);
let consumer_inputs = input_params(consumer);
for out_name in &producer_outputs {
for in_name in &consumer_inputs {
if out_name == in_name {
return Some(out_name.clone());
}
}
}
None
}
fn output_params(func: &PtxFunction) -> Vec<String> {
let mut outputs = Vec::new();
if let Some((name, _)) = func.params.iter().rev().find(|(_, ty)| *ty == PtxType::U64) {
outputs.push(name.clone());
}
for (name, ty) in &func.params {
if *ty == PtxType::U64 {
let lower = name.to_lowercase();
if (lower.contains("out") || lower.contains("dst") || lower.contains("result"))
&& !outputs.contains(name)
{
outputs.push(name.clone());
}
}
}
outputs
}
fn input_params(func: &PtxFunction) -> Vec<String> {
let mut inputs = Vec::new();
let outputs = output_params(func);
for (name, ty) in &func.params {
if *ty == PtxType::U64 && !outputs.contains(name) {
inputs.push(name.clone());
}
}
for (name, ty) in &func.params {
if *ty == PtxType::U64 {
let lower = name.to_lowercase();
if (lower.contains("in") || lower.contains("src")) && !inputs.contains(name) {
inputs.push(name.clone());
}
}
}
if inputs.is_empty() {
for (name, ty) in &func.params {
if *ty == PtxType::U64 {
inputs.push(name.clone());
}
}
}
inputs
}
fn infer_access_pattern(func: &PtxFunction) -> AccessPattern {
let has_tid = func.body.iter().any(|inst| {
matches!(
inst,
Instruction::MovSpecial {
special: crate::ir::SpecialReg::TidX,
..
}
)
});
let has_stride_mul = func.body.iter().any(|inst| {
matches!(
inst,
Instruction::Mul { .. } | Instruction::Shl { .. } | Instruction::Mad { .. }
)
});
if has_tid && !has_stride_mul {
AccessPattern::Streaming
} else if has_tid && has_stride_mul {
AccessPattern::Strided(1)
} else {
AccessPattern::Unknown
}
}
const fn compatible_grid_hints(a: &PtxFunction, b: &PtxFunction) -> bool {
match (a.max_threads, b.max_threads) {
(Some(ma), Some(mb)) => ma == mb,
(None, None) => true,
_ => false,
}
}
fn rejection_reason(
candidate: &FusionCandidate,
constraints: &[FusionConstraint],
_max_shared_mem: u32,
) -> String {
for constraint in constraints {
match constraint {
FusionConstraint::NoSharedMemoryConflict => {
let combined = candidate.producer_shared_bytes + candidate.consumer_shared_bytes;
if combined > 49152 {
return format!(
"combined shared memory ({combined} bytes) exceeds 48 KiB limit"
);
}
}
FusionConstraint::NoBarrierConflict => {
if candidate.producer_shared_bytes > 0
&& candidate.consumer_shared_bytes > 0
&& candidate.fusion_type != FusionType::Elementwise
{
return "barrier conflict: both kernels use shared memory".to_string();
}
}
FusionConstraint::RegisterBudget(max_regs) => {
if candidate.estimated_registers > *max_regs {
return format!(
"register budget exceeded ({} > {max_regs})",
candidate.estimated_registers
);
}
}
FusionConstraint::SameGridDimensions => {}
}
}
"unknown reason".to_string()
}
fn cost_model_rejection_reason(verdict: FusionVerdict) -> String {
match verdict {
FusionVerdict::RegisterSpill => {
"cost model: fused kernel would spill registers to local memory".to_string()
}
FusionVerdict::SharedMemoryOverflow => {
"cost model: combined shared memory exceeds the target's per-block budget".to_string()
}
FusionVerdict::NotWorthwhile => {
"cost model: estimated benefit below the fusion threshold".to_string()
}
FusionVerdict::NothingToFuse => "cost model: fewer than two kernels to fuse".to_string(),
FusionVerdict::Beneficial => "cost model: beneficial".to_string(),
}
}
fn uf_find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
fn build_fusion_groups(candidates: &[FusionCandidate], num_kernels: usize) -> Vec<Vec<usize>> {
let mut parent: Vec<usize> = (0..num_kernels).collect();
for cand in candidates {
let pa = uf_find(&mut parent, cand.producer_index);
let pb = uf_find(&mut parent, cand.consumer_index);
if pa != pb {
let (small, big) = if pa < pb { (pa, pb) } else { (pb, pa) };
parent[big] = small;
}
}
let mut groups: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
for i in 0..num_kernels {
let root = uf_find(&mut parent, i);
groups.entry(root).or_default().push(i);
}
let mut result: Vec<Vec<usize>> = groups.into_values().collect();
result.sort_by_key(|g| g[0]);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{
CacheQualifier, ImmValue, Instruction, MemorySpace, Operand, PtxType, Register, SpecialReg,
VectorWidth, WmmaLayout, WmmaOp, WmmaShape,
};
fn reg(name: &str, ty: PtxType) -> Register {
Register {
name: name.to_string(),
ty,
}
}
fn imm_f32(v: f32) -> Operand {
Operand::Immediate(ImmValue::F32(v))
}
fn make_elementwise_kernel(name: &str, params: &[(&str, PtxType)]) -> PtxFunction {
let mut func = PtxFunction::new(name);
for (pname, pty) in params {
func.add_param(*pname, *pty);
}
func.body.push(Instruction::MovSpecial {
dst: reg("%r0", PtxType::U32),
special: SpecialReg::TidX,
});
func.body.push(Instruction::Add {
ty: PtxType::F32,
dst: reg("%f0", PtxType::F32),
a: imm_f32(1.0),
b: imm_f32(2.0),
});
func.body.push(Instruction::Store {
space: MemorySpace::Global,
qualifier: CacheQualifier::None,
vec: VectorWidth::V1,
ty: PtxType::F32,
addr: Operand::Address {
base: reg("%rd0", PtxType::U64),
offset: None,
},
src: reg("%f0", PtxType::F32),
});
func
}
fn make_reduction_kernel(name: &str) -> PtxFunction {
let mut func = PtxFunction::new(name);
func.add_param("input", PtxType::U64);
func.add_param("output", PtxType::U64);
func.add_shared_mem("smem", PtxType::F32, 256);
func.body.push(Instruction::MovSpecial {
dst: reg("%r0", PtxType::U32),
special: SpecialReg::TidX,
});
func.body.push(Instruction::BarSync { id: 0 });
func.body.push(Instruction::Redux {
op: crate::ir::ReduxOp::Add,
dst: reg("%r1", PtxType::U32),
src: Operand::Register(reg("%r0", PtxType::U32)),
membership_mask: 0xFFFF_FFFF,
});
func
}
fn base_candidate(fusion_type: FusionType) -> FusionCandidate {
FusionCandidate {
producer_index: 0,
consumer_index: 1,
shared_buffer: String::new(),
fusion_type,
estimated_speedup: 0.0,
producer_shared_bytes: 0,
consumer_shared_bytes: 0,
estimated_registers: 32,
producer_global_bytes: 256,
consumer_global_bytes: 256,
eliminated_global_bytes: 256,
producer_flops: 64,
consumer_flops: 64,
kernel_count: 2,
}
}
#[test]
fn test_is_elementwise_simple() {
let kernel =
make_elementwise_kernel("ew", &[("input", PtxType::U64), ("output", PtxType::U64)]);
assert!(FusionAnalysis::is_elementwise(&kernel));
}
#[test]
fn test_is_not_elementwise_with_shared_mem() {
let kernel = make_reduction_kernel("reduce");
assert!(!FusionAnalysis::is_elementwise(&kernel));
}
#[test]
fn test_is_not_elementwise_with_barrier() {
let mut kernel = PtxFunction::new("barrier_kernel");
kernel.body.push(Instruction::BarSync { id: 0 });
assert!(!FusionAnalysis::is_elementwise(&kernel));
}
#[test]
fn test_is_not_elementwise_with_wmma() {
let mut kernel = PtxFunction::new("wmma_kernel");
kernel.body.push(Instruction::Wmma {
op: WmmaOp::Mma,
shape: WmmaShape::M16N16K16,
layout: WmmaLayout::RowMajor,
ty: PtxType::F16,
fragments: vec![reg("%f0", PtxType::F16)],
addr: None,
stride: None,
});
assert!(!FusionAnalysis::is_elementwise(&kernel));
}
#[test]
fn test_analyze_pair_elementwise() {
let k0 =
make_elementwise_kernel("add", &[("input", PtxType::U64), ("output", PtxType::U64)]);
let k1 =
make_elementwise_kernel("mul", &[("input", PtxType::U64), ("output", PtxType::U64)]);
let cand = FusionAnalysis::analyze_pair(&k0, &k1);
assert!(cand.is_some());
let c = cand.as_ref().map(|c| c.fusion_type);
assert_eq!(c, Some(FusionType::Elementwise));
}
#[test]
fn test_analyze_pair_producer_consumer() {
let k0 = make_elementwise_kernel(
"producer",
&[("input", PtxType::U64), ("buf", PtxType::U64)],
);
let mut k1 = PtxFunction::new("consumer");
k1.add_param("buf", PtxType::U64);
k1.add_param("output", PtxType::U64);
k1.add_shared_mem("smem", PtxType::F32, 64);
k1.body.push(Instruction::Load {
space: MemorySpace::Shared,
qualifier: CacheQualifier::None,
vec: VectorWidth::V1,
ty: PtxType::F32,
dst: reg("%f0", PtxType::F32),
addr: Operand::Address {
base: reg("%rd0", PtxType::U64),
offset: None,
},
});
let cand = FusionAnalysis::analyze_pair(&k0, &k1);
assert!(cand.is_some());
let c = cand.as_ref().map(|c| c.fusion_type);
assert_eq!(c, Some(FusionType::ProducerConsumer));
}
#[test]
fn test_analyze_pair_horizontal() {
let mut k0 = PtxFunction::new("kernel_a");
k0.add_param("a_in", PtxType::U64);
k0.add_param("a_out", PtxType::U64);
k0.max_threads = Some(256);
k0.body.push(Instruction::BarSync { id: 0 });
let mut k1 = PtxFunction::new("kernel_b");
k1.add_param("b_in", PtxType::U64);
k1.add_param("b_out", PtxType::U64);
k1.max_threads = Some(256);
k1.body.push(Instruction::BarSync { id: 0 });
let cand = FusionAnalysis::analyze_pair(&k0, &k1);
assert!(cand.is_some());
let c = cand.as_ref().map(|c| c.fusion_type);
assert_eq!(c, Some(FusionType::Horizontal));
}
#[test]
fn test_constraints_pass() {
let cand = FusionCandidate {
estimated_speedup: 1.5,
..base_candidate(FusionType::Elementwise)
};
let constraints = vec![
FusionConstraint::SameGridDimensions,
FusionConstraint::NoSharedMemoryConflict,
FusionConstraint::RegisterBudget(255),
];
assert!(FusionAnalysis::check_constraints(&cand, &constraints));
}
#[test]
fn test_constraints_register_budget_exceeded() {
let cand = FusionCandidate {
estimated_speedup: 1.3,
estimated_registers: 300,
..base_candidate(FusionType::ProducerConsumer)
};
let constraints = vec![FusionConstraint::RegisterBudget(255)];
assert!(!FusionAnalysis::check_constraints(&cand, &constraints));
}
#[test]
fn test_constraints_shared_mem_exceeded() {
let cand = FusionCandidate {
estimated_speedup: 1.3,
producer_shared_bytes: 32768,
consumer_shared_bytes: 32768,
..base_candidate(FusionType::ProducerConsumer)
};
let constraints = vec![FusionConstraint::NoSharedMemoryConflict];
assert!(!FusionAnalysis::check_constraints(&cand, &constraints));
}
#[test]
fn test_constraints_barrier_conflict() {
let cand = FusionCandidate {
shared_buffer: "buf".to_string(),
estimated_speedup: 1.3,
producer_shared_bytes: 1024,
consumer_shared_bytes: 1024,
..base_candidate(FusionType::ProducerConsumer)
};
let constraints = vec![FusionConstraint::NoBarrierConflict];
assert!(!FusionAnalysis::check_constraints(&cand, &constraints));
}
#[test]
fn test_plan_fusion_two_elementwise() {
let k0 =
make_elementwise_kernel("add", &[("input", PtxType::U64), ("output", PtxType::U64)]);
let k1 =
make_elementwise_kernel("mul", &[("input", PtxType::U64), ("output", PtxType::U64)]);
let report = plan_fusion(&[k0, k1], 255, 49152);
assert!(!report.plan.candidates.is_empty());
assert_eq!(report.plan.original_kernel_count, 2);
assert!(report.plan.fused_kernel_count <= 2);
}
#[test]
fn test_plan_fusion_empty_sequence() {
let report = plan_fusion(&[], 255, 49152);
assert!(report.plan.candidates.is_empty());
assert_eq!(report.plan.original_kernel_count, 0);
assert_eq!(report.plan.fused_kernel_count, 0);
}
#[test]
fn test_plan_fusion_single_kernel() {
let k0 =
make_elementwise_kernel("only", &[("input", PtxType::U64), ("output", PtxType::U64)]);
let report = plan_fusion(&[k0], 255, 49152);
assert!(report.plan.candidates.is_empty());
assert_eq!(report.plan.fused_kernel_count, 1);
}
#[test]
fn test_speedup_multi_kernel_is_greater_than_one() {
let cand = FusionCandidate {
estimated_registers: 16,
..base_candidate(FusionType::Elementwise)
};
let speedup = FusionAnalysis::estimate_fusion_speedup(&cand);
assert!(speedup.is_finite(), "speedup must be finite, got {speedup}");
assert!(speedup >= 1.0, "speedup must be >= 1.0, got {speedup}");
assert!(speedup > 1.0, "fusible pair should beat 1.0, got {speedup}");
}
#[test]
fn test_speedup_single_kernel_is_one() {
let cand = FusionCandidate {
kernel_count: 1,
..base_candidate(FusionType::Elementwise)
};
let speedup = FusionAnalysis::estimate_fusion_speedup(&cand);
assert!(
(speedup - 1.0).abs() < 1e-9,
"single-kernel group must be ~1.0, got {speedup}"
);
}
#[test]
fn test_speedup_more_eliminated_traffic_is_strictly_larger() {
let small = FusionCandidate {
producer_global_bytes: 1024,
consumer_global_bytes: 1024,
eliminated_global_bytes: 64,
producer_flops: 1,
consumer_flops: 1,
..base_candidate(FusionType::ProducerConsumer)
};
let large = FusionCandidate {
eliminated_global_bytes: 1024,
..small.clone()
};
let s_small = FusionAnalysis::estimate_fusion_speedup(&small);
let s_large = FusionAnalysis::estimate_fusion_speedup(&large);
assert!(
s_large > s_small,
"more eliminated traffic ({s_large}) must exceed less ({s_small})"
);
assert!(s_large.is_finite() && s_large >= 1.0);
}
#[test]
fn test_speedup_high_register_pressure() {
let high = FusionCandidate {
estimated_registers: 200,
..base_candidate(FusionType::Elementwise)
};
let low = FusionCandidate {
estimated_registers: 16,
..base_candidate(FusionType::Elementwise)
};
let high_speedup = FusionAnalysis::estimate_fusion_speedup(&high);
let low_speedup = FusionAnalysis::estimate_fusion_speedup(&low);
assert!(
high_speedup < low_speedup,
"high regs ({high_speedup}) should be < low regs ({low_speedup})"
);
assert!(high_speedup >= 1.0 && high_speedup.is_finite());
}
#[test]
fn test_speedup_no_eliminated_traffic_still_at_least_one() {
let cand = FusionCandidate {
eliminated_global_bytes: 0,
..base_candidate(FusionType::Horizontal)
};
let speedup = FusionAnalysis::estimate_fusion_speedup(&cand);
assert!(speedup.is_finite(), "must be finite, got {speedup}");
assert!(speedup >= 1.0, "must be >= 1.0, got {speedup}");
}
#[test]
fn test_speedup_capped_and_finite_on_extreme_input() {
let cand = FusionCandidate {
producer_global_bytes: usize::MAX / 4,
consumer_global_bytes: usize::MAX / 4,
eliminated_global_bytes: usize::MAX / 4,
producer_flops: 0,
consumer_flops: 0,
..base_candidate(FusionType::Elementwise)
};
let speedup = FusionAnalysis::estimate_fusion_speedup(&cand);
assert!(speedup.is_finite(), "must be finite, got {speedup}");
assert!(
(1.0..=MAX_FUSION_SPEEDUP).contains(&speedup),
"must be clamped, got {speedup}"
);
}
#[test]
fn test_global_traffic_and_op_count_from_kernel() {
let kernel =
make_elementwise_kernel("ew", &[("input", PtxType::U64), ("output", PtxType::U64)]);
assert_eq!(global_traffic_bytes(&kernel), 4);
assert_eq!(arithmetic_op_count(&kernel), 1);
}
#[test]
fn test_analyze_pair_populates_cost_fields() {
let k0 =
make_elementwise_kernel("add", &[("input", PtxType::U64), ("output", PtxType::U64)]);
let k1 =
make_elementwise_kernel("mul", &[("input", PtxType::U64), ("output", PtxType::U64)]);
let cand = FusionAnalysis::analyze_pair(&k0, &k1).expect("pair should fuse");
assert_eq!(cand.kernel_count, 2);
assert!(cand.producer_global_bytes >= 1);
assert!(cand.consumer_global_bytes >= 1);
assert!(cand.eliminated_global_bytes > 0);
assert!(cand.estimated_speedup.is_finite());
assert!(cand.estimated_speedup >= 1.0);
}
#[test]
fn test_find_data_dependencies() {
let k0 = make_elementwise_kernel(
"producer",
&[("input", PtxType::U64), ("buf", PtxType::U64)],
);
let k1 = make_elementwise_kernel(
"consumer",
&[("buf", PtxType::U64), ("output", PtxType::U64)],
);
let deps = FusionAnalysis::find_data_dependencies(&[k0, k1]);
assert!(!deps.is_empty(), "expected at least one dependency");
assert_eq!(deps[0].producer, 0);
assert_eq!(deps[0].consumer, 1);
assert_eq!(deps[0].buffer_name, "buf");
}
#[test]
fn test_no_data_dependencies() {
let k0 = make_elementwise_kernel("a", &[("a_in", PtxType::U64), ("a_out", PtxType::U64)]);
let k1 = make_elementwise_kernel("b", &[("b_in", PtxType::U64), ("b_out", PtxType::U64)]);
let deps = FusionAnalysis::find_data_dependencies(&[k0, k1]);
assert!(
deps.is_empty(),
"expected no dependencies, got {}",
deps.len()
);
}
#[test]
fn test_fusion_plan_display() {
let plan = FusionPlan {
candidates: vec![],
fused_groups: vec![vec![0, 1], vec![2]],
original_kernel_count: 3,
fused_kernel_count: 2,
estimated_total_speedup: 1.5,
};
let display = format!("{plan}");
assert!(display.contains("Original kernels: 3"));
assert!(display.contains("Fused kernels: 2"));
assert!(display.contains("1.50x"));
}
#[test]
fn test_fusion_report_display() {
let rejected_cand = FusionCandidate {
consumer_index: 2,
shared_buffer: "buf".to_string(),
estimated_speedup: 1.3,
estimated_registers: 300,
..base_candidate(FusionType::ProducerConsumer)
};
let report = FusionReport {
plan: FusionPlan {
candidates: vec![],
fused_groups: vec![vec![0], vec![1], vec![2]],
original_kernel_count: 3,
fused_kernel_count: 3,
estimated_total_speedup: 1.0,
},
rejected: vec![(rejected_cand, "register budget exceeded".to_string())],
};
let display = format!("{report}");
assert!(display.contains("Rejected candidates:"));
assert!(display.contains("register budget exceeded"));
}
#[test]
fn test_fusion_type_display() {
assert_eq!(format!("{}", FusionType::Elementwise), "elementwise");
assert_eq!(
format!("{}", FusionType::ProducerConsumer),
"producer-consumer"
);
assert_eq!(format!("{}", FusionType::Horizontal), "horizontal");
assert_eq!(format!("{}", FusionType::Vertical), "vertical");
}
#[test]
fn test_access_pattern_display() {
assert_eq!(format!("{}", AccessPattern::Streaming), "streaming");
assert_eq!(format!("{}", AccessPattern::Random), "random");
assert_eq!(format!("{}", AccessPattern::Strided(4)), "strided(4)");
assert_eq!(format!("{}", AccessPattern::Unknown), "unknown");
}
}