diff --git a/.gitignore b/.gitignore index 9e4fc48bb1..4e1477fc45 100644 --- a/.gitignore +++ b/.gitignore @@ -111,5 +111,6 @@ test/test # Ignore build tree outputs (version metadata lives in top-level # gecode-version.m4). build/ +/results/ CMakeUserPresets.json CMakePresets.json.user diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d0600f6b..dfb37cf3fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1354,6 +1354,64 @@ if(GECODE_ENABLE_FLATZINC) endif() if(BUILD_TESTING) + unset(GECODE_CURRENT_META_HOOKS_COMPILE CACHE) + unset(GECODE_REMOVED_MASTER_HOOK_COMPILES CACHE) + unset(GECODE_REMOVED_SLAVE_HOOK_COMPILES CACHE) + set(GECODE_HAD_TRY_COMPILE_TARGET_TYPE FALSE) + if(DEFINED CMAKE_TRY_COMPILE_TARGET_TYPE) + set(GECODE_HAD_TRY_COMPILE_TARGET_TYPE TRUE) + endif() + set(GECODE_SAVED_TRY_COMPILE_TARGET_TYPE + "${CMAKE_TRY_COMPILE_TARGET_TYPE}") + set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + try_compile( + GECODE_CURRENT_META_HOOKS_COMPILE + "${CMAKE_CURRENT_BINARY_DIR}/origin-variant-api-pass" + SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/test/search/origin-variant-api-pass.cpp" + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_BINARY_DIR}" + "-DCMAKE_CXX_STANDARD=17" + "-DCMAKE_CXX_STANDARD_REQUIRED=ON") + if(NOT GECODE_CURRENT_META_HOOKS_COMPILE) + message(FATAL_ERROR + "Current origin/variant Space hooks do not compile") + endif() + + try_compile( + GECODE_REMOVED_MASTER_HOOK_COMPILES + "${CMAKE_CURRENT_BINARY_DIR}/origin-variant-api-fail-master" + SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/test/search/origin-variant-api-fail.cpp" + COMPILE_DEFINITIONS + -DGECODE_TEST_REMOVED_MASTER + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_BINARY_DIR}" + "-DCMAKE_CXX_STANDARD=17" + "-DCMAKE_CXX_STANDARD_REQUIRED=ON") + try_compile( + GECODE_REMOVED_SLAVE_HOOK_COMPILES + "${CMAKE_CURRENT_BINARY_DIR}/origin-variant-api-fail-slave" + SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/test/search/origin-variant-api-fail.cpp" + COMPILE_DEFINITIONS + -DGECODE_TEST_REMOVED_SLAVE + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_BINARY_DIR}" + "-DCMAKE_CXX_STANDARD=17" + "-DCMAKE_CXX_STANDARD_REQUIRED=ON") + if(GECODE_REMOVED_MASTER_HOOK_COMPILES OR + GECODE_REMOVED_SLAVE_HOOK_COMPILES) + message(FATAL_ERROR + "Removed meta-search Space hooks unexpectedly compile") + endif() + if(GECODE_HAD_TRY_COMPILE_TARGET_TYPE) + set(CMAKE_TRY_COMPILE_TARGET_TYPE + "${GECODE_SAVED_TRY_COMPILE_TARGET_TYPE}") + else() + unset(CMAKE_TRY_COMPILE_TARGET_TYPE) + endif() + set(GECODE_CAN_BUILD_TESTS TRUE) foreach(required search int minimodel driver) if(NOT TARGET gecode${required}) @@ -1399,6 +1457,13 @@ if(BUILD_TESTING) list(APPEND GECODE_TEST_LINK_LIBS gecodeflatzinc) endif() target_link_libraries(gecode-test PRIVATE ${GECODE_TEST_LINK_LIBS}) + if(GECODE_ENABLE_THREAD) + add_executable(gecode-worker-control-benchmark EXCLUDE_FROM_ALL + test/benchmark-worker-control.cpp) + target_compile_features(gecode-worker-control-benchmark PRIVATE cxx_std_17) + target_link_libraries(gecode-worker-control-benchmark + PRIVATE gecodeminimodel) + endif() if(GECODE_ENABLE_FAULT_INJECTION) add_executable(gecode-fault-test EXCLUDE_FROM_ALL ${GECODE_FAULT_TEST_SOURCES}) @@ -1500,6 +1565,17 @@ if(BUILD_TESTING) ENVIRONMENT "${GECODE_TEST_BLACKBOX_ENV}") endif() set(GECODE_CHECK_DEPENDS gecode-test) + add_executable(gecode-verify-origin-variant-terminology EXCLUDE_FROM_ALL + test/search/verify-origin-variant-terminology.cpp) + target_compile_features(gecode-verify-origin-variant-terminology + PRIVATE cxx_std_17) + add_custom_target(verify-origin-variant-terminology + COMMAND $ + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/test/search/origin-variant-terminology.expected + DEPENDS gecode-verify-origin-variant-terminology + VERBATIM) + list(APPEND GECODE_CHECK_DEPENDS verify-origin-variant-terminology) if(GECODE_ENABLE_FLOAT_VARS) add_custom_target(verify-gecode-check-tests COMMAND ${CMAKE_COMMAND} diff --git a/Makefile.in b/Makefile.in index b446405125..cb69f55747 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 \ @@ -903,7 +903,7 @@ INTEXAMPLESRC0 = \ word-square crossword open-shop car-sequencing sat \ bin-packing knights tsp perfect-square schurs-lemma \ dominating-queens colored-matrix multi-bin-packing \ - qcp job-shop + qcp job-shop origin-variant INTEXAMPLEHDR = $(INTEXAMPLEHDR0:%=examples/%.hpp) @@ -1267,7 +1267,8 @@ BRANCHTESTSRC0 = \ test/assign/set.cpp test/assign/float.cpp SEARCHTESTSRC0 = \ - test/search.cpp test/nogoods.cpp + test/search.cpp test/nogoods.cpp \ + test/search/origin-variant-api-pass.cpp ARRAYTESTSRC0 = \ test/array.cpp @@ -1297,6 +1298,8 @@ TESTOBJ = $(INTTESTOBJ) $(SETTESTOBJ) $(FLOATTESTOBJ) \ $(TESTSRC0:%.cpp=%$(OBJSUFFIX)) TESTSBJ = $(TESTOBJ:%$(OBJSUFFIX)=%$(SBJSUFFIX)) TESTEXE = test/test$(EXESUFFIX) +ORIGINVARIANTTERMSRC = test/search/verify-origin-variant-terminology.cpp +ORIGINVARIANTTERMEXE = test/search/verify-origin-variant-terminology$(EXESUFFIX) BLACKBOXEXECOBJ = $(BLACKBOXEXECSRC:%.cpp=%$(OBJSUFFIX)) BLACKBOXDLLOBJ = $(BLACKBOXDLLSRC:%.cpp=%$(OBJSUFFIX)) BLACKBOXEXEC = test/flatzinc/blackbox-exec$(EXESUFFIX) @@ -1310,7 +1313,7 @@ BLACKBOXFIXTURES = endif TESTBUILDDIRS = \ - test test/int test/set test/float \ + test test/int test/set test/float test/search \ test/branch test/assign \ test/flatzinc @@ -1387,7 +1390,21 @@ REQUIREDFLOATCHECKTESTS = Float::Arithmetic::PositiveNRootBounds \ endif # A basic integrity test -check: test +check: test verify-origin-variant-terminology \ + $(top_srcdir)/test/search/origin-variant-api-fail.cpp + @for hook in MASTER SLAVE; do \ + if $(CXX) $(CXXFLAGS) $(EXAMPLES_EXTRA_CXXFLAGS) \ + -DGECODE_TEST_REMOVED_$$hook \ + -I$(top_builddir) -I$(top_srcdir) -c \ + $(top_srcdir)/test/search/origin-variant-api-fail.cpp \ + -o test/search/origin-variant-api-fail$(OBJSUFFIX) \ + >/dev/null 2>&1; then \ + $(RMF) test/search/origin-variant-api-fail$(OBJSUFFIX); \ + echo "A removed meta-search Space hook unexpectedly compiles" >&2; \ + exit 1; \ + fi; \ + $(RMF) test/search/origin-variant-api-fail$(OBJSUFFIX); \ + done @for t in $(REQUIREDFLOATCHECKTESTS); do \ case " $(CHECKTESTS) " in *" $$t "*) ;; \ *) echo "Required Float check test is missing: $$t" >&2; exit 1 ;; \ @@ -1401,6 +1418,17 @@ check: test $(TESTEXE) -iter 2 -threads 0 -fixprob 1 \ $(CHECKTESTS:%=-test %) +.PHONY: verify-origin-variant-terminology +verify-origin-variant-terminology: mkcompiledirs \ + $(top_srcdir)/$(ORIGINVARIANTTERMSRC) \ + $(top_srcdir)/test/search/origin-variant-terminology.expected + $(CXX) $(CXXFLAGS) $(EXAMPLES_EXTRA_CXXFLAGS) -std=c++17 \ + $(top_srcdir)/$(ORIGINVARIANTTERMSRC) \ + -o $(ORIGINVARIANTTERMEXE) + $(ORIGINVARIANTTERMEXE) \ + $(top_srcdir) \ + $(top_srcdir)/test/search/origin-variant-terminology.expected + .PHONY: regenerate regenerate: bash "$(top_srcdir)/.github/ci/generated-source-freshness.sh" --update @@ -1978,6 +2006,7 @@ license.hh: $(ALLGECODEHDR:%=$(top_srcdir)/%) $(ALLSRC:%=$(top_srcdir)/%) \ stat.hh: $(ALLGECODEHDR:%=$(top_srcdir)/%) $(ALLSRC:%=$(top_srcdir)/%) \ $(TESTHDR:%=$(top_srcdir)/%) $(TESTSRC:%=$(top_srcdir)/%) \ + $(ORIGINVARIANTTERMSRC:%=$(top_srcdir)/%) \ $(VARIMPHDR) $(UVRUN) $(top_srcdir)/misc/genstatistics.py $^ > stat.hh @@ -2177,6 +2206,8 @@ clean: $(EXAMPLEEXE:%=%.rc) $(EXAMPLEEXE:%=%.res) $(RMF) $(TESTEXE:%.exe=%.pdb) $(TESTEXE:%=%.manifest) \ $(TESTEXE:%=%.rc) $(TESTEXE:%=%.res) + $(RMF) $(ORIGINVARIANTTERMEXE) \ + $(ORIGINVARIANTTERMEXE:%.exe=%.pdb) $(RMF) $(BLACKBOXEXEC:%.exe=%.pdb) $(BLACKBOXEXEC:%=%.manifest) \ $(BLACKBOXDLL:%$(DLLSUFFIX)=%$(LIBSUFFIX)) \ $(BLACKBOXDLL:%$(DLLSUFFIX)=%$(PDBSUFFIX)) \ @@ -2213,6 +2244,6 @@ depend: mkcompiledirs $(top_srcdir) \ $(ALLSRC) \ $(FLATZINC_GENSRC) \ - $(TESTSRC) > Makefile.dep + $(TESTSRC) $(ORIGINVARIANTTERMSRC) > Makefile.dep -include Makefile.dep diff --git a/README.md b/README.md index a75b48fbf3..0b381c46c2 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ In particular, Gecode comes with [extensive tutorial and reference documentation](https://gecode.github.io/documentation.html). +For source-level breaking changes, see the +[Gecode 7 migration notes](docs/gecode-7-migration.md). +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/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index 0c9defc0f1..bfe4988aa3 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 @@ -412,6 +413,7 @@ set(GECODE_TEST_SOURCES test/nogoods.cpp test/region.cpp test/search.cpp + test/search/origin-variant-api-pass.cpp test/set.cpp test/set/channel.cpp test/set/construct.cpp diff --git a/docs/gecode-7-migration.md b/docs/gecode-7-migration.md new file mode 100644 index 0000000000..a96b9603f3 --- /dev/null +++ b/docs/gecode-7-migration.md @@ -0,0 +1,66 @@ +# Gecode 7 migration notes + +## Meta-search Space roles + +Gecode 7 names the two `Space` roles used by restart-based search (RBS) and +portfolio-based search (PBS) **Origin** and **Variant**. +The [Origin and Variant terminology contract](origin-variant-terminology.md) +records the lifecycle and naming rationale behind these roles. + +An Origin is the persistent or common Space from which an exploration Space +is derived. A Variant is the derived Space configured for one RBS restart +episode or one PBS portfolio asset. These are logical roles. In RBS, the +prepared object becomes the explored Variant while its clone becomes the next +Origin. + +The same terms apply to both engines because both use the same derivation: +prepare common state, derive an exploration Space, and specialize that Space. +The callback contracts remain engine-specific. RBS uses the return values to +control restart and completeness behavior, while PBS ignores them during +one-time Origin and per-asset Variant configuration. + +Gecode 7 makes the following breaking override rename: + +```cpp +// Gecode 6 +bool master(const Gecode::MetaInfo& mi) override; +bool slave(const Gecode::MetaInfo& mi) override; + +// Gecode 7 +bool origin(const Gecode::MetaInfo& mi) override; +bool variant(const Gecode::MetaInfo& mi) override; +``` + +There are no compatibility aliases. Keeping `override` on model hooks makes +the compiler identify every model that still uses a Gecode 6 signature. +The build-checked +[Origin and Variant migration example](../examples/origin-variant.cpp) +contains both current override declarations. + +The rename does not change callback timing or behavior: + +- RBS calls `origin` when persistent restart state is updated and calls + `variant` for each derived restart exploration. +- PBS calls `origin` once before constructing assets and calls `variant` once + per asset, including the single-asset path. +- The default RBS Origin behavior still constrains against the last solution, + posts no-goods, and requests a restart. +- The default PBS Origin behavior still removes common branchers before + asset-specific branching is installed. +- The default Variant behavior still performs no additional configuration and + returns `true`. + +Origin and Variant describe Spaces. They do not replace runtime terms such as +portfolio coordinator, wrapped engine, or worker thread where those terms +describe different objects. + +The [retained terminology inventory](origin-variant-terminology-inventory.md) +classifies every remaining occurrence of the Gecode 6 role terms. + +## Adjustable parallel-search workers + +Gecode 7 can adjust the active worker request of a running DFS or BAB engine +within a fixed initial capacity. The +[worker-control contract](worker-control.md) documents construction, +asynchronous resize semantics, parked-thread cost, handle lifetime, and the +explicit per-asset controls used by PBS. diff --git a/docs/origin-variant-terminology-inventory.md b/docs/origin-variant-terminology-inventory.md new file mode 100644 index 0000000000..76ac3fbf2c --- /dev/null +++ b/docs/origin-variant-terminology-inventory.md @@ -0,0 +1,52 @@ +# Origin and Variant terminology inventory + +This inventory retains the result of a case-insensitive, whole-word scan for +the two Gecode 6 meta-search role terms, including their plurals. The verifier +walks the entire repository. It excludes only `.git/`, `.zd/`, +`autom4te.cache/`, directories whose names begin with `build`, binary files, +and the manifest itself. Root build files are therefore in scope. + +`test/search/verify-origin-variant-terminology.cpp` builds a standalone +verifier for the Autoconf and CMake `check` targets. The occurrence-level +manifest is +[`test/search/origin-variant-terminology.expected`](../test/search/origin-variant-terminology.expected). +Each tab-separated row records `path:line:column`, the exact spelling, a +classification, and the escaped complete source line as its excerpt. Thus +two terms on one line are two independently classified records. A new, +removed, moved, respelled, or contextually changed occurrence fails the +check. + +Run the exact checked scan through either build surface: + +```sh +make verify-origin-variant-terminology +cmake --build BUILD_DIRECTORY --target verify-origin-variant-terminology +``` + +To print the whole-repository occurrence list in manifest format, with each +classification marked `UNCLASSIFIED`, run: + +```sh +BUILD_DIRECTORY/bin/gecode-verify-origin-variant-terminology REPOSITORY_ROOT - +``` + +## Classification guide + +The manifest contains one row per occurrence. These groups explain why its +classifications are retained: + +| Files | Classification | +| --- | --- | --- | +| `CMakeLists.txt`, `Makefile.in`, `test/search/origin-variant-api-fail.cpp` | Intentional negative API validation. The declarations must fail to compile. | +| `examples/origin-variant.cpp`, `docs/gecode-7-migration.md` | Historical migration mapping. | +| `docs/origin-variant-terminology.md` | Rejected terminology retained as design rationale. | +| `changelog.in` | Historical release record, not current API documentation. | +| `examples/scowl.hpp` | Third-party word-list data and licence. | +| `examples/perfect-square.cpp` | Unrelated geometric domain language. | +| `gecode/support/run-jobs.hpp` | Unrelated internal scheduler topology. | +| `gecode/int/cumulative/tree.hpp` | Bibliographic title. | + +The checked scan contains no current RBS/PBS identifier or descriptive prose +using either removed role term. A source change that adds one must either fix +the stale terminology or update this inventory and the checked manifest with a +specific unrelated or historical classification. diff --git a/docs/origin-variant-terminology.md b/docs/origin-variant-terminology.md new file mode 100644 index 0000000000..d7d3d5c2f1 --- /dev/null +++ b/docs/origin-variant-terminology.md @@ -0,0 +1,95 @@ +# Origin and Variant terminology contract + +## Decision + +Gecode 7 uses **Origin** and **Variant** for the two `Space` roles shared by +restart-based search (RBS) and portfolio-based search (PBS). + +An Origin is the persistent or common `Space` from which an exploration space +is derived. A Variant is the derived `Space` configured for one RBS restart +episode or one PBS portfolio asset. These are logical roles. An RBS Origin can +itself be a clone of the state used by the preceding episode while remaining +the Origin of the next exploration. + +## Lifecycles + +```text +RBS +origin[n] -- origin(restart_info) --> prepared state + |-- clone --> origin[n+1] + `-- variant(restart_info) --> explored variant[n] + +PBS +input -- optional clone --> origin -- origin(portfolio_info) + |-- derive/configure --> variant[0] + |-- derive/configure --> variant[1] + `-- derive/configure --> variant[n] +``` + +The RBS allocation identity changes across an episode: the prepared object +becomes the explored Variant and its clone becomes the next Origin. Origin +therefore names the source role for the next derivation, not the oldest object +in the solve. In single-asset PBS, one object passes through the Origin stage +and then the Variant stage; the names do not require separate allocations. + +## Behavior matrix + +| Engine | Role | Invocation and relationship | Information and result | Default behavior | +| --- | --- | --- | --- | --- | +| RBS | Origin | Repeated after solutions and at restart boundaries; prepares state before the next Origin clone is retained | Restart reason, counts, last solution, and no-goods. The Boolean can decide whether a solution triggers a restart | Constrain against the last solution when present, post no-goods, and request restart | +| RBS | Variant | Once for each derived restart exploration | Restart information. The Boolean declares the exploration complete or incomplete | No additional configuration; complete | +| PBS | Origin | Once on the common space before asset construction | Portfolio metadata; result ignored | Remove common branchers so assets can install their own branching | +| PBS | Variant | Once per asset, including the single-asset path | Asset number starting at zero; result ignored | No additional configuration | + +The common nouns do not require one callback contract. PBS and RBS have +different invocation frequencies and result semantics. Gecode 7 can later +represent those differences with engine-specific metadata, result types, or +overloads while retaining Origin and Variant. The rename itself preserves +current behavior. + +## Why topology names are weaker + +`Controller/Worker`, `Driver/Executor`, and `Scheduler/Worker` describe runtime +components that allocate or execute work. The public hooks belong to `Space`, +whose relevant relationship is persistence, cloning, specialization, and +exploration. The PBS coordinator is an engine object rather than the `Space` +passed to the first hook, and the retained RBS `Space` stores persistent state +without scheduling the underlying engine. + +Internal names therefore follow the actual objects. A retained or common +`Space*` can be an `origin`; a derived exploration space or per-asset runnable +can be a `variant`. A pointer to the enclosing PBS object is a `portfolio` or +`coordinator`, and its wrapped `Engine*` is an `engine` or `variant_engine`. + +## Alternatives considered but not selected + +| Pair | Strength | Reason not selected | +| --- | --- | --- | +| Archetype / Variant | Exact common-form relationship | `Archetype` is less immediate in a low-level API | +| Basis / Variant | Handles an evolving RBS source | `Basis` carries linear-algebra associations | +| Coordinator / Explorer | Good engine-topology terminology | The first public hook is on a `Space` that does not coordinate execution | +| Principal / Agent | Clear noun pair | Suggests delegation and autonomy rather than cloning | +| Archetype / Instance | Clear type-to-realization relation | `Instance` already commonly means a problem instance | +| Control / Asset | `Asset` fits PBS | `Control` can be a verb and the pair fits RBS poorly | +| Portfolio / Asset; Origin / Episode | Locally expressive for each engine | Four role names hide the shared derivation and complicate models supporting both engines | + +These are considered alternatives. They were not explicitly rejected by the +user. + +## Explicitly rejected names and designs + +- **Master / Slave** is the terminology being removed. It describes authority + and subordination rather than common state and a derived exploration. +- **Run / Search** was rejected because both words are verbs and already occur + frequently in this API. As member names, they do not clearly denote roles. +- **Different public role vocabularies for PBS and RBS** were rejected. Contract + differences should be represented by contracts, not by replacing one shared + derivation vocabulary with four unrelated nouns. + +## Migration boundary + +The Gecode 7 migration renames the public virtual functions, overrides, calls, +documentation, tests, and role-bearing internal identifiers. It does not add +deprecated aliases, change restart decisions, change completeness reporting, +change PBS asset numbering, or mechanically rename unrelated historical and +third-party occurrences. diff --git a/docs/worker-control.md b/docs/worker-control.md new file mode 100644 index 0000000000..b260a70b7f --- /dev/null +++ b/docs/worker-control.md @@ -0,0 +1,185 @@ +# 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. + +## Benchmarking and release gate + +The opt-in CMake target `gecode-worker-control-benchmark` and +`misc/benchmark-worker-control.py` measure: + +- paired DFS and BAB steady-state overhead when control remains fixed at + capacity; +- cooperative shrink and grow latency for cheap and propagation-heavy work; +- process CPU and peak-RSS differences with three of four resident workers + parked; and +- a two-asset PBS reallocation sequence under a fixed four-worker budget. + +Build and run the retained suite with: + +```sh +cmake --build build --target gecode-worker-control-benchmark +uv run --script misc/benchmark-worker-control.py run \ + --name worker-control-release --build-dir build +uv run --script misc/benchmark-worker-control.py analyze \ + --name worker-control-release +uv run --script misc/benchmark-worker-control.py report \ + --name worker-control-release +uv run --script misc/benchmark-worker-control.py plot \ + --name worker-control-release +``` + +Raw JSON, stdout, stderr, provenance, analysis, Markdown, LaTeX, PNG, and SVG +artifacts are stored under `results//`. That directory is ignored by +Git so measurements are not mistaken for portable golden data. + +The provisional unchanged-limit release gate is, for both DFS and BAB, median +paired overhead no greater than 3% and the deterministic-bootstrap 95% +confidence-interval upper bound no greater than 5%. Latency and parked-memory +measurements are reported rather than assigned universal limits: acceptable +values depend on propagation granularity, platform, stack configuration, and +the portfolio's reallocation timescale. A human release decision is required +after reviewing the retained results. + +### Retained Gecode 7 measurement + +The retained `worker-control-release-20260724-final` run at commit +`6e2e8a707771bf540b03c809cf3422d4d006b49b` executed 590 child cases on +Apple Silicon with Apple Clang 21.0.0: + +| Measurement | Result | +| --- | ---: | +| DFS fixed-capacity median overhead; bootstrap 95% CI | -0.896%; [-3.873%, 3.885%] | +| BAB fixed-capacity median overhead; bootstrap 95% CI | -0.045%; [-0.224%, 0.129%] | +| Cheap grow latency, p50 / p95 | 12.688 µs / 17.966 µs | +| Propagation-heavy grow latency, p50 / p95 | 12.500 µs / 19.664 µs | +| Cheap shrink latency, p50 / p95 | 7.488 ms / 7.532 ms | +| Propagation-heavy shrink latency, p50 / p95 | 7.493 ms / 7.518 ms | +| Median peak-RSS delta, four resident workers with three parked | 368 KiB | +| Median CPU/wall ratio, one worker / three of four parked | 1.000 / 1.459 | +| PBS maximum summed admitted high-water mark / budget | 4 / 4 | + +Both unchanged-limit overhead gates passed, all child cases completed, and all +30 PBS samples respected the active-worker bound. Negative point estimates are +measurement noise, not a claim that the control improves fixed-capacity +performance. Shrink latency reflects cooperative completion of current work; +grow latency reflects waking already resident workers. Peak RSS is a +process-level comparison and not an exact per-thread stack-reservation +measurement. + +The automated result is **provisional pass**. After reviewing these results +and their platform-specific limitations, the user accepted the feature for +Gecode 7 release on 2026-07-24. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a223ae844a..4970a09b9c 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -41,6 +41,7 @@ set(GECODE_EXAMPLE_SOURCES multi-bin-packing.cpp nonogram.cpp open-shop.cpp + origin-variant.cpp ortho-latin.cpp partition.cpp pentominoes.cpp diff --git a/examples/crossword.cpp b/examples/crossword.cpp index d3c6075047..a675e4d0a6 100755 --- a/examples/crossword.cpp +++ b/examples/crossword.cpp @@ -282,7 +282,7 @@ class Crossword : public Script { << n; } /// Do not perform a restart when a solution is found - bool master(const MetaInfo& mi) { + bool origin(const MetaInfo& mi) { if (mi.type() == MetaInfo::RESTART) // Post no-goods mi.nogoods().post(*this); diff --git a/examples/job-shop.cpp b/examples/job-shop.cpp index 356fac4cfd..7e303492a1 100755 --- a/examples/job-shop.cpp +++ b/examples/job-shop.cpp @@ -629,16 +629,16 @@ solve(const JobShopOptions& opt) { { Support::Timer t; t.start(); Search::Statistics stat; - JobShopProbe* master = new JobShopProbe(opt); + JobShopProbe* origin = new JobShopProbe(opt); - if (master->status() != SS_SOLVED) { - delete master; + if (origin->status() != SS_SOLVED) { + delete origin; std::cerr << "Error: has no solution..." << std::endl; return; } - l = master->cost().min(); - u = master->cost().max(); + l = origin->cost().min(); + u = origin->cost().max(); FailTimeStop fts(opt.fail_probe(),opt.time_probe()); CommonOptions so(opt); @@ -651,7 +651,7 @@ solve(const JobShopOptions& opt) { << std::endl; for (unsigned int p=0; p(master->clone()); + JobShopProbe* jsp = static_cast(origin->clone()); jsp->branch(p,rnd); DFS dfs(jsp,so); JobShopProbe* s = dfs.next(); @@ -669,7 +669,7 @@ solve(const JobShopOptions& opt) { } stat += statj; } - delete master; + delete origin; print(stat,false); std::cout << "\t\t\truntime: "; @@ -685,11 +685,11 @@ solve(const JobShopOptions& opt) { // Dichotomic search { - JobShopSolve* master = new JobShopSolve(opt); + JobShopSolve* origin = new JobShopSolve(opt); - if (master->status() == SS_FAILED) { - delete master; + if (origin->status() == SS_FAILED) { + delete origin; std::cerr << "Error: has no solution..." << std::endl; return; } @@ -704,7 +704,7 @@ solve(const JobShopOptions& opt) { while (l < u) { std::cout << "\t\tBounds: [" << l << "," << u << "]" << std::endl; - JobShopSolve* jss = static_cast(master->clone()); + JobShopSolve* jss = static_cast(origin->clone()); int m = (l + u) / 2; rel(*jss, jss->cost() >= l); rel(*jss, jss->cost() <= m); @@ -739,7 +739,7 @@ solve(const JobShopOptions& opt) { } if (l == u) { - delete master; + delete origin; std::cout << std::endl << "\tFound best solution and proved optimality." << std::endl; @@ -751,15 +751,15 @@ solve(const JobShopOptions& opt) { Support::Timer t; t.start(); std::cout << std::endl << "\tSolving..." << std::endl; - rel(*master, master->cost() >= l); - rel(*master, master->cost() < u); + rel(*origin, origin->cost() >= l); + rel(*origin, origin->cost() < u); CommonOptions so(opt); if (opt.time_solve() > 0U) so.stop = Search::Stop::time(opt.time_solve()); so.cutoff = Search::Cutoff::geometric(JobShopConfig::restart_scale, JobShopConfig::restart_base); - RBS rbs(master,so); + RBS rbs(origin,so); while (JobShopSolve* s = rbs.next()) { s->print(std::cout); u = s->cost().val(); diff --git a/examples/origin-variant.cpp b/examples/origin-variant.cpp new file mode 100644 index 0000000000..75545b9aee --- /dev/null +++ b/examples/origin-variant.cpp @@ -0,0 +1,83 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * 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. + * + * Example: migrate meta-search callbacks to Gecode 7 + * + * Gecode 6 declaration Gecode 7 declaration + * bool master(const MetaInfo&) override; bool origin(const MetaInfo&) override; + * bool slave(const MetaInfo&) override; bool variant(const MetaInfo&) override; + * + * Gecode 7 provides no compatibility aliases for the removed declarations. + */ + +#include +#include + +using namespace Gecode; + +class OriginVariant : public Script { +protected: + IntVar x; + +public: + OriginVariant(const Options& opt) + : Script(opt), x(*this,0,1) { + branch(*this,x,INT_VAL_MIN()); + } + + OriginVariant(OriginVariant& s) + : Script(s) { + x.update(*this,s.x); + } + + Space* copy(void) override { + return new OriginVariant(*this); + } + + bool origin(const MetaInfo& mi) override { + return Space::origin(mi); + } + + bool variant(const MetaInfo& mi) override { + return Space::variant(mi); + } + + void print(std::ostream& os) const override { + os << "x = " << x << std::endl; + } +}; + +int +main(int argc, char* argv[]) { + Options opt("OriginVariant"); + opt.parse(argc,argv); + Script::run(opt); + return 0; +} + +// STATISTICS: example-any diff --git a/examples/photo.cpp b/examples/photo.cpp index ed264158c5..2fdba79cc6 100644 --- a/examples/photo.cpp +++ b/examples/photo.cpp @@ -128,8 +128,8 @@ class Photo : public IntMinimizeScript { break; } } - /// Slave function for restarts - bool slave(const MetaInfo& mi) { + /// Variant function for restarts + bool variant(const MetaInfo& mi) { if ((mi.type() == MetaInfo::RESTART) && (mi.restart() > 0) && (p > 0.0)) { const Photo& l = static_cast(*mi.last()); diff --git a/examples/qcp.cpp b/examples/qcp.cpp index 49a75d5f92..363cb3d204 100644 --- a/examples/qcp.cpp +++ b/examples/qcp.cpp @@ -211,8 +211,8 @@ class QCP : public Script { } } } - /// Slave function for portfolio search - virtual bool slave(const MetaInfo& mi) { + /// Variant function for portfolio search + virtual bool variant(const MetaInfo& mi) { if (mi.type() == MetaInfo::PORTFOLIO) { double tbf = opt.tbf(); Rnd r(seeds[mi.asset() % n_seeds]); @@ -3997,4 +3997,3 @@ namespace { } // STATISTICS: example-any - diff --git a/gecode/flatzinc.hh b/gecode/flatzinc.hh index f4d934d3d1..1ed3a18827 100755 --- a/gecode/flatzinc.hh +++ b/gecode/flatzinc.hh @@ -672,8 +672,8 @@ namespace Gecode { namespace FlatZinc { virtual void constrain(const Space& s); /// Copy function virtual Gecode::Space* copy(void); - /// Slave function for restarts - virtual bool slave(const MetaInfo& mi); + /// Variant function for restarts + virtual bool variant(const MetaInfo& mi); /// \name AST to variable and value conversion //@{ diff --git a/gecode/flatzinc/flatzinc.cpp b/gecode/flatzinc/flatzinc.cpp index e35bd3638c..78c5d4047f 100644 --- a/gecode/flatzinc/flatzinc.cpp +++ b/gecode/flatzinc/flatzinc.cpp @@ -2077,7 +2077,7 @@ namespace Gecode { namespace FlatZinc { } bool - FlatZincSpace::slave(const MetaInfo& mi) { + FlatZincSpace::variant(const MetaInfo& mi) { if (mi.type() == MetaInfo::RESTART) { if (restart_data.initialized() && restart_data().mark_complete) { // Fail the space diff --git a/gecode/kernel/core.cpp b/gecode/kernel/core.cpp index 4293cfe6a8..81a227d289 100644 --- a/gecode/kernel/core.cpp +++ b/gecode/kernel/core.cpp @@ -897,7 +897,7 @@ namespace Gecode { } bool - Space::master(const MetaInfo& mi) { + Space::origin(const MetaInfo& mi) { switch (mi.type()) { case MetaInfo::RESTART: if (mi.last() != nullptr) @@ -915,7 +915,7 @@ namespace Gecode { } bool - Space::slave(const MetaInfo&) { + Space::variant(const MetaInfo&) { return true; } diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index 169aab6599..16b664c123 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -2087,13 +2087,13 @@ namespace Gecode { */ GECODE_KERNEL_EXPORT virtual void constrain(const Space& best); /** - * \brief Master configuration function for meta search engines + * \brief Origin configuration function for meta search engines * * This configuration function is used by both restart and * portfolio meta search engines. * * \li A restart meta search engine calls this function on its - * master space whenever it finds a solution or exploration + * origin space whenever it finds a solution or exploration * restarts. \a mi contains information about the current restart. * * If a solution has been found, then search will continue with @@ -2103,33 +2103,33 @@ namespace Gecode { * The default function posts no-goods obtained from \a mi. * * \li A portfolio meta engine calls this function once on - * the master space. The return value is ignored. + * the origin space. The return value is ignored. * - * The default function does nothing. + * The default function removes all common branchers. * * \ingroup TaskModelScript */ GECODE_KERNEL_EXPORT - virtual bool master(const MetaInfo& mi); + virtual bool origin(const MetaInfo& mi); /** - * \brief Slave configuration function for meta search engines + * \brief Variant configuration function for meta search engines * * This configuration function is used by both restart and * portfolio meta search engines. * * \li A restart meta search engine calls this function on its - * slave space whenever it finds a solution or exploration + * variant space whenever it finds a solution or exploration * restarts. \a mi contains information about the current restart. * - * If the function returns true, the search on the slave space is + * If the function returns true, the search on the variant space is * considered complete, i.e., if it fails or exhaustively explores the * entire search space, the meta search engine finishes. If the - * function returns false, the search on the slave space is considered + * function returns false, the search on the variant space is considered * incomplete, and the meta engine will restart the search regardless - * of whether the search on the slave space finishes or times out. + * of whether the search on the variant space finishes or times out. * * \li A portfolio meta engine calls this function once on each asset - * (that is, on each slave) and passes the number of the asset, + * (that is, on each variant) and passes the number of the asset, * starting from zero. * * The default function does nothing and returns true. @@ -2137,7 +2137,7 @@ namespace Gecode { * \ingroup TaskModelScript */ GECODE_KERNEL_EXPORT - virtual bool slave(const MetaInfo& mi); + virtual bool variant(const MetaInfo& mi); /* * Member functions for search engines diff --git a/gecode/search.hh b/gecode/search.hh index 38668804ad..11e079b79c 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) @@ -783,6 +827,8 @@ namespace Gecode { namespace Search { }} +#include + #include namespace Gecode { namespace Search { @@ -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; @@ -1181,13 +1234,13 @@ namespace Gecode { * periodically restart the search of engine \a E. * * The class \a T can implement member functions - * \code virtual bool master(const MetaInfo& mi) \endcode + * \code virtual bool origin(const MetaInfo& mi) \endcode * and - * \code virtual bool slave(const MetaInfo& mi) \endcode + * \code virtual bool variant(const MetaInfo& mi) \endcode * * Whenever exploration restarts or a solution is found, the - * engine executes the functions on the master and slave - * space. For more details, consult "Modeling and Programming + * engine executes the functions on the origin and variant + * spaces. For more details, consult "Modeling and Programming * with Gecode". * * \ingroup TaskModelSearch @@ -1209,13 +1262,13 @@ namespace Gecode { * periodically restart the search of engine \a E. * * The class \a T can implement member functions - * \code virtual bool master(const MetaInfo& mi) \endcode + * \code virtual bool origin(const MetaInfo& mi) \endcode * and - * \code virtual bool slave(const MetaInfo& mi) \endcode + * \code virtual bool variant(const MetaInfo& mi) \endcode * * Whenever exploration restarts or a solution is found, the - * engine executes the functions on the master and slave - * space. For more details, consult "Modeling and Programming + * engine executes the functions on the origin and variant + * spaces. For more details, consult "Modeling and Programming * with Gecode". * * \ingroup TaskModelSearch @@ -1235,22 +1288,22 @@ namespace Gecode { namespace Search { namespace Meta { /// Build a sequential engine template class E> - Engine* sequential(T* master, const Search::Statistics& stat, Options& opt); + Engine* sequential(T* origin, const Search::Statistics& stat, Options& opt); /// Build a sequential engine template class E> - Engine* sequential(T* master, SEBs& sebs, + Engine* sequential(T* origin, SEBs& sebs, const Search::Statistics& stat, Options& opt, bool best); #ifdef GECODE_HAS_THREADS /// Build a parallel engine template class E> - Engine* parallel(T* master, const Search::Statistics& stat, Options& opt); + Engine* parallel(T* origin, const Search::Statistics& stat, Options& opt); /// Build a parallel engine template class E> - Engine* parallel(T* master, SEBs& sebs, + Engine* parallel(T* origin, SEBs& sebs, const Search::Statistics& stat, Options& opt, bool best); #endif @@ -1265,11 +1318,15 @@ 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 + * \code virtual bool origin(const MetaInfo& mi) \endcode * and - * \code virtual bool slave(const MetaInfo& mi) \endcode + * \code virtual bool variant(const MetaInfo& mi) \endcode * * When the assets are created, these functions are executed. * For more details, consult "Modeling and Programming with Gecode". @@ -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::best; }; @@ -1297,11 +1361,13 @@ 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 + * \code virtual bool origin(const MetaInfo& mi) \endcode * and - * \code virtual bool slave(const MetaInfo& mi) \endcode + * \code virtual bool variant(const MetaInfo& mi) \endcode * * When the assets are created, these functions are executed. * For more details, consult "Modeling and Programming with Gecode". 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/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..f3073cac98 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; i 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 @@ -381,6 +403,7 @@ namespace Gecode { namespace Search { namespace Par { m_wait_reset.release(); // Wait for reset cycle stopped e_reset_ack_stop.wait(); + this->scheduler_reset(worker(0U)->owns_work()); } 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..74c007de7e 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; i 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 @@ -323,6 +344,7 @@ namespace Gecode { namespace Search { namespace Par { m_wait_reset.release(); // Wait for reset cycle stopped e_reset_ack_stop.wait(); + this->scheduler_reset(worker(0U)->owns_work()); } diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index e3b59077be..2f8a203bf0 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,70 @@ 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 + }; + /// Per-worker adjustable parallel admission state + struct SchedulerWorker { + bool lease; + bool parked; + SchedulerLogical logical; + }; + /// 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 + 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 parallel admission + void scheduler_enable(bool root_owner); + /// Reset adjustable parallel state while all workers are blocked + 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); /// \name Commands from engine to workers and wait management //@{ @@ -187,6 +247,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 8ff5ed66e9..a096546c4a 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 @@ -118,7 +121,13 @@ 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), 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) { // Initialize termination information _n_term_not_ack = workers(); _n_not_terminated = workers(); @@ -129,6 +138,271 @@ 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(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 + (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; + 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); + 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 (requested == workers()) + WorkerControlAccess::fast_admission(worker_control,generation); + if (wake) + 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) { + 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_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, + 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(); + 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); + } /* * Statistics @@ -183,6 +457,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 @@ -221,6 +504,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 @@ -371,6 +657,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/par/pbs.hh b/gecode/search/par/pbs.hh index 6bdcf5c82a..00dc95d06e 100644 --- a/gecode/search/par/pbs.hh +++ b/gecode/search/par/pbs.hh @@ -43,10 +43,10 @@ namespace Gecode { namespace Search { namespace Par { - /// Stop object used for controlling slaves in a portfolio + /// Stop object used for controlling variants in a portfolio class GECODE_SEARCH_EXPORT PortfolioStop : public Stop { private: - /// The stop object for the slaves + /// The stop object for the variants Stop* so; /// Whether search must be stopped std::atomic* tostop; @@ -67,9 +67,9 @@ namespace Gecode { namespace Search { namespace Par { template class PBS; - /// Runnable slave of a portfolio master + /// Runnable variant in a portfolio template - class GECODE_SEARCH_EXPORT Slave : public Support::Runnable { + class GECODE_SEARCH_EXPORT Variant : public Support::Runnable { protected: /// Completion event for the current run class Completion : public Support::Terminator { @@ -84,29 +84,29 @@ namespace Gecode { namespace Search { namespace Par { /// Wait for completion and consume the signal void wait(void); } completion; - /// The master engine - PBS* master; - /// The slave engine - Engine* slave; + /// The portfolio engine + PBS* portfolio; + /// The wrapped search engine + Engine* engine; /// Stop object Stop* stop; public: - /// Initialize with master \a m, slave \a s, and its stop object \a so - Slave(PBS* m, Engine* s, Stop* so); - /// Return statistics of slave + /// Initialize with portfolio \a p, engine \a e, and its stop object \a so + Variant(PBS* p, Engine* e, Stop* so); + /// Return statistics of variant Statistics statistics(void) const; - /// Check whether slave has been stopped + /// Check whether variant has been stopped bool stopped(void) const; /// Return the completion terminator virtual Support::Terminator* terminator(void) const; - /// Wait for the slave to complete its current run + /// Wait for the variant to complete its current run void wait(void); /// Constrain with better solution \a b void constrain(const Space& b); /// Perform one run virtual void run(void); - /// Delete slave - virtual ~Slave(void); + /// Delete variant + virtual ~Variant(void); }; /// Collect all solutions @@ -120,13 +120,13 @@ namespace Gecode { namespace Search { namespace Par { /// Initialize CollectAll(void); /// Add a solution \a a reported by \a r and always return true - bool add(Space* s, Slave* r); + bool add(Space* s, Variant* r); /// Dummy function bool constrain(const Space& b); /// Check whether there is any solution left bool empty(void) const; /// Return solution reported by \a r - Space* get(Slave*& r); + Space* get(Variant*& r); /// Destructor ~CollectAll(void); }; @@ -137,20 +137,20 @@ namespace Gecode { namespace Search { namespace Par { /// Currently best solution Space* b; /// Who has reported the best solution (nullptr if solution has already been reported) - Slave* reporter; + Variant* reporter; public: /// Whether it collects best solutions static const bool best = true; /// Initialize CollectBest(void); /// Add a solution \a s by \a r and return whether is was better - bool add(Space* s, Slave* r); + bool add(Space* s, Variant* r); /// Check whether \a b better and update accordingly bool constrain(const Space& b); /// Check whether there is any solution left bool empty(void) const; /// Return solution reported by \a r (only if a better one was found) - Space* get(Slave*& r); + Space* get(Variant*& r); /// Destructor ~CollectBest(void); }; @@ -158,37 +158,37 @@ namespace Gecode { namespace Search { namespace Par { /// Parallel portfolio engine implementation template class GECODE_SEARCH_EXPORT PBS : public Engine { - friend class Slave; + friend class Variant; protected: - /// Master statistics + /// Portfolio statistics Statistics stat; - /// Slave engines - Slave** slaves; - /// Number of slave engines - unsigned int n_slaves; - /// Number of active slave engines + /// Variant engines + Variant** variants; + /// Number of variant engines + unsigned int n_variants; + /// Number of active variant engines unsigned int n_active; - /// Whether a slave has been stopped - std::atomic slave_stop; + /// Whether a variant has been stopped + std::atomic variant_stop; /// Shared stop flag std::atomic tostop; /// Collect solutions in this Collect solutions; /// Mutex for synchronization Support::Mutex m; - /// Number of busy slaves + /// Number of busy variants unsigned int n_busy; - /// Signal that number of busy slaves becomes zero + /// Signal that number of busy variants becomes zero Support::Event idle; - /// Process report from slave, return false if solution was ignored - bool report(Slave* slave, Space* s); + /// Process report from variant, return false if solution was ignored + bool report(Variant* variant, Space* s); /** * The key invariant of the engine is as follows: * - n_busy is always zero outside the next() function. * - that entails, that locking is only needed inside next(). - * - the slaves 0..n_active-1 still might not have exhausted their + * - the variants 0..n_active-1 still might not have exhausted their * search space. - * - the slaves n_active..n_slaves-1 have exhausted their search space. + * - the variants n_active..n_variants-1 have exhausted their search space. */ public: /// Initialize diff --git a/gecode/search/par/pbs.hpp b/gecode/search/par/pbs.hpp index 1fa593aead..5b4f1bca8a 100755 --- a/gecode/search/par/pbs.hpp +++ b/gecode/search/par/pbs.hpp @@ -41,8 +41,8 @@ namespace Gecode { namespace Search { namespace Par { template forceinline - Slave::Completion::Completion(void) { - // A slave has not been submitted yet. This initial signal lets the + Variant::Completion::Completion(void) { + // A variant has not been submitted yet. This initial signal lets the // first submission use the same consume-before-reuse handshake as all // subsequent submissions. done.signal(); @@ -50,13 +50,13 @@ namespace Gecode { namespace Search { namespace Par { template forceinline void - Slave::Completion::terminated(void) { + Variant::Completion::terminated(void) { done.signal(); } template forceinline void - Slave::Completion::wait(void) { + Variant::Completion::wait(void) { done.wait(); } @@ -65,7 +65,7 @@ namespace Gecode { namespace Search { namespace Par { CollectAll::CollectAll(void) : solutions(heap) {} forceinline bool - CollectAll::add(Space* s, Slave*) { + CollectAll::add(Space* s, Variant*) { solutions.push(s); return true; } @@ -79,7 +79,7 @@ namespace Gecode { namespace Search { namespace Par { return solutions.empty(); } forceinline Space* - CollectAll::get(Slave*&) { + CollectAll::get(Variant*&) { return solutions.pop(); } forceinline @@ -93,7 +93,7 @@ namespace Gecode { namespace Search { namespace Par { CollectBest::CollectBest(void) : b(nullptr), reporter(nullptr) {} forceinline bool - CollectBest::add(Space* s, Slave* r) { + CollectBest::add(Space* s, Variant* r) { if (b != nullptr) { b->constrain(*s); if (b->status() == SS_FAILED) { @@ -126,7 +126,7 @@ namespace Gecode { namespace Search { namespace Par { return reporter == nullptr; } forceinline Space* - CollectBest::get(Slave*& r) { + CollectBest::get(Variant*& r) { assert(!empty()); r = reporter; reporter = nullptr; @@ -150,36 +150,36 @@ namespace Gecode { namespace Search { namespace Par { template forceinline - Slave::Slave(PBS* m, Engine* s, Stop* so) - : Support::Runnable(false), master(m), slave(s), stop(so) {} + Variant::Variant(PBS* p, Engine* e, Stop* so) + : Support::Runnable(false), portfolio(p), engine(e), stop(so) {} template forceinline Statistics - Slave::statistics(void) const { - return slave->statistics(); + Variant::statistics(void) const { + return engine->statistics(); } template forceinline bool - Slave::stopped(void) const { - return slave->stopped(); + Variant::stopped(void) const { + return engine->stopped(); } template forceinline Support::Terminator* - Slave::terminator(void) const { + Variant::terminator(void) const { return const_cast(&completion); } template forceinline void - Slave::wait(void) { + Variant::wait(void) { completion.wait(); } template forceinline void - Slave::constrain(const Space& b) { - slave->constrain(b); + Variant::constrain(const Space& b) { + engine->constrain(b); } template - Slave::~Slave(void) { - delete slave; + Variant::~Variant(void) { + delete engine; delete stop; } @@ -189,12 +189,12 @@ namespace Gecode { namespace Search { namespace Par { forceinline PBS::PBS(Engine** engines, Stop** stops, unsigned int n, const Statistics& stat0) - : stat(stat0), slaves(heap.alloc*>(n)), - n_slaves(n), n_active(n), - slave_stop(false), tostop(false), n_busy(0) { - // Initialize slaves - for (unsigned int i=0U; i(this,engines[i],stops[i]); + : stat(stat0), variants(heap.alloc*>(n)), + n_variants(n), n_active(n), + variant_stop(false), tostop(false), n_busy(0) { + // Initialize variants + for (unsigned int i=0U; i(this,engines[i],stops[i]); static_cast(stops[i])->share(&tostop); } } @@ -202,25 +202,25 @@ namespace Gecode { namespace Search { namespace Par { template forceinline bool - PBS::report(Slave* slave, Space* s) { + PBS::report(Variant* variant, Space* s) { // If b is false the report should be repeated (solution was worse) bool b = true; m.acquire(); if (s != nullptr) { - b = solutions.add(s,slave); + b = solutions.add(s,variant); if (b) tostop.store(true, std::memory_order_release); - } else if (slave->stopped()) { + } else if (variant->stopped()) { if (!tostop.load(std::memory_order_acquire)) - slave_stop.store(true, std::memory_order_release); + variant_stop.store(true, std::memory_order_release); } else { - // Move slave to inactive, as it has exhausted its engine + // Move variant to inactive, as it has exhausted its engine unsigned int i=0; - while (slaves[i] != slave) + while (variants[i] != variant) i++; assert(i < n_active); assert(n_active > 0); - std::swap(slaves[i],slaves[--n_active]); + std::swap(variants[i],variants[--n_active]); tostop.store(true, std::memory_order_release); } if (b) { @@ -233,43 +233,43 @@ namespace Gecode { namespace Search { namespace Par { template void - Slave::run(void) { + Variant::run(void) { Space* s; do { - s = slave->next(); - } while (!master->report(this,s)); + s = engine->next(); + } while (!portfolio->report(this,s)); } template Space* PBS::next(void) { m.acquire(); - if (solutions.empty()) { - // Clear all + if (solutions.empty()) + variant_stop.store(false, std::memory_order_release); + while (solutions.empty() && (n_active > 0) && + !variant_stop.load(std::memory_order_acquire)) { + // Clear the internal stop used to interrupt sibling variants tostop.store(false, std::memory_order_release); - slave_stop.store(false, std::memory_order_release); - // Invariant: all slaves are idle! + // Invariant: all variants 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 variants + n_busy = n_active; + for (unsigned int i=0U; iwait(); + Support::Thread::run(variants[i]); } + m.release(); + // Wait for all variants to become idle + idle.wait(); + m.acquire(); } - // Invariant all slaves are idle! + // Invariant all variants are idle! assert(n_busy == 0); Space* s; @@ -278,12 +278,12 @@ namespace Gecode { namespace Search { namespace Par { if (solutions.empty()) { s = nullptr; } else { - Slave* r; + Variant* r; s = solutions.get(r); if (Collect::best) for (unsigned int i=0U; iconstrain(*s); + if (variants[i] != r) + variants[i]->constrain(*s); } m.release(); @@ -293,7 +293,7 @@ namespace Gecode { namespace Search { namespace Par { template bool PBS::stopped(void) const { - return slave_stop.load(std::memory_order_acquire); + return variant_stop.load(std::memory_order_acquire); } template @@ -301,8 +301,8 @@ namespace Gecode { namespace Search { namespace Par { PBS::statistics(void) const { assert(n_busy == 0); Statistics s(stat); - for (unsigned int i=0U; istatistics(); + for (unsigned int i=0U; istatistics(); return s; } @@ -315,21 +315,21 @@ namespace Gecode { namespace Search { namespace Par { if (solutions.constrain(b)) { // The solution is better for (unsigned int i=0U; iconstrain(b); + variants[i]->constrain(b); } } template PBS::~PBS(void) { - // A report can make n_busy zero before Slave::run and Thread::Run::exec + // A report can make n_busy zero before Variant::run and Thread::Run::exec // have returned. Wait for the completion handshake before deleting the - // PBS-owned slaves. - for (unsigned int i=0U; iwait(); + // PBS-owned variants. + for (unsigned int i=0U; iwait(); assert(n_busy == 0); - for (unsigned int i=0U; i*>(slaves,n_slaves); + for (unsigned int i=0U; i*>(variants,n_variants); } }}} diff --git a/gecode/search/pbs.cpp b/gecode/search/pbs.cpp index 701a74b79e..dd7a11c6ec 100644 --- a/gecode/search/pbs.cpp +++ b/gecode/search/pbs.cpp @@ -41,12 +41,12 @@ namespace Gecode { namespace Search { namespace Seq { } Engine* - pbsengine(Engine** slaves, Stop** stops, unsigned int n_slaves, + pbsengine(Engine** variants, Stop** stops, unsigned int n_variants, const Statistics& stat, const Search::Options& opt, bool best) { if (best) - return new PBS(slaves,stops,n_slaves,stat,opt); + return new PBS(variants,stops,n_variants,stat,opt); else - return new PBS(slaves,stops,n_slaves,stat,opt); + return new PBS(variants,stops,n_variants,stat,opt); } }}} @@ -63,12 +63,12 @@ namespace Gecode { namespace Search { namespace Par { } Engine* - pbsengine(Engine** slaves, Stop** stops, unsigned int n_slaves, + pbsengine(Engine** variants, Stop** stops, unsigned int n_variants, const Statistics& stat, bool best) { if (best) - return new PBS(slaves,stops,n_slaves,stat); + return new PBS(variants,stops,n_variants,stat); else - return new PBS(slaves,stops,n_slaves,stat); + return new PBS(variants,stops,n_variants,stat); } }}} diff --git a/gecode/search/pbs.hpp b/gecode/search/pbs.hpp index aa626cf750..bebca3fd2b 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** variants, Stop** stops, unsigned int n_variants, + Space* origin) { + 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 { @@ -70,7 +126,7 @@ namespace Gecode { namespace Search { namespace Seq { /// Create sequential portfolio engine GECODE_SEARCH_EXPORT Engine* - pbsengine(Engine** slaves, Stop** stops, unsigned int n_slaves, + pbsengine(Engine** variants, Stop** stops, unsigned int n_variants, const Statistics& stat, const Search::Options& opt, bool best); }}} @@ -83,7 +139,7 @@ namespace Gecode { namespace Search { namespace Par { /// Create parallel portfolio engine GECODE_SEARCH_EXPORT Engine* - pbsengine(Engine** slaves, Stop** stops, unsigned int n_slaves, + pbsengine(Engine** variants, Stop** stops, unsigned int n_variants, const Statistics& stat, bool best); }}} @@ -92,7 +148,7 @@ namespace Gecode { namespace Search { template class E> Engine* - pbsseq(T* master, const Search::Statistics& stat, Options& opt) { + pbsseq(T* origin, const Search::Statistics& stat, Options& opt) { Stop* stop = opt.stop; Region r; @@ -100,119 +156,199 @@ namespace Gecode { namespace Search { opt.threads = std::max(floor(opt.threads / static_cast(opt.assets)),1.0); - unsigned int n_slaves = opt.assets; - Engine** slaves = r.alloc(n_slaves); - Stop** stops = r.alloc(n_slaves); + unsigned int n_variants = opt.assets; + Engine** variants = r.alloc(n_variants); + Stop** stops = r.alloc(n_variants); + for (unsigned int i=0U; iclone(); - (void) slave->slave(i); - slaves[i] = build(slave,opt); + SearchTracer::EngineType::PBS, n_variants); + + Space* unowned_origin = origin; + try { + for (unsigned int i=0U; iclone(); + try { + (void) variant->variant(i); + variants[i] = build(variant,opt); + } catch (...) { + delete variant; + if (variant == origin) + unowned_origin = nullptr; + throw; + } + if (variant == origin) + unowned_origin = nullptr; + } + return Seq::pbsengine( + variants,stops,n_variants,stat,opt,E::best); + } catch (...) { + pbscleanup(variants,stops,n_variants,unowned_origin); + throw; } - - return Seq::pbsengine(slaves,stops,n_slaves,stat,opt,E::best); } template class E> Engine* - pbsseq(T* master, SEBs& sebs, + pbsseq(T* origin, SEBs& sebs, const Search::Statistics& stat, Options& opt, bool best) { Region r; - int n_slaves = sebs.size(); - Engine** slaves = r.alloc(n_slaves); - Stop** stops = r.alloc(n_slaves); + int n_variants = sebs.size(); + Engine** variants = r.alloc(n_variants); + Stop** stops = r.alloc(n_variants); + 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]; + static_cast(n_variants)); + + Space* unowned_origin = origin; + bool builders_owned = true; + try { + for (int i=0; ioptions().stop); + sebs[i]->options().stop = stops[i]; + sebs[i]->options().clone = false; + Space* variant = (i == n_variants-1) ? + origin : origin->clone(); + try { + (void) variant->variant(static_cast(i)); + variants[i] = (*sebs[i])(variant); + } catch (...) { + delete variant; + if (variant == origin) + unowned_origin = nullptr; + throw; + } + if (variant == origin) + unowned_origin = nullptr; + } + pbscleanup(sebs); + builders_owned = false; + return Seq::pbsengine( + variants,stops,static_cast(n_variants),stat,opt,best); + } catch (...) { + if (builders_owned) + pbscleanup(sebs); + pbscleanup(variants,stops,static_cast(n_variants), + unowned_origin); + throw; } - - return Seq::pbsengine(slaves,stops,static_cast(n_slaves), - stat,opt,best); } #ifdef GECODE_HAS_THREADS template class E> Engine* - pbspar(T* master, const Search::Statistics& stat, Options& opt) { + pbspar(T* origin, const Search::Statistics& stat, Options& opt) { Stop* stop = opt.stop; Region r; - // Limit the number of slaves to the number of threads - unsigned int n_slaves = std::min(static_cast(opt.threads), - opt.assets); - // Redistribute additional threads to slaves - opt.threads = floor(opt.threads / static_cast(n_slaves)); + // Limit the number of variants to the number of threads + unsigned int n_variants = std::min(static_cast(opt.threads), + opt.assets); + // Redistribute additional threads to variants + opt.threads = floor(opt.threads / static_cast(n_variants)); WrapTraceRecorder::engine(opt.tracer, - SearchTracer::EngineType::PBS, n_slaves); + SearchTracer::EngineType::PBS, n_variants); - 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); + Engine** variants = r.alloc(n_variants); + Stop** stops = r.alloc(n_variants); + for (unsigned int i=0U; i::best); + Space* unowned_origin = origin; + try { + for (unsigned int i=0U; iclone(); + try { + (void) variant->variant(static_cast(i)); + variants[i] = build(variant,opt); + } catch (...) { + delete variant; + if (variant == origin) + unowned_origin = nullptr; + throw; + } + if (variant == origin) + unowned_origin = nullptr; + } + return Par::pbsengine(variants,stops,n_variants,stat,E::best); + } catch (...) { + pbscleanup(variants,stops,n_variants,unowned_origin); + throw; + } } template class E> Engine* - pbspar(T* master, SEBs& sebs, + pbspar(T* origin, SEBs& sebs, const Search::Statistics& stat, Options& opt, bool best) { Region r; - // Limit the number of slaves to the number of threads - int n_slaves = std::min(static_cast(opt.threads), - sebs.size()); + // Limit the number of variants to the number of threads + int n_variants = std::min(static_cast(opt.threads), + sebs.size()); WrapTraceRecorder::engine(opt.tracer, SearchTracer::EngineType::PBS, - static_cast(n_slaves)); - - 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]; + static_cast(n_variants)); + + Engine** variants = r.alloc(n_variants); + Stop** stops = r.alloc(n_variants); + for (int i=0; i(n_slaves), - stat,best); + Space* unowned_origin = origin; + bool builders_owned = true; + try { + for (int i=0; ioptions().stop); + sebs[i]->options().stop = stops[i]; + sebs[i]->options().clone = false; + Space* variant = (i == n_variants-1) ? + origin : origin->clone(); + try { + (void) variant->variant(static_cast(i)); + variants[i] = (*sebs[i])(variant); + } catch (...) { + delete variant; + if (variant == origin) + unowned_origin = nullptr; + throw; + } + if (variant == origin) + unowned_origin = nullptr; + } + pbscleanup(sebs); + builders_owned = false; + return Par::pbsengine( + variants,stops,static_cast(n_variants),stat,best); + } catch (...) { + if (builders_owned) + pbscleanup(sebs); + pbscleanup(variants,stops,static_cast(n_variants), + unowned_origin); + throw; + } } #endif @@ -227,6 +363,7 @@ namespace Gecode { if (opt.assets == 0) throw Search::NoAssets("PBS::PBS"); + Search::pbscontrol(opt); Search::Statistics stat; @@ -239,31 +376,34 @@ namespace Gecode { } // Check whether a clone must be used - T* master = opt.clone ? + T* origin = opt.clone ? dynamic_cast(s->clone()) : s; opt.clone = false; - // Always execute master function - (void) master->master(0); + // Always execute origin function + (void) origin->origin(0); - // No need to create a portfolio engine but must run slave function + // No need to create a portfolio engine but must run variant function if (o.assets == 1) { - (void) master->slave(0); - e = Search::build(master,opt); + (void) origin->variant(0); + e = Search::build(origin,opt); return; } #ifdef GECODE_HAS_THREADS if (opt.threads > 1.0) - e = Search::pbspar(master,stat,opt); + e = Search::pbspar(origin,stat,opt); else #endif - e = Search::pbsseq(master,stat,opt); + e = Search::pbsseq(origin,stat,opt); } 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) { @@ -287,19 +426,19 @@ namespace Gecode { } // Check whether a clone must be used - T* master = opt.clone ? + T* origin = opt.clone ? dynamic_cast(s->clone()) : s; opt.clone = false; - // Always execute master function - (void) master->master(0); + // Always execute origin function + (void) origin->origin(0); #ifdef GECODE_HAS_THREADS if (opt.threads > 1.0) - e = Search::pbspar(master,sebs,stat,opt,best); + e = Search::pbspar(origin,sebs,stat,opt,best); else #endif - e = Search::pbsseq(master,sebs,stat,opt,best); + e = Search::pbsseq(origin,sebs,stat,opt,best); } template class E> @@ -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/rbs.cpp b/gecode/search/rbs.cpp index 5eaaae30f0..b4c578e30e 100644 --- a/gecode/search/rbs.cpp +++ b/gecode/search/rbs.cpp @@ -41,9 +41,9 @@ namespace Gecode { namespace Search { namespace Seq { } Engine* - rbsengine(Space* master, Stop* stop, Engine* slave, + rbsengine(Space* origin, Stop* stop, Engine* variant, const Search::Statistics& stat, const Options& opt, bool best) { - return new RBS(master,static_cast(stop), slave, + return new RBS(origin,static_cast(stop), variant, stat,opt,best); } diff --git a/gecode/search/rbs.hpp b/gecode/search/rbs.hpp index 3c47f01ae4..237f1b78b7 100644 --- a/gecode/search/rbs.hpp +++ b/gecode/search/rbs.hpp @@ -44,7 +44,7 @@ namespace Gecode { namespace Search { namespace Seq { /// Create restart engine GECODE_SEARCH_EXPORT Engine* - rbsengine(Space* master, Stop* stop, Engine* slave, + rbsengine(Space* origin, Stop* stop, Engine* variant, const Search::Statistics& stat, const Options& opt, bool best); @@ -95,12 +95,12 @@ namespace Gecode { delete s; e = Search::Seq::dead(e_opt, stat); } else { - Space* master = m_opt.clone ? s->clone() : s; - Space* slave = master->clone(); + Space* origin = m_opt.clone ? s->clone() : s; + Space* variant = origin->clone(); MetaInfo mi(0,MetaInfo::RR_INIT,0,0,nullptr,NoGoods::eng); - slave->slave(mi); - e = Search::Seq::rbsengine(master,e_opt.stop, - Search::build(slave,e_opt), + variant->variant(mi); + e = Search::Seq::rbsengine(origin,e_opt.stop, + Search::build(variant,e_opt), stat,m_opt,E::best); } } diff --git a/gecode/search/seq/pbs.hh b/gecode/search/seq/pbs.hh index 6ed977b04a..79b420c4d1 100644 --- a/gecode/search/seq/pbs.hh +++ b/gecode/search/seq/pbs.hh @@ -47,10 +47,10 @@ namespace Gecode { namespace Search { namespace Seq { unsigned long long int l; }; - /// Stop object used for controlling slaves in a portfolio + /// Stop object used for controlling variants in a portfolio class GECODE_SEARCH_EXPORT PortfolioStop : public Stop { private: - /// The stop object for the slaves + /// The stop object for the variants Stop* so; /// Pointer to shared stop information SharedStopInfo* ssi; @@ -63,57 +63,57 @@ namespace Gecode { namespace Search { namespace Seq { virtual bool stop(const Statistics& s, const Options& o); }; - /// Runnable slave of a portfolio master - class Slave { + /// Variant engine in a sequential portfolio + class Variant { protected: - /// The slave engine - Engine* slave; + /// The wrapped search engine + Engine* engine; /// Stop object Stop* stop; public: /// Default constructor - Slave(void); + Variant(void); /// Copy constructor - Slave(const Slave& s) = default; + Variant(const Variant& s) = default; /// Assignment operator - Slave& operator =(const Slave& s) = default; - /// Initialize with slave \a s and its stop object \a so + Variant& operator =(const Variant& s) = default; + /// Initialize with engine \a s and its stop object \a so void init(Engine* s, Stop* so); /// Return next solution Space* next(void); - /// Return statistics of slave + /// Return statistics of the variant Statistics statistics(void) const; - /// Check whether slave has been stopped + /// Check whether the variant has been stopped bool stopped(void) const; /// Constrain with better solution \a b void constrain(const Space& b); /// Perform one run void run(void); - /// Delete slave - ~Slave(void); + /// Delete variant + ~Variant(void); }; /// Sequential portfolio engine implementation template class GECODE_SEARCH_EXPORT PBS : public Engine { protected: - /// Master statistics + /// Initial statistics Statistics stat; - /// Shared slave information + /// Shared variant information SharedStopInfo ssi; /// Size of a slice unsigned int slice; - /// Slaves - Slave* slaves; - /// Number of slave engines - unsigned int n_slaves; - /// Current slave to run + /// Variants + Variant* variants; + /// Number of variant engines + unsigned int n_variants; + /// Current variant to run unsigned int cur; - /// Whether a slave has been stopped - bool slave_stop; + /// Whether a variant has been stopped + bool variant_stop; public: /// Initialize - PBS(Engine** slaves, Stop** stops, unsigned int n, + PBS(Engine** variants, Stop** stops, unsigned int n, const Statistics& stat, const Search::Options& opt); /// Return next solution (nullptr, if none exists or search has been stopped) virtual Space* next(void); diff --git a/gecode/search/seq/pbs.hpp b/gecode/search/seq/pbs.hpp index 59d66f43cc..ead3edea30 100644 --- a/gecode/search/seq/pbs.hpp +++ b/gecode/search/seq/pbs.hpp @@ -46,31 +46,31 @@ namespace Gecode { namespace Search { namespace Seq { forceinline - Slave::Slave(void) - : slave(nullptr), stop(nullptr) {} + Variant::Variant(void) + : engine(nullptr), stop(nullptr) {} forceinline void - Slave::init(Engine* e, Stop* s) { - slave = e; stop = s; + Variant::init(Engine* e, Stop* s) { + engine = e; stop = s; } forceinline Space* - Slave::next(void) { - return slave->next(); + Variant::next(void) { + return engine->next(); } forceinline Statistics - Slave::statistics(void) const { - return slave->statistics(); + Variant::statistics(void) const { + return engine->statistics(); } forceinline bool - Slave::stopped(void) const { - return slave->stopped(); + Variant::stopped(void) const { + return engine->stopped(); } forceinline void - Slave::constrain(const Space& b) { - slave->constrain(b); + Variant::constrain(const Space& b) { + engine->constrain(b); } forceinline - Slave::~Slave(void) { - delete slave; + Variant::~Variant(void) { + delete engine; delete stop; } @@ -81,13 +81,13 @@ namespace Gecode { namespace Search { namespace Seq { const Statistics& stat0, const Search::Options& opt) : stat(stat0), slice(opt.slice), - slaves(heap.alloc(n)), n_slaves(n), cur(0), - slave_stop(false) { + variants(heap.alloc(n)), n_variants(n), cur(0), + variant_stop(false) { ssi.done = false; ssi.l = opt.slice; for (unsigned int i=0U; i(s[i])); + variants[i].init(e[i],static_cast(s[i])); static_cast(s[i])->share(&ssi); } } @@ -95,41 +95,41 @@ namespace Gecode { namespace Search { namespace Seq { template Space* PBS::next(void) { - slave_stop = false; + variant_stop = false; unsigned int n_exhausted = 0; - while (n_slaves > 0) { - if (Space* s = slaves[cur].next()) { - // Constrain other slaves + while (n_variants > 0) { + if (Space* s = variants[cur].next()) { + // Constrain other variants if (best) { for (unsigned int i=0U; i bool PBS::stopped(void) const { - return slave_stop; + return variant_stop; } template Statistics PBS::statistics(void) const { Statistics s(stat); - for (unsigned int i=0U; i::constrain(const Space& b) { if (!best) throw NoBest("PBS::constrain"); - for (unsigned int i=0U; i PBS::~PBS(void) { - for (unsigned int i=0U; irestarts(),MetaInfo::RR_SOL,sslr,e->statistics().fail,last,ng); - bool r = master->master(mi); + bool r = origin->origin(mi); stop->nogood(ng.ng()); - if (stop->status(master) == SS_FAILED) { + if (stop->status(origin) == SS_FAILED) { stop->update(e->statistics()); - delete master; - master = nullptr; + delete origin; + origin = nullptr; e->reset(nullptr); return nullptr; } else if (r) { stop->update(e->statistics()); - Space* slave = master; - master = master->clone(); - complete = slave->slave(mi); - e->reset(slave); + Space* variant = origin; + origin = origin->clone(); + complete = variant->variant(mi); + e->reset(variant); sslr = 0; stop->restart(); } @@ -103,16 +103,16 @@ namespace Gecode { namespace Search { namespace Seq { NoGoods& ng = e->nogoods(); ng.ng(0); MetaInfo mi(stop->restarts(),e->stopped() ? MetaInfo::RR_LIM : MetaInfo::RR_CMPL,sslr,e->statistics().fail,last,ng); - (void) master->master(mi); + (void) origin->origin(mi); stop->nogood(ng.ng()); unsigned long long int nl = ++(*co); stop->limit(e->statistics(),nl); - if (stop->status(master) == SS_FAILED) + if (stop->status(origin) == SS_FAILED) return nullptr; - Space* slave = master; - master = master->clone(); - complete = slave->slave(mi); - e->reset(slave); + Space* variant = origin; + origin = origin->clone(); + complete = variant->variant(mi); + e->reset(variant); } else { return nullptr; } @@ -139,7 +139,7 @@ namespace Gecode { namespace Search { namespace Seq { } } last = b.clone(); - master->constrain(b); + origin->constrain(b); e->constrain(b); } @@ -157,7 +157,7 @@ namespace Gecode { namespace Search { namespace Seq { RBS::~RBS(void) { delete e; - delete master; + delete origin; delete last; delete co; delete stop; diff --git a/gecode/search/seq/rbs.hh b/gecode/search/seq/rbs.hh index 6b506200de..385b3b0259 100755 --- a/gecode/search/seq/rbs.hh +++ b/gecode/search/seq/rbs.hh @@ -70,7 +70,7 @@ namespace Gecode { namespace Search { namespace Seq { void restart(void); /// Add no-goods to meta statistics void nogood(unsigned long int n); - /// Test master status with meta statistics + /// Test origin status with meta statistics SpaceStatus status(Space* s); /// Set current limit for the engine to \a l fails void limit(const Statistics& s, unsigned long long int l); @@ -87,8 +87,8 @@ namespace Gecode { namespace Search { namespace Seq { protected: /// The actual engine Engine* e; - /// The master space to restart from - Space* master; + /// The origin space to restart from + Space* origin; /// The last solution space (possibly nullptr) Space* last; /// The cutoff object diff --git a/gecode/search/seq/rbs.hpp b/gecode/search/seq/rbs.hpp index c55e4e28b5..5581b0c0a1 100755 --- a/gecode/search/seq/rbs.hpp +++ b/gecode/search/seq/rbs.hpp @@ -97,7 +97,7 @@ namespace Gecode { namespace Search { namespace Seq { RBS::RBS(Space* s, RestartStop* stop0, Engine* e0, const Search::Statistics& stat, const Options& opt, bool best0) - : e(e0), master(s), last(nullptr), co(opt.cutoff), stop(stop0), + : e(e0), origin(s), last(nullptr), co(opt.cutoff), stop(stop0), sslr(0), complete(true), restart(false), best(best0) { stop->limit(stat,(*co)()); 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..3cc87c8bda --- /dev/null +++ b/gecode/search/worker-control.cpp @@ -0,0 +1,847 @@ +/* -*- 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 +#include + +namespace Gecode { namespace Search { + + class WorkerControl::State : public Support::RefCount { + public: + enum Lifecycle { + NEVER_BOUND, + ATTACHED, + 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) {} + ~State(void) { + delete [] events; + delete test_support.load(std::memory_order_relaxed); + } + }; + + 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"); + + 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->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; + } + } + if (changed) + WorkerControlAccess::signal_all(*this); + } + + 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->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; + } + + 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; + } + signal_all(control); + } + + unsigned long long int + WorkerControlAccess::generation(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->generation.load(std::memory_order_acquire); + } + + bool + WorkerControlAccess::engaged(const WorkerControl& control) { + 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 : + &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) { + return x.state == y.state; + } + + 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::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); + } + +}} diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh new file mode 100644 index 0000000000..560b17dda9 --- /dev/null +++ b/gecode/search/worker-control.hh @@ -0,0 +1,169 @@ +/* -*- 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 + +namespace Gecode { namespace Search { + + /// Private access to worker-control attachment state + class GECODE_SEARCH_EXPORT WorkerControlAccess { + public: + /// Test boundary in adjustable parallel scheduling + enum Gate { + GATE_ADMISSION, + GATE_EVENT_WAIT, + GATE_FAILED_SCAN, + GATE_ACTION_BEGIN, + 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 + 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 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); + /// 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 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); + }; + +}} + +#endif diff --git a/misc/benchmark-worker-control.py b/misc/benchmark-worker-control.py new file mode 100755 index 0000000000..0e7dda86c6 --- /dev/null +++ b/misc/benchmark-worker-control.py @@ -0,0 +1,994 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "matplotlib>=3.8", +# ] +# /// + +"""Benchmark Gecode's externally adjustable parallel-search workers.""" + +from __future__ import annotations + +import argparse +import dataclasses +import datetime as dt +import json +import math +import os +import pathlib +import platform +import random +import re +import shutil +import socket +import statistics +import subprocess +import sys +import time +from collections import Counter, defaultdict +from typing import Any, Iterable, Sequence + + +SCRIPT_PATH = pathlib.Path(__file__).resolve() +DEFAULT_REPO_ROOT = SCRIPT_PATH.parent.parent +TERMINAL_STATUSES = {"ok", "error", "timeout"} +OVERHEAD_MEDIAN_GATE_PCT = 3.0 +OVERHEAD_CI_GATE_PCT = 5.0 +BOOTSTRAP_SEED = 20260724 +BOOTSTRAP_SAMPLES = 10_000 +CASE_GROUPS = ("overhead", "latency", "parked", "portfolio") + + +class BenchmarkError(RuntimeError): + pass + + +@dataclasses.dataclass(frozen=True) +class Layout: + root: pathlib.Path + runs: pathlib.Path + analysis: pathlib.Path + reports: pathlib.Path + plots: pathlib.Path + + +@dataclasses.dataclass(frozen=True) +class Case: + run_id: str + command: list[str] + dimensions: dict[str, Any] + timeout_sec: int + + +@dataclasses.dataclass(frozen=True) +class TimeStrategy: + name: str + prefix: list[str] + + +def utc_now() -> str: + return ( + dt.datetime.now(dt.timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) + + +def write_json(path: pathlib.Path, payload: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + + +def read_json(path: pathlib.Path) -> Any: + return json.loads(path.read_text()) + + +def maybe_json(path: pathlib.Path) -> Any | None: + try: + return read_json(path) + except (FileNotFoundError, json.JSONDecodeError, OSError): + return None + + +def repo_root(args: argparse.Namespace) -> pathlib.Path: + raw = getattr(args, "repo_root", None) + return pathlib.Path(raw).expanduser().resolve() if raw else DEFAULT_REPO_ROOT + + +def results_root(args: argparse.Namespace, root: pathlib.Path) -> pathlib.Path: + raw = getattr(args, "results_root", None) + return pathlib.Path(raw).expanduser().resolve() if raw else root / "results" + + +def layout(args: argparse.Namespace) -> Layout: + name = args.name.strip() + if not name: + raise BenchmarkError("--name must not be empty") + root = results_root(args, repo_root(args)) / name + result = Layout( + root=root, + runs=root / "runs", + analysis=root / "analysis", + reports=root / "reports", + plots=root / "plots", + ) + for path in dataclasses.astuple(result): + pathlib.Path(path).mkdir(parents=True, exist_ok=True) + return result + + +def benchmark_binary(args: argparse.Namespace) -> pathlib.Path: + build = pathlib.Path(args.build_dir).expanduser().resolve() + candidates = [ + build / "bin" / "gecode-worker-control-benchmark", + build / "gecode-worker-control-benchmark", + ] + for candidate in candidates: + if candidate.is_file() and os.access(candidate, os.X_OK): + return candidate + raise BenchmarkError( + "gecode-worker-control-benchmark not found; build the CMake target " + f"in {build}" + ) + + +def select_time_strategy() -> TimeStrategy: + gtime = shutil.which("gtime") + if gtime: + return TimeStrategy("gnu-time-v", [gtime, "-v"]) + system_time = pathlib.Path("/usr/bin/time") + if system_time.exists(): + probe = subprocess.run( + [str(system_time), "-l", "/usr/bin/true"], + capture_output=True, + text=True, + check=False, + ) + if probe.returncode == 0: + return TimeStrategy("bsd-time-l", [str(system_time), "-l"]) + return TimeStrategy("gnu-time-v", [str(system_time), "-v"]) + return TimeStrategy("python-wall", []) + + +def parse_time(stderr: str, strategy: TimeStrategy) -> dict[str, Any]: + peak_kib: int | None = None + reported_sec: float | None = None + gnu_rss = re.search(r"Maximum resident set size \(kbytes\):\s*(\d+)", stderr) + if gnu_rss: + peak_kib = int(gnu_rss.group(1)) + bsd_rss = re.search( + r"^\s*(\d+)\s+maximum resident set size", stderr, re.MULTILINE + ) + if bsd_rss: + peak_kib = int(bsd_rss.group(1)) // 1024 + bsd_real = re.search(r"^\s*([0-9.]+)\s+real\b", stderr, re.MULTILINE) + if bsd_real: + reported_sec = float(bsd_real.group(1)) + return { + "time_strategy": strategy.name, + "reported_tool_sec": reported_sec, + "peak_memory_kib": peak_kib, + } + + +def case_selected(args: argparse.Namespace, group: str) -> bool: + selected = getattr(args, "cases", None) + return not selected or group in selected + + +def make_case( + binary: pathlib.Path, + run_id: str, + benchmark_case: str, + variant: str, + sample: int, + group: str, + timeout_sec: int, + extra: Sequence[str] = (), +) -> Case: + command = [ + str(binary), + "--case", + benchmark_case, + "--threads", + "4", + *extra, + ] + if variant: + command += ["--variant", variant] + return Case( + run_id=run_id, + command=command, + dimensions={ + "group": group, + "case": benchmark_case, + "variant": variant, + "sample": sample, + }, + timeout_sec=timeout_sec, + ) + + +def build_cases(args: argparse.Namespace, binary: pathlib.Path) -> list[Case]: + cases: list[Case] = [] + timeout = args.timeout_sec + if case_selected(args, "overhead"): + for sample in range(args.overhead_samples): + order = ("baseline", "control") if sample % 2 == 0 else ( + "control", + "baseline", + ) + for workload, iterations in (("overhead-dfs", 8), ("overhead-bab", 8)): + for variant in order: + cases.append( + make_case( + binary, + f"{workload}-{sample:03d}-{variant}", + workload, + variant, + sample, + "overhead", + timeout, + ("--iterations", str(iterations)), + ) + ) + if case_selected(args, "latency"): + latency_cases = ( + "latency-cheap-shrink", + "latency-cheap-grow", + "latency-expensive-shrink", + "latency-expensive-grow", + ) + for sample in range(args.latency_samples): + for benchmark_case in latency_cases: + variant = benchmark_case.rsplit("-", 1)[-1] + cases.append( + make_case( + binary, + f"{benchmark_case}-{sample:03d}", + benchmark_case, + variant, + sample, + "latency", + timeout, + ) + ) + if case_selected(args, "parked"): + for sample in range(args.parked_samples): + order = ("one-worker", "parked") if sample % 2 == 0 else ( + "parked", + "one-worker", + ) + for variant in order: + cases.append( + make_case( + binary, + f"parked-cost-{sample:03d}-{variant}", + "parked-cost", + variant, + sample, + "parked", + timeout, + ("--duration-ms", str(args.parked_duration_ms)), + ) + ) + if case_selected(args, "portfolio"): + for sample in range(args.portfolio_samples): + cases.append( + make_case( + binary, + f"portfolio-{sample:03d}", + "portfolio", + "reallocate", + sample, + "portfolio", + timeout, + ) + ) + if args.limit is not None: + cases = cases[: max(0, args.limit)] + return cases + + +def complete_result(path: pathlib.Path) -> bool: + payload = maybe_json(path) + return isinstance(payload, dict) and payload.get("status") in TERMINAL_STATUSES + + +def parse_helper_stdout(stdout: str) -> dict[str, Any]: + for line in reversed(stdout.splitlines()): + line = line.strip() + if not line: + continue + try: + payload = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(payload, dict): + return payload + raise BenchmarkError("benchmark helper produced no JSON object") + + +def execute(case: Case, paths: Layout, strategy: TimeStrategy) -> dict[str, Any]: + stdout_path = paths.runs / f"{case.run_id}.stdout" + stderr_path = paths.runs / f"{case.run_id}.stderr" + json_path = paths.runs / f"{case.run_id}.json" + start = time.monotonic() + started = utc_now() + timed_out = False + try: + proc = subprocess.run( + [*strategy.prefix, *case.command], + capture_output=True, + text=True, + timeout=case.timeout_sec, + check=False, + ) + stdout = proc.stdout or "" + stderr = proc.stderr or "" + return_code = proc.returncode + except subprocess.TimeoutExpired as exc: + timed_out = True + stdout = ( + exc.stdout.decode(errors="replace") + if isinstance(exc.stdout, bytes) + else (exc.stdout or "") + ) + stderr = ( + exc.stderr.decode(errors="replace") + if isinstance(exc.stderr, bytes) + else (exc.stderr or "") + ) + stderr += f"\nTIMEOUT after {case.timeout_sec}s\n" + return_code = -1 + elapsed = time.monotonic() - start + stdout_path.write_text(stdout) + stderr_path.write_text(stderr) + metrics: dict[str, Any] | None = None + parse_error: str | None = None + if not timed_out: + try: + metrics = parse_helper_stdout(stdout) + except BenchmarkError as error: + parse_error = str(error) + timing = parse_time(stderr, strategy) + status = "timeout" if timed_out else "ok" + if not timed_out and ( + return_code != 0 + or metrics is None + or metrics.get("ok") is not True + ): + status = "error" + result = { + "run_id": case.run_id, + "dimensions": case.dimensions, + "command": case.command, + "status": status, + "return_code": return_code, + "timed_out": timed_out, + "parse_error": parse_error, + "elapsed_wall_sec": elapsed, + "reported_tool_sec": timing["reported_tool_sec"], + "peak_memory_kib": timing["peak_memory_kib"], + "time_strategy": timing["time_strategy"], + "metrics": metrics, + "stdout_path": str(stdout_path), + "stderr_path": str(stderr_path), + "started_at_utc": started, + "completed_at_utc": utc_now(), + } + write_json(json_path, result) + return result + + +def git_value(root: pathlib.Path, *args: str) -> str | None: + proc = subprocess.run( + ["git", *args], cwd=root, capture_output=True, text=True, check=False + ) + return proc.stdout.strip() if proc.returncode == 0 else None + + +def provenance(root: pathlib.Path, strategy: TimeStrategy) -> dict[str, Any]: + compiler = subprocess.run( + ["c++", "--version"], capture_output=True, text=True, check=False + ) + return { + "generated_at_utc": utc_now(), + "git_commit": git_value(root, "rev-parse", "HEAD"), + "git_status": git_value(root, "status", "--short"), + "platform": platform.platform(), + "hostname": socket.gethostname(), + "python": platform.python_version(), + "compiler": (compiler.stdout or compiler.stderr).splitlines()[0], + "time_strategy": strategy.name, + "bootstrap_seed": BOOTSTRAP_SEED, + "bootstrap_samples": BOOTSTRAP_SAMPLES, + } + + +def run_command(args: argparse.Namespace) -> int: + root = repo_root(args) + paths = layout(args) + binary = benchmark_binary(args) + strategy = select_time_strategy() + cases = build_cases(args, binary) + plan = { + "name": args.name, + "generated_at_utc": utc_now(), + "repo_root": str(root), + "results_root": str(paths.root.parent), + "build_dir": str(pathlib.Path(args.build_dir).resolve()), + "binary": str(binary), + "time_strategy": dataclasses.asdict(strategy), + "options": { + "overhead_samples": args.overhead_samples, + "latency_samples": args.latency_samples, + "parked_samples": args.parked_samples, + "portfolio_samples": args.portfolio_samples, + "parked_duration_ms": args.parked_duration_ms, + "cases": args.cases, + "limit": args.limit, + "timeout_sec": args.timeout_sec, + }, + "cases": [dataclasses.asdict(case) for case in cases], + } + write_json(paths.analysis / "plan.json", plan) + write_json( + paths.analysis / "repo-context.json", + { + "project_type": "C++ constraint solver", + "benchmark_shape": [ + "paired DFS/BAB unchanged-limit overhead", + "cooperative shrink/grow response latency", + "resident parked-thread process cost", + "PBS asset budget reallocation", + ], + "report_targets": ["markdown", "latex", "png", "svg"], + }, + ) + write_json(paths.analysis / "provenance.json", provenance(root, strategy)) + print(f"run root: {paths.root}") + print(f"cases: {len(cases)}") + completed = 0 + skipped = 0 + for case in cases: + target = paths.runs / f"{case.run_id}.json" + if complete_result(target) and not args.force: + skipped += 1 + continue + if args.dry_run: + print("$", " ".join(case.command)) + completed += 1 + continue + result = execute(case, paths, strategy) + completed += 1 + marker = "+" if result["status"] == "ok" else "!" + print(f"{marker} {case.run_id}") + print(f"completed={completed} skipped={skipped}") + return 0 + + +def percentile(values: Sequence[float], pct: float) -> float | None: + if not values: + return None + ordered = sorted(values) + if len(ordered) == 1: + return ordered[0] + position = (len(ordered) - 1) * pct / 100.0 + lower = math.floor(position) + upper = math.ceil(position) + if lower == upper: + return ordered[lower] + fraction = position - lower + return ordered[lower] * (1.0 - fraction) + ordered[upper] * fraction + + +def stats(values: Sequence[float]) -> dict[str, Any]: + return { + "count": len(values), + "mean": statistics.fmean(values) if values else None, + "median": statistics.median(values) if values else None, + "p50": percentile(values, 50.0), + "p95": percentile(values, 95.0), + "min": min(values) if values else None, + "max": max(values) if values else None, + } + + +def bootstrap_median_ci(values: Sequence[float]) -> tuple[float | None, float | None]: + if not values: + return None, None + rng = random.Random(BOOTSTRAP_SEED) + n = len(values) + medians = [ + statistics.median(values[rng.randrange(n)] for _ in range(n)) + for _ in range(BOOTSTRAP_SAMPLES) + ] + return percentile(medians, 2.5), percentile(medians, 97.5) + + +def load_runs(paths: Layout) -> list[dict[str, Any]]: + result: list[dict[str, Any]] = [] + for path in sorted(paths.runs.glob("*.json")): + payload = maybe_json(path) + if isinstance(payload, dict): + result.append(payload) + return result + + +def metric(run: dict[str, Any], name: str) -> float | None: + metrics = run.get("metrics") + value = metrics.get(name) if isinstance(metrics, dict) else None + return float(value) if isinstance(value, (int, float)) else None + + +def paired_overhead( + runs: Iterable[dict[str, Any]], workload: str +) -> dict[str, Any]: + pairs: dict[int, dict[str, float]] = defaultdict(dict) + for run in runs: + dimensions = run.get("dimensions", {}) + if ( + run.get("status") != "ok" + or dimensions.get("case") != workload + ): + continue + elapsed = metric(run, "elapsed_ns") + if elapsed is not None: + pairs[int(dimensions["sample"])][str(dimensions["variant"])] = elapsed + overhead: list[float] = [] + for variants in pairs.values(): + baseline = variants.get("baseline") + control = variants.get("control") + if baseline and control: + overhead.append((control / baseline - 1.0) * 100.0) + low, high = bootstrap_median_ci(overhead) + median = statistics.median(overhead) if overhead else None + gate = bool( + median is not None + and high is not None + and median <= OVERHEAD_MEDIAN_GATE_PCT + and high <= OVERHEAD_CI_GATE_PCT + ) + return { + "workload": workload.removeprefix("overhead-"), + "pairs": len(overhead), + "overhead_pct": stats(overhead), + "bootstrap_95_ci_pct": [low, high], + "median_gate_pct": OVERHEAD_MEDIAN_GATE_PCT, + "ci_upper_gate_pct": OVERHEAD_CI_GATE_PCT, + "gate_pass": gate, + } + + +def analyze_command(args: argparse.Namespace) -> int: + paths = layout(args) + runs = load_runs(paths) + statuses = Counter(str(run.get("status", "unknown")) for run in runs) + failures = [ + { + "run_id": run.get("run_id"), + "status": run.get("status"), + "stderr_path": run.get("stderr_path"), + } + for run in runs + if run.get("status") != "ok" + ] + overhead = { + workload: paired_overhead(runs, f"overhead-{workload}") + for workload in ("dfs", "bab") + } + latency: dict[str, Any] = {} + for benchmark_case in ( + "latency-cheap-shrink", + "latency-cheap-grow", + "latency-expensive-shrink", + "latency-expensive-grow", + ): + values = [ + value / 1000.0 + for run in runs + if run.get("status") == "ok" + and run.get("dimensions", {}).get("case") == benchmark_case + and (value := metric(run, "latency_ns")) is not None + ] + latency[benchmark_case.removeprefix("latency-")] = stats(values) + + parked_pairs: dict[int, dict[str, dict[str, float]]] = defaultdict(dict) + for run in runs: + dimensions = run.get("dimensions", {}) + if ( + run.get("status") != "ok" + or dimensions.get("case") != "parked-cost" + ): + continue + sample = int(dimensions["sample"]) + variant = str(dimensions["variant"]) + parked_pairs[sample][variant] = { + "peak_memory_kib": float(run["peak_memory_kib"]) + if isinstance(run.get("peak_memory_kib"), (int, float)) + else math.nan, + "cpu_ratio": ( + metric(run, "cpu_ns") or 0.0 + ) + / max(metric(run, "elapsed_ns") or 1.0, 1.0), + } + memory_delta: list[float] = [] + one_cpu: list[float] = [] + parked_cpu: list[float] = [] + for pair in parked_pairs.values(): + one = pair.get("one-worker") + parked = pair.get("parked") + if not one or not parked: + continue + if not math.isnan(one["peak_memory_kib"]) and not math.isnan( + parked["peak_memory_kib"] + ): + memory_delta.append( + parked["peak_memory_kib"] - one["peak_memory_kib"] + ) + one_cpu.append(one["cpu_ratio"]) + parked_cpu.append(parked["cpu_ratio"]) + parked_summary = { + "pairs": sum( + 1 + for pair in parked_pairs.values() + if "one-worker" in pair and "parked" in pair + ), + "peak_memory_delta_kib": stats(memory_delta), + "one_worker_cpu_to_wall": stats(one_cpu), + "parked_cpu_to_wall": stats(parked_cpu), + "interpretation": ( + "Peak process RSS is a best-effort comparison, not an exact " + "per-thread stack-reservation measurement." + ), + } + portfolio_runs = [ + run + for run in runs + if run.get("status") == "ok" + and run.get("dimensions", {}).get("case") == "portfolio" + ] + portfolio_totals = [ + value + for run in portfolio_runs + if (value := metric(run, "max_total_admitted")) is not None + ] + portfolio = { + "samples": len(portfolio_runs), + "budget": 4, + "max_total_admitted": stats(portfolio_totals), + "bounded": bool(portfolio_totals) + and all(value <= 4.0 for value in portfolio_totals), + "allocations": [[3, 1], [1, 3], [2, 2]], + } + gate_pass = ( + not failures + and all(item["gate_pass"] for item in overhead.values()) + and portfolio["bounded"] + and all(item["count"] > 0 for item in latency.values()) + ) + summary = { + "name": args.name, + "generated_at_utc": utc_now(), + "total_runs": len(runs), + "status_counts": dict(statuses), + "failures": failures[:20], + "overhead": overhead, + "latency_us": latency, + "parked_cost": parked_summary, + "portfolio": portfolio, + "provisional_release_gate_pass": gate_pass, + "human_release_decision": "pending", + "gate": { + "median_overhead_pct": OVERHEAD_MEDIAN_GATE_PCT, + "bootstrap_95_ci_upper_pct": OVERHEAD_CI_GATE_PCT, + }, + "provenance": maybe_json(paths.analysis / "provenance.json"), + "plan": maybe_json(paths.analysis / "plan.json"), + } + write_json(paths.analysis / "summary.json", summary) + print(paths.analysis / "summary.json") + return 0 + + +def fmt(value: Any, digits: int = 3) -> str: + return "n/a" if value is None else f"{float(value):.{digits}f}" + + +def markdown_report(summary: dict[str, Any]) -> str: + gate = "PASS" if summary["provisional_release_gate_pass"] else "FAIL" + lines = [ + f"# Worker-control benchmark: {summary['name']}", + "", + f"- Generated: {summary['generated_at_utc']}", + f"- Samples: {summary['total_runs']}", + f"- Provisional release gate: **{gate}**", + f"- Human release decision: **{summary['human_release_decision']}**", + "", + "## Unchanged-limit overhead", + "", + "| Workload | Pairs | Median | Bootstrap 95% CI | Gate |", + "| --- | ---: | ---: | ---: | --- |", + ] + for workload in ("dfs", "bab"): + item = summary["overhead"][workload] + ci = item["bootstrap_95_ci_pct"] + lines.append( + f"| {workload.upper()} | {item['pairs']} | " + f"{fmt(item['overhead_pct']['median'])}% | " + f"[{fmt(ci[0])}%, {fmt(ci[1])}%] | " + f"{'pass' if item['gate_pass'] else 'fail'} |" + ) + lines += [ + "", + "The gate requires median paired overhead no greater than 3% and " + "a bootstrap 95% upper bound no greater than 5% for both workloads.", + "", + "## Cooperative resize latency", + "", + "| Workload and direction | Samples | P50 (µs) | P95 (µs) |", + "| --- | ---: | ---: | ---: |", + ] + for key, item in summary["latency_us"].items(): + lines.append( + f"| {key} | {item['count']} | {fmt(item['p50'])} | " + f"{fmt(item['p95'])} |" + ) + parked = summary["parked_cost"] + portfolio = summary["portfolio"] + lines += [ + "", + "## Parked-thread process cost", + "", + f"- Paired samples: {parked['pairs']}", + "- Median peak-RSS delta, four resident workers with one active " + f"versus one physical worker: " + f"{fmt(parked['peak_memory_delta_kib']['median'])} KiB.", + "- Median CPU/wall ratio, one physical worker: " + f"{fmt(parked['one_worker_cpu_to_wall']['median'])}.", + "- Median CPU/wall ratio, four resident workers with three parked: " + f"{fmt(parked['parked_cpu_to_wall']['median'])}.", + f"- Caveat: {parked['interpretation']}", + "", + "## Portfolio budget reallocation", + "", + f"- Samples: {portfolio['samples']}", + f"- Fixed active-worker budget: {portfolio['budget']}", + f"- Allocation phases: {portfolio['allocations']}", + "- Maximum summed admitted high-water mark: " + f"{fmt(portfolio['max_total_admitted']['max'], 0)}", + f"- Bound respected: **{'yes' if portfolio['bounded'] else 'no'}**", + "", + "This experiment demonstrates the control seam and the concurrency " + "bound. It does not claim that this allocation sequence is a " + "production policy or that it improves every portfolio.", + "", + "## Method and provenance", + "", + ] + provenance = summary.get("provenance") or {} + for key in ( + "git_commit", + "platform", + "hostname", + "compiler", + "python", + "time_strategy", + "bootstrap_seed", + "bootstrap_samples", + ): + lines.append(f"- {key}: {provenance.get(key)}") + lines += [ + "", + "Each overhead sample is paired by sample number; execution order " + "alternates baseline/control to reduce order bias. Timing comes from " + "the helper's in-process steady clock, excluding process startup. " + "Raw JSON, stdout, and stderr remain under the named result root.", + ] + return "\n".join(lines) + "\n" + + +def latex_report(summary: dict[str, Any]) -> str: + rows = [] + for workload in ("dfs", "bab"): + item = summary["overhead"][workload] + low, high = item["bootstrap_95_ci_pct"] + rows.append( + f"{workload.upper()} & {item['pairs']} & " + f"{fmt(item['overhead_pct']['median'])}\\% & " + f"[{fmt(low)}\\%, {fmt(high)}\\%] \\\\" + ) + return "\n".join( + [ + "% Generated by misc/benchmark-worker-control.py", + "\\begin{tabular}{lrrr}", + "\\hline", + "Workload & Pairs & Median overhead & Bootstrap 95\\% CI \\\\", + "\\hline", + *rows, + "\\hline", + "\\end{tabular}", + "", + ] + ) + + +def ensure_summary(args: argparse.Namespace, paths: Layout) -> dict[str, Any]: + summary = maybe_json(paths.analysis / "summary.json") + if not isinstance(summary, dict): + analyze_command(args) + summary = read_json(paths.analysis / "summary.json") + return summary + + +def report_command(args: argparse.Namespace) -> int: + paths = layout(args) + summary = ensure_summary(args, paths) + markdown = paths.reports / "report.md" + latex = paths.reports / "tables.tex" + markdown.write_text(markdown_report(summary)) + latex.write_text(latex_report(summary)) + print(markdown) + print(latex) + return 0 + + +def plot_command(args: argparse.Namespace) -> int: + import matplotlib.pyplot as plt + + paths = layout(args) + summary = ensure_summary(args, paths) + # Catppuccin Latte colors on a white, documentation-friendly background. + blue, mauve, green, red, text = ( + "#1e66f5", + "#8839ef", + "#40a02b", + "#d20f39", + "#4c4f69", + ) + plt.rcParams.update( + { + "figure.facecolor": "white", + "axes.facecolor": "white", + "axes.edgecolor": text, + "axes.labelcolor": text, + "text.color": text, + "xtick.color": text, + "ytick.color": text, + "font.size": 10, + } + ) + fig, axes = plt.subplots(1, 3, figsize=(13.5, 4.2)) + workloads = ["dfs", "bab"] + medians = [ + summary["overhead"][name]["overhead_pct"]["median"] + for name in workloads + ] + lows = [ + summary["overhead"][name]["bootstrap_95_ci_pct"][0] + for name in workloads + ] + highs = [ + summary["overhead"][name]["bootstrap_95_ci_pct"][1] + for name in workloads + ] + axes[0].bar( + [name.upper() for name in workloads], + medians, + color=[blue, mauve], + yerr=[ + [median - low for median, low in zip(medians, lows)], + [high - median for median, high in zip(medians, highs)], + ], + capsize=5, + ) + axes[0].axhline(OVERHEAD_MEDIAN_GATE_PCT, color=red, linestyle="--") + axes[0].set_title("Fixed-control overhead") + axes[0].set_ylabel("Paired overhead (%)") + + labels = list(summary["latency_us"]) + p50 = [summary["latency_us"][key]["p50"] for key in labels] + p95 = [summary["latency_us"][key]["p95"] for key in labels] + positions = range(len(labels)) + axes[1].bar( + [position - 0.18 for position in positions], + p50, + width=0.36, + label="p50", + color=green, + ) + axes[1].bar( + [position + 0.18 for position in positions], + p95, + width=0.36, + label="p95", + color=blue, + ) + axes[1].set_xticks(list(positions)) + axes[1].set_xticklabels( + [label.replace("expensive", "prop").replace("-", "\n") for label in labels] + ) + axes[1].set_title("Cooperative resize latency") + axes[1].set_ylabel("Microseconds") + axes[1].legend(frameon=False) + + parked = summary["parked_cost"] + cpu = [ + parked["one_worker_cpu_to_wall"]["median"], + parked["parked_cpu_to_wall"]["median"], + ] + axes[2].bar(["1 resident", "4 resident\n1 active"], cpu, color=[blue, mauve]) + axes[2].set_title("Search CPU / wall") + axes[2].set_ylabel("Ratio") + axes[2].text( + 0.5, + 0.95, + "Portfolio bound: " + + ("respected" if summary["portfolio"]["bounded"] else "violated"), + transform=axes[2].transAxes, + ha="center", + va="top", + ) + fig.suptitle("Gecode adjustable-worker benchmark") + fig.tight_layout() + png = paths.plots / "worker-control-benchmark.png" + svg = paths.plots / "worker-control-benchmark.svg" + fig.savefig(png, dpi=180) + fig.savefig(svg) + plt.close(fig) + print(png) + print(svg) + return 0 + + +def add_paths(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--repo-root") + parser.add_argument("--results-root") + + +def parser() -> argparse.ArgumentParser: + root = argparse.ArgumentParser( + description="Benchmark externally adjustable Gecode search workers" + ) + commands = root.add_subparsers(dest="command", required=True) + run = commands.add_parser("run") + run.add_argument("--name", required=True) + add_paths(run) + run.add_argument("--build-dir", required=True) + run.add_argument("--force", action="store_true") + run.add_argument("--dry-run", action="store_true") + run.add_argument("--limit", type=int) + run.add_argument("--cases", nargs="+", choices=CASE_GROUPS) + run.add_argument("--overhead-samples", type=int, default=30) + run.add_argument("--latency-samples", type=int, default=100) + run.add_argument("--parked-samples", type=int, default=20) + run.add_argument("--portfolio-samples", type=int, default=30) + run.add_argument("--parked-duration-ms", type=int, default=200) + run.add_argument("--timeout-sec", type=int, default=30) + run.set_defaults(func=run_command) + for name, function in ( + ("analyze", analyze_command), + ("report", report_command), + ("plot", plot_command), + ): + command = commands.add_parser(name) + command.add_argument("--name", required=True) + add_paths(command) + command.set_defaults(func=function) + return root + + +def main(argv: Sequence[str] | None = None) -> int: + args = parser().parse_args(argv) + try: + return args.func(args) + except BenchmarkError as error: + print(f"error: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/benchmark-worker-control.cpp b/test/benchmark-worker-control.cpp new file mode 100644 index 0000000000..584333425a --- /dev/null +++ b/test/benchmark-worker-control.cpp @@ -0,0 +1,641 @@ +/* -*- 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + + using Clock = std::chrono::steady_clock; + using Gecode::BoolVarArray; + using Gecode::IntArgs; + using Gecode::IntVar; + using Gecode::IntVarArray; + using Gecode::MetaInfo; + using Gecode::Space; + using Gecode::Search::WorkerControl; + using Gecode::Search::WorkerControlAccess; + + class EnumerationSpace : public Space { + public: + BoolVarArray x; + + explicit EnumerationSpace(unsigned int bits) + : x(*this,static_cast(bits),0,1) { + Gecode::branch(*this,x,Gecode::BOOL_VAR_NONE(), + Gecode::BOOL_VAL_MIN()); + } + + EnumerationSpace(EnumerationSpace& s) + : Space(s) { + x.update(*this,s.x); + } + + Space* copy(void) override { + return new EnumerationSpace(*this); + } + }; + + class OptimizationSpace : public Space { + public: + BoolVarArray x; + IntVar cost; + + explicit OptimizationSpace(unsigned int bits) + : x(*this,static_cast(bits),0,1), + cost(*this,0,static_cast(bits)) { + Gecode::linear(*this,x,Gecode::IRT_EQ,cost); + Gecode::branch(*this,x,Gecode::BOOL_VAR_NONE(), + Gecode::BOOL_VAL_MAX()); + } + + OptimizationSpace(OptimizationSpace& s) + : Space(s) { + x.update(*this,s.x); + cost.update(*this,s.cost); + } + + Space* copy(void) override { + return new OptimizationSpace(*this); + } + + void constrain(const Space& _s) override { + const OptimizationSpace& s = + static_cast(_s); + Gecode::rel(*this,cost,Gecode::IRT_LE,s.cost.val()); + } + }; + + class PropagationSpace : public Space { + public: + IntVarArray x; + + explicit PropagationSpace(unsigned int size) + : x(*this,static_cast(size),0,static_cast(size)-1) { + Gecode::distinct(*this,x); + Gecode::branch(*this,x,Gecode::INT_VAR_SIZE_MIN(), + Gecode::INT_VAL_MIN()); + } + + PropagationSpace(PropagationSpace& s) + : Space(s) { + x.update(*this,s.x); + } + + Space* copy(void) override { + return new PropagationSpace(*this); + } + }; + + class AtomicStop : public Gecode::Search::Stop { + public: + std::atomic requested; + + AtomicStop(void) : requested(false) {} + + bool stop(const Gecode::Search::Statistics&, + const Gecode::Search::Options&) override { + return requested.load(std::memory_order_acquire); + } + }; + + class PortfolioSpace : public Space { + public: + IntVar x; + + PortfolioSpace(void) + : x(*this,0,65535) { + Gecode::branch(*this,x,Gecode::INT_VAL_MAX()); + } + + PortfolioSpace(PortfolioSpace& s) + : Space(s) { + x.update(*this,s.x); + } + + Space* copy(void) override { + return new PortfolioSpace(*this); + } + + bool origin(const MetaInfo&) override { + return false; + } + + bool variant(const MetaInfo& mi) override { + if (mi.type() == MetaInfo::PORTFOLIO) { + if (mi.asset() == 0U) + Gecode::rel(*this,x,Gecode::IRT_LQ,32767); + else + Gecode::rel(*this,x,Gecode::IRT_GR,32767); + } + return true; + } + }; + + struct Arguments { + std::string benchmark_case; + std::string variant; + unsigned int iterations = 1U; + unsigned int threads = 4U; + unsigned int duration_ms = 200U; + }; + + unsigned int + parse_unsigned(const char* value, const char* option) { + char* end = nullptr; + unsigned long parsed = std::strtoul(value,&end,10); + if ((end == value) || (*end != '\0') || (parsed == 0UL) || + (parsed > 100000000UL)) + throw std::runtime_error(std::string("invalid ")+option); + return static_cast(parsed); + } + + Arguments + parse_arguments(int argc, char* argv[]) { + Arguments args; + for (int i=1; i + Gecode::Search::Engine* + make_dfs_engine(Model* root, Gecode::Search::Options& options) { + Gecode::Search::Engine* engine; + try { + engine = Gecode::Search::dfsengine(root,options); + } catch (...) { + delete root; + throw; + } + delete root; + return engine; + } + + std::uint64_t + enumerate_dfs(unsigned int bits, const Gecode::Search::Options& source) { + Gecode::Search::Options options(source); + Gecode::Search::Engine* engine = make_enumeration_engine(bits,options); + std::uint64_t solutions = 0U; + while (Space* solution = engine->next()) { + solutions++; + delete solution; + } + bool ok = !engine->stopped(); + delete engine; + if (!ok) + throw std::runtime_error("DFS stopped"); + return solutions; + } + + int + optimize_bab(unsigned int bits, const Gecode::Search::Options& source) { + Gecode::Search::Options options(source); + OptimizationSpace* root = new OptimizationSpace(bits); + Gecode::Search::Engine* engine; + try { + engine = Gecode::Search::babengine(root,options); + } catch (...) { + delete root; + throw; + } + delete root; + int best = static_cast(bits)+1; + while (OptimizationSpace* solution = + static_cast(engine->next())) { + best = solution->cost.val(); + delete solution; + } + bool ok = !engine->stopped(); + delete engine; + if (!ok) + throw std::runtime_error("BAB stopped"); + return best; + } + + void + print_common(const std::string& benchmark_case, + const std::string& variant, std::uint64_t elapsed_ns, + bool ok) { + std::cout << "{\"case\":\"" << benchmark_case + << "\",\"variant\":\"" << variant + << "\",\"elapsed_ns\":" << elapsed_ns + << ",\"ok\":" << (ok ? "true" : "false"); + } + + int + run_overhead(const Arguments& args, bool bab) { + const unsigned int bits = bab ? 24U : 17U; + bool controlled = args.variant == "control"; + if (!controlled && (args.variant != "baseline")) + throw std::runtime_error("overhead variant must be baseline or control"); + auto options = [&] { + Gecode::Search::Options result; + result.threads = static_cast(args.threads); + if (controlled) + result.worker_control = WorkerControl(args.threads); + return result; + }; + + // Warm up allocator, worker creation, and dynamic loader paths. + { + Gecode::Search::Options warmup = options(); + if (bab) + (void) optimize_bab(bits,warmup); + else + (void) enumerate_dfs(bits,warmup); + } + + std::uint64_t solutions = 0U; + int best = static_cast(bits)+1; + Clock::time_point start = Clock::now(); + for (unsigned int i=0U; i( + std::chrono::duration_cast( + Clock::now()-start).count()); + std::uint64_t expected = + static_cast(args.iterations) << bits; + bool ok = bab ? (best == 0) : (solutions == expected); + print_common(args.benchmark_case,args.variant,elapsed_ns,ok); + std::cout << ",\"iterations\":" << args.iterations + << ",\"threads\":" << args.threads + << ",\"solutions\":" << solutions + << ",\"best\":" << best << "}\n"; + return ok ? 0 : 1; + } + + template + int + run_latency_model(const Arguments& args, bool grow, Model* root) { + unsigned int initial = grow ? 1U : args.threads; + unsigned int requested = grow ? args.threads : 1U; + WorkerControl control(initial); + AtomicStop stop; + Gecode::Search::Options options; + options.threads = static_cast(args.threads); + options.worker_control = control; + options.stop = &stop; + Gecode::Search::Engine* engine = make_dfs_engine(root,options); + std::atomic consumer_done(false); + std::thread consumer([&] { + while (Space* solution = engine->next()) + delete solution; + consumer_done.store(true,std::memory_order_release); + }); + + unsigned long long int initial_generation = + WorkerControlAccess::generation(control); + bool initial_ok = + wait_for(control,initial_generation,initial,std::chrono::seconds(5)); + Clock::time_point start = Clock::now(); + control.request(requested); + unsigned long long int generation = + WorkerControlAccess::generation(control); + bool converged = + wait_for(control,generation,requested,std::chrono::seconds(5)); + unsigned int converged_leases = WorkerControlAccess::leases(control); + std::uint64_t latency_ns = + static_cast( + std::chrono::duration_cast( + Clock::now()-start).count()); + + stop.requested.store(true,std::memory_order_release); + control.request(args.threads); + consumer.join(); + bool ok = initial_ok && converged && + consumer_done.load(std::memory_order_acquire) && engine->stopped(); + unsigned int final_leases = WorkerControlAccess::leases(control); + delete engine; + print_common(args.benchmark_case,grow ? "grow" : "shrink", + latency_ns,ok); + std::cout << ",\"latency_ns\":" << latency_ns + << ",\"initial_limit\":" << initial + << ",\"requested_limit\":" << requested + << ",\"generation\":" << generation + << ",\"converged_leases\":" << converged_leases + << ",\"shutdown_leases\":" << final_leases << "}\n"; + return ok ? 0 : 1; + } + + int + run_latency(const Arguments& args, bool expensive, bool grow) { + if (expensive) + return run_latency_model(args,grow,new PropagationSpace(12U)); + return run_latency_model(args,grow,new EnumerationSpace(28U)); + } + + int + run_parked(const Arguments& args) { + bool parked_variant = args.variant == "parked"; + if (!parked_variant && (args.variant != "one-worker")) + throw std::runtime_error( + "parked variant must be one-worker or parked"); + AtomicStop stop; + Gecode::Search::Options options; + options.threads = + static_cast(parked_variant ? args.threads : 1U); + WorkerControl control; + if (parked_variant) { + control = WorkerControl(1U); + options.worker_control = control; + } + options.stop = &stop; + Gecode::Search::Engine* engine = + make_enumeration_engine(28U,options); + std::thread consumer([&] { + while (Space* solution = engine->next()) + delete solution; + }); + bool converged = true; + if (parked_variant) { + unsigned long long int generation = + WorkerControlAccess::generation(control); + converged = + wait_for(control,generation,1U,std::chrono::seconds(5)); + } + std::clock_t cpu_start = std::clock(); + Clock::time_point wall_start = Clock::now(); + std::this_thread::sleep_for(std::chrono::milliseconds(args.duration_ms)); + std::uint64_t wall_ns = + static_cast( + std::chrono::duration_cast( + Clock::now()-wall_start).count()); + std::uint64_t cpu_ns = + static_cast( + (static_cast(std::clock()-cpu_start) / + static_cast(CLOCKS_PER_SEC)) * 1000000000.0L); + unsigned int observed_parked = + parked_variant ? WorkerControlAccess::parked(control) : 0U; + stop.requested.store(true,std::memory_order_release); + if (parked_variant) + control.request(args.threads); + consumer.join(); + bool ok = converged && engine->stopped(); + delete engine; + print_common(args.benchmark_case,args.variant,wall_ns,ok); + std::cout << ",\"cpu_ns\":" << cpu_ns + << ",\"duration_ms\":" << args.duration_ms + << ",\"capacity\":" << (parked_variant ? args.threads : 1U) + << ",\"active_limit\":1" + << ",\"observed_parked\":" << observed_parked << "}\n"; + return ok ? 0 : 1; + } + + bool + settled(const WorkerControl& control, unsigned long long int generation, + unsigned int leases) { + Clock::time_point deadline = Clock::now()+std::chrono::seconds(5); + do { + if ((WorkerControlAccess::observed_generation(control) == generation) && + (WorkerControlAccess::leases(control) == leases) && + (WorkerControlAccess::admitted(control) == 0U)) + return true; + std::this_thread::yield(); + } while (Clock::now() < deadline); + return false; + } + + template + bool + portfolio_leaf(Engine& engine, WorkerControl& control, + unsigned int request, unsigned long long int generation, + std::vector& results, + unsigned int& high_water) { + WorkerControlAccess::gate_install( + control,WorkerControlAccess::GATE_ACTION_BEGIN, + WorkerControlAccess::ALL_WORKERS,request); + WorkerControlAccess::reset_max_admitted(control,generation); + std::atomic ready(false); + std::atomic exhausted(false); + std::thread consumer([&] { + while (true) { + PortfolioSpace* result = engine.next(); + if (result == nullptr) { + exhausted.store(true,std::memory_order_release); + return; + } + results.push_back(result); + if (ready.load(std::memory_order_acquire)) + return; + } + }); + Clock::time_point deadline = Clock::now()+std::chrono::seconds(5); + unsigned int waiting = 0U; + do { + waiting = 0U; + for (unsigned int worker=0U; worker= request) + break; + std::this_thread::yield(); + } while (!exhausted.load(std::memory_order_acquire) && + (Clock::now() < deadline)); + high_water = WorkerControlAccess::max_admitted(control); + ready.store(true,std::memory_order_release); + WorkerControlAccess::gate_release_all( + control,WorkerControlAccess::GATE_ACTION_BEGIN); + consumer.join(); + return !exhausted.load(std::memory_order_acquire) && + (waiting >= request) && (high_water <= request) && + settled(control,generation,request) && + (WorkerControlAccess::completed_generation(control) >= generation); + } + + template + bool + portfolio_phase(Engine& engine, WorkerControl& first, + unsigned int first_request, WorkerControl& second, + unsigned int second_request, + std::vector& results, + unsigned int& total_high_water) { + first.request(first_request); + second.request(second_request); + unsigned long long int first_generation = + WorkerControlAccess::generation(first); + unsigned long long int second_generation = + WorkerControlAccess::generation(second); + unsigned int first_high = 0U, second_high = 0U; + bool first_ok = portfolio_leaf( + engine,first,first_request,first_generation,results,first_high); + bool second_ok = portfolio_leaf( + engine,second,second_request,second_generation,results,second_high); + total_high_water = first_high+second_high; + return first_ok && second_ok && + (WorkerControlAccess::observed_generation(first) == first_generation) && + (WorkerControlAccess::observed_generation(second) == + second_generation) && + (WorkerControlAccess::leases(first) == first_request) && + (WorkerControlAccess::leases(second) == second_request) && + (total_high_water <= first_request+second_request); + } + + int + run_portfolio(const Arguments& args) { + if (args.threads != 4U) + throw std::runtime_error("portfolio benchmark requires four threads"); + WorkerControl first(4U), second(4U); + 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; + Gecode::SEBs builders(2); + builders[0] = Gecode::dfs(child[0]); + builders[1] = Gecode::dfs(child[1]); + Gecode::Search::Options outer; + outer.threads = 2.0; + PortfolioSpace* root = new PortfolioSpace; + Gecode::PBS engine(root,builders,outer); + delete root; + + static const unsigned int allocation[3][2] = { + {3U,1U}, {1U,3U}, {2U,2U} + }; + std::vector results; + unsigned int maximum_total = 0U; + bool ok = true; + Clock::time_point start = Clock::now(); + for (unsigned int phase=0U; phase<3U; phase++) { + unsigned int total = 0U; + bool phase_ok = portfolio_phase( + engine,first,allocation[phase][0],second,allocation[phase][1], + results,total); + ok = ok && phase_ok && (total <= 4U); + if (maximum_total < total) + maximum_total = total; + } + std::uint64_t elapsed_ns = + static_cast( + std::chrono::duration_cast( + Clock::now()-start).count()); + for (PortfolioSpace* result : results) + delete result; + print_common(args.benchmark_case,"reallocate",elapsed_ns,ok); + std::cout << ",\"budget\":4,\"phases\":3" + << ",\"max_total_admitted\":" << maximum_total + << ",\"first_capacity\":" << first.capacity() + << ",\"second_capacity\":" << second.capacity() << "}\n"; + return ok ? 0 : 1; + } + +} + +int +main(int argc, char* argv[]) { + try { + Arguments args = parse_arguments(argc,argv); + if (args.benchmark_case == "overhead-dfs") + return run_overhead(args,false); + if (args.benchmark_case == "overhead-bab") + return run_overhead(args,true); + if (args.benchmark_case == "latency-cheap-shrink") + return run_latency(args,false,false); + if (args.benchmark_case == "latency-cheap-grow") + return run_latency(args,false,true); + if (args.benchmark_case == "latency-expensive-shrink") + return run_latency(args,true,false); + if (args.benchmark_case == "latency-expensive-grow") + return run_latency(args,true,true); + if (args.benchmark_case == "parked-cost") + return run_parked(args); + if (args.benchmark_case == "portfolio") + return run_portfolio(args); + throw std::runtime_error("unknown benchmark case: "+ + args.benchmark_case); + } catch (const std::exception& exception) { + std::cerr << "error: " << exception.what() << '\n'; + return 2; + } +} diff --git a/test/search.cpp b/test/search.cpp index 48e0bdfdcb..542a686ceb 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -37,10 +37,15 @@ #include #include +#include #include "test/test.hh" +#include +#include +#include #include +#include static_assert(std::is_copy_constructible::value, "NoGoods must remain copy constructible"); @@ -62,6 +67,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 { @@ -106,8 +121,8 @@ namespace Test { virtual int solutions(void) const = 0; /// Verify that this is best solution virtual bool best(void) const = 0; - /// Master configuration function that does not restart - virtual bool master(const MetaInfo& mi) { + /// Origin configuration function that does not restart + bool origin(const MetaInfo& mi) override { if (mi.type() == MetaInfo::RESTART) { if (mi.last() != nullptr) constrain(*mi.last()); @@ -313,7 +328,7 @@ namespace Test { return "Sol"; } /// Rule out that solution is found more than once during restarts - virtual bool master(const MetaInfo& mi) { + bool origin(const MetaInfo& mi) override { switch (mi.type()) { case MetaInfo::RESTART: if (mi.last() != nullptr) { @@ -333,6 +348,2900 @@ namespace Test { } return false; } + /// Use the default completeness contract for each derived exploration + bool variant(const MetaInfo&) override { + return true; + } + }; + + /// 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 origin_calls; + std::atomic variant_calls; + std::atomic portfolio_assets; + std::atomic restart_variants; + std::atomic nogood_handoffs; + std::atomic live_spaces; + bool partition_portfolio; + unsigned int maximum; + + explicit MetaResizeState(unsigned int maximum0=15U) + : origin_calls(0U), variant_calls(0U), portfolio_assets(0U), + restart_variants(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 origin(const MetaInfo& mi) override { + observations->origin_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 variant(const MetaInfo& mi) override { + observations->variant_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_variants.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.origin_calls.load(std::memory_order_relaxed) == 1U) && + (single_state.variant_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.origin_calls.load( + std::memory_order_relaxed) == 0U) && + (homogeneous_state.variant_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.origin_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.origin_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_variants.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_variants.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_variants.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_variants.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); + } + }; + + /// Observations shared by meta-search dispatch test spaces + struct MetaDispatchState { + std::atomic origin_calls; + std::atomic variant_calls; + std::atomic origin_reasons; + std::atomic variant_reasons; + std::atomic assets; + std::atomic live_spaces; + std::atomic copies; + std::atomic constrain_calls; + std::atomic constrain_origin_calls; + std::atomic constrain_variant_calls; + std::atomic constrain_asset_mask; + std::atomic constrain_origin_asset_mask; + std::atomic constrain_variant_asset_mask; + std::atomic nogood_handoffs; + bool restart_after_solution; + bool solution_variant_complete; + bool fail_solution_variant; + bool nogood_model; + + /// Initialize without observations + MetaDispatchState(void) + : origin_calls(0), variant_calls(0), + origin_reasons(0), variant_reasons(0), assets(0), + live_spaces(0), copies(0), constrain_calls(0), + constrain_origin_calls(0), constrain_variant_calls(0), + constrain_asset_mask(0), constrain_origin_asset_mask(0), + constrain_variant_asset_mask(0), + nogood_handoffs(0), + restart_after_solution(true), + solution_variant_complete(true), + fail_solution_variant(false), nogood_model(false) {} + }; + + /// Common implementation for meta-search dispatch test spaces + class MetaDispatchSpace : public Space { + protected: + /// Variable used to provide two solutions + IntVar x; + /// Optional queens model used to force restart no-good extraction + IntVarArray q; + /// Shared hook observations and configuration + MetaDispatchState* state; + /// RBS role retained across cloning + enum RestartRole { + RESTART_ROLE_ORIGIN, + RESTART_ROLE_VARIANT + } restart_role; + /// PBS asset identity, or -1 before portfolio dispatch + int portfolio_asset; + + /// Record and implement an Origin callback + bool observe_origin(const MetaInfo& mi) { + state->origin_calls.fetch_add(1, std::memory_order_relaxed); + if (mi.type() == MetaInfo::PORTFOLIO) + // PBS must ignore the callback result. + return false; + restart_role = RESTART_ROLE_ORIGIN; + state->origin_reasons.fetch_or( + 1U << static_cast(mi.reason()), + std::memory_order_relaxed); + bool default_result = Space::origin(mi); + if ((mi.type() == MetaInfo::RESTART) && (mi.nogoods().ng() > 0)) + state->nogood_handoffs.fetch_add(1,std::memory_order_relaxed); + return (mi.reason() == MetaInfo::RR_SOL) ? + state->restart_after_solution : default_result; + } + + /// Record and implement a Variant callback + bool observe_variant(const MetaInfo& mi) { + state->variant_calls.fetch_add(1, std::memory_order_relaxed); + if (mi.type() == MetaInfo::PORTFOLIO) { + portfolio_asset = static_cast(mi.asset()); + state->assets.fetch_or(1U << mi.asset(), + std::memory_order_relaxed); + // PBS must ignore the callback result. + return false; + } + state->variant_reasons.fetch_or( + 1U << static_cast(mi.reason()), + std::memory_order_relaxed); + restart_role = RESTART_ROLE_VARIANT; + if ((mi.reason() == MetaInfo::RR_SOL) && + state->fail_solution_variant) + fail(); + return (mi.reason() == MetaInfo::RR_SOL) ? + state->solution_variant_complete : true; + } + + public: + /// Initialize with shared observations \a s + MetaDispatchSpace(MetaDispatchState& s) + : x(*this,0,1), q(*this,s.nogood_model ? 8 : 0,0,7), state(&s), + restart_role(RESTART_ROLE_ORIGIN), portfolio_asset(-1) { + state->live_spaces.fetch_add(1,std::memory_order_relaxed); + if (state->nogood_model) { + distinct(*this,IntArgs::create(q.size(),0,1),q,IPL_VAL); + distinct(*this,IntArgs::create(q.size(),0,-1),q,IPL_VAL); + distinct(*this,q,IPL_VAL); + Gecode::branch(*this,q,INT_VAR_NONE(),INT_VAL_MIN()); + } + Gecode::branch(*this,x,INT_VAL_MIN()); + } + /// Clone \a s + MetaDispatchSpace(MetaDispatchSpace& s) + : Space(s), state(s.state), restart_role(s.restart_role), + portfolio_asset(s.portfolio_asset) { + state->live_spaces.fetch_add(1,std::memory_order_relaxed); + state->copies.fetch_add(1,std::memory_order_relaxed); + x.update(*this,s.x); + q.update(*this,s.q); + } + /// Record destruction for clone-ownership checks + ~MetaDispatchSpace(void) override { + state->live_spaces.fetch_sub(1,std::memory_order_relaxed); + } + /// Exclude the preceding solution during an RBS restart + void constrain(const Space& _s) override { + state->constrain_calls.fetch_add(1,std::memory_order_relaxed); + if (restart_role == RESTART_ROLE_ORIGIN) + state->constrain_origin_calls.fetch_add(1, + std::memory_order_relaxed); + else if (restart_role == RESTART_ROLE_VARIANT) + state->constrain_variant_calls.fetch_add(1, + std::memory_order_relaxed); + if (portfolio_asset >= 0) { + const unsigned int asset = 1U << + static_cast(portfolio_asset); + state->constrain_asset_mask.fetch_or(asset, + std::memory_order_relaxed); + if (restart_role == RESTART_ROLE_ORIGIN) + state->constrain_origin_asset_mask.fetch_or( + asset,std::memory_order_relaxed); + else if (restart_role == RESTART_ROLE_VARIANT) + state->constrain_variant_asset_mask.fetch_or( + asset,std::memory_order_relaxed); + } + const MetaDispatchSpace& s = + static_cast(_s); + rel(*this,x,IRT_NQ,s.x.val()); + } + /// Return the solution value + int value(void) const { + return x.val(); + } + }; + + /// Dispatch test space overriding only Origin + class OriginDispatchSpace : public MetaDispatchSpace { + public: + /// Initialize with shared observations \a s + OriginDispatchSpace(MetaDispatchState& s) + : MetaDispatchSpace(s) {} + /// Clone \a s + OriginDispatchSpace(OriginDispatchSpace& s) + : MetaDispatchSpace(s) {} + /// Copy during cloning + Space* copy(void) override { + return new OriginDispatchSpace(*this); + } + /// Observe Origin dispatch + bool origin(const MetaInfo& mi) override { + return observe_origin(mi); + } + }; + + /// Dispatch test space overriding only Variant + class VariantDispatchSpace : public MetaDispatchSpace { + public: + /// Initialize with shared observations \a s + VariantDispatchSpace(MetaDispatchState& s) + : MetaDispatchSpace(s) {} + /// Clone \a s + VariantDispatchSpace(VariantDispatchSpace& s) + : MetaDispatchSpace(s) {} + /// Copy during cloning + Space* copy(void) override { + return new VariantDispatchSpace(*this); + } + /// Observe Variant dispatch + bool variant(const MetaInfo& mi) override { + return observe_variant(mi); + } + }; + + /// Dispatch test space overriding both Origin and Variant + class OriginVariantDispatchSpace : public MetaDispatchSpace { + public: + /// Initialize with shared observations \a s + OriginVariantDispatchSpace(MetaDispatchState& s) + : MetaDispatchSpace(s) {} + /// Clone \a s + OriginVariantDispatchSpace(OriginVariantDispatchSpace& s) + : MetaDispatchSpace(s) {} + /// Copy during cloning + Space* copy(void) override { + return new OriginVariantDispatchSpace(*this); + } + /// Observe Origin dispatch + bool origin(const MetaInfo& mi) override { + return observe_origin(mi); + } + /// Observe Variant dispatch + bool variant(const MetaInfo& mi) override { + return observe_variant(mi); + } + }; + + /// Focused meta-search hook dispatch test + class MetaDispatch : public Base { + public: + /// Dispatch scenario + enum Scenario { + RBS_ORIGIN_ONLY, + RBS_VARIANT_ONLY, + RBS_ORIGIN_CONTINUE, + RBS_ORIGIN_RESTART, + RBS_VARIANT_COMPLETE, + RBS_VARIANT_INCOMPLETE, + PBS_ORIGIN_ONLY, + PBS_VARIANT_ONLY, + PBS_SINGLE, + PBS_SEQUENTIAL_ORIGIN_ONLY, + PBS_SEQUENTIAL_VARIANT_ONLY, + PBS_SEQUENTIAL, +#ifdef GECODE_HAS_THREADS + PBS_PARALLEL_ORIGIN_ONLY, + PBS_PARALLEL_VARIANT_ONLY, +#endif + PBS_PARALLEL, + RBS_NOGOODS_RESET_OWNERSHIP, + META_STOP_OWNERSHIP, + BAB_INCUMBENT + }; + protected: + /// Scenario under test + Scenario scenario; + + /// Return a bit for restart reason \a r + static unsigned int reason(MetaInfo::RestartReason r) { + return 1U << static_cast(r); + } + + /// Check independent callback dispatch for one RBS model + template + bool rbs_dispatch(unsigned int origin_calls, + unsigned int variant_calls) { + MetaDispatchState state; + Model* m = new Model(state); + Gecode::Search::Options o; + o.threads = 1; + o.cutoff = Gecode::Search::Cutoff::constant(100); + Gecode::RBS search(m,o); + delete m; + + Model* first = search.next(); + Model* second = search.next(); + bool ok = (first != nullptr) && (second != nullptr) && + (state.origin_calls.load(std::memory_order_relaxed) == + origin_calls) && + (state.variant_calls.load(std::memory_order_relaxed) == + variant_calls); + delete first; + delete second; + return ok; + } + + /// Run one RBS dispatch scenario + bool rbs(bool restart_after_solution, + bool fail_solution_variant, + bool solution_variant_complete) { + MetaDispatchState state; + state.restart_after_solution = restart_after_solution; + state.fail_solution_variant = fail_solution_variant; + state.solution_variant_complete = solution_variant_complete; + + OriginVariantDispatchSpace* m = + new OriginVariantDispatchSpace(state); + Gecode::Search::Options o; + o.threads = 1; + o.cutoff = Gecode::Search::Cutoff::constant(100); + Gecode::RBS search(m,o); + delete m; + + OriginVariantDispatchSpace* first = search.next(); + bool ok = first != nullptr; + delete first; + + OriginVariantDispatchSpace* second = search.next(); + if (!fail_solution_variant) { + ok = ok && (second != nullptr) && + (state.origin_calls.load(std::memory_order_relaxed) == 1U) && + (state.variant_calls.load(std::memory_order_relaxed) == + (restart_after_solution ? 2U : 1U)) && + (state.origin_reasons.load(std::memory_order_relaxed) == + reason(MetaInfo::RR_SOL)) && + (state.variant_reasons.load(std::memory_order_relaxed) == + (reason(MetaInfo::RR_INIT) | + (restart_after_solution ? reason(MetaInfo::RR_SOL) : 0U))); + } else if (solution_variant_complete) { + ok = ok && (second == nullptr) && + (state.origin_calls.load(std::memory_order_relaxed) == 1U) && + (state.variant_calls.load(std::memory_order_relaxed) == 2U) && + (state.origin_reasons.load(std::memory_order_relaxed) == + reason(MetaInfo::RR_SOL)) && + (state.variant_reasons.load(std::memory_order_relaxed) == + (reason(MetaInfo::RR_INIT) | reason(MetaInfo::RR_SOL))); + } else { + ok = ok && (second != nullptr) && + (state.origin_calls.load(std::memory_order_relaxed) == 2U) && + (state.variant_calls.load(std::memory_order_relaxed) == 3U) && + (state.origin_reasons.load(std::memory_order_relaxed) == + (reason(MetaInfo::RR_SOL) | reason(MetaInfo::RR_CMPL))) && + (state.variant_reasons.load(std::memory_order_relaxed) == + (reason(MetaInfo::RR_INIT) | reason(MetaInfo::RR_SOL) | + reason(MetaInfo::RR_CMPL))); + } + delete second; + return ok; + } + + /// Run one PBS dispatch scenario + template + bool pbs(unsigned int assets, double threads, + unsigned int origin_calls, unsigned int variant_calls, + unsigned int asset_mask) { + MetaDispatchState state; + Model* m = new Model(state); + Gecode::Search::Options o; + o.assets = assets; + o.threads = threads; + Gecode::PBS search(m,o); + delete m; + + Model* solution = search.next(); + bool ok = + (solution != nullptr) && + (state.origin_calls.load(std::memory_order_relaxed) == + origin_calls) && + (state.variant_calls.load(std::memory_order_relaxed) == + variant_calls) && + (state.assets.load(std::memory_order_relaxed) == asset_mask); + delete solution; + return ok; + } + + /// Exercise RBS no-good posting, reset, and clone ownership + bool rbs_nogoods_reset_ownership(void) { + MetaDispatchState state; + state.nogood_model = true; + bool ok = true; + { + OriginVariantDispatchSpace* m = + new OriginVariantDispatchSpace(state); + Gecode::Search::Options o; + o.threads = 1; + o.nogoods_limit = 16; + o.cutoff = Gecode::Search::Cutoff::constant(1); + Gecode::RBS search(m,o); + delete m; + + unsigned int solutions = 0; + while (OriginVariantDispatchSpace* solution = search.next()) { + solutions++; + delete solution; + } + ok = (solutions == 2U) && + (state.variant_calls.load(std::memory_order_relaxed) >= 2U) && + (state.constrain_calls.load(std::memory_order_relaxed) >= 1U) && + (state.nogood_handoffs.load(std::memory_order_relaxed) >= 1U); + } + return ok && + (state.live_spaces.load(std::memory_order_relaxed) == 0U) && + (state.copies.load(std::memory_order_relaxed) > 0U); + } + + /// Stop RBS and PBS, then verify orderly clone destruction + bool meta_stop_ownership(void) { + MetaDispatchState rbs_state; + bool ok = true; + { + Gecode::Search::NodeStop stop(0); + OriginVariantDispatchSpace* m = + new OriginVariantDispatchSpace(rbs_state); + Gecode::Search::Options o; + o.threads = 1; + o.stop = &stop; + o.cutoff = Gecode::Search::Cutoff::constant(1); + Gecode::RBS search(m,o); + delete m; + OriginVariantDispatchSpace* solution = search.next(); + ok = (solution == nullptr) && search.stopped(); + delete solution; + } + ok = ok && + (rbs_state.live_spaces.load(std::memory_order_relaxed) == 0U); + + MetaDispatchState pbs_state; + { + Gecode::Search::NodeStop stop(0); + OriginVariantDispatchSpace* m = + new OriginVariantDispatchSpace(pbs_state); + Gecode::Search::Options o; + o.assets = 3; +#ifdef GECODE_HAS_THREADS + o.threads = 3; +#else + o.threads = 1; +#endif + o.stop = &stop; + Gecode::PBS search(m,o); + delete m; + OriginVariantDispatchSpace* solution = search.next(); + ok = ok && (solution == nullptr) && search.stopped(); + delete solution; + } + return ok && + (pbs_state.live_spaces.load(std::memory_order_relaxed) == 0U); + } + + /// Exercise incumbent propagation through RBS and PBS wrappers + bool bab_incumbent(void) { + MetaDispatchState rbs_state; + bool ok = true; + { + OriginVariantDispatchSpace* m = + new OriginVariantDispatchSpace(rbs_state); + Gecode::Search::Options o; + o.threads = 1; + o.cutoff = Gecode::Search::Cutoff::constant(100); + Gecode::SEB builder = + Gecode::rbs(o); + Gecode::Search::Engine* search = (*builder)(m); + delete builder; + delete m; + OriginVariantDispatchSpace* best = nullptr; + while (OriginVariantDispatchSpace* solution = + static_cast(search->next())) { + search->constrain(*solution); + delete best; + best = solution; + } + ok = (best != nullptr) && (best->value() == 1) && + (rbs_state.constrain_origin_calls.load( + std::memory_order_relaxed) > 0U) && + (rbs_state.constrain_variant_calls.load( + std::memory_order_relaxed) > 0U); + delete best; + delete search; + } + ok = ok && + (rbs_state.live_spaces.load(std::memory_order_relaxed) == 0U); + + MetaDispatchState pbs_state; + { + OriginVariantDispatchSpace* m = + new OriginVariantDispatchSpace(pbs_state); + Gecode::Search::Options o; + o.assets = 3; + o.threads = 1; + Gecode::PBS search(m,o); + delete m; + OriginVariantDispatchSpace* best = nullptr; + while (OriginVariantDispatchSpace* solution = search.next()) { + delete best; + best = solution; + } + ok = ok && (best != nullptr) && (best->value() == 1) && + (pbs_state.constrain_asset_mask.load( + std::memory_order_relaxed) == 7U); + delete best; + } + ok = ok && + (pbs_state.live_spaces.load(std::memory_order_relaxed) == 0U); + + MetaDispatchState nested_state; + { + OriginVariantDispatchSpace* m = + new OriginVariantDispatchSpace(nested_state); + Gecode::Search::Options outer; + outer.assets = 3; + outer.threads = 1; + Gecode::Search::Options inner[3]; + for (unsigned int i=0; i<3; i++) { + inner[i].threads = 1; + // Each RBS engine owns and deletes its cutoff. + inner[i].cutoff = Gecode::Search::Cutoff::constant(100); + } + Gecode::SEBs variants(3); + variants[0] = + Gecode::rbs(inner[0]); + variants[1] = + Gecode::rbs(inner[1]); + variants[2] = + Gecode::rbs(inner[2]); + Gecode::PBS + search(m,variants,outer); + delete m; + OriginVariantDispatchSpace* best = nullptr; + while (OriginVariantDispatchSpace* solution = search.next()) { + delete best; + best = solution; + } + ok = ok && (best != nullptr) && (best->value() == 1) && + (nested_state.constrain_origin_asset_mask.load( + std::memory_order_relaxed) == 7U) && + (nested_state.constrain_variant_asset_mask.load( + std::memory_order_relaxed) == 7U); + delete best; + } + return ok && + (nested_state.live_spaces.load(std::memory_order_relaxed) == 0U); + } + + public: + /// Initialize scenario \a s with name \a n + MetaDispatch(const std::string& n, Scenario s) + : Base("Search::MetaDispatch::"+n), scenario(s) {} + /// Run test + bool run(void) override { + switch (scenario) { + case RBS_ORIGIN_ONLY: + return rbs_dispatch(1,0); + case RBS_VARIANT_ONLY: + return rbs_dispatch(0,2); + case RBS_ORIGIN_CONTINUE: + return rbs(false,false,true); + case RBS_ORIGIN_RESTART: + return rbs(true,false,true); + case RBS_VARIANT_COMPLETE: + return rbs(true,true,true); + case RBS_VARIANT_INCOMPLETE: + return rbs(true,true,false); + case PBS_ORIGIN_ONLY: + return pbs(1,1,1,0,0); + case PBS_VARIANT_ONLY: + return pbs(1,1,0,1,1); + case PBS_SINGLE: + return pbs(1,1,1,1,1); + case PBS_SEQUENTIAL_ORIGIN_ONLY: + return pbs(3,1,1,0,0); + case PBS_SEQUENTIAL_VARIANT_ONLY: + return pbs(3,1,0,3,7); + case PBS_SEQUENTIAL: + return pbs(3,1,1,3,7); +#ifdef GECODE_HAS_THREADS + case PBS_PARALLEL_ORIGIN_ONLY: + return pbs(3,3,1,0,0); + case PBS_PARALLEL_VARIANT_ONLY: + return pbs(3,3,0,3,7); +#endif + case PBS_PARALLEL: + return pbs(3,3,1,3,7); + case RBS_NOGOODS_RESET_OWNERSHIP: + return rbs_nogoods_reset_ownership(); + case META_STOP_OWNERSHIP: + return meta_stop_ownership(); + case BAB_INCUMBENT: + return bab_incumbent(); + default: + GECODE_NEVER; + } + return false; + } }; /// %Base class for search tests @@ -605,9 +3514,9 @@ namespace Test { private: /// Whether best solution search is used bool best; - /// Number of master threads + /// Number of portfolio threads unsigned int mt; - /// Number of slave threads + /// Number of variant threads unsigned int st; public: /// Initialize test @@ -733,6 +3642,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++) @@ -781,6 +3692,24 @@ namespace Test { (HTC_NONE,HTB_NONE,HTB_NONE,HTB_NONE,c_d,a_d,t); } // Restart-based search + (void) new MetaDispatch("RBS::OriginOnly", + MetaDispatch::RBS_ORIGIN_ONLY); + (void) new MetaDispatch("RBS::VariantOnly", + MetaDispatch::RBS_VARIANT_ONLY); + (void) new MetaDispatch("RBS::OriginContinue", + MetaDispatch::RBS_ORIGIN_CONTINUE); + (void) new MetaDispatch("RBS::OriginRestart", + MetaDispatch::RBS_ORIGIN_RESTART); + (void) new MetaDispatch("RBS::VariantComplete", + MetaDispatch::RBS_VARIANT_COMPLETE); + (void) new MetaDispatch("RBS::VariantIncomplete", + MetaDispatch::RBS_VARIANT_INCOMPLETE); + (void) new MetaDispatch("RBS::NoGoodsResetOwnership", + MetaDispatch::RBS_NOGOODS_RESET_OWNERSHIP); + (void) new MetaDispatch("Lifecycle::StopOwnership", + MetaDispatch::META_STOP_OWNERSHIP); + (void) new MetaDispatch("BAB::Incumbent", + MetaDispatch::BAB_INCUMBENT); for (unsigned int t=1; t<=4; t++) { (void) new RBS("DFS",t); (void) new RBS("LDS",t); @@ -793,6 +3722,26 @@ namespace Test { (void) new RBS("BAB",t); } // Portfolio-based search + (void) new MetaDispatch("PBS::OriginOnly", + MetaDispatch::PBS_ORIGIN_ONLY); + (void) new MetaDispatch("PBS::VariantOnly", + MetaDispatch::PBS_VARIANT_ONLY); + (void) new MetaDispatch("PBS::Single", + MetaDispatch::PBS_SINGLE); + (void) new MetaDispatch("PBS::SequentialOriginOnly", + MetaDispatch::PBS_SEQUENTIAL_ORIGIN_ONLY); + (void) new MetaDispatch("PBS::SequentialVariantOnly", + MetaDispatch::PBS_SEQUENTIAL_VARIANT_ONLY); + (void) new MetaDispatch("PBS::Sequential", + MetaDispatch::PBS_SEQUENTIAL); +#ifdef GECODE_HAS_THREADS + (void) new MetaDispatch("PBS::ParallelOriginOnly", + MetaDispatch::PBS_PARALLEL_ORIGIN_ONLY); + (void) new MetaDispatch("PBS::ParallelVariantOnly", + MetaDispatch::PBS_PARALLEL_VARIANT_ONLY); + (void) new MetaDispatch("PBS::Parallel", + MetaDispatch::PBS_PARALLEL); +#endif for (unsigned int a=1; a<=4; a++) for (unsigned int t=1; t<=2*a; t++) { (void) new PBS("DFS",false,a,t); diff --git a/test/search/origin-variant-api-fail.cpp b/test/search/origin-variant-api-fail.cpp new file mode 100644 index 0000000000..220da06def --- /dev/null +++ b/test/search/origin-variant-api-fail.cpp @@ -0,0 +1,56 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * 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. + * + * This file is expected not to compile. Its two declarations use the + * removed Gecode 6 meta-search callbacks. A successful compilation means + * that a compatibility hook has accidentally returned. + */ + +#include + +class OriginVariantApiFail : public Gecode::Space { +public: + OriginVariantApiFail(void) {} + OriginVariantApiFail(OriginVariantApiFail& s) : Gecode::Space(s) {} + + Gecode::Space* copy(void) override { + return new OriginVariantApiFail(*this); + } + +#if defined(GECODE_TEST_REMOVED_MASTER) + bool master(const Gecode::MetaInfo&) override { + return true; + } +#elif defined(GECODE_TEST_REMOVED_SLAVE) + bool slave(const Gecode::MetaInfo&) override { + return true; + } +#else +#error Select one removed meta-search hook +#endif +}; diff --git a/test/search/origin-variant-api-pass.cpp b/test/search/origin-variant-api-pass.cpp new file mode 100644 index 0000000000..f52e858623 --- /dev/null +++ b/test/search/origin-variant-api-pass.cpp @@ -0,0 +1,55 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * 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 Test { namespace Search { + + /// Compile-pass fixture for the Gecode 7 meta-search callback API + class OriginVariantApiPass : public Gecode::Space { + public: + OriginVariantApiPass(void) {} + OriginVariantApiPass(OriginVariantApiPass& s) : Gecode::Space(s) {} + + Gecode::Space* copy(void) override { + return new OriginVariantApiPass(*this); + } + + bool origin(const Gecode::MetaInfo&) override { + return true; + } + + bool variant(const Gecode::MetaInfo&) override { + return true; + } + }; + +}} + +// STATISTICS: test-search diff --git a/test/search/origin-variant-terminology.expected b/test/search/origin-variant-terminology.expected new file mode 100644 index 0000000000..4cb47603e1 --- /dev/null +++ b/test/search/origin-variant-terminology.expected @@ -0,0 +1,75 @@ +CMakeLists.txt:1383:58 master negative-api-build-check "${CMAKE_CURRENT_BINARY_DIR}/origin-variant-api-fail-master" +CMakeLists.txt:1394:58 slave negative-api-build-check "${CMAKE_CURRENT_BINARY_DIR}/origin-variant-api-fail-slave" +Makefile.in:1395:15 MASTER negative-api-build-check \t@for hook in MASTER SLAVE; do \\ +Makefile.in:1395:22 SLAVE negative-api-build-check \t@for hook in MASTER SLAVE; do \\ +changelog.in:343:12 slave historical-changelog races, PBS slave lifetime hazards, restart-stop state races, stop-object +changelog.in:2936:60 master historical-changelog BrancherGroup::all.kill(home). This is the default for the master() +changelog.in:2969:5 master historical-changelog The master() and slave() configuration functions has been changed +changelog.in:2969:18 slave historical-changelog The master() and slave() configuration functions has been changed +changelog.in:3319:12 slave historical-changelog Change the slave function for meta search engines to return whether the search +changelog.in:3320:8 slave historical-changelog in the slave is going to be complete. This is necessary for example in LNS, +changelog.in:3321:36 slave historical-changelog where completing the search in the slave does not mean that the overall search +changelog.in:3444:1 master historical-changelog master() and slave() configuration functions have been extended +changelog.in:3444:14 slave historical-changelog master() and slave() configuration functions have been extended +changelog.in:3728:39 master historical-changelog Fix for restart-based search when the master does not have a branching. +changelog.in:3906:5 master historical-changelog The master() member function used during restart-based search now +changelog.in:3907:52 master historical-changelog takes an additional no-goods argument. The default master() +docs/gecode-7-migration.md:26:6 master historical-migration bool master(const Gecode::MetaInfo& mi) override; +docs/gecode-7-migration.md:27:6 slave historical-migration bool slave(const Gecode::MetaInfo& mi) override; +docs/origin-variant-terminology.md:81:5 Master design-rationale - **Master / Slave** is the terminology being removed. It describes authority +docs/origin-variant-terminology.md:81:14 Slave design-rationale - **Master / Slave** is the terminology being removed. It describes authority +examples/origin-variant.cpp:32:10 master historical-migration * bool master(const MetaInfo&) override; bool origin(const MetaInfo&) override; +examples/origin-variant.cpp:33:10 slave historical-migration * bool slave(const MetaInfo&) override; bool variant(const MetaInfo&) override; +examples/perfect-square.cpp:46:27 master unrelated-domain * second the size of the master square, and the remaining the +examples/scowl.hpp:1089:42 slave third-party "slaps", "slash", "slate", "slats", "slave", "slays", "sleds", +examples/scowl.hpp:1900:56 master third-party "masons", "masque", "massed", "masses", "massif", "master", +examples/scowl.hpp:2242:26 slaves third-party "slaved", "slaver", "slaves", "slayed", "slayer", "sleaze", +examples/scowl.hpp:3414:6 masters third-party "masters", "mastery", "mastiff", "mastoid", "matador", "matched", +examples/scowl.hpp:14029:17 master third-party * The ENABLE master word list, WORD.LST, is herewith formally released +gecode/int/cumulative/tree.hpp:146:8 Master bibliography * Master Thesis, Uppsala University, 2010 (in preparation). +gecode/support/run-jobs.hpp:78:11 Master unrelated-scheduler class Master; +gecode/support/run-jobs.hpp:84:15 master unrelated-scheduler /// The master to communicate with +gecode/support/run-jobs.hpp:85:7 Master unrelated-scheduler Master* master; +gecode/support/run-jobs.hpp:85:15 master unrelated-scheduler Master* master; +gecode/support/run-jobs.hpp:90:31 Master unrelated-scheduler Worker(Job* j, Master* m, int i); +gecode/support/run-jobs.hpp:96:11 Master unrelated-scheduler class Master { +gecode/support/run-jobs.hpp:124:7 Master unrelated-scheduler Master(Jobs& j, unsigned int m); +gecode/support/run-jobs.hpp:134:8 Master unrelated-scheduler ~Master(void); +gecode/support/run-jobs.hpp:136:31 master unrelated-scheduler /// A class to delete the master (running in parallel) +gecode/support/run-jobs.hpp:139:15 master unrelated-scheduler /// The master to be deleted +gecode/support/run-jobs.hpp:140:7 Master unrelated-scheduler Master* master; +gecode/support/run-jobs.hpp:140:15 master unrelated-scheduler Master* master; +gecode/support/run-jobs.hpp:142:27 master unrelated-scheduler /// Initialize with master \\a m +gecode/support/run-jobs.hpp:143:15 Master unrelated-scheduler Deleter(Master* m); +gecode/support/run-jobs.hpp:147:20 master unrelated-scheduler /// The actual master +gecode/support/run-jobs.hpp:148:5 Master unrelated-scheduler Master* master; +gecode/support/run-jobs.hpp:148:13 master unrelated-scheduler Master* master; +gecode/support/run-jobs.hpp:177:41 Master unrelated-scheduler Master* m, +gecode/support/run-jobs.hpp:179:31 master unrelated-scheduler : Runnable(true), job(j), master(m), idx(i) {} +gecode/support/run-jobs.hpp:187:26 Master unrelated-scheduler RunJobs::Master::stoppedjob(RetType& r) const { +gecode/support/run-jobs.hpp:194:26 Master unrelated-scheduler RunJobs::Master::stopped(void) const { +gecode/support/run-jobs.hpp:200:26 Master unrelated-scheduler RunJobs::Master::next(int& i) { +gecode/support/run-jobs.hpp:216:26 Master unrelated-scheduler RunJobs::Master::report(RetType r) { +gecode/support/run-jobs.hpp:225:26 Master unrelated-scheduler RunJobs::Master::stop(RetType r, int i) { +gecode/support/run-jobs.hpp:241:9 master unrelated-scheduler master->report(r); +gecode/support/run-jobs.hpp:243:9 master unrelated-scheduler master->stop(js.result(),idx); +gecode/support/run-jobs.hpp:246:13 master unrelated-scheduler job = master->next(idx); +gecode/support/run-jobs.hpp:252:26 Master unrelated-scheduler RunJobs::Master::done(void) const { +gecode/support/run-jobs.hpp:258:26 Master unrelated-scheduler RunJobs::Master::Master(Jobs& j, unsigned int m_threads)\s +gecode/support/run-jobs.hpp:258:34 Master unrelated-scheduler RunJobs::Master::Master(Jobs& j, unsigned int m_threads)\s +gecode/support/run-jobs.hpp:272:26 Master unrelated-scheduler RunJobs::Master::run(RetType& r) { +gecode/support/run-jobs.hpp:303:26 Master unrelated-scheduler RunJobs::Master::needthread(void) { +gecode/support/run-jobs.hpp:316:26 Master unrelated-scheduler RunJobs::Master::~Master(void) { +gecode/support/run-jobs.hpp:316:35 Master unrelated-scheduler RunJobs::Master::~Master(void) { +gecode/support/run-jobs.hpp:325:43 Master unrelated-scheduler RunJobs::Deleter::Deleter(Master* m)\s +gecode/support/run-jobs.hpp:326:23 master unrelated-scheduler : Runnable(true), master(m) {} +gecode/support/run-jobs.hpp:331:12 master unrelated-scheduler delete master; +gecode/support/run-jobs.hpp:341:9 master unrelated-scheduler i = master->stoppedjob(r); +gecode/support/run-jobs.hpp:348:7 master unrelated-scheduler : master(new Master(j,m)) {} +gecode/support/run-jobs.hpp:348:18 Master unrelated-scheduler : master(new Master(j,m)) {} +gecode/support/run-jobs.hpp:353:12 master unrelated-scheduler return master->run(r); +gecode/support/run-jobs.hpp:359:10 master unrelated-scheduler if (!master->needthread()) +gecode/support/run-jobs.hpp:360:14 master unrelated-scheduler delete master; +gecode/support/run-jobs.hpp:362:31 master unrelated-scheduler Thread::run(new Deleter(master)); +test/search/origin-variant-api-fail.cpp:46:8 master negative-api-fixture bool master(const Gecode::MetaInfo&) override { +test/search/origin-variant-api-fail.cpp:50:8 slave negative-api-fixture bool slave(const Gecode::MetaInfo&) override { diff --git a/test/search/verify-origin-variant-terminology.cpp b/test/search/verify-origin-variant-terminology.cpp new file mode 100644 index 0000000000..631ed6c0d6 --- /dev/null +++ b/test/search/verify-origin-variant-terminology.cpp @@ -0,0 +1,291 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + + struct Occurrence { + std::string path; + unsigned int line; + unsigned int column; + std::string spelling; + std::string classification; + std::string excerpt; + }; + + bool + word_character(char c) { + unsigned char uc = static_cast(c); + return std::isalnum(uc) || (c == '_'); + } + + std::string + escape(const std::string& text) { + std::string result; + std::string::size_type end = text.size(); + while ((end > 0) && (text[end-1] == ' ')) + end--; + for (std::string::size_type i=0; i= 5) && (name.substr(0,5) == "build")); + } + + void + scan(const std::filesystem::path& root, + const std::filesystem::path& relative, + std::vector& result) { + std::ifstream input(root / relative); + if (!input) { + std::cerr << "Cannot read " << (root / relative) << std::endl; + std::exit(2); + } + const std::string first = std::string("mas") + "ter"; + const std::string second = std::string("sla") + "ve"; + const std::string roles[] = { + first, second, first + "s", second + "s" + }; + const std::size_t result_start = result.size(); + std::string line; + unsigned int line_number = 0; + while (std::getline(input,line)) { + line_number++; + if (line.find('\0') != std::string::npos) { + result.resize(result_start); + return; + } + std::string lower(line); + std::transform(lower.begin(),lower.end(),lower.begin(), + [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + for (const std::string& role : roles) { + std::string::size_type at = lower.find(role); + while (at != std::string::npos) { + const std::string::size_type after = at + role.size(); + if (((at == 0) || !word_character(lower[at-1])) && + ((after == lower.size()) || !word_character(lower[after]))) { + Occurrence occurrence = { + relative.generic_string(), line_number, + static_cast(at+1), + line.substr(at,role.size()), std::string(), escape(line) + }; + result.push_back(occurrence); + } + at = lower.find(role,at+1); + } + } + } + } + + std::string + format(const Occurrence& occurrence) { + std::ostringstream result; + result << occurrence.path << ":" << occurrence.line << ":" + << occurrence.column << "\t" << occurrence.spelling << "\t" + << occurrence.classification << "\t" << occurrence.excerpt; + return result.str(); + } + + bool + parse(const std::string& row, Occurrence& occurrence) { + static const std::set classifications = { + "bibliography", "design-rationale", "historical-changelog", + "historical-migration", "negative-api-build-check", + "negative-api-fixture", "third-party", "unrelated-domain", + "unrelated-scheduler" + }; + std::string::size_type first_tab = row.find('\t'); + std::string::size_type second_tab = + (first_tab == std::string::npos) ? first_tab : + row.find('\t',first_tab+1); + std::string::size_type third_tab = + (second_tab == std::string::npos) ? second_tab : + row.find('\t',second_tab+1); + if ((first_tab == std::string::npos) || + (second_tab == std::string::npos) || + (third_tab == std::string::npos)) + return false; + std::string location = row.substr(0,first_tab); + std::string::size_type last_colon = location.rfind(':'); + std::string::size_type line_colon = + (last_colon == std::string::npos) ? last_colon : + location.rfind(':',last_colon-1); + if ((last_colon == std::string::npos) || + (line_colon == std::string::npos)) + return false; + occurrence.path = location.substr(0,line_colon); + occurrence.line = static_cast( + std::strtoul(location.substr(line_colon+1, + last_colon-line_colon-1).c_str(), + nullptr,10)); + occurrence.column = static_cast( + std::strtoul(location.substr(last_colon+1).c_str(),nullptr,10)); + occurrence.spelling = + row.substr(first_tab+1,second_tab-first_tab-1); + occurrence.classification = + row.substr(second_tab+1,third_tab-second_tab-1); + occurrence.excerpt = row.substr(third_tab+1); + return (occurrence.line != 0U) && (occurrence.column != 0U) && + !occurrence.spelling.empty() && !occurrence.classification.empty() && + !occurrence.excerpt.empty() && + (classifications.find(occurrence.classification) != + classifications.end()); + } + + bool + same_evidence(const Occurrence& actual, const Occurrence& expected) { + return (actual.path == expected.path) && + (actual.line == expected.line) && + (actual.column == expected.column) && + (actual.spelling == expected.spelling) && + (actual.excerpt == expected.excerpt); + } + +} + +int +main(int argc, char* argv[]) { + if (argc != 3) { + std::cerr << "Usage: " << argv[0] << " ROOT EXPECTED" << std::endl; + return 2; + } + + const std::filesystem::path root = + std::filesystem::absolute(argv[1]).lexically_normal(); + const std::filesystem::path expected_path = + std::filesystem::absolute(argv[2]).lexically_normal(); + std::vector files; + std::filesystem::recursive_directory_iterator iterator(root), end; + while (iterator != end) { + if (iterator->is_directory() && excluded_directory(iterator->path())) { + iterator.disable_recursion_pending(); + } else if (iterator->is_regular_file()) { + const std::filesystem::path path = + std::filesystem::absolute(iterator->path()).lexically_normal(); + const std::filesystem::path relative = + std::filesystem::relative(path,root); + if ((path != expected_path) && + (relative != + std::filesystem::path( + "test/search/origin-variant-terminology.expected"))) + files.push_back(relative); + } + ++iterator; + } + std::sort(files.begin(),files.end()); + + std::vector actual; + for (const std::filesystem::path& file : files) + scan(root,file,actual); + std::sort(actual.begin(),actual.end(), + [](const Occurrence& a, const Occurrence& b) { + if (a.path != b.path) + return a.path < b.path; + if (a.line != b.line) + return a.line < b.line; + return a.column < b.column; + }); + + if (std::string(argv[2]) == "-") { + for (Occurrence occurrence : actual) { + occurrence.classification = "UNCLASSIFIED"; + std::cout << format(occurrence) << std::endl; + } + return 0; + } + + std::ifstream expected_input(expected_path); + if (!expected_input) { + std::cerr << "Cannot read " << expected_path << std::endl; + return 2; + } + std::vector expected; + std::string row; + unsigned int row_number = 0; + while (std::getline(expected_input,row)) { + row_number++; + Occurrence occurrence; + if (!parse(row,occurrence)) { + std::cerr << "Invalid terminology inventory row " + << row_number << std::endl; + return 2; + } + expected.push_back(occurrence); + } + + bool matches = actual.size() == expected.size(); + if (matches) + for (std::size_t i=0; i