0
$\begingroup$

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.

$\endgroup$
4
  • $\begingroup$ Did you profile the code? $\endgroup$ Jul 25 at 13:14
  • $\begingroup$ No, but the 3 timings I print are pretty explicit: the qiskit.pulse.schedule function is taking between 90% and 95% of the total execution time. I got an idea in the meantime, I'll test it and update the question if needed $\endgroup$ Jul 25 at 14:03
  • 1
    $\begingroup$ So I do not think there as been much work on optimization of those routines. If you profile the execution you can see where the bottleneck(s) is and make an issue. $\endgroup$ Jul 25 at 14:05
  • $\begingroup$ The new method that consist in directly generating the Schedules is way more efficient, I do not know why I did not started with this. I'll try to go one more step down by avoiding the pulse builder and directly using Schedules. I also need to see why the runtime of this new approach is not constant but sometimes double. $\endgroup$ Jul 25 at 14:41
1
$\begingroup$

I'm glad to see that you have increased the performance of the pulse path. The scheduler does need to be profiled, but this path will be deprecated in favor of pulse gate calibrations at the end of 2021. I would recommend creating the gate calibration and attaching it directly to the QuantumCircuit. This will be the long-term integration path for pulses in Qiskit. See this documentation for more info. I imagine this will be significantly faster.

$\endgroup$
1
  • $\begingroup$ In order to avoid writing an answer in my question, I include the code here. @user47787 feel free to copy-paste it in your answer if you want, I have no issue with this. Thank you for the answer! $\endgroup$ Aug 2 at 7:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.