Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
work_assignment.h
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
14#ifndef OR_TOOLS_SAT_WORK_ASSIGNMENT_H_
15#define OR_TOOLS_SAT_WORK_ASSIGNMENT_H_
16
17#include <stdint.h>
18#include <sys/stat.h>
19
20#include <array>
21#include <deque>
22#include <functional>
23#include <limits>
24#include <memory>
25#include <optional>
26#include <string>
27#include <utility>
28#include <vector>
29
30#include "absl/base/thread_annotations.h"
31#include "absl/container/flat_hash_map.h"
32#include "absl/container/flat_hash_set.h"
33#include "absl/container/node_hash_map.h"
34#include "absl/log/check.h"
35#include "absl/synchronization/mutex.h"
36#include "absl/types/span.h"
39#include "ortools/sat/integer.h"
42#include "ortools/sat/model.h"
43#include "ortools/sat/restart.h"
46#include "ortools/sat/sat_parameters.pb.h"
49#include "ortools/sat/util.h"
53
55
57 public:
58 ProtoLiteral() = default;
59 ProtoLiteral(int var, IntegerValue lb) : proto_var_(var), lb_(lb) {}
61 return ProtoLiteral(NegatedRef(proto_var_), -lb_ + 1);
62 }
63 int proto_var() const { return proto_var_; }
64 IntegerValue lb() const { return lb_; }
65 bool operator==(const ProtoLiteral& other) const {
66 return proto_var_ == other.proto_var_ && lb_ == other.lb_;
67 }
68 bool operator!=(const ProtoLiteral& other) const { return !(*this == other); }
69 template <typename H>
70 friend H AbslHashValue(H h, const ProtoLiteral& literal) {
71 return H::combine(std::move(h), literal.proto_var_, literal.lb_);
72 }
73
74 // Note you should only decode integer literals at the root level.
76
77 // Enodes a literal as a ProtoLiteral. This can encode literals that occur in
78 // the proto model, and also integer bounds literals.
79 static std::optional<ProtoLiteral> Encode(Literal, CpModelMapping*,
81
82 // As above, but will only encode literals that are boolean variables or their
83 // negations (i.e. not integer bounds literals).
84 static std::optional<ProtoLiteral> EncodeLiteral(Literal, CpModelMapping*);
85
86 private:
87 IntegerLiteral DecodeInteger(CpModelMapping*) const;
88 static std::optional<ProtoLiteral> EncodeInteger(IntegerLiteral,
90
91 int proto_var_ = std::numeric_limits<int>::max();
92 IntegerValue lb_ = kMaxIntegerValue;
93};
94
95// ProtoTrail acts as an intermediate datastructure that can be synced
96// with the shared tree and the local Trail as appropriate.
97// It's intended that you sync a ProtoTrail with the tree on restart or when
98// a subtree is closed, and with the local trail after propagation.
99// Specifically it stores objective lower bounds, and literals that have been
100// branched on and later proven to be implied by the prior decisions (i.e. they
101// can be enqueued at this level).
102// TODO(user): It'd be good to store an earlier level at which
103// implications may be propagated.
105 public:
106 ProtoTrail();
107
108 // Adds a new assigned level to the trail.
109 void PushLevel(const ProtoLiteral& decision, IntegerValue objective_lb,
110 int node_id);
111
112 // Asserts that the decision at `level` is implied by earlier decisions.
113 void SetLevelImplied(int level);
114
115 // Clear the trail, removing all levels.
116 void Clear();
117
118 // Set a lower bound on the objective at level.
119 void SetObjectiveLb(int level, IntegerValue objective_lb);
120
121 // Returns the maximum decision level stored in the trail.
122 int MaxLevel() const { return decision_indexes_.size(); }
123
124 // Returns the decision assigned at `level`.
125 ProtoLiteral Decision(int level) const {
126 CHECK_GE(level, 1);
127 CHECK_LE(level, decision_indexes_.size());
128 return literals_[decision_indexes_[level - 1]];
129 }
130
131 // Returns the node ids for decisions and implications at `level`.
132 absl::Span<const int> NodeIds(int level) const;
133
134 // Returns literals which may be propagated at `level`, this does not include
135 // the decision.
136 absl::Span<const ProtoLiteral> Implications(int level) const;
137 void AddImplication(int level, ProtoLiteral implication) {
138 auto it = implication_level_.find(implication);
139 if (it != implication_level_.end() && it->second <= level) return;
140 MutableImplications(level).push_back(implication);
141 implication_level_[implication] = level;
142 }
143
144 IntegerValue ObjectiveLb(int level) const {
145 CHECK_GE(level, 1);
146 return level_to_objective_lbs_[level - 1];
147 }
148
149 absl::Span<const ProtoLiteral> Literals() const { return literals_; }
150
151 const std::vector<ProtoLiteral>& TargetPhase() const { return target_phase_; }
152 void ClearTargetPhase() { target_phase_.clear(); }
153 // Appends a literal to the target phase, returns false if the phase is full.
154 bool AddPhase(const ProtoLiteral& lit) {
155 if (target_phase_.size() >= kMaxPhaseSize) return false;
156 if (!implication_level_.contains(lit)) {
157 target_phase_.push_back(lit);
158 }
159 return true;
160 }
161 void SetTargetPhase(absl::Span<const ProtoLiteral> phase) {
163 for (const ProtoLiteral& lit : phase) {
164 if (!AddPhase(lit)) break;
165 }
166 }
167
168 private:
169 // 256 ProtoLiterals take up 4KiB
170 static constexpr int kMaxPhaseSize = 256;
171
172 std::vector<ProtoLiteral>& MutableImplications(int level) {
173 return implications_[level - 1];
174 }
175 // Parallel vectors encoding the literals and node ids on the trail.
176 std::vector<ProtoLiteral> literals_;
177 std::vector<int> node_ids_;
178
179 // Extra implications that can be propagated at each level but were never
180 // branches in the shared tree.
181 std::vector<std::vector<ProtoLiteral>> implications_;
182 absl::flat_hash_map<ProtoLiteral, int> implication_level_;
183
184 // The index in the literals_/node_ids_ vectors for the start of each level.
185 std::vector<int> decision_indexes_;
186
187 // The objective lower bound of each level.
188 std::vector<IntegerValue> level_to_objective_lbs_;
189
190 std::vector<ProtoLiteral> target_phase_;
191};
192
193// Experimental thread-safe class for managing work assignments between workers.
194// This API is intended to investigate Graeme Gange & Peter Stuckey's proposal
195// "Scalable Parallelization of Learning Solvers".
196// The core idea of this implementation is that workers can maintain several
197// ProtoTrails, and periodically replace the "worst" one.
198// With 1 assignment per worker, this leads to something very similar to
199// Embarassingly Parallel Search.
201 public:
202 explicit SharedTreeManager(Model* model);
204
205 int NumWorkers() const { return num_workers_; }
206 int NumNodes() const ABSL_LOCKS_EXCLUDED(mu_);
207
208 // Syncs the state of path with the shared search tree.
209 // Clears `path` and returns false if the assigned subtree is closed or a
210 // restart has invalidated the path.
211 bool SyncTree(ProtoTrail& path) ABSL_LOCKS_EXCLUDED(mu_);
212
213 // Assigns a path prefix that the worker should explore.
214 void ReplaceTree(ProtoTrail& path);
215
216 // Asserts that the subtree in path up to `level` contains no improving
217 // solutions. Clears path.
218 void CloseTree(ProtoTrail& path, int level);
219
220 // Called by workers in order to split the shared tree.
221 // `path` may or may not be extended by one level, branching on `decision`.
222 void ProposeSplit(ProtoTrail& path, ProtoLiteral decision);
223
224 void Restart() {
225 absl::MutexLock l(&mu_);
226 RestartLockHeld();
227 }
228
229 private:
230 // Because it is quite difficult to get a flat_hash_map to release memory,
231 // we store info we need only for open nodes implications via a unique_ptr.
232 // Note to simplify code, the root will always have a NodeTrailInfo after it
233 // is closed.
234 struct NodeTrailInfo {
235 // A map from literal to the best lower bound proven at this node.
236 absl::flat_hash_map<int, IntegerValue> implications;
237 // This is only non-empty for nodes where all but one descendent is closed
238 // (i.e. mostly leaves).
239 std::vector<ProtoLiteral> phase;
240 };
241 struct Node {
242 ProtoLiteral literal;
243 IntegerValue objective_lb = kMinIntegerValue;
244 Node* parent = nullptr;
245 std::array<Node*, 2> children = {nullptr, nullptr};
246 // A node's id is related to its index in `nodes_`, see `node_id_offset_`.
247 int id;
248 bool closed = false;
249 bool implied = false;
250 // Only set for open, non-implied nodes.
251 std::unique_ptr<NodeTrailInfo> trail_info;
252 };
253 bool IsValid(const ProtoTrail& path) const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
254 Node* GetSibling(Node* node) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
255 // Returns the NodeTrailInfo for `node` or it's closest non-closed,
256 // non-implied ancestor. `node` must be valid, never returns nullptr.
257 NodeTrailInfo* GetTrailInfo(Node* node);
258 void Split(std::vector<std::pair<Node*, int>>& nodes, ProtoLiteral lit)
259 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
260 Node* MakeSubtree(Node* parent, ProtoLiteral literal)
261 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
262 void ProcessNodeChanges() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
263 std::vector<std::pair<Node*, int>> GetAssignedNodes(const ProtoTrail& path)
264 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
265 void AssignLeaf(ProtoTrail& path, Node* leaf)
266 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
267 void RestartLockHeld() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
268 std::string ShortStatus() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
269
270 mutable absl::Mutex mu_;
271 const SatParameters& params_;
272 const int num_workers_;
273 SharedResponseManager* const shared_response_manager_;
274
275 // Stores the node id of the root, this is used to handle global restarts.
276 int node_id_offset_ ABSL_GUARDED_BY(mu_) = 0;
277
278 // Stores the nodes in the search tree.
279 std::deque<Node> nodes_ ABSL_GUARDED_BY(mu_);
280 std::vector<Node*> unassigned_leaves_ ABSL_GUARDED_BY(mu_);
281
282 // How many splits we should generate now to keep the desired number of
283 // leaves.
284 int num_splits_wanted_ ABSL_GUARDED_BY(mu_);
285
286 // We limit the total nodes generated per restart to cap the RAM usage and
287 // communication overhead. If we exceed this, workers become portfolio
288 // workers when no unassigned leaves are available.
289 const int max_nodes_;
290 int num_leaves_assigned_ ABSL_GUARDED_BY(mu_) = 0;
291
292 // Temporary vectors used to maintain the state of the tree when nodes are
293 // closed and/or children are updated.
294 std::vector<Node*> to_close_ ABSL_GUARDED_BY(mu_);
295 std::vector<Node*> to_update_ ABSL_GUARDED_BY(mu_);
296
297 int64_t num_restarts_ ABSL_GUARDED_BY(mu_) = 0;
298 int64_t num_syncs_since_restart_ ABSL_GUARDED_BY(mu_) = 0;
299 int num_closed_nodes_ ABSL_GUARDED_BY(mu_) = 0;
300};
301
303 public:
304 explicit SharedTreeWorker(Model* model);
307
309 const std::function<void()>& feasible_solution_observer);
310
311 private:
312 // Syncs the assigned tree with the local trail, ensuring that any new
313 // implications are synced. This is a noop if the search is deeper than the
314 // assigned tree. Returns false if the problem is unsat.
315 bool SyncWithLocalTrail();
316 bool SyncWithSharedTree();
317 Literal DecodeDecision(ProtoLiteral literal);
318 std::optional<ProtoLiteral> EncodeDecision(Literal decision);
319 bool NextDecision(LiteralIndex* decision_index);
320 void MaybeProposeSplit();
321 bool ShouldReplaceSubtree();
322 bool FinishedMinRestarts() const {
323 return assigned_tree_.MaxLevel() > 0 &&
324 restart_policy_->NumRestarts() >=
325 parameters_->shared_tree_worker_min_restarts_per_subtree();
326 }
327
328 // Add any implications to the clause database for the current level.
329 // Return true if any new information was added.
330 bool AddImplications();
331 bool AddDecisionImplication(Literal literal, int level);
332
333 const std::vector<Literal>& DecisionReason(int level);
334
335 SatParameters* parameters_;
336 SharedResponseManager* shared_response_;
337 TimeLimit* time_limit_;
338 SharedTreeManager* manager_;
339 CpModelMapping* mapping_;
340 SatSolver* sat_solver_;
341 Trail* trail_;
342 IntegerTrail* integer_trail_;
343 IntegerEncoder* encoder_;
344 const ObjectiveDefinition* objective_;
345 ModelRandomGenerator* random_;
346 IntegerSearchHelper* helper_;
347 SearchHeuristics* heuristics_;
348 SatDecisionPolicy* decision_policy_;
349 RestartPolicy* restart_policy_;
350 LevelZeroCallbackHelper* level_zero_callbacks_;
351 RevIntRepository* reversible_int_repository_;
352
353 int64_t num_trees_ = 0;
354
355 ProtoTrail assigned_tree_;
356 std::vector<Literal> assigned_tree_literals_;
357 std::vector<std::vector<Literal>> assigned_tree_implications_;
358
359 // True if the last decision may split the assigned tree and has not yet been
360 // proposed to the SharedTreeManager.
361 // We propagate the decision before sharing with the SharedTreeManager so we
362 // don't share any decision that immediately leads to conflict.
363 bool new_split_available_ = false;
364
365 std::vector<Literal> reason_;
366 // Stores the average LBD of learned clauses for each tree assigned since it
367 // was assigned.
368 // If a tree has worse LBD than the average over the last few trees we replace
369 // the tree.
370 RunningAverage assigned_tree_lbds_;
371 double earliest_replacement_dtime_ = 0;
372
373 // Stores the trail index of the last implication added to assigned_tree_.
374 int reversible_trail_index_ = 0;
375 // Stores the number of implications processed for each level in
376 // assigned_tree_.
377 std::deque<int> rev_num_processed_implications_;
378};
379
380} // namespace operations_research::sat
381
382#endif // OR_TOOLS_SAT_WORK_ASSIGNMENT_H_
An helper class to share the code used by the different kind of search.
static std::optional< ProtoLiteral > Encode(Literal, CpModelMapping *, IntegerEncoder *)
friend H AbslHashValue(H h, const ProtoLiteral &literal)
Literal Decode(CpModelMapping *, IntegerEncoder *) const
Note you should only decode integer literals at the root level.
ProtoLiteral(int var, IntegerValue lb)
bool operator==(const ProtoLiteral &other) const
static std::optional< ProtoLiteral > EncodeLiteral(Literal, CpModelMapping *)
bool operator!=(const ProtoLiteral &other) const
int MaxLevel() const
Returns the maximum decision level stored in the trail.
void Clear()
Clear the trail, removing all levels.
void SetTargetPhase(absl::Span< const ProtoLiteral > phase)
ProtoLiteral Decision(int level) const
Returns the decision assigned at level.
bool AddPhase(const ProtoLiteral &lit)
Appends a literal to the target phase, returns false if the phase is full.
void SetObjectiveLb(int level, IntegerValue objective_lb)
Set a lower bound on the objective at level.
absl::Span< const ProtoLiteral > Implications(int level) const
absl::Span< const int > NodeIds(int level) const
Returns the node ids for decisions and implications at level.
void AddImplication(int level, ProtoLiteral implication)
IntegerValue ObjectiveLb(int level) const
void PushLevel(const ProtoLiteral &decision, IntegerValue objective_lb, int node_id)
Adds a new assigned level to the trail.
absl::Span< const ProtoLiteral > Literals() const
const std::vector< ProtoLiteral > & TargetPhase() const
void SetLevelImplied(int level)
Asserts that the decision at level is implied by earlier decisions.
Contain the logic to decide when to restart a SAT tree search.
Definition restart.h:32
void CloseTree(ProtoTrail &path, int level)
SharedTreeManager(const SharedTreeManager &)=delete
void ProposeSplit(ProtoTrail &path, ProtoLiteral decision)
void ReplaceTree(ProtoTrail &path)
Assigns a path prefix that the worker should explore.
bool SyncTree(ProtoTrail &path) ABSL_LOCKS_EXCLUDED(mu_)
int NumNodes() const ABSL_LOCKS_EXCLUDED(mu_)
SharedTreeWorker & operator=(const SharedTreeWorker &)=delete
SharedTreeWorker(const SharedTreeWorker &)=delete
SatSolver::Status Search(const std::function< void()> &feasible_solution_observer)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
int NegatedRef(int ref)
Small utility functions to deal with negative variable/literal references.