Qdislib — 08 · Backends under the hood¶
Every execution target — a CPU simulator (Qibo / Qiskit / CUDA-Q), a GPU, or a real QPU — is a BaseBackend. The cutting algorithms never branch on the backend: they hand each subcircuit to a backend object that build_execution_backend chose from your software= / gpu= / qpu= arguments. This notebook opens that box — and shows how to plug in your own backend.
[ ]:
import Qdislib as qd
The registry¶
Backends are registered by name and imported only on first use, so a missing SDK never breaks import Qdislib.
[ ]:
print(qd.available_backends())
How software= picks a backend¶
build_execution_backend turns (software, gpu, qpu, qpu_dict, num_qubits) into one concrete backend — the single place that decides. software= maps to a CPU backend; qpu=True + a big-enough QPU in qpu_dict routes to that QPU; a large-enough gpu=True routes to GPU Aer.
[ ]:
for sw in ('qiskit', 'qibo', 'cudaq'):
backend = qd.build_execution_backend(sw, num_qubits=4)
print(f'{sw:7s} -> {backend.name}')
# qpu=True + a big-enough QPU in qpu_dict routes to the QPU backend instead:
qpu = qd.build_execution_backend('qibo', num_qubits=4, qpu=True, qpu_dict={'MN_Ona': 8})
print('qpu ->', qpu.name)
The BaseBackend interface¶
A backend implements a few hooks: transpile, _execute_samples, _execute_estimator (the generic sample/estimator path), plus the cutting seams the algorithms call — evaluate_expectation (⟨O⟩ of a subcircuit), evaluate_samples (counts), and evaluate_joint_counts (joint final + cut-point counts, for sampling reconstruction). Any backend that implements these can run cut subcircuits.
Plug in your own backend¶
Subclass BaseBackend, implement the hooks (wrap an in-house simulator or a cloud QPU’s API), register it by name, then select it with software='...' — with no change to the cutting code:
[ ]:
from Qdislib import BaseBackend, register_backend
class MyBackend(BaseBackend):
def __init__(self, config):
super().__init__('MY_BACKEND', config)
def transpile(self, circuit):
...
def _execute_samples(self, circuit, transpile, shots, job):
... # -> SampleJobResult(counts=...)
def _execute_estimator(self, circuit, transpile, observable, shots, job):
... # -> ExpectationJobResult(expectation_value=...)
def evaluate_expectation(self, dag, num_qubits, prep_qubits=(), shots=1024,
method='automatic'):
... # build the native circuit from the DAG, run it, return <O>
# Registered by import path + class name (imported lazily on first use):
register_backend('MY_BACKEND', 'my_pkg.my_backend', 'MyBackend')
value = qd.gate_cutting(circuit, cut, software='MY_BACKEND', shots=8192)
The unified front-end: QdBackEnd¶
QdBackEnd holds a pool of backends and routes each job for you. Given a circuit it (1) keeps the backends with enough qubits for it — the qubit fit;
tries them shortest queue first (
queue_length); and (3) falls back to the next candidate if one errors (or lacks an estimator). Handy when you have several devices — but optional; the cutting API callsbuild_execution_backenddirectly.
The pool is a dict of name -> {'num_qubits': ..., <extra config>}. We use two simulators here so it runs anywhere; a real deployment lists QPUs ('IBM_BACKEND', 'BSC_QPU_BACKEND', …) with their URLs and tokens.
[ ]:
import qibo
from qibo import Circuit, gates
qibo.set_backend('numpy')
backend = qd.QdBackEnd(qpu_dict={
'AER_BACKEND': {'num_qubits': 30},
'QIBO_BACKEND': {'num_qubits': 20},
})
circuit = Circuit(2)
circuit.add(gates.H(0)); circuit.add(gates.CZ(0, 1)); circuit.add(gates.M(0, 1))
res = backend.sample_circuit(circuit, shots=1024) # shortest-queue backend that fits
exp = backend.get_expectation_value(circuit, 'ZZ', shots=1024)
print('counts:', res.counts)
print('<ZZ> :', round(exp.expectation_value, 3))
Next: 09 · Distributed execution & real QPUs — run the pieces in parallel with PyCOMPSs, and on IBM / BSC hardware.