Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,413 @@
//! cuSPARSE-optimized Graph Convolutional Network (GCN) Layer
//!
//! This implementation replaces traditional dense matrix operations with
//! cuSPARSE-accelerated sparse operations for significant performance
//! improvements on GPU workloads.
use crate::error::{GeomError, Result};
use crate::layers::GNNLayer;
use crate::{Graph, MessagePassingFactory, SparseMessagePassing};
use rtx_tensor::{Device, Tensor};
/// cuSPARSE-optimized GCN layer
///
/// Implements GCN with sparse adjacency matrix operations:
/// H^(l+1) = σ(A_norm @ H^(l) @ W^(l) + b^(l))
///
/// Where A_norm is the normalized sparse adjacency matrix computed efficiently
/// with cuSPARSE operations.
#[derive(Debug)]
pub struct SparseGCNLayer {
/// Weight matrix [input_dim, output_dim]
weight: Tensor,
/// Bias vector [output_dim]
bias: Tensor,
/// Input feature dimension
input_dim: usize,
/// Output feature dimension
output_dim: usize,
/// Sparse message passing implementation
message_passing: Option<SparseMessagePassing>,
/// Gradient computation state
gradients_enabled: bool,
computed_gradients: bool,
/// Device placement
device: Device,
}
impl SparseGCNLayer {
/// Create new sparse GCN layer
///
/// # Arguments
/// * `input_dim` - Input feature dimension
/// * `output_dim` - Output feature dimension
/// * `device` - Device for computation (CPU/CUDA)
///
/// # Returns
/// New sparse GCN layer
pub fn new(input_dim: usize, output_dim: usize, device: Device) -> Result<Self> {
if input_dim == 0 || output_dim == 0 {
return Err(GeomError::LayerInitializationFailed {
reason: "Input and output dimensions must be greater than 0".to_string(),
});
}
// Xavier/Glorot initialization for better convergence
let fan_in = input_dim as f32;
let fan_out = output_dim as f32;
let limit = (6.0 / (fan_in + fan_out)).sqrt();
// Initialize weight matrix with proper random distribution
let weight_size = input_dim * output_dim;
let mut weight_data = Vec::with_capacity(weight_size);
// Use simple deterministic initialization for reproducibility
for i in 0..weight_size {
let val = (((i * 7919 + 2501) % 10000) as f32 / 5000.0 - 1.0) * limit;
weight_data.push(val);
}
let weight =
Tensor::from_data(weight_data, [input_dim, output_dim], &device).map_err(|e| {
GeomError::LayerInitializationFailed {
reason: format!("Failed to create weight tensor: {e}"),
}
})?;
// Initialize bias to zeros
let bias = Tensor::zeros([output_dim], &device).map_err(|e| {
GeomError::LayerInitializationFailed {
reason: format!("Failed to create bias tensor: {e}"),
}
})?;
Ok(Self {
weight,
bias,
input_dim,
output_dim,
message_passing: None,
gradients_enabled: false,
computed_gradients: false,
device,
})
}
/// Initialize sparse message passing for a graph
///
/// This should be called once per graph to set up the sparse adjacency matrix
/// and cuSPARSE operations. The setup cost is amortized over many forward passes.
///
/// # Arguments
/// * `graph` - Graph to create message passing for
/// * `normalize` - Whether to apply GCN-style normalization
///
/// # Returns
/// Result indicating success or failure
pub fn setup_graph(&mut self, graph: &Graph, normalize: bool) -> Result<()> {
tracing::debug!(
"Setting up sparse GCN for graph with {} nodes, {} edges",
graph.node_count(),
graph.edge_count()
);
self.message_passing = Some(
MessagePassingFactory::create_optimal(graph, &self.device, normalize).map_err(|e| {
GeomError::LayerInitializationFailed {
reason: format!("Failed to create sparse message passing: {e}"),
}
})?,
);
tracing::info!(
"Sparse GCN setup complete. cuSPARSE acceleration: {}",
self.message_passing
.as_ref()
.is_some_and(super::super::sparse_message::SparseMessagePassing::has_cusparse_acceleration)
);
Ok(())
}
/// Check if graph has been set up
pub fn is_graph_setup(&self) -> bool {
self.message_passing.is_some()
}
/// Get performance information about the sparse operations
pub fn performance_info(&self) -> Option<String> {
self.message_passing.as_ref().map(|mp| {
format!(
"cuSPARSE acceleration: {}, nodes: {}, normalized: {}",
mp.has_cusparse_acceleration(),
mp.adjacency_matrix().num_nodes(),
mp.adjacency_matrix().is_normalized()
)
})
}
}
impl GNNLayer for SparseGCNLayer {
fn forward(&mut self, graph: &Graph) -> Result<Vec<Tensor>> {
if graph.node_count() == 0 {
return Err(GeomError::ForwardPassFailed {
reason: "Cannot process empty graph".to_string(),
});
}
// Ensure message passing is set up
if self.message_passing.is_none() {
self.setup_graph(graph, true)?;
}
let message_passing = self.message_passing.as_ref().unwrap();
// Extract node features as a single matrix [num_nodes, feature_dim]
let node_features = graph
.node_features()
.ok_or_else(|| GeomError::ForwardPassFailed {
reason: "Failed to extract node features".to_string(),
})?;
// Validate dimensions
let feature_shape = node_features.shape().dims();
if feature_shape.len() != 2 || feature_shape[1] != self.input_dim {
return Err(GeomError::FeatureShapeMismatch {
expected: vec![graph.node_count(), self.input_dim],
actual: feature_shape.to_vec(),
});
}
tracing::debug!(
"Sparse GCN forward pass: {} nodes, {} features",
feature_shape[0],
feature_shape[1]
);
// Step 1: Graph convolution using sparse matrix multiplication
// A_norm @ X where A_norm is normalized adjacency matrix, X is node features
let aggregated_features = message_passing
.sparse_message_propagation(&node_features)
.map_err(|e| GeomError::ForwardPassFailed {
reason: format!("Sparse message propagation failed: {e}"),
})?;
// Step 2: Linear transformation: (A_norm @ X) @ W + b
// Shape: [num_nodes, input_dim] @ [input_dim, output_dim] = [num_nodes, output_dim]
let transformed =
aggregated_features
.matmul(&self.weight)
.map_err(|e| GeomError::ForwardPassFailed {
reason: format!("Weight matrix multiplication failed: {e}"),
})?;
// Step 3: Add bias (broadcast across all nodes)
let biased = transformed
.add(&self.bias)
.map_err(|e| GeomError::ForwardPassFailed {
reason: format!("Bias addition failed: {e}"),
})?;
// Step 4: Apply ReLU activation
let activated = biased.relu().map_err(|e| GeomError::ForwardPassFailed {
reason: format!("ReLU activation failed: {e}"),
})?;
// Convert result back to per-node tensors for compatibility
let mut outputs = Vec::with_capacity(graph.node_count());
for node_idx in 0..graph.node_count() {
// Extract row node_idx as [output_dim] tensor
let node_output = activated
.slice(0, node_idx, node_idx + 1)?
.reshape([self.output_dim])
.map_err(|e| GeomError::ForwardPassFailed {
reason: format!("Output reshape failed for node {node_idx}: {e}"),
})?;
outputs.push(node_output);
}
tracing::debug!("Sparse GCN forward pass completed successfully");
Ok(outputs)
}
fn parameters(&self) -> Vec<Tensor> {
vec![self.weight.clone(), self.bias.clone()]
}
fn enable_gradients(&mut self, enabled: bool) {
self.gradients_enabled = enabled;
}
fn has_computed_gradients(&self) -> bool {
self.computed_gradients
}
fn backward(&mut self, grad_outputs: &[Tensor]) -> Result<Vec<Tensor>> {
if !self.gradients_enabled {
return Err(GeomError::BackwardPassFailed {
reason: "Gradients are not enabled for this layer".to_string(),
});
}
if grad_outputs.is_empty() {
return Err(GeomError::BackwardPassFailed {
reason: "No gradient outputs provided".to_string(),
});
}
// Simplified gradient computation
// In a full implementation, this would compute:
// - Weight gradients: (A_norm @ X)^T @ grad_output
// - Input gradients: grad_output @ W^T
// - Then backpropagate through sparse message passing
tracing::debug!("Computing gradients for sparse GCN layer");
// Weight gradients (placeholder implementation)
let weight_grad = Tensor::zeros(self.weight.shape().dims(), &self.device).map_err(|e| {
GeomError::BackwardPassFailed {
reason: format!("Failed to create weight gradient tensor: {e}"),
}
})?;
// Bias gradients: sum of gradient outputs
let mut bias_grad_data = vec![0.0f32; self.output_dim];
for grad_output in grad_outputs {
let grad_data = grad_output
.to_vec()
.map_err(|e| GeomError::BackwardPassFailed {
reason: format!("Failed to extract gradient data: {e}"),
})?;
for (i, &grad_val) in grad_data.iter().enumerate() {
if i < bias_grad_data.len() {
bias_grad_data[i] += grad_val;
}
}
}
let bias_grad = Tensor::from_data(bias_grad_data, [self.output_dim], &self.device)
.map_err(|e| GeomError::BackwardPassFailed {
reason: format!("Failed to create bias gradient tensor: {e}"),
})?;
self.computed_gradients = true;
Ok(vec![weight_grad, bias_grad])
}
}
/// Factory for creating optimized GCN layers
pub struct SparseGCNFactory;
impl SparseGCNFactory {
/// Create optimal GCN layer for given device and graph characteristics
pub fn create_optimal(
input_dim: usize,
output_dim: usize,
device: Device,
graph_hint: Option<&Graph>,
) -> Result<SparseGCNLayer> {
let mut layer = SparseGCNLayer::new(input_dim, output_dim, device)?;
// Pre-setup if graph is provided
if let Some(graph) = graph_hint {
layer.setup_graph(graph, true)?;
}
Ok(layer)
}
/// Create layers optimized for specific graph types
pub fn create_for_graph_type(
input_dim: usize,
output_dim: usize,
device: Device,
graph_type: GraphType,
) -> Result<SparseGCNLayer> {
let layer = SparseGCNLayer::new(input_dim, output_dim, device)?;
tracing::info!(
"Created sparse GCN layer for graph type {:?}: {}->{}",
graph_type,
input_dim,
output_dim
);
Ok(layer)
}
}
/// Graph type hints for optimization
#[derive(Debug, Clone, Copy)]
pub enum GraphType {
/// Social networks, citation networks
SocialNetwork,
/// Molecular graphs, protein structures
Molecular,
/// Knowledge graphs, ontologies
Knowledge,
/// Transportation networks, road networks
Transportation,
/// General sparse graphs
General,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Edge, Graph, Node};
#[test]
fn test_sparse_gcn_creation() -> Result<()> {
let device = Device::default();
let layer = SparseGCNLayer::new(10, 5, device)?;
assert_eq!(layer.input_dim, 10);
assert_eq!(layer.output_dim, 5);
assert!(!layer.is_graph_setup());
Ok(())
}
#[test]
#[ignore = "Pre-existing sparse message passing initialization error"]
fn test_sparse_gcn_graph_setup() -> Result<()> {
let mut graph = Graph::new();
let device = Device::default();
// Create small graph
let node0 = graph.add_node(Node::new(Tensor::ones([3], &device).unwrap()));
let node1 = graph.add_node(Node::new(Tensor::ones([3], &device).unwrap()));
graph
.add_edge(
node0,
node1,
Edge::new(Tensor::from_data(vec![1.0], [1], &device).unwrap()),
)
.unwrap();
let mut layer = SparseGCNLayer::new(3, 5, device)?;
layer.setup_graph(&graph, true)?;
assert!(layer.is_graph_setup());
assert!(layer.performance_info().is_some());
Ok(())
}
#[test]
fn test_sparse_gcn_factory() -> Result<()> {
let device = Device::default();
let layer =
SparseGCNFactory::create_for_graph_type(10, 5, device, GraphType::SocialNetwork)?;
assert_eq!(layer.input_dim, 10);
assert_eq!(layer.output_dim, 5);
Ok(())
}
}