diff --git a/makefiles/Makefile.gen.mk b/makefiles/Makefile.gen.mk index e44b479ad1..ef481831e3 100644 --- a/makefiles/Makefile.gen.mk +++ b/makefiles/Makefile.gen.mk @@ -40,7 +40,7 @@ BASE_DEPS = \ $(SRC_DIR)/ortools/base/threadpool.h \ $(SRC_DIR)/ortools/base/timer.h \ $(SRC_DIR)/ortools/base/typeid.h \ - $(SRC_DIR)/ortools/base/version.h \ + $(SRC_DIR)/ortools/base/version.h BASE_LIB_OBJS = \ $(OBJ_DIR)/base/bitmap.$O \ @@ -93,9 +93,7 @@ objs/base/timer.$O: ortools/base/timer.cc ortools/base/timer.h \ ortools/base/logging.h ortools/base/macros.h | $(OBJ_DIR)/base $(CCC) $(CFLAGS) -c $(SRC_DIR)$Sortools$Sbase$Stimer.cc $(OBJ_OUT)$(OBJ_DIR)$Sbase$Stimer.$O -objs/base/version.$O: ortools/base/version.cc ortools/base/version.h \ - ortools/base/basictypes.h ortools/base/integral_types.h \ - ortools/base/logging.h ortools/base/macros.h | $(OBJ_DIR)/base +objs/base/version.$O: ortools/base/version.cc | $(OBJ_DIR)/base $(CCC) $(CFLAGS) -c $(SRC_DIR)$Sortools$Sbase$Sversion.cc $(OBJ_OUT)$(OBJ_DIR)$Sbase$Sversion.$O PORT_DEPS = \ @@ -1110,7 +1108,6 @@ SAT_DEPS = \ $(SRC_DIR)/ortools/sat/linear_constraint_manager.h \ $(SRC_DIR)/ortools/sat/linear_programming_constraint.h \ $(SRC_DIR)/ortools/sat/linear_relaxation.h \ - $(SRC_DIR)/ortools/sat/lns.h \ $(SRC_DIR)/ortools/sat/lp_utils.h \ $(SRC_DIR)/ortools/sat/model.h \ $(SRC_DIR)/ortools/sat/optimization.h \ @@ -1125,6 +1122,7 @@ SAT_DEPS = \ $(SRC_DIR)/ortools/sat/sat_decision.h \ $(SRC_DIR)/ortools/sat/sat_solver.h \ $(SRC_DIR)/ortools/sat/simplification.h \ + $(SRC_DIR)/ortools/sat/subsolver.h \ $(SRC_DIR)/ortools/sat/swig_helper.h \ $(SRC_DIR)/ortools/sat/symmetry.h \ $(SRC_DIR)/ortools/sat/synchronization.h \ @@ -1182,6 +1180,7 @@ SAT_LIB_OBJS = \ $(OBJ_DIR)/sat/sat_decision.$O \ $(OBJ_DIR)/sat/sat_solver.$O \ $(OBJ_DIR)/sat/simplification.$O \ + $(OBJ_DIR)/sat/subsolver.$O \ $(OBJ_DIR)/sat/symmetry.$O \ $(OBJ_DIR)/sat/synchronization.$O \ $(OBJ_DIR)/sat/table.$O \ @@ -1321,13 +1320,12 @@ objs/sat/cp_model_expand.$O: ortools/sat/cp_model_expand.cc \ objs/sat/cp_model_lns.$O: ortools/sat/cp_model_lns.cc \ ortools/sat/cp_model_lns.h ortools/base/integral_types.h \ - ortools/gen/ortools/sat/cp_model.pb.h ortools/sat/lns.h \ - ortools/base/threadpool.h ortools/sat/model.h ortools/base/logging.h \ - ortools/base/macros.h ortools/base/map_util.h ortools/base/typeid.h \ - ortools/sat/cp_model_loader.h ortools/base/int_type.h \ - ortools/base/int_type_indexed_vector.h ortools/sat/cp_model_utils.h \ - ortools/util/sorted_interval_list.h ortools/sat/integer.h \ - ortools/base/hash.h ortools/base/basictypes.h ortools/graph/iterators.h \ + ortools/gen/ortools/sat/cp_model.pb.h ortools/sat/model.h \ + ortools/base/logging.h ortools/base/macros.h ortools/base/map_util.h \ + ortools/base/typeid.h ortools/sat/subsolver.h ortools/base/threadpool.h \ + ortools/sat/synchronization.h ortools/sat/integer.h ortools/base/hash.h \ + ortools/base/basictypes.h ortools/base/int_type.h \ + ortools/base/int_type_indexed_vector.h ortools/graph/iterators.h \ ortools/sat/sat_base.h ortools/util/bitset.h ortools/sat/sat_solver.h \ ortools/base/timer.h ortools/sat/clause.h \ ortools/sat/drat_proof_handler.h ortools/sat/drat_checker.h \ @@ -1337,9 +1335,11 @@ objs/sat/cp_model_lns.$O: ortools/sat/cp_model_lns.cc \ ortools/util/running_stat.h ortools/sat/sat_decision.h \ ortools/util/integer_pq.h ortools/util/time_limit.h \ ortools/base/commandlineflags.h ortools/util/rev.h \ - ortools/util/saturated_arithmetic.h ortools/sat/intervals.h \ - ortools/sat/cp_constraints.h ortools/sat/integer_expr.h \ - ortools/sat/precedences.h ortools/sat/linear_programming_constraint.h \ + ortools/util/saturated_arithmetic.h ortools/util/sorted_interval_list.h \ + ortools/sat/cp_model_loader.h ortools/sat/cp_model_utils.h \ + ortools/sat/intervals.h ortools/sat/cp_constraints.h \ + ortools/sat/integer_expr.h ortools/sat/precedences.h \ + ortools/sat/linear_programming_constraint.h \ ortools/glop/revised_simplex.h ortools/glop/basis_representation.h \ ortools/glop/lu_factorization.h ortools/glop/markowitz.h \ ortools/gen/ortools/glop/parameters.pb.h ortools/glop/status.h \ @@ -1462,12 +1462,13 @@ objs/sat/cp_model_solver.$O: ortools/sat/cp_model_solver.cc \ ortools/sat/cp_model_checker.h ortools/sat/cp_model_expand.h \ ortools/sat/cp_model_presolve.h ortools/sat/cp_model_utils.h \ ortools/util/affine_relation.h ortools/base/iterator_adaptors.h \ - ortools/sat/cp_model_lns.h ortools/sat/lns.h \ - ortools/sat/cp_model_loader.h ortools/sat/intervals.h \ - ortools/sat/cp_constraints.h ortools/sat/integer_expr.h \ - ortools/sat/precedences.h ortools/sat/cp_model_search.h \ - ortools/sat/integer_search.h ortools/sat/cuts.h \ - ortools/sat/linear_constraint.h ortools/sat/linear_constraint_manager.h \ + ortools/sat/cp_model_lns.h ortools/sat/subsolver.h \ + ortools/sat/synchronization.h ortools/sat/cp_model_loader.h \ + ortools/sat/intervals.h ortools/sat/cp_constraints.h \ + ortools/sat/integer_expr.h ortools/sat/precedences.h \ + ortools/sat/cp_model_search.h ortools/sat/integer_search.h \ + ortools/sat/cuts.h ortools/sat/linear_constraint.h \ + ortools/sat/linear_constraint_manager.h \ ortools/sat/linear_programming_constraint.h \ ortools/glop/revised_simplex.h ortools/glop/basis_representation.h \ ortools/glop/lu_factorization.h ortools/glop/markowitz.h \ @@ -1486,7 +1487,7 @@ objs/sat/cp_model_solver.$O: ortools/sat/cp_model_solver.cc \ ortools/sat/util.h ortools/sat/linear_relaxation.h \ ortools/sat/optimization.h ortools/gen/ortools/sat/boolean_problem.pb.h \ ortools/sat/probing.h ortools/sat/rins.h ortools/sat/simplification.h \ - ortools/base/adjustable_priority_queue.h ortools/sat/synchronization.h | $(OBJ_DIR)/sat + ortools/base/adjustable_priority_queue.h | $(OBJ_DIR)/sat $(CCC) $(CFLAGS) -c $(SRC_DIR)$Sortools$Ssat$Scp_model_solver.cc $(OBJ_OUT)$(OBJ_DIR)$Ssat$Scp_model_solver.$O objs/sat/cp_model_symmetries.$O: ortools/sat/cp_model_symmetries.cc \ @@ -2111,6 +2112,10 @@ objs/sat/simplification.$O: ortools/sat/simplification.cc \ ortools/sat/util.h | $(OBJ_DIR)/sat $(CCC) $(CFLAGS) -c $(SRC_DIR)$Sortools$Ssat$Ssimplification.cc $(OBJ_OUT)$(OBJ_DIR)$Ssat$Ssimplification.$O +objs/sat/subsolver.$O: ortools/sat/subsolver.cc ortools/sat/subsolver.h \ + ortools/base/integral_types.h ortools/base/threadpool.h | $(OBJ_DIR)/sat + $(CCC) $(CFLAGS) -c $(SRC_DIR)$Sortools$Ssat$Ssubsolver.cc $(OBJ_OUT)$(OBJ_DIR)$Ssat$Ssubsolver.$O + objs/sat/symmetry.$O: ortools/sat/symmetry.cc ortools/sat/symmetry.h \ ortools/algorithms/sparse_permutation.h ortools/base/logging.h \ ortools/base/integral_types.h ortools/base/macros.h \ @@ -3788,3 +3793,4 @@ $(GEN_DIR)/ortools/constraint_solver/solver_parameters.pb.h: \ $(OBJ_DIR)/constraint_solver/solver_parameters.pb.$O: \ $(GEN_DIR)/ortools/constraint_solver/solver_parameters.pb.cc | $(OBJ_DIR)/constraint_solver $(CCC) $(CFLAGS) -c $(GEN_PATH)$Sortools$Sconstraint_solver$Ssolver_parameters.pb.cc $(OBJ_OUT)$(OBJ_DIR)$Sconstraint_solver$Ssolver_parameters.pb.$O + diff --git a/ortools/sat/BUILD b/ortools/sat/BUILD index 1e31667da5..cebeb58908 100644 --- a/ortools/sat/BUILD +++ b/ortools/sat/BUILD @@ -170,7 +170,7 @@ cc_library( ":integer_search", ":linear_programming_constraint", ":linear_relaxation", - ":lns", + ":subsolver", ":model", ":optimization", ":precedences", @@ -1010,9 +1010,10 @@ cc_library( ":cp_model_loader", ":cp_model_utils", ":linear_programming_constraint", - ":lns", ":model", ":rins", + ":subsolver", + ":synchronization", "//ortools/base", "//ortools/base:threadpool", "//ortools/util:random_engine", @@ -1038,12 +1039,16 @@ cc_library( ) cc_library( - name = "lns", - hdrs = ["lns.h"], + name = "subsolver", + srcs = ["subsolver.cc"], + hdrs = ["subsolver.h"], visibility = ["//visibility:public"], deps = [ "//ortools/base", + "//ortools/base:threadpool", "//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 1b4a2825f3..fc94a881c0 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -27,15 +27,67 @@ namespace operations_research { namespace sat { NeighborhoodGeneratorHelper::NeighborhoodGeneratorHelper( - CpModelProto const* model_proto, bool focus_on_decision_variables) - : model_proto_(*model_proto) { - UpdateHelperData(focus_on_decision_variables); + int id, const CpModelProto& model_proto, SatParameters const* parameters, + class SharedTimeLimit* shared_time_limit, + SharedBoundsManager* shared_bounds, SharedResponseManager* shared_response) + : SubSolver(id, "helper"), + model_proto_(model_proto), + parameters_(*parameters), + shared_time_limit_(shared_time_limit), + shared_bounds_(shared_bounds), + shared_response_(shared_response) { + RecomputeHelperData(); + Synchronize(); } -void NeighborhoodGeneratorHelper::UpdateHelperData( - bool focus_on_decision_variables) { - var_to_constraint_.resize(model_proto_.variables_size()); - constraint_to_var_.resize(model_proto_.constraints_size()); +void NeighborhoodGeneratorHelper::Synchronize() { + absl::MutexLock mutex_lock(&mutex_); + if (shared_bounds_ != nullptr) { + std::vector model_variables; + std::vector new_lower_bounds; + std::vector new_upper_bounds; + shared_bounds_->GetChangedBounds(id_, &model_variables, &new_lower_bounds, + &new_upper_bounds); + + for (int i = 0; i < model_variables.size(); ++i) { + const int var = model_variables[i]; + const int64 new_lb = new_lower_bounds[i]; + const int64 new_ub = new_upper_bounds[i]; + if (VLOG_IS_ON(3)) { + const auto& domain = model_proto_.variables(var).domain(); + const int64 old_lb = domain.Get(0); + const int64 old_ub = domain.Get(domain.size() - 1); + VLOG(3) << "Variable: " << var << " old domain: [" << old_lb << ", " + << old_ub << "] new domain: [" << new_lb << ", " << new_ub + << "]"; + } + const Domain old_domain = + ReadDomainFromProto(model_proto_.variables(var)); + const Domain new_domain = + old_domain.IntersectionWith(Domain(new_lb, new_ub)); + if (new_domain.IsEmpty()) { + shared_response_->NotifyThatImprovingProblemIsInfeasible( + "LNS base problem"); + if (shared_time_limit_ != nullptr) shared_time_limit_->Stop(); + return; + } + FillDomainInProto(new_domain, model_proto_.mutable_variables(var)); + } + + // Only trigger the computation if needed. + if (!model_variables.empty()) { + RecomputeHelperData(); + } + } + if (shared_response_ != nullptr) { + shared_response_->MutableSolutionsRepository()->Synchronize(); + } +} + +void NeighborhoodGeneratorHelper::RecomputeHelperData() { + // Recompute all the data in case new variables have been fixed. + var_to_constraint_.assign(model_proto_.variables_size(), {}); + constraint_to_var_.assign(model_proto_.constraints_size(), {}); for (int ct_index = 0; ct_index < model_proto_.constraints_size(); ++ct_index) { for (const int var : UsedVariables(model_proto_.constraints(ct_index))) { @@ -46,9 +98,20 @@ void NeighborhoodGeneratorHelper::UpdateHelperData( CHECK_LT(var, model_proto_.variables_size()); } } - active_variables_set_.resize(model_proto_.variables_size(), false); - if (focus_on_decision_variables) { + type_to_constraints_.clear(); + const int num_constraints = model_proto_.constraints_size(); + for (int c = 0; c < num_constraints; ++c) { + const int type = model_proto_.constraints(c).constraint_case(); + if (type >= type_to_constraints_.size()) { + type_to_constraints_.resize(type + 1); + } + type_to_constraints_[type].push_back(c); + } + + bool add_all_variables = true; + active_variables_set_.assign(model_proto_.variables_size(), false); + if (parameters_.lns_focus_on_decision_variables()) { for (const auto& search_strategy : model_proto_.search_strategy()) { for (const int var : search_strategy.variables()) { const int pos_var = PositiveRef(var); @@ -58,12 +121,11 @@ void NeighborhoodGeneratorHelper::UpdateHelperData( } } } - if (!active_variables_.empty()) { - // No decision variables, then no focus. - focus_on_decision_variables = false; - } + + // Revert to no focus if active_variables_ is empty(). + if (!active_variables_.empty()) add_all_variables = false; } - if (!focus_on_decision_variables) { // Could have be set to false above. + if (add_all_variables) { for (int i = 0; i < model_proto_.variables_size(); ++i) { if (!IsConstant(i)) { active_variables_.push_back(i); @@ -71,18 +133,6 @@ void NeighborhoodGeneratorHelper::UpdateHelperData( } } } - - type_to_constraints_.clear(); - const int num_constraints = model_proto_.constraints_size(); - for (int c = 0; c < num_constraints; ++c) { - const int type = model_proto_.constraints(c).constraint_case(); - CHECK_GE(type, 0) << "Negative constraint case ??"; - CHECK_LT(type, 10000) << "Large constraint case ??"; - if (type >= type_to_constraints_.size()) { - type_to_constraints_.resize(type + 1); - } - type_to_constraints_[type].push_back(c); - } } bool NeighborhoodGeneratorHelper::IsActive(int var) const { @@ -204,6 +254,8 @@ void NeighborhoodGenerator::Synchronize() { } else { current_average_ = 0.9 * current_average_ + 0.1 * gain_per_time_unit; } + + deterministic_time_ += data.deterministic_time; } // Bump the time limit if we saw no better solution in the last few calls. diff --git a/ortools/sat/cp_model_lns.h b/ortools/sat/cp_model_lns.h index f1b7a9fee4..29e0ab28ee 100644 --- a/ortools/sat/cp_model_lns.h +++ b/ortools/sat/cp_model_lns.h @@ -20,8 +20,9 @@ #include "absl/types/span.h" #include "ortools/base/integral_types.h" #include "ortools/sat/cp_model.pb.h" -#include "ortools/sat/lns.h" #include "ortools/sat/model.h" +#include "ortools/sat/subsolver.h" +#include "ortools/sat/synchronization.h" namespace operations_research { namespace sat { @@ -44,15 +45,21 @@ struct Neighborhood { // Contains pre-computed information about a given CpModelProto that is meant // to be used to generate LNS neighborhood. This class can be shared between // more than one generator in order to reduce memory usage. -class NeighborhoodGeneratorHelper { +// +// Note that its implement the SubSolver interface to be able to Synchronize() +// the bounds of the base problem with the external world. +class NeighborhoodGeneratorHelper : public SubSolver { public: - NeighborhoodGeneratorHelper(CpModelProto const* model_proto, - bool focus_on_decision_variables); + NeighborhoodGeneratorHelper(int id, const CpModelProto& model_proto, + SatParameters const* parameters, + SharedTimeLimit* shared_time_limit = nullptr, + SharedBoundsManager* shared_bounds = nullptr, + SharedResponseManager* shared_response = nullptr); - // Updates private variables using the current 'model_proto_'. This is NOT - // thread-safe, but the other function in this class are if this is not called - // at the same time. - void UpdateHelperData(bool focus_on_decision_variables); + // SubSolver interface. + bool TaskIsAvailable() override { return false; } + std::function GenerateTask(int64 task_id) override { return {}; } + void Synchronize() override; // Returns the LNS fragment where the given variables are fixed to the value // they take in the given solution. @@ -99,12 +106,33 @@ class NeighborhoodGeneratorHelper { // The initial problem. // Note that the domain of the variables are not updated here. const CpModelProto& ModelProto() const { return model_proto_; } + const SatParameters& Parameters() const { return parameters_; } + + // This mutex must be aquired before calling any of the function that access + // data that can be updated by Synchronize(). + // + // TODO(user): Refactor the class to be thread-safe instead, it should be + // safer and more easily maintenable. Some complication with accessing the + // variable<->constraint graph efficiently though. + absl::Mutex* MutableMutex() const { return &mutex_; } private: + // Recompute most of the class member. This needs to be called when the + // domains of the variables are updated. + void RecomputeHelperData(); + // Indicates if a variable is fixed in the model. bool IsConstant(int var) const; - const CpModelProto& model_proto_; + // TODO(user): To reduce memory, take a const proto and keep the updated + // variable bounds separated. + CpModelProto model_proto_; + const SatParameters& parameters_; + SharedTimeLimit* shared_time_limit_; + SharedBoundsManager* shared_bounds_; + SharedResponseManager* shared_response_; + + mutable absl::Mutex mutex_; // Constraints by types. std::vector> type_to_constraints_; @@ -113,10 +141,10 @@ class NeighborhoodGeneratorHelper { std::vector> constraint_to_var_; std::vector> var_to_constraint_; - // The set of active variables, that is the list of non constant variables - // if focus_on_decision_variables_ is false, or the list of non constant - // decision variables otherwise. - // It is stored both as a list and as a set (using a Boolean vector). + // The set of active variables, that is the list of non constant variables if + // parameters_.focus_on_decision_variables() is false, or the list of non + // constant decision variables otherwise. It is stored both as a list and as a + // set (using a Boolean vector). std::vector active_variables_set_; std::vector active_variables_; }; @@ -126,7 +154,7 @@ class NeighborhoodGenerator { public: NeighborhoodGenerator(const std::string& name, NeighborhoodGeneratorHelper const* helper) - : helper_(*helper), name_(name), difficulty_(0.5) {} + : name_(name), helper_(*helper), difficulty_(0.5) {} virtual ~NeighborhoodGenerator() {} // Generates a "local" subproblem for the given seed. @@ -155,7 +183,7 @@ class NeighborhoodGenerator { // 'total_num_calls' should be the sum of calls across all generators part of // the multi armed bandit problem. // If the generator is called less than 10 times then the method returns - // inifinity as score in order to get more data about the generator + // infinity as score in order to get more data about the generator // performance. double GetUCBScore(int64 total_num_calls) const; @@ -218,9 +246,15 @@ class NeighborhoodGenerator { return deterministic_limit_; } + // The sum of the deterministic time spent in this generator. + double deterministic_time() const { + absl::MutexLock mutex_lock(&mutex_); + return deterministic_time_; + } + protected: - const NeighborhoodGeneratorHelper& helper_; const std::string name_; + const NeighborhoodGeneratorHelper& helper_; private: mutable absl::Mutex mutex_; @@ -236,7 +270,7 @@ class NeighborhoodGenerator { int64 num_calls_ = 0; int64 num_fully_solved_calls_ = 0; int64 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 2bf1283bb2..0f46fce31e 100644 --- a/ortools/sat/cp_model_presolve.cc +++ b/ortools/sat/cp_model_presolve.cc @@ -425,6 +425,40 @@ bool CpModelPresolver::RemoveConstraint(ConstraintProto* ct) { return true; } +void CpModelPresolver::SyncDomainAndRemoveEmptyConstraints() { + // Remove all empty constraints. Note that we need to remap the interval + // references. + std::vector interval_mapping(context_.working_model->constraints_size(), + -1); + int new_num_constraints = 0; + const int old_num_constraints = context_.working_model->constraints_size(); + for (int c = 0; c < old_num_constraints; ++c) { + const auto type = context_.working_model->constraints(c).constraint_case(); + if (type == ConstraintProto::ConstraintCase::CONSTRAINT_NOT_SET) continue; + if (type == ConstraintProto::ConstraintCase::kInterval) { + interval_mapping[c] = new_num_constraints; + } + context_.working_model->mutable_constraints(new_num_constraints++) + ->Swap(context_.working_model->mutable_constraints(c)); + } + context_.working_model->mutable_constraints()->DeleteSubrange( + new_num_constraints, old_num_constraints - new_num_constraints); + for (ConstraintProto& ct_ref : + *context_.working_model->mutable_constraints()) { + ApplyToAllIntervalIndices( + [&interval_mapping](int* ref) { + *ref = interval_mapping[*ref]; + CHECK_NE(-1, *ref); + }, + &ct_ref); + } + + for (int i = 0; i < context_.working_model->variables_size(); ++i) { + FillDomainInProto(context_.DomainOf(i), + context_.working_model->mutable_variables(i)); + } +} + bool CpModelPresolver::PresolveEnforcementLiteral(ConstraintProto* ct) { if (context_.ModelIsUnsat()) return false; if (!HasEnforcementLiteral(*ct)) return false; @@ -4377,32 +4411,7 @@ bool CpModelPresolver::Presolve() { DCHECK(context_.ConstraintVariableUsageIsConsistent()); } - // Remove all empty constraints. Note that we need to remap the interval - // references. - std::vector interval_mapping(context_.working_model->constraints_size(), - -1); - int new_num_constraints = 0; - const int old_num_constraints = context_.working_model->constraints_size(); - for (int c = 0; c < old_num_constraints; ++c) { - const auto type = context_.working_model->constraints(c).constraint_case(); - if (type == ConstraintProto::ConstraintCase::CONSTRAINT_NOT_SET) continue; - if (type == ConstraintProto::ConstraintCase::kInterval) { - interval_mapping[c] = new_num_constraints; - } - context_.working_model->mutable_constraints(new_num_constraints++) - ->Swap(context_.working_model->mutable_constraints(c)); - } - context_.working_model->mutable_constraints()->DeleteSubrange( - new_num_constraints, old_num_constraints - new_num_constraints); - for (ConstraintProto& ct_ref : - *context_.working_model->mutable_constraints()) { - ApplyToAllIntervalIndices( - [&interval_mapping](int* ref) { - *ref = interval_mapping[*ref]; - CHECK_NE(-1, *ref); - }, - &ct_ref); - } + SyncDomainAndRemoveEmptyConstraints(); // The strategy variable indices will be remapped in ApplyVariableMapping() // but first we use the representative of the affine relations for the @@ -4455,12 +4464,6 @@ bool CpModelPresolver::Presolve() { } } - // Update the variables domain of the presolved_model. - for (int i = 0; i < context_.working_model->variables_size(); ++i) { - FillDomainInProto(context_.DomainOf(i), - context_.working_model->mutable_variables(i)); - } - // Set the variables of the mapping_model. context_.mapping_model->mutable_variables()->CopyFrom( context_.working_model->variables()); diff --git a/ortools/sat/cp_model_presolve.h b/ortools/sat/cp_model_presolve.h index f9c0dccf30..b845bb065f 100644 --- a/ortools/sat/cp_model_presolve.h +++ b/ortools/sat/cp_model_presolve.h @@ -229,6 +229,12 @@ class CpModelPresolver { // initial model is valid! bool Presolve(); + // Executes presolve method for the given constraint. Public for testing only. + bool PresolveOneConstraint(int c); + + // Public for testing only. + void SyncDomainAndRemoveEmptyConstraints(); + private: void PresolveToFixPoint(); @@ -246,7 +252,6 @@ class CpModelPresolver { // the current code. This way we shouldn't keep doing computation on an // inconsistent state. // TODO(user,user): Make these public and unit test. - bool PresolveOneConstraint(int c); bool PresolveAutomaton(ConstraintProto* ct); bool PresolveCircuit(ConstraintProto* ct); bool PresolveCumulative(ConstraintProto* ct); diff --git a/ortools/sat/cp_model_search.cc b/ortools/sat/cp_model_search.cc index 30f75788c0..bca8c8063f 100644 --- a/ortools/sat/cp_model_search.cc +++ b/ortools/sat/cp_model_search.cc @@ -351,8 +351,7 @@ SatParameters DiversifySearchParameters(const SatParameters& params, // Use LNS for the remaining workers. new_params.set_search_branching(SatParameters::AUTOMATIC_SEARCH); - new_params.set_use_lns(true); - new_params.set_lns_num_threads(1); + new_params.set_use_lns_only(true); *name = absl::StrFormat("lns_%i", index); return new_params; } else { diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index 7b57782502..698bc09651 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -68,7 +68,6 @@ #include "ortools/sat/integer_search.h" #include "ortools/sat/linear_programming_constraint.h" #include "ortools/sat/linear_relaxation.h" -#include "ortools/sat/lns.h" #include "ortools/sat/optimization.h" #include "ortools/sat/precedences.h" #include "ortools/sat/probing.h" @@ -77,6 +76,7 @@ #include "ortools/sat/sat_parameters.pb.h" #include "ortools/sat/sat_solver.h" #include "ortools/sat/simplification.h" +#include "ortools/sat/subsolver.h" #include "ortools/sat/synchronization.h" #include "ortools/util/sorted_interval_list.h" #include "ortools/util/time_limit.h" @@ -777,7 +777,7 @@ IntegerVariable AddLPConstraints(const CpModelProto& model_proto, VLOG(3) << "LP constraint: " << lp_constraint->DimensionString() << "."; } - VLOG(2) << top_level_cp_terms.size() + VLOG(3) << top_level_cp_terms.size() << " terms in the main objective linear equation (" << num_components_containing_objective << " from LP constraints)."; return main_objective_var; @@ -1218,10 +1218,10 @@ void LoadCpModel(const CpModelProto& model_proto, const Domain automatic_domain = model->GetOrCreate()->InitialVariableDomain( objective_var); - VLOG(2) << "Objective offset:" << model_proto.objective().offset() + VLOG(3) << "Objective offset:" << model_proto.objective().offset() << " scaling_factor:" << model_proto.objective().scaling_factor(); - VLOG(2) << "Automatic internal objective domain: " << automatic_domain; - VLOG(2) << "User specified internal objective domain: " << user_domain; + VLOG(3) << "Automatic internal objective domain: " << automatic_domain; + VLOG(3) << "User specified internal objective domain: " << user_domain; CHECK_NE(objective_var, kNoIntegerVariable); const bool ok = model->GetOrCreate()->UpdateInitialDomain( objective_var, user_domain); @@ -1697,459 +1697,364 @@ CpSolverResponse SolvePureSatModel(const CpModelProto& model_proto, return response; } -namespace { - -// Returns false if the new domain is empty. Otherwise updates the domain in the -// 'mutable_var' proto by intersecting the current domain with ['new_lb', -// 'new_ub'] -bool UpdateDomain(int64 new_lb, int64 new_ub, - IntegerVariableProto* mutable_var) { - const Domain old_domain = ReadDomainFromProto(*mutable_var); - const Domain new_domain = old_domain.IntersectionWith(Domain(new_lb, new_ub)); - if (new_domain.IsEmpty()) { - return false; - } - - FillDomainInProto(new_domain, mutable_var); - return true; -} -} // namespace - -void SolveCpModelWithLNS(const CpModelProto& model_proto, int worker_id, - SharedResponseManager* shared_response_manager, - SharedBoundsManager* shared_bounds_manager, - WallTimer* wall_timer, Model* model) { - CHECK(shared_response_manager != nullptr); - SatParameters* parameters = model->GetOrCreate(); - - CpSolverResponse response = shared_response_manager->GetResponse(); - - const bool focus_on_decision_variables = - parameters->lns_focus_on_decision_variables(); - - // This mutex should protect all access to the helper or time_limit. - absl::Mutex mutex; - CpModelProto mutable_model_proto = model_proto; - TimeLimit* limit = model->GetOrCreate(); - NeighborhoodGeneratorHelper helper(&mutable_model_proto, - focus_on_decision_variables); - - // For now we will just alternate between our possible neighborhoods. - // - // TODO(user): select with higher probability the one that seems to work best? - // We can evaluate this compared to the best solution at the time of the - // neighborhood creation. - std::vector> generators; - generators.push_back( - absl::make_unique(&helper, "rnd_lns")); - generators.push_back(absl::make_unique( - &helper, "var_lns")); - generators.push_back(absl::make_unique( - &helper, "cst_lns")); - if (!helper.TypeToConstraints(ConstraintProto::kNoOverlap).empty()) { - generators.push_back( - absl::make_unique( - &helper, "scheduling_time_window_lns")); - generators.push_back(absl::make_unique( - &helper, "scheduling_random_lns")); - } - if (parameters->use_rins_lns()) { - // TODO(user): Only do it if we have a linear relaxation. - generators.push_back( - absl::make_unique( - &helper, model, "rins")); - } - bool all_generators_require_solution = true; - for (int i = 0; i < generators.size(); ++i) { - if (!generators[i]->NeedsFirstSolution()) { - all_generators_require_solution = false; - break; - } - } - if (response.status() == CpSolverStatus::UNKNOWN) { - if (all_generators_require_solution) { - parameters->set_stop_after_first_solution(true); - LoadCpModel(model_proto, shared_response_manager, model); - SolveLoadedCpModel(model_proto, shared_response_manager, model); - response = shared_response_manager->GetResponse(); - if (response.status() != CpSolverStatus::FEASIBLE) { - return; - } - } - } - if (response.status() != CpSolverStatus::FEASIBLE && - response.status() != CpSolverStatus::UNKNOWN) { - return; - } - - const auto synchronize_and_maybe_stop_function = [&]() { - // Update the bounds on mutable model proto. - if (shared_bounds_manager != nullptr) { - absl::MutexLock mutex_lock(&mutex); - std::vector model_variables; - std::vector new_lower_bounds; - std::vector new_upper_bounds; - shared_bounds_manager->GetChangedBounds( - worker_id, &model_variables, &new_lower_bounds, &new_upper_bounds); - - for (int i = 0; i < model_variables.size(); ++i) { - const int var = model_variables[i]; - const int64 new_lb = new_lower_bounds[i]; - const int64 new_ub = new_upper_bounds[i]; - if (VLOG_IS_ON(3)) { - const auto& domain = mutable_model_proto.variables(var).domain(); - const int64 old_lb = domain.Get(0); - const int64 old_ub = domain.Get(domain.size() - 1); - VLOG(3) << "Variable: " << var << " old domain: [" << old_lb << ", " - << old_ub << "] new domain: [" << new_lb << ", " << new_ub - << "]"; - } - if (!UpdateDomain(new_lb, new_ub, - mutable_model_proto.mutable_variables(var))) { - // Model is unsat. - return true; - } - } - if (!model_variables.empty()) { - helper.UpdateHelperData(focus_on_decision_variables); - } - } - - // Synchronize the solution repository and process all the latest - // "SolveData" since the last synchronization point. - shared_response_manager->MutableSolutionsRepository()->Synchronize(); - for (std::unique_ptr& generator : generators) { - generator->Synchronize(); - } - - absl::MutexLock mutex_lock(&mutex); - response = shared_response_manager->GetResponse(); - return limit->LimitReached() || - response.objective_value() == response.best_objective_bound(); - }; - - const auto generate_and_solve_function = [&](int64 seed) { - const int num_solutions = - shared_response_manager->SolutionsRepository().NumSolutions(); - int selected_generator = seed % generators.size(); - while (num_solutions == 0 && - generators[selected_generator]->NeedsFirstSolution()) { - selected_generator = (selected_generator + 1) % generators.size(); - } - - CpSolverResponse base_response; - if (num_solutions > 0) { - base_response.set_status(CpSolverStatus::FEASIBLE); - const int selected_solution = seed / generators.size() % num_solutions; - const SharedSolutionRepository::Solution solution = - shared_response_manager->SolutionsRepository().GetSolution( - selected_solution); - for (const int64 value : solution.variable_values) { - base_response.add_solution(value); - } - } else { - base_response.set_status(CpSolverStatus::UNKNOWN); - } - - NeighborhoodGenerator* generator = generators[selected_generator].get(); - - const double saved_difficulty = generator->difficulty(); - const double saved_limit = generator->deterministic_limit(); - Neighborhood neighborhood; - { - absl::MutexLock mutex_lock(&mutex); - - // TODO(user): currently the full neigbhorhood generation cannot be - // parallelized since we can change the underlying helper data when - // synchronizing new bounds. Improve? - if (limit->LimitReached()) return; - neighborhood = generator->Generate(base_response, seed, saved_difficulty); - } - if (!neighborhood.is_generated) return; - - CHECK_NE(saved_limit, 0); - CpModelProto& local_problem = neighborhood.cp_model; - const std::string solution_info = absl::StrFormat( - "%s(d=%0.2f s=%i t=%0.2f p=%0.2f)", generator->name(), saved_difficulty, - seed, saved_limit, - static_cast(generator->num_fully_solved_calls()) / - std::max(int64{1}, generator->num_calls())); - #if !defined(__PORTABLE_PLATFORM__) - if (FLAGS_cp_model_dump_lns_problems) { - const std::string name = absl::StrCat("/tmp/lns_", seed, ".pb.txt"); - LOG(INFO) << "Dumping LNS model to '" << name << "'."; - CHECK_OK(file::SetTextProto(name, local_problem, file::Defaults())); - } -#endif // __PORTABLE_PLATFORM__ - Model local_model; - { - SatParameters local_parameters; - local_parameters = *parameters; - local_parameters.set_max_deterministic_time(saved_limit); - local_parameters.set_stop_after_first_solution(false); - local_model.Add(NewSatParameters(local_parameters)); - } - { - absl::MutexLock mutex_lock(&mutex); - local_model.GetOrCreate()->MergeWithGlobalTimeLimit(limit); +// Small wrapper to simplify the constructions of the two SubSolver below. +struct SharedClasses { + CpModelProto const* model_proto; + WallTimer* wall_timer; + SharedTimeLimit* time_limit; + SharedBoundsManager* bounds; + SharedResponseManager* response; + SharedRINSNeighborhoodManager* rins_manager; +}; + +// Encapsulate a full CP-SAT solve without presolve in the SubSolver API. +class FullProblemSolver : public SubSolver { + public: + FullProblemSolver(int id, const std::string& name, + const SatParameters& local_parameters, + SharedClasses* shared) + : SubSolver(id, name), + shared_(shared), + local_model_(absl::make_unique()) { + // Setup the local model parameters and time limit. + local_model_->Add(NewSatParameters(local_parameters)); + shared_->time_limit->UpdateLocalLimit( + local_model_->GetOrCreate()); + + // Stores info that will be used for logs in the local model. + WorkerInfo* worker_info = local_model_->GetOrCreate(); + worker_info->worker_name = name; + worker_info->worker_id = id; + + // Add shared neighborhood only if RINS is enabled in global parameters. + if (shared_->rins_manager != nullptr) { + local_model_->Register( + shared_->rins_manager); } - // Presolve and solve the LNS fragment. - CpModelProto mapping_proto; - std::vector postsolve_mapping; - PresolveOptions options; - options.log_info = VLOG_IS_ON(3); - options.parameters = *local_model.GetOrCreate(); - options.time_limit = local_model.GetOrCreate(); - PresolveCpModel(options, &local_problem, &mapping_proto, - &postsolve_mapping); - CpSolverResponse local_response = - LocalSolve(local_problem, wall_timer, &local_model); - - // TODO(user): we actually do not need to postsolve if the solution is - // not going to be used... - PostsolveResponse(model_proto, mapping_proto, postsolve_mapping, wall_timer, - &local_response); - if (local_response.solution_info().empty()) { - local_response.set_solution_info(solution_info); - } else { - local_response.set_solution_info( - absl::StrCat(local_response.solution_info(), " ", solution_info)); + // Level zero variable bounds sharing. + if (shared_->bounds != nullptr) { + RegisterVariableBoundsLevelZeroExport( + *shared_->model_proto, shared_->bounds, local_model_.get()); + RegisterVariableBoundsLevelZeroImport( + *shared_->model_proto, shared_->bounds, local_model_.get()); } - - NeighborhoodGenerator::SolveData data; - data.status = local_response.status(); - data.difficulty = saved_difficulty; - data.deterministic_time = local_response.deterministic_time(); - - data.objective_diff = 0.0; - if (local_response.status() == CpSolverStatus::OPTIMAL || - local_response.status() == CpSolverStatus::FEASIBLE) { - // TODO(user): Compute the diff with respect to the base solution - // instead. - data.objective_diff = std::abs(local_response.objective_value() - - response.objective_value()); - } - - generator->AddSolveData(data); - - // The total number of call when this was called is the same as the - // seed. - // TODO(user): maybe rename? Also move to the synchronize part... - const int total_num_calls = seed; - VLOG(2) << generator->name() << ": [difficulty: " << saved_difficulty - << ", deterministic time: " << local_response.deterministic_time() - << ", status: " - << ProtoEnumToString(local_response.status()) - << ", num calls: " << generator->num_calls() - << ", UCB1 Score: " << generator->GetUCBScore(total_num_calls) - << "]"; - - // Report any feasible solution we have. Note that we do not want - // to keep the full model in this lambda, so for now we do not - // report local stats in the solution. - // - // TODO(user): depending on the problem, the bound sharing may or - // may not restrict the objective though. Uniformize the behavior. - if (local_response.status() == CpSolverStatus::OPTIMAL || - local_response.status() == CpSolverStatus::FEASIBLE) { - shared_response_manager->NewSolution(local_response, - /*model=*/nullptr); - } - if (!neighborhood.is_reduced && - (local_response.status() == CpSolverStatus::OPTIMAL || - local_response.status() == CpSolverStatus::INFEASIBLE)) { - shared_response_manager->NotifyThatImprovingProblemIsInfeasible( - local_response.solution_info()); - } - }; - - const int num_threads = std::max(1, parameters->lns_num_threads()); - if (parameters->lns_is_deterministic()) { - const int batch_size = 4 * num_threads; - OptimizeWithLNS(num_threads, batch_size, - synchronize_and_maybe_stop_function, - generate_and_solve_function); - } else { - NonDeterministicOptimizeWithLNS(num_threads, - synchronize_and_maybe_stop_function, - generate_and_solve_function); } -} -#if !defined(__PORTABLE_PLATFORM__) + // TODO(user): Add support for splitting generated tasks in chunk. For now + // each task is actually a "full" solve and we only ever generate a single + // task. + bool TaskIsAvailable() override { return !task_is_generated_; } + + std::function GenerateTask(int64 task_id) override { + task_is_generated_ = true; + return [this]() { + LoadCpModel(*shared_->model_proto, shared_->response, local_model_.get()); + SolveLoadedCpModel(*shared_->model_proto, shared_->response, + local_model_.get()); + + // Abort if the problem is solved. + if (shared_->response->ProblemIsSolved()) { + shared_->time_limit->Stop(); + } + + // Once a solver is done clear its memory and do not wait for the + // destruction of the SubSolver. This is important because the full solve + // might not be done at all, for instance this might have been configured + // with stop_after_first_solution. + local_model_.reset(); + }; + } + + // This is currently doing nothing and the synchronization is not + // deterministic. TODO(user): fix. + void Synchronize() override {} + + private: + SharedClasses* shared_; + + bool task_is_generated_ = false; + std::unique_ptr local_model_; +}; + +// A Subsolver that generate LNS solve from a given neighborhood. +class LnsSolver : public SubSolver { + public: + LnsSolver(int id, std::unique_ptr generator, + NeighborhoodGeneratorHelper* helper, SharedClasses* shared) + : SubSolver(id, generator->name()), + generator_(std::move(generator)), + helper_(helper), + shared_(shared) {} + + bool SearchIsDone() const { + return shared_->response->ProblemIsSolved() || + shared_->time_limit->LimitReached(); + } + + bool TaskIsAvailable() override { + if (SearchIsDone()) return false; + if (generator_->NeedsFirstSolution()) { + if (shared_->response->SolutionsRepository().NumSolutions() == 0) { + return false; + } + } + return true; + } + + std::function GenerateTask(int64 task_id) override { + return [task_id, this]() { + if (SearchIsDone()) return; + const int num_solutions = + shared_->response->SolutionsRepository().NumSolutions(); + + CpSolverResponse base_response; + if (num_solutions > 0) { + random_engine_t random; + random.seed(task_id); + std::uniform_int_distribution random_var(0, num_solutions - 1); + + base_response.set_status(CpSolverStatus::FEASIBLE); + const SharedSolutionRepository::Solution solution = + shared_->response->SolutionsRepository().GetSolution( + random_var(random)); + for (const int64 value : solution.variable_values) { + base_response.add_solution(value); + } + base_response.set_objective_value(ScaleObjectiveValue( + shared_->model_proto->objective(), solution.internal_objective)); + } else { + base_response.set_status(CpSolverStatus::UNKNOWN); + } + + const double saved_difficulty = generator_->difficulty(); + const double saved_limit = generator_->deterministic_limit(); + Neighborhood neighborhood; + { + absl::MutexLock mutex_lock(helper_->MutableMutex()); + neighborhood = + generator_->Generate(base_response, task_id, saved_difficulty); + } + neighborhood.cp_model.set_name(absl::StrCat("lns_", task_id)); + if (!neighborhood.is_generated) return; + + CHECK_NE(saved_limit, 0); + const double fully_solved_proportion = + static_cast(generator_->num_fully_solved_calls()) / + std::max(int64{1}, generator_->num_calls()); + const std::string solution_info = absl::StrFormat( + "%s(d=%0.2f s=%i t=%0.2f p=%0.2f)", name(), saved_difficulty, task_id, + saved_limit, fully_solved_proportion); + + SatParameters local_params; + local_params = helper_->Parameters(); + local_params.set_max_deterministic_time(saved_limit); + local_params.set_stop_after_first_solution(false); + + if (FLAGS_cp_model_dump_lns_problems) { + const std::string name = + absl::StrCat("/tmp/", neighborhood.cp_model.name(), ".pb.txt"); + LOG(INFO) << "Dumping LNS model to '" << name << "'."; + CHECK_OK( + file::SetTextProto(name, neighborhood.cp_model, file::Defaults())); + } + + Model local_model; + local_model.Add(NewSatParameters(local_params)); + shared_->time_limit->UpdateLocalLimit( + local_model.GetOrCreate()); + + // Presolve and solve the LNS fragment. + CpModelProto mapping_proto; + std::vector postsolve_mapping; + PresolveOptions options; + options.log_info = VLOG_IS_ON(3); + options.parameters = *local_model.GetOrCreate(); + options.time_limit = local_model.GetOrCreate(); + PresolveCpModel(options, &neighborhood.cp_model, &mapping_proto, + &postsolve_mapping); + CpSolverResponse local_response = + LocalSolve(neighborhood.cp_model, shared_->wall_timer, &local_model); + + // TODO(user): we actually do not need to postsolve if the solution is + // not going to be used... + PostsolveResponse(*shared_->model_proto, mapping_proto, postsolve_mapping, + shared_->wall_timer, &local_response); + + if (local_response.solution_info().empty()) { + local_response.set_solution_info(solution_info); + } else { + local_response.set_solution_info( + absl::StrCat(local_response.solution_info(), " ", solution_info)); + } + + NeighborhoodGenerator::SolveData data; + data.status = local_response.status(); + data.difficulty = saved_difficulty; + data.deterministic_time = local_response.deterministic_time(); + data.objective_diff = 0.0; + if (local_response.status() == CpSolverStatus::OPTIMAL || + local_response.status() == CpSolverStatus::FEASIBLE) { + data.objective_diff = std::abs(local_response.objective_value() - + base_response.objective_value()); + } + generator_->AddSolveData(data); + + // The total number of call when this was called is the same as task_id. + const int total_num_calls = task_id; + VLOG(2) << name() << ": [difficulty: " << saved_difficulty + << ", deterministic time: " << local_response.deterministic_time() + << ", status: " + << ProtoEnumToString(local_response.status()) + << ", num calls: " << generator_->num_calls() + << ", UCB1 Score: " << generator_->GetUCBScore(total_num_calls) + << ", p: " << fully_solved_proportion << "]"; + + // Report any feasible solution we have. + if (local_response.status() == CpSolverStatus::OPTIMAL || + local_response.status() == CpSolverStatus::FEASIBLE) { + shared_->response->NewSolution(local_response, + /*model=*/nullptr); + } + if (!neighborhood.is_reduced && + (local_response.status() == CpSolverStatus::OPTIMAL || + local_response.status() == CpSolverStatus::INFEASIBLE)) { + shared_->response->NotifyThatImprovingProblemIsInfeasible( + local_response.solution_info()); + shared_->time_limit->Stop(); + } + }; + } + + void Synchronize() override { + generator_->Synchronize(); + deterministic_time_ += generator_->deterministic_time(); + } + + private: + std::unique_ptr generator_; + NeighborhoodGeneratorHelper* helper_; + SharedClasses* shared_; +}; void SolveCpModelParallel(const CpModelProto& model_proto, SharedResponseManager* shared_response_manager, - WallTimer* wall_timer, Model* model) { - const SatParameters& params = *model->GetOrCreate(); - CHECK(!params.enumerate_all_solutions()) + SharedTimeLimit* shared_time_limit, + WallTimer* wall_timer, Model* global_model) { + CHECK(shared_response_manager != nullptr); + const SatParameters& parameters = *global_model->GetOrCreate(); + const int num_search_workers = parameters.num_search_workers(); + const bool log_search = parameters.log_search_progress() || VLOG_IS_ON(1); + CHECK(!parameters.enumerate_all_solutions()) << "Enumerating all solutions in parallel is not supported."; - // This is a bit hacky. If the provided TimeLimit as a "stop" Boolean, we - // use this one instead. - std::atomic stopped_boolean(false); - std::atomic* stopped = &stopped_boolean; - if (model->GetOrCreate()->ExternalBooleanAsLimit() != nullptr) { - stopped = model->GetOrCreate()->ExternalBooleanAsLimit(); - } - - absl::Mutex mutex; - - // In the LNS threads, we wait for this notification before starting work. - absl::Notification first_solution_found_or_search_finished; - - const int num_search_workers = params.num_search_workers(); - const bool log_search = - model->GetOrCreate()->log_search_progress() || - VLOG_IS_ON(1); - std::unique_ptr shared_bounds_manager; - if (model->GetOrCreate()->share_level_zero_bounds()) { - shared_bounds_manager = - absl::make_unique(num_search_workers, model_proto); + if (global_model->GetOrCreate()->share_level_zero_bounds()) { + // TODO(user): The current code is a bit brittle because we may have + // more SubSolver ids than num_search_workers, and each SubSolver might + // need to synchronize bounds. Fix, it should be easy to make this number + // adapt dynamically in the SharedBoundsManager. + shared_bounds_manager = absl::make_unique( + num_search_workers + 1, model_proto); } - - // Note: This is shared only if the rins is enabled in the global parameters. - std::unique_ptr rins_manager; - if (model->GetOrCreate()->use_rins_lns()) { - rins_manager = absl::make_unique( + std::unique_ptr shared_rins_manager; + if (global_model->GetOrCreate()->use_rins_lns()) { + shared_rins_manager = absl::make_unique( model_proto.variables_size()); } - // Collect per-worker parameters and names. - std::vector worker_parameters; - std::vector worker_names; - for (int worker_id = 0; worker_id < num_search_workers; ++worker_id) { - std::string worker_name; - const SatParameters local_params = - DiversifySearchParameters(params, model_proto, worker_id, &worker_name); - worker_names.push_back(worker_name); - worker_parameters.push_back(local_params); + SharedClasses shared; + shared.model_proto = &model_proto; + shared.wall_timer = wall_timer; + shared.time_limit = shared_time_limit; + shared.bounds = shared_bounds_manager.get(); + shared.rins_manager = shared_rins_manager.get(); + shared.response = shared_response_manager; + + // The list of all the SubSolver that will be used in this parallel search. + std::vector> subsolvers; + + if (parameters.use_lns_only()) { + // Register something to find a first solution. + SatParameters local_params = parameters; + local_params.set_stop_after_first_solution(true); + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), "first_solution", local_params, &shared)); + } else { + // Add a solver for each non-LNS workers. + std::vector worker_names; + for (int worker_id = 0; worker_id < num_search_workers; ++worker_id) { + std::string worker_name; + const SatParameters local_params = DiversifySearchParameters( + parameters, model_proto, worker_id, &worker_name); + if (!local_params.use_lns_only()) { + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), worker_name, local_params, &shared)); + } + worker_names.push_back(worker_name); + } + LOG_IF(INFO, log_search) << absl::StrFormat( + "*** starting parallel search at %.2fs with %i workers: [ %s ]", + wall_timer->Get(), num_search_workers, + absl::StrJoin(worker_names, ", ")); } - LOG_IF(INFO, log_search) << absl::StrFormat( - "*** starting parallel search at %.2fs with %i workers: [ %s ]", - wall_timer->Get(), num_search_workers, absl::StrJoin(worker_names, ", ")); - // Add a callback to know when a first solution is available so we can awaken - // the LNS threads. - // - // TODO(user): We could unregister it when a solution is found. - const int callback_id = shared_response_manager->AddSolutionCallback( - [&mutex, - &first_solution_found_or_search_finished](const CpSolverResponse& r) { - absl::MutexLock lock(&mutex); - if (!first_solution_found_or_search_finished.HasBeenNotified()) { - first_solution_found_or_search_finished.Notify(); - } - }); - const auto cleanup = - ::gtl::MakeCleanup([&shared_response_manager, callback_id]() { - shared_response_manager->UnregisterCallback(callback_id); - }); + // Only register LNS SubSolver if there is an objective. + if (model_proto.has_objective()) { + // Add the NeighborhoodGeneratorHelper as a special subsolver so that its + // Synchronize() is called before any LNS neighborhood solvers. + auto unique_helper = absl::make_unique( + /*id=*/subsolvers.size(), model_proto, ¶meters, shared_time_limit, + shared_bounds_manager.get(), shared_response_manager); + NeighborhoodGeneratorHelper* helper = unique_helper.get(); + subsolvers.push_back(std::move(unique_helper)); - // The LNS threads are handled differently. - int num_lns_workers = 0; - int lns_worker_id = 0; - for (int worker_id = 0; worker_id < num_search_workers; ++worker_id) { - const SatParameters local_params = worker_parameters[worker_id]; - if (local_params.use_lns()) { - lns_worker_id = worker_id; - ++num_lns_workers; + // Enqueue all the possible LNS neighborhood subsolvers. + // Each will have their own metrics. + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), + absl::make_unique(helper, "rnd_lns"), + helper, &shared)); + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), + absl::make_unique(helper, + "var_lns"), + helper, &shared)); + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), + absl::make_unique(helper, + "cst_lns"), + helper, &shared)); + + if (!helper->TypeToConstraints(ConstraintProto::kNoOverlap).empty()) { + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), + absl::make_unique( + helper, "scheduling_tim_window_lns"), + helper, &shared)); + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), + absl::make_unique( + helper, "scheduling_random_lns"), + helper, &shared)); + } + if (parameters.use_rins_lns()) { + subsolvers.push_back(absl::make_unique( + /*id=*/subsolvers.size(), + absl::make_unique( + helper, global_model, "rins"), + helper, &shared)); } } - ThreadPool pool("Parallel_search", num_search_workers); - pool.StartWorkers(); - - for (int worker_id = 0; worker_id < num_search_workers; ++worker_id) { - const std::string worker_name = worker_names[worker_id]; - const SatParameters local_params = worker_parameters[worker_id]; - if (local_params.use_lns()) continue; - - pool.Schedule([&model_proto, stopped, local_params, worker_id, &mutex, - num_search_workers, wall_timer, - &first_solution_found_or_search_finished, - &shared_response_manager, &shared_bounds_manager, - worker_name, &rins_manager]() { - Model local_model; - local_model.Add(NewSatParameters(local_params)); - local_model.GetOrCreate()->RegisterExternalBooleanAsLimit( - stopped); - - // Add shared neighborhood only if RINS is enabled in global parameters. - if (rins_manager != nullptr) { - local_model.Register(rins_manager.get()); - } - - // Stores info that will be used for logs in the local model. - WorkerInfo* worker_info = local_model.GetOrCreate(); - worker_info->worker_name = worker_name; - worker_info->worker_id = worker_id; - - LoadCpModel(model_proto, shared_response_manager, &local_model); - - // Level zero variable bounds sharing. - // - // TODO(user): move these function to the shared_bounds_manager class? - if (shared_bounds_manager != nullptr) { - RegisterVariableBoundsLevelZeroExport( - model_proto, shared_bounds_manager.get(), &local_model); - RegisterVariableBoundsLevelZeroImport( - model_proto, shared_bounds_manager.get(), &local_model); - } - - SolveLoadedCpModel(model_proto, shared_response_manager, &local_model); - - // TODO(user): Accumulate the stats? - // - // TODO(user): For now we assume that each worker only terminate when - // the time limit is reached or when the problem is solved, so we just - // abort all other threads and return. - { - absl::MutexLock lock(&mutex); - *stopped = true; - if (!first_solution_found_or_search_finished.HasBeenNotified()) { - first_solution_found_or_search_finished.Notify(); - } - } - }); - } - - // Schedule the LNS worker that itself will contain a thread-pool. - if (num_lns_workers > 0) { - pool.Schedule([&] { - // Wait for a solution before starting the LNS threads. - first_solution_found_or_search_finished.WaitForNotification(); - - // Note that because the other worker are not deterministic, there is - // no point having a deterministic LNS here as it will pull out - // non-deterministic info like bounds and best solutions... - Model local_model; - local_model.GetOrCreate()->set_use_lns(true); - local_model.GetOrCreate()->set_lns_num_threads( - num_lns_workers); - local_model.GetOrCreate()->set_lns_is_deterministic(false); - local_model.GetOrCreate()->RegisterExternalBooleanAsLimit( - stopped); - - // Stores info that will be used for logs in the local model. - WorkerInfo* worker_info = local_model.GetOrCreate(); - worker_info->worker_name = worker_names[lns_worker_id]; - worker_info->worker_id = lns_worker_id; - - SolveCpModelWithLNS(model_proto, lns_worker_id, shared_response_manager, - shared_bounds_manager.get(), wall_timer, - &local_model); - - absl::MutexLock lock(&mutex); - *stopped = true; - if (!first_solution_found_or_search_finished.HasBeenNotified()) { - first_solution_found_or_search_finished.Notify(); - } - }); + // Launch the main search loop. + if (parameters.deterministic_parallel_search()) { + const int batch_size = 4 * num_search_workers; + DeterministicLoop(subsolvers, num_search_workers, batch_size); + } else { + NonDeterministicLoop(subsolvers, num_search_workers); } } @@ -2162,6 +2067,7 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { UserTimer user_timer; wall_timer.Start(); user_timer.Start(); + SharedTimeLimit shared_time_limit(model->GetOrCreate()); #if !defined(__PORTABLE_PLATFORM__) // Dump? @@ -2183,17 +2089,9 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { } // Register SIGINT handler if requested by the parameters. - std::atomic stopped_boolean(false); SigintHandler handler; if (model->GetOrCreate()->catch_sigint_signal()) { - std::atomic* stopped = - model->GetOrCreate()->ExternalBooleanAsLimit(); - if (stopped == nullptr) { - stopped = &stopped_boolean; - model->GetOrCreate()->RegisterExternalBooleanAsLimit(stopped); - } - - handler.Register([stopped]() { *stopped = true; }); + handler.Register([&shared_time_limit]() { shared_time_limit.Stop(); }); } #endif // __PORTABLE_PLATFORM__ @@ -2220,7 +2118,7 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { // TODO(user): Support solution hint, but then the first TODO will make it // automatic. if (!model_proto.has_objective() && !model_proto.has_solution_hint() && - !params.enumerate_all_solutions() && !params.use_lns()) { + !params.enumerate_all_solutions() && !params.use_lns_only()) { bool is_pure_sat = true; for (const IntegerVariableProto& var : model_proto.variables()) { if (var.domain_size() != 2 || var.domain(0) < 0 || var.domain(1) > 1) { @@ -2354,15 +2252,8 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { #else // __PORTABLE_PLATFORM__ if (params.num_search_workers() > 1) { SolveCpModelParallel(new_cp_model_proto, &shared_response_manager, - &wall_timer, model); + &shared_time_limit, &wall_timer, model); #endif // __PORTABLE_PLATFORM__ - } else if (params.use_lns() && new_cp_model_proto.has_objective() && - !params.enumerate_all_solutions()) { - // TODO(user,user): Provide a better diversification for different - // seeds. - SolveCpModelWithLNS(new_cp_model_proto, /*worker_id=*/1, - &shared_response_manager, - /*shared_bounds_manager=*/nullptr, &wall_timer, model); } else { if (log_search) { LOG(INFO) << absl::StrFormat("*** starting to load the model at %.2fs", diff --git a/ortools/sat/lns.h b/ortools/sat/lns.h deleted file mode 100644 index 67b7b9754b..0000000000 --- a/ortools/sat/lns.h +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2010-2018 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_SAT_LNS_H_ -#define OR_TOOLS_SAT_LNS_H_ - -#include -#include -#include -#include - -#if !defined(__PORTABLE_PLATFORM__) -#include "ortools/base/threadpool.h" -#endif // __PORTABLE_PLATFORM__ - -namespace operations_research { -namespace sat { - -// A simple deterministic and multithreaded LNS design. -// -// While !synchronize_and_maybe_stop(), we call a batch of batch_size -// generate_and_solve() in parallel using num_threads threads. The -// two given functions must be thread-safe. -// -// The general idea to enforce determinism is that each -// generate_and_solve() can update a global state asynchronously, but -// should still use the past state until the synchronize_and_maybe_stop() call -// has been done. -// -// The seed starts at zero and will be increased one by one, so it also -// represent the number of calls to generate_and_solve(). Each -// generate_and_solve() will get a different seed. -// -// The two templated types should behave like: -// - StopFunction: std::function -// - SubSolveFunction: std::function -template -void OptimizeWithLNS(int num_threads, int batch_size, - const StopFunction& synchronize_and_maybe_stop, - const SubSolveFunction& generate_and_solve); - -// This one just keep num_threads tasks always in flight and call -// synchronize_and_maybe_stop() before each generate_and_solve(). It is not -// deterministic. -template -void NonDeterministicOptimizeWithLNS( - int num_threads, const StopFunction& synchronize_and_maybe_stop, - const SubSolveFunction& generate_and_solve); - -// Basic adaptive [0.0, 1.0] parameter that can be increased or decreased with a -// step that get smaller and smaller with the number of updates. -// -// Note(user): The current logic work well in practice, but has no theoretical -// foundation. So it might be possible to use better formulas depending on the -// situation. -// -// TODO(user): In multithread, we get Increase()/Decrease() signal from -// different thread potentially working on different difficulty. The class need -// to be updated to properly handle this case. Increase()/Decrease() should take -// in the difficulty at which the signal was computed, and the update formula -// should be changed accordingly. -class AdaptiveParameterValue { - public: - // Initial value is in [0.0, 1.0], both 0.0 and 1.0 are valid. - explicit AdaptiveParameterValue(double initial_value) - : value_(initial_value) {} - - void Reset() { num_changes_ = 0; } - - void Increase() { - const double factor = IncreaseNumChangesAndGetFactor(); - value_ = std::min(1.0 - (1.0 - value_) / factor, value_ * factor); - } - - void Decrease() { - const double factor = IncreaseNumChangesAndGetFactor(); - value_ = std::max(value_ / factor, 1.0 - (1.0 - value_) * factor); - } - - double value() const { return value_; } - - private: - // We want to change the parameters more and more slowly. - double IncreaseNumChangesAndGetFactor() { - ++num_changes_; - return 1.0 + 1.0 / std::sqrt(num_changes_ + 1); - } - - double value_; - int64_t num_changes_ = 0; -}; - -// ============================================================================ -// Implementation. -// ============================================================================ - -template -inline void OptimizeWithLNS(int num_threads, int batch_size, - const StopFunction& synchronize_and_maybe_stop, - const SubSolveFunction& generate_and_solve) { - int64_t seed = 0; - -#if defined(__PORTABLE_PLATFORM__) - while (!synchronize_and_maybe_stop()) generate_and_solve(seed++); - return; -#else // __PORTABLE_PLATFORM__ - - if (num_threads == 1) { - while (!synchronize_and_maybe_stop()) generate_and_solve(seed++); - return; - } - - // TODO(user): We could reuse the same ThreadPool as long as we wait for all - // the task in a batch to finish before scheduling new ones. Not sure how - // to easily do that, so for now we just recreate the pool for each batch. - while (!synchronize_and_maybe_stop()) { - // We simply rely on the fact that a ThreadPool will only schedule - // num_threads workers in parallel. - ThreadPool pool("Deterministic_Parallel_LNS", num_threads); - pool.StartWorkers(); - for (int i = 0; i < batch_size; ++i) { - pool.Schedule( - [&generate_and_solve, seed]() { generate_and_solve(seed); }); - ++seed; - } - } -#endif // __PORTABLE_PLATFORM__ -} - -template -inline void NonDeterministicOptimizeWithLNS( - int num_threads, const StopFunction& synchronize_and_maybe_stop, - const SubSolveFunction& generate_and_solve) { - int64_t seed = 0; - -#if defined(__PORTABLE_PLATFORM__) - while (!synchronize_and_maybe_stop()) { - generate_and_solve(seed++); - } -#else // __PORTABLE_PLATFORM__ - - if (num_threads == 1) { - while (!synchronize_and_maybe_stop()) generate_and_solve(seed++); - return; - } - - // The lambda below are using little space, but there is no reason - // to create millions of them, so we use the blocking nature of - // pool.Schedule() when the queue capacity is set. - ThreadPool pool("Parallel_LNS", num_threads); - pool.SetQueueCapacity(10 * num_threads); - pool.StartWorkers(); - while (!synchronize_and_maybe_stop()) { - pool.Schedule([&generate_and_solve, seed]() { generate_and_solve(seed); }); - ++seed; - } - -#endif // __PORTABLE_PLATFORM__ -} - -} // namespace sat -} // namespace operations_research - -#endif // OR_TOOLS_SAT_LNS_H_ diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index 851f5fff2f..97879c9139 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -670,11 +670,14 @@ message SatParameters { // Specify the number of parallel workers to use during search. // A number <= 1 means no parallelism. - // - // WARNING: For now the code is non-deterministic. There is plans to make it - // deterministic (or provide an options) soon. optional int32 num_search_workers = 100 [default = 0]; + // Make the parallelization deterministic. Currently, this only work with + // use_lns_only(). + // + // TODO(user): make it work without use_lns_only(). + optional bool deterministic_parallel_search = 134 [default = false]; + // Allows objective sharing between workers. optional bool share_objective_bounds = 113 [default = true]; @@ -682,9 +685,7 @@ message SatParameters { optional bool share_level_zero_bounds = 114 [default = true]; // LNS parameters. - optional bool use_lns = 101 [default = false]; - optional int32 lns_num_threads = 102 [default = 1]; - optional bool lns_is_deterministic = 134 [default = true]; + optional bool use_lns_only = 101 [default = false]; optional bool lns_focus_on_decision_variables = 105 [default = false]; // Turns on relaxation induced neighborhood generator. diff --git a/ortools/sat/subsolver.cc b/ortools/sat/subsolver.cc new file mode 100644 index 0000000000..b07102405a --- /dev/null +++ b/ortools/sat/subsolver.cc @@ -0,0 +1,185 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "absl/time/clock.h" +#include "absl/synchronization/mutex.h" +#include "ortools/base/logging.h" +#include "ortools/sat/subsolver.h" + + +namespace operations_research { +namespace sat { + +namespace { + +// Returns the next SubSolver index from which to call GenerateTask(). Note that +// only SubSolvers for which TaskIsAvailable() is true are considered. Return -1 +// if no SubSolver can generate a new task. +// +// For now we use a really basic logic: call the least frequently called. +int NextSubsolverToSchedule( + const std::vector>& subsolvers, + const std::vector& num_generated_tasks) { + int best = -1; + for (int i = 0; i < subsolvers.size(); ++i) { + if (subsolvers[i]->TaskIsAvailable()) { + if (best == -1 || num_generated_tasks[i] < num_generated_tasks[best]) { + best = i; + } + } + } + return best; +} + +void SynchronizeAll(const std::vector>& subsolvers) { + for (const auto& subsolver : subsolvers) subsolver->Synchronize(); +} + +} // namespace + +void SequentialLoop(const std::vector>& subsolvers) { + int64 task_id = 0; + std::vector num_generated_tasks(subsolvers.size(), 0); + while (true) { + SynchronizeAll(subsolvers); + const int best = NextSubsolverToSchedule(subsolvers, num_generated_tasks); + if (best == -1) break; + num_generated_tasks[best]++; + subsolvers[best]->GenerateTask(task_id++)(); + } +} + +#if defined(__PORTABLE_PLATFORM__) + +// On portable platform, we don't support multi-threading for now. + +void NonDeterministicLoop( + const std::vector>& subsolvers, + int num_threads) { + SequentialLoop(subsolvers); +} + +void DeterministicLoop( + const std::vector>& subsolvers, int num_threads, + int batch_size) { + SequentialLoop(subsolvers); +} + +#else // __PORTABLE_PLATFORM__ + +void DeterministicLoop( + const std::vector>& subsolvers, int num_threads, + int batch_size) { + CHECK_GT(num_threads, 0); + CHECK_GT(batch_size, 0); + if (batch_size == 1) { + return SequentialLoop(subsolvers); + } + + int64 task_id = 0; + std::vector num_generated_tasks(subsolvers.size(), 0); + while (true) { + SynchronizeAll(subsolvers); + + // TODO(user): We could reuse the same ThreadPool as long as we wait for all + // the task in a batch to finish before scheduling new ones. Not sure how + // to easily do that, so for now we just recreate the pool for each batch. + ThreadPool pool("DeterministicLoop", num_threads); + pool.StartWorkers(); + + int num_in_batch = 0; + for (int t = 0; t < batch_size; ++t) { + const int best = NextSubsolverToSchedule(subsolvers, num_generated_tasks); + if (best == -1) break; + ++num_in_batch; + num_generated_tasks[best]++; + pool.Schedule(subsolvers[best]->GenerateTask(task_id++)); + } + if (num_in_batch == 0) break; + } +} + +void NonDeterministicLoop( + const std::vector>& subsolvers, + int num_threads) { + CHECK_GT(num_threads, 0); + if (num_threads == 1) { + return SequentialLoop(subsolvers); + } + + // The mutex will protect these two fields. This is used to only keep + // num_threads task in-flight and detect when the search is done. + absl::Mutex mutex; + absl::CondVar thread_available_condition; + int num_scheduled_and_not_done = 0; + + ThreadPool pool("NonDeterministicLoop", num_threads); + pool.StartWorkers(); + + // The lambda below are using little space, but there is no reason + // to create millions of them, so we use the blocking nature of + // pool.Schedule() when the queue capacity is set. + int64 task_id = 0; + std::vector num_generated_tasks(subsolvers.size(), 0); + while (true) { + bool all_done = false; + { + absl::MutexLock mutex_lock(&mutex); + + // The stopping condition is that we do not have anything else to generate + // once all the task are done and synchronized. + if (num_scheduled_and_not_done == 0) all_done = true; + + // Wait if num_scheduled_and_not_done == num_threads. + if (num_scheduled_and_not_done == num_threads) { + thread_available_condition.Wait(&mutex); + } + } + + SynchronizeAll(subsolvers); + const int best = NextSubsolverToSchedule(subsolvers, num_generated_tasks); + if (best == -1) { + if (all_done) break; + + // It is hard to know when new info will allows for more task to be + // scheduled, so for now we just sleep for a bit. Note that in practice We + // will never reach here except at the end of the search because we can + // always schedule LNS threads. + absl::SleepFor(absl::Milliseconds(1)); + continue; + } + + // Schedule next task. + num_generated_tasks[best]++; + { + absl::MutexLock mutex_lock(&mutex); + num_scheduled_and_not_done++; + } + std::function task = subsolvers[best]->GenerateTask(task_id++); + pool.Schedule([task, num_threads, &mutex, &num_scheduled_and_not_done, + &thread_available_condition]() { + task(); + + absl::MutexLock mutex_lock(&mutex); + num_scheduled_and_not_done--; + if (num_scheduled_and_not_done == num_threads - 1) { + thread_available_condition.SignalAll(); + } + }); + } +} + +#endif // __PORTABLE_PLATFORM__ + +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/subsolver.h b/ortools/sat/subsolver.h new file mode 100644 index 0000000000..4bfb8fce96 --- /dev/null +++ b/ortools/sat/subsolver.h @@ -0,0 +1,174 @@ +// Copyright 2010-2018 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Simple framework for choosing and distributing a solver "sub-tasks" on a set +// of threads. + +#ifndef OR_TOOLS_SAT_SUBSOLVER_H_ +#define OR_TOOLS_SAT_SUBSOLVER_H_ + +#include +#include +#include +#include +#include + +#include "ortools/base/integral_types.h" + +#if !defined(__PORTABLE_PLATFORM__) +#include "ortools/base/threadpool.h" +#endif // __PORTABLE_PLATFORM__ + +namespace operations_research { +namespace sat { + +// The API used for distributing work. Each subsolver can generate tasks and +// synchronize itself with the rest of the world. +// +// Note that currently only the main thread interact with subsolvers. Only the +// tasks generated by GenerateTask() are executed in parallel in a threadpool. +class SubSolver { + public: + SubSolver(int id, const std::string& name) : id_(id), name_(name) {} + virtual ~SubSolver() {} + + // Returns true iff GenerateTask() can be called. + // + // Note(user): In the current design, a SubSolver is never deleted until the + // end of the Solve() that created it. But is is okay to always return false + // here and release the memory used by the Subsolver internal if there is no + // need to call this Subsolver ever again. The overhead of iterating over it + // in the main solver loop should be minimal. + virtual bool TaskIsAvailable() = 0; + + // Returns a task to run. The task_id is just an ever increasing counter that + // correspond to the number of total calls to GenerateTask(). + // + // TODO(user): We could use a more complex selection logic and pass in the + // deterministic time limit this subtask should run for. Unclear at this + // stage. + virtual std::function GenerateTask(int64 task_id) = 0; + + // Synchronizes with the external world from this SubSolver point of view. + // Also incorporate the results of the latest completed tasks if any. + // + // Note(user): The intended implementation for determinism is that tasks + // update asynchronously (and so non-deterministically) global "shared" + // classes, but this global state is incorporated by the Subsolver only when + // Synchronize() is called. + virtual void Synchronize() = 0; + + // Returns the score as updated by the completed tasks before the last + // Synchronize() call. Everything else being equal, we prefer to run a + // SubSolver with the highest score. + // + // TODO(user): This is unused for now. + double Score() const { return score_; } + + // Returns the total deterministic time spend by the completed tasks before + // the last Synchronize() call. + // + // TODO(user): This is unused for now. + double DeterminisitcTime() const { return deterministic_time_; } + + // Returns the name of this SubSolver. Used in logs. + std::string name() const { return name_; } + + protected: + const int id_; + const std::string name_; + double score_ = 0.0; + double deterministic_time_ = 0.0; +}; + +// Executes the following loop: +// 1/ Synchronize all in given order. +// 2/ generate and schedule one task from the current "best" subsolver. +// 3/ repeat until no extra task can be generated and all tasks are done. +// +// The complexity of each selection is in O(num_subsolvers), but that should +// be okay given that we don't expect more than 100 such subsolvers. +// +// Note that it is okay to incorporate "special" subsolver that never produce +// any tasks. This can be used to synchronize classes used by many subsolvers +// just once for instance. +void NonDeterministicLoop( + const std::vector>& subsolvers, int num_threads); + +// Similar to NonDeterministicLoop() except this should result in a +// deterministic solver provided that all SubSolver respect the Synchronize() +// contract. +// +// Executes the following loop: +// 1/ Synchronize all in given order. +// 2/ generate and schedule up to batch_size tasks using an heuristic to select +// which one to run. +// 3/ wait for all task to finish. +// 4/ repeat until no task can be generated in step 2. +void DeterministicLoop( + const std::vector>& subsolvers, int num_threads, + int batch_size); + +// Same as above, but specialized implementation for the case num_threads=1. +// This avoids using a Threadpool altogether. It should have the same behavior +// than the functions above with num_threads=1 and batch_size=1. Note that an +// higher batch size will not behave in the same way, even if num_threads=1. +void SequentialLoop(const std::vector>& subsolvers); + +// Basic adaptive [0.0, 1.0] parameter that can be increased or decreased with a +// step that get smaller and smaller with the number of updates. +// +// Note(user): The current logic work well in practice, but has no theoretical +// foundation. So it might be possible to use better formulas depending on the +// situation. +// +// TODO(user): In multithread, we get Increase()/Decrease() signal from +// different thread potentially working on different difficulty. The class need +// to be updated to properly handle this case. Increase()/Decrease() should take +// in the difficulty at which the signal was computed, and the update formula +// should be changed accordingly. +class AdaptiveParameterValue { + public: + // Initial value is in [0.0, 1.0], both 0.0 and 1.0 are valid. + explicit AdaptiveParameterValue(double initial_value) + : value_(initial_value) {} + + void Reset() { num_changes_ = 0; } + + void Increase() { + const double factor = IncreaseNumChangesAndGetFactor(); + value_ = std::min(1.0 - (1.0 - value_) / factor, value_ * factor); + } + + void Decrease() { + const double factor = IncreaseNumChangesAndGetFactor(); + value_ = std::max(value_ / factor, 1.0 - (1.0 - value_) * factor); + } + + double value() const { return value_; } + + private: + // We want to change the parameters more and more slowly. + double IncreaseNumChangesAndGetFactor() { + ++num_changes_; + return 1.0 + 1.0 / std::sqrt(num_changes_ + 1); + } + + double value_; + int64_t num_changes_ = 0; +}; + +} // namespace sat +} // namespace operations_research + +#endif // OR_TOOLS_SAT_SUBSOLVER_H_ diff --git a/ortools/sat/synchronization.cc b/ortools/sat/synchronization.cc index f9c6e1067b..12a63fbabd 100644 --- a/ortools/sat/synchronization.cc +++ b/ortools/sat/synchronization.cc @@ -343,6 +343,12 @@ void SharedResponseManager::SetStatsFromModelInternal(Model* model) { time_limit->GetElapsedDeterministicTime()); } +bool SharedResponseManager::ProblemIsSolved() const { + absl::MutexLock mutex_lock(&mutex_); + return best_response_.status() == CpSolverStatus::OPTIMAL || + best_response_.status() == CpSolverStatus::INFEASIBLE; +} + SharedBoundsManager::SharedBoundsManager(int num_workers, const CpModelProto& model_proto) : num_workers_(num_workers), diff --git a/ortools/sat/synchronization.h b/ortools/sat/synchronization.h index 3a6ee8edc1..34a361d468 100644 --- a/ortools/sat/synchronization.h +++ b/ortools/sat/synchronization.h @@ -29,6 +29,48 @@ namespace operations_research { namespace sat { +// Wrapper around TimeLimit to make it thread safe and add Stop() support. +class SharedTimeLimit { + public: + explicit SharedTimeLimit(TimeLimit* time_limit) + : time_limit_(time_limit), stopped_boolean_(false) { + // We use the one already registered if present or ours otherwise. + stopped_ = time_limit->ExternalBooleanAsLimit(); + if (stopped_ == nullptr) { + stopped_ = &stopped_boolean_; + time_limit->RegisterExternalBooleanAsLimit(stopped_); + } + + // We reset the Boolean to false in case it starts at true. + *stopped_ = false; + } + + ~SharedTimeLimit() { + if (stopped_ == &stopped_boolean_) { + time_limit_->RegisterExternalBooleanAsLimit(nullptr); + } + } + + bool LimitReached() const { + absl::MutexLock mutex_lock(&mutex_); + return time_limit_->LimitReached(); + } + + void Stop() { *stopped_ = true; } + + void UpdateLocalLimit(TimeLimit* local_limit) { + absl::MutexLock mutex_lock(&mutex_); + local_limit->MergeWithGlobalTimeLimit(time_limit_); + } + + private: + mutable absl::Mutex mutex_; + TimeLimit* time_limit_; + + std::atomic stopped_boolean_; + std::atomic* stopped_; +}; + // Thread-safe. Keeps a set of n unique best solution found so far. // // TODO(user): Maybe add some criteria to only keep solution with an objective @@ -151,6 +193,10 @@ class SharedResponseManager { // TODO(user): Also support merging statistics together. void SetStatsFromModel(Model* model); + // Returns true if we found the optimal solution or the problem was proven + // infeasible. + bool ProblemIsSolved() const; + // Returns the underlying solution repository where we keep a set of best // solutions. const SharedSolutionRepository& SolutionsRepository() const { @@ -166,7 +212,7 @@ class SharedResponseManager { const CpModelProto& model_proto_; const WallTimer& wall_timer_; - absl::Mutex mutex_; + mutable absl::Mutex mutex_; CpSolverResponse best_response_; SharedSolutionRepository solutions_;