I need to be able to generate very quickly pulse schedules to submit them on quantum chips.
For the moment I am generating instances of QuantumCircuit
:
import typing as ty
from copy import deepcopy
from math import pi
from time import time as now
import numpy.random
from qiskit import IBMQ, QuantumCircuit, pulse, schedule, transpile
from qiskit.providers.ibmq.ibmqbackend import IBMQBackend
from qiskit.pulse import InstructionScheduleMap, Schedule
start = now()
n = 100
m = 20
# Random circuit generation with only sqrt(X) or sqrt(Y) gates.
rng = numpy.random.default_rng()
is_sx_all: numpy.ndarray = rng.integers(low=0, high=2, dtype=bool, size=(m, n))
circuits: ty.List[QuantumCircuit] = list()
for is_sx_list in is_sx_all:
circuit = QuantumCircuit(1, 1)
for is_sx in is_sx_list:
if is_sx:
circuit.sx(0)
else:
circuit.rz(-pi / 2, 0)
circuit.sx(0)
circuit.rz(pi / 2, 0)
circuits.append(circuit)
end_random_circ_gen = now()
print(f"Generated random circuits in {end_random_circ_gen - start:.2f} seconds")
# Recovering backend information from the cloud
IBMQ.load_account()
provider = IBMQ.get_provider(hub="ibm-q", group="open", project="main")
ibmq_bogota = provider.get_backend("ibmq_bogota")
and I schedule them using qiskit.pulse.schedule
:
start_pulse = now()
# Create a dummy implementation of the sqrt(X) gate
with pulse.build(ibmq_bogota) as sx_impl:
pulse.play(pulse.library.Waveform(rng.random(160) / 10), pulse.DriveChannel(0))
# Replace the default sqrt(X) implementation by our dummy implementation.
# Here we only need 1 qubit because all our circuits are on 1 qubit.
backend_instruction_map: InstructionScheduleMap = deepcopy(
ibmq_bogota.defaults().instruction_schedule_map
)
backend_instruction_map.add("sx", [0], sx_impl)
end_instr_map = now()
print(f"Generated the instruction map in {end_instr_map - start_pulse:.2f} seconds")
# Create the actual schedules.
schedules = schedule(circuits, backend=ibmq_bogota, inst_map=backend_instruction_map)
end_schedule = now()
print(f"Generated the schedules in {end_schedule - end_instr_map:.2f} seconds")
My actual application allow me to keep the initially generated circuits in memory (so I only need to execute the first portion of code once), but I will need to re-generate the schedules for different pulses shape a lot of times. In other words, the second code portion will be executed several time.
My issue is the following: on my computer, the previous code outputs the following:
Generated random circuits in 0.05 seconds
Generated the instruction map in 0.86 seconds
Generated the schedules in 2.50 seconds
which is... a lot! In fact, my "time budget" to create this is more or less $200$ms, with some pre-processing (in the first code block) allowed.
My question is the following: do you have any idea / trick to improve the execution time of the second code block, i.e. pulse schedule generation ?
Here are several constraints (or non-constraints) I have:
- The setup phase that is only executed once can have a greater runtime. One way I tried to solve this problem is by doing (possibly costly) pre-processing at the beginning to speed-up the repeated part, but I did not find any idea.
- The circuits are all $1$-qubit, hardware-compliant, circuits. So there is no need to transpile them, they can directly be translated to pulses.
- The only hardware gate used is $\sqrt{X}$, and it is the only gate whose implementation will change in the different iterations of the second code block.
- My final goal is to copy/paste the $1$-qubit circuit constructed above on all the qubits of the chip in parallel, with different pulse implementation for each qubit (same length, but different amplitudes). This is not depicted in the code above for simplification purpose, but it basically multiply the runtime of the previous code by the number of qubits considered (see results below).
With $5$ qubits in parallel, I have the following results:
Generated random circuits in 0.32 seconds
Generated the instruction map in 0.87 seconds
Generated the schedules in 13.08 seconds
EDIT: I experimented and successfully lowered down the runtime by directly generating the schedules without constructing the QuantumCircuit
intermediate representation. The new code:
# Create dummy implementations of the sqrt(X) gate
sx_implementations: ty.List[pulse.library.Waveform] = list()
for qubit_index in range(qubit_number):
sx_implementations.append(pulse.library.Waveform(rng.random(160) / 10))
start_direct_schedule_construction = now()
direct_schedules: ty.List[Schedule] = list()
for is_sx_list in is_sx_all:
start = now()
with pulse.build(backend) as direct_schedule:
drive_channels = [pulse.drive_channel(qi) for qi in range(qubit_number)]
for is_sx in is_sx_list:
for qi in range(qubit_number):
if is_sx:
pulse.play(sx_implementations[qi], drive_channels[qi])
else:
pulse.shift_phase(-pi / 2, drive_channels[qi])
pulse.play(sx_implementations[qi], drive_channels[qi])
pulse.shift_phase(pi / 2, drive_channels[qi])
pulse.measure_all()
# print(f"Done in {(now()-start)*1000:.0f} ms.")
direct_schedules.append(direct_schedule)
end_direct_schedule_construction = now()
dir_schd_construction_time = (
end_direct_schedule_construction - start_direct_schedule_construction
)
print(f"Generated Schedules directly in {dir_schd_construction_time:.2f} seconds")
Executing this code, the output on my machine is:
Generated Schedules directly in 0.41 seconds
One issue being that it is not consistent and sometimes goes up to 0.90 seconds. This is still above my threshold, but it is a huge improvement.