Last active
May 22, 2024 21:22
-
-
Save wshanks/4992ecff91198795ea99e86c044af5dd to your computer and use it in GitHub Desktop.
Limited transpilation benchmarking
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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