From f36a11c9a9e7b5f167d1a62107b1e23054ae3440 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Mon, 26 Jun 2023 18:15:28 +0200 Subject: [PATCH] [CP-SAT] new Lns; tweak log --- ortools/sat/BUILD.bazel | 1 + ortools/sat/cp_model_lns.cc | 37 ++++--- ortools/sat/cp_model_lns.h | 22 ++++- ortools/sat/cp_model_presolve.cc | 15 --- ortools/sat/cp_model_search.cc | 4 +- ortools/sat/cp_model_solver.cc | 142 ++++++++++++++++++++------- ortools/sat/feasibility_jump.cc | 6 +- ortools/sat/feasibility_pump.cc | 4 +- ortools/sat/parameters_validation.cc | 16 +-- ortools/sat/rins.cc | 7 +- ortools/sat/sat_parameters.proto | 13 ++- ortools/sat/subsolver.cc | 37 +++++-- ortools/sat/subsolver.h | 17 ++++ ortools/sat/synchronization.cc | 51 +++++----- ortools/sat/synchronization.h | 49 +++++++-- ortools/sat/util.h | 15 +++ 16 files changed, 309 insertions(+), 127 deletions(-) diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index e2e97995174..349dc539dd5 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -1556,6 +1556,7 @@ cc_library( deps = [ "//ortools/base", "//ortools/base:threadpool", + "//ortools/util:stats", "//ortools/util:time_limit", "@com_google_absl//absl/synchronization", "@com_google_absl//absl/time", diff --git a/ortools/sat/cp_model_lns.cc b/ortools/sat/cp_model_lns.cc index b37adb1c902..f7c54316446 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -228,7 +228,7 @@ void NeighborhoodGeneratorHelper::RecomputeHelperData() { // linear constraint somewhere else. This is not the case if we have a fixed // size optional interval variable. But it should not matter as the // intervals are replaced by their underlying variables in the scheduling - // constrainst. + // constraints. if (constraints[ct_index].constraint_case() == ConstraintProto::kInterval) { continue; } @@ -555,7 +555,7 @@ struct Demand { // Because of the binary splitting of the capacity in the procedure used to // extract precedences out of a cumulative constraint, processing bigger - // heigts first will decrease its probability of being split across the 2 + // heights first will decrease its probability of being split across the 2 // halves of the current split. bool operator<(const Demand& other) const { return std::tie(start, height, end) < @@ -1171,6 +1171,10 @@ void NeighborhoodGenerator::Synchronize() { ++num_consecutive_non_improving_calls_; } + if (data.base_objective > data.new_objective) { + ++num_improving_calls_; + } + // TODO(user): Weight more recent data. // degrade the current average to forget old learnings. const double gain_per_time_unit = @@ -1255,18 +1259,21 @@ Neighborhood RelaxRandomConstraintsGenerator::Generate( const int num_active_vars = helper_.ActiveVariablesWhileHoldingLock().size(); const int target_size = std::ceil(difficulty * num_active_vars); - DCHECK_GT(target_size, 0); + if (target_size == num_active_vars) return helper_.FullNeighborhood(); + // TODO(user): Clean-up when target_size == 0. for (const int constraint_index : active_constraints) { + // TODO(user): randomize order of variable addition when close to the + // limit. for (const int var : helper_.ConstraintToVar()[constraint_index]) { if (visited_variables_set[var]) continue; visited_variables_set[var] = true; if (helper_.IsActive(var)) { relaxed_variables.push_back(var); - if (relaxed_variables.size() == target_size) break; + if (relaxed_variables.size() >= target_size) break; } } - if (relaxed_variables.size() == target_size) break; + if (relaxed_variables.size() >= target_size) break; } } @@ -1295,13 +1302,17 @@ Neighborhood VariableGraphNeighborhoodGenerator::Generate( // We read the exact number while locked. const int num_active_vars = helper_.ActiveVariablesWhileHoldingLock().size(); + const int num_objective_variables = + helper_.ActiveObjectiveVariablesWhileHoldingLock().size(); const int target_size = std::ceil(difficulty * num_active_vars); - if (target_size == 0) return helper_.FullNeighborhood(); + if (target_size == num_active_vars) return helper_.FullNeighborhood(); const int first_var = - helper_.ActiveVariablesWhileHoldingLock()[absl::Uniform( - random, 0, num_active_vars)]; - + num_objective_variables > 0 // Prefer objective variables. + ? helper_.ActiveObjectiveVariablesWhileHoldingLock() + [absl::Uniform(random, 0, num_objective_variables)] + : helper_.ActiveVariablesWhileHoldingLock()[absl::Uniform( + random, 0, num_active_vars)]; visited_variables_set[first_var] = true; visited_variables.push_back(first_var); relaxed_variables.push_back(first_var); @@ -1362,7 +1373,7 @@ Neighborhood ConstraintGraphNeighborhoodGenerator::Generate( const int num_active_vars = helper_.ActiveVariablesWhileHoldingLock().size(); const int target_size = std::ceil(difficulty * num_active_vars); - if (target_size == 0) return helper_.FullNeighborhood(); + if (target_size == num_active_vars) return helper_.FullNeighborhood(); // Start by a random constraint. const int num_active_constraints = helper_.ConstraintToVar().size(); @@ -1393,7 +1404,7 @@ Neighborhood ConstraintGraphNeighborhoodGenerator::Generate( if (helper_.IsActive(var)) { relaxed_variables.push_back(var); } - if (relaxed_variables.size() == target_size) break; + if (relaxed_variables.size() >= target_size) break; for (const int ct : helper_.VarToConstraint()[var]) { if (added_constraints[ct]) continue; @@ -1425,7 +1436,7 @@ Neighborhood DecompositionGraphNeighborhoodGenerator::Generate( const int num_active_vars = helper_.ActiveVariablesWhileHoldingLock().size(); const int target_size = std::ceil(difficulty * num_active_vars); - if (target_size == 0) return helper_.FullNeighborhood(); + if (target_size == num_active_vars) return helper_.FullNeighborhood(); const int num_vars = helper_.VarToConstraint().size(); const int num_constraints = helper_.ConstraintToVar().size(); @@ -1917,7 +1928,7 @@ Neighborhood RoutingFullPathNeighborhoodGenerator::Generate( } bool RelaxationInducedNeighborhoodGenerator::ReadyToGenerate() const { - return (incomplete_solutions_->HasNewSolution() || + return (incomplete_solutions_->HasSolution() || lp_solutions_->NumSolutions() > 0); } diff --git a/ortools/sat/cp_model_lns.h b/ortools/sat/cp_model_lns.h index 47d60626c11..a74fd9699be 100644 --- a/ortools/sat/cp_model_lns.h +++ b/ortools/sat/cp_model_lns.h @@ -174,6 +174,15 @@ class NeighborhoodGeneratorHelper : public SubSolver { return active_variables_; } + // Returns the vector of active objective variables. The graph_mutex_ must be + // locked before calling this method. + std::vector ActiveObjectiveVariablesWhileHoldingLock() const + ABSL_SHARED_LOCKS_REQUIRED(graph_mutex_) { + std::vector result; + result = active_objective_variables_; + return result; + } + // Constraints <-> Variables graph. // Note that only non-constant variable are listed here. const std::vector>& ConstraintToVar() const @@ -203,8 +212,8 @@ class NeighborhoodGeneratorHelper : public SubSolver { // cumulative, or as a dimension of a no_overlap_2d constraint. std::vector> GetUniqueIntervalSets() const; - // Returns one sub-vector per circuit or per single vehicle ciruit in a routes - // constraints. Each circuit is non empty, and does not contain any + // Returns one sub-vector per circuit or per single vehicle circuit in a + // routes constraints. Each circuit is non empty, and does not contain any // self-looping arcs. Path are sorted, starting from the arc with the lowest // tail index, and going in sequence up to the last arc before the circuit is // closed. Each entry correspond to the arc literal on the circuit. @@ -357,7 +366,7 @@ class NeighborhoodGenerator { // The difficulty when this neighborhood was generated. double difficulty = 0.0; - // The determinitic time limit given to the solver for this neighborhood. + // The deterministic time limit given to the solver for this neighborhood. double deterministic_limit = 0.0; // The time it took to solve this neighborhood. @@ -410,6 +419,12 @@ class NeighborhoodGenerator { return num_fully_solved_calls_; } + // Out of num_calls(), how many improved the given solution. + int64_t num_improving_calls() const { + absl::MutexLock mutex_lock(&generator_mutex_); + return num_improving_calls_; + } + // The current difficulty of this generator double difficulty() const { absl::MutexLock mutex_lock(&generator_mutex_); @@ -445,6 +460,7 @@ class NeighborhoodGenerator { // Only updated on Synchronize(). int64_t num_calls_ = 0; int64_t num_fully_solved_calls_ = 0; + int64_t num_improving_calls_ = 0; int64_t num_consecutive_non_improving_calls_ = 0; double deterministic_time_ = 0.0; double current_average_ = 0.0; diff --git a/ortools/sat/cp_model_presolve.cc b/ortools/sat/cp_model_presolve.cc index 6b010bd90fd..480fcb2cc5a 100644 --- a/ortools/sat/cp_model_presolve.cc +++ b/ortools/sat/cp_model_presolve.cc @@ -38,7 +38,6 @@ #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/types/span.h" -#include "ortools/base/integral_types.h" #include "ortools/base/logging.h" #include "ortools/base/mathutil.h" #include "ortools/base/stl_util.h" @@ -709,20 +708,6 @@ bool CpModelPresolver::PresolveLinMax(ConstraintProto* ct) { if (context_->ModelIsUnsat()) return false; if (HasEnforcementLiteral(*ct)) return false; - int64_t min_offset = std::numeric_limits::max(); - for (const LinearExpressionProto& expr : ct->lin_max().exprs()) { - min_offset = std::min(min_offset, expr.offset()); - } - if (min_offset != std::numeric_limits::max() && min_offset != 0) { - LinearArgumentProto* lin_max = ct->mutable_lin_max(); - lin_max->mutable_target()->set_offset(lin_max->target().offset() - - min_offset); - for (LinearExpressionProto& expr : *(lin_max->mutable_exprs())) { - expr.set_offset(expr.offset() - min_offset); - } - context_->UpdateRuleStats("lin_max: shift offset"); - } - const LinearExpressionProto& target = ct->lin_max().target(); // x = max(x, xi...) => forall i, x >= xi. diff --git a/ortools/sat/cp_model_search.cc b/ortools/sat/cp_model_search.cc index 258ded33b10..79d523f8a6c 100644 --- a/ortools/sat/cp_model_search.cc +++ b/ortools/sat/cp_model_search.cc @@ -818,7 +818,7 @@ std::vector GetFirstSolutionParams( new_params.set_randomize_search(true); new_params.set_search_randomization_tolerance(num_random + 1); new_params.set_random_seed(ValidSumSeed(base_seed, 2 * num_random + 1)); - new_params.set_name(absl::StrCat("random_", num_random)); + new_params.set_name("random"); num_random++; } else { // Random quick restart. new_params.set_search_branching( @@ -826,7 +826,7 @@ std::vector GetFirstSolutionParams( new_params.set_randomize_search(true); new_params.set_search_randomization_tolerance(num_random_qr + 1); new_params.set_random_seed(ValidSumSeed(base_seed, 2 * num_random_qr)); - new_params.set_name(absl::StrCat("random_quick_restart_", num_random_qr)); + new_params.set_name("random_quick_restart"); num_random_qr++; } result.push_back(new_params); diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index 48eb09097e0..94fdb2f2d90 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -2585,6 +2585,18 @@ class FullProblemSolver : public SubSolver { dtime_since_last_sync_ = 0.0; } + std::string OneLineStats() const override { + CpSolverResponse r; + FillSolveStatsInResponse(local_model_.get(), &r); + return absl::StrCat( + RightAlign(FormatCounter(r.num_booleans())), + RightAlign(FormatCounter(r.num_conflicts())), + RightAlign(FormatCounter(r.num_branches())), + RightAlign(FormatCounter(r.num_restarts())), + RightAlign(FormatCounter(r.num_binary_propagations())), + RightAlign(FormatCounter(r.num_integer_propagations()))); + } + std::string StatisticsString() const override { // Padding. const std::string p4(4, ' '); @@ -3250,7 +3262,18 @@ class LnsSolver : public SubSolver { shared_->time_limit->AdvanceDeterministicTime(deterministic_time_ - old); } - // TODO(user): Display LNS success rate. + std::string OneLineStats() const override { + const double fully_solved_proportion = + static_cast(generator_->num_fully_solved_calls()) / + static_cast(std::max(int64_t{1}, generator_->num_calls())); + return absl::StrCat( + RightAlign(absl::StrCat(generator_->num_improving_calls(), "/", + generator_->num_calls())), + RightAlign(absl::StrFormat("%2.0f%%", 100 * fully_solved_proportion)), + RightAlign(absl::StrFormat("%0.2f", generator_->difficulty())), + RightAlign(absl::StrFormat("%0.2f", generator_->deterministic_limit())), + TimingInfo()); + } private: std::unique_ptr generator_; @@ -3399,27 +3422,20 @@ void SolveCpModelParallel(const CpModelProto& model_proto, !params.interleave_search() && helper->TypeToConstraints(ConstraintProto::kNoOverlap2D).empty(); const LinearModel* linear_model = global_model->Get(); - if (params.use_violation_ls() && feasibility_jump_possible && + if (params.num_violation_ls() > 0 && feasibility_jump_possible && model_proto.has_objective()) { - if (params.test_feasibility_jump()) { - for (int i = 0; i < params.num_workers(); ++i) { - SatParameters local_params = params; - local_params.set_random_seed(ValidSumSeed(params.random_seed(), i)); - local_params.set_feasibility_jump_decay(((i / 2) % 2) == 1 ? 0.95 - : 1.0); - incomplete_subsolvers.push_back(std::make_unique( - absl::StrCat("violation_ls_", i), SubSolver::INCOMPLETE, - linear_model, local_params, shared.time_limit, shared.response, - shared.bounds.get(), shared.stats)); - } - } else { + const int num_violation_ls = params.test_feasibility_jump() + ? params.num_workers() + : params.num_violation_ls(); + for (int i = 0; i < num_violation_ls; ++i) { SatParameters local_params = params; - local_params.set_random_seed(params.random_seed()); - local_params.set_feasibility_jump_decay(0.95); + local_params.set_random_seed(ValidSumSeed(params.random_seed(), i)); + const bool use_decay = i % 2 == 1; + local_params.set_feasibility_jump_decay(use_decay ? 0.95 : 1.0); incomplete_subsolvers.push_back(std::make_unique( - "violation_ls", SubSolver::INCOMPLETE, linear_model, local_params, - shared.time_limit, shared.response, shared.bounds.get(), - shared.stats)); + (use_decay ? "violation_ls_decay" : "violation_ls"), + SubSolver::INCOMPLETE, linear_model, local_params, shared.time_limit, + shared.response, shared.bounds.get(), shared.stats)); } } @@ -3480,38 +3496,37 @@ void SolveCpModelParallel(const CpModelProto& model_proto, case 1: { // Adds randomized values on restart and decay. local_params.set_feasibility_jump_decay(0.95); local_params.set_feasibility_jump_var_randomization_probability(0.05); - name = "jump_decay_random_values_on_restarts_0"; + name = "jump_decay_rnd_on_rst"; break; } case 2: { // Adds perturbation and decay. local_params.set_feasibility_jump_decay(0.95); local_params.set_feasibility_jump_var_randomization_probability(0.05); local_params.set_feasibility_jump_enable_restarts(false); - name = "jump_decay_random_perturbations_0"; + name = "jump_decay_perturb"; break; } case 3: { // Disable restarts and perturbations. local_params.set_feasibility_jump_var_randomization_probability(0.0); local_params.set_feasibility_jump_enable_restarts(false); - name = "jump_no_restarts"; + name = "jump_no_rst"; break; } case 4: { // Adds decay and disable restarts and perturbations. local_params.set_feasibility_jump_decay(0.95); local_params.set_feasibility_jump_var_randomization_probability(0.0); local_params.set_feasibility_jump_enable_restarts(false); - name = "jump_decay_no_restarts"; + name = "jump_decay_no_rst"; break; } default: { // Alternate random_restarts and random_perturbations. - const int index = (i - 3) / 2; // starts at 1. local_params.set_feasibility_jump_decay(0.95); local_params.set_feasibility_jump_var_randomization_probability(0.05); if (i % 2 == 0) { // Adds randomized restart and decay. - name = absl::StrCat("jump_decay_random_values_on_restarts_", index); + name = "jump_decay_rnd_on_rst"; } else { // Adds perturbation and decay. local_params.set_feasibility_jump_enable_restarts(false); - name = absl::StrCat("jump_decay_random_perturbations_", index); + name = "jump_decay_perturb"; } } } @@ -3581,11 +3596,11 @@ void SolveCpModelParallel(const CpModelProto& model_proto, !helper->TypeToConstraints(ConstraintProto::kCumulative).empty()) { subsolvers.push_back(std::make_unique( std::make_unique( - helper, "scheduling_random_intervals_lns"), + helper, "scheduling_intervals_lns"), params, helper, &shared)); subsolvers.push_back(std::make_unique( std::make_unique( - helper, "scheduling_random_precedences_lns"), + helper, "scheduling_precedences_lns"), params, helper, &shared)); subsolvers.push_back(std::make_unique( std::make_unique( @@ -3678,9 +3693,27 @@ void SolveCpModelParallel(const CpModelProto& model_proto, const std::vector& names, const absl::string_view type_name) { if (!names.empty()) { + absl::btree_map solvers_and_count; + for (const auto& name : names) { + solvers_and_count[name]++; + } + std::string solver_list; + bool first = true; + for (const auto& [name, count] : solvers_and_count) { + if (first) { + first = false; + } else { + absl::StrAppend(&solver_list, ", "); + } + if (count == 1) { + absl::StrAppend(&solver_list, name); + } else { + absl::StrAppend(&solver_list, name, "(", count, ")"); + } + } SOLVER_LOG(logger, names.size(), " ", absl::StrCat(type_name, names.size() == 1 ? "" : "s"), ": [", - absl::StrJoin(names.begin(), names.end(), ", "), "]"); + solver_list, "]"); } }; @@ -3709,23 +3742,60 @@ void SolveCpModelParallel(const CpModelProto& model_proto, // Log statistics. if (logger->LoggingIsEnabled()) { if (params.log_subsolver_statistics()) { - bool first = true; + SOLVER_LOG(logger, ""); + SOLVER_LOG(logger, "Sub-solver detailed search statistics:"); for (const auto& subsolver : subsolvers) { if (subsolver == nullptr) continue; const std::string stats = subsolver->StatisticsString(); if (stats.empty()) continue; - if (first) { - SOLVER_LOG(logger, ""); - SOLVER_LOG(logger, "Sub-solver search statistics:"); - first = false; - } SOLVER_LOG(logger, absl::StrCat(" '", subsolver->name(), "':\n", stats)); } } + // Subsolver one-liner. + SOLVER_LOG(logger, ""); + SOLVER_LOG(logger, HeaderStr("Subsolver statistics"), RightAlign("Bools"), + RightAlign("Conflicts"), RightAlign("Branches"), + RightAlign("Restarts"), RightAlign("BoolPropag"), + RightAlign("IntegerPropag")); + for (const auto& subsolver : subsolvers) { + if (subsolver == nullptr) continue; + if (subsolver->type() != SubSolver::FULL_PROBLEM) continue; + if (subsolver->name().empty()) continue; + const std::string stats = subsolver->OneLineStats(); + if (stats.empty()) continue; + SOLVER_LOG(logger, absl::StrCat(RowNameStr(subsolver->name()), stats)); + } + + SOLVER_LOG(logger, ""); + SOLVER_LOG( + logger, HeaderStr("LNS statistics"), RightAlign("Improv/Calls"), + RightAlign("Closed"), RightAlign("Difficulty"), RightAlign("TimeLimit"), + RightAlign(" [ min, max] avg dev sum")); + for (const auto& subsolver : subsolvers) { + if (subsolver == nullptr) continue; + if (subsolver->type() != SubSolver::INCOMPLETE) continue; + if (subsolver->name().empty()) continue; + const std::string stats = subsolver->OneLineStats(); + if (stats.empty()) continue; + SOLVER_LOG(logger, absl::StrCat(RowNameStr(subsolver->name()), stats)); + } + shared.response->DisplayImprovementStatistics(); + SOLVER_LOG(logger, ""); + SOLVER_LOG(logger, HeaderStr("Solution repositories"), RightAlign("Added"), + RightAlign("Queried"), RightAlign("Ignored"), + RightAlign("Synchro")); + SOLVER_LOG(logger, shared.response->SolutionsRepository().Stats()); + if (shared.lp_solutions != nullptr) { + SOLVER_LOG(logger, shared.lp_solutions->Stats()); + } + if (shared.incomplete_solutions != nullptr) { + SOLVER_LOG(logger, shared.incomplete_solutions->Stats()); + } + if (shared.bounds) { shared.bounds->LogStatistics(logger); } @@ -4343,7 +4413,7 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { // Linear model (used by feasibility_jump and violation_ls) if (params.num_workers() > 1 || params.test_feasibility_jump() || - params.use_violation_ls()) { + params.num_violation_ls() > 0) { LinearModel* linear_model = new LinearModel(new_cp_model_proto); model->TakeOwnership(linear_model); model->Register(linear_model); diff --git a/ortools/sat/feasibility_jump.cc b/ortools/sat/feasibility_jump.cc index 0612c727116..89ec75e1e7a 100644 --- a/ortools/sat/feasibility_jump.cc +++ b/ortools/sat/feasibility_jump.cc @@ -289,11 +289,11 @@ std::function FeasibilityJumpSolver::GenerateTask(int64_t /*task_id*/) { VLOG(2) << name() << " import a solution with value " << solution.rank; ++num_solutions_imported_; num_batches_before_perturbation_ = - params_.violation_ls_perturbation_frequency(); + params_.violation_ls_perturbation_period(); } else if (num_batches_before_perturbation_ <= 0) { // TODO(user): Tune the improvement constant, maybe use luby. num_batches_before_perturbation_ = - params_.violation_ls_perturbation_frequency(); + params_.violation_ls_perturbation_period(); ++num_perturbations_; PerturbateCurrentSolution(); should_recompute_violations = true; @@ -383,7 +383,7 @@ std::function FeasibilityJumpSolver::GenerateTask(int64_t /*task_id*/) { evaluator_->current_solution(), absl::StrCat(name(), "(", OneLineStats(), ")")); num_batches_before_perturbation_ = - params_.violation_ls_perturbation_frequency(); + params_.violation_ls_perturbation_period(); } else { shared_response_->LogMessage(name(), "infeasible solution. Aborting."); model_is_supported_ = false; diff --git a/ortools/sat/feasibility_pump.cc b/ortools/sat/feasibility_pump.cc index 11e7956ec1f..cab21b72a12 100644 --- a/ortools/sat/feasibility_pump.cc +++ b/ortools/sat/feasibility_pump.cc @@ -211,7 +211,7 @@ void FeasibilityPump::MaybePushToRepo() { lp_solution[model_var] = GetLPSolutionValue(positive_var); } } - incomplete_solutions_->AddNewSolution(lp_solution); + incomplete_solutions_->AddSolution(lp_solution); } if (integer_solution_is_feasible_) { @@ -223,7 +223,7 @@ void FeasibilityPump::MaybePushToRepo() { lp_solution[model_var] = GetIntegerSolutionValue(positive_var); } } - incomplete_solutions_->AddNewSolution(lp_solution); + incomplete_solutions_->AddSolution(lp_solution); } } diff --git a/ortools/sat/parameters_validation.cc b/ortools/sat/parameters_validation.cc index 90fefe3bc77..c0fcf1fd5a2 100644 --- a/ortools/sat/parameters_validation.cc +++ b/ortools/sat/parameters_validation.cc @@ -94,6 +94,14 @@ std::string ValidateParameters(const SatParameters& params) { TEST_NOT_NAN(max_time_in_seconds); TEST_NOT_NAN(max_deterministic_time); + // Parallelism. + const int kMaxReasonableParallelism = 1'000'000; + TEST_IN_RANGE(num_workers, 0, kMaxReasonableParallelism); + TEST_IN_RANGE(num_search_workers, 0, kMaxReasonableParallelism); + TEST_IN_RANGE(min_num_lns_workers, 0, kMaxReasonableParallelism); + TEST_IN_RANGE(shared_tree_num_workers, 0, kMaxReasonableParallelism); + TEST_IN_RANGE(interleave_batch_size, 0, kMaxReasonableParallelism); + // TODO(user): Consider using annotations directly in the proto for these // validation. It is however not open sourced. TEST_IN_RANGE(mip_max_activity_exponent, 1, 62); @@ -111,7 +119,8 @@ std::string ValidateParameters(const SatParameters& params) { TEST_IN_RANGE(feasibility_jump_var_perburbation_range_ratio, 0.0, 1.0); // Violation ls. - TEST_IN_RANGE(violation_ls_perturbation_frequency, 1, 1000000000); + TEST_IN_RANGE(num_violation_ls, 0, kMaxReasonableParallelism); + TEST_IN_RANGE(violation_ls_perturbation_period, 1, 1'000'000'000); TEST_POSITIVE(glucose_decay_increment_period); TEST_POSITIVE(shared_tree_max_nodes_per_worker); @@ -120,11 +129,6 @@ std::string ValidateParameters(const SatParameters& params) { TEST_NON_NEGATIVE(max_time_in_seconds); TEST_NON_NEGATIVE(max_deterministic_time); TEST_NON_NEGATIVE(new_constraints_batch_size); - TEST_NON_NEGATIVE(num_workers); - TEST_NON_NEGATIVE(num_search_workers); - TEST_NON_NEGATIVE(min_num_lns_workers); - TEST_NON_NEGATIVE(shared_tree_num_workers); - TEST_NON_NEGATIVE(interleave_batch_size); TEST_NON_NEGATIVE(probing_deterministic_time_limit); TEST_NON_NEGATIVE(presolve_probing_deterministic_time_limit); TEST_NON_NEGATIVE(linearization_level); diff --git a/ortools/sat/rins.cc b/ortools/sat/rins.cc index 1cdffc85f9a..13adbe073ad 100644 --- a/ortools/sat/rins.cc +++ b/ortools/sat/rins.cc @@ -78,12 +78,11 @@ std::vector GetIncompleteSolutionValues( SharedIncompleteSolutionManager* incomplete_solutions) { std::vector empty_solution_values; - if (incomplete_solutions == nullptr || - !incomplete_solutions->HasNewSolution()) { + if (incomplete_solutions == nullptr || !incomplete_solutions->HasSolution()) { return empty_solution_values; } - return incomplete_solutions->GetNewSolution(); + return incomplete_solutions->PopLast(); } static double kEpsilon = 1e-7; @@ -174,7 +173,7 @@ ReducedDomainNeighborhood GetRinsRensNeighborhood( CHECK(incomplete_solutions != nullptr); const bool lp_solution_available = lp_solutions->NumSolutions() > 0; const bool incomplete_solution_available = - incomplete_solutions->HasNewSolution(); + incomplete_solutions->HasSolution(); if (!lp_solution_available && !incomplete_solution_available) { return reduced_domains; // Not generated. diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index 73112b9bb64..56e05ac8423 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -388,8 +388,9 @@ message SatParameters { // Whether the solver should display per sub-solver search statistics. // This is only useful is log_search_progress is set to true, and if the - // number of search workers is > 1. - optional bool log_subsolver_statistics = 189 [default = true]; + // number of search workers is > 1. Note that in all case we display a bit + // of stats with one line per subsolver. + optional bool log_subsolver_statistics = 189 [default = false]; // Add a prefix to all logs. optional string log_prefix = 185 [default = ""]; @@ -1108,11 +1109,13 @@ message SatParameters { // current solution. This parameter selects the first option. optional bool feasibility_jump_enable_restarts = 250 [default = true]; - // Use feasibility_jump to find improving solutions. - optional bool use_violation_ls = 244 [default = false]; + // This will create incomplete subsolvers (that are not LNS subsolvers) + // that use the feasibility jump code to find improving solution, treating + // the objective improvement as a hard constraint. + optional int32 num_violation_ls = 244 [default = 0]; // How long violation_ls should wait before perturbating a solution. - optional int32 violation_ls_perturbation_frequency = 249 [default = 100]; + optional int32 violation_ls_perturbation_period = 249 [default = 100]; // Enables experimental workstealing-like shared tree search. // If non-zero, start this many complete worker threads to explore a shared diff --git a/ortools/sat/subsolver.cc b/ortools/sat/subsolver.cc index 45223e9ea60..ae1d577ebd5 100644 --- a/ortools/sat/subsolver.cc +++ b/ortools/sat/subsolver.cc @@ -47,12 +47,6 @@ int NextSubsolverToSchedule(std::vector>& subsolvers, int best = -1; for (int i = 0; i < subsolvers.size(); ++i) { if (subsolvers[i] == nullptr) continue; - if (subsolvers[i]->IsDone()) { - // We can free the memory used by this solver for good. - VLOG(1) << "Deleting " << subsolvers[i]->name(); - subsolvers[i].reset(); - continue; - } if (subsolvers[i]->TaskIsAvailable()) { if (best == -1 || num_generated_tasks[i] < num_generated_tasks[best]) { best = i; @@ -63,6 +57,19 @@ int NextSubsolverToSchedule(std::vector>& subsolvers, return best; } +void ClearSubsolversThatAreDone( + std::vector>& subsolvers) { + for (int i = 0; i < subsolvers.size(); ++i) { + if (subsolvers[i] == nullptr) continue; + if (subsolvers[i]->IsDone()) { + // We can free the memory used by this solver for good. + VLOG(1) << "Deleting " << subsolvers[i]->name(); + subsolvers[i].reset(); + continue; + } + } +} + void SynchronizeAll(const std::vector>& subsolvers) { for (const auto& subsolver : subsolvers) { if (subsolver == nullptr) continue; @@ -77,6 +84,7 @@ void SequentialLoop(std::vector>& subsolvers) { std::vector num_generated_tasks(subsolvers.size(), 0); while (true) { SynchronizeAll(subsolvers); + ClearSubsolversThatAreDone(subsolvers); const int best = NextSubsolverToSchedule(subsolvers, num_generated_tasks); if (best == -1) break; num_generated_tasks[best]++; @@ -116,6 +124,7 @@ void DeterministicLoop(std::vector>& subsolvers, pool.StartWorkers(); while (true) { SynchronizeAll(subsolvers); + ClearSubsolversThatAreDone(subsolvers); // We first generate all task to run in this batch. // Note that we can't start the task right away since if a task finish @@ -196,6 +205,12 @@ void NonDeterministicLoop(std::vector>& subsolvers, } SynchronizeAll(subsolvers); + { + // We need to do that while holding the lock since substask below might + // be currently updating the time via AddTaskDuration(). + const absl::MutexLock mutex_lock(&mutex); + ClearSubsolversThatAreDone(subsolvers); + } const int best = NextSubsolverToSchedule(subsolvers, num_generated_tasks); if (best == -1) { if (all_done) break; @@ -216,11 +231,17 @@ void NonDeterministicLoop(std::vector>& subsolvers, } std::function task = subsolvers[best]->GenerateTask(task_id++); const std::string name = subsolvers[best]->name(); - pool.Schedule([task = std::move(task), name, &mutex, &num_in_flight]() { + pool.Schedule([task = std::move(task), name, best, &subsolvers, &mutex, + &num_in_flight]() { + WallTimer timer; + timer.Start(); task(); const absl::MutexLock mutex_lock(&mutex); - VLOG(1) << name << " done."; + VLOG(1) << name << " done in " << timer.Get() << "s."; + if (subsolvers[best] != nullptr) { + subsolvers[best]->AddTaskDuration(timer.Get()); + } num_in_flight--; }); } diff --git a/ortools/sat/subsolver.h b/ortools/sat/subsolver.h index 4f647293f2a..9ca35a9dbef 100644 --- a/ortools/sat/subsolver.h +++ b/ortools/sat/subsolver.h @@ -28,6 +28,7 @@ #include "absl/strings/string_view.h" #include "ortools/base/integral_types.h" +#include "ortools/util/stats.h" #if !defined(__PORTABLE_PLATFORM__) #include "ortools/base/threadpool.h" @@ -90,11 +91,27 @@ class SubSolver { // Returns search statistics. virtual std::string StatisticsString() const { return std::string(); } + virtual std::string OneLineStats() const { return std::string(); } + + // Note that this is protected by the global execution mutex and so it is + // called sequentially. + void AddTaskDuration(double duration_in_seconds) { + timing_.AddTimeInSec(duration_in_seconds); + } + + std::string TimingInfo() const { + // TODO(user): remove trailing "\n" from ValueAsString(). + std::string data = timing_.ValueAsString(); + if (!data.empty()) data.pop_back(); + return data; + } protected: const std::string name_; const SubsolverType type_; + double deterministic_time_ = 0.0; + TimeDistribution timing_ = TimeDistribution("tasks"); }; // A simple wrapper to add a synchronization point in the list of subsolvers. diff --git a/ortools/sat/synchronization.cc b/ortools/sat/synchronization.cc index 0601dbb9fa1..0a7db3a432c 100644 --- a/ortools/sat/synchronization.cc +++ b/ortools/sat/synchronization.cc @@ -77,32 +77,34 @@ void SharedLPSolutionRepository::NewLPSolution( AddInternal(solution); } -bool SharedIncompleteSolutionManager::HasNewSolution() const { +void SharedIncompleteSolutionManager::AddSolution( + const std::vector& lp_solution) { + absl::MutexLock mutex_lock(&mutex_); + ++num_added_; + solutions_.push_back(lp_solution); + if (solutions_.size() > 100) solutions_.pop_front(); +} + +bool SharedIncompleteSolutionManager::HasSolution() const { absl::MutexLock mutex_lock(&mutex_); return !solutions_.empty(); } -std::vector SharedIncompleteSolutionManager::GetNewSolution() { +std::vector SharedIncompleteSolutionManager::PopLast() { absl::MutexLock mutex_lock(&mutex_); - std::vector solution; - if (solutions_.empty()) return solution; + if (solutions_.empty()) return {}; - solution = std::move(solutions_.back()); + ++num_queried_; + std::vector solution = std::move(solutions_.back()); solutions_.pop_back(); return solution; } -void SharedIncompleteSolutionManager::AddNewSolution( - const std::vector& lp_solution) { - absl::MutexLock mutex_lock(&mutex_); - solutions_.push_back(lp_solution); -} - SharedResponseManager::SharedResponseManager(Model* model) : parameters_(*model->GetOrCreate()), wall_timer_(*model->GetOrCreate()), shared_time_limit_(model->GetOrCreate()), - solutions_(parameters_.solution_pool_size()), + solutions_(parameters_.solution_pool_size(), "feasible solutions"), logger_(model->GetOrCreate()) {} namespace { @@ -730,20 +732,23 @@ void SharedResponseManager::DisplayImprovementStatistics() { absl::MutexLock mutex_lock(&mutex_); if (!primal_improvements_count_.empty()) { SOLVER_LOG(logger_, ""); - SOLVER_LOG(logger_, "Solutions found per subsolver (", num_solutions_, - "):"); + SOLVER_LOG(logger_, + HeaderStr(absl::StrCat("Solutions (", num_solutions_, ")")), + RightAlign("Num"), RightAlign("Rank")); for (const auto& entry : primal_improvements_count_) { const int min_rank = primal_improvements_min_rank_[entry.first]; const int max_rank = primal_improvements_max_rank_[entry.first]; - SOLVER_LOG(logger_, " '", entry.first, "':", entry.second, " rank:[", - min_rank, ",", max_rank, "]"); + SOLVER_LOG(logger_, RowNameStr(entry.first), + RightAlign(FormatCounter(entry.second)), + RightAlign(absl::StrCat("[", min_rank, ",", max_rank, "]"))); } } if (!dual_improvements_count_.empty()) { SOLVER_LOG(logger_, ""); - SOLVER_LOG(logger_, "Objective bounds found per subsolver:"); + SOLVER_LOG(logger_, HeaderStr("Objective bounds"), RightAlign("Num")); for (const auto& entry : dual_improvements_count_) { - SOLVER_LOG(logger_, " '", entry.first, "': ", entry.second); + SOLVER_LOG(logger_, RowNameStr(entry.first), + RightAlign(FormatCounter(entry.second))); } } } @@ -918,9 +923,10 @@ void SharedBoundsManager::LogStatistics(SolverLogger* logger) { absl::MutexLock mutex_lock(&mutex_); if (!bounds_exported_.empty()) { SOLVER_LOG(logger, ""); - SOLVER_LOG(logger, "Improving variable bounds shared per subsolver:"); + SOLVER_LOG(logger, HeaderStr("Improving bounds shared"), RightAlign("Num")); for (const auto& entry : bounds_exported_) { - SOLVER_LOG(logger, " '", entry.first, "': ", entry.second); + SOLVER_LOG(logger, RowNameStr(entry.first), + RightAlign(FormatCounter(entry.second))); } } } @@ -992,9 +998,10 @@ void SharedClausesManager::LogStatistics(SolverLogger* logger) { } if (!name_to_clauses.empty()) { SOLVER_LOG(logger, ""); - SOLVER_LOG(logger, "Clauses shared per subsolver:"); + SOLVER_LOG(logger, HeaderStr("Clauses shared"), RightAlign("Num")); for (const auto& entry : name_to_clauses) { - SOLVER_LOG(logger, " '", entry.first, "': ", entry.second); + SOLVER_LOG(logger, RowNameStr(entry.first), + RightAlign(FormatCounter(entry.second))); } } } diff --git a/ortools/sat/synchronization.h b/ortools/sat/synchronization.h index 5b68cecafa0..eb8e50220d4 100644 --- a/ortools/sat/synchronization.h +++ b/ortools/sat/synchronization.h @@ -54,8 +54,9 @@ namespace sat { template class SharedSolutionRepository { public: - explicit SharedSolutionRepository(int num_solutions_to_keep) - : num_solutions_to_keep_(num_solutions_to_keep) {} + explicit SharedSolutionRepository(int num_solutions_to_keep, + absl::string_view name = "") + : name_(name), num_solutions_to_keep_(num_solutions_to_keep) {} // The solution format used by this class. struct Solution { @@ -117,13 +118,26 @@ class SharedSolutionRepository { // Works in O(num_solutions_to_keep_). void Synchronize(); + std::string Stats() const { + absl::MutexLock mutex_lock(&mutex_); + return absl::StrCat(RowNameStr(name_), RightAlign(absl::StrCat(num_added_)), + RightAlign(FormatCounter(num_queried_)), + RightAlign(FormatCounter(num_ignored_)), + RightAlign(FormatCounter(num_synchronization_))); + } + protected: // Helper method for adding the solutions once the mutex is acquired. void AddInternal(const Solution& solution) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + const std::string name_; const int num_solutions_to_keep_; + mutable absl::Mutex mutex_; + int64_t num_added_ ABSL_GUARDED_BY(mutex_) = 0; + int64_t num_ignored_ ABSL_GUARDED_BY(mutex_) = 0; + mutable int64_t num_queried_ ABSL_GUARDED_BY(mutex_) = 0; int64_t num_synchronization_ ABSL_GUARDED_BY(mutex_) = 0; // Our two solutions pools, the current one and the new one that will be @@ -136,7 +150,8 @@ class SharedSolutionRepository { class SharedLPSolutionRepository : public SharedSolutionRepository { public: explicit SharedLPSolutionRepository(int num_solutions_to_keep) - : SharedSolutionRepository(num_solutions_to_keep) {} + : SharedSolutionRepository(num_solutions_to_keep, + "lp solutions") {} void NewLPSolution(std::vector lp_solution); }; @@ -151,15 +166,27 @@ class SharedLPSolutionRepository : public SharedSolutionRepository { // complete feasible solutions. class SharedIncompleteSolutionManager { public: - bool HasNewSolution() const; - std::vector GetNewSolution(); + // This adds a new solution to the stack. + // Note that we keep the last 100 ones at most. + void AddSolution(const std::vector& lp_solution); + + bool HasSolution() const; - void AddNewSolution(const std::vector& lp_solution); + // If there are no solution, this return an empty vector. + std::vector PopLast(); + + std::string Stats() const { + absl::MutexLock mutex_lock(&mutex_); + return absl::StrCat(RowNameStr("pump"), + RightAlign(FormatCounter(num_added_)), + RightAlign(FormatCounter(num_queried_))); + } private: - // New solutions are added and removed from the back. - std::vector> solutions_; mutable absl::Mutex mutex_; + std::deque> solutions_ ABSL_GUARDED_BY(mutex_); + int64_t num_added_ ABSL_GUARDED_BY(mutex_) = 0; + mutable int64_t num_queried_ ABSL_GUARDED_BY(mutex_) = 0; }; // Used by FillSolveStatsInResponse() to extract statistic to put in a @@ -574,6 +601,7 @@ template typename SharedSolutionRepository::Solution SharedSolutionRepository::GetSolution(int i) const { absl::MutexLock mutex_lock(&mutex_); + ++num_queried_; return solutions_[i]; } @@ -590,6 +618,7 @@ typename SharedSolutionRepository::Solution SharedSolutionRepository::GetRandomBiasedSolution( absl::BitGenRef random) const { absl::MutexLock mutex_lock(&mutex_); + ++num_queried_; const int64_t best_rank = solutions_[0].rank; // As long as we have solution with the best objective that haven't been @@ -640,9 +669,13 @@ void SharedSolutionRepository::AddInternal( } } if (new_solutions_.size() < num_solutions_to_keep_) { + ++num_added_; new_solutions_.push_back(solution); } else if (solution < new_solutions_[worse_solution_index]) { + ++num_added_; new_solutions_[worse_solution_index] = solution; + } else { + ++num_ignored_; } } diff --git a/ortools/sat/util.h b/ortools/sat/util.h index 6c51bfbe32f..3bedca68e14 100644 --- a/ortools/sat/util.h +++ b/ortools/sat/util.h @@ -47,6 +47,21 @@ namespace sat { // Prints a positive number with separators for easier reading (ex: 1'348'065). std::string FormatCounter(int64_t num); +// Helper to align vertically multi-line messages. +inline std::string LeftAlign(std::string s, int size = 16) { + if (s.size() >= size) return s; + s.resize(size, ' '); + return s; +} +inline std::string RightAlign(std::string s, int size = 16) { + if (s.size() >= size) return s; + return absl::StrCat(std::string(size - s.size(), ' '), s); +} +inline std::string HeaderStr(std::string s) { return LeftAlign(s, 30); } +inline std::string RowNameStr(std::string name) { + return RightAlign(absl::StrCat("'", name, "':"), 30); +} + // Returns a in [0, m) such that a * x = 1 modulo m. // If gcd(x, m) != 1, there is no inverse, and it returns 0. //