Skip to content

Commit

Permalink
Add a default optimization level to generate_preset_pass_manager (#12150
Browse files Browse the repository at this point in the history
)

* Add a default optimization level to generate_preset_pass_manager

This commit adds a default value to the generate_preset_pass_manager's
optimization_level argument. If it's not specified optimization level 2
will be used. After #12148 optimization level 2 is a better fit for an
optimal tradeoff between heuristic effort and runtime that makes it
well suited as a default optimization level.

* Update transpile()'s default opt level to match

This commit updates the transpile() function's optimization_level argument
default value to match generate_preset_pass_manager's new default to use 2
instead of 1. This is arguably a breaking API change, but since the
semantics are equivalent with two minor edge cases with implicit behavior
that were a side effect of the level 1 preset pass manager's construction
(which are documented in the release notes) we're ok making it in this
case. Some tests which we're relying on the implicit behavior of
optimization level 1 are updated to explicitly set the optimization
level argument which will retain this behavior.

* Update more tests expecting optimization level 1

* * Set optimization level to 1 in test_approximation_degree.

* Replace use of transpile with specific pass in  HLS tests.

* Set optimization_level=1 in layout-dependent tests.

* Expand upgrade note explanation on benefits of level 2

* Apply Elena's reno suggestions

---------

Co-authored-by: Elena Peña Tapia <[email protected]>
Co-authored-by: Elena Peña Tapia <[email protected]>
3 people authored Jul 26, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 1512535 commit c8c53cc
Showing 13 changed files with 121 additions and 30 deletions.
4 changes: 2 additions & 2 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
@@ -218,7 +218,7 @@ def transpile( # pylint: disable=too-many-return-statements
* 2: heavy optimization
* 3: even heavier optimization
If ``None``, level 1 will be chosen as default.
If ``None``, level 2 will be chosen as default.
callback: A callback function that will be called after each
pass execution. The function will be called with 5 keyword
arguments,
@@ -312,7 +312,7 @@ def callback_func(**kwargs):
if optimization_level is None:
# Take optimization level from the configuration or 1 as default.
config = user_config.get_config()
optimization_level = config.get("transpile_optimization_level", 1)
optimization_level = config.get("transpile_optimization_level", 2)

if backend is not None and getattr(backend, "version", 0) <= 1:
# This is a temporary conversion step to allow for a smoother transition
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES
from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping
from qiskit.circuit.quantumregister import Qubit
from qiskit.providers.backend import Backend
from qiskit.providers.backend_compat import BackendV2Converter
from qiskit.transpiler.coupling import CouplingMap
from qiskit.transpiler.exceptions import TranspilerError
@@ -35,7 +36,7 @@


def generate_preset_pass_manager(
optimization_level,
optimization_level=2,
backend=None,
target=None,
basis_gates=None,
@@ -96,9 +97,10 @@ def generate_preset_pass_manager(
Args:
optimization_level (int): The optimization level to generate a
:class:`~.PassManager` for. This can be 0, 1, 2, or 3. Higher
levels generate more optimized circuits, at the expense of
longer transpilation time:
:class:`~.StagedPassManager` for. By default optimization level 2
is used if this is not specified. This can be 0, 1, 2, or 3. Higher
levels generate potentially more optimized circuits, at the expense
of longer transpilation time:
* 0: no optimization
* 1: light optimization
@@ -238,6 +240,16 @@ def generate_preset_pass_manager(
ValueError: if an invalid value for ``optimization_level`` is passed in.
"""

# Handle positional arguments for target and backend. This enables the usage
# pattern `generate_preset_pass_manager(backend.target)` to generate a default
# pass manager for a given target.
if isinstance(optimization_level, Target):
target = optimization_level
optimization_level = 2
elif isinstance(optimization_level, Backend):
backend = optimization_level
optimization_level = 2

if backend is not None and getattr(backend, "version", 0) <= 1:
# This is a temporary conversion step to allow for a smoother transition
# to a fully target-based transpiler pipeline while maintaining the behavior
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
features_transpiler:
- |
The ``optimization_level`` argument for the :func:`.generate_preset_pass_manager` function is
now optional. If it's not specified it will default to using optimization level 2. As the argument
is now optional, the first positional argument has been expanded to enable passing a :class:`.Target`
or a :class:`.BackendV2` as the first argument for more convenient construction. For example::
from qiskit.transpiler.preset_passmanager import generate_preset_pass_manager
from qiskit.providers.fake_provider import GenericBackendV2
backend = GenericBackendV2(100)
generate_preset_pass_manager(backend.Target)
will construct a default pass manager for the 100 qubit :class`.GenericBackendV2` instance.
upgrade_transpiler:
- |
The default ``optimization_level`` used by the :func:`.transpile` function when one is not
specified has been changed to level 2. This makes it consistent with the default used
by :func:`.generate_preset_pass_manager` which is used internally by :func:`.transpile`. Optimization
level 2 provides a much better balance between the run time of the function and the optimizations it
performs, it's a better tradeoff to use by default.
The API of :func:`.transpile` remains unchanged because, fundamentally, level 2 and level 1
have the same semantics. If you were previously relying on the implicit default of level 1,
you can simply set the argument ``optimization_level=1`` when you call :func:`.transpile`.
Similarly you can change the default back in your local environment by using a user config
file and setting the ``transpile_optimization_level`` field to 1.
The only potential issue is that your transpilation workflow may be relying on an implicit trivial layout (where qubit 0
in the circuit passed to :func:`.transpile` is mapped to qubit 0 on the target backend/coupling,
1->1, 2->2, etc.) without specifying ``optimization_level=1``, ``layout_method="trivial"``, or
explicitly setting ``initial_layout`` when calling :func:`.transpile`. This behavior was a side
effect of the preset pass manager construction in optimization level 1 and is not mirrored in
level 2. If you need this behavior you can use any of the three options listed previously to make
this behavior explicit.
Similarly, if you were targeting a discrete basis gate set you may encounter an issue using the
new default with optimization level 2 (or running explicitly optimization level 3), as the additional optimization passes that run in
level 2 and 3 don't work in all cases with a discrete basis. You can explicitly set
``optimization_level=1`` manually in this case. In general the transpiler does not currently
fully support discrete basis sets and if you're relying on this you should likely construct a
pass manager manually to build a compilation pipeline that will work with your target.
2 changes: 1 addition & 1 deletion test/python/circuit/library/test_qft.py
Original file line number Diff line number Diff line change
@@ -139,7 +139,7 @@ def test_qft_num_gates(self, num_qubits, approximation_degree, insert_barriers):
qft = QFT(
num_qubits, approximation_degree=approximation_degree, insert_barriers=insert_barriers
)
ops = transpile(qft, basis_gates=basis_gates).count_ops()
ops = transpile(qft, basis_gates=basis_gates, optimization_level=1).count_ops()

with self.subTest(msg="assert H count"):
self.assertEqual(ops["h"], num_qubits)
21 changes: 17 additions & 4 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
@@ -517,11 +517,21 @@ def test_transpile_bell_discrete_basis(self):

# Try with the initial layout in both directions to ensure we're dealing with the basis
# having only a single direction.

# Use optimization level=1 because the synthesis that runs as part of optimization at
# higher optimization levels will create intermediate gates that the transpiler currently
# lacks logic to translate to a discrete basis.
self.assertIsInstance(
transpile(qc, target=target, initial_layout=[0, 1], seed_transpiler=42), QuantumCircuit
transpile(
qc, target=target, initial_layout=[0, 1], seed_transpiler=42, optimization_level=1
),
QuantumCircuit,
)
self.assertIsInstance(
transpile(qc, target=target, initial_layout=[1, 0], seed_transpiler=42), QuantumCircuit
transpile(
qc, target=target, initial_layout=[1, 0], seed_transpiler=42, optimization_level=1
),
QuantumCircuit,
)

def test_transpile_one(self):
@@ -1318,6 +1328,7 @@ def test_transpile_calibrated_custom_gate_on_diff_qubit(self):
backend=GenericBackendV2(num_qubits=4),
layout_method="trivial",
seed_transpiler=42,
optimization_level=1,
)

def test_transpile_calibrated_nonbasis_gate_on_diff_qubit(self):
@@ -1334,7 +1345,7 @@ def test_transpile_calibrated_nonbasis_gate_on_diff_qubit(self):
circ.add_calibration("h", [1], q0_x180)

transpiled_circuit = transpile(
circ, backend=GenericBackendV2(num_qubits=4), seed_transpiler=42
circ, backend=GenericBackendV2(num_qubits=4), seed_transpiler=42, optimization_level=1
)
self.assertEqual(transpiled_circuit.calibrations, circ.calibrations)
self.assertEqual(set(transpiled_circuit.count_ops().keys()), {"rz", "sx", "h"})
@@ -1781,7 +1792,7 @@ def test_approximation_degree_invalid(self):
)

def test_approximation_degree(self):
"""Test more approximation gives lower-cost circuit."""
"""Test more approximation can give lower-cost circuit."""
circuit = QuantumCircuit(2)
circuit.swap(0, 1)
circuit.h(0)
@@ -1791,13 +1802,15 @@ def test_approximation_degree(self):
translation_method="synthesis",
approximation_degree=0.1,
seed_transpiler=42,
optimization_level=1,
)
circ_90 = transpile(
circuit,
basis_gates=["u", "cx"],
translation_method="synthesis",
approximation_degree=0.9,
seed_transpiler=42,
optimization_level=1,
)
self.assertLess(circ_10.depth(), circ_90.depth())

6 changes: 4 additions & 2 deletions test/python/primitives/test_backend_estimator.py
Original file line number Diff line number Diff line change
@@ -430,7 +430,7 @@ def test_layout(self, backend):
backend.set_options(seed_simulator=15)
with self.assertWarns(DeprecationWarning):
estimator = BackendEstimator(backend)
estimator.set_transpile_options(seed_transpiler=15)
estimator.set_transpile_options(seed_transpiler=15, optimization_level=1)
value = estimator.run(qc, op, shots=10000).result().values[0]
if optionals.HAS_AER:
ref_value = -0.9954 if isinstance(backend, GenericBackendV2) else -0.916
@@ -446,7 +446,9 @@ def test_layout(self, backend):
op = SparsePauliOp("IZI")
with self.assertWarns(DeprecationWarning):
estimator = BackendEstimator(backend)
estimator.set_transpile_options(initial_layout=[0, 1, 2], seed_transpiler=15)
estimator.set_transpile_options(
initial_layout=[0, 1, 2], seed_transpiler=15, optimization_level=1
)
estimator.set_options(seed_simulator=15)
value = estimator.run(qc, op, shots=10000).result().values[0]
if optionals.HAS_AER:
2 changes: 1 addition & 1 deletion test/python/primitives/test_primitive.py
Original file line number Diff line number Diff line change
@@ -142,7 +142,7 @@ def test_with_scheduling(n):
qc = QuantumCircuit(1)
qc.x(0)
qc.add_calibration("x", qubits=(0,), schedule=custom_gate)
return transpile(qc, Fake20QV1(), scheduling_method="alap")
return transpile(qc, Fake20QV1(), scheduling_method="alap", optimization_level=1)

keys = [_circuit_key(test_with_scheduling(i)) for i in range(1, 5)]
self.assertEqual(len(keys), len(set(keys)))
8 changes: 4 additions & 4 deletions test/python/providers/test_backend_v2.py
Original file line number Diff line number Diff line change
@@ -147,7 +147,7 @@ def test_transpile_respects_arg_constraints(self):
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(1, 0)
tqc = transpile(qc, self.backend)
tqc = transpile(qc, self.backend, optimization_level=1)
self.assertTrue(Operator.from_circuit(tqc).equiv(qc))
# Below is done to check we're decomposing cx(1, 0) with extra
# rotations to correct for direction. However because of fp
@@ -163,7 +163,7 @@ def test_transpile_respects_arg_constraints(self):
qc = QuantumCircuit(2)
qc.h(0)
qc.ecr(0, 1)
tqc = transpile(qc, self.backend)
tqc = transpile(qc, self.backend, optimization_level=1)
self.assertTrue(Operator.from_circuit(tqc).equiv(qc))
self.assertEqual(tqc.count_ops(), {"ecr": 1, "u": 4})
self.assertMatchesTargetConstraints(tqc, self.backend.target)
@@ -173,7 +173,7 @@ def test_transpile_relies_on_gate_direction(self):
qc = QuantumCircuit(2)
qc.h(0)
qc.ecr(0, 1)
tqc = transpile(qc, self.backend)
tqc = transpile(qc, self.backend, optimization_level=1)
expected = QuantumCircuit(2)
expected.u(0, 0, -math.pi, 0)
expected.u(math.pi / 2, 0, 0, 1)
@@ -191,7 +191,7 @@ def test_transpile_mumbai_target(self):
qc.h(0)
qc.cx(1, 0)
qc.measure_all()
tqc = transpile(qc, backend)
tqc = transpile(qc, backend, optimization_level=1)
qr = QuantumRegister(27, "q")
cr = ClassicalRegister(2, "meas")
expected = QuantumCircuit(qr, cr, global_phase=math.pi / 4)
8 changes: 5 additions & 3 deletions test/python/pulse/test_builder.py
Original file line number Diff line number Diff line change
@@ -764,7 +764,9 @@ def get_sched(qubit_idx: [int], backend):
qc = circuit.QuantumCircuit(2)
for idx in qubit_idx:
qc.append(circuit.library.U2Gate(0, pi / 2), [idx])
return compiler.schedule(compiler.transpile(qc, backend=backend), backend)
return compiler.schedule(
compiler.transpile(qc, backend=backend, optimization_level=1), backend
)

with pulse.build(self.backend) as schedule:
with pulse.align_sequential():
@@ -784,7 +786,7 @@ def get_sched(qubit_idx: [int], backend):
# prepare and schedule circuits that will be used.
single_u2_qc = circuit.QuantumCircuit(2)
single_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1])
single_u2_qc = compiler.transpile(single_u2_qc, self.backend)
single_u2_qc = compiler.transpile(single_u2_qc, self.backend, optimization_level=1)
single_u2_sched = compiler.schedule(single_u2_qc, self.backend)

# sequential context
@@ -809,7 +811,7 @@ def get_sched(qubit_idx: [int], backend):
triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0])
triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1])
triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0])
triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend)
triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend, optimization_level=1)
align_left_reference = compiler.schedule(triple_u2_qc, self.backend, method="alap")

# measurement
1 change: 1 addition & 0 deletions test/python/transpiler/test_basis_translator.py
Original file line number Diff line number Diff line change
@@ -1106,6 +1106,7 @@ def test_skip_target_basis_equivalences_1(self):
circ,
basis_gates=["id", "rz", "sx", "x", "cx"],
seed_transpiler=42,
optimization_level=1,
)
self.assertEqual(circ_transpiled.count_ops(), {"cx": 91, "rz": 66, "sx": 22})

12 changes: 4 additions & 8 deletions test/python/transpiler/test_high_level_synthesis.py
Original file line number Diff line number Diff line change
@@ -2118,23 +2118,19 @@ def test_qft_plugins_qft(self, qft_plugin_name):
qc.cx(1, 3)
qc.append(QFTGate(3).inverse(), [0, 1, 2])
hls_config = HLSConfig(qft=[qft_plugin_name])
basis_gates = ["cx", "u"]
qct = transpile(qc, hls_config=hls_config, basis_gates=basis_gates)
hls_pass = HighLevelSynthesis(hls_config=hls_config)
qct = hls_pass(qc)
self.assertEqual(Operator(qc), Operator(qct))
ops = set(qct.count_ops().keys())
self.assertEqual(ops, {"u", "cx"})

@data("line", "full")
def test_qft_line_plugin_annotated_qft(self, qft_plugin_name):
"""Test QFTSynthesisLine plugin for circuits with annotated QFTGates."""
qc = QuantumCircuit(4)
qc.append(QFTGate(3).inverse(annotated=True).control(annotated=True), [0, 1, 2, 3])
hls_config = HLSConfig(qft=[qft_plugin_name])
basis_gates = ["cx", "u"]
qct = transpile(qc, hls_config=hls_config, basis_gates=basis_gates)
hls_pass = HighLevelSynthesis(hls_config=hls_config)
qct = hls_pass(qc)
self.assertEqual(Operator(qc), Operator(qct))
ops = set(qct.count_ops().keys())
self.assertEqual(ops, {"u", "cx"})


if __name__ == "__main__":
18 changes: 18 additions & 0 deletions test/python/transpiler/test_preset_passmanagers.py
Original file line number Diff line number Diff line change
@@ -1219,6 +1219,24 @@ def test_with_backend(self, optimization_level):
pm = generate_preset_pass_manager(optimization_level, target)
self.assertIsInstance(pm, PassManager)

def test_default_optimization_level(self):
"""Test a pass manager is constructed with no optimization level."""
backend = GenericBackendV2(num_qubits=14, coupling_map=MELBOURNE_CMAP)
pm = generate_preset_pass_manager(backend=backend)
self.assertIsInstance(pm, PassManager)

def test_default_optimization_level_backend_first_pos_arg(self):
"""Test a pass manager is constructed with only a positional backend."""
backend = GenericBackendV2(num_qubits=14, coupling_map=MELBOURNE_CMAP)
pm = generate_preset_pass_manager(backend)
self.assertIsInstance(pm, PassManager)

def test_default_optimization_level_target_first_pos_arg(self):
"""Test a pass manager is constructed with only a positional target."""
backend = GenericBackendV2(num_qubits=14, coupling_map=MELBOURNE_CMAP)
pm = generate_preset_pass_manager(backend.target)
self.assertIsInstance(pm, PassManager)

@data(0, 1, 2, 3)
def test_with_no_backend(self, optimization_level):
"""Test a passmanager is constructed with no backend and optimization level."""
5 changes: 4 additions & 1 deletion test/python/transpiler/test_sabre_layout.py
Original file line number Diff line number Diff line change
@@ -195,7 +195,9 @@ def test_layout_with_classical_bits(self):
rz(0) q4835[1];
"""
)
res = transpile(qc, Fake27QPulseV1(), layout_method="sabre", seed_transpiler=1234)
res = transpile(
qc, Fake27QPulseV1(), layout_method="sabre", seed_transpiler=1234, optimization_level=1
)
self.assertIsInstance(res, QuantumCircuit)
layout = res._layout.initial_layout
self.assertEqual(
@@ -251,6 +253,7 @@ def test_layout_many_search_trials(self):
layout_method="sabre",
routing_method="stochastic",
seed_transpiler=12345,
optimization_level=1,
)
self.assertIsInstance(res, QuantumCircuit)
layout = res._layout.initial_layout

0 comments on commit c8c53cc

Please sign in to comment.