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