From d07127d40aa1c9912c762b5a5ba01dca32051401 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Mon, 2 Oct 2023 19:52:11 +0200 Subject: [PATCH] [CP-SAT] revisit search heuristics, lns workers search heuristics; scheduling propagation --- examples/cpp/jobshop_sat.cc | 42 ++++-------- examples/cpp/knapsack_2d_sat.cc | 3 +- examples/cpp/slitherlink_sat.cc | 21 +++--- examples/cpp/weighted_tardiness_sat.cc | 10 +-- examples/python/rcpsp_sat.py | 18 +++-- ortools/sat/cp_model_lns.cc | 76 +++++++++++---------- ortools/sat/cp_model_lns.h | 20 +++--- ortools/sat/cp_model_search.cc | 91 ++++++++++++-------------- ortools/sat/cp_model_solver.cc | 57 +++++++++------- ortools/sat/diffn.cc | 9 ++- ortools/sat/disjunctive.cc | 59 +++++++++++++++-- ortools/sat/disjunctive.h | 6 ++ ortools/sat/intervals.h | 2 + ortools/sat/precedences.cc | 68 ++++++++++++++++++- ortools/sat/precedences.h | 40 ++++++++++- ortools/sat/sat_parameters.proto | 12 ++-- ortools/sat/subsolver.cc | 5 ++ 17 files changed, 355 insertions(+), 184 deletions(-) diff --git a/examples/cpp/jobshop_sat.cc b/examples/cpp/jobshop_sat.cc index ecba200c84d..85c893011e4 100644 --- a/examples/cpp/jobshop_sat.cc +++ b/examples/cpp/jobshop_sat.cc @@ -23,6 +23,7 @@ #include "absl/flags/flag.h" #include "absl/log/check.h" #include "absl/strings/str_join.h" +#include "absl/types/span.h" #include "google/protobuf/text_format.h" #include "google/protobuf/wrappers.pb.h" #include "ortools/base/init_google.h" @@ -197,7 +198,7 @@ struct AlternativeTaskData { // main task of the job. void CreateAlternativeTasks( const JsspInputProblem& problem, - const std::vector>& job_to_tasks, int64_t horizon, + absl::Span> job_to_tasks, int64_t horizon, std::vector>>& job_task_to_alternatives, CpModelBuilder& cp_model) { @@ -296,7 +297,7 @@ struct MachineTaskData { std::vector> GetDataPerMachine( const JsspInputProblem& problem, - const std::vector>>& + absl::Span>> job_task_to_alternatives) { const int num_jobs = problem.jobs_size(); const int num_machines = problem.machines_size(); @@ -442,8 +443,8 @@ void CreateMachines( // Collect all objective terms and add them to the model. void CreateObjective( const JsspInputProblem& problem, - const std::vector>& job_to_tasks, - const std::vector>>& + absl::Span> job_to_tasks, + absl::Span>> job_task_to_alternatives, int64_t horizon, IntVar makespan, CpModelBuilder& cp_model) { LinearExpr objective; @@ -512,7 +513,7 @@ void CreateObjective( // and not the alternate copies. void AddCumulativeRelaxation( const JsspInputProblem& problem, - const std::vector>& job_to_tasks, + absl::Span> job_to_tasks, IntervalVar makespan_interval, CpModelBuilder& cp_model) { const int num_jobs = problem.jobs_size(); const int num_machines = problem.machines_size(); @@ -579,33 +580,13 @@ void AddCumulativeRelaxation( cumul.AddDemand(makespan_interval, component.size()); } } - - // Add a global cumulative that contains all main intervals. - // - // On most benchmarks, this is the only cumulative constraint created as the - // graph of connected interval has only one component. - // - // Even on benchmarks with cliques, it still helps, as it allows a global - // energetic reasoning that uses the makespan. - LOG(INFO) << "Add global cumulative with " << num_tasks << " intervals and " - << num_machines << " machines"; - CumulativeConstraint global_cumul = cp_model.AddCumulative(num_machines); - for (int j = 0; j < num_jobs; ++j) { - const int num_tasks_in_job = problem.jobs(j).tasks_size(); - for (int t = 0; t < num_tasks_in_job; ++t) { - global_cumul.AddDemand(job_to_tasks[j][t].interval, 1); - } - } - if (absl::GetFlag(FLAGS_use_interval_makespan)) { - global_cumul.AddDemand(makespan_interval, num_machines); - } } // This redundant linear constraints states that the sum of durations of all // tasks is a lower bound of the makespan * number of machines. void AddMakespanRedundantConstraints( const JsspInputProblem& problem, - const std::vector>& job_to_tasks, IntVar makespan, + absl::Span> job_to_tasks, IntVar makespan, CpModelBuilder& cp_model) { const int num_machines = problem.machines_size(); @@ -621,8 +602,8 @@ void AddMakespanRedundantConstraints( void DisplayJobStatistics( const JsspInputProblem& problem, int64_t horizon, - const std::vector>& job_to_tasks, - const std::vector>>& + absl::Span> job_to_tasks, + absl::Span>> job_task_to_alternatives) { const int num_jobs = job_to_tasks.size(); int num_tasks = 0; @@ -747,7 +728,7 @@ void Solve(const JsspInputProblem& problem) { // CP-SAT now has a default strategy for scheduling problem that works best. if (absl::GetFlag(FLAGS_display_sat_model)) { - LOG(INFO) << cp_model.Proto().DebugString(); + LOG(INFO) << cp_model.Proto(); } // Setup parameters. @@ -766,6 +747,9 @@ void Solve(const JsspInputProblem& problem) { parameters.add_extra_subsolvers("objective_shaving_search"); } + // Tells the solver we have a makespan objective. + parameters.set_push_all_tasks_toward_start(true); + const CpSolverResponse response = SolveWithParameters(cp_model.Build(), parameters); diff --git a/examples/cpp/knapsack_2d_sat.cc b/examples/cpp/knapsack_2d_sat.cc index 6764a90d455..c80da92e2cf 100644 --- a/examples/cpp/knapsack_2d_sat.cc +++ b/examples/cpp/knapsack_2d_sat.cc @@ -22,6 +22,7 @@ #include #include "absl/flags/flag.h" +#include "absl/types/span.h" #include "google/protobuf/text_format.h" #include "ortools/base/commandlineflags.h" #include "ortools/base/init_google.h" @@ -43,7 +44,7 @@ namespace sat { void CheckAndPrint2DSolution( const CpSolverResponse& response, const packing::MultipleDimensionsBinPackingProblem& problem, - const std::vector>& interval_by_item_dimension, + absl::Span> interval_by_item_dimension, std::string* solution_in_ascii_form) { const int num_items = problem.items_size(); diff --git a/examples/cpp/slitherlink_sat.cc b/examples/cpp/slitherlink_sat.cc index 4137cff7354..c476c6a7048 100644 --- a/examples/cpp/slitherlink_sat.cc +++ b/examples/cpp/slitherlink_sat.cc @@ -21,6 +21,7 @@ #include "absl/flags/parse.h" #include "absl/log/check.h" #include "absl/strings/str_format.h" +#include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/sat/cp_model.h" #include "ortools/sat/cp_model.pb.h" @@ -29,9 +30,9 @@ namespace operations_research { namespace sat { -void PrintSolution(const std::vector >& data, - const std::vector >& h_arcs, - const std::vector >& v_arcs) { +void PrintSolution(absl::Span> data, + absl::Span> h_arcs, + absl::Span> v_arcs) { const int num_rows = data.size(); const int num_columns = data[0].size(); @@ -65,7 +66,7 @@ void PrintSolution(const std::vector >& data, std::cout << last_line << std::endl; } -void SlitherLink(const std::vector >& data) { +void SlitherLink(const std::vector>& data) { const int num_rows = data.size(); const int num_columns = data[0].size(); @@ -201,7 +202,7 @@ void SlitherLink(const std::vector >& data) { const CpSolverResponse response = Solve(builder.Build()); - std::vector > h_arcs(num_rows + 1); + std::vector> h_arcs(num_rows + 1); for (int y = 0; y < num_rows + 1; ++y) { for (int x = 0; x < num_columns; ++x) { const int arc = undirected_horizontal_arc(x, y); @@ -211,7 +212,7 @@ void SlitherLink(const std::vector >& data) { } } - std::vector > v_arcs(num_columns + 1); + std::vector> v_arcs(num_columns + 1); for (int y = 0; y < num_rows; ++y) { for (int x = 0; x < num_columns + 1; ++x) { const int arc = undirected_vertical_arc(x, y); @@ -230,18 +231,18 @@ void SlitherLink(const std::vector >& data) { int main(int argc, char** argv) { absl::ParseCommandLine(argc, argv); - const std::vector > tiny = {{3, 3, 1}}; + const std::vector> tiny = {{3, 3, 1}}; - const std::vector > small = { + const std::vector> small = { {3, 2, -1, 3}, {-1, -1, -1, 2}, {3, -1, -1, -1}, {3, -1, 3, 1}}; - const std::vector > medium = { + const std::vector> medium = { {-1, 0, -1, 1, -1, -1, 1, -1}, {-1, 3, -1, -1, 2, 3, -1, 2}, {-1, -1, 0, -1, -1, -1, -1, 0}, {-1, 3, -1, -1, 0, -1, -1, -1}, {-1, -1, -1, 3, -1, -1, 0, -1}, {1, -1, -1, -1, -1, 3, -1, -1}, {3, -1, 1, 3, -1, -1, 3, -1}, {-1, 0, -1, -1, 3, -1, 3, -1}}; - const std::vector > big = { + const std::vector> big = { {3, -1, -1, -1, 2, -1, 1, -1, 1, 2}, {1, -1, 0, -1, 3, -1, 2, 0, -1, -1}, {-1, 3, -1, -1, -1, -1, -1, -1, 3, -1}, diff --git a/examples/cpp/weighted_tardiness_sat.cc b/examples/cpp/weighted_tardiness_sat.cc index 0998febb057..2e97804ba66 100644 --- a/examples/cpp/weighted_tardiness_sat.cc +++ b/examples/cpp/weighted_tardiness_sat.cc @@ -20,6 +20,7 @@ #include "absl/flags/flag.h" #include "absl/strings/numbers.h" #include "absl/strings/str_split.h" +#include "absl/types/span.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" #include "ortools/sat/cp_model.h" @@ -36,9 +37,9 @@ namespace operations_research { namespace sat { // Solve a single machine problem with weighted tardiness cost. -void Solve(const std::vector& durations, - const std::vector& due_dates, - const std::vector& weights) { +void Solve(absl::Span durations, + absl::Span due_dates, + absl::Span weights) { const int num_tasks = durations.size(); CHECK_EQ(due_dates.size(), num_tasks); CHECK_EQ(weights.size(), num_tasks); @@ -163,7 +164,8 @@ void Solve(const std::vector& durations, Model model; model.Add(NewSatParameters(absl::GetFlag(FLAGS_params))); model.GetOrCreate()->set_log_search_progress(true); - model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { + model.Add(NewFeasibleSolutionObserver([&, due_dates, durations, + weights](const CpSolverResponse& r) { // Note that we compute the "real" cost here and do not use the tardiness // variables. This is because in the core based approach, the tardiness // variable might be fixed before the end date, and we just have a >= diff --git a/examples/python/rcpsp_sat.py b/examples/python/rcpsp_sat.py index 855ec992807..1cd34e69328 100755 --- a/examples/python/rcpsp_sat.py +++ b/examples/python/rcpsp_sat.py @@ -530,24 +530,32 @@ def SolveRcpsp( # Solve model. solver = cp_model.CpSolver() - if not _USE_INTERVAL_MAKESPAN.value: - solver.parameters.exploit_all_precedences = True - solver.parameters.use_hard_precedences_in_cumulative = True + + # Parse user specified parameters. if params: text_format.Parse(params, solver.parameters) + + # Favor objective_shaving_search over objective_lb_search. if solver.parameters.num_workers >= 16 and solver.parameters.num_workers < 24: solver.parameters.ignore_subsolvers.append("objective_lb_search") solver.parameters.extra_subsolvers.append("objective_shaving_search") + + # Experimental: Specify the fact that the objective is a makespan + solver.parameters.push_all_tasks_toward_start = True + + # Enable logging in the main solve. + if in_main_solve: solver.parameters.log_search_progress = True + # status = solver.Solve(model) if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: assignment = rcpsp_pb2.RcpspAssignment() for t, _ in enumerate(problem.tasks): if t in task_starts: assignment.start_of_task.append(solver.Value(task_starts[t])) - for r in range(len(task_to_presence_literals[t])): - if solver.BooleanValue(task_to_presence_literals[t][r]): + for r, recipe_literal in enumerate(task_to_presence_literals[t]): + if solver.BooleanValue(recipe_literal): assignment.selected_recipe_of_task.append(r) break else: # t is not an active task. diff --git a/ortools/sat/cp_model_lns.cc b/ortools/sat/cp_model_lns.cc index 6ba1b86011c..a589ac9dfe2 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -418,23 +418,22 @@ bool NeighborhoodGeneratorHelper::IntervalIsActive( return false; } -void NeighborhoodGeneratorHelper::FilterInactiveIntervals( +std::vector NeighborhoodGeneratorHelper::KeepActiveIntervals( absl::Span unfiltered_intervals, - const CpSolverResponse& initial_solution, - std::vector* filtered_intervals) const { - filtered_intervals->clear(); + const CpSolverResponse& initial_solution) const { + std::vector filtered_intervals; + filtered_intervals.reserve(unfiltered_intervals.size()); absl::ReaderMutexLock lock(&domain_mutex_); for (const int i : unfiltered_intervals) { - if (IntervalIsActive(i, initial_solution)) filtered_intervals->push_back(i); + if (IntervalIsActive(i, initial_solution)) filtered_intervals.push_back(i); } + return filtered_intervals; } std::vector NeighborhoodGeneratorHelper::GetActiveIntervals( const CpSolverResponse& initial_solution) const { - std::vector result; - FilterInactiveIntervals(TypeToConstraints(ConstraintProto::kInterval), - initial_solution, &result); - return result; + return KeepActiveIntervals(TypeToConstraints(ConstraintProto::kInterval), + initial_solution); } std::vector> @@ -2024,6 +2023,19 @@ Neighborhood RandomPrecedenceSchedulingNeighborhoodGenerator::Generate( precedences, initial_solution, helper_); } +namespace { +void AppendVarsFromAllIntervalIndices(absl::Span indices, + absl::Span all_intervals, + const CpModelProto& model_proto, + std::vector* variables) { + for (const int index : indices) { + const std::vector vars = + UsedVariables(model_proto.constraints(all_intervals[index])); + variables->insert(variables->end(), vars.begin(), vars.end()); + } +} +} // namespace + Neighborhood SchedulingTimeWindowNeighborhoodGenerator::Generate( const CpSolverResponse& initial_solution, double difficulty, absl::BitGenRef random) { @@ -2038,21 +2050,17 @@ Neighborhood SchedulingTimeWindowNeighborhoodGenerator::Generate( std::vector intervals_to_relax; intervals_to_relax.reserve(partition.selected_indices.size()); std::vector variables_to_fix; - for (const int index : partition.selected_indices) { - intervals_to_relax.push_back(active_intervals[index]); - } + intervals_to_relax.insert(intervals_to_relax.end(), + partition.selected_indices.begin(), + partition.selected_indices.end()); if (helper_.Parameters().push_all_tasks_toward_start()) { - for (const int index : partition.indices_before_selected) { - const int interval = active_intervals[index]; - // Remove the interval so that we do not use it for precedences. - intervals_to_relax.push_back(interval); - - // Fix all variables. - const std::vector vars = - UsedVariables(helper_.ModelProto().constraints(interval)); - variables_to_fix.insert(variables_to_fix.end(), vars.begin(), vars.end()); - } + intervals_to_relax.insert(intervals_to_relax.end(), + partition.indices_before_selected.begin(), + partition.indices_before_selected.end()); + AppendVarsFromAllIntervalIndices(partition.indices_before_selected, + active_intervals, helper_.ModelProto(), + &variables_to_fix); } gtl::STLSortAndRemoveDuplicates(&intervals_to_relax); @@ -2068,25 +2076,21 @@ Neighborhood SchedulingResourceWindowsNeighborhoodGenerator::Generate( std::vector variables_to_fix; std::vector active_intervals; for (const std::vector& intervals : intervals_in_constraints_) { - helper_.FilterInactiveIntervals(intervals, initial_solution, - &active_intervals); + active_intervals = helper_.KeepActiveIntervals(intervals, initial_solution); const TimePartition partition = PartitionIndicesAroundRandomTimeWindow( active_intervals, helper_.ModelProto(), initial_solution, difficulty, random); - for (const int index : partition.selected_indices) { - intervals_to_relax.push_back(intervals[index]); - } + intervals_to_relax.insert(intervals_to_relax.end(), + partition.selected_indices.begin(), + partition.selected_indices.end()); if (helper_.Parameters().push_all_tasks_toward_start()) { - for (const int index : partition.indices_before_selected) { - const int interval = intervals[index]; - // Remove the interval so that we do not use it for precedences. - intervals_to_relax.push_back(interval); - const std::vector vars = - UsedVariables(helper_.ModelProto().constraints(interval)); - variables_to_fix.insert(variables_to_fix.end(), vars.begin(), - vars.end()); - } + intervals_to_relax.insert(intervals_to_relax.end(), + partition.indices_before_selected.begin(), + partition.indices_before_selected.end()); + AppendVarsFromAllIntervalIndices(partition.indices_before_selected, + active_intervals, helper_.ModelProto(), + &variables_to_fix); } } diff --git a/ortools/sat/cp_model_lns.h b/ortools/sat/cp_model_lns.h index 8fbd5438657..3a9efd35fd4 100644 --- a/ortools/sat/cp_model_lns.h +++ b/ortools/sat/cp_model_lns.h @@ -204,18 +204,11 @@ class NeighborhoodGeneratorHelper : public SubSolver { return absl::MakeSpan(type_to_constraints_[type]); } - // Checks if an interval is active w.r.t. the initial_solution. - // An interval is inactive if and only if it is either unperformed in the - // solution or constant in the model. - bool IntervalIsActive(int index, - const CpSolverResponse& initial_solution) const - ABSL_SHARED_LOCKS_REQUIRED(domain_mutex_); - // Filters a vector of intervals against the initial_solution, and returns // only the active intervals. - void FilterInactiveIntervals(absl::Span unfiltered_intervals, - const CpSolverResponse& initial_solution, - std::vector* filtered_intervals) const; + std::vector KeepActiveIntervals( + absl::Span unfiltered_intervals, + const CpSolverResponse& initial_solution) const; // Returns the list of indices of active interval constraints according // to the initial_solution and the parameter lns_focus_on_performed_intervals. @@ -291,6 +284,13 @@ class NeighborhoodGeneratorHelper : public SubSolver { bool ObjectiveDomainIsConstraining() const ABSL_SHARED_LOCKS_REQUIRED(domain_mutex_); + // Checks if an interval is active w.r.t. the initial_solution. + // An interval is inactive if and only if it is either unperformed in the + // solution or constant in the model. + bool IntervalIsActive(int index, + const CpSolverResponse& initial_solution) const + ABSL_SHARED_LOCKS_REQUIRED(domain_mutex_); + const SatParameters& parameters_; const CpModelProto& model_proto_; int shared_bounds_id_; diff --git a/ortools/sat/cp_model_search.cc b/ortools/sat/cp_model_search.cc index 4d68af9be34..59954e023be 100644 --- a/ortools/sat/cp_model_search.cc +++ b/ortools/sat/cp_model_search.cc @@ -573,7 +573,6 @@ absl::flat_hash_map GetNamedParameters( { SatParameters new_params = base_params; - new_params.set_linearization_level(2); new_params.set_use_objective_shaving_search(true); new_params.set_cp_model_presolve(true); new_params.set_cp_model_probing_level(0); @@ -581,7 +580,14 @@ absl::flat_hash_map GetNamedParameters( if (base_params.use_dual_scheduling_heuristics()) { AddDualSchedulingHeuristics(new_params); } + strategies["objective_shaving_search"] = new_params; + + new_params.set_linearization_level(0); + strategies["objective_shaving_search_no_lp"] = new_params; + + new_params.set_linearization_level(2); + strategies["objective_shaving_search_max_lp"] = new_params; } { @@ -696,44 +702,27 @@ std::vector GetDiverseSetOfParameters( names.push_back(name); } - const int num_workers_to_generate = - base_params.num_workers() - base_params.shared_tree_num_workers(); // We use the default if empty. if (base_params.subsolvers().empty()) { + // Note that the order is important as the list can be truncated. names.push_back("default_lp"); names.push_back("fixed"); names.push_back("core"); - names.push_back("less_encoding"); - names.push_back("no_lp"); names.push_back("max_lp"); - - names.push_back("reduced_costs"); - names.push_back("pseudo_costs"); - names.push_back("quick_restart"); + names.push_back("reduced_costs"); names.push_back("quick_restart_no_lp"); + names.push_back("pseudo_costs"); names.push_back("lb_tree_search"); names.push_back("probing"); + names.push_back("objective_lb_search"); + names.push_back("objective_shaving_search_no_lp"); + names.push_back("objective_shaving_search_max_lp"); + names.push_back("probing_max_lp"); + names.push_back("objective_lb_search_no_lp"); + names.push_back("objective_lb_search_max_lp"); - // Do not add objective_lb_search if core is active and num_workers <= 16. - if (cp_model.has_objective() && - (cp_model.objective().vars().size() == 1 || // core is not active - num_workers_to_generate >= 18)) { - names.push_back("objective_lb_search"); - } - if (num_workers_to_generate >= 20) { - names.push_back("objective_lb_search_no_lp"); - } - if (num_workers_to_generate >= 22) { - names.push_back("objective_shaving_search"); - } - if (num_workers_to_generate >= 24) { - names.push_back("probing_max_lp"); - } - if (num_workers_to_generate >= 28) { - names.push_back("objective_lb_search_max_lp"); - } #if !defined(__PORTABLE_PLATFORM__) && defined(USE_SCIP) if (absl::GetFlag(FLAGS_cp_model_use_max_hs)) names.push_back("max_hs"); #endif // !defined(__PORTABLE_PLATFORM__) && defined(USE_SCIP) @@ -824,34 +813,40 @@ std::vector GetDiverseSetOfParameters( result.push_back(params); } + // In interleaved mode, we run all of them + // TODO(user): Actually make sure the gap num_workers <-> num_heuristics is + // contained. + if (base_params.interleave_search()) return result; + + const int num_non_shared_workers = std::max( + 0, base_params.num_workers() - base_params.shared_tree_num_workers()); + if (cp_model.has_objective() && !cp_model.objective().vars().empty()) { + const auto heuristic_num_workers = [](int num_workers) { + DCHECK_GE(num_workers, 0); + if (num_workers == 1) return 1; + if (num_workers <= 4) return num_workers - 1; + if (num_workers <= 8) return num_workers - 2; + if (num_workers <= 16) return num_workers - (num_workers / 4 + 1); + return num_workers - (num_workers / 2 - 3); + }; + const int target = std::min( + heuristic_num_workers(num_non_shared_workers), result.size()); + // If there is an objective, the extra workers will use LNS. // Make sure we have at least min_num_lns_workers() of them. - const int target = - std::max(base_params.shared_tree_num_workers() > 0 ? 0 : 1, - num_workers_to_generate - base_params.min_num_lns_workers()); - if (!base_params.interleave_search() && result.size() > target) { - result.resize(target); - } - } else { + if (result.size() > target) result.resize(target); + } else { // No objective. // If strategies that do not require a full worker are present, leave a // few workers for them. const bool need_extra_workers = - !base_params.interleave_search() && (base_params.use_rins_lns() || base_params.use_feasibility_pump()); - int target = num_workers_to_generate; - if (need_extra_workers && target > 4) { - if (target <= 8) { - target -= 1; - } else if (target == 9) { - target -= 2; - } else { - target -= 3; - } - } - if (!base_params.interleave_search() && result.size() > target) { - result.resize(target); - } + // Currently, we have 8 SAT search heuristics. So + const int num_extra_workers = + num_non_shared_workers <= 4 ? 0 : 1 + need_extra_workers; + const int target = std::min(num_non_shared_workers - num_extra_workers, + result.size()); + if (result.size() > target) result.resize(target); } return result; } diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index ab36cc2a802..1883cc4329f 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -126,11 +126,6 @@ ABSL_FLAG(bool, cp_model_dump_models, false, "format to 'FLAGS_cp_model_dump_prefix'{model|presolved_model|" "mapping_model}.pb.txt."); -ABSL_FLAG(bool, cp_model_export_model, false, - "DEBUG ONLY. When set to true, SolveCpModel() will dump the model " - "proto of the original model format to " - "'FLAGS_cp_model_dump_prefix'{name}.pb.txt."); - ABSL_FLAG(bool, cp_model_dump_text_proto, true, "DEBUG ONLY, dump models in text proto instead of binary proto."); @@ -1537,6 +1532,7 @@ void LoadBaseModel(const CpModelProto& model_proto, Model* model) { model->GetOrCreate()->ProcessImplicationGraph( model->GetOrCreate()); + model->GetOrCreate()->Build(); } void LoadFeasibilityPump(const CpModelProto& model_proto, Model* model) { @@ -3051,18 +3047,6 @@ class LnsSolver : public SubSolver { if (!neighborhood.is_generated) return; - const int64_t num_calls = std::max(int64_t{1}, generator_->num_calls()); - const double fully_solved_proportion = - static_cast(generator_->num_fully_solved_calls()) / - static_cast(num_calls); - std::string source_info = - neighborhood.source_info.empty() ? name() : neighborhood.source_info; - const std::string lns_info = - absl::StrFormat("%s(d=%0.2f s=%i t=%0.2f p=%0.2f stall=%d)", - source_info, data.difficulty, task_id, - data.deterministic_limit, fully_solved_proportion, - generator_->num_consecutive_non_improving_calls()); - SatParameters local_params(parameters_); local_params.set_max_deterministic_time(data.deterministic_limit); local_params.set_stop_after_first_solution(false); @@ -3072,8 +3056,35 @@ class LnsSolver : public SubSolver { local_params.set_symmetry_level(0); local_params.set_find_big_linear_overlap(false); local_params.set_solution_pool_size(1); // Keep the best solution found. - local_params.set_search_branching(SatParameters::PORTFOLIO_SEARCH); - local_params.set_search_random_variable_pool_size(5); + + // TODO(user): Tune these. + // TODO(user): This could be a good candidate for bandits. + const int64_t stall = generator_->num_consecutive_non_improving_calls(); + const int search_index = stall < 10 ? 0 : task_id % 2; + absl::string_view search_info; + switch (search_index) { + case 0: + local_params.set_search_branching(SatParameters::AUTOMATIC_SEARCH); + local_params.set_linearization_level(0); + search_info = "auto_l0"; + break; + default: + local_params.set_search_branching(SatParameters::PORTFOLIO_SEARCH); + local_params.set_search_random_variable_pool_size(5); + search_info = "folio_rnd"; + break; + } + + std::string source_info = + neighborhood.source_info.empty() ? name() : neighborhood.source_info; + const int64_t num_calls = std::max(int64_t{1}, generator_->num_calls()); + const double fully_solved_proportion = + static_cast(generator_->num_fully_solved_calls()) / + static_cast(num_calls); + const std::string lns_info = absl::StrFormat( + "%s(d=%0.2f s=%i t=%0.2f p=%0.2f stall=%d h=%s)", source_info, + data.difficulty, task_id, data.deterministic_limit, + fully_solved_proportion, stall, search_info); Model local_model(lns_info); *(local_model.GetOrCreate()) = local_params; @@ -3432,7 +3443,7 @@ void SolveCpModelParallel(const CpModelProto& model_proto, int num_full_problem_solvers = 0; if (testing) { // Register something to find a first solution. Note that this is mainly - // used for experimentation, and using no LP ususally result in a faster + // used for experimentation, and using no_lp usually result in a faster // first solution. // // TODO(user): merge code with standard solver. Just make sure that all @@ -3531,7 +3542,7 @@ void SolveCpModelParallel(const CpModelProto& model_proto, if (!model_proto.has_objective() || model_proto.objective().vars().empty() || !params.interleave_search() || params.test_feasibility_jump()) { const int max_num_incomplete_solvers_running_before_the_first_solution = - params.num_workers() <= 8 ? 1 : (params.num_workers() <= 16 ? 2 : 3); + params.num_workers() <= 16 ? 1 : 2; const int num_reserved_incomplete_solvers = params.test_feasibility_jump() ? 0 @@ -3980,10 +3991,6 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { if (absl::GetFlag(FLAGS_cp_model_dump_models)) { DumpModelProto(model_proto, "model"); } - if (absl::GetFlag(FLAGS_cp_model_export_model)) { - DumpModelProto(model_proto, - model_proto.name().empty() ? "model" : model_proto.name()); - } #endif // __PORTABLE_PLATFORM__ #if !defined(__PORTABLE_PLATFORM__) diff --git a/ortools/sat/diffn.cc b/ortools/sat/diffn.cc index a3f42e3f72f..e5afafcf1c8 100644 --- a/ortools/sat/diffn.cc +++ b/ortools/sat/diffn.cc @@ -32,6 +32,7 @@ #include "ortools/sat/intervals.h" #include "ortools/sat/linear_constraint.h" #include "ortools/sat/model.h" +#include "ortools/sat/precedences.h" #include "ortools/sat/sat_base.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/sat/timetable.h" @@ -282,8 +283,12 @@ NonOverlappingRectanglesDisjunctivePropagator:: x_(x->NumTasks(), model), watcher_(model->GetOrCreate()), overload_checker_(&x_), - forward_detectable_precedences_(true, &x_), - backward_detectable_precedences_(false, &x_), + forward_detectable_precedences_( + true, model->GetOrCreate(), + model->GetOrCreate(), &x_), + backward_detectable_precedences_( + false, model->GetOrCreate(), + model->GetOrCreate(), &x_), forward_not_last_(true, &x_), backward_not_last_(false, &x_), forward_edge_finding_(true, &x_), diff --git a/ortools/sat/disjunctive.cc b/ortools/sat/disjunctive.cc index 6c3b700de96..b75af997af7 100644 --- a/ortools/sat/disjunctive.cc +++ b/ortools/sat/disjunctive.cc @@ -19,6 +19,7 @@ #include "absl/algorithm/container.h" #include "absl/log/check.h" +#include "ortools/base/logging.h" #include "ortools/sat/all_different.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_expr.h" @@ -102,7 +103,9 @@ std::function Disjunctive( } for (const bool time_direction : {true, false}) { DisjunctiveDetectablePrecedences* detectable_precedences = - new DisjunctiveDetectablePrecedences(time_direction, helper); + new DisjunctiveDetectablePrecedences( + time_direction, model->GetOrCreate(), + model->GetOrCreate(), helper); const int id = detectable_precedences->RegisterWith(watcher); watcher->SetPropagatorPriority(id, 2); model->TakeOwnership(detectable_precedences); @@ -867,25 +870,71 @@ bool DisjunctiveDetectablePrecedences::PropagateSubwindow() { // We need: // - StartMax(ct) < EndMin(t) for the detectable precedence. // - StartMin(ct) >= window_start for the value of task_set_end_min. + const AffineExpression after = helper_->Starts()[t]; const IntegerValue end_min_if_present = helper_->ShiftedStartMin(t) + helper_->SizeMin(t); const IntegerValue window_start = sorted_tasks[critical_index].start_min; + IntegerValue min_slack = kMaxIntegerValue; + bool all_statically_before = true; for (int i = critical_index; i < sorted_tasks.size(); ++i) { const int ct = sorted_tasks[i].task; DCHECK_NE(ct, t); helper_->AddPresenceReason(ct); helper_->AddEnergyAfterReason(ct, sorted_tasks[i].size_min, window_start); - helper_->AddStartMaxReason(ct, end_min_if_present - 1); + + // We only need the reason for being before if we don't already have + // a static precedence between the tasks. + const AffineExpression before = helper_->Ends()[ct]; + const IntegerValue needed = before.constant - after.constant; + const IntegerValue known = + precedence_relations_->GetOffset(before.var, after.var); + if (known < needed || before.coeff != 1 || after.coeff != 1) { + all_statically_before = false; + helper_->AddStartMaxReason(ct, end_min_if_present - 1); + } else { + min_slack = std::min(min_slack, known - needed); + } + } + + // We only need the end-min of t if not all the task are already known + // to be before. + IntegerValue new_start_min = task_set_end_min; + if (all_statically_before) { + // We can actually push further ! + new_start_min += min_slack; + } else { + helper_->AddEndMinReason(t, end_min_if_present); } - // Add the reason for t (we only need the end-min). - helper_->AddEndMinReason(t, end_min_if_present); + // If we detect precedences at level zero, lets add them to the + // precedence propagator. The hope is that this leads to faster + // propagation with better reason if they ever trigger. + if (helper_->CurrentDecisionLevel() == 0 && helper_->IsPresent(t)) { + if (after.coeff == 1 && after.var != kNoIntegerVariable) { + for (int i = critical_index; i < sorted_tasks.size(); ++i) { + const AffineExpression before = + helper_->Ends()[sorted_tasks[i].task]; + if (before.coeff == 1 && before.var != kNoIntegerVariable) { + const IntegerValue offset = before.constant - after.constant; + precedence_relations_->UpdateOffset(before.var, after.var, + offset); + if (precedences_->AddPrecedenceWithOffsetIfNew( + before.var, after.var, offset)) { + VLOG(2) << t << " after #" + << sorted_tasks.size() - critical_index << " " + << before.DebugString() << " " << after.DebugString() + << " " << offset; + } + } + } + } + } // This augment the start-min of t. Note that t is not in task set // yet, so we will use this updated start if we ever add it there. - if (!helper_->IncreaseStartMin(t, task_set_end_min)) { + if (!helper_->IncreaseStartMin(t, new_start_min)) { return false; } diff --git a/ortools/sat/disjunctive.h b/ortools/sat/disjunctive.h index 42f39bcf0e4..01336de205e 100644 --- a/ortools/sat/disjunctive.h +++ b/ortools/sat/disjunctive.h @@ -159,8 +159,12 @@ class DisjunctiveOverloadChecker : public PropagatorInterface { class DisjunctiveDetectablePrecedences : public PropagatorInterface { public: DisjunctiveDetectablePrecedences(bool time_direction, + PrecedenceRelations* relations, + PrecedencesPropagator* precedences, SchedulingConstraintHelper* helper) : time_direction_(time_direction), + precedence_relations_(relations), + precedences_(precedences), helper_(helper), task_set_(helper->NumTasks()) {} bool Propagate() final; @@ -176,6 +180,8 @@ class DisjunctiveDetectablePrecedences : public PropagatorInterface { std::vector to_propagate_; const bool time_direction_; + PrecedenceRelations* precedence_relations_; + PrecedencesPropagator* precedences_; SchedulingConstraintHelper* helper_; TaskSet task_set_; }; diff --git a/ortools/sat/intervals.h b/ortools/sat/intervals.h index 28e95cb1dce..bf008072246 100644 --- a/ortools/sat/intervals.h +++ b/ortools/sat/intervals.h @@ -433,6 +433,8 @@ class SchedulingConstraintHelper : public PropagatorInterface, // not handle this correctly. bool InPropagationLoop() const { return integer_trail_->InPropagationLoop(); } + int CurrentDecisionLevel() const { return trail_->CurrentDecisionLevel(); } + private: // Generic reason for a <= upper_bound, given that a = b + c in case the // current upper bound of a is not good enough. diff --git a/ortools/sat/precedences.cc b/ortools/sat/precedences.cc index afa65fa271e..e94a22a66fa 100644 --- a/ortools/sat/precedences.cc +++ b/ortools/sat/precedences.cc @@ -71,7 +71,7 @@ void PrecedenceRelations::Add(IntegerVariable tail, IntegerVariable head, } void PrecedenceRelations::Build() { - CHECK(!is_built_); + if (is_built_) return; is_built_ = true; std::vector permutation; @@ -103,6 +103,49 @@ void PrecedenceRelations::Build() { } } is_dag_ = !graph_has_cycle; + + // Lets build full precedences if we don't have too many of them. + // TODO(user): Also do that if we don't have a DAG? + if (!is_dag_) return; + + int work = 0; + const int kWorkLimit = 1e6; + absl::StrongVector> before( + graph_.num_nodes()); + const auto add = [&before, this](IntegerVariable a, IntegerVariable b, + IntegerValue offset) { + const auto [it, inserted] = all_relations_.insert({{a, b}, offset}); + if (inserted) { + before[b].push_back(a); + } else { + it->second = std::max(it->second, offset); + } + }; + + // TODO(user): We probably do not need to do both var and its negation. + for (const IntegerVariable tail_var : topological_order_) { + if (++work > kWorkLimit) break; + for (const int arc : graph_.OutgoingArcs(tail_var.value())) { + CHECK_EQ(tail_var.value(), graph_.Tail(arc)); + const IntegerVariable head_var = IntegerVariable(graph_.Head(arc)); + const IntegerValue arc_offset = arc_offset_[arc]; + + if (++work > kWorkLimit) break; + add(tail_var, head_var, arc_offset); + add(NegationOf(head_var), NegationOf(tail_var), -arc_offset); + + for (const IntegerVariable before_var : before[tail_var]) { + if (++work > kWorkLimit) break; + const IntegerValue offset = + all_relations_.at({before_var, tail_var}) + arc_offset; + add(before_var, head_var, offset); + add(NegationOf(head_var), NegationOf(before_var), -offset); + } + } + } + + VLOG(2) << "Full precedences. Work=" << work + << " Relations=" << all_relations_.size(); } void PrecedenceRelations::ComputeFullPrecedences( @@ -589,6 +632,29 @@ void PrecedencesPropagator::AddArc( } } +bool PrecedencesPropagator::AddPrecedenceWithOffsetIfNew(IntegerVariable i1, + IntegerVariable i2, + IntegerValue offset) { + DCHECK_EQ(trail_->CurrentDecisionLevel(), 0); + if (i1 < impacted_arcs_.size() && i2 < impacted_arcs_.size()) { + for (const ArcIndex index : impacted_arcs_[i1]) { + const ArcInfo& arc = arcs_[index]; + if (arc.head_var == i2) { + const IntegerValue current = ArcOffset(arc); + if (offset <= current) { + return false; + } else { + // TODO(user): Modify arc in place! + } + break; + } + } + } + + AddPrecedenceWithOffset(i1, i2, offset); + return true; +} + // TODO(user): On jobshop problems with a lot of tasks per machine (500), this // takes up a big chunk of the running time even before we find a solution. // This is because, for each lower bound changed, we inspect 500 arcs even diff --git a/ortools/sat/precedences.h b/ortools/sat/precedences.h index d52984db6a2..747dce55d52 100644 --- a/ortools/sat/precedences.h +++ b/ortools/sat/precedences.h @@ -78,9 +78,40 @@ class PrecedenceRelations { void ComputeFullPrecedences(const std::vector& vars, std::vector* output); - private: + // If we don't have too many variable, we compute the full transtive closure + // and can query in O(1) if there is a relation between two variables. + // This can be used to optimize some scheduling propagation and reasons. + // + // Warning: If we there are too many, this will NOT contain all relations. + // + // Returns kMinIntegerValue if there are none. + // Otherwise a + offset <= b. + IntegerValue GetOffset(IntegerVariable a, IntegerVariable b) { + const auto it = all_relations_.find({a, b}); + return it == all_relations_.end() ? kMinIntegerValue : it->second; + } + + // Update the hash table of precedence relation. + void UpdateOffset(IntegerVariable a, IntegerVariable b, IntegerValue offset) { + InternalUpdate(a, b, offset); + InternalUpdate(NegationOf(b), NegationOf(a), -offset); + } + + // The current code requires the internal data to be processed once all + // relations are loaded. + // + // TODO(user): Be more dynamic as we start to add relations during search. void Build(); + private: + void InternalUpdate(IntegerVariable a, IntegerVariable b, + IntegerValue offset) { + const auto [it, inserted] = all_relations_.insert({{a, b}, offset}); + if (!inserted) { + it->second = std::max(it->second, offset); + } + } + IntegerTrail* integer_trail_; util::StaticGraph<> graph_; @@ -89,6 +120,9 @@ class PrecedenceRelations { bool is_built_ = false; bool is_dag_ = false; std::vector topological_order_; + + absl::flat_hash_map, IntegerValue> + all_relations_; }; // This class implement a propagator on simple inequalities between integer @@ -159,6 +193,10 @@ class PrecedencesPropagator : public SatPropagator, PropagatorInterface { IntegerVariable offset_var, absl::Span presence_literals); + // This version check current precedence. It is however "slow". + bool AddPrecedenceWithOffsetIfNew(IntegerVariable i1, IntegerVariable i2, + IntegerValue offset); + // Finds all the IntegerVariable that are "after" at least two of the // IntegerVariable in vars. Returns a vector of these precedences relation // sorted by IntegerPrecedences.var so that it is efficient to find all the diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index 5109a7dd759..e30ba2498e4 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -567,11 +567,7 @@ message SatParameters { optional int32 num_workers = 206 [default = 0]; optional int32 num_search_workers = 100 [default = 0]; - // If there is an objective and we are not in interleave mode, we will reserve - // at least this number of worker for LNS thread. - // - // TODO(user): Also define like for subsolvers the list of "active" type of - // neighborhood used. + // Obsolete parameter. No-op. optional int32 min_num_lns_workers = 211 [default = 2]; // In multi-thread, the solver can be mainly seen as a portfolio of solvers @@ -582,7 +578,6 @@ message SatParameters { // left empty) that looks like: // - default_lp (linearization_level:1) // - fixed (only if fixed search specified or scheduling) - // - less_encoding (only if no objective) // - no_lp (linearization_level:0) // - max_lp (linearization_level:2) // - pseudo_costs (only if objective, change search heuristic) @@ -618,6 +613,9 @@ message SatParameters { // possible to overwrite the default names above. repeated SatParameters subsolver_params = 210; + // TODO(user): Also define like for subsolvers the list of "active" + // type of neighborhood used. + // Experimental. If this is true, then we interleave all our major search // strategy and distribute the work amongst num_workers. // @@ -1278,7 +1276,7 @@ message SatParameters { // Experimental code: specify if the objective pushes all tasks toward the // start of the schedule. - optional bool push_all_tasks_toward_start = 262 [default = true]; + optional bool push_all_tasks_toward_start = 262 [default = false]; // If true, we automatically detect variables whose constraint are always // enforced by the same literal and we mark them as optional. This allows diff --git a/ortools/sat/subsolver.cc b/ortools/sat/subsolver.cc index fe3e7d84702..d3cfc459fd2 100644 --- a/ortools/sat/subsolver.cc +++ b/ortools/sat/subsolver.cc @@ -28,6 +28,7 @@ #include "absl/time/clock.h" #include "absl/time/time.h" #include "ortools/base/logging.h" +#include "ortools/base/timer.h" #if !defined(__PORTABLE_PLATFORM__) #include "ortools/base/threadpool.h" #endif // __PORTABLE_PLATFORM__ @@ -88,7 +89,11 @@ void SequentialLoop(std::vector>& subsolvers) { const int best = NextSubsolverToSchedule(subsolvers, num_generated_tasks); if (best == -1) break; num_generated_tasks[best]++; + + WallTimer timer; + timer.Start(); subsolvers[best]->GenerateTask(task_id++)(); + subsolvers[best]->AddTaskDuration(timer.Get()); } }