use std::fmt;
use crate::analysis::kernel_fusion::FusionCandidate;
use crate::arch::SmVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FusionVerdict {
Beneficial,
RegisterSpill,
SharedMemoryOverflow,
NotWorthwhile,
NothingToFuse,
}
impl FusionVerdict {
#[must_use]
pub const fn is_fuse(self) -> bool {
matches!(self, Self::Beneficial)
}
}
impl fmt::Display for FusionVerdict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Beneficial => write!(f, "beneficial"),
Self::RegisterSpill => write!(f, "register spill"),
Self::SharedMemoryOverflow => write!(f, "shared-memory overflow"),
Self::NotWorthwhile => write!(f, "not worthwhile"),
Self::NothingToFuse => write!(f, "nothing to fuse"),
}
}
}
#[derive(Debug, Clone)]
pub struct FusionDecision {
pub should_fuse: bool,
pub verdict: FusionVerdict,
pub combined_registers: u32,
pub combined_shared_bytes: usize,
pub ilp_score: f64,
pub benefit_score: f64,
}
impl fmt::Display for FusionDecision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"fuse={} ({}): regs={}, smem={}B, ilp={:.2}, benefit={:.2}",
self.should_fuse,
self.verdict,
self.combined_registers,
self.combined_shared_bytes,
self.ilp_score,
self.benefit_score
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FusionCostModel {
pub register_file: u32,
pub register_pressure_threshold: u32,
pub shared_mem_budget: usize,
pub benefit_threshold: f64,
}
impl FusionCostModel {
#[must_use]
pub fn for_target(sm: SmVersion) -> Self {
let register_file = MAX_REGISTERS_PER_THREAD;
let regs_per_sm = REGISTERS_PER_SM;
let warps_target = OCCUPANCY_WARPS_TARGET * sm.warp_size();
let pressure_threshold = (regs_per_sm / warps_target.max(1))
.min(register_file - 1)
.max(32);
Self {
register_file,
register_pressure_threshold: pressure_threshold,
shared_mem_budget: sm.max_shared_mem_per_block() as usize,
benefit_threshold: DEFAULT_BENEFIT_THRESHOLD,
}
}
#[must_use]
pub const fn with_register_file(mut self, regs: u32) -> Self {
self.register_file = regs;
self
}
#[must_use]
pub const fn with_register_pressure_threshold(mut self, regs: u32) -> Self {
self.register_pressure_threshold = regs;
self
}
#[must_use]
pub const fn with_shared_mem_budget(mut self, bytes: usize) -> Self {
self.shared_mem_budget = bytes;
self
}
#[must_use]
pub const fn with_benefit_threshold(mut self, threshold: f64) -> Self {
self.benefit_threshold = threshold;
self
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn ilp_score(candidate: &FusionCandidate) -> f64 {
let ops = (candidate.producer_flops + candidate.consumer_flops) as f64;
let regs = f64::from(candidate.estimated_registers.max(1));
ops / regs
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn decide(&self, candidate: &FusionCandidate) -> FusionDecision {
let combined_registers = candidate.estimated_registers;
let combined_shared_bytes =
candidate.producer_shared_bytes + candidate.consumer_shared_bytes;
let ilp_score = Self::ilp_score(candidate);
if candidate.kernel_count < 2 {
return FusionDecision {
should_fuse: false,
verdict: FusionVerdict::NothingToFuse,
combined_registers,
combined_shared_bytes,
ilp_score,
benefit_score: 0.0,
};
}
if combined_registers > self.register_file {
return FusionDecision {
should_fuse: false,
verdict: FusionVerdict::RegisterSpill,
combined_registers,
combined_shared_bytes,
ilp_score,
benefit_score: 0.0,
};
}
if combined_shared_bytes > self.shared_mem_budget {
return FusionDecision {
should_fuse: false,
verdict: FusionVerdict::SharedMemoryOverflow,
combined_registers,
combined_shared_bytes,
ilp_score,
benefit_score: 0.0,
};
}
let benefit_score = self.benefit_score(candidate, ilp_score);
if benefit_score >= self.benefit_threshold {
FusionDecision {
should_fuse: true,
verdict: FusionVerdict::Beneficial,
combined_registers,
combined_shared_bytes,
ilp_score,
benefit_score,
}
} else {
FusionDecision {
should_fuse: false,
verdict: FusionVerdict::NotWorthwhile,
combined_registers,
combined_shared_bytes,
ilp_score,
benefit_score,
}
}
}
#[must_use]
pub fn should_fuse(&self, candidate: &FusionCandidate) -> bool {
self.decide(candidate).should_fuse
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
fn benefit_score(&self, candidate: &FusionCandidate, ilp_score: f64) -> f64 {
let base = candidate.estimated_speedup.max(1.0);
let reg_penalty = if candidate.estimated_registers <= self.register_pressure_threshold {
1.0
} else {
let over = f64::from(candidate.estimated_registers - self.register_pressure_threshold);
let span = f64::from(
self.register_file
.saturating_sub(self.register_pressure_threshold)
.max(1),
);
(over / span)
.mul_add(-(1.0 - OCCUPANCY_FLOOR), 1.0)
.max(OCCUPANCY_FLOOR)
};
let smem = (candidate.producer_shared_bytes + candidate.consumer_shared_bytes) as f64;
let budget = self.shared_mem_budget.max(1) as f64;
let smem_penalty = if smem <= budget * 0.5 {
1.0
} else {
let over = smem - budget * 0.5;
let span = budget * 0.5;
(over / span)
.mul_add(-(1.0 - OCCUPANCY_FLOOR), 1.0)
.max(OCCUPANCY_FLOOR)
};
let ilp_bonus =
1.0 + ((ilp_score - ILP_NEUTRAL) * ILP_BONUS_WEIGHT).clamp(0.0, ILP_BONUS_CAP);
base * reg_penalty * smem_penalty * ilp_bonus
}
}
impl Default for FusionCostModel {
fn default() -> Self {
Self::for_target(SmVersion::Sm80)
}
}
const MAX_REGISTERS_PER_THREAD: u32 = 256;
const REGISTERS_PER_SM: u32 = 65_536;
const OCCUPANCY_WARPS_TARGET: u32 = 8;
const OCCUPANCY_FLOOR: f64 = 0.5;
const DEFAULT_BENEFIT_THRESHOLD: f64 = 1.05;
const ILP_NEUTRAL: f64 = 1.0;
const ILP_BONUS_WEIGHT: f64 = 0.05;
const ILP_BONUS_CAP: f64 = 0.25;
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::kernel_fusion::FusionType;
fn light_candidate() -> FusionCandidate {
FusionCandidate {
producer_index: 0,
consumer_index: 1,
shared_buffer: String::new(),
fusion_type: FusionType::Elementwise,
estimated_speedup: 1.4,
producer_shared_bytes: 0,
consumer_shared_bytes: 0,
estimated_registers: 40,
producer_global_bytes: 256,
consumer_global_bytes: 256,
eliminated_global_bytes: 256,
producer_flops: 64,
consumer_flops: 64,
kernel_count: 2,
}
}
#[test]
fn test_for_target_budgets() {
let m80 = FusionCostModel::for_target(SmVersion::Sm80);
assert_eq!(m80.register_file, 256);
assert_eq!(m80.shared_mem_budget, 163_840);
assert!(m80.register_pressure_threshold < m80.register_file);
assert!(m80.register_pressure_threshold >= 32);
let m75 = FusionCostModel::for_target(SmVersion::Sm75);
assert_eq!(m75.shared_mem_budget, 65_536);
assert!(m75.shared_mem_budget < m80.shared_mem_budget);
}
#[test]
fn test_builder_overrides() {
let m = FusionCostModel::for_target(SmVersion::Sm80)
.with_register_file(200)
.with_register_pressure_threshold(96)
.with_shared_mem_budget(8192)
.with_benefit_threshold(2.0);
assert_eq!(m.register_file, 200);
assert_eq!(m.register_pressure_threshold, 96);
assert_eq!(m.shared_mem_budget, 8192);
assert!((m.benefit_threshold - 2.0).abs() < 1e-12);
}
#[test]
fn test_default_is_ampere() {
let d = FusionCostModel::default();
let a = FusionCostModel::for_target(SmVersion::Sm80);
assert_eq!(d.register_file, a.register_file);
assert_eq!(d.shared_mem_budget, a.shared_mem_budget);
}
#[test]
fn test_fuses_low_pressure_pair() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let decision = model.decide(&light_candidate());
assert!(decision.should_fuse, "{decision}");
assert_eq!(decision.verdict, FusionVerdict::Beneficial);
assert!(decision.benefit_score >= model.benefit_threshold);
}
#[test]
fn test_should_fuse_wrapper_agrees_with_decide() {
let model = FusionCostModel::for_target(SmVersion::Sm90);
let cand = light_candidate();
assert_eq!(model.should_fuse(&cand), model.decide(&cand).should_fuse);
}
#[test]
fn test_fuses_with_modest_shared_memory() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let cand = FusionCandidate {
producer_shared_bytes: 4096,
consumer_shared_bytes: 4096,
..light_candidate()
};
assert!(model.decide(&cand).should_fuse);
}
#[test]
fn test_refuses_register_spill() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let cand = FusionCandidate {
estimated_registers: 400, ..light_candidate()
};
let decision = model.decide(&cand);
assert!(!decision.should_fuse);
assert_eq!(decision.verdict, FusionVerdict::RegisterSpill);
}
#[test]
fn test_register_spill_exactly_at_limit_is_allowed() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let cand = FusionCandidate {
estimated_registers: model.register_file,
estimated_speedup: 2.0,
..light_candidate()
};
let decision = model.decide(&cand);
assert_ne!(decision.verdict, FusionVerdict::RegisterSpill);
}
#[test]
fn test_refuses_shared_memory_overflow() {
let model = FusionCostModel::for_target(SmVersion::Sm75).with_shared_mem_budget(32_768);
let cand = FusionCandidate {
producer_shared_bytes: 20_000,
consumer_shared_bytes: 20_000, ..light_candidate()
};
let decision = model.decide(&cand);
assert!(!decision.should_fuse);
assert_eq!(decision.verdict, FusionVerdict::SharedMemoryOverflow);
}
#[test]
fn test_shared_memory_just_fits() {
let model = FusionCostModel::for_target(SmVersion::Sm80).with_shared_mem_budget(16_384);
let cand = FusionCandidate {
producer_shared_bytes: 8_000,
consumer_shared_bytes: 8_000, estimated_speedup: 2.0,
..light_candidate()
};
let decision = model.decide(&cand);
assert_ne!(decision.verdict, FusionVerdict::SharedMemoryOverflow);
}
#[test]
fn test_refuses_when_not_worthwhile() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let cand = FusionCandidate {
estimated_speedup: 1.0,
producer_flops: 0,
consumer_flops: 0,
..light_candidate()
};
let decision = model.decide(&cand);
assert!(!decision.should_fuse);
assert_eq!(decision.verdict, FusionVerdict::NotWorthwhile);
}
#[test]
fn test_high_register_pressure_taxes_benefit() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let low = FusionCandidate {
estimated_registers: 32,
..light_candidate()
};
let high = FusionCandidate {
estimated_registers: model.register_file - 1,
..light_candidate()
};
let low_b = model.decide(&low).benefit_score;
let high_b = model.decide(&high).benefit_score;
assert!(
high_b < low_b,
"high pressure benefit ({high_b}) must be < low ({low_b})"
);
}
#[test]
fn test_single_kernel_nothing_to_fuse() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let cand = FusionCandidate {
kernel_count: 1,
..light_candidate()
};
let decision = model.decide(&cand);
assert!(!decision.should_fuse);
assert_eq!(decision.verdict, FusionVerdict::NothingToFuse);
}
#[test]
fn test_ilp_score_scales_with_ops_per_register() {
let few_ops = FusionCandidate {
producer_flops: 8,
consumer_flops: 8,
estimated_registers: 64,
..light_candidate()
};
let many_ops = FusionCandidate {
producer_flops: 256,
consumer_flops: 256,
estimated_registers: 64,
..light_candidate()
};
assert!(
FusionCostModel::ilp_score(&many_ops) > FusionCostModel::ilp_score(&few_ops),
"more ops per register must raise the ILP score"
);
}
#[test]
fn test_ilp_bonus_lifts_benefit() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let low_ilp = FusionCandidate {
producer_flops: 1,
consumer_flops: 1,
estimated_registers: 64,
..light_candidate()
};
let high_ilp = FusionCandidate {
producer_flops: 1024,
consumer_flops: 1024,
estimated_registers: 64,
..light_candidate()
};
let low_b = model.decide(&low_ilp).benefit_score;
let high_b = model.decide(&high_ilp).benefit_score;
assert!(high_b >= low_b, "ILP bonus must not reduce the benefit");
}
#[test]
fn test_verdict_display() {
assert_eq!(format!("{}", FusionVerdict::Beneficial), "beneficial");
assert_eq!(
format!("{}", FusionVerdict::RegisterSpill),
"register spill"
);
assert_eq!(
format!("{}", FusionVerdict::SharedMemoryOverflow),
"shared-memory overflow"
);
assert_eq!(
format!("{}", FusionVerdict::NotWorthwhile),
"not worthwhile"
);
assert_eq!(
format!("{}", FusionVerdict::NothingToFuse),
"nothing to fuse"
);
}
#[test]
fn test_decision_display_contains_fields() {
let model = FusionCostModel::for_target(SmVersion::Sm80);
let s = format!("{}", model.decide(&light_candidate()));
assert!(s.contains("fuse=true"));
assert!(s.contains("regs="));
assert!(s.contains("smem="));
assert!(s.contains("ilp="));
}
#[test]
fn test_verdict_is_fuse_helper() {
assert!(FusionVerdict::Beneficial.is_fuse());
assert!(!FusionVerdict::RegisterSpill.is_fuse());
assert!(!FusionVerdict::NotWorthwhile.is_fuse());
}
}