[CP-SAT] improve scheduling default search

This commit is contained in:
Laurent Perron
2021-09-13 14:09:11 +02:00
parent 324ae93e64
commit 1ea133254a
8 changed files with 100 additions and 102 deletions

View File

@@ -135,9 +135,11 @@ struct VarValue {
namespace {
bool ModelContainsNoOverlaps(const CpModelProto& cp_model_proto) {
// TODO(user): Save this somewhere instead of recomputing it.
bool ModelHasSchedulingConstraints(const CpModelProto& cp_model_proto) {
for (const ConstraintProto& ct : cp_model_proto.constraints()) {
if (ct.constraint_case() == ConstraintProto::kNoOverlap) return true;
if (ct.constraint_case() == ConstraintProto::kCumulative) return true;
}
return false;
}
@@ -305,9 +307,7 @@ std::function<BooleanOrIntegerLiteral()> ConstructSearchStrategy(
// If there are some scheduling constraint, we complete with a custom
// "scheduling" strategy.
//
// TODO(user): also deal with cumulative.
if (ModelContainsNoOverlaps(cp_model_proto)) {
if (ModelHasSchedulingConstraints(cp_model_proto)) {
heuristics.push_back(SchedulingSearchHeuristic(model));
}
@@ -492,6 +492,15 @@ std::vector<SatParameters> GetDiverseSetOfParameters(
strategies["less_encoding"] = new_params;
}
// We only use a "fixed search" worker if some strategy is specified or
// if we have a scheduling model.
//
// TODO(user): For scheduling, this is important to find good first solution
// but afterwards it is not really great and should probably be replaced by a
// LNS worker.
const bool use_fixed_strategy = !cp_model.search_strategy().empty() ||
ModelHasSchedulingConstraints(cp_model);
// Our current set of strategies
//
// TODO(user, fdid): Avoid launching two strategies if they are the same,
@@ -502,26 +511,30 @@ std::vector<SatParameters> GetDiverseSetOfParameters(
// Low memory mode for interleaved search in single thread.
if (cp_model.has_objective()) {
names.push_back("default_lp");
names.push_back(!cp_model.search_strategy().empty() ? "fixed"
: "pseudo_costs");
names.push_back(use_fixed_strategy ? "fixed" : "pseudo_costs");
names.push_back(cp_model.objective().vars_size() > 1 ? "core" : "no_lp");
names.push_back("max_lp");
} else {
names.push_back("default_lp");
names.push_back(cp_model.search_strategy_size() > 0 ? "fixed" : "no_lp");
names.push_back(use_fixed_strategy ? "fixed" : "no_lp");
names.push_back("less_encoding");
names.push_back("max_lp");
names.push_back("quick_restart");
}
} else if (cp_model.has_objective()) {
names.push_back("default_lp");
names.push_back(!cp_model.search_strategy().empty() ? "fixed"
: "reduced_costs");
if (use_fixed_strategy) {
names.push_back("fixed");
if (num_workers > 8) names.push_back("reduced_costs");
} else {
names.push_back("reduced_costs");
}
names.push_back("pseudo_costs");
names.push_back("no_lp");
names.push_back("max_lp");
if (cp_model.objective().vars_size() > 1) names.push_back("core");
// TODO(user): Experiment with core and LP.
if (cp_model.objective().vars_size() > 1) names.push_back("core");
// Only add this strategy if we have enough worker left for LNS.
if (num_workers > 8 || base_params.interleave_search()) {
@@ -532,7 +545,7 @@ std::vector<SatParameters> GetDiverseSetOfParameters(
}
} else {
names.push_back("default_lp");
if (cp_model.search_strategy_size() > 0) names.push_back("fixed");
if (use_fixed_strategy) names.push_back("fixed");
names.push_back("less_encoding");
names.push_back("no_lp");
names.push_back("max_lp");

View File

@@ -62,8 +62,6 @@ std::function<void(Model*)> Disjunctive(
SchedulingConstraintHelper* helper =
new SchedulingConstraintHelper(vars, model);
model->TakeOwnership(helper);
model->GetOrCreate<ModelNoOverlapHelperRepository>()->no_overlaps.push_back(
helper);
// Experiments to use the timetable only to propagate the disjunctive.
if (/*DISABLES_CODE*/ (false)) {

View File

@@ -30,12 +30,6 @@
namespace operations_research {
namespace sat {
// Regroup all the set of intervals that are in no-overlap relation.
// This is used by the simple scheduling search strategy.
struct ModelNoOverlapHelperRepository {
std::vector<SchedulingConstraintHelper*> no_overlaps;
};
// Enforces a disjunctive (or no overlap) constraint on the given interval
// variables. The intervals are interpreted as [start, end) and the constraint
// enforces that no time point belongs to two intervals.

View File

@@ -578,6 +578,7 @@ bool IntegerTrail::Propagate(Trail* trail) {
void IntegerTrail::Untrail(const Trail& trail, int literal_trail_index) {
++num_untrails_;
conditional_lbs_.clear();
const int level = trail.CurrentDecisionLevel();
var_to_current_lb_interval_index_.SetLevel(level);
propagation_trail_index_ =
@@ -1057,6 +1058,21 @@ bool IntegerTrail::ConditionalEnqueue(
}
// We can't push anything in this case.
//
// We record it for this propagation phase (until the next untrail) as this
// is relatively fast and heuristics can exploit this.
//
// Note that currently we only use ConditionalEnqueue() in scheduling
// propagator, and these propagator are quite slow so this is not visible.
//
// TODO(user): We could even keep the reason and maybe do some reasoning using
// at_least_one constraint on a set of the Boolean used here.
const auto [it, inserted] =
conditional_lbs_.insert({{lit.Index(), i_lit.var}, i_lit.bound});
if (!inserted) {
it->second = std::max(it->second, i_lit.bound);
}
return true;
}

View File

@@ -705,6 +705,11 @@ class IntegerTrail : public SatPropagator {
// Returns true if the variable is fixed at level 0.
bool IsFixedAtLevelZero(IntegerVariable var) const;
// Advanced usage.
// Returns the current lower bound assuming the literal is true.
IntegerValue ConditionalLowerBound(Literal l, IntegerVariable i) const;
IntegerValue ConditionalLowerBound(Literal l, AffineExpression expr) const;
// Advanced usage. Given the reason for
// (Sum_i coeffs[i] * reason[i].var >= current_lb) initially in reason,
// this function relaxes the reason given that we only need the explanation of
@@ -1087,6 +1092,12 @@ class IntegerTrail : public SatPropagator {
Trail* trail_;
const SatParameters& parameters_;
// Temporary "hash" to keep track of all the conditional enqueue that were
// done. Note that we currently do not keep any reason for them, and as such,
// we can only use this in heuristics. See ConditionalLowerBound().
absl::flat_hash_map<std::pair<LiteralIndex, IntegerVariable>, IntegerValue>
conditional_lbs_;
DISALLOW_COPY_AND_ASSIGN(IntegerTrail);
};
@@ -1343,14 +1354,26 @@ inline bool IntegerTrail::IsFixed(IntegerVariable i) const {
return vars_[i].current_bound == -vars_[NegationOf(i)].current_bound;
}
// TODO(user): Use capped arithmetic? It might be slow though and we better just
// make sure there is no overflow at model creation.
inline IntegerValue IntegerTrail::LowerBound(AffineExpression expr) const {
if (expr.var == kNoIntegerVariable) return expr.constant;
return LowerBound(expr.var) * expr.coeff + expr.constant;
}
// TODO(user): Use capped arithmetic? same remark as for LowerBound().
inline IntegerValue IntegerTrail::ConditionalLowerBound(
Literal l, IntegerVariable i) const {
const auto it = conditional_lbs_.find({l.Index(), i});
if (it != conditional_lbs_.end()) {
return std::max(vars_[i].current_bound, it->second);
}
return vars_[i].current_bound;
}
inline IntegerValue IntegerTrail::ConditionalLowerBound(
Literal l, AffineExpression expr) const {
if (expr.var == kNoIntegerVariable) return expr.constant;
return ConditionalLowerBound(l, expr.var) * expr.coeff + expr.constant;
}
inline IntegerValue IntegerTrail::UpperBound(AffineExpression expr) const {
if (expr.var == kNoIntegerVariable) return expr.constant;
return UpperBound(expr.var) * expr.coeff + expr.constant;

View File

@@ -346,16 +346,10 @@ std::function<BooleanOrIntegerLiteral()> PseudoCost(Model* model) {
};
}
// A simple heuristic for scheduling with no overlaps.
//
// TODO(user): Extend to cumulative constraint. A better design is probably
// just to collect for each interval, the start_min that was pushed during the
// last propagation if that interval was present. In case of optional interval,
// we cannot "commit" the push, but we still do it, so if we record in the
// interval repository that value, that should be enough for a good heuristic.
// A simple heuristic for scheduling models.
std::function<BooleanOrIntegerLiteral()> SchedulingSearchHeuristic(
Model* model) {
auto* repo = model->GetOrCreate<ModelNoOverlapHelperRepository>();
auto* repo = model->GetOrCreate<IntervalsRepository>();
auto* heuristic = model->GetOrCreate<SearchHeuristics>();
auto* trail = model->GetOrCreate<Trail>();
auto* integer_trail = model->GetOrCreate<IntegerTrail>();
@@ -367,85 +361,48 @@ std::function<BooleanOrIntegerLiteral()> SchedulingSearchHeuristic(
AffineExpression end;
// Information to select best.
int index = 0;
int task;
IntegerValue size_min = kMaxIntegerValue;
IntegerValue time = kMaxIntegerValue;
};
ToSchedule best;
int index = 0;
for (SchedulingConstraintHelper* helper : repo->no_overlaps) {
if (!helper->SynchronizeAndSetTimeDirection(true)) {
return BooleanOrIntegerLiteral();
}
// TODO(user): we should also precompute fixed precedences and only fix
// interval that have all their predecessors fixed.
const int num_intervals = repo->NumIntervals();
for (IntervalVariable i(0); i < num_intervals; ++i) {
if (repo->IsAbsent(i)) continue;
if (!repo->IsPresent(i) || !integer_trail->IsFixed(repo->Start(i)) ||
!integer_trail->IsFixed(repo->End(i))) {
IntegerValue time = integer_trail->LowerBound(repo->Start(i));
if (repo->IsOptional(i)) {
// For task whose presence is still unknown, our propagators should
// have propagated the minimium time as if it was present. So this
// should reflect the earliest time at which this interval can be
// scheduled.
time = std::max(time, integer_trail->ConditionalLowerBound(
repo->PresenceLiteral(i), repo->Start(i)));
}
// The heuristic idea is to find the interval that can be scheduled first,
// and fully schedule it. Note however than for optional interval,
// depending on the model, one cannot use the current start min to decide
// this since it might not be propagated.
//
// For now, we assume that this heuristic was used before, so the max
// of the fixed interval is used as the earliest start time.
//
// TODO(user): we should also precompute fixed precedences and only fix
// interval that have all their predecessors fixed.
//
// TODO(user): We could actually compute the earliest possible start of
// each task. This is best in the presence of fixed interval from the
// start or during a non-fixed search to be smarter about the next integer
// variable to instantiate.
IntegerValue first_free_spot = kMinIntegerValue;
for (int t = 0; t < helper->NumTasks(); ++t) {
if (!helper->IsPresent(t)) continue;
if (!integer_trail->IsFixed(helper->Starts()[t])) continue;
if (!integer_trail->IsFixed(helper->Ends()[t])) continue;
first_free_spot = std::max(first_free_spot, helper->EndMin(t));
}
for (int t = 0; t < helper->NumTasks(); ++t) {
if (helper->IsAbsent(t)) continue;
if (!helper->IsPresent(t) ||
!integer_trail->IsFixed(helper->Starts()[t]) ||
!integer_trail->IsFixed(helper->Ends()[t])) {
IntegerValue time;
if (helper->IsPresent(t)) {
// If t is present, we asssume its start min was propagated
// correctly.
time = helper->StartMin(t);
} else {
// Otherwise we use "first_free_spot" even though it is not perfect
// in case not everything was fixed by this heuristic there.
time = std::max(helper->StartMin(t), first_free_spot);
}
// For variable size, we compute the min size once the start is fixed
// to time. This is needed to never pick the "artificial" makespan
// interval at the end in priority compared to intervals that still
// need to be scheduled.
const IntegerValue size_min =
std::max(helper->SizeMin(t), helper->EndMin(t) - time);
if (time < best.time ||
(time == best.time && size_min < best.size_min)) {
best.presence = helper->IsOptional(t)
? helper->PresenceLiteral(t).Index()
: kNoLiteralIndex;
best.start = helper->Starts()[t];
best.end = helper->Ends()[t];
best.size_min = size_min;
best.task = t;
best.index = index;
best.time = time;
}
// For variable size, we compute the min size once the start is fixed
// to time. This is needed to never pick the "artificial" makespan
// interval at the end in priority compared to intervals that still
// need to be scheduled.
const IntegerValue size_min =
std::max(integer_trail->LowerBound(repo->Size(i)),
integer_trail->LowerBound(repo->End(i)) - time);
if (time < best.time ||
(time == best.time && size_min < best.size_min)) {
best.presence = repo->IsOptional(i) ? repo->PresenceLiteral(i).Index()
: kNoLiteralIndex;
best.start = repo->Start(i);
best.end = repo->End(i);
best.time = time;
best.size_min = size_min;
}
}
++index;
}
if (best.time == kMaxIntegerValue) return BooleanOrIntegerLiteral();
VLOG(1) << "========= Schedule on " << best.index << " @" << best.time
<< " (" << repo->no_overlaps[best.index]->TaskDebugString(best.task)
<< ")";
// Use the next_decision_override to fix in turn all the variables from
// the selected interval.
int num_times = 0;
@@ -486,8 +443,7 @@ std::function<BooleanOrIntegerLiteral()> SchedulingSearchHeuristic(
}
// Everything is fixed, dettach the override.
VLOG(1) << "Fixed on " << best.index << " @["
<< integer_trail->LowerBound(best.start) << ", "
VLOG(1) << "Fixed @[" << integer_trail->LowerBound(best.start) << ", "
<< integer_trail->LowerBound(best.end) << "]";
return BooleanOrIntegerLiteral();
};

View File

@@ -25,7 +25,6 @@
#include <vector>
#include "ortools/sat/disjunctive.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/linear_programming_constraint.h"
#include "ortools/sat/pseudo_costs.h"

View File

@@ -82,7 +82,6 @@ PROTO2_RETURN(operations_research::sat::CpSolverResponse,
%}
%typemap(in) std::function<void(const std::string&)> %{
// $input will be deleted once this function return.
// So we create a JNI global reference to keep it alive.
jobject $input_object = jenv->NewGlobalRef($input);