Energy and Precision
Every backend in this chapter returns the same kind of number, computed the same way, no matter what found the sample: an integer. This page covers how that number is computed and compared, and where precision is lost or preserved as a model moves from an exact integer formula onto real hardware.
The Formula
A sample’s energy with respect to a model is:
$$E = \sum_i \text{linear}[i] \cdot x_i ;+; \sum_{i \le j} \text{quadratic}[i,j] \cdot x_i \cdot x_j$$
identical to the model’s Hamiltonian from
Quadratic Models, evaluated at one
concrete assignment x, and identical to the XQVM ENERGY opcode (see
Energy Evaluation). A verifier program
run through XQVM and xqsa’s own recomputation compute the same
formula two independent ways.
The Precision Contract
SolverResult.energy must equal compute_energy(model, sample)
exactly, an integer-to-integer comparison with zero tolerance for
drift. Every XQMX
coefficient and every variable assignment is an integer, so the energy
formula is a sum of integer products, not a float that merely rounds to
the right answer. It is an exact integer whenever it is representable at
all, and representable is a real condition: compute_energy checks every
term product and every partial sum against the i64 range and raises
ArithmeticOverflow rather than returning a wrapped number, so a model
whose energy leaves that range has no energy this contract can report.
Solver._recompute_energy()
calls compute_energy() and casts to int; every backend in this
chapter uses it instead of trusting whatever float its underlying
library reports. That raw float, where one exists, survives only as
metadata["params"]["raw_energy"], for diagnostics – never as the
authoritative value.
A verifier program never has to take a solver’s word for its own answer.
It recomputes the same integer independently with ENERGY. There are
three outcomes, not two: the comparison holds, the comparison fails, or
ENERGY raises ArithmeticOverflow and the verifier produces no verdict
at all. The third is not a solver disagreement and must not be read as
one – it says the model’s energy left the i64 range on the way to
being computed. Both ENERGY and compute_energy accumulate in the same
sorted key order and check the same intermediates, so the two agree on
which models fall into it.
A Large Penalty Does Not Corrupt the Model
Choosing a Penalty Weight
argues that a large \(P\) cannot corrupt a model, because energy
differences between feasible assignments survive exactly however large
\(P\) gets. Take that page’s three-node Max-Cut model with a one-hot
penalty \(P \cdot (x_0 + x_2 - 1)^2\) added on top, and score two
feasible assignments and one infeasible one through compute_energy()
at three wildly different weights:
P= 2 | E(feasible_a)= -8 E(feasible_b)= -6 E(infeasible)= -8 diff= 2
P= 1000 | E(feasible_a)= -1006 E(feasible_b)= -1004 E(infeasible)= -8 diff= 2
P= 2000000000 | E(feasible_a)= -2000000006 E(feasible_b)= -2000000004 E(infeasible)= -8 diff= 2
The difference between the two feasible assignments stays exactly 2
at every weight. The penalty term is 0 on both, so what separates them
is the underlying objective alone, and integer arithmetic never lets it
drift.
The infeasible assignment’s own energy does not move either – it sits
at -8 at every \(P\) above. What moves is the two feasible
energies, and they fall without bound as \(P\) grows. That is the
missing +P constant, not the penalty term itself: the true,
unstorable Hamiltonian adds +P to every assignment that violates the
constraint and +0 to every assignment that satisfies it, so dropping
that constant subtracts a uniform P from every stored energy. On the
infeasible sample the missing +P exactly cancels the penalty’s own
+P, leaving it unchanged; on a feasible sample there is nothing to
cancel, so the stored energy falls by P.
What a Large Penalty Costs Instead
Nothing in the model degrades, but two things outside it do, and both matter to backends in this chapter.
Fixed-precision hardware loses resolution. metal-gpu computes
coefficients in float32 on the GPU, not the integer arithmetic
ENERGY uses – result.energy is still recomputed in exact integers
afterward, but the search that finds the sample runs at float32
precision. cuda-gpu does not share this: its kernels take
const double* throughout, its acceptance draws stay float64, and
result.energy is recomputed the same way as every other backend. See
Local Solvers for the
per-backend split. The rest of this section applies to metal-gpu and
to fixed-precision annealing hardware, not to every GPU backend.
A float32 mantissa carries about seven decimal digits. Once a term in
the combined Hamiltonian is large enough that a difference of 2 no
longer changes the float, metal-gpu’s search cannot distinguish the
two assignments that difference separates, even though ENERGY still
resolves them exactly afterward:
import numpy as np
np.float32(-2000000006) == np.float32(-2000000004) # True -- the 2 vanished, at metal-gpu's width
np.float64(-2000000006) == np.float64(-2000000004) # False -- still resolved, at cuda-gpu's width
np.float32(-1006) == np.float32(-1004) # False -- still resolved
At P = 2000000000 the two feasible assignments above are
computationally indistinguishable to metal-gpu’s float32 search even
though compute_energy() still tells them apart exactly; at
P = 1000 they are not. cuda-gpu’s float64 search keeps this pair
distinguishable at this scale, though a large enough \(P\) would
eventually exhaust its wider but still finite mantissa too. Neither
GPU backend’s correctness ever suffers – the returned energy is
always the exact integer – but metal-gpu’s search can lose the
signal, at penalty weights where cuda-gpu’s still keeps it visible.
Real annealing hardware rescales, and the Quip network encodes at
fixed precision. A physical D-Wave annealer maps coefficients onto
its analog range with a monotonic, optimum-preserving rescaling, so an
oversized \(P\) is not rejected there – it costs usable range on the
device instead. SolverQuip goes further: it scales every coefficient
by MILLI_SCALE = 1000 into the chain’s i32 fields, so an oversized
coefficient can overflow i32 and raise EncodingError rather than
losing precision silently. MAX_NATURAL_COEFFICIENT = 2_147_483 is the
exact bound only for a SPIN model; a BINARY model – the domain every
worked example on this page uses – goes through a basis change first
that shifts the bound in both directions, and can overflow well before
any single coefficient looks anywhere near that large. See
Quip Network for the full
derivation.
A large penalty also raises the barrier between feasible regions, independent of any hardware precision question: a heuristic search such as simulated annealing has to cross that barrier to leave the first feasible region it reaches, and a larger \(P\) makes crossing less likely within a fixed number of sweeps. Sizing \(P\) in practice, and reading a solver’s output to tell which of these failure modes you hit, belongs to Constraints.