Skip to content

Commit 1b43c89

Browse files
authored
Delegate BasePass.__call__ to PassManager.run (#13820)
* Delegate `BasePass.__call__` to `PassManager.run` This ensures that calling a pass directly will follow the same execution logic as a regular pass-manager construction. This particularly matters for passes that have `requires`. It also ensures that the conversion from a circuit and back to a DAG will follow the same rules, which is expected since it is a shorthand notation, but liable to get out of sync as they are complex. * Correctly propagate exceptions through `PassManager.run` * Fix lint
1 parent b77a822 commit 1b43c89

8 files changed

Lines changed: 136 additions & 62 deletions

File tree

qiskit/passmanager/passmanager.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ def run(
174174
in_programs: Any | list[Any],
175175
callback: Callable = None,
176176
num_processes: int = None,
177+
*,
178+
property_set: dict[str, object] | None = None,
177179
**kwargs,
178180
) -> Any:
179181
"""Run all the passes on the specified ``in_programs``.
@@ -211,7 +213,11 @@ def callback_func(**kwargs):
211213
execution is enabled. This argument overrides ``num_processes`` in the user
212214
configuration file, and the ``QISKIT_NUM_PROCS`` environment variable. If set
213215
to ``None`` the system default or local user configuration will be used.
214-
216+
property_set: If given, the initial value to use as the :class:`.PropertySet` for the
217+
pass manager pipeline. This can be used to persist analysis from one run to
218+
another, in cases where you know the analysis is safe to share. Beware that some
219+
analysis will be specific to the input circuit and the particular :class:`.Target`,
220+
so you should take a lot of care when using this argument.
215221
kwargs: Arbitrary arguments passed to the compiler frontend and backend.
216222
217223
Returns:
@@ -229,7 +235,13 @@ def callback_func(**kwargs):
229235
# ourselves, since that can be quite expensive.
230236
if len(in_programs) == 1 or not should_run_in_parallel(num_processes):
231237
out = [
232-
_run_workflow(program=program, pass_manager=self, callback=callback, **kwargs)
238+
_run_workflow(
239+
program=program,
240+
pass_manager=self,
241+
callback=callback,
242+
initial_property_set=property_set,
243+
**kwargs,
244+
)
233245
for program in in_programs
234246
]
235247
if len(in_programs) == 1 and not is_list:
@@ -246,7 +258,10 @@ def callback_func(**kwargs):
246258
return parallel_map(
247259
_run_workflow_in_new_process,
248260
values=in_programs,
249-
task_kwargs={"pass_manager_bin": dill.dumps(self)},
261+
task_kwargs={
262+
"pass_manager_bin": dill.dumps(self),
263+
"initial_property_set": property_set,
264+
},
250265
num_processes=num_processes,
251266
)
252267

@@ -270,6 +285,8 @@ def _flatten_tasks(self, elements: Iterable | Task) -> Iterable:
270285
def _run_workflow(
271286
program: Any,
272287
pass_manager: BasePassManager,
288+
*,
289+
initial_property_set: dict[str, object] | None = None,
273290
**kwargs,
274291
) -> Any:
275292
"""Run single program optimization with a pass manager.
@@ -289,12 +306,12 @@ def _run_workflow(
289306
input_program=program,
290307
**kwargs,
291308
)
309+
property_set = (
310+
PropertySet() if initial_property_set is None else PropertySet(initial_property_set)
311+
)
292312
passmanager_ir, final_state = flow_controller.execute(
293313
passmanager_ir=passmanager_ir,
294-
state=PassManagerState(
295-
workflow_status=initial_status,
296-
property_set=PropertySet(),
297-
),
314+
state=PassManagerState(workflow_status=initial_status, property_set=property_set),
298315
callback=kwargs.get("callback", None),
299316
)
300317
# The `property_set` has historically been returned as a mutable attribute on `PassManager`
@@ -317,6 +334,8 @@ def _run_workflow(
317334
def _run_workflow_in_new_process(
318335
program: Any,
319336
pass_manager_bin: bytes,
337+
*,
338+
initial_property_set: dict[str, object] | None,
320339
) -> Any:
321340
"""Run single program optimization in new process.
322341
@@ -330,4 +349,5 @@ def _run_workflow_in_new_process(
330349
return _run_workflow(
331350
program=program,
332351
pass_manager=dill.loads(pass_manager_bin),
352+
initial_property_set=initial_property_set,
333353
)

qiskit/transpiler/basepasses.py

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@
1919
from inspect import signature
2020

2121
from qiskit.circuit import QuantumCircuit
22-
from qiskit.converters import circuit_to_dag, dag_to_circuit
2322
from qiskit.dagcircuit import DAGCircuit
2423
from qiskit.passmanager.base_tasks import GenericPass, PassManagerIR
2524
from qiskit.passmanager.compilation_status import PropertySet, RunState, PassManagerState
2625

2726
from .exceptions import TranspilerError
28-
from .layout import TranspileLayout
2927

3028

3129
class MetaPass(abc.ABCMeta):
@@ -126,57 +124,31 @@ def __call__(
126124
127125
Args:
128126
circuit: The dag on which the pass is run.
129-
property_set: Input/output property set. An analysis pass
130-
might change the property set in-place.
127+
property_set: Input/output property set. An analysis pass might change the property set
128+
in-place. If not given, the existing ``property_set`` attribute of the pass will
129+
be used (if set).
131130
132131
Returns:
133132
If on transformation pass, the resulting QuantumCircuit.
134133
If analysis pass, the input circuit.
135134
"""
136-
property_set_ = None
137-
if isinstance(property_set, dict): # this includes (dict, PropertySet)
138-
property_set_ = PropertySet(property_set)
139-
140-
if isinstance(property_set_, PropertySet):
141-
# pylint: disable=attribute-defined-outside-init
142-
self.property_set = property_set_
143-
144-
result = self.run(circuit_to_dag(circuit))
145-
146-
result_circuit = circuit
147-
148-
if isinstance(property_set, dict): # this includes (dict, PropertySet)
135+
from qiskit.transpiler import PassManager # pylint: disable=cyclic-import
136+
137+
pm = PassManager([self])
138+
# Previous versions of the `__call__` function would not construct a `PassManager`, but just
139+
# call `self.run` directly (this caused issues with `requires`). It only overrode
140+
# `self.property_set` if the input was not `None`, which some users might have been relying
141+
# on (as our test suite was).
142+
if property_set is None:
143+
property_set = self.property_set
144+
out = pm.run(circuit, property_set=property_set)
145+
if property_set is not None and property_set is not pm.property_set:
146+
# When this `__call__` was first added, it contained this behaviour of mutating the
147+
# input `property_set` in-place, but didn't use the `PassManager` infrastructure. This
148+
# preserves the output-variable nature of the `property_set` parameter.
149149
property_set.clear()
150-
property_set.update(self.property_set)
151-
152-
if isinstance(result, DAGCircuit):
153-
result_circuit = dag_to_circuit(result, copy_operations=False)
154-
elif result is None:
155-
result_circuit = circuit.copy()
156-
157-
if self.property_set["layout"]:
158-
result_circuit._layout = TranspileLayout(
159-
initial_layout=self.property_set["layout"],
160-
input_qubit_mapping=self.property_set["original_qubit_indices"],
161-
final_layout=self.property_set["final_layout"],
162-
_input_qubit_count=len(circuit.qubits),
163-
_output_qubit_list=result_circuit.qubits,
164-
)
165-
if self.property_set["clbit_write_latency"] is not None:
166-
result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"]
167-
if self.property_set["conditional_latency"] is not None:
168-
result_circuit._conditional_latency = self.property_set["conditional_latency"]
169-
if self.property_set["node_start_time"]:
170-
# This is dictionary keyed on the DAGOpNode, which is invalidated once
171-
# dag is converted into circuit. So this schedule information is
172-
# also converted into list with the same ordering with circuit.data.
173-
topological_start_times = []
174-
start_times = self.property_set["node_start_time"]
175-
for dag_node in result.topological_op_nodes():
176-
topological_start_times.append(start_times[dag_node])
177-
result_circuit._op_start_times = topological_start_times
178-
179-
return result_circuit
150+
property_set.update(pm.property_set)
151+
return out
180152

181153

182154
class AnalysisPass(BasePass): # pylint: disable=abstract-method

qiskit/transpiler/passmanager.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ def run( # pylint:disable=arguments-renamed
174174
output_name: str | None = None,
175175
callback: Callable = None,
176176
num_processes: int = None,
177+
*,
178+
property_set: dict[str, object] | None = None,
177179
) -> _CircuitsT:
178180
"""Run all the passes on the specified ``circuits``.
179181
@@ -216,6 +218,11 @@ def callback_func(**kwargs):
216218
execution is enabled. This argument overrides ``num_processes`` in the user
217219
configuration file, and the ``QISKIT_NUM_PROCS`` environment variable. If set
218220
to ``None`` the system default or local user configuration will be used.
221+
property_set: If given, the initial value to use as the :class:`.PropertySet` for the
222+
pass manager pipeline. This can be used to persist analysis from one run to
223+
another, in cases where you know the analysis is safe to share. Beware that some
224+
analysis will be specific to the input circuit and the particular :class:`.Target`,
225+
so you should take a lot of care when using this argument.
219226
220227
Returns:
221228
The transformed circuit(s).
@@ -228,6 +235,7 @@ def callback_func(**kwargs):
228235
callback=callback,
229236
output_name=output_name,
230237
num_processes=num_processes,
238+
property_set=property_set,
231239
)
232240

233241
def draw(self, filename=None, style=None, raw=False):
@@ -436,6 +444,8 @@ def run(
436444
output_name: str | None = None,
437445
callback: Callable | None = None,
438446
num_processes: int = None,
447+
*,
448+
property_set: dict[str, object] | None = None,
439449
) -> _CircuitsT:
440450
self._update_passmanager()
441451
return super().run(circuits, output_name, callback, num_processes=num_processes)
@@ -462,6 +472,9 @@ def _replace_error(meth):
462472
def wrapper(*meth_args, **meth_kwargs):
463473
try:
464474
return meth(*meth_args, **meth_kwargs)
475+
except TranspilerError:
476+
# If it's already a `TranspilerError` subclass, don't erase the extra information.
477+
raise
465478
except PassManagerError as ex:
466479
raise TranspilerError(ex.message) from ex
467480

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
features_transpiler:
3+
- |
4+
:meth:`.PassManager.run` now accepts a ``property_set`` argument, which can be set to a
5+
:class:`~collections.abc.Mapping`-like object to provide the initial values of the pipeline's
6+
:class:`.PropertySet`. This can be used to recommence a partially applied compilation, or to
7+
reuse certain analysis from a prior compilation in a new place.
8+
upgrade_transpiler:
9+
- |
10+
The keyword argument ``property_set`` is now reserved in :meth:`.BasePassManager.run`, and
11+
cannot be used as a ``kwarg`` that will be forwarded to the subclass's conversion from the
12+
front-end representation to the internal representation.
13+
fixes:
14+
- |
15+
Calling an :class:`.AnalysisPass` or a :class:`.TransformationPass` like a function (as in
16+
``pass_ = MyPass(); pass_(qc)``) will now respect any requirements that the pass might have.
17+
For example, scheduling passes such as :class:`.ALAPScheduleAnalysis` require that
18+
:class:`.TimeUnitConversion` runs before them. Running the pass via a :class:`.PassManager`
19+
always respected this requirement, but until this note, it was not respected by calling the
20+
pass directly.
21+
- |
22+
When a :exc:`.TranspilerError` subclass is raised by a pass inside a call to
23+
:meth:`.PassManger.run`, the exception will now be propagated through losslessly, rather than
24+
becoming a chained exception with an erased type.

test/python/transpiler/test_elide_permutations.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,7 @@ def test_partial_permutation_in_middle(self):
225225

226226
# Make sure that the transpiled circuit *with* the final permutation
227227
# is equivalent to the original circuit
228-
perm = pass_.property_set["virtual_permutation_layout"].to_permutation(qc.qubits)
229-
res.append(PermutationGate(perm), [0, 1, 2, 3, 4])
228+
res.append(PermutationGate(res.layout.routing_permutation()), [0, 1, 2, 3, 4])
230229
self.assertEqual(Operator(res), Operator(qc))
231230

232231
def test_permutation_at_beginning(self):

test/python/transpiler/test_pass_call.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"""Test calling passes (passmanager-less)"""
1414

1515
from qiskit import QuantumRegister, QuantumCircuit
16-
from qiskit.transpiler import PropertySet
16+
from qiskit.transpiler import PropertySet, TransformationPass, AnalysisPass
1717
from ._dummy_passes import PassD_TP_NR_NP, PassE_AP_NR_NP, PassN_AP_NR_NP
1818
from test import QiskitTestCase # pylint: disable=wrong-import-order
1919

@@ -90,3 +90,32 @@ def test_analysis_pass_remove_property(self):
9090
self.assertEqual(property_set, PropertySet({"to none": None}))
9191
self.assertIsInstance(property_set, dict)
9292
self.assertEqual(circuit, result)
93+
94+
def test_pass_requires(self):
95+
"""The 'requires' field of a pass should be respected when called."""
96+
name = "my_property"
97+
value = "hello, world"
98+
99+
assert_equal = self.assertEqual
100+
101+
class Analyse(AnalysisPass):
102+
"""Dummy pass to set a property."""
103+
104+
def run(self, dag):
105+
self.property_set[name] = value
106+
return dag
107+
108+
class Execute(TransformationPass):
109+
"""Dummy pass to assert that its required pass ran."""
110+
111+
def __init__(self):
112+
super().__init__()
113+
self.requires.append(Analyse())
114+
115+
def run(self, dag):
116+
assert_equal(self.property_set[name], value)
117+
return dag
118+
119+
pass_ = Execute()
120+
pass_(QuantumCircuit())
121+
self.assertEqual(pass_.property_set[name], value)

test/python/transpiler/test_passmanager.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
ConditionalController,
2727
DoWhileController,
2828
)
29-
from qiskit.transpiler import PassManager, PropertySet, TransformationPass
29+
from qiskit.transpiler import PassManager, PropertySet, TransformationPass, AnalysisPass
3030
from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator, ResourceEstimation
3131
from qiskit.circuit.library.standard_gates.equivalence_library import (
3232
StandardEquivalenceLibrary as std_eqlib,
@@ -191,3 +191,20 @@ def callback(pass_, **_):
191191
"third 4",
192192
]
193193
self.assertEqual(calls, expected)
194+
195+
def test_override_initial_property_set(self):
196+
"""Test that the ``property_set`` argument allows seeding the base analysis."""
197+
input_name = "my_property"
198+
output_name = "output_property"
199+
200+
class Analyse(AnalysisPass):
201+
def run(self, dag):
202+
self.property_set[output_name] = self.property_set[input_name]
203+
return dag
204+
205+
pm = PassManager([Analyse()])
206+
pm.run(QuantumCircuit(), property_set={input_name: "hello, world"})
207+
self.assertEqual(pm.property_set[output_name], "hello, world")
208+
209+
pm.run(QuantumCircuit(), property_set={input_name: "a different string"})
210+
self.assertEqual(pm.property_set[output_name], "a different string")

test/python/visualization/test_circuit_text_drawer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3275,8 +3275,8 @@ def test_mixed_layout(self):
32753275
circuit.h(qr)
32763276

32773277
pass_ = ApplyLayout()
3278-
pass_.property_set["layout"] = Layout({qr[0]: 0, ancilla[1]: 1, ancilla[0]: 2, qr[1]: 3})
3279-
circuit_with_layout = pass_(circuit)
3278+
layout = Layout({qr[0]: 0, ancilla[1]: 1, ancilla[0]: 2, qr[1]: 3})
3279+
circuit_with_layout = pass_(circuit, property_set={"layout": layout})
32803280

32813281
self.assertEqual(
32823282
str(circuit_drawer(circuit_with_layout, output="text", initial_state=True)), expected
@@ -3337,8 +3337,8 @@ def test_with_classical_regs(self):
33373337
circuit.measure(qr2[1], cr[1])
33383338

33393339
pass_ = ApplyLayout()
3340-
pass_.property_set["layout"] = Layout({qr1[0]: 0, qr1[1]: 1, qr2[0]: 2, qr2[1]: 3})
3341-
circuit_with_layout = pass_(circuit)
3340+
layout = Layout({qr1[0]: 0, qr1[1]: 1, qr2[0]: 2, qr2[1]: 3})
3341+
circuit_with_layout = pass_(circuit, property_set={"layout": layout})
33423342

33433343
self.assertEqual(
33443344
str(circuit_drawer(circuit_with_layout, output="text", initial_state=True)), expected

0 commit comments

Comments
 (0)