improve StopSearch() robustness

This commit is contained in:
Laurent Perron
2021-01-08 09:48:52 +01:00
parent 58455ea5d7
commit c55f00b7ed
9 changed files with 108 additions and 90 deletions

View File

@@ -329,6 +329,7 @@ void GLOPInterface::SetStartingLpBasis(
void GLOPInterface::SetParameters(const MPSolverParameters& param) {
parameters_.Clear();
parameters_.set_log_search_progress(!quiet_);
SetCommonParameters(param);
SetScalingMode(param.GetIntegerParam(MPSolverParameters::SCALING));
}

View File

@@ -68,6 +68,7 @@ std::function<int(GRBenv*, GRBmodel**, const char*, int numvars, double*,
double*, double*, char*, char**)>
GRBnewmodel = nullptr;
std::function<int(GRBmodel*)> GRBoptimize = nullptr;
std::function<int(GRBenv *dest, GRBenv *src)> GRBcopyparams = nullptr;
std::function<int(GRBenv*, const char*)> GRBreadparams = nullptr;
std::function<int(GRBenv*)> GRBresetparams = nullptr;
std::function<int(GRBmodel*, const char*, int, char)> GRBsetcharattrelement =
@@ -158,6 +159,7 @@ void LoadGurobiFunctions() {
gurobi_dynamic_library->GetFunction(&GRBloadenv, NAMEOF(GRBloadenv));
gurobi_dynamic_library->GetFunction(&GRBnewmodel, NAMEOF(GRBnewmodel));
gurobi_dynamic_library->GetFunction(&GRBoptimize, NAMEOF(GRBoptimize));
gurobi_dynamic_library->GetFunction(&GRBcopyparams, NAMEOF(GRBcopyparams));
gurobi_dynamic_library->GetFunction(&GRBreadparams, NAMEOF(GRBreadparams));
gurobi_dynamic_library->GetFunction(&GRBresetparams, NAMEOF(GRBresetparams));
gurobi_dynamic_library->GetFunction(&GRBsetcharattrelement,

View File

@@ -72,6 +72,7 @@ extern std::function<int(GRBenv*, GRBmodel**, const char*, int numvars, double*,
extern std::function<int(GRBmodel*)> GRBoptimize;
extern std::function<int(GRBenv*, const char*)> GRBreadparams;
extern std::function<int(GRBenv*)> GRBresetparams;
extern std::function<int(GRBenv *dest, GRBenv *src)> GRBcopyparams;
extern std::function<int(GRBmodel*, const char*, int, char)>
GRBsetcharattrelement;
extern std::function<int(GRBmodel*, const char*, double)> GRBsetdblattr;

View File

@@ -145,6 +145,7 @@ class GurobiInterface : public MPSolverInterface {
}
bool InterruptSolve() override {
const absl::MutexLock lock(&hold_interruptions_mutex_);
if (model_ != nullptr) GRBterminate(model_);
return true;
}
@@ -204,9 +205,6 @@ class GurobiInterface : public MPSolverInterface {
void SetScalingMode(int value) override;
void SetLpAlgorithm(int value) override;
bool ReadParameterFile(const std::string& filename) override;
std::string ValidFileExtensionForParameterFile() const override;
MPSolver::BasisStatus TransformGRBVarBasisStatus(
int gurobi_basis_status) const;
MPSolver::BasisStatus TransformGRBConstraintBasisStatus(
@@ -255,6 +253,11 @@ class GurobiInterface : public MPSolverInterface {
int num_gurobi_linear_cons_ = 0;
// See the implementation note at the top of file on incrementalism.
bool had_nonincremental_change_ = false;
// Mutex is held to prevent InterruptSolve() to call GRBterminate() when
// model_ is not completely built. It also prevents model_ to be changed
// during the execution of GRBterminate().
mutable absl::Mutex hold_interruptions_mutex_;
};
namespace {
@@ -621,7 +624,10 @@ GurobiInterface::~GurobiInterface() {
// ------ Model modifications and extraction -----
void GurobiInterface::Reset() {
CheckedGurobiCall(GRBfreemodel(model_));
// We hold calls to GRBterminate() until the new model_ is ready.
const absl::MutexLock lock(&hold_interruptions_mutex_);
GRBmodel* old_model = model_;
CheckedGurobiCall(GRBnewmodel(env_, &model_, solver_->name_.c_str(),
0, // numvars
nullptr, // obj
@@ -629,6 +635,20 @@ void GurobiInterface::Reset() {
nullptr, // ub
nullptr, // vtype
nullptr)); // varnames
// Copy all existing parameters from the previous model to the new one. This
// ensures that if a user calls multiple times
// SetSolverSpecificParametersAsString() and then Reset() is called, we still
// take into account all parameters.
//
// The current code only reapplies the parameters stored in
// solver_specific_parameter_string_ at the start of the solve; other
// parameters set by previous calls are only kept in the Gurobi model.
CheckedGurobiCall(GRBcopyparams(GRBgetenv(model_), GRBgetenv(old_model)));
CheckedGurobiCall(GRBfreemodel(old_model));
old_model = nullptr;
ResetExtractionInformation();
mp_var_to_gurobi_var_.clear();
mp_cons_to_gurobi_linear_cons_.clear();
@@ -1359,15 +1379,6 @@ void GurobiInterface::Write(const std::string& filename) {
}
}
bool GurobiInterface::ReadParameterFile(const std::string& filename) {
// A non-zero return value indicates that a problem occurred.
return GRBreadparams(GRBgetenv(model_), filename.c_str()) == 0;
}
std::string GurobiInterface::ValidFileExtensionForParameterFile() const {
return ".prm";
}
MPSolverInterface* BuildGurobiInterface(bool mip, MPSolver* const solver) {
return new GurobiInterface(solver, mip);
}

View File

@@ -1784,51 +1784,15 @@ absl::Status MPSolverInterface::SetNumThreads(int num_threads) {
bool MPSolverInterface::SetSolverSpecificParametersAsString(
const std::string& parameters) {
// Note(user): this method needs to return a success/failure boolean
// immediately, so we also perform the actual parameter parsing right away.
// Some implementations will keep them forever and won't need to re-parse
// them; some (eg. Gurobi) need to re-parse the parameters every time they do
// Solve(). We just store the parameters string anyway.
//
// Note(user): This is not implemented on Android because there is no
// temporary directory to write files to without a pointer to the Java
// environment.
if (parameters.empty()) return true;
if (parameters.empty()) {
return true;
}
std::string extension = ValidFileExtensionForParameterFile();
std::string filename;
bool no_error_so_far = PortableTemporaryFile(nullptr, &filename);
filename += extension;
if (no_error_so_far) {
no_error_so_far = PortableFileSetContents(filename, parameters).ok();
}
if (no_error_so_far) {
no_error_so_far = ReadParameterFile(filename);
// We need to clean up the file even if ReadParameterFile() returned
// false. In production we can continue even if the deletion failed.
if (!PortableDeleteFile(filename).ok()) {
LOG(DFATAL) << "Couldn't delete temporary parameters file: " << filename;
}
}
if (!no_error_so_far) {
LOG(WARNING) << "Error in SetSolverSpecificParametersAsString() "
<< "for solver type: "
<< ProtoEnumToString<MPModelRequest::SolverType>(
static_cast<MPModelRequest::SolverType>(
solver_->ProblemType()));
}
return no_error_so_far;
}
bool MPSolverInterface::ReadParameterFile(const std::string& filename) {
LOG(WARNING) << "ReadParameterFile() not supported by this solver.";
LOG(WARNING) << "SetSolverSpecificParametersAsString() not supported by "
<< SolverVersion();
return false;
}
std::string MPSolverInterface::ValidFileExtensionForParameterFile() const {
return ".tmp";
}
// ---------- MPSolverParameters ----------
const double MPSolverParameters::kDefaultRelativeMipGap = 1e-4;

View File

@@ -1782,22 +1782,11 @@ class MPSolverInterface {
// solver-specific and is the same as the corresponding solver configuration
// file format. Returns true if the operation was successful.
//
// The default implementation of this method stores the parameters in a
// temporary file and calls ReadParameterFile to import the parameter file
// into the solver. Solvers that support passing the parameters directly can
// override this method to skip the temporary file logic.
// Default implementation returns true if the input is empty. It returns false
// and logs a WARNING if the input is not empty.
virtual bool SetSolverSpecificParametersAsString(
const std::string& parameters);
// Reads a solver-specific file of parameters and set them.
// Returns true if there was no errors.
virtual bool ReadParameterFile(const std::string& filename);
// Returns a file extension like ".tmp", this is needed because some solvers
// require a given extension for the ReadParameterFile() filename and we need
// to know it to generate a temporary parameter file.
virtual std::string ValidFileExtensionForParameterFile() const;
// Sets the scaling mode.
virtual void SetScalingMode(int value) = 0;
virtual void SetLpAlgorithm(int value) = 0;

View File

@@ -17,15 +17,16 @@
#define OR_TOOLS_LINEAR_SOLVER_LINEAR_SOLVER_CALLBACK_H_
#include <string>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "ortools/base/integral_types.h"
namespace operations_research {
class MPVariable;
class LinearExpr;
class LinearRange;
class MPVariable;
// The current state of the solver when the callback is invoked.
//

View File

@@ -310,6 +310,7 @@ PY_CONVERT(MPVariable);
%newobject operations_research::MPSolver::CreateSolver;
%unignore operations_research::MPSolver::CreateSolver;
%unignore operations_research::MPSolver::ParseAndCheckSupportForProblemType;
%unignore operations_research::MPSolver::Solve;
%unignore operations_research::MPSolver::VerifySolution;
%unignore operations_research::MPSolver::infinity;

View File

@@ -24,6 +24,7 @@
#include "absl/status/status.h"
#include "absl/strings/str_format.h"
#include "absl/types/optional.h"
#include "ortools/base/cleanup.h"
#include "ortools/base/commandlineflags.h"
#include "ortools/base/hash.h"
#include "ortools/base/integral_types.h"
@@ -39,6 +40,7 @@
#include "ortools/linear_solver/scip_proto_solver.h"
#include "scip/cons_indicator.h"
#include "scip/scip.h"
#include "scip/scip_copy.h"
#include "scip/scip_param.h"
#include "scip/scip_prob.h"
#include "scip/scipdefplugins.h"
@@ -108,7 +110,12 @@ class SCIPInterface : public MPSolverInterface {
}
bool InterruptSolve() override {
if (scip_ == nullptr) return true; // NOTE(user): Is this weird?
const absl::MutexLock lock(&hold_interruptions_mutex_);
if (scip_ == nullptr) {
LOG_IF(DFATAL, status_.ok()) << "scip_ is null is unexpected here, since "
"status_ did not report any error";
return true;
}
return SCIPinterruptSolve(scip_) == SCIP_OKAY;
}
@@ -173,7 +180,10 @@ class SCIPInterface : public MPSolverInterface {
void SetSolution(SCIP_SOL* solution);
absl::Status CreateSCIP();
void DeleteSCIP();
// Deletes variables and constraints from scip_ and reset scip_ to null. If
// return_scip is false, deletes the SCIP object; if true, returns it (but
// scip_ is still set to null).
SCIP* DeleteSCIP(bool return_scip = false);
// SCIP has many internal checks (many of which are numerical) that can fail
// during various phases: upon startup, when loading the model, when solving,
@@ -193,6 +203,11 @@ class SCIPInterface : public MPSolverInterface {
EmptyStruct constraint_data_for_handler_;
bool branching_priority_reset_ = false;
bool callback_reset_ = false;
// Mutex that is held to prevent InterruptSolve() to call SCIPinterruptSolve()
// when scip_ is being built. It also prevents rebuilding scip_ until
// SCIPinterruptSolve() has returned.
mutable absl::Mutex hold_interruptions_mutex_;
};
class ScipConstraintHandlerForMPCallback
@@ -206,14 +221,30 @@ class ScipConstraintHandlerForMPCallback
std::vector<CallbackRangeConstraint> SeparateIntegerSolution(
const ScipConstraintHandlerContext& context, const EmptyStruct&) override;
MPCallback* const mp_callback() const { return mp_callback_; }
private:
std::vector<CallbackRangeConstraint> SeparateSolution(
const ScipConstraintHandlerContext& context,
const bool at_integer_solution);
MPCallback* mp_callback_;
MPCallback* const mp_callback_;
};
#define RETURN_IF_ALREADY_IN_ERROR_STATE \
do { \
if (!status_.ok()) { \
VLOG_EVERY_N(1, 10) << "Early abort: SCIP is in error state."; \
return; \
} \
} while (false)
#define RETURN_AND_STORE_IF_SCIP_ERROR(x) \
do { \
status_ = SCIP_TO_STATUS(x); \
if (!status_.ok()) return; \
} while (false)
SCIPInterface::SCIPInterface(MPSolver* solver)
: MPSolverInterface(solver), scip_(nullptr) {
status_ = CreateSCIP();
@@ -222,10 +253,30 @@ SCIPInterface::SCIPInterface(MPSolver* solver)
SCIPInterface::~SCIPInterface() { DeleteSCIP(); }
void SCIPInterface::Reset() {
DeleteSCIP();
// We hold calls to SCIPinterruptSolve() until the new scip_ is fully built.
const absl::MutexLock lock(&hold_interruptions_mutex_);
// Remove existing one but keep it alive to copy parameters from it.
SCIP* old_scip = DeleteSCIP(/*return_scip=*/true);
const auto scip_deleter = absl::MakeCleanup(
[&old_scip]() { CHECK_EQ(SCIPfree(&old_scip), SCIP_OKAY); });
scip_constraint_handler_.reset();
status_ = CreateSCIP();
ResetExtractionInformation();
// Install the new one.
status_ = CreateSCIP();
if (!status_.ok()) {
return;
}
// Copy all existing parameters from the previous SCIP to the new one. This
// ensures that if a user calls multiple times
// SetSolverSpecificParametersAsString() and then Reset() is called, we still
// take into account all parameters. Note though that at the end of Solve(),
// parameters are reset so after Solve() has been called, only the last set
// parameters are kept.
RETURN_AND_STORE_IF_SCIP_ERROR(SCIPcopyParamSettings(old_scip, scip_));
}
absl::Status SCIPInterface::CreateSCIP() {
@@ -253,7 +304,7 @@ absl::Status SCIPInterface::CreateSCIP() {
return absl::OkStatus();
}
void SCIPInterface::DeleteSCIP() {
SCIP* SCIPInterface::DeleteSCIP(bool return_scip) {
// NOTE(user): DeleteSCIP() shouldn't "give up" mid-stage if it fails, since
// it might be the user's chance to reset the solver to start fresh without
// errors. The current code isn't perfect, since some CHECKs() remain, but
@@ -267,24 +318,15 @@ void SCIPInterface::DeleteSCIP() {
CHECK_EQ(SCIPreleaseCons(scip_, &scip_constraints_[j]), SCIP_OKAY);
}
scip_constraints_.clear();
CHECK_EQ(SCIPfree(&scip_), SCIP_OKAY);
SCIP* old_scip = scip_;
scip_ = nullptr;
if (!return_scip) {
CHECK_EQ(SCIPfree(&old_scip), SCIP_OKAY);
}
return old_scip;
}
#define RETURN_IF_ALREADY_IN_ERROR_STATE \
do { \
if (!status_.ok()) { \
VLOG_EVERY_N(1, 10) << "Early abort: SCIP is in error state."; \
return; \
} \
} while (false)
#define RETURN_AND_STORE_IF_SCIP_ERROR(x) \
do { \
status_ = SCIP_TO_STATUS(x); \
if (!status_.ok()) return; \
} while (false)
// Not cached.
void SCIPInterface::SetOptimizationDirection(bool maximize) {
RETURN_IF_ALREADY_IN_ERROR_STATE;
@@ -662,7 +704,13 @@ MPSolver::ResultStatus SCIPInterface::Solve(const MPSolverParameters& param) {
ExtractModel();
VLOG(1) << absl::StrFormat("Model built in %s.",
absl::FormatDuration(timer.GetDuration()));
if (callback_ != nullptr) {
if (scip_constraint_handler_ != nullptr) {
// When the value of `callback_` is changed, `callback_reset_` is set and
// code above you call Reset() that should have cleared
// `scip_constraint_handler_`. Here we assert that if this has not happened
// then `callback_` value has not changed.
CHECK_EQ(scip_constraint_handler_->mp_callback(), callback_);
} else if (callback_ != nullptr) {
scip_constraint_handler_ =
absl::make_unique<ScipConstraintHandlerForMPCallback>(callback_);
RegisterConstraintHandler<EmptyStruct>(scip_constraint_handler_.get(),