Skip to content

Instantly share code, notes, and snippets.

@wshanks
Last active May 22, 2024 21:22
Show Gist options
  • Save wshanks/4992ecff91198795ea99e86c044af5dd to your computer and use it in GitHub Desktop.
Save wshanks/4992ecff91198795ea99e86c044af5dd to your computer and use it in GitHub Desktop.
Limited transpilation benchmarking
from math import pi
from time import perf_counter
from qiskit import QuantumCircuit, QuantumRegister, pulse, transpile
from qiskit.circuit import Gate
from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel
from qiskit.pulse.calibration_entries import CalibrationPublisher
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
ApplyLayout,
BasisTranslator,
EnlargeWithAncilla,
FullAncillaAllocation,
PulseGates,
SetLayout,
)
from qiskit_experiments.framework import BackendTiming
from qiskit_ibm_runtime import QiskitRuntimeService
def t1_circuits(delays, backend, qnum=0):
timing = BackendTiming(backend)
circuits = []
for delay in delays:
circ = QuantumCircuit(qnum + 1, 1)
circ.x(qnum)
circ.barrier(qnum)
circ.delay(timing.round_delay(time=delay), qnum, timing.delay_unit)
circ.barrier(qnum)
circ.measure(qnum, 0)
circ.metadata = {"xval": timing.delay_time(time=delay)}
circuits.append(circ)
return circuits
def fast_transpile(
circuits,
backend,
physical_qubits,
translate_and_pulse_gates=False,
custom_translate_and_pulse_gates_check=False,
num_processes=1,
) -> list:
if hasattr(backend, "target"):
# V2 backend model
# This model assumes qubit dependent instruction set,
# but we assume experiment mixed with this class doesn't have such architecture.
basis_gates = set(backend.target.operation_names)
n_qubits = backend.target.num_qubits
elif hasattr(backend, "configuration"):
# V1 backend model
basis_gates = set(backend.configuration().basis_gates)
n_qubits = backend.configuration().n_qubits
else:
# Backend is not set. Naively guess qubit size.
basis_gates = None
n_qubits = max(physical_qubits) + 1
circuits = [index_mapper(c, basis_gates, n_qubits, physical_qubits, backend) for c in circuits]
if not translate_and_pulse_gates and custom_translate_and_pulse_gates_check:
for circ in circuits:
for inst in circ.data:
if not isinstance(inst.operation, Gate):
continue
qubits = tuple(circ.find_bit(q).index for q in inst.qubits)
if not backend.target.instruction_supported(inst.operation.name, qubits):
translate_and_pulse_gates = True
break
if not circ.has_calibration_for(inst) and backend.target.has_calibration(
inst.operation.name, qubits
):
cal = backend.target.get_calibration(
inst.operation.name, qubits, *inst.operation.params
)
if (
cal.metadata.get("publisher", CalibrationPublisher.QISKIT)
!= CalibrationPublisher.BACKEND_PROVIDER
):
translate_and_pulse_gates = True
break
if translate_and_pulse_gates:
break
if translate_and_pulse_gates:
pm = build_pass_manager(backend)
circuits = pm.run(circuits, num_processes=num_processes)
return circuits
def t1_circuits_with_translation_pulse_gates_checks(
delays,
backend,
qnum=0,
num_processes=1,
):
circuits = t1_circuits(delays, backend, qnum=qnum)
translate_and_pulse_gates = False
for circ in circuits:
for inst in circ.data:
if not isinstance(inst.operation, Gate):
continue
qubits = tuple(circ.find_bit(q).index for q in inst.qubits)
if not backend.target.instruction_supported(inst.operation.name, qubits):
translate_and_pulse_gates = True
break
if not circ.has_calibration_for(inst) and backend.target.has_calibration(
inst.operation.name, qubits
):
cal = backend.target.get_calibration(
inst.operation.name, qubits, *inst.operation.params
)
if (
cal.metadata.get("publisher", CalibrationPublisher.QISKIT)
!= CalibrationPublisher.BACKEND_PROVIDER
):
translate_and_pulse_gates = True
break
if translate_and_pulse_gates:
break
if translate_and_pulse_gates:
pm = build_pass_manager(backend)
circuits = pm.run(circuits, num_processes=num_processes)
return circuits
def index_mapper(
v_circ: QuantumCircuit,
basis_gates: set[str] | None,
n_qubits: int,
physical_qubits,
backend,
) -> QuantumCircuit:
# Commenting out in favor of other checks
# if basis_gates is not None and not basis_gates.issuperset(
# set(v_circ.count_ops().keys()) - {"barrier"}
# ):
# # In Qiskit provider model barrier is not included in target.
# # Use standard circuit transpile when circuit is not ISA.
# return transpile(
# v_circ,
# backend=backend,
# initial_layout=list(physical_qubits),
# )
p_qregs = QuantumRegister(n_qubits)
v_p_map = {q: p_qregs[physical_qubits[i]] for i, q in enumerate(v_circ.qubits)}
p_circ = QuantumCircuit(p_qregs, *v_circ.cregs)
p_circ.metadata = v_circ.metadata
for inst, v_qubits, clbits in v_circ.data:
p_qubits = list(map(v_p_map.get, v_qubits))
p_circ._append(inst, p_qubits, clbits)
return p_circ
def timeit(name, func, kwargs, min_time=1, max_rounds=100):
start = perf_counter()
func(**kwargs)
run_time = perf_counter() - start
if run_time < min_time:
rounds = min(int(round(min_time / run_time)), max_rounds)
start = perf_counter()
for _ in range(rounds):
func(**kwargs)
run_time = ((perf_counter() - start) + run_time) / (rounds + 1)
print(f"{name}: {run_time:0.2g} seconds per call")
def fast_transpile_t1_circuits(
delays,
backend,
physical_qubits,
translate_and_pulse_gates=False,
custom_translate_and_pulse_gates_check=False,
num_processes=1,
):
circuits = t1_circuits(delays, backend)
circuits = fast_transpile(
circuits,
backend,
physical_qubits,
translate_and_pulse_gates=translate_and_pulse_gates,
custom_translate_and_pulse_gates_check=custom_translate_and_pulse_gates_check,
num_processes=1,
)
return circuits
def build_pass_manager(backend, initial_layout=None):
if initial_layout is not None:
passes = [
SetLayout(list(initial_layout)),
FullAncillaAllocation(backend.coupling_map),
EnlargeWithAncilla(),
ApplyLayout(),
]
else:
passes = []
passes.extend([
BasisTranslator(
sel,
list(backend.target.operation_names),
target=backend.target,
),
PulseGates(target=backend.target),
])
pm = PassManager(passes)
return pm
def t1_transpiled_circuits(delays, backend, physical_qubits, num_processes=1):
circs = t1_circuits(delays, backend)
return transpile(circs, backend, initial_layout=physical_qubits, num_processes=num_processes)
def t1_limited_pm_circuits(delays, backend, physical_qubits, num_processes=1):
circs = t1_circuits(delays, backend)
pm = build_pass_manager(backend, initial_layout=physical_qubits)
return pm.run(circs, num_processes=num_processes)
def test_translation_and_pulse_gates(backend):
"""NOTE: this mutates the Target!"""
circ = QuantumCircuit(1)
circ.rx(pi, 0)
expected = QuantumCircuit(backend.target.num_qubits, global_phase=-pi/2)
expected.x(5)
# Check that basis translation runs okay
#
# These are not great tests...they at least check that the transpile
# variant does not mess up the circuit, but we have to have to transpile
# them again in order to get something that is predictable. Otherwise, we
# get something with rz and sx gates.
tcirc, = fast_transpile([circ], backend, (5,), custom_translate_and_pulse_gates_check=True)
assert all(backend.target.instruction_supported(inst.operation.name, inst.qubits, parameters=inst.operation.params) for inst in tcirc.data)
assert transpile(tcirc, backend, optimization_level=1) == expected
tcirc, = transpile([circ], backend, initial_layout=(5,), optimization_level=0)
assert all(backend.target.instruction_supported(inst.operation.name, inst.qubits, parameters=inst.operation.params) for inst in tcirc.data)
assert transpile(tcirc, backend, optimization_level=1) == expected
pm = build_pass_manager(backend, initial_layout=(5,))
tcirc, = pm.run([circ])
assert all(backend.target.instruction_supported(inst.operation.name, inst.qubits, parameters=inst.operation.params) for inst in tcirc.data)
assert transpile(tcirc, backend, optimization_level=1) == expected
# Check that pulse gate pass runs
with pulse.builder.build() as sched:
pulse.shift_phase(pi, pulse.DriveChannel(5))
backend.target["x"][(5,)].calibration = sched
circ = QuantumCircuit(1)
circ.x(0)
tcirc, = fast_transpile([circ], backend, (5,), custom_translate_and_pulse_gates_check=True)
assert tcirc.calibrations["x"][((5,), ())] == sched
tcirc, = transpile([circ], backend, initial_layout=(5,), optimization_level=0)
assert tcirc.calibrations["x"][((5,), ())] == sched
pm = build_pass_manager(backend, initial_layout=(5,))
tcirc, = pm.run([circ])
assert tcirc.calibrations["x"][((5,), ())] == sched
def main():
service = QiskitRuntimeService(channel="ibm_quantum")
backend = service.get_backend("ibm_sherbrooke")
delays = [1e-6 * t for t in range(100)]
timeit(
"t1_circuits_warmup",
t1_circuits,
{"delays": delays, "backend": backend, "qnum": 5},
)
timeit(
"t1_circuits",
t1_circuits,
{"delays": delays, "backend": backend, "qnum": 5},
)
timeit(
"t1_circuits_with_translation_pulse_gates_checks",
t1_circuits_with_translation_pulse_gates_checks,
{"delays": delays, "backend": backend, "qnum": 5},
)
# Try to warm up any lazy backend loading
timeit(
"warm_up_fast_transpile_no_pm",
fast_transpile_t1_circuits,
{"delays": delays, "backend": backend, "physical_qubits": (5,)},
)
timeit(
"fast_transpile_no_pm",
fast_transpile_t1_circuits,
{"delays": delays, "backend": backend, "physical_qubits": (5,)},
)
# 40 us
timeit(
"build_pass_manager",
build_pass_manager,
{"backend": backend},
)
timeit(
"fast_transpile_t1_serial_with_basis_and_pulse_gate_passes",
fast_transpile_t1_circuits,
{
"delays": delays,
"backend": backend,
"physical_qubits": (5,),
"translate_and_pulse_gates": True,
},
)
# timeit(
# "fast_transpile_t1_parallel_pm",
# fast_transpile_t1_circuits,
# {
# "delays": delays,
# "backend": backend,
# "physical_qubits": (5,),
# "translate_and_pulse_gates": True,
# "num_processes": None,
# },
# )
timeit(
"fast_transpile_t1_serial_with_custom_basis_and_pulsegate_checks",
fast_transpile_t1_circuits,
{
"delays": delays,
"backend": backend,
"physical_qubits": (5,),
"custom_translate_and_pulse_gates_check": True,
},
)
timeit(
"standard_transpile_serial",
t1_transpiled_circuits,
{
"delays": delays,
"backend": backend,
"physical_qubits": (5,),
"num_processes": 1,
},
)
# timeit(
# "standard_transpile_parallel",
# t1_transpiled_circuits,
# {
# "delays": delays,
# "backend": backend,
# "physical_qubits": (5,),
# "num_processes": None,
# },
# )
timeit(
"limited_pm_serial",
t1_limited_pm_circuits,
{
"delays": delays,
"backend": backend,
"physical_qubits": (5,),
"num_processes": 1,
},
)
test_translation_and_pulse_gates(backend)
if __name__ == "__main__":
main()
t1_circuits_warmup: 3.7 seconds per call
t1_circuits: 0.0098 seconds per call
t1_circuits_with_translation_pulse_gates_checks: 0.011 seconds per call
warm_up_fast_transpile_no_pm: 0.076 seconds per call
fast_transpile_no_pm: 0.074 seconds per call
build_pass_manager: 4.3e-05 seconds per call
fast_transpile_t1_serial_with_basis_and_pulse_gate_passes: 0.23 seconds per call
fast_transpile_t1_serial_with_custom_basis_and_pulsegate_checks: 0.079 seconds per call
standard_transpile_serial: 0.34 seconds per call
limited_pm_serial: 0.32 seconds per call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment