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 7 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 @@ -219,7 +219,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 @@ -313,7 +313,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
20 changes: 16 additions & 4 deletions qiskit/transpiler/preset_passmanagers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@

from qiskit.transpiler.passmanager_config import PassManagerConfig
from qiskit.transpiler.target import Target, target_to_backend_properties
from qiskit.providers.backend import Backend
from qiskit.transpiler import CouplingMap

from qiskit.transpiler.exceptions import TranspilerError
Expand All @@ -79,7 +80,7 @@


def generate_preset_pass_manager(
optimization_level,
optimization_level=2,
backend=None,
target=None,
basis_gates=None,
Expand Down Expand Up @@ -140,9 +141,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 @@ -282,6 +284,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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'd rather this function not support any positional arguments.

I think we want a more ergonomic API on top of this function eventually anyways, e.g. #12161, so it seems too kludgy to me to try and make this accept a target positionally here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#12161 is actually what I'm not sure we want to do. Adding a 3rd API with different semantics to do what we already have two interfaces for seems like a mistake to me. If people really want a different name I feel like we really should just alias it (but I still don't think it's worth it). But adding yet another entrypoint to accomplish the same thing feels like a mistake. If people are complaining about the ergonomics of the existing interface I feel like we should just evolve it in-place instead of diverging it again and requiring people to learn yet another thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's unfortunate that we've ended up here, but I think the right thing to do for users is to make it so the common patterns {transpile,generate_preset_pass_manager}({backend,target}, optimization_level=2) work the same in both forms. It ends up in an ugly signature for us, but we can tidy that up in place and potentially fix the signature properly for Qiskit 2.0+.

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,41 @@
---
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`. 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`. There isn't an API
change though because fundamentally level 2 and level 1 have the same semantics. 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 if you were 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
ElePT marked this conversation as resolved.
Show resolved Hide resolved
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 list previously to make
ElePT marked this conversation as resolved.
Show resolved Hide resolved
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 3) as the additional optimization passes that run in
ElePT marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -391,7 +391,7 @@ def test_layout(self, backend):
op = SparsePauliOp("IZI")
backend.set_options(seed_simulator=15)
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 @@ -406,7 +406,9 @@ def test_layout(self, backend):
qc.cx(0, 2)
op = SparsePauliOp("IZI")
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 @@ -1101,6 +1101,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