Skip to content

Commit 113c4fa

Browse files
committed
[CP-SAT] improve memory usage for LNS
1 parent e6dbe93 commit 113c4fa

File tree

4 files changed

+74
-40
lines changed

4 files changed

+74
-40
lines changed

ortools/sat/cp_model_lns.cc

+28-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <deque>
2020
#include <functional>
2121
#include <limits>
22+
#include <memory>
2223
#include <random>
2324
#include <string>
2425
#include <tuple>
@@ -77,16 +78,19 @@ NeighborhoodGeneratorHelper::NeighborhoodGeneratorHelper(
7778
shared_bounds_(shared_bounds),
7879
shared_response_(shared_response) {
7980
// Initialize proto memory.
81+
local_arena_storage_.assign(Neighborhood::kDefaultArenaSizePerVariable *
82+
model_proto_.variables_size(),
83+
0);
84+
local_arena_ = std::make_unique<google::protobuf::Arena>(
85+
local_arena_storage_.data(), local_arena_storage_.size());
8086
simplified_model_proto_ =
81-
google::protobuf::Arena::Create<CpModelProto>(&local_arena_);
82-
model_proto_with_only_variables_ =
83-
google::protobuf::Arena::Create<CpModelProto>(&local_arena_);
87+
google::protobuf::Arena::Create<CpModelProto>(local_arena_.get());
8488

8589
CHECK(shared_response_ != nullptr);
8690
if (shared_bounds_ != nullptr) {
8791
shared_bounds_id_ = shared_bounds_->RegisterNewId();
8892
}
89-
*model_proto_with_only_variables_->mutable_variables() =
93+
*model_proto_with_only_variables_.mutable_variables() =
9094
model_proto_.variables();
9195
InitializeHelperData();
9296
RecomputeHelperData();
@@ -112,15 +116,15 @@ void NeighborhoodGeneratorHelper::Synchronize() {
112116
const int64_t new_ub = new_upper_bounds[i];
113117
if (VLOG_IS_ON(3)) {
114118
const auto& domain =
115-
model_proto_with_only_variables_->variables(var).domain();
119+
model_proto_with_only_variables_.variables(var).domain();
116120
const int64_t old_lb = domain.Get(0);
117121
const int64_t old_ub = domain.Get(domain.size() - 1);
118122
VLOG(3) << "Variable: " << var << " old domain: [" << old_lb << ", "
119123
<< old_ub << "] new domain: [" << new_lb << ", " << new_ub
120124
<< "]";
121125
}
122126
const Domain old_domain = ReadDomainFromProto(
123-
model_proto_with_only_variables_->variables(var));
127+
model_proto_with_only_variables_.variables(var));
124128
const Domain new_domain =
125129
old_domain.IntersectionWith(Domain(new_lb, new_ub));
126130
if (new_domain.IsEmpty()) {
@@ -141,7 +145,7 @@ void NeighborhoodGeneratorHelper::Synchronize() {
141145
}
142146
FillDomainInProto(
143147
new_domain,
144-
model_proto_with_only_variables_->mutable_variables(var));
148+
model_proto_with_only_variables_.mutable_variables(var));
145149
new_variables_have_been_fixed |= new_domain.IsFixed();
146150
}
147151
}
@@ -164,7 +168,7 @@ bool NeighborhoodGeneratorHelper::ObjectiveDomainIsConstraining() const {
164168
const int var = PositiveRef(model_proto_.objective().vars(i));
165169
const int64_t coeff = model_proto_.objective().coeffs(i);
166170
const auto& var_domain =
167-
model_proto_with_only_variables_->variables(var).domain();
171+
model_proto_with_only_variables_.variables(var).domain();
168172
const int64_t v1 = coeff * var_domain[0];
169173
const int64_t v2 = coeff * var_domain[var_domain.size() - 1];
170174
min_activity += std::min(v1, v2);
@@ -218,9 +222,20 @@ void NeighborhoodGeneratorHelper::RecomputeHelperData() {
218222
{
219223
Model local_model;
220224
CpModelProto mapping_proto;
225+
// We want to replace the simplified_model_proto_ by a new one. Since
226+
// deleting an object in the arena doesn't free the memory, we also delete
227+
// and recreate the arena, but reusing the same storage.
228+
int64_t new_size = local_arena_->SpaceUsed();
229+
new_size += new_size / 2;
221230
simplified_model_proto_->Clear();
231+
local_arena_.reset();
232+
local_arena_storage_.resize(new_size);
233+
local_arena_ = std::make_unique<google::protobuf::Arena>(
234+
local_arena_storage_.data(), local_arena_storage_.size());
235+
simplified_model_proto_ =
236+
google::protobuf::Arena::Create<CpModelProto>(local_arena_.get());
222237
*simplified_model_proto_->mutable_variables() =
223-
model_proto_with_only_variables_->variables();
238+
model_proto_with_only_variables_.variables();
224239
PresolveContext context(&local_model, simplified_model_proto_,
225240
&mapping_proto);
226241
ModelCopy copier(&context);
@@ -383,7 +398,7 @@ bool NeighborhoodGeneratorHelper::IsActive(int var) const {
383398
}
384399

385400
bool NeighborhoodGeneratorHelper::IsConstant(int var) const {
386-
const auto& var_proto = model_proto_with_only_variables_->variables(var);
401+
const auto& var_proto = model_proto_with_only_variables_.variables(var);
387402
return var_proto.domain_size() == 2 &&
388403
var_proto.domain(0) == var_proto.domain(1);
389404
}
@@ -395,7 +410,7 @@ Neighborhood NeighborhoodGeneratorHelper::FullNeighborhood() const {
395410
{
396411
absl::ReaderMutexLock lock(&domain_mutex_);
397412
*neighborhood.delta.mutable_variables() =
398-
model_proto_with_only_variables_->variables();
413+
model_proto_with_only_variables_.variables();
399414
}
400415
return neighborhood;
401416
}
@@ -1057,7 +1072,7 @@ Neighborhood NeighborhoodGeneratorHelper::FixGivenVariables(
10571072
absl::ReaderMutexLock domain_lock(&domain_mutex_);
10581073
for (int i = 0; i < num_variables; ++i) {
10591074
const IntegerVariableProto& current_var =
1060-
model_proto_with_only_variables_->variables(i);
1075+
model_proto_with_only_variables_.variables(i);
10611076
IntegerVariableProto* new_var = neighborhood.delta.add_variables();
10621077

10631078
// We only copy the name in debug mode.
@@ -1203,7 +1218,7 @@ CpModelProto NeighborhoodGeneratorHelper::UpdatedModelProtoCopy() const {
12031218
{
12041219
absl::MutexLock domain_lock(&domain_mutex_);
12051220
*updated_model.mutable_variables() =
1206-
model_proto_with_only_variables_->variables();
1221+
model_proto_with_only_variables_.variables();
12071222
}
12081223
return updated_model;
12091224
}

ortools/sat/cp_model_lns.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,15 @@ class NeighborhoodGeneratorHelper : public SubSolver {
319319
// Arena holding the memory of the CpModelProto* of this class. This saves the
320320
// destruction cost that can take time on problem with millions of
321321
// variables/constraints.
322-
google::protobuf::Arena local_arena_;
322+
std::vector<char> local_arena_storage_;
323+
std::unique_ptr<google::protobuf::Arena> local_arena_;
323324

324325
// This proto will only contain the field variables() with an updated version
325326
// of the domains compared to model_proto_.variables(). We do it like this to
326327
// reduce the memory footprint of the helper when the model is large.
327328
//
328329
// TODO(user): Use custom domain repository rather than a proto?
329-
CpModelProto* model_proto_with_only_variables_ ABSL_GUARDED_BY(domain_mutex_);
330+
CpModelProto model_proto_with_only_variables_ ABSL_GUARDED_BY(domain_mutex_);
330331

331332
// Constraints by types. This never changes.
332333
std::vector<std::vector<int>> type_to_constraints_;

ortools/sat/diffn_cuts.cc

+42-25
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,28 @@ std::string DiffnCtEvent::DebugString() const {
391391
// The original cut is:
392392
// sum(end_min_i * duration_min_i) >=
393393
// (sum(duration_min_i^2) + sum(duration_min_i)^2) / 2
394+
//
395+
// Let's build a figure where each horizontal rectangle represent a task. It
396+
// ends at the end of the task, and its height is the duration of the task.
397+
// For a given order, we pack each rectangle to the left while not overlapping,
398+
// that is one rectangle starts when the previous one ends.
399+
//
400+
// e1
401+
// -----
402+
// :\ | s1
403+
// : \| e2
404+
// -------------
405+
// :\ |
406+
// : \ | s2
407+
// : \| e3
408+
// ----------------
409+
// : \| s3
410+
// ----------------
411+
//
412+
// We can notice that the total area is independent of the order of tasks.
413+
// The first term of the rhs is the area above the diagonal.
414+
// The second term of the rhs is the area below the diagonal.
415+
//
394416
// We apply the following changes (see the code for cumulative constraints):
395417
// - we strengthen this cuts by noticing that if all tasks starts after S,
396418
// then replacing end_min_i by (end_min_i - S) is still valid.
@@ -461,12 +483,11 @@ void GenerateNoOvelap2dCompletionTimeCuts(absl::string_view cut_name,
461483
// Best cut so far for this loop.
462484
int best_end = -1;
463485
double best_efficacy = 0.01;
464-
IntegerValue best_min_contrib = 0;
465-
IntegerValue best_capacity = 0;
466-
IntegerValue best_y_range = 0;
486+
IntegerValue best_min_rhs = 0;
487+
bool best_use_subset_sum = false;
467488

468489
// Used in the first term of the rhs of the equation.
469-
IntegerValue sum_event_contributions = 0;
490+
IntegerValue sum_event_areas = 0;
470491
// Used in the second term of the rhs of the equation.
471492
IntegerValue sum_energy = 0;
472493
// For normalization.
@@ -486,8 +507,7 @@ void GenerateNoOvelap2dCompletionTimeCuts(absl::string_view cut_name,
486507
DCHECK_GE(event.x_start_min, sequence_start_min);
487508
// Make sure we do not overflow.
488509
if (!AddTo(event.energy_min, &sum_energy)) break;
489-
if (!AddProductTo(event.energy_min, event.x_size_min,
490-
&sum_event_contributions)) {
510+
if (!AddProductTo(event.energy_min, event.x_size_min, &sum_event_areas)) {
491511
break;
492512
}
493513
if (!AddSquareTo(event.energy_min, &sum_square_energy)) break;
@@ -505,7 +525,8 @@ void GenerateNoOvelap2dCompletionTimeCuts(absl::string_view cut_name,
505525
if (i == 0) {
506526
dp.Reset((y_max_of_subset - y_min_of_subset).value());
507527
} else {
508-
if (y_max_of_subset - y_min_of_subset != dp.Bound()) {
528+
// TODO(user): Can we increase the bound dynamically ?
529+
if (y_max_of_subset - y_min_of_subset > dp.Bound()) {
509530
use_dp = false;
510531
}
511532
}
@@ -522,23 +543,21 @@ void GenerateNoOvelap2dCompletionTimeCuts(absl::string_view cut_name,
522543
if (sum_of_y_size_min <= reachable_capacity) continue;
523544

524545
// Do we have a violated cut ?
525-
const IntegerValue large_rectangle_contrib =
526-
CapProdI(sum_energy, sum_energy);
527-
if (AtMinOrMaxInt64I(large_rectangle_contrib)) break;
528-
const IntegerValue mean_large_rectangle_contrib =
529-
CeilRatio(large_rectangle_contrib, reachable_capacity);
546+
const IntegerValue square_sum_energy = CapProdI(sum_energy, sum_energy);
547+
if (AtMinOrMaxInt64I(square_sum_energy)) break;
548+
const IntegerValue rhs_second_term =
549+
CeilRatio(square_sum_energy, reachable_capacity);
530550

531-
IntegerValue min_contrib =
532-
CapAddI(sum_event_contributions, mean_large_rectangle_contrib);
533-
if (AtMinOrMaxInt64I(min_contrib)) break;
534-
min_contrib = CeilRatio(min_contrib, 2);
551+
IntegerValue min_rhs = CapAddI(sum_event_areas, rhs_second_term);
552+
if (AtMinOrMaxInt64I(min_rhs)) break;
553+
min_rhs = CeilRatio(min_rhs, 2);
535554

536555
// shift contribution by current_start_min.
537-
if (!AddProductTo(sum_energy, current_start_min, &min_contrib)) break;
556+
if (!AddProductTo(sum_energy, current_start_min, &min_rhs)) break;
538557

539558
// The efficacy of the cut is the normalized violation of the above
540559
// equation. We will normalize by the sqrt of the sum of squared energies.
541-
const double efficacy = (ToDouble(min_contrib) - lp_contrib) /
560+
const double efficacy = (ToDouble(min_rhs) - lp_contrib) /
542561
std::sqrt(ToDouble(sum_square_energy));
543562

544563
// For a given start time, we only keep the best cut.
@@ -550,13 +569,13 @@ void GenerateNoOvelap2dCompletionTimeCuts(absl::string_view cut_name,
550569
if (efficacy > best_efficacy) {
551570
best_efficacy = efficacy;
552571
best_end = i;
553-
best_min_contrib = min_contrib;
554-
best_capacity = reachable_capacity;
555-
best_y_range = y_max_of_subset - y_min_of_subset;
572+
best_min_rhs = min_rhs;
573+
best_use_subset_sum =
574+
reachable_capacity < y_max_of_subset - y_min_of_subset;
556575
}
557576
}
558577
if (best_end != -1) {
559-
LinearConstraintBuilder cut(model, best_min_contrib, kMaxIntegerValue);
578+
LinearConstraintBuilder cut(model, best_min_rhs, kMaxIntegerValue);
560579
bool is_lifted = false;
561580
bool add_energy_to_name = false;
562581
for (int i = 0; i <= best_end; ++i) {
@@ -568,9 +587,7 @@ void GenerateNoOvelap2dCompletionTimeCuts(absl::string_view cut_name,
568587
std::string full_name(cut_name);
569588
if (is_lifted) full_name.append("_lifted");
570589
if (add_energy_to_name) full_name.append("_energy");
571-
if (best_capacity < best_y_range) {
572-
full_name.append("_subsetsum");
573-
}
590+
if (best_use_subset_sum) full_name.append("_subsetsum");
574591
top_n_cuts.AddCut(cut.Build(), full_name, manager->LpValues());
575592
}
576593
}

ortools/util/fixed_shape_binary_tree.h

+1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ class FixedShapeBinaryTree {
218218
template <typename TypeWithPushBack>
219219
void PartitionIntervalIntoNodes(LeafIndex first_leaf, LeafIndex last_leaf,
220220
TypeWithPushBack* result) const {
221+
DCHECK_LE(first_leaf, last_leaf);
221222
TreeNodeIndex prev(0);
222223
TreeNodeIndex current = GetNodeStartOfRange(first_leaf, last_leaf);
223224
if (current == Root()) {

0 commit comments

Comments
 (0)