Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b534005
bench: add normalized native BFS validation harness
sauravsingla Aug 28, 2026
51c19a3
docs: define normalized native BFS gate
sauravsingla Aug 28, 2026
4192e38
bench: add LAGraph normalized BFS runner
sauravsingla Aug 28, 2026
d94a935
bench: add GAP normalized BFS runner
sauravsingla Aug 28, 2026
0ea7591
ci: run normalized native BFS correctness gate
sauravsingla Aug 28, 2026
033e648
bench: include LAGraph experimental BFS API explicitly
sauravsingla Aug 28, 2026
ec0798a
ci: fix normalized native runner build wiring
sauravsingla Aug 28, 2026
4fb12a5
bench: link GAP BFS runner against native kernel object
sauravsingla Aug 28, 2026
778bac4
bench: declare GAP verification entry point
sauravsingla Aug 28, 2026
b741939
ci: compile GAP BFS kernel separately for normalized runner
sauravsingla Aug 28, 2026
1ebaa2c
fix: use pinned LAGraph v1.2.2 BFS API
sauravsingla Aug 29, 2026
37d6b53
test: make work-stealing assertion deterministic
sauravsingla Aug 29, 2026
44223d7
fix: use pinned LAGraph BFS API
sauravsingla Aug 29, 2026
2b8decd
fix: define pinned GAPBS v1.5 runner aliases
sauravsingla Aug 29, 2026
f961470
ci: isolate native BFS runner builds
sauravsingla Aug 29, 2026
9103dd4
fix: use stable LAGraph runner surface
sauravsingla Aug 29, 2026
c17db8b
fix: link LAGraph runner via exported CMake target
sauravsingla Aug 29, 2026
9dec245
fix: build LAGraph runner in pinned build tree
sauravsingla Aug 29, 2026
c400c7d
fix: compile GAP BFS kernel in runner translation unit
sauravsingla Aug 29, 2026
406444f
ci: build GAP runner with pinned C++11 model
sauravsingla Aug 29, 2026
bd696da
fix: parse GAP JSON payload from native stdout
sauravsingla Aug 29, 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
139 changes: 139 additions & 0 deletions .github/workflows/normalized-native-bfs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: Normalized Native BFS

on:
pull_request:
paths:
- '.github/workflows/normalized-native-bfs.yml'
- 'tools/validate_normalized_native_bfs.py'
- 'tools/competitor_benchmark.py'
- 'tools/native_competitors/**'
- 'docs/normalized-native-bfs.md'
workflow_dispatch:

jobs:
normalized-bfs:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
GRAPHBLAS_TAG: v10.3.2
LAGRAPH_TAG: v1.2.2
GAPBS_TAG: v1.5
steps:
- uses: actions/checkout@v4

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build libomp-dev

- name: Clone immutable native competitors
run: |
git clone --depth 1 --branch "$GRAPHBLAS_TAG" https://github.com/DrTimothyAldenDavis/GraphBLAS.git external/GraphBLAS
git clone --depth 1 --branch "$LAGRAPH_TAG" https://github.com/GraphBLAS/LAGraph.git external/LAGraph
git clone --depth 1 --branch "$GAPBS_TAG" https://github.com/sbeamer/gapbs.git external/gapbs

- name: Build SuiteSparse GraphBLAS
run: |
cmake -S external/GraphBLAS -B external/GraphBLAS/build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$PWD/external/install"
cmake --build external/GraphBLAS/build --parallel 2
cmake --install external/GraphBLAS/build

- name: Prepare normalized native runner build
run: |
set -euxo pipefail
mkdir -p build/native artifacts/normalized

- name: Build LAGraph and native BFS runner
run: |
set -euxo pipefail
# Build the thin runner in the exact pinned LAGraph build graph. This
# avoids a second find_package consumer project and links against the
# same LAGraph/GraphBLAS targets that CMake has already resolved.
cat >> external/LAGraph/CMakeLists.txt <<'CMAKE'

add_executable(velographx_lagraph_bfs_runner
${CMAKE_CURRENT_SOURCE_DIR}/../../tools/native_competitors/lagraph_bfs_json_runner.c)
target_compile_features(velographx_lagraph_bfs_runner PRIVATE c_std_11)
target_compile_options(velographx_lagraph_bfs_runner PRIVATE -O3)
target_link_libraries(velographx_lagraph_bfs_runner PRIVATE
LAGraph GraphBLAS::GraphBLAS)
set_target_properties(velographx_lagraph_bfs_runner PROPERTIES
OUTPUT_NAME lagraph_bfs_json_runner
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../../build/native")
CMAKE
cmake -S external/LAGraph -B external/LAGraph/build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$PWD/external/install" \
-DCMAKE_INSTALL_PREFIX="$PWD/external/install"
cmake --build external/LAGraph/build --parallel 2
cmake --install external/LAGraph/build
test -x build/native/lagraph_bfs_json_runner

- name: Build GAP native BFS runner
run: |
set -euxo pipefail
# The thin runner includes the pinned GAPBS v1.5 bfs.cc directly,
# renaming only its CLI main. GAPBS v1.5's own Makefile uses C++11,
# so keep the hosted correctness runner on that exact language mode.
c++ -O3 -std=c++11 -fopenmp \
-Iexternal/gapbs/src \
tools/native_competitors/gap_bfs_json_runner.cc \
-o build/native/gap_bfs_json_runner
test -x build/native/gap_bfs_json_runner

- name: Generate deterministic normalization graph
run: |
python - <<'PY'
from pathlib import Path
n = 4096
edges = set()
for u in range(n):
for delta in (1, 7, 31, 127):
v = (u + delta) % n
a, b = sorted((u, v))
if a != b:
edges.add((a, b))
Path('artifacts/normalized/hosted-native.el').write_text(
''.join(f'{u} {v}\n' for u, v in sorted(edges))
)
PY

- name: Validate full-distance normalization
env:
LD_LIBRARY_PATH: ${{ github.workspace }}/external/install/lib:${{ github.workspace }}/external/install/lib64
VELOGRAPHX_LAGRAPH_BFS_BIN: ${{ github.workspace }}/build/native/lagraph_bfs_json_runner
VELOGRAPHX_GAP_BFS_BIN: ${{ github.workspace }}/build/native/gap_bfs_json_runner
run: |
python tools/validate_normalized_native_bfs.py \
--dataset artifacts/normalized/hosted-native.el \
--source 0 \
--repeat 3 \
--lagraph-command "python tools/native_competitors/lagraph_wrapper.py" \
--gap-command "python tools/native_competitors/gap_wrapper.py" \
--output artifacts/normalized/normalized-native-bfs.json

- name: Validate claim gate
run: |
python - <<'PY'
import json
from pathlib import Path
r = json.loads(Path('artifacts/normalized/normalized-native-bfs.json').read_text())
assert r['normalized_cross_engine_claim'] is True
assert r['correctness_gate']['passed'] is True
assert r['correctness_gate']['same_dataset'] is True
assert r['correctness_gate']['same_source'] is True
assert r['correctness_gate']['same_directedness'] is True
assert r['correctness_gate']['same_full_distance_digest'] is True
assert r['research_claim'] is False
assert r['publication_grade'] is False
assert r['claim_gate']['publication_ready'] is False
PY

- name: Upload normalized native BFS evidence
uses: actions/upload-artifact@v4
with:
name: velographx-normalized-native-bfs
path: artifacts/normalized
retention-days: 30
35 changes: 35 additions & 0 deletions docs/normalized-native-bfs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Normalized native BFS comparison

This stage closes the semantic gap between VeloGraphX hosted engineering evidence and like-for-like native competitor evidence.

The validator `tools/validate_normalized_native_bfs.py` runs the builtin reference plus the existing external LAGraph and GAP adapter contracts and refuses to produce a normalized result unless every engine agrees on:

- dataset SHA-256;
- source vertex;
- directedness;
- vertex and edge counts;
- reachable-vertex count; and
- the SHA-256 digest of the complete BFS distance vector.

A successful run may set `normalized_cross_engine_claim: true` only for correctness normalization and same-runner engineering timing. It must continue to emit `research_claim: false`, `publication_grade: false`, and `publication_ready: false` on GitHub-hosted hardware.

## Native runner contract

Both native runners must accept:

```text
--dataset PATH --source N --vertices N [--directed]
```

and emit exactly one JSON object containing a `distances` array with one integer per vertex (`-1` for unreachable vertices) and an immutable framework version/commit identifier where available.

The existing shims are:

- `tools/native_competitors/lagraph_wrapper.py`
- `tools/native_competitors/gap_wrapper.py`

## Remaining implementation

The hosted workflow still needs native runner binaries that expose full BFS distance vectors from the pinned LAGraph/GraphBLAS and GAP implementations. Once those runners are wired in, the normalization validator becomes the mandatory correctness gate before any cross-engine timing summary is emitted.

Publication-grade competitor claims remain blocked until the same normalized contract is executed on the dedicated hardware campaign required by Issue #10.
22 changes: 22 additions & 0 deletions tests/test_work_stealing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

#include <atomic>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <thread>
#include <vector>

int main() {
Expand All @@ -17,6 +19,26 @@ int main() {
assert(pool.queue_group(2) == 0);
assert(pool.queue_group(3) == 1);

// Force a real stealing opportunity instead of relying on scheduler timing.
// All tasks start on queue 0 and remain blocked until at least one worker has
// attempted to steal from another queue.
std::atomic<bool> release_probe{false};
for (std::size_t i = 0; i < 32; ++i) {
pool.submit([&release_probe] {
while (!release_probe.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
}, 0);
}

const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (pool.stats().steal_attempts == 0 && std::chrono::steady_clock::now() < deadline) {
std::this_thread::yield();
}
assert(pool.stats().steal_attempts > 0);
release_probe.store(true, std::memory_order_release);
pool.wait_idle();

std::atomic<std::size_t> sum{0};
for (std::size_t i = 0; i < 1000; ++i) {
pool.submit([&sum, i] { sum.fetch_add(i, std::memory_order_relaxed); }, 0);
Expand Down
84 changes: 84 additions & 0 deletions tools/native_competitors/gap_bfs_json_runner.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>

// Compile the pinned GAP Benchmark Suite BFS implementation in this same
// translation unit. This keeps the runner on GAPBS v1.5's native C++11 build
// model and avoids a fragile cross-translation-unit re-declaration boundary.
// Rename only GAP's CLI entry point; DOBFS and BFSVerifier remain unchanged.
#define main gapbs_original_main
#include "bfs.cc"
#undef main

static void die(const std::string &msg) {
std::cerr << msg << "\n";
std::exit(2);
}

int main(int argc, char **argv) {
std::string dataset;
int source = -1;
int vertices = -1;
bool directed = false;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--dataset" && i + 1 < argc) dataset = argv[++i];
else if (a == "--source" && i + 1 < argc) source = std::stoi(argv[++i]);
else if (a == "--vertices" && i + 1 < argc) vertices = std::stoi(argv[++i]);
else if (a == "--directed") directed = true;
else die("invalid argument");
}
if (dataset.empty() || source < 0 || vertices <= 0 || source >= vertices) {
die("invalid runner arguments");
}

std::vector<std::string> args = {"gap-json-runner"};
if (!directed) args.push_back("-s");
args.push_back("-f");
args.push_back(dataset);
args.push_back("-r");
args.push_back(std::to_string(source));
std::vector<char *> cargs;
cargs.reserve(args.size());
for (auto &s : args) cargs.push_back(&s[0]);

CLApp cli(static_cast<int>(cargs.size()), cargs.data(),
"normalized breadth-first search");
if (!cli.ParseArgs()) die("GAP argument parsing failed");
Builder b(cli);
Graph g = b.MakeGraph();
if (g.num_nodes() != vertices) die("GAP graph vertex count mismatch");

pvector<NodeID> parent = DOBFS(g, static_cast<NodeID>(source), false, 15, 18);
if (!BFSVerifier(g, static_cast<NodeID>(source), parent)) {
die("GAP BFS verification failed");
}

std::vector<int> dist(vertices, -1);
dist[source] = 0;
for (int v = 0; v < vertices; ++v) {
if (v == source || parent[v] < 0) continue;
NodeID cur = static_cast<NodeID>(v);
int depth = 0;
while (cur != source) {
if (cur < 0 || cur >= vertices || parent[cur] < 0) {
depth = -1;
break;
}
cur = parent[cur];
++depth;
if (depth > vertices) die("cycle detected in GAP BFS parent tree");
}
if (depth >= 0) dist[v] = depth;
}

std::cout << "{\"framework_version\":\"GAPBS-1.5\",\"distances\":[";
for (int i = 0; i < vertices; ++i) {
if (i) std::cout << ',';
std::cout << dist[i];
}
std::cout << "]}\n";
return 0;
}
22 changes: 16 additions & 6 deletions tools/native_competitors/gap_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ def require_env(name: str) -> str:
return value


def parse_native_payload(stdout: str) -> dict:
"""Extract the runner JSON while tolerating GAPBS timing output."""
for raw_line in reversed(stdout.splitlines()):
line = raw_line.strip()
if not line.startswith("{"):
continue
try:
payload = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(payload, dict) and "distances" in payload:
return payload
raise RuntimeError("GAP BFS runner did not emit a valid JSON distances payload")


def main() -> int:
if os.environ.get("VELOGRAPHX_ALGORITHM") != "bfs":
raise RuntimeError("GAP wrapper currently supports only bfs")
Expand All @@ -47,12 +62,7 @@ def main() -> int:
if proc.returncode != 0:
detail = proc.stderr.strip() or proc.stdout.strip() or f"exit code {proc.returncode}"
raise RuntimeError(f"GAP BFS runner failed: {detail}")
try:
payload = json.loads(proc.stdout)
except json.JSONDecodeError as exc:
raise RuntimeError("GAP BFS runner must emit one JSON object") from exc
if not isinstance(payload, dict) or "distances" not in payload:
raise RuntimeError("GAP BFS runner JSON must contain distances")
payload = parse_native_payload(proc.stdout)
if len(payload["distances"]) != vertices:
raise RuntimeError("GAP BFS runner returned the wrong number of distances")
payload.setdefault("framework_version", "GAP-local")
Expand Down
Loading
Loading