Skip to content
4 changes: 2 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
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
16 changes: 16 additions & 0 deletions changelog.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
117 changes: 117 additions & 0 deletions docs/worker-control.md
Original file line number Diff line number Diff line change
@@ -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<MySpace> 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<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`.

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.
68 changes: 67 additions & 1 deletion gecode/search.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -783,6 +827,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 +987,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 +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
Expand All @@ -1285,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<T>::best;
};
Expand All @@ -1297,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
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 has already been bound") {}

}}

// STATISTICS: search-other
18 changes: 18 additions & 0 deletions gecode/search/exception.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
//@}
}}

Expand Down
1 change: 1 addition & 0 deletions gecode/search/options.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading
Loading