linear solver: Add InterruptSolve feature
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -32,6 +33,8 @@
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_replace.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/synchronization/notification.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "ortools/base/accurate_sum.h"
|
||||
#include "ortools/base/commandlineflags.h"
|
||||
#include "ortools/base/integral_types.h"
|
||||
@@ -39,6 +42,7 @@
|
||||
#include "ortools/base/map_util.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/base/stl_util.h"
|
||||
#include "ortools/base/threadpool.h"
|
||||
#include "ortools/linear_solver/linear_solver.pb.h"
|
||||
#include "ortools/linear_solver/model_exporter.h"
|
||||
#include "ortools/linear_solver/model_validator.h"
|
||||
@@ -838,10 +842,35 @@ void MPSolver::FillSolutionResponseProto(MPSolutionResponse* response) const {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool InCategory(int status, int category) {
|
||||
if (category == MPSOLVER_OPTIMAL) return status == MPSOLVER_OPTIMAL;
|
||||
while (status > category) status >>= 4;
|
||||
return status == category;
|
||||
}
|
||||
|
||||
void AppendStatusStr(const std::string& msg, MPSolutionResponse* response) {
|
||||
response->set_status_str(
|
||||
absl::StrCat(response->status_str(),
|
||||
(response->status_str().empty() ? "" : "\n"), msg));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
void MPSolver::SolveWithProto(const MPModelRequest& model_request,
|
||||
MPSolutionResponse* response) {
|
||||
MPSolutionResponse* response,
|
||||
const std::atomic<bool>* interrupt) {
|
||||
CHECK(response != nullptr);
|
||||
|
||||
if (interrupt != nullptr &&
|
||||
!SolverTypeSupportsInterruption(model_request.solver_type())) {
|
||||
response->set_status(MPSOLVER_INCOMPATIBLE_OPTIONS);
|
||||
response->set_status_str(
|
||||
"Called MPSolver::SolveWithProto with an underlying solver that "
|
||||
"doesn't support interruption.");
|
||||
return;
|
||||
}
|
||||
|
||||
MPSolver solver(model_request.model().name(),
|
||||
static_cast<MPSolver::OptimizationProblemType>(
|
||||
model_request.solver_type()));
|
||||
@@ -849,10 +878,15 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request,
|
||||
solver.EnableOutput();
|
||||
}
|
||||
|
||||
auto optional_response = solver.interface_->DirectlySolveProto(model_request);
|
||||
if (optional_response) {
|
||||
*response = std::move(optional_response).value();
|
||||
return;
|
||||
// If interruption support is not required, we don't need access to the
|
||||
// underlying solver and can solve it directly if the interface supports it.
|
||||
if (interrupt == nullptr) {
|
||||
auto optional_response =
|
||||
solver.interface_->DirectlySolveProto(model_request);
|
||||
if (optional_response) {
|
||||
*response = std::move(optional_response).value();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const absl::optional<LazyMutableCopy<MPModelProto>> optional_model =
|
||||
@@ -898,12 +932,113 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request,
|
||||
}
|
||||
}
|
||||
}
|
||||
solver.Solve();
|
||||
solver.FillSolutionResponseProto(response);
|
||||
|
||||
if (interrupt == nullptr) {
|
||||
// If we don't need interruption support, we can save some overhead by
|
||||
// running the solve in the current thread.
|
||||
solver.Solve();
|
||||
solver.FillSolutionResponseProto(response);
|
||||
} else {
|
||||
const absl::Time start_time = absl::Now();
|
||||
absl::Time interrupt_time;
|
||||
bool interrupted_by_user = false;
|
||||
{
|
||||
absl::Notification solve_finished;
|
||||
auto polling_func = [&interrupt, &solve_finished, &solver,
|
||||
&interrupted_by_user, &interrupt_time,
|
||||
&model_request]() {
|
||||
constexpr absl::Duration kPollDelay = absl::Microseconds(100);
|
||||
constexpr absl::Duration kMaxInterruptionDelay = absl::Seconds(10);
|
||||
|
||||
while (!interrupt) {
|
||||
if (solve_finished.HasBeenNotified()) return;
|
||||
absl::SleepFor(kPollDelay);
|
||||
}
|
||||
|
||||
// If we get here, we received an interruption notification before the
|
||||
// solve finished "naturally".
|
||||
solver.InterruptSolve();
|
||||
interrupt_time = absl::Now();
|
||||
interrupted_by_user = true;
|
||||
|
||||
// SUBTLE: our call to InterruptSolve() can be ignored by the
|
||||
// underlying solver for several reasons:
|
||||
// 1) The solver thread doesn't poll its 'interrupted' bit often
|
||||
// enough and takes too long to realize that it should return, or
|
||||
// its mere return + FillSolutionResponse() takes too long.
|
||||
// 2) The user interrupted the solve so early that Solve() hadn't
|
||||
// really started yet when we called InterruptSolve().
|
||||
// In case 1), we should just wait a little longer. In case 2), we
|
||||
// should call InterruptSolve() again, maybe several times. To both
|
||||
// accommodate cases where the solver takes really a long time to
|
||||
// react to the interruption, while returning as quickly as possible,
|
||||
// we poll the solve_finished notification with increasing durations
|
||||
// and call InterruptSolve again, each time.
|
||||
for (absl::Duration poll_delay = kPollDelay;
|
||||
absl::Now() <= interrupt_time + kMaxInterruptionDelay;
|
||||
poll_delay *= 2) {
|
||||
if (solve_finished.WaitForNotificationWithTimeout(poll_delay)) {
|
||||
return;
|
||||
} else {
|
||||
solver.InterruptSolve();
|
||||
}
|
||||
}
|
||||
|
||||
LOG(DFATAL)
|
||||
<< "MPSolver::InterruptSolve() seems to be ignored by the "
|
||||
"underlying solver, despite repeated calls over at least "
|
||||
<< absl::FormatDuration(kMaxInterruptionDelay)
|
||||
<< ". Solver type used: "
|
||||
<< MPModelRequest_SolverType_Name(model_request.solver_type());
|
||||
|
||||
// Note that in opt builds, the polling thread terminates here with an
|
||||
// error message, but we let Solve() finish, ignoring the user
|
||||
// interruption request.
|
||||
};
|
||||
|
||||
// The choice to do polling rather than solving in the second thread is
|
||||
// not arbitrary, as we want to maintain any custom thread options set by
|
||||
// the user. They shouldn't matter for polling, but for solving we might
|
||||
// e.g. use a larger stack.
|
||||
ThreadPool thread_pool("SolverThread", /*num_threads=*/1);
|
||||
thread_pool.StartWorkers();
|
||||
thread_pool.Schedule(polling_func);
|
||||
|
||||
// Make sure the interruption notification didn't arrived while waiting to
|
||||
// be scheduled.
|
||||
if (!interrupt) {
|
||||
solver.Solve();
|
||||
solver.FillSolutionResponseProto(response);
|
||||
} else {
|
||||
response->set_status(MPSOLVER_CANCELLED_BY_USER);
|
||||
response->set_status_str(
|
||||
"Solve not started, because the user set the atomic<bool> in "
|
||||
"MPSolver::SolveWithProto() to true before solving could "
|
||||
"start.");
|
||||
}
|
||||
solve_finished.Notify();
|
||||
|
||||
// We block until the thread finishes when thread_pool goes out of scope.
|
||||
}
|
||||
|
||||
if (interrupted_by_user) {
|
||||
// Despite the interruption, the solver might still have found a useful
|
||||
// result. If so, don't overwrite the status.
|
||||
if (InCategory(response->status(), MPSOLVER_NOT_SOLVED)) {
|
||||
response->set_status(MPSOLVER_CANCELLED_BY_USER);
|
||||
}
|
||||
AppendStatusStr(
|
||||
absl::StrFormat(
|
||||
"User interrupted MPSolver::SolveWithProto() by setting the "
|
||||
"atomic<bool> to true at %s (%s after solving started.)",
|
||||
absl::FormatTime(interrupt_time),
|
||||
absl::FormatDuration(interrupt_time - start_time)),
|
||||
response);
|
||||
}
|
||||
}
|
||||
|
||||
if (!warning_message.empty()) {
|
||||
response->set_status_str(absl::StrCat(
|
||||
response->status_str(), (response->status_str().empty() ? "" : "\n"),
|
||||
warning_message));
|
||||
AppendStatusStr(warning_message, response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
#ifndef OR_TOOLS_LINEAR_SOLVER_LINEAR_SOLVER_H_
|
||||
#define OR_TOOLS_LINEAR_SOLVER_LINEAR_SOLVER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -507,6 +508,8 @@ class MPSolver {
|
||||
* true regardless of whether there's an ongoing Solve() or not. The Solve()
|
||||
* call may still linger for a while depending on the conditions. If
|
||||
* interruption is not supported; returns false and does nothing.
|
||||
* MPSolver::SolverTypeSupportsInterruption can be used to check if
|
||||
* interruption is supported for a given solver type.
|
||||
*/
|
||||
bool InterruptSolve();
|
||||
|
||||
@@ -534,21 +537,33 @@ class MPSolver {
|
||||
|
||||
/**
|
||||
* Solves the model encoded by a MPModelRequest protocol buffer and fills the
|
||||
* solution encoded as a MPSolutionResponse.
|
||||
* solution encoded as a MPSolutionResponse. The solve is stopped prematurely
|
||||
* if interrupt is non-null at set to true during (or before) solving.
|
||||
* Interruption is only supported if SolverTypeSupportsInterruption() returns
|
||||
* true for the requested solver. Passing a non-null interruption with any
|
||||
* other solver type immediately returns an MPSOLVER_INCOMPATIBLE_OPTIONS
|
||||
* error.
|
||||
*
|
||||
* Note(user): This creates a temporary MPSolver and destroys it at the end.
|
||||
* If you want to keep the MPSolver alive (for debugging, or for incremental
|
||||
* solving), you should write another version of this function that creates
|
||||
* the MPSolver object on the heap and returns it.
|
||||
*
|
||||
* Note(pawell): This attempts to first use `DirectlySolveProto()` (if
|
||||
* Note(user): This attempts to first use `DirectlySolveProto()` (if
|
||||
* implemented). Consequently, this most likely does *not* override any of
|
||||
* the default parameters of the underlying solver. This behavior *differs*
|
||||
* from `MPSolver::Solve()` which by default sets the feasibility tolerance
|
||||
* and the gap limit (as of 2020/02/11, to 1e-7 and 0.0001, respectively).
|
||||
*/
|
||||
static void SolveWithProto(const MPModelRequest& model_request,
|
||||
MPSolutionResponse* response);
|
||||
MPSolutionResponse* response,
|
||||
const std::atomic<bool>* interrupt = nullptr);
|
||||
|
||||
static bool SolverTypeSupportsInterruption(
|
||||
const MPModelRequest::SolverType solver) {
|
||||
// Interruption requires that MPSolver::InterruptSolve is supported for the
|
||||
// underlying solver. Interrupting requests using SCIP is also not supported
|
||||
// as of 2021/08/23, since InterruptSolve is not thread-safe for SCIP.
|
||||
return solver == MPModelRequest::GLOP_LINEAR_PROGRAMMING ||
|
||||
solver == MPModelRequest::GUROBI_LINEAR_PROGRAMMING ||
|
||||
solver == MPModelRequest::GUROBI_MIXED_INTEGER_PROGRAMMING ||
|
||||
solver == MPModelRequest::SAT_INTEGER_PROGRAMMING;
|
||||
}
|
||||
|
||||
/// Exports model to protocol buffer.
|
||||
void ExportModelToProto(MPModelProto* output_model) const;
|
||||
|
||||
Reference in New Issue
Block a user