Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions crates/solverforge-config/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,8 @@ sizes partitioned search separately.
| `Count(n >= 2)` | `min(n, rayon::current_num_threads(), 32)` evaluators: the main director plus that many minus one replicas; 32 is one full evaluation window |

Every policy reaches the sequential trajectory; `auto` changes speed only.
`rayon::current_num_threads()` is read when the local-search phase starts, on
the pool it solves in; compiling the configuration starts no thread pool.
`PartitionedSearchConfig::thread_count`: `auto` runs one thread per partition
up to the available CPU cores and the partitioner's recommended partition count, `none` one thread, `{ count = n }` at most `n`.

Expand Down
2 changes: 1 addition & 1 deletion crates/solverforge-solver/src/phase/localsearch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ pub(crate) use forager::{
AcceptedCountForager, BestScoreForager, FirstAcceptedForager, FirstBestScoreImprovingForager,
FirstLastStepScoreImprovingForager, ForagerDecision, LocalSearchForager,
};
pub(crate) use parallel::{resolve_evaluation_policy, EvaluationPolicy};
pub(crate) use parallel::resolve_evaluation_policy;
pub(crate) use phase::solve_local_search_with_resources;
pub(crate) use phase::LocalSearchPhase;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use std::fmt::Debug;

use solverforge_config::ScoreTieBreak;
use solverforge_config::{MoveThreadCount, ScoreTieBreak};
use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::{ParseableScore, Score};
use solverforge_scoring::Director;
Expand All @@ -15,8 +15,8 @@ use crate::builder::{AnyAcceptor, AnyForager, CompiledAcceptor, CompiledForager}
use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
use crate::phase::localsearch::vnd::solve_vnd_with_resources;
use crate::phase::localsearch::{
solve_local_search_with_resources, AcceptedCountForager, BestScoreForager,
DiversifiedLateAcceptanceAcceptor, EvaluationPolicy, FirstLastStepScoreImprovingForager,
resolve_evaluation_policy, solve_local_search_with_resources, AcceptedCountForager,
BestScoreForager, DiversifiedLateAcceptanceAcceptor, FirstLastStepScoreImprovingForager,
LateAcceptanceAcceptor, SimulatedAnnealingAcceptor,
};
use crate::runtime::compiler::{
Expand Down Expand Up @@ -46,7 +46,9 @@ where
forager: Option<CompiledForager>,
score_tie_break: ScoreTieBreak,
random_seed: Option<u64>,
evaluation_policy: EvaluationPolicy,
/// Resolved against the solve's thread pool when the phase runs, so
/// compiling a runner never starts a pool.
move_thread_count: MoveThreadCount,
},
VariableNeighborhoodDescent {
phase_termination: Option<solverforge_config::TerminationConfig>,
Expand Down Expand Up @@ -80,7 +82,7 @@ where
forager,
score_tie_break,
random_seed,
evaluation_policy,
move_thread_count,
} => {
let (mut acceptor, mut forager) = build_acceptor_and_forager::<S>(
*components,
Expand All @@ -89,6 +91,7 @@ where
*score_tie_break,
*random_seed,
);
let evaluation_policy = resolve_evaluation_policy(move_thread_count);
let mut source = selector.borrowed_source();
solver_scope.with_phase_termination(phase_termination.as_ref(), |solver_scope| {
solve_local_search_with_resources(
Expand All @@ -97,7 +100,7 @@ where
&mut acceptor,
&mut forager,
None,
*evaluation_policy,
evaluation_policy,
solver_scope,
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::{ParseableScore, Score};

use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
use crate::phase::localsearch::resolve_evaluation_policy;
use crate::runtime::compiler::{
CompiledAcceptorForagerSelector, CompiledLocalSearch, CompiledRuntimeExtension,
CompiledRuntimePhase, DefaultLocalSearchEligibility, DefaultRuntimeBindings,
Expand Down Expand Up @@ -88,9 +87,7 @@ where
forager: None,
score_tie_break: solverforge_config::ScoreTieBreak::Random,
random_seed: solver_config.resolved_random_seed(),
evaluation_policy: resolve_evaluation_policy(
&solver_config.move_thread_count,
),
move_thread_count: solver_config.move_thread_count.clone(),
}),
Some(plan.candidate_trace_plan()),
)
Expand Down Expand Up @@ -177,7 +174,7 @@ where
forager,
score_tie_break: config.score_tie_break,
random_seed: solver_config.resolved_random_seed(),
evaluation_policy: resolve_evaluation_policy(&solver_config.move_thread_count),
move_thread_count: solver_config.move_thread_count.clone(),
},
declaration,
))
Expand Down
42 changes: 42 additions & 0 deletions crates/solverforge-solver/tests/runner_compile_threads.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Compiling a runner whose local search uses `move_thread_count = "auto"`
//! starts no threads: the policy resolves against the pool the phase solves
//! in, so the global Rayon pool is not started at compile time.
//!
//! This is its own test binary because it reads the process's thread count,
//! which any other test starting a pool would change. It reads it from
//! procfs, so it runs on Linux only.
#![cfg(target_os = "linux")]

use solverforge_config::{
ChangeMoveConfig, LocalSearchConfig, MoveSelectorConfig, MoveThreadCount, PhaseConfig,
SolverConfig,
};
use solverforge_solver::__bench::CompiledRuntimeRunner;

mod support;

fn thread_count() -> usize {
std::fs::read_dir("/proc/self/task")
.expect("procfs lists this process's threads")
.count()
}

#[test]
fn compiling_an_auto_local_search_starts_no_threads() {
let config = SolverConfig {
random_seed: Some(7),
move_thread_count: MoveThreadCount::Auto,
phases: vec![PhaseConfig::LocalSearch(LocalSearchConfig {
move_selector: Some(MoveSelectorConfig::ChangeMoveSelector(
ChangeMoveConfig::default(),
)),
..LocalSearchConfig::default()
})],
..SolverConfig::default()
};
let before = thread_count();
let runner =
CompiledRuntimeRunner::compile(&config, support::context()).expect("the runner compiles");
assert_eq!(thread_count(), before);
drop(runner);
}
106 changes: 106 additions & 0 deletions crates/solverforge-solver/tests/support/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! A one-variable scalar model the solver's integration tests compile:
//! tasks assigned a worker from `0..4`. Each test binary uses part of it.
#![allow(dead_code)]

use std::any::TypeId;

use solverforge_core::domain::{
EntityCollectionExtractor, EntityDescriptor, PlanningSolution, SolutionDescriptor,
VariableDescriptor,
};
use solverforge_core::score::SoftScore;
use solverforge_solver::{
CrossEntityDistanceMeter, RuntimeModel, ScalarVariableSlot, SearchContext, ValueSource,
VariableSlot,
};

#[derive(Clone, Debug)]
pub struct Task {
pub worker: Option<usize>,
}

#[derive(Clone, Debug)]
pub struct Plan {
pub tasks: Vec<Task>,
pub score: Option<SoftScore>,
}

impl PlanningSolution for Plan {
type Score = SoftScore;

fn score(&self) -> Option<Self::Score> {
self.score
}

fn set_score(&mut self, score: Option<Self::Score>) {
self.score = score;
}
}

#[derive(Clone, Copy, Debug)]
pub struct NoMeter;

impl CrossEntityDistanceMeter<Plan> for NoMeter {
fn distance(&self, _: &Plan, _: usize, _: usize, _: usize, _: usize) -> f64 {
0.0
}
}

fn tasks(plan: &Plan) -> &Vec<Task> {
&plan.tasks
}

fn tasks_mut(plan: &mut Plan) -> &mut Vec<Task> {
&mut plan.tasks
}

fn task_count(plan: &Plan) -> usize {
plan.tasks.len()
}

fn worker_get(plan: &Plan, entity: usize, _variable_index: usize) -> Option<usize> {
plan.tasks[entity].worker
}

fn worker_set(plan: &mut Plan, entity: usize, _variable_index: usize, value: Option<usize>) {
plan.tasks[entity].worker = value;
}

fn descriptor() -> SolutionDescriptor {
let entity = EntityDescriptor::new("Task", TypeId::of::<Task>(), "tasks")
.with_extractor(Box::new(EntityCollectionExtractor::new(
"Task", "tasks", tasks, tasks_mut,
)))
.with_variable(VariableDescriptor::genuine("worker"));
SolutionDescriptor::new("Plan", TypeId::of::<Plan>()).with_entity(entity)
}

/// The search context binding the model's one scalar slot.
pub fn context() -> SearchContext<Plan, usize, NoMeter, NoMeter> {
let slot = VariableSlot::Scalar(ScalarVariableSlot::new(
0,
0,
"Task",
task_count,
"worker",
worker_get,
worker_set,
ValueSource::CountableRange { from: 0, to: 4 },
false,
));
SearchContext::new(descriptor(), RuntimeModel::new(vec![slot]), Some(7))
.expect("the model binds to its descriptor")
}

/// `count` tasks, none assigned.
pub fn unassigned(count: usize) -> Plan {
Plan {
tasks: (0..count).map(|_| Task { worker: None }).collect(),
score: None,
}
}

/// A director over `plan` scoring the task count.
pub fn director(plan: Plan) -> solverforge_scoring::ScoreDirector<Plan, ()> {
solverforge_scoring::ScoreDirector::simple(plan, descriptor(), |plan, _| plan.tasks.len())
}
Loading