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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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