Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bbadb46
feat(hg_analytics): rayon-parallel PageRank + betweenness (the scale …
mdheller Jul 9, 2026
3682213
style: cargo fmt (rustfmt CI)
mdheller Jul 9, 2026
751e8b2
feat(hg_analytics): distributed (BSP) PageRank over sharded partition…
mdheller Jul 9, 2026
45c819a
feat(hg_analytics): out-of-core mmap CSR PageRank — kill the RAM ceiling
mdheller Jul 9, 2026
d510140
feat(hg_analytics): streaming O(n)-heap CSR builder — ingest graphs l…
mdheller Jul 9, 2026
3d39ea5
bench(ooc): 100M-edge out-of-core PageRank — RAM-ceiling break at scale
mdheller Jul 9, 2026
e6c23c3
feat(ooc): bucketed external-memory CSR builder — fully-sequential-I/…
mdheller Jul 9, 2026
516d67a
feat(dist): REAL multi-process distributed PageRank over TCP sockets
mdheller Jul 9, 2026
d8dc04d
feat(cc): distributed connected components — the partition-native mod…
mdheller Jul 9, 2026
df4606f
bench(warm): incremental warm-start PageRank — honest number
mdheller Jul 9, 2026
cc03130
bench(ooc): billion-scale runner — honest single-box ceiling is ~500M…
mdheller Jul 9, 2026
2aeb2df
hg_analytics: Graph500 Kronecker (RMAT) generator for scale benchmarks
mdheller Jul 10, 2026
4b455ef
hg_analytics: boundary-only halo (ghost vertices) distributed PageRank
mdheller Jul 10, 2026
e994782
hg_analytics: halo_bench example — measures boundary halo vs full bro…
mdheller Jul 10, 2026
7423614
hg_analytics: streaming edge-cut partitioner (Fennel + LDG)
mdheller Jul 10, 2026
9c9968b
hg_analytics: dress_rehearsal — end-to-end scaling curve + cluster si…
mdheller Jul 10, 2026
16e0520
hg_analytics: boundary-halo connected components
mdheller Jul 10, 2026
955eb15
hg_analytics: dist_boundary — real multi-process boundary-halo runtime
mdheller Jul 10, 2026
321818e
deploy/bench: containerize boundary-halo runtime + GKE manifests
mdheller Jul 10, 2026
df82bf2
hg_analytics: vs_baseline head-to-head against networkx + scipy
mdheller Jul 10, 2026
b1295e8
hg_analytics: dist_p2p — pure peer-to-peer boundary halo (no coordina…
mdheller Jul 10, 2026
e556c16
hg_analytics: distributed BFS (boundary-halo traversal)
mdheller Jul 10, 2026
abbf6d5
hg_analytics: ldbc_suite — self-verifying distributed benchmark score…
mdheller Jul 10, 2026
e69b1ac
deploy/bench: one-command Saturday runbook + Cloud Build (no local do…
mdheller Jul 10, 2026
a69c863
hg_analytics: BENCHMARKS.md — reproducible results doc (the publishab…
mdheller Jul 10, 2026
031fee7
hg_analytics: weighted SSSP (4th LDBC kernel), boundary-halo distributed
mdheller Jul 10, 2026
3516766
hg_analytics: CDLP community detection (5th LDBC kernel), boundary-ha…
mdheller Jul 10, 2026
c07dacb
hg_analytics: LCC (6th LDBC kernel) — full LDBC Graphalytics suite co…
mdheller Jul 10, 2026
969348b
hg_analytics: head-to-head vs a REAL graph database (KuzuDB)
mdheller Jul 10, 2026
765fb65
hg_analytics: union-find CC + extend graph-DB head-to-head to WCC
mdheller Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

147 changes: 147 additions & 0 deletions crates/hg_analytics/BENCHMARKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# hg_analytics — distributed graph analytics benchmarks

Every number here is reproducible from this crate on a commodity machine (measured on an 8-core laptop,
`--release`). Determinism is a hard invariant: every distributed result is checked **bit-for-bit** against
the single-graph reference, so "distributed" never means "approximate". The synthetic graph is Graph500
Kronecker/RMAT (`Kronecker`), so there is nothing to download.

## The thesis, in one line

Rust + a boundary-only halo + an edge-cut partition beats the usual distributed-BSP graph engine on the
axis that actually caps scale — **the bytes that cross the network each superstep** — while staying exact,
and it is competitive-to-faster than optimized single-node sparse linear algebra.

## 1. Single-node vs off-the-shelf (same graph, same machine, matched damping/tol)

```
cargo run -p hg_analytics --release --example vs_baseline # emits the shared graph + our result
python3 scripts/bench/vs_baseline.py # runs networkx + scipy on it
```

| graph (RMAT ef=16) | hg_analytics | scipy sparse (C/BLAS) | networkx (pure Py) | agreement |
|--------------------|-------------:|----------------------:|-------------------:|:---------:|
| scale 15, 524K edges | **9.4 ms** | 32 ms (3.4× slower) | 271 ms (29× slower) | top-100 100% |
| scale 17, 2.1M edges | **44 ms** | 122 ms (2.8× slower) | 1.28 s (29× slower) | top-100 100% |

PageRank is memory-bound, so parallel-vs-serial is ~1.6× (honest) and scipy's spmv is also well-optimized —
~3× is a real single-node win, not a strawman, and the ranking is identical.

### vs a real graph database (KuzuDB, native PageRank)

```
pip install kuzu
HG_SCALE=18 cargo run -p hg_analytics --release --example vs_baseline
HG_OUT=/tmp/hg_vs18 python3 scripts/bench/vs_kuzu.py
```

Not a linear-algebra library — an actual embedded graph DB (Cypher, native `page_rank` +
`weakly_connected_components`), same graph, same machine, compute-to-compute (scale-18, 262K nodes / 4.2M
edges):

| kernel | hg_analytics | KuzuDB | speedup | agreement |
|--------|-------------:|-------:|:-------:|:---------:|
| PageRank | 112 ms | 1134 ms | **10.1×** | top-100 100% |
| WCC | 13 ms | 98 ms | **7.6×** | same component count (88 104) |

Faster on both, same result — and Kuzu additionally pays a ~0.9 s bulk-load step we don't (in-memory).
Caveats: Kuzu is single-machine embedded (not distributed); PageRank damping/iterations may differ, but the
100% top-100 agreement shows both reach the same ranking.

> This head-to-head paid for itself: the first run had our WCC at 117 ms (**losing** to Kuzu) because
> `connected_components` is label-propagation, chosen for the distributed BSP path. Adding the right
> single-machine algorithm — `connected_components_uf` (union-find, path halving + union by size) — cut it
> to 13 ms and flipped a loss into a 7.6× win. Honest benchmarking finds real gaps.

## 2. Boundary-only halo — the scaling unlock

```
cargo run -p hg_analytics --release --example halo_bench
```

The naive distributed BSP broadcasts the full O(n) rank vector to every shard each superstep (k·n). The
boundary-only halo sends each shard only the ghost ranks its edges reference. Same fixed point (max|Δ|
< 1e-16), far less wire. On RMAT scale-16, k=16:

| partition | edge cut | per-step halo vs full broadcast | balance |
|-----------|---------:|--------------------------------:|--------:|
| range (naive) | 85% | 5.9× less | 1.0× |
| **Fennel (edge-cut)** | **20%** | **13.1× less** | 3.0× |
| LDG | 76% | 7.2× less | 1.1× |

Fennel = 4× fewer crossing edges. The advantage **grows with scale**: edge cut falls 23% → 15% from
scale 14 → 20 (`dress_rehearsal`), the opposite of how a full-broadcast BSP degrades.

## 3. Real multi-process, over TCP (not shared memory)

```
cargo run -p hg_analytics --release --example dist_boundary # coordinator-relay
cargo run -p hg_analytics --release --example dist_p2p # pure peer-to-peer mesh
```

N worker **processes**, Fennel-partitioned, boundary halo over real sockets. Edges never leave a worker.

| runtime | scale-18, 4.2M edges, 8 procs | correctness |
|---------|-------------------------------|:-----------:|
| relay (`dist_boundary`) | 122 ms, 6.5× less wire than full-broadcast BSP | max\|Δ\| 8.6e-16 |
| **P2P mesh (`dist_p2p`)** | **99.99% of bytes peer-to-peer, coordinator 128 B/step (O(k))** | max\|Δ\| 8.6e-16 |

The P2P mesh removes the coordinator from the hot path: its traffic is O(k), independent of graph size —
the difference between an 8-node demo and a 64-node run.

## 4. The full LDBC Graphalytics suite — all six kernels, all exact

```
HG_SCALE=18 HG_SHARDS=16 cargo run -p hg_analytics --release --example ldbc_suite
```

One Fennel partition, every computational shape, self-verifying scorecard. Scale-18 (262K nodes / 4.2M
edges, 16 shards):

| kernel | shape | time | vs single-graph |
|--------|-------|-----:|:---------------:|
| PageRank | fixpoint | 0.18 s | max\|Δ\| 1e-16 ✓ |
| WCC | min-label prop | 0.04 s | exact ✓ |
| CDLP | label voting | 0.98 s | exact ✓ |
| BFS | unit traversal | 0.04 s | exact ✓ |
| SSSP | weighted traversal | 0.07 s | max\|Δ\| 0 ✓ |
| LCC | 2-hop triangle count | 21.3 s | max\|Δ\| 0 ✓ |

**The complete six-kernel LDBC Graphalytics suite (BFS, PR, WCC, CDLP, LCC, SSSP), boundary-halo
distributed, all bit-exact** against their single-graph references. LCC is the outlier on time — triangle
counting on RMAT is adversarial (a few power-law super-hubs own most triangles, and RMAT does not cap
degree the way real LDBC datasets do); it is single-pass and exact, just heavy on this synthetic graph.
The other five are sub-second-to-~1s at 4.2M edges. LCC is also the only kernel needing a 2-hop halo
(ghosts carry adjacency, not a scalar) — exchanged once, since it is single-pass.

## 5. Out-of-core (single machine, > RAM)

```
cargo run -p hg_analytics --release --example billions
```

Bucketed CSR on disk (mmap'd), O(n) heap: **500M edges** built + PageRanked on a laptop (159 s build,
11.9 s / 3 iters, ~1.2 GB resident, Σrank = 1.0). Streaming builder thrashes past ~100M; the bucketed
(sequential-I/O) builder is the one that holds.

## Cluster sizing (measured, not guessed)

~126 bytes/edge resident (`dress_rehearsal`):

| edges | nodes @ 16 GB | nodes @ 32 GB |
|------:|--------------:|--------------:|
| 1B | 8 | 4 |
| 10B | ~79 | ~40 |
| ~17B | ~135 | ~68 |

Run it on GKE: `deploy/bench/` (`saturday.sh` = create → Cloud Build → run → teardown).

## Honest limits

- Head-to-head covers networkx, scipy (libraries) and **KuzuDB (a real embedded graph DB)** — all beaten
on the same graph/machine with identical ranking. A managed **distributed** server (Neptune/TigerGraph)
on matched hardware is still the one comparison left, and it needs the cluster.
- `dist_p2p` uses a full mesh (only among peers with real boundary overlap); >100 nodes wants a sparser
mesh and pod-IP wiring.
- Laptop out-of-core ceiling ~500M edges; beyond that is the cluster.
- LDBC coverage is complete: all 6 Graphalytics kernels (BFS, PR, WCC, CDLP, LCC, SSSP) are distributed
and verified. LCC is slow on RMAT hubs (inherent to triangle counting on uncapped power-law degree).
3 changes: 3 additions & 0 deletions crates/hg_analytics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ edition = "2021"

[dependencies]
hg_core = { path = "../hg_core" }
rayon = "1.10"
memmap2 = "0.9"
bytemuck = "1"
91 changes: 91 additions & 0 deletions crates/hg_analytics/examples/billions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! billions — out-of-core PageRank at BILLION-edge scale on a single machine. Streams the edges into
//! an mmap CSR (O(n) heap, edges never materialized) and runs PageRank over the disk-backed mapping.
//! The whole point: a graph this size would need ~16 GB just for an in-heap edge list; here the O(E)
//! structure lives on disk and only O(n) rank vectors are resident. Run:
//! `cargo run --release --example billions [n_nodes] [n_edges] [iters]` (default 100M / 1B / 3).

use hg_analytics::{pagerank_mmap, write_csr_bucketed, MmapCsr};
use std::time::Instant;

struct Gen {
s: u64,
rem: usize,
n: usize,
}
impl Gen {
fn new(seed: u64, m: usize, n: usize) -> Self {
Gen { s: seed, rem: m, n }
}
}
impl Iterator for Gen {
type Item = (usize, usize);
#[inline]
fn next(&mut self) -> Option<(usize, usize)> {
if self.rem == 0 {
return None;
}
self.rem -= 1;
self.s ^= self.s << 13;
self.s ^= self.s >> 7;
self.s ^= self.s << 17;
let u = (self.s as usize) % self.n;
self.s ^= self.s << 13;
self.s ^= self.s >> 7;
self.s ^= self.s << 17;
let v = (self.s as usize) % self.n;
Some((u, v))
}
}

fn main() {
let n: usize = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(50_000_000); // proven clean on a laptop; larger is hardware-bound
let m: usize = std::env::args()
.nth(2)
.and_then(|s| s.parse().ok())
.unwrap_or(500_000_000);
let iters: usize = std::env::args()
.nth(3)
.and_then(|s| s.parse().ok())
.unwrap_or(3);
const SEED: u64 = 0x9e3779b97f4a7c15;

println!(
"BILLION-scale out-of-core PageRank: {} nodes / {} edges",
n, m
);
println!(
" an in-heap edge Vec alone would be ~{} GB — here edges are streamed to disk",
m * 16 / 1_000_000_000
);

let path = std::env::temp_dir().join("hg_billions.csr");
// Bucketed = fully sequential writes (no random-write page-thrash on the big neighbours array).
let tb = Instant::now();
write_csr_bucketed(&path, n, || Gen::new(SEED, m, n), 64).unwrap();
let build = tb.elapsed();

let csr = MmapCsr::open(&path).unwrap();
let tp = Instant::now();
let pr = pagerank_mmap(&csr, 0.85, iters, -1.0);
let prt = tp.elapsed();
let mass: f64 = pr.iter().sum();

println!(
" CSR on disk (mmap'd): {:.2} GB heap resident: ~{:.1} GB (O(n) only)",
csr.mapped_bytes() as f64 / 1e9,
(n * 8 * 3) as f64 / 1e9
);
println!(
" stream-build: {:>7.2?} pagerank_mmap({} iters): {:>7.2?}",
build, iters, prt
);
println!(
" Σrank = {:.4} (≈1) {:.0}M edges·iter/s",
mass,
(m as f64 * iters as f64 / prt.as_secs_f64()) / 1e6
);
std::fs::remove_file(&path).ok();
}
Loading
Loading