ddsolver)BEN’s double-dummy solver wrapper. As of the DDS 3.0.0 upgrade this uses the
dds3 Python extension instead of the old ctypes-loaded dds.dll / libdds.so.
| Before (DDS 2.9.x) | Now (DDS 3.0.0) |
|---|---|
ctypes.cdll.LoadLibrary("dds.dll") |
import dds3 (compiled pybind11 extension) |
dds.py, ddss.py ctypes wrappers |
removed |
DDSSolver (ddss fast-fork) + fallback |
removed — single DDSolver class |
bin/ddss.dll / .exp / .lib |
removed (ddss fast-fork dropped) |
DDSolver keeps the same public API (solve, calculatepar, version,
expected_tricks_dds, …), so callers did not change.
Note:
bin/dds.dll,bin/libdds.soandbin/darwin/libdds.2.9.0.dylibare kept — they are not used byddsolveranymore, but the BGADLL / PIMC native engine (src/pimc/BGADLL_Native.py) still P/Invokes the DDS C ABI and preloads them. Do not delete them unless BGADLL is rebuilt against DDS 3.0.0.
Concurrency. DDSolver runs a Python ThreadPoolExecutor and issues one
dds3.solve_board_pbn(..., context=ctx) per board, keeping one SolverContext
per worker thread — the modern DDS 3.x model (internal batch threading was
removed). solve_board_pbn releases the GIL during the search, so the pool runs
truly in parallel and each thread’s context keeps a warm transposition table.
The deprecated set_max_threads / solve_all_boards bindings are not used
(and no longer exist in current dds3).
dds3 extensiondds3 is a compiled extension and must be built per platform from the DDS
repository (https://github.com/dds-bridge/dds). It is not on PyPI.
# The DDS repo uses bazelisk (the `bazel` launcher); `bazel` may not be on PATH.
bazelisk build -c opt //python:_dds3 # -> bazel-bin/python/_dds3.{so,pyd}
bazelisk build -c opt //python:dds3_wheel_dist # -> a wheel under bazel-bin/python/dist/
Match the Python version. A compiled extension is locked to one CPython version (it links e.g.
python312.dll). BEN’sbenconda env is Python 3.12, so build the extension for 3.12, not the DDS repo’s default 3.14:bazelisk build -c opt \ --@rules_python//python/config_settings:python_version=3.12 //python:_dds3This requires the 3.12 toolchain to be registered in the DDS repo’s
MODULE.bazel(the default only registers 3.14). Verify the result withgrep -a python3 _dds3.pyd— it must referencepython312.dll. A 3.14-built extension fails to import under 3.12 with a confusingImportError.
See docs/python_interface.md in the DDS repo for per-OS prerequisites
(Windows: MSVC; Linux: GCC/clang; macOS: bazelisk).
Linux: build against an old glibc for portability. A
.sorecords the highest glibc symbol version it uses and will not load on an older glibc (version 'GLIBC_2.XX' not found). Build the Linux extension on the oldest glibc you need to support — a manylinux container (manylinux_2_28= glibc 2.28, ships CPython 3.12) is ideal: it runs on every current distro and on the Ubuntu 24.04 Docker image. Building on Ubuntu 24.04 (glibc 2.39) instead yields a.sothat needs glibc ≥ 2.38, so it will not run on Ubuntu 22.04 / Debian 12. The vendoredbin/dds3-linux/binary in this repo is currently a 24.04 build (glibc ≥ 2.38) — fine for Docker and Ubuntu 24.04+, but a native install on an older distro needs a rebuild. For macOS, set a lowMACOSX_DEPLOYMENT_TARGETfor the same reason.
ddsolver.py imports dds3 and finds it either way:
pip install dds3-1.0.0-*.whl
bin/ — copy the built dds3 package into the
platform-specific folder so it is shipped with BEN:
bin/dds3-win/dds3/__init__.py + _dds3.pyd (Windows)
bin/dds3-linux/dds3/__init__.py + _dds3.so (Linux)
bin/dds3-darwin/dds3/__init__.py + _dds3.so (macOS)
ddsolver._load_dds3() adds the right folder to sys.path automatically.
The Docker images expect option 2 (bin/dds3-linux/). PyInstaller builds
expect option 1 (dds3 installed in the build environment; the .spec files
list it under hiddenimports).
ddsrecorder.py + ddsreplay.py turn a played session into a repeatable DDS
performance test. Every DDS call BEN makes is written to a JSON Lines file with
its full input and output; the replayer re-issues those calls, times them,
and checks the answers still match.
Benchmarking this way rather than on generated deals matters because DDS runtime
is dominated by which calls BEN makes — the trick-1 solutions=1 batches for
bidding cost far more than the trick-11 ones for card play, and their mix is not
easy to guess. In a sample 16-deal session, trick 1 was 81 % of all DDS time.
Recording is off unless you ask for it with --ddsrecord, accepted by
game.py, gameserver.py, gameapi.py and table_manager_client.py. The
value is a file or a directory (a directory gets one file per process, so
several BEN processes can record at once); a .gz suffix compresses.
python game.py --boards mysession.pbn --auto True --ddsrecord d:/tmp/dds-rec.jsonl
Where there is no command line to reach — Docker, frozen builds — the
BEN_DDS_RECORD environment variable does the same thing. --ddsrecord wins if
both are given.
The entry points arm the recorder right after parsing arguments, before any
DDSolver is constructed, so no call is missed. The header record is written
lazily with the first DDS call, which is how it can carry the DDS version, mode
and thread count that only become known later.
Calls are tagged with the board being played in game.py (so also
gameserver.py) and table_manager_client.py. gameapi.py is not tagged —
it serves requests concurrently under gevent, so a single “current board” would
mis-attribute interleaved requests. Its records still carry the complete input
and replay fine, just with board: null.
Records are flushed per line, so a session ended with Ctrl-C still yields a usable file; the replayer skips a truncated final line.
Expect roughly 4 MB per 800 calls (200-sample batches dominate). Recording adds
the cost of serialising those batches, so the recorded ms are not a clean
benchmark — they are there for reference and appear in the report as
rec ms/call. The replay timings are the ones to compare.
# straight benchmark, best of 3
python src/ddsolver/ddsreplay.py d:/tmp/dds-rec/dds-12345.jsonl --repeat 3
# what does the recording contain? (does not run DDS)
python src/ddsolver/ddsreplay.py rec.jsonl --list
# tune dds_max_threads for this machine
python src/ddsolver/ddsreplay.py rec.jsonl --threads 8 --threads 16 --threads 32
# is a DDS rebuild / a dds_mode change faster, and still correct?
python src/ddsolver/ddsreplay.py rec.jsonl --json new.json --baseline old.json
python src/ddsolver/ddsreplay.py rec.jsonl --dds-mode 2
The report breaks time down by purpose (bid, lead, play, claimcheck,
claimtricks, contract, par) and, with --tricks, by trick number. Other
useful filters: --purpose, --board, --solutions, --min-trick,
--max-trick, --limit, --no-par.
--json writes a machine-readable result that a later run can --baseline
against; the exit status is non-zero if any call returned something other than
what was recorded, so it can be used as a CI check that a DDS change is
behaviour-preserving.
Replay calls solve_helper / _calculatepar_impl rather than the public
solve / calculatepar, so it measures DDS itself and never re-triggers the
recorder.
On repeatability: with dds_mode=1 DDS reuses a transposition table across
consecutive solves on the same SolverContext, and the thread pool assigns
boards to contexts nondeterministically, so a single run carries a few percent
of noise. Use --repeat (the best run is reported) and --warmup.