1use crate::error::SparseError;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum MessagePassingOp {
29 Sum,
31 Mean,
33 Max,
35 Min,
37}
38
39#[derive(Debug, Clone)]
41pub struct GnnSparseConfig {
42 pub num_nodes: usize,
44 pub feature_dim: usize,
46 pub num_edges: usize,
48 pub op: MessagePassingOp,
50 pub normalize: bool,
52}
53
54#[derive(Debug, Clone)]
58pub struct EdgeFeatures {
59 pub values: Vec<f64>,
61 pub edge_count: usize,
63}
64
65pub fn scatter_reduce(
86 src: &[f64],
87 index: &[usize],
88 num_targets: usize,
89 op: MessagePassingOp,
90) -> Result<Vec<f64>, SparseError> {
91 if src.len() != index.len() {
92 return Err(SparseError::DimensionMismatch(format!(
93 "src length {} != index length {}",
94 src.len(),
95 index.len()
96 )));
97 }
98
99 for (i, &idx) in index.iter().enumerate() {
101 if idx >= num_targets {
102 return Err(SparseError::InvalidArgument(format!(
103 "index[{}] = {} is out of bounds for num_targets = {}",
104 i, idx, num_targets
105 )));
106 }
107 }
108
109 let init_val = match op {
110 MessagePassingOp::Sum | MessagePassingOp::Mean => 0.0_f64,
111 MessagePassingOp::Max => f64::NEG_INFINITY,
112 MessagePassingOp::Min => f64::INFINITY,
113 };
114
115 let mut output = vec![init_val; num_targets];
116 let mut counts = if matches!(op, MessagePassingOp::Mean) {
117 vec![0usize; num_targets]
118 } else {
119 Vec::new()
120 };
121
122 for (i, &idx) in index.iter().enumerate() {
123 match op {
124 MessagePassingOp::Sum => {
125 output[idx] += src[i];
126 }
127 MessagePassingOp::Mean => {
128 output[idx] += src[i];
129 counts[idx] += 1;
130 }
131 MessagePassingOp::Max => {
132 if src[i] > output[idx] {
133 output[idx] = src[i];
134 }
135 }
136 MessagePassingOp::Min => {
137 if src[i] < output[idx] {
138 output[idx] = src[i];
139 }
140 }
141 }
142 }
143
144 if matches!(op, MessagePassingOp::Mean) {
146 for (val, &cnt) in output.iter_mut().zip(counts.iter()) {
147 if cnt > 0 {
148 *val /= cnt as f64;
149 } else {
150 *val = 0.0;
151 }
152 }
153 }
154
155 if matches!(op, MessagePassingOp::Max | MessagePassingOp::Min) {
157 for val in &mut output {
158 if *val == f64::NEG_INFINITY || *val == f64::INFINITY {
159 *val = 0.0;
160 }
161 }
162 }
163
164 Ok(output)
165}
166
167pub fn gather(src: &[f64], index: &[usize]) -> Result<Vec<f64>, SparseError> {
176 let mut result = Vec::with_capacity(index.len());
177 for (i, &idx) in index.iter().enumerate() {
178 if idx >= src.len() {
179 return Err(SparseError::InvalidArgument(format!(
180 "gather index[{}] = {} out of bounds for src of length {}",
181 i,
182 idx,
183 src.len()
184 )));
185 }
186 result.push(src[idx]);
187 }
188 Ok(result)
189}
190
191pub fn sparse_message_passing(
219 adj_row_offsets: &[usize],
220 adj_col_indices: &[usize],
221 node_features: &[f64],
222 feature_dim: usize,
223 config: &GnnSparseConfig,
224) -> Result<Vec<f64>, SparseError> {
225 let num_nodes = config.num_nodes;
226
227 if adj_row_offsets.len() != num_nodes + 1 {
228 return Err(SparseError::DimensionMismatch(format!(
229 "adj_row_offsets length {} != num_nodes + 1 = {}",
230 adj_row_offsets.len(),
231 num_nodes + 1
232 )));
233 }
234 if feature_dim != config.feature_dim {
235 return Err(SparseError::DimensionMismatch(format!(
236 "feature_dim {} != config.feature_dim {}",
237 feature_dim, config.feature_dim
238 )));
239 }
240 if node_features.len() != num_nodes * feature_dim {
241 return Err(SparseError::DimensionMismatch(format!(
242 "node_features length {} != num_nodes * feature_dim = {}",
243 node_features.len(),
244 num_nodes * feature_dim
245 )));
246 }
247
248 let mut output = vec![0.0_f64; num_nodes * feature_dim];
249
250 for i in 0..num_nodes {
251 let start = adj_row_offsets[i];
252 let end = adj_row_offsets[i + 1];
253 let degree = end - start;
254
255 if degree == 0 {
256 continue;
257 }
258
259 match config.op {
260 MessagePassingOp::Sum | MessagePassingOp::Mean => {
261 for &j in &adj_col_indices[start..end] {
262 if j >= num_nodes {
263 return Err(SparseError::InvalidArgument(format!(
264 "column index {} out of bounds for {} nodes",
265 j, num_nodes
266 )));
267 }
268 for f in 0..feature_dim {
269 output[i * feature_dim + f] += node_features[j * feature_dim + f];
270 }
271 }
272 if config.op == MessagePassingOp::Mean || config.normalize {
273 let inv_degree = 1.0 / degree as f64;
274 for f in 0..feature_dim {
275 output[i * feature_dim + f] *= inv_degree;
276 }
277 }
278 }
279 MessagePassingOp::Max => {
280 for f in 0..feature_dim {
282 output[i * feature_dim + f] = f64::NEG_INFINITY;
283 }
284 for &j in &adj_col_indices[start..end] {
285 if j >= num_nodes {
286 return Err(SparseError::InvalidArgument(format!(
287 "column index {} out of bounds for {} nodes",
288 j, num_nodes
289 )));
290 }
291 for f in 0..feature_dim {
292 let val = node_features[j * feature_dim + f];
293 if val > output[i * feature_dim + f] {
294 output[i * feature_dim + f] = val;
295 }
296 }
297 }
298 }
300 MessagePassingOp::Min => {
301 for f in 0..feature_dim {
302 output[i * feature_dim + f] = f64::INFINITY;
303 }
304 for &j in &adj_col_indices[start..end] {
305 if j >= num_nodes {
306 return Err(SparseError::InvalidArgument(format!(
307 "column index {} out of bounds for {} nodes",
308 j, num_nodes
309 )));
310 }
311 for f in 0..feature_dim {
312 let val = node_features[j * feature_dim + f];
313 if val < output[i * feature_dim + f] {
314 output[i * feature_dim + f] = val;
315 }
316 }
317 }
318 }
319 }
320 }
321
322 Ok(output)
323}
324
325pub fn sparse_attention_message(
347 adj_row_offsets: &[usize],
348 adj_col_indices: &[usize],
349 node_features: &[f64],
350 attention_weights: &[f64],
351 feature_dim: usize,
352) -> Result<Vec<f64>, SparseError> {
353 if adj_row_offsets.is_empty() {
354 return Err(SparseError::InvalidArgument(
355 "adj_row_offsets must not be empty".to_string(),
356 ));
357 }
358 let num_nodes = adj_row_offsets.len() - 1;
359 let num_edges = adj_col_indices.len();
360
361 if attention_weights.len() != num_edges {
362 return Err(SparseError::DimensionMismatch(format!(
363 "attention_weights length {} != num_edges {}",
364 attention_weights.len(),
365 num_edges
366 )));
367 }
368 if node_features.len() != num_nodes * feature_dim {
369 return Err(SparseError::DimensionMismatch(format!(
370 "node_features length {} != num_nodes * feature_dim = {}",
371 node_features.len(),
372 num_nodes * feature_dim
373 )));
374 }
375
376 let mut output = vec![0.0_f64; num_nodes * feature_dim];
377
378 for i in 0..num_nodes {
379 let start = adj_row_offsets[i];
380 let end = adj_row_offsets[i + 1];
381
382 for edge_idx in start..end {
383 let j = adj_col_indices[edge_idx];
384 if j >= num_nodes {
385 return Err(SparseError::InvalidArgument(format!(
386 "column index {} out of bounds for {} nodes",
387 j, num_nodes
388 )));
389 }
390 let alpha = attention_weights[edge_idx];
391 for f in 0..feature_dim {
392 output[i * feature_dim + f] += alpha * node_features[j * feature_dim + f];
393 }
394 }
395 }
396
397 Ok(output)
398}
399
400pub fn compute_degree_matrix(adj_row_offsets: &[usize], num_nodes: usize) -> Vec<f64> {
413 let mut degrees = Vec::with_capacity(num_nodes);
414 for i in 0..num_nodes {
415 let start = if i < adj_row_offsets.len() {
416 adj_row_offsets[i]
417 } else {
418 0
419 };
420 let end = if i + 1 < adj_row_offsets.len() {
421 adj_row_offsets[i + 1]
422 } else {
423 start
424 };
425 degrees.push((end - start) as f64);
426 }
427 degrees
428}
429
430pub fn symmetric_normalize(
453 adj_row_offsets: &[usize],
454 adj_col_indices: &[usize],
455 adj_values: &[f64],
456 degrees: &[f64],
457) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
458 let num_nodes = if adj_row_offsets.is_empty() {
459 0
460 } else {
461 adj_row_offsets.len() - 1
462 };
463
464 let mut new_values = Vec::with_capacity(adj_values.len());
465
466 for i in 0..num_nodes {
467 let start = adj_row_offsets[i];
468 let end = adj_row_offsets[i + 1];
469
470 let di = degrees[i];
471 let di_inv_sqrt = if di > 0.0 { 1.0 / di.sqrt() } else { 0.0 };
472
473 for edge_idx in start..end {
474 let j = adj_col_indices[edge_idx];
475 let dj = if j < degrees.len() { degrees[j] } else { 0.0 };
476 let dj_inv_sqrt = if dj > 0.0 { 1.0 / dj.sqrt() } else { 0.0 };
477
478 new_values.push(adj_values[edge_idx] * di_inv_sqrt * dj_inv_sqrt);
479 }
480 }
481
482 (
483 adj_row_offsets.to_vec(),
484 adj_col_indices.to_vec(),
485 new_values,
486 )
487}
488
489pub fn add_self_loops(
505 adj_row_offsets: &[usize],
506 adj_col_indices: &[usize],
507 adj_values: &[f64],
508 num_nodes: usize,
509) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
510 let mut new_row_offsets = Vec::with_capacity(num_nodes + 1);
511 let mut new_col_indices = Vec::new();
512 let mut new_values = Vec::new();
513
514 new_row_offsets.push(0);
515
516 for i in 0..num_nodes {
517 let start = adj_row_offsets[i];
518 let end = adj_row_offsets[i + 1];
519
520 let mut has_self_loop = false;
521 let mut inserted_self = false;
522
523 for edge_idx in start..end {
524 let j = adj_col_indices[edge_idx];
525
526 if !inserted_self && j >= i {
528 if j == i {
529 has_self_loop = true;
531 new_col_indices.push(i);
532 new_values.push(adj_values[edge_idx] + 1.0);
533 inserted_self = true;
534 continue;
535 }
536 new_col_indices.push(i);
538 new_values.push(1.0);
539 inserted_self = true;
540 }
541
542 new_col_indices.push(j);
543 new_values.push(adj_values[edge_idx]);
544 }
545
546 if !has_self_loop && !inserted_self {
548 new_col_indices.push(i);
549 new_values.push(1.0);
550 }
551
552 new_row_offsets.push(new_col_indices.len());
553 }
554
555 (new_row_offsets, new_col_indices, new_values)
556}
557
558pub fn sparse_row_softmax(row_offsets: &[usize], values: &[f64]) -> Vec<f64> {
581 if row_offsets.len() <= 1 {
582 return values.to_vec();
583 }
584
585 let num_rows = row_offsets.len() - 1;
586 let mut result = vec![0.0_f64; values.len()];
587
588 for i in 0..num_rows {
589 let start = row_offsets[i];
590 let end = row_offsets[i + 1];
591
592 if start >= end {
593 continue;
594 }
595
596 let mut max_val = f64::NEG_INFINITY;
598 for v in &values[start..end] {
599 if *v > max_val {
600 max_val = *v;
601 }
602 }
603
604 let mut sum = 0.0_f64;
606 for (r, v) in result[start..end].iter_mut().zip(&values[start..end]) {
607 let e = (*v - max_val).exp();
608 *r = e;
609 sum += e;
610 }
611
612 if sum > 0.0 {
614 let inv_sum = 1.0 / sum;
615 for r in &mut result[start..end] {
616 *r *= inv_sum;
617 }
618 }
619 }
620
621 result
622}
623
624pub fn generate_message_passing_ptx(config: &GnnSparseConfig) -> Result<String, SparseError> {
644 let op_name = match config.op {
645 MessagePassingOp::Sum => "sum",
646 MessagePassingOp::Mean => "mean",
647 MessagePassingOp::Max => "max",
648 MessagePassingOp::Min => "min",
649 };
650 let normalize_flag = if config.normalize || config.op == MessagePassingOp::Mean {
651 1
652 } else {
653 0
654 };
655 let feature_dim = config.feature_dim;
656 let num_nodes = config.num_nodes;
657
658 if feature_dim == 0 {
659 return Err(SparseError::PtxGeneration(
660 "feature_dim must be > 0".to_string(),
661 ));
662 }
663
664 let ptx = format!(
666 r#"//
667// GNN Message Passing Kernel ({op_name})
668// Generated by oxicuda-sparse tensor module
669// num_nodes={num_nodes}, feature_dim={feature_dim}, normalize={normalize_flag}
670//
671.version 7.0
672.target sm_70
673.address_size 64
674
675.visible .entry gnn_message_passing_f64(
676 .param .u64 row_offsets_ptr,
677 .param .u64 col_indices_ptr,
678 .param .u64 node_features_ptr,
679 .param .u64 output_ptr,
680 .param .u32 num_nodes_param,
681 .param .u32 feature_dim_param
682)
683{{
684 .reg .u32 %r<32>;
685 .reg .u64 %rd<32>;
686 .reg .f64 %fd<16>;
687 .reg .pred %p<8>;
688
689 // tid = blockIdx.x * blockDim.x + threadIdx.x
690 mov.u32 %r0, %ctaid.x;
691 mov.u32 %r1, %ntid.x;
692 mov.u32 %r2, %tid.x;
693 mad.lo.u32 %r3, %r0, %r1, %r2;
694
695 // Each thread handles one node
696 ld.param.u32 %r4, [num_nodes_param];
697 setp.ge.u32 %p0, %r3, %r4;
698 @%p0 ret;
699
700 // Load row_offsets[node] and row_offsets[node+1]
701 ld.param.u64 %rd0, [row_offsets_ptr];
702 cvt.u64.u32 %rd1, %r3;
703
704 // offset = node * 8 (u64 = 8 bytes)
705 shl.b64 %rd2, %rd1, 3;
706 add.u64 %rd3, %rd0, %rd2;
707 ld.global.u64 %rd4, [%rd3]; // row_start
708 ld.global.u64 %rd5, [%rd3 + 8]; // row_end
709
710 // degree = row_end - row_start
711 sub.u64 %rd6, %rd5, %rd4;
712
713 // Skip if degree == 0
714 setp.eq.u64 %p1, %rd6, 0;
715 @%p1 ret;
716
717 // Iterate over features and neighbors
718 // (simplified: actual implementation would loop over feature_dim
719 // and accumulate per-feature aggregates)
720
721 ret;
722}}
723"#
724 );
725
726 Ok(ptx)
727}
728
729#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[test]
740 fn scatter_reduce_sum() {
741 let src = vec![1.0, 2.0, 3.0, 4.0];
742 let index = vec![0, 1, 0, 1];
743 let result = scatter_reduce(&src, &index, 2, MessagePassingOp::Sum)
744 .expect("scatter_reduce Sum failed");
745 assert!((result[0] - 4.0).abs() < 1e-12);
746 assert!((result[1] - 6.0).abs() < 1e-12);
747 }
748
749 #[test]
750 fn scatter_reduce_mean() {
751 let src = vec![1.0, 2.0, 3.0, 4.0];
752 let index = vec![0, 1, 0, 1];
753 let result = scatter_reduce(&src, &index, 2, MessagePassingOp::Mean)
754 .expect("scatter_reduce Mean failed");
755 assert!((result[0] - 2.0).abs() < 1e-12);
756 assert!((result[1] - 3.0).abs() < 1e-12);
757 }
758
759 #[test]
760 fn scatter_reduce_max() {
761 let src = vec![1.0, 5.0, 3.0, 2.0];
762 let index = vec![0, 1, 0, 1];
763 let result = scatter_reduce(&src, &index, 2, MessagePassingOp::Max)
764 .expect("scatter_reduce Max failed");
765 assert!((result[0] - 3.0).abs() < 1e-12);
766 assert!((result[1] - 5.0).abs() < 1e-12);
767 }
768
769 #[test]
770 fn scatter_reduce_min() {
771 let src = vec![1.0, 5.0, 3.0, 2.0];
772 let index = vec![0, 1, 0, 1];
773 let result = scatter_reduce(&src, &index, 2, MessagePassingOp::Min)
774 .expect("scatter_reduce Min failed");
775 assert!((result[0] - 1.0).abs() < 1e-12);
776 assert!((result[1] - 2.0).abs() < 1e-12);
777 }
778
779 #[test]
782 fn gather_basic() {
783 let src = vec![10.0, 20.0, 30.0, 40.0];
784 let index = vec![3, 0, 2, 1, 0];
785 let result = gather(&src, &index).expect("gather failed");
786 assert_eq!(result, vec![40.0, 10.0, 30.0, 20.0, 10.0]);
787 }
788
789 fn triangle_csr() -> (Vec<usize>, Vec<usize>) {
793 let row_offsets = vec![0, 2, 4, 6];
797 let col_indices = vec![1, 2, 0, 2, 0, 1];
798 (row_offsets, col_indices)
799 }
800
801 #[test]
802 fn message_passing_sum_triangle() {
803 let (row_offsets, col_indices) = triangle_csr();
804 let features = vec![1.0, 2.0, 3.0]; let config = GnnSparseConfig {
806 num_nodes: 3,
807 feature_dim: 1,
808 num_edges: 6,
809 op: MessagePassingOp::Sum,
810 normalize: false,
811 };
812 let result = sparse_message_passing(&row_offsets, &col_indices, &features, 1, &config)
813 .expect("message_passing failed");
814 assert!((result[0] - 5.0).abs() < 1e-12);
818 assert!((result[1] - 4.0).abs() < 1e-12);
819 assert!((result[2] - 3.0).abs() < 1e-12);
820 }
821
822 #[test]
825 fn attention_message_triangle() {
826 let (row_offsets, col_indices) = triangle_csr();
827 let features = vec![1.0, 2.0, 3.0];
828 let attention = vec![0.5, 0.5, 0.3, 0.7, 0.6, 0.4];
829 let result = sparse_attention_message(&row_offsets, &col_indices, &features, &attention, 1)
830 .expect("attention message failed");
831 assert!((result[0] - 2.5).abs() < 1e-12);
835 assert!((result[1] - 2.4).abs() < 1e-12);
836 assert!((result[2] - 1.4).abs() < 1e-12);
837 }
838
839 #[test]
842 fn degree_matrix_triangle() {
843 let (row_offsets, _col_indices) = triangle_csr();
844 let degrees = compute_degree_matrix(&row_offsets, 3);
845 assert!((degrees[0] - 2.0).abs() < 1e-12);
846 assert!((degrees[1] - 2.0).abs() < 1e-12);
847 assert!((degrees[2] - 2.0).abs() < 1e-12);
848 }
849
850 #[test]
853 fn symmetric_normalize_triangle() {
854 let (row_offsets, col_indices) = triangle_csr();
855 let values = vec![1.0; 6];
856 let degrees = compute_degree_matrix(&row_offsets, 3);
857 let (new_rp, new_ci, new_vals) =
858 symmetric_normalize(&row_offsets, &col_indices, &values, °rees);
859
860 assert_eq!(new_rp, row_offsets);
862 assert_eq!(new_ci, col_indices);
863
864 for v in &new_vals {
867 assert!((v - 0.5).abs() < 1e-12);
868 }
869 }
870
871 #[test]
874 fn add_self_loops_triangle() {
875 let (row_offsets, col_indices) = triangle_csr();
876 let values = vec![1.0; 6];
877 let (new_rp, new_ci, new_vals) = add_self_loops(&row_offsets, &col_indices, &values, 3);
878
879 assert_eq!(new_rp.len(), 4); assert_eq!(new_ci.len(), 9);
882 assert_eq!(new_vals.len(), 9);
883
884 for node in 0..3 {
886 let start = new_rp[node];
887 let end = new_rp[node + 1];
888 let row_cols = &new_ci[start..end];
889 assert!(row_cols.contains(&node), "node {} missing self-loop", node);
890 }
891 }
892
893 #[test]
896 fn sparse_softmax_sums_to_one() {
897 let row_offsets = vec![0, 3, 5];
898 let values = vec![1.0, 2.0, 3.0, 0.5, 1.5];
899 let result = sparse_row_softmax(&row_offsets, &values);
900
901 let row0_sum: f64 = result[0..3].iter().sum();
903 assert!((row0_sum - 1.0).abs() < 1e-12);
904
905 let row1_sum: f64 = result[3..5].iter().sum();
907 assert!((row1_sum - 1.0).abs() < 1e-12);
908
909 for v in &result {
911 assert!(*v >= 0.0);
912 }
913 }
914
915 #[test]
918 fn empty_graph_message_passing() {
919 let row_offsets = vec![0];
920 let col_indices: Vec<usize> = vec![];
921 let features: Vec<f64> = vec![];
922 let config = GnnSparseConfig {
923 num_nodes: 0,
924 feature_dim: 1,
925 num_edges: 0,
926 op: MessagePassingOp::Sum,
927 normalize: false,
928 };
929 let result = sparse_message_passing(&row_offsets, &col_indices, &features, 1, &config)
930 .expect("empty graph failed");
931 assert!(result.is_empty());
932 }
933
934 #[test]
937 fn single_node_no_edges() {
938 let row_offsets = vec![0, 0];
939 let col_indices: Vec<usize> = vec![];
940 let features = vec![42.0];
941 let config = GnnSparseConfig {
942 num_nodes: 1,
943 feature_dim: 1,
944 num_edges: 0,
945 op: MessagePassingOp::Sum,
946 normalize: false,
947 };
948 let result = sparse_message_passing(&row_offsets, &col_indices, &features, 1, &config)
949 .expect("single node failed");
950 assert!((result[0] - 0.0).abs() < 1e-12);
952 }
953
954 #[test]
957 fn disconnected_graph() {
958 let row_offsets = vec![0, 1, 2, 2, 2];
960 let col_indices = vec![1, 0];
961 let features = vec![1.0, 2.0, 3.0, 4.0];
962 let config = GnnSparseConfig {
963 num_nodes: 4,
964 feature_dim: 1,
965 num_edges: 2,
966 op: MessagePassingOp::Sum,
967 normalize: false,
968 };
969 let result = sparse_message_passing(&row_offsets, &col_indices, &features, 1, &config)
970 .expect("disconnected graph failed");
971 assert!((result[0] - 2.0).abs() < 1e-12); assert!((result[1] - 1.0).abs() < 1e-12); assert!((result[2] - 0.0).abs() < 1e-12); assert!((result[3] - 0.0).abs() < 1e-12); }
976
977 #[test]
980 fn feature_dim_greater_than_one() {
981 let row_offsets = vec![0, 1, 2];
983 let col_indices = vec![1, 0];
984 let features = vec![
985 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, ];
988 let config = GnnSparseConfig {
989 num_nodes: 2,
990 feature_dim: 3,
991 num_edges: 2,
992 op: MessagePassingOp::Sum,
993 normalize: false,
994 };
995 let result = sparse_message_passing(&row_offsets, &col_indices, &features, 3, &config)
996 .expect("multi-dim features failed");
997 assert!((result[0] - 4.0).abs() < 1e-12);
999 assert!((result[1] - 5.0).abs() < 1e-12);
1000 assert!((result[2] - 6.0).abs() < 1e-12);
1001 assert!((result[3] - 1.0).abs() < 1e-12);
1003 assert!((result[4] - 2.0).abs() < 1e-12);
1004 assert!((result[5] - 3.0).abs() < 1e-12);
1005 }
1006
1007 #[test]
1010 fn normalize_flag_divides_by_degree() {
1011 let (row_offsets, col_indices) = triangle_csr();
1012 let features = vec![2.0, 4.0, 6.0];
1013 let config = GnnSparseConfig {
1014 num_nodes: 3,
1015 feature_dim: 1,
1016 num_edges: 6,
1017 op: MessagePassingOp::Sum,
1018 normalize: true,
1019 };
1020 let result = sparse_message_passing(&row_offsets, &col_indices, &features, 1, &config)
1021 .expect("normalize failed");
1022 assert!((result[0] - 5.0).abs() < 1e-12);
1026 assert!((result[1] - 4.0).abs() < 1e-12);
1027 assert!((result[2] - 3.0).abs() < 1e-12);
1028 }
1029
1030 #[test]
1033 fn ptx_generation_smoke_test() {
1034 let config = GnnSparseConfig {
1035 num_nodes: 1024,
1036 feature_dim: 64,
1037 num_edges: 8192,
1038 op: MessagePassingOp::Sum,
1039 normalize: true,
1040 };
1041 let ptx = generate_message_passing_ptx(&config).expect("PTX generation failed");
1042 assert!(ptx.contains("gnn_message_passing_f64"));
1043 assert!(ptx.contains(".version 7.0"));
1044 assert!(ptx.contains(".target sm_70"));
1045 assert!(ptx.contains("num_nodes"));
1046 }
1047
1048 #[test]
1049 fn ptx_generation_all_ops() {
1050 for op in &[
1051 MessagePassingOp::Sum,
1052 MessagePassingOp::Mean,
1053 MessagePassingOp::Max,
1054 MessagePassingOp::Min,
1055 ] {
1056 let config = GnnSparseConfig {
1057 num_nodes: 256,
1058 feature_dim: 32,
1059 num_edges: 1024,
1060 op: *op,
1061 normalize: false,
1062 };
1063 let ptx = generate_message_passing_ptx(&config).expect("PTX generation failed");
1064 assert!(!ptx.is_empty());
1065 }
1066 }
1067}