use std::fmt::Debug;
use scirs2_core::numeric::Float;
use std::collections::{HashMap, HashSet, VecDeque};
use super::super::frontend::{
OperandId, OperationAttributes, OperationId, OperationType, XLAComputation, XLAOperation,
};
use super::OptimizationPipelineConfig;
use crate::error::Result;
pub struct KernelFusionEngine<T: Float + Debug + Send + Sync + 'static> {
config: FusionConfig,
elementwise_fusion: ElementwiseFusionPass<T>,
producer_consumer_fusion: ProducerConsumerFusionPass<T>,
loop_fusion: LoopFusionPass<T>,
multi_output_fusion: MultiOutputFusionPass<T>,
convolution_fusion: ConvolutionFusionPass<T>,
custom_fusion: CustomFusionPass<T>,
fusion_stats: FusionStatistics,
}
#[derive(Debug, Clone)]
pub struct FusionConfig {
pub enable_elementwise_fusion: bool,
pub enable_producer_consumer_fusion: bool,
pub enable_loop_fusion: bool,
pub enable_multi_output_fusion: bool,
pub enable_convolution_fusion: bool,
pub max_cluster_size: usize,
pub memory_threshold: usize,
pub aggressive_fusion: bool,
pub min_ops_for_fusion: usize,
}
#[derive(Debug, Default)]
pub struct FusionStatistics {
pub total_fusions: usize,
pub fusions_by_type: HashMap<String, usize>,
pub memory_savings: usize,
pub estimated_speedup: f64,
pub ops_before_fusion: usize,
pub ops_after_fusion: usize,
}
#[derive(Debug, Clone)]
pub struct FusionCluster<T: Float + Debug + Send + Sync + 'static> {
pub id: String,
pub operations: Vec<OperationId>,
pub inputs: Vec<OperandId>,
pub outputs: Vec<OperandId>,
pub fusion_type: FusionType,
pub estimated_benefit: f64,
pub memory_requirements: usize,
pub execution_info: ClusterExecutionInfo,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FusionType {
Elementwise,
ProducerConsumer,
Loop,
MultiOutput,
Convolution,
Custom(String),
}
#[derive(Debug, Clone, Default)]
pub struct ClusterExecutionInfo {
pub execution_time_us: u64,
pub memory_bandwidth_util: f64,
pub compute_utilization: f64,
pub parallelization_factor: f64,
}
pub struct ElementwiseFusionPass<T: Float + Debug + Send + Sync + 'static> {
clusters: Vec<FusionCluster<T>>,
supported_ops: HashSet<OperationType>,
max_cluster_size: usize,
}
pub struct ProducerConsumerFusionPass<T: Float + Debug + Send + Sync + 'static> {
chains: Vec<ProducerConsumerChain>,
max_chain_length: usize,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug)]
pub struct ProducerConsumerChain {
pub operations: Vec<OperationId>,
pub score: f64,
pub memory_reduction: usize,
}
pub struct LoopFusionPass<T: Float + Debug + Send + Sync + 'static> {
loops: Vec<LoopStructure>,
fusion_candidates: Vec<LoopFusionCandidate>,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug)]
pub struct LoopStructure {
pub id: String,
pub body_operations: Vec<OperationId>,
pub bounds: LoopBounds,
pub iteration_count: Option<usize>,
}
#[derive(Debug)]
pub struct LoopBounds {
pub lower: i64,
pub upper: i64,
pub step: i64,
}
#[derive(Debug)]
pub struct LoopFusionCandidate {
pub loops: Vec<String>,
pub fusion_type: LoopFusionType,
pub benefit: f64,
}
#[derive(Debug)]
pub enum LoopFusionType {
Horizontal,
Vertical,
Diagonal,
}
pub struct MultiOutputFusionPass<T: Float + Debug + Send + Sync + 'static> {
opportunities: Vec<MultiOutputOpportunity>,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug)]
pub struct MultiOutputOpportunity {
pub common_computation: Vec<OperationId>,
pub outputs: Vec<OperandId>,
pub savings: f64,
}
pub struct ConvolutionFusionPass<T: Float + Debug + Send + Sync + 'static> {
patterns: Vec<ConvolutionPattern>,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug)]
pub struct ConvolutionPattern {
pub name: String,
pub operations: Vec<OperationType>,
pub benefit: f64,
}
pub struct CustomFusionPass<T: Float + Debug + Send + Sync + 'static> {
patterns: Vec<CustomFusionPattern>,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug)]
pub struct CustomFusionPattern {
pub name: String,
pub matcher: String,
pub generator: String,
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> KernelFusionEngine<T> {
pub fn new(config: &OptimizationPipelineConfig) -> Self {
let fusion_config = FusionConfig {
enable_elementwise_fusion: true,
enable_producer_consumer_fusion: true,
enable_loop_fusion: config.aggressive_mode,
enable_multi_output_fusion: true,
enable_convolution_fusion: true,
max_cluster_size: if config.aggressive_mode { 16 } else { 8 },
memory_threshold: 1024 * 1024, aggressive_fusion: config.aggressive_mode,
min_ops_for_fusion: 2,
};
Self {
config: fusion_config.clone(),
elementwise_fusion: ElementwiseFusionPass::new(&fusion_config),
producer_consumer_fusion: ProducerConsumerFusionPass::new(&fusion_config),
loop_fusion: LoopFusionPass::new(&fusion_config),
multi_output_fusion: MultiOutputFusionPass::new(&fusion_config),
convolution_fusion: ConvolutionFusionPass::new(&fusion_config),
custom_fusion: CustomFusionPass::new(&fusion_config),
fusion_stats: FusionStatistics::default(),
}
}
pub fn fuse_kernels(&mut self, computation: XLAComputation<T>) -> Result<XLAComputation<T>> {
let (fused, _changed) = self.fuse_kernels_tracked(computation)?;
Ok(fused)
}
pub fn fuse_kernels_tracked(
&mut self,
computation: XLAComputation<T>,
) -> Result<(XLAComputation<T>, bool)> {
let mut current_computation = computation;
self.fusion_stats.ops_before_fusion = current_computation.operations.len();
self.fusion_stats.fusions_by_type.clear();
if self.config.enable_elementwise_fusion {
let fused = self
.elementwise_fusion
.apply_fusion_tracked(&mut current_computation)?;
if fused > 0 {
self.fusion_stats
.fusions_by_type
.insert("elementwise".to_string(), fused);
}
}
self.fusion_stats.ops_after_fusion = current_computation.operations.len();
self.fusion_stats.total_fusions = self.fusion_stats.fusions_by_type.values().sum();
self.fusion_stats.memory_savings = self
.elementwise_fusion
.clusters
.iter()
.map(|cluster| cluster.memory_requirements)
.sum();
self.fusion_stats.estimated_speedup = self
.elementwise_fusion
.clusters
.iter()
.map(|cluster| cluster.estimated_benefit)
.sum::<f64>();
Ok((current_computation, self.fusion_stats.total_fusions > 0))
}
pub fn unimplemented_strategies(&self) -> Vec<&'static str> {
let mut unimplemented = Vec::new();
if self.config.enable_producer_consumer_fusion {
unimplemented.push("producer_consumer");
}
if self.config.enable_loop_fusion {
unimplemented.push("loop");
}
if self.config.enable_multi_output_fusion {
unimplemented.push("multi_output");
}
if self.config.enable_convolution_fusion {
unimplemented.push("convolution");
}
unimplemented
}
pub fn get_statistics(&self) -> &FusionStatistics {
&self.fusion_stats
}
pub fn reset(&mut self) {
self.elementwise_fusion.clusters.clear();
self.producer_consumer_fusion.chains.clear();
self.loop_fusion.loops.clear();
self.loop_fusion.fusion_candidates.clear();
self.multi_output_fusion.opportunities.clear();
self.convolution_fusion.patterns.clear();
self.custom_fusion.patterns.clear();
self.fusion_stats = FusionStatistics::default();
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> ElementwiseFusionPass<T> {
pub fn new(config: &FusionConfig) -> Self {
let mut supported_ops = HashSet::new();
supported_ops.insert(OperationType::Add);
supported_ops.insert(OperationType::Multiply);
supported_ops.insert(OperationType::Subtract);
supported_ops.insert(OperationType::Divide);
supported_ops.insert(OperationType::Maximum);
supported_ops.insert(OperationType::Minimum);
supported_ops.insert(OperationType::Abs);
supported_ops.insert(OperationType::Exp);
supported_ops.insert(OperationType::Log);
supported_ops.insert(OperationType::Sqrt);
Self {
clusters: Vec::new(),
supported_ops,
max_cluster_size: config.max_cluster_size.max(2),
}
}
pub fn apply_fusion(
&mut self,
mut computation: XLAComputation<T>,
) -> Result<XLAComputation<T>> {
self.apply_fusion_tracked(&mut computation)?;
Ok(computation)
}
pub fn apply_fusion_tracked(&mut self, computation: &mut XLAComputation<T>) -> Result<usize> {
self.find_elementwise_clusters(computation)?;
self.create_fused_operations(computation)
}
fn find_elementwise_clusters(&mut self, computation: &XLAComputation<T>) -> Result<()> {
self.clusters.clear();
let mut visited = HashSet::new();
for operation in &computation.operations {
if visited.contains(&operation.id) || !self.is_elementwise_operation(&operation.op_type)
{
continue;
}
let cluster = self.build_elementwise_cluster(operation, computation, &mut visited)?;
if cluster.operations.len() >= 2 {
self.clusters.push(cluster);
}
}
Ok(())
}
fn build_elementwise_cluster(
&self,
start_op: &XLAOperation<T>,
computation: &XLAComputation<T>,
visited: &mut HashSet<OperationId>,
) -> Result<FusionCluster<T>> {
let mut cluster_ops = vec![start_op.id];
let mut cluster_set: HashSet<OperationId> = HashSet::new();
cluster_set.insert(start_op.id);
let mut queue = VecDeque::new();
let mut inputs: Vec<OperandId> = Vec::new();
queue.push_back(start_op.id);
visited.insert(start_op.id);
while let Some(op_id) = queue.pop_front() {
let Some(operation) = computation.operations.iter().find(|op| op.id == op_id) else {
continue;
};
for &input_id in &operation.inputs {
let producer = self.find_producer_operation(input_id, computation);
let fusible = match producer {
Some(producer) => {
self.is_elementwise_operation(&producer.op_type)
&& !visited.contains(&producer.id)
&& cluster_ops.len() < self.max_cluster_size
&& self.can_fuse_operations(producer, operation, computation)
}
None => false,
};
match (fusible, producer) {
(true, Some(producer)) => {
cluster_ops.push(producer.id);
cluster_set.insert(producer.id);
queue.push_back(producer.id);
visited.insert(producer.id);
}
_ => {
if !inputs.contains(&input_id) {
inputs.push(input_id);
}
}
}
}
}
let declared_outputs: HashSet<OperandId> =
computation.outputs.iter().map(|o| o.operand).collect();
let mut outputs: Vec<OperandId> = Vec::new();
for &op_id in &cluster_ops {
let Some(operation) = computation.operations.iter().find(|op| op.id == op_id) else {
continue;
};
let escapes = declared_outputs.contains(&operation.output)
|| computation.operations.iter().any(|other| {
!cluster_set.contains(&other.id) && other.inputs.contains(&operation.output)
});
if escapes && !outputs.contains(&operation.output) {
outputs.push(operation.output);
}
}
let produced_inside: HashSet<OperandId> = cluster_ops
.iter()
.filter_map(|id| computation.operations.iter().find(|op| op.id == *id))
.map(|op| op.output)
.collect();
inputs.retain(|operand| !produced_inside.contains(operand));
let estimated_benefit = self.estimate_elementwise_benefit(&cluster_ops);
let memory_requirements = self.estimate_memory_requirements(&cluster_ops, computation);
let cluster = FusionCluster {
id: format!("elementwise_cluster_{}", start_op.id.0),
operations: cluster_ops,
inputs,
outputs,
fusion_type: FusionType::Elementwise,
estimated_benefit,
memory_requirements,
execution_info: ClusterExecutionInfo::default(),
_phantom: std::marker::PhantomData,
};
Ok(cluster)
}
fn is_elementwise_operation(&self, op_type: &OperationType) -> bool {
self.supported_ops.contains(op_type)
}
fn can_fuse_operations(
&self,
producer: &XLAOperation<T>,
consumer: &XLAOperation<T>,
computation: &XLAComputation<T>,
) -> bool {
let producer_shape = computation.operands.get(&producer.output).map(|o| &o.shape);
let consumer_shape = computation.operands.get(&consumer.output).map(|o| &o.shape);
match (producer_shape, consumer_shape) {
(Some(a), Some(b)) if a.dimensions == b.dimensions => {}
_ => return false,
}
if computation
.outputs
.iter()
.any(|output| output.operand == producer.output)
{
return false;
}
let external_consumers = computation
.operations
.iter()
.filter(|op| op.id != consumer.id && op.inputs.contains(&producer.output))
.count();
external_consumers == 0
}
fn find_producer_operation<'a>(
&self,
operand_id: OperandId,
computation: &'a XLAComputation<T>,
) -> Option<&'a XLAOperation<T>> {
computation
.operations
.iter()
.find(|op| op.output == operand_id)
}
fn estimate_elementwise_benefit(&self, operations: &[OperationId]) -> f64 {
operations.len().saturating_sub(1) as f64 * 0.2
}
fn estimate_memory_requirements(
&self,
operations: &[OperationId],
computation: &XLAComputation<T>,
) -> usize {
operations
.iter()
.filter_map(|id| computation.operations.iter().find(|op| op.id == *id))
.filter_map(|op| computation.operands.get(&op.output))
.map(|operand| {
operand
.shape
.element_count
.saturating_mul(std::mem::size_of::<T>())
})
.sum()
}
fn create_fused_operations(&self, computation: &mut XLAComputation<T>) -> Result<usize> {
let mut fused = 0usize;
for cluster in &self.clusters {
if cluster.operations.len() < 2 {
continue;
}
let Some(&primary_output) = cluster.outputs.first() else {
continue;
};
let insert_at = computation
.operations
.iter()
.rposition(|op| cluster.operations.contains(&op.id))
.map(|pos| pos + 1)
.unwrap_or(computation.operations.len());
let mut new_ops: Vec<XLAOperation<T>> = Vec::new();
let mut next_op_id = computation.next_free_operation_id().0;
let fused_custom = super::super::frontend::graph_capture::CustomOperation {
name: format!("fused_{}", cluster.id),
custom_attributes: HashMap::new(),
backend_config: Some("elementwise_fusion".to_string()),
};
if cluster.outputs.len() == 1 {
new_ops.push(XLAOperation {
id: super::super::frontend::graph_capture::OperationId(next_op_id),
op_type: OperationType::Custom(fused_custom),
inputs: cluster.inputs.clone(),
output: primary_output,
attributes: OperationAttributes::default(),
performance: Default::default(),
memory_requirements: Default::default(),
source_location: None,
_phantom: std::marker::PhantomData,
});
} else {
let tuple_operand = computation.allocate_operand_id();
let tuple_shape = {
let mut shape = super::super::frontend::graph_capture::TensorShape::default();
shape.tuple_shapes = cluster
.outputs
.iter()
.filter_map(|id| computation.operands.get(id))
.map(|operand| operand.shape.clone())
.collect();
shape
};
let template = computation.operands.get(&primary_output).cloned();
let tuple_operand_value = super::super::frontend::graph_capture::Operand {
id: tuple_operand,
shape: tuple_shape,
layout: template
.as_ref()
.map(|o| o.layout.clone())
.unwrap_or_default(),
dtype: template
.as_ref()
.map(|o| o.dtype)
.unwrap_or(super::super::frontend::graph_capture::DataType::F32),
metadata: Default::default(),
_phantom: std::marker::PhantomData,
};
computation
.operands
.insert(tuple_operand, tuple_operand_value);
new_ops.push(XLAOperation {
id: super::super::frontend::graph_capture::OperationId(next_op_id),
op_type: OperationType::Custom(fused_custom),
inputs: cluster.inputs.clone(),
output: tuple_operand,
attributes: OperationAttributes::default(),
performance: Default::default(),
memory_requirements: Default::default(),
source_location: None,
_phantom: std::marker::PhantomData,
});
for (index, &operand) in cluster.outputs.iter().enumerate() {
next_op_id = next_op_id.saturating_add(1);
let mut attributes = OperationAttributes::default();
attributes.attributes.insert(
"tuple_index".to_string(),
super::super::frontend::graph_capture::AttributeValue::Int(index as i64),
);
new_ops.push(XLAOperation {
id: super::super::frontend::graph_capture::OperationId(next_op_id),
op_type: OperationType::GetTupleElement,
inputs: vec![tuple_operand],
output: operand,
attributes,
performance: Default::default(),
memory_requirements: Default::default(),
source_location: None,
_phantom: std::marker::PhantomData,
});
}
}
let removed_before_insert = computation.operations
[..insert_at.min(computation.operations.len())]
.iter()
.filter(|op| cluster.operations.contains(&op.id))
.count();
computation
.operations
.retain(|op| !cluster.operations.contains(&op.id));
let splice_at = insert_at
.saturating_sub(removed_before_insert)
.min(computation.operations.len());
for (offset, op) in new_ops.into_iter().enumerate() {
computation.operations.insert(splice_at + offset, op);
}
fused += 1;
}
if fused > 0 {
computation.rebuild_dependencies();
}
Ok(fused)
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync>
ProducerConsumerFusionPass<T>
{
pub fn new(config: &FusionConfig) -> Self {
Self {
chains: Vec::new(),
max_chain_length: config.max_cluster_size,
_phantom: std::marker::PhantomData,
}
}
pub fn apply_fusion(&mut self, computation: XLAComputation<T>) -> Result<XLAComputation<T>> {
self.find_producer_consumer_chains(&computation)?;
Ok(computation)
}
pub fn chains(&self) -> &[ProducerConsumerChain] {
&self.chains
}
fn find_producer_consumer_chains(&mut self, computation: &XLAComputation<T>) -> Result<()> {
self.chains.clear();
if self.max_chain_length < 2 {
return Ok(());
}
let mut consumers: HashMap<OperandId, Vec<OperationId>> = HashMap::new();
let mut producer: HashMap<OperandId, OperationId> = HashMap::new();
for operation in &computation.operations {
producer.insert(operation.output, operation.id);
for input in &operation.inputs {
consumers.entry(*input).or_default().push(operation.id);
}
}
let by_id: HashMap<OperationId, &XLAOperation<T>> = computation
.operations
.iter()
.map(|operation| (operation.id, operation))
.collect();
let outputs: HashSet<OperandId> = computation
.outputs
.iter()
.map(|spec| spec.operand)
.collect();
let mut claimed: HashSet<OperationId> = HashSet::new();
for operation in &computation.operations {
if claimed.contains(&operation.id) {
continue;
}
let mut chain = vec![operation.id];
let mut score = 0.0f64;
let mut current = operation;
while chain.len() < self.max_chain_length {
if outputs.contains(¤t.output) {
break;
}
let Some(next_ids) = consumers.get(¤t.output) else {
break;
};
if next_ids.len() != 1 {
break;
}
let Some(next) = next_ids.first().and_then(|id| by_id.get(id)).copied() else {
break;
};
if claimed.contains(&next.id) {
break;
}
if let Some(operand) = computation.operands.get(¤t.output) {
score += operand.shape.element_count as f64;
}
chain.push(next.id);
current = next;
}
if chain.len() >= 2 {
for id in &chain {
claimed.insert(*id);
}
let memory_reduction = score as usize;
self.chains.push(ProducerConsumerChain {
operations: chain,
score,
memory_reduction,
});
}
}
self.chains.sort_by(|a, b| b.score.total_cmp(&a.score));
Ok(())
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> LoopFusionPass<T> {
pub fn new(_config: &FusionConfig) -> Self {
Self {
loops: Vec::new(),
fusion_candidates: Vec::new(),
_phantom: std::marker::PhantomData,
}
}
pub fn apply_fusion(&mut self, computation: XLAComputation<T>) -> Result<XLAComputation<T>> {
Ok(computation)
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> MultiOutputFusionPass<T> {
pub fn new(_config: &FusionConfig) -> Self {
Self {
opportunities: Vec::new(),
_phantom: std::marker::PhantomData,
}
}
pub fn apply_fusion(&mut self, computation: XLAComputation<T>) -> Result<XLAComputation<T>> {
Ok(computation)
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> ConvolutionFusionPass<T> {
pub fn new(_config: &FusionConfig) -> Self {
let patterns = vec![ConvolutionPattern {
name: "conv_bias_relu".to_string(),
operations: vec![
OperationType::Convolution(
super::super::frontend::graph_capture::ConvolutionConfig {
strides: vec![1, 1],
padding: super::super::frontend::graph_capture::PaddingConfig::Same,
dilation: vec![1, 1],
feature_group_count: 1,
batch_group_count: 1,
},
),
OperationType::Add, OperationType::Maximum, ],
benefit: 0.3,
}];
Self {
patterns,
_phantom: std::marker::PhantomData,
}
}
pub fn apply_fusion(&mut self, computation: XLAComputation<T>) -> Result<XLAComputation<T>> {
Ok(computation)
}
}
impl<T: Float + Debug + Default + std::fmt::Debug + Clone + Send + Sync> CustomFusionPass<T> {
pub fn new(_config: &FusionConfig) -> Self {
Self {
patterns: Vec::new(),
_phantom: std::marker::PhantomData,
}
}
pub fn apply_fusion(&mut self, computation: XLAComputation<T>) -> Result<XLAComputation<T>> {
Ok(computation)
}
}
#[cfg(test)]
mod tests {
use super::super::{ComputeCapability, HardwareTarget};
use super::*;
#[test]
fn test_kernel_fusion_engine_creation() {
let config = OptimizationPipelineConfig {
optimization_level: crate::main_types::XLAOptimizationLevel::Standard,
enable_graph_optimization: true,
enable_kernel_fusion: true,
enable_memory_optimization: true,
enable_scheduling_optimization: true,
max_optimization_time: 300,
target_hardware: HardwareTarget {
tpu_version: "v4".to_string(),
num_cores: 4,
memory_capacity: 1024 * 1024 * 1024,
memory_bandwidth: 1600.0,
compute_capability: ComputeCapability {
matrix_unit_dims: (128, 128),
vector_unit_width: 256,
supported_dtypes: vec!["F32".to_string()],
special_instructions: vec![],
},
},
custom_passes: vec![],
aggressive_mode: false,
debug_mode: false,
};
let engine: KernelFusionEngine<f32> = KernelFusionEngine::new(&config);
assert_eq!(engine.fusion_stats.total_fusions, 0);
assert!(engine.config.enable_elementwise_fusion);
}
#[test]
fn test_elementwise_fusion_pass() {
let config = FusionConfig {
enable_elementwise_fusion: true,
enable_producer_consumer_fusion: true,
enable_loop_fusion: false,
enable_multi_output_fusion: true,
enable_convolution_fusion: true,
max_cluster_size: 8,
memory_threshold: 1024 * 1024,
aggressive_fusion: false,
min_ops_for_fusion: 2,
};
let pass: ElementwiseFusionPass<f32> = ElementwiseFusionPass::new(&config);
assert!(!pass.supported_ops.is_empty());
assert!(pass.supported_ops.contains(&OperationType::Add));
assert!(pass.supported_ops.contains(&OperationType::Multiply));
}
}