mimir-graph bench — 2026-10-12
Runner: cargo run --release -p nornir-mimir-graph --example graph_bench -- [--nodes N --edges M] (an example, not nornir's register_bench! registry — that registry compiles into the nornir lib and would make every nornir build link libgraphblas).
Box: dwarf — AMD Ryzen 5 7535HS, 6 cores / 12 threads, no AVX-512, 61 GB. Ubuntu libgraphblas7 7.4.0 (OpenMP), GraphBLAS threads = 12.
Caveat: the box was shared and heavily oversubscribed (other agents' cargo builds and mimir-driver runs; load average 33–41 on 12 threads during these runs). Wall times swing 1.5–3× between runs; min is the better estimate of the op's own cost. An idle-box rerun is still owed.
Graph: random power-law, from = π(⌊n·u^1.5⌋), to = π(⌊n·u^2⌋), π a fixed bijection of the ids; sorted by (from, to) and de-duplicated (the shape of a sorted code_edges scan). Being random, it has one giant strongly connected part, so unbounded walks touch ~every node — the worst case for reach. similarity runs on a separate 10⁵-node / 10⁶-edge graph from the same generator. "10 000 seeds" are random node ids.
10⁶ nodes / 10⁷ edges (load average 36 at start)
| op | min ms | median ms | note |
| import Arrow, sorted (from,to) → pack | 83.4 | 104.2 | SortedPack, 9999687 edges |
| import Arrow, shuffled → GxB build | 1070.5 | 1374.4 | Build |
| from_csr (validate + 1 memcpy + GxB pack) | 84.0 | 176.3 | zero-copy pack after one copy |
| from_csr_import (validate + GrB_Matrix_import) | 306.8 | 361.0 | GraphBLAS copies |
| reach, 1 seed | 260.1 | 520.9 | 998331 reached |
| reach, 10 000 seeds | 197.8 | 308.9 | 998347 reached |
| reach, 10 000 seeds, ≤ 2 hops | 67.4 | 127.0 | 659559 reached |
| reverse_reach (mxv on A), 1 seed | 1790.0 | 2062.0 | 999670 reached |
| reverse_reach (mxv on A), 10 000 seeds | 691.4 | 1320.0 | 999675 reached |
| transpose Aᵀ (once, cacheable) | 365.0 | 371.9 | |
| reverse walk = reach on cached Aᵀ, 1 seed | 174.6 | 418.9 | 999670 reached |
| reverse walk = reach on cached Aᵀ, 10 000 seeds | 307.2 | 394.9 | 999675 reached |
| fan_in (ones PLUS.PAIR A) | 74.0 | 120.0 | 998344 nodes with in-edges |
| fan_out (row reduce) | 63.0 | 84.0 | 999670 nodes with out-edges |
| min_hops (MIN_PLUS), 10 000 seeds | 257.6 | 452.7 | 998347 reached |
| max_on_path (MAX_SECOND), 1 000 valued nodes | 1487.8 | 1584.8 | 999671 nodes reach a value |
| components (WCC) | 1725.1 | 2357.5 | 3 components |
| mask_by, 1e5-node scope | 345.7 | 579.8 | 90917 edges kept |
| diff vs a snapshot with 1 % removed / 1 % added | 625.9 | 659.6 | +99987 / −99997 |
| similarity A·Aᵀ (PLUS_PAIR) | 755.6 | 908.9 | 100000 nodes / 999726 edges → 38547079 entries |
peak RSS (VmHWM): 1565 MB
Earlier runs the same evening (load 15–33) for scale: sorted Arrow pack 85–203 ms, reach 1 seed 120–380 ms, reach 10 000 seeds 125–331 ms, transpose 357–510 ms, components 1.5–3.1 s.
10⁷ nodes / 10⁸ edges (load average 38 at start; queries median of 3)
| op | min ms | median ms | note |
| import Arrow, sorted (from,to) → pack | 1131.0 | 2079.3 | SortedPack, 99999668 edges |
| import Arrow, shuffled → GxB build | 5057.1 | 5691.3 | Build |
| from_csr (validate + 1 memcpy + GxB pack) | 1835.4 | 2477.9 | zero-copy pack after one copy |
| from_csr_import (validate + GrB_Matrix_import) | 2170.2 | 2406.0 | GraphBLAS copies |
| reach, 1 seed | 2721.7 | 2966.2 | 9982427 reached |
| reach, 10 000 seeds | 3297.7 | 3416.5 | 9982443 reached |
| reach, 10 000 seeds, ≤ 2 hops | 626.3 | 888.8 | 1487705 reached |
| reverse_reach (mxv on A), 1 seed | 11668.0 | 12768.6 | 9996314 reached |
| reverse_reach (mxv on A), 10 000 seeds | 8066.4 | 8314.0 | 9996320 reached |
| transpose Aᵀ (once, cacheable) | 4708.5 | 4938.9 | |
| reverse walk = reach on cached Aᵀ, 1 seed | 2030.9 | 2355.7 | 9996314 reached |
| reverse walk = reach on cached Aᵀ, 10 000 seeds | 1646.2 | 2112.3 | 9996320 reached |
| fan_in (ones PLUS.PAIR A) | 1435.6 | 1902.1 | 9982557 nodes with in-edges |
| fan_out (row reduce) | 342.4 | 507.9 | 9996319 nodes with out-edges |
| min_hops (MIN_PLUS), 10 000 seeds | 1944.4 | 2006.3 | 9982443 reached |
| max_on_path (MAX_SECOND), 1 000 valued nodes | 11675.5 | 11889.3 | 9996314 nodes reach a value |
| components (WCC) | 8799.7 | 9398.7 | 14 components |
| mask_by, 1e5-node scope | 64.7 | 70.0 | 10018 edges kept |
| diff vs a snapshot with 1 % removed / 1 % added | 1302.7 | 1534.4 | +999987 / −999997 |
| similarity A·Aᵀ (PLUS_PAIR) | 868.7 | 871.6 | 100000 nodes / 999726 edges → 38547079 entries |
peak RSS (VmHWM): 9390 MB
(mask_by and similarity keep their 10⁵-node sizes in this run; the 1e5-node scope on the 10⁷-node graph keeps only ~10⁴ edges, hence the small time.)
Reading
- Import: a sorted edge scan → CSR is ~0.1 s per 10⁷ edges and ~1.1 s per 10⁸ (single-threaded Rust scan + cumsum + one memcpy, then a zero-copy
GxB_Matrix_pack_CSR). Unsorted input through GraphBLAS' own build is 4.5–13× slower — keepcode_edgessorted by (from, to) as §11 says.GrB_Matrix_import(GraphBLAS copies) is 2–4× slower than pack at 10⁷. - Peak RSS: 1.6 GB at 10⁷ edges, 9.4 GB at 10⁸ — the bench holds several copies on purpose (generator edges, two Arrow batches, CSR arrays, A, Aᵀ, a second snapshot). One resident 10⁸-edge boolean matrix is ~0.8 GB (64-bit
Aj) + 80 MB offsets. - Reverse walks:
reverse_reach(mxv on A) is 2–10× slower (typically ~5×) thanreachon a cachedAᵀ; transposing once costs about one walk at 10⁷ (0.4 s) and ~2 walks at 10⁸ (4.7 s). Lenses doing dependents walks should keepAᵀ. - Bounded walks (≤ 2 hops) are 3–5× cheaper than full ones.
max_on_pathandcomponentsare fixpoint loops over dense vectors and are the slowest ops (1.5–12 s); fine for a per-snapshot job, not for an interactive query on 10⁸ edges without a smarter algorithm (frontier-based max propagation, FastSV).