QCFPGA on JupyterLab
Connecting to the jlab portal¶
Please connect to the jlab portal first.
Start a server. Don't forget to select FPGA instead of CPU (see below):
- Select a working directory on the left pane, and then click on Terminal
- Clone the training repository:
git clone https://github.com/LuxProvide/QuantumFPGA
- Finally, on the left pane, click
QuantumFPGA/notebooks/qcfpa.ipynbto open the notebookqcfpa.ipynbin JupyterLab.
Setting the IPython kernel¶
We need now to create a dedicated IPython kernel to be able to run efficiently on efficiently on FPGA node
Kernels are by default located in this folder
$HOME/.local/share/jupyter/kernelsExecute the following cell to create a custom kernel named QCFPGA
%%bash
KERNEL="$HOME/.local/share/jupyter/kernels/qcfpga"
mkdir -p $KERNEL
PRELOAD="$KERNEL/start.sh"
JSON="$KERNEL/kernel.json"
cat << 'EOF' > $JSON
{
"argv": [
"{resource_dir}/start.sh",
"python",
"-m",
"ipykernel_launcher",
"-f",
"{connection_file}"
],
"display_name": "QCFPGA",
"language": "python",
"metadata": {
"debugger": true
}
}
EOF
cat << 'EOF' > $PRELOAD
#!/bin/bash
module load QCFPGA
module load jemalloc
export JEMALLOC_PRELOAD=$(jemalloc-config --libdir)/libjemalloc.so.$(jemalloc-config --revision)
export LD_PRELOAD=${JEMALLOC_PRELOAD}
export PYOPENCL_COMPILER_OUTPUT=1
exec "$@"
EOF
chmod u+x $PRELOAD
- Execute the following cell to reload everything
%load_ext autoreload
%autoreload 2
- Now change the current kernel by the new one:
Kernel --> Change Kernels --> QCFPGA - If you see a white circle, the kernel is ready
The QCFPGA library¶
QCFPGA is a software library which is a fork of the public QCGPU software that was designed to perform quantum computing simulations on graphics processing units (GPUs) using PyOpenCL. The main idea behind QCFPGA is to utilize the parallel processing capabilities of modern FPGAs to speed up quantum simulations, which are computationally intensive tasks that can benefit greatly from the pipeline parallelism offered by modern FPGAs.
The library provides a high-level interface for defining quantum states, applying gates, and performing measurements, much like other quantum computing frameworks. Nonetheless, the library is far from being complete as the Qiskit (IBM) or Cirq (Google).
QFPGA was adapted from QCGPU as a proof of concept with the intent to make quantum computing simulations more accessible and faster, leveraging the powerful computational capabilities of FPGAs to handle state vector manipulations typical in quantum computing.
# Import QCFPGA
import qcfpga
import numpy as np
# Create a new quantum register with 1 qubits
register = qcfpga.State(1)
# Let's try the Hadamard gate
register.h(0)
np.round(register.probabilities(),2)
/apps/USE/easybuild/release/2023.1/software/PyOpenCL/2023.1.4-foss-2023a-ifpgasdk-20.4/lib/python3.11/site-packages/pyopencl/__init__.py:528: CompilerWarning: From-binary build succeeded, but resulted in non-empty logs: Build on <pyopencl.Device 'p520_hpc_m210h_g3x16 : BittWare Stratix 10 MX OpenCL platform (aclbitt_s10mx_pcie0)' on 'Intel(R) FPGA SDK for OpenCL(TM)' at 0x145395ce4898> succeeded, but said: Trivial build lambda: self._prg.build(options_bytes, devices),
array([0.5, 0.5], dtype=float32)
Built-In Gates¶
In Quantum Computing, gates are used to manipulate quantum registers and to implement quantum algorithms.
There are a number of gates built into QCGPU and QCFPGA. They can all be applied the same way:
register = qcfpga.State(2)
register.h(0) # Applies the Hadamard gate to the first qubit.
register.x(1) # Applies a pauli-x gate to the second qubit.
np.round(register.probabilities(),2)
array([0. , 0. , 0.5, 0.5], dtype=float32)
These are the gates that can be applied to a register:
The Hadamard gate: h -
state.h(0)The S gate: s -
state.s(0)The T gate: t -
state.t(0)The Pauli-X / NOT gate: x -
state.x(0)The Pauli-Y gate: y -
state.y(0)The Pauli-Z gate: z -
state.z(0)The CNOT gate: cx -
state.cx(0, 1) # CNOT with control = 0, target = 1The SWAP gate: swap -
state.swap(0,1) # Swaps the 0th and 1st qubitThe Toffoli gate: toffoli -
state.toffoli(0, 1, 2) # Toffoli with controls = (0, 1), target = 2
'''
For example, you can also use any of the gates as controlled gates.
Controlled gates can be also used to entangle state
'''
x = qcfpga.gate.x()
h = qcfpga.gate.h()
register = qcfpga.State(5)
register.apply_gate(h,0)
register.apply_controlled_gate(x, 0, 1)
np.round(register.probabilities(),2)
array([0.5, 0. , 0. , 0.5, 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. ], dtype=float32)
Applying a gate to all qubits in parallel¶
'''
It is also trivial to apply a gate to all qubit of a register
'''
h = qcfpga.gate.h()
register = qcfpga.State(3)
register.apply_all(h)
np.round(register.probabilities(),3)
array([0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125],
dtype=float32)
Define your own gate¶
Custom gates in QCFPGA use the
qcfpga.Gateclass.Only single gate qubits can be defined
gate_matrix = np.array([
[np.cos(np.pi/6), -np.sin(np.pi/6)],
[np.sin(np.pi/6),np.cos(np.pi/6)]
])
gate = qcfpga.Gate(gate_matrix)
register = qcfpga.State(3)
register.apply_all(gate)
np.round(register.probabilities(),3)
array([0.422, 0.141, 0.141, 0.047, 0.141, 0.047, 0.047, 0.016],
dtype=float32)
Mesuring register¶
- Normally, real qubits will collapse, i.e., become classical qubits after measuring the register
- For obvious reason, it would require to rebuild a new circuit and repeat the experience
# Create a new quantum register with 2 qubits
register = qcfpga.State(2)
# Apply a hadamard (H) gate to the first qubit.
# You should note that the qubits are zero indexed
register.h(0)
# Add a controlled not (CNOT/CX) gate, with the control as
# the first qubit and target as the second.
# The register will now be in the bell state.
register.cx(0, 1)
# Perform a measurement with 1000 samples
results = register.measure(samples=1000)
results
{'11': 502, '00': 498}
Use case: Bernstein-Vazirani Algorithm¶
The Bernstein-Vazirani algorithm is a quantum algorithm that highlights the superiority of quantum computers in solving specific problems more efficiently than classical computers. This algorithm solves the problem of determining a hidden binary string with minimal queries to a given function.
Problem Setup¶
You are given a black box function (oracle) that computes:
- Function: $ f(x) = a \cdot x $
- a is a hidden string of $ n $ bits.
- x is an $n$-bit string.
- The dot product $a \cdot x $ is calculated as $ (a_1x_1 + a_2x_2 + \dots + a_nx_n) $ modulo 2.
- Goal: Determine the hidden string $a $ using the fewest number of queries to $f$.
Quantum Solution¶
The Bernstein-Vazirani algorithm uses a quantum computer to identify $ a $ with a single query, showing an exponential improvement in query complexity.
Steps of the Algorithm:¶
Initialization: Start with $ n $ qubits in the state $ |0\rangle $ and one auxiliary qubit in the state $|1\rangle $.
Apply Hadamard Gates: Apply Hadamard gates to all qubits, transforming each $ |0\rangle $to $ \frac{|0\rangle + |1\rangle}{\sqrt{2}} $ and $ |1\rangle $ to $\frac{|0\rangle - |1\rangle}{\sqrt{2}}$.
Query the Oracle: The function $ f(x) $ modifies the auxiliary qubit by $ (-1)^{f(x)} $, encoding the dot product $ a \cdot x $ in the quantum state.
Apply Hadamard Gates Again: Applying Hadamard gates again to all but the auxiliary qubit uses quantum interference to amplify the probability amplitudes of the states corresponding to $ a$.
Measurement: Measure the first $ n $ qubits to directly obtain $a $ in binary form.
Application¶
- To simulate this algorithm, we can use $n$ qubits and apply interference by using the $z$ gate through the inner product oracle.
- Applying the $z$ gate to those qubits will change their state from $|+\rangle$ to $|-\rangle$.
- The last $h$ gates will bring them to the $|1>$ state.
import qcfpga
num_qubits = 20 # The number of qubits to use
a = 70 # The hidden integer, bitstring is 1000110
register = qcfpga.State(num_qubits) # Create a new quantum register
register.apply_all(qcfpga.gate.h()) # Apply a hadamard gate to each qubit
# Apply the inner products oracle
for i in range(num_qubits):
if a & (1 << i) != 0:
register.z(i)
register.apply_all(qcfpga.gate.h()) # Apply a hadamard gate to each qubit
results = register.measure(samples=1000) # Measure the register (sample 1000 times)
print(results)
{'00000000000001000110': 1000}
Conclusion and Significance¶
The Bernstein-Vazirani algorithm demonstrates quantum parallelism and serves as an introductory example for more complex quantum algorithms like Shor's and Grover's algorithms, highlighting quantum computational speed-ups.