Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,8 @@ if(BUILD_TESTING)
Int::Linear::Bool::Int::Lq
Int::MiniModel::LinExpr::Bool::352
NoGoods::Queens
Search::PBS::ConstructionFailure
Search::WorkerControl::
Search::DFS::Sol::Binary::Nary::Binary::1::1::1)
if(GECODE_ENABLE_FLATZINC)
list(INSERT GECODE_CHECK_TESTS 1
Expand Down
6 changes: 4 additions & 2 deletions Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -1356,6 +1356,8 @@ CHECKTESTS = Branch::Int::Dense::3 \
Int::Linear::Bool::Int::Lq \
Int::MiniModel::LinExpr::Bool::352 \
NoGoods::Queens \
Search::PBS::ConstructionFailure \
Search::WorkerControl:: \
Search::DFS::Sol::Binary::Nary::Binary::1::1::1 \
Set::Dom::Dom::Gr \
Set::RelOp::ConstSSI::Union \
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions changelog.in
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ 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. Callers can redistribute a fixed worker budget
without rebuilding the engines. Requesting zero pauses an engine without
discarding its search state or holding a PBS round open.

[RELEASE]
Version: 6.4.0
Date: 2026-07-15
Expand Down
1 change: 1 addition & 0 deletions cmake/GecodeSources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
146 changes: 146 additions & 0 deletions docs/worker-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Adjustable parallel-search workers

## Purpose

`Search::WorkerControl` lets an external scheduler change how many workers a
running DFS or BAB engine may use. A portfolio with a fixed thread budget can
move workers among its engines at runtime.

The engine still starts with a fixed maximum:

```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<MySpace> engine(root, options);

// Safe from another thread while engine.next() is running.
control.request(6);
control.request(0); // Pause
control.request(1); // Resume
```

Gecode resolves the `threads` option when it constructs the engine. The result
is the fixed worker capacity. A request must be between zero and that capacity,
inclusive. Zero pauses the engine without discarding its search state.
This also applies when the resolved capacity is one: sequential DFS and BAB
wait at search boundaries until the control resumes them.
Pausing requires a build with thread support. Without thread support, attaching
an initially paused control or calling `request(0)` raises
`Search::InvalidWorkerRequest`; controls must be used from one thread.

## 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 makes
parked workers eligible immediately. A shrink request cannot interrupt a
worker in the middle of a search action. Excess workers finish that action and
park before beginning another, so the engine may briefly use more workers than
requested. Once a request for zero has taken effect, `next` remains blocked
until a positive request resumes the engine.

`TimeStop` follows the same cooperative rule. The engine checks stop objects at
search boundaries; there is no timer thread to wake a paused engine. If a
`TimeStop` expires while the worker request is zero, `next` remains blocked.
After the engine resumes, it observes the expired stop at a normal stop check.

Requests affect scheduling, not search correctness. DFS still enumerates the
same solution set and BAB still returns the same optimum. Parallel exploration
order, solution order, node counts, failure counts, and the time at which a
request becomes visible remain nondeterministic.

Shrinking parks resident operating-system threads. It does not destroy their
thread objects or discard their engine-local search state. Growing wakes those
threads again. Set the capacity to the largest allocation the engine may
receive. Parked workers retain their stacks and other per-worker state.

## Handle lifetime and ownership

A control is a copyable handle with shared identity. 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<MySpace> 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`.

A shared identity can be bound to only one leaf engine at a time. It cannot be
shared by two DFS/BAB engines or attached directly to an enclosing meta-engine.
Ordinary reuse for a replacement engine after destruction is also rejected.
These uses raise `Search::WorkerControlInUse`.

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.

An internal engine owner that constructs successive leaf engines can call
`WorkerControlAccess::prepare_reuse` after destroying the previous engine and
joining its workers. The replacement must have the same resolved capacity.
The control retains its shared identity, latest request (including pause),
request generation, and event storage. Preparing an attached control raises
`Search::WorkerControlInUse`; attaching with a different capacity raises
`Search::InvalidWorkerRequest`. No public reuse operation is provided.

## Meta-search

Restart-based search keeps one leaf control through construction and reset.
The same DFS or BAB leaf engine remains the adjustment target across restarts.

Portfolio-based search does not divide a global thread budget. Give each PBS
asset its own control in the corresponding sequential-engine builder options.
Controlled assets require a parallel outer PBS (`threads` resolving above one).
Sequential PBS rejects them with `Search::WorkerControlInUse`, since a paused
asset would prevent it from advancing to another asset. This check occurs before
PBS consumes the root or builders.

For example, configure the leaf builders as follows:

```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. Making the
decrease before the increase keeps the requested counts within the budget, but
does not prevent temporary oversubscription while workers finish their current
actions. A strict active-worker limit requires independent confirmation that
the decrease has taken effect before increasing another asset's request.
Gecode does not provide that acknowledgement or choose an allocation policy.

PBS completes a `next` round only after every active asset has reported. An
asset at zero still observes the internal stop used to close the round. It
reports without doing more search, and its worker request remains zero. A
solution from another asset returns while the asset stays paused. If every
active asset is paused, `next` blocks until at least one resumes.
70 changes: 69 additions & 1 deletion gecode/search.hh
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,45 @@ 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.
* Without thread support, concurrent use and pausing are unsupported:
* attaching a zero request or calling request(0) raises InvalidWorkerRequest.
*
* \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 (zero pauses)
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
bool engaged(void) const noexcept;
/// Return the requested number of workers (zero also denotes an empty handle)
unsigned int requested(void) const noexcept;
/// Request \a workers workers (zero pauses the engine)
void request(unsigned int workers);
/// Return the fixed worker capacity, or zero before attachment
unsigned int capacity(void) const noexcept;
};

class Stop;

/**
Expand Down Expand Up @@ -754,6 +793,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)
Expand Down Expand Up @@ -783,6 +829,8 @@ namespace Gecode { namespace Search {

}}

#include <gecode/search/worker-control.hh>

#include <gecode/search/options.hpp>

namespace Gecode { namespace Search {
Expand Down Expand Up @@ -941,6 +989,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;
Expand Down Expand Up @@ -1265,6 +1320,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
Expand All @@ -1285,8 +1344,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<T>::best;
};
Expand All @@ -1297,6 +1363,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
Expand Down
13 changes: 13 additions & 0 deletions gecode/search/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,18 @@
*/

#include <gecode/search.hh>
#include <gecode/search/worker-control.hh>

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;
Expand All @@ -49,6 +58,10 @@ namespace Gecode { namespace Search {
return NoGoods::eng;
}

Engine::~Engine(void) {
WorkerControlAccess::detach(worker_control);
}

}}

// STATISTICS: search-other
7 changes: 0 additions & 7 deletions gecode/search/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,4 @@
*
*/

namespace Gecode { namespace Search {

forceinline
Engine::~Engine(void) {}

}}

// STATISTICS: search-other
9 changes: 9 additions & 0 deletions gecode/search/exception.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 is already bound or has unsupported engine placement") {}

}}

// STATISTICS: search-other
Loading
Loading