polish third_party_solvers; speed up cp-sat on no_ovrlap_2d

This commit is contained in:
Laurent Perron
2025-06-19 14:08:49 +02:00
parent c4fd63cd61
commit 9888ce3da1
11 changed files with 177 additions and 84 deletions

View File

@@ -31,7 +31,6 @@
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/memory/memory.h"
#include "absl/meta/type_traits.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
@@ -2756,6 +2755,7 @@ absl::StatusOr<bool> GurobiSolver::Update(
absl::StatusOr<std::unique_ptr<GurobiSolver>> GurobiSolver::New(
const ModelProto& input_model, const SolverInterface::InitArgs& init_args) {
// TODO(user): Correctly load the gurobi library in open source.
RETURN_IF_ERROR(
ModelIsSupported(input_model, kGurobiSupportedStructures, "Gurobi"));
if (!input_model.auxiliary_objectives().empty() &&

View File

@@ -46,7 +46,6 @@
#include "ortools/third_party_solvers/gurobi_environment.h"
#include "ortools/util/solve_interrupter.h"
namespace operations_research {
namespace math_opt {

View File

@@ -32,7 +32,6 @@
#include "ortools/sat/precedences.h"
#include "ortools/sat/scheduling_helpers.h"
#include "ortools/sat/synchronization.h"
#include "ortools/util/bitset.h"
namespace operations_research {
namespace sat {
@@ -42,22 +41,14 @@ Precedences2DPropagator::Precedences2DPropagator(
: helper_(*helper),
linear2_bounds_(model->GetOrCreate<Linear2Bounds>()),
linear2_watcher_(model->GetOrCreate<Linear2Watcher>()),
shared_stats_(model->GetOrCreate<SharedStatistics>()) {
shared_stats_(model->GetOrCreate<SharedStatistics>()),
non_trivial_bounds_(
model->GetOrCreate<Linear2WithPotentialNonTrivalBounds>()) {
model->GetOrCreate<LinearPropagator>()->SetPushAffineUbForBinaryRelation();
}
void Precedences2DPropagator::CollectPairsOfBoxesWithNonTrivialDistance() {
helper_.SynchronizeAndSetDirection();
non_trivial_pairs_.clear();
struct VarUsage {
// boxes[0=x, 1=y][0=start, 1=end]
std::vector<int> boxes[2][2];
};
absl::flat_hash_map<IntegerVariable, VarUsage> var_to_box_and_coeffs;
SparseBitset<IntegerVariable>& var_set =
*linear2_bounds_->GetTemporyClearedAndResizedBitset();
void Precedences2DPropagator::UpdateVarLookups() {
var_to_box_and_coeffs_.clear();
for (int dim = 0; dim < 2; ++dim) {
const SchedulingConstraintHelper& dim_helper =
dim == 0 ? helper_.x_helper() : helper_.y_helper();
@@ -67,41 +58,52 @@ void Precedences2DPropagator::CollectPairsOfBoxesWithNonTrivialDistance() {
for (int i = 0; i < helper_.NumBoxes(); ++i) {
const IntegerVariable var = interval_points[i].var;
if (var != kNoIntegerVariable) {
var_set.Set(PositiveVariable(var));
var_to_box_and_coeffs[PositiveVariable(var)].boxes[dim][j].push_back(
var_to_box_and_coeffs_[PositiveVariable(var)].boxes[dim][j].push_back(
i);
}
}
}
}
}
void Precedences2DPropagator::CollectNewPairsOfBoxesWithNonTrivialDistance() {
const absl::Span<const LinearExpression2> exprs =
linear2_bounds_->GetAllExpressionsWithPotentialNonTrivialBounds(
var_set.BitsetConstView());
VLOG(2) << "CollectPairsOfBoxesWithNonTrivialDistance called, num_exprs: "
<< exprs.size();
for (const LinearExpression2& expr : exprs) {
auto it1 = var_to_box_and_coeffs.find(PositiveVariable(expr.vars[0]));
auto it2 = var_to_box_and_coeffs.find(PositiveVariable(expr.vars[1]));
DCHECK(it1 != var_to_box_and_coeffs.end());
DCHECK(it2 != var_to_box_and_coeffs.end());
non_trivial_bounds_->GetLinear2WithPotentialNonTrivalBounds();
if (exprs.size() != num_known_linear2_) {
VLOG(2) << "CollectPairsOfBoxesWithNonTrivialDistance called, num_exprs: "
<< exprs.size();
}
for (; num_known_linear2_ < exprs.size(); ++num_known_linear2_) {
const LinearExpression2& positive_expr = exprs[num_known_linear2_];
LinearExpression2 negated_expr = positive_expr;
negated_expr.Negate();
for (const LinearExpression2& expr : {positive_expr, negated_expr}) {
auto it1 = var_to_box_and_coeffs_.find(PositiveVariable(expr.vars[0]));
auto it2 = var_to_box_and_coeffs_.find(PositiveVariable(expr.vars[1]));
if (it1 == var_to_box_and_coeffs_.end()) {
continue;
}
if (it2 == var_to_box_and_coeffs_.end()) {
continue;
}
const VarUsage& usage1 = it1->second;
const VarUsage& usage2 = it2->second;
for (int dim = 0; dim < 2; ++dim) {
const SchedulingConstraintHelper& dim_helper =
dim == 0 ? helper_.x_helper() : helper_.y_helper();
for (const int box1 : usage1.boxes[dim][0 /* start */]) {
for (const int box2 : usage2.boxes[dim][1 /* end */]) {
if (box1 == box2) continue;
const auto [expr2, unused] = EncodeDifferenceLowerThan(
dim_helper.Starts()[box1], dim_helper.Ends()[box2],
/*ub=unused*/ 0);
if (expr == expr2) {
if (box1 < box2) {
non_trivial_pairs_.push_back({box1, box2});
} else {
non_trivial_pairs_.push_back({box2, box1});
const VarUsage& usage1 = it1->second;
const VarUsage& usage2 = it2->second;
for (int dim = 0; dim < 2; ++dim) {
const SchedulingConstraintHelper& dim_helper =
dim == 0 ? helper_.x_helper() : helper_.y_helper();
for (const int box1 : usage1.boxes[dim][0 /* start */]) {
for (const int box2 : usage2.boxes[dim][1 /* end */]) {
if (box1 == box2) continue;
const auto [expr2, unused] = EncodeDifferenceLowerThan(
dim_helper.Starts()[box1], dim_helper.Ends()[box2],
/*ub=unused*/ 0);
if (expr == expr2) {
if (box1 < box2) {
non_trivial_pairs_.push_back({box1, box2});
} else {
non_trivial_pairs_.push_back({box2, box1});
}
}
}
}
@@ -114,13 +116,13 @@ void Precedences2DPropagator::CollectPairsOfBoxesWithNonTrivialDistance() {
bool Precedences2DPropagator::Propagate() {
if (!helper_.SynchronizeAndSetDirection()) return false;
if (last_helper_inprocessing_count_ != helper_.InProcessingCount() ||
helper_.x_helper().CurrentDecisionLevel() == 0 ||
last_linear2_timestamp_ != linear2_watcher_->Timestamp()) {
if (last_helper_inprocessing_count_ != helper_.InProcessingCount()) {
last_helper_inprocessing_count_ = helper_.InProcessingCount();
last_linear2_timestamp_ = linear2_watcher_->Timestamp();
CollectPairsOfBoxesWithNonTrivialDistance();
UpdateVarLookups();
num_known_linear2_ = 0;
non_trivial_pairs_.clear();
}
CollectNewPairsOfBoxesWithNonTrivialDistance();
num_calls_++;

View File

@@ -18,7 +18,9 @@
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/integer_base.h"
#include "ortools/sat/model.h"
#include "ortools/sat/no_overlap_2d_helper.h"
#include "ortools/sat/precedences.h"
@@ -30,7 +32,7 @@ namespace sat {
// This class implements a propagator for non_overlap_2d constraints that uses
// the Linear2Bounds to detect precedences between pairs of boxes and
// detect a conflict if the precedences implies an overlap between the two
// boxes. For doing this efficiently, it keep track of pairs of boxes that have
// boxes. For doing this efficiently, it keeps track of pairs of boxes that have
// non-fixed precedences in the Linear2Bounds and only check those in the
// propagation.
class Precedences2DPropagator : public PropagatorInterface {
@@ -43,16 +45,25 @@ class Precedences2DPropagator : public PropagatorInterface {
int RegisterWith(GenericLiteralWatcher* watcher);
private:
void CollectPairsOfBoxesWithNonTrivialDistance();
void CollectNewPairsOfBoxesWithNonTrivialDistance();
void UpdateVarLookups();
std::vector<std::pair<int, int>> non_trivial_pairs_;
struct VarUsage {
// boxes[0=x, 1=y][0=start, 1=end]
std::vector<int> boxes[2][2];
};
absl::flat_hash_map<IntegerVariable, VarUsage> var_to_box_and_coeffs_;
NoOverlap2DConstraintHelper& helper_;
Linear2Bounds* linear2_bounds_;
Linear2Watcher* linear2_watcher_;
SharedStatistics* shared_stats_;
Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_;
int last_helper_inprocessing_count_ = -1;
int num_known_linear2_ = 0;
int64_t last_linear2_timestamp_ = -1;
int64_t num_conflicts_ = 0;

View File

@@ -129,7 +129,6 @@ cc_library(
":scheduling_helpers",
":synchronization",
"//ortools/base:stl_util",
"//ortools/util:bitset",
"@abseil-cpp//absl/container:flat_hash_map",
"@abseil-cpp//absl/log",
"@abseil-cpp//absl/log:check",

View File

@@ -100,6 +100,7 @@ std::pair<bool, bool> RootLevelLinear2Bounds::Add(LinearExpression2 expr,
status_ub == AddResult::ADDED || status_ub == AddResult::UPDATED;
if (!lb_restricted && !ub_restricted) return {false, false};
non_trivial_bounds_->AddOrGet(expr);
++num_updates_;
linear2_watcher_->NotifyBoundChanged(expr);
@@ -348,6 +349,7 @@ void EnforcedLinear2Bounds::PushConditionalRelation(
const int new_index = conditional_stack_.size();
const auto [it, inserted] = conditional_relations_.insert({expr, new_index});
if (inserted) {
non_trivial_bounds_->AddOrGet(expr);
CreateLevelEntryIfNeeded();
conditional_stack_.emplace_back(/*prev_entry=*/-1, rhs, expr, enforcements);
@@ -1815,7 +1817,9 @@ Linear2BoundsFromLinear3::Linear2BoundsFromLinear3(Model* model)
linear2_watcher_(model->GetOrCreate<Linear2Watcher>()),
watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
shared_stats_(model->GetOrCreate<SharedStatistics>()),
root_level_bounds_(model->GetOrCreate<RootLevelLinear2Bounds>()) {}
root_level_bounds_(model->GetOrCreate<RootLevelLinear2Bounds>()),
non_trivial_bounds_(
model->GetOrCreate<Linear2WithPotentialNonTrivalBounds>()) {}
// Note that for speed we do not compare to the trivial or root level bounds.
//
@@ -1857,6 +1861,7 @@ bool Linear2BoundsFromLinear3::AddAffineUpperBound(LinearExpression2 expr,
it->second = {affine_ub, divisor}; // Overwrite.
} else {
// Note that this should almost never happen (only once per lin2).
non_trivial_bounds_->AddOrGet(expr);
best_affine_ub_[expr] = {affine_ub, divisor};
}

View File

@@ -14,10 +14,10 @@
#ifndef OR_TOOLS_SAT_PRECEDENCES_H_
#define OR_TOOLS_SAT_PRECEDENCES_H_
#include <algorithm>
#include <cstdint>
#include <deque>
#include <functional>
#include <limits>
#include <tuple>
#include <utility>
#include <vector>
@@ -45,6 +45,63 @@
namespace operations_research {
namespace sat {
DEFINE_STRONG_INDEX_TYPE(LinearExpression2Index);
const LinearExpression2Index kNoLinearExpression2Index(-1);
inline LinearExpression2Index NegationOf(LinearExpression2Index i) {
return LinearExpression2Index(i.value() ^ 1);
}
inline bool Linear2IsPositive(LinearExpression2Index i) {
return (i.value() & 1) == 0;
}
inline LinearExpression2Index PositiveLinear2(LinearExpression2Index i) {
return LinearExpression2Index(i.value() & (~1));
}
// Class to hold a list of LinearExpression2 that have (potentially) non-trivial
// bounds. This class is overzealous, in the sense that if a linear2 is in the
// list, it does not necessarily mean that it has a non-trivial bound, but the
// converse is true: if a linear2 is not in the list,
// Linear2Bounds::GetUpperBound() will return a trivial bound.
class Linear2WithPotentialNonTrivalBounds {
public:
Linear2WithPotentialNonTrivalBounds() = default;
// Returns a never-changing index for the given linear expression.
// The expression must already be canonicalized and divided by its GCD.
LinearExpression2Index AddOrGet(LinearExpression2 expr) {
DCHECK(expr.IsCanonicalized());
DCHECK_EQ(expr.DivideByGcd(), 1);
const bool negated = expr.NegateForCanonicalization();
auto [it, inserted] = expr_to_index_.insert({expr, exprs_.size()});
if (inserted) {
CHECK_LT(2 * exprs_.size() + 1,
std::numeric_limits<LinearExpression2Index>::max());
exprs_.push_back(expr);
}
const LinearExpression2Index positive_index(2 * it->second);
if (negated) {
return NegationOf(positive_index);
} else {
return positive_index;
}
}
// Return all positive linear2 expressions that have a potentially non-trivial
// bound. When calling this code it is often a good idea to check both the
// expression on the span and its negation. The order is fixed forever and
// this span can only grow by appending new expressions.
absl::Span<const LinearExpression2> GetLinear2WithPotentialNonTrivalBounds()
const {
return exprs_;
}
private:
util_intops::StrongVector<LinearExpression2Index, LinearExpression2> exprs_;
absl::flat_hash_map<LinearExpression2, int> expr_to_index_;
};
// Simple "watcher" class that will be notified if a linear2 bound changed. It
// can also be queried to see if LinearExpression2 involving a specific variable
// changed since last time.
@@ -79,7 +136,9 @@ class RootLevelLinear2Bounds {
explicit RootLevelLinear2Bounds(Model* model)
: integer_trail_(model->GetOrCreate<IntegerTrail>()),
linear2_watcher_(model->GetOrCreate<Linear2Watcher>()),
shared_stats_(model->GetOrCreate<SharedStatistics>()) {}
shared_stats_(model->GetOrCreate<SharedStatistics>()),
non_trivial_bounds_(
model->GetOrCreate<Linear2WithPotentialNonTrivalBounds>()) {}
~RootLevelLinear2Bounds();
@@ -148,6 +207,7 @@ class RootLevelLinear2Bounds {
IntegerTrail* integer_trail_;
Linear2Watcher* linear2_watcher_;
SharedStatistics* shared_stats_;
Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_;
// Lookup table to find all the LinearExpression2 with a given variable and
// having both coefficient 1.
@@ -258,7 +318,9 @@ class EnforcedLinear2Bounds : public ReversibleInterface {
integer_trail_(model->GetOrCreate<IntegerTrail>()),
linear2_watcher_(model->GetOrCreate<Linear2Watcher>()),
root_level_bounds_(model->GetOrCreate<RootLevelLinear2Bounds>()),
shared_stats_(model->GetOrCreate<SharedStatistics>()) {
shared_stats_(model->GetOrCreate<SharedStatistics>()),
non_trivial_bounds_(
model->GetOrCreate<Linear2WithPotentialNonTrivalBounds>()) {
integer_trail_->RegisterReversibleClass(this);
}
@@ -324,6 +386,7 @@ class EnforcedLinear2Bounds : public ReversibleInterface {
Linear2Watcher* linear2_watcher_;
RootLevelLinear2Bounds* root_level_bounds_;
SharedStatistics* shared_stats_;
Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_;
int64_t num_conditional_relation_updates_ = 0;
@@ -476,6 +539,7 @@ class Linear2BoundsFromLinear3 {
GenericLiteralWatcher* watcher_;
SharedStatistics* shared_stats_;
RootLevelLinear2Bounds* root_level_bounds_;
Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_;
int64_t num_affine_updates_ = 0;
@@ -558,7 +622,8 @@ class Linear2Bounds {
GetAllExpressionsWithPotentialNonTrivialBounds(
Bitset64<IntegerVariable>::ConstView var_set) const;
// Returns a temporay bitset, cleared, and resized for all existing variables.
// Returns a temporary bitset, cleared, and resized for all existing
// variables.
//
// If we have many class calling
// GetAllExpressionsWithPotentialNonTrivialBounds() it is important that not

View File

@@ -57,6 +57,7 @@ cc_library(
"//ortools/base",
"//ortools/base:file",
"//ortools/base:status_macros",
"@abseil-cpp//absl/base:core_headers",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
"@abseil-cpp//absl/strings",

View File

@@ -32,5 +32,5 @@ target_link_libraries(${NAME} PRIVATE
absl::status
absl::strings
absl::str_format
absl::synchronization
)
absl::synchronization)

View File

@@ -13,16 +13,16 @@
#include "ortools/third_party_solvers/gurobi_environment.h"
#include <cstdlib>
#include <functional>
#include <mutex>
#include <string>
#include <vector>
#include "absl/base/no_destructor.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "ortools/base/logging.h"
#include "ortools/third_party_solvers/dynamic_library.h"
@@ -332,12 +332,14 @@ void LoadGurobiFunctions(DynamicLibrary* gurobi_dynamic_library) {
gurobi_dynamic_library->GetFunction(&GRBplatform, "GRBplatform");
}
// clang-format on
std::vector<std::string> GurobiDynamicLibraryPotentialPaths() {
std::vector<std::string> potential_paths;
const std::vector<absl::string_view> kGurobiVersions = {
"1202", "1201", "1200", "1103", "1102", "1101", "1100", "1003",
"1002", "1001", "1000", "952", "951", "950", "911",
"910", "903", "902", "811", "801", "752"};
"1202", "1201", "1200", "1103", "1102", "1101", "1100",
"1003", "1002", "1001", "1000", "952", "951", "950",
"911", "910", "903", "902", "811", "801", "752"};
potential_paths.reserve(kGurobiVersions.size() * 3);
// Look for libraries pointed by GUROBI_HOME first.
@@ -396,7 +398,7 @@ std::vector<std::string> GurobiDynamicLibraryPotentialPaths() {
#if defined(__GNUC__) // path in linux64 gurobi/optimizer docker image.
for (const absl::string_view version :
{"12.0.2","12.0.1", "12.0.0", "11.0.3", "11.0.2", "11.0.1", "11.0.0",
{"12.0.2", "12.0.1", "12.0.0", "11.0.3", "11.0.2", "11.0.1", "11.0.0",
"10.0.3", "10.0.2", "10.0.1", "10.0.0", "9.5.2", "9.5.1", "9.5.0"}) {
potential_paths.push_back(
absl::StrCat("/opt/gurobi/linux64/lib/libgurobi.so.", version));
@@ -407,37 +409,47 @@ std::vector<std::string> GurobiDynamicLibraryPotentialPaths() {
absl::Status LoadGurobiDynamicLibrary(
std::vector<absl::string_view> potential_paths) {
static std::once_flag gurobi_loading_done;
static absl::Status gurobi_load_status;
static DynamicLibrary gurobi_library;
static absl::Mutex mutex;
struct GurobiLibraryStruct {
absl::Status gurobi_load_status;
DynamicLibrary gurobi_library;
};
absl::MutexLock lock(&mutex);
std::call_once(gurobi_loading_done, [&potential_paths]() {
const std::vector<std::string> canonical_paths =
GurobiDynamicLibraryPotentialPaths();
potential_paths.insert(potential_paths.end(), canonical_paths.begin(),
canonical_paths.end());
static absl::NoDestructor<GurobiLibraryStruct> loaded([&potential_paths]() {
GurobiLibraryStruct result;
// Try to load the library from the potential paths.
for (const absl::string_view path : potential_paths) {
if (gurobi_library.TryToLoad(path)) {
LOG(INFO) << "Found the Gurobi library in '" << path << ".";
if (result.gurobi_library.TryToLoad(path)) {
VLOG(1) << "Found the Gurobi library in '" << path << ".";
break;
}
}
if (gurobi_library.LibraryIsLoaded()) {
LoadGurobiFunctions(&gurobi_library);
gurobi_load_status = absl::OkStatus();
// Fallback to the canonical paths.
if (!result.gurobi_library.LibraryIsLoaded()) {
const std::vector<std::string> canonical_paths =
GurobiDynamicLibraryPotentialPaths();
for (const absl::string_view path : canonical_paths) {
if (result.gurobi_library.TryToLoad(path)) {
VLOG(1) << "Found the Gurobi library in '" << path << ".";
break;
}
}
}
if (result.gurobi_library.LibraryIsLoaded()) {
LoadGurobiFunctions(&result.gurobi_library);
result.gurobi_load_status = absl::OkStatus();
} else {
gurobi_load_status = absl::NotFoundError(absl::StrCat(
result.gurobi_load_status = absl::NotFoundError(absl::StrCat(
"Could not find the Gurobi shared library. Looked in: [",
absl::StrJoin(potential_paths, "', '"),
"]. If you know where it"
" is, pass the full path to 'LoadGurobiDynamicLibrary()'."));
}
});
return gurobi_load_status;
return result;
}());
return loaded->gurobi_load_status;
}
} // namespace operations_research

View File

@@ -27,7 +27,6 @@
#endif
extern "C" {
typedef struct _GRBmodel GRBmodel;
typedef struct _GRBenv GRBenv;
typedef struct _GRBsvec {