Skip to content

Combinatorial TSP: reproducible under joblib(threads) — per-worker RNG streams (closes #117) - #119

Merged
fundthmcalculus merged 3 commits into
mainfrom
fix/combinatorial-thread-safety
Aug 23, 2026
Merged

Combinatorial TSP: reproducible under joblib(threads) — per-worker RNG streams (closes #117)#119
fundthmcalculus merged 3 commits into
mainfrom
fix/combinatorial-thread-safety

Conversation

@fundthmcalculus

Copy link
Copy Markdown
Owner

Closes #117. Makes the combinatorial TSP solvers (ACO, GA, ACO-MST) reproducible under joblib.Parallel(prefer="threads"), adopting the per-worker stream infrastructure the continuous stack already uses.

Problem

The solvers drew from the process-global, thread-unsafe np.random while dispatching ants/individuals across joblib threads → a seeded run gave a different tour every execution at n_jobs>1.

Fix

  • Route every RNG draw through core.random.rng() (thread-local stream) in aco.py, ga.py, aco_mst.py.
  • Per-generation spawn_streams(n_jobs) + wrap each task body in use_stream(stream), so which numbers a task sees depends on its index, not scheduling.
  • GA concurrency bug also fixed: _tournament_selection returned a view into the shared genome that the genetic operators mutate in place — under threads that corrupts the array other workers read. Return a .copy(). (This was the residual non-determinism after the RNG fix — reproducible serial, not parallel.)

Verified (same seed → identical solution, n_jobs=4)

ACO 6.151822==6.151822, ACO-MST 3.51003==3.51003, GA 8.96930==8.96930 (and GA at n_jobs=1). test_combinatorics + test_determinism green (27).

Note: TSP-solver outputs are not in any reported dissertation table, so the (now-correct) result changes are internal.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LxgEQmoqtAj589pvbwuGo4

fundthmcalculus and others added 3 commits August 23, 2026 15:13
…#117)

The combinatorial TSP solvers drew from the process-global, thread-unsafe
np.random while dispatching ants/individuals via joblib.Parallel(prefer=threads),
so a seeded run gave a different tour every execution at n_jobs>1 (and none at all
under loky). The continuous stack already solved this with core.random's
per-worker streams; the combinatorial solvers never adopted it.

- Route every RNG draw in aco.py/ga.py/aco_mst.py through core.random.rng()
  (the thread-local stream), replacing all np.random.* calls.
- Give each parallel task an independent, seed-derived stream: spawn_streams(n_jobs)
  once per generation, wrap each task body in use_stream(stream). Which numbers a
  task sees now depends on its index, not on scheduling.
- GA-specific concurrency bug also fixed: _tournament_selection returned a VIEW
  into the shared genome, which the genetic operators mutate in place -- under
  threads that corrupts the array other workers are reading. Return a copy.

Verified reproducible at n_jobs=4 (same seed -> identical solution) for all three:
ACO (6.151822==6.151822), ACO-MST (3.51003==3.51003), GA (8.96930==8.96930), and
GA also at n_jobs=1. tests/test_combinatorics.py + tests/test_determinism.py green (27).

Closes #117.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxgEQmoqtAj589pvbwuGo4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxgEQmoqtAj589pvbwuGo4
@fundthmcalculus
fundthmcalculus merged commit 6ee5476 into main Aug 23, 2026
1 check passed
@fundthmcalculus
fundthmcalculus deleted the fix/combinatorial-thread-safety branch August 23, 2026 19:50
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.

Combinatorial solvers (ACO/GA) draw from thread-unsafe legacy np.random under joblib(threads)

1 participant