Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7c7b933
feat(scoring): fork evaluator replicas from ScoreDirector
blackopsrepl Oct 22, 2026
bd818aa
feat(solver): build constraint sets from a constructor at every solve…
blackopsrepl Oct 22, 2026
845a571
refactor(solver): split the local-search trial kernel from its accoun…
blackopsrepl Oct 22, 2026
c5584ea
refactor(solver): extract local-search candidate generation and consu…
blackopsrepl Oct 22, 2026
8cece00
feat(solver): report speculative evaluation in telemetry and candidat…
blackopsrepl Oct 22, 2026
c01efac
feat(solver): parallel evaluation windows for acceptor-forager local …
blackopsrepl Oct 22, 2026
6f882a6
feat(solver): drive acceptor-forager evaluation from move_thread_count
blackopsrepl Oct 22, 2026
f05fc15
test(solverforge): parallel local-search parity through the generated…
blackopsrepl Oct 22, 2026
e7a296f
docs: describe parallel local-search evaluation
blackopsrepl Oct 22, 2026
0fc8020
perf(solver): reuse local-search window buffers across steps
blackopsrepl Oct 22, 2026
6add550
refactor(scoring): fork replicas through a borrowed replica descriptor
blackopsrepl Oct 22, 2026
bdcdb64
perf(solver): lend move tabu signatures to the acceptor
blackopsrepl Oct 22, 2026
4e69175
feat(solver): size parallel evaluation windows from the step's consum…
blackopsrepl Oct 22, 2026
7496f68
perf(solver): aim parallel windows at the previous step's consumption
blackopsrepl Oct 25, 2026
66b020b
perf(solver): select the local-search step loop once per phase
blackopsrepl Oct 25, 2026
844651a
refactor(solver)!: lend tabu signatures through one Cow-returning method
blackopsrepl Oct 25, 2026
2619725
test(solver): split parallel evaluation tests and pin the evaluator c…
blackopsrepl Oct 25, 2026
5fdfda1
fix(solver): cap the evaluator pool at one full window
blackopsrepl Oct 25, 2026
958286a
test(solver): pin allocation-free window trials and runtime K-opt jou…
blackopsrepl Oct 25, 2026
e8ee5be
perf(solver): dispatch evaluation windows without heap allocation
blackopsrepl Oct 25, 2026
dfdfe64
fix(solver): recycle K-opt undo journals through the compiled runtime…
blackopsrepl Oct 25, 2026
e446566
test(bridge): pin DynamicScore equality, thread-independent zero, and…
blackopsrepl Oct 25, 2026
8cf6a0c
fix(bridge)!: make DynamicScore a plain three-level value
blackopsrepl Oct 25, 2026
d1cc3e8
test(solverforge): pin list-variable parallel evaluation against the …
blackopsrepl Oct 25, 2026
efbfc48
test(solverforge): pin incremental scoring of streams that read list …
blackopsrepl Oct 25, 2026
c0b5b98
feat(core): let solutions report the elements their list shadows write
blackopsrepl Oct 25, 2026
090df3b
test(scoring): pin element rows that read list shadows against fresh …
blackopsrepl Oct 25, 2026
3795a1c
fix(scoring)!: score list-shadow writes as element mutations
blackopsrepl Oct 25, 2026
17d9334
test(solverforge): pin candidate-trace parity for scalar parallel eva…
blackopsrepl Oct 25, 2026
910ecb3
docs: state the evaluator ceiling, the sequential Auto, and the host-…
blackopsrepl Oct 25, 2026
944d867
fix(solver)!: record solve statistics once, per phase
blackopsrepl Oct 26, 2026
d4594cc
fix(solver): read the selector gate's generated-move count through it…
blackopsrepl Oct 26, 2026
cf37627
perf(solver): close phase records without allocating
blackopsrepl Oct 26, 2026
087ad17
perf(solver): skip progress snapshots for the no-op callback
blackopsrepl Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,12 @@ composition.
Clone is acceptable only for:

1. Solution snapshots sent through channels.
2. Partitioned search creating partition copies.
2. Parallel search creating worker-private solution copies: partitioned
search's partition copies, and parallel local-search evaluation's
evaluator replicas — one working-solution copy per evaluator beyond the
main director, made at phase start and again after the working solution is
written outside the parallel loop's own commits. Replicas borrow the solve's
descriptor; they never copy it.
3. Explicit user-owned result snapshots outside retained scoring state, such as
`CollectedVec::to_vec()` when the caller deliberately asks for an owned
`Vec<T>`.
Expand Down Expand Up @@ -276,6 +281,11 @@ Use `rayon` for CPU-bound work and `tokio` for async coordination.
- Do not add `std::thread::spawn`.
- Do not add `std::thread::sleep`.
- Prefer ownership transfer over callback APIs that only expose `&Self`.
- Parallel local-search evaluation runs one fork-join (`rayon::join` plus a
parallel iterator over the replicas) per evaluation window on the solve
thread's current pool. Fork-join jobs live on the stack, so window dispatch
allocates nothing; do not reintroduce per-window spawned tasks, dedicated
pools, or blocking waits inside evaluation workers.

### Configured Completion

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,14 @@ SolverForge leverages Rust's zero-cost abstractions:
- **Cache-friendly**: Contiguous memory layouts for hot paths
- **Concrete native execution**: Score directors, deciders, bounders, move carriers, and selector carriers remain monomorphized; documented scorer-agnostic `&dyn Director` callbacks are narrow intentional seams

Local search can evaluate candidates on several cores within one solve:
`move_thread_count = { count = 4 }` in `solver.toml` (Python configs through
solverforge-py use `{"move_thread_count": {"count": 4}}`) runs each step's
trials on up to four evaluators while one coordinator keeps the same
acceptance order. The count is bounded by the solve's Rayon threads and by 32,
the size of one evaluation window. Results are reproducible at fixed work, and
the default (`auto`) currently stays sequential.

Absolute throughput depends on the model, constraints, neighborhood, hardware,
and termination policy. Use emitted generated/evaluated throughput for one run
and the controlled paired benchmark gate below for regression decisions.
Expand Down
1 change: 1 addition & 0 deletions assets/solverforge-architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 33 additions & 12 deletions crates/solverforge-bridge/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ src/
├── ids.rs - Re-exports logical descriptor ID types from `solverforge-core`
├── lib.rs - Crate root; module declarations and public re-exports
├── runner.rs - Binding-oriented dynamic runner helper
├── score.rs - `DynamicScore`, `DynamicScoreFamily`, and scoped family guard
├── score.rs - `DynamicScore`, `DynamicScoreFamily`, and the scoped presentation family
├── score_tests.rs - Dynamic score parse/display/arithmetic tests
└── slots.rs - Re-exports dynamic scalar/list slot adapters from `solverforge-core`
```
Expand All @@ -43,7 +43,9 @@ pub use backend::{
};
pub use ids::{EntityClassId, ProblemFactClassId, VariableId};
pub use runner::try_run_dynamic_solver_with_config_parts;
pub use score::{scoped_dynamic_score_family, DynamicScore, DynamicScoreFamily};
pub use score::{
active_dynamic_score_family, scoped_dynamic_score_family, DynamicScore, DynamicScoreFamily,
};
pub use slots::{DynamicListVariableSlot, DynamicScalarVariableSlot};
```

Expand Down Expand Up @@ -145,20 +147,15 @@ pub struct DynamicScore {
pub hard: i64,
pub medium: i64,
pub soft: i64,
pub family: DynamicScoreFamily,
}
```

Constructors:

- `ZERO`
- `of(hard, medium, soft)`
- `with_family(hard, medium, soft, family)`
- `soft(soft)`
- `hard_soft(hard, soft)`
- `hard_soft_decimal(hard_scaled, soft_scaled)`
- `hard_medium_soft(hard, medium, soft)`
- `zero_for_family(family)`

Methods:

Expand All @@ -173,10 +170,28 @@ Implements:
- `Add`, `Sub`, `Neg`
- `Ord`, `PartialOrd`, `Eq`, `PartialEq`, `Hash`, `Clone`, `Copy`, `Default`

The static `Score` trait still reports three levels. The `family` field controls
presentation and host-boundary conversion. `scoped_dynamic_score_family(family,
callback)` sets the thread-local family used by `DynamicScore::zero()` and
`DynamicScore::from_level_numbers(...)` during the callback.
Every family shares one three-level value: unused levels stay zero and
`HardSoftDecimal` levels hold values scaled by 100 000. Equality, ordering, and
hashing compare the levels only, and `zero()`, `from_level_numbers(...)`, and
the arithmetic never read a family, so scores computed on any thread agree.

### Family scope

```rust
pub fn scoped_dynamic_score_family<T>(
family: DynamicScoreFamily,
callback: impl FnOnce() -> T,
) -> T;

pub fn active_dynamic_score_family() -> DynamicScoreFamily;
```

The declared family is a property of the solve, held per thread. It decides
presentation and parsing only: `Display` renders the active family's levels,
and `ParseableScore::parse` reads the active family's units, scaling
whole-number parts under `HardSoftDecimal` so parsing inverts `Display`.
Bindings enter the family around the solve; parallel evaluation's worker
threads need no scope.

## Public Functions

Expand All @@ -185,7 +200,7 @@ callback)` sets the thread-local family used by `DynamicScore::zero()` and
```rust
pub fn try_run_dynamic_solver_with_config_parts<S, C, V, DM, IDM>(
solution: S,
constraints: C,
constraints: fn(&S) -> C,
descriptor: SolutionDescriptor,
entity_count_by_descriptor: fn(&S, usize) -> usize,
runtime: SolverRuntime<S>,
Expand All @@ -207,6 +222,12 @@ where
This is the binding-oriented entrypoint. It consumes frozen dynamic model parts
into the same compiled graph and phase runner used by native models. It accepts
no phase builder, selector registration, or alternate construction path.
`constraints` builds a constraint set for a given solution; the runtime calls it
for the working solution and for every evaluator replica, so
`move_thread_count` evaluates dynamic models in parallel. Bindings rebuild
their host constraint set from the solution they receive. Replicas score on
Rayon worker threads, so a binding whose host runtime has a global lock must
release it on the calling thread for the duration of the solve.
The optional qualified provenance changes only the candidate-trace header; it
does not select another runner.

Expand Down
4 changes: 3 additions & 1 deletion crates/solverforge-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ pub use backend::{
};
pub use ids::{EntityClassId, ProblemFactClassId, VariableId};
pub use runner::try_run_dynamic_solver_with_config_parts;
pub use score::{scoped_dynamic_score_family, DynamicScore, DynamicScoreFamily};
pub use score::{
active_dynamic_score_family, scoped_dynamic_score_family, DynamicScore, DynamicScoreFamily,
};
pub use slots::{DynamicListVariableSlot, DynamicScalarVariableSlot};

#[cfg(test)]
Expand Down
6 changes: 5 additions & 1 deletion crates/solverforge-bridge/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,14 @@ where
///
/// The one model value is consumed by the canonical compiled runner. There is
/// no host-language construction branch or deferred alternate execution path.
///
/// `constraints` builds a constraint set for a given solution. The runtime
/// calls it for the working solution and for every evaluator replica, so
/// `move_thread_count` evaluates dynamic models in parallel.
#[allow(clippy::too_many_arguments)]
pub fn try_run_dynamic_solver_with_config_parts<S, C, V, DM, IDM>(
solution: S,
constraints: C,
constraints: fn(&S) -> C,
descriptor: SolutionDescriptor,
entity_count_by_descriptor: fn(&S, usize) -> usize,
runtime: SolverRuntime<S>,
Expand Down
Loading
Loading