Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,9 @@ jobs:
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ${{ runner.temp }}\vcpkg-binary-cache
key: vcpkg-${{ runner.os }}-x64-windows-d015e31e90838a4c9dfa3eed45979bc70d9357fc-${{ hashFiles('vcpkg.json') }}
key: vcpkg-${{ runner.os }}-x64-windows-45f9f39362a4c52e2b1fbe57b7e649db7f3d96d4-${{ hashFiles('vcpkg.json') }}
restore-keys: |
vcpkg-${{ runner.os }}-x64-windows-d015e31e90838a4c9dfa3eed45979bc70d9357fc-
vcpkg-${{ runner.os }}-x64-windows-45f9f39362a4c52e2b1fbe57b7e649db7f3d96d4-

- name: Setup vcpkg
shell: pwsh
Expand All @@ -439,7 +439,7 @@ jobs:
$vcpkgBinaryCache = Join-Path $env:RUNNER_TEMP "vcpkg-binary-cache"
New-Item -ItemType Directory -Force -Path $vcpkgBinaryCache | Out-Null
git clone https://github.com/microsoft/vcpkg $vcpkgRoot
git -C $vcpkgRoot checkout d015e31e90838a4c9dfa3eed45979bc70d9357fc
git -C $vcpkgRoot checkout 45f9f39362a4c52e2b1fbe57b7e649db7f3d96d4
& (Join-Path $vcpkgRoot "bootstrap-vcpkg.bat") -disableMetrics
"VCPKG_ROOT=$vcpkgRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"VCPKG_DEFAULT_BINARY_CACHE=$vcpkgBinaryCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
Expand Down
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
17 changes: 17 additions & 0 deletions changelog.in
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@
# 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. 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
127 changes: 127 additions & 0 deletions docs/worker-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# 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.

## 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 logical leaf engine. It cannot be
shared by two DFS/BAB engines, reused for a replacement engine after
destruction, or attached directly to an enclosing meta-engine. These uses raise
`Search::WorkerControlInUse`. Construct a new control for each leaf-engine
lifetime.

Destroying an engine safely detaches its state, but it does not make that
identity reusable. A copied handle may outlive the engine; further in-range
requests are harmless and cannot access destroyed scheduler state.

## 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:

```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;
resizing is cooperative, so a controller that must measure the handoff needs
its own acknowledgement or accounting. Gecode does not choose an allocation
policy.

PBS completes a `next` round only after every active asset has reported. An
asset 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.
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 (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
explicit operator bool(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 +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
Loading
Loading