Running on several backends#
A cut experiment is a collection of many independent circuits, so it does not have to run on one
machine. ParallelBackend takes several backends and
hands them the batches in turn.
from qiskit import QuantumCircuit
from qiskit.circuit.library import CXGate
import QCut as ck
from QCut import ParallelBackend, cutGate
circuit = QuantumCircuit(6)
for qubit in range(6):
circuit.ry(0.4 + 0.2 * qubit, qubit)
circuit.cx(0, 1)
circuit.cx(3, 4)
circuit.append(**cutGate(CXGate(), 2, 3))
circuit.cx(4, 5)
cut_circuit = ck.get_locations_and_subcircuits(circuit)
experiment = ck.get_experiment_circuits(cut_circuit, ["IIIIIZ"])
backend = ParallelBackend([first_device, second_device])
results = ck.run_experiments(experiment, shots=4096, backend=backend)
expectation_values = ck.estimate_expectation_values(results)
backend.submitted # circuits given to each, e.g. [1728, 1728]
Machines of different sizes#
A batch only goes to a backend with room for it, so the pieces that fit anywhere are shared while the widest go to the backends that can fit them. Cutting a circuit into a wide piece and a narrow one and giving it a large device and a small one uses both:
backend = ParallelBackend([five_qubit_device, twenty_qubit_device])
ck.run_experiments(experiment, shots=4096, backend=backend)
If nothing is wide enough for a subcircuit, QCut says so rather than letting the device
refuse the job. Cut into smaller pieces with max_qubits, or pass a larger backend.
Writing your own#
ParallelBackend is not special: QCut asks a backend only for
run(circuits, shots=..., **options) returning a job whose result() gives counts
by position. Anything answering that can decide for itself where circuits go.
import QCut as ck
class MyParallelBackend:
def __init__(self, backends):
self.backends = list(backends)
self._turn = 0
def run(self, circuits, shots=1024, **options):
backend = self.backends[self._turn % len(self.backends)]
self._turn += 1
ready = ck.transpile_circuits(list(circuits), backend)
return backend.run(ready, shots=shots, **options)
transpile_circuits() is what QCut transpiles with
itself, so an IQM device goes through IQM’s own transpiler and a resonator machine gets
its MOVE routing. It takes a CutCircuit, a CutExperiment or plain circuits.
Four things keep it correct:
Return the backend’s own job. Its
result()already answersget_counts(index)for that batch, which is what QCut reads. Splitting one batch across machines instead means merging the results back into submission order yourself.Pass ``shots`` through untouched. It is what the estimate divides by, and the waves allocate it themselves.
Submit one batch per call and do not merge them. The batches are already sized, and waves are grouped so that circuits wanting very different shot counts do not share a job.
Do not wait for results inside
run. Returning immediately is what leaves the machines running at the same time.
A backend that declares max_shots has it respected; ParallelBackend reports the
smallest of its own, since a batch may land on any of them. Add max_batch_size to
run’s signature to use the value QCut used.