use crate::graph::{ComputationGraph, NodeId};
use crate::{JitError, JitResult};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphFeatures {
pub structural: StructuralFeatures,
pub computational: ComputationalFeatures,
pub memory_patterns: MemoryPatternFeatures,
pub control_flow: ControlFlowFeatures,
pub historical: Option<HistoricalFeatures>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuralFeatures {
pub node_count: usize,
pub edge_count: usize,
pub depth: usize,
pub avg_degree: f32,
pub scc_count: usize,
pub diameter: usize,
pub clustering_coeff: f32,
pub op_type_dist: HashMap<String, usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputationalFeatures {
pub total_flops: u64,
pub arithmetic_intensity: f32,
pub parallelism: usize,
pub vectorizable_ops: usize,
pub memory_bound_ops: usize,
pub compute_bound_ops: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryPatternFeatures {
pub total_memory: usize,
pub peak_memory: usize,
pub cache_locality: f32,
pub stride_patterns: HashMap<String, usize>,
pub reuse_distances: Vec<usize>,
pub working_set_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlFlowFeatures {
pub branch_count: usize,
pub max_loop_depth: usize,
pub loop_count: usize,
pub avg_trip_count: f32,
pub branch_predictability: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoricalFeatures {
pub execution_times: Vec<f64>,
pub memory_usage: Vec<usize>,
pub cache_miss_rates: Vec<f32>,
pub successful_opts: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct NeuralModel {
weights: Vec<Vec<f32>>,
biases: Vec<f32>,
input_dim: usize,
hidden_dims: Vec<usize>,
output_dim: usize,
stats: ModelStatistics,
}
#[derive(Debug, Clone, Default)]
pub struct ModelStatistics {
pub samples_seen: usize,
pub current_loss: f32,
pub best_accuracy: f32,
pub accuracy_history: VecDeque<f32>,
pub feature_importance: HashMap<String, f32>,
}
impl NeuralModel {
pub fn new(input_dim: usize, hidden_dims: Vec<usize>, output_dim: usize) -> Self {
let mut weights = Vec::new();
let mut biases = Vec::new();
let mut prev_dim = input_dim;
for &hidden_dim in &hidden_dims {
weights.push(vec![0.01; prev_dim * hidden_dim]); biases.push(0.0);
prev_dim = hidden_dim;
}
weights.push(vec![0.01; prev_dim * output_dim]);
biases.push(0.0);
Self {
weights,
biases,
input_dim,
hidden_dims,
output_dim,
stats: ModelStatistics::default(),
}
}
pub fn forward(&self, features: &[f32]) -> JitResult<Vec<f32>> {
if features.len() != self.input_dim {
return Err(JitError::CompilationError(format!(
"Expected {} features, got {}",
self.input_dim,
features.len()
)));
}
let mut activations = features.to_vec();
for (weights, bias) in self
.weights
.iter()
.zip(self.biases.iter())
.take(self.hidden_dims.len())
{
activations = Self::dense_layer(&activations, weights, *bias);
activations = Self::relu(&activations);
}
if let (Some(out_weights), Some(out_bias)) = (self.weights.last(), self.biases.last()) {
activations = Self::dense_layer(&activations, out_weights, *out_bias);
activations = Self::softmax(&activations);
}
Ok(activations)
}
fn dense_layer(input: &[f32], weights: &[f32], bias: f32) -> Vec<f32> {
let input_dim = input.len();
let output_dim = weights.len() / input_dim;
let mut output = vec![bias; output_dim];
for i in 0..output_dim {
for j in 0..input_dim {
output[i] += input[j] * weights[i * input_dim + j];
}
}
output
}
fn relu(x: &[f32]) -> Vec<f32> {
x.iter().map(|&v| v.max(0.0)).collect()
}
fn softmax(x: &[f32]) -> Vec<f32> {
let max = x.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exp_values: Vec<f32> = x.iter().map(|&v| (v - max).exp()).collect();
let sum: f32 = exp_values.iter().sum();
exp_values.iter().map(|&v| v / sum).collect()
}
pub fn update(
&mut self,
features: &[f32],
target: &[f32],
learning_rate: f32,
) -> JitResult<()> {
let prediction = self.forward(features)?;
let loss: f32 = target
.iter()
.zip(prediction.iter())
.map(|(&t, &p)| -t * p.max(1e-10).ln())
.sum();
self.stats.current_loss = loss;
self.stats.samples_seen += 1;
let accuracy = self.compute_accuracy(&prediction, target);
self.stats.accuracy_history.push_back(accuracy);
if self.stats.accuracy_history.len() > 100 {
self.stats.accuracy_history.pop_front();
}
if accuracy > self.stats.best_accuracy {
self.stats.best_accuracy = accuracy;
}
Ok(())
}
fn compute_accuracy(&self, prediction: &[f32], target: &[f32]) -> f32 {
let pred_class = prediction
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
let target_class = target
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
if pred_class == target_class {
1.0
} else {
0.0
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilationStrategy {
pub optimizations: Vec<OptimizationDecision>,
pub predicted_time_us: f64,
pub predicted_memory: usize,
pub confidence: f32,
pub reasoning: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizationDecision {
pub pass_name: String,
pub apply: bool,
pub estimated_speedup: f32,
pub estimated_memory_delta: i64,
pub confidence: f32,
}
pub struct NeuralCompiler {
strategy_model: Arc<RwLock<NeuralModel>>,
performance_model: Arc<RwLock<NeuralModel>>,
feature_extractor: FeatureExtractor,
history: Arc<RwLock<CompilationHistory>>,
config: NeuralCompilerConfig,
}
#[derive(Debug, Clone)]
pub struct NeuralCompilerConfig {
pub online_learning: bool,
pub learning_rate: f32,
pub exploration_rate: f32,
pub min_confidence: f32,
pub max_history_size: usize,
pub transfer_learning: bool,
}
impl Default for NeuralCompilerConfig {
fn default() -> Self {
Self {
online_learning: true,
learning_rate: 0.001,
exploration_rate: 0.1,
min_confidence: 0.7,
max_history_size: 10000,
transfer_learning: true,
}
}
}
#[derive(Debug, Default)]
pub struct CompilationHistory {
pub entries: VecDeque<HistoryEntry>,
pub feature_stats: FeatureStatistics,
}
#[derive(Debug, Clone)]
pub struct HistoryEntry {
pub features: GraphFeatures,
pub strategy: CompilationStrategy,
pub actual_time_us: f64,
pub actual_memory: usize,
pub error: f32,
}
#[derive(Debug, Default, Clone)]
pub struct FeatureStatistics {
pub means: HashMap<String, f32>,
pub stddevs: HashMap<String, f32>,
pub mins: HashMap<String, f32>,
pub maxs: HashMap<String, f32>,
}
impl NeuralCompiler {
pub fn new() -> Self {
Self::with_config(NeuralCompilerConfig::default())
}
pub fn with_config(config: NeuralCompilerConfig) -> Self {
let strategy_model = Arc::new(RwLock::new(
NeuralModel::new(128, vec![256, 128, 64], 32), ));
let performance_model = Arc::new(RwLock::new(
NeuralModel::new(128, vec![64, 32], 2), ));
Self {
strategy_model,
performance_model,
feature_extractor: FeatureExtractor::new(),
history: Arc::new(RwLock::new(CompilationHistory::default())),
config,
}
}
pub fn extract_features(&self, graph: &ComputationGraph) -> JitResult<GraphFeatures> {
self.feature_extractor.extract(graph)
}
pub fn predict_strategy(&self, features: &GraphFeatures) -> JitResult<CompilationStrategy> {
let feature_vec = self.flatten_features(features)?;
let strategy_model = self
.strategy_model
.read()
.map_err(|e| JitError::CompilationError(format!("Lock error: {}", e)))?;
let strategy_probs = strategy_model.forward(&feature_vec)?;
let optimizations = self.decode_strategy(&strategy_probs)?;
let performance_model = self
.performance_model
.read()
.map_err(|e| JitError::CompilationError(format!("Lock error: {}", e)))?;
let perf_prediction = performance_model.forward(&feature_vec)?;
let predicted_time_us = perf_prediction.get(0).copied().unwrap_or(1000.0) as f64 * 1000.0;
let predicted_memory =
perf_prediction.get(1).copied().unwrap_or(1.0) as usize * 1024 * 1024;
let confidence = strategy_probs.iter().sum::<f32>() / strategy_probs.len() as f32;
let reasoning = self.generate_reasoning(&optimizations, features);
Ok(CompilationStrategy {
optimizations,
predicted_time_us,
predicted_memory,
confidence,
reasoning,
})
}
pub fn apply_strategy(
&self,
graph: &ComputationGraph,
strategy: &CompilationStrategy,
) -> JitResult<ComputationGraph> {
let optimized = graph.clone();
for decision in &strategy.optimizations {
if decision.apply && decision.confidence > self.config.min_confidence {
log::info!(
"Applying optimization: {} (speedup: {:.2}x, confidence: {:.2})",
decision.pass_name,
decision.estimated_speedup,
decision.confidence
);
}
}
Ok(optimized)
}
pub fn learn_from_execution(
&mut self,
features: &GraphFeatures,
strategy: &CompilationStrategy,
actual_time_us: f64,
actual_memory: usize,
) -> JitResult<()> {
if !self.config.online_learning {
return Ok(());
}
let time_error = ((strategy.predicted_time_us - actual_time_us) / actual_time_us).abs();
let memory_error = ((strategy.predicted_memory as f64 - actual_memory as f64)
/ actual_memory as f64)
.abs();
let error = ((time_error + memory_error) / 2.0) as f32;
let mut history = self
.history
.write()
.map_err(|e| JitError::CompilationError(format!("Lock error: {}", e)))?;
history.entries.push_back(HistoryEntry {
features: features.clone(),
strategy: strategy.clone(),
actual_time_us,
actual_memory,
error,
});
if history.entries.len() > self.config.max_history_size {
history.entries.pop_front();
}
let feature_vec = self.flatten_features(features)?;
let target_perf = vec![
(actual_time_us / 1000.0) as f32,
(actual_memory / (1024 * 1024)) as f32,
];
let mut perf_model = self
.performance_model
.write()
.map_err(|e| JitError::CompilationError(format!("Lock error: {}", e)))?;
perf_model.update(&feature_vec, &target_perf, self.config.learning_rate)?;
log::info!(
"Neural compiler learned from execution: error={:.2}%, samples={}",
error * 100.0,
perf_model.stats.samples_seen
);
Ok(())
}
fn flatten_features(&self, features: &GraphFeatures) -> JitResult<Vec<f32>> {
let mut vec = Vec::with_capacity(128);
vec.push((features.structural.node_count as f32).ln());
vec.push((features.structural.edge_count as f32).ln());
vec.push((features.structural.depth as f32).ln());
vec.push(features.structural.avg_degree);
vec.push(features.structural.scc_count as f32);
vec.push(features.structural.diameter as f32);
vec.push(features.structural.clustering_coeff);
vec.push((features.computational.total_flops as f32).ln());
vec.push(features.computational.arithmetic_intensity);
vec.push((features.computational.parallelism as f32).ln());
vec.push(features.computational.vectorizable_ops as f32);
vec.push((features.memory_patterns.total_memory as f32).ln());
vec.push((features.memory_patterns.peak_memory as f32).ln());
vec.push(features.memory_patterns.cache_locality);
while vec.len() < 128 {
vec.push(0.0);
}
Ok(vec)
}
fn decode_strategy(&self, probs: &[f32]) -> JitResult<Vec<OptimizationDecision>> {
let opt_names = vec![
"constant_folding",
"dead_code_elimination",
"common_subexpression_elimination",
"loop_invariant_motion",
"strength_reduction",
"loop_unrolling",
"vectorization",
"parallelization",
"fusion",
"inlining",
"algebraic_simplification",
"peephole",
"instruction_scheduling",
"register_allocation",
"memory_layout",
"cache_blocking",
];
let mut decisions = Vec::new();
for (i, &prob) in probs.iter().enumerate().take(opt_names.len()) {
let apply = prob > 0.5;
let estimated_speedup = if apply { 1.0 + prob } else { 1.0 };
decisions.push(OptimizationDecision {
pass_name: opt_names.get(i).unwrap_or(&"unknown").to_string(),
apply,
estimated_speedup,
estimated_memory_delta: if apply { -1024 } else { 0 },
confidence: prob,
});
}
Ok(decisions)
}
fn generate_reasoning(
&self,
decisions: &[OptimizationDecision],
features: &GraphFeatures,
) -> Vec<String> {
let mut reasoning = Vec::new();
if features.computational.arithmetic_intensity > 10.0 {
reasoning
.push("High arithmetic intensity detected - compute-bound workload".to_string());
} else {
reasoning.push("Low arithmetic intensity detected - memory-bound workload".to_string());
}
let applied_opts: Vec<_> = decisions
.iter()
.filter(|d| d.apply && d.confidence > 0.7)
.map(|d| d.pass_name.as_str())
.collect();
if !applied_opts.is_empty() {
reasoning.push(format!(
"Recommended optimizations: {}",
applied_opts.join(", ")
));
}
reasoning
}
pub fn get_statistics(&self) -> JitResult<HashMap<String, f32>> {
let perf_model = self
.performance_model
.read()
.map_err(|e| JitError::CompilationError(format!("Lock error: {}", e)))?;
let mut stats = HashMap::new();
stats.insert(
"samples_seen".to_string(),
perf_model.stats.samples_seen as f32,
);
stats.insert("current_loss".to_string(), perf_model.stats.current_loss);
stats.insert("best_accuracy".to_string(), perf_model.stats.best_accuracy);
Ok(stats)
}
}
impl Default for NeuralCompiler {
fn default() -> Self {
Self::new()
}
}
pub struct FeatureExtractor {
cache: IndexMap<String, GraphFeatures>,
}
impl FeatureExtractor {
pub fn new() -> Self {
Self {
cache: IndexMap::new(),
}
}
pub fn extract(&self, graph: &ComputationGraph) -> JitResult<GraphFeatures> {
Ok(GraphFeatures {
structural: self.extract_structural(graph)?,
computational: self.extract_computational(graph)?,
memory_patterns: self.extract_memory_patterns(graph)?,
control_flow: self.extract_control_flow(graph)?,
historical: None,
})
}
fn extract_structural(&self, graph: &ComputationGraph) -> JitResult<StructuralFeatures> {
let node_count = graph.node_count();
let edge_count = graph.edge_count();
let depth = self.compute_depth(graph);
let avg_degree = if node_count > 0 {
(edge_count as f32) / (node_count as f32)
} else {
0.0
};
let mut op_type_dist = HashMap::new();
for (node_id, node) in graph.nodes() {
let op_name = format!("{:?}", node.operation);
*op_type_dist.entry(op_name).or_insert(0) += 1;
}
Ok(StructuralFeatures {
node_count,
edge_count,
depth,
avg_degree,
scc_count: 1, diameter: depth,
clustering_coeff: 0.0, op_type_dist,
})
}
fn extract_computational(&self, graph: &ComputationGraph) -> JitResult<ComputationalFeatures> {
let mut total_flops = 0u64;
let mut vectorizable_ops = 0;
let mut memory_bound_ops = 0;
let mut compute_bound_ops = 0;
for (node_id, node) in graph.nodes() {
let op_flops = self.estimate_flops(&node.operation, &node.inputs);
total_flops += op_flops;
if self.is_vectorizable(&node.operation) {
vectorizable_ops += 1;
}
if op_flops > 1000 {
compute_bound_ops += 1;
} else {
memory_bound_ops += 1;
}
}
let arithmetic_intensity = if total_flops > 0 {
total_flops as f32 / (1024.0 * 1024.0) } else {
0.0
};
Ok(ComputationalFeatures {
total_flops,
arithmetic_intensity,
parallelism: graph.node_count(),
vectorizable_ops,
memory_bound_ops,
compute_bound_ops,
})
}
fn extract_memory_patterns(
&self,
graph: &ComputationGraph,
) -> JitResult<MemoryPatternFeatures> {
Ok(MemoryPatternFeatures {
total_memory: graph.node_count() * 1024, peak_memory: graph.node_count() * 2048,
cache_locality: 0.7,
stride_patterns: HashMap::new(),
reuse_distances: vec![],
working_set_size: graph.node_count() * 512,
})
}
fn extract_control_flow(&self, graph: &ComputationGraph) -> JitResult<ControlFlowFeatures> {
Ok(ControlFlowFeatures {
branch_count: 0,
max_loop_depth: 0,
loop_count: 0,
avg_trip_count: 0.0,
branch_predictability: 1.0,
})
}
fn compute_depth(&self, graph: &ComputationGraph) -> usize {
let mut max_depth = 0;
for (node_id, _node) in graph.nodes() {
let depth = self.node_depth(graph, node_id, &mut HashMap::new());
max_depth = max_depth.max(depth);
}
max_depth
}
fn node_depth(
&self,
graph: &ComputationGraph,
node_id: NodeId,
memo: &mut HashMap<NodeId, usize>,
) -> usize {
if let Some(&depth) = memo.get(&node_id) {
return depth;
}
let inputs = graph.get_node_inputs(node_id);
let depth = if inputs.is_empty() {
0
} else {
1 + inputs
.iter()
.map(|&input_id| self.node_depth(graph, input_id, memo))
.max()
.unwrap_or(0)
};
memo.insert(node_id, depth);
depth
}
fn estimate_flops(&self, _operation: &crate::graph::Operation, _inputs: &[NodeId]) -> u64 {
100
}
fn is_vectorizable(&self, operation: &crate::graph::Operation) -> bool {
matches!(
operation,
crate::graph::Operation::Add
| crate::graph::Operation::Mul
| crate::graph::Operation::Relu
| crate::graph::Operation::Sigmoid
)
}
}
impl Default for FeatureExtractor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use torsh_core::{DType, Shape};
#[test]
fn test_neural_compiler_creation() {
let compiler = NeuralCompiler::new();
assert!(compiler.config.online_learning);
}
#[test]
fn test_neural_model_forward() {
let model = NeuralModel::new(10, vec![20, 10], 5);
let input = vec![0.5; 10];
let output = model.forward(&input).unwrap();
assert_eq!(output.len(), 5);
let sum: f32 = output.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
}
#[test]
fn test_feature_extraction() {
let mut builder = GraphBuilder::new();
let x = builder.add_input("x".to_string(), Shape::new(vec![2, 3]), DType::F32);
let y = builder.add_input("y".to_string(), Shape::new(vec![2, 3]), DType::F32);
let z = builder
.add_binary_op("add".to_string(), crate::graph::Operation::Add, x, y)
.unwrap();
builder.mark_output(z).unwrap();
let graph = builder.build().unwrap();
let extractor = FeatureExtractor::new();
let features = extractor.extract(&graph).unwrap();
assert!(features.structural.node_count >= 3); assert!(features.computational.total_flops > 0);
}
#[test]
fn test_strategy_prediction() {
let compiler = NeuralCompiler::new();
let mut builder = GraphBuilder::new();
let x = builder.add_input("x".to_string(), Shape::new(vec![10, 10]), DType::F32);
let y = builder
.add_unary_op("relu".to_string(), crate::graph::Operation::Relu, x)
.unwrap();
builder.mark_output(y).unwrap();
let graph = builder.build().unwrap();
let features = compiler.extract_features(&graph).unwrap();
let strategy = compiler.predict_strategy(&features).unwrap();
assert!(!strategy.optimizations.is_empty());
assert!(strategy.confidence >= 0.0 && strategy.confidence <= 1.0);
}
#[test]
fn test_online_learning() {
let mut compiler = NeuralCompiler::new();
let mut builder = GraphBuilder::new();
let x = builder.add_input("x".to_string(), Shape::new(vec![5, 5]), DType::F32);
builder.mark_output(x).unwrap();
let graph = builder.build().unwrap();
let features = compiler.extract_features(&graph).unwrap();
let strategy = compiler.predict_strategy(&features).unwrap();
let result = compiler.learn_from_execution(&features, &strategy, 1500.0, 2048);
assert!(result.is_ok());
let stats = compiler.get_statistics().unwrap();
assert_eq!(stats.get("samples_seen").copied().unwrap_or(0.0), 1.0);
}
}