Running on hardware#
Running on FiQCI#
For running on real hardware through the Lumi supercomputer’s FiQCI partition follow the instructions here. If you are used to using Qiskit on jupyter notebooks it is recommended to use the Lumi web interface.
fiqci-ems can run the experiments too, with its error mitigation applied: see Running experiments.
Running on other hardware#
Running on other providers such as IBM is untested at the moment but as long as the hardware can be accessed with Qiskit version > 1.0 QCut should be compatible.
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
gives each batch to the one with the fewest shots so far.
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])
job = ck.submit_experiments(experiment, shots=4096, backend=backend)
expectation_values = ck.estimate_expectation_values(job.result())
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])
job = ck.submit_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.