From a8e18b60cf71c44de3abbce1bc709f1f9e0ac1d2 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 00:50:29 +0100 Subject: [PATCH 01/15] Add external worker control API --- Makefile.in | 4 +- cmake/GecodeSources.cmake | 1 + gecode/search.hh | 51 ++++++ gecode/search/engine.cpp | 13 ++ gecode/search/engine.hpp | 7 - gecode/search/exception.cpp | 9 ++ gecode/search/exception.hpp | 18 +++ gecode/search/options.hpp | 1 + gecode/search/par/engine.hpp | 3 +- gecode/search/support.hh | 2 +- gecode/search/worker-control.cpp | 155 ++++++++++++++++++ gecode/search/worker-control.hh | 53 +++++++ test/search.cpp | 264 +++++++++++++++++++++++++++++++ 13 files changed, 570 insertions(+), 11 deletions(-) create mode 100644 gecode/search/worker-control.cpp create mode 100644 gecode/search/worker-control.hh diff --git a/Makefile.in b/Makefile.in index b446405125..43a330d5d3 100755 --- a/Makefile.in +++ b/Makefile.in @@ -266,10 +266,10 @@ SEARCHSRC0 = \ dfs bab lds \ seq/rbs seq/dead seq/pbs par/pbs \ rbs pbs nogoods exception tracer \ - cpprofiler/tracer + cpprofiler/tracer worker-control SEARCHHDR0 = \ statistics.hpp stop.hpp options.hpp cutoff.hpp \ - support.hh worker.hh exception.hpp engine.hpp base.hpp \ + support.hh worker.hh worker-control.hh exception.hpp engine.hpp base.hpp \ nogoods.hh nogoods.hpp build.hpp traits.hpp sebs.hpp \ seq/path.hh seq/path.hpp seq/dfs.hh seq/dfs.hpp \ seq/bab.hh seq/bab.hpp seq/lds.hh seq/lds.hpp \ diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index 0c9defc0f1..c0d389f116 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -48,6 +48,7 @@ set(GECODE_SEARCH_SOURCES gecode/search/seq/rbs.cpp gecode/search/stop.cpp gecode/search/tracer.cpp + gecode/search/worker-control.cpp ) set(GECODE_INT_SOURCES diff --git a/gecode/search.hh b/gecode/search.hh index 38668804ad..67019b3ff3 100755 --- a/gecode/search.hh +++ b/gecode/search.hh @@ -709,6 +709,43 @@ namespace Gecode { namespace Search { namespace Gecode { namespace Search { + class WorkerControlAccess; + + /** + * \brief External control for the requested number of search workers + * + * A worker control is a copyable handle. Copies share the same request + * and can be used concurrently. The worker capacity is fixed when the + * handle is first attached to a search engine. + * + * \ingroup TaskModelSearch + */ + class GECODE_SEARCH_EXPORT WorkerControl { + private: + class State; + State* state; + friend class WorkerControlAccess; + public: + /// Construct an empty handle + WorkerControl(void) noexcept; + /// Construct an engaged handle with initial request \a requested + explicit WorkerControl(unsigned int requested); + /// Copy constructor + WorkerControl(const WorkerControl& control); + /// Assignment operator + WorkerControl& operator =(const WorkerControl& control); + /// Destructor + ~WorkerControl(void); + /// Whether this handle is engaged + explicit operator bool(void) const noexcept; + /// Return the requested number of workers, or zero for an empty handle + unsigned int requested(void) const noexcept; + /// Request \a workers workers + void request(unsigned int workers); + /// Return the fixed worker capacity, or zero before attachment + unsigned int capacity(void) const noexcept; + }; + class Stop; /** @@ -754,6 +791,13 @@ namespace Gecode { namespace Search { bool clone; /// Number of threads to use double threads; + /** + * External worker control + * + * After option expansion, \a threads is the immutable worker capacity. + * Requests through this handle never change \a threads. + */ + WorkerControl worker_control; /// Create a clone after every \a c_d commits (commit distance) unsigned int c_d; /// Create a clone during recomputation if distance is greater than \a a_d (adaptive distance) @@ -941,6 +985,13 @@ namespace Gecode { namespace Search { * \brief %Search engine implementation interface */ class GECODE_SEARCH_EXPORT Engine : public HeapAllocated { + protected: + /// Control retained for the lifetime of a leaf engine + WorkerControl worker_control; + /// Construct a meta engine without worker control + Engine(void); + /// Construct a leaf engine and bind worker control to \a capacity + Engine(const Options& o, unsigned int capacity); public: /// Return next solution (nullptr, if none exists or search has been stopped) virtual Space* next(void) = 0; diff --git a/gecode/search/engine.cpp b/gecode/search/engine.cpp index ac62b44484..e7551ddcdf 100644 --- a/gecode/search/engine.cpp +++ b/gecode/search/engine.cpp @@ -32,9 +32,18 @@ */ #include +#include namespace Gecode { namespace Search { + Engine::Engine(void) + : worker_control() {} + + Engine::Engine(const Options& o, unsigned int capacity) + : worker_control(o.worker_control) { + WorkerControlAccess::attach(worker_control,capacity); + } + void Engine::constrain(const Space& b) { (void) b; @@ -49,6 +58,10 @@ namespace Gecode { namespace Search { return NoGoods::eng; } + Engine::~Engine(void) { + WorkerControlAccess::detach(worker_control); + } + }} // STATISTICS: search-other diff --git a/gecode/search/engine.hpp b/gecode/search/engine.hpp index a7003217c2..5eba732c3a 100644 --- a/gecode/search/engine.hpp +++ b/gecode/search/engine.hpp @@ -31,11 +31,4 @@ * */ -namespace Gecode { namespace Search { - - forceinline - Engine::~Engine(void) {} - -}} - // STATISTICS: search-other diff --git a/gecode/search/exception.cpp b/gecode/search/exception.cpp index eb95353ad8..773262f828 100644 --- a/gecode/search/exception.cpp +++ b/gecode/search/exception.cpp @@ -48,6 +48,15 @@ namespace Gecode { namespace Search { NoBest::NoBest(const char* l) : Exception(l,"Best solution search is not supported") {} + InvalidWorkerRequest::InvalidWorkerRequest(const char* l) + : Exception(l,"Invalid number of requested search workers") {} + + UninitializedWorkerControl::UninitializedWorkerControl(const char* l) + : Exception(l,"Worker control is not initialized") {} + + WorkerControlInUse::WorkerControlInUse(const char* l) + : Exception(l,"Worker control has already been bound") {} + }} // STATISTICS: search-other diff --git a/gecode/search/exception.hpp b/gecode/search/exception.hpp index 01ff8cbd50..13bdda7748 100644 --- a/gecode/search/exception.hpp +++ b/gecode/search/exception.hpp @@ -62,6 +62,24 @@ namespace Gecode { namespace Search { /// Initialize with location \a l NoBest(const char* l); }; + /// %Exception: Invalid requested number of search workers + class GECODE_SEARCH_EXPORT InvalidWorkerRequest : public Exception { + public: + /// Initialize with location \a l + InvalidWorkerRequest(const char* l); + }; + /// %Exception: Request through an empty worker control + class GECODE_SEARCH_EXPORT UninitializedWorkerControl : public Exception { + public: + /// Initialize with location \a l + UninitializedWorkerControl(const char* l); + }; + /// %Exception: Worker control is already bound to a search engine + class GECODE_SEARCH_EXPORT WorkerControlInUse : public Exception { + public: + /// Initialize with location \a l + WorkerControlInUse(const char* l); + }; //@} }} diff --git a/gecode/search/options.hpp b/gecode/search/options.hpp index b85d969008..bb442c6786 100644 --- a/gecode/search/options.hpp +++ b/gecode/search/options.hpp @@ -37,6 +37,7 @@ namespace Gecode { namespace Search { Options::Options(void) : clone(Config::clone), threads(Config::threads), + worker_control(), c_d(Config::c_d), a_d(Config::a_d), d_l(Config::d_l), assets(0), slice(Config::slice), nogoods_limit(0), diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index 8ff5ed66e9..69cef3cf4b 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -118,7 +118,8 @@ namespace Gecode { namespace Search { namespace Par { template forceinline Engine::Engine(const Options& o) - : _opt(o), _cmd(C_WAIT), solutions(heap) { + : Search::Engine(o,static_cast(o.threads)), + _opt(o), _cmd(C_WAIT), solutions(heap) { // Initialize termination information _n_term_not_ack = workers(); _n_not_terminated = workers(); diff --git a/gecode/search/support.hh b/gecode/search/support.hh index 1d0fc78172..fe25d487b2 100644 --- a/gecode/search/support.hh +++ b/gecode/search/support.hh @@ -75,7 +75,7 @@ namespace Gecode { namespace Search { template WorkerToEngine::WorkerToEngine(Space* s, const Options& o) - : w(s,o) {} + : Engine(o,1U), w(s,o) {} template Space* WorkerToEngine::next(void) { diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp new file mode 100644 index 0000000000..3d2c7aaa03 --- /dev/null +++ b/gecode/search/worker-control.cpp @@ -0,0 +1,155 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +namespace Gecode { namespace Search { + + class WorkerControl::State : public Support::RefCount { + public: + enum Lifecycle { + NEVER_BOUND, + ATTACHED, + DETACHED + }; + + std::atomic requested; + std::atomic capacity; + std::atomic generation; + Support::Mutex mutex; + Lifecycle lifecycle; + + State(unsigned int workers) + : RefCount(1U), requested(workers), capacity(0U), generation(0U), + lifecycle(NEVER_BOUND) {} + }; + + WorkerControl::WorkerControl(void) noexcept + : state(nullptr) {} + + WorkerControl::WorkerControl(unsigned int requested) + : state(nullptr) { + if (requested == 0U) + throw InvalidWorkerRequest("WorkerControl::WorkerControl"); + state = new State(requested); + } + + WorkerControl::WorkerControl(const WorkerControl& control) + : state(control.state) { + if (state != nullptr) + state->inc(); + } + + WorkerControl& + WorkerControl::operator =(const WorkerControl& control) { + if (control.state != nullptr) + control.state->inc(); + if ((state != nullptr) && state->dec()) + delete state; + state = control.state; + return *this; + } + + WorkerControl::~WorkerControl(void) { + if ((state != nullptr) && state->dec()) + delete state; + } + + WorkerControl::operator bool(void) const noexcept { + return state != nullptr; + } + + unsigned int + WorkerControl::requested(void) const noexcept { + return (state == nullptr) ? 0U : + state->requested.load(std::memory_order_acquire); + } + + void + WorkerControl::request(unsigned int workers) { + if (state == nullptr) + throw UninitializedWorkerControl("WorkerControl::request"); + if (workers == 0U) + throw InvalidWorkerRequest("WorkerControl::request"); + + Support::Lock lock(state->mutex); + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + if ((capacity != 0U) && (workers > capacity)) + throw InvalidWorkerRequest("WorkerControl::request"); + unsigned int old = + state->requested.load(std::memory_order_relaxed); + if (old != workers) { + state->requested.store(workers,std::memory_order_release); + (void) state->generation.fetch_add(1U,std::memory_order_release); + } + } + + unsigned int + WorkerControl::capacity(void) const noexcept { + return (state == nullptr) ? 0U : + state->capacity.load(std::memory_order_acquire); + } + + void + WorkerControlAccess::attach(WorkerControl& control, + unsigned int capacity) { + if (control.state == nullptr) + return; + WorkerControl::State* state = control.state; + Support::Lock lock(state->mutex); + if (state->lifecycle != WorkerControl::State::NEVER_BOUND) + throw WorkerControlInUse("WorkerControlAccess::attach"); + if ((capacity == 0U) || + (state->requested.load(std::memory_order_relaxed) > capacity)) + throw InvalidWorkerRequest("WorkerControlAccess::attach"); + state->capacity.store(capacity,std::memory_order_release); + state->lifecycle = WorkerControl::State::ATTACHED; + } + + void + WorkerControlAccess::detach(WorkerControl& control) { + if (control.state == nullptr) + return; + WorkerControl::State* state = control.state; + Support::Lock lock(state->mutex); + if (state->lifecycle == WorkerControl::State::ATTACHED) + state->lifecycle = WorkerControl::State::DETACHED; + } + + unsigned long long int + WorkerControlAccess::generation(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->generation.load(std::memory_order_acquire); + } + +}} diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh new file mode 100644 index 0000000000..4d7a72e640 --- /dev/null +++ b/gecode/search/worker-control.hh @@ -0,0 +1,53 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef GECODE_SEARCH_WORKER_CONTROL_HH +#define GECODE_SEARCH_WORKER_CONTROL_HH + +#include + +namespace Gecode { namespace Search { + + /// Private access to worker-control attachment state + class WorkerControlAccess { + public: + /// Attach \a control to a leaf engine with fixed \a capacity + static void attach(WorkerControl& control, unsigned int capacity); + /// Detach \a control from its leaf engine + static void detach(WorkerControl& control); + /// Return the request generation + static unsigned long long int generation(const WorkerControl& control); + }; + +}} + +#endif diff --git a/test/search.cpp b/test/search.cpp index 48e0bdfdcb..df89d78bbb 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -40,7 +40,10 @@ #include "test/test.hh" +#include +#include #include +#include static_assert(std::is_copy_constructible::value, "NoGoods must remain copy constructible"); @@ -62,6 +65,16 @@ static_assert(std::is_copy_constructible::value, "RestartStop must remain copy constructible"); static_assert(std::is_copy_assignable::value, "RestartStop must remain copy assignable"); +static_assert( + std::is_copy_constructible::value, + "WorkerControl must remain copy constructible"); +static_assert( + std::is_copy_assignable::value, + "WorkerControl must remain copy assignable"); +static_assert(std::is_copy_constructible::value, + "Search::Options must remain copy constructible"); +static_assert(std::is_copy_assignable::value, + "Search::Options must remain copy assignable"); namespace Test { @@ -335,6 +348,257 @@ namespace Test { } }; + /// Focused tests for external worker control + class WorkerControl : public Base { + private: + enum Scenario { + VALUES, + ERRORS, + BINDING, + CONCURRENT, + COMPATIBILITY + } scenario; + + template + static bool throws(Function function) { + try { + function(); + } catch (const Exception&) { + return true; + } catch (...) { + return false; + } + return false; + } + + static unsigned int capacity(void) { +#ifdef GECODE_HAS_THREADS + return 4U; +#else + return 1U; +#endif + } + + static Gecode::Search::Engine* + dfs_engine(Gecode::Search::Options& o) { + HasSolutions* m = + new HasSolutions(HTB_BINARY,HTB_BINARY,HTB_BINARY); + Gecode::Search::Engine* e; + try { + e = Gecode::Search::dfsengine(m,o); + } catch (...) { + delete m; + throw; + } + delete m; + return e; + } + + static bool values(void) { + Gecode::Search::WorkerControl empty; + Gecode::Search::Options defaults; + if (empty || (empty.requested() != 0U) || + (empty.capacity() != 0U) || defaults.worker_control) + return false; + + Gecode::Search::WorkerControl control(3U); + Gecode::Search::WorkerControl copy(control); + Gecode::Search::WorkerControl assigned; + assigned = copy; + Gecode::Search::Options o; + o.worker_control = assigned; + Gecode::Search::Options copied_options(o); + copy.request(2U); + return control && (control.requested() == 2U) && + (assigned.requested() == 2U) && + (o.worker_control.requested() == 2U) && + (copied_options.worker_control.requested() == 2U) && + (control.capacity() == 0U); + } + + static bool errors(void) { + if (!throws([] { + Gecode::Search::WorkerControl invalid(0U); + (void) invalid; + })) + return false; + Gecode::Search::WorkerControl empty; + if (!throws( + [&] { empty.request(1U); })) + return false; + Gecode::Search::WorkerControl unbound(1U); + if (!throws( + [&] { unbound.request(0U); })) + return false; + + Gecode::Search::Options too_many; + too_many.threads = 1.0; + too_many.worker_control = + Gecode::Search::WorkerControl(2U); + if (!throws([&] { + Gecode::Search::Engine* e = dfs_engine(too_many); + delete e; + })) + return false; + + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = Gecode::Search::WorkerControl(1U); + Gecode::Search::Engine* e = dfs_engine(o); + bool exact = throws( + [&] { o.worker_control.request(capacity()+1U); }); + delete e; + return exact; + } + + static bool binding(void) { + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = Gecode::Search::WorkerControl(1U); + Gecode::Search::Options copied(o); + Gecode::Search::Engine* first = dfs_engine(o); + bool ok = (o.threads == 4.0) && (copied.threads == 4.0) && + (o.worker_control.capacity() == capacity()) && + (copied.worker_control.capacity() == capacity()); + bool while_live = throws([&] { + Gecode::Search::Engine* e = dfs_engine(copied); + delete e; + }); + delete first; + bool after_destroy = throws([&] { + Gecode::Search::Engine* e = dfs_engine(copied); + delete e; + }); + o.worker_control.request(1U); + + Gecode::Search::Options sequential_dfs; + sequential_dfs.threads = 1.0; + sequential_dfs.worker_control = + Gecode::Search::WorkerControl(1U); + Gecode::Search::Engine* dfs = dfs_engine(sequential_dfs); + ok = ok && (sequential_dfs.worker_control.capacity() == 1U); + delete dfs; + + Gecode::Search::Options sequential_bab; + sequential_bab.threads = 1.0; + sequential_bab.worker_control = + Gecode::Search::WorkerControl(1U); + HasSolutions* m = new HasSolutions( + HTB_BINARY,HTB_BINARY,HTB_BINARY,HTC_LEX_LE); + Gecode::Search::Engine* bab = + Gecode::Search::babengine(m,sequential_bab); + delete m; + ok = ok && (sequential_bab.worker_control.capacity() == 1U); + delete bab; + + return ok && while_live && after_destroy && + (copied.worker_control.requested() == 1U); + } + + static bool concurrent(void) { + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = Gecode::Search::WorkerControl(1U); + Gecode::Search::Engine* e = dfs_engine(o); +#ifdef GECODE_HAS_THREADS + std::vector writers; + for (unsigned int writer=0U; writer<4U; writer++) { + Gecode::Search::WorkerControl copy(o.worker_control); + writers.emplace_back([copy,writer] () mutable { + for (unsigned int i=0U; i<2000U; i++) + copy.request(1U + ((i+writer) % 4U)); + }); + } + for (std::thread& writer : writers) + writer.join(); + + std::atomic started(false); + Gecode::Search::WorkerControl copy(o.worker_control); + std::thread during_destruction([copy,&started] () mutable { + started.store(true,std::memory_order_release); + for (unsigned int i=0U; i<10000U; i++) + copy.request(1U + (i % 4U)); + }); + while (!started.load(std::memory_order_acquire)) {} + delete e; + during_destruction.join(); +#else + o.worker_control.request(1U); + delete e; +#endif + o.worker_control.request(1U); + return (o.worker_control.capacity() == capacity()) && + (o.worker_control.requested() == 1U); + } + + static int dfs_solutions(const Gecode::Search::WorkerControl& control) { + HasSolutions* m = + new HasSolutions(HTB_BINARY,HTB_BINARY,HTB_BINARY); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + Gecode::DFS dfs(m,o); + delete m; + int solutions = 0; + while (HasSolutions* solution = dfs.next()) { + solutions++; + delete solution; + } + return solutions; + } + + static bool bab_best(const Gecode::Search::WorkerControl& control) { + HasSolutions* m = new HasSolutions( + HTB_BINARY,HTB_BINARY,HTB_BINARY,HTC_LEX_LE); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + Gecode::BAB bab(m,o); + delete m; + HasSolutions* best = nullptr; + while (HasSolutions* solution = bab.next()) { + delete best; + best = solution; + } + bool ok = (best != nullptr) && best->best(); + delete best; + return ok; + } + + static bool compatibility(void) { + Gecode::Search::WorkerControl empty; + Gecode::Search::WorkerControl dfs_control(capacity()); + Gecode::Search::WorkerControl bab_control(capacity()); + return (dfs_solutions(empty) == 8) && + (dfs_solutions(dfs_control) == 8) && + bab_best(empty) && bab_best(bab_control); + } + + public: + WorkerControl(const std::string& name, Scenario s) + : Base("Search::WorkerControl::"+name), scenario(s) {} + + bool run(void) override { + switch (scenario) { + case VALUES: return values(); + case ERRORS: return errors(); + case BINDING: return binding(); + case CONCURRENT: return concurrent(); + case COMPATIBILITY: return compatibility(); + default: GECODE_NEVER; + } + return false; + } + + static void create(void) { + (void) new WorkerControl("Values",VALUES); + (void) new WorkerControl("Errors",ERRORS); + (void) new WorkerControl("Binding",BINDING); + (void) new WorkerControl("Concurrent",CONCURRENT); + (void) new WorkerControl("Compatibility",COMPATIBILITY); + } + }; + /// %Base class for search tests class Test : public Base { public: From 6ee536c629a9c2dc3b4d6cba0b8ffd8389be6b1a Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 01:33:29 +0100 Subject: [PATCH 02/15] Adjust active DFS workers during search --- gecode/search/par/dfs.hh | 8 +- gecode/search/par/dfs.hpp | 30 ++++- gecode/search/par/engine.hh | 48 +++++++ gecode/search/par/engine.hpp | 222 ++++++++++++++++++++++++++++++- gecode/search/worker-control.cpp | 188 ++++++++++++++++++++++++-- gecode/search/worker-control.hh | 35 ++++- test/search.cpp | 173 +++++++++++++++++++++++- 7 files changed, 678 insertions(+), 26 deletions(-) diff --git a/gecode/search/par/dfs.hh b/gecode/search/par/dfs.hh index ead09e2c90..326db42664 100644 --- a/gecode/search/par/dfs.hh +++ b/gecode/search/par/dfs.hh @@ -76,16 +76,20 @@ namespace Gecode { namespace Search { namespace Par { using Engine::Worker::start; using Engine::Worker::tracer; using Engine::Worker::stop; + /// Fixed worker index + unsigned int index; /// Initialize for space \a s with engine \a e - Worker(Space* s, DFS& e); + Worker(Space* s, DFS& e, unsigned int index); /// Provide access to engine DFS& engine(void) const; /// Start execution of worker virtual void run(void); /// Try to find some work - void find(void); + bool find(void); /// Reset worker to restart at space \a s void reset(Space* s, unsigned int ngdl); + /// Whether this worker currently owns local search + bool owns_work(void) const; }; /// Array of worker references Worker** _worker; diff --git a/gecode/search/par/dfs.hpp b/gecode/search/par/dfs.hpp index 2108b0ee75..d50e771462 100755 --- a/gecode/search/par/dfs.hpp +++ b/gecode/search/par/dfs.hpp @@ -53,8 +53,8 @@ namespace Gecode { namespace Search { namespace Par { */ template forceinline - DFS::Worker::Worker(Space* s, DFS& e) - : Engine::Worker(s,e) {} + DFS::Worker::Worker(Space* s, DFS& e, unsigned int index0) + : Engine::Worker(s,e), index(index0) {} template forceinline DFS::DFS(Space* s, const Options& o) @@ -65,10 +65,11 @@ namespace Gecode { namespace Search { namespace Par { _worker = static_cast (heap.ralloc(workers() * sizeof(Worker*))); // The first worker gets the entire search tree - _worker[0] = new Worker(s,*this); + _worker[0] = new Worker(s,*this,0U); // All other workers start with no work for (unsigned int i=1; ischeduler_enable(_worker[0]->owns_work()); // Block all workers block(); // Create and start threads @@ -97,6 +98,12 @@ namespace Gecode { namespace Search { namespace Par { Search::Worker::reset(); } + template + forceinline bool + DFS::Worker::owns_work(void) const { + return (cur != nullptr) || !path.empty(); + } + /* * Engine: search control @@ -118,7 +125,7 @@ namespace Gecode { namespace Search { namespace Par { * Worker: finding and stealing working */ template - forceinline void + forceinline bool DFS::Worker::find(void) { // Try to find new work (even if there is none) for (unsigned int i=0U; ischeduler_reset(worker(0U)->owns_work()); } diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index e3b59077be..566164c478 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -86,11 +87,56 @@ namespace Gecode { namespace Search { namespace Par { }; /// Search options Options _opt; + /// Logical worker state used by adjustable DFS admission + enum SchedulerLogical { + SL_OWNER, + SL_PENDING, + SL_IDLE + }; + /// Per-worker adjustable DFS admission state + struct SchedulerWorker { + bool lease; + bool parked; + SchedulerLogical logical; + }; + /// Whether adjustable DFS admission is enabled + bool scheduler_enabled; + /// Mutex for adjustable DFS admission + Support::Mutex scheduler_mutex; + /// Per-worker adjustable DFS admission state + SchedulerWorker* scheduler_worker; + /// Current requested lease count + unsigned int scheduler_requested; + /// Current lease count + unsigned int scheduler_leases; + /// Round-robin cursor for lease handoff + unsigned int scheduler_cursor; + /// Last request generation reconciled by the scheduler + std::atomic scheduler_generation; + /// Select a no-lease worker by logical state + unsigned int scheduler_select(SchedulerLogical logical, + unsigned int exclude) const; + /// Grant leases up to the current request + bool scheduler_grow(void); + /// Publish scheduler state for focused tests + void scheduler_observe(void); public: /// Provide access to search options const Options& opt(void) const; /// Return number of workers unsigned int workers(void) const; + /// Enable adjustable DFS admission + void scheduler_enable(bool root_owner); + /// Reset adjustable DFS state while all workers are blocked + void scheduler_reset(bool root_owner); + /// Admit worker \a worker for one DFS exploration action + bool scheduler_admit(unsigned int worker); + /// Record that worker \a worker owns search + void scheduler_owner(unsigned int worker); + /// Record that worker \a worker is idle + void scheduler_idle(unsigned int worker); + /// Hand worker \a worker's lease to a parked logical worker + void scheduler_handoff(unsigned int worker, bool work_remains); /// \name Commands from engine to workers and wait management //@{ @@ -187,6 +233,8 @@ namespace Gecode { namespace Search { namespace Par { void busy(void); /// Report that worker has been stopped void stop(void); + /// Whether logical search work remains + bool work_remains(void); //@} /// \name Engine interface diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index 69cef3cf4b..f01a9236e2 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -82,6 +82,9 @@ namespace Gecode { namespace Search { namespace Par { forceinline void Engine::release(Cmd c) { _cmd.store(c, std::memory_order_release); + if (scheduler_enabled && ((c == C_WORK) || (c == C_RESET) || + (c == C_TERMINATE))) + WorkerControlAccess::signal_all(worker_control); Support::Thread::releaseGlobalMutex(&_m_wait); } template @@ -119,7 +122,10 @@ namespace Gecode { namespace Search { namespace Par { forceinline Engine::Engine(const Options& o) : Search::Engine(o,static_cast(o.threads)), - _opt(o), _cmd(C_WAIT), solutions(heap) { + _opt(o), scheduler_enabled(false), + scheduler_worker(nullptr), scheduler_requested(workers()), + scheduler_leases(0U), scheduler_cursor(0U), scheduler_generation(0U), + _cmd(C_WAIT), solutions(heap) { // Initialize termination information _n_term_not_ack = workers(); _n_not_terminated = workers(); @@ -130,6 +136,209 @@ namespace Gecode { namespace Search { namespace Par { _n_reset_not_ack = workers(); } + template + unsigned int + Engine::scheduler_select(SchedulerLogical logical, + unsigned int exclude) const { + for (unsigned int offset=0U; offset + bool + Engine::scheduler_grow(void) { + bool wake = false; + while (scheduler_leases < scheduler_requested) { + unsigned int i = scheduler_select(SL_OWNER,workers()); + if (i == workers()) + i = scheduler_select(SL_PENDING,workers()); + if (i == workers()) + i = scheduler_select(SL_IDLE,workers()); + if (i == workers()) { + for (i=0U; i + void + Engine::scheduler_observe(void) { + unsigned int parked = 0U; + unsigned int owners = 0U; + unsigned int parked_owners = 0U; + for (unsigned int i=0U; i + void + Engine::scheduler_enable(bool root_owner) { + if (!WorkerControlAccess::engaged(worker_control)) + return; + scheduler_worker = static_cast + (heap.ralloc(workers() * sizeof(SchedulerWorker))); + unsigned long long int generation; + WorkerControlAccess::snapshot( + worker_control,scheduler_requested,generation); + scheduler_leases = scheduler_requested; + for (unsigned int i=0U; i + void + Engine::scheduler_reset(bool root_owner) { + if (!scheduler_enabled) + return; + unsigned int requested; + unsigned long long int generation; + WorkerControlAccess::snapshot(worker_control,requested,generation); + scheduler_mutex.acquire(); + scheduler_requested = requested; + scheduler_leases = scheduler_requested; + scheduler_cursor = 0U; + for (unsigned int i=0U; i + bool + Engine::scheduler_admit(unsigned int worker) { + if (!scheduler_enabled) + return true; + while (cmd() == C_WORK) { + unsigned long long int generation = + WorkerControlAccess::generation(worker_control); + if ((generation == + scheduler_generation.load(std::memory_order_acquire)) && + (WorkerControlAccess::requested(worker_control) == workers())) + return true; + + bool wake; + bool admitted; + unsigned int requested; + WorkerControlAccess::snapshot(worker_control,requested,generation); + scheduler_mutex.acquire(); + if (generation > + scheduler_generation.load(std::memory_order_relaxed)) { + scheduler_requested = requested; + scheduler_generation.store(generation,std::memory_order_release); + } + wake = scheduler_grow(); + if (scheduler_worker[worker].lease && + (scheduler_leases > scheduler_requested)) { + scheduler_worker[worker].lease = false; + scheduler_leases--; + } + admitted = scheduler_worker[worker].lease; + scheduler_worker[worker].parked = !admitted; + scheduler_observe(); + scheduler_mutex.release(); + + if (wake) + WorkerControlAccess::signal_all(worker_control); + if (admitted) + return true; + WorkerControlAccess::wait(worker_control,worker); + } + return false; + } + + template + void + Engine::scheduler_owner(unsigned int worker) { + if (!scheduler_enabled) + return; + scheduler_mutex.acquire(); + scheduler_worker[worker].logical = SL_OWNER; + scheduler_observe(); + scheduler_mutex.release(); + } + + template + void + Engine::scheduler_idle(unsigned int worker) { + if (!scheduler_enabled) + return; + scheduler_mutex.acquire(); + scheduler_worker[worker].logical = SL_IDLE; + scheduler_observe(); + scheduler_mutex.release(); + } + + template + void + Engine::scheduler_handoff(unsigned int worker, + bool work_remains) { + if (!scheduler_enabled) + return; + unsigned int target = workers(); + scheduler_mutex.acquire(); + if (scheduler_worker[worker].lease) { + if (scheduler_leases > scheduler_requested) { + scheduler_worker[worker].lease = false; + scheduler_worker[worker].parked = true; + scheduler_leases--; + } else if (work_remains) { + target = scheduler_select(SL_OWNER,worker); + if (target == workers()) + target = scheduler_select(SL_PENDING,worker); + if (target != workers()) { + scheduler_worker[worker].lease = false; + scheduler_worker[worker].parked = true; + scheduler_worker[target].lease = true; + scheduler_worker[target].parked = false; + scheduler_cursor = (target+1U) % workers(); + WorkerControlAccess::handoff(worker_control); + } + } + } + scheduler_observe(); + scheduler_mutex.release(); + if (target != workers()) + WorkerControlAccess::signal(worker_control,target); + } /* * Statistics @@ -184,6 +393,15 @@ namespace Gecode { namespace Search { namespace Par { m_search.release(); } + template + forceinline bool + Engine::work_remains(void) { + m_search.acquire(); + bool remains = n_busy > 0; + m_search.release(); + return remains; + } + /* * Engine: termination control @@ -372,6 +590,8 @@ namespace Gecode { namespace Search { namespace Par { Engine::~Engine(void) { while (!solutions.empty()) delete solutions.pop(); + if (scheduler_worker != nullptr) + heap.rfree(scheduler_worker); } }}} diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 3d2c7aaa03..80db9e4703 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -45,12 +45,24 @@ namespace Gecode { namespace Search { std::atomic requested; std::atomic capacity; std::atomic generation; + std::atomic observed_generation; + std::atomic handoffs; + std::atomic leases; + std::atomic parked; + std::atomic owners; + std::atomic parked_owners; Support::Mutex mutex; Lifecycle lifecycle; + Support::Event* events; State(unsigned int workers) : RefCount(1U), requested(workers), capacity(0U), generation(0U), - lifecycle(NEVER_BOUND) {} + observed_generation(0U), handoffs(0U), leases(0U), parked(0U), + owners(0U), parked_owners(0U), + lifecycle(NEVER_BOUND), events(nullptr) {} + ~State(void) { + delete [] events; + } }; WorkerControl::WorkerControl(void) noexcept @@ -101,17 +113,23 @@ namespace Gecode { namespace Search { if (workers == 0U) throw InvalidWorkerRequest("WorkerControl::request"); - Support::Lock lock(state->mutex); - unsigned int capacity = - state->capacity.load(std::memory_order_relaxed); - if ((capacity != 0U) && (workers > capacity)) - throw InvalidWorkerRequest("WorkerControl::request"); - unsigned int old = - state->requested.load(std::memory_order_relaxed); - if (old != workers) { - state->requested.store(workers,std::memory_order_release); - (void) state->generation.fetch_add(1U,std::memory_order_release); + bool changed = false; + { + Support::Lock lock(state->mutex); + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + if ((capacity != 0U) && (workers > capacity)) + throw InvalidWorkerRequest("WorkerControl::request"); + unsigned int old = + state->requested.load(std::memory_order_relaxed); + if (old != workers) { + state->requested.store(workers,std::memory_order_release); + (void) state->generation.fetch_add(1U,std::memory_order_release); + changed = true; + } } + if (changed) + WorkerControlAccess::signal_all(*this); } unsigned int @@ -132,6 +150,7 @@ namespace Gecode { namespace Search { if ((capacity == 0U) || (state->requested.load(std::memory_order_relaxed) > capacity)) throw InvalidWorkerRequest("WorkerControlAccess::attach"); + state->events = new Support::Event[capacity]; state->capacity.store(capacity,std::memory_order_release); state->lifecycle = WorkerControl::State::ATTACHED; } @@ -141,9 +160,12 @@ namespace Gecode { namespace Search { if (control.state == nullptr) return; WorkerControl::State* state = control.state; - Support::Lock lock(state->mutex); - if (state->lifecycle == WorkerControl::State::ATTACHED) - state->lifecycle = WorkerControl::State::DETACHED; + { + Support::Lock lock(state->mutex); + if (state->lifecycle == WorkerControl::State::ATTACHED) + state->lifecycle = WorkerControl::State::DETACHED; + } + signal_all(control); } unsigned long long int @@ -152,4 +174,142 @@ namespace Gecode { namespace Search { control.state->generation.load(std::memory_order_acquire); } + bool + WorkerControlAccess::engaged(const WorkerControl& control) { + return control.state != nullptr; + } + + unsigned int + WorkerControlAccess::requested(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->requested.load(std::memory_order_acquire); + } + + void + WorkerControlAccess::snapshot(const WorkerControl& control, + unsigned int& requested, + unsigned long long int& generation) { + WorkerControl::State* state = control.state; + if (state == nullptr) { + requested = 0U; + generation = 0U; + return; + } + Support::Lock lock(state->mutex); + requested = state->requested.load(std::memory_order_relaxed); + generation = state->generation.load(std::memory_order_relaxed); + } + + void + WorkerControlAccess::wait(WorkerControl& control, unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + Support::Event* event = nullptr; + { + Support::Lock lock(state->mutex); + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + assert((state->events == nullptr) || (worker < capacity)); + if ((state->events != nullptr) && (worker < capacity)) + event = &state->events[worker]; + } + if (event != nullptr) + event->wait(); + } + + void + WorkerControlAccess::signal(WorkerControl& control, unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + Support::Event* event = nullptr; + { + Support::Lock lock(state->mutex); + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + assert((state->events == nullptr) || (worker < capacity)); + if ((state->events != nullptr) && (worker < capacity)) + event = &state->events[worker]; + } + if (event != nullptr) + event->signal(); + } + + void + WorkerControlAccess::signal_all(WorkerControl& control) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + Support::Event* events; + unsigned int capacity; + { + Support::Lock lock(state->mutex); + events = state->events; + capacity = state->capacity.load(std::memory_order_relaxed); + } + if (events == nullptr) + return; + for (unsigned int i=0U; ileases.store(leases,std::memory_order_release); + state->parked.store(parked,std::memory_order_release); + state->owners.store(owners,std::memory_order_release); + state->parked_owners.store(parked_owners,std::memory_order_release); + state->observed_generation.store(generation,std::memory_order_release); + } + + void + WorkerControlAccess::handoff(WorkerControl& control) { + if (control.state != nullptr) + (void) control.state->handoffs.fetch_add(1U,std::memory_order_relaxed); + } + + unsigned long long int + WorkerControlAccess::observed_generation(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->observed_generation.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::leases(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->leases.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::parked(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->parked.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::owners(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->owners.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::parked_owners(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->parked_owners.load(std::memory_order_acquire); + } + + unsigned long long int + WorkerControlAccess::handoffs(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->handoffs.load(std::memory_order_acquire); + } + }} diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh index 4d7a72e640..1484e76f2f 100644 --- a/gecode/search/worker-control.hh +++ b/gecode/search/worker-control.hh @@ -38,7 +38,7 @@ namespace Gecode { namespace Search { /// Private access to worker-control attachment state - class WorkerControlAccess { + class GECODE_SEARCH_EXPORT WorkerControlAccess { public: /// Attach \a control to a leaf engine with fixed \a capacity static void attach(WorkerControl& control, unsigned int capacity); @@ -46,6 +46,39 @@ namespace Gecode { namespace Search { static void detach(WorkerControl& control); /// Return the request generation static unsigned long long int generation(const WorkerControl& control); + /// Return whether \a control is engaged + static bool engaged(const WorkerControl& control); + /// Return the current request + static unsigned int requested(const WorkerControl& control); + /// Return a consistent request and generation snapshot + static void snapshot(const WorkerControl& control, unsigned int& requested, + unsigned long long int& generation); + /// Wait for the event belonging to worker \a worker + static void wait(WorkerControl& control, unsigned int worker); + /// Signal the event belonging to worker \a worker + static void signal(WorkerControl& control, unsigned int worker); + /// Signal every worker event + static void signal_all(WorkerControl& control); + /// Publish an internal scheduler snapshot + static void observe(WorkerControl& control, + unsigned long long int generation, + unsigned int leases, unsigned int parked, + unsigned int owners, unsigned int parked_owners); + /// Record an internal scheduler handoff + static void handoff(WorkerControl& control); + /// Return the scheduler's observed request generation + static unsigned long long int observed_generation( + const WorkerControl& control); + /// Return the observed execution lease count + static unsigned int leases(const WorkerControl& control); + /// Return the observed parked worker count + static unsigned int parked(const WorkerControl& control); + /// Return the observed logical owner count + static unsigned int owners(const WorkerControl& control); + /// Return the observed parked logical owner count + static unsigned int parked_owners(const WorkerControl& control); + /// Return the observed lease handoff count + static unsigned long long int handoffs(const WorkerControl& control); }; }} diff --git a/test/search.cpp b/test/search.cpp index df89d78bbb..e549b29264 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -37,6 +37,7 @@ #include #include +#include #include "test/test.hh" @@ -356,9 +357,39 @@ namespace Test { ERRORS, BINDING, CONCURRENT, - COMPATIBILITY + COMPATIBILITY, + DFS_RESIZE_ENUMERATION, + DFS_RESIZE_LIFECYCLE } scenario; + /// Stable finite tree used by the DFS resizing tests + class ResizeSpace : public Gecode::Space { + public: + Gecode::BoolVarArray x; + + ResizeSpace(void) + : x(*this,12,0,1) { + Gecode::branch(*this,x,Gecode::BOOL_VAR_NONE(), + Gecode::BOOL_VAL_MIN()); + } + + ResizeSpace(ResizeSpace& s) + : Gecode::Space(s) { + x.update(*this,s.x); + } + + virtual Gecode::Space* copy(void) { + return new ResizeSpace(*this); + } + + unsigned int id(void) const { + unsigned int result = 0U; + for (int i=0; i(x[i].val()); + return result; + } + }; + template static bool throws(Function function) { try { @@ -574,6 +605,140 @@ namespace Test { bab_best(empty) && bab_best(bab_control); } + static bool resized_enumeration(void) { +#ifdef GECODE_HAS_THREADS + Gecode::Search::WorkerControl control(4U); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + ResizeSpace* root = new ResizeSpace; + Gecode::DFS dfs(root,o); + delete root; + + std::atomic solutions(0U); + std::atomic controller_ok(true); + std::thread controller([&] { + const unsigned int limits[] = {1U,3U,1U,4U}; + const unsigned int milestones[] = {1U,64U,128U,192U}; + for (unsigned int i=0U; i<4U; i++) { + while (solutions.load(std::memory_order_acquire) < + milestones[i]) + std::this_thread::yield(); + if (i == 0U) { + bool multiple_owners = false; + for (unsigned int spin=0U; spin<10000000U; spin++) { + if (Gecode::Search::WorkerControlAccess::owners(control) >= + 2U) { + multiple_owners = true; + break; + } + std::this_thread::yield(); + } + if (!multiple_owners) { + controller_ok.store(false,std::memory_order_release); + return; + } + } + unsigned long long int handoffs = + Gecode::Search::WorkerControlAccess::handoffs(control); + try { + control.request(limits[i]); + } catch (...) { + controller_ok.store(false,std::memory_order_release); + return; + } + unsigned long long int generation = + Gecode::Search::WorkerControlAccess::generation(control); + bool converged = false; + for (unsigned int spin=0U; spin<10000000U; spin++) { + if ((Gecode::Search::WorkerControlAccess:: + observed_generation(control) == generation) && + (Gecode::Search::WorkerControlAccess::leases(control) <= + limits[i]) && + (Gecode::Search::WorkerControlAccess::parked(control) >= + 4U-limits[i]) && + ((i != 0U) || + (Gecode::Search::WorkerControlAccess:: + parked_owners(control) > 0U))) { + converged = true; + break; + } + std::this_thread::yield(); + } + if (!converged) { + controller_ok.store(false,std::memory_order_release); + return; + } + if (i == 0U) { + bool handed_off = false; + for (unsigned int spin=0U; spin<50000000U; spin++) { + if (Gecode::Search::WorkerControlAccess::handoffs(control) > + handoffs) { + handed_off = true; + break; + } + std::this_thread::yield(); + } + if (!handed_off) { + controller_ok.store(false,std::memory_order_release); + return; + } + } + } + }); + + std::vector counts(1U << 12,0U); + while (ResizeSpace* solution = dfs.next()) { + counts[solution->id()]++; + delete solution; + (void) solutions.fetch_add(1U,std::memory_order_release); + } + controller.join(); + if (!controller_ok.load(std::memory_order_acquire) || + (solutions.load(std::memory_order_acquire) != counts.size())) + return false; + for (unsigned int count : counts) + if (count != 1U) + return false; +#endif + return true; + } + + static bool resize_lifecycle(void) { +#ifdef GECODE_HAS_THREADS + Gecode::Search::WorkerControl control(1U); + Gecode::Search::NodeStop stop(10U); + Gecode::Search::Options o; + o.threads = 4.0; + o.nogoods_limit = 16U; + o.stop = &stop; + o.worker_control = control; + ResizeSpace* root = new ResizeSpace; + Gecode::Search::Engine* dfs = + Gecode::Search::dfsengine(root,o); + delete root; + + ResizeSpace* solution = static_cast(dfs->next()); + bool ok = (solution == nullptr) && dfs->stopped() && + (dfs->statistics().node > 0U); + delete solution; + (void) dfs->nogoods(); + + control.request(3U); + root = new ResizeSpace; + dfs->reset(root); + control.request(1U); + stop.limit(100000U); + solution = static_cast(dfs->next()); + ok = ok && (solution != nullptr); + delete solution; + delete dfs; + return ok; +#else + return true; +#endif + } + public: WorkerControl(const std::string& name, Scenario s) : Base("Search::WorkerControl::"+name), scenario(s) {} @@ -585,6 +750,8 @@ namespace Test { case BINDING: return binding(); case CONCURRENT: return concurrent(); case COMPATIBILITY: return compatibility(); + case DFS_RESIZE_ENUMERATION: return resized_enumeration(); + case DFS_RESIZE_LIFECYCLE: return resize_lifecycle(); default: GECODE_NEVER; } return false; @@ -596,6 +763,10 @@ namespace Test { (void) new WorkerControl("Binding",BINDING); (void) new WorkerControl("Concurrent",CONCURRENT); (void) new WorkerControl("Compatibility",COMPATIBILITY); + (void) new WorkerControl("DFSResize::Enumeration", + DFS_RESIZE_ENUMERATION); + (void) new WorkerControl("DFSResize::Lifecycle", + DFS_RESIZE_LIFECYCLE); } }; From fc8428326e4674625b654590cf844c0bcb0e9192 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 02:15:26 +0100 Subject: [PATCH 03/15] Add deterministic DFS worker control tests --- gecode/search/par/dfs.hpp | 4 + gecode/search/par/engine.hh | 8 + gecode/search/par/engine.hpp | 40 +++- gecode/search/worker-control.cpp | 367 ++++++++++++++++++++++++++++++- gecode/search/worker-control.hh | 56 ++++- test/search.cpp | 306 +++++++++++++++++++++++++- 6 files changed, 773 insertions(+), 8 deletions(-) diff --git a/gecode/search/par/dfs.hpp b/gecode/search/par/dfs.hpp index d50e771462..518f5b4566 100755 --- a/gecode/search/par/dfs.hpp +++ b/gecode/search/par/dfs.hpp @@ -210,11 +210,13 @@ namespace Gecode { namespace Search { namespace Par { { if (!engine().scheduler_admit(index)) break; + engine().scheduler_action_begin(); m.acquire(); if (idle) { m.release(); // Try to find new work if (!find()) { + engine().scheduler_failed_scan(index); engine().scheduler_handoff(index,engine().work_remains()); } } else if (cur != nullptr) { @@ -264,6 +266,7 @@ namespace Gecode { namespace Search { namespace Par { cur = nullptr; path.next(); m.release(); + engine().scheduler_solution(index); engine().solution(s); } break; @@ -305,6 +308,7 @@ namespace Gecode { namespace Search { namespace Par { engine().scheduler_idle(index); engine().scheduler_handoff(index,engine().work_remains()); } + engine().scheduler_action_end(); } break; default: diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index 566164c478..a3a6e11086 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -131,10 +131,18 @@ namespace Gecode { namespace Search { namespace Par { void scheduler_reset(bool root_owner); /// Admit worker \a worker for one DFS exploration action bool scheduler_admit(unsigned int worker); + /// Begin an admitted DFS exploration action + void scheduler_action_begin(void); + /// End an admitted DFS exploration action + void scheduler_action_end(void); + /// Pause after worker \a worker completes a failed steal scan + void scheduler_failed_scan(unsigned int worker); /// Record that worker \a worker owns search void scheduler_owner(unsigned int worker); /// Record that worker \a worker is idle void scheduler_idle(unsigned int worker); + /// Record that worker \a worker produced a solution + void scheduler_solution(unsigned int worker); /// Hand worker \a worker's lease to a parked logical worker void scheduler_handoff(unsigned int worker, bool work_remains); diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index f01a9236e2..709b41df86 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -195,6 +195,11 @@ namespace Gecode { namespace Search { namespace Par { worker_control, scheduler_generation.load(std::memory_order_relaxed), scheduler_leases,parked,owners,parked_owners); + for (unsigned int i=0U; i(scheduler_worker[i].logical), + scheduler_worker[i].lease,scheduler_worker[i].parked); } template @@ -247,6 +252,8 @@ namespace Gecode { namespace Search { namespace Par { Engine::scheduler_admit(unsigned int worker) { if (!scheduler_enabled) return true; + WorkerControlAccess::gate(worker_control, + WorkerControlAccess::GATE_ADMISSION,worker); while (cmd() == C_WORK) { unsigned long long int generation = WorkerControlAccess::generation(worker_control); @@ -280,11 +287,35 @@ namespace Gecode { namespace Search { namespace Par { WorkerControlAccess::signal_all(worker_control); if (admitted) return true; + WorkerControlAccess::gate(worker_control, + WorkerControlAccess::GATE_EVENT_WAIT,worker); WorkerControlAccess::wait(worker_control,worker); } return false; } + template + forceinline void + Engine::scheduler_action_begin(void) { + if (scheduler_enabled) + WorkerControlAccess::action_begin(worker_control); + } + + template + forceinline void + Engine::scheduler_action_end(void) { + if (scheduler_enabled) + WorkerControlAccess::action_end(worker_control); + } + + template + forceinline void + Engine::scheduler_failed_scan(unsigned int worker) { + if (scheduler_enabled) + WorkerControlAccess::gate(worker_control, + WorkerControlAccess::GATE_FAILED_SCAN,worker); + } + template void Engine::scheduler_owner(unsigned int worker) { @@ -307,6 +338,13 @@ namespace Gecode { namespace Search { namespace Par { scheduler_mutex.release(); } + template + void + Engine::scheduler_solution(unsigned int worker) { + if (scheduler_enabled) + WorkerControlAccess::solution(worker_control,worker); + } + template void Engine::scheduler_handoff(unsigned int worker, @@ -330,7 +368,7 @@ namespace Gecode { namespace Search { namespace Par { scheduler_worker[target].lease = true; scheduler_worker[target].parked = false; scheduler_cursor = (target+1U) % workers(); - WorkerControlAccess::handoff(worker_control); + WorkerControlAccess::handoff(worker_control,worker,target); } } } diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 80db9e4703..46f2327a5a 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -42,6 +42,70 @@ namespace Gecode { namespace Search { DETACHED }; + class TestSupport { + public: + unsigned int capacity; + std::atomic + epoch[WorkerControlAccess::GATE_COUNT]; + std::atomic + worker[WorkerControlAccess::GATE_COUNT]; + std::atomic + expected[WorkerControlAccess::GATE_COUNT]; + std::atomic + reached_count[WorkerControlAccess::GATE_COUNT]; + std::atomic enabled[WorkerControlAccess::GATE_COUNT]; + Support::Event reached[WorkerControlAccess::GATE_COUNT]; + Support::Event* release; + std::atomic* seen; + std::atomic* waiting; + std::atomic admitted; + std::atomic max_admitted; + std::atomic* logical; + std::atomic* lease; + std::atomic* parked; + + TestSupport(unsigned int capacity0) + : capacity(capacity0), + release(new Support::Event + [WorkerControlAccess::GATE_COUNT * capacity]), + seen(new std::atomic + [WorkerControlAccess::GATE_COUNT * capacity]), + waiting(new std::atomic + [WorkerControlAccess::GATE_COUNT * capacity]), + admitted(0U), max_admitted(0U), + logical(new std::atomic[capacity]), + lease(new std::atomic[capacity]), + parked(new std::atomic[capacity]) { + for (unsigned int i=0U; i requested; std::atomic capacity; std::atomic generation; @@ -51,6 +115,12 @@ namespace Gecode { namespace Search { std::atomic parked; std::atomic owners; std::atomic parked_owners; + std::atomic last_handoff_from; + std::atomic last_handoff_to; + std::atomic last_solution_worker; + std::atomic solution_handoff_from; + std::atomic solution_handoff_to; + std::atomic test_support; Support::Mutex mutex; Lifecycle lifecycle; Support::Event* events; @@ -59,9 +129,16 @@ namespace Gecode { namespace Search { : RefCount(1U), requested(workers), capacity(0U), generation(0U), observed_generation(0U), handoffs(0U), leases(0U), parked(0U), owners(0U), parked_owners(0U), + last_handoff_from(WorkerControlAccess::ALL_WORKERS), + last_handoff_to(WorkerControlAccess::ALL_WORKERS), + last_solution_worker(WorkerControlAccess::ALL_WORKERS), + solution_handoff_from(WorkerControlAccess::ALL_WORKERS), + solution_handoff_to(WorkerControlAccess::ALL_WORKERS), + test_support(nullptr), lifecycle(NEVER_BOUND), events(nullptr) {} ~State(void) { delete [] events; + delete test_support.load(std::memory_order_relaxed); } }; @@ -271,9 +348,227 @@ namespace Gecode { namespace Search { } void - WorkerControlAccess::handoff(WorkerControl& control) { - if (control.state != nullptr) - (void) control.state->handoffs.fetch_add(1U,std::memory_order_relaxed); + WorkerControlAccess::observe_worker(WorkerControl& control, + unsigned int worker, + unsigned int logical, + bool lease, bool parked) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + if (support == nullptr) + return; + assert(worker < support->capacity); + support->logical[worker].store(logical,std::memory_order_release); + support->lease[worker].store(lease,std::memory_order_release); + support->parked[worker].store(parked,std::memory_order_release); + } + + void + WorkerControlAccess::handoff(WorkerControl& control, unsigned int from, + unsigned int to) { + if (control.state == nullptr) + return; + control.state->last_handoff_from.store(from,std::memory_order_release); + control.state->last_handoff_to.store(to,std::memory_order_release); + (void) control.state->handoffs.fetch_add(1U,std::memory_order_release); + } + + void + WorkerControlAccess::solution(WorkerControl& control, unsigned int worker) { + if (control.state == nullptr) + return; + control.state->solution_handoff_from.store( + control.state->last_handoff_from.load(std::memory_order_acquire), + std::memory_order_release); + control.state->solution_handoff_to.store( + control.state->last_handoff_to.load(std::memory_order_acquire), + std::memory_order_release); + control.state->last_solution_worker.store(worker, + std::memory_order_release); + } + + void + WorkerControlAccess::gate(WorkerControl& control, Gate gate, + unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + if (support == nullptr) + return; + unsigned int g = static_cast(gate); + if (!support->enabled[g].load(std::memory_order_acquire)) + return; + unsigned int selected = + support->worker[g].load(std::memory_order_relaxed); + if ((selected != ALL_WORKERS) && (selected != worker)) + return; + unsigned int capacity = support->capacity; + if (worker >= capacity) + return; + unsigned long long int epoch = + support->epoch[g].load(std::memory_order_acquire); + std::atomic& seen = + support->seen[g*capacity+worker]; + unsigned long long int old = seen.load(std::memory_order_relaxed); + while ((old != epoch) && + !seen.compare_exchange_weak(old,epoch, + std::memory_order_acq_rel, + std::memory_order_relaxed)) {} + if (old == epoch) + return; + support->waiting[g*capacity+worker].store(true, + std::memory_order_release); + unsigned int reached = + support->reached_count[g].fetch_add(1U, + std::memory_order_acq_rel) + 1U; + if (reached == support->expected[g].load(std::memory_order_acquire)) + support->reached[g].signal(); + support->release[g*capacity+worker].wait(); + support->waiting[g*capacity+worker].store(false, + std::memory_order_release); + } + + void + WorkerControlAccess::gate_install(WorkerControl& control, Gate gate, + unsigned int worker, + unsigned int expected) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + WorkerControl::State::TestSupport* support; + { + Support::Lock lock(state->mutex); + support = state->test_support.load(std::memory_order_relaxed); + if (support == nullptr) { + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + support = new WorkerControl::State::TestSupport(capacity); + state->test_support.store(support,std::memory_order_release); + } + } + unsigned int capacity = support->capacity; + assert((worker == ALL_WORKERS) || (worker < capacity)); + assert((expected > 0U) && (expected <= capacity)); + unsigned int g = static_cast(gate); + support->enabled[g].store(false,std::memory_order_release); + support->worker[g].store(worker,std::memory_order_relaxed); + support->expected[g].store(expected,std::memory_order_relaxed); + support->reached_count[g].store(0U,std::memory_order_relaxed); + (void) support->epoch[g].fetch_add(1U,std::memory_order_release); + support->enabled[g].store(true,std::memory_order_release); + } + + void + WorkerControlAccess::gate_wait(WorkerControl& control, Gate gate) { + if (control.state == nullptr) + return; + WorkerControl::State::TestSupport* support = + control.state->test_support.load(std::memory_order_acquire); + assert(support != nullptr); + support->reached[static_cast(gate)].wait(); + } + + void + WorkerControlAccess::gate_release(WorkerControl& control, Gate gate, + unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + assert(support != nullptr); + unsigned int capacity = support->capacity; + assert(worker < capacity); + unsigned int slot = + static_cast(gate)*capacity+worker; + if (support->waiting[slot].load(std::memory_order_acquire)) + support->release[slot].signal(); + } + + void + WorkerControlAccess::gate_release_all(WorkerControl& control, Gate gate) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + if (support == nullptr) + return; + unsigned int g = static_cast(gate); + support->enabled[g].store(false,std::memory_order_release); + unsigned int capacity = support->capacity; + unsigned long long int epoch = + support->epoch[g].load(std::memory_order_acquire); + for (unsigned int i=0U; iseen[g*capacity+i].load(std::memory_order_acquire) == + epoch && + support->waiting[g*capacity+i].load(std::memory_order_acquire)) + support->release[g*capacity+i].signal(); + } + + void + WorkerControlAccess::action_begin(WorkerControl& control) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + if (support == nullptr) + return; + unsigned int current = + support->admitted.fetch_add(1U,std::memory_order_acq_rel) + 1U; + unsigned int maximum = + support->max_admitted.load(std::memory_order_relaxed); + while ((maximum < current) && + !support->max_admitted.compare_exchange_weak( + maximum,current,std::memory_order_release, + std::memory_order_relaxed)) {} + } + + void + WorkerControlAccess::action_end(WorkerControl& control) { + if (control.state == nullptr) + return; + WorkerControl::State::TestSupport* support = + control.state->test_support.load(std::memory_order_acquire); + if (support != nullptr) + (void) support->admitted.fetch_sub(1U,std::memory_order_release); + } + + void + WorkerControlAccess::reset_max_admitted(WorkerControl& control) { + if (control.state != nullptr) { + WorkerControl::State::TestSupport* support = + control.state->test_support.load(std::memory_order_acquire); + assert(support != nullptr); + unsigned int current = + support->admitted.load(std::memory_order_acquire); + support->max_admitted.store(current,std::memory_order_release); + } + } + + unsigned int + WorkerControlAccess::admitted(const WorkerControl& control) { + if (control.state == nullptr) + return 0U; + WorkerControl::State::TestSupport* support = + control.state->test_support.load(std::memory_order_acquire); + return (support == nullptr) ? 0U : + support->admitted.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::max_admitted(const WorkerControl& control) { + if (control.state == nullptr) + return 0U; + WorkerControl::State::TestSupport* support = + control.state->test_support.load(std::memory_order_acquire); + return (support == nullptr) ? 0U : + support->max_admitted.load(std::memory_order_acquire); } unsigned long long int @@ -312,4 +607,70 @@ namespace Gecode { namespace Search { control.state->handoffs.load(std::memory_order_acquire); } + unsigned int + WorkerControlAccess::last_handoff_from(const WorkerControl& control) { + return (control.state == nullptr) ? ALL_WORKERS : + control.state->last_handoff_from.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::last_handoff_to(const WorkerControl& control) { + return (control.state == nullptr) ? ALL_WORKERS : + control.state->last_handoff_to.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::last_solution_worker(const WorkerControl& control) { + return (control.state == nullptr) ? ALL_WORKERS : + control.state->last_solution_worker.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::solution_handoff_from(const WorkerControl& control) { + return (control.state == nullptr) ? ALL_WORKERS : + control.state->solution_handoff_from.load(std::memory_order_acquire); + } + + unsigned int + WorkerControlAccess::solution_handoff_to(const WorkerControl& control) { + return (control.state == nullptr) ? ALL_WORKERS : + control.state->solution_handoff_to.load(std::memory_order_acquire); + } + + bool + WorkerControlAccess::owner(const WorkerControl& control, + unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return false; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + assert((support != nullptr) && (worker < support->capacity)); + return support->logical[worker].load(std::memory_order_acquire) == 0U; + } + + bool + WorkerControlAccess::leased(const WorkerControl& control, + unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return false; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + assert((support != nullptr) && (worker < support->capacity)); + return support->lease[worker].load(std::memory_order_acquire); + } + + bool + WorkerControlAccess::worker_parked(const WorkerControl& control, + unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return false; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + assert((support != nullptr) && (worker < support->capacity)); + return support->parked[worker].load(std::memory_order_acquire); + } + }} diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh index 1484e76f2f..43f0776603 100644 --- a/gecode/search/worker-control.hh +++ b/gecode/search/worker-control.hh @@ -40,6 +40,15 @@ namespace Gecode { namespace Search { /// Private access to worker-control attachment state class GECODE_SEARCH_EXPORT WorkerControlAccess { public: + /// Test boundary in adjustable DFS scheduling + enum Gate { + GATE_ADMISSION, + GATE_EVENT_WAIT, + GATE_FAILED_SCAN, + GATE_COUNT + }; + /// Select every worker for a test boundary gate + static const unsigned int ALL_WORKERS = static_cast(-1); /// Attach \a control to a leaf engine with fixed \a capacity static void attach(WorkerControl& control, unsigned int capacity); /// Detach \a control from its leaf engine @@ -64,8 +73,36 @@ namespace Gecode { namespace Search { unsigned long long int generation, unsigned int leases, unsigned int parked, unsigned int owners, unsigned int parked_owners); + /// Record the state of worker \a worker + static void observe_worker(WorkerControl& control, unsigned int worker, + unsigned int logical, bool lease, bool parked); /// Record an internal scheduler handoff - static void handoff(WorkerControl& control); + static void handoff(WorkerControl& control, unsigned int from, + unsigned int to); + /// Record which worker produced a solution + static void solution(WorkerControl& control, unsigned int worker); + /// Pause a worker at an installed internal test boundary + static void gate(WorkerControl& control, Gate gate, unsigned int worker); + /// Install an internal test boundary gate + static void gate_install(WorkerControl& control, Gate gate, + unsigned int worker, unsigned int expected); + /// Wait until the installed gate has reached its expected count + static void gate_wait(WorkerControl& control, Gate gate); + /// Release one worker from an installed gate + static void gate_release(WorkerControl& control, Gate gate, + unsigned int worker); + /// Release all workers and disable an installed gate + static void gate_release_all(WorkerControl& control, Gate gate); + /// Begin one admitted DFS action + static void action_begin(WorkerControl& control); + /// End one admitted DFS action + static void action_end(WorkerControl& control); + /// Reset the admitted-action high-water mark + static void reset_max_admitted(WorkerControl& control); + /// Return the current admitted-action count + static unsigned int admitted(const WorkerControl& control); + /// Return the admitted-action high-water mark + static unsigned int max_admitted(const WorkerControl& control); /// Return the scheduler's observed request generation static unsigned long long int observed_generation( const WorkerControl& control); @@ -79,6 +116,23 @@ namespace Gecode { namespace Search { static unsigned int parked_owners(const WorkerControl& control); /// Return the observed lease handoff count static unsigned long long int handoffs(const WorkerControl& control); + /// Return the source worker of the last handoff + static unsigned int last_handoff_from(const WorkerControl& control); + /// Return the target worker of the last handoff + static unsigned int last_handoff_to(const WorkerControl& control); + /// Return the worker that most recently produced a solution + static unsigned int last_solution_worker(const WorkerControl& control); + /// Return the handoff source observed when the last solution was produced + static unsigned int solution_handoff_from(const WorkerControl& control); + /// Return the handoff target observed when the last solution was produced + static unsigned int solution_handoff_to(const WorkerControl& control); + /// Return whether worker \a worker is a logical owner + static bool owner(const WorkerControl& control, unsigned int worker); + /// Return whether worker \a worker holds an execution lease + static bool leased(const WorkerControl& control, unsigned int worker); + /// Return whether worker \a worker is parked + static bool worker_parked(const WorkerControl& control, + unsigned int worker); }; }} diff --git a/test/search.cpp b/test/search.cpp index e549b29264..84454d0aff 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -359,6 +359,8 @@ namespace Test { CONCURRENT, COMPATIBILITY, DFS_RESIZE_ENUMERATION, + DFS_RESIZE_BOUNDARIES, + DFS_RESIZE_HANDOFF, DFS_RESIZE_LIFECYCLE } scenario; @@ -390,6 +392,57 @@ namespace Test { } }; + /// One non-stealable current solution and no stealable path + class SingleSolutionSpace : public Gecode::Space { + public: + SingleSolutionSpace(void) {} + SingleSolutionSpace(SingleSolutionSpace& s) + : Gecode::Space(s) {} + virtual Gecode::Space* copy(void) { + return new SingleSolutionSpace(*this); + } + }; + + /// Thread-safe trace lifecycle observations + class CountingTracer : public Gecode::SearchTracer { + public: + std::atomic init_count; + std::atomic node_count; + std::atomic done_count; + std::atomic nodes_at_done; + + CountingTracer(void) + : init_count(0U), node_count(0U), done_count(0U), + nodes_at_done(0U) {} + virtual void init(void) { + (void) init_count.fetch_add(1U,std::memory_order_relaxed); + } + virtual void round(unsigned int) {} + virtual void skip(const EdgeInfo&) {} + virtual void node(const EdgeInfo&, const NodeInfo&) { + (void) node_count.fetch_add(1U,std::memory_order_relaxed); + } + virtual void done(void) { + nodes_at_done.store(node_count.load(std::memory_order_acquire), + std::memory_order_release); + (void) done_count.fetch_add(1U,std::memory_order_release); + } + }; + + static bool converged(const Gecode::Search::WorkerControl& control, + unsigned long long int generation, + unsigned int leases) { + for (unsigned int spin=0U; spin<10000000U; spin++) { + if ((Gecode::Search::WorkerControlAccess:: + observed_generation(control) == generation) && + (Gecode::Search::WorkerControlAccess::leases(control) == + leases)) + return true; + std::this_thread::yield(); + } + return false; + } + template static bool throws(Function function) { try { @@ -704,24 +757,260 @@ namespace Test { return true; } + static bool resize_boundaries(void) { +#ifdef GECODE_HAS_THREADS + using Gecode::Search::WorkerControlAccess; + Gecode::Search::WorkerControl control(4U); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + ResizeSpace* root = new ResizeSpace; + Gecode::DFS dfs(root,o); + delete root; + + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,4U); + std::atomic finish(false); + std::atomic solutions(0U); + std::thread consumer([&] { + while (!finish.load(std::memory_order_acquire)) { + ResizeSpace* solution = dfs.next(); + if (solution == nullptr) + return; + delete solution; + (void) solutions.fetch_add(1U,std::memory_order_release); + } + }); + + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_ADMISSION); + if (WorkerControlAccess::admitted(control) != 0U) { + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ADMISSION); + finish.store(true,std::memory_order_release); + consumer.join(); + return false; + } + + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,3U); + control.request(1U); + unsigned long long int generation = + WorkerControlAccess::generation(control); + WorkerControlAccess::reset_max_admitted(control); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,0U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,2U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_EVENT_WAIT); + bool ok = converged(control,generation,1U) && + (WorkerControlAccess::max_admitted(control) <= 1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,3U); + for (unsigned int spin=0U; + (spin<10000000U) && + (WorkerControlAccess::max_admitted(control) == 0U); spin++) + std::this_thread::yield(); + ok = ok && (WorkerControlAccess::max_admitted(control) == 1U); + + // The request is published and signals worker events while the + // parked workers are still stopped immediately before event wait. + control.request(3U); + generation = WorkerControlAccess::generation(control); + WorkerControlAccess::reset_max_admitted(control); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); + ok = ok && converged(control,generation,3U) && + (WorkerControlAccess::max_admitted(control) <= 3U); + + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,3U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_ADMISSION); + unsigned int leased[3]; + unsigned int n_leased = 0U; + for (unsigned int i=0U; i<4U; i++) + if (WorkerControlAccess::leased(control,i)) { + if (n_leased < 3U) + leased[n_leased] = i; + n_leased++; + } + if (n_leased != 3U) { + finish.store(true,std::memory_order_release); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ADMISSION); + consumer.join(); + return false; + } + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,2U); + control.request(1U); + generation = WorkerControlAccess::generation(control); + WorkerControlAccess::reset_max_admitted(control); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,leased[0]); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,leased[1]); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_EVENT_WAIT); + ok = ok && converged(control,generation,1U) && + (WorkerControlAccess::max_admitted(control) <= 1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,leased[2]); + for (unsigned int spin=0U; + (spin<10000000U) && + (WorkerControlAccess::max_admitted(control) == 0U); spin++) + std::this_thread::yield(); + ok = ok && (WorkerControlAccess::max_admitted(control) == 1U); + + finish.store(true,std::memory_order_release); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); + consumer.join(); + return ok && (solutions.load(std::memory_order_acquire) > 0U); +#else + return true; +#endif + } + + static bool resize_handoff(void) { +#ifdef GECODE_HAS_THREADS + using Gecode::Search::WorkerControlAccess; + Gecode::Search::WorkerControl control(4U); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + SingleSolutionSpace* root = new SingleSolutionSpace; + Gecode::DFS dfs(root,o); + delete root; + + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,4U); + std::atomic result(nullptr); + std::thread consumer([&] { + result.store(dfs.next(),std::memory_order_release); + }); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_ADMISSION); + + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_FAILED_SCAN,3U,1U); + // Let worker 3 declare its initially empty state idle. With no parked + // target at capacity, its next action completes a failed steal scan. + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,3U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_FAILED_SCAN); + + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,3U); + control.request(1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,0U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,2U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_EVENT_WAIT); + + bool exact_setup = + WorkerControlAccess::owner(control,0U) && + WorkerControlAccess::worker_parked(control,0U) && + WorkerControlAccess::leased(control,3U); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); + + unsigned long long int handoffs = + WorkerControlAccess::handoffs(control); + bool before_handoff = exact_setup && + WorkerControlAccess::leased(control,3U) && + WorkerControlAccess::worker_parked(control,0U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_FAILED_SCAN,3U); + consumer.join(); + + SingleSolutionSpace* solution = + result.load(std::memory_order_acquire); + bool ok = before_handoff && (solution != nullptr) && + (WorkerControlAccess::handoffs(control) >= handoffs+1U) && + (WorkerControlAccess::last_solution_worker(control) == 0U); + ok = ok && + (WorkerControlAccess::solution_handoff_from(control) == 3U) && + (WorkerControlAccess::solution_handoff_to(control) == 0U); + delete solution; + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_FAILED_SCAN); + return ok; +#else + return true; +#endif + } + static bool resize_lifecycle(void) { #ifdef GECODE_HAS_THREADS Gecode::Search::WorkerControl control(1U); Gecode::Search::NodeStop stop(10U); + CountingTracer tracer; Gecode::Search::Options o; o.threads = 4.0; o.nogoods_limit = 16U; o.stop = &stop; + o.tracer = &tracer; o.worker_control = control; ResizeSpace* root = new ResizeSpace; Gecode::Search::Engine* dfs = Gecode::Search::dfsengine(root,o); delete root; - ResizeSpace* solution = static_cast(dfs->next()); + using Gecode::Search::WorkerControlAccess; + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,4U); + std::atomic stopped_result(nullptr); + std::thread stopped_next([&] { + stopped_result.store(static_cast(dfs->next()), + std::memory_order_release); + }); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,3U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,2U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,3U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_EVENT_WAIT); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,0U); + stopped_next.join(); + ResizeSpace* solution = + stopped_result.load(std::memory_order_acquire); bool ok = (solution == nullptr) && dfs->stopped() && - (dfs->statistics().node > 0U); + (dfs->statistics().node > 0U) && + (WorkerControlAccess::parked(control) >= 3U); delete solution; + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); (void) dfs->nogoods(); control.request(3U); @@ -733,7 +1022,12 @@ namespace Test { ok = ok && (solution != nullptr); delete solution; delete dfs; - return ok; + return ok && + (tracer.init_count.load(std::memory_order_acquire) == 1U) && + (tracer.node_count.load(std::memory_order_acquire) > 0U) && + (tracer.done_count.load(std::memory_order_acquire) == 1U) && + (tracer.nodes_at_done.load(std::memory_order_acquire) == + tracer.node_count.load(std::memory_order_acquire)); #else return true; #endif @@ -751,6 +1045,8 @@ namespace Test { case CONCURRENT: return concurrent(); case COMPATIBILITY: return compatibility(); case DFS_RESIZE_ENUMERATION: return resized_enumeration(); + case DFS_RESIZE_BOUNDARIES: return resize_boundaries(); + case DFS_RESIZE_HANDOFF: return resize_handoff(); case DFS_RESIZE_LIFECYCLE: return resize_lifecycle(); default: GECODE_NEVER; } @@ -765,6 +1061,10 @@ namespace Test { (void) new WorkerControl("Compatibility",COMPATIBILITY); (void) new WorkerControl("DFSResize::Enumeration", DFS_RESIZE_ENUMERATION); + (void) new WorkerControl("DFSResize::Boundaries", + DFS_RESIZE_BOUNDARIES); + (void) new WorkerControl("DFSResize::Handoff", + DFS_RESIZE_HANDOFF); (void) new WorkerControl("DFSResize::Lifecycle", DFS_RESIZE_LIFECYCLE); } From edd3ac91d7515f5eb07b2ae5942443b3aa0b49e0 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 03:14:28 +0100 Subject: [PATCH 04/15] Add adjustable worker control to parallel BAB --- gecode/search/par/bab.hh | 10 +- gecode/search/par/bab.hpp | 39 ++++- gecode/search/par/engine.hh | 22 +-- gecode/search/par/engine.hpp | 7 + gecode/search/worker-control.cpp | 35 +++- gecode/search/worker-control.hh | 11 +- test/search.cpp | 288 ++++++++++++++++++++++++++----- 7 files changed, 344 insertions(+), 68 deletions(-) diff --git a/gecode/search/par/bab.hh b/gecode/search/par/bab.hh index 5284f131cf..847bc659e5 100644 --- a/gecode/search/par/bab.hh +++ b/gecode/search/par/bab.hh @@ -80,9 +80,11 @@ namespace Gecode { namespace Search { namespace Par { int mark; /// Best solution found so far Space* best; + /// Fixed worker index + unsigned int index; public: /// Initialize for space \a s with engine \a e - Worker(Space* s, BAB& e); + Worker(Space* s, BAB& e, unsigned int index); /// Provide access to engine BAB& engine(void) const; /// Start execution of worker @@ -90,9 +92,11 @@ namespace Gecode { namespace Search { namespace Par { /// Accept better solution \a b void better(Space* b); /// Try to find some work - void find(void); + bool find(void); /// Reset engine to restart at space \a s void reset(Space* s, unsigned int ngdl); + /// Whether this worker currently owns local search + bool owns_work(void) const; /// Destructor virtual ~Worker(void); }; @@ -107,7 +111,7 @@ namespace Gecode { namespace Search { namespace Par { /// \name Search control //@{ /// Report solution \a s - void solution(Space* s); + void solution(Space* s, unsigned int producer); //@} /// \name Engine interface diff --git a/gecode/search/par/bab.hpp b/gecode/search/par/bab.hpp index 8e6d3f047d..a25004dab5 100755 --- a/gecode/search/par/bab.hpp +++ b/gecode/search/par/bab.hpp @@ -71,14 +71,20 @@ namespace Gecode { namespace Search { namespace Par { Search::Worker::reset(); } + template + forceinline bool + BAB::Worker::owns_work(void) const { + return (cur != nullptr) || !path.empty(); + } + /* * Engine: initialization */ template forceinline - BAB::Worker::Worker(Space* s, BAB& e) - : Engine::Worker(s,e), mark(0), best(nullptr) {} + BAB::Worker::Worker(Space* s, BAB& e, unsigned int index0) + : Engine::Worker(s,e), mark(0), best(nullptr), index(index0) {} template forceinline @@ -90,10 +96,11 @@ namespace Gecode { namespace Search { namespace Par { _worker = static_cast (heap.ralloc(workers() * sizeof(Worker*))); // The first worker gets the entire search tree - _worker[0] = new Worker(s,*this); + _worker[0] = new Worker(s,*this,0U); // All other workers start with no work for (unsigned int i=1U; ischeduler_enable(_worker[0]->owns_work()); // Block all workers block(); // Create and start threads @@ -114,11 +121,12 @@ namespace Gecode { namespace Search { namespace Par { mark = path.entries(); if (cur != nullptr) cur->constrain(*best); + engine().scheduler_incumbent(index); m.release(); } template forceinline void - BAB::solution(Space* s) { + BAB::solution(Space* s, unsigned int producer) { m_search.acquire(); if (best != nullptr) { s->constrain(*best); @@ -141,6 +149,7 @@ namespace Gecode { namespace Search { namespace Par { if (bs) e_search.signal(); m_search.release(); + this->scheduler_solution(producer); } @@ -148,7 +157,7 @@ namespace Gecode { namespace Search { namespace Par { * Worker: finding and stealing working */ template - forceinline void + forceinline bool BAB::Worker::find(void) { // Try to find new work (even if there is none) for (unsigned int i=0U; ischeduler_reset(worker(0U)->owns_work()); } diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index a3a6e11086..28aa4b0693 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -87,23 +87,23 @@ namespace Gecode { namespace Search { namespace Par { }; /// Search options Options _opt; - /// Logical worker state used by adjustable DFS admission + /// Logical worker state used by adjustable parallel admission enum SchedulerLogical { SL_OWNER, SL_PENDING, SL_IDLE }; - /// Per-worker adjustable DFS admission state + /// Per-worker adjustable parallel admission state struct SchedulerWorker { bool lease; bool parked; SchedulerLogical logical; }; - /// Whether adjustable DFS admission is enabled + /// Whether adjustable parallel admission is enabled bool scheduler_enabled; - /// Mutex for adjustable DFS admission + /// Mutex for adjustable parallel admission Support::Mutex scheduler_mutex; - /// Per-worker adjustable DFS admission state + /// Per-worker adjustable parallel admission state SchedulerWorker* scheduler_worker; /// Current requested lease count unsigned int scheduler_requested; @@ -125,15 +125,15 @@ namespace Gecode { namespace Search { namespace Par { const Options& opt(void) const; /// Return number of workers unsigned int workers(void) const; - /// Enable adjustable DFS admission + /// Enable adjustable parallel admission void scheduler_enable(bool root_owner); - /// Reset adjustable DFS state while all workers are blocked + /// Reset adjustable parallel state while all workers are blocked void scheduler_reset(bool root_owner); - /// Admit worker \a worker for one DFS exploration action + /// Admit worker \a worker for one exploration action bool scheduler_admit(unsigned int worker); - /// Begin an admitted DFS exploration action + /// Begin an admitted exploration action void scheduler_action_begin(void); - /// End an admitted DFS exploration action + /// End an admitted exploration action void scheduler_action_end(void); /// Pause after worker \a worker completes a failed steal scan void scheduler_failed_scan(unsigned int worker); @@ -143,6 +143,8 @@ namespace Gecode { namespace Search { namespace Par { void scheduler_idle(unsigned int worker); /// Record that worker \a worker produced a solution void scheduler_solution(unsigned int worker); + /// Record delivery of an incumbent to worker \a worker + void scheduler_incumbent(unsigned int worker); /// Hand worker \a worker's lease to a parked logical worker void scheduler_handoff(unsigned int worker, bool work_remains); diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index 709b41df86..fe66ddbe6b 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -345,6 +345,13 @@ namespace Gecode { namespace Search { namespace Par { WorkerControlAccess::solution(worker_control,worker); } + template + forceinline void + Engine::scheduler_incumbent(unsigned int worker) { + if (scheduler_enabled) + WorkerControlAccess::incumbent(worker_control,worker); + } + template void Engine::scheduler_handoff(unsigned int worker, diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 46f2327a5a..2ea1b1c498 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -63,6 +63,7 @@ namespace Gecode { namespace Search { std::atomic* logical; std::atomic* lease; std::atomic* parked; + std::atomic* incumbent_deliveries; TestSupport(unsigned int capacity0) : capacity(capacity0), @@ -75,11 +76,14 @@ namespace Gecode { namespace Search { admitted(0U), max_admitted(0U), logical(new std::atomic[capacity]), lease(new std::atomic[capacity]), - parked(new std::atomic[capacity]) { + parked(new std::atomic[capacity]), + incumbent_deliveries( + new std::atomic[capacity]) { for (unsigned int i=0U; itest_support.load(std::memory_order_acquire); + if (support == nullptr) + return; + assert(worker < support->capacity); + (void) support->incumbent_deliveries[worker].fetch_add( + 1U,std::memory_order_release); + } + void WorkerControlAccess::gate(WorkerControl& control, Gate gate, unsigned int worker) { @@ -673,4 +693,17 @@ namespace Gecode { namespace Search { return support->parked[worker].load(std::memory_order_acquire); } + unsigned long long int + WorkerControlAccess::incumbent_deliveries( + const WorkerControl& control, unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return 0U; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + assert((support != nullptr) && (worker < support->capacity)); + return support->incumbent_deliveries[worker].load( + std::memory_order_acquire); + } + }} diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh index 43f0776603..c22eb1e9cb 100644 --- a/gecode/search/worker-control.hh +++ b/gecode/search/worker-control.hh @@ -40,7 +40,7 @@ namespace Gecode { namespace Search { /// Private access to worker-control attachment state class GECODE_SEARCH_EXPORT WorkerControlAccess { public: - /// Test boundary in adjustable DFS scheduling + /// Test boundary in adjustable parallel scheduling enum Gate { GATE_ADMISSION, GATE_EVENT_WAIT, @@ -81,6 +81,8 @@ namespace Gecode { namespace Search { unsigned int to); /// Record which worker produced a solution static void solution(WorkerControl& control, unsigned int worker); + /// Record incumbent delivery to worker \a worker + static void incumbent(WorkerControl& control, unsigned int worker); /// Pause a worker at an installed internal test boundary static void gate(WorkerControl& control, Gate gate, unsigned int worker); /// Install an internal test boundary gate @@ -93,9 +95,9 @@ namespace Gecode { namespace Search { unsigned int worker); /// Release all workers and disable an installed gate static void gate_release_all(WorkerControl& control, Gate gate); - /// Begin one admitted DFS action + /// Begin one admitted parallel-search action static void action_begin(WorkerControl& control); - /// End one admitted DFS action + /// End one admitted parallel-search action static void action_end(WorkerControl& control); /// Reset the admitted-action high-water mark static void reset_max_admitted(WorkerControl& control); @@ -133,6 +135,9 @@ namespace Gecode { namespace Search { /// Return whether worker \a worker is parked static bool worker_parked(const WorkerControl& control, unsigned int worker); + /// Return incumbent deliveries observed for worker \a worker + static unsigned long long int incumbent_deliveries( + const WorkerControl& control, unsigned int worker); }; }} diff --git a/test/search.cpp b/test/search.cpp index 84454d0aff..bf4594be55 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -361,7 +361,9 @@ namespace Test { DFS_RESIZE_ENUMERATION, DFS_RESIZE_BOUNDARIES, DFS_RESIZE_HANDOFF, - DFS_RESIZE_LIFECYCLE + DFS_RESIZE_LIFECYCLE, + BAB_RESIZE_INCUMBENTS, + BAB_RESIZE_LIFECYCLE } scenario; /// Stable finite tree used by the DFS resizing tests @@ -403,6 +405,39 @@ namespace Test { } }; + /// Finite minimization tree with a unique optimum + class BABResizeSpace : public Gecode::Space { + public: + Gecode::IntVar x; + + BABResizeSpace(void) + : x(*this,0,4095) { + Gecode::branch(*this,x,Gecode::INT_VAL_MAX()); + } + + explicit BABResizeSpace(int value) + : x(*this,value,value) {} + + BABResizeSpace(BABResizeSpace& s) + : Gecode::Space(s) { + x.update(*this,s.x); + } + + virtual Gecode::Space* copy(void) { + return new BABResizeSpace(*this); + } + + virtual void constrain(const Gecode::Space& _s) { + const BABResizeSpace& s = + static_cast(_s); + Gecode::rel(*this,x,Gecode::IRT_LE,s.x.val()); + } + + int value(void) const { + return x.val(); + } + }; + /// Thread-safe trace lifecycle observations class CountingTracer : public Gecode::SearchTracer { public: @@ -478,6 +513,20 @@ namespace Test { return e; } + static Gecode::Search::Engine* + bab_resize_engine(Gecode::Search::Options& o) { + BABResizeSpace* m = new BABResizeSpace; + Gecode::Search::Engine* e; + try { + e = Gecode::Search::babengine(m,o); + } catch (...) { + delete m; + throw; + } + delete m; + return e; + } + static bool values(void) { Gecode::Search::WorkerControl empty; Gecode::Search::Options defaults; @@ -670,38 +719,27 @@ namespace Test { std::atomic solutions(0U); std::atomic controller_ok(true); + std::atomic paused(0U); + std::atomic resumed(0U); + std::atomic completed(0U); + const unsigned int limits[] = {1U,3U,1U,4U}; + const unsigned int milestones[] = {1U,64U,128U,192U}; + // Pause enumeration at each milestone so every resize precedes + // exhaustion. DFSResize::Handoff covers causal owner handoff. std::thread controller([&] { - const unsigned int limits[] = {1U,3U,1U,4U}; - const unsigned int milestones[] = {1U,64U,128U,192U}; for (unsigned int i=0U; i<4U; i++) { - while (solutions.load(std::memory_order_acquire) < - milestones[i]) + while (paused.load(std::memory_order_acquire) < i+1U) std::this_thread::yield(); - if (i == 0U) { - bool multiple_owners = false; - for (unsigned int spin=0U; spin<10000000U; spin++) { - if (Gecode::Search::WorkerControlAccess::owners(control) >= - 2U) { - multiple_owners = true; - break; - } - std::this_thread::yield(); - } - if (!multiple_owners) { - controller_ok.store(false,std::memory_order_release); - return; - } - } - unsigned long long int handoffs = - Gecode::Search::WorkerControlAccess::handoffs(control); try { control.request(limits[i]); } catch (...) { controller_ok.store(false,std::memory_order_release); + resumed.store(i+1U,std::memory_order_release); return; } unsigned long long int generation = Gecode::Search::WorkerControlAccess::generation(control); + resumed.store(i+1U,std::memory_order_release); bool converged = false; for (unsigned int spin=0U; spin<10000000U; spin++) { if ((Gecode::Search::WorkerControlAccess:: @@ -709,10 +747,7 @@ namespace Test { (Gecode::Search::WorkerControlAccess::leases(control) <= limits[i]) && (Gecode::Search::WorkerControlAccess::parked(control) >= - 4U-limits[i]) && - ((i != 0U) || - (Gecode::Search::WorkerControlAccess:: - parked_owners(control) > 0U))) { + 4U-limits[i])) { converged = true; break; } @@ -720,31 +755,31 @@ namespace Test { } if (!converged) { controller_ok.store(false,std::memory_order_release); + resumed.store(4U,std::memory_order_release); + completed.store(4U,std::memory_order_release); return; } - if (i == 0U) { - bool handed_off = false; - for (unsigned int spin=0U; spin<50000000U; spin++) { - if (Gecode::Search::WorkerControlAccess::handoffs(control) > - handoffs) { - handed_off = true; - break; - } - std::this_thread::yield(); - } - if (!handed_off) { - controller_ok.store(false,std::memory_order_release); - return; - } - } + completed.store(i+1U,std::memory_order_release); } }); std::vector counts(1U << 12,0U); + unsigned int next_milestone = 0U; while (ResizeSpace* solution = dfs.next()) { counts[solution->id()]++; delete solution; - (void) solutions.fetch_add(1U,std::memory_order_release); + unsigned int n = + solutions.fetch_add(1U,std::memory_order_release)+1U; + if ((next_milestone < 4U) && + (n >= milestones[next_milestone]) && + (completed.load(std::memory_order_acquire) >= + next_milestone)) { + paused.store(next_milestone+1U,std::memory_order_release); + while (resumed.load(std::memory_order_acquire) < + next_milestone+1U) + std::this_thread::yield(); + next_milestone++; + } } controller.join(); if (!controller_ok.load(std::memory_order_acquire) || @@ -1033,6 +1068,169 @@ namespace Test { #endif } + static bool bab_resize_incumbents(void) { +#ifdef GECODE_HAS_THREADS + using Gecode::Search::WorkerControlAccess; + Gecode::Search::WorkerControl control(4U); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + Gecode::Search::Engine* bab = bab_resize_engine(o); + + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,4U); + std::atomic first(nullptr); + std::atomic first_done(false); + std::atomic proceed(false); + std::atomic solutions(0U); + std::atomic best(4096); + std::thread consumer([&] { + BABResizeSpace* solution = + static_cast(bab->next()); + first.store(solution,std::memory_order_release); + first_done.store(true,std::memory_order_release); + while (!proceed.load(std::memory_order_acquire)) + std::this_thread::yield(); + while (solution != nullptr) { + best.store(solution->value(),std::memory_order_release); + (void) solutions.fetch_add(1U,std::memory_order_release); + delete solution; + solution = static_cast(bab->next()); + } + }); + + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,3U); + control.request(1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,2U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,3U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_EVENT_WAIT); + + unsigned long long int before_external[4]; + for (unsigned int i=0U; i<4U; i++) + before_external[i] = + WorkerControlAccess::incumbent_deliveries(control,i); + BABResizeSpace external(3000); + (void) external.status(); + bab->constrain(external); + bool ok = true; + for (unsigned int i=0U; i<4U; i++) + ok = ok && + (WorkerControlAccess::incumbent_deliveries(control,i) == + before_external[i]+1U); + ok = ok && WorkerControlAccess::worker_parked(control,1U) && + WorkerControlAccess::worker_parked(control,2U) && + WorkerControlAccess::worker_parked(control,3U); + + unsigned long long int before_internal[4]; + for (unsigned int i=0U; i<4U; i++) + before_internal[i] = + WorkerControlAccess::incumbent_deliveries(control,i); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,0U); + while (!first_done.load(std::memory_order_acquire)) + std::this_thread::yield(); + BABResizeSpace* first_solution = + first.load(std::memory_order_acquire); + ok = ok && (first_solution != nullptr) && + (first_solution->value() < external.value()); + for (unsigned int i=0U; i<4U; i++) + ok = ok && + (WorkerControlAccess::incumbent_deliveries(control,i) >= + before_internal[i]+1U); + + // Exercise the equal-to-capacity transition from the exact parked + // state above. The pending request is reconciled when next resumes. + control.request(4U); + unsigned long long int generation = + WorkerControlAccess::generation(control); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); + proceed.store(true,std::memory_order_release); + ok = ok && converged(control,generation,4U) && + (control.requested() == control.capacity()); + + consumer.join(); + ok = ok && !bab->stopped() && + (solutions.load(std::memory_order_acquire) > 0U) && + (best.load(std::memory_order_acquire) == 0); + delete bab; + return ok; +#else + Gecode::Search::WorkerControl control(1U); + Gecode::Search::Options o; + o.threads = 1.0; + o.worker_control = control; + Gecode::Search::Engine* bab = bab_resize_engine(o); + BABResizeSpace external(3000); + (void) external.status(); + bab->constrain(external); + int best = 4096; + while (BABResizeSpace* solution = + static_cast(bab->next())) { + best = solution->value(); + delete solution; + } + bool ok = (control.capacity() == 1U) && + (control.requested() == 1U) && !bab->stopped() && (best == 0); + delete bab; + return ok; +#endif + } + + static bool bab_resize_lifecycle(void) { + Gecode::Search::WorkerControl control(capacity()); + Gecode::Search::NodeStop stop(0U); + CountingTracer tracer; + Gecode::Search::Options o; + o.threads = 4.0; + o.nogoods_limit = 16U; + o.stop = &stop; + o.tracer = &tracer; + o.worker_control = control; + Gecode::Search::Engine* bab = bab_resize_engine(o); + + BABResizeSpace* solution = + static_cast(bab->next()); + bool ok = (solution == nullptr) && bab->stopped() && + (bab->statistics().node > 0U); + delete solution; + (void) bab->nogoods(); + + control.request(capacity()); + BABResizeSpace* root = new BABResizeSpace; + bab->reset(root); + control.request(1U); + BABResizeSpace external(1000); + (void) external.status(); + bab->constrain(external); + stop.limit(100000U); + int best = 4096; + while ((solution = static_cast(bab->next())) + != nullptr) { + best = solution->value(); + delete solution; + } + ok = ok && !bab->stopped() && (best == 0) && + (control.requested() == 1U); + delete bab; + return ok && + (tracer.init_count.load(std::memory_order_acquire) == 1U) && + (tracer.node_count.load(std::memory_order_acquire) > 0U) && + (tracer.done_count.load(std::memory_order_acquire) == 1U) && + (tracer.nodes_at_done.load(std::memory_order_acquire) == + tracer.node_count.load(std::memory_order_acquire)); + } + public: WorkerControl(const std::string& name, Scenario s) : Base("Search::WorkerControl::"+name), scenario(s) {} @@ -1048,6 +1246,8 @@ namespace Test { case DFS_RESIZE_BOUNDARIES: return resize_boundaries(); case DFS_RESIZE_HANDOFF: return resize_handoff(); case DFS_RESIZE_LIFECYCLE: return resize_lifecycle(); + case BAB_RESIZE_INCUMBENTS: return bab_resize_incumbents(); + case BAB_RESIZE_LIFECYCLE: return bab_resize_lifecycle(); default: GECODE_NEVER; } return false; @@ -1067,6 +1267,10 @@ namespace Test { DFS_RESIZE_HANDOFF); (void) new WorkerControl("DFSResize::Lifecycle", DFS_RESIZE_LIFECYCLE); + (void) new WorkerControl("BABResize::Incumbents", + BAB_RESIZE_INCUMBENTS); + (void) new WorkerControl("BABResize::Lifecycle", + BAB_RESIZE_LIFECYCLE); } }; From 531255e01e8e99ad455433d29e2a080f1350ff64 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 04:19:46 +0100 Subject: [PATCH 05/15] Strengthen parallel worker control evidence --- gecode/search/par/bab.hpp | 4 +- gecode/search/par/dfs.hpp | 4 +- gecode/search/par/engine.hh | 4 +- gecode/search/par/engine.hpp | 9 +- gecode/search/worker-control.cpp | 103 +++++++- gecode/search/worker-control.hh | 15 +- test/search.cpp | 392 +++++++++++++++++++++++++++++-- 7 files changed, 485 insertions(+), 46 deletions(-) diff --git a/gecode/search/par/bab.hpp b/gecode/search/par/bab.hpp index a25004dab5..d9e5a05259 100755 --- a/gecode/search/par/bab.hpp +++ b/gecode/search/par/bab.hpp @@ -268,7 +268,7 @@ namespace Gecode { namespace Search { namespace Par { { if (!engine().scheduler_admit(index)) break; - engine().scheduler_action_begin(); + engine().scheduler_action_begin(index); m.acquire(); if (idle) { m.release(); @@ -365,7 +365,7 @@ namespace Gecode { namespace Search { namespace Par { engine().scheduler_idle(index); engine().scheduler_handoff(index,engine().work_remains()); } - engine().scheduler_action_end(); + engine().scheduler_action_end(index); } break; default: diff --git a/gecode/search/par/dfs.hpp b/gecode/search/par/dfs.hpp index 518f5b4566..7bcb80565c 100755 --- a/gecode/search/par/dfs.hpp +++ b/gecode/search/par/dfs.hpp @@ -210,7 +210,7 @@ namespace Gecode { namespace Search { namespace Par { { if (!engine().scheduler_admit(index)) break; - engine().scheduler_action_begin(); + engine().scheduler_action_begin(index); m.acquire(); if (idle) { m.release(); @@ -308,7 +308,7 @@ namespace Gecode { namespace Search { namespace Par { engine().scheduler_idle(index); engine().scheduler_handoff(index,engine().work_remains()); } - engine().scheduler_action_end(); + engine().scheduler_action_end(index); } break; default: diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index 28aa4b0693..8223f25cc4 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -132,9 +132,9 @@ namespace Gecode { namespace Search { namespace Par { /// Admit worker \a worker for one exploration action bool scheduler_admit(unsigned int worker); /// Begin an admitted exploration action - void scheduler_action_begin(void); + void scheduler_action_begin(unsigned int worker); /// End an admitted exploration action - void scheduler_action_end(void); + void scheduler_action_end(unsigned int worker); /// Pause after worker \a worker completes a failed steal scan void scheduler_failed_scan(unsigned int worker); /// Record that worker \a worker owns search diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index fe66ddbe6b..100b12cd6f 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -296,16 +296,17 @@ namespace Gecode { namespace Search { namespace Par { template forceinline void - Engine::scheduler_action_begin(void) { + Engine::scheduler_action_begin(unsigned int worker) { if (scheduler_enabled) - WorkerControlAccess::action_begin(worker_control); + WorkerControlAccess::action_begin( + worker_control,worker,scheduler_generation); } template forceinline void - Engine::scheduler_action_end(void) { + Engine::scheduler_action_end(unsigned int worker) { if (scheduler_enabled) - WorkerControlAccess::action_end(worker_control); + WorkerControlAccess::action_end(worker_control,worker); } template diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 2ea1b1c498..deab29940d 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -59,7 +59,11 @@ namespace Gecode { namespace Search { std::atomic* seen; std::atomic* waiting; std::atomic admitted; + std::atomic generation_admitted; std::atomic max_admitted; + std::atomic measured_generation; + std::atomic completed_generation; + std::atomic* action_generation; std::atomic* logical; std::atomic* lease; std::atomic* parked; @@ -73,7 +77,10 @@ namespace Gecode { namespace Search { [WorkerControlAccess::GATE_COUNT * capacity]), waiting(new std::atomic [WorkerControlAccess::GATE_COUNT * capacity]), - admitted(0U), max_admitted(0U), + admitted(0U), generation_admitted(0U), max_admitted(0U), + measured_generation(0U), completed_generation(0U), + action_generation( + new std::atomic[capacity]), logical(new std::atomic[capacity]), lease(new std::atomic[capacity]), parked(new std::atomic[capacity]), @@ -84,6 +91,7 @@ namespace Gecode { namespace Search { lease[i].store(false,std::memory_order_relaxed); parked[i].store(false,std::memory_order_relaxed); incumbent_deliveries[i].store(0U,std::memory_order_relaxed); + action_generation[i].store(0U,std::memory_order_relaxed); } for (unsigned int g=0U; grelease[g*capacity+i].signal(); } + bool + WorkerControlAccess::gate_waiting(const WorkerControl& control, Gate gate, + unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return false; + WorkerControl::State::TestSupport* support = + state->test_support.load(std::memory_order_acquire); + if ((support == nullptr) || (worker >= support->capacity)) + return false; + unsigned int g = static_cast(gate); + return support->waiting[g*support->capacity+worker].load( + std::memory_order_acquire); + } + void - WorkerControlAccess::action_begin(WorkerControl& control) { + WorkerControlAccess::action_begin( + WorkerControl& control, unsigned int worker, + const std::atomic& generation) { WorkerControl::State* state = control.state; if (state == nullptr) return; @@ -539,38 +565,85 @@ namespace Gecode { namespace Search { state->test_support.load(std::memory_order_acquire); if (support == nullptr) return; + unsigned long long int action_generation = + generation.load(std::memory_order_acquire); + support->action_generation[worker].store(action_generation, + std::memory_order_release); unsigned int current = support->admitted.fetch_add(1U,std::memory_order_acq_rel) + 1U; - unsigned int maximum = - support->max_admitted.load(std::memory_order_relaxed); - while ((maximum < current) && - !support->max_admitted.compare_exchange_weak( - maximum,current,std::memory_order_release, - std::memory_order_relaxed)) {} + { + Support::Lock lock(state->mutex); + unsigned long long int measured = + support->measured_generation.load(std::memory_order_relaxed); + if (measured != 0U) { + if (action_generation != measured) + return; + current = support->generation_admitted.fetch_add( + 1U,std::memory_order_relaxed) + 1U; + } + unsigned int maximum = + support->max_admitted.load(std::memory_order_relaxed); + if (maximum < current) + support->max_admitted.store(current,std::memory_order_release); + } } void - WorkerControlAccess::action_end(WorkerControl& control) { + WorkerControlAccess::action_end(WorkerControl& control, + unsigned int worker) { if (control.state == nullptr) return; WorkerControl::State::TestSupport* support = control.state->test_support.load(std::memory_order_acquire); - if (support != nullptr) + if (support != nullptr) { (void) support->admitted.fetch_sub(1U,std::memory_order_release); + unsigned long long int generation = + support->action_generation[worker].load(std::memory_order_acquire); + { + Support::Lock lock(control.state->mutex); + if (support->measured_generation.load(std::memory_order_relaxed) == + generation) + (void) support->generation_admitted.fetch_sub( + 1U,std::memory_order_relaxed); + } + unsigned long long int old = + support->completed_generation.load(std::memory_order_relaxed); + while ((old < generation) && + !support->completed_generation.compare_exchange_weak( + old,generation,std::memory_order_release, + std::memory_order_relaxed)) {} + } } void WorkerControlAccess::reset_max_admitted(WorkerControl& control) { if (control.state != nullptr) { + Support::Lock lock(control.state->mutex); WorkerControl::State::TestSupport* support = control.state->test_support.load(std::memory_order_acquire); assert(support != nullptr); unsigned int current = support->admitted.load(std::memory_order_acquire); + support->measured_generation.store(0U,std::memory_order_release); support->max_admitted.store(current,std::memory_order_release); } } + void + WorkerControlAccess::reset_max_admitted( + WorkerControl& control, unsigned long long int generation) { + if (control.state != nullptr) { + Support::Lock lock(control.state->mutex); + WorkerControl::State::TestSupport* support = + control.state->test_support.load(std::memory_order_acquire); + assert(support != nullptr); + support->generation_admitted.store(0U,std::memory_order_release); + support->max_admitted.store(0U,std::memory_order_release); + support->measured_generation.store(generation, + std::memory_order_release); + } + } + unsigned int WorkerControlAccess::admitted(const WorkerControl& control) { if (control.state == nullptr) @@ -591,6 +664,16 @@ namespace Gecode { namespace Search { support->max_admitted.load(std::memory_order_acquire); } + unsigned long long int + WorkerControlAccess::completed_generation(const WorkerControl& control) { + if (control.state == nullptr) + return 0U; + WorkerControl::State::TestSupport* support = + control.state->test_support.load(std::memory_order_acquire); + return (support == nullptr) ? 0U : + support->completed_generation.load(std::memory_order_acquire); + } + unsigned long long int WorkerControlAccess::observed_generation(const WorkerControl& control) { return (control.state == nullptr) ? 0U : diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh index c22eb1e9cb..1380ce0591 100644 --- a/gecode/search/worker-control.hh +++ b/gecode/search/worker-control.hh @@ -95,16 +95,27 @@ namespace Gecode { namespace Search { unsigned int worker); /// Release all workers and disable an installed gate static void gate_release_all(WorkerControl& control, Gate gate); + /// Return whether worker \a worker is stopped at gate \a gate + static bool gate_waiting(const WorkerControl& control, Gate gate, + unsigned int worker); /// Begin one admitted parallel-search action - static void action_begin(WorkerControl& control); + static void action_begin( + WorkerControl& control, unsigned int worker, + const std::atomic& generation); /// End one admitted parallel-search action - static void action_end(WorkerControl& control); + static void action_end(WorkerControl& control, unsigned int worker); /// Reset the admitted-action high-water mark static void reset_max_admitted(WorkerControl& control); + /// Reset the high-water mark for actions admitted in \a generation + static void reset_max_admitted(WorkerControl& control, + unsigned long long int generation); /// Return the current admitted-action count static unsigned int admitted(const WorkerControl& control); /// Return the admitted-action high-water mark static unsigned int max_admitted(const WorkerControl& control); + /// Return the newest generation with a completed admitted action + static unsigned long long int completed_generation( + const WorkerControl& control); /// Return the scheduler's observed request generation static unsigned long long int observed_generation( const WorkerControl& control); diff --git a/test/search.cpp b/test/search.cpp index bf4594be55..0bfe5e7c79 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -362,6 +362,8 @@ namespace Test { DFS_RESIZE_BOUNDARIES, DFS_RESIZE_HANDOFF, DFS_RESIZE_LIFECYCLE, + BAB_RESIZE_OPTIMALITY_DEFAULT, + BAB_RESIZE_OPTIMALITY_FREQUENT, BAB_RESIZE_INCUMBENTS, BAB_RESIZE_LIFECYCLE } scenario; @@ -418,6 +420,11 @@ namespace Test { explicit BABResizeSpace(int value) : x(*this,value,value) {} + BABResizeSpace(int min, int max) + : x(*this,min,max) { + Gecode::branch(*this,x,Gecode::INT_VAL_MAX()); + } + BABResizeSpace(BABResizeSpace& s) : Gecode::Space(s) { x.update(*this,s.x); @@ -478,6 +485,22 @@ namespace Test { return false; } + static bool settled(const Gecode::Search::WorkerControl& control, + unsigned long long int generation, + unsigned int leases) { + for (unsigned int spin=0U; spin<10000000U; spin++) { + if ((Gecode::Search::WorkerControlAccess:: + observed_generation(control) == generation) && + (Gecode::Search::WorkerControlAccess::leases(control) == + leases) && + (Gecode::Search::WorkerControlAccess::admitted(control) <= + leases)) + return true; + std::this_thread::yield(); + } + return false; + } + template static bool throws(Function function) { try { @@ -527,6 +550,19 @@ namespace Test { return e; } + static int bab_resize_best(Gecode::Search::Options o) { + Gecode::Search::Engine* bab = bab_resize_engine(o); + int best = 4096; + while (BABResizeSpace* solution = + static_cast(bab->next())) { + best = solution->value(); + delete solution; + } + bool ok = !bab->stopped(); + delete bab; + return ok ? best : 4096; + } + static bool values(void) { Gecode::Search::WorkerControl empty; Gecode::Search::Options defaults; @@ -720,28 +756,39 @@ namespace Test { std::atomic solutions(0U); std::atomic controller_ok(true); std::atomic paused(0U); - std::atomic resumed(0U); + std::atomic published(0U); std::atomic completed(0U); + std::atomic exhausted(false); const unsigned int limits[] = {1U,3U,1U,4U}; const unsigned int milestones[] = {1U,64U,128U,192U}; - // Pause enumeration at each milestone so every resize precedes - // exhaustion. DFSResize::Handoff covers causal owner handoff. + // The consumer processes no solution beyond a milestone until the + // controller has observed convergence for that milestone's request. std::thread controller([&] { for (unsigned int i=0U; i<4U; i++) { - while (paused.load(std::memory_order_acquire) < i+1U) + while ((paused.load(std::memory_order_acquire) < i+1U) && + !exhausted.load(std::memory_order_acquire)) std::this_thread::yield(); + if (exhausted.load(std::memory_order_acquire)) { + controller_ok.store(false,std::memory_order_release); + published.store(4U,std::memory_order_release); + completed.store(4U,std::memory_order_release); + return; + } try { control.request(limits[i]); } catch (...) { controller_ok.store(false,std::memory_order_release); - resumed.store(i+1U,std::memory_order_release); + published.store(4U,std::memory_order_release); + completed.store(4U,std::memory_order_release); return; } unsigned long long int generation = Gecode::Search::WorkerControlAccess::generation(control); - resumed.store(i+1U,std::memory_order_release); + published.store(i+1U,std::memory_order_release); bool converged = false; for (unsigned int spin=0U; spin<10000000U; spin++) { + if (exhausted.load(std::memory_order_acquire)) + break; if ((Gecode::Search::WorkerControlAccess:: observed_generation(control) == generation) && (Gecode::Search::WorkerControlAccess::leases(control) <= @@ -755,7 +802,6 @@ namespace Test { } if (!converged) { controller_ok.store(false,std::memory_order_release); - resumed.store(4U,std::memory_order_release); completed.store(4U,std::memory_order_release); return; } @@ -764,20 +810,43 @@ namespace Test { }); std::vector counts(1U << 12,0U); + std::vector pending; unsigned int next_milestone = 0U; - while (ResizeSpace* solution = dfs.next()) { + bool search_exhausted = false; + while (!search_exhausted || !pending.empty()) { + ResizeSpace* solution; + if (pending.empty()) { + solution = dfs.next(); + if (solution == nullptr) { + search_exhausted = true; + exhausted.store(true,std::memory_order_release); + continue; + } + } else { + solution = pending.back(); + pending.pop_back(); + } counts[solution->id()]++; delete solution; unsigned int n = solutions.fetch_add(1U,std::memory_order_release)+1U; if ((next_milestone < 4U) && - (n >= milestones[next_milestone]) && - (completed.load(std::memory_order_acquire) >= - next_milestone)) { + (n == milestones[next_milestone])) { + unsigned int phase = next_milestone+1U; paused.store(next_milestone+1U,std::memory_order_release); - while (resumed.load(std::memory_order_acquire) < - next_milestone+1U) + while ((published.load(std::memory_order_acquire) < phase) && + controller_ok.load(std::memory_order_acquire)) std::this_thread::yield(); + while ((completed.load(std::memory_order_acquire) < phase) && + controller_ok.load(std::memory_order_acquire)) { + ResizeSpace* extra = dfs.next(); + if (extra == nullptr) { + search_exhausted = true; + exhausted.store(true,std::memory_order_release); + break; + } + pending.push_back(extra); + } next_milestone++; } } @@ -1068,6 +1137,124 @@ namespace Test { #endif } + static bool bab_resize_optimality(bool frequent) { +#ifdef GECODE_HAS_THREADS + using Gecode::Search::WorkerControlAccess; + Gecode::Search::Options baseline_options; + baseline_options.threads = 4.0; + if (frequent) { + baseline_options.c_d = 1U; + baseline_options.a_d = 1U; + } + int baseline = bab_resize_best(baseline_options); + + Gecode::Search::WorkerControl control(4U); + Gecode::Search::Options o(baseline_options); + o.worker_control = control; + Gecode::Search::Engine* bab = bab_resize_engine(o); + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,4U); + std::atomic best(4096); + std::atomic allowed(0U); + std::atomic completed(0U); + std::atomic phase_generation(0U); + std::atomic complete(false); + std::atomic consumer_ok(true); + std::thread consumer([&] { + for (unsigned int phase=0U; phase<4U; phase++) { + while (allowed.load(std::memory_order_acquire) <= phase) + std::this_thread::yield(); + do { + BABResizeSpace* solution = + static_cast(bab->next()); + if (solution == nullptr) { + consumer_ok.store(false,std::memory_order_release); + completed.store(4U,std::memory_order_release); + return; + } + best.store(solution->value(),std::memory_order_release); + delete solution; + } while (WorkerControlAccess::completed_generation(control) < + phase_generation.load(std::memory_order_acquire)); + while (WorkerControlAccess::admitted(control) != 0U) + std::this_thread::yield(); + completed.store(phase+1U,std::memory_order_release); + } + while (BABResizeSpace* solution = + static_cast(bab->next())) { + best.store(solution->value(),std::memory_order_release); + delete solution; + } + complete.store(true,std::memory_order_release); + }); + + allowed.store(1U,std::memory_order_release); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,3U); + control.request(1U); + unsigned long long int generation = + WorkerControlAccess::generation(control); + phase_generation.store(generation,std::memory_order_release); + WorkerControlAccess::reset_max_admitted(control,generation); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,2U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,3U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_EVENT_WAIT); + bool ok = settled(control,generation,1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,0U); + while (completed.load(std::memory_order_acquire) < 1U) + std::this_thread::yield(); + ok = ok && consumer_ok.load(std::memory_order_acquire) && + (WorkerControlAccess::max_admitted(control) <= 1U); + + const unsigned int limits[] = {3U,1U,4U}; + for (unsigned int i=0U; i<3U; i++) { + unsigned int limit = limits[i]; + control.request(limit); + generation = WorkerControlAccess::generation(control); + phase_generation.store(generation,std::memory_order_release); + WorkerControlAccess::reset_max_admitted(control,generation); + if (limit > 1U) + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); + allowed.store(i+2U,std::memory_order_release); + while (completed.load(std::memory_order_acquire) < i+2U) + std::this_thread::yield(); + ok = ok && consumer_ok.load(std::memory_order_acquire) && + settled(control,generation,limit) && + (WorkerControlAccess::max_admitted(control) <= limit); + } + + consumer.join(); + ok = ok && complete.load(std::memory_order_acquire) && + !bab->stopped() && (baseline == 0) && + (best.load(std::memory_order_acquire) == baseline); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); + delete bab; + return ok; +#else + Gecode::Search::Options o; + o.threads = 1.0; + if (frequent) { + o.c_d = 1U; + o.a_d = 1U; + } + return bab_resize_best(o) == 0; +#endif + } + static bool bab_resize_incumbents(void) { #ifdef GECODE_HAS_THREADS using Gecode::Search::WorkerControlAccess; @@ -1083,6 +1270,7 @@ namespace Test { std::atomic first(nullptr); std::atomic first_done(false); std::atomic proceed(false); + std::atomic solutions_better(true); std::atomic solutions(0U); std::atomic best(4096); std::thread consumer([&] { @@ -1093,6 +1281,8 @@ namespace Test { while (!proceed.load(std::memory_order_acquire)) std::this_thread::yield(); while (solution != nullptr) { + if (solution->value() >= 3000) + solutions_better.store(false,std::memory_order_release); best.store(solution->value(),std::memory_order_release); (void) solutions.fetch_add(1U,std::memory_order_release); delete solution; @@ -1161,9 +1351,87 @@ namespace Test { consumer.join(); ok = ok && !bab->stopped() && + solutions_better.load(std::memory_order_acquire) && (solutions.load(std::memory_order_acquire) > 0U) && (best.load(std::memory_order_acquire) == 0); delete bab; + + // Repeat external constrain while a resize generation is pending + // and worker 0 remains stopped at the admission boundary. + Gecode::Search::WorkerControl transition_control(4U); + Gecode::Search::Options transition_options; + transition_options.threads = 4.0; + transition_options.worker_control = transition_control; + bab = bab_resize_engine(transition_options); + WorkerControlAccess::gate_install( + transition_control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,4U); + std::atomic transition_ok(true); + std::atomic transition_best(4096); + std::thread transition_consumer([&] { + while (BABResizeSpace* solution = + static_cast(bab->next())) { + if (solution->value() >= 2000) + transition_ok.store(false,std::memory_order_release); + transition_best.store(solution->value(), + std::memory_order_release); + delete solution; + } + }); + WorkerControlAccess::gate_wait( + transition_control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_install( + transition_control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,3U); + transition_control.request(1U); + WorkerControlAccess::gate_release( + transition_control,WorkerControlAccess::GATE_ADMISSION,1U); + WorkerControlAccess::gate_release( + transition_control,WorkerControlAccess::GATE_ADMISSION,2U); + WorkerControlAccess::gate_release( + transition_control,WorkerControlAccess::GATE_ADMISSION,3U); + WorkerControlAccess::gate_wait( + transition_control,WorkerControlAccess::GATE_EVENT_WAIT); + + unsigned long long int transition_before[4]; + for (unsigned int i=0U; i<4U; i++) + transition_before[i] = + WorkerControlAccess::incumbent_deliveries( + transition_control,i); + transition_control.request(3U); + generation = + WorkerControlAccess::generation(transition_control); + BABResizeSpace transition_external(2000); + (void) transition_external.status(); + bab->constrain(transition_external); + ok = ok && + WorkerControlAccess::gate_waiting( + transition_control,WorkerControlAccess::GATE_ADMISSION,0U) && + (WorkerControlAccess::observed_generation(transition_control) != + generation); + for (unsigned int i=0U; i<4U; i++) + ok = ok && + (WorkerControlAccess::incumbent_deliveries( + transition_control,i) == transition_before[i]+1U); + ok = ok && + WorkerControlAccess::worker_parked(transition_control,1U) && + WorkerControlAccess::worker_parked(transition_control,2U) && + WorkerControlAccess::worker_parked(transition_control,3U); + + // Worker 1 reconciles the pending generation before any original + // active worker leaves admission. + WorkerControlAccess::gate_release( + transition_control,WorkerControlAccess::GATE_EVENT_WAIT,1U); + ok = ok && settled(transition_control,generation,3U); + WorkerControlAccess::gate_release_all( + transition_control,WorkerControlAccess::GATE_EVENT_WAIT); + WorkerControlAccess::gate_release_all( + transition_control,WorkerControlAccess::GATE_ADMISSION); + transition_consumer.join(); + ok = ok && transition_ok.load(std::memory_order_acquire) && + !bab->stopped() && + (transition_best.load(std::memory_order_acquire) == 0); + delete bab; return ok; #else Gecode::Search::WorkerControl control(1U); @@ -1188,6 +1456,8 @@ namespace Test { } static bool bab_resize_lifecycle(void) { +#ifdef GECODE_HAS_THREADS + using Gecode::Search::WorkerControlAccess; Gecode::Search::WorkerControl control(capacity()); Gecode::Search::NodeStop stop(0U); CountingTracer tracer; @@ -1199,36 +1469,102 @@ namespace Test { o.worker_control = control; Gecode::Search::Engine* bab = bab_resize_engine(o); + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ADMISSION, + WorkerControlAccess::ALL_WORKERS,4U); + std::atomic stopped_result(nullptr); + std::thread stopped_next([&] { + stopped_result.store( + static_cast(bab->next()), + std::memory_order_release); + }); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_ADMISSION); + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_EVENT_WAIT, + WorkerControlAccess::ALL_WORKERS,3U); + control.request(1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,1U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,2U); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,3U); + WorkerControlAccess::gate_wait( + control,WorkerControlAccess::GATE_EVENT_WAIT); + BABResizeSpace old_external(1000); + (void) old_external.status(); + bab->constrain(old_external); + bool ok = (WorkerControlAccess::parked(control) == 3U); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_EVENT_WAIT); + WorkerControlAccess::gate_release( + control,WorkerControlAccess::GATE_ADMISSION,0U); + stopped_next.join(); BABResizeSpace* solution = - static_cast(bab->next()); - bool ok = (solution == nullptr) && bab->stopped() && - (bab->statistics().node > 0U); + stopped_result.load(std::memory_order_acquire); + ok = ok && (solution == nullptr) && bab->stopped() && + (bab->statistics().node > 0U) && + (WorkerControlAccess::parked(control) >= 3U); delete solution; (void) bab->nogoods(); + ok = ok && (WorkerControlAccess::leases(control) == 1U) && + (WorkerControlAccess::parked(control) >= 3U); - control.request(capacity()); - BABResizeSpace* root = new BABResizeSpace; + control.request(3U); + unsigned long long int generation = + WorkerControlAccess::generation(control); + BABResizeSpace* root = new BABResizeSpace(2000,4095); bab->reset(root); - control.request(1U); - BABResizeSpace external(1000); + ok = ok && settled(control,generation,3U); + stop.limit(100000U); + BABResizeSpace external(3000); (void) external.status(); bab->constrain(external); - stop.limit(100000U); int best = 4096; while ((solution = static_cast(bab->next())) != nullptr) { best = solution->value(); delete solution; } - ok = ok && !bab->stopped() && (best == 0) && - (control.requested() == 1U); + // Reaching 2000 proves reset discarded the prior bound of 1000. + ok = ok && !bab->stopped() && (best == 2000) && + (control.requested() == 3U); delete bab; - return ok && + bool trace_ok = (tracer.init_count.load(std::memory_order_acquire) == 1U) && (tracer.node_count.load(std::memory_order_acquire) > 0U) && (tracer.done_count.load(std::memory_order_acquire) == 1U) && (tracer.nodes_at_done.load(std::memory_order_acquire) == tracer.node_count.load(std::memory_order_acquire)); + return ok && trace_ok; +#else + Gecode::Search::WorkerControl control(1U); + Gecode::Search::Options o; + o.threads = 1.0; + o.worker_control = control; + Gecode::Search::Engine* bab = bab_resize_engine(o); + control.request(1U); + BABResizeSpace old_external(1000); + (void) old_external.status(); + bab->constrain(old_external); + BABResizeSpace* root = new BABResizeSpace(2000,4095); + bab->reset(root); + BABResizeSpace external(3000); + (void) external.status(); + bab->constrain(external); + BABResizeSpace* solution; + int best = 4096; + while ((solution = static_cast(bab->next())) + != nullptr) { + best = solution->value(); + delete solution; + } + bool ok = !bab->stopped() && (best == 2000) && + (control.capacity() == 1U) && (control.requested() == 1U); + delete bab; + return ok; +#endif } public: @@ -1246,6 +1582,10 @@ namespace Test { case DFS_RESIZE_BOUNDARIES: return resize_boundaries(); case DFS_RESIZE_HANDOFF: return resize_handoff(); case DFS_RESIZE_LIFECYCLE: return resize_lifecycle(); + case BAB_RESIZE_OPTIMALITY_DEFAULT: + return bab_resize_optimality(false); + case BAB_RESIZE_OPTIMALITY_FREQUENT: + return bab_resize_optimality(true); case BAB_RESIZE_INCUMBENTS: return bab_resize_incumbents(); case BAB_RESIZE_LIFECYCLE: return bab_resize_lifecycle(); default: GECODE_NEVER; @@ -1267,6 +1607,10 @@ namespace Test { DFS_RESIZE_HANDOFF); (void) new WorkerControl("DFSResize::Lifecycle", DFS_RESIZE_LIFECYCLE); + (void) new WorkerControl("BABResize::Optimality::Default", + BAB_RESIZE_OPTIMALITY_DEFAULT); + (void) new WorkerControl("BABResize::Optimality::Frequent", + BAB_RESIZE_OPTIMALITY_FREQUENT); (void) new WorkerControl("BABResize::Incumbents", BAB_RESIZE_INCUMBENTS); (void) new WorkerControl("BABResize::Lifecycle", From 03d4d329a736adb1fe6d45ddf54b91e30624bb95 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 07:02:08 +0100 Subject: [PATCH 06/15] Complete worker control across meta search --- gecode/search.hh | 17 +- gecode/search/par/bab.hpp | 2 + gecode/search/par/dfs.hpp | 2 + gecode/search/par/engine.hpp | 3 + gecode/search/par/pbs.hpp | 32 +- gecode/search/pbs.hpp | 237 ++++++-- gecode/search/worker-control.cpp | 35 +- gecode/search/worker-control.hh | 8 +- test/search.cpp | 909 +++++++++++++++++++++++++++++++ 9 files changed, 1173 insertions(+), 72 deletions(-) diff --git a/gecode/search.hh b/gecode/search.hh index 67019b3ff3..692ab5ad6d 100755 --- a/gecode/search.hh +++ b/gecode/search.hh @@ -827,6 +827,8 @@ namespace Gecode { namespace Search { }} +#include + #include namespace Gecode { namespace Search { @@ -1316,6 +1318,10 @@ namespace Gecode { * The engine will run a portfolio with a number of assets as defined * by the options \a o. The engine supports parallel execution of * assets by using the number of threads as defined by the options. + * An external worker control in \a o is supported only for a single + * homogeneous asset. Multiple controlled assets require explicit engine + * builders, each with its own control and immutable worker capacity. + * PBS does not allocate workers or adjust those controls. * * The class \a T can implement member functions * \code virtual bool master(const MetaInfo& mi) \endcode @@ -1336,8 +1342,15 @@ namespace Gecode { public: /// Initialize with engines running copies of \a s with options \a o PBS(T* s, const Search::Options& o=Search::Options::def); - /// Initialize with engine builders \a sebs + /** + * Initialize with engine builders \a sebs + * + * The outer options must not contain a worker control. Each builder can + * instead supply a distinct control for its underlying engine. + */ PBS(T* s, SEBs& sebs, const Search::Options& o=Search::Options::def); + /// Constrain future portfolio solutions to be better than \a b + void constrain(const T& b); /// Whether engine does best solution search static const bool best = E::best; }; @@ -1348,6 +1361,8 @@ namespace Gecode { * The engine will run a portfolio with a number of assets as defined * by the options \a o. The engine supports parallel execution of * assets by using the number of threads as defined by the options. + * An external worker control is supported only when \a o selects one + * asset. PBS does not implement worker-allocation policy. * * The class \a T can implement member functions * \code virtual bool master(const MetaInfo& mi) \endcode diff --git a/gecode/search/par/bab.hpp b/gecode/search/par/bab.hpp index d9e5a05259..f3073cac98 100755 --- a/gecode/search/par/bab.hpp +++ b/gecode/search/par/bab.hpp @@ -382,6 +382,8 @@ namespace Gecode { namespace Search { namespace Par { template void BAB::reset(Space* s) { + WorkerControlAccess::gate_release_all( + this->_opt.worker_control,WorkerControlAccess::GATE_ACTION_BEGIN); // Grab wait lock for reset m_wait_reset.acquire(); // Release workers for reset diff --git a/gecode/search/par/dfs.hpp b/gecode/search/par/dfs.hpp index 7bcb80565c..74c007de7e 100755 --- a/gecode/search/par/dfs.hpp +++ b/gecode/search/par/dfs.hpp @@ -325,6 +325,8 @@ namespace Gecode { namespace Search { namespace Par { template void DFS::reset(Space* s) { + WorkerControlAccess::gate_release_all( + this->_opt.worker_control,WorkerControlAccess::GATE_ACTION_BEGIN); // Grab wait lock for reset m_wait_reset.acquire(); // Release workers for reset diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index 100b12cd6f..7a46df0c23 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -486,6 +486,9 @@ namespace Gecode { namespace Search { namespace Par { Engine::terminate(void) { // Grab the wait mutex for termination _m_wait_terminate.acquire(); + // Test gates must not keep a worker from observing termination + WorkerControlAccess::gate_release_all( + _opt.worker_control,WorkerControlAccess::GATE_ACTION_BEGIN); // Release all threads release(C_TERMINATE); // Wait until all threads have acknowledged termination request diff --git a/gecode/search/par/pbs.hpp b/gecode/search/par/pbs.hpp index 1fa593aead..00d7f42ca5 100755 --- a/gecode/search/par/pbs.hpp +++ b/gecode/search/par/pbs.hpp @@ -244,29 +244,29 @@ namespace Gecode { namespace Search { namespace Par { Space* PBS::next(void) { m.acquire(); - if (solutions.empty()) { - // Clear all - tostop.store(false, std::memory_order_release); + if (solutions.empty()) slave_stop.store(false, std::memory_order_release); + while (solutions.empty() && (n_active > 0) && + !slave_stop.load(std::memory_order_acquire)) { + // Clear the internal stop used to interrupt sibling slaves + tostop.store(false, std::memory_order_release); // Invariant: all slaves are idle! assert(n_busy == 0); assert(!tostop.load(std::memory_order_acquire)); - if (n_active > 0) { - // Run all active slaves - n_busy = n_active; - for (unsigned int i=0U; iwait(); - Support::Thread::run(slaves[i]); - } - m.release(); - // Wait for all slaves to become idle - idle.wait(); - m.acquire(); + // Run all active slaves + n_busy = n_active; + for (unsigned int i=0U; iwait(); + Support::Thread::run(slaves[i]); } + m.release(); + // Wait for all slaves to become idle + idle.wait(); + m.acquire(); } // Invariant all slaves are idle! diff --git a/gecode/search/pbs.hpp b/gecode/search/pbs.hpp index aa626cf750..258afafa20 100644 --- a/gecode/search/pbs.hpp +++ b/gecode/search/pbs.hpp @@ -33,9 +33,65 @@ #include #include +#include namespace Gecode { namespace Search { + /// Dispose partially constructed portfolio assets + forceinline void + pbscleanup(Engine** slaves, Stop** stops, unsigned int n_slaves, + Space* master) { + for (unsigned int i=0U; i 1U) && + WorkerControlAccess::engaged(opt.worker_control)) + throw WorkerControlInUse("PBS::PBS"); + } + + /// Return the number of explicit builders admitted by this portfolio + forceinline int + pbsadmitted(const SEBs& sebs, const Options& opt) { +#ifdef GECODE_HAS_THREADS + if (opt.threads > 1.0) + return std::min(static_cast(opt.threads),sebs.size()); +#endif + return sebs.size(); + } + + /// Validate worker-control placement for an explicit portfolio + forceinline void + pbscontrol(const SEBs& sebs, const Options& opt) { + if (WorkerControlAccess::engaged(opt.worker_control)) + throw WorkerControlInUse("PBS::PBS"); + int admitted = pbsadmitted(sebs,opt); + for (int i=0; ioptions().worker_control; + if (!WorkerControlAccess::engaged(x)) + continue; + for (int j=0; joptions().worker_control; + if (WorkerControlAccess::engaged(y) && + WorkerControlAccess::same_identity(x,y)) + throw WorkerControlInUse("PBS::PBS"); + } + } + } + /// A PBS engine builder template class E> class PbsBuilder : public Builder { @@ -103,19 +159,38 @@ namespace Gecode { namespace Search { unsigned int n_slaves = opt.assets; Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); + for (unsigned int i=0U; iclone(); - (void) slave->slave(i); - slaves[i] = build(slave,opt); + Space* unowned_master = master; + try { + for (unsigned int i=0U; iclone(); + try { + (void) slave->slave(i); + slaves[i] = build(slave,opt); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + return Seq::pbsengine( + slaves,stops,n_slaves,stat,opt,E::best); + } catch (...) { + pbscleanup(slaves,stops,n_slaves,unowned_master); + throw; } - - return Seq::pbsengine(slaves,stops,n_slaves,stat,opt,E::best); } template class E> @@ -127,25 +202,48 @@ namespace Gecode { namespace Search { int n_slaves = sebs.size(); Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); + for (int i=0; i(n_slaves)); - for (int i=0; ioptions().stop); - sebs[i]->options().stop = stops[i]; - sebs[i]->options().clone = false; - Space* slave = (i == n_slaves-1) ? - master : master->clone(); - (void) slave->slave(static_cast(i)); - slaves[i] = (*sebs[i])(slave); - delete sebs[i]; + Space* unowned_master = master; + bool builders_owned = true; + try { + for (int i=0; ioptions().stop); + sebs[i]->options().stop = stops[i]; + sebs[i]->options().clone = false; + Space* slave = (i == n_slaves-1) ? + master : master->clone(); + try { + (void) slave->slave(static_cast(i)); + slaves[i] = (*sebs[i])(slave); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + pbscleanup(sebs); + builders_owned = false; + return Seq::pbsengine( + slaves,stops,static_cast(n_slaves),stat,opt,best); + } catch (...) { + if (builders_owned) + pbscleanup(sebs); + pbscleanup(slaves,stops,static_cast(n_slaves), + unowned_master); + throw; } - - return Seq::pbsengine(slaves,stops,static_cast(n_slaves), - stat,opt,best); } #ifdef GECODE_HAS_THREADS @@ -158,7 +256,7 @@ namespace Gecode { namespace Search { // Limit the number of slaves to the number of threads unsigned int n_slaves = std::min(static_cast(opt.threads), - opt.assets); + opt.assets); // Redistribute additional threads to slaves opt.threads = floor(opt.threads / static_cast(n_slaves)); @@ -167,16 +265,34 @@ namespace Gecode { namespace Search { Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); - for (unsigned int i=0U; iclone(); - (void) slave->slave(static_cast(i)); - slaves[i] = build(slave,opt); + slaves[i] = nullptr; + stops[i] = nullptr; } - return Par::pbsengine(slaves,stops,n_slaves,stat,E::best); + Space* unowned_master = master; + try { + for (unsigned int i=0U; iclone(); + try { + (void) slave->slave(static_cast(i)); + slaves[i] = build(slave,opt); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + return Par::pbsengine(slaves,stops,n_slaves,stat,E::best); + } catch (...) { + pbscleanup(slaves,stops,n_slaves,unowned_master); + throw; + } } template class E> @@ -187,7 +303,7 @@ namespace Gecode { namespace Search { // Limit the number of slaves to the number of threads int n_slaves = std::min(static_cast(opt.threads), - sebs.size()); + sebs.size()); WrapTraceRecorder::engine(opt.tracer, SearchTracer::EngineType::PBS, @@ -195,24 +311,44 @@ namespace Gecode { namespace Search { Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); - for (int i=0; ioptions().stop); - sebs[i]->options().stop = stops[i]; - sebs[i]->options().clone = false; - Space* slave = (i == n_slaves-1) ? - master : master->clone(); - (void) slave->slave(static_cast(i)); - slaves[i] = (*sebs[i])(slave); - delete sebs[i]; + slaves[i] = nullptr; + stops[i] = nullptr; } - // Delete excess builders - for (int i=n_slaves; i(n_slaves), - stat,best); + Space* unowned_master = master; + bool builders_owned = true; + try { + for (int i=0; ioptions().stop); + sebs[i]->options().stop = stops[i]; + sebs[i]->options().clone = false; + Space* slave = (i == n_slaves-1) ? + master : master->clone(); + try { + (void) slave->slave(static_cast(i)); + slaves[i] = (*sebs[i])(slave); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + pbscleanup(sebs); + builders_owned = false; + return Par::pbsengine( + slaves,stops,static_cast(n_slaves),stat,best); + } catch (...) { + if (builders_owned) + pbscleanup(sebs); + pbscleanup(slaves,stops,static_cast(n_slaves), + unowned_master); + throw; + } } #endif @@ -227,6 +363,7 @@ namespace Gecode { if (opt.assets == 0) throw Search::NoAssets("PBS::PBS"); + Search::pbscontrol(opt); Search::Statistics stat; @@ -264,6 +401,9 @@ namespace Gecode { template class E> void PBS::build(T* s, SEBs& sebs, const Search::Options& o) { + Search::Options opt(o.expand()); + Search::pbscontrol(sebs,opt); + // Check whether all sebs do either best solution search or not bool best; { @@ -275,7 +415,6 @@ namespace Gecode { best = (b == sebs.size()); } - Search::Options opt(o.expand()); Search::Statistics stat; if (s->status(stat) == SS_FAILED) { @@ -308,6 +447,12 @@ namespace Gecode { build(s,sebs,o); } + template class E> + inline void + PBS::constrain(const T& b) { + e->constrain(b); + } + template class E> inline T* pbs(T* s, const Search::Options& o) { diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index deab29940d..7ec03114cf 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -30,6 +30,7 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#include #include namespace Gecode { namespace Search { @@ -270,6 +271,20 @@ namespace Gecode { namespace Search { return control.state != nullptr; } + bool + WorkerControlAccess::attached(const WorkerControl& control) { + if (control.state == nullptr) + return false; + Support::Lock lock(control.state->mutex); + return control.state->lifecycle == WorkerControl::State::ATTACHED; + } + + bool + WorkerControlAccess::same_identity(const WorkerControl& x, + const WorkerControl& y) { + return x.state == y.state; + } + unsigned int WorkerControlAccess::requested(const WorkerControl& control) { return (control.state == nullptr) ? 0U : @@ -571,21 +586,27 @@ namespace Gecode { namespace Search { std::memory_order_release); unsigned int current = support->admitted.fetch_add(1U,std::memory_order_acq_rel) + 1U; + bool measured_action = true; { Support::Lock lock(state->mutex); unsigned long long int measured = support->measured_generation.load(std::memory_order_relaxed); if (measured != 0U) { if (action_generation != measured) - return; - current = support->generation_admitted.fetch_add( - 1U,std::memory_order_relaxed) + 1U; + measured_action = false; + else + current = support->generation_admitted.fetch_add( + 1U,std::memory_order_relaxed) + 1U; + } + if (measured_action) { + unsigned int maximum = + support->max_admitted.load(std::memory_order_relaxed); + if (maximum < current) + support->max_admitted.store(current,std::memory_order_release); } - unsigned int maximum = - support->max_admitted.load(std::memory_order_relaxed); - if (maximum < current) - support->max_admitted.store(current,std::memory_order_release); } + if (measured_action) + WorkerControlAccess::gate(control,GATE_ACTION_BEGIN,worker); } void diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh index 1380ce0591..dd7d0f1dfd 100644 --- a/gecode/search/worker-control.hh +++ b/gecode/search/worker-control.hh @@ -33,8 +33,6 @@ #ifndef GECODE_SEARCH_WORKER_CONTROL_HH #define GECODE_SEARCH_WORKER_CONTROL_HH -#include - namespace Gecode { namespace Search { /// Private access to worker-control attachment state @@ -45,6 +43,7 @@ namespace Gecode { namespace Search { GATE_ADMISSION, GATE_EVENT_WAIT, GATE_FAILED_SCAN, + GATE_ACTION_BEGIN, GATE_COUNT }; /// Select every worker for a test boundary gate @@ -57,6 +56,11 @@ namespace Gecode { namespace Search { static unsigned long long int generation(const WorkerControl& control); /// Return whether \a control is engaged static bool engaged(const WorkerControl& control); + /// Return whether \a control is currently attached to a leaf engine + static bool attached(const WorkerControl& control); + /// Return whether \a x and \a y share one control identity + static bool same_identity(const WorkerControl& x, + const WorkerControl& y); /// Return the current request static unsigned int requested(const WorkerControl& control); /// Return a consistent request and generation snapshot diff --git a/test/search.cpp b/test/search.cpp index 0bfe5e7c79..270bf888ef 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -42,6 +42,7 @@ #include "test/test.hh" #include +#include #include #include #include @@ -1618,6 +1619,912 @@ namespace Test { } }; + /// Shared observations for worker-controlled meta-engine tests + struct MetaResizeState { + std::atomic master_calls; + std::atomic slave_calls; + std::atomic portfolio_assets; + std::atomic restart_slaves; + std::atomic nogood_handoffs; + std::atomic live_spaces; + bool partition_portfolio; + unsigned int maximum; + + explicit MetaResizeState(unsigned int maximum0=15U) + : master_calls(0U), slave_calls(0U), portfolio_assets(0U), + restart_slaves(0U), nogood_handoffs(0U), live_spaces(0U), + partition_portfolio(false), maximum(maximum0) {} + }; + + /// Partitionable finite model for RBS and PBS worker-control tests + class MetaResizeSpace : public Space { + public: + IntVar x; + MetaResizeState* observations; + + MetaResizeSpace(MetaResizeState& state) + : x(*this,0,static_cast(state.maximum)), observations(&state) { + observations->live_spaces.fetch_add(1U,std::memory_order_relaxed); + Gecode::branch(*this,x,INT_VAL_MAX()); + } + + MetaResizeSpace(MetaResizeState& state, int value) + : x(*this,value,value), observations(&state) { + observations->live_spaces.fetch_add(1U,std::memory_order_relaxed); + } + + MetaResizeSpace(MetaResizeSpace& s) + : Space(s), observations(s.observations) { + observations->live_spaces.fetch_add(1U,std::memory_order_relaxed); + x.update(*this,s.x); + } + + ~MetaResizeSpace(void) override { + observations->live_spaces.fetch_sub(1U,std::memory_order_relaxed); + } + + Space* copy(void) override { + return new MetaResizeSpace(*this); + } + + void constrain(const Space& _s) override { + const MetaResizeSpace& s = + static_cast(_s); + rel(*this,x,IRT_LE,s.x.val()); + } + + bool master(const MetaInfo& mi) override { + observations->master_calls.fetch_add(1U,std::memory_order_relaxed); + if ((mi.type() == MetaInfo::RESTART) && (mi.last() != nullptr)) { + const MetaResizeSpace& last = + static_cast(*mi.last()); + rel(*this,x,IRT_NQ,last.x.val()); + if (mi.nogoods().ng() > 0U) + observations->nogood_handoffs.fetch_add( + 1U,std::memory_order_relaxed); + return true; + } + return false; + } + + bool slave(const MetaInfo& mi) override { + observations->slave_calls.fetch_add(1U, + std::memory_order_relaxed); + if (mi.type() == MetaInfo::PORTFOLIO) { + unsigned int asset = mi.asset(); + observations->portfolio_assets.fetch_or( + 1U << asset,std::memory_order_relaxed); + if (observations->partition_portfolio) { + int boundary = static_cast(observations->maximum / 2U); + if (asset == 0U) + rel(*this,x,IRT_LQ,boundary); + else + rel(*this,x,IRT_GR,boundary); + } + } else { + observations->restart_slaves.fetch_add( + 1U,std::memory_order_relaxed); + } + return true; + } + + int value(void) const { + return x.val(); + } + }; + + struct MetaResizeBuilderFailure {}; + + /// Builder used to verify cleanup after explicit PBS construction fails + class MetaResizeThrowBuilder : public Gecode::Search::Builder { + private: + std::atomic* live; + bool fail; + public: + MetaResizeThrowBuilder(const Gecode::Search::Options& o, + std::atomic& live0, bool fail0) + : Gecode::Search::Builder(o,false), live(&live0), fail(fail0) { + live->fetch_add(1U,std::memory_order_relaxed); + } + + Gecode::Search::Engine* operator()(Space* s) const override { + if (fail) + throw MetaResizeBuilderFailure(); + return Gecode::Search::build< + MetaResizeSpace,Gecode::DFS >(s,opt); + } + + ~MetaResizeThrowBuilder(void) override { + live->fetch_sub(1U,std::memory_order_relaxed); + } + }; + + /// Focused RBS and PBS worker-control transport tests + class MetaResize : public Base { + private: + enum Scenario { + VALIDATION, + RBS_DFS, + RBS_BAB, + PBS_EXPLICIT_DFS, + PBS_EXPLICIT_BAB, + PBS_EXPLICIT_BAB_CONSTRAIN, + PBS_NESTED, + PBS_NESTED_BAB, + PBS_STOP_LIFECYCLE + } scenario; + + template + static bool throws(Function function) { + try { + function(); + } catch (const Exception&) { + return true; + } catch (...) { + return false; + } + return false; + } + + static bool settled(const Gecode::Search::WorkerControl& control, + unsigned long long int generation, + unsigned int leases) { + for (unsigned int spin=0U; spin<10000000U; spin++) { + if ((Gecode::Search::WorkerControlAccess:: + observed_generation(control) == generation) && + (Gecode::Search::WorkerControlAccess::leases(control) == + leases) && + (Gecode::Search::WorkerControlAccess::admitted(control) == 0U)) + return true; + std::this_thread::yield(); + } + return false; + } + + static unsigned int exhaust_dfs( + Gecode::Search::Base& engine, + unsigned int seen[16]) { + unsigned int solutions = 0U; + while (MetaResizeSpace* solution = engine.next()) { + int value = solution->value(); + if ((value >= 0) && (value < 16)) + seen[value]++; + solutions++; + delete solution; + } + return solutions; + } + +#ifdef GECODE_HAS_THREADS + template + static void action_portfolio_leaf( + Engine& engine, + Gecode::Search::WorkerControl& control, unsigned int request, + unsigned long long int generation, + std::vector& results, bool& ok) { + using Gecode::Search::WorkerControlAccess; + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ACTION_BEGIN, + WorkerControlAccess::ALL_WORKERS,request); + WorkerControlAccess::reset_max_admitted(control,generation); + std::atomic action_ready(false); + std::atomic exhausted(false); + std::atomic consumer_done(false); + std::thread consumer([&] { + while (true) { + MetaResizeSpace* result = + static_cast(engine.next()); + if (result == nullptr) { + exhausted.store(true,std::memory_order_release); + consumer_done.store(true,std::memory_order_release); + return; + } + results.push_back(result); + if (action_ready.load(std::memory_order_acquire)) { + consumer_done.store(true,std::memory_order_release); + return; + } + } + }); + unsigned int waiting = 0U; + auto deadline = std::chrono::steady_clock::now() + + std::chrono::seconds(3); + do { + waiting = 0U; + for (unsigned int worker=0U; worker= request) + break; + std::this_thread::yield(); + } while (!consumer_done.load(std::memory_order_acquire) && + (std::chrono::steady_clock::now() < deadline)); + bool reached = waiting >= request; + bool action_ok = reached && + (WorkerControlAccess::observed_generation(control) == generation) && + (WorkerControlAccess::leases(control) == request) && + (WorkerControlAccess::max_admitted(control) == request); + action_ready.store(true,std::memory_order_release); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ACTION_BEGIN); + consumer.join(); + ok = ok && action_ok && + !exhausted.load(std::memory_order_acquire) && + settled(control,generation,request) && + (WorkerControlAccess::completed_generation(control) >= generation); + } + + template + static void action_portfolio_phase( + Engine& engine, + Gecode::Search::WorkerControl& first, unsigned int first_request, + Gecode::Search::WorkerControl& second, unsigned int second_request, + std::vector& results, bool& ok) { + using Gecode::Search::WorkerControlAccess; + first.request(first_request); + second.request(second_request); + unsigned long long int fg = WorkerControlAccess::generation(first); + unsigned long long int sg = WorkerControlAccess::generation(second); + action_portfolio_leaf( + engine,first,first_request,fg,results,ok); + action_portfolio_leaf( + engine,second,second_request,sg,results,ok); + ok = ok && + (WorkerControlAccess::observed_generation(first) == fg) && + (WorkerControlAccess::observed_generation(second) == sg) && + (WorkerControlAccess::leases(first) == first_request) && + (WorkerControlAccess::leases(second) == second_request) && + (WorkerControlAccess::max_admitted(first) == first_request) && + (WorkerControlAccess::max_admitted(second) == second_request); + } +#endif + + static bool validation(void) { +#ifdef GECODE_HAS_THREADS + const unsigned int leaf_threads = 4U; +#else + const unsigned int leaf_threads = 1U; +#endif + bool ok = true; + + MetaResizeState single_state; + Gecode::Search::WorkerControl single(1U); + { + MetaResizeSpace* root = new MetaResizeSpace(single_state); + Gecode::Search::Options o; + o.assets = 1U; + o.threads = static_cast(leaf_threads); + o.worker_control = single; + Gecode::PBS engine(root,o); + delete root; + ok = ok && (single.capacity() == leaf_threads); + unsigned int seen[16] = {}; + ok = ok && (exhaust_dfs(engine,seen) == 16U); + for (unsigned int count : seen) + ok = ok && (count == 1U); + } + single.request(1U); + ok = ok && + (single_state.master_calls.load(std::memory_order_relaxed) == 1U) && + (single_state.slave_calls.load(std::memory_order_relaxed) == 1U); + if (!ok) + return false; + + MetaResizeState homogeneous_state; + Gecode::Search::WorkerControl homogeneous(1U); + MetaResizeSpace* homogeneous_root = + new MetaResizeSpace(homogeneous_state); + Gecode::Search::Options homogeneous_options; + homogeneous_options.assets = 2U; + homogeneous_options.threads = 2.0; + homogeneous_options.worker_control = homogeneous; + ok = ok && throws([&] { + Gecode::PBS + engine(homogeneous_root,homogeneous_options); + }); + ok = ok && (homogeneous.capacity() == 0U) && + (homogeneous_state.master_calls.load( + std::memory_order_relaxed) == 0U) && + (homogeneous_state.slave_calls.load( + std::memory_order_relaxed) == 0U); + delete homogeneous_root; + if (!ok) + return false; + + MetaResizeState outer_state; + Gecode::Search::WorkerControl outer(1U); + Gecode::Search::Options builder_options; + builder_options.threads = static_cast(leaf_threads); + Gecode::SEBs outer_builders(1); + outer_builders[0] = Gecode::dfs(builder_options); + MetaResizeSpace* outer_root = new MetaResizeSpace(outer_state); + Gecode::Search::Options outer_options; + outer_options.threads = 1.0; + outer_options.worker_control = outer; + ok = ok && throws([&] { + Gecode::PBS + engine(outer_root,outer_builders,outer_options); + }); + ok = ok && (outer.capacity() == 0U) && + (outer_state.master_calls.load(std::memory_order_relaxed) == 0U); + delete outer_builders[0]; + delete outer_root; + if (!ok) + return false; + +#ifdef GECODE_HAS_THREADS + MetaResizeState duplicate_state; + Gecode::Search::WorkerControl duplicate(1U); + Gecode::Search::Options duplicate_options; + duplicate_options.threads = 2.0; + duplicate_options.worker_control = duplicate; + Gecode::SEBs duplicate_builders(2); + duplicate_builders[0] = + Gecode::dfs(duplicate_options); + duplicate_builders[1] = + Gecode::dfs(duplicate_options); + MetaResizeSpace* duplicate_root = + new MetaResizeSpace(duplicate_state); + Gecode::Search::Options duplicate_outer; + duplicate_outer.threads = 2.0; + ok = ok && throws([&] { + Gecode::PBS + engine(duplicate_root,duplicate_builders,duplicate_outer); + }); + ok = ok && (duplicate.capacity() == 0U) && + (duplicate_state.master_calls.load( + std::memory_order_relaxed) == 0U); + delete duplicate_builders[0]; + delete duplicate_builders[1]; + delete duplicate_root; + if (!ok) + return false; + + MetaResizeState distinct_state; + distinct_state.partition_portfolio = true; + Gecode::Search::WorkerControl first(1U), second(1U), discarded(1U); + Gecode::Search::Options + first_options, second_options, discarded_options; + first_options.threads = 2.0; + first_options.worker_control = first; + second_options.threads = 3.0; + second_options.worker_control = second; + discarded_options.threads = 4.0; + discarded_options.worker_control = discarded; + Gecode::SEBs distinct_builders(3); + distinct_builders[0] = Gecode::dfs(first_options); + distinct_builders[1] = Gecode::dfs(second_options); + distinct_builders[2] = + Gecode::dfs(discarded_options); + unsigned int distinct_solutions = 0U; + bool distinct_exact = true; + { + MetaResizeSpace* root = new MetaResizeSpace(distinct_state); + Gecode::Search::Options portfolio; + portfolio.threads = 2.0; + Gecode::PBS + engine(root,distinct_builders,portfolio); + delete root; + ok = ok && (first.capacity() == 2U) && + (second.capacity() == 3U) && (discarded.capacity() == 0U); + unsigned int seen[16] = {}; + distinct_solutions = exhaust_dfs(engine,seen); + ok = ok && (distinct_solutions == 16U); + for (unsigned int count : seen) + distinct_exact = distinct_exact && (count == 1U); + ok = ok && distinct_exact; + } + + MetaResizeState failure_state; + Gecode::Search::WorkerControl bound(1U), failing(1U), excess(1U); + Gecode::Search::WorkerControl bound_copy(bound); + Gecode::Search::Options failure_options[3]; + failure_options[0].threads = 2.0; + failure_options[0].worker_control = bound; + failure_options[1].threads = 3.0; + failure_options[1].worker_control = failing; + failure_options[2].threads = 4.0; + failure_options[2].worker_control = excess; + std::atomic live_builders(0U); + Gecode::SEBs failure_builders(3); + failure_builders[0] = new MetaResizeThrowBuilder( + failure_options[0],live_builders,false); + failure_builders[1] = new MetaResizeThrowBuilder( + failure_options[1],live_builders,true); + failure_builders[2] = new MetaResizeThrowBuilder( + failure_options[2],live_builders,false); + MetaResizeSpace* failure_root = new MetaResizeSpace(failure_state); + Gecode::Search::Options failure_outer; + failure_outer.threads = 2.0; + bool construction_failed = false; + try { + Gecode::PBS + engine(failure_root,failure_builders,failure_outer); + } catch (const MetaResizeBuilderFailure&) { + construction_failed = true; + } + delete failure_root; + ok = ok && construction_failed && + (live_builders.load(std::memory_order_relaxed) == 0U) && + (failure_state.live_spaces.load(std::memory_order_relaxed) == 0U) && + Gecode::Search::WorkerControlAccess::same_identity( + bound,bound_copy) && + !Gecode::Search::WorkerControlAccess::attached(bound) && + !Gecode::Search::WorkerControlAccess::attached(bound_copy) && + !Gecode::Search::WorkerControlAccess::attached(failing) && + (excess.capacity() == 0U); +#endif + return ok; + } + + static bool rbs_dfs(void) { + MetaResizeState state; + state.partition_portfolio = true; +#ifdef GECODE_HAS_THREADS + const unsigned int capacity = 4U; +#else + const unsigned int capacity = 1U; +#endif + Gecode::Search::WorkerControl control(capacity); + Gecode::Search::Options o; + o.threads = static_cast(capacity); + o.worker_control = control; + o.nogoods_limit = 16U; + o.cutoff = Gecode::Search::Cutoff::constant(1U); + MetaResizeSpace* root = new MetaResizeSpace(state); + Gecode::RBS engine(root,o); + delete root; + bool ok = control.capacity() == capacity; + unsigned int seen[16] = {}; +#ifdef GECODE_HAS_THREADS + const unsigned int requests[] = {1U,3U,1U,4U}; +#else + const unsigned int requests[] = {1U,1U,1U,1U}; +#endif + std::vector results; + for (unsigned int request : requests) { + control.request(request); + unsigned long long int generation = + Gecode::Search::WorkerControlAccess::generation(control); + MetaResizeSpace* solution = engine.next(); + if (solution != nullptr) + results.push_back(solution); +#ifdef GECODE_HAS_THREADS + ok = ok && (solution != nullptr) && + settled(control,generation,request); +#else + ok = ok && (solution != nullptr); +#endif + } + for (MetaResizeSpace* solution : results) { + seen[solution->value()]++; + delete solution; + } + unsigned int solutions = + static_cast(results.size()) + + exhaust_dfs(engine,seen); + ok = ok && (solutions == 16U) && + (control.capacity() == capacity) && + (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); + for (unsigned int count : seen) + ok = ok && (count == 1U); + return ok; + } + + static bool rbs_bab(void) { + MetaResizeState state; + state.partition_portfolio = true; +#ifdef GECODE_HAS_THREADS + const unsigned int capacity = 4U; +#else + const unsigned int capacity = 1U; +#endif + Gecode::Search::WorkerControl control(capacity); + Gecode::Search::Options o; + o.threads = static_cast(capacity); + o.worker_control = control; + o.cutoff = Gecode::Search::Cutoff::constant(1U); + MetaResizeSpace* root = new MetaResizeSpace(state); + Gecode::SEB builder = + Gecode::rbs(o); + Gecode::Search::Engine* engine = (*builder)(root); + delete builder; + delete root; + bool ok = control.capacity() == capacity; + int best = 16; +#ifdef GECODE_HAS_THREADS + control.request(1U); + unsigned long long int first_generation = + Gecode::Search::WorkerControlAccess::generation(control); +#else + control.request(1U); +#endif + MetaResizeSpace* solution = + static_cast(engine->next()); + ok = ok && (solution != nullptr); + if (solution != nullptr) { + best = solution->value(); + delete solution; + } +#ifdef GECODE_HAS_THREADS + ok = ok && settled(control,first_generation,1U); +#endif + MetaResizeSpace external(state,10); + (void) external.status(); + engine->constrain(external); +#ifdef GECODE_HAS_THREADS + control.request(3U); + unsigned long long int second_generation = + Gecode::Search::WorkerControlAccess::generation(control); +#else + control.request(1U); +#endif + while ((solution = + static_cast(engine->next())) != nullptr) { + best = solution->value(); + delete solution; + } +#ifdef GECODE_HAS_THREADS + ok = ok && settled(control,second_generation,3U); +#endif + delete engine; + return ok && (best == 0) && + (control.capacity() == capacity) && + (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); + } + + template class Engine> + static bool pbs_explicit(bool best_search, bool constant=false, + bool external_incumbent=false) { +#ifndef GECODE_HAS_THREADS + (void) best_search; + (void) constant; + (void) external_incumbent; + return true; +#else + MetaResizeState state( + (best_search && !external_incumbent) ? 15U : 4095U); + state.partition_portfolio = true; + Gecode::Search::WorkerControl + first(constant ? 3U : 4U), second(constant ? 1U : 4U); + Gecode::Search::NodeStop first_stop(0U), second_stop(0U); + Gecode::Search::Options child[2]; + for (unsigned int i=0U; i<2U; i++) + child[i].threads = 4.0; + child[0].worker_control = first; + child[1].worker_control = second; + if (external_incumbent) { + child[0].stop = &first_stop; + child[1].stop = &second_stop; + } + Gecode::SEBs builders(2); + builders[0] = best_search ? + Gecode::bab(child[0]) : + Gecode::dfs(child[0]); + builders[1] = best_search ? + Gecode::bab(child[1]) : + Gecode::dfs(child[1]); + Gecode::Search::Options outer; + outer.threads = 2.0; + MetaResizeSpace* root = new MetaResizeSpace(state); + Gecode::PBS engine(root,builders,outer); + delete root; + bool ok = (first.capacity() == 4U) && (second.capacity() == 4U); + bool phase_ok = true, parked_ok = true, result_ok = true; + std::vector results; + if (external_incumbent) { + first.request(1U); + second.request(1U); + Gecode::Search::WorkerControl* controls[2] = {&first,&second}; + for (unsigned int c=0U; c<2U; c++) { + Gecode::Search::WorkerControlAccess::gate_install( + *controls[c], + Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN, + Gecode::Search::WorkerControlAccess::ALL_WORKERS,1U); + Gecode::Search::WorkerControlAccess::gate_release_all( + *controls[c], + Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN); + } + MetaResizeSpace* prelude = engine.next(); + parked_ok = + (prelude == nullptr) && engine.stopped() && + (Gecode::Search::WorkerControlAccess::parked(first) == 3U) && + (Gecode::Search::WorkerControlAccess::parked(second) == 3U); + delete prelude; + first_stop.limit(1000000U); + second_stop.limit(1000000U); + unsigned long long int before[2][4]; + unsigned int forwarded = 0U, parked_forwarded = 0U; + for (unsigned int c=0U; c<2U; c++) + for (unsigned int worker=0U; worker<4U; worker++) + before[c][worker] = + Gecode::Search::WorkerControlAccess:: + incumbent_deliveries(*controls[c],worker); + MetaResizeSpace external(state,-1); + (void) external.status(); + engine.constrain(external); + for (unsigned int c=0U; c<2U; c++) + for (unsigned int worker=0U; worker<4U; worker++) + if (Gecode::Search::WorkerControlAccess:: + incumbent_deliveries(*controls[c],worker) > + before[c][worker]) { + forwarded++; + if (Gecode::Search::WorkerControlAccess:: + worker_parked(*controls[c],worker)) + parked_forwarded++; + } + ok = ok && parked_ok && (forwarded == 8U) && + (parked_forwarded == 6U); + } else if (!constant) { + action_portfolio_phase( + engine,first,3U,second,1U,results,phase_ok); + ok = ok && phase_ok; + parked_ok = + (Gecode::Search::WorkerControlAccess::parked(first) == 1U) && + (Gecode::Search::WorkerControlAccess::parked(second) == 3U); + ok = ok && parked_ok; + if (!best_search) { + action_portfolio_phase( + engine,first,1U,second,3U,results,ok); + action_portfolio_phase( + engine,first,2U,second,2U,results,ok); + } + } + + if (best_search) { + int best = static_cast(state.maximum+1U); + for (MetaResizeSpace* solution : results) { + best = solution->value(); + delete solution; + } + while (MetaResizeSpace* solution = engine.next()) { + best = solution->value(); + delete solution; + } + result_ok = external_incumbent ? + (best == static_cast(state.maximum+1U)) : + (best == 0); + ok = ok && result_ok; + } else { + std::vector seen(state.maximum+1U,0U); + bool valid = constant || !results.empty(); + for (MetaResizeSpace* solution : results) { + valid = valid && (solution->value() >= 0) && + (static_cast(solution->value()) <= + state.maximum); + if ((solution->value() >= 0) && + (static_cast(solution->value()) <= + state.maximum)) + seen[static_cast(solution->value())]++; + delete solution; + } + while (MetaResizeSpace* solution = engine.next()) { + valid = valid && (solution->value() >= 0) && + (static_cast(solution->value()) <= + state.maximum); + if ((solution->value() >= 0) && + (static_cast(solution->value()) <= + state.maximum)) + seen[static_cast(solution->value())]++; + delete solution; + } + for (unsigned int i=0U; i(capacity); + child[i].cutoff = Gecode::Search::Cutoff::constant(1U); + } + child[0].worker_control = first; + child[1].worker_control = second; + Gecode::SEBs builders(2); + builders[0] = + Gecode::rbs(child[0]); + builders[1] = + Gecode::rbs(child[1]); + Gecode::Search::Options outer; +#ifdef GECODE_HAS_THREADS + outer.threads = 2.0; +#else + outer.threads = 1.0; +#endif + MetaResizeSpace* root = new MetaResizeSpace(state); + Gecode::PBS + engine(root,builders,outer); + delete root; + first.request(1U); + second.request(capacity); + unsigned int seen[16] = {}; + unsigned int solutions = exhaust_dfs(engine,seen); + bool ok = (solutions == 16U) && + (first.capacity() == capacity) && + (second.capacity() == capacity) && + (state.portfolio_assets.load(std::memory_order_relaxed) == 3U) && + (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); + for (unsigned int count : seen) + ok = ok && (count == 1U); + return ok; + } + + static bool nested_bab(void) { + MetaResizeState state; + state.partition_portfolio = true; +#ifdef GECODE_HAS_THREADS + const unsigned int capacity = 2U; +#else + const unsigned int capacity = 1U; +#endif + Gecode::Search::WorkerControl first(capacity), second(capacity); + Gecode::Search::Options child[2]; + for (unsigned int i=0U; i<2U; i++) { + child[i].threads = static_cast(capacity); + child[i].cutoff = Gecode::Search::Cutoff::constant(1U); + } + child[0].worker_control = first; + child[1].worker_control = second; + Gecode::SEBs builders(2); + builders[0] = + Gecode::rbs(child[0]); + builders[1] = + Gecode::rbs(child[1]); + Gecode::Search::Options outer; +#ifdef GECODE_HAS_THREADS + outer.threads = 2.0; +#else + outer.threads = 1.0; +#endif + MetaResizeSpace* root = new MetaResizeSpace(state); + Gecode::PBS + engine(root,builders,outer); + delete root; + first.request(1U); + second.request(capacity); + int best = 16; + while (MetaResizeSpace* solution = engine.next()) { + best = solution->value(); + delete solution; + } + return (best == 0) && + (first.capacity() == capacity) && + (second.capacity() == capacity) && + (state.portfolio_assets.load(std::memory_order_relaxed) == 3U) && + (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); + } + + static bool pbs_stop_lifecycle(void) { + MetaResizeState state(4095U); + state.partition_portfolio = true; +#ifdef GECODE_HAS_THREADS + const unsigned int capacity = 4U; +#else + const unsigned int capacity = 1U; +#endif + Gecode::Search::WorkerControl first(capacity), second(capacity); + Gecode::Search::WorkerControl first_copy(first), second_copy(second); + Gecode::Search::NodeStop first_stop(0U), second_stop(0U); + Gecode::Search::Options child[2]; + for (unsigned int i=0U; i<2U; i++) + child[i].threads = static_cast(capacity); + child[0].worker_control = first; + child[1].worker_control = second; + child[0].stop = &first_stop; + child[1].stop = &second_stop; + bool ok = true; + { + Gecode::SEBs builders(2); + builders[0] = Gecode::dfs(child[0]); + builders[1] = Gecode::dfs(child[1]); + Gecode::Search::Options outer; +#ifdef GECODE_HAS_THREADS + outer.threads = 2.0; +#else + outer.threads = 1.0; +#endif + MetaResizeSpace* root = new MetaResizeSpace(state); + Gecode::PBS + engine(root,builders,outer); + delete root; + first.request(1U); + second.request(1U); + unsigned long long int fg = + Gecode::Search::WorkerControlAccess::generation(first); + unsigned long long int sg = + Gecode::Search::WorkerControlAccess::generation(second); + Gecode::Search::WorkerControlAccess::gate_install( + first,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN, + Gecode::Search::WorkerControlAccess::ALL_WORKERS,1U); + Gecode::Search::WorkerControlAccess::gate_install( + second,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN, + Gecode::Search::WorkerControlAccess::ALL_WORKERS,1U); + Gecode::Search::WorkerControlAccess::gate_release_all( + first,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN); + Gecode::Search::WorkerControlAccess::gate_release_all( + second,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN); + MetaResizeSpace* solution = engine.next(); + delete solution; + ok = ok && (solution == nullptr) && engine.stopped() && + (Gecode::Search::WorkerControlAccess:: + completed_generation(first) >= fg) && + (Gecode::Search::WorkerControlAccess:: + completed_generation(second) >= sg); +#ifdef GECODE_HAS_THREADS + ok = ok && + (Gecode::Search::WorkerControlAccess::parked(first) == 3U) && + (Gecode::Search::WorkerControlAccess::parked(second) == 3U); +#endif + } + return ok && + !Gecode::Search::WorkerControlAccess::attached(first) && + !Gecode::Search::WorkerControlAccess::attached(second) && + !Gecode::Search::WorkerControlAccess::attached(first_copy) && + !Gecode::Search::WorkerControlAccess::attached(second_copy) && + (state.live_spaces.load(std::memory_order_relaxed) == 0U); + } + + public: + MetaResize(const std::string& name, Scenario s) + : Base("Search::WorkerControl::MetaResize::"+name), scenario(s) {} + + bool run(void) override { + switch (scenario) { + case VALIDATION: return validation(); + case RBS_DFS: return rbs_dfs(); + case RBS_BAB: return rbs_bab(); + case PBS_EXPLICIT_DFS: { + bool constant = pbs_explicit(false,true); + bool dynamic = pbs_explicit(false); + return constant && dynamic; + } + case PBS_EXPLICIT_BAB: + return pbs_explicit(true,true); + case PBS_EXPLICIT_BAB_CONSTRAIN: + return pbs_explicit(true,false,true); + case PBS_NESTED: return nested(); + case PBS_NESTED_BAB: return nested_bab(); + case PBS_STOP_LIFECYCLE: + return pbs_stop_lifecycle(); + default: GECODE_NEVER; + } + return false; + } + + static void create(void) { + (void) new MetaResize("Validation",VALIDATION); + (void) new MetaResize("RBS::DFS",RBS_DFS); + (void) new MetaResize("RBS::BAB",RBS_BAB); + (void) new MetaResize("PBS::Explicit::DFS",PBS_EXPLICIT_DFS); + (void) new MetaResize("PBS::Explicit::BAB::Optimality", + PBS_EXPLICIT_BAB); + (void) new MetaResize("PBS::Explicit::BAB::Constrain", + PBS_EXPLICIT_BAB_CONSTRAIN); + (void) new MetaResize("PBS::NestedRBS",PBS_NESTED); + (void) new MetaResize("PBS::NestedRBS::BAB",PBS_NESTED_BAB); + (void) new MetaResize("PBS::StopLifecycle",PBS_STOP_LIFECYCLE); + } + }; + /// %Base class for search tests class Test : public Base { public: @@ -2016,6 +2923,8 @@ namespace Test { public: /// Perform creation and registration Create(void) { + WorkerControl::create(); + MetaResize::create(); // Depth-first search for (unsigned int t = 1; t<=4; t++) for (unsigned int c_d = 1; c_d<10; c_d++) From 0df8a26189c994bf4f29a8a480d4a32a6f9ea784 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 07:38:35 +0100 Subject: [PATCH 07/15] Stress concurrent worker resizing --- test/search.cpp | 150 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/test/search.cpp b/test/search.cpp index 270bf888ef..e61664d65d 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -366,7 +366,10 @@ namespace Test { BAB_RESIZE_OPTIMALITY_DEFAULT, BAB_RESIZE_OPTIMALITY_FREQUENT, BAB_RESIZE_INCUMBENTS, - BAB_RESIZE_LIFECYCLE + BAB_RESIZE_LIFECYCLE, + STRESS_DFS, + STRESS_BAB, + STRESS_DESTRUCTION } scenario; /// Stable finite tree used by the DFS resizing tests @@ -1568,6 +1571,144 @@ namespace Test { #endif } + static bool stress_dfs(void) { +#ifdef GECODE_HAS_THREADS + for (unsigned int round=0U; round<8U; round++) { + Gecode::Search::WorkerControl control(4U); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + ResizeSpace* root = new ResizeSpace; + Gecode::DFS dfs(root,o); + delete root; + + std::atomic complete(false); + std::atomic controller_ok(true); + std::thread controller([&] { + static const unsigned int limits[] = { + 1U,4U,2U,3U,1U,2U,4U,3U + }; + for (unsigned int request=0U; + (request<4096U) && + !complete.load(std::memory_order_acquire); request++) { + try { + control.request(limits[(request+round) % 8U]); + } catch (...) { + controller_ok.store(false,std::memory_order_release); + return; + } + std::this_thread::yield(); + } + }); + + std::vector counts(1U << 12,0U); + while (ResizeSpace* solution = dfs.next()) { + counts[solution->id()]++; + delete solution; + } + complete.store(true,std::memory_order_release); + controller.join(); + if (!controller_ok.load(std::memory_order_acquire) || + dfs.stopped()) + return false; + for (unsigned int count : counts) + if (count != 1U) + return false; + } +#endif + return true; + } + + static bool stress_bab(void) { +#ifdef GECODE_HAS_THREADS + for (unsigned int round=0U; round<8U; round++) { + Gecode::Search::WorkerControl control(4U); + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = control; + if ((round & 1U) != 0U) { + o.c_d = 1U; + o.a_d = 1U; + } + Gecode::Search::Engine* bab = bab_resize_engine(o); + std::atomic complete(false); + std::atomic controller_ok(true); + std::thread controller([&] { + static const unsigned int limits[] = { + 4U,1U,3U,2U,1U,4U,2U,3U + }; + for (unsigned int request=0U; + (request<4096U) && + !complete.load(std::memory_order_acquire); request++) { + try { + control.request(limits[(request+round) % 8U]); + } catch (...) { + controller_ok.store(false,std::memory_order_release); + return; + } + std::this_thread::yield(); + } + }); + + int best = 4096; + while (BABResizeSpace* solution = + static_cast(bab->next())) { + best = solution->value(); + delete solution; + } + complete.store(true,std::memory_order_release); + controller.join(); + bool ok = controller_ok.load(std::memory_order_acquire) && + !bab->stopped() && (best == 0); + delete bab; + if (!ok) + return false; + } +#endif + return true; + } + + static bool stress_destruction(void) { +#ifdef GECODE_HAS_THREADS + for (unsigned int round=0U; round<64U; round++) { + Gecode::Search::Options o; + o.threads = 4.0; + o.worker_control = Gecode::Search::WorkerControl(4U); + Gecode::Search::Engine* engine = + ((round & 1U) == 0U) ? dfs_engine(o) : bab_resize_engine(o); + std::atomic started(false); + std::atomic controller_ok(true); + Gecode::Search::WorkerControl control(o.worker_control); + std::thread controller([control,&started,&controller_ok,round] + () mutable { + static const unsigned int limits[] = { + 1U,3U,2U,4U,2U,1U,4U,3U + }; + started.store(true,std::memory_order_release); + for (unsigned int request=0U; request<2048U; request++) { + try { + control.request(limits[(request+round) % 8U]); + } catch (...) { + controller_ok.store(false,std::memory_order_release); + return; + } + } + }); + while (!started.load(std::memory_order_acquire)) + std::this_thread::yield(); + delete engine; + controller.join(); + if (!controller_ok.load(std::memory_order_acquire)) + return false; + control.request(1U); + if ((control.capacity() != 4U) || + (control.requested() != 1U)) + return false; + } +#endif + return true; + } + public: WorkerControl(const std::string& name, Scenario s) : Base("Search::WorkerControl::"+name), scenario(s) {} @@ -1589,6 +1730,9 @@ namespace Test { return bab_resize_optimality(true); case BAB_RESIZE_INCUMBENTS: return bab_resize_incumbents(); case BAB_RESIZE_LIFECYCLE: return bab_resize_lifecycle(); + case STRESS_DFS: return stress_dfs(); + case STRESS_BAB: return stress_bab(); + case STRESS_DESTRUCTION: return stress_destruction(); default: GECODE_NEVER; } return false; @@ -1616,6 +1760,10 @@ namespace Test { BAB_RESIZE_INCUMBENTS); (void) new WorkerControl("BABResize::Lifecycle", BAB_RESIZE_LIFECYCLE); + (void) new WorkerControl("Stress::DFS",STRESS_DFS); + (void) new WorkerControl("Stress::BAB",STRESS_BAB); + (void) new WorkerControl("Stress::Destruction", + STRESS_DESTRUCTION); } }; From d4abe010b43055d900a9c9afa4c6693db0ad6568 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Fri, 24 Jul 2026 08:20:58 +0100 Subject: [PATCH 08/15] Reduce worker-control steady-state overhead --- gecode/search/par/engine.hh | 4 ++++ gecode/search/par/engine.hpp | 38 +++++++++++++++++++++++--------- gecode/search/worker-control.cpp | 36 +++++++++++++++++++++++++++++- gecode/search/worker-control.hh | 9 ++++++++ 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index 8223f25cc4..2f8a203bf0 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -101,6 +101,10 @@ namespace Gecode { namespace Search { namespace Par { }; /// Whether adjustable parallel admission is enabled bool scheduler_enabled; + /// Dynamically enabled test instrumentation + const std::atomic* scheduler_instrumented; + /// Unchanged-capacity admission fast path + const std::atomic* scheduler_fast_admit; /// Mutex for adjustable parallel admission Support::Mutex scheduler_mutex; /// Per-worker adjustable parallel admission state diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index 7a46df0c23..a096546c4a 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -123,6 +123,8 @@ namespace Gecode { namespace Search { namespace Par { Engine::Engine(const Options& o) : Search::Engine(o,static_cast(o.threads)), _opt(o), scheduler_enabled(false), + scheduler_instrumented(nullptr), + scheduler_fast_admit(nullptr), scheduler_worker(nullptr), scheduler_requested(workers()), scheduler_leases(0U), scheduler_cursor(0U), scheduler_generation(0U), _cmd(C_WAIT), solutions(heap) { @@ -207,6 +209,10 @@ namespace Gecode { namespace Search { namespace Par { Engine::scheduler_enable(bool root_owner) { if (!WorkerControlAccess::engaged(worker_control)) return; + scheduler_instrumented = + WorkerControlAccess::instrumentation(worker_control); + scheduler_fast_admit = + WorkerControlAccess::fast_admission(worker_control); scheduler_worker = static_cast (heap.ralloc(workers() * sizeof(SchedulerWorker))); unsigned long long int generation; @@ -252,8 +258,11 @@ namespace Gecode { namespace Search { namespace Par { Engine::scheduler_admit(unsigned int worker) { if (!scheduler_enabled) return true; - WorkerControlAccess::gate(worker_control, - WorkerControlAccess::GATE_ADMISSION,worker); + if (scheduler_fast_admit->load(std::memory_order_acquire)) + return true; + if (scheduler_instrumented->load(std::memory_order_acquire)) + WorkerControlAccess::gate(worker_control, + WorkerControlAccess::GATE_ADMISSION,worker); while (cmd() == C_WORK) { unsigned long long int generation = WorkerControlAccess::generation(worker_control); @@ -283,12 +292,15 @@ namespace Gecode { namespace Search { namespace Par { scheduler_observe(); scheduler_mutex.release(); + if (requested == workers()) + WorkerControlAccess::fast_admission(worker_control,generation); if (wake) WorkerControlAccess::signal_all(worker_control); if (admitted) return true; - WorkerControlAccess::gate(worker_control, - WorkerControlAccess::GATE_EVENT_WAIT,worker); + if (scheduler_instrumented->load(std::memory_order_acquire)) + WorkerControlAccess::gate(worker_control, + WorkerControlAccess::GATE_EVENT_WAIT,worker); WorkerControlAccess::wait(worker_control,worker); } return false; @@ -297,7 +309,8 @@ namespace Gecode { namespace Search { namespace Par { template forceinline void Engine::scheduler_action_begin(unsigned int worker) { - if (scheduler_enabled) + if (scheduler_enabled && + scheduler_instrumented->load(std::memory_order_acquire)) WorkerControlAccess::action_begin( worker_control,worker,scheduler_generation); } @@ -305,14 +318,16 @@ namespace Gecode { namespace Search { namespace Par { template forceinline void Engine::scheduler_action_end(unsigned int worker) { - if (scheduler_enabled) + if (scheduler_enabled && + scheduler_instrumented->load(std::memory_order_acquire)) WorkerControlAccess::action_end(worker_control,worker); } template forceinline void Engine::scheduler_failed_scan(unsigned int worker) { - if (scheduler_enabled) + if (scheduler_enabled && + scheduler_instrumented->load(std::memory_order_acquire)) WorkerControlAccess::gate(worker_control, WorkerControlAccess::GATE_FAILED_SCAN,worker); } @@ -342,14 +357,16 @@ namespace Gecode { namespace Search { namespace Par { template void Engine::scheduler_solution(unsigned int worker) { - if (scheduler_enabled) + if (scheduler_enabled && + scheduler_instrumented->load(std::memory_order_acquire)) WorkerControlAccess::solution(worker_control,worker); } template forceinline void Engine::scheduler_incumbent(unsigned int worker) { - if (scheduler_enabled) + if (scheduler_enabled && + scheduler_instrumented->load(std::memory_order_acquire)) WorkerControlAccess::incumbent(worker_control,worker); } @@ -376,7 +393,8 @@ namespace Gecode { namespace Search { namespace Par { scheduler_worker[target].lease = true; scheduler_worker[target].parked = false; scheduler_cursor = (target+1U) % workers(); - WorkerControlAccess::handoff(worker_control,worker,target); + if (scheduler_instrumented->load(std::memory_order_acquire)) + WorkerControlAccess::handoff(worker_control,worker,target); } } } diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 7ec03114cf..3cc87c8bda 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -135,6 +135,8 @@ namespace Gecode { namespace Search { std::atomic last_solution_worker; std::atomic solution_handoff_from; std::atomic solution_handoff_to; + std::atomic fast_admit; + std::atomic instrumented; std::atomic test_support; Support::Mutex mutex; Lifecycle lifecycle; @@ -149,7 +151,7 @@ namespace Gecode { namespace Search { last_solution_worker(WorkerControlAccess::ALL_WORKERS), solution_handoff_from(WorkerControlAccess::ALL_WORKERS), solution_handoff_to(WorkerControlAccess::ALL_WORKERS), - test_support(nullptr), + fast_admit(false), instrumented(false), test_support(nullptr), lifecycle(NEVER_BOUND), events(nullptr) {} ~State(void) { delete [] events; @@ -215,6 +217,7 @@ namespace Gecode { namespace Search { unsigned int old = state->requested.load(std::memory_order_relaxed); if (old != workers) { + state->fast_admit.store(false,std::memory_order_release); state->requested.store(workers,std::memory_order_release); (void) state->generation.fetch_add(1U,std::memory_order_release); changed = true; @@ -244,6 +247,9 @@ namespace Gecode { namespace Search { throw InvalidWorkerRequest("WorkerControlAccess::attach"); state->events = new Support::Event[capacity]; state->capacity.store(capacity,std::memory_order_release); + state->fast_admit.store( + state->requested.load(std::memory_order_relaxed) == capacity, + std::memory_order_release); state->lifecycle = WorkerControl::State::ATTACHED; } @@ -279,6 +285,32 @@ namespace Gecode { namespace Search { return control.state->lifecycle == WorkerControl::State::ATTACHED; } + const std::atomic* + WorkerControlAccess::instrumentation(const WorkerControl& control) { + return (control.state == nullptr) ? nullptr : + &control.state->instrumented; + } + + const std::atomic* + WorkerControlAccess::fast_admission(const WorkerControl& control) { + return (control.state == nullptr) ? nullptr : + &control.state->fast_admit; + } + + void + WorkerControlAccess::fast_admission( + WorkerControl& control, unsigned long long int generation) { + if (control.state == nullptr) + return; + WorkerControl::State* state = control.state; + Support::Lock lock(state->mutex); + if (!state->instrumented.load(std::memory_order_relaxed) && + (state->generation.load(std::memory_order_relaxed) == generation) && + (state->requested.load(std::memory_order_relaxed) == + state->capacity.load(std::memory_order_relaxed))) + state->fast_admit.store(true,std::memory_order_release); + } + bool WorkerControlAccess::same_identity(const WorkerControl& x, const WorkerControl& y) { @@ -486,12 +518,14 @@ namespace Gecode { namespace Search { WorkerControl::State::TestSupport* support; { Support::Lock lock(state->mutex); + state->fast_admit.store(false,std::memory_order_release); support = state->test_support.load(std::memory_order_relaxed); if (support == nullptr) { unsigned int capacity = state->capacity.load(std::memory_order_relaxed); support = new WorkerControl::State::TestSupport(capacity); state->test_support.store(support,std::memory_order_release); + state->instrumented.store(true,std::memory_order_release); } } unsigned int capacity = support->capacity; diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh index dd7d0f1dfd..560b17dda9 100644 --- a/gecode/search/worker-control.hh +++ b/gecode/search/worker-control.hh @@ -58,6 +58,15 @@ namespace Gecode { namespace Search { static bool engaged(const WorkerControl& control); /// Return whether \a control is currently attached to a leaf engine static bool attached(const WorkerControl& control); + /// Return the test-instrumentation flag retained by \a control + static const std::atomic* + instrumentation(const WorkerControl& control); + /// Return the unchanged-capacity fast-admission flag + static const std::atomic* + fast_admission(const WorkerControl& control); + /// Publish fast admission after reconciling \a generation at capacity + static void fast_admission(WorkerControl& control, + unsigned long long int generation); /// Return whether \a x and \a y share one control identity static bool same_identity(const WorkerControl& x, const WorkerControl& y); From 5f9d5e0b9cadeee3995f1245870dca6eeb90b5bc Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 28 Jul 2026 10:16:10 +0200 Subject: [PATCH 09/15] Document adjustable worker control --- README.md | 3 ++ docs/worker-control.md | 117 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 docs/worker-control.md diff --git a/README.md b/README.md index a75b48fbf3..59cea0c05c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ In particular, Gecode comes with [extensive tutorial and reference documentation](https://gecode.github.io/documentation.html). +The [adjustable-worker guide](docs/worker-control.md) describes external, +asynchronous control of parallel-search worker allocation. + ## CMake Build Options CMake exposes options aligned with the Autoconf build switches. diff --git a/docs/worker-control.md b/docs/worker-control.md new file mode 100644 index 0000000000..268e71f11c --- /dev/null +++ b/docs/worker-control.md @@ -0,0 +1,117 @@ +# Adjustable parallel-search workers + +## Purpose + +`Search::WorkerControl` lets an external scheduler change how many workers a +running DFS or BAB engine may use. Its main use is a portfolio that has a fixed +thread budget but wants to move that budget among independently useful search +engines as conditions change. + +The engine still starts with a fixed maximum: + +```cpp +Gecode::Search::Options options; +options.threads = 8; // Resident worker capacity + +Gecode::Search::WorkerControl control(2); // Initially request two workers +options.worker_control = control; + +Gecode::DFS engine(root, options); + +// Safe from another thread while engine.next() is running. +control.request(6); +control.request(1); +``` + +The `threads` option is expanded once and becomes the engine's immutable +capacity. A request must be between one and that capacity, inclusive. Zero is +not a pause operation: at least one worker remains available to make progress. + +## Asynchronous semantics + +`request` is thread-safe and non-blocking. It publishes a desired worker count +and wakes parked workers when necessary; it does not wait for the engine to +reach that count. + +Changes take effect cooperatively at scheduler boundaries. A grow request can +make parked workers eligible immediately. A shrink request does not interrupt +a worker in the middle of a search action: excess workers finish their current +action and park before beginning another. Consequently, the old and new +requests may overlap briefly. + +Requests affect scheduling, not search correctness. DFS still enumerates the +same solution set and BAB still returns the same optimum. Parallel exploration +order, solution order, node counts, failure counts, and the time at which a +request becomes visible are deliberately nondeterministic. + +Shrinking parks resident operating-system threads. It does not destroy their +thread objects or discard their engine-local search state. Growing wakes those +threads again. Applications should therefore choose capacity as a real +resource maximum: parked workers retain stacks and other per-worker state. + +## Handle lifetime and ownership + +A control is a copyable shared-identity handle. Copies made before or after +engine construction publish to the same request state: + +```cpp +Gecode::Search::WorkerControl portfolio_control(4); +Gecode::Search::Options options; +options.threads = 8; +options.worker_control = portfolio_control; + +Gecode::DFS engine(root, options); +auto scheduler_control = portfolio_control; +scheduler_control.request(3); +``` + +An empty default-constructed handle means that worker adjustment is disabled. +Calling `request` on an empty handle raises +`Search::UninitializedWorkerControl`. + +One shared identity can bind to one logical leaf engine once. It cannot be +shared by two simultaneously constructed DFS/BAB engines, reused for a +replacement engine after destruction, or attached directly to an enclosing +meta-engine. Such reuse raises `Search::WorkerControlInUse`. Construct a new +control for each leaf-engine lifetime. + +Destroying an engine safely detaches its state, but it does not make that +identity reusable. A copied handle may outlive the engine; further in-range +requests are harmless and cannot access destroyed scheduler state. + +## Meta-search + +Restart-based search transports one leaf control through restart construction +and reset. The same underlying DFS or BAB leaf engine remains the adjustment +target across episodes. + +Portfolio-based search does not divide a global thread budget. Give each PBS +asset its own control in the corresponding sequential-engine builder options: + +```cpp +constexpr unsigned int budget = 8; + +Gecode::Search::WorkerControl asset_a(6); +Gecode::Search::WorkerControl asset_b(2); + +Gecode::Search::Options a; +a.threads = budget; +a.worker_control = asset_a; + +Gecode::Search::Options b; +b.threads = budget; +b.worker_control = asset_b; + +// Construct the PBS assets from builders carrying a and b. + +// Later, preserve the external invariant sum(requests) <= budget. +asset_a.request(2); +asset_b.request(6); +``` + +The portfolio controller must enforce its own active-worker budget. Make the +decrease before the increase if even a brief oversubscription is unacceptable; +because resizing is cooperative, applications needing a strict observed +handoff must also provide their own acknowledgement or accounting layer. +Gecode intentionally exposes the scheduling seam without embedding a +portfolio allocation policy. From 23b3a8c1e046c48da09d3d0489acc75125e97c05 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 28 Jul 2026 15:05:34 +0200 Subject: [PATCH 10/15] Reduce worker-control test infrastructure --- gecode/search/par/bab.hh | 2 +- gecode/search/par/bab.hpp | 11 +- gecode/search/par/dfs.hpp | 6 - gecode/search/par/engine.hh | 14 - gecode/search/par/engine.hpp | 88 -- gecode/search/worker-control.cpp | 578 +------ gecode/search/worker-control.hh | 100 +- test/search.cpp | 2488 ++---------------------------- 8 files changed, 160 insertions(+), 3127 deletions(-) diff --git a/gecode/search/par/bab.hh b/gecode/search/par/bab.hh index 847bc659e5..4a1911b208 100644 --- a/gecode/search/par/bab.hh +++ b/gecode/search/par/bab.hh @@ -111,7 +111,7 @@ namespace Gecode { namespace Search { namespace Par { /// \name Search control //@{ /// Report solution \a s - void solution(Space* s, unsigned int producer); + void solution(Space* s); //@} /// \name Engine interface diff --git a/gecode/search/par/bab.hpp b/gecode/search/par/bab.hpp index f3073cac98..98e717cdae 100755 --- a/gecode/search/par/bab.hpp +++ b/gecode/search/par/bab.hpp @@ -121,12 +121,11 @@ namespace Gecode { namespace Search { namespace Par { mark = path.entries(); if (cur != nullptr) cur->constrain(*best); - engine().scheduler_incumbent(index); m.release(); } template forceinline void - BAB::solution(Space* s, unsigned int producer) { + BAB::solution(Space* s) { m_search.acquire(); if (best != nullptr) { s->constrain(*best); @@ -149,7 +148,6 @@ namespace Gecode { namespace Search { namespace Par { if (bs) e_search.signal(); m_search.release(); - this->scheduler_solution(producer); } @@ -268,13 +266,11 @@ namespace Gecode { namespace Search { namespace Par { { if (!engine().scheduler_admit(index)) break; - engine().scheduler_action_begin(index); m.acquire(); if (idle) { m.release(); // Try to find new work if (!find()) { - engine().scheduler_failed_scan(index); engine().scheduler_handoff(index,engine().work_remains()); } } else if (cur != nullptr) { @@ -324,7 +320,7 @@ namespace Gecode { namespace Search { namespace Par { cur = nullptr; path.next(); m.release(); - engine().solution(s,index); + engine().solution(s); } break; case SS_BRANCH: @@ -365,7 +361,6 @@ namespace Gecode { namespace Search { namespace Par { engine().scheduler_idle(index); engine().scheduler_handoff(index,engine().work_remains()); } - engine().scheduler_action_end(index); } break; default: @@ -382,8 +377,6 @@ namespace Gecode { namespace Search { namespace Par { template void BAB::reset(Space* s) { - WorkerControlAccess::gate_release_all( - this->_opt.worker_control,WorkerControlAccess::GATE_ACTION_BEGIN); // Grab wait lock for reset m_wait_reset.acquire(); // Release workers for reset diff --git a/gecode/search/par/dfs.hpp b/gecode/search/par/dfs.hpp index 74c007de7e..d50e771462 100755 --- a/gecode/search/par/dfs.hpp +++ b/gecode/search/par/dfs.hpp @@ -210,13 +210,11 @@ namespace Gecode { namespace Search { namespace Par { { if (!engine().scheduler_admit(index)) break; - engine().scheduler_action_begin(index); m.acquire(); if (idle) { m.release(); // Try to find new work if (!find()) { - engine().scheduler_failed_scan(index); engine().scheduler_handoff(index,engine().work_remains()); } } else if (cur != nullptr) { @@ -266,7 +264,6 @@ namespace Gecode { namespace Search { namespace Par { cur = nullptr; path.next(); m.release(); - engine().scheduler_solution(index); engine().solution(s); } break; @@ -308,7 +305,6 @@ namespace Gecode { namespace Search { namespace Par { engine().scheduler_idle(index); engine().scheduler_handoff(index,engine().work_remains()); } - engine().scheduler_action_end(index); } break; default: @@ -325,8 +321,6 @@ namespace Gecode { namespace Search { namespace Par { template void DFS::reset(Space* s) { - WorkerControlAccess::gate_release_all( - this->_opt.worker_control,WorkerControlAccess::GATE_ACTION_BEGIN); // Grab wait lock for reset m_wait_reset.acquire(); // Release workers for reset diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index 2f8a203bf0..db02b0be9f 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -101,8 +101,6 @@ namespace Gecode { namespace Search { namespace Par { }; /// Whether adjustable parallel admission is enabled bool scheduler_enabled; - /// Dynamically enabled test instrumentation - const std::atomic* scheduler_instrumented; /// Unchanged-capacity admission fast path const std::atomic* scheduler_fast_admit; /// Mutex for adjustable parallel admission @@ -122,8 +120,6 @@ namespace Gecode { namespace Search { namespace Par { unsigned int exclude) const; /// Grant leases up to the current request bool scheduler_grow(void); - /// Publish scheduler state for focused tests - void scheduler_observe(void); public: /// Provide access to search options const Options& opt(void) const; @@ -135,20 +131,10 @@ namespace Gecode { namespace Search { namespace Par { void scheduler_reset(bool root_owner); /// Admit worker \a worker for one exploration action bool scheduler_admit(unsigned int worker); - /// Begin an admitted exploration action - void scheduler_action_begin(unsigned int worker); - /// End an admitted exploration action - void scheduler_action_end(unsigned int worker); - /// Pause after worker \a worker completes a failed steal scan - void scheduler_failed_scan(unsigned int worker); /// Record that worker \a worker owns search void scheduler_owner(unsigned int worker); /// Record that worker \a worker is idle void scheduler_idle(unsigned int worker); - /// Record that worker \a worker produced a solution - void scheduler_solution(unsigned int worker); - /// Record delivery of an incumbent to worker \a worker - void scheduler_incumbent(unsigned int worker); /// Hand worker \a worker's lease to a parked logical worker void scheduler_handoff(unsigned int worker, bool work_remains); diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index a096546c4a..ad3d860dd0 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -123,7 +123,6 @@ namespace Gecode { namespace Search { namespace Par { Engine::Engine(const Options& o) : Search::Engine(o,static_cast(o.threads)), _opt(o), scheduler_enabled(false), - scheduler_instrumented(nullptr), scheduler_fast_admit(nullptr), scheduler_worker(nullptr), scheduler_requested(workers()), scheduler_leases(0U), scheduler_cursor(0U), scheduler_generation(0U), @@ -178,39 +177,11 @@ namespace Gecode { namespace Search { namespace Par { return wake; } - template - void - Engine::scheduler_observe(void) { - unsigned int parked = 0U; - unsigned int owners = 0U; - unsigned int parked_owners = 0U; - for (unsigned int i=0U; i(scheduler_worker[i].logical), - scheduler_worker[i].lease,scheduler_worker[i].parked); - } - template void Engine::scheduler_enable(bool root_owner) { if (!WorkerControlAccess::engaged(worker_control)) return; - scheduler_instrumented = - WorkerControlAccess::instrumentation(worker_control); scheduler_fast_admit = WorkerControlAccess::fast_admission(worker_control); scheduler_worker = static_cast @@ -226,7 +197,6 @@ namespace Gecode { namespace Search { namespace Par { ((i == 0U) && root_owner) ? SL_OWNER : SL_PENDING; } scheduler_generation.store(generation,std::memory_order_release); - scheduler_observe(); scheduler_enabled = true; } @@ -249,7 +219,6 @@ namespace Gecode { namespace Search { namespace Par { ((i == 0U) && root_owner) ? SL_OWNER : SL_PENDING; } scheduler_generation.store(generation,std::memory_order_release); - scheduler_observe(); scheduler_mutex.release(); } @@ -260,9 +229,6 @@ namespace Gecode { namespace Search { namespace Par { return true; if (scheduler_fast_admit->load(std::memory_order_acquire)) return true; - if (scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::gate(worker_control, - WorkerControlAccess::GATE_ADMISSION,worker); while (cmd() == C_WORK) { unsigned long long int generation = WorkerControlAccess::generation(worker_control); @@ -289,7 +255,6 @@ namespace Gecode { namespace Search { namespace Par { } admitted = scheduler_worker[worker].lease; scheduler_worker[worker].parked = !admitted; - scheduler_observe(); scheduler_mutex.release(); if (requested == workers()) @@ -298,40 +263,11 @@ namespace Gecode { namespace Search { namespace Par { WorkerControlAccess::signal_all(worker_control); if (admitted) return true; - if (scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::gate(worker_control, - WorkerControlAccess::GATE_EVENT_WAIT,worker); WorkerControlAccess::wait(worker_control,worker); } return false; } - template - forceinline void - Engine::scheduler_action_begin(unsigned int worker) { - if (scheduler_enabled && - scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::action_begin( - worker_control,worker,scheduler_generation); - } - - template - forceinline void - Engine::scheduler_action_end(unsigned int worker) { - if (scheduler_enabled && - scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::action_end(worker_control,worker); - } - - template - forceinline void - Engine::scheduler_failed_scan(unsigned int worker) { - if (scheduler_enabled && - scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::gate(worker_control, - WorkerControlAccess::GATE_FAILED_SCAN,worker); - } - template void Engine::scheduler_owner(unsigned int worker) { @@ -339,7 +275,6 @@ namespace Gecode { namespace Search { namespace Par { return; scheduler_mutex.acquire(); scheduler_worker[worker].logical = SL_OWNER; - scheduler_observe(); scheduler_mutex.release(); } @@ -350,26 +285,9 @@ namespace Gecode { namespace Search { namespace Par { return; scheduler_mutex.acquire(); scheduler_worker[worker].logical = SL_IDLE; - scheduler_observe(); scheduler_mutex.release(); } - template - void - Engine::scheduler_solution(unsigned int worker) { - if (scheduler_enabled && - scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::solution(worker_control,worker); - } - - template - forceinline void - Engine::scheduler_incumbent(unsigned int worker) { - if (scheduler_enabled && - scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::incumbent(worker_control,worker); - } - template void Engine::scheduler_handoff(unsigned int worker, @@ -393,12 +311,9 @@ namespace Gecode { namespace Search { namespace Par { scheduler_worker[target].lease = true; scheduler_worker[target].parked = false; scheduler_cursor = (target+1U) % workers(); - if (scheduler_instrumented->load(std::memory_order_acquire)) - WorkerControlAccess::handoff(worker_control,worker,target); } } } - scheduler_observe(); scheduler_mutex.release(); if (target != workers()) WorkerControlAccess::signal(worker_control,target); @@ -504,9 +419,6 @@ namespace Gecode { namespace Search { namespace Par { Engine::terminate(void) { // Grab the wait mutex for termination _m_wait_terminate.acquire(); - // Test gates must not keep a worker from observing termination - WorkerControlAccess::gate_release_all( - _opt.worker_control,WorkerControlAccess::GATE_ACTION_BEGIN); // Release all threads release(C_TERMINATE); // Wait until all threads have acknowledged termination request diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 3cc87c8bda..17f1d6957d 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -43,119 +43,19 @@ namespace Gecode { namespace Search { DETACHED }; - class TestSupport { - public: - unsigned int capacity; - std::atomic - epoch[WorkerControlAccess::GATE_COUNT]; - std::atomic - worker[WorkerControlAccess::GATE_COUNT]; - std::atomic - expected[WorkerControlAccess::GATE_COUNT]; - std::atomic - reached_count[WorkerControlAccess::GATE_COUNT]; - std::atomic enabled[WorkerControlAccess::GATE_COUNT]; - Support::Event reached[WorkerControlAccess::GATE_COUNT]; - Support::Event* release; - std::atomic* seen; - std::atomic* waiting; - std::atomic admitted; - std::atomic generation_admitted; - std::atomic max_admitted; - std::atomic measured_generation; - std::atomic completed_generation; - std::atomic* action_generation; - std::atomic* logical; - std::atomic* lease; - std::atomic* parked; - std::atomic* incumbent_deliveries; - - TestSupport(unsigned int capacity0) - : capacity(capacity0), - release(new Support::Event - [WorkerControlAccess::GATE_COUNT * capacity]), - seen(new std::atomic - [WorkerControlAccess::GATE_COUNT * capacity]), - waiting(new std::atomic - [WorkerControlAccess::GATE_COUNT * capacity]), - admitted(0U), generation_admitted(0U), max_admitted(0U), - measured_generation(0U), completed_generation(0U), - action_generation( - new std::atomic[capacity]), - logical(new std::atomic[capacity]), - lease(new std::atomic[capacity]), - parked(new std::atomic[capacity]), - incumbent_deliveries( - new std::atomic[capacity]) { - for (unsigned int i=0U; i requested; std::atomic capacity; std::atomic generation; - std::atomic observed_generation; - std::atomic handoffs; - std::atomic leases; - std::atomic parked; - std::atomic owners; - std::atomic parked_owners; - std::atomic last_handoff_from; - std::atomic last_handoff_to; - std::atomic last_solution_worker; - std::atomic solution_handoff_from; - std::atomic solution_handoff_to; std::atomic fast_admit; - std::atomic instrumented; - std::atomic test_support; Support::Mutex mutex; Lifecycle lifecycle; Support::Event* events; State(unsigned int workers) : RefCount(1U), requested(workers), capacity(0U), generation(0U), - observed_generation(0U), handoffs(0U), leases(0U), parked(0U), - owners(0U), parked_owners(0U), - last_handoff_from(WorkerControlAccess::ALL_WORKERS), - last_handoff_to(WorkerControlAccess::ALL_WORKERS), - last_solution_worker(WorkerControlAccess::ALL_WORKERS), - solution_handoff_from(WorkerControlAccess::ALL_WORKERS), - solution_handoff_to(WorkerControlAccess::ALL_WORKERS), - fast_admit(false), instrumented(false), test_support(nullptr), - lifecycle(NEVER_BOUND), events(nullptr) {} + fast_admit(false), lifecycle(NEVER_BOUND), events(nullptr) {} ~State(void) { delete [] events; - delete test_support.load(std::memory_order_relaxed); } }; @@ -277,20 +177,6 @@ namespace Gecode { namespace Search { return control.state != nullptr; } - bool - WorkerControlAccess::attached(const WorkerControl& control) { - if (control.state == nullptr) - return false; - Support::Lock lock(control.state->mutex); - return control.state->lifecycle == WorkerControl::State::ATTACHED; - } - - const std::atomic* - WorkerControlAccess::instrumentation(const WorkerControl& control) { - return (control.state == nullptr) ? nullptr : - &control.state->instrumented; - } - const std::atomic* WorkerControlAccess::fast_admission(const WorkerControl& control) { return (control.state == nullptr) ? nullptr : @@ -304,8 +190,7 @@ namespace Gecode { namespace Search { return; WorkerControl::State* state = control.state; Support::Lock lock(state->mutex); - if (!state->instrumented.load(std::memory_order_relaxed) && - (state->generation.load(std::memory_order_relaxed) == generation) && + if ((state->generation.load(std::memory_order_relaxed) == generation) && (state->requested.load(std::memory_order_relaxed) == state->capacity.load(std::memory_order_relaxed))) state->fast_admit.store(true,std::memory_order_release); @@ -386,462 +271,9 @@ namespace Gecode { namespace Search { events = state->events; capacity = state->capacity.load(std::memory_order_relaxed); } - if (events == nullptr) - return; - for (unsigned int i=0U; ileases.store(leases,std::memory_order_release); - state->parked.store(parked,std::memory_order_release); - state->owners.store(owners,std::memory_order_release); - state->parked_owners.store(parked_owners,std::memory_order_release); - state->observed_generation.store(generation,std::memory_order_release); - } - - void - WorkerControlAccess::observe_worker(WorkerControl& control, - unsigned int worker, - unsigned int logical, - bool lease, bool parked) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - if (support == nullptr) - return; - assert(worker < support->capacity); - support->logical[worker].store(logical,std::memory_order_release); - support->lease[worker].store(lease,std::memory_order_release); - support->parked[worker].store(parked,std::memory_order_release); - } - - void - WorkerControlAccess::handoff(WorkerControl& control, unsigned int from, - unsigned int to) { - if (control.state == nullptr) - return; - control.state->last_handoff_from.store(from,std::memory_order_release); - control.state->last_handoff_to.store(to,std::memory_order_release); - (void) control.state->handoffs.fetch_add(1U,std::memory_order_release); - } - - void - WorkerControlAccess::solution(WorkerControl& control, unsigned int worker) { - if (control.state == nullptr) - return; - control.state->solution_handoff_from.store( - control.state->last_handoff_from.load(std::memory_order_acquire), - std::memory_order_release); - control.state->solution_handoff_to.store( - control.state->last_handoff_to.load(std::memory_order_acquire), - std::memory_order_release); - control.state->last_solution_worker.store(worker, - std::memory_order_release); - } - - void - WorkerControlAccess::incumbent(WorkerControl& control, - unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - if (support == nullptr) - return; - assert(worker < support->capacity); - (void) support->incumbent_deliveries[worker].fetch_add( - 1U,std::memory_order_release); - } - - void - WorkerControlAccess::gate(WorkerControl& control, Gate gate, - unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - if (support == nullptr) - return; - unsigned int g = static_cast(gate); - if (!support->enabled[g].load(std::memory_order_acquire)) - return; - unsigned int selected = - support->worker[g].load(std::memory_order_relaxed); - if ((selected != ALL_WORKERS) && (selected != worker)) - return; - unsigned int capacity = support->capacity; - if (worker >= capacity) - return; - unsigned long long int epoch = - support->epoch[g].load(std::memory_order_acquire); - std::atomic& seen = - support->seen[g*capacity+worker]; - unsigned long long int old = seen.load(std::memory_order_relaxed); - while ((old != epoch) && - !seen.compare_exchange_weak(old,epoch, - std::memory_order_acq_rel, - std::memory_order_relaxed)) {} - if (old == epoch) - return; - support->waiting[g*capacity+worker].store(true, - std::memory_order_release); - unsigned int reached = - support->reached_count[g].fetch_add(1U, - std::memory_order_acq_rel) + 1U; - if (reached == support->expected[g].load(std::memory_order_acquire)) - support->reached[g].signal(); - support->release[g*capacity+worker].wait(); - support->waiting[g*capacity+worker].store(false, - std::memory_order_release); - } - - void - WorkerControlAccess::gate_install(WorkerControl& control, Gate gate, - unsigned int worker, - unsigned int expected) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return; - WorkerControl::State::TestSupport* support; - { - Support::Lock lock(state->mutex); - state->fast_admit.store(false,std::memory_order_release); - support = state->test_support.load(std::memory_order_relaxed); - if (support == nullptr) { - unsigned int capacity = - state->capacity.load(std::memory_order_relaxed); - support = new WorkerControl::State::TestSupport(capacity); - state->test_support.store(support,std::memory_order_release); - state->instrumented.store(true,std::memory_order_release); - } - } - unsigned int capacity = support->capacity; - assert((worker == ALL_WORKERS) || (worker < capacity)); - assert((expected > 0U) && (expected <= capacity)); - unsigned int g = static_cast(gate); - support->enabled[g].store(false,std::memory_order_release); - support->worker[g].store(worker,std::memory_order_relaxed); - support->expected[g].store(expected,std::memory_order_relaxed); - support->reached_count[g].store(0U,std::memory_order_relaxed); - (void) support->epoch[g].fetch_add(1U,std::memory_order_release); - support->enabled[g].store(true,std::memory_order_release); - } - - void - WorkerControlAccess::gate_wait(WorkerControl& control, Gate gate) { - if (control.state == nullptr) - return; - WorkerControl::State::TestSupport* support = - control.state->test_support.load(std::memory_order_acquire); - assert(support != nullptr); - support->reached[static_cast(gate)].wait(); - } - - void - WorkerControlAccess::gate_release(WorkerControl& control, Gate gate, - unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - assert(support != nullptr); - unsigned int capacity = support->capacity; - assert(worker < capacity); - unsigned int slot = - static_cast(gate)*capacity+worker; - if (support->waiting[slot].load(std::memory_order_acquire)) - support->release[slot].signal(); - } - - void - WorkerControlAccess::gate_release_all(WorkerControl& control, Gate gate) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - if (support == nullptr) - return; - unsigned int g = static_cast(gate); - support->enabled[g].store(false,std::memory_order_release); - unsigned int capacity = support->capacity; - unsigned long long int epoch = - support->epoch[g].load(std::memory_order_acquire); - for (unsigned int i=0U; iseen[g*capacity+i].load(std::memory_order_acquire) == - epoch && - support->waiting[g*capacity+i].load(std::memory_order_acquire)) - support->release[g*capacity+i].signal(); - } - - bool - WorkerControlAccess::gate_waiting(const WorkerControl& control, Gate gate, - unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return false; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - if ((support == nullptr) || (worker >= support->capacity)) - return false; - unsigned int g = static_cast(gate); - return support->waiting[g*support->capacity+worker].load( - std::memory_order_acquire); - } - - void - WorkerControlAccess::action_begin( - WorkerControl& control, unsigned int worker, - const std::atomic& generation) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - if (support == nullptr) - return; - unsigned long long int action_generation = - generation.load(std::memory_order_acquire); - support->action_generation[worker].store(action_generation, - std::memory_order_release); - unsigned int current = - support->admitted.fetch_add(1U,std::memory_order_acq_rel) + 1U; - bool measured_action = true; - { - Support::Lock lock(state->mutex); - unsigned long long int measured = - support->measured_generation.load(std::memory_order_relaxed); - if (measured != 0U) { - if (action_generation != measured) - measured_action = false; - else - current = support->generation_admitted.fetch_add( - 1U,std::memory_order_relaxed) + 1U; - } - if (measured_action) { - unsigned int maximum = - support->max_admitted.load(std::memory_order_relaxed); - if (maximum < current) - support->max_admitted.store(current,std::memory_order_release); - } - } - if (measured_action) - WorkerControlAccess::gate(control,GATE_ACTION_BEGIN,worker); - } - - void - WorkerControlAccess::action_end(WorkerControl& control, - unsigned int worker) { - if (control.state == nullptr) - return; - WorkerControl::State::TestSupport* support = - control.state->test_support.load(std::memory_order_acquire); - if (support != nullptr) { - (void) support->admitted.fetch_sub(1U,std::memory_order_release); - unsigned long long int generation = - support->action_generation[worker].load(std::memory_order_acquire); - { - Support::Lock lock(control.state->mutex); - if (support->measured_generation.load(std::memory_order_relaxed) == - generation) - (void) support->generation_admitted.fetch_sub( - 1U,std::memory_order_relaxed); - } - unsigned long long int old = - support->completed_generation.load(std::memory_order_relaxed); - while ((old < generation) && - !support->completed_generation.compare_exchange_weak( - old,generation,std::memory_order_release, - std::memory_order_relaxed)) {} - } - } - - void - WorkerControlAccess::reset_max_admitted(WorkerControl& control) { - if (control.state != nullptr) { - Support::Lock lock(control.state->mutex); - WorkerControl::State::TestSupport* support = - control.state->test_support.load(std::memory_order_acquire); - assert(support != nullptr); - unsigned int current = - support->admitted.load(std::memory_order_acquire); - support->measured_generation.store(0U,std::memory_order_release); - support->max_admitted.store(current,std::memory_order_release); - } - } - - void - WorkerControlAccess::reset_max_admitted( - WorkerControl& control, unsigned long long int generation) { - if (control.state != nullptr) { - Support::Lock lock(control.state->mutex); - WorkerControl::State::TestSupport* support = - control.state->test_support.load(std::memory_order_acquire); - assert(support != nullptr); - support->generation_admitted.store(0U,std::memory_order_release); - support->max_admitted.store(0U,std::memory_order_release); - support->measured_generation.store(generation, - std::memory_order_release); - } - } - - unsigned int - WorkerControlAccess::admitted(const WorkerControl& control) { - if (control.state == nullptr) - return 0U; - WorkerControl::State::TestSupport* support = - control.state->test_support.load(std::memory_order_acquire); - return (support == nullptr) ? 0U : - support->admitted.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::max_admitted(const WorkerControl& control) { - if (control.state == nullptr) - return 0U; - WorkerControl::State::TestSupport* support = - control.state->test_support.load(std::memory_order_acquire); - return (support == nullptr) ? 0U : - support->max_admitted.load(std::memory_order_acquire); - } - - unsigned long long int - WorkerControlAccess::completed_generation(const WorkerControl& control) { - if (control.state == nullptr) - return 0U; - WorkerControl::State::TestSupport* support = - control.state->test_support.load(std::memory_order_acquire); - return (support == nullptr) ? 0U : - support->completed_generation.load(std::memory_order_acquire); - } - - unsigned long long int - WorkerControlAccess::observed_generation(const WorkerControl& control) { - return (control.state == nullptr) ? 0U : - control.state->observed_generation.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::leases(const WorkerControl& control) { - return (control.state == nullptr) ? 0U : - control.state->leases.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::parked(const WorkerControl& control) { - return (control.state == nullptr) ? 0U : - control.state->parked.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::owners(const WorkerControl& control) { - return (control.state == nullptr) ? 0U : - control.state->owners.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::parked_owners(const WorkerControl& control) { - return (control.state == nullptr) ? 0U : - control.state->parked_owners.load(std::memory_order_acquire); - } - - unsigned long long int - WorkerControlAccess::handoffs(const WorkerControl& control) { - return (control.state == nullptr) ? 0U : - control.state->handoffs.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::last_handoff_from(const WorkerControl& control) { - return (control.state == nullptr) ? ALL_WORKERS : - control.state->last_handoff_from.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::last_handoff_to(const WorkerControl& control) { - return (control.state == nullptr) ? ALL_WORKERS : - control.state->last_handoff_to.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::last_solution_worker(const WorkerControl& control) { - return (control.state == nullptr) ? ALL_WORKERS : - control.state->last_solution_worker.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::solution_handoff_from(const WorkerControl& control) { - return (control.state == nullptr) ? ALL_WORKERS : - control.state->solution_handoff_from.load(std::memory_order_acquire); - } - - unsigned int - WorkerControlAccess::solution_handoff_to(const WorkerControl& control) { - return (control.state == nullptr) ? ALL_WORKERS : - control.state->solution_handoff_to.load(std::memory_order_acquire); - } - - bool - WorkerControlAccess::owner(const WorkerControl& control, - unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return false; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - assert((support != nullptr) && (worker < support->capacity)); - return support->logical[worker].load(std::memory_order_acquire) == 0U; - } - - bool - WorkerControlAccess::leased(const WorkerControl& control, - unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return false; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - assert((support != nullptr) && (worker < support->capacity)); - return support->lease[worker].load(std::memory_order_acquire); - } - - bool - WorkerControlAccess::worker_parked(const WorkerControl& control, - unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return false; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - assert((support != nullptr) && (worker < support->capacity)); - return support->parked[worker].load(std::memory_order_acquire); - } - - unsigned long long int - WorkerControlAccess::incumbent_deliveries( - const WorkerControl& control, unsigned int worker) { - WorkerControl::State* state = control.state; - if (state == nullptr) - return 0U; - WorkerControl::State::TestSupport* support = - state->test_support.load(std::memory_order_acquire); - assert((support != nullptr) && (worker < support->capacity)); - return support->incumbent_deliveries[worker].load( - std::memory_order_acquire); + if (events != nullptr) + for (unsigned int i=0U; i(-1); /// Attach \a control to a leaf engine with fixed \a capacity static void attach(WorkerControl& control, unsigned int capacity); /// Detach \a control from its leaf engine static void detach(WorkerControl& control); /// Return the request generation static unsigned long long int generation(const WorkerControl& control); - /// Return whether \a control is engaged + /// Return whether \a control has state static bool engaged(const WorkerControl& control); - /// Return whether \a control is currently attached to a leaf engine - static bool attached(const WorkerControl& control); - /// Return the test-instrumentation flag retained by \a control - static const std::atomic* - instrumentation(const WorkerControl& control); /// Return the unchanged-capacity fast-admission flag static const std::atomic* fast_admission(const WorkerControl& control); @@ -81,87 +66,6 @@ namespace Gecode { namespace Search { static void signal(WorkerControl& control, unsigned int worker); /// Signal every worker event static void signal_all(WorkerControl& control); - /// Publish an internal scheduler snapshot - static void observe(WorkerControl& control, - unsigned long long int generation, - unsigned int leases, unsigned int parked, - unsigned int owners, unsigned int parked_owners); - /// Record the state of worker \a worker - static void observe_worker(WorkerControl& control, unsigned int worker, - unsigned int logical, bool lease, bool parked); - /// Record an internal scheduler handoff - static void handoff(WorkerControl& control, unsigned int from, - unsigned int to); - /// Record which worker produced a solution - static void solution(WorkerControl& control, unsigned int worker); - /// Record incumbent delivery to worker \a worker - static void incumbent(WorkerControl& control, unsigned int worker); - /// Pause a worker at an installed internal test boundary - static void gate(WorkerControl& control, Gate gate, unsigned int worker); - /// Install an internal test boundary gate - static void gate_install(WorkerControl& control, Gate gate, - unsigned int worker, unsigned int expected); - /// Wait until the installed gate has reached its expected count - static void gate_wait(WorkerControl& control, Gate gate); - /// Release one worker from an installed gate - static void gate_release(WorkerControl& control, Gate gate, - unsigned int worker); - /// Release all workers and disable an installed gate - static void gate_release_all(WorkerControl& control, Gate gate); - /// Return whether worker \a worker is stopped at gate \a gate - static bool gate_waiting(const WorkerControl& control, Gate gate, - unsigned int worker); - /// Begin one admitted parallel-search action - static void action_begin( - WorkerControl& control, unsigned int worker, - const std::atomic& generation); - /// End one admitted parallel-search action - static void action_end(WorkerControl& control, unsigned int worker); - /// Reset the admitted-action high-water mark - static void reset_max_admitted(WorkerControl& control); - /// Reset the high-water mark for actions admitted in \a generation - static void reset_max_admitted(WorkerControl& control, - unsigned long long int generation); - /// Return the current admitted-action count - static unsigned int admitted(const WorkerControl& control); - /// Return the admitted-action high-water mark - static unsigned int max_admitted(const WorkerControl& control); - /// Return the newest generation with a completed admitted action - static unsigned long long int completed_generation( - const WorkerControl& control); - /// Return the scheduler's observed request generation - static unsigned long long int observed_generation( - const WorkerControl& control); - /// Return the observed execution lease count - static unsigned int leases(const WorkerControl& control); - /// Return the observed parked worker count - static unsigned int parked(const WorkerControl& control); - /// Return the observed logical owner count - static unsigned int owners(const WorkerControl& control); - /// Return the observed parked logical owner count - static unsigned int parked_owners(const WorkerControl& control); - /// Return the observed lease handoff count - static unsigned long long int handoffs(const WorkerControl& control); - /// Return the source worker of the last handoff - static unsigned int last_handoff_from(const WorkerControl& control); - /// Return the target worker of the last handoff - static unsigned int last_handoff_to(const WorkerControl& control); - /// Return the worker that most recently produced a solution - static unsigned int last_solution_worker(const WorkerControl& control); - /// Return the handoff source observed when the last solution was produced - static unsigned int solution_handoff_from(const WorkerControl& control); - /// Return the handoff target observed when the last solution was produced - static unsigned int solution_handoff_to(const WorkerControl& control); - /// Return whether worker \a worker is a logical owner - static bool owner(const WorkerControl& control, unsigned int worker); - /// Return whether worker \a worker holds an execution lease - static bool leased(const WorkerControl& control, unsigned int worker); - /// Return whether worker \a worker is parked - static bool worker_parked(const WorkerControl& control, - unsigned int worker); - /// Return incumbent deliveries observed for worker \a worker - static unsigned long long int incumbent_deliveries( - const WorkerControl& control, unsigned int worker); }; }} diff --git a/test/search.cpp b/test/search.cpp index e61664d65d..81c0ea4353 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -37,15 +37,12 @@ #include #include -#include #include "test/test.hh" #include -#include #include #include -#include static_assert(std::is_copy_constructible::value, "NoGoods must remain copy constructible"); @@ -73,10 +70,6 @@ static_assert( static_assert( std::is_copy_assignable::value, "WorkerControl must remain copy assignable"); -static_assert(std::is_copy_constructible::value, - "Search::Options must remain copy constructible"); -static_assert(std::is_copy_assignable::value, - "Search::Options must remain copy assignable"); namespace Test { @@ -350,2329 +343,6 @@ namespace Test { } }; - /// Focused tests for external worker control - class WorkerControl : public Base { - private: - enum Scenario { - VALUES, - ERRORS, - BINDING, - CONCURRENT, - COMPATIBILITY, - DFS_RESIZE_ENUMERATION, - DFS_RESIZE_BOUNDARIES, - DFS_RESIZE_HANDOFF, - DFS_RESIZE_LIFECYCLE, - BAB_RESIZE_OPTIMALITY_DEFAULT, - BAB_RESIZE_OPTIMALITY_FREQUENT, - BAB_RESIZE_INCUMBENTS, - BAB_RESIZE_LIFECYCLE, - STRESS_DFS, - STRESS_BAB, - STRESS_DESTRUCTION - } scenario; - - /// Stable finite tree used by the DFS resizing tests - class ResizeSpace : public Gecode::Space { - public: - Gecode::BoolVarArray x; - - ResizeSpace(void) - : x(*this,12,0,1) { - Gecode::branch(*this,x,Gecode::BOOL_VAR_NONE(), - Gecode::BOOL_VAL_MIN()); - } - - ResizeSpace(ResizeSpace& s) - : Gecode::Space(s) { - x.update(*this,s.x); - } - - virtual Gecode::Space* copy(void) { - return new ResizeSpace(*this); - } - - unsigned int id(void) const { - unsigned int result = 0U; - for (int i=0; i(x[i].val()); - return result; - } - }; - - /// One non-stealable current solution and no stealable path - class SingleSolutionSpace : public Gecode::Space { - public: - SingleSolutionSpace(void) {} - SingleSolutionSpace(SingleSolutionSpace& s) - : Gecode::Space(s) {} - virtual Gecode::Space* copy(void) { - return new SingleSolutionSpace(*this); - } - }; - - /// Finite minimization tree with a unique optimum - class BABResizeSpace : public Gecode::Space { - public: - Gecode::IntVar x; - - BABResizeSpace(void) - : x(*this,0,4095) { - Gecode::branch(*this,x,Gecode::INT_VAL_MAX()); - } - - explicit BABResizeSpace(int value) - : x(*this,value,value) {} - - BABResizeSpace(int min, int max) - : x(*this,min,max) { - Gecode::branch(*this,x,Gecode::INT_VAL_MAX()); - } - - BABResizeSpace(BABResizeSpace& s) - : Gecode::Space(s) { - x.update(*this,s.x); - } - - virtual Gecode::Space* copy(void) { - return new BABResizeSpace(*this); - } - - virtual void constrain(const Gecode::Space& _s) { - const BABResizeSpace& s = - static_cast(_s); - Gecode::rel(*this,x,Gecode::IRT_LE,s.x.val()); - } - - int value(void) const { - return x.val(); - } - }; - - /// Thread-safe trace lifecycle observations - class CountingTracer : public Gecode::SearchTracer { - public: - std::atomic init_count; - std::atomic node_count; - std::atomic done_count; - std::atomic nodes_at_done; - - CountingTracer(void) - : init_count(0U), node_count(0U), done_count(0U), - nodes_at_done(0U) {} - virtual void init(void) { - (void) init_count.fetch_add(1U,std::memory_order_relaxed); - } - virtual void round(unsigned int) {} - virtual void skip(const EdgeInfo&) {} - virtual void node(const EdgeInfo&, const NodeInfo&) { - (void) node_count.fetch_add(1U,std::memory_order_relaxed); - } - virtual void done(void) { - nodes_at_done.store(node_count.load(std::memory_order_acquire), - std::memory_order_release); - (void) done_count.fetch_add(1U,std::memory_order_release); - } - }; - - static bool converged(const Gecode::Search::WorkerControl& control, - unsigned long long int generation, - unsigned int leases) { - for (unsigned int spin=0U; spin<10000000U; spin++) { - if ((Gecode::Search::WorkerControlAccess:: - observed_generation(control) == generation) && - (Gecode::Search::WorkerControlAccess::leases(control) == - leases)) - return true; - std::this_thread::yield(); - } - return false; - } - - static bool settled(const Gecode::Search::WorkerControl& control, - unsigned long long int generation, - unsigned int leases) { - for (unsigned int spin=0U; spin<10000000U; spin++) { - if ((Gecode::Search::WorkerControlAccess:: - observed_generation(control) == generation) && - (Gecode::Search::WorkerControlAccess::leases(control) == - leases) && - (Gecode::Search::WorkerControlAccess::admitted(control) <= - leases)) - return true; - std::this_thread::yield(); - } - return false; - } - - template - static bool throws(Function function) { - try { - function(); - } catch (const Exception&) { - return true; - } catch (...) { - return false; - } - return false; - } - - static unsigned int capacity(void) { -#ifdef GECODE_HAS_THREADS - return 4U; -#else - return 1U; -#endif - } - - static Gecode::Search::Engine* - dfs_engine(Gecode::Search::Options& o) { - HasSolutions* m = - new HasSolutions(HTB_BINARY,HTB_BINARY,HTB_BINARY); - Gecode::Search::Engine* e; - try { - e = Gecode::Search::dfsengine(m,o); - } catch (...) { - delete m; - throw; - } - delete m; - return e; - } - - static Gecode::Search::Engine* - bab_resize_engine(Gecode::Search::Options& o) { - BABResizeSpace* m = new BABResizeSpace; - Gecode::Search::Engine* e; - try { - e = Gecode::Search::babengine(m,o); - } catch (...) { - delete m; - throw; - } - delete m; - return e; - } - - static int bab_resize_best(Gecode::Search::Options o) { - Gecode::Search::Engine* bab = bab_resize_engine(o); - int best = 4096; - while (BABResizeSpace* solution = - static_cast(bab->next())) { - best = solution->value(); - delete solution; - } - bool ok = !bab->stopped(); - delete bab; - return ok ? best : 4096; - } - - static bool values(void) { - Gecode::Search::WorkerControl empty; - Gecode::Search::Options defaults; - if (empty || (empty.requested() != 0U) || - (empty.capacity() != 0U) || defaults.worker_control) - return false; - - Gecode::Search::WorkerControl control(3U); - Gecode::Search::WorkerControl copy(control); - Gecode::Search::WorkerControl assigned; - assigned = copy; - Gecode::Search::Options o; - o.worker_control = assigned; - Gecode::Search::Options copied_options(o); - copy.request(2U); - return control && (control.requested() == 2U) && - (assigned.requested() == 2U) && - (o.worker_control.requested() == 2U) && - (copied_options.worker_control.requested() == 2U) && - (control.capacity() == 0U); - } - - static bool errors(void) { - if (!throws([] { - Gecode::Search::WorkerControl invalid(0U); - (void) invalid; - })) - return false; - Gecode::Search::WorkerControl empty; - if (!throws( - [&] { empty.request(1U); })) - return false; - Gecode::Search::WorkerControl unbound(1U); - if (!throws( - [&] { unbound.request(0U); })) - return false; - - Gecode::Search::Options too_many; - too_many.threads = 1.0; - too_many.worker_control = - Gecode::Search::WorkerControl(2U); - if (!throws([&] { - Gecode::Search::Engine* e = dfs_engine(too_many); - delete e; - })) - return false; - - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = Gecode::Search::WorkerControl(1U); - Gecode::Search::Engine* e = dfs_engine(o); - bool exact = throws( - [&] { o.worker_control.request(capacity()+1U); }); - delete e; - return exact; - } - - static bool binding(void) { - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = Gecode::Search::WorkerControl(1U); - Gecode::Search::Options copied(o); - Gecode::Search::Engine* first = dfs_engine(o); - bool ok = (o.threads == 4.0) && (copied.threads == 4.0) && - (o.worker_control.capacity() == capacity()) && - (copied.worker_control.capacity() == capacity()); - bool while_live = throws([&] { - Gecode::Search::Engine* e = dfs_engine(copied); - delete e; - }); - delete first; - bool after_destroy = throws([&] { - Gecode::Search::Engine* e = dfs_engine(copied); - delete e; - }); - o.worker_control.request(1U); - - Gecode::Search::Options sequential_dfs; - sequential_dfs.threads = 1.0; - sequential_dfs.worker_control = - Gecode::Search::WorkerControl(1U); - Gecode::Search::Engine* dfs = dfs_engine(sequential_dfs); - ok = ok && (sequential_dfs.worker_control.capacity() == 1U); - delete dfs; - - Gecode::Search::Options sequential_bab; - sequential_bab.threads = 1.0; - sequential_bab.worker_control = - Gecode::Search::WorkerControl(1U); - HasSolutions* m = new HasSolutions( - HTB_BINARY,HTB_BINARY,HTB_BINARY,HTC_LEX_LE); - Gecode::Search::Engine* bab = - Gecode::Search::babengine(m,sequential_bab); - delete m; - ok = ok && (sequential_bab.worker_control.capacity() == 1U); - delete bab; - - return ok && while_live && after_destroy && - (copied.worker_control.requested() == 1U); - } - - static bool concurrent(void) { - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = Gecode::Search::WorkerControl(1U); - Gecode::Search::Engine* e = dfs_engine(o); -#ifdef GECODE_HAS_THREADS - std::vector writers; - for (unsigned int writer=0U; writer<4U; writer++) { - Gecode::Search::WorkerControl copy(o.worker_control); - writers.emplace_back([copy,writer] () mutable { - for (unsigned int i=0U; i<2000U; i++) - copy.request(1U + ((i+writer) % 4U)); - }); - } - for (std::thread& writer : writers) - writer.join(); - - std::atomic started(false); - Gecode::Search::WorkerControl copy(o.worker_control); - std::thread during_destruction([copy,&started] () mutable { - started.store(true,std::memory_order_release); - for (unsigned int i=0U; i<10000U; i++) - copy.request(1U + (i % 4U)); - }); - while (!started.load(std::memory_order_acquire)) {} - delete e; - during_destruction.join(); -#else - o.worker_control.request(1U); - delete e; -#endif - o.worker_control.request(1U); - return (o.worker_control.capacity() == capacity()) && - (o.worker_control.requested() == 1U); - } - - static int dfs_solutions(const Gecode::Search::WorkerControl& control) { - HasSolutions* m = - new HasSolutions(HTB_BINARY,HTB_BINARY,HTB_BINARY); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - Gecode::DFS dfs(m,o); - delete m; - int solutions = 0; - while (HasSolutions* solution = dfs.next()) { - solutions++; - delete solution; - } - return solutions; - } - - static bool bab_best(const Gecode::Search::WorkerControl& control) { - HasSolutions* m = new HasSolutions( - HTB_BINARY,HTB_BINARY,HTB_BINARY,HTC_LEX_LE); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - Gecode::BAB bab(m,o); - delete m; - HasSolutions* best = nullptr; - while (HasSolutions* solution = bab.next()) { - delete best; - best = solution; - } - bool ok = (best != nullptr) && best->best(); - delete best; - return ok; - } - - static bool compatibility(void) { - Gecode::Search::WorkerControl empty; - Gecode::Search::WorkerControl dfs_control(capacity()); - Gecode::Search::WorkerControl bab_control(capacity()); - return (dfs_solutions(empty) == 8) && - (dfs_solutions(dfs_control) == 8) && - bab_best(empty) && bab_best(bab_control); - } - - static bool resized_enumeration(void) { -#ifdef GECODE_HAS_THREADS - Gecode::Search::WorkerControl control(4U); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - ResizeSpace* root = new ResizeSpace; - Gecode::DFS dfs(root,o); - delete root; - - std::atomic solutions(0U); - std::atomic controller_ok(true); - std::atomic paused(0U); - std::atomic published(0U); - std::atomic completed(0U); - std::atomic exhausted(false); - const unsigned int limits[] = {1U,3U,1U,4U}; - const unsigned int milestones[] = {1U,64U,128U,192U}; - // The consumer processes no solution beyond a milestone until the - // controller has observed convergence for that milestone's request. - std::thread controller([&] { - for (unsigned int i=0U; i<4U; i++) { - while ((paused.load(std::memory_order_acquire) < i+1U) && - !exhausted.load(std::memory_order_acquire)) - std::this_thread::yield(); - if (exhausted.load(std::memory_order_acquire)) { - controller_ok.store(false,std::memory_order_release); - published.store(4U,std::memory_order_release); - completed.store(4U,std::memory_order_release); - return; - } - try { - control.request(limits[i]); - } catch (...) { - controller_ok.store(false,std::memory_order_release); - published.store(4U,std::memory_order_release); - completed.store(4U,std::memory_order_release); - return; - } - unsigned long long int generation = - Gecode::Search::WorkerControlAccess::generation(control); - published.store(i+1U,std::memory_order_release); - bool converged = false; - for (unsigned int spin=0U; spin<10000000U; spin++) { - if (exhausted.load(std::memory_order_acquire)) - break; - if ((Gecode::Search::WorkerControlAccess:: - observed_generation(control) == generation) && - (Gecode::Search::WorkerControlAccess::leases(control) <= - limits[i]) && - (Gecode::Search::WorkerControlAccess::parked(control) >= - 4U-limits[i])) { - converged = true; - break; - } - std::this_thread::yield(); - } - if (!converged) { - controller_ok.store(false,std::memory_order_release); - completed.store(4U,std::memory_order_release); - return; - } - completed.store(i+1U,std::memory_order_release); - } - }); - - std::vector counts(1U << 12,0U); - std::vector pending; - unsigned int next_milestone = 0U; - bool search_exhausted = false; - while (!search_exhausted || !pending.empty()) { - ResizeSpace* solution; - if (pending.empty()) { - solution = dfs.next(); - if (solution == nullptr) { - search_exhausted = true; - exhausted.store(true,std::memory_order_release); - continue; - } - } else { - solution = pending.back(); - pending.pop_back(); - } - counts[solution->id()]++; - delete solution; - unsigned int n = - solutions.fetch_add(1U,std::memory_order_release)+1U; - if ((next_milestone < 4U) && - (n == milestones[next_milestone])) { - unsigned int phase = next_milestone+1U; - paused.store(next_milestone+1U,std::memory_order_release); - while ((published.load(std::memory_order_acquire) < phase) && - controller_ok.load(std::memory_order_acquire)) - std::this_thread::yield(); - while ((completed.load(std::memory_order_acquire) < phase) && - controller_ok.load(std::memory_order_acquire)) { - ResizeSpace* extra = dfs.next(); - if (extra == nullptr) { - search_exhausted = true; - exhausted.store(true,std::memory_order_release); - break; - } - pending.push_back(extra); - } - next_milestone++; - } - } - controller.join(); - if (!controller_ok.load(std::memory_order_acquire) || - (solutions.load(std::memory_order_acquire) != counts.size())) - return false; - for (unsigned int count : counts) - if (count != 1U) - return false; -#endif - return true; - } - - static bool resize_boundaries(void) { -#ifdef GECODE_HAS_THREADS - using Gecode::Search::WorkerControlAccess; - Gecode::Search::WorkerControl control(4U); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - ResizeSpace* root = new ResizeSpace; - Gecode::DFS dfs(root,o); - delete root; - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,4U); - std::atomic finish(false); - std::atomic solutions(0U); - std::thread consumer([&] { - while (!finish.load(std::memory_order_acquire)) { - ResizeSpace* solution = dfs.next(); - if (solution == nullptr) - return; - delete solution; - (void) solutions.fetch_add(1U,std::memory_order_release); - } - }); - - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_ADMISSION); - if (WorkerControlAccess::admitted(control) != 0U) { - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_ADMISSION); - finish.store(true,std::memory_order_release); - consumer.join(); - return false; - } - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,3U); - control.request(1U); - unsigned long long int generation = - WorkerControlAccess::generation(control); - WorkerControlAccess::reset_max_admitted(control); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,0U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,2U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_EVENT_WAIT); - bool ok = converged(control,generation,1U) && - (WorkerControlAccess::max_admitted(control) <= 1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,3U); - for (unsigned int spin=0U; - (spin<10000000U) && - (WorkerControlAccess::max_admitted(control) == 0U); spin++) - std::this_thread::yield(); - ok = ok && (WorkerControlAccess::max_admitted(control) == 1U); - - // The request is published and signals worker events while the - // parked workers are still stopped immediately before event wait. - control.request(3U); - generation = WorkerControlAccess::generation(control); - WorkerControlAccess::reset_max_admitted(control); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - ok = ok && converged(control,generation,3U) && - (WorkerControlAccess::max_admitted(control) <= 3U); - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,3U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_ADMISSION); - unsigned int leased[3]; - unsigned int n_leased = 0U; - for (unsigned int i=0U; i<4U; i++) - if (WorkerControlAccess::leased(control,i)) { - if (n_leased < 3U) - leased[n_leased] = i; - n_leased++; - } - if (n_leased != 3U) { - finish.store(true,std::memory_order_release); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_ADMISSION); - consumer.join(); - return false; - } - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,2U); - control.request(1U); - generation = WorkerControlAccess::generation(control); - WorkerControlAccess::reset_max_admitted(control); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,leased[0]); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,leased[1]); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_EVENT_WAIT); - ok = ok && converged(control,generation,1U) && - (WorkerControlAccess::max_admitted(control) <= 1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,leased[2]); - for (unsigned int spin=0U; - (spin<10000000U) && - (WorkerControlAccess::max_admitted(control) == 0U); spin++) - std::this_thread::yield(); - ok = ok && (WorkerControlAccess::max_admitted(control) == 1U); - - finish.store(true,std::memory_order_release); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - consumer.join(); - return ok && (solutions.load(std::memory_order_acquire) > 0U); -#else - return true; -#endif - } - - static bool resize_handoff(void) { -#ifdef GECODE_HAS_THREADS - using Gecode::Search::WorkerControlAccess; - Gecode::Search::WorkerControl control(4U); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - SingleSolutionSpace* root = new SingleSolutionSpace; - Gecode::DFS dfs(root,o); - delete root; - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,4U); - std::atomic result(nullptr); - std::thread consumer([&] { - result.store(dfs.next(),std::memory_order_release); - }); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_ADMISSION); - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_FAILED_SCAN,3U,1U); - // Let worker 3 declare its initially empty state idle. With no parked - // target at capacity, its next action completes a failed steal scan. - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,3U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_FAILED_SCAN); - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,3U); - control.request(1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,0U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,2U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_EVENT_WAIT); - - bool exact_setup = - WorkerControlAccess::owner(control,0U) && - WorkerControlAccess::worker_parked(control,0U) && - WorkerControlAccess::leased(control,3U); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - - unsigned long long int handoffs = - WorkerControlAccess::handoffs(control); - bool before_handoff = exact_setup && - WorkerControlAccess::leased(control,3U) && - WorkerControlAccess::worker_parked(control,0U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_FAILED_SCAN,3U); - consumer.join(); - - SingleSolutionSpace* solution = - result.load(std::memory_order_acquire); - bool ok = before_handoff && (solution != nullptr) && - (WorkerControlAccess::handoffs(control) >= handoffs+1U) && - (WorkerControlAccess::last_solution_worker(control) == 0U); - ok = ok && - (WorkerControlAccess::solution_handoff_from(control) == 3U) && - (WorkerControlAccess::solution_handoff_to(control) == 0U); - delete solution; - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_FAILED_SCAN); - return ok; -#else - return true; -#endif - } - - static bool resize_lifecycle(void) { -#ifdef GECODE_HAS_THREADS - Gecode::Search::WorkerControl control(1U); - Gecode::Search::NodeStop stop(10U); - CountingTracer tracer; - Gecode::Search::Options o; - o.threads = 4.0; - o.nogoods_limit = 16U; - o.stop = &stop; - o.tracer = &tracer; - o.worker_control = control; - ResizeSpace* root = new ResizeSpace; - Gecode::Search::Engine* dfs = - Gecode::Search::dfsengine(root,o); - delete root; - - using Gecode::Search::WorkerControlAccess; - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,4U); - std::atomic stopped_result(nullptr); - std::thread stopped_next([&] { - stopped_result.store(static_cast(dfs->next()), - std::memory_order_release); - }); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,3U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,2U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,3U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_EVENT_WAIT); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,0U); - stopped_next.join(); - ResizeSpace* solution = - stopped_result.load(std::memory_order_acquire); - bool ok = (solution == nullptr) && dfs->stopped() && - (dfs->statistics().node > 0U) && - (WorkerControlAccess::parked(control) >= 3U); - delete solution; - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - (void) dfs->nogoods(); - - control.request(3U); - root = new ResizeSpace; - dfs->reset(root); - control.request(1U); - stop.limit(100000U); - solution = static_cast(dfs->next()); - ok = ok && (solution != nullptr); - delete solution; - delete dfs; - return ok && - (tracer.init_count.load(std::memory_order_acquire) == 1U) && - (tracer.node_count.load(std::memory_order_acquire) > 0U) && - (tracer.done_count.load(std::memory_order_acquire) == 1U) && - (tracer.nodes_at_done.load(std::memory_order_acquire) == - tracer.node_count.load(std::memory_order_acquire)); -#else - return true; -#endif - } - - static bool bab_resize_optimality(bool frequent) { -#ifdef GECODE_HAS_THREADS - using Gecode::Search::WorkerControlAccess; - Gecode::Search::Options baseline_options; - baseline_options.threads = 4.0; - if (frequent) { - baseline_options.c_d = 1U; - baseline_options.a_d = 1U; - } - int baseline = bab_resize_best(baseline_options); - - Gecode::Search::WorkerControl control(4U); - Gecode::Search::Options o(baseline_options); - o.worker_control = control; - Gecode::Search::Engine* bab = bab_resize_engine(o); - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,4U); - std::atomic best(4096); - std::atomic allowed(0U); - std::atomic completed(0U); - std::atomic phase_generation(0U); - std::atomic complete(false); - std::atomic consumer_ok(true); - std::thread consumer([&] { - for (unsigned int phase=0U; phase<4U; phase++) { - while (allowed.load(std::memory_order_acquire) <= phase) - std::this_thread::yield(); - do { - BABResizeSpace* solution = - static_cast(bab->next()); - if (solution == nullptr) { - consumer_ok.store(false,std::memory_order_release); - completed.store(4U,std::memory_order_release); - return; - } - best.store(solution->value(),std::memory_order_release); - delete solution; - } while (WorkerControlAccess::completed_generation(control) < - phase_generation.load(std::memory_order_acquire)); - while (WorkerControlAccess::admitted(control) != 0U) - std::this_thread::yield(); - completed.store(phase+1U,std::memory_order_release); - } - while (BABResizeSpace* solution = - static_cast(bab->next())) { - best.store(solution->value(),std::memory_order_release); - delete solution; - } - complete.store(true,std::memory_order_release); - }); - - allowed.store(1U,std::memory_order_release); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,3U); - control.request(1U); - unsigned long long int generation = - WorkerControlAccess::generation(control); - phase_generation.store(generation,std::memory_order_release); - WorkerControlAccess::reset_max_admitted(control,generation); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,2U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,3U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_EVENT_WAIT); - bool ok = settled(control,generation,1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,0U); - while (completed.load(std::memory_order_acquire) < 1U) - std::this_thread::yield(); - ok = ok && consumer_ok.load(std::memory_order_acquire) && - (WorkerControlAccess::max_admitted(control) <= 1U); - - const unsigned int limits[] = {3U,1U,4U}; - for (unsigned int i=0U; i<3U; i++) { - unsigned int limit = limits[i]; - control.request(limit); - generation = WorkerControlAccess::generation(control); - phase_generation.store(generation,std::memory_order_release); - WorkerControlAccess::reset_max_admitted(control,generation); - if (limit > 1U) - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - allowed.store(i+2U,std::memory_order_release); - while (completed.load(std::memory_order_acquire) < i+2U) - std::this_thread::yield(); - ok = ok && consumer_ok.load(std::memory_order_acquire) && - settled(control,generation,limit) && - (WorkerControlAccess::max_admitted(control) <= limit); - } - - consumer.join(); - ok = ok && complete.load(std::memory_order_acquire) && - !bab->stopped() && (baseline == 0) && - (best.load(std::memory_order_acquire) == baseline); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - delete bab; - return ok; -#else - Gecode::Search::Options o; - o.threads = 1.0; - if (frequent) { - o.c_d = 1U; - o.a_d = 1U; - } - return bab_resize_best(o) == 0; -#endif - } - - static bool bab_resize_incumbents(void) { -#ifdef GECODE_HAS_THREADS - using Gecode::Search::WorkerControlAccess; - Gecode::Search::WorkerControl control(4U); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - Gecode::Search::Engine* bab = bab_resize_engine(o); - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,4U); - std::atomic first(nullptr); - std::atomic first_done(false); - std::atomic proceed(false); - std::atomic solutions_better(true); - std::atomic solutions(0U); - std::atomic best(4096); - std::thread consumer([&] { - BABResizeSpace* solution = - static_cast(bab->next()); - first.store(solution,std::memory_order_release); - first_done.store(true,std::memory_order_release); - while (!proceed.load(std::memory_order_acquire)) - std::this_thread::yield(); - while (solution != nullptr) { - if (solution->value() >= 3000) - solutions_better.store(false,std::memory_order_release); - best.store(solution->value(),std::memory_order_release); - (void) solutions.fetch_add(1U,std::memory_order_release); - delete solution; - solution = static_cast(bab->next()); - } - }); - - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,3U); - control.request(1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,2U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,3U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_EVENT_WAIT); - - unsigned long long int before_external[4]; - for (unsigned int i=0U; i<4U; i++) - before_external[i] = - WorkerControlAccess::incumbent_deliveries(control,i); - BABResizeSpace external(3000); - (void) external.status(); - bab->constrain(external); - bool ok = true; - for (unsigned int i=0U; i<4U; i++) - ok = ok && - (WorkerControlAccess::incumbent_deliveries(control,i) == - before_external[i]+1U); - ok = ok && WorkerControlAccess::worker_parked(control,1U) && - WorkerControlAccess::worker_parked(control,2U) && - WorkerControlAccess::worker_parked(control,3U); - - unsigned long long int before_internal[4]; - for (unsigned int i=0U; i<4U; i++) - before_internal[i] = - WorkerControlAccess::incumbent_deliveries(control,i); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,0U); - while (!first_done.load(std::memory_order_acquire)) - std::this_thread::yield(); - BABResizeSpace* first_solution = - first.load(std::memory_order_acquire); - ok = ok && (first_solution != nullptr) && - (first_solution->value() < external.value()); - for (unsigned int i=0U; i<4U; i++) - ok = ok && - (WorkerControlAccess::incumbent_deliveries(control,i) >= - before_internal[i]+1U); - - // Exercise the equal-to-capacity transition from the exact parked - // state above. The pending request is reconciled when next resumes. - control.request(4U); - unsigned long long int generation = - WorkerControlAccess::generation(control); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - proceed.store(true,std::memory_order_release); - ok = ok && converged(control,generation,4U) && - (control.requested() == control.capacity()); - - consumer.join(); - ok = ok && !bab->stopped() && - solutions_better.load(std::memory_order_acquire) && - (solutions.load(std::memory_order_acquire) > 0U) && - (best.load(std::memory_order_acquire) == 0); - delete bab; - - // Repeat external constrain while a resize generation is pending - // and worker 0 remains stopped at the admission boundary. - Gecode::Search::WorkerControl transition_control(4U); - Gecode::Search::Options transition_options; - transition_options.threads = 4.0; - transition_options.worker_control = transition_control; - bab = bab_resize_engine(transition_options); - WorkerControlAccess::gate_install( - transition_control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,4U); - std::atomic transition_ok(true); - std::atomic transition_best(4096); - std::thread transition_consumer([&] { - while (BABResizeSpace* solution = - static_cast(bab->next())) { - if (solution->value() >= 2000) - transition_ok.store(false,std::memory_order_release); - transition_best.store(solution->value(), - std::memory_order_release); - delete solution; - } - }); - WorkerControlAccess::gate_wait( - transition_control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_install( - transition_control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,3U); - transition_control.request(1U); - WorkerControlAccess::gate_release( - transition_control,WorkerControlAccess::GATE_ADMISSION,1U); - WorkerControlAccess::gate_release( - transition_control,WorkerControlAccess::GATE_ADMISSION,2U); - WorkerControlAccess::gate_release( - transition_control,WorkerControlAccess::GATE_ADMISSION,3U); - WorkerControlAccess::gate_wait( - transition_control,WorkerControlAccess::GATE_EVENT_WAIT); - - unsigned long long int transition_before[4]; - for (unsigned int i=0U; i<4U; i++) - transition_before[i] = - WorkerControlAccess::incumbent_deliveries( - transition_control,i); - transition_control.request(3U); - generation = - WorkerControlAccess::generation(transition_control); - BABResizeSpace transition_external(2000); - (void) transition_external.status(); - bab->constrain(transition_external); - ok = ok && - WorkerControlAccess::gate_waiting( - transition_control,WorkerControlAccess::GATE_ADMISSION,0U) && - (WorkerControlAccess::observed_generation(transition_control) != - generation); - for (unsigned int i=0U; i<4U; i++) - ok = ok && - (WorkerControlAccess::incumbent_deliveries( - transition_control,i) == transition_before[i]+1U); - ok = ok && - WorkerControlAccess::worker_parked(transition_control,1U) && - WorkerControlAccess::worker_parked(transition_control,2U) && - WorkerControlAccess::worker_parked(transition_control,3U); - - // Worker 1 reconciles the pending generation before any original - // active worker leaves admission. - WorkerControlAccess::gate_release( - transition_control,WorkerControlAccess::GATE_EVENT_WAIT,1U); - ok = ok && settled(transition_control,generation,3U); - WorkerControlAccess::gate_release_all( - transition_control,WorkerControlAccess::GATE_EVENT_WAIT); - WorkerControlAccess::gate_release_all( - transition_control,WorkerControlAccess::GATE_ADMISSION); - transition_consumer.join(); - ok = ok && transition_ok.load(std::memory_order_acquire) && - !bab->stopped() && - (transition_best.load(std::memory_order_acquire) == 0); - delete bab; - return ok; -#else - Gecode::Search::WorkerControl control(1U); - Gecode::Search::Options o; - o.threads = 1.0; - o.worker_control = control; - Gecode::Search::Engine* bab = bab_resize_engine(o); - BABResizeSpace external(3000); - (void) external.status(); - bab->constrain(external); - int best = 4096; - while (BABResizeSpace* solution = - static_cast(bab->next())) { - best = solution->value(); - delete solution; - } - bool ok = (control.capacity() == 1U) && - (control.requested() == 1U) && !bab->stopped() && (best == 0); - delete bab; - return ok; -#endif - } - - static bool bab_resize_lifecycle(void) { -#ifdef GECODE_HAS_THREADS - using Gecode::Search::WorkerControlAccess; - Gecode::Search::WorkerControl control(capacity()); - Gecode::Search::NodeStop stop(0U); - CountingTracer tracer; - Gecode::Search::Options o; - o.threads = 4.0; - o.nogoods_limit = 16U; - o.stop = &stop; - o.tracer = &tracer; - o.worker_control = control; - Gecode::Search::Engine* bab = bab_resize_engine(o); - - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ADMISSION, - WorkerControlAccess::ALL_WORKERS,4U); - std::atomic stopped_result(nullptr); - std::thread stopped_next([&] { - stopped_result.store( - static_cast(bab->next()), - std::memory_order_release); - }); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_ADMISSION); - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_EVENT_WAIT, - WorkerControlAccess::ALL_WORKERS,3U); - control.request(1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,1U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,2U); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,3U); - WorkerControlAccess::gate_wait( - control,WorkerControlAccess::GATE_EVENT_WAIT); - BABResizeSpace old_external(1000); - (void) old_external.status(); - bab->constrain(old_external); - bool ok = (WorkerControlAccess::parked(control) == 3U); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_EVENT_WAIT); - WorkerControlAccess::gate_release( - control,WorkerControlAccess::GATE_ADMISSION,0U); - stopped_next.join(); - BABResizeSpace* solution = - stopped_result.load(std::memory_order_acquire); - ok = ok && (solution == nullptr) && bab->stopped() && - (bab->statistics().node > 0U) && - (WorkerControlAccess::parked(control) >= 3U); - delete solution; - (void) bab->nogoods(); - ok = ok && (WorkerControlAccess::leases(control) == 1U) && - (WorkerControlAccess::parked(control) >= 3U); - - control.request(3U); - unsigned long long int generation = - WorkerControlAccess::generation(control); - BABResizeSpace* root = new BABResizeSpace(2000,4095); - bab->reset(root); - ok = ok && settled(control,generation,3U); - stop.limit(100000U); - BABResizeSpace external(3000); - (void) external.status(); - bab->constrain(external); - int best = 4096; - while ((solution = static_cast(bab->next())) - != nullptr) { - best = solution->value(); - delete solution; - } - // Reaching 2000 proves reset discarded the prior bound of 1000. - ok = ok && !bab->stopped() && (best == 2000) && - (control.requested() == 3U); - delete bab; - bool trace_ok = - (tracer.init_count.load(std::memory_order_acquire) == 1U) && - (tracer.node_count.load(std::memory_order_acquire) > 0U) && - (tracer.done_count.load(std::memory_order_acquire) == 1U) && - (tracer.nodes_at_done.load(std::memory_order_acquire) == - tracer.node_count.load(std::memory_order_acquire)); - return ok && trace_ok; -#else - Gecode::Search::WorkerControl control(1U); - Gecode::Search::Options o; - o.threads = 1.0; - o.worker_control = control; - Gecode::Search::Engine* bab = bab_resize_engine(o); - control.request(1U); - BABResizeSpace old_external(1000); - (void) old_external.status(); - bab->constrain(old_external); - BABResizeSpace* root = new BABResizeSpace(2000,4095); - bab->reset(root); - BABResizeSpace external(3000); - (void) external.status(); - bab->constrain(external); - BABResizeSpace* solution; - int best = 4096; - while ((solution = static_cast(bab->next())) - != nullptr) { - best = solution->value(); - delete solution; - } - bool ok = !bab->stopped() && (best == 2000) && - (control.capacity() == 1U) && (control.requested() == 1U); - delete bab; - return ok; -#endif - } - - static bool stress_dfs(void) { -#ifdef GECODE_HAS_THREADS - for (unsigned int round=0U; round<8U; round++) { - Gecode::Search::WorkerControl control(4U); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - ResizeSpace* root = new ResizeSpace; - Gecode::DFS dfs(root,o); - delete root; - - std::atomic complete(false); - std::atomic controller_ok(true); - std::thread controller([&] { - static const unsigned int limits[] = { - 1U,4U,2U,3U,1U,2U,4U,3U - }; - for (unsigned int request=0U; - (request<4096U) && - !complete.load(std::memory_order_acquire); request++) { - try { - control.request(limits[(request+round) % 8U]); - } catch (...) { - controller_ok.store(false,std::memory_order_release); - return; - } - std::this_thread::yield(); - } - }); - - std::vector counts(1U << 12,0U); - while (ResizeSpace* solution = dfs.next()) { - counts[solution->id()]++; - delete solution; - } - complete.store(true,std::memory_order_release); - controller.join(); - if (!controller_ok.load(std::memory_order_acquire) || - dfs.stopped()) - return false; - for (unsigned int count : counts) - if (count != 1U) - return false; - } -#endif - return true; - } - - static bool stress_bab(void) { -#ifdef GECODE_HAS_THREADS - for (unsigned int round=0U; round<8U; round++) { - Gecode::Search::WorkerControl control(4U); - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = control; - if ((round & 1U) != 0U) { - o.c_d = 1U; - o.a_d = 1U; - } - Gecode::Search::Engine* bab = bab_resize_engine(o); - std::atomic complete(false); - std::atomic controller_ok(true); - std::thread controller([&] { - static const unsigned int limits[] = { - 4U,1U,3U,2U,1U,4U,2U,3U - }; - for (unsigned int request=0U; - (request<4096U) && - !complete.load(std::memory_order_acquire); request++) { - try { - control.request(limits[(request+round) % 8U]); - } catch (...) { - controller_ok.store(false,std::memory_order_release); - return; - } - std::this_thread::yield(); - } - }); - - int best = 4096; - while (BABResizeSpace* solution = - static_cast(bab->next())) { - best = solution->value(); - delete solution; - } - complete.store(true,std::memory_order_release); - controller.join(); - bool ok = controller_ok.load(std::memory_order_acquire) && - !bab->stopped() && (best == 0); - delete bab; - if (!ok) - return false; - } -#endif - return true; - } - - static bool stress_destruction(void) { -#ifdef GECODE_HAS_THREADS - for (unsigned int round=0U; round<64U; round++) { - Gecode::Search::Options o; - o.threads = 4.0; - o.worker_control = Gecode::Search::WorkerControl(4U); - Gecode::Search::Engine* engine = - ((round & 1U) == 0U) ? dfs_engine(o) : bab_resize_engine(o); - std::atomic started(false); - std::atomic controller_ok(true); - Gecode::Search::WorkerControl control(o.worker_control); - std::thread controller([control,&started,&controller_ok,round] - () mutable { - static const unsigned int limits[] = { - 1U,3U,2U,4U,2U,1U,4U,3U - }; - started.store(true,std::memory_order_release); - for (unsigned int request=0U; request<2048U; request++) { - try { - control.request(limits[(request+round) % 8U]); - } catch (...) { - controller_ok.store(false,std::memory_order_release); - return; - } - } - }); - while (!started.load(std::memory_order_acquire)) - std::this_thread::yield(); - delete engine; - controller.join(); - if (!controller_ok.load(std::memory_order_acquire)) - return false; - control.request(1U); - if ((control.capacity() != 4U) || - (control.requested() != 1U)) - return false; - } -#endif - return true; - } - - public: - WorkerControl(const std::string& name, Scenario s) - : Base("Search::WorkerControl::"+name), scenario(s) {} - - bool run(void) override { - switch (scenario) { - case VALUES: return values(); - case ERRORS: return errors(); - case BINDING: return binding(); - case CONCURRENT: return concurrent(); - case COMPATIBILITY: return compatibility(); - case DFS_RESIZE_ENUMERATION: return resized_enumeration(); - case DFS_RESIZE_BOUNDARIES: return resize_boundaries(); - case DFS_RESIZE_HANDOFF: return resize_handoff(); - case DFS_RESIZE_LIFECYCLE: return resize_lifecycle(); - case BAB_RESIZE_OPTIMALITY_DEFAULT: - return bab_resize_optimality(false); - case BAB_RESIZE_OPTIMALITY_FREQUENT: - return bab_resize_optimality(true); - case BAB_RESIZE_INCUMBENTS: return bab_resize_incumbents(); - case BAB_RESIZE_LIFECYCLE: return bab_resize_lifecycle(); - case STRESS_DFS: return stress_dfs(); - case STRESS_BAB: return stress_bab(); - case STRESS_DESTRUCTION: return stress_destruction(); - default: GECODE_NEVER; - } - return false; - } - - static void create(void) { - (void) new WorkerControl("Values",VALUES); - (void) new WorkerControl("Errors",ERRORS); - (void) new WorkerControl("Binding",BINDING); - (void) new WorkerControl("Concurrent",CONCURRENT); - (void) new WorkerControl("Compatibility",COMPATIBILITY); - (void) new WorkerControl("DFSResize::Enumeration", - DFS_RESIZE_ENUMERATION); - (void) new WorkerControl("DFSResize::Boundaries", - DFS_RESIZE_BOUNDARIES); - (void) new WorkerControl("DFSResize::Handoff", - DFS_RESIZE_HANDOFF); - (void) new WorkerControl("DFSResize::Lifecycle", - DFS_RESIZE_LIFECYCLE); - (void) new WorkerControl("BABResize::Optimality::Default", - BAB_RESIZE_OPTIMALITY_DEFAULT); - (void) new WorkerControl("BABResize::Optimality::Frequent", - BAB_RESIZE_OPTIMALITY_FREQUENT); - (void) new WorkerControl("BABResize::Incumbents", - BAB_RESIZE_INCUMBENTS); - (void) new WorkerControl("BABResize::Lifecycle", - BAB_RESIZE_LIFECYCLE); - (void) new WorkerControl("Stress::DFS",STRESS_DFS); - (void) new WorkerControl("Stress::BAB",STRESS_BAB); - (void) new WorkerControl("Stress::Destruction", - STRESS_DESTRUCTION); - } - }; - - /// Shared observations for worker-controlled meta-engine tests - struct MetaResizeState { - std::atomic master_calls; - std::atomic slave_calls; - std::atomic portfolio_assets; - std::atomic restart_slaves; - std::atomic nogood_handoffs; - std::atomic live_spaces; - bool partition_portfolio; - unsigned int maximum; - - explicit MetaResizeState(unsigned int maximum0=15U) - : master_calls(0U), slave_calls(0U), portfolio_assets(0U), - restart_slaves(0U), nogood_handoffs(0U), live_spaces(0U), - partition_portfolio(false), maximum(maximum0) {} - }; - - /// Partitionable finite model for RBS and PBS worker-control tests - class MetaResizeSpace : public Space { - public: - IntVar x; - MetaResizeState* observations; - - MetaResizeSpace(MetaResizeState& state) - : x(*this,0,static_cast(state.maximum)), observations(&state) { - observations->live_spaces.fetch_add(1U,std::memory_order_relaxed); - Gecode::branch(*this,x,INT_VAL_MAX()); - } - - MetaResizeSpace(MetaResizeState& state, int value) - : x(*this,value,value), observations(&state) { - observations->live_spaces.fetch_add(1U,std::memory_order_relaxed); - } - - MetaResizeSpace(MetaResizeSpace& s) - : Space(s), observations(s.observations) { - observations->live_spaces.fetch_add(1U,std::memory_order_relaxed); - x.update(*this,s.x); - } - - ~MetaResizeSpace(void) override { - observations->live_spaces.fetch_sub(1U,std::memory_order_relaxed); - } - - Space* copy(void) override { - return new MetaResizeSpace(*this); - } - - void constrain(const Space& _s) override { - const MetaResizeSpace& s = - static_cast(_s); - rel(*this,x,IRT_LE,s.x.val()); - } - - bool master(const MetaInfo& mi) override { - observations->master_calls.fetch_add(1U,std::memory_order_relaxed); - if ((mi.type() == MetaInfo::RESTART) && (mi.last() != nullptr)) { - const MetaResizeSpace& last = - static_cast(*mi.last()); - rel(*this,x,IRT_NQ,last.x.val()); - if (mi.nogoods().ng() > 0U) - observations->nogood_handoffs.fetch_add( - 1U,std::memory_order_relaxed); - return true; - } - return false; - } - - bool slave(const MetaInfo& mi) override { - observations->slave_calls.fetch_add(1U, - std::memory_order_relaxed); - if (mi.type() == MetaInfo::PORTFOLIO) { - unsigned int asset = mi.asset(); - observations->portfolio_assets.fetch_or( - 1U << asset,std::memory_order_relaxed); - if (observations->partition_portfolio) { - int boundary = static_cast(observations->maximum / 2U); - if (asset == 0U) - rel(*this,x,IRT_LQ,boundary); - else - rel(*this,x,IRT_GR,boundary); - } - } else { - observations->restart_slaves.fetch_add( - 1U,std::memory_order_relaxed); - } - return true; - } - - int value(void) const { - return x.val(); - } - }; - - struct MetaResizeBuilderFailure {}; - - /// Builder used to verify cleanup after explicit PBS construction fails - class MetaResizeThrowBuilder : public Gecode::Search::Builder { - private: - std::atomic* live; - bool fail; - public: - MetaResizeThrowBuilder(const Gecode::Search::Options& o, - std::atomic& live0, bool fail0) - : Gecode::Search::Builder(o,false), live(&live0), fail(fail0) { - live->fetch_add(1U,std::memory_order_relaxed); - } - - Gecode::Search::Engine* operator()(Space* s) const override { - if (fail) - throw MetaResizeBuilderFailure(); - return Gecode::Search::build< - MetaResizeSpace,Gecode::DFS >(s,opt); - } - - ~MetaResizeThrowBuilder(void) override { - live->fetch_sub(1U,std::memory_order_relaxed); - } - }; - - /// Focused RBS and PBS worker-control transport tests - class MetaResize : public Base { - private: - enum Scenario { - VALIDATION, - RBS_DFS, - RBS_BAB, - PBS_EXPLICIT_DFS, - PBS_EXPLICIT_BAB, - PBS_EXPLICIT_BAB_CONSTRAIN, - PBS_NESTED, - PBS_NESTED_BAB, - PBS_STOP_LIFECYCLE - } scenario; - - template - static bool throws(Function function) { - try { - function(); - } catch (const Exception&) { - return true; - } catch (...) { - return false; - } - return false; - } - - static bool settled(const Gecode::Search::WorkerControl& control, - unsigned long long int generation, - unsigned int leases) { - for (unsigned int spin=0U; spin<10000000U; spin++) { - if ((Gecode::Search::WorkerControlAccess:: - observed_generation(control) == generation) && - (Gecode::Search::WorkerControlAccess::leases(control) == - leases) && - (Gecode::Search::WorkerControlAccess::admitted(control) == 0U)) - return true; - std::this_thread::yield(); - } - return false; - } - - static unsigned int exhaust_dfs( - Gecode::Search::Base& engine, - unsigned int seen[16]) { - unsigned int solutions = 0U; - while (MetaResizeSpace* solution = engine.next()) { - int value = solution->value(); - if ((value >= 0) && (value < 16)) - seen[value]++; - solutions++; - delete solution; - } - return solutions; - } - -#ifdef GECODE_HAS_THREADS - template - static void action_portfolio_leaf( - Engine& engine, - Gecode::Search::WorkerControl& control, unsigned int request, - unsigned long long int generation, - std::vector& results, bool& ok) { - using Gecode::Search::WorkerControlAccess; - WorkerControlAccess::gate_install( - control,WorkerControlAccess::GATE_ACTION_BEGIN, - WorkerControlAccess::ALL_WORKERS,request); - WorkerControlAccess::reset_max_admitted(control,generation); - std::atomic action_ready(false); - std::atomic exhausted(false); - std::atomic consumer_done(false); - std::thread consumer([&] { - while (true) { - MetaResizeSpace* result = - static_cast(engine.next()); - if (result == nullptr) { - exhausted.store(true,std::memory_order_release); - consumer_done.store(true,std::memory_order_release); - return; - } - results.push_back(result); - if (action_ready.load(std::memory_order_acquire)) { - consumer_done.store(true,std::memory_order_release); - return; - } - } - }); - unsigned int waiting = 0U; - auto deadline = std::chrono::steady_clock::now() + - std::chrono::seconds(3); - do { - waiting = 0U; - for (unsigned int worker=0U; worker= request) - break; - std::this_thread::yield(); - } while (!consumer_done.load(std::memory_order_acquire) && - (std::chrono::steady_clock::now() < deadline)); - bool reached = waiting >= request; - bool action_ok = reached && - (WorkerControlAccess::observed_generation(control) == generation) && - (WorkerControlAccess::leases(control) == request) && - (WorkerControlAccess::max_admitted(control) == request); - action_ready.store(true,std::memory_order_release); - WorkerControlAccess::gate_release_all( - control,WorkerControlAccess::GATE_ACTION_BEGIN); - consumer.join(); - ok = ok && action_ok && - !exhausted.load(std::memory_order_acquire) && - settled(control,generation,request) && - (WorkerControlAccess::completed_generation(control) >= generation); - } - - template - static void action_portfolio_phase( - Engine& engine, - Gecode::Search::WorkerControl& first, unsigned int first_request, - Gecode::Search::WorkerControl& second, unsigned int second_request, - std::vector& results, bool& ok) { - using Gecode::Search::WorkerControlAccess; - first.request(first_request); - second.request(second_request); - unsigned long long int fg = WorkerControlAccess::generation(first); - unsigned long long int sg = WorkerControlAccess::generation(second); - action_portfolio_leaf( - engine,first,first_request,fg,results,ok); - action_portfolio_leaf( - engine,second,second_request,sg,results,ok); - ok = ok && - (WorkerControlAccess::observed_generation(first) == fg) && - (WorkerControlAccess::observed_generation(second) == sg) && - (WorkerControlAccess::leases(first) == first_request) && - (WorkerControlAccess::leases(second) == second_request) && - (WorkerControlAccess::max_admitted(first) == first_request) && - (WorkerControlAccess::max_admitted(second) == second_request); - } -#endif - - static bool validation(void) { -#ifdef GECODE_HAS_THREADS - const unsigned int leaf_threads = 4U; -#else - const unsigned int leaf_threads = 1U; -#endif - bool ok = true; - - MetaResizeState single_state; - Gecode::Search::WorkerControl single(1U); - { - MetaResizeSpace* root = new MetaResizeSpace(single_state); - Gecode::Search::Options o; - o.assets = 1U; - o.threads = static_cast(leaf_threads); - o.worker_control = single; - Gecode::PBS engine(root,o); - delete root; - ok = ok && (single.capacity() == leaf_threads); - unsigned int seen[16] = {}; - ok = ok && (exhaust_dfs(engine,seen) == 16U); - for (unsigned int count : seen) - ok = ok && (count == 1U); - } - single.request(1U); - ok = ok && - (single_state.master_calls.load(std::memory_order_relaxed) == 1U) && - (single_state.slave_calls.load(std::memory_order_relaxed) == 1U); - if (!ok) - return false; - - MetaResizeState homogeneous_state; - Gecode::Search::WorkerControl homogeneous(1U); - MetaResizeSpace* homogeneous_root = - new MetaResizeSpace(homogeneous_state); - Gecode::Search::Options homogeneous_options; - homogeneous_options.assets = 2U; - homogeneous_options.threads = 2.0; - homogeneous_options.worker_control = homogeneous; - ok = ok && throws([&] { - Gecode::PBS - engine(homogeneous_root,homogeneous_options); - }); - ok = ok && (homogeneous.capacity() == 0U) && - (homogeneous_state.master_calls.load( - std::memory_order_relaxed) == 0U) && - (homogeneous_state.slave_calls.load( - std::memory_order_relaxed) == 0U); - delete homogeneous_root; - if (!ok) - return false; - - MetaResizeState outer_state; - Gecode::Search::WorkerControl outer(1U); - Gecode::Search::Options builder_options; - builder_options.threads = static_cast(leaf_threads); - Gecode::SEBs outer_builders(1); - outer_builders[0] = Gecode::dfs(builder_options); - MetaResizeSpace* outer_root = new MetaResizeSpace(outer_state); - Gecode::Search::Options outer_options; - outer_options.threads = 1.0; - outer_options.worker_control = outer; - ok = ok && throws([&] { - Gecode::PBS - engine(outer_root,outer_builders,outer_options); - }); - ok = ok && (outer.capacity() == 0U) && - (outer_state.master_calls.load(std::memory_order_relaxed) == 0U); - delete outer_builders[0]; - delete outer_root; - if (!ok) - return false; - -#ifdef GECODE_HAS_THREADS - MetaResizeState duplicate_state; - Gecode::Search::WorkerControl duplicate(1U); - Gecode::Search::Options duplicate_options; - duplicate_options.threads = 2.0; - duplicate_options.worker_control = duplicate; - Gecode::SEBs duplicate_builders(2); - duplicate_builders[0] = - Gecode::dfs(duplicate_options); - duplicate_builders[1] = - Gecode::dfs(duplicate_options); - MetaResizeSpace* duplicate_root = - new MetaResizeSpace(duplicate_state); - Gecode::Search::Options duplicate_outer; - duplicate_outer.threads = 2.0; - ok = ok && throws([&] { - Gecode::PBS - engine(duplicate_root,duplicate_builders,duplicate_outer); - }); - ok = ok && (duplicate.capacity() == 0U) && - (duplicate_state.master_calls.load( - std::memory_order_relaxed) == 0U); - delete duplicate_builders[0]; - delete duplicate_builders[1]; - delete duplicate_root; - if (!ok) - return false; - - MetaResizeState distinct_state; - distinct_state.partition_portfolio = true; - Gecode::Search::WorkerControl first(1U), second(1U), discarded(1U); - Gecode::Search::Options - first_options, second_options, discarded_options; - first_options.threads = 2.0; - first_options.worker_control = first; - second_options.threads = 3.0; - second_options.worker_control = second; - discarded_options.threads = 4.0; - discarded_options.worker_control = discarded; - Gecode::SEBs distinct_builders(3); - distinct_builders[0] = Gecode::dfs(first_options); - distinct_builders[1] = Gecode::dfs(second_options); - distinct_builders[2] = - Gecode::dfs(discarded_options); - unsigned int distinct_solutions = 0U; - bool distinct_exact = true; - { - MetaResizeSpace* root = new MetaResizeSpace(distinct_state); - Gecode::Search::Options portfolio; - portfolio.threads = 2.0; - Gecode::PBS - engine(root,distinct_builders,portfolio); - delete root; - ok = ok && (first.capacity() == 2U) && - (second.capacity() == 3U) && (discarded.capacity() == 0U); - unsigned int seen[16] = {}; - distinct_solutions = exhaust_dfs(engine,seen); - ok = ok && (distinct_solutions == 16U); - for (unsigned int count : seen) - distinct_exact = distinct_exact && (count == 1U); - ok = ok && distinct_exact; - } - - MetaResizeState failure_state; - Gecode::Search::WorkerControl bound(1U), failing(1U), excess(1U); - Gecode::Search::WorkerControl bound_copy(bound); - Gecode::Search::Options failure_options[3]; - failure_options[0].threads = 2.0; - failure_options[0].worker_control = bound; - failure_options[1].threads = 3.0; - failure_options[1].worker_control = failing; - failure_options[2].threads = 4.0; - failure_options[2].worker_control = excess; - std::atomic live_builders(0U); - Gecode::SEBs failure_builders(3); - failure_builders[0] = new MetaResizeThrowBuilder( - failure_options[0],live_builders,false); - failure_builders[1] = new MetaResizeThrowBuilder( - failure_options[1],live_builders,true); - failure_builders[2] = new MetaResizeThrowBuilder( - failure_options[2],live_builders,false); - MetaResizeSpace* failure_root = new MetaResizeSpace(failure_state); - Gecode::Search::Options failure_outer; - failure_outer.threads = 2.0; - bool construction_failed = false; - try { - Gecode::PBS - engine(failure_root,failure_builders,failure_outer); - } catch (const MetaResizeBuilderFailure&) { - construction_failed = true; - } - delete failure_root; - ok = ok && construction_failed && - (live_builders.load(std::memory_order_relaxed) == 0U) && - (failure_state.live_spaces.load(std::memory_order_relaxed) == 0U) && - Gecode::Search::WorkerControlAccess::same_identity( - bound,bound_copy) && - !Gecode::Search::WorkerControlAccess::attached(bound) && - !Gecode::Search::WorkerControlAccess::attached(bound_copy) && - !Gecode::Search::WorkerControlAccess::attached(failing) && - (excess.capacity() == 0U); -#endif - return ok; - } - - static bool rbs_dfs(void) { - MetaResizeState state; - state.partition_portfolio = true; -#ifdef GECODE_HAS_THREADS - const unsigned int capacity = 4U; -#else - const unsigned int capacity = 1U; -#endif - Gecode::Search::WorkerControl control(capacity); - Gecode::Search::Options o; - o.threads = static_cast(capacity); - o.worker_control = control; - o.nogoods_limit = 16U; - o.cutoff = Gecode::Search::Cutoff::constant(1U); - MetaResizeSpace* root = new MetaResizeSpace(state); - Gecode::RBS engine(root,o); - delete root; - bool ok = control.capacity() == capacity; - unsigned int seen[16] = {}; -#ifdef GECODE_HAS_THREADS - const unsigned int requests[] = {1U,3U,1U,4U}; -#else - const unsigned int requests[] = {1U,1U,1U,1U}; -#endif - std::vector results; - for (unsigned int request : requests) { - control.request(request); - unsigned long long int generation = - Gecode::Search::WorkerControlAccess::generation(control); - MetaResizeSpace* solution = engine.next(); - if (solution != nullptr) - results.push_back(solution); -#ifdef GECODE_HAS_THREADS - ok = ok && (solution != nullptr) && - settled(control,generation,request); -#else - ok = ok && (solution != nullptr); -#endif - } - for (MetaResizeSpace* solution : results) { - seen[solution->value()]++; - delete solution; - } - unsigned int solutions = - static_cast(results.size()) + - exhaust_dfs(engine,seen); - ok = ok && (solutions == 16U) && - (control.capacity() == capacity) && - (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); - for (unsigned int count : seen) - ok = ok && (count == 1U); - return ok; - } - - static bool rbs_bab(void) { - MetaResizeState state; - state.partition_portfolio = true; -#ifdef GECODE_HAS_THREADS - const unsigned int capacity = 4U; -#else - const unsigned int capacity = 1U; -#endif - Gecode::Search::WorkerControl control(capacity); - Gecode::Search::Options o; - o.threads = static_cast(capacity); - o.worker_control = control; - o.cutoff = Gecode::Search::Cutoff::constant(1U); - MetaResizeSpace* root = new MetaResizeSpace(state); - Gecode::SEB builder = - Gecode::rbs(o); - Gecode::Search::Engine* engine = (*builder)(root); - delete builder; - delete root; - bool ok = control.capacity() == capacity; - int best = 16; -#ifdef GECODE_HAS_THREADS - control.request(1U); - unsigned long long int first_generation = - Gecode::Search::WorkerControlAccess::generation(control); -#else - control.request(1U); -#endif - MetaResizeSpace* solution = - static_cast(engine->next()); - ok = ok && (solution != nullptr); - if (solution != nullptr) { - best = solution->value(); - delete solution; - } -#ifdef GECODE_HAS_THREADS - ok = ok && settled(control,first_generation,1U); -#endif - MetaResizeSpace external(state,10); - (void) external.status(); - engine->constrain(external); -#ifdef GECODE_HAS_THREADS - control.request(3U); - unsigned long long int second_generation = - Gecode::Search::WorkerControlAccess::generation(control); -#else - control.request(1U); -#endif - while ((solution = - static_cast(engine->next())) != nullptr) { - best = solution->value(); - delete solution; - } -#ifdef GECODE_HAS_THREADS - ok = ok && settled(control,second_generation,3U); -#endif - delete engine; - return ok && (best == 0) && - (control.capacity() == capacity) && - (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); - } - - template class Engine> - static bool pbs_explicit(bool best_search, bool constant=false, - bool external_incumbent=false) { -#ifndef GECODE_HAS_THREADS - (void) best_search; - (void) constant; - (void) external_incumbent; - return true; -#else - MetaResizeState state( - (best_search && !external_incumbent) ? 15U : 4095U); - state.partition_portfolio = true; - Gecode::Search::WorkerControl - first(constant ? 3U : 4U), second(constant ? 1U : 4U); - Gecode::Search::NodeStop first_stop(0U), second_stop(0U); - Gecode::Search::Options child[2]; - for (unsigned int i=0U; i<2U; i++) - child[i].threads = 4.0; - child[0].worker_control = first; - child[1].worker_control = second; - if (external_incumbent) { - child[0].stop = &first_stop; - child[1].stop = &second_stop; - } - Gecode::SEBs builders(2); - builders[0] = best_search ? - Gecode::bab(child[0]) : - Gecode::dfs(child[0]); - builders[1] = best_search ? - Gecode::bab(child[1]) : - Gecode::dfs(child[1]); - Gecode::Search::Options outer; - outer.threads = 2.0; - MetaResizeSpace* root = new MetaResizeSpace(state); - Gecode::PBS engine(root,builders,outer); - delete root; - bool ok = (first.capacity() == 4U) && (second.capacity() == 4U); - bool phase_ok = true, parked_ok = true, result_ok = true; - std::vector results; - if (external_incumbent) { - first.request(1U); - second.request(1U); - Gecode::Search::WorkerControl* controls[2] = {&first,&second}; - for (unsigned int c=0U; c<2U; c++) { - Gecode::Search::WorkerControlAccess::gate_install( - *controls[c], - Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN, - Gecode::Search::WorkerControlAccess::ALL_WORKERS,1U); - Gecode::Search::WorkerControlAccess::gate_release_all( - *controls[c], - Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN); - } - MetaResizeSpace* prelude = engine.next(); - parked_ok = - (prelude == nullptr) && engine.stopped() && - (Gecode::Search::WorkerControlAccess::parked(first) == 3U) && - (Gecode::Search::WorkerControlAccess::parked(second) == 3U); - delete prelude; - first_stop.limit(1000000U); - second_stop.limit(1000000U); - unsigned long long int before[2][4]; - unsigned int forwarded = 0U, parked_forwarded = 0U; - for (unsigned int c=0U; c<2U; c++) - for (unsigned int worker=0U; worker<4U; worker++) - before[c][worker] = - Gecode::Search::WorkerControlAccess:: - incumbent_deliveries(*controls[c],worker); - MetaResizeSpace external(state,-1); - (void) external.status(); - engine.constrain(external); - for (unsigned int c=0U; c<2U; c++) - for (unsigned int worker=0U; worker<4U; worker++) - if (Gecode::Search::WorkerControlAccess:: - incumbent_deliveries(*controls[c],worker) > - before[c][worker]) { - forwarded++; - if (Gecode::Search::WorkerControlAccess:: - worker_parked(*controls[c],worker)) - parked_forwarded++; - } - ok = ok && parked_ok && (forwarded == 8U) && - (parked_forwarded == 6U); - } else if (!constant) { - action_portfolio_phase( - engine,first,3U,second,1U,results,phase_ok); - ok = ok && phase_ok; - parked_ok = - (Gecode::Search::WorkerControlAccess::parked(first) == 1U) && - (Gecode::Search::WorkerControlAccess::parked(second) == 3U); - ok = ok && parked_ok; - if (!best_search) { - action_portfolio_phase( - engine,first,1U,second,3U,results,ok); - action_portfolio_phase( - engine,first,2U,second,2U,results,ok); - } - } - - if (best_search) { - int best = static_cast(state.maximum+1U); - for (MetaResizeSpace* solution : results) { - best = solution->value(); - delete solution; - } - while (MetaResizeSpace* solution = engine.next()) { - best = solution->value(); - delete solution; - } - result_ok = external_incumbent ? - (best == static_cast(state.maximum+1U)) : - (best == 0); - ok = ok && result_ok; - } else { - std::vector seen(state.maximum+1U,0U); - bool valid = constant || !results.empty(); - for (MetaResizeSpace* solution : results) { - valid = valid && (solution->value() >= 0) && - (static_cast(solution->value()) <= - state.maximum); - if ((solution->value() >= 0) && - (static_cast(solution->value()) <= - state.maximum)) - seen[static_cast(solution->value())]++; - delete solution; - } - while (MetaResizeSpace* solution = engine.next()) { - valid = valid && (solution->value() >= 0) && - (static_cast(solution->value()) <= - state.maximum); - if ((solution->value() >= 0) && - (static_cast(solution->value()) <= - state.maximum)) - seen[static_cast(solution->value())]++; - delete solution; - } - for (unsigned int i=0U; i(capacity); - child[i].cutoff = Gecode::Search::Cutoff::constant(1U); - } - child[0].worker_control = first; - child[1].worker_control = second; - Gecode::SEBs builders(2); - builders[0] = - Gecode::rbs(child[0]); - builders[1] = - Gecode::rbs(child[1]); - Gecode::Search::Options outer; -#ifdef GECODE_HAS_THREADS - outer.threads = 2.0; -#else - outer.threads = 1.0; -#endif - MetaResizeSpace* root = new MetaResizeSpace(state); - Gecode::PBS - engine(root,builders,outer); - delete root; - first.request(1U); - second.request(capacity); - unsigned int seen[16] = {}; - unsigned int solutions = exhaust_dfs(engine,seen); - bool ok = (solutions == 16U) && - (first.capacity() == capacity) && - (second.capacity() == capacity) && - (state.portfolio_assets.load(std::memory_order_relaxed) == 3U) && - (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); - for (unsigned int count : seen) - ok = ok && (count == 1U); - return ok; - } - - static bool nested_bab(void) { - MetaResizeState state; - state.partition_portfolio = true; -#ifdef GECODE_HAS_THREADS - const unsigned int capacity = 2U; -#else - const unsigned int capacity = 1U; -#endif - Gecode::Search::WorkerControl first(capacity), second(capacity); - Gecode::Search::Options child[2]; - for (unsigned int i=0U; i<2U; i++) { - child[i].threads = static_cast(capacity); - child[i].cutoff = Gecode::Search::Cutoff::constant(1U); - } - child[0].worker_control = first; - child[1].worker_control = second; - Gecode::SEBs builders(2); - builders[0] = - Gecode::rbs(child[0]); - builders[1] = - Gecode::rbs(child[1]); - Gecode::Search::Options outer; -#ifdef GECODE_HAS_THREADS - outer.threads = 2.0; -#else - outer.threads = 1.0; -#endif - MetaResizeSpace* root = new MetaResizeSpace(state); - Gecode::PBS - engine(root,builders,outer); - delete root; - first.request(1U); - second.request(capacity); - int best = 16; - while (MetaResizeSpace* solution = engine.next()) { - best = solution->value(); - delete solution; - } - return (best == 0) && - (first.capacity() == capacity) && - (second.capacity() == capacity) && - (state.portfolio_assets.load(std::memory_order_relaxed) == 3U) && - (state.restart_slaves.load(std::memory_order_relaxed) >= 2U); - } - - static bool pbs_stop_lifecycle(void) { - MetaResizeState state(4095U); - state.partition_portfolio = true; -#ifdef GECODE_HAS_THREADS - const unsigned int capacity = 4U; -#else - const unsigned int capacity = 1U; -#endif - Gecode::Search::WorkerControl first(capacity), second(capacity); - Gecode::Search::WorkerControl first_copy(first), second_copy(second); - Gecode::Search::NodeStop first_stop(0U), second_stop(0U); - Gecode::Search::Options child[2]; - for (unsigned int i=0U; i<2U; i++) - child[i].threads = static_cast(capacity); - child[0].worker_control = first; - child[1].worker_control = second; - child[0].stop = &first_stop; - child[1].stop = &second_stop; - bool ok = true; - { - Gecode::SEBs builders(2); - builders[0] = Gecode::dfs(child[0]); - builders[1] = Gecode::dfs(child[1]); - Gecode::Search::Options outer; -#ifdef GECODE_HAS_THREADS - outer.threads = 2.0; -#else - outer.threads = 1.0; -#endif - MetaResizeSpace* root = new MetaResizeSpace(state); - Gecode::PBS - engine(root,builders,outer); - delete root; - first.request(1U); - second.request(1U); - unsigned long long int fg = - Gecode::Search::WorkerControlAccess::generation(first); - unsigned long long int sg = - Gecode::Search::WorkerControlAccess::generation(second); - Gecode::Search::WorkerControlAccess::gate_install( - first,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN, - Gecode::Search::WorkerControlAccess::ALL_WORKERS,1U); - Gecode::Search::WorkerControlAccess::gate_install( - second,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN, - Gecode::Search::WorkerControlAccess::ALL_WORKERS,1U); - Gecode::Search::WorkerControlAccess::gate_release_all( - first,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN); - Gecode::Search::WorkerControlAccess::gate_release_all( - second,Gecode::Search::WorkerControlAccess::GATE_ACTION_BEGIN); - MetaResizeSpace* solution = engine.next(); - delete solution; - ok = ok && (solution == nullptr) && engine.stopped() && - (Gecode::Search::WorkerControlAccess:: - completed_generation(first) >= fg) && - (Gecode::Search::WorkerControlAccess:: - completed_generation(second) >= sg); -#ifdef GECODE_HAS_THREADS - ok = ok && - (Gecode::Search::WorkerControlAccess::parked(first) == 3U) && - (Gecode::Search::WorkerControlAccess::parked(second) == 3U); -#endif - } - return ok && - !Gecode::Search::WorkerControlAccess::attached(first) && - !Gecode::Search::WorkerControlAccess::attached(second) && - !Gecode::Search::WorkerControlAccess::attached(first_copy) && - !Gecode::Search::WorkerControlAccess::attached(second_copy) && - (state.live_spaces.load(std::memory_order_relaxed) == 0U); - } - - public: - MetaResize(const std::string& name, Scenario s) - : Base("Search::WorkerControl::MetaResize::"+name), scenario(s) {} - - bool run(void) override { - switch (scenario) { - case VALIDATION: return validation(); - case RBS_DFS: return rbs_dfs(); - case RBS_BAB: return rbs_bab(); - case PBS_EXPLICIT_DFS: { - bool constant = pbs_explicit(false,true); - bool dynamic = pbs_explicit(false); - return constant && dynamic; - } - case PBS_EXPLICIT_BAB: - return pbs_explicit(true,true); - case PBS_EXPLICIT_BAB_CONSTRAIN: - return pbs_explicit(true,false,true); - case PBS_NESTED: return nested(); - case PBS_NESTED_BAB: return nested_bab(); - case PBS_STOP_LIFECYCLE: - return pbs_stop_lifecycle(); - default: GECODE_NEVER; - } - return false; - } - - static void create(void) { - (void) new MetaResize("Validation",VALIDATION); - (void) new MetaResize("RBS::DFS",RBS_DFS); - (void) new MetaResize("RBS::BAB",RBS_BAB); - (void) new MetaResize("PBS::Explicit::DFS",PBS_EXPLICIT_DFS); - (void) new MetaResize("PBS::Explicit::BAB::Optimality", - PBS_EXPLICIT_BAB); - (void) new MetaResize("PBS::Explicit::BAB::Constrain", - PBS_EXPLICIT_BAB_CONSTRAIN); - (void) new MetaResize("PBS::NestedRBS",PBS_NESTED); - (void) new MetaResize("PBS::NestedRBS::BAB",PBS_NESTED_BAB); - (void) new MetaResize("PBS::StopLifecycle",PBS_STOP_LIFECYCLE); - } - }; - /// %Base class for search tests class Test : public Base { public: @@ -2719,6 +389,100 @@ namespace Test { htb1(_htb1), htb2(_htb2), htb3(_htb3), htc(_htc) {} }; + /// Randomly resize a live leaf engine from another thread + class WorkerResizer { + private: + Gecode::Search::WorkerControl control; + unsigned int capacity; + std::atomic done; + std::thread worker; + void resize(unsigned int seed) { + Gecode::Support::RandomGenerator rand(seed); + while (!done.load(std::memory_order_acquire)) { + control.request(1U + rand(capacity)); + std::this_thread::yield(); + } + } + public: + WorkerResizer(const Gecode::Search::WorkerControl& control0, + unsigned int capacity0, unsigned int seed, bool enabled) + : control(control0), capacity(capacity0), done(false) { + if (enabled) + worker = std::thread(&WorkerResizer::resize,this,seed); + } + ~WorkerResizer(void) { + done.store(true,std::memory_order_release); + if (worker.joinable()) + worker.join(); + } + }; + + /// Return the effective leaf capacity for this build + static unsigned int + worker_capacity(unsigned int threads) { +#ifdef GECODE_HAS_THREADS + return threads; +#else + (void) threads; + return 1U; +#endif + } + + /// Focused API and binding checks not covered by randomized search + class WorkerControlAPI : public Base { + private: + template + static bool throws(Function function) { + try { + function(); + } catch (const Exception&) { + return true; + } catch (...) { + } + return false; + } + public: + WorkerControlAPI(void) : Base("Search::WorkerControl::API") {} + bool run(void) override { + using Gecode::Search::WorkerControl; + WorkerControl empty; + bool ok = !empty && (empty.requested() == 0U) && + (empty.capacity() == 0U) && + throws( + [&] { empty.request(1U); }) && + throws( + [] { WorkerControl invalid(0U); }); + + const unsigned int capacity = worker_capacity(2U); + WorkerControl control(capacity); + WorkerControl copy(control); + control.request(1U); + ok = ok && copy && (copy.requested() == 1U); + + Gecode::Search::Options o; + o.threads = 2.0; + o.worker_control = control; + SolveImmediate* root = + new SolveImmediate(HTB_NONE,HTB_NONE,HTB_NONE); + { + Gecode::DFS engine(root,o); + delete root; + root = nullptr; + ok = ok && (control.capacity() == capacity) && + throws( + [&] { control.request(capacity+1U); }); + delete engine.next(); + } + root = new SolveImmediate(HTB_NONE,HTB_NONE,HTB_NONE); + ok = ok && throws( + [&] { Gecode::DFS engine(root,o); }); + delete root; + return ok; + } + }; + + WorkerControlAPI worker_control_api; + /// %Test for depth-first search template class DFS : public Test { @@ -2746,7 +510,17 @@ namespace Test { o.a_d = a_d; o.threads = t; o.stop = &f; + const unsigned int capacity = worker_capacity(t); + const bool adjustable = _rand(2U) != 0U; + Gecode::Search::WorkerControl control; + if (adjustable) { + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::DFS dfs(m,o); + WorkerResizer resize(control,capacity,_rand.next(), + adjustable && (capacity > 1U) && + (_rand(64U) == 0U)); int n = m->solutions(); delete m; while (true) { @@ -2827,7 +601,17 @@ namespace Test { o.a_d = a_d; o.threads = t; o.stop = &f; + const unsigned int capacity = worker_capacity(t); + const bool adjustable = _rand(2U) != 0U; + Gecode::Search::WorkerControl control; + if (adjustable) { + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::BAB bab(m,o); + WorkerResizer resize(control,capacity,_rand.next(), + adjustable && (capacity > 1U) && + (_rand(64U) == 0U)); delete m; Model* b = nullptr; while (true) { @@ -2865,7 +649,16 @@ namespace Test { o.stop = &f; o.d_l = 100; o.cutoff = Gecode::Search::Cutoff::geometric(1,2); + const unsigned int capacity = worker_capacity(t); + const bool adjustable = _rand(2U) != 0U; + Gecode::Search::WorkerControl control; + if (adjustable) { + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::RBS rbs(m,o); + WorkerResizer resize(control,capacity,_rand.next(), + adjustable && (capacity > 1U)); int n = m->solutions(); delete m; while (true) { @@ -2905,7 +698,15 @@ namespace Test { o.threads = t; o.d_l = 100; o.stop = &f; + Gecode::Search::WorkerControl control; + if (a == 1U) { + const unsigned int capacity = worker_capacity(t); + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::PBS pbs(m,o); + WorkerResizer resize(control,worker_capacity(t),_rand.next(), + (a == 1U) && (worker_capacity(t) > 1U)); if (best) { Model* b = nullptr; while (true) { @@ -2967,16 +768,27 @@ namespace Test { so.threads = st; so.d_l = 100; so.cutoff = Gecode::Search::Cutoff::constant(1000000); + const unsigned int capacity = worker_capacity(st); + Gecode::Search::WorkerControl controls[3]; + Gecode::Search::Options leaf[3] = {so,so,so}; + for (unsigned int i=0U; i<3U; i++) + if (best || (i != 1U)) { + controls[i] = + Gecode::Search::WorkerControl(1U + _rand(capacity)); + leaf[i].worker_control = controls[i]; + } if (best) { SEBs sebs(3); - sebs[0] = bab(so); - sebs[1] = bab(so); - sebs[2] = rbs(so); + sebs[0] = bab(leaf[0]); + sebs[1] = bab(leaf[1]); + sebs[2] = rbs(leaf[2]); Gecode::PBS pbs(m, sebs, mo); delete m; Model* b = nullptr; while (true) { + for (unsigned int i=0U; i<3U; i++) + controls[i].request(1U + _rand(capacity)); Model* s = pbs.next(); if (s != nullptr) { delete b; b=s; @@ -2990,15 +802,17 @@ namespace Test { return ok; } else { SEBs sebs(3); - sebs[0] = dfs(so); - sebs[1] = lds(so); - sebs[2] = rbs(so); + sebs[0] = dfs(leaf[0]); + sebs[1] = lds(leaf[1]); + sebs[2] = rbs(leaf[2]); Gecode::PBS pbs(m, sebs, mo); int n = 3 * m->solutions(); delete m; while (true) { + controls[0].request(1U + _rand(capacity)); + controls[2].request(1U + _rand(capacity)); Model* s = pbs.next(); if (s != nullptr) { n--; delete s; @@ -3071,8 +885,6 @@ namespace Test { public: /// Perform creation and registration Create(void) { - WorkerControl::create(); - MetaResize::create(); // Depth-first search for (unsigned int t = 1; t<=4; t++) for (unsigned int c_d = 1; c_d<10; c_d++) From 12f88cbc73710a72963c7646964a0f931d447799 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 28 Jul 2026 15:29:39 +0200 Subject: [PATCH 11/15] Document worker control for 6.5.0 --- changelog.in | 16 ++++++++++++++++ gecode/search/par/engine.hh | 8 ++++---- gecode/search/par/engine.hpp | 20 +++++++++++--------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/changelog.in b/changelog.in index 4f8129b317..1d801f68ec 100755 --- a/changelog.in +++ b/changelog.in @@ -67,6 +67,22 @@ # optional section in the html page. # +[RELEASE] +Version: 6.5.0 +Date: unreleased +[DESCRIPTION] +This is the development changelog for the next Gecode release. + +[ENTRY] +Module: search +What: new +Rank: major +[DESCRIPTION] +Add Search::WorkerControl for asynchronously adjusting how many pre-created +workers a parallel DFS or BAB engine may execute. Controls also apply to leaf +engines owned by RBS and PBS, allowing callers to redistribute a fixed worker +budget among engines without rebuilding them. + [RELEASE] Version: 6.4.0 Date: 2026-07-15 diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index db02b0be9f..a8a7cf16f9 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -88,10 +88,10 @@ namespace Gecode { namespace Search { namespace Par { /// Search options Options _opt; /// Logical worker state used by adjustable parallel admission - enum SchedulerLogical { - SL_OWNER, - SL_PENDING, - SL_IDLE + enum class SchedulerLogical { + OWNER, + PENDING, + IDLE }; /// Per-worker adjustable parallel admission state struct SchedulerWorker { diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index ad3d860dd0..75d7737fa2 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -156,11 +156,11 @@ namespace Gecode { namespace Search { namespace Par { Engine::scheduler_grow(void) { bool wake = false; while (scheduler_leases < scheduler_requested) { - unsigned int i = scheduler_select(SL_OWNER,workers()); + unsigned int i = scheduler_select(SchedulerLogical::OWNER,workers()); if (i == workers()) - i = scheduler_select(SL_PENDING,workers()); + i = scheduler_select(SchedulerLogical::PENDING,workers()); if (i == workers()) - i = scheduler_select(SL_IDLE,workers()); + i = scheduler_select(SchedulerLogical::IDLE,workers()); if (i == workers()) { for (i=0U; i Date: Thu, 20 Aug 2026 08:48:08 +0200 Subject: [PATCH 12/15] Allow worker controls to pause search --- changelog.in | 3 ++- docs/worker-control.md | 16 ++++++++++++---- gecode/search.hh | 6 +++--- gecode/search/worker-control.cpp | 9 +-------- test/search.cpp | 15 +++++++-------- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/changelog.in b/changelog.in index 1d801f68ec..9e2cae8d31 100755 --- a/changelog.in +++ b/changelog.in @@ -81,7 +81,8 @@ Rank: major Add Search::WorkerControl for asynchronously adjusting how many pre-created workers a parallel DFS or BAB engine may execute. Controls also apply to leaf engines owned by RBS and PBS, allowing callers to redistribute a fixed worker -budget among engines without rebuilding them. +budget among engines without rebuilding them. Requesting zero pauses an engine +without discarding its search state. [RELEASE] Version: 6.4.0 diff --git a/docs/worker-control.md b/docs/worker-control.md index 268e71f11c..2d86674013 100644 --- a/docs/worker-control.md +++ b/docs/worker-control.md @@ -20,12 +20,13 @@ Gecode::DFS engine(root, options); // Safe from another thread while engine.next() is running. control.request(6); -control.request(1); +control.request(0); // Pause +control.request(1); // Resume ``` The `threads` option is expanded once and becomes the engine's immutable -capacity. A request must be between one and that capacity, inclusive. Zero is -not a pause operation: at least one worker remains available to make progress. +capacity. A request must be between zero and that capacity, inclusive. Zero +pauses the engine without discarding its search state. ## Asynchronous semantics @@ -37,7 +38,8 @@ Changes take effect cooperatively at scheduler boundaries. A grow request can make parked workers eligible immediately. A shrink request does not interrupt a worker in the middle of a search action: excess workers finish their current action and park before beginning another. Consequently, the old and new -requests may overlap briefly. +requests may overlap briefly. Once a request for zero has taken effect, a call +to `next` remains blocked until a positive request resumes the engine. Requests affect scheduling, not search correctness. DFS still enumerates the same solution set and BAB still returns the same optimum. Parallel exploration @@ -115,3 +117,9 @@ because resizing is cooperative, applications needing a strict observed handoff must also provide their own acknowledgement or accounting layer. Gecode intentionally exposes the scheduling seam without embedding a portfolio allocation policy. + +PBS completes a `next` round only after every active asset has reported. An +asset paused while that call is running therefore must eventually be resumed; +otherwise the whole PBS `next` call remains blocked even if another asset has +already found a solution. A controller may still assign zero temporarily, but +it must account for this round barrier when deciding when to resume the asset. diff --git a/gecode/search.hh b/gecode/search.hh index 692ab5ad6d..887d810fbf 100755 --- a/gecode/search.hh +++ b/gecode/search.hh @@ -728,7 +728,7 @@ namespace Gecode { namespace Search { public: /// Construct an empty handle WorkerControl(void) noexcept; - /// Construct an engaged handle with initial request \a requested + /// Construct an engaged handle with initial request \a requested (zero pauses) explicit WorkerControl(unsigned int requested); /// Copy constructor WorkerControl(const WorkerControl& control); @@ -738,9 +738,9 @@ namespace Gecode { namespace Search { ~WorkerControl(void); /// Whether this handle is engaged explicit operator bool(void) const noexcept; - /// Return the requested number of workers, or zero for an empty handle + /// Return the requested number of workers (zero also denotes an empty handle) unsigned int requested(void) const noexcept; - /// Request \a workers workers + /// Request \a workers workers (zero pauses the engine) void request(unsigned int workers); /// Return the fixed worker capacity, or zero before attachment unsigned int capacity(void) const noexcept; diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 17f1d6957d..02b9fce0a2 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -63,11 +63,7 @@ namespace Gecode { namespace Search { : state(nullptr) {} WorkerControl::WorkerControl(unsigned int requested) - : state(nullptr) { - if (requested == 0U) - throw InvalidWorkerRequest("WorkerControl::WorkerControl"); - state = new State(requested); - } + : state(new State(requested)) {} WorkerControl::WorkerControl(const WorkerControl& control) : state(control.state) { @@ -104,9 +100,6 @@ namespace Gecode { namespace Search { WorkerControl::request(unsigned int workers) { if (state == nullptr) throw UninitializedWorkerControl("WorkerControl::request"); - if (workers == 0U) - throw InvalidWorkerRequest("WorkerControl::request"); - bool changed = false; { Support::Lock lock(state->mutex); diff --git a/test/search.cpp b/test/search.cpp index 81c0ea4353..714e673fdf 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -399,7 +399,7 @@ namespace Test { void resize(unsigned int seed) { Gecode::Support::RandomGenerator rand(seed); while (!done.load(std::memory_order_acquire)) { - control.request(1U + rand(capacity)); + control.request(rand(capacity+1U)); std::this_thread::yield(); } } @@ -449,15 +449,12 @@ namespace Test { bool ok = !empty && (empty.requested() == 0U) && (empty.capacity() == 0U) && throws( - [&] { empty.request(1U); }) && - throws( - [] { WorkerControl invalid(0U); }); + [&] { empty.request(1U); }); const unsigned int capacity = worker_capacity(2U); - WorkerControl control(capacity); + WorkerControl control(0U); WorkerControl copy(control); - control.request(1U); - ok = ok && copy && (copy.requested() == 1U); + ok = ok && copy && (copy.requested() == 0U); Gecode::Search::Options o; o.threads = 2.0; @@ -468,7 +465,9 @@ namespace Test { Gecode::DFS engine(root,o); delete root; root = nullptr; - ok = ok && (control.capacity() == capacity) && + ok = ok && (control.capacity() == capacity); + control.request(1U); + ok = ok && (copy.requested() == 1U) && throws( [&] { control.request(capacity+1U); }); delete engine.next(); From efc53380611c3b23e498fc3d719276db7b7acad1 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 20 Aug 2026 09:38:09 +0200 Subject: [PATCH 13/15] Wake paused assets at PBS round boundaries --- changelog.in | 6 +++--- docs/worker-control.md | 37 ++++++++++++++++------------------ gecode/search/par/bab.hpp | 7 +++++++ gecode/search/par/dfs.hpp | 7 +++++++ gecode/search/par/engine.hh | 2 ++ gecode/search/par/engine.hpp | 9 +++++++++ gecode/search/par/pbs.cpp | 6 ++++++ gecode/search/par/pbs.hh | 8 +++++++- gecode/search/par/pbs.hpp | 23 +++++++++++++++++---- gecode/search/pbs.cpp | 4 ++-- gecode/search/pbs.hpp | 7 ++++--- test/search.cpp | 39 ++++++++++++++++++++++++++++++++++++ 12 files changed, 122 insertions(+), 33 deletions(-) diff --git a/changelog.in b/changelog.in index 9e2cae8d31..7aa41b2dcd 100755 --- a/changelog.in +++ b/changelog.in @@ -80,9 +80,9 @@ Rank: major [DESCRIPTION] Add Search::WorkerControl for asynchronously adjusting how many pre-created workers a parallel DFS or BAB engine may execute. Controls also apply to leaf -engines owned by RBS and PBS, allowing callers to redistribute a fixed worker -budget among engines without rebuilding them. Requesting zero pauses an engine -without discarding its search state. +engines owned by RBS and PBS. Callers can redistribute a fixed worker budget +without rebuilding the engines. Requesting zero pauses an engine without +discarding its search state or holding a PBS round open. [RELEASE] Version: 6.4.0 diff --git a/docs/worker-control.md b/docs/worker-control.md index 2d86674013..19c1e86444 100644 --- a/docs/worker-control.md +++ b/docs/worker-control.md @@ -3,9 +3,8 @@ ## Purpose `Search::WorkerControl` lets an external scheduler change how many workers a -running DFS or BAB engine may use. Its main use is a portfolio that has a fixed -thread budget but wants to move that budget among independently useful search -engines as conditions change. +running DFS or BAB engine may use. A portfolio with a fixed thread budget can +move workers among its engines at runtime. The engine still starts with a fixed maximum: @@ -37,19 +36,19 @@ reach that count. Changes take effect cooperatively at scheduler boundaries. A grow request can make parked workers eligible immediately. A shrink request does not interrupt a worker in the middle of a search action: excess workers finish their current -action and park before beginning another. Consequently, the old and new -requests may overlap briefly. Once a request for zero has taken effect, a call -to `next` remains blocked until a positive request resumes the engine. +action and park before beginning another. The old and new requests may overlap +briefly. Once a request for zero has taken effect, a call to `next` remains +blocked until a positive request resumes the engine. Requests affect scheduling, not search correctness. DFS still enumerates the same solution set and BAB still returns the same optimum. Parallel exploration order, solution order, node counts, failure counts, and the time at which a -request becomes visible are deliberately nondeterministic. +request becomes visible remain nondeterministic. Shrinking parks resident operating-system threads. It does not destroy their thread objects or discard their engine-local search state. Growing wakes those -threads again. Applications should therefore choose capacity as a real -resource maximum: parked workers retain stacks and other per-worker state. +threads again. Choose capacity as a real resource maximum. Parked workers +retain stacks and other per-worker state. ## Handle lifetime and ownership @@ -83,9 +82,8 @@ requests are harmless and cannot access destroyed scheduler state. ## Meta-search -Restart-based search transports one leaf control through restart construction -and reset. The same underlying DFS or BAB leaf engine remains the adjustment -target across episodes. +Restart-based search keeps one leaf control through construction and reset. +The same DFS or BAB leaf engine remains the adjustment target across restarts. Portfolio-based search does not divide a global thread budget. Give each PBS asset its own control in the corresponding sequential-engine builder options: @@ -113,13 +111,12 @@ asset_b.request(6); The portfolio controller must enforce its own active-worker budget. Make the decrease before the increase if even a brief oversubscription is unacceptable; -because resizing is cooperative, applications needing a strict observed -handoff must also provide their own acknowledgement or accounting layer. -Gecode intentionally exposes the scheduling seam without embedding a -portfolio allocation policy. +resizing is cooperative, so a controller that must measure the handoff needs +its own acknowledgement or accounting. Gecode does not choose an allocation +policy. PBS completes a `next` round only after every active asset has reported. An -asset paused while that call is running therefore must eventually be resumed; -otherwise the whole PBS `next` call remains blocked even if another asset has -already found a solution. A controller may still assign zero temporarily, but -it must account for this round barrier when deciding when to resume the asset. +asset at zero still observes the internal stop used to close the round. It +reports without doing more search, and its worker request remains zero. A +solution from another asset returns while the asset stays paused. If every +active asset is paused, `next` blocks until at least one resumes. diff --git a/gecode/search/par/bab.hpp b/gecode/search/par/bab.hpp index 98e717cdae..3afbe8ca9f 100755 --- a/gecode/search/par/bab.hpp +++ b/gecode/search/par/bab.hpp @@ -264,6 +264,13 @@ namespace Gecode { namespace Search { namespace Par { case C_WORK: // Perform exploration work { + if (engine().scheduler_paused()) { + start(); + if (stop(engine().opt())) { + engine().stop(); + break; + } + } if (!engine().scheduler_admit(index)) break; m.acquire(); diff --git a/gecode/search/par/dfs.hpp b/gecode/search/par/dfs.hpp index d50e771462..205d7c31be 100755 --- a/gecode/search/par/dfs.hpp +++ b/gecode/search/par/dfs.hpp @@ -208,6 +208,13 @@ namespace Gecode { namespace Search { namespace Par { case C_WORK: // Perform exploration work { + if (engine().scheduler_paused()) { + start(); + if (stop(engine().opt())) { + engine().stop(); + break; + } + } if (!engine().scheduler_admit(index)) break; m.acquire(); diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index a8a7cf16f9..c717d9acb3 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -131,6 +131,8 @@ namespace Gecode { namespace Search { namespace Par { void scheduler_reset(bool root_owner); /// Admit worker \a worker for one exploration action bool scheduler_admit(unsigned int worker); + /// Whether the external worker request currently pauses the engine + bool scheduler_paused(void) const; /// Record that worker \a worker owns search void scheduler_owner(unsigned int worker); /// Record that worker \a worker is idle diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index 75d7737fa2..32a438a8d3 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -266,10 +266,19 @@ namespace Gecode { namespace Search { namespace Par { if (admitted) return true; WorkerControlAccess::wait(worker_control,worker); + // Re-enter the worker loop to observe a stop or command change. + return false; } return false; } + template + forceinline bool + Engine::scheduler_paused(void) const { + return scheduler_enabled && + (WorkerControlAccess::requested(worker_control) == 0U); + } + template void Engine::scheduler_owner(unsigned int worker) { diff --git a/gecode/search/par/pbs.cpp b/gecode/search/par/pbs.cpp index a05deeeda1..6d3fc8a5a8 100644 --- a/gecode/search/par/pbs.cpp +++ b/gecode/search/par/pbs.cpp @@ -36,6 +36,7 @@ */ #include +#include namespace Gecode { namespace Search { namespace Par { @@ -45,6 +46,11 @@ namespace Gecode { namespace Search { namespace Par { ((so != nullptr) && so->stop(s,o)); } + void + PortfolioStop::wake(void) { + WorkerControlAccess::signal_all(worker_control); + } + }}} // STATISTICS: search-par diff --git a/gecode/search/par/pbs.hh b/gecode/search/par/pbs.hh index 6bdcf5c82a..8db69e5507 100644 --- a/gecode/search/par/pbs.hh +++ b/gecode/search/par/pbs.hh @@ -48,15 +48,19 @@ namespace Gecode { namespace Search { namespace Par { private: /// The stop object for the slaves Stop* so; + /// Worker control for waking a paused asset + WorkerControl worker_control; /// Whether search must be stopped std::atomic* tostop; public: /// Initialize - PortfolioStop(Stop* so); + PortfolioStop(Stop* so, const WorkerControl& control); /// Set pointer to shared \a tostop variable void share(std::atomic* ts); /// Return true if portfolio engine must be stopped virtual bool stop(const Statistics& s, const Options& o); + /// Wake workers so that a paused asset can observe the portfolio stop + void wake(void); /// Signal whether search must be stopped void stop(bool s); /// Whether search must be stopped @@ -101,6 +105,8 @@ namespace Gecode { namespace Search { namespace Par { virtual Support::Terminator* terminator(void) const; /// Wait for the slave to complete its current run void wait(void); + /// Wake a paused slave so that it can observe the portfolio stop + void wake(void); /// Constrain with better solution \a b void constrain(const Space& b); /// Perform one run diff --git a/gecode/search/par/pbs.hpp b/gecode/search/par/pbs.hpp index 00d7f42ca5..e4c8f33135 100755 --- a/gecode/search/par/pbs.hpp +++ b/gecode/search/par/pbs.hpp @@ -139,8 +139,8 @@ namespace Gecode { namespace Search { namespace Par { forceinline - PortfolioStop::PortfolioStop(Stop* so0) - : so(so0), tostop(nullptr) {} + PortfolioStop::PortfolioStop(Stop* so0, const WorkerControl& control) + : so(so0), worker_control(control), tostop(nullptr) {} forceinline void PortfolioStop::share(std::atomic* ts) { @@ -174,6 +174,11 @@ namespace Gecode { namespace Search { namespace Par { } template forceinline void + Slave::wake(void) { + static_cast(stop)->wake(); + } + template + forceinline void Slave::constrain(const Space& b) { slave->constrain(b); } @@ -205,14 +210,20 @@ namespace Gecode { namespace Search { namespace Par { PBS::report(Slave* slave, Space* s) { // If b is false the report should be repeated (solution was worse) bool b = true; + bool wake = false; m.acquire(); if (s != nullptr) { b = solutions.add(s,slave); - if (b) + if (b) { tostop.store(true, std::memory_order_release); + wake = true; + } } else if (slave->stopped()) { - if (!tostop.load(std::memory_order_acquire)) + if (!tostop.load(std::memory_order_acquire)) { slave_stop.store(true, std::memory_order_release); + tostop.store(true, std::memory_order_release); + wake = true; + } } else { // Move slave to inactive, as it has exhausted its engine unsigned int i=0; @@ -222,7 +233,11 @@ namespace Gecode { namespace Search { namespace Par { assert(n_active > 0); std::swap(slaves[i],slaves[--n_active]); tostop.store(true, std::memory_order_release); + wake = true; } + if (wake) + for (unsigned int i=0U; iwake(); if (b) { if (--n_busy == 0) idle.signal(); diff --git a/gecode/search/pbs.cpp b/gecode/search/pbs.cpp index 701a74b79e..a910158b30 100644 --- a/gecode/search/pbs.cpp +++ b/gecode/search/pbs.cpp @@ -58,8 +58,8 @@ namespace Gecode { namespace Search { namespace Seq { namespace Gecode { namespace Search { namespace Par { Stop* - pbsstop(Stop* so) { - return new PortfolioStop(so); + pbsstop(Stop* so, const WorkerControl& control) { + return new PortfolioStop(so,control); } Engine* diff --git a/gecode/search/pbs.hpp b/gecode/search/pbs.hpp index 258afafa20..0c8807b753 100644 --- a/gecode/search/pbs.hpp +++ b/gecode/search/pbs.hpp @@ -135,7 +135,7 @@ namespace Gecode { namespace Search { namespace Par { /// Create stop object GECODE_SEARCH_EXPORT Stop* - pbsstop(Stop* so); + pbsstop(Stop* so, const WorkerControl& control); /// Create parallel portfolio engine GECODE_SEARCH_EXPORT Engine* @@ -273,7 +273,7 @@ namespace Gecode { namespace Search { Space* unowned_master = master; try { for (unsigned int i=0U; iclone(); try { @@ -321,7 +321,8 @@ namespace Gecode { namespace Search { try { for (int i=0; ioptions().stop); + stops[i] = Par::pbsstop(sebs[i]->options().stop, + sebs[i]->options().worker_control); sebs[i]->options().stop = stops[i]; sebs[i]->options().clone = false; Space* slave = (i == n_slaves-1) ? diff --git a/test/search.cpp b/test/search.cpp index 714e673fdf..9fd03b1636 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -482,6 +482,45 @@ namespace Test { WorkerControlAPI worker_control_api; + /// A paused PBS asset must not hold the portfolio round open + class WorkerControlPBSPause : public Base { + public: + WorkerControlPBSPause(void) + : Base("Search::WorkerControl::PBS::Pause") {} + bool run(void) override { +#ifdef GECODE_HAS_THREADS + Gecode::Search::WorkerControl paused(0U); + Gecode::Search::Options paused_options; + paused_options.threads = 2.0; + paused_options.worker_control = paused; + + Gecode::Search::Options active_options; + active_options.threads = 2.0; + + Gecode::SEBs sebs(2); + sebs[0] = Gecode::dfs(paused_options); + sebs[1] = Gecode::dfs(active_options); + + Gecode::Search::Options portfolio_options; + portfolio_options.threads = 2.0; + SolveImmediate* root = + new SolveImmediate(HTB_NONE,HTB_NONE,HTB_NONE); + Gecode::PBS + pbs(root,sebs,portfolio_options); + delete root; + + SolveImmediate* solution = pbs.next(); + bool ok = (solution != nullptr) && (paused.requested() == 0U); + delete solution; + return ok; +#else + return true; +#endif + } + }; + + WorkerControlPBSPause worker_control_pbs_pause; + /// %Test for depth-first search template class DFS : public Test { From 787a604dc2ee50d8ec2c0925774f48a7ebe8c5d7 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 20 Aug 2026 09:43:28 +0200 Subject: [PATCH 14/15] Update vcpkg pins for Windows CI --- .github/workflows/build.yml | 6 +++--- vcpkg.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7630dcdeff..1a4db0af5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -428,9 +428,9 @@ jobs: uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ${{ runner.temp }}\vcpkg-binary-cache - key: vcpkg-${{ runner.os }}-x64-windows-d015e31e90838a4c9dfa3eed45979bc70d9357fc-${{ hashFiles('vcpkg.json') }} + key: vcpkg-${{ runner.os }}-x64-windows-45f9f39362a4c52e2b1fbe57b7e649db7f3d96d4-${{ hashFiles('vcpkg.json') }} restore-keys: | - vcpkg-${{ runner.os }}-x64-windows-d015e31e90838a4c9dfa3eed45979bc70d9357fc- + vcpkg-${{ runner.os }}-x64-windows-45f9f39362a4c52e2b1fbe57b7e649db7f3d96d4- - name: Setup vcpkg shell: pwsh @@ -439,7 +439,7 @@ jobs: $vcpkgBinaryCache = Join-Path $env:RUNNER_TEMP "vcpkg-binary-cache" New-Item -ItemType Directory -Force -Path $vcpkgBinaryCache | Out-Null git clone https://github.com/microsoft/vcpkg $vcpkgRoot - git -C $vcpkgRoot checkout d015e31e90838a4c9dfa3eed45979bc70d9357fc + git -C $vcpkgRoot checkout 45f9f39362a4c52e2b1fbe57b7e649db7f3d96d4 & (Join-Path $vcpkgRoot "bootstrap-vcpkg.bat") -disableMetrics "VCPKG_ROOT=$vcpkgRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "VCPKG_DEFAULT_BINARY_CACHE=$vcpkgBinaryCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append diff --git a/vcpkg.json b/vcpkg.json index 7af80062c3..cc6088cd36 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name": "gecode", "version-string": "6.4.0", - "builtin-baseline": "01e2abc86d9a6e2bee0f6355eb7f8650b6cea100", + "builtin-baseline": "45f9f39362a4c52e2b1fbe57b7e649db7f3d96d4", "dependencies": [ "mpfr" ] From a4d05008285c221607920505483c5672c0098f3c Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 20 Aug 2026 10:42:23 +0200 Subject: [PATCH 15/15] Document paused TimeStop behavior --- docs/worker-control.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/worker-control.md b/docs/worker-control.md index 19c1e86444..cde8e49495 100644 --- a/docs/worker-control.md +++ b/docs/worker-control.md @@ -23,9 +23,9 @@ control.request(0); // Pause control.request(1); // Resume ``` -The `threads` option is expanded once and becomes the engine's immutable -capacity. A request must be between zero and that capacity, inclusive. Zero -pauses the engine without discarding its search state. +Gecode resolves the `threads` option when it constructs the engine. The result +is the fixed worker capacity. A request must be between zero and that capacity, +inclusive. Zero pauses the engine without discarding its search state. ## Asynchronous semantics @@ -33,12 +33,17 @@ pauses the engine without discarding its search state. and wakes parked workers when necessary; it does not wait for the engine to reach that count. -Changes take effect cooperatively at scheduler boundaries. A grow request can -make parked workers eligible immediately. A shrink request does not interrupt -a worker in the middle of a search action: excess workers finish their current -action and park before beginning another. The old and new requests may overlap -briefly. Once a request for zero has taken effect, a call to `next` remains -blocked until a positive request resumes the engine. +Changes take effect cooperatively at scheduler boundaries. A grow request makes +parked workers eligible immediately. A shrink request cannot interrupt a +worker in the middle of a search action. Excess workers finish that action and +park before beginning another, so the engine may briefly use more workers than +requested. Once a request for zero has taken effect, `next` remains blocked +until a positive request resumes the engine. + +`TimeStop` follows the same cooperative rule. The engine checks stop objects at +search boundaries; there is no timer thread to wake a paused engine. If a +`TimeStop` expires while the worker request is zero, `next` remains blocked. +After the engine resumes, it observes the expired stop at a normal stop check. Requests affect scheduling, not search correctness. DFS still enumerates the same solution set and BAB still returns the same optimum. Parallel exploration @@ -47,13 +52,13 @@ request becomes visible remain nondeterministic. Shrinking parks resident operating-system threads. It does not destroy their thread objects or discard their engine-local search state. Growing wakes those -threads again. Choose capacity as a real resource maximum. Parked workers -retain stacks and other per-worker state. +threads again. Set the capacity to the largest allocation the engine may +receive. Parked workers retain their stacks and other per-worker state. ## Handle lifetime and ownership -A control is a copyable shared-identity handle. Copies made before or after -engine construction publish to the same request state: +A control is a copyable handle with shared identity. Copies made before or +after engine construction publish to the same request state: ```cpp Gecode::Search::WorkerControl portfolio_control(4); @@ -70,11 +75,11 @@ An empty default-constructed handle means that worker adjustment is disabled. Calling `request` on an empty handle raises `Search::UninitializedWorkerControl`. -One shared identity can bind to one logical leaf engine once. It cannot be -shared by two simultaneously constructed DFS/BAB engines, reused for a -replacement engine after destruction, or attached directly to an enclosing -meta-engine. Such reuse raises `Search::WorkerControlInUse`. Construct a new -control for each leaf-engine lifetime. +A shared identity can be bound to only one logical leaf engine. It cannot be +shared by two DFS/BAB engines, reused for a replacement engine after +destruction, or attached directly to an enclosing meta-engine. These uses raise +`Search::WorkerControlInUse`. Construct a new control for each leaf-engine +lifetime. Destroying an engine safely detaches its state, but it does not make that identity reusable. A copied handle may outlive the engine; further in-range