Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a default optimization level to generate_preset_pass_manager #12150

Merged
merged 14 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading