Files
rustytorch/crates/core/rtx-fusion/src/analyzer.rs
T
osobhandClaude Sonnet 5 4aaa36a57a style: cargo fmt --workspace (whitespace/wrapping only, no semantic change)
Whole-workspace rustfmt pass picked up while iterating on Mamba GPU
backward work. Verified formatting-only via diff sampling; no logic
changed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-10 07:09:36 -07:00

1296 lines
46 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Fusion pattern detection and analysis
//!
//! This module analyzes operation streams to detect fusion opportunities.
//! It supports multiple fusion patterns including elementwise chains,
//! softmax patterns, layer normalization, and activation chains.
use crate::config::FusionConfig;
use crate::kernel::{DType, KernelSignature, StreamOpKind, StreamOperation, TensorId};
use crate::stream::OperationStream;
use std::collections::{HashMap, HashSet};
/// Pre-computed analysis context for efficient pattern detection
/// This avoids rebuilding data structures for each detection pass
struct AnalysisContext<'a> {
/// Reference to the operations slice
ops: &'a [StreamOperation],
/// Map from output tensor ID to operation index
output_to_op: HashMap<TensorId, usize>,
/// Map from operation index to indices of operations that consume its output
op_consumers: HashMap<usize, Vec<usize>>,
/// Map from tensor ID to indices of operations that consume it
tensor_consumers: HashMap<TensorId, Vec<usize>>,
}
impl<'a> AnalysisContext<'a> {
/// Build analysis context from operations
fn new(ops: &'a [StreamOperation]) -> Self {
let capacity = ops.len();
// Pre-allocate with estimated capacity
let mut output_to_op = HashMap::with_capacity(capacity);
let mut op_consumers: HashMap<usize, Vec<usize>> = HashMap::with_capacity(capacity);
let mut tensor_consumers: HashMap<TensorId, Vec<usize>> =
HashMap::with_capacity(capacity * 2);
// Single pass to build all maps
for (idx, op) in ops.iter().enumerate() {
output_to_op.insert(op.output, idx);
for &input in &op.inputs {
tensor_consumers.entry(input).or_default().push(idx);
}
}
// Build op_consumers from tensor_consumers
for (idx, op) in ops.iter().enumerate() {
if let Some(consumers) = tensor_consumers.get(&op.output) {
op_consumers.insert(idx, consumers.clone());
}
}
Self {
ops,
output_to_op,
op_consumers,
tensor_consumers,
}
}
/// Get the operation at an index
#[inline]
fn get_op(&self, idx: usize) -> &StreamOperation {
&self.ops[idx]
}
/// Get the producer operation index for a tensor
#[inline]
fn producer_of(&self, tensor: TensorId) -> Option<usize> {
self.output_to_op.get(&tensor).copied()
}
/// Get consumer operation indices for an operation
#[inline]
fn consumers_of_op(&self, op_idx: usize) -> &[usize] {
self.op_consumers
.get(&op_idx)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
/// Get consumer operation indices for a tensor
#[inline]
fn consumers_of_tensor(&self, tensor: TensorId) -> &[usize] {
self.tensor_consumers
.get(&tensor)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
/// Check if a tensor is produced by an operation in the stream
#[inline]
fn is_internal(&self, tensor: TensorId) -> bool {
self.output_to_op.contains_key(&tensor)
}
}
/// Types of fusion patterns that can be detected
#[derive(Debug, Clone, PartialEq)]
pub enum FusionPattern {
/// Chain of elementwise operations (add, mul, exp, relu, etc.)
ElementwiseChain,
/// Softmax pattern: subtract_max -> exp -> sum -> divide
/// Includes the reduction dimension for proper kernel generation
SoftmaxPattern { dim: Option<usize> },
/// Layer normalization pattern: (x - mean) / sqrt(var + eps) * scale + bias
LayerNormPattern { eps: f32, dim: Option<usize> },
/// RMS normalization pattern: x / sqrt(mean(x^2) + eps) * scale
RmsNormPattern { eps: f32, dim: Option<usize> },
/// Activation chain (e.g., GELU, SiLU with surrounding ops)
ActivationChain,
/// Reduction followed by broadcast and element-wise ops
ReductionBroadcast {
reduction_op: ReductionType,
dim: Option<usize>,
},
/// Generic fuseable sequence
Generic,
}
/// Types of reduction operations
#[derive(Debug, Clone, PartialEq)]
pub enum ReductionType {
Sum,
Mean,
Max,
Min,
}
impl FusionPattern {
/// Get the expected speedup multiplier for this pattern
pub fn expected_speedup(&self) -> f64 {
match self {
FusionPattern::ElementwiseChain => 2.0,
FusionPattern::SoftmaxPattern { .. } => 3.0,
FusionPattern::LayerNormPattern { .. } => 2.5,
FusionPattern::RmsNormPattern { .. } => 2.5,
FusionPattern::ActivationChain => 1.8,
FusionPattern::ReductionBroadcast { .. } => 1.5,
FusionPattern::Generic => 1.5,
}
}
/// Get the name of this pattern
pub fn name(&self) -> &'static str {
match self {
FusionPattern::ElementwiseChain => "elementwise_chain",
FusionPattern::SoftmaxPattern { .. } => "softmax",
FusionPattern::LayerNormPattern { .. } => "layer_norm",
FusionPattern::RmsNormPattern { .. } => "rms_norm",
FusionPattern::ActivationChain => "activation_chain",
FusionPattern::ReductionBroadcast { .. } => "reduction_broadcast",
FusionPattern::Generic => "generic",
}
}
/// Check if this pattern involves reduction operations
pub fn has_reduction(&self) -> bool {
matches!(
self,
FusionPattern::SoftmaxPattern { .. }
| FusionPattern::LayerNormPattern { .. }
| FusionPattern::RmsNormPattern { .. }
| FusionPattern::ReductionBroadcast { .. }
)
}
/// Get the reduction dimension if applicable
pub fn reduction_dim(&self) -> Option<usize> {
match self {
FusionPattern::SoftmaxPattern { dim } => *dim,
FusionPattern::LayerNormPattern { dim, .. } => *dim,
FusionPattern::RmsNormPattern { dim, .. } => *dim,
FusionPattern::ReductionBroadcast { dim, .. } => *dim,
_ => None,
}
}
}
/// A detected fusion opportunity
#[derive(Debug, Clone)]
pub struct FusionOpportunity {
/// The type of fusion pattern detected
pub pattern: FusionPattern,
/// Indices of operations in the stream that form this opportunity
pub operation_indices: Vec<usize>,
/// The operations themselves
pub operations: Vec<StreamOpKind>,
/// External input tensor IDs (not produced within the fusion group)
pub external_inputs: Vec<TensorId>,
/// Output tensor ID
pub output: TensorId,
/// Estimated speedup from fusing these operations
pub estimated_speedup: f64,
/// Data type of the operations
pub dtype: DType,
}
impl FusionOpportunity {
/// Get the number of operations in this opportunity
pub fn operation_count(&self) -> usize {
self.operation_indices.len()
}
/// Check if this opportunity is worthwhile based on minimum ops threshold
pub fn is_worthwhile(&self, min_ops: usize) -> bool {
self.operation_indices.len() >= min_ops && self.estimated_speedup > 1.0
}
/// Generate a kernel signature for this opportunity
pub fn to_kernel_signature(&self) -> KernelSignature {
KernelSignature::new(
self.operations.clone(),
self.external_inputs.len(),
self.dtype,
)
}
/// Calculate the benefit score (ops × speedup)
pub fn benefit_score(&self) -> f64 {
self.operation_count() as f64 * self.estimated_speedup
}
}
/// Analyzer for detecting fusion opportunities in operation streams
#[derive(Debug)]
pub struct FusionAnalyzer {
config: FusionConfig,
}
impl FusionAnalyzer {
/// Create a new fusion analyzer with the given configuration
pub fn new(config: FusionConfig) -> Self {
Self { config }
}
/// Create an analyzer with default configuration
pub fn with_defaults() -> Self {
Self::new(FusionConfig::default())
}
/// Analyze an operation stream and return fusion opportunities
pub fn analyze(&self, stream: &OperationStream) -> Vec<FusionOpportunity> {
if !self.config.enabled || stream.is_empty() {
return Vec::new();
}
// Build analysis context once for all detection passes
let ops = stream.operations();
let ctx = AnalysisContext::new(&ops);
let mut opportunities = Vec::with_capacity(ops.len() / 2);
// Detect elementwise chains (primary fusion target)
opportunities.extend(self.detect_elementwise_chains_opt(&ctx));
// Detect advanced patterns if enabled
if self.config.detect_advanced_patterns {
opportunities.extend(self.detect_softmax_patterns_opt(&ctx));
opportunities.extend(self.detect_layer_norm_patterns_opt(&ctx));
opportunities.extend(self.detect_activation_chains_opt(&ctx));
}
// Sort by benefit score (highest first)
opportunities.sort_by(|a, b| {
b.benefit_score()
.partial_cmp(&a.benefit_score())
.unwrap_or(std::cmp::Ordering::Equal)
});
// Remove overlapping opportunities (greedy selection)
self.remove_overlaps(opportunities)
}
/// Original analyze method that doesn't use the optimized context (for backward compatibility)
#[allow(dead_code)]
fn analyze_legacy(&self, stream: &OperationStream) -> Vec<FusionOpportunity> {
if !self.config.enabled || stream.is_empty() {
return Vec::new();
}
let mut opportunities = Vec::new();
// Detect elementwise chains (primary fusion target)
opportunities.extend(self.detect_elementwise_chains(stream));
// Detect advanced patterns if enabled
if self.config.detect_advanced_patterns {
opportunities.extend(self.detect_softmax_patterns(stream));
opportunities.extend(self.detect_layer_norm_patterns(stream));
opportunities.extend(self.detect_activation_chains(stream));
}
// Sort by benefit score (highest first)
opportunities.sort_by(|a, b| {
b.benefit_score()
.partial_cmp(&a.benefit_score())
.unwrap_or(std::cmp::Ordering::Equal)
});
// Remove overlapping opportunities (greedy selection)
self.remove_overlaps(opportunities)
}
/// Detect chains of elementwise operations
fn detect_elementwise_chains(&self, stream: &OperationStream) -> Vec<FusionOpportunity> {
let ops = stream.operations();
if ops.is_empty() {
return Vec::new();
}
let mut opportunities = Vec::new();
let mut visited: HashSet<usize> = HashSet::new();
// Build a map from output tensor to operation index
let mut output_to_op: HashMap<TensorId, usize> = HashMap::new();
for (idx, op) in ops.iter().enumerate() {
output_to_op.insert(op.output, idx);
}
// Build consumer map
let mut consumers: HashMap<usize, Vec<usize>> = HashMap::new();
for (idx, op) in ops.iter().enumerate() {
for &input in &op.inputs {
if let Some(&producer_idx) = output_to_op.get(&input) {
consumers.entry(producer_idx).or_default().push(idx);
}
}
}
// Find chain start points (fuseable ops with no fuseable producer)
for (idx, op) in ops.iter().enumerate() {
if visited.contains(&idx) || !op.op.is_fuseable() {
continue;
}
// Check if this is a chain start
let is_start = op.inputs.iter().all(|input| {
output_to_op
.get(input)
.map(|&producer_idx| !ops[producer_idx].op.is_fuseable())
.unwrap_or(true)
});
if !is_start {
continue;
}
// Extend the chain greedily
let mut chain = vec![idx];
let mut chain_outputs: HashSet<TensorId> = HashSet::new();
chain_outputs.insert(op.output);
visited.insert(idx);
let mut current_idx = idx;
loop {
// Find the next operation in the chain
let next_candidates: Vec<_> = consumers
.get(&current_idx)
.map(|c| c.as_slice())
.unwrap_or(&[])
.iter()
.filter(|&&next_idx| {
!visited.contains(&next_idx)
&& ops[next_idx].op.is_fuseable()
&& ops[next_idx].inputs.iter().all(|input| {
// All inputs must be either external or from our chain
chain_outputs.contains(input) || !output_to_op.contains_key(input)
})
})
.copied()
.collect();
if next_candidates.is_empty() {
break;
}
// Take the first valid candidate (could be smarter about this)
let next_idx = next_candidates[0];
let next_op = &ops[next_idx];
chain.push(next_idx);
chain_outputs.insert(next_op.output);
visited.insert(next_idx);
current_idx = next_idx;
// Check max chain length
if chain.len() >= self.config.max_fusion_ops {
break;
}
}
// Create opportunity if chain is long enough
if chain.len() >= self.config.min_fusion_ops {
let chain_ops: Vec<_> = chain.iter().map(|&i| ops[i].op.clone()).collect();
let external_inputs = self.find_external_inputs(&chain, ops, &output_to_op);
let output = ops[*chain.last().unwrap()].output;
let dtype = ops[chain[0]].dtype;
// Calculate speedup with bonus for longer chains
let base_speedup = FusionPattern::ElementwiseChain.expected_speedup();
let chain_bonus = 1.0 + (chain.len() as f64 - 2.0) * 0.1;
let estimated_speedup = base_speedup * chain_bonus;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::ElementwiseChain,
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup,
dtype,
});
}
}
opportunities
}
/// Detect softmax patterns
/// Pattern: (sub max) -> exp -> (sum dim) -> div
/// Or simpler: exp -> sum -> div
fn detect_softmax_patterns(&self, stream: &OperationStream) -> Vec<FusionOpportunity> {
let ops = stream.operations();
let mut opportunities = Vec::new();
// Build maps for traversal
let mut output_to_op: HashMap<TensorId, usize> = HashMap::new();
let mut consumers: HashMap<TensorId, Vec<usize>> = HashMap::new();
for (idx, op) in ops.iter().enumerate() {
output_to_op.insert(op.output, idx);
for &input in &op.inputs {
consumers.entry(input).or_default().push(idx);
}
}
// Look for Exp operations as anchor points
for (exp_idx, exp_op) in ops.iter().enumerate() {
if !matches!(exp_op.op, StreamOpKind::Exp) {
continue;
}
let mut chain = vec![exp_idx];
let mut dim = None;
// Look backward for Sub(x, max) pattern (numerical stability)
if let Some(&input_id) = exp_op.inputs.first() {
if let Some(&sub_idx) = output_to_op.get(&input_id) {
if matches!(ops[sub_idx].op, StreamOpKind::Sub) {
// Check if second input could be a max reduction
if ops[sub_idx].inputs.len() >= 2 {
let second_input = ops[sub_idx].inputs[1];
if let Some(&max_idx) = output_to_op.get(&second_input) {
if matches!(ops[max_idx].op, StreamOpKind::Max) {
chain.insert(0, max_idx);
chain.insert(1, sub_idx);
}
}
}
// Even without max, sub before exp could be part of pattern
if chain.len() == 1 {
chain.insert(0, sub_idx);
}
}
}
}
// Look forward for Sum operation
let exp_output = exp_op.output;
let sum_consumer = consumers.get(&exp_output).and_then(|cons| {
cons.iter().find(|&&idx| {
matches!(ops[idx].op, StreamOpKind::Sum | StreamOpKind::SumDim { .. })
})
});
if let Some(&sum_idx) = sum_consumer {
chain.push(sum_idx);
// Extract dimension from SumDim if present
if let StreamOpKind::SumDim { dim: d } = &ops[sum_idx].op {
dim = Some(*d);
}
// Look for Div after Sum
let sum_output = ops[sum_idx].output;
let div_consumer = consumers.get(&sum_output).and_then(|cons| {
cons.iter()
.find(|&&idx| matches!(ops[idx].op, StreamOpKind::Div))
});
if let Some(&div_idx) = div_consumer {
chain.push(div_idx);
}
}
// Only create opportunity if we have at least exp + sum (core softmax)
if chain.len() >= 2
&& chain
.iter()
.any(|&i| matches!(ops[i].op, StreamOpKind::Sum | StreamOpKind::SumDim { .. }))
{
let chain_ops: Vec<_> = chain.iter().map(|&i| ops[i].op.clone()).collect();
let external_inputs = self.find_external_inputs(&chain, ops, &output_to_op);
let output = ops[*chain.last().unwrap()].output;
let dtype = exp_op.dtype;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::SoftmaxPattern { dim },
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup: FusionPattern::SoftmaxPattern { dim }.expected_speedup(),
dtype,
});
}
}
opportunities
}
/// Detect layer normalization patterns
/// Pattern: mean -> sub -> square -> mean -> (add eps) -> sqrt -> div -> (mul scale) -> (add bias)
fn detect_layer_norm_patterns(&self, stream: &OperationStream) -> Vec<FusionOpportunity> {
let ops = stream.operations();
let mut opportunities = Vec::new();
// Build maps
let mut output_to_op: HashMap<TensorId, usize> = HashMap::new();
let mut consumers: HashMap<TensorId, Vec<usize>> = HashMap::new();
for (idx, op) in ops.iter().enumerate() {
output_to_op.insert(op.output, idx);
for &input in &op.inputs {
consumers.entry(input).or_default().push(idx);
}
}
// Look for Mean operations as potential pattern starts
for (mean_idx, mean_op) in ops.iter().enumerate() {
if !matches!(
mean_op.op,
StreamOpKind::Mean | StreamOpKind::MeanDim { .. }
) {
continue;
}
let mut chain = vec![mean_idx];
let mut dim = None;
let eps = 1e-5f32; // Default eps
// Extract dimension
if let StreamOpKind::MeanDim { dim: d } = &mean_op.op {
dim = Some(*d);
}
// Look for Sub(x, mean) consumer
let mean_output = mean_op.output;
let sub_consumer = consumers.get(&mean_output).and_then(|cons| {
cons.iter().find(|&&idx| {
matches!(ops[idx].op, StreamOpKind::Sub)
&& ops[idx].inputs.get(1) == Some(&mean_output)
})
});
if let Some(&sub_idx) = sub_consumer {
chain.push(sub_idx);
// Look for Square(sub_result)
let sub_output = ops[sub_idx].output;
let square_consumer = consumers.get(&sub_output).and_then(|cons| {
cons.iter()
.find(|&&idx| matches!(ops[idx].op, StreamOpKind::Square))
});
if let Some(&square_idx) = square_consumer {
chain.push(square_idx);
// Look for second Mean (variance computation)
let square_output = ops[square_idx].output;
let mean2_consumer = consumers.get(&square_output).and_then(|cons| {
cons.iter().find(|&&idx| {
matches!(
ops[idx].op,
StreamOpKind::Mean | StreamOpKind::MeanDim { .. }
)
})
});
if let Some(&mean2_idx) = mean2_consumer {
chain.push(mean2_idx);
// Continue looking for add_eps -> sqrt -> div pattern
let var_output = ops[mean2_idx].output;
// Look for AddScalar (eps) or Sqrt directly
if let Some(add_eps_idx) = consumers.get(&var_output).and_then(|cons| {
cons.iter()
.find(|&&idx| matches!(ops[idx].op, StreamOpKind::AddScalar(_)))
}) {
chain.push(*add_eps_idx);
}
// Continue extending if we find sqrt, div, etc.
// This is a simplified version - full detection would trace the complete path
}
}
}
// Create opportunity if we found enough of the pattern
if chain.len() >= 3 {
let chain_ops: Vec<_> = chain.iter().map(|&i| ops[i].op.clone()).collect();
let external_inputs = self.find_external_inputs(&chain, ops, &output_to_op);
let output = ops[*chain.last().unwrap()].output;
let dtype = mean_op.dtype;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::LayerNormPattern { eps, dim },
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup: FusionPattern::LayerNormPattern { eps, dim }
.expected_speedup(),
dtype,
});
}
}
opportunities
}
/// Detect activation chains (e.g., mul -> sigmoid -> mul for SiLU)
fn detect_activation_chains(&self, stream: &OperationStream) -> Vec<FusionOpportunity> {
let ops = stream.operations();
let mut opportunities = Vec::new();
let mut visited: HashSet<usize> = HashSet::new();
// Build output to op map
let mut output_to_op: HashMap<TensorId, usize> = HashMap::new();
for (idx, op) in ops.iter().enumerate() {
output_to_op.insert(op.output, idx);
}
// Look for activation functions
for (idx, op) in ops.iter().enumerate() {
if visited.contains(&idx) {
continue;
}
let is_activation = matches!(
op.op,
StreamOpKind::GELU
| StreamOpKind::SiLU
| StreamOpKind::Sigmoid
| StreamOpKind::Tanh
| StreamOpKind::ReLU
);
if !is_activation || !op.op.is_fuseable() {
continue;
}
let mut chain = vec![idx];
visited.insert(idx);
// Look backward for preceding elementwise op
if let Some(&input_id) = op.inputs.first() {
if let Some(&prev_idx) = output_to_op.get(&input_id) {
if ops[prev_idx].op.is_fuseable() && !visited.contains(&prev_idx) {
chain.insert(0, prev_idx);
visited.insert(prev_idx);
}
}
}
// Look forward for following elementwise op
for (next_idx, next_op) in ops.iter().enumerate() {
if next_op.inputs.contains(&op.output)
&& next_op.op.is_fuseable()
&& !visited.contains(&next_idx)
{
chain.push(next_idx);
visited.insert(next_idx);
break;
}
}
if chain.len() >= self.config.min_fusion_ops {
let chain_ops: Vec<_> = chain.iter().map(|&i| ops[i].op.clone()).collect();
let external_inputs = self.find_external_inputs(&chain, ops, &output_to_op);
let output = ops[*chain.last().unwrap()].output;
let dtype = op.dtype;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::ActivationChain,
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup: FusionPattern::ActivationChain.expected_speedup(),
dtype,
});
}
}
opportunities
}
// ========================================================================
// Optimized detection methods using AnalysisContext
// ========================================================================
/// Optimized elementwise chain detection using pre-computed context
fn detect_elementwise_chains_opt(&self, ctx: &AnalysisContext<'_>) -> Vec<FusionOpportunity> {
if ctx.ops.is_empty() {
return Vec::new();
}
let mut opportunities = Vec::with_capacity(ctx.ops.len() / 4);
let mut visited: HashSet<usize> = HashSet::with_capacity(ctx.ops.len());
// Find chain start points (fuseable ops with no fuseable producer)
for (idx, op) in ctx.ops.iter().enumerate() {
if visited.contains(&idx) || !op.op.is_fuseable() {
continue;
}
// Check if this is a chain start
let is_start = op.inputs.iter().all(|input| {
ctx.producer_of(*input)
.map(|producer_idx| !ctx.get_op(producer_idx).op.is_fuseable())
.unwrap_or(true)
});
if !is_start {
continue;
}
// Extend the chain greedily
let mut chain = Vec::with_capacity(self.config.max_fusion_ops);
chain.push(idx);
let mut chain_outputs: HashSet<TensorId> =
HashSet::with_capacity(self.config.max_fusion_ops);
chain_outputs.insert(op.output);
visited.insert(idx);
let mut current_idx = idx;
loop {
// Find the next operation in the chain
let consumers = ctx.consumers_of_op(current_idx);
let next_candidate = consumers
.iter()
.filter(|&&next_idx| {
!visited.contains(&next_idx)
&& ctx.get_op(next_idx).op.is_fuseable()
&& ctx.get_op(next_idx).inputs.iter().all(|input| {
chain_outputs.contains(input) || !ctx.is_internal(*input)
})
})
.next();
match next_candidate {
Some(&next_idx) => {
let next_op = ctx.get_op(next_idx);
chain.push(next_idx);
chain_outputs.insert(next_op.output);
visited.insert(next_idx);
current_idx = next_idx;
if chain.len() >= self.config.max_fusion_ops {
break;
}
}
None => break,
}
}
// Create opportunity if chain is long enough
if chain.len() >= self.config.min_fusion_ops {
let chain_ops: Vec<_> = chain.iter().map(|&i| ctx.get_op(i).op.clone()).collect();
let external_inputs = self.find_external_inputs_opt(&chain, ctx);
let output = ctx.get_op(*chain.last().unwrap()).output;
let dtype = ctx.get_op(chain[0]).dtype;
let base_speedup = FusionPattern::ElementwiseChain.expected_speedup();
let chain_bonus = 1.0 + (chain.len() as f64 - 2.0) * 0.1;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::ElementwiseChain,
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup: base_speedup * chain_bonus,
dtype,
});
}
}
opportunities
}
/// Optimized softmax pattern detection
fn detect_softmax_patterns_opt(&self, ctx: &AnalysisContext<'_>) -> Vec<FusionOpportunity> {
let mut opportunities = Vec::new();
for (exp_idx, exp_op) in ctx.ops.iter().enumerate() {
if !matches!(exp_op.op, StreamOpKind::Exp) {
continue;
}
let mut chain = vec![exp_idx];
let mut dim = None;
// Look backward for Sub(x, max) pattern
if let Some(&input_id) = exp_op.inputs.first() {
if let Some(sub_idx) = ctx.producer_of(input_id) {
if matches!(ctx.get_op(sub_idx).op, StreamOpKind::Sub) {
if ctx.get_op(sub_idx).inputs.len() >= 2 {
let second_input = ctx.get_op(sub_idx).inputs[1];
if let Some(max_idx) = ctx.producer_of(second_input) {
if matches!(ctx.get_op(max_idx).op, StreamOpKind::Max) {
chain.insert(0, max_idx);
chain.insert(1, sub_idx);
}
}
}
if chain.len() == 1 {
chain.insert(0, sub_idx);
}
}
}
}
// Look forward for Sum operation
let exp_output = exp_op.output;
let sum_consumer = ctx.consumers_of_tensor(exp_output).iter().find(|&&idx| {
matches!(
ctx.get_op(idx).op,
StreamOpKind::Sum | StreamOpKind::SumDim { .. }
)
});
if let Some(&sum_idx) = sum_consumer {
chain.push(sum_idx);
if let StreamOpKind::SumDim { dim: d } = &ctx.get_op(sum_idx).op {
dim = Some(*d);
}
let sum_output = ctx.get_op(sum_idx).output;
let div_consumer = ctx
.consumers_of_tensor(sum_output)
.iter()
.find(|&&idx| matches!(ctx.get_op(idx).op, StreamOpKind::Div));
if let Some(&div_idx) = div_consumer {
chain.push(div_idx);
}
}
if chain.len() >= 2
&& chain.iter().any(|&i| {
matches!(
ctx.get_op(i).op,
StreamOpKind::Sum | StreamOpKind::SumDim { .. }
)
})
{
let chain_ops: Vec<_> = chain.iter().map(|&i| ctx.get_op(i).op.clone()).collect();
let external_inputs = self.find_external_inputs_opt(&chain, ctx);
let output = ctx.get_op(*chain.last().unwrap()).output;
let dtype = exp_op.dtype;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::SoftmaxPattern { dim },
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup: FusionPattern::SoftmaxPattern { dim }.expected_speedup(),
dtype,
});
}
}
opportunities
}
/// Optimized layer norm pattern detection
fn detect_layer_norm_patterns_opt(&self, ctx: &AnalysisContext<'_>) -> Vec<FusionOpportunity> {
let mut opportunities = Vec::new();
let eps = 1e-5f32;
for (mean_idx, mean_op) in ctx.ops.iter().enumerate() {
if !matches!(
mean_op.op,
StreamOpKind::Mean | StreamOpKind::MeanDim { .. }
) {
continue;
}
let mut chain = vec![mean_idx];
let mut dim = None;
if let StreamOpKind::MeanDim { dim: d } = &mean_op.op {
dim = Some(*d);
}
let mean_output = mean_op.output;
let sub_consumer = ctx.consumers_of_tensor(mean_output).iter().find(|&&idx| {
matches!(ctx.get_op(idx).op, StreamOpKind::Sub)
&& ctx.get_op(idx).inputs.get(1) == Some(&mean_output)
});
if let Some(&sub_idx) = sub_consumer {
chain.push(sub_idx);
let sub_output = ctx.get_op(sub_idx).output;
let square_consumer = ctx
.consumers_of_tensor(sub_output)
.iter()
.find(|&&idx| matches!(ctx.get_op(idx).op, StreamOpKind::Square));
if let Some(&square_idx) = square_consumer {
chain.push(square_idx);
let square_output = ctx.get_op(square_idx).output;
let mean2_consumer =
ctx.consumers_of_tensor(square_output).iter().find(|&&idx| {
matches!(
ctx.get_op(idx).op,
StreamOpKind::Mean | StreamOpKind::MeanDim { .. }
)
});
if let Some(&mean2_idx) = mean2_consumer {
chain.push(mean2_idx);
let var_output = ctx.get_op(mean2_idx).output;
if let Some(&add_eps_idx) = ctx
.consumers_of_tensor(var_output)
.iter()
.find(|&&idx| matches!(ctx.get_op(idx).op, StreamOpKind::AddScalar(_)))
{
chain.push(add_eps_idx);
}
}
}
}
if chain.len() >= 3 {
let chain_ops: Vec<_> = chain.iter().map(|&i| ctx.get_op(i).op.clone()).collect();
let external_inputs = self.find_external_inputs_opt(&chain, ctx);
let output = ctx.get_op(*chain.last().unwrap()).output;
let dtype = mean_op.dtype;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::LayerNormPattern { eps, dim },
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup: FusionPattern::LayerNormPattern { eps, dim }
.expected_speedup(),
dtype,
});
}
}
opportunities
}
/// Optimized activation chain detection
fn detect_activation_chains_opt(&self, ctx: &AnalysisContext<'_>) -> Vec<FusionOpportunity> {
let mut opportunities = Vec::new();
let mut visited: HashSet<usize> = HashSet::with_capacity(ctx.ops.len());
for (idx, op) in ctx.ops.iter().enumerate() {
if visited.contains(&idx) {
continue;
}
let is_activation = matches!(
op.op,
StreamOpKind::GELU
| StreamOpKind::SiLU
| StreamOpKind::Sigmoid
| StreamOpKind::Tanh
| StreamOpKind::ReLU
);
if !is_activation || !op.op.is_fuseable() {
continue;
}
let mut chain = vec![idx];
visited.insert(idx);
// Look backward
if let Some(&input_id) = op.inputs.first() {
if let Some(prev_idx) = ctx.producer_of(input_id) {
if ctx.get_op(prev_idx).op.is_fuseable() && !visited.contains(&prev_idx) {
chain.insert(0, prev_idx);
visited.insert(prev_idx);
}
}
}
// Look forward
for (next_idx, next_op) in ctx.ops.iter().enumerate() {
if next_op.inputs.contains(&op.output)
&& next_op.op.is_fuseable()
&& !visited.contains(&next_idx)
{
chain.push(next_idx);
visited.insert(next_idx);
break;
}
}
if chain.len() >= self.config.min_fusion_ops {
let chain_ops: Vec<_> = chain.iter().map(|&i| ctx.get_op(i).op.clone()).collect();
let external_inputs = self.find_external_inputs_opt(&chain, ctx);
let output = ctx.get_op(*chain.last().unwrap()).output;
let dtype = op.dtype;
opportunities.push(FusionOpportunity {
pattern: FusionPattern::ActivationChain,
operation_indices: chain,
operations: chain_ops,
external_inputs,
output,
estimated_speedup: FusionPattern::ActivationChain.expected_speedup(),
dtype,
});
}
}
opportunities
}
/// Optimized external input detection using context
fn find_external_inputs_opt(
&self,
chain: &[usize],
ctx: &AnalysisContext<'_>,
) -> Vec<TensorId> {
let chain_set: HashSet<_> = chain.iter().copied().collect();
let mut external = Vec::with_capacity(chain.len());
for &idx in chain {
for &input in &ctx.get_op(idx).inputs {
let is_external = ctx
.producer_of(input)
.map(|producer_idx| !chain_set.contains(&producer_idx))
.unwrap_or(true);
if is_external && !external.contains(&input) {
external.push(input);
}
}
}
external
}
/// Find external inputs for a chain of operations
fn find_external_inputs(
&self,
chain: &[usize],
ops: &[StreamOperation],
output_to_op: &HashMap<TensorId, usize>,
) -> Vec<TensorId> {
let chain_set: HashSet<_> = chain.iter().copied().collect();
let mut external = Vec::new();
for &idx in chain {
for &input in &ops[idx].inputs {
// External if not produced by an op in the chain
let is_external = output_to_op
.get(&input)
.map(|&producer_idx| !chain_set.contains(&producer_idx))
.unwrap_or(true);
if is_external && !external.contains(&input) {
external.push(input);
}
}
}
external
}
/// Remove overlapping opportunities (greedy: keep highest benefit)
fn remove_overlaps(&self, mut opportunities: Vec<FusionOpportunity>) -> Vec<FusionOpportunity> {
let mut result = Vec::new();
let mut used_ops: HashSet<usize> = HashSet::new();
for opp in opportunities {
// Check if any operation in this opportunity is already used
let has_overlap = opp
.operation_indices
.iter()
.any(|idx| used_ops.contains(idx));
if !has_overlap {
// Mark operations as used
for &idx in &opp.operation_indices {
used_ops.insert(idx);
}
result.push(opp);
}
}
result
}
/// Analyze a single operation for fusion potential
pub fn analyze_operation(&self, op: &StreamOpKind) -> OperationAnalysis {
OperationAnalysis {
is_fuseable: op.is_fuseable(),
is_sync_point: op.is_sync_point(),
fusion_benefit: op.fusion_benefit(),
preferred_patterns: if op.is_fuseable() {
vec![
FusionPattern::ElementwiseChain,
FusionPattern::ActivationChain,
]
} else {
vec![]
},
}
}
}
/// Analysis result for a single operation
#[derive(Debug)]
pub struct OperationAnalysis {
/// Whether the operation can be fused
pub is_fuseable: bool,
/// Whether this is a sync point (triggers flush)
pub is_sync_point: bool,
/// Benefit score for fusion (0.0-1.0)
pub fusion_benefit: f64,
/// Preferred fusion patterns for this operation
pub preferred_patterns: Vec<FusionPattern>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::TensorId;
fn make_op(op: StreamOpKind, inputs: Vec<TensorId>, output: TensorId) -> StreamOperation {
StreamOperation::new(op, inputs, output, vec![1024], DType::F32)
}
#[test]
fn test_pattern_speedup() {
assert!(FusionPattern::ElementwiseChain.expected_speedup() > 1.0);
assert!(
FusionPattern::SoftmaxPattern { dim: None }.expected_speedup()
> FusionPattern::ElementwiseChain.expected_speedup()
);
assert!(
FusionPattern::LayerNormPattern {
eps: 1e-5,
dim: None
}
.expected_speedup()
> FusionPattern::ElementwiseChain.expected_speedup()
);
}
#[test]
fn test_analyzer_creation() {
let analyzer = FusionAnalyzer::with_defaults();
assert!(analyzer.config.enabled);
}
#[test]
fn test_empty_stream() {
let analyzer = FusionAnalyzer::with_defaults();
let stream = OperationStream::with_defaults();
let opportunities = analyzer.analyze(&stream);
assert!(opportunities.is_empty());
}
#[test]
fn test_elementwise_chain_detection() {
let analyzer = FusionAnalyzer::new(FusionConfig::default().with_min_fusion_ops(2));
let mut stream = OperationStream::with_defaults();
let a = TensorId::new();
let b = TensorId::new();
let c = TensorId::new();
let d = TensorId::new();
let e = TensorId::new();
stream.register_input(a);
stream.register_input(b);
// Chain: add(a, b) -> mul(c, a) -> relu(d)
stream.record(make_op(StreamOpKind::Add, vec![a, b], c));
stream.record(make_op(StreamOpKind::Mul, vec![c, a], d));
stream.record(make_op(StreamOpKind::ReLU, vec![d], e));
let opportunities = analyzer.analyze(&stream);
assert!(!opportunities.is_empty());
let first = &opportunities[0];
assert_eq!(first.pattern, FusionPattern::ElementwiseChain);
assert!(first.operation_count() >= 2);
}
#[test]
fn test_sync_point_breaks_chain() {
let analyzer = FusionAnalyzer::new(FusionConfig::default().with_min_fusion_ops(2));
let mut stream = OperationStream::with_defaults();
let a = TensorId::new();
let b = TensorId::new();
let c = TensorId::new();
let d = TensorId::new();
stream.register_input(a);
// add -> matmul (sync point) -> mul
stream.record(make_op(StreamOpKind::Add, vec![a, a], b));
stream.record(make_op(StreamOpKind::MatMul, vec![b], c));
stream.record(make_op(StreamOpKind::Mul, vec![c, c], d));
let opportunities = analyzer.analyze(&stream);
// MatMul should break the chain
for opp in &opportunities {
assert!(
!opp.operations
.iter()
.any(|op| matches!(op, StreamOpKind::MatMul))
);
}
}
#[test]
fn test_opportunity_worthwhile() {
let opp = FusionOpportunity {
pattern: FusionPattern::ElementwiseChain,
operation_indices: vec![0, 1],
operations: vec![StreamOpKind::Add, StreamOpKind::Mul],
external_inputs: vec![TensorId::new()],
output: TensorId::new(),
estimated_speedup: 2.0,
dtype: DType::F32,
};
assert!(opp.is_worthwhile(2));
assert!(!opp.is_worthwhile(3));
}
#[test]
fn test_kernel_signature_generation() {
let opp = FusionOpportunity {
pattern: FusionPattern::ElementwiseChain,
operation_indices: vec![0, 1, 2],
operations: vec![StreamOpKind::Add, StreamOpKind::Mul, StreamOpKind::ReLU],
external_inputs: vec![TensorId::new(), TensorId::new()],
output: TensorId::new(),
estimated_speedup: 2.5,
dtype: DType::F32,
};
let sig = opp.to_kernel_signature();
assert_eq!(sig.ops.len(), 3);
assert_eq!(sig.num_inputs, 2);
assert_eq!(sig.dtype, DType::F32);
}
#[test]
fn test_operation_analysis() {
let analyzer = FusionAnalyzer::with_defaults();
let add_analysis = analyzer.analyze_operation(&StreamOpKind::Add);
assert!(add_analysis.is_fuseable);
assert!(!add_analysis.is_sync_point);
let matmul_analysis = analyzer.analyze_operation(&StreamOpKind::MatMul);
assert!(!matmul_analysis.is_fuseable);
assert!(matmul_analysis.is_sync_point);
}
}