Skip to content

Commit

Permalink
[CP-SAT] more work on logging; remove unused LNS
Browse files Browse the repository at this point in the history
  • Loading branch information
lperron committed Jun 27, 2023
1 parent a743431 commit c50650e
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 153 deletions.
32 changes: 15 additions & 17 deletions ortools/sat/cp_model_lns.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1167,10 +1167,12 @@ void NeighborhoodGenerator::Synchronize() {
data.initial_best_objective.value(), data.new_objective.value()));
if (best_objective_improvement > 0) {
num_consecutive_non_improving_calls_ = 0;
next_time_limit_bump_ = 50;
} else {
++num_consecutive_non_improving_calls_;
}

// Confusing: this one is however comparing to the base solution objective.
if (data.base_objective > data.new_objective) {
++num_improving_calls_;
}
Expand Down Expand Up @@ -1199,8 +1201,8 @@ void NeighborhoodGenerator::Synchronize() {
//
// TODO(user): experiment with resetting the time limit if a solution is
// found.
if (num_consecutive_non_improving_calls_ > 50) {
num_consecutive_non_improving_calls_ = 0;
if (num_consecutive_non_improving_calls_ > next_time_limit_bump_) {
next_time_limit_bump_ = num_consecutive_non_improving_calls_ + 50;
deterministic_limit_ *= 1.02;

// We do not want the limit to go to high. Intuitively, the goal is to try
Expand Down Expand Up @@ -1471,12 +1473,17 @@ Neighborhood DecompositionGraphNeighborhoodGenerator::Generate(
elements[i].tie_break = absl::Uniform<double>(random, 0.0, 1.0);
}

// We start by a random node.
const int first_index = absl::Uniform<int>(random, 0, num_nodes);
elements[first_index].score =
first_index < num_vars
? helper_.VarToConstraint()[first_index].size()
: helper_.ConstraintToVar()[first_index - num_vars].size();
// We start by a random active variable.
//
// Note that while num_vars contains all variables, all the fixed variable
// will have no associated constraint, so we don't want to start from a
// random variable.
//
// TODO(user): Does starting by a constraint make sense too?
const int first_index =
helper_.ActiveVariablesWhileHoldingLock()[absl::Uniform<int>(
random, 0, num_active_vars)];
elements[first_index].score = helper_.VarToConstraint()[first_index].size();
pq.Add(elements[first_index]);
added_or_connected[first_index] = true;

Expand Down Expand Up @@ -1577,15 +1584,6 @@ Neighborhood DecompositionGraphNeighborhoodGenerator::Generate(
return helper_.RelaxGivenVariables(initial_solution, relaxed_variables);
}

Neighborhood RelaxObjectiveVariablesGenerator::Generate(
const CpSolverResponse& initial_solution, double difficulty,
absl::BitGenRef random) {
std::vector<int> fixed_variables = helper_.ActiveObjectiveVariables();
GetRandomSubset(1.0 - difficulty, &fixed_variables, random);
return helper_.FixGivenVariables(
initial_solution, {fixed_variables.begin(), fixed_variables.end()});
}

namespace {

void AddPrecedence(const LinearExpressionProto& before,
Expand Down
19 changes: 9 additions & 10 deletions ortools/sat/cp_model_lns.h
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,14 @@ class NeighborhoodGenerator {
return num_improving_calls_;
}

// Returns the number of the last calls to this generator that didn't improve
// the best solution. Note that this count improvement to the best known
// solution not the base one used to generate one neighborhood.
int64_t num_consecutive_non_improving_calls() const {
absl::MutexLock mutex_lock(&generator_mutex_);
return num_consecutive_non_improving_calls_;
}

// The current difficulty of this generator
double difficulty() const {
absl::MutexLock mutex_lock(&generator_mutex_);
Expand Down Expand Up @@ -462,6 +470,7 @@ class NeighborhoodGenerator {
int64_t num_fully_solved_calls_ = 0;
int64_t num_improving_calls_ = 0;
int64_t num_consecutive_non_improving_calls_ = 0;
int64_t next_time_limit_bump_ = 50;
double deterministic_time_ = 0.0;
double current_average_ = 0.0;
};
Expand Down Expand Up @@ -543,16 +552,6 @@ class DecompositionGraphNeighborhoodGenerator : public NeighborhoodGenerator {
double difficulty, absl::BitGenRef random) final;
};

// Pick a random subset of objective terms.
class RelaxObjectiveVariablesGenerator : public NeighborhoodGenerator {
public:
explicit RelaxObjectiveVariablesGenerator(
NeighborhoodGeneratorHelper const* helper, const std::string& name)
: NeighborhoodGenerator(name, helper) {}
Neighborhood Generate(const CpSolverResponse& initial_solution,
double difficulty, absl::BitGenRef random) final;
};

// Helper method for the scheduling neighborhood generators. Returns a
// neighborhood defined from the given set of intervals to relax. For each
// scheduling constraint, it adds strict relation order between the non-relaxed
Expand Down
158 changes: 88 additions & 70 deletions ortools/sat/cp_model_solver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2585,16 +2585,16 @@ class FullProblemSolver : public SubSolver {
dtime_since_last_sync_ = 0.0;
}

std::string OneLineStats() const override {
std::vector<std::string> TableLineStats() const override {
CpSolverResponse r;
FillSolveStatsInResponse(local_model_.get(), &r);
return absl::StrCat(
RightAlign(FormatCounter(r.num_booleans())),
RightAlign(FormatCounter(r.num_conflicts())),
RightAlign(FormatCounter(r.num_branches())),
RightAlign(FormatCounter(r.num_restarts())),
RightAlign(FormatCounter(r.num_binary_propagations())),
RightAlign(FormatCounter(r.num_integer_propagations())));
return {FormatName(name()),
FormatCounter(r.num_booleans()),
FormatCounter(r.num_conflicts()),
FormatCounter(r.num_branches()),
FormatCounter(r.num_restarts()),
FormatCounter(r.num_binary_propagations()),
FormatCounter(r.num_integer_propagations())};
}

std::string StatisticsString() const override {
Expand Down Expand Up @@ -2918,8 +2918,6 @@ class FeasibilityPumpSolver : public SubSolver {
dtime_since_last_sync_ = 0.0;
}

// TODO(user): Display feasibility pump statistics.

private:
SharedClasses* shared_;
std::unique_ptr<Model> local_model_;
Expand Down Expand Up @@ -3009,9 +3007,11 @@ class LnsSolver : public SubSolver {
static_cast<double>(num_calls);
std::string source_info =
neighborhood.source_info.empty() ? name() : neighborhood.source_info;
const std::string lns_info = absl::StrFormat(
"%s(d=%0.2f s=%i t=%0.2f p=%0.2f)", source_info, data.difficulty,
task_id, data.deterministic_limit, fully_solved_proportion);
const std::string lns_info =
absl::StrFormat("%s(d=%0.2f s=%i t=%0.2f p=%0.2f stall=%d)",
source_info, data.difficulty, task_id,
data.deterministic_limit, fully_solved_proportion,
generator_->num_consecutive_non_improving_calls());

SatParameters local_params(parameters_);
local_params.set_max_deterministic_time(data.deterministic_limit);
Expand Down Expand Up @@ -3062,6 +3062,31 @@ class LnsSolver : public SubSolver {
*lns_fragment.mutable_solution_hint() =
neighborhood.delta.solution_hint();
}
if (generator_->num_consecutive_non_improving_calls() > 10 &&
absl::Bernoulli(random, 0.5)) {
// If we seems to be stalling, lets try to solve without the hint in
// order to diversify our solution pool. Otherwise non-improving
// neighborhood will just return the base solution always.
lns_fragment.clear_solution_hint();
}
if (neighborhood.is_simple &&
neighborhood.num_relaxed_variables_in_objective == 0) {
// If we didn't relax the objective, there can be no improving solution.
// However, we might have some diversity if they are multiple feasible
// solution. Note that removing the objective might slightly speed up
// presolving.
//
// TODO(user): How can we teak the search to favor diversity.
if (generator_->num_consecutive_non_improving_calls() > 10) {
// We have been staling, try to find diverse solution?
lns_fragment.clear_solution_hint();
lns_fragment.clear_objective();
} else {
// Just regenerate.
// Note that we do not change the difficulty.
return;
}
}

CpModelProto debug_copy;
if (absl::GetFlag(FLAGS_cp_model_dump_problematic_lns)) {
Expand Down Expand Up @@ -3262,17 +3287,16 @@ class LnsSolver : public SubSolver {
shared_->time_limit->AdvanceDeterministicTime(deterministic_time_ - old);
}

std::string OneLineStats() const override {
std::vector<std::string> TableLineStats() const override {
const double fully_solved_proportion =
static_cast<double>(generator_->num_fully_solved_calls()) /
static_cast<double>(std::max(int64_t{1}, generator_->num_calls()));
return absl::StrCat(
RightAlign(absl::StrCat(generator_->num_improving_calls(), "/",
generator_->num_calls())),
RightAlign(absl::StrFormat("%2.0f%%", 100 * fully_solved_proportion)),
RightAlign(absl::StrFormat("%0.2f", generator_->difficulty())),
RightAlign(absl::StrFormat("%0.2f", generator_->deterministic_limit())),
TimingInfo());
return {FormatName(name()),
absl::StrCat(generator_->num_improving_calls(), "/",
generator_->num_calls()),
absl::StrFormat("%2.0f%%", 100 * fully_solved_proportion),
absl::StrFormat("%0.2f", generator_->difficulty()),
absl::StrFormat("%0.2f", generator_->deterministic_limit())};
}

private:
Expand Down Expand Up @@ -3576,19 +3600,6 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
helper, "graph_dec_lns"),
params, helper, &shared));

// Create the rnd_obj_lns worker if the number of terms in the objective is
// big enough, and it is no more than half the number of variables in the
// model.
if (model_proto.objective().vars().size() >=
params.objective_lns_min_size() &&
model_proto.objective().vars_size() >=
model_proto.objective().vars().size() * 2) {
subsolvers.push_back(std::make_unique<LnsSolver>(
std::make_unique<RelaxObjectiveVariablesGenerator>(helper,
"rnd_obj_lns"),
params, helper, &shared));
}

// TODO(user): If we have a model with scheduling + routing. We create
// a lot of LNS generators. Investigate if we can reduce this number.
if (!helper->TypeToConstraints(ConstraintProto::kNoOverlap).empty() ||
Expand Down Expand Up @@ -3697,23 +3708,19 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
for (const auto& name : names) {
solvers_and_count[name]++;
}
std::string solver_list;
bool first = true;
std::vector<std::string> counted_names;
for (const auto& [name, count] : solvers_and_count) {
if (first) {
first = false;
} else {
absl::StrAppend(&solver_list, ", ");
}
if (count == 1) {
absl::StrAppend(&solver_list, name);
counted_names.push_back(name);
} else {
absl::StrAppend(&solver_list, name, "(", count, ")");
counted_names.push_back(absl::StrCat(name, "(", count, ")"));
}
}
SOLVER_LOG(logger, names.size(), " ",
absl::StrCat(type_name, names.size() == 1 ? "" : "s"), ": [",
solver_list, "]");
SOLVER_LOG(
logger, names.size(), " ",
absl::StrCat(type_name, names.size() == 1 ? "" : "s"), ": [",
absl::StrJoin(counted_names.begin(), counted_names.end(), ", "),
"]");
}
};

Expand All @@ -3740,9 +3747,11 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
}

// Log statistics.
// TODO(user): Store and display first solution solvers.
if (logger->LoggingIsEnabled()) {
SOLVER_LOG(logger, "");

if (params.log_subsolver_statistics()) {
SOLVER_LOG(logger, "");
SOLVER_LOG(logger, "Sub-solver detailed search statistics:");
for (const auto& subsolver : subsolvers) {
if (subsolver == nullptr) continue;
Expand All @@ -3751,50 +3760,60 @@ void SolveCpModelParallel(const CpModelProto& model_proto,
SOLVER_LOG(logger,
absl::StrCat(" '", subsolver->name(), "':\n", stats));
}
SOLVER_LOG(logger, "");
}

// Subsolver one-liner.
SOLVER_LOG(logger, "");
SOLVER_LOG(logger, HeaderStr("Subsolver statistics"), RightAlign("Bools"),
RightAlign("Conflicts"), RightAlign("Branches"),
RightAlign("Restarts"), RightAlign("BoolPropag"),
RightAlign("IntegerPropag"));
// Generic task timing table.
std::vector<std::vector<std::string>> table;
table.push_back(
{"Task timing", "n [ min, max] avg dev sum"});
for (const auto& subsolver : subsolvers) {
if (subsolver == nullptr) continue;
table.push_back({FormatName(subsolver->name()), subsolver->TimingInfo()});
}
if (table.size() > 1) SOLVER_LOG(logger, FormatTable(table));

// Subsolver tables.
table.clear();
table.push_back({"Search stats", "Bools", "Conflicts", "Branches",
"Restarts", "BoolPropag", "IntegerPropag"});
for (const auto& subsolver : subsolvers) {
if (subsolver == nullptr) continue;
if (subsolver->type() != SubSolver::FULL_PROBLEM) continue;
if (subsolver->name().empty()) continue;
const std::string stats = subsolver->OneLineStats();
std::vector<std::string> stats = subsolver->TableLineStats();
if (stats.empty()) continue;
SOLVER_LOG(logger, absl::StrCat(RowNameStr(subsolver->name()), stats));
table.push_back(std::move(stats));
}
if (table.size() > 1) SOLVER_LOG(logger, FormatTable(table));

SOLVER_LOG(logger, "");
SOLVER_LOG(
logger, HeaderStr("LNS statistics"), RightAlign("Improv/Calls"),
RightAlign("Closed"), RightAlign("Difficulty"), RightAlign("TimeLimit"),
RightAlign(" [ min, max] avg dev sum"));
// TODO(user): Split feasibility_jump and pump.
table.clear();
table.push_back(
{"LNS stats", "Improv/Calls", "Closed", "Difficulty", "TimeLimit"});
for (const auto& subsolver : subsolvers) {
if (subsolver == nullptr) continue;
if (subsolver->type() != SubSolver::INCOMPLETE) continue;
if (subsolver->name().empty()) continue;
const std::string stats = subsolver->OneLineStats();
std::vector<std::string> stats = subsolver->TableLineStats();
if (stats.empty()) continue;
SOLVER_LOG(logger, absl::StrCat(RowNameStr(subsolver->name()), stats));
table.push_back(std::move(stats));
}
if (table.size() > 1) SOLVER_LOG(logger, FormatTable(table));

shared.response->DisplayImprovementStatistics();

SOLVER_LOG(logger, "");
SOLVER_LOG(logger, HeaderStr("Solution repositories"), RightAlign("Added"),
RightAlign("Queried"), RightAlign("Ignored"),
RightAlign("Synchro"));
SOLVER_LOG(logger, shared.response->SolutionsRepository().Stats());
table.clear();
table.push_back(
{"Solution repositories", "Added", "Queried", "Ignored", "Synchro"});
table.push_back(shared.response->SolutionsRepository().TableLineStats());
if (shared.lp_solutions != nullptr) {
SOLVER_LOG(logger, shared.lp_solutions->Stats());
table.push_back(shared.lp_solutions->TableLineStats());
}
if (shared.incomplete_solutions != nullptr) {
SOLVER_LOG(logger, shared.incomplete_solutions->Stats());
table.push_back(shared.incomplete_solutions->TableLineStats());
}
SOLVER_LOG(logger, FormatTable(table));

if (shared.bounds) {
shared.bounds->LogStatistics(logger);
Expand Down Expand Up @@ -3931,7 +3950,6 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) {
// This also copy the logs to the response if requested.
shared_response_manager->AddFinalResponsePostprocessor(
[logger, &model_proto, &log_string](CpSolverResponse* response) {
SOLVER_LOG(logger, "");
SOLVER_LOG(logger, CpSolverResponseStats(
*response,
model_proto.has_objective() ||
Expand Down
10 changes: 7 additions & 3 deletions ortools/sat/feasibility_jump.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,16 @@ class FeasibilityJumpSolver : public SubSolver {
// No synchronization needed for TaskIsAvailable().
void Synchronize() final {}

// Note that this should only returns true if there is a need to delete this
// subsolver early to reclaim memory, otherwise we will not properly have the
// stats.
//
// TODO(user): Save the logging stats before deletion.
bool IsDone() final {
// Tricky: we cannot delete something if there is a task in flight, we will
// have to wait.
if (task_generated_.load()) return false;

if (!model_is_supported_.load()) return true;
if (shared_response_->ProblemIsSolved()) return true;
if (shared_time_limit_->LimitReached()) return true;

// We are done after the first task is done in the FIRST_SOLUTION mode.
return type() == SubSolver::FIRST_SOLUTION &&
Expand All @@ -84,6 +86,8 @@ class FeasibilityJumpSolver : public SubSolver {

bool TaskIsAvailable() final {
if (task_generated_.load()) return false;
if (shared_response_->ProblemIsSolved()) return false;
if (shared_time_limit_->LimitReached()) return false;

return (shared_response_->SolutionsRepository().NumSolutions() > 0) ==
(type() == SubSolver::INCOMPLETE);
Expand Down
Loading

0 comments on commit c50650e

Please sign in to comment.