Google OR-Tools v9.14
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
subsolver.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <cstdint>
17#include <functional>
18#include <limits>
19#include <memory>
20#include <string>
21#include <utility>
22#include <vector>
23
24#include "absl/flags/flag.h"
25#include "absl/log/check.h"
26#include "absl/log/log.h"
27#include "absl/log/vlog_is_on.h"
28#include "absl/strings/str_cat.h"
29#include "absl/strings/str_join.h"
30#include "absl/strings/string_view.h"
31#include "absl/synchronization/blocking_counter.h"
32#include "absl/synchronization/mutex.h"
33#include "absl/time/clock.h"
34#include "absl/time/time.h"
35#include "absl/types/span.h"
37#include "ortools/base/timer.h"
38#include "ortools/sat/util.h"
39#if !defined(__PORTABLE_PLATFORM__)
41#endif // __PORTABLE_PLATFORM__
42
43namespace operations_research {
44namespace sat {
45
46namespace {
47
48// Returns the next SubSolver index from which to call GenerateTask(). Note that
49// only SubSolvers for which TaskIsAvailable() is true are considered. Return -1
50// if no SubSolver can generate a new task.
51//
52// For now we use a really basic logic that tries to equilibrate the walltime or
53// deterministic time spent in each subsolver.
54int NextSubsolverToSchedule(std::vector<std::unique_ptr<SubSolver>>& subsolvers,
55 bool deterministic = true) {
56 int best = -1;
57 double best_score = std::numeric_limits<double>::infinity();
58 for (int i = 0; i < subsolvers.size(); ++i) {
59 if (subsolvers[i] == nullptr) continue;
60 if (subsolvers[i]->TaskIsAvailable()) {
61 const double score = subsolvers[i]->GetSelectionScore(deterministic);
62 if (best == -1 || score < best_score) {
63 best_score = score;
64 best = i;
65 }
66 }
67 }
68
69 if (best != -1) VLOG(1) << "Scheduling " << subsolvers[best]->name();
70 return best;
71}
72
73void ClearSubsolversThatAreDone(
74 absl::Span<const int> num_in_flight_per_subsolvers,
75 std::vector<std::unique_ptr<SubSolver>>& subsolvers) {
76 for (int i = 0; i < subsolvers.size(); ++i) {
77 if (subsolvers[i] == nullptr) continue;
78 if (num_in_flight_per_subsolvers[i] > 0) continue;
79 if (subsolvers[i]->IsDone()) {
80 // We can free the memory used by this solver for good.
81 VLOG(1) << "Deleting " << subsolvers[i]->name();
82 subsolvers[i].reset();
83 continue;
84 }
85 }
86}
87
88void SynchronizeAll(absl::Span<const std::unique_ptr<SubSolver>> subsolvers) {
89 for (const auto& subsolver : subsolvers) {
90 if (subsolver == nullptr) continue;
91 subsolver->Synchronize();
92 }
93}
94
95} // namespace
96
97void SequentialLoop(std::vector<std::unique_ptr<SubSolver>>& subsolvers) {
98 int64_t task_id = 0;
99 std::vector<int> num_in_flight_per_subsolvers(subsolvers.size(), 0);
100 while (true) {
101 SynchronizeAll(subsolvers);
102 ClearSubsolversThatAreDone(num_in_flight_per_subsolvers, subsolvers);
103 const int best = NextSubsolverToSchedule(subsolvers);
104 if (best == -1) break;
105 subsolvers[best]->NotifySelection();
106
107 WallTimer timer;
108 timer.Start();
109 subsolvers[best]->GenerateTask(task_id++)();
110 subsolvers[best]->AddTaskDuration(timer.Get());
111 }
112}
113
114#if defined(__PORTABLE_PLATFORM__)
115
116// On portable platform, we don't support multi-threading for now.
117
118void NonDeterministicLoop(std::vector<std::unique_ptr<SubSolver>>& subsolvers,
119 int num_threads, ModelSharedTimeLimit* time_limit) {
120 SequentialLoop(subsolvers);
121}
122
123void DeterministicLoop(std::vector<std::unique_ptr<SubSolver>>& subsolvers,
124 int num_threads, int batch_size, int max_num_batches) {
125 SequentialLoop(subsolvers);
126}
127
128#else // __PORTABLE_PLATFORM__
129
130void DeterministicLoop(std::vector<std::unique_ptr<SubSolver>>& subsolvers,
131 int num_threads, int batch_size, int max_num_batches) {
132 CHECK_GT(num_threads, 0);
133 CHECK_GT(batch_size, 0);
134 if (batch_size == 1) {
135 return SequentialLoop(subsolvers);
136 }
137
138 int64_t task_id = 0;
139 std::vector<int> num_in_flight_per_subsolvers(subsolvers.size(), 0);
140 std::vector<std::function<void()>> to_run;
141 std::vector<int> indices;
142 std::vector<double> timing;
143 to_run.reserve(batch_size);
144 ThreadPool pool(num_threads);
145 pool.StartWorkers();
146 for (int batch_index = 0;; ++batch_index) {
147 VLOG(2) << "Starting deterministic batch of size " << batch_size;
148 SynchronizeAll(subsolvers);
149 ClearSubsolversThatAreDone(num_in_flight_per_subsolvers, subsolvers);
150
151 // We abort the loop after the last synchronize to properly reports final
152 // status in case max_num_batches is used.
153 if (max_num_batches > 0 && batch_index >= max_num_batches) break;
154
155 // We first generate all task to run in this batch.
156 // Note that we can't start the task right away since if a task finish
157 // before we schedule everything, we will not be deterministic.
158 to_run.clear();
159 indices.clear();
160 for (int t = 0; t < batch_size; ++t) {
161 const int best = NextSubsolverToSchedule(subsolvers);
162 if (best == -1) break;
163 num_in_flight_per_subsolvers[best]++;
164 subsolvers[best]->NotifySelection();
165 to_run.push_back(subsolvers[best]->GenerateTask(task_id++));
166 indices.push_back(best);
167 }
168 if (to_run.empty()) break;
169
170 // Schedule each task.
171 timing.resize(to_run.size());
172 absl::BlockingCounter blocking_counter(static_cast<int>(to_run.size()));
173 for (int i = 0; i < to_run.size(); ++i) {
174 pool.Schedule(
175 [i, f = std::move(to_run[i]), &timing, &blocking_counter]() {
176 WallTimer timer;
177 timer.Start();
178 f();
179 timing[i] = timer.Get();
180 blocking_counter.DecrementCount();
181 });
182 }
183
184 // Wait for all tasks of this batch to be done before scheduling another
185 // batch.
186 blocking_counter.Wait();
187
188 // Update times.
189 num_in_flight_per_subsolvers.assign(subsolvers.size(), 0);
190 for (int i = 0; i < to_run.size(); ++i) {
191 subsolvers[indices[i]]->AddTaskDuration(timing[i]);
192 }
193 }
194}
195
196void NonDeterministicLoop(std::vector<std::unique_ptr<SubSolver>>& subsolvers,
197 const int num_threads,
199 CHECK_GT(num_threads, 0);
200 if (num_threads == 1) {
201 return SequentialLoop(subsolvers);
202 }
203
204 // The mutex guards num_in_flight and num_in_flight_per_subsolvers.
205 // This is used to detect when the search is done.
206 absl::Mutex mutex;
207 int num_in_flight = 0; // Guarded by `mutex`.
208 std::vector<int> num_in_flight_per_subsolvers(subsolvers.size(), 0);
209
210 // Predicate to be used with absl::Condition to detect that num_in_flight <
211 // num_threads. Must only be called while locking `mutex`.
212 const auto num_in_flight_lt_num_threads = [&num_in_flight, num_threads]() {
213 return num_in_flight < num_threads;
214 };
215
216 ThreadPool pool(num_threads);
217 pool.StartWorkers();
218
219 // The lambda below are using little space, but there is no reason
220 // to create millions of them, so we use the blocking nature of
221 // pool.Schedule() when the queue capacity is set.
222 int64_t task_id = 0;
223 while (true) {
224 // Set to true if no task is pending right now.
225 bool all_done = false;
226 {
227 // Wait if num_in_flight == num_threads.
228 const bool condition = mutex.LockWhenWithTimeout(
229 absl::Condition(&num_in_flight_lt_num_threads),
230 absl::Milliseconds(100));
231
232 // To support some "advanced" cancelation of subsolve, we still call
233 // synchronize every 0.1 seconds even if there is no worker available.
234 //
235 // TODO(user): We could also directly register callback to set stopping
236 // Boolean to false in a few places.
237 if (!condition) {
238 mutex.Unlock();
239 SynchronizeAll(subsolvers);
240 continue;
241 }
242
243 // The stopping condition is that we do not have anything else to generate
244 // once all the task are done and synchronized.
245 if (num_in_flight == 0) all_done = true;
246 mutex.Unlock();
247 }
248
249 SynchronizeAll(subsolvers);
250 int best = -1;
251 {
252 // We need to do that while holding the lock since substask below might
253 // be currently updating the time via AddTaskDuration().
254 const absl::MutexLock mutex_lock(&mutex);
255 ClearSubsolversThatAreDone(num_in_flight_per_subsolvers, subsolvers);
256 best = NextSubsolverToSchedule(subsolvers, /*deterministic=*/false);
257 if (VLOG_IS_ON(1) && time_limit->LimitReached()) {
258 std::vector<std::string> debug;
259 for (int i = 0; i < subsolvers.size(); ++i) {
260 if (subsolvers[i] != nullptr && num_in_flight_per_subsolvers[i] > 0) {
261 debug.push_back(absl::StrCat(subsolvers[i]->name(), ":",
262 num_in_flight_per_subsolvers[i]));
263 }
264 }
265 if (!debug.empty()) {
266 VLOG_EVERY_N_SEC(1, 1)
267 << "Subsolvers still running after time limit: "
268 << absl::StrJoin(debug, ",");
269 }
270 }
271 }
272 if (best == -1) {
273 if (all_done) break;
274
275 // It is hard to know when new info will allows for more task to be
276 // scheduled, so for now we just sleep for a bit. Note that in practice We
277 // will never reach here except at the end of the search because we can
278 // always schedule LNS threads.
279 absl::SleepFor(absl::Milliseconds(1));
280 continue;
281 }
282
283 // Schedule next task.
284 subsolvers[best]->NotifySelection();
285 {
286 absl::MutexLock mutex_lock(&mutex);
287 num_in_flight++;
288 num_in_flight_per_subsolvers[best]++;
289 }
290 std::function<void()> task = subsolvers[best]->GenerateTask(task_id++);
291 const std::string name = subsolvers[best]->name();
292 pool.Schedule([task = std::move(task), name, best, &subsolvers, &mutex,
293 &num_in_flight, &num_in_flight_per_subsolvers]() {
294 WallTimer timer;
295 timer.Start();
296 task();
297
298 const absl::MutexLock mutex_lock(&mutex);
299 DCHECK(subsolvers[best] != nullptr);
300 DCHECK_GT(num_in_flight_per_subsolvers[best], 0);
301 num_in_flight_per_subsolvers[best]--;
302 VLOG(1) << name << " done in " << timer.Get() << "s.";
303 subsolvers[best]->AddTaskDuration(timer.Get());
304 num_in_flight--;
305 });
306 }
307}
308
309#endif // __PORTABLE_PLATFORM__
310
311} // namespace sat
312} // namespace operations_research
double Get() const
Definition timer.h:44
void Start()
When Start() is called multiple times, only the most recent is used.
Definition timer.h:30
void Schedule(std::function< void()> closure)
Definition threadpool.cc:83
The model "singleton" shared time limit.
Definition util.h:354
time_limit
Definition solve.cc:22
void DeterministicLoop(std::vector< std::unique_ptr< SubSolver > > &subsolvers, int num_threads, int batch_size, int max_num_batches)
Definition subsolver.cc:130
void NonDeterministicLoop(std::vector< std::unique_ptr< SubSolver > > &subsolvers, const int num_threads, ModelSharedTimeLimit *time_limit)
Definition subsolver.cc:196
void SequentialLoop(std::vector< std::unique_ptr< SubSolver > > &subsolvers)
Definition subsolver.cc:97
In SWIG mode, we don't want anything besides these top-level includes.