Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Solving Overview

Backends names the five solvers and what distinguishes them. This chapter covers each one in enough depth to run it: installation, parameters, and what its result actually contains.

xqsa is the package that does this. It defines one base class, Solver, and four classes built on it – SolverCPU, SolverGPU, SolverDWave, and SolverQuip – which cover the five backends: SolverGPU runs on either a CUDA or a Metal GPU, picked by its engine. A model built from xqcp or by hand in XQVM bytecode does not know or care which one samples it.

The Solver Interface

Every backend is called through the same single method:

class Solver:
    def solve(self, model: XqmxModel, **kwargs: Any) -> SolverResult: ...

model is an XqmxModel over the binary or spin domain. **kwargs override that solver’s constructor defaults for this call only – solver = SolverCPU(num_reads=100) then solver.solve(model, num_reads=500) runs 500 reads without building a new solver. On every backend except quip, a keyword the solver does not take raises TypeError instead of being ignored, so a typo such as num_raeds=500 fails loudly. solve() raises on failure (hardware unreachable, no embedding found, connection lost); it never returns a partial or empty result.

solve() is the same code for every backend except quip: it validates the model, runs the backend, recomputes the energy and assembles the result. A backend only supplies the part that turns a model into a sample, so the result has the same shape whichever one ran.

The return value is a frozen dataclass:

@dataclass(frozen=True)
class SolverResult:
    sample: XqmxSample
    energy: int
    timing: float
    metadata: dict[str, Any] = field(default_factory=dict)

sample is the solution, an XqmxSample with one value per variable. energy is the authoritative Hamiltonian energy. timing is wall-clock seconds spent solving. metadata holds what the run used, covered below.

energy is never the backend’s own reported value taken on faith. Every solve() recomputes it with the same integer formula the XQVM ENERGY opcode uses, over the model and the returned sample. See Energy and Precision for what that buys you and what it costs on hardware with limited precision.

metadata has exactly three keys on cpu, cuda, metal, and dwave:

KeyHolds
seedThe seed the run used; None when unseeded, and always None on dwave (physical hardware has no seed)
readsHow many samples the backend took
paramsEverything else: the selectors that chose the backend (algorithm, engine, mode), the parameters the run used, and diagnostics such as the raw pre-recompute energy
result = SolverCPU(seed=42).solve(model)
result.metadata["params"]["algorithm"]
# 'sa'

Backend-specific detail lives under params, so code that reads seed and reads works on any of the four; see D-Wave QPU for what dwave puts there. quip is the exception: it returns a different set of keys entirely, with no seed, reads, or params; see Quip Network.

Picking a Backend by Name

build_solver constructs any of the five from a short string, so a caller – an example runner, a CLI flag, a script – can stay backend-agnostic:

from xqsa import SOLVERS, DEFAULT_SOLVER, build_solver

print(sorted(SOLVERS))
# ['cpu', 'cuda', 'dwave', 'metal', 'quip']
print(DEFAULT_SOLVER)
# cpu

solver = build_solver("cpu", seed=42)
result = solver.solve(model)

seed reaches the three classical simulated-annealing backends (cpu, cuda, metal) and is ignored for dwave (physical hardware has no seed) and quip (configured from the environment; the miner’s own randomness is out of the caller’s control).

The registry otherwise uses each backend’s own constructor defaults, with one exception: its cuda and metal entries raise num_reads to 200 and num_sweeps to 2000, rather than the 100 and 1000 the bare constructor defaults to. They build SolverGPU(engine="cuda", num_reads=200, num_sweeps=2000) and the same with engine="metal". Local Solvers has the constructor defaults for every backend.

Swapping Backends Without Changing the Model

This is the claim the whole toolchain rests on, so here it is checked rather than asserted. examples/maxcut/runner.py builds one Max-Cut model with xqcp, runs the encoder on the Rust XQVM, and samples the result with whichever solver --solver names, built through build_solver. Run it against two different backends – cpu (CPU simulated annealing, 100 reads and 1000 sweeps) and metal (an Apple Silicon GPU kernel, a different process on different hardware, and – per the registry’s override above – 200 reads and 2000 sweeps) – and nothing about the model or the encoder changes:

uv run python examples/maxcut/runner.py --n 6 --seed 42 --solver cpu
uv run python examples/maxcut/runner.py --n 6 --seed 42 --solver metal

Both print the same result: "energy": -571, "cut_weight": 571, "valid": 1, the identical partition. The encoder produced one QUBO; two unrelated solvers minimised it and agreed. That agreement is not guaranteed in general – a harder model can leave different backends in different local optima – but the model they were handed, and the verifier that checked what came back, never changed.

The Domain Every Backend Rejects

XQVM has three domains: binary, spin, and integer. Every current xqsa solver’s _validate_model() accepts binary and spin and raises ValueError on integer:

from xqsa import SolverCPU
from xquad.types import XqmxModel

model = XqmxModel.integer(size=3, k=4)
SolverCPU().solve(model)
# ValueError: Unsupported domain for solving: integer

An integer XqmxModel is a real thing you can build in XQVM bytecode today – see Three Domains – but nothing in this chapter can solve one. spec/xqsa/DOMAINS.md calls this “reserved”: a future solver may relax the check, but none of the current five does. Re-encoding the problem in binary or spin is the only route around it today.

Choosing Among the Rest

You wantRead
A baseline that runs anywhere, no hardware or credentials requiredLocal Solvers, cpu
A local GPU for a bigger or faster runLocal Solvers, cuda / metal
Real quantum annealing hardwareD-Wave QPU
A decentralised, miner-solved compute marketQuip Network
What the numbers in SolverResult actually meanEnergy and Precision

That baseline requirement – runs anywhere, needs nothing – is exactly DEFAULT_SOLVER: every other backend is worth comparing against cpu before you trust its answer.