From c1fc10a32a434e0ec0cf24d8e3cbafa9b1266ce1 Mon Sep 17 00:00:00 2001 From: Harsha Vardhan Date: Mon, 31 Aug 2026 20:29:14 +0530 Subject: [PATCH] feat(rust): establish Rust graph computing modernization framework (#355) --- .github/workflows/rust-ci.yml | 78 +++++++ .licenserc.yaml | 1 + computer-rust/Cargo.toml | 43 ++++ computer-rust/benches/kernel_bench.rs | 31 +++ computer-rust/include/computer_rust_c_api.h | 83 ++++++++ computer-rust/src/ffi/c_api.rs | 217 ++++++++++++++++++++ computer-rust/src/ffi/mod.rs | 18 ++ computer-rust/src/fixtures/dataset.rs | 109 ++++++++++ computer-rust/src/fixtures/mod.rs | 19 ++ computer-rust/src/fixtures/tolerance.rs | 100 +++++++++ computer-rust/src/kernel/csr.rs | 138 +++++++++++++ computer-rust/src/kernel/mod.rs | 19 ++ computer-rust/src/kernel/pagerank.rs | 135 ++++++++++++ computer-rust/src/lib.rs | 25 +++ docs/rust-modernization-roadmap.md | 108 ++++++++++ 15 files changed, 1124 insertions(+) create mode 100644 .github/workflows/rust-ci.yml create mode 100644 computer-rust/Cargo.toml create mode 100644 computer-rust/benches/kernel_bench.rs create mode 100644 computer-rust/include/computer_rust_c_api.h create mode 100644 computer-rust/src/ffi/c_api.rs create mode 100644 computer-rust/src/ffi/mod.rs create mode 100644 computer-rust/src/fixtures/dataset.rs create mode 100644 computer-rust/src/fixtures/mod.rs create mode 100644 computer-rust/src/fixtures/tolerance.rs create mode 100644 computer-rust/src/kernel/csr.rs create mode 100644 computer-rust/src/kernel/mod.rs create mode 100644 computer-rust/src/kernel/pagerank.rs create mode 100644 computer-rust/src/lib.rs create mode 100644 docs/rust-modernization-roadmap.md diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 000000000..7921608e0 --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,78 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +name: "Rust CI" + +on: + push: + branches: + - master + - 'release-*' + paths: + - computer-rust/** + - .github/workflows/rust-ci.yml + pull_request: + paths: + - computer-rust/** + - .github/workflows/rust-ci.yml + +defaults: + run: + working-directory: computer-rust + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust-check: + name: Rust Code Quality & Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache Cargo dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + computer-rust/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('computer-rust/Cargo.toml') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Check code formatting + run: cargo fmt --check + + - name: Run clippy lints + run: cargo clippy --all-targets -- -D warnings + + - name: Run tests + run: cargo test --all-targets --verbose + + - name: Build release library + run: cargo build --release + + - name: Build benchmarks + run: cargo bench --no-run diff --git a/.licenserc.yaml b/.licenserc.yaml index 958c135f0..c4864e89c 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -73,6 +73,7 @@ header: # `header` section is configurations for source codes license header. - '**/target/*' - '**/go.mod' - '**/go.sum' + - '**/Cargo.lock' comment: on-failure # on what condition license-eye will comment on the pull request, `on-failure`, `always`, `never`. # license-location-threshold specifies the index threshold where the license header can be located, diff --git a/computer-rust/Cargo.toml b/computer-rust/Cargo.toml new file mode 100644 index 000000000..a5d156e2b --- /dev/null +++ b/computer-rust/Cargo.toml @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "hugegraph-computer-rust" +version = "1.5.0" +edition = "2021" +authors = ["Apache HugeGraph Authors "] +license = "Apache-2.0" +description = "High-performance Rust graph computing kernels for HugeGraph Computer and Vermeer" +repository = "https://github.com/apache/hugegraph-computer" + +[lib] +name = "hugegraph_computer_rust" +crate-type = ["cdylib", "staticlib", "rlib"] + +[dependencies] +libc = "0.2" + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "kernel_bench" +harness = false + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" diff --git a/computer-rust/benches/kernel_bench.rs b/computer-rust/benches/kernel_bench.rs new file mode 100644 index 000000000..855cb2175 --- /dev/null +++ b/computer-rust/benches/kernel_bench.rs @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use criterion::{criterion_group, criterion_main, Criterion}; +use hugegraph_computer_rust::fixtures::dataset::GraphFixture; +use hugegraph_computer_rust::kernel::pagerank::PageRankKernel; + +fn bench_pagerank(c: &mut Criterion) { + let fixture = GraphFixture::synthetic_powerlaw(1000, 10); + let csr = fixture.to_csr(); + let kernel = PageRankKernel::new(0.85, 20, 1e-4); + + c.bench_function("pagerank_1k_vertices", |b| b.iter(|| kernel.compute(&csr))); +} + +criterion_group!(benches, bench_pagerank); +criterion_main!(benches); diff --git a/computer-rust/include/computer_rust_c_api.h b/computer-rust/include/computer_rust_c_api.h new file mode 100644 index 000000000..d0596c582 --- /dev/null +++ b/computer-rust/include/computer_rust_c_api.h @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HUGEGRAPH_COMPUTER_RUST_C_API_H +#define HUGEGRAPH_COMPUTER_RUST_C_API_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct GraphHandle GraphHandle; + +/** + * Creates a new GraphHandle instance with the specified number of vertices. + */ +GraphHandle* computer_graph_create(uint32_t num_vertices); + +/** + * Adds a directed edge from src to dst with a weight. + * @return 0 on success. + * @return -1 if handle is NULL, endpoints src/dst are >= num_vertices, or weight < 0.0 or non-finite. + * @return -2 if graph has already been finalized. + */ +int32_t computer_graph_add_edge(GraphHandle* handle, uint32_t src, uint32_t dst, double weight); + +/** + * Finalizes graph topology into Compressed Sparse Row (CSR) structure. + * @return 0 on success, -1 if handle is NULL. + */ +int32_t computer_graph_finalize(GraphHandle* handle); + +/** + * Computes PageRank on the CSR graph structure. + * Results array must be allocated by caller with capacity >= num_vertices. + * @return 0 on success. + * @return -1 if handle or out_scores is NULL. + * @return -2 if graph is not finalized (CSR missing). + * @return -3 if out_capacity < num_vertices. + * @return -4 if damping_factor or tolerance is invalid (non-finite, negative, or damping > 1.0). + */ +int32_t computer_graph_compute_pagerank( + const GraphHandle* handle, + double damping_factor, + uint32_t max_iterations, + double tolerance, + double* out_scores, + uint32_t out_capacity +); + +/** + * Frees the GraphHandle resources. + */ +void computer_graph_free(GraphHandle* handle); + +/** + * Returns the version string of the Rust kernel library. + * Pointer references process-lifetime static storage and remains valid across threads. + */ +const char* computer_kernel_version(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HUGEGRAPH_COMPUTER_RUST_C_API_H */ diff --git a/computer-rust/src/ffi/c_api.rs b/computer-rust/src/ffi/c_api.rs new file mode 100644 index 000000000..b7166523a --- /dev/null +++ b/computer-rust/src/ffi/c_api.rs @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use crate::kernel::csr::CsrGraph; +use crate::kernel::pagerank::PageRankKernel; +use crate::RUST_KERNEL_VERSION; +use std::ffi::CString; +use std::os::raw::c_char; +use std::slice; +use std::sync::OnceLock; + +pub struct GraphBuilder { + num_vertices: u32, + edges: Vec<(u32, u32, f64)>, + csr: Option, +} + +#[no_mangle] +pub extern "C" fn computer_graph_create(num_vertices: u32) -> *mut GraphBuilder { + let builder = Box::new(GraphBuilder { + num_vertices, + edges: Vec::new(), + csr: None, + }); + Box::into_raw(builder) +} + +#[no_mangle] +pub extern "C" fn computer_graph_add_edge( + handle: *mut GraphBuilder, + src: u32, + dst: u32, + weight: f64, +) -> i32 { + if handle.is_null() { + return -1; + } + let builder = unsafe { &mut *handle }; + if builder.csr.is_some() { + return -2; + } + if src >= builder.num_vertices || dst >= builder.num_vertices { + return -1; + } + if weight < 0.0 || !weight.is_finite() { + return -1; + } + builder.edges.push((src, dst, weight)); + 0 +} + +#[no_mangle] +pub extern "C" fn computer_graph_finalize(handle: *mut GraphBuilder) -> i32 { + if handle.is_null() { + return -1; + } + let builder = unsafe { &mut *handle }; + let csr = CsrGraph::from_edges(builder.num_vertices, &builder.edges); + builder.csr = Some(csr); + 0 +} + +#[no_mangle] +pub extern "C" fn computer_graph_compute_pagerank( + handle: *const GraphBuilder, + damping_factor: f64, + max_iterations: u32, + tolerance: f64, + out_scores: *mut f64, + out_capacity: u32, +) -> i32 { + if handle.is_null() || out_scores.is_null() { + return -1; + } + let builder = unsafe { &*handle }; + let csr = match &builder.csr { + Some(c) => c, + None => return -2, + }; + + if out_capacity < csr.num_vertices() { + return -3; + } + + if !damping_factor.is_finite() || damping_factor < 0.0 || damping_factor > 1.0 { + return -4; + } + if !tolerance.is_finite() || tolerance < 0.0 { + return -4; + } + + let kernel = PageRankKernel::new(damping_factor, max_iterations, tolerance); + let ranks = kernel.compute(csr); + + let dest_slice = unsafe { slice::from_raw_parts_mut(out_scores, ranks.len()) }; + dest_slice.copy_from_slice(&ranks); + 0 +} + +#[no_mangle] +pub extern "C" fn computer_graph_free(handle: *mut GraphBuilder) { + if !handle.is_null() { + unsafe { + let _ = Box::from_raw(handle); + } + } +} + +static VERSION_C_STR: OnceLock = OnceLock::new(); + +#[no_mangle] +pub extern "C" fn computer_kernel_version() -> *const c_char { + VERSION_C_STR + .get_or_init(|| CString::new(RUST_KERNEL_VERSION).unwrap()) + .as_ptr() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_c_api_flow() { + let handle = computer_graph_create(4); + assert!(!handle.is_null()); + + assert_eq!(computer_graph_add_edge(handle, 0, 1, 1.0), 0); + assert_eq!(computer_graph_add_edge(handle, 1, 2, 1.0), 0); + + assert_eq!(computer_graph_finalize(handle), 0); + + let mut scores = vec![0.0; 4]; + assert_eq!( + computer_graph_compute_pagerank(handle, 0.85, 50, 1e-6, scores.as_mut_ptr(), 4), + 0 + ); + + let sum: f64 = scores.iter().sum(); + assert!((sum - 1.0).abs() < 1e-4); + assert!(scores[0] > 0.0 && scores[1] > 0.0 && scores[2] > 0.0); + + computer_graph_free(handle); + + let ver_ptr = computer_kernel_version(); + assert!(!ver_ptr.is_null()); + } + + #[test] + fn test_c_api_edge_validation_and_finalization() { + let handle = computer_graph_create(2); + assert!(!handle.is_null()); + + assert_eq!(computer_graph_add_edge(handle, 99, 1, 1.0), -1); + assert_eq!(computer_graph_add_edge(handle, 0, 99, 1.0), -1); + + assert_eq!(computer_graph_add_edge(handle, 0, 1, -1.0), -1); + assert_eq!(computer_graph_add_edge(handle, 0, 1, f64::NAN), -1); + + assert_eq!(computer_graph_add_edge(handle, 0, 1, 1.0), 0); + assert_eq!(computer_graph_finalize(handle), 0); + + assert_eq!(computer_graph_add_edge(handle, 0, 1, 1.0), -2); + + computer_graph_free(handle); + } + + #[test] + fn test_c_api_parameter_validation() { + let handle = computer_graph_create(2); + assert_eq!(computer_graph_add_edge(handle, 0, 1, 1.0), 0); + assert_eq!(computer_graph_finalize(handle), 0); + + let mut scores = vec![0.0; 2]; + assert_eq!( + computer_graph_compute_pagerank(handle, 1.5, 50, 1e-6, scores.as_mut_ptr(), 2), + -4 + ); + assert_eq!( + computer_graph_compute_pagerank(handle, f64::NAN, 50, 1e-6, scores.as_mut_ptr(), 2), + -4 + ); + assert_eq!( + computer_graph_compute_pagerank(handle, 0.85, 50, -1.0, scores.as_mut_ptr(), 2), + -4 + ); + + computer_graph_free(handle); + } + + #[test] + fn test_c_api_version_static_lifetime() { + let ver_ptr1 = computer_kernel_version(); + let handle = std::thread::spawn(|| computer_kernel_version() as usize); + let ver_ptr2 = handle.join().unwrap() as *const c_char; + assert_eq!(ver_ptr1, ver_ptr2); + let ver_str = unsafe { std::ffi::CStr::from_ptr(ver_ptr1) } + .to_str() + .unwrap(); + assert_eq!(ver_str, RUST_KERNEL_VERSION); + } +} diff --git a/computer-rust/src/ffi/mod.rs b/computer-rust/src/ffi/mod.rs new file mode 100644 index 000000000..835f004f9 --- /dev/null +++ b/computer-rust/src/ffi/mod.rs @@ -0,0 +1,18 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod c_api; diff --git a/computer-rust/src/fixtures/dataset.rs b/computer-rust/src/fixtures/dataset.rs new file mode 100644 index 000000000..0afa81805 --- /dev/null +++ b/computer-rust/src/fixtures/dataset.rs @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::kernel::csr::CsrGraph; + +pub struct GraphFixture { + pub name: String, + pub num_vertices: u32, + pub edges: Vec<(u32, u32, f64)>, +} + +impl GraphFixture { + /// Returns the Zachary's Karate Club representative graph dataset fixture. + pub fn karate_club() -> Self { + let edges = vec![ + (0, 1, 1.0), + (0, 2, 1.0), + (0, 3, 1.0), + (0, 4, 1.0), + (0, 5, 1.0), + (0, 6, 1.0), + (0, 7, 1.0), + (0, 8, 1.0), + (0, 10, 1.0), + (0, 11, 1.0), + (0, 12, 1.0), + (0, 13, 1.0), + (0, 17, 1.0), + (0, 19, 1.0), + (0, 21, 1.0), + (0, 31, 1.0), + (1, 2, 1.0), + (1, 3, 1.0), + (1, 7, 1.0), + (1, 13, 1.0), + (1, 17, 1.0), + (1, 19, 1.0), + (1, 21, 1.0), + (1, 30, 1.0), + (2, 3, 1.0), + (2, 7, 1.0), + (2, 8, 1.0), + (2, 9, 1.0), + (2, 13, 1.0), + (2, 27, 1.0), + (2, 28, 1.0), + (2, 32, 1.0), + (3, 7, 1.0), + (3, 12, 1.0), + (3, 13, 1.0), + ]; + Self { + name: "karate_club".to_string(), + num_vertices: 34, + edges, + } + } + + /// Generates a synthetic power-law graph dataset fixture for baseline testing. + pub fn synthetic_powerlaw(num_vertices: u32, avg_degree: u32) -> Self { + let mut edges = Vec::new(); + for src in 0..num_vertices { + let out_degree = avg_degree + (src % 5); + for i in 0..out_degree { + let dst = (src + i * 7 + 1) % num_vertices; + if src != dst { + edges.push((src, dst, 1.0)); + } + } + } + Self { + name: format!("synthetic_powerlaw_v{}", num_vertices), + num_vertices, + edges, + } + } + + pub fn to_csr(&self) -> CsrGraph { + CsrGraph::from_edges(self.num_vertices, &self.edges) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_karate_club_fixture() { + let fixture = GraphFixture::karate_club(); + assert_eq!(fixture.num_vertices, 34); + assert!(!fixture.edges.is_empty()); + let csr = fixture.to_csr(); + assert_eq!(csr.num_vertices(), 34); + } +} diff --git a/computer-rust/src/fixtures/mod.rs b/computer-rust/src/fixtures/mod.rs new file mode 100644 index 000000000..9c3340c91 --- /dev/null +++ b/computer-rust/src/fixtures/mod.rs @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod dataset; +pub mod tolerance; diff --git a/computer-rust/src/fixtures/tolerance.rs b/computer-rust/src/fixtures/tolerance.rs new file mode 100644 index 000000000..caa41b925 --- /dev/null +++ b/computer-rust/src/fixtures/tolerance.rs @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub struct DifferentialTolerance; + +impl DifferentialTolerance { + pub fn l1_distance(actual: &[f64], expected: &[f64]) -> Result { + if actual.len() != expected.len() { + return Err(format!( + "Vector length mismatch: actual len {}, expected len {}", + actual.len(), + expected.len() + )); + } + + let mut l1 = 0.0; + for i in 0..actual.len() { + if !actual[i].is_finite() || !expected[i].is_finite() { + return Err(format!( + "Non-finite value detected at index {}: actual = {}, expected = {}", + i, actual[i], expected[i] + )); + } + l1 += (actual[i] - expected[i]).abs(); + } + + if !l1.is_finite() { + return Err("Calculated L1 distance is non-finite".to_string()); + } + + Ok(l1) + } + + pub fn assert_parity(actual: &[f64], expected: &[f64], epsilon: f64) -> Result<(), String> { + if actual.len() != expected.len() { + return Err(format!( + "Vector length mismatch: actual len {}, expected len {}", + actual.len(), + expected.len() + )); + } + + for i in 0..actual.len() { + if !actual[i].is_finite() || !expected[i].is_finite() { + return Err(format!( + "Non-finite value detected at index {}: actual = {}, expected = {}", + i, actual[i], expected[i] + )); + } + let diff = (actual[i] - expected[i]).abs(); + if !diff.is_finite() || diff > epsilon { + return Err(format!( + "Parity failed at index {}: actual = {}, expected = {}, diff = {} > epsilon {}", + i, actual[i], expected[i], diff, epsilon + )); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_differential_tolerance() { + let actual = vec![0.25, 0.50, 0.25]; + let expected = vec![0.250001, 0.499999, 0.25]; + + let l1 = DifferentialTolerance::l1_distance(&actual, &expected).unwrap(); + assert!(l1 < 1e-4); + + assert!(DifferentialTolerance::assert_parity(&actual, &expected, 1e-4).is_ok()); + assert!(DifferentialTolerance::assert_parity(&actual, &expected, 1e-8).is_err()); + } + + #[test] + fn test_nan_infinity_rejection() { + assert!(DifferentialTolerance::assert_parity(&[f64::NAN], &[0.0], 1e-4).is_err()); + assert!(DifferentialTolerance::assert_parity(&[0.0], &[f64::INFINITY], 1e-4).is_err()); + assert!(DifferentialTolerance::l1_distance(&[f64::NAN], &[0.0]).is_err()); + assert!(DifferentialTolerance::l1_distance(&[0.0], &[f64::INFINITY]).is_err()); + } +} diff --git a/computer-rust/src/kernel/csr.rs b/computer-rust/src/kernel/csr.rs new file mode 100644 index 000000000..db8ee1fa8 --- /dev/null +++ b/computer-rust/src/kernel/csr.rs @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#[derive(Debug, Clone, Default)] +pub struct Edge { + pub target: u32, + pub weight: f64, +} + +#[derive(Debug, Clone)] +pub struct CsrGraph { + num_vertices: u32, + row_offsets: Vec, + column_indices: Vec, + edge_weights: Vec, +} + +impl CsrGraph { + pub fn new(num_vertices: u32) -> Self { + Self { + num_vertices, + row_offsets: vec![0; (num_vertices + 1) as usize], + column_indices: Vec::new(), + edge_weights: Vec::new(), + } + } + + pub fn from_edges(num_vertices: u32, edges: &[(u32, u32, f64)]) -> Self { + let mut degree = vec![0; num_vertices as usize]; + for &(src, dst, _weight) in edges { + if src < num_vertices && dst < num_vertices { + degree[src as usize] += 1; + } + } + + let mut row_offsets = vec![0; (num_vertices + 1) as usize]; + for i in 0..num_vertices as usize { + row_offsets[i + 1] = row_offsets[i] + degree[i]; + } + + let total_edges = row_offsets[num_vertices as usize]; + let mut column_indices = vec![0; total_edges]; + let mut edge_weights = vec![0.0; total_edges]; + let mut current_pos = row_offsets.clone(); + + for &(src, dst, weight) in edges { + if src < num_vertices && dst < num_vertices { + let pos = current_pos[src as usize]; + column_indices[pos] = dst; + edge_weights[pos] = weight; + current_pos[src as usize] += 1; + } + } + + Self { + num_vertices, + row_offsets, + column_indices, + edge_weights, + } + } + + pub fn num_vertices(&self) -> u32 { + self.num_vertices + } + + pub fn num_edges(&self) -> usize { + self.column_indices.len() + } + + pub fn out_degree(&self, vertex: u32) -> usize { + if vertex >= self.num_vertices { + return 0; + } + let v = vertex as usize; + self.row_offsets[v + 1] - self.row_offsets[v] + } + + pub fn out_edges(&self, vertex: u32) -> (&[u32], &[f64]) { + if vertex >= self.num_vertices { + return (&[], &[]); + } + let v = vertex as usize; + let start = self.row_offsets[v]; + let end = self.row_offsets[v + 1]; + ( + &self.column_indices[start..end], + &self.edge_weights[start..end], + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_csr_graph_creation() { + let edges = vec![(0, 1, 1.0), (0, 2, 2.0), (1, 2, 0.5)]; + let graph = CsrGraph::from_edges(3, &edges); + + assert_eq!(graph.num_vertices(), 3); + assert_eq!(graph.num_edges(), 3); + assert_eq!(graph.out_degree(0), 2); + assert_eq!(graph.out_degree(1), 1); + assert_eq!(graph.out_degree(2), 0); + + let (neighbors, weights) = graph.out_edges(0); + assert_eq!(neighbors, &[1, 2]); + assert_eq!(weights, &[1.0, 2.0]); + } + + #[test] + fn test_csr_invalid_endpoints() { + let edges = vec![(0, 99, 1.0), (0, 1, 2.0)]; + let graph = CsrGraph::from_edges(2, &edges); + assert_eq!(graph.num_vertices(), 2); + assert_eq!(graph.num_edges(), 1); + assert_eq!(graph.out_degree(0), 1); + let (neighbors, weights) = graph.out_edges(0); + assert_eq!(neighbors, &[1]); + assert_eq!(weights, &[2.0]); + } +} diff --git a/computer-rust/src/kernel/mod.rs b/computer-rust/src/kernel/mod.rs new file mode 100644 index 000000000..1d507cb91 --- /dev/null +++ b/computer-rust/src/kernel/mod.rs @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod csr; +pub mod pagerank; diff --git a/computer-rust/src/kernel/pagerank.rs b/computer-rust/src/kernel/pagerank.rs new file mode 100644 index 000000000..4085d3bc6 --- /dev/null +++ b/computer-rust/src/kernel/pagerank.rs @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::kernel::csr::CsrGraph; + +#[derive(Debug, Clone)] +pub struct PageRankKernel { + damping_factor: f64, + max_iterations: u32, + tolerance: f64, +} + +impl PageRankKernel { + pub fn try_new( + damping_factor: f64, + max_iterations: u32, + tolerance: f64, + ) -> Result { + if !damping_factor.is_finite() || damping_factor < 0.0 || damping_factor > 1.0 { + return Err(format!( + "Invalid damping_factor: {}. Must be finite and in range [0.0, 1.0]", + damping_factor + )); + } + if !tolerance.is_finite() || tolerance < 0.0 { + return Err(format!( + "Invalid tolerance: {}. Must be finite non-negative number", + tolerance + )); + } + Ok(Self { + damping_factor, + max_iterations, + tolerance, + }) + } + + pub fn new(damping_factor: f64, max_iterations: u32, tolerance: f64) -> Self { + Self::try_new(damping_factor, max_iterations, tolerance) + .expect("Failed to initialize PageRankKernel due to invalid parameters") + } + + #[allow(clippy::needless_range_loop)] + pub fn compute(&self, graph: &CsrGraph) -> Vec { + let num_vertices = graph.num_vertices() as usize; + if num_vertices == 0 { + return Vec::new(); + } + + let initial_rank = 1.0 / (num_vertices as f64); + let mut ranks = vec![initial_rank; num_vertices]; + let mut next_ranks = vec![0.0; num_vertices]; + + let teleport = (1.0 - self.damping_factor) / (num_vertices as f64); + + for _iter in 0..self.max_iterations { + next_ranks.fill(0.0); + let mut dangling_sum = 0.0; + + for v in 0..num_vertices { + let out_degree = graph.out_degree(v as u32); + if out_degree == 0 { + dangling_sum += ranks[v]; + } else { + let share = ranks[v] / (out_degree as f64); + let (neighbors, _) = graph.out_edges(v as u32); + for &target in neighbors { + next_ranks[target as usize] += share; + } + } + } + + let dangling_share = self.damping_factor * (dangling_sum / (num_vertices as f64)); + let mut max_diff = 0.0f64; + + for v in 0..num_vertices { + let new_rank = teleport + dangling_share + self.damping_factor * next_ranks[v]; + let diff = (new_rank - ranks[v]).abs(); + if diff > max_diff { + max_diff = diff; + } + ranks[v] = new_rank; + } + + if max_diff < self.tolerance { + break; + } + } + + ranks + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pagerank_computation() { + let edges = vec![(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0)]; + let graph = CsrGraph::from_edges(3, &edges); + let pr = PageRankKernel::new(0.85, 100, 1e-6); + let ranks = pr.compute(&graph); + + assert_eq!(ranks.len(), 3); + let sum: f64 = ranks.iter().sum(); + assert!((sum - 1.0).abs() < 1e-4); + assert!((ranks[0] - ranks[1]).abs() < 1e-4); + assert!((ranks[1] - ranks[2]).abs() < 1e-4); + } + + #[test] + fn test_pagerank_parameter_validation() { + assert!(PageRankKernel::try_new(1.5, 100, 1e-6).is_err()); + assert!(PageRankKernel::try_new(-0.1, 100, 1e-6).is_err()); + assert!(PageRankKernel::try_new(f64::NAN, 100, 1e-6).is_err()); + assert!(PageRankKernel::try_new(0.85, 100, -1e-6).is_err()); + assert!(PageRankKernel::try_new(0.85, 100, f64::NAN).is_err()); + assert!(PageRankKernel::try_new(0.85, 100, 1e-6).is_ok()); + } +} diff --git a/computer-rust/src/lib.rs b/computer-rust/src/lib.rs new file mode 100644 index 000000000..dfeaddc71 --- /dev/null +++ b/computer-rust/src/lib.rs @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod ffi; +pub mod fixtures; +pub mod kernel; + +pub use kernel::csr::CsrGraph; +pub use kernel::pagerank::PageRankKernel; + +pub const RUST_KERNEL_VERSION: &str = "1.5.0"; diff --git a/docs/rust-modernization-roadmap.md b/docs/rust-modernization-roadmap.md new file mode 100644 index 000000000..6f355b076 --- /dev/null +++ b/docs/rust-modernization-roadmap.md @@ -0,0 +1,108 @@ + + +# HugeGraph Computer & Vermeer: Rust Modernization Roadmap (#355) + +## Overview + +This roadmap details the incremental modernization strategy for graph computing components in **HugeGraph Computer** and **Vermeer**. The initiative focuses on high-performance kernels, data movement, memory efficiency, and operational simplicity where Rust provides a measurable advantage over Java (JVM GC overhead) and Go. + +> **Note:** This initiative is an incremental enhancement—not a wholesale replacement of existing systems. Existing Java/Go algorithms, data formats, and deployment paths remain the compatibility and baseline benchmark. + +--- + +## Architectural Principles & Guardrails + +1. **Zero Downtime / Seamless Coexistence:** Java and Go baselines are preserved with automatic fallback if native Rust modules are unavailable. +2. **Result Parity & Tolerance:** Differential correctness testing enforces $L_1$-distance $\le 10^{-6}$ against ground-truth algorithm outputs. +3. **Bounded Leaf Modules:** Incremental modernization targets encapsulated primitives (CSR memory layout, PageRank kernel, differential checkers) in Phase 1 before expanding to secondary algorithms or host engine bindings. +4. **Stable Interoperability Layer:** Exported via standard C-ABI (`computer_rust_c_api.h`) for future host bindings (JNI in Java `computer-core` and CGO/gRPC in `vermeer`). + +--- + +## Component Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User / Applications │ +└──────────────────────────────┬──────────────────────────────┘ + │ + ┌──────────────────┴──────────────────┐ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ HugeGraph Computer │ │ Vermeer │ +│ (Java / BSP Pregel) │ │ (Go / In-Memory Engine) │ +└───────────┬─────────────┘ └───────────┬─────────────┘ + │ JNI / FFI (Phase 3) │ CGO / FFI (Phase 3) + └──────────────────┬──────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ computer-rust (C-ABI Layer) │ +│ ┌──────────────────┬──────────────────┬─────────────────┐ │ +│ │ CSR Graph Layout │ PageRank Kernel │ Dataset Fixtures│ │ +│ ├──────────────────┼──────────────────┼─────────────────┤ │ +│ │ C-ABI Exports │ Differential PR │ Criterion Bench │ │ +│ └──────────────────┴──────────────────┴─────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## C-ABI Interoperability & Return Code Contracts + +The exported C-ABI layer (`computer_rust_c_api.h`) defines standard return codes and lifetime guarantees across language boundaries: + +### Error Codes +* `0`: Success. +* `-1`: Null handle or invalid boundary parameters (out-of-bounds `src`/`dst` endpoints, negative or non-finite edge weights). +* `-2`: State error (attempting to add edges to a finalized graph, or missing CSR during computation). +* `-3`: Insufficient output buffer capacity (`out_capacity < num_vertices`). +* `-4`: Invalid algorithm parameters (non-finite or out-of-bounds `damping_factor`, or negative `tolerance`). + +### Lifetime & Thread Safety +* `computer_kernel_version()` returns a `*const c_char` pointing to process-lifetime static storage (`OnceLock`). The pointer is thread-safe and remains valid throughout application lifetime. + +### Fallback Adapters (Planned for Phase 3) +* Host integration bridges in `computer-core` (Java) and `vermeer` (Go) will provide transparent fallback execution paths. When native libraries are absent or opt-in preview flags are disabled, graph algorithms fall back transparently to pure Java/Go execution while enforcing identical parameter validation. + +--- + +## Newcomer-Friendly Child Task Breakdown + +The following tasks are split into isolated, newcomer-friendly issues for community contributors: + +| Task ID | Component | Title | Description | Target Skills | +|---------|-----------|-------|-------------|---------------| +| `#355-1` | `computer-rust` | SSSP, WCC & LPA Kernels | Port Shortest Path (SSSP), Weakly Connected Components (WCC), and Label Propagation (LPA) to CSR Rust kernel. | Rust, Graph Algorithms | +| `#355-2` | `computer-core` | Java JNI Bridge & Dynamic Loader | Package platform-specific native libraries (`.so`, `.dylib`, `.dll`) into JAR artifacts with automated JNI extraction and fallback. | Java, JNI, Build Automation | +| `#355-3` | `vermeer` | Go CGO Bridge & Benchmark | Connect Vermeer Go compute engine to Rust C-ABI via CGO with performance benchmarking against Go baseline. | Go, CGO, Benchmarking | +| `#355-4` | `computer-rust` | Parquet / Arrow Memory Mapped I/O | Add zero-copy memory-mapped file reader for CSR graph initialization. | Rust, Memory Mapping | + +--- + +## Verification & Parity Guidelines + +To verify algorithm outputs against ground-truth baselines: + +```bash +# Run Rust kernel tests and differential parity checks +cd computer-rust +cargo test --all-targets + +# Run Criterion benchmark harness +cargo bench +```