Scaling for Production#
This section provides guidance on coding your application for performance on problems of the industrial scale supported by Leap’s quantum-classical hybrid solvers.
While the code examples below focus on constrained quadratic models (CQMs), most the guidance is also applicable to binary quadratic models (BQMs) and quadratic models (QMs).
This section does not discuss algorithmic complexity or problem formulation. For information about problem formulation, the Getting Started with D‑Wave Solvers guide provides an introduction and the Problem-Solving Handbook guide describes more advanced techniques.
Simple Example Application#
The code below formulates a simple bin packing problem, as explained in the Ocean SDK’s Bin Packing example.
For simplicity, assume that each bin has a capacity of 1 and start by generating weights for the items you wish to pack. A packing problem with n items results in a CQM with \(n \times (n+1)\) binary variables.
import numpy as np
num_items = 100 # results in 10100 binary variables
weights = np.random.default_rng(42).random(num_items)
Initial Code#
The first implementation is written for readability and pedagogy. As shown below, this comes at the cost of speed.
import typing
import dimod
def bin_packing(weights: typing.Sequence[float]) -> dimod.ConstrainedQuadraticModel:
"""Generate a bin packing problem as a constrained quadratic model."""
n = len(weights)
# y_j indicates that bin j is used
y = [dimod.Binary(f'y_{j}') for j in range(n)]
# x_i,j indicates that item i is put in bin j
x = [[dimod.Binary(f'x_{i},{j}') for j in range(n)] for i in range(n)]
cqm = dimod.ConstrainedQuadraticModel()
# minimize the number of bins used
cqm.set_objective(sum(y))
# each item can go in only one bin
for i in range(n):
cqm.add_constraint(sum(x[i]) == 1, label=f'item_placing_{i}')
# each bin has a capacity that must be respected
for j in range(n):
cqm.add_constraint(sum(weights[i] * x[i][j] for i in range(n)) - y[j] <= 0,
label=f'capacity_bin_{j}')
return cqm
Time the construction:
In [1]: %timeit bin_packing(weights)
385 ms ± 9.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Note
Because runtimes are highly system dependent, running the code on your system will likely result in different values. The results shown here are illustrative.
Use the quicksum Function#
The easiest improvement you can make is to substitute quicksum()
for the Python sum()
, which creates a large number of intermediate objects
not created by quicksum()
.
import typing
import dimod
def bin_packing(weights: typing.Sequence[float]) -> dimod.ConstrainedQuadraticModel:
"""Generate a bin packing problem as a constrained quadratic model."""
n = len(weights)
# y_j indicates that bin j is used
y = [dimod.Binary(f'y_{j}') for j in range(n)]
# x_i,j indicates that item i is put in bin j
x = [[dimod.Binary(f'x_{i},{j}') for j in range(n)] for i in range(n)]
cqm = dimod.ConstrainedQuadraticModel()
# minimize the number of bins used
cqm.set_objective(dimod.quicksum(y))
# each item can only go in one bin
for i in range(n):
cqm.add_constraint(dimod.quicksum(x[i]) == 1, label=f'item_placing_{i}')
# each bin has a capacity that must be respected
for j in range(n):
cqm.add_constraint(dimod.quicksum(weights[i] * x[i][j] for i in range(n)) - y[j] <= 0,
label=f'capacity_bin_{j}')
return cqm
This simple change already reduces the runtime.
In [1]: %timeit bin_packing(weights)
294 ms ± 9.39 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Construct Models Directly#
You can achieve an even bigger improvement by skipping symbolic construction altogether, working directly with variable labels and a single BQM object.
The following small example demonstrates the performance difference. See Symbolic Math for a discussion of the difference between variables and labels.
import dimod
def make_bqm_symbolic(num_variables: int) -> dimod.BinaryQuadraticModel:
return dimod.quicksum(2*dimod.Binary(v) for v in range(num_variables))
def make_bqm_labels(num_variables: int) -> dimod.BinaryQuadraticModel:
bqm = dimod.BinaryQuadraticModel('BINARY')
bqm.add_linear_from((v, 2) for v in range(num_variables))
return bqm
In [1]: %timeit make_bqm_symbolic(1000)
12.7 ms ± 213 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [2]: %timeit make_bqm_labels(1000)
194 µs ± 2.32 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Apply this same model construction to the binpacking example:
import typing
import dimod
def bin_packing(weights: typing.Sequence[float]) -> dimod.ConstrainedQuadraticModel:
"""Generate a bin packing problem as a constrained quadratic model."""
n = len(weights)
# y_j indicates that bin j is used
y_labels = [f'y_{j}' for j in range(n)]
# x_i,j indicates that item i is put in bin j
x_labels = [[f'x_{i},{j}' for j in range(n)] for i in range(n)]
cqm = dimod.ConstrainedQuadraticModel()
# minimize the number of bins used
objective = dimod.QuadraticModel()
objective.add_linear_from(((v, 1) for v in y_labels), default_vartype='BINARY')
cqm.set_objective(objective)
# each item can only go in one bin
for i in range(n):
lhs = dimod.QuadraticModel()
lhs.add_linear_from(((v, 1) for v in x_labels[i]), default_vartype='BINARY')
cqm.add_constraint_from_model(lhs, rhs=1, sense='==', label=f'item_placing_{i}')
# each bin has a capacity that must be respected
for j in range(n):
lhs = dimod.QuadraticModel()
lhs.add_linear_from(((x_labels[i][j], weights[i]) for i in range(n)), default_vartype='BINARY')
lhs.add_linear(y_labels[j], -1, default_vartype='BINARY')
cqm.add_constraint_from_model(lhs, rhs=0, sense='<=', label=f'capacity_bin_{j}')
return cqm
This change significantly reduces runtime.
In [1]: %timeit bin_packing(weights)
95.5 ms ± 2.87 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Add Constraints Without Copying#
By default, the add_constraint()
method
creates a copy of the objects you give it to avert mutation of objects that might
be used elsewhere in your code. If these objects are used solely for the
construction of constraints, as in this case, you can safely skip the copying.
import typing
import dimod
def bin_packing(weights: typing.Sequence[float]) -> dimod.ConstrainedQuadraticModel:
"""Generate a bin packing problem as a constrained quadratic model."""
n = len(weights)
# y_j indicates that bin j is used
y_labels = [f'y_{j}' for j in range(n)]
# x_i,j indicates that item i is put in bin j
x_labels = [[f'x_{i},{j}' for j in range(n)] for i in range(n)]
cqm = dimod.ConstrainedQuadraticModel()
# we wish to minimize the number of bins used
objective = dimod.QuadraticModel()
objective.add_linear_from(((v, 1) for v in y_labels), default_vartype='BINARY')
cqm.set_objective(objective)
# each item can only go in one bin
for i in range(n):
lhs = dimod.QuadraticModel()
lhs.add_linear_from(((v, 1) for v in x_labels[i]), default_vartype='BINARY')
cqm.add_constraint_from_model(lhs, rhs=1, sense='==', label=f'item_placing_{i}', copy=False)
# each bin has a capacity that must be respected
for j in range(n):
lhs = dimod.QuadraticModel()
lhs.add_linear_from(((x_labels[i][j], weights[i]) for i in range(n)), default_vartype='BINARY')
lhs.add_linear(y_labels[j], -1, default_vartype='BINARY')
cqm.add_constraint_from_model(lhs, rhs=0, sense='<=', label=f'capacity_bin_{j}', copy=False)
return cqm
This results in another performance improvement.
In [1]: %timeit bin_packing(weights)
68.1 ms ± 299 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)