#![allow(unused_variables)]
use crate::compiler::{ComputationGraph, DeviceType, GraphNode, HardwareTarget};
use crate::errors::invalid_input;
use crate::errors::TrustformersError;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceAnalysis {
pub total_execution_time_ms: f64,
pub critical_path: Vec<usize>,
pub critical_path_length_ms: f64,
pub parallelizable_operations: Vec<Vec<usize>>,
pub bottlenecks: Vec<BottleneckInfo>,
pub load_balance_score: f64,
pub hardware_utilization: HardwareUtilization,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BottleneckInfo {
pub node_id: usize,
pub operation_type: String,
pub execution_time_ms: f64,
pub memory_usage_mb: f64,
pub criticality_score: f64,
pub optimization_suggestions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HardwareUtilization {
pub compute_utilization: f64, pub memory_utilization: f64, pub memory_bandwidth_utilization: f64,
pub cache_hit_rate_prediction: Option<f64>,
pub parallel_efficiency: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryAnalysis {
pub peak_memory_usage: u64,
pub memory_timeline: Vec<MemorySnapshot>,
pub allocation_patterns: Vec<AllocationPattern>,
pub reuse_opportunities: Vec<ReuseOpportunity>,
pub fragmentation_analysis: Option<FragmentationAnalysis>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemorySnapshot {
pub operation_id: usize,
pub allocated_memory: u64,
pub active_tensors: Vec<TensorInfo>,
pub memory_pressure: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TensorInfo {
pub id: usize,
pub shape: Vec<usize>,
pub dtype: String,
pub size_bytes: u64,
pub lifetime_start: usize,
pub lifetime_end: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllocationPattern {
pub pattern_type: AllocationType,
pub frequency: usize,
pub total_size: u64,
pub optimization_potential: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AllocationType {
Sequential,
Scattered,
Temporary,
LongLived,
Reusable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReuseOpportunity {
pub tensor_id: usize,
pub reusable_with: Vec<usize>,
pub memory_savings: u64,
pub implementation_complexity: ComplexityLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComplexityLevel {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentationAnalysis {
pub fragmentation_ratio: f64,
pub largest_free_block: u64,
pub allocation_efficiency: f64,
pub defragmentation_potential: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyAnalysis {
pub topological_order: Vec<usize>,
pub connected_components: Vec<Vec<usize>>,
pub data_dependencies: Vec<Dependency>,
pub loop_analysis: Option<LoopAnalysis>,
pub parallelization: ParallelizationAnalysis,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub from: usize,
pub to: usize,
pub dependency_type: DependencyType,
pub data_size: u64,
pub latency_impact: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DependencyType {
DataFlow,
Control,
Memory,
Synchronization,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoopAnalysis {
pub detected_loops: Vec<LoopInfo>,
pub loop_carried_dependencies: Vec<Dependency>,
pub vectorization_opportunities: Vec<VectorizationOpportunity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoopInfo {
pub loop_id: usize,
pub operations: Vec<usize>,
pub iteration_count: Option<usize>,
pub loop_type: LoopType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LoopType {
CountBased,
DataDependent,
Infinite,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VectorizationOpportunity {
pub operations: Vec<usize>,
pub vector_width: usize,
pub performance_gain: f64,
pub instruction_set: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParallelizationAnalysis {
pub parallel_regions: Vec<ParallelRegion>,
pub synchronization_points: Vec<usize>,
pub load_balance_analysis: LoadBalanceAnalysis,
pub communication_analysis: CommunicationAnalysis,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParallelRegion {
pub operations: Vec<usize>,
pub parallelism_type: ParallelismType,
pub estimated_speedup: f64,
pub resource_requirements: ResourceRequirements,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ParallelismType {
DataParallel,
TaskParallel,
Pipeline,
Mixed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceRequirements {
pub min_threads: usize,
pub optimal_threads: usize,
pub memory_per_thread: u64,
pub communication_bandwidth: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadBalanceAnalysis {
pub balance_score: f64,
pub work_distribution: Vec<f64>,
pub synchronization_overhead: f64,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunicationAnalysis {
pub communication_volume: u64,
pub communication_patterns: Vec<CommunicationPattern>,
pub network_utilization: f64,
pub latency_sensitivity: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunicationPattern {
pub pattern_type: CommunicationType,
pub data_size: u64,
pub frequency: usize,
pub optimization_potential: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CommunicationType {
AllToAll,
AllReduce,
PointToPoint,
Broadcast,
Gather,
Scatter,
}
pub struct GraphAnalyzer {
hardware_target: HardwareTarget,
#[allow(dead_code)]
analysis_cache: HashMap<String, AnalysisResult>,
}
#[derive(Debug, Clone)]
pub enum AnalysisResult {
Performance(PerformanceAnalysis),
Memory(MemoryAnalysis),
Dependency(DependencyAnalysis),
}
impl GraphAnalyzer {
pub fn new(hardware_target: HardwareTarget) -> Self {
Self {
hardware_target,
analysis_cache: HashMap::new(),
}
}
pub fn analyze_performance(
&mut self,
graph: &ComputationGraph,
) -> Result<PerformanceAnalysis, TrustformersError> {
let critical_path = self.find_critical_path(graph)?;
let critical_path_length = self.calculate_path_length(&critical_path, graph)?;
let bottlenecks = self.detect_bottlenecks(graph)?;
let parallelizable_ops = self.find_parallelizable_operations(graph)?;
let load_balance_score = self.calculate_load_balance_score(graph)?;
let hardware_utilization = self.predict_hardware_utilization(graph)?;
let total_execution_time =
graph.nodes.iter().map(|node| self.estimate_execution_time(node)).sum();
Ok(PerformanceAnalysis {
total_execution_time_ms: total_execution_time,
critical_path,
critical_path_length_ms: critical_path_length,
parallelizable_operations: parallelizable_ops,
bottlenecks,
load_balance_score,
hardware_utilization,
})
}
pub fn analyze_memory(
&mut self,
graph: &ComputationGraph,
) -> Result<MemoryAnalysis, TrustformersError> {
let memory_timeline = self.simulate_memory_usage(graph)?;
let peak_memory = memory_timeline
.iter()
.map(|snapshot| snapshot.allocated_memory)
.max()
.unwrap_or(0);
let allocation_patterns = self.analyze_allocation_patterns(graph)?;
let reuse_opportunities = self.find_reuse_opportunities(graph)?;
let fragmentation_analysis = self.analyze_fragmentation(graph).ok();
Ok(MemoryAnalysis {
peak_memory_usage: peak_memory,
memory_timeline,
allocation_patterns,
reuse_opportunities,
fragmentation_analysis,
})
}
pub fn analyze_dependencies(
&mut self,
graph: &ComputationGraph,
) -> Result<DependencyAnalysis, TrustformersError> {
let topological_order = self.topological_sort(graph)?;
let connected_components = self.find_connected_components(graph)?;
let data_dependencies = self.analyze_data_dependencies(graph)?;
let loop_analysis = self.analyze_loops(graph).ok();
let parallelization = self.analyze_parallelization(graph)?;
Ok(DependencyAnalysis {
topological_order,
connected_components,
data_dependencies,
loop_analysis,
parallelization,
})
}
fn find_critical_path(
&self,
graph: &ComputationGraph,
) -> Result<Vec<usize>, TrustformersError> {
let mut longest_path = HashMap::new();
let mut predecessors = HashMap::new();
for node in &graph.nodes {
longest_path.insert(node.id, 0.0);
}
let topo_order = self.topological_sort(graph)?;
for &node_id in &topo_order {
let node_time = self.estimate_execution_time(&graph.nodes[node_id]);
for edge in &graph.edges {
if edge.from != node_id {
continue;
}
let new_distance = longest_path[&node_id] + node_time;
if new_distance > longest_path[&edge.to] {
longest_path.insert(edge.to, new_distance);
predecessors.insert(edge.to, node_id);
}
}
}
let end_node = longest_path
.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(node_id, _)| *node_id)
.unwrap_or(0);
let mut path = Vec::new();
let mut current = end_node;
while let Some(&predecessor) = predecessors.get(¤t) {
path.push(current);
current = predecessor;
}
path.push(current);
path.reverse();
Ok(path)
}
fn calculate_path_length(
&self,
path: &[usize],
graph: &ComputationGraph,
) -> Result<f64, TrustformersError> {
let total_time = path
.iter()
.map(|&node_id| {
if let Some(node) = graph.get_node(node_id) {
self.estimate_execution_time(node)
} else {
0.0
}
})
.sum();
Ok(total_time)
}
fn estimate_execution_time(&self, node: &GraphNode) -> f64 {
let base_time = match node.op_type.as_str() {
"MatMul" => {
let flops = node.compute_cost;
match self.hardware_target.device_type {
DeviceType::GPU => flops / 10e12, DeviceType::CPU => flops / 1e12, _ => flops / 1e9, }
},
"Conv2D" => node.compute_cost / 5e12, "Add" | "Mul" | "Sub" | "Div" => node.compute_cost / 1e13, "ReLU" | "Sigmoid" | "Tanh" => node.compute_cost / 1e12,
_ => node.compute_cost / 1e9, };
let memory_time = node.memory_cost / self.hardware_target.memory_bandwidth;
(base_time + memory_time) * 1000.0 }
fn detect_bottlenecks(
&self,
graph: &ComputationGraph,
) -> Result<Vec<BottleneckInfo>, TrustformersError> {
let mut bottlenecks = Vec::new();
let total_time: f64 =
graph.nodes.iter().map(|node| self.estimate_execution_time(node)).sum();
for node in &graph.nodes {
let execution_time = self.estimate_execution_time(node);
let time_percentage = execution_time / total_time;
if time_percentage > 0.1 {
let memory_usage = node.memory_cost / (1024.0 * 1024.0); let criticality_score = time_percentage * 100.0;
let suggestions = self.generate_optimization_suggestions(node);
bottlenecks.push(BottleneckInfo {
node_id: node.id,
operation_type: node.op_type.clone(),
execution_time_ms: execution_time,
memory_usage_mb: memory_usage,
criticality_score,
optimization_suggestions: suggestions,
});
}
}
bottlenecks.sort_by(|a, b| {
b.criticality_score
.partial_cmp(&a.criticality_score)
.unwrap_or(::std::cmp::Ordering::Equal)
});
Ok(bottlenecks)
}
fn generate_optimization_suggestions(&self, node: &GraphNode) -> Vec<String> {
let mut suggestions = Vec::new();
match node.op_type.as_str() {
"MatMul" => {
suggestions.push("Consider using optimized BLAS libraries".to_string());
suggestions.push("Try different matrix multiplication algorithms".to_string());
suggestions
.push("Consider batch processing for multiple small matrices".to_string());
},
"Conv2D" => {
suggestions.push("Use optimized convolution libraries (cuDNN, oneDNN)".to_string());
suggestions
.push("Consider different convolution algorithms (Winograd, FFT)".to_string());
suggestions.push("Try different data layouts (NCHW vs NHWC)".to_string());
},
"Attention" => {
suggestions.push(
"Use FlashAttention or similar memory-efficient implementations".to_string(),
);
suggestions.push("Consider attention sparsity patterns".to_string());
suggestions.push("Try different attention approximations".to_string());
},
_ => {
suggestions.push("Profile the operation to understand bottlenecks".to_string());
suggestions
.push("Consider operation fusion with neighboring operations".to_string());
},
}
suggestions
}
fn find_parallelizable_operations(
&self,
graph: &ComputationGraph,
) -> Result<Vec<Vec<usize>>, TrustformersError> {
let mut parallel_groups = Vec::new();
let mut visited = HashSet::new();
for (i, node1) in graph.nodes.iter().enumerate() {
if visited.contains(&i) {
continue;
}
let mut group = vec![i];
visited.insert(i);
for (j, node2) in graph.nodes.iter().enumerate() {
if i == j || visited.contains(&j) {
continue;
}
if self.has_dependency_path(i, j, graph) || self.has_dependency_path(j, i, graph) {
continue;
}
group.push(j);
visited.insert(j);
}
if group.len() > 1 {
parallel_groups.push(group);
}
}
Ok(parallel_groups)
}
fn has_dependency_path(&self, from: usize, to: usize, graph: &ComputationGraph) -> bool {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(from);
visited.insert(from);
while let Some(current) = queue.pop_front() {
if current == to {
return true;
}
for edge in &graph.edges {
if edge.from == current && !visited.contains(&edge.to) {
visited.insert(edge.to);
queue.push_back(edge.to);
}
}
}
false
}
fn calculate_load_balance_score(
&self,
graph: &ComputationGraph,
) -> Result<f64, TrustformersError> {
let execution_times: Vec<f64> =
graph.nodes.iter().map(|node| self.estimate_execution_time(node)).collect();
if execution_times.is_empty() {
return Ok(1.0);
}
let mean_time: f64 = execution_times.iter().sum::<f64>() / execution_times.len() as f64;
let variance: f64 =
execution_times.iter().map(|&time| (time - mean_time).powi(2)).sum::<f64>()
/ execution_times.len() as f64;
let coefficient_of_variation = variance.sqrt() / mean_time.max(1e-10);
Ok((1.0 / (1.0 + coefficient_of_variation)).min(1.0))
}
fn available_memory_bytes(&self) -> f64 {
match self.hardware_target.device_type {
DeviceType::GPU => 16e9, DeviceType::CPU => 64e9, _ => 8e9, }
}
fn predict_hardware_utilization(
&self,
graph: &ComputationGraph,
) -> Result<HardwareUtilization, TrustformersError> {
let total_compute = graph.total_compute_cost();
let total_memory = graph.total_memory_cost();
let compute_intensive_ops = graph
.nodes
.iter()
.filter(|node| matches!(node.op_type.as_str(), "MatMul" | "Conv2D" | "Attention"))
.count();
let compute_utilization =
(compute_intensive_ops as f64 / graph.nodes.len().max(1) as f64) * 0.8;
let estimated_memory = total_memory;
let memory_utilization = (estimated_memory / self.available_memory_bytes()).min(1.0);
let memory_bandwidth_utilization =
(total_memory / 1e9) / self.hardware_target.memory_bandwidth;
let cache_hit_rate_prediction = None;
let parallelizable_ops = self.find_parallelizable_operations(graph)?.len();
let parallel_efficiency =
(parallelizable_ops as f64 / graph.nodes.len().max(1) as f64) * 0.9;
Ok(HardwareUtilization {
compute_utilization,
memory_utilization,
memory_bandwidth_utilization,
cache_hit_rate_prediction,
parallel_efficiency,
})
}
fn simulate_memory_usage(
&self,
graph: &ComputationGraph,
) -> Result<Vec<MemorySnapshot>, TrustformersError> {
let mut snapshots = Vec::new();
let mut active_tensors = HashMap::new();
let mut total_memory = 0u64;
let topo_order = self.topological_sort(graph)?;
for &node_id in &topo_order {
if let Some(node) = graph.get_node(node_id) {
for (i, shape) in node.output_shapes.iter().enumerate() {
let tensor_size = self.calculate_tensor_size(shape, "f32");
let tensor_info = TensorInfo {
id: node_id * 100 + i, shape: shape.clone(),
dtype: "f32".to_string(),
size_bytes: tensor_size,
lifetime_start: node_id,
lifetime_end: node_id + 10, };
active_tensors.insert(tensor_info.id, tensor_info);
total_memory += tensor_size;
}
let memory_pressure = total_memory as f64 / self.available_memory_bytes();
let snapshot = MemorySnapshot {
operation_id: node_id,
allocated_memory: total_memory,
active_tensors: active_tensors.values().cloned().collect(),
memory_pressure,
};
snapshots.push(snapshot);
active_tensors.retain(|_, tensor| tensor.lifetime_end > node_id);
total_memory = active_tensors.values().map(|t| t.size_bytes).sum();
}
}
Ok(snapshots)
}
fn calculate_tensor_size(&self, shape: &[usize], dtype: &str) -> u64 {
let element_size = match dtype {
"f32" | "i32" => 4,
"f16" | "i16" => 2,
"f64" | "i64" => 8,
"i8" | "u8" => 1,
_ => 4, };
let elements: usize = shape.iter().product();
(elements * element_size) as u64
}
fn topological_sort(&self, graph: &ComputationGraph) -> Result<Vec<usize>, TrustformersError> {
let mut in_degree = vec![0; graph.nodes.len()];
let mut adj_list = vec![Vec::new(); graph.nodes.len()];
for edge in &graph.edges {
if edge.from < graph.nodes.len() && edge.to < graph.nodes.len() {
adj_list[edge.from].push(edge.to);
in_degree[edge.to] += 1;
}
}
let mut queue = VecDeque::new();
let mut result = Vec::new();
for (i, °ree) in in_degree.iter().enumerate() {
if degree == 0 {
queue.push_back(i);
}
}
while let Some(node) = queue.pop_front() {
result.push(node);
for &neighbor in &adj_list[node] {
in_degree[neighbor] -= 1;
if in_degree[neighbor] == 0 {
queue.push_back(neighbor);
}
}
}
if result.len() != graph.nodes.len() {
return Err(invalid_input("Graph contains cycles"));
}
Ok(result)
}
fn find_connected_components(
&self,
graph: &ComputationGraph,
) -> Result<Vec<Vec<usize>>, TrustformersError> {
let node_count = graph.nodes.len();
if node_count == 0 {
return Ok(Vec::new());
}
let index_of: HashMap<usize, usize> =
graph.nodes.iter().enumerate().map(|(index, node)| (node.id, index)).collect();
let mut parent: Vec<usize> = (0..node_count).collect();
fn find(parent: &mut [usize], mut node: usize) -> usize {
while parent[node] != node {
parent[node] = parent[parent[node]];
node = parent[node];
}
node
}
for edge in &graph.edges {
let (Some(&from), Some(&to)) = (index_of.get(&edge.from), index_of.get(&edge.to))
else {
continue;
};
let root_from = find(&mut parent, from);
let root_to = find(&mut parent, to);
if root_from != root_to {
parent[root_to] = root_from;
}
}
let mut components: HashMap<usize, Vec<usize>> = HashMap::new();
for index in 0..node_count {
let root = find(&mut parent, index);
components.entry(root).or_default().push(graph.nodes[index].id);
}
let mut result: Vec<Vec<usize>> = components.into_values().collect();
for component in &mut result {
component.sort_unstable();
}
result.sort_by(|a, b| a.first().cmp(&b.first()));
Ok(result)
}
fn analyze_data_dependencies(
&self,
graph: &ComputationGraph,
) -> Result<Vec<Dependency>, TrustformersError> {
let node_by_id: HashMap<usize, &GraphNode> =
graph.nodes.iter().map(|node| (node.id, node)).collect();
let mut dependencies = Vec::with_capacity(graph.edges.len());
for edge in &graph.edges {
let Some(producer) = node_by_id.get(&edge.from) else {
continue;
};
let data_size = producer
.output_shapes
.get(edge.output_idx)
.map(|shape| shape.iter().product::<usize>() as u64 * 4)
.unwrap_or(0);
dependencies.push(Dependency {
from: edge.from,
to: edge.to,
dependency_type: DependencyType::DataFlow,
data_size,
latency_impact: producer.compute_cost,
});
}
Ok(dependencies)
}
fn analyze_loops(&self, _graph: &ComputationGraph) -> Result<LoopAnalysis, TrustformersError> {
Err(TrustformersError::not_implemented(
"loop analysis: the compiler IR has no loop constructs to analyse".to_string(),
))
}
fn analyze_parallelization(
&self,
graph: &ComputationGraph,
) -> Result<ParallelizationAnalysis, TrustformersError> {
let components = self.find_connected_components(graph)?;
let cost_of: HashMap<usize, f64> =
graph.nodes.iter().map(|node| (node.id, node.compute_cost)).collect();
let mut parallel_regions = Vec::with_capacity(components.len());
let mut work_distribution = Vec::with_capacity(components.len());
for component in &components {
let work: f64 = component.iter().filter_map(|id| cost_of.get(id)).copied().sum();
work_distribution.push(work);
parallel_regions.push(ParallelRegion {
operations: component.clone(),
parallelism_type: ParallelismType::TaskParallel,
estimated_speedup: 1.0,
resource_requirements: ResourceRequirements {
min_threads: 1,
optimal_threads: 1,
memory_per_thread: component
.iter()
.filter_map(|id| graph.nodes.iter().find(|node| node.id == *id))
.map(|node| node.memory_cost as u64)
.sum(),
communication_bandwidth: 0.0,
},
});
}
let total_work: f64 = work_distribution.iter().sum();
let balance_score = if work_distribution.len() < 2 || total_work <= 0.0 {
1.0
} else {
let mean = total_work / work_distribution.len() as f64;
let max_deviation =
work_distribution.iter().map(|work| (work - mean).abs()).fold(0.0f64, f64::max);
(1.0 - max_deviation / total_work).clamp(0.0, 1.0)
};
let mut recommendations = Vec::new();
if work_distribution.len() > 1 && balance_score < 0.8 {
recommendations.push(format!(
"work is unevenly distributed across {} independent regions (balance {:.2})",
work_distribution.len(),
balance_score
));
}
recommendations.push(
"communication volume and network utilization are not modelled by this analyser"
.to_string(),
);
Ok(ParallelizationAnalysis {
parallel_regions,
synchronization_points: Vec::new(),
load_balance_analysis: LoadBalanceAnalysis {
balance_score,
work_distribution,
synchronization_overhead: 0.0,
recommendations,
},
communication_analysis: CommunicationAnalysis {
communication_volume: graph
.edges
.iter()
.filter_map(|edge| {
let producer = graph.nodes.iter().find(|node| node.id == edge.from)?;
producer
.output_shapes
.get(edge.output_idx)
.map(|shape| shape.iter().product::<usize>() as u64 * 4)
})
.sum(),
communication_patterns: Vec::new(),
network_utilization: 0.0,
latency_sensitivity: 0.0,
},
})
}
fn analyze_allocation_patterns(
&self,
graph: &ComputationGraph,
) -> Result<Vec<AllocationPattern>, TrustformersError> {
let mut consumers: HashMap<(usize, usize), usize> = HashMap::new();
for edge in &graph.edges {
*consumers.entry((edge.from, edge.output_idx)).or_insert(0) += 1;
}
let mut temporary_total = 0u64;
let mut temporary_count = 0usize;
let mut long_lived_total = 0u64;
let mut long_lived_count = 0usize;
for node in &graph.nodes {
for (output_idx, shape) in node.output_shapes.iter().enumerate() {
let size = shape.iter().product::<usize>() as u64 * 4;
match consumers.get(&(node.id, output_idx)).copied().unwrap_or(0) {
1 => {
temporary_total += size;
temporary_count += 1;
},
_ => {
long_lived_total += size;
long_lived_count += 1;
},
}
}
}
let mut patterns = Vec::new();
if temporary_count > 0 {
patterns.push(AllocationPattern {
pattern_type: AllocationType::Temporary,
frequency: temporary_count,
total_size: temporary_total,
optimization_potential: 1.0,
});
}
if long_lived_count > 0 {
patterns.push(AllocationPattern {
pattern_type: AllocationType::LongLived,
frequency: long_lived_count,
total_size: long_lived_total,
optimization_potential: 0.0,
});
}
Ok(patterns)
}
fn find_reuse_opportunities(
&self,
graph: &ComputationGraph,
) -> Result<Vec<ReuseOpportunity>, TrustformersError> {
let node_by_id: HashMap<usize, &GraphNode> =
graph.nodes.iter().map(|node| (node.id, node)).collect();
let mut consumers: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
for edge in &graph.edges {
consumers.entry((edge.from, edge.output_idx)).or_default().push(edge.to);
}
let mut opportunities = Vec::new();
for ((producer_id, output_idx), consumer_ids) in &consumers {
if consumer_ids.len() != 1 {
continue;
}
let Some(producer) = node_by_id.get(producer_id) else {
continue;
};
let Some(shape) = producer.output_shapes.get(*output_idx) else {
continue;
};
let Some(consumer) = node_by_id.get(&consumer_ids[0]) else {
continue;
};
if !consumer.output_shapes.iter().any(|candidate| candidate == shape) {
continue;
}
opportunities.push(ReuseOpportunity {
tensor_id: *producer_id,
reusable_with: consumer_ids.clone(),
memory_savings: shape.iter().product::<usize>() as u64 * 4,
implementation_complexity: ComplexityLevel::Low,
});
}
opportunities.sort_by_key(|opportunity| opportunity.tensor_id);
Ok(opportunities)
}
fn analyze_fragmentation(
&self,
_graph: &ComputationGraph,
) -> Result<FragmentationAnalysis, TrustformersError> {
Err(TrustformersError::not_implemented(
"memory fragmentation analysis: fragmentation is a runtime allocator property and \
cannot be derived from a static graph"
.to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_component_graph() -> ComputationGraph {
let node = |id: usize, cost: f64, out: Vec<usize>| GraphNode {
id,
op_type: "matmul".to_string(),
attributes: HashMap::new(),
input_shapes: vec![],
output_shapes: vec![out],
compute_cost: cost,
memory_cost: 16.0,
};
ComputationGraph {
nodes: vec![
node(0, 10.0, vec![2, 2]),
node(1, 5.0, vec![2, 2]),
node(2, 1.0, vec![4]),
],
edges: vec![crate::compiler::GraphEdge {
from: 0,
to: 1,
output_idx: 0,
input_idx: 0,
shape: vec![2, 2],
dtype: "f32".to_string(),
}],
metadata: HashMap::new(),
}
}
#[test]
fn test_dependency_analysis_reflects_the_real_graph() {
let mut analyzer = GraphAnalyzer::new(HardwareTarget::default());
let graph = two_component_graph();
let analysis = analyzer.analyze_dependencies(&graph).expect("analysis failed");
assert_eq!(analysis.connected_components.len(), 2);
assert!(analysis.connected_components.contains(&vec![0, 1]));
assert!(analysis.connected_components.contains(&vec![2]));
assert_eq!(analysis.data_dependencies.len(), 1);
let dependency = &analysis.data_dependencies[0];
assert_eq!(dependency.from, 0);
assert_eq!(dependency.to, 1);
assert_eq!(
dependency.data_size,
2 * 2 * 4,
"a 2x2 f32 tensor is 16 bytes"
);
assert!((dependency.latency_impact - 10.0).abs() < 1e-9);
assert!(
analysis.loop_analysis.is_none(),
"an all-empty LoopAnalysis would look like a completed analysis"
);
}
#[test]
fn test_parallelization_metrics_are_measured_from_the_graph() {
let mut analyzer = GraphAnalyzer::new(HardwareTarget::default());
let graph = two_component_graph();
let analysis = analyzer.analyze_dependencies(&graph).expect("analysis failed");
let parallelization = &analysis.parallelization;
assert_eq!(parallelization.parallel_regions.len(), 2);
let mut work = parallelization.load_balance_analysis.work_distribution.clone();
work.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
assert!((work[0] - 1.0).abs() < 1e-9, "got {work:?}");
assert!((work[1] - 15.0).abs() < 1e-9, "got {work:?}");
let balance = parallelization.load_balance_analysis.balance_score;
assert_ne!(balance, 0.8, "the hardcoded balance score must be gone");
assert!(
balance < 0.6,
"a 15:1 split is badly balanced, got {balance}"
);
assert_eq!(
parallelization.load_balance_analysis.synchronization_overhead,
0.0
);
assert_eq!(
parallelization.communication_analysis.network_utilization,
0.0
);
assert_eq!(
parallelization.communication_analysis.latency_sensitivity,
0.0
);
assert_eq!(
parallelization.communication_analysis.communication_volume,
16
);
}
#[test]
fn test_memory_analysis_reflects_the_real_graph() {
let mut analyzer = GraphAnalyzer::new(HardwareTarget::default());
let graph = two_component_graph();
let analysis = analyzer.analyze_memory(&graph).expect("analysis failed");
assert!(!analysis.allocation_patterns.is_empty());
let temporary = analysis
.allocation_patterns
.iter()
.find(|pattern| matches!(pattern.pattern_type, AllocationType::Temporary))
.expect("the single-use tensor must be classified temporary");
assert_eq!(temporary.frequency, 1);
assert_eq!(temporary.total_size, 16);
assert_eq!(analysis.reuse_opportunities.len(), 1);
assert_eq!(analysis.reuse_opportunities[0].tensor_id, 0);
assert_eq!(analysis.reuse_opportunities[0].memory_savings, 16);
assert!(
analysis.fragmentation_analysis.is_none(),
"fragmentation cannot be derived from a static graph and must not be invented"
);
}
#[test]
fn test_empty_graph_analyses_are_empty() {
let mut analyzer = GraphAnalyzer::new(HardwareTarget::default());
let graph = ComputationGraph {
nodes: Vec::new(),
edges: Vec::new(),
metadata: HashMap::new(),
};
let dependencies = analyzer.analyze_dependencies(&graph).expect("analysis failed");
assert!(dependencies.connected_components.is_empty());
assert!(dependencies.data_dependencies.is_empty());
assert!(dependencies.parallelization.parallel_regions.is_empty());
}
use crate::compiler::{ComputationGraph, GraphNode, HardwareTarget};
fn create_test_graph() -> ComputationGraph {
let mut graph = ComputationGraph::new();
let node1 = GraphNode {
id: 0,
op_type: "MatMul".to_string(),
attributes: HashMap::new(),
input_shapes: vec![vec![128, 256], vec![256, 512]],
output_shapes: vec![vec![128, 512]],
compute_cost: 100.0,
memory_cost: 50.0,
};
graph.add_node(node1);
graph
}
#[test]
fn test_graph_analyzer_creation() {
let hardware = HardwareTarget::default();
let analyzer = GraphAnalyzer::new(hardware);
assert_eq!(analyzer.analysis_cache.len(), 0);
}
#[test]
fn test_performance_analysis() {
let hardware = HardwareTarget::default();
let mut analyzer = GraphAnalyzer::new(hardware);
let graph = create_test_graph();
let result = analyzer.analyze_performance(&graph);
assert!(result.is_ok());
let analysis = result.expect("operation failed in test");
assert!(analysis.total_execution_time_ms >= 0.0);
}
#[test]
fn test_memory_analysis() {
let hardware = HardwareTarget::default();
let mut analyzer = GraphAnalyzer::new(hardware);
let graph = create_test_graph();
let result = analyzer.analyze_memory(&graph);
assert!(result.is_ok());
}
#[test]
fn test_dependency_analysis() {
let hardware = HardwareTarget::default();
let mut analyzer = GraphAnalyzer::new(hardware);
let graph = create_test_graph();
let result = analyzer.analyze_dependencies(&graph);
assert!(result.is_ok());
}
#[test]
fn test_critical_path_analysis() {
let hardware = HardwareTarget::default();
let analyzer = GraphAnalyzer::new(hardware);
let graph = create_test_graph();
let result = analyzer.find_critical_path(&graph);
assert!(result.is_ok());
assert!(!result.expect("operation failed in test").is_empty());
}
#[test]
fn test_topological_sort() {
let hardware = HardwareTarget::default();
let analyzer = GraphAnalyzer::new(hardware);
let graph = create_test_graph();
let result = analyzer.topological_sort(&graph);
assert!(result.is_ok());
assert_eq!(
result.expect("operation failed in test").len(),
graph.nodes.len()
);
}
#[test]
fn test_available_memory_bytes_varies_by_device_type() {
let gpu = GraphAnalyzer::new(HardwareTarget {
device_type: DeviceType::GPU,
..HardwareTarget::default()
});
let cpu = GraphAnalyzer::new(HardwareTarget {
device_type: DeviceType::CPU,
..HardwareTarget::default()
});
let other = GraphAnalyzer::new(HardwareTarget {
device_type: DeviceType::TPU,
..HardwareTarget::default()
});
assert_ne!(gpu.available_memory_bytes(), cpu.available_memory_bytes());
assert_ne!(cpu.available_memory_bytes(), other.available_memory_bytes());
assert_eq!(gpu.available_memory_bytes(), 16e9);
assert_eq!(cpu.available_memory_bytes(), 64e9);
assert_eq!(other.available_memory_bytes(), 8e9);
}
#[test]
fn test_cache_hit_rate_prediction_is_honestly_absent() {
let hardware = HardwareTarget::default();
let analyzer = GraphAnalyzer::new(hardware);
let graph = create_test_graph();
let utilization = analyzer.predict_hardware_utilization(&graph).expect("prediction failed");
assert!(
utilization.cache_hit_rate_prediction.is_none(),
"no cache simulation model exists to back this field; a Some(_) would be fabricated"
);
let json = serde_json::to_string(&utilization).expect("serialize failed");
let round_tripped: HardwareUtilization =
serde_json::from_str(&json).expect("deserialize failed");
assert!(round_tripped.cache_hit_rate_prediction.is_none());
}
#[test]
fn test_simulate_memory_usage_reflects_target_device_capacity() {
let graph = create_test_graph();
let gpu_analyzer = GraphAnalyzer::new(HardwareTarget {
device_type: DeviceType::GPU,
..HardwareTarget::default()
});
let cpu_analyzer = GraphAnalyzer::new(HardwareTarget {
device_type: DeviceType::CPU,
..HardwareTarget::default()
});
let gpu_snapshots = gpu_analyzer.simulate_memory_usage(&graph).expect("simulation failed");
let cpu_snapshots = cpu_analyzer.simulate_memory_usage(&graph).expect("simulation failed");
assert_eq!(gpu_snapshots.len(), cpu_snapshots.len());
assert!(!gpu_snapshots.is_empty());
for (gpu_snap, cpu_snap) in gpu_snapshots.iter().zip(cpu_snapshots.iter()) {
assert_eq!(
gpu_snap.allocated_memory, cpu_snap.allocated_memory,
"same graph must produce the same real allocation regardless of target device"
);
assert!(
gpu_snap.memory_pressure > cpu_snap.memory_pressure,
"the same allocation must show higher pressure against the GPU's smaller \
assumed capacity (16GB) than the CPU's larger one (64GB): gpu={}, cpu={}",
gpu_snap.memory_pressure,
cpu_snap.memory_pressure
);
}
}
}