Skip to content

Add an opt-in free-threaded search executor - #10

Draft
senegrom wants to merge 9 commits into
masterfrom
agent/free-threaded-search-executor
Draft

Add an opt-in free-threaded search executor#10
senegrom wants to merge 9 commits into
masterfrom
agent/free-threaded-search-executor

Conversation

@senegrom

@senegrom senegrom commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add an explicit free-threaded top-level search executor while keeping process execution as the default. The final implementation is direct source, tests, documentation, and durable benchmark evidence; the encoded experiment payload and all one-use export/validation workflows have been removed.

Design

  • Add parallel_backend="process" | "thread" to solver.solve() and --parallel-backend to the CLI.
  • Reject thread mode unless processes > 1 and CPython is a free-threaded build with the GIL actually disabled.
  • Give every executor thread a private root object graph by unpickling one cache-free solver-owned root per worker.
  • Create a fresh detached task grid for every submitted branch; custom grids, rules, guarantees, and referenced state must therefore be picklable in thread mode.
  • Submit at most one outstanding branch per worker and consume results in deterministic branch order.
  • Cooperatively cancel queued and running siblings after a positive solution cap is satisfied.
  • Keep cancellation polling entirely inside the thread-only recursive solver. The ordinary sequential and process recursion remains unchanged.
  • Suppress branch logging through context-local state and merge per-thread technique statistics in the parent.

The public usage and extension contract is documented in FREE_THREADED.md.

Correctness validation

The exact promoted source passed the complete bounded suite on both runtimes:

  • regular Python 3.14: 498 passed, 32 deselected;
  • free-threaded Python 3.14t: 498 passed, 32 deselected.

Coverage includes sequential/process/thread solution equivalence, deterministic positive caps, caller-state preservation, runtime rejection with a normal GIL, private worker rule graphs, fresh task grids, cache stripping, exception cleanup, bounded submission, cancellation, statistics merging, and context-local logging.

Normal repository CI and forward-compatibility CI also pass on the cleaned direct-source head.

Performance validation

All solver benchmarks used depth_gate=None, alternating run order, and exact deterministic solution fingerprints.

Default sequential/process path against master

  • geometric-mean ratio: 0.999608× (limit 1.01×);
  • worst ratio: 1.026823× (limit 1.03×).

Thread executor against the process executor on Python 3.14t

  • real-workload geometric-mean ratio: 0.823100× (must be no greater than 1.0×);
  • worst real-workload ratio: 1.013592× (limit 1.03×);
  • positive-cap ratio: 0.970613× (limit 1.03×);
  • synthetic 500-branch ratio: 0.196166×.

The decisive optimization was isolating cooperative cancellation in the thread-only recursion. The earlier candidate polled an optional cancellation event at every ordinary recursive search node, which measurably regressed the default path.

Benchmark reports and machine-readable samples are committed under benchmarks/.

Open correctness items before merge

  • Capped-result contract: parallel and thread execution stops after the earliest deterministically consumed branches provide max_sols. The returned solutions are valid and repeatable, but they are not guaranteed to be the globally smallest solutions by _solution_key. The current solver.solve() docstring overstates this by saying parallel runs keep the smallest solutions by content key. Either document the existing branch-priority/early-cancellation contract or compute every branch before selecting a canonical global top-N; the latter would defeat the main purpose of a small positive cap.
  • Extension thread-safety boundary: pickle isolates each worker's instance graph, but it cannot isolate module globals, class-level mutable state, external resources, callbacks, or C-extension state. Custom rules and grids must be semantically side-effect-free or explicitly thread-safe beyond merely being picklable. This needs to be stated directly in FREE_THREADED.md.
  • Coverage breadth: the hosted 3.14t suite is green, but the dedicated thread-backend tests are concentrated on Grid and Sudoku. Before merge, add direct thread-mode equivalence coverage for the built-in compact and cage families and a concurrent multi-solve stress case under a genuinely GIL-disabled runtime.

@senegrom
senegrom force-pushed the agent/free-threaded-search-executor branch from e686e40 to 80b594d Compare August 13, 2026 16:19
@senegrom
senegrom marked this pull request as ready for review August 13, 2026 20:26
@senegrom
senegrom marked this pull request as draft August 13, 2026 22:10
senegrom and others added 3 commits August 15, 2026 16:26
Brings in the capped-undercount fix, the extension-rule sandbox, the
depth-gate retirement, and the restored negative-results ledger, and
adapts the opt-in thread executor to them:

- All depth_gate plumbing removed from the thread path (solve(),
  _solve_validated_thread, _solve_top_threaded, solve_thread_trials,
  _ThreadBranchRunner, _solve_full_cancellable, and their tests),
  matching the retirement on master.
- Both threaded call sites of _atomic_pass_or_branches now pass
  allow_overlapping_guarantee_branches=max_sols == -1, mirroring the
  sequential and process paths. The cancellable recursion previously
  carried the pre-fix branching verbatim, so a positive cap could
  re-enter the capped undercount through mid-depth guarantee branches;
  the top-level full-cap-per-branch consumption only masked the
  top-level case.

The historical wording in the branch's two benchmark records is kept
verbatim per the ledger policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K4voGGQ5AHaCkDr4JAZXv9
Two regressions for the undercount family on the thread path, which the
branch predated: an end-to-end capped thread solve over overlapping
at-least-once guarantees (15 solutions, cap 10 must return 10), and a
direct call of the cancellable recursion asserting set equality with the
sequential capped subset — the top level's full-cap-per-branch
consumption can mask the recursion's behaviour, so the mirror is pinned
on its own.

The two executor benchmark records from 2026-08-12 join the verdict
index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K4voGGQ5AHaCkDr4JAZXv9
Brings the branch up to date with the September work on master: the
capped/parallel oracle tests, ruff simplifications, Dependabot and
action bumps, the deterministic weekly corpus selections, PR 16
(implicit-clique branch pressure, cage dictionary parsing, pool
exception cleanup, output encoding, wheel smoke test), PR 17 (exact
staircase cage partitions, write-once size fields, single-pass loop
pruning), PR 19 (iterative partition DFS, uncapped failure observer)
and the checkout-runnable failure probes.

Clean auto-merge; the only shared files were benchmarks/README.md and
logger.py, in disjoint regions. The thread executor needs no adaptation:
solve_threaded.py already cancels and terminates on any exception, and
the shared branch-choice change applies to both backends identically.
Full suite in the merged tree: 659 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01US1sGacWn9pHbhJyynAG4y
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