Noisy simulations on GPU¶

Noisy simulations running in "stochastic" mode can be offloaded to run on a GPU. As in the case of ideal simulations, to use the GPU based simulator, it is sufficient to set the fields "use_GPU" (to 'True') and "precision" (to either 1 or 2) while initializing the 'NoisyQProc'. In this notebook we show some use cases, where we obtain a speedup in running the simulations on a GPU.

In [1]:
import numpy as np

from qat.core import Observable, Term
from qat.lang.AQASM import Program
from qat.lang.AQASM.qftarith import QFT
from qat.hardware import DefaultHardwareModel
from qat.quops.quantum_channels import ParametricAmplitudeDamping, ParametricPureDephasing
from qlmaas.noisy.noisy_simulation import compute_fidelity
from qlmaas.qpus import NoisyQProc

As an example, we consider the Quantum Fourier Transform (QFT) circuit and we suppose a simple noise model with idle qubits subject to parametric amplitude damping.

In [2]:
nqbits = 14
prog = Program()
reg = prog.qalloc(nqbits)
prog.apply(QFT(nqbits), reg)
circ = prog.to_circ()

hardware_model = DefaultHardwareModel(gate_times = {"H": 0.2, "C-PH": lambda angle:0.65},
                                      idle_noise = [ParametricAmplitudeDamping(T_1 = 75)])    

Here we choose to run the simulation with 1000 samples and initialize the NoisyQProc with different arguments to compare the output and their respective runtimes.

In [3]:
n_samples = 1000

noisy_qpu_gpu_single = NoisyQProc(hardware_model=hardware_model, sim_method="stochastic", 
                                  n_samples=n_samples, use_GPU=True, precision=1)
noisy_qpu_gpu_double = NoisyQProc(hardware_model=hardware_model, sim_method="stochastic", 
                                  n_samples=n_samples, use_GPU=True, precision=2)
noisy_qpu_cpu = NoisyQProc(hardware_model=hardware_model, sim_method="stochastic", n_samples=n_samples)

I Fidelity of noisy QFT¶

In [4]:
%%time
fid_cpu, err_cpu = compute_fidelity(circ, noisy_qpu_cpu) # Simulation running on 1 node (cpu)
print(fid_cpu, err_cpu)
Submitted a new batch: Job78
0.8902669708985654 0.011005438050435032
CPU times: user 17.4 ms, sys: 7 ms, total: 24.4 ms
Wall time: 20.8 s
In [5]:
%%time
fid_gpu_single, err_gpu_single = compute_fidelity(circ, noisy_qpu_gpu_single) # Simulation running in single precision on a GPU
print(fid_gpu_single, err_gpu_single)
Submitted a new batch: Job87
0.8920676350285077 0.011049284447080927
CPU times: user 12.5 ms, sys: 5.85 ms, total: 18.4 ms
Wall time: 6.92 s
In [6]:
%%time
fid_gpu_double, err_gpu_double = compute_fidelity(circ, noisy_qpu_gpu_double) # Simulation running in double precision on a GPU
print(fid_gpu_double, err_gpu_double)
Submitted a new batch: Job90
0.9196244592174656 0.010597123925486795
CPU times: user 11.3 ms, sys: 4.86 ms, total: 16.2 ms
Wall time: 9.28 s

Here we compare the stochastic results with a deterministic evaluation and check the time it takes to get an exact value.

In [7]:
noisy_qpu_det = NoisyQProc(hardware_model=hardware_model, sim_method="deterministic-vectorized")
In [8]:
%%time
fid_cpu_det, _ = compute_fidelity(circ, noisy_qpu_det) # Deterministic simulation running on 1 node (cpu)
print(fid_cpu_det)
Submitted a new batch: Job94
0.9076533148415741
CPU times: user 20.9 ms, sys: 18.8 ms, total: 39.7 ms
Wall time: 1min 24s

II Sampling a noisy QFT¶

In [9]:
job = circ.to_job(nbshots=100)
In [10]:
%%time
res_cpu = noisy_qpu_cpu.submit(job)
Submitted a new batch: Job170
CPU times: user 2.32 ms, sys: 955 μs, total: 3.28 ms
Wall time: 94.7 ms
In [11]:
%%time
res_gpu_single = noisy_qpu_gpu_single.submit(job)
Submitted a new batch: Job171
CPU times: user 1.58 ms, sys: 1.77 ms, total: 3.35 ms
Wall time: 144 ms
In [12]:
%%time
res_gpu_double = noisy_qpu_gpu_double.submit(job)
Submitted a new batch: Job172
CPU times: user 1.9 ms, sys: 1.91 ms, total: 3.81 ms
Wall time: 134 ms

III Observable evaluation¶

Here we generate a random observable with 40 terms and evaluate it

In [13]:
n_terms = 40
terms = ["X", "Y", "Z"]
pauli_terms = []
for _ in range(n_terms):
    term = ""
    for _ in range(np.random.choice([1, 2], 1)[0]):
        term += np.random.choice(terms, 1)[0]
    pauli_terms.append(Term(1.0, term, list(np.random.choice(nqbits, len(term), replace=False))))
    
obs = Observable(nqbits, pauli_terms=pauli_terms)
In [14]:
job_obs = circ.to_job("OBS", observable=obs)
In [15]:
%%time
res_cpu_obs = noisy_qpu_cpu.submit(job_obs)
print(res_cpu_obs.value, res_cpu_obs.error)
Submitted a new batch: Job173
7.029930403058114 0.07012425740358547
CPU times: user 10.6 ms, sys: 5.93 ms, total: 16.6 ms
Wall time: 7.24 s
In [16]:
%%time
res_gpu_single_obs = noisy_qpu_gpu_single.submit(job_obs)
print(res_gpu_single_obs.value, res_gpu_single_obs.error)
Submitted a new batch: Job184
7.007927849746339 0.07047572914974513
CPU times: user 12.2 ms, sys: 4.93 ms, total: 17.1 ms
Wall time: 8.3 s
In [17]:
%%time
res_gpu_double_obs = noisy_qpu_gpu_double.submit(job_obs)
print(res_gpu_double_obs.value, res_gpu_double_obs.error)
Submitted a new batch: Job195
7.020572723231589 0.07021269024856479
CPU times: user 16.1 ms, sys: 1.04 ms, total: 17.1 ms
Wall time: 10.1 s