January 17, 2023
How to run big quantum circuits on small quantum computers in PennyLane
One of the biggest obstacles to practical applications of near-term quantum devices is the limited number of qubits available in the current, NISQ era. However, Peng et al. (2020) has proposed a method to cut big quantum circuits into smaller fragments, which require fewer qubits altogether. These fragments can then be run on a smaller quantum device and their execution results can be recombined, with an additional overhead in classical computation. PennyLane versions v0.22 and above allow you to introduce cuts into your circuit and make use of devices with a limited number of qubits. In this how-to, we will guide you through some examples of circuit cutting and its usage.
The basics
Circuits can be fragmented by introducing the resolution of the identity at the place of the cut. The figure below visualizes this procedure in detail (figure taken from Perlin et al. (2021)).
Here \mathcal{B} = \{I, X, Y, Z\} is the set of Pauli operators that form an orthogonal basis for the space of single-qubit operators, \lambda(M) is the spectrum of M \in \mathcal{B}, and M_s = |M_s\rangle\langle M_s| is the projector onto an eigenstate |M_s\rangle of M with eigenvalue s. Red boxes denote the projections and green boxes denote the preparations of a qubit in the corresponding state. This way, individual fragments can be simulated independently and their results recombined with the appropriate post-processing, as shown in the figure above.
Now let’s see how to cut circuits in PennyLane. First, let’s import the required packages.
import pennylane as qml from pennylane import numpy as np
Circuit cutting is enabled by decorating a QNode with the @qml.cut_circuit
transform. For example, to execute the three-wire circuit shown in the figure, which generates a GHZ state on a two-wire device, we can run the following:
dev = qml.device("default.qubit", wires=2) @qml.cut_circuit @qml.qnode(dev) def ghz_circuit(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) # cut the circuit into two fragments at wire 1 qml.WireCut(wires=1) qml.CNOT(wires=[1, 2]) return qml.expval(qml.grouping.string_to_pauli_word("XXX"))
Instead of executing the circuit directly, the circuit will be partitioned into smaller fragments according to the qml.WireCut
locations. Combining the results of the fragment executions will recover the expected output of the original uncut circuit:
>>> ghz_circuit() 0.9999999999999992
If we now draw the resulting circuit, we will get a list of two-wire circuits obtained after the cut:
>>> print(qml.draw(ghz_circuit)()) 0: ──H─╭●─┤ ╭<X@I> ╭<X@Z> 1: ────╰X─┤ ╰<X@I> ╰<X@Z> 0: ──H─╭●─┤ ╭<X@X> 1: ────╰X─┤ ╰<X@X> 0: ──H─╭●─┤ ╭<X@Y> 1: ────╰X─┤ ╰<X@Y> 0: ──I─╭●─┤ ╭<X@X> 1: ────╰X─┤ ╰<X@X> 0: ──X─╭●─┤ ╭<X@X> 1: ────╰X─┤ ╰<X@X> 0: ──H─╭●─┤ ╭<X@X> 1: ────╰X─┤ ╰<X@X> 0: ──H──S─╭●─┤ ╭<X@X> 1: ───────╰X─┤ ╰<X@X>
Differentiable circuit cuts
Of course, qml.WireCut
is differentiable and we can seamlessly take gradients of our fragmented circuits. For example, consider the following parameterized circuit that can be used for approximating the ground state of a hydrogen molecule:
dev = qml.device("default.qubit", wires=3) @qml.cut_circuit() @qml.qnode(dev) def circuit(x): qml.PauliX(wires=0) qml.PauliX(wires=1) qml.RX(np.pi / 2, wires=0) for i in range(1, 4): qml.Hadamard(wires=i) qml.CNOT(wires=[i - 1, i]) if i == 1: # we introduce a cut on wire 1 qml.WireCut(wires=i) qml.RZ(2 * x, wires=3) for i in reversed(range(1, 4)): if i == 1: # we introduce another cut on wire 1 qml.WireCut(wires=i) qml.CNOT(wires=[i - 1, i]) qml.Hadamard(wires=i) qml.RX(-np.pi / 2, wires=0) return qml.expval(qml.grouping.string_to_pauli_word("XXYY"))
We can now calculate the gradient in the usual way:
>>> x = np.array(0.12, requires_grad=True) >>> qml.grad(circuit)(x) tensor(1.94267595, requires_grad=True)
Automatic circuit cuts
A quantum circuit can be seen as a graph where inputs, outputs, and gates are represented by nodes, with wires represented by edges. PennyLane makes use of this representation when performing circuit cuts. In addition to manually placing the cuts, the @qml.cut_circuit
transform also supports automatic cutting via the auto_cutter
argument. By specifying the auto_cutter
argument, PennyLane will automatically find the locations where the circuit can be cut such that it can be run on a smaller device. The default automatic cutter search algorithm utilizes the KaHyPar package to determine the best cut locations and requires KaHyPar to be installed separately. Let’s consider the circuit that calculates the ground state of a hydrogen molecule again, and use this argument to remove the manual cut at wire 1:
dev = qml.device("default.qubit", wires=3) @qml.cut_circuit(auto_cutter=True) @qml.qnode(dev) def circuit(x): qml.PauliX(wires=0) qml.PauliX(wires=1) qml.RX(np.pi / 2, wires=0) for i in range(1, 4): qml.Hadamard(wires=i) qml.CNOT(wires=[i - 1, i]) qml.RZ(2 * x, wires=3) for i in reversed(range(1, 4)): qml.CNOT(wires=[i - 1, i]) qml.Hadamard(wires=i) qml.RX(-np.pi / 2, wires=0) return qml.expval(qml.grouping.string_to_pauli_word("XXYY"))
We can now calculate the expectation value and its gradient as usual, obtaining the same results as above:
>>> x = np.array(0.12, requires_grad=True) >>> circuit(x) 0.23770262642713402 >>> qml.grad(circuit)(x) tensor(1.94267595, requires_grad=True)
You can also pass custom cutting functions to the auto_cutter
argument; these functions should accept a NetworkX MultiDiGraph
representing the circuit and return the list of edges to cut. Additional arguments to such custom cutting functions can also be directly supplied as keyword arguments to @qml.cut_circuit
.
For more details on the built-in KaHyPar automatic cut placement algorithm, please see qml.transforms.qcut.find_and_place_cuts()
.
Cutting sampling circuits
In case you want to cut sample-based circuits, you can do so with the help of Monte Carlo methods, through the @qml.cut_circuit_mc
transform. You can also provide processing functions that allow the ‘cut’ samples to be processed into expectation values. For example, if wanted to calculate the occupancy of the first orbital of the hydrogen molecule, we could do something like the following:
dev = qml.device("default.qubit", wires=3, shots=10000) def Z_expectation(bitstring): return (-1) ** np.sum(bitstring) @qml.cut_circuit_mc(classical_processing_fn=Z_expectation) @qml.qnode(dev) def circuit(x): qml.PauliX(wires=0) qml.PauliX(wires=1) qml.RX(np.pi / 2, wires=0) for i in range(1, 4): qml.Hadamard(wires=i) qml.CNOT(wires=[i - 1, i]) if i == 1: qml.WireCut(wires=i) qml.RZ(2 * x, wires=3) for i in reversed(range(1, 4)): if i == 1: qml.WireCut(wires=i) qml.CNOT(wires=[i - 1, i]) qml.Hadamard(wires=i) qml.RX(-np.pi / 2, wires=0) return qml.sample(wires=[0]) >>> x = np.array(0.12, requires_grad=True) >>> circuit(x) tensor(-0.944, requires_grad=True)
Automatic cut placement is supported with @qml.cut_circuit_mc
as well by passing the auto_cutter=True
keyword argument.
We just saw how cutting circuits can be very useful and easily performed with PennyLane. However, it also involves a classical computational overhead that depends on how easy it is to cut the circuit into distinct parts. This means that a small circuit with lots of connections between qubits (i.e., multi-qubit gates) can sometimes take longer to run than a larger circuit with fewer connections. If you are interested in digging deeper into quantum circuit cutting, you can take a look at this demo.
About the author
Radoica Draškić
I am a trained theoretical physicist and a wannabe computer scientist. I am currently working as a summer resident at Xanadu.