Skip to content

Commit

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

* 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 Qiskit#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 <epenatap@gmail.com>
Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
  • Loading branch information
3 people authored and Procatv committed Aug 1, 2024
1 parent b1747ee commit a061908
Show file tree
Hide file tree
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
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,7 +36,7 @@


def generate_preset_pass_manager(
optimization_level,
optimization_level=2,
backend=None,
target=None,
basis_gates=None,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
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
Expand Up @@ -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)
Expand Down
21 changes: 17 additions & 4 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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"})
Expand Down Expand Up @@ -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)
Expand All @@ -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())

Expand Down
6 changes: 4 additions & 2 deletions test/python/primitives/test_backend_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion test/python/primitives/test_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
8 changes: 4 additions & 4 deletions test/python/providers/test_backend_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions test/python/pulse/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions test/python/transpiler/test_basis_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand Down
12 changes: 4 additions & 8 deletions test/python/transpiler/test_high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
18 changes: 18 additions & 0 deletions test/python/transpiler/test_preset_passmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
5 changes: 4 additions & 1 deletion test/python/transpiler/test_sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a061908

Please sign in to comment.