Skip to content

Commit

Permalink
[CP-SAT] fix bug on objective_shaving_search; experimental graph_arc_lns
Browse files Browse the repository at this point in the history
  • Loading branch information
lperron committed Jun 27, 2023
1 parent c50650e commit 5194c98
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 58 deletions.
68 changes: 68 additions & 0 deletions ortools/sat/cp_model_lns.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,74 @@ Neighborhood VariableGraphNeighborhoodGenerator::Generate(
return helper_.RelaxGivenVariables(initial_solution, relaxed_variables);
}

// Note that even if difficulty means full neighborhood, we go through the
// generation process to never get out of a connected components.
Neighborhood ArcGraphNeighborhoodGenerator::Generate(
const CpSolverResponse& initial_solution, double difficulty,
absl::BitGenRef random) {
const int num_model_vars = helper_.ModelProto().variables_size();
if (num_model_vars == 0) return helper_.FullNeighborhood();

std::vector<bool> relaxed_variables_set(num_model_vars, false);
std::vector<int> relaxed_variables;
{
absl::ReaderMutexLock graph_lock(&helper_.graph_mutex_);

// The number of active variables can decrease asynchronously.
// We read the exact number while locked.
const int num_active_vars =
helper_.ActiveVariablesWhileHoldingLock().size();
const int num_objective_variables =
helper_.ActiveObjectiveVariablesWhileHoldingLock().size();
const int target_size = std::ceil(difficulty * num_active_vars);
if (target_size == num_active_vars) return helper_.FullNeighborhood();
if (target_size == 0) return helper_.FullNeighborhood();

const int first_var =
num_objective_variables > 0 // Prefer objective variables.
? helper_.ActiveObjectiveVariablesWhileHoldingLock()
[absl::Uniform<int>(random, 0, num_objective_variables)]
: helper_.ActiveVariablesWhileHoldingLock()[absl::Uniform<int>(
random, 0, num_active_vars)];

relaxed_variables_set[first_var] = true;
relaxed_variables.push_back(first_var);

int empty_loops = 0;
while (relaxed_variables.size() < target_size) {
const int tail_var = relaxed_variables[absl::Uniform<int>(
random, 0, relaxed_variables.size())];
const auto& cts = helper_.VarToConstraint()[tail_var];
int head_var = tail_var;
if (!cts.empty()) {
const int label_ct = cts[absl::Uniform<int>(random, 0, cts.size())];
const auto& vars = helper_.ConstraintToVar()[label_ct];
if (!vars.empty()) {
head_var = vars[absl::Uniform<int>(random, 0, vars.size())];
}
}
if (relaxed_variables_set[head_var]) {
if (++empty_loops == 1000) {
while (true) {
const int new_var =
helper_.ActiveVariablesWhileHoldingLock()[absl::Uniform<int>(
random, 0, num_active_vars)];
if (relaxed_variables_set[new_var]) continue;
relaxed_variables_set[new_var] = true;
relaxed_variables.push_back(new_var);
empty_loops = 0;
break;
}
}
continue;
}
relaxed_variables_set[head_var] = true;
relaxed_variables.push_back(head_var);
}
}
return helper_.RelaxGivenVariables(initial_solution, relaxed_variables);
}

// Note that even if difficulty means full neighborhood, we go through the
// generation process to never get out of a connected components.
Neighborhood ConstraintGraphNeighborhoodGenerator::Generate(
Expand Down
9 changes: 9 additions & 0 deletions ortools/sat/cp_model_lns.h
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,15 @@ class VariableGraphNeighborhoodGenerator : public NeighborhoodGenerator {
double difficulty, absl::BitGenRef random) final;
};

class ArcGraphNeighborhoodGenerator : public NeighborhoodGenerator {
public:
explicit ArcGraphNeighborhoodGenerator(
NeighborhoodGeneratorHelper const* helper, const std::string& name)
: NeighborhoodGenerator(name, helper) {}
Neighborhood Generate(const CpSolverResponse& initial_solution,
double difficulty, absl::BitGenRef random) final;
};

// Pick a random subset of constraint and relax all of their variables. We are a
// bit smarter than this because after the first constraint is selected, we only
// select constraints that share at least one variable with the already selected
Expand Down
109 changes: 55 additions & 54 deletions ortools/sat/cp_model_solver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2580,7 +2580,7 @@ class FullProblemSolver : public SubSolver {
// can have a deterministic parallel mode.
void Synchronize() override {
absl::MutexLock mutex_lock(&mutex_);
deterministic_time_ += dtime_since_last_sync_;
AddTaskDeterministicDuration(dtime_since_last_sync_);
shared_->time_limit->AdvanceDeterministicTime(dtime_since_last_sync_);
dtime_since_last_sync_ = 0.0;
}
Expand Down Expand Up @@ -2654,27 +2654,22 @@ class FullProblemSolver : public SubSolver {
bool previous_task_is_completed_ ABSL_GUARDED_BY(mutex_) = true;
};

class ObjectiveLbSolver : public SubSolver {
class ObjectiveShavingSolver : public SubSolver {
public:
ObjectiveLbSolver(const SatParameters& local_parameters,
NeighborhoodGeneratorHelper* helper, SharedClasses* shared)
ObjectiveShavingSolver(const SatParameters& local_parameters,
NeighborhoodGeneratorHelper* helper,
SharedClasses* shared)
: SubSolver(local_parameters.name(), FULL_PROBLEM),
local_params_(local_parameters),
helper_(helper),
shared_(shared),
local_proto_(*shared->model_proto) {}

~ObjectiveLbSolver() override = default;

bool IsDone() override {
{
absl::MutexLock mutex_lock(&mutex_);
if (task_in_flight_) return false;
}
return shared_->SearchIsDone();
}
~ObjectiveShavingSolver() override = default;

bool TaskIsAvailable() override {
if (shared_->SearchIsDone()) return false;

// We only support one task at the time.
absl::MutexLock mutex_lock(&mutex_);
return !task_in_flight_;
Expand All @@ -2688,35 +2683,39 @@ class ObjectiveLbSolver : public SubSolver {
objective_lb_ = shared_->response->GetInnerObjectiveLowerBound();
}
return [this]() {
if (!ResetModel()) return;

SolveLoadedCpModel(local_proto_, local_repo_.get());
const CpSolverResponse local_response =
local_repo_->GetOrCreate<SharedResponseManager>()->GetResponse();

if (local_response.status() == CpSolverStatus::OPTIMAL ||
local_response.status() == CpSolverStatus::FEASIBLE) {
std::vector<int64_t> solution_values(local_response.solution().begin(),
local_response.solution().end());
if (local_params_.cp_model_presolve()) {
const int num_original_vars = shared_->model_proto->variables_size();
PostsolveResponseWrapper(local_params_, num_original_vars,
mapping_proto_, postsolve_mapping_,
&solution_values);
if (ResetModel()) {
SolveLoadedCpModel(local_proto_, local_repo_.get());
const CpSolverResponse local_response =
local_repo_->GetOrCreate<SharedResponseManager>()->GetResponse();

if (local_response.status() == CpSolverStatus::OPTIMAL ||
local_response.status() == CpSolverStatus::FEASIBLE) {
std::vector<int64_t> solution_values(
local_response.solution().begin(),
local_response.solution().end());
if (local_params_.cp_model_presolve()) {
const int num_original_vars =
shared_->model_proto->variables_size();
PostsolveResponseWrapper(local_params_, num_original_vars,
mapping_proto_, postsolve_mapping_,
&solution_values);
}
shared_->response->NewSolution(solution_values, Info());
} else if (local_response.status() == CpSolverStatus::INFEASIBLE) {
absl::MutexLock mutex_lock(&mutex_);
shared_->response->UpdateInnerObjectiveBounds(
Info(), objective_lb_ + 1, kMaxIntegerValue);
}
shared_->response->NewSolution(solution_values, Info());
} else if (local_response.status() == CpSolverStatus::INFEASIBLE) {
absl::MutexLock mutex_lock(&mutex_);
shared_->response->UpdateInnerObjectiveBounds(Info(), objective_lb_ + 1,
kMaxIntegerValue);
}

absl::MutexLock mutex_lock(&mutex_);
task_in_flight_ = false;
const double dtime =
local_repo_->GetOrCreate<TimeLimit>()->GetElapsedDeterministicTime();
deterministic_time_ += dtime;
shared_->time_limit->AdvanceDeterministicTime(dtime);
if (local_repo_ != nullptr) {
const double dtime = local_repo_->GetOrCreate<TimeLimit>()
->GetElapsedDeterministicTime();
AddTaskDeterministicDuration(dtime);
shared_->time_limit->AdvanceDeterministicTime(dtime);
}
};
}

Expand All @@ -2738,16 +2737,14 @@ class ObjectiveLbSolver : public SubSolver {
}
}

std::string StatisticsString() const override { return ""; }

private:
std::string Info() {
return absl::StrCat(name_, " #vars=", local_proto_.variables().size(),
return absl::StrCat(name(), " #vars=", local_proto_.variables().size(),
" #csts=", local_proto_.constraints().size());
}

bool ResetModel() {
local_repo_ = std::make_unique<Model>(name_);
local_repo_ = std::make_unique<Model>(name());
*local_repo_->GetOrCreate<SatParameters>() = local_params_;

auto* time_limit = local_repo_->GetOrCreate<TimeLimit>();
Expand Down Expand Up @@ -2834,7 +2831,7 @@ class FeasibilityPumpSolver : public SubSolver {
SharedClasses* shared)
: SubSolver("feasibility_pump", INCOMPLETE),
shared_(shared),
local_model_(std::make_unique<Model>(name_)) {
local_model_(std::make_unique<Model>(name())) {
// Setup the local model parameters and time limit.
*(local_model_->GetOrCreate<SatParameters>()) = local_parameters;
shared_->time_limit->UpdateLocalLimit(
Expand Down Expand Up @@ -2891,7 +2888,7 @@ class FeasibilityPumpSolver : public SubSolver {
const double saved_dtime = time_limit->GetElapsedDeterministicTime();
auto* feasibility_pump = local_model_->Mutable<FeasibilityPump>();
if (!feasibility_pump->Solve()) {
shared_->response->NotifyThatImprovingProblemIsInfeasible(name_);
shared_->response->NotifyThatImprovingProblemIsInfeasible(name());
}

{
Expand All @@ -2913,7 +2910,7 @@ class FeasibilityPumpSolver : public SubSolver {

void Synchronize() override {
absl::MutexLock mutex_lock(&mutex_);
deterministic_time_ += dtime_since_last_sync_;
AddTaskDeterministicDuration(dtime_since_last_sync_);
shared_->time_limit->AdvanceDeterministicTime(dtime_since_last_sync_);
dtime_since_last_sync_ = 0.0;
}
Expand Down Expand Up @@ -3282,9 +3279,9 @@ class LnsSolver : public SubSolver {

void Synchronize() override {
generator_->Synchronize();
const double old = deterministic_time_;
deterministic_time_ = generator_->deterministic_time();
shared_->time_limit->AdvanceDeterministicTime(deterministic_time_ - old);
const double diff = generator_->deterministic_time() - deterministic_time();
AddTaskDeterministicDuration(diff);
shared_->time_limit->AdvanceDeterministicTime(diff);
}

std::vector<std::string> TableLineStats() const override {
Expand Down Expand Up @@ -3409,8 +3406,8 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
++num_full_problem_solvers;

if (local_params.use_objective_shaving_search()) {
subsolvers.push_back(
std::make_unique<ObjectiveLbSolver>(local_params, helper, &shared));
subsolvers.push_back(std::make_unique<ObjectiveShavingSolver>(
local_params, helper, &shared));
continue;
}

Expand Down Expand Up @@ -3591,6 +3588,10 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
std::make_unique<VariableGraphNeighborhoodGenerator>(helper,
"graph_var_lns"),
params, helper, &shared));
subsolvers.push_back(std::make_unique<LnsSolver>(
std::make_unique<ArcGraphNeighborhoodGenerator>(helper,
"graph_arc_lns"),
params, helper, &shared));
subsolvers.push_back(std::make_unique<LnsSolver>(
std::make_unique<ConstraintGraphNeighborhoodGenerator>(helper,
"graph_cst_lns"),
Expand Down Expand Up @@ -3765,11 +3766,13 @@ void SolveCpModelParallel(const CpModelProto& model_proto,

// Generic task timing table.
std::vector<std::vector<std::string>> table;
table.push_back(
{"Task timing", "n [ min, max] avg dev sum"});
table.push_back({"Task timing",
"n [ min, max] avg dev time",
"n [ min, max] avg dev dtime"});
for (const auto& subsolver : subsolvers) {
if (subsolver == nullptr) continue;
table.push_back({FormatName(subsolver->name()), subsolver->TimingInfo()});
table.push_back({FormatName(subsolver->name()), subsolver->TimingInfo(),
subsolver->DeterministicTimingInfo()});
}
if (table.size() > 1) SOLVER_LOG(logger, FormatTable(table));

Expand All @@ -3780,7 +3783,6 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
for (const auto& subsolver : subsolvers) {
if (subsolver == nullptr) continue;
if (subsolver->type() != SubSolver::FULL_PROBLEM) continue;
if (subsolver->name().empty()) continue;
std::vector<std::string> stats = subsolver->TableLineStats();
if (stats.empty()) continue;
table.push_back(std::move(stats));
Expand All @@ -3794,7 +3796,6 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
for (const auto& subsolver : subsolvers) {
if (subsolver == nullptr) continue;
if (subsolver->type() != SubSolver::INCOMPLETE) continue;
if (subsolver->name().empty()) continue;
std::vector<std::string> stats = subsolver->TableLineStats();
if (stats.empty()) continue;
table.push_back(std::move(stats));
Expand Down
32 changes: 28 additions & 4 deletions ortools/sat/subsolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class SubSolver {
// update asynchronously (and so non-deterministically) global "shared"
// classes, but this global state is incorporated by the Subsolver only when
// Synchronize() is called.
//
// This is only called by the main thread in Subsolver creation order.
virtual void Synchronize() = 0;

// Returns true if this SubSolver is done and its memory can be freed. Note
Expand All @@ -66,9 +68,12 @@ class SubSolver {
//
// This is needed since some subsolve can be done before the overal Solve() is
// finished. This is the case for first solution subsolvers for instances.
//
// This is only called by the main thread in a sequential fashion.
virtual bool IsDone() { return false; }

// Returns true iff GenerateTask() can be called.
// This is only called by the main thread in a sequential fashion.
virtual bool TaskIsAvailable() = 0;

// Returns a task to run. The task_id is just an ever increasing counter that
Expand All @@ -77,6 +82,8 @@ class SubSolver {
// 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.
//
// This is only called by the main thread.
virtual std::function<void()> GenerateTask(int64_t task_id) = 0;

// Returns the total deterministic time spend by the completed tasks before
Expand All @@ -94,24 +101,41 @@ class SubSolver {
virtual std::vector<std::string> TableLineStats() const { return {}; }

// Note that this is protected by the global execution mutex and so it is
// called sequentially.
// called sequentially. Subclasses do not need to call this.
void AddTaskDuration(double duration_in_seconds) {
timing_.AddTimeInSec(duration_in_seconds);
}

// This one need to be called by the Subclasses. Usually from Synchronize(),
// or from the task itself it we execute a single task at the same time.
void AddTaskDeterministicDuration(double deterministic_duration) {
if (deterministic_duration <= 0) return;
deterministic_time_ += deterministic_duration;
dtiming_.AddTimeInSec(deterministic_duration);
}

std::string TimingInfo() const {
// TODO(user): remove trailing "\n" from ValueAsString().
// TODO(user): remove trailing "\n" from ValueAsString() or just build the
// table line directly.
std::string data = timing_.ValueAsString();
if (!data.empty()) data.pop_back();
return data;
}

protected:
std::string DeterministicTimingInfo() const {
// TODO(user): remove trailing "\n" from ValueAsString().
std::string data = dtiming_.ValueAsString();
if (!data.empty()) data.pop_back();
return data;
}

private:
const std::string name_;
const SubsolverType type_;

double deterministic_time_ = 0.0;
TimeDistribution timing_ = TimeDistribution("tasks");
TimeDistribution timing_ = TimeDistribution("task time");
TimeDistribution dtiming_ = TimeDistribution("task dtime");
};

// A simple wrapper to add a synchronization point in the list of subsolvers.
Expand Down

0 comments on commit 5194c98

Please sign in to comment.