Files
rustytorch/crates/specialized/rtx-geom/src/layers/sparse_gcn.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

414 lines
13 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.
//! 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(())
}
}