Repository navigation
feat: parallel local-search evaluation for Python models - #1
Draft
blackopsrepl wants to merge 3 commits into
Draft
blackopsrepl wants to merge 3 commits into
blackopsrepl wants to merge 3 commits into
Conversation
Exposes the solver's new moves_speculative telemetry counter and the discarded_speculative candidate-trace outcome, so Python callers see the work parallel evaluation performed ahead of the decision point and then discarded.
Every solve entry now hands the runtime a constraint-set constructor instead of a ready-made set, so the solver builds one set for the working solution and one for each evaluator replica, and move_thread_count applies to Python models. The bridge's active score family is per thread and only the solve thread enters it. Replicas are scored on Rayon worker threads, so PyDynamicConstraintSet now records its declared family and enters it inside every ConstraintSet method; scores stay comparable whichever thread produced them.
A fixed-work solve reaches the same assignment and score with move_thread_count 1, 2 and 4, and a constraint callback that raises on a worker thread surfaces its Python traceback.
49 of 50 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets Python models use
move_thread_count: local-search candidates are evaluated on several cores within one solve, with the same trajectory at fixed work. The engine side is SolverForge/solverforge#78.Verified before the split, against #78's crates through a local, uncommitted
[patch.crates-io], on CPython 3.14.3 free-threaded with the GIL disabled: Rust tests pass and 330 Python tests pass, including the new 1/2/4-thread parity test and the worker-thread traceback test.test_examples_playwright.pycrashed inside greenlet during a browser asset import that never runs the solver and was excluded from that run.Python scaling is measured with the engine benchmark. Each dynamic constraint-set call attaches its worker thread to the interpreter; if Python scaling flattens early, profile thread attachment first.
Checklist
P0 —
chore: adopt solverforge <release>(the version bump needs the owner's confirmation)Cargo.toml: bump the six=0.22.0solverforge pins to the release.src/manager/candidate_trace.rs,disposition_name: addCandidateTraceDisposition::DiscardedSpeculative => "discarded_speculative".src/manager/events.rs: adddict.set_item("moves_speculative", telemetry.moves_speculative)?;next tomoves_evaluated(around line 134), and the new field to theSolverTelemetry { ... }literal (around line 400).WIREFRAME.md,README.mdandpython/solverforge/_native.pyiforinterrupted_before_evaluationandmoves_evaluated, and add the new disposition and telemetry key wherever those lists appear.P1 —
feat: evaluate Python models on evaluator replicassrc/solver/solvable.rs. Addlet constraints = ...construction, and passdynamic_constraintsas the second argument oftry_run_dynamic_solver_with_config_parts. The surroundingscoped_dynamic_score_familycall stays: it covers the solve thread.src/constraints/state.rs:family: ScoreFamilytoPyDynamicConstraintSet, importingscoped_dynamic_score_family,score_family_from_nameandScoreFamilyfromcrate::score.new(constraints, family: ScoreFamily)stores the family.from_solutionreadslet family = score_family_from_name(&solution.schema().score_family)?;and compiles inside the scope:let compiled = scoped_dynamic_score_family(family, || CompiledConstraintSet::from_solution(py, solution))?;. Compiling then gives the same result on every thread.impl ConstraintSet<PyDynamicSolution, DynamicScore> for PyDynamicConstraintSet, wrap the bodies ofevaluate_all,initialize_all,on_insert_allandon_retract_allin the scope. Copy the family into a local first so the closure can borrowselfmutably:tests/rust/constraint_set.rs. Indynamic_constraint_set_reports_constraint_count, construct the set withPyDynamicConstraintSet::new(constraints, ScoreFamily::HardSoft)and expectDynamicScore::zero_for_family(ScoreFamily::HardSoft)fromevaluate_all. The test thread never enters a solve scope, so this pins thread-independent scoring.README.md:move_thread_countevaluates local-search candidates on several Rust worker threads, each scoring its own solution clone. Callbacks already have to be safe to run concurrently, so there is no new requirement. GIL builds stay correct but serialize evaluation.move_thread_countwith the dict form{"move_thread_count": {"count": 4}}.WIREFRAME.md. In the config paragraph (around line 231), add thatmove_thread_countpasses through to the upstream solver unchanged.P2 —
test: parallel evaluation parity for Python modelstests/python/test_threading.py. Add the model and test below, extending the imports fromsolverforgeas needed:tests/python/test_tracebacks.py. Add a model whose callback fails only during local search, mirroringBrokenPlan: two tasks pre-assigned to worker0, workers[0, 1], and a filterexplode_on_second_workerthat raisesValueError("parallel callback failed")whentask.worker == 1. Solve it with{"random_seed": 7, "move_thread_count": {"count": 2}, "phases": [{"type": "local_search", "termination": {"step_count_limit": 1}}]}and assert, astest_callback_traceback_surfacesdoes, that theValueErrorsurfaces with a frame namedexplode_on_second_worker.make teston CPython 3.14 free-threaded.