iTranslated by AI
Exploring cuQuantum (1) — From GTC2022
Purpose
I briefly learned about cuQuantum at GTC2022, so I would like to summarize it in this article.
Specifically, I watched Scaling Quantum Circuit Simulations with cuQuantum for Quantum Algorithms [A41102].
I am writing this as a sort of personal note, so the intermediate calculations might be a bit messy and I haven't delved deeply into the underlying meanings, but I will consider those in the future. Also, as a bonus, I will take a brief look at the calculation of circuits with a large number of qubits.
What is cuQuantum?
From reference [CQ]:
NVIDIA cuQuantum is an SDK of optimized libraries and tools for accelerating quantum computing workflows. With NVIDIA Tensor Core GPUs, developers can use cuQuantum to speed up quantum circuit simulations based on state vector and tensor network methods by orders of magnitude.
Basically, it is an incredibly fast GPU simulator for quantum computers. For tensor networks, see for example reference [TN].
Let's install cuQuantum
Since my local environment is CUDA 11.0, I will use the following command:
pip install cuquantum cuquantum-python cupy-cuda110
That should do it. Incidentally, the content I will discuss below uses features from cuquantum-python 22.7.0 or later[1], which require Python 3.8+. On the other hand, Google Colaboratory's Jupyter kernel seems to be 3.7.12, and only supports up to cuquantum-python 22.5.0. This means the API cannot be called, so I will give up on using Colab this time.
The experimental environment uses an NVIDIA T4, and I am not doing anything too elaborate. Since CuPy is used as the backend, the specific type of the tensors appearing below is cupy.ndarray.
What am I going to do?
I will convert a Qiskit quantum circuit into a tensor network, perform contraction calculations, and find the probability amplitudes of the circuit's output states.
Pros
- Since it doesn't maintain the state vector, there's no need to perform matrix calculations while holding a vector of length
for2^n qubits. In other words, it is memory-efficient.n
Cons
- This is just a calculation in the classical supercomputer world and doesn't benefit from quantum mechanics, so, for example, superposition cannot be utilized, and all probability amplitudes cannot be obtained at once. Probably.
Let's look at a simple sample right away
First, let's import the necessary modules as usual.
from qiskit import QuantumCircuit, QuantumRegister
from cuquantum import CircuitToEinsum, contract
import numpy as np
Then, if we construct a quantum circuit and pass it to cuQuantum:
qc = QuantumCircuit(1)
qc.x(0)
converter = CircuitToEinsum(qc)
expr, operands = converter.amplitude(bitstring='1')
print(expr)
for op in operands:
print(op)
print()
print(contract(expr, *operands))
We get the following output.
a,ba,b->
[1.+0.j 0.+0.j]
[[0.+0.j 1.+0.j]
[1.+0.j 0.+0.j]]
[0.+0.j 1.+0.j]
(1+0j)
The result of the contraction calculation contract(expr, *operands) is (1+0j), i.e., 1. What does this mean?
Let's do manual calculations
Writing operands[0], operands[1], and operands[2] as mathematical expressions gives the following:
Since expr = 'a,ba,b->', writing it as a mathematical expression gives:
In other words, it is calculating
It turns out we aren't doing anything scary. Next, let's consider a slightly more complex circuit.
Let's consider an entangled state
A so-called Bell state is created with a circuit like this:
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)

Let's put this into cuQuantum:
converter = CircuitToEinsum(qc, dtype='complex128', backend='cupy')
for bitstring in ['00', '01', '10', '11']:
expr, operands = converter.amplitude(bitstring=bitstring)
amplitude = contract(expr, *operands)
print(amplitude)
(0.7071067811865475+0j)
0j
0j
(0.7071067811865475+0j)
What I want to say is that by calculating the probability amplitudes of the states
Let's do manual calculations again
This time, let's find only the probability amplitude of state
expr, operands = converter.amplitude(bitstring='11')
print(expr)
for op in operands:
print(op)
print()
a,b,ca,debc,e,d->
[1.+0.j 0.+0.j]
[1.+0.j 0.+0.j]
[[ 0.70710678+0.j 0.70710678+0.j]
[ 0.70710678+0.j -0.70710678+0.j]]
[[[[1.+0.j 0.+0.j]
[0.+0.j 0.+0.j]]
[[0.+0.j 0.+0.j]
[0.+0.j 1.+0.j]]]]
The results are quite daunting. Let's write them as mathematical expressions again:
These might not be the best symbols, but I've chosen them this way. Since expr = 'a,b,ca,debc,e,d->', writing this calculation as a mathematical expression gives:
And so on. As for why the CX gate ended up like this, I can't write it cleanly yet and just followed the calculation, but the result seems to be like calculating
What I want to say here is that even complex quantum circuit calculations like entanglement have become just a calculation of tensor products.
Circuits with a Large Number of Qubits
In a previous article Playing with Qiskit (3), I concluded by saying:
This clarifies why simulating state vectors only works for a few dozen qubits. In this experiment, a VM with 26GB of memory was used, and it's suspected that it exceeded the limit at 31 qubits, which happened to be just barely manageable. This seems to be the limit in a typically available experimental environment.
This time, I want to challenge 200 qubits[3]. With this many qubits, attempting to find probability amplitudes using the settings from the previous article would have completely failed.
n_qubits = 200
qr = QuantumRegister(n_qubits, 'q')
qc = QuantumCircuit(qr)
qc.h(qr[:]);
The circuit diagram is quite plain, with an H gate placed after each input qubit, so I'll omit it. Or rather, it's so long that I don't feel like taking a screenshot. And then,
converter = CircuitToEinsum(qc, dtype='complex128', backend='cupy')
bitstring = '0' * n_qubits
expr, operands = converter.amplitude(bitstring=bitstring)
amplitude = contract(expr, *operands)
...with this, the probability amplitude for the state
amplitude = contract(expr, *operands)
print(np.isclose(amplitude, np.sqrt(1/2**n_qubits)))
print(amplitude)
True
(7.888609052209983e-31+0j)
And so, the probability amplitude
Summary
I tried using cuQuantum, which allows for GPU-accelerated tensor calculations via tensor networks for quantum circuit simulations. While this doesn't explain why tensor networks are used or their specific benefits, since the scope of this article is limited to "using cuQuantum," I'll address those topics separately in the future... For now, I'll wrap up with: I'm glad I could calculate probability amplitudes for a 200-qubit circuit!
On a related note, one could perform measurements using Qiskit's MPS simulator based on the matrix product concept discussed in Thinking about Matrix Products and following the Matrix product state simulation method as shown below. However, since this would require something on the scale of
qc.measure_all()
simulator = AerSimulator(method='matrix_product_state')
tcirc = transpile(qc, simulator)
result = simulator.run(tcirc).result()
counts = result.get_counts(0)
References
[CQ] cuQuantum
[TN] Tensor Network
[BQ] Yuichiro Minato, Decomposition of CNOT Gate, 2022
-
Specifically, cuquantum.CircuitToEinsum is used. ↩︎
-
When calculating the probability amplitudes of other states, the inner
part should change. ↩︎\ket{0}\bra{1} -
There's no deep meaning; it's just a number of qubits sufficiently larger than in the previous article. ↩︎
Discussion