D-Wave (Ocean) QPU Ising miner for the quip.network
v0.3 mining protocol. Ships as a pip-installable Python package with a
quip-dwave-qa console entry point.
Unlike the classical miners (quip-miner-cpu/-cuda/-metal), there is no
SA/Gibbs binary split: this backend samples on a D-Wave QPU via Ocean
(dwave.system.DWaveSampler), and every job goes to the QPU.
For tests and offline runs, QUIP_DWAVE_MOCK=1 or --mock swaps in a dimod
sampler. The default is ExactSolver, which enumerates every state and so
handles only the smallest problems. Set QUIP_DWAVE_MOCK_BACKEND=sa for
SimulatedAnnealingSampler, which scales to realistic topology sizes. You
select this path explicitly; the miner never switches to it on its own.
Each release publishes quip-dwave-qa-darwin-arm64, a self-contained
executable that bundles Python, the Ocean SDK, and the compiled
quip_solver_core extension. Drop it in and run it. The host needs only the
file.
TAG=v0.3.0-rc3 # or a later release
BASE=https://gitlab.com/api/v4/projects/84792347/packages/generic/quip-miner-dwave
curl -fLO "$BASE/$TAG/quip-dwave-qa-darwin-arm64"
chmod +x quip-dwave-qa-darwin-arm64
mv quip-dwave-qa-darwin-arm64 /usr/local/bin/quip-dwave-qaRename it, because the coordinator spawns whatever [dwave] binary names and
its default is quip-dwave-qa. The CPU, CUDA, and Metal miners install the
same way.
pip install "quip-miner-dwave @ git+https://gitlab.com/quip.network/quip-miner-dwave.git"The quip_solver_core SDK dependency installs from a published PyPI wheel on
Linux (amd64 and aarch64) and macOS (Apple Silicon). Elsewhere pip falls back
to building it from source through maturin, which needs Rust on the
machine.
Reaching real QPU hardware takes a D-Wave Leap token, set through
DWAVE_API_TOKEN. DWAVE_API_KEY, the name the v0.2 stack used, still works:
the miner copies it to DWAVE_API_TOKEN at startup and logs a deprecation
warning. Offline and classical sampling take no token.
pip install ".[dev]" pyinstaller
pyinstaller --clean --noconfirm pyinstaller/quip-dwave-qa.spec
QUIP_DWAVE_MOCK=1 ./dist/quip-dwave-qa --checkpyinstaller/quip-dwave-qa.spec names each collected package and gives the
reason. Ocean keeps compiled modules inside PEP 420 namespace packages that
PyInstaller does not find on its own, and PyInstaller reports a successful
build for a binary that cannot import dimod. Run the result before trusting
it.
Connect to a coordinator (production):
quip-dwave-qa --quip-coordinator unix:///run/quip/coord.sock
# or: python -m quip_miner_dwave --quip-coordinator ...Driver / fixed-input (run in isolation, no chain). Use the coordinator's
drive harness pointed at the quip-dwave-qa entry point — --source random
for golden-drawn problems, --source list <jsonl> for a fixed replay.
Introspection:
quip-dwave-qa --capabilities
quip-dwave-qa --checkThe miner keeps up to queue_depth submissions on the QPU at once. A
cloud-attached QPU spends most of each job's wall time on the round trip, so a
shallow pipeline leaves the device idle between jobs.
The default is 96. It comes from Little's Law:
depth = chip throughput * round trip
Measured on Advantage2_system1 with a production-sized problem (4577 nodes,
41514 couplers, num_reads=48): 43.2 ms of access time per job gives a chip
ceiling of 23.2 jobs/s, and an uncontended round trip is 1.57 s. That needs 36
in flight. A round trip of 3.05 s, which session logs show under load, needs
71. The default covers a round trip of 4.14 s, so a connectivity blip reduces
throughput instead of stalling the QPU.
Set queue_depth in the coordinator's [dwave] backend config to override
it:
queue_depth = 64The operator value wins. If unset, the miner uses the depth the coordinator sent. If that is also unset, the miner uses 96.
Depth does not raise jobs per hour under a budget. The budget paces spend across the quota period, and it funds fewer jobs per second than even a shallow pipeline delivers. Depth sets the burst rate and the duty cycle.
Depth does set how much work a reseed can strand. When the coordinator cancels a generation, the miner asks D-Wave to drop every job of that generation still on the QPU. D-Wave refunds only the jobs it has not started annealing, so the quota pays for the rest. Each session log reports the measured hit rate:
[QPU] cancel gen<=339: asked D-Wave to drop 3 in-flight job(s), 3 still
running | session: 12 cancelled, 4 annealed anyway (33% missed)
The same config holds other [dwave] keys: budget, budget_reset_day,
usage_db, num_reads, and anneal_time_us. This README does not document
them yet.
The miner keeps three tables in the usage database, beside the budget
ledger. One tracks throughput per UTC hour, covering job completions, QPU
busy time, round trips, and the D-Wave service time from SAPI's
submitted_on and solved_on. Another holds one row per qblock round. The
last holds a histogram of each job's best energy relative to the round's
target. The round strategy reads them. Nothing about mining depends on
them. A database that fails to open disables the history and logs one
warning. Every write from the session thread and the job workers goes
through the recorder's one worker thread. The seed and outcome-pickup
threads below write through the database's own lock instead.
At start the miner seeds past rounds from the coordinator's attempts files,
<data_dir>/<qblock_id>/attempts.jsonl. The default location is the
attempts directory beside the usage database, which is where the node
manager renders it. --attempts-dir PATH overrides it. Seeding and outcome
pickup keep only the lines for --miner-id, so a node running more than
one QPU miner does not absorb a sibling miner's history.
quip-dwave-qa --profile [--usage-db PATH] prints a grid of jobs per
second by day of the month and hour of the day, the hour factors and the
day factors behind it, a grid of D-Wave queue wait, the round totals, and the last 20
rounds with the jobs the strategy expected beside what happened. It needs
no QPU and no token. --usage-db applies to --profile only. A session
reads the coordinator's usage_db key instead.
At every qblock boundary the budget decides first whether the miner can afford the round. When it can, one function decides whether this round is worth joining or whether the funds should wait for a faster hour. It reads a snapshot of the history: throughput per slot, the round length, and the QPU time per job. A joined round runs to its end. The budget line is a target for the month and is consulted at boundaries only. The one thing that stops a round in progress is the period's whole allotment being spent.
Winning a round depends on the round's difficulty, which the protocol sets and the miner cannot see ahead, and on how many models the QPU evaluates while the round is open. Only the second varies with time, so the strategy compares slots by deliverable jobs and nothing else. More jobs per round is more of the search space covered.
A slot is an hour of the day on a day of the month. D-Wave accounts run on monthly contracts, so the shared queue follows the month as well as the working day. Months of 28 to 31 days map onto 28 bins, so every month fills every bin. The estimate for each of the 672 cells is the global rate times an hour-of-day factor times a day-of-month factor. A cell with enough jobs of its own shrinks toward that prediction instead of replacing it. This is the multiplicative main-effects model that call-center forecasting uses for arrival rates by day and time of day.
A join costs a whole round at this slot's rate, and the funds the rest of the period will have, the headroom now plus the accrual until the reset, cover only some of the rounds left in it. The best use of a fixed allotment across hours of varying rate is to spend it in the fastest ones. The decision ranks the remaining rounds by the jobs each would deliver, walks down until their cost exhausts the funds, and calls that slot's jobs the bar. It joins when this round's jobs clear the bar, within a tolerance. The verdict and its numbers are one log line per boundary.
Two [dwave] keys tune it. The defaults change nothing until history
exists.
min_throughput_advantage = 0.25 # skip a round only when the bar sits more than 25% above its jobs
participation_chance = 0.10 # join this share of rounds regardless, so every slot keeps getting measuredThe reason column in --profile and the strategy's log line name the verdict with one of these strings:
no-data: no history yet, so every round the budget allows is joined.explore: the participation chance drew this round regardless of the verdict below.saturated: the funds cover every round left in the period, so the round is joined.fast-slot: this round clears the bar, so it is joined.slow-slot: this round is under the bar, so it is skipped and the funds wait for a faster hour.
Three reasons predate the strategy and still appear where it plays no part: budget (the budget allowed the round and no strategy is attached yet), budget-sat-out (spend is past the budget line, so the miner sits the round out before any strategy is consulted), and unbudgeted (no budget is configured, so every round is joined).
pip install -e ".[dev]"
pytest quip_miner_dwave/tests -vAGPL-3.0-or-later. See LICENSE.