Skip to content

perf(parallel): persistent helper pool for parallelFor; finer reduction partials - #31

Merged
tannevaled merged 3 commits into
mainfrom
perf/parallel-pool
Oct 7, 2026
Merged

tannevaled merged 3 commits into
mainfrom
perf/parallel-pool

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Why

On POWER9 bare metal, a 2^20 dot ran slower on 8 cores (553–625 µs) than on 4 (212 µs), with 2.4 of 8 CPUs busy. parallelFor forked goroutines per call, and Go wakes parked threads one after another. Three schedulers that kept per-call goroutines were measured in v0.5.2 (experiment/parallel-for-scheduling); each fixed one worker count and broke another.

Change

  • internal/kernels/pool.go (new). Up to GOMAXPROCS-1 helper goroutines, started once. After a job they poll for 200 µs (spinWindow), then park; publish wakes the parked ones. The park/publish race is closed by announce-then-recheck: either the publisher sees the flag and sends a wake-up, or the helper takes the flag back. If another call holds the pool, the new call falls back to the old fork/join.
  • parallelFor. The caller and the helpers each own a contiguous run of blocks, then steal what is left of the other runs. Block boundaries depend only on (n, w).
  • Reductions (mapReduceP, Dot1DP) use 4 partials per worker, each of at least 8192 elements. With one each, a helper that saw the job late lost its only block to the caller (Sum 4 Mi on 4 cores: 2.8 of 4 CPUs busy). The regrouping can change Sum/Dot by a few ULP compared with v0.5.2; results stay deterministic for a given GOMAXPROCS.

Tests

  • Pool tests: parked helpers and awake helpers, many back-to-back jobs, the busy-pool fallback (made deterministic by the test holding the lock), more helpers than workers, and concurrent callers.
  • Participation test: TestPoolHelpersShareTheWork asserts that blocks really run concurrently. A sabotaged publish that never wakes helpers fails it (peak concurrency 1); every other test would pass, because the caller does all the work.
  • CI-equivalent run: -race and coverage at 100%. go vet passes on linux amd64/ppc64le/loong64/386/arm/riscv64/s390x and on windows.

Measured (POWER9 bare metal, one thread per core, each case in a fresh process, 6 interleaved rounds, median vs v0.5.2)

2 4 8 workers 8: before → after
Dot 2^20 3.29× 2.41× 10.8× 625 → 58 µs
MatVec 1024² 2.01× 1.65× 5.78× 323 → 56 µs
SumAxis(1) 2.06× 1.80× 7.09× 310 → 44 µs
Sum 256 Ki 2.33× 3.23× 5.60× 61 → 11 µs
Max 256 Ki 1.75× 1.34× 6.78×
AddInto 256 Ki 2.22× 1.91× 5.57×
Exp 4 Mi 1.00× 1.10× (p25 0.84×) 1.29×
Chain 4 Mi 0.91× 1.04×
MatMul (own driver, unchanged) 1.00× 0.96–0.99×
  • An artefact I excluded. A combined run with every benchmark in one process also showed serial ops speeding up (Sqrt 1 Ki ×1.5–2.2). Run alone, they are equal: the earlier benchmarks had left state behind. Only fresh-process numbers are reported.
  • Not measured on amd64 or Apple M. The cfarm42x hosts answered "No route to host" and the Mac was 80% busy.

Behaviour

The helpers are never stopped, so goleak-style checks will list them. This is documented in the README (new Goroutines section) and in the package doc. They burn at most 200 µs of polling after each parallel operation.

Version: v0.6.0 (visible behaviour change; reduction regrouping).

🤖 Generated with Claude Code

tannevaled and others added 3 commits October 7, 2026 17:19
…ed runs + stealing; finer reduction partials

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ess table)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 0241ff3 into main Oct 7, 2026
12 checks passed
@tannevaled
tannevaled deleted the perf/parallel-pool branch October 7, 2026 15:30
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.

1 participant