Skip to content

fix(floyd-warshall): parallel generate() must use a full barrier (root cause of #13) - #15

Merged
apotema merged 1 commit into
mainfrom
fix/parallel-fw-barrier
Jun 18, 2026
Merged

apotema merged 1 commit into
mainfrom
fix/parallel-fw-barrier

Conversation

@apotema

@apotema apotema commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Fixes the root cause behind #13 (the v0.7.1 cycle-guard treated the symptom).

Root cause

generateParallel's per-iteration synchronization was not a real barrier: only the owner of the current pivot row k signaled iteration k+1 (a single fetchAdd(thread_count)). A fast thread could then enter iteration k+1 and read the next pivot row (k+1) while its owner was still updating it in iteration k → dist/next become mutually inconsistent → cycles in next → infinite path reconstruction (hang + OOM, seen loading a 1000-worker flying-platform colony).

Fix

Every thread signals counter[k+1] += 1 after finishing iteration k; iteration k+1 only starts once all thread_count threads have arrived (true barrier), so the next pivot row is guaranteed final.

Verification

A parallel-vs-scalar harness over 256-node random graphs: before = 60/60 runs mismatched (with cycles); after = 0/60, bit-identical to the scalar reference. Added that as a regression test.

⚠️ The regression test lives in tests/root.zig (top-level), not a nested *Spec group — because nested spec tests don't actually execute under zig build test on Zig 0.16 (filed separately). Verified the root-level test FAILS with the old barrier and PASSES with the fix (3/3 each).

version → 0.7.2. Closes #13.

… corrupting next -> cycles)

Root cause behind #13 (the v0.7.1 cycle-guard symptom). generateParallel's
per-iteration sync was NOT a real barrier: only the owner of the current
pivot row k signaled iteration k+1 (one fetchAdd of thread_count). A fast
thread could then enter iteration k+1 and read the NEXT pivot row (k+1) while
ITS owner was still updating it in iteration k -> dist/next become mutually
inconsistent -> cycles in next -> infinite path reconstruction (hang + OOM,
seen loading a 1000-worker flying-platform colony).

Fix: every thread signals counter[k+1] += 1 after finishing iteration k, and
iteration k+1 only starts once all thread_count threads have arrived (true
barrier) -> the next pivot row is guaranteed final.

Verified: parallel result is now bit-identical to the scalar reference across
24 seeded 256-node graphs (was 60/60 mismatched with cycles before). Adds
that regression test at the test ROOT.

NOTE: discovered while writing the test that the zspec suite's nested *Spec
tests do NOT execute under `zig build test` on Zig 0.16 (only refAllDecls
runs) -- so the regression test lives top-level in tests/root.zig where it
actually runs. Filed separately; the existing nested tests need the same
treatment or a runner fix.

Bumps version to 0.7.2.
@cursor

cursor Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

PR Summary

High Risk
Fixes a concurrency bug in parallel all-pairs shortest paths; incorrect synchronization can silently corrupt graph results used for pathfinding at scale.

Overview
Parallel Floyd-Warshall now uses a real per-iteration barrier: after each pivot k, every worker increments sync_counters[k+1] instead of only the thread that owns row k adding thread_count once. That closes a race where a fast thread could start pivot k+1 before row k+1 was finished, corrupting dist/next (cycles in next, hangs/OOM during path reconstruction — #13).

Adds a top-level regression test in tests/root.zig (24 random 256-node graphs) asserting parallel dist/next match the scalar reference, placed at the test root so it runs under zig build test. Package version bumped to 0.7.2.

The diff also includes a large vendored zspec tree under zig-pkg/ (dependency packaging); the algorithm change itself is a small edit in floyd_warshall_optimized.zig.

Reviewed by Cursor Bugbot for commit fb43cf5. Bugbot is set up for automated code reviews on this repo. Configure here.

@apotema
apotema merged commit 1ffe1dc into main Jun 18, 2026
2 checks passed
@apotema
apotema deleted the fix/parallel-fw-barrier branch June 18, 2026 14:22

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several improvements and fixes, including a critical fix for the Floyd-Warshall parallel implementation to ensure proper synchronization using a full barrier, and the addition of a regression test for issue #13. It also adds new examples and integration modules for ECS and FSM patterns, along with a JUnit XML report writer. My review identified that committing the zig-pkg/ directory is unnecessary and should be avoided, and suggested an optimization for the regression test to move memory allocations outside the loop to improve performance.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@@ -0,0 +1,13 @@
.{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Committing the fetched dependency package (zig-pkg/) to the repository is unnecessary and discouraged when using Zig's package manager.

Zig's package manager automatically downloads, verifies, and caches dependencies based on the URL and hash declared in the root build.zig.zon. Committing these files bloats the repository size and can lead to maintenance issues (e.g., out-of-sync dependency code).

Consider removing the zig-pkg/ directory from version control and adding it to your .gitignore file.

Comment thread tests/root.zig
Comment on lines +33 to +41
for (0..24) |seed| {
var par = FloydWarshallParallel.init(allocator);
defer par.deinit();
var scal = FloydWarshallScalar.init(allocator);
defer scal.deinit();
par.resize(N);
try par.clean();
scal.resize(N);
try scal.clean();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Initializing and resizing the par and scal instances inside the loop causes redundant allocations and deallocations of the dist and next matrices (each of size 256 * 256 * 4 bytes = 256 KB) on every iteration.

Since clean() resets the state and reuses the allocated matrices when the capacity is sufficient, we can optimize this by moving the initialization and resizing of par and scal outside the loop.

    var par = FloydWarshallParallel.init(allocator);
    defer par.deinit();
    var scal = FloydWarshallScalar.init(allocator);
    defer scal.deinit();
    par.resize(N);
    scal.resize(N);

    for (0..24) |seed| {
        try par.clean();
        try scal.clean();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FloydWarshallOptimized: next matrix can contain a cycle for a reachable pair (root cause behind the v0.7.1 hang guard)

1 participant