Google OR-Tools v9.15
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 ORTOOLS_SAT_WORK_ASSIGNMENT_H_
15#define ORTOOLS_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/functional/function_ref.h"
35#include "absl/log/check.h"
36#include "absl/synchronization/mutex.h"
37#include "absl/types/span.h"
38#include "ortools/sat/clause.h"
41#include "ortools/sat/integer.h"
45#include "ortools/sat/model.h"
46#include "ortools/sat/restart.h"
52#include "ortools/sat/util.h"
56
58
60 public:
61 ProtoLiteral() = default;
62 ProtoLiteral(int var, IntegerValue lb) : proto_var_(var), lb_(lb) {}
64 return ProtoLiteral(NegatedRef(proto_var_), -lb_ + 1);
65 }
66 int proto_var() const { return proto_var_; }
67 IntegerValue lb() const { return lb_; }
68 bool operator==(const ProtoLiteral& other) const {
69 return proto_var_ == other.proto_var_ && lb_ == other.lb_;
70 }
71 bool operator!=(const ProtoLiteral& other) const { return !(*this == other); }
72 template <typename H>
73 friend H AbslHashValue(H h, const ProtoLiteral& literal) {
74 return H::combine(std::move(h), literal.proto_var_, literal.lb_);
75 }
76
77 // Note you should only decode integer literals at the root level.
79
80 // Encodes a literal as a ProtoLiteral. This can encode literals that occur in
81 // the proto model, and also integer bounds literals.
82 static std::optional<ProtoLiteral> Encode(Literal, CpModelMapping*,
84
85 // As above, but will only encode literals that are boolean variables or their
86 // negations (i.e. not integer bounds literals).
87 static std::optional<ProtoLiteral> EncodeLiteral(Literal, CpModelMapping*);
88
89 private:
90 IntegerLiteral DecodeInteger(CpModelMapping*) const;
91 static std::optional<ProtoLiteral> EncodeInteger(IntegerLiteral,
93
94 int proto_var_ = std::numeric_limits<int>::max();
95 IntegerValue lb_ = kMaxIntegerValue;
96};
97
98// ProtoTrail acts as an intermediate datastructure that can be synced
99// with the shared tree and the local Trail as appropriate.
100// It's intended that you sync a ProtoTrail with the tree on restart or when
101// a subtree is closed, and with the local trail after propagation.
102// Specifically it stores objective lower bounds, and literals that have been
103// branched on and later proven to be implied by the prior decisions (i.e. they
104// can be enqueued at this level).
105// TODO(user): It'd be good to store an earlier level at which
106// implications may be propagated.
108 public:
109 ProtoTrail();
110
111 // Adds a new assigned level to the trail.
112 void PushLevel(const ProtoLiteral& decision, IntegerValue objective_lb,
113 int node_id);
114
115 // Asserts that the decision at `level` is implied by earlier decisions.
116 void SetLevelImplied(int level);
117
118 // Clear the trail, removing all levels.
119 void Clear();
120
121 // Set a lower bound on the objective at level.
122 void SetObjectiveLb(int level, IntegerValue objective_lb);
123
124 // Returns the maximum decision level stored in the trail.
125 int MaxLevel() const { return decision_indexes_.size(); }
126
127 // Returns the decision assigned at `level`.
128 ProtoLiteral Decision(int level) const {
129 CHECK_GE(level, 1);
130 CHECK_LE(level, decision_indexes_.size());
131 return literals_[decision_indexes_[level - 1]];
132 }
133
134 // Returns the node ID for the decision at `level`.
135 int DecisionNodeId(int level) const;
136
137 // Returns the node ids for decisions and implications at `level`.
138 absl::Span<const int> NodeIds(int level) const;
139
140 // Returns literals which may be propagated at `level`, this does not include
141 // the decision.
142 absl::Span<const ProtoLiteral> Implications(int level) const;
143
144 // Adds a literal which is implied by the decisions from level 1 to `level`.
145 // The caller must add a corresponding LRAT inferred clause (if LRAT is
146 // enabled). This implication can then be used by other workers as an LRAT
147 // imported clause, without proof.
148 bool AddImplication(int level, ProtoLiteral implication) {
149 auto it = assigned_at_level_.find(implication);
150 if (it != assigned_at_level_.end() && it->second <= level) return false;
151 MutableImplications(level).push_back(implication);
152 assigned_at_level_[implication] = level;
153 return true;
154 }
155
156 // Removes implications that are already assigned at an earlier level, as well
157 // as duplicate implications at the same level.
159
160 IntegerValue ObjectiveLb(int level) const {
161 CHECK_GE(level, 1);
162 return level_to_objective_lbs_[level - 1];
163 }
164
165 absl::Span<const ProtoLiteral> Literals() const { return literals_; }
166
167 const std::vector<ProtoLiteral>& TargetPhase() const { return target_phase_; }
168
169 // Returns the target phase and clears it.
170 std::vector<ProtoLiteral> TakeTargetPhase() {
171 return std::move(target_phase_);
172 }
173 void ClearTargetPhase() { target_phase_.clear(); }
174 // Appends a literal to the target phase, returns false if the phase is full.
175 bool AddPhase(const ProtoLiteral& lit) {
176 if (target_phase_.size() >= kMaxPhaseSize) return false;
177 if (!IsAssigned(lit)) {
178 target_phase_.push_back(lit);
179 }
180 return true;
181 }
182 void SetTargetPhase(std::vector<ProtoLiteral> phase) {
183 target_phase_ = std::move(phase);
184 }
185 bool IsAssigned(const ProtoLiteral& lit) const {
186 return assigned_at_level_.contains(lit) ||
187 assigned_at_level_.contains(lit.Negated());
188 }
189
190 private:
191 // Store up to 4 KiB of literals in the target phase.
192 static constexpr int kMaxPhaseSize = 256;
193
194 std::vector<ProtoLiteral>& MutableImplications(int level) {
195 return implications_[level - 1];
196 }
197 // Parallel vectors encoding the literals and node ids on the trail.
198 std::vector<ProtoLiteral> literals_;
199 std::vector<int> node_ids_;
200
201 // Extra implications that can be propagated at each level but were never
202 // branches in the shared tree.
203 std::vector<std::vector<ProtoLiteral>> implications_;
204 absl::flat_hash_map<ProtoLiteral, int> assigned_at_level_;
205
206 // The index in the literals_/node_ids_ vectors for the start of each level.
207 std::vector<int> decision_indexes_;
208
209 // The objective lower bound of each level.
210 std::vector<IntegerValue> level_to_objective_lbs_;
211
212 std::vector<ProtoLiteral> target_phase_;
213};
214
215// Experimental thread-safe class for managing work assignments between workers.
216// This API is intended to investigate Graeme Gange & Peter Stuckey's proposal
217// "Scalable Parallelization of Learning Solvers".
218// The core idea of this implementation is that workers can maintain several
219// ProtoTrails, and periodically replace the "worst" one.
220// With 1 assignment per worker, this leads to something very similar to
221// Embarassingly Parallel Search.
223 public:
224 explicit SharedTreeManager(Model* model);
226
227 int NumWorkers() const { return num_workers_; }
228 int NumNodes() const ABSL_LOCKS_EXCLUDED(mu_);
229 int MaxPathDepth() const { return max_path_depth_; }
230
231 // Syncs the state of path with the shared search tree.
232 // Clears `path` and returns false if the assigned subtree is closed or a
233 // restart has invalidated the path.
234 bool SyncTree(ProtoTrail& path) ABSL_LOCKS_EXCLUDED(mu_);
235
236 // Assigns a path prefix that the worker should explore.
237 void ReplaceTree(ProtoTrail& path);
238
239 // Asserts that the subtree in path up to `level` contains no improving
240 // solutions. Clears path. The caller must add a corresponding LRAT inferred
241 // clause first (stating that the negation of the decision at `level` is
242 // implied by the previous decisions).
243 void CloseTree(ProtoTrail& path, int level);
244
245 // Attempts to split the tree repeatedly with the given decisions.
246 // `path` will be extended with the accepted splits, the opposite branches
247 // will be added as unassigned leaves.
248 // Returns the number of splits accepted.
249 int TrySplitTree(absl::Span<const ProtoLiteral> decisions, ProtoTrail& path)
250 ABSL_LOCKS_EXCLUDED(mu_);
251
252 void Restart() {
253 absl::MutexLock l(mu_);
254 RestartLockHeld();
255 }
256
257 void CloseLratProof();
258
259 private:
260 // Because it is quite difficult to get a flat_hash_map to release memory,
261 // we store info we need only for open nodes implications via a unique_ptr.
262 // Note to simplify code, the root will always have a NodeTrailInfo after it
263 // is closed.
264 struct NodeTrailInfo {
265 // A map from literal to the best lower bound proven at this node, and to
266 // the LRAT clause "decisions of node and its parents => literal" (with
267 // literals of implied nodes filtered out).
268 absl::flat_hash_map<int, std::pair<IntegerValue, ClauseId>> implications;
269 // This is only non-empty for nodes where all but one descendent is closed
270 // (i.e. mostly leaves).
271 std::vector<ProtoLiteral> phase;
272 };
273 struct Node {
274 ProtoLiteral decision;
275 IntegerValue objective_lb = kMinIntegerValue;
276 Node* parent = nullptr;
277 std::array<Node*, 2> children = {nullptr, nullptr};
278 // A node's id is related to its index in `nodes_`, see `node_id_offset_`.
279 int id;
280 // For each closed node, closing_clause_id is an LRAT clause stating that
281 // all the parent decisions imply the negation of the node's decision.
282 bool closed = false;
283 ClauseId closing_clause_id = kNoClauseId;
284 // A node is implied if its sibling is closed. The closing clause of the
285 // sibling is also the clause stating that all the parent decisions imply
286 // the node's decision.
287 bool implied = false;
288 bool implied_and_processed = false;
289 // Only set for open, non-implied nodes.
290 std::unique_ptr<NodeTrailInfo> trail_info;
291 };
292 bool IsValid(const ProtoTrail& path) const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
293 Node* GetSibling(const Node* node) const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
294 // Returns the NodeTrailInfo for `node` or it's closest non-closed,
295 // non-implied ancestor. `node` must be valid, never returns nullptr.
296 NodeTrailInfo* GetTrailInfo(Node* node);
297 void ClearTrailInfo(Node* node, bool implications_only = false);
298 bool TrySplitTreeLockHeld(ProtoLiteral decision, ProtoTrail& path)
299 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
300 void Split(std::vector<std::pair<Node*, int>>& nodes, ProtoLiteral lit)
301 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
302 Node* MakeSubtree(Node* parent, ProtoLiteral decision)
303 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
304 void ProcessNodeChanges() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
305 void ProcessImpliedNode(Node* node) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
306 void UpdateLratClausesInSubtree(Node* node, Node* n,
307 std::vector<ClauseId>& clauses)
308 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
309 Node* GetNode(int node_id) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
310 std::vector<std::pair<Node*, int>> GetAssignedNodes(const ProtoTrail& path)
311 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
312 void AssignLeaf(ProtoTrail& path, Node* leaf)
313 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
314 void RestartLockHeld() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
315 std::string ShortStatus() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
316
317 // Returns true if `literal` is a decision of `node` or one of its ancestors.
318 bool IsDecisionOfNodeOrAncestor(ProtoLiteral literal, const Node* node) const
319 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
320
321 // Returns "non-implied decisions of `node` and its ancestors => implied". A
322 // node is considered implied if its `implied` field is true, and if its
323 // `implied_and_processed` field or `skip_unprocessed_implied_nodes` is
324 // true.
325 std::vector<Literal> ImplicationClause(
326 const Node* node, ProtoLiteral implied,
327 bool skip_unprocessed_implied_nodes = false) const
328 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
329
330 // Returns "decisions of `node`'s ancestors => negation of `node`'s decision"
331 // (with decisions of implied nodes filtered out -- a node is considered
332 // implied if its `implied` field is true, and if its `implied_and_processed`
333 // field or `skip_unprocessed_implied_nodes` is true).
334 std::vector<Literal> ClosingClause(
335 const Node* node, bool skip_unprocessed_implied_nodes = false) const
336 ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
337
338 // Adds `imported_clause` to the LRAT proof handler, as well as
339 // `inferred_clause`, inferred from `imported_clause` with `lrat_proof` (which
340 // should contain clauses proving that literals removed from `imported_clause`
341 // can actually be removed). Then deletes `imported_clause` and returns the ID
342 // of the inferred clause.
343 ClauseId AddImportedAndInferredClauses(
344 absl::Span<const Literal> imported_clause,
345 absl::Span<const Literal> inferred_clause,
346 std::vector<ClauseId>& lrat_proof) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
347
348 bool CheckLratInvariants() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
349
350 mutable absl::Mutex mu_;
351 const SatParameters& params_;
352 const int num_workers_;
353 const int max_path_depth_;
354 SharedResponseManager* const shared_response_manager_;
355 ClauseIdGenerator clause_id_generator_;
356 std::unique_ptr<LratProofHandler> lrat_proof_handler_;
357
358 // Stores the node id of the root, this is used to handle global restarts.
359 int node_id_offset_ ABSL_GUARDED_BY(mu_) = 0;
360
361 // Stores the nodes in the search tree.
362 std::deque<Node> nodes_ ABSL_GUARDED_BY(mu_);
363 std::deque<Node*> unassigned_leaves_ ABSL_GUARDED_BY(mu_);
364
365 // How many splits we should generate now to keep the desired number of
366 // leaves.
367 int num_splits_wanted_ ABSL_GUARDED_BY(mu_);
368
369 // We limit the total nodes generated per restart to cap the RAM usage and
370 // communication overhead. If we exceed this, workers become portfolio
371 // workers when no unassigned leaves are available.
372 const int max_nodes_;
373 int num_leaves_assigned_since_restart_ ABSL_GUARDED_BY(mu_) = 0;
374
375 // Temporary vectors used to maintain the state of the tree when nodes are
376 // closed and/or children are updated. Each node to close is associated with
377 // the clause stating that the literals of its parents imply the negation of
378 // its own literal (with literals of implied nodes filtered out).
379 std::vector<std::pair<Node*, ClauseId>> to_close_ ABSL_GUARDED_BY(mu_);
380 std::vector<Node*> to_update_ ABSL_GUARDED_BY(mu_);
381
382 int64_t num_restarts_ ABSL_GUARDED_BY(mu_) = 0;
383 int num_closed_nodes_ ABSL_GUARDED_BY(mu_) = 0;
384};
385
387 public:
388 explicit SharedTreeWorker(Model* model);
391
393 const std::function<void()>& feasible_solution_observer);
394
395 private:
396 // Syncs the assigned tree with the local trail, ensuring that any new
397 // implications are synced. This is a noop if the search is deeper than the
398 // assigned tree. Returns false if the problem is unsat.
399 bool SyncWithLocalTrail();
400 bool SyncWithSharedTree();
401 Literal DecodeDecision(ProtoLiteral literal);
402 std::optional<ProtoLiteral> EncodeDecision(Literal decision);
403 bool NextDecision(LiteralIndex* decision_index);
404 void MaybeProposeSplits();
405 bool ShouldReplaceSubtree();
406 bool FinishedMinRestarts() const {
407 return assigned_tree_.MaxLevel() > 0 &&
408 restart_policy_->NumRestarts() >=
409 parameters_->shared_tree_worker_min_restarts_per_subtree();
410 }
411
412 // Add any implications to the clause database for the current level.
413 // Return true if any new information was added.
414 bool AddImplications();
415 bool AddDecisionImplication(Literal literal, int level, ClauseId clause_id);
416
417 void ClearAssignedTreeDecisionsAndImplications();
418
419 // Adds the LRAT inferred clause "assigned tree decisions up to `level` =>
420 // `literal`" if `lrat_proof_handler_` is not null.
421 ClauseId AddLratClauseAndProofForImplication(
422 Literal literal, int level,
423 std::optional<absl::FunctionRef<ClauseId(int, int)>> root_literals = {});
424
425 // Adds the LRAT imported clause "assigned tree decisions up to `level` =>
426 // `literal`" if `lrat_proof_handler_` is not null.
427 ClauseId ImportLratClauseForImplication(Literal literal, int level);
428
429 std::vector<Literal>& DecisionReason(int level);
430
431 bool CheckLratInvariants();
432
433 SatParameters* parameters_;
434 SharedResponseManager* shared_response_;
435 TimeLimit* time_limit_;
436 SharedTreeManager* manager_;
437 CpModelMapping* mapping_;
438 SatSolver* sat_solver_;
439 Trail* trail_;
440 BinaryImplicationGraph* binary_propagator_;
441 ClauseManager* clause_manager_;
442 ClauseIdGenerator* clause_id_generator_;
443 LratProofHandler* lrat_proof_handler_;
444 IntegerTrail* integer_trail_;
445 IntegerEncoder* encoder_;
446 const ObjectiveDefinition* objective_;
447 ModelRandomGenerator* random_;
448 IntegerSearchHelper* helper_;
449 SearchHeuristics* heuristics_;
450 SatDecisionPolicy* decision_policy_;
451 RestartPolicy* restart_policy_;
452 LevelZeroCallbackHelper* level_zero_callbacks_;
453 RevIntRepository* reversible_int_repository_;
454
455 int64_t num_trees_ = 0;
456
457 ProtoTrail assigned_tree_;
458 std::vector<Literal> assigned_tree_decisions_;
459 // The i-th element contains the literals implied by the first i elements of
460 // assigned_tree_decisions_, together with the IDs of the corresponding LRAT
461 // clauses (or kNoClauseId if lrat_proof_handler_ is null).
462 std::vector<std::vector<std::pair<Literal, ClauseId>>>
463 assigned_tree_implications_;
464 double next_split_dtime_ = 0;
465
466 // For each literal on the trail, the ID of the LRAT clause stating that this
467 // literal is implied by the previous decisions on the trail, or kNoClauseId
468 // if there is no such clause.
469 std::vector<ClauseId> trail_implication_clauses_;
470
471 std::vector<ProtoLiteral> tmp_splits_;
472 std::vector<Literal> reason_;
473 // Stores the average LBD of learned clauses for each tree assigned since it
474 // was assigned.
475 // If a tree has worse LBD than the average over the last few trees we replace
476 // the tree.
477 RunningAverage assigned_tree_lbds_;
478 double earliest_replacement_dtime_ = 0;
479
480 // Stores the trail index of the last implication added to assigned_tree_.
481 int reversible_trail_index_ = 0;
482 // Stores the number of implications processed for each level in
483 // assigned_tree_.
484 std::deque<int> rev_num_processed_implications_;
485};
486
487} // namespace operations_research::sat
488
489#endif // ORTOOLS_SAT_WORK_ASSIGNMENT_H_
static std::optional< ProtoLiteral > Encode(Literal, CpModelMapping *, IntegerEncoder *)
friend H AbslHashValue(H h, const ProtoLiteral &literal)
Literal Decode(CpModelMapping *, IntegerEncoder *) const
ProtoLiteral(int var, IntegerValue lb)
bool operator==(const ProtoLiteral &other) const
static std::optional< ProtoLiteral > EncodeLiteral(Literal, CpModelMapping *)
bool operator!=(const ProtoLiteral &other) const
std::vector< ProtoLiteral > TakeTargetPhase()
void SetTargetPhase(std::vector< ProtoLiteral > phase)
ProtoLiteral Decision(int level) const
bool AddPhase(const ProtoLiteral &lit)
void SetObjectiveLb(int level, IntegerValue objective_lb)
bool IsAssigned(const ProtoLiteral &lit) const
absl::Span< const ProtoLiteral > Implications(int level) const
absl::Span< const int > NodeIds(int level) const
IntegerValue ObjectiveLb(int level) const
void PushLevel(const ProtoLiteral &decision, IntegerValue objective_lb, int node_id)
absl::Span< const ProtoLiteral > Literals() const
const std::vector< ProtoLiteral > & TargetPhase() const
bool AddImplication(int level, ProtoLiteral implication)
void CloseTree(ProtoTrail &path, int level)
SharedTreeManager(const SharedTreeManager &)=delete
bool SyncTree(ProtoTrail &path) ABSL_LOCKS_EXCLUDED(mu_)
int NumNodes() const ABSL_LOCKS_EXCLUDED(mu_)
int TrySplitTree(absl::Span< const ProtoLiteral > decisions, ProtoTrail &path) 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())
constexpr ClauseId kNoClauseId(0)