From 77d717f20d6f8edb2373378945843f0d7fc2255e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 Aug 2022 10:52:21 -0400 Subject: [PATCH 01/12] Add support for custom backend transpiler stages This commit adds initial support to the transpiler and backend interfaces for backends to specify custom transpiler stages. There are often specific hardware compilation requirements that a general purpose transpiler's preset pass manager can't account for. While we strive to provide interfaces to outline all the hardware constraints via the Target class and general purposes passes to fit and optimize an input circuit to the target backend there are some constraints that aren't easily addressed in a general purpose way. For such cases having an interface for a specific backend which has the necessary context of its own constraints to provide custom compilation steps to integrate into the pipeline is a necessary feature. The two initial examples of this are custom basis gate translation and custom scheduling. This commit adds two new hook point methods for BackendV2 objects, get_post_translation_stage() and get_scheduling_stage(). These allow for backends to specify custom PassManager objects that will run after the translation stage and for the scheduling stage by default when compilation is targetting the backend. This should enable backends with custom hardware specific requirements to influence the compilation process so that any required custom steps to ensure the output circuit is executable. In the future we may add additional hook points in a similar manner to enable backends to assert more hardware-specific compilation where the need arises. Closes #8329 --- qiskit/compiler/transpiler.py | 5 ++ qiskit/providers/__init__.py | 48 +++++++++++++ qiskit/providers/backend.py | 14 ++++ qiskit/transpiler/passmanager_config.py | 10 ++- .../transpiler/preset_passmanagers/level0.py | 6 ++ .../transpiler/preset_passmanagers/level1.py | 7 ++ .../transpiler/preset_passmanagers/level2.py | 7 ++ .../transpiler/preset_passmanagers/level3.py | 13 ++-- ...ackend-custom-passes-cddfd05c8704a4b1.yaml | 11 +++ ...age-plugin-interface-47daae40f7d0ad3c.yaml | 13 ++-- .../transpiler/test_preset_passmanagers.py | 70 ++++++++++++++++++- 11 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index d5545e9fdda6..020a59f3c9e0 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -660,6 +660,9 @@ def _parse_transpile_args( } list_transpile_args = [] + if scheduling_method is None and hasattr(backend, "get_scheduling_stage"): + scheduling_method = backend.get_scheduling_stage() + for key, value in { "inst_map": inst_map, "coupling_map": coupling_map, @@ -691,6 +694,8 @@ def _parse_transpile_args( "pass_manager_config": kwargs, } list_transpile_args.append(transpile_args) + if hasattr(backend, "get_post_translation_stage"): + shared_dict["post_translation_pm"] = backend.get_post_translation_stage() return list_transpile_args, shared_dict diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index e83b145b2f5a..9ada9625f245 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -404,6 +404,54 @@ def _define(self): transpiler will ensure that it continues to be well supported by Qiskit moving forward. +.. _custom_transpiler_backend: + +Custom Transpiler Passes +^^^^^^^^^^^^^^^^^^^^^^^^ +As part of the transpiler there is a provision for backends to provide custom +stage implementation to facilitate hardware specific optimizations and +circuit transformations. Currently there are two hook points supported, +``get_post_translation_stage()`` which is used for a backend to specify a +:class:`~PassManager` which will be run after basis translation stage in the +compiler and ``get_scheduling_stage()`` which is used for a backend to +specify a :class:`~PassManager` which will be run for the scheduling stage +by default (which is the last defined stage in a default compilation). These +hook points in a :class:`~.BackendV2` class should only be used if your +backend has special requirements for compilation that are not met by the +default backend + +To leverage these hook points you just need to add the methods to your +:class:`~.BackendV2` implementation and have them return a +:class:`~.PassManager` object. For example:: + + from qiskit.circuit.library import XGate + from qiskit.transpiler.passes import ( + ALAPScheduleAnalysis, + PadDynamicalDecoupling, + ResetAfterMeasureSimplification + ) + + class Mybackend(BackendV2): + + def get_scheduling_stage(self): + dd_sequence = [XGate(), XGate()] + pm = PassManager([ + ALAPScheduleAnalysis(self.instruction_durations), + PadDynamicalDecoupling(self.instruction_durations, dd_sequence) + ]) + return pm + + def get_post_translation_stage(self): + pm = PassManager([ResetAfterMeasureSimplification()]) + return pm + +This snippet of a backend implementation will now have the :func:`~.transpile` +function run a custom stage for scheduling (unless the user manually requests a +different one explicitly) which will insert dynamical decoupling sequences and +also simplify resets after measurements after the basis translation stage. This +way if these two compilation steps are **required** for running on ``Mybackend`` +the transpiler will be able to perform these steps without any manual user input. + Run Method ---------- diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 7ed69cadfa12..9db3e74a8000 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -290,6 +290,20 @@ class BackendV2(Backend, ABC): will build a :class:`~qiskit.providers.models.BackendConfiguration` object and :class:`~qiskit.providers.models.BackendProperties` from the attributes defined in this class for backwards compatibility. + + A backend object can optionally contain methods named + ``get_post_translation_stage`` and ``get_scheduling_stage``. If these + methods are present on a backend object and this object is used for + :func:`~.transpile` or :func:`~.generate_preset_pass_manager` the + transpilation process will default to using the output from those methods + as the scheduling stage and the post-translation compilation stage. This + enables a backend which has custom requirements for compilation to transform + the circuit to ensure it is runnable on the backend. These hooks are enabled + by default and should only be used to enable extra compilation steps + if they are **required** to ensure a circuit is executable on the backend. + These methods are passed no input arguments and are expected to return + a :class:`~.PassManager` object representing that stage of the transpilation + process. """ version = 2 diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index fc412dc4f286..23309e053045 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -41,7 +41,7 @@ def __init__( target=None, init_method=None, optimization_method=None, - optimization_level=None, + post_translation_pm=None, ): """Initialize a PassManagerConfig object @@ -81,6 +81,8 @@ def __init__( optimization_method (str): The plugin name for the optimization stage plugin to use. optimization_level (int): The optimization level being used for compilation. + post_translation_pm (PassManager): An optional pass manager representing a + post-translation stage. """ self.initial_layout = initial_layout self.basis_gates = basis_gates @@ -100,7 +102,7 @@ def __init__( self.unitary_synthesis_method = unitary_synthesis_method self.unitary_synthesis_plugin_config = unitary_synthesis_plugin_config self.target = target - self.optimization_level = optimization_level + self.post_translation_pm = post_translation_pm @classmethod def from_backend(cls, backend, **pass_manager_options): @@ -152,6 +154,10 @@ def from_backend(cls, backend, **pass_manager_options): if res.target is None: if backend_version >= 2: res.target = backend.target + if res.scheduling_method is None and hasattr(backend, "get_scheduling_stage"): + res.scheduling_method = backend.get_scheduling_stage() + if hasattr(backend, "get_post_translation_stage"): + res.post_translation_pm = backend.get_post_translation_stage() return res def __str__(self): diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index c02c517a23e1..7b76262b477b 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -174,6 +174,8 @@ def _choose_layout_condition(property_set): sched = common.generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map ) + elif isinstance(scheduling_method, PassManager): + sched = scheduling_method else: sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=0 @@ -189,6 +191,9 @@ def _choose_layout_condition(property_set): optimization = plugin_manager.get_passmanager_stage( "optimization", optimization_method, pass_manager_config, optimization_level=0 ) + post_translation = None + if pass_manager_config.post_translation_pm is not None: + post_translation = pass_manager_config.post_translation_pm return StagedPassManager( init=init, @@ -196,6 +201,7 @@ def _choose_layout_condition(property_set): pre_routing=pre_routing, routing=routing, translation=translation, + post_translation=post_translation, pre_optimization=pre_opt, optimization=optimization, scheduling=sched, diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 05e7d56a736e..1e08b0cce05c 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -266,6 +266,8 @@ def _opt_control(property_set): sched = common.generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map ) + elif isinstance(scheduling_method, PassManager): + sched = scheduling_method else: sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=1 @@ -277,12 +279,17 @@ def _opt_control(property_set): else: init = unroll_3q + post_translation = None + if pass_manager_config.post_translation_pm is not None: + post_translation = pass_manager_config.post_translation_pm + return StagedPassManager( init=init, layout=layout, pre_routing=pre_routing, routing=routing, translation=translation, + post_translation=post_translation, pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 5a8dcde691e8..dfd1a44f3653 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -242,6 +242,8 @@ def _opt_control(property_set): sched = common.generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map ) + elif isinstance(scheduling_method, PassManager): + sched = scheduling_method else: sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=2 @@ -253,12 +255,17 @@ def _opt_control(property_set): else: init = unroll_3q + post_translation = None + if pass_manager_config.post_translation_pm is not None: + post_translation = pass_manager_config.post_translation_pm + return StagedPassManager( init=init, layout=layout, pre_routing=pre_routing, routing=routing, translation=translation, + post_translation=post_translation, pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 0d664f2172a0..aecf5cd847ee 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -91,11 +91,6 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config target = pass_manager_config.target - # Override an unset optimization_level for stage plugin use. - # it will be restored to None before this is returned - optimization_level = pass_manager_config.optimization_level - if optimization_level is None: - pass_manager_config.optimization_level = 3 # Layout on good qubits if calibration info available, otherwise on dense links _given_layout = SetLayout(initial_layout) @@ -295,13 +290,16 @@ def _opt_control(property_set): sched = common.generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map ) + elif isinstance(scheduling_method, PassManager): + sched = scheduling_method else: sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=3 ) - # Restore PassManagerConfig optimization_level override - pass_manager_config.optimization_level = optimization_level + post_translation = None + if pass_manager_config.post_translation_pm is not None: + post_translation = pass_manager_config.post_translation_pm return StagedPassManager( init=init, @@ -309,6 +307,7 @@ def _opt_control(property_set): pre_routing=pre_routing, routing=routing, translation=translation, + post_translation=post_translation, pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, diff --git a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml new file mode 100644 index 000000000000..e63bb50d2e85 --- /dev/null +++ b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + The :class:`~.BackendV2` class now has support for two new optional hook + points enabling backends to inject custom compilation steps as part of + :func:`~.transpile` and :func:`~.generate_preset_pass_manager`. If a + :class:`~.BackendV2` implementation includes the methods + ``get_scheduling_stage()`` or ``get_post_translation_stage()`` the + transpiler will use the returned :class:`~.PassManager` object to run + additional custom transpiler passes when targetting that backend. + For more details on how to use this see :ref:`custom_transpiler_backend` diff --git a/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml b/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml index ea4c88c097df..cb4c5638b544 100644 --- a/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml +++ b/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml @@ -21,12 +21,9 @@ features: ``optimization_method`` which are used to specify alternative plugins to use for the ``init`` stage and ``optimization`` stages respectively. - | - The :class:`~.PassManagerConfig` class has 3 new attributes, - :attr:`~.PassManagerConfig.init_method`, - :attr:`~.PassManagerConfig.optimization_method`, and - :attr:`~.PassManagerConfig.optimization_level` along with matching keyword - arguments on the constructor methods. The first two attributes represent + The :class:`~.PassManagerConfig` class has 2 new attributes, + :attr:`~.PassManagerConfig.init_method` and + :attr:`~.PassManagerConfig.optimization_method` + along with matching keyword arguments on the constructor methods. These represent the user specified ``init`` and ``optimization`` plugins to use for - compilation. The :attr:`~.PassManagerConfig.optimization_level` attribute - represents the compilations optimization level if specified which can - be used to inform stage plugin behavior. + compilation. diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 2ce8ef1d0343..8f5e148da17e 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -23,7 +23,12 @@ from qiskit.circuit import Qubit from qiskit.compiler import transpile, assemble from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError -from qiskit.circuit.library import U2Gate, U3Gate +from qiskit.transpiler.passes import ( + ALAPScheduleAnalysis, + PadDynamicalDecoupling, + RemoveResetInZeroState, +) +from qiskit.circuit.library import U2Gate, U3Gate, XGate from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import ( FakeTenerife, @@ -404,6 +409,37 @@ def test_partial_layout_fully_connected_cm(self, level): Layout.from_qubit_list([ancilla[0], ancilla[1], qr[1], ancilla[2], qr[0]]), ) + @data(0, 1, 2, 3) + def test_backend_with_custom_stages(self, optimization_level): + """Test transpile() executes backend specific custom stage.""" + target = FakeLagosV2() + + def get_scheduling_stage(backend): + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + ALAPScheduleAnalysis(backend.instruction_durations), + PadDynamicalDecoupling(backend.instruction_durations, dd_sequence), + ] + ) + return pm + + def get_post_translation_stage(_backend): + pm = PassManager([RemoveResetInZeroState()]) + return pm + + target.get_scheduling_stage = get_scheduling_stage + target.get_post_translation_stage = get_post_translation_stage + + qr = QuantumRegister(2, "q") + qc = QuantumCircuit(qr) + qc.h(qr[0]) + qc.cx(qr[0], qr[1]) + _ = transpile(qc, target, optimization_level=optimization_level, callback=self.callback) + self.assertIn("ALAPScheduleAnalysis", self.passes) + self.assertIn("PadDynamicalDecoupling", self.passes) + self.assertIn("RemoveResetInZeroState()", self.passes) + @ddt class TestInitialLayouts(QiskitTestCase): @@ -1025,3 +1061,35 @@ def test_invalid_optimization_level(self): """Assert we fail with an invalid optimization_level.""" with self.assertRaises(ValueError): generate_preset_pass_manager(42) + + @data(0, 1, 2, 3) + def test_backend_with_custom_stages(self, optimization_level): + """Test generated preset pass manager includes backend specific custom stages.""" + target = FakeLagosV2() + + def get_scheduling_stage(backend): + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + ALAPScheduleAnalysis(backend.instruction_durations), + PadDynamicalDecoupling(backend.instruction_durations, dd_sequence), + ] + ) + return pm + + def get_post_translation_stage(_backend): + pm = PassManager([RemoveResetInZeroState()]) + return pm + + target.get_scheduling_stage = get_scheduling_stage + target.get_post_translation_stage = get_post_translation_stage + pm = generate_preset_pass_manager(optimization_level, target) + self.assertIsInstance(pm, PassManager) + pass_list = [x.__class__.__name__ for x in pm.passes()] + self.assertIn("PadDynamicalDecoupling", pass_list) + self.assertIn("ALAPScheduleAnalysis", pass_list) + self.assertIsNotNone(pm.post_translation) # pylint: disable=no-member + post_translation_pass_list = [ + x.__class__.__name__ for x in pm.post_translation.passes() # pylint: disable=no-member + ] + self.assertIn("RemoveResetInZeroState", post_translation_pass_list) From 061ebc432fe123404da267f57475e7d264dc723b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 Aug 2022 11:16:54 -0400 Subject: [PATCH 02/12] Remove stray optimization level args from pm config --- qiskit/compiler/transpiler.py | 2 +- qiskit/transpiler/preset_passmanagers/__init__.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 020a59f3c9e0..2c2a24206bbd 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -418,7 +418,7 @@ def _log_transpile_time(start_time, end_time): def _combine_args(shared_transpiler_args, unique_config): # Pop optimization_level to exclude it from the kwargs when building a # PassManagerConfig - level = shared_transpiler_args.get("optimization_level") + level = shared_transpiler_args.pop("optimization_level") pass_manager_config = shared_transpiler_args pass_manager_config.update(unique_config.pop("pass_manager_config")) pass_manager_config = PassManagerConfig(**pass_manager_config) diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index 0af4e40dd031..cb15d2ec372a 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -199,7 +199,6 @@ def generate_preset_pass_manager( initial_layout=initial_layout, init_method=init_method, optimization_method=optimization_method, - optimization_level=optimization_level, ) if backend is not None: From d7c479d0451101547e353febcb20e067e1003407 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 Aug 2022 11:28:18 -0400 Subject: [PATCH 03/12] Fix tests --- .../transpiler/test_preset_passmanagers.py | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 8f5e148da17e..90a3c7d2c6eb 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -412,25 +412,27 @@ def test_partial_layout_fully_connected_cm(self, level): @data(0, 1, 2, 3) def test_backend_with_custom_stages(self, optimization_level): """Test transpile() executes backend specific custom stage.""" - target = FakeLagosV2() - - def get_scheduling_stage(backend): - dd_sequence = [XGate(), XGate()] - pm = PassManager( - [ - ALAPScheduleAnalysis(backend.instruction_durations), - PadDynamicalDecoupling(backend.instruction_durations, dd_sequence), - ] - ) - return pm - - def get_post_translation_stage(_backend): - pm = PassManager([RemoveResetInZeroState()]) - return pm - - target.get_scheduling_stage = get_scheduling_stage - target.get_post_translation_stage = get_post_translation_stage + class TargetBackend(FakeLagosV2): + """Fake Lagos subclass with custom transpiler stages.""" + + def get_scheduling_stage(self): + """Custom scheduling passes.""" + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + ALAPScheduleAnalysis(self.instruction_durations), + PadDynamicalDecoupling(self.instruction_durations, dd_sequence), + ] + ) + return pm + + def get_post_translation_stage(self): + """Custom post translation stage.""" + pm = PassManager([RemoveResetInZeroState()]) + return pm + + target = TargetBackend() qr = QuantumRegister(2, "q") qc = QuantumCircuit(qr) qc.h(qr[0]) @@ -438,7 +440,7 @@ def get_post_translation_stage(_backend): _ = transpile(qc, target, optimization_level=optimization_level, callback=self.callback) self.assertIn("ALAPScheduleAnalysis", self.passes) self.assertIn("PadDynamicalDecoupling", self.passes) - self.assertIn("RemoveResetInZeroState()", self.passes) + self.assertIn("RemoveResetInZeroState", self.passes) @ddt @@ -1065,31 +1067,36 @@ def test_invalid_optimization_level(self): @data(0, 1, 2, 3) def test_backend_with_custom_stages(self, optimization_level): """Test generated preset pass manager includes backend specific custom stages.""" - target = FakeLagosV2() - def get_scheduling_stage(backend): - dd_sequence = [XGate(), XGate()] - pm = PassManager( - [ - ALAPScheduleAnalysis(backend.instruction_durations), - PadDynamicalDecoupling(backend.instruction_durations, dd_sequence), - ] - ) - return pm - - def get_post_translation_stage(_backend): - pm = PassManager([RemoveResetInZeroState()]) - return pm - - target.get_scheduling_stage = get_scheduling_stage - target.get_post_translation_stage = get_post_translation_stage + class TargetBackend(FakeLagosV2): + """Fake lagos subclass with custom transpiler stages.""" + + def get_scheduling_stage(self): + """Custom scheduling stage.""" + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + ALAPScheduleAnalysis(self.instruction_durations), + PadDynamicalDecoupling(self.instruction_durations, dd_sequence), + ] + ) + return pm + + def get_post_translation_stage(self): + """Custom post translation stage.""" + pm = PassManager([RemoveResetInZeroState()]) + return pm + + target = TargetBackend() pm = generate_preset_pass_manager(optimization_level, target) self.assertIsInstance(pm, PassManager) - pass_list = [x.__class__.__name__ for x in pm.passes()] + pass_list = [y.__class__.__name__ for x in pm.passes() for y in x["passes"]] self.assertIn("PadDynamicalDecoupling", pass_list) self.assertIn("ALAPScheduleAnalysis", pass_list) self.assertIsNotNone(pm.post_translation) # pylint: disable=no-member post_translation_pass_list = [ - x.__class__.__name__ for x in pm.post_translation.passes() # pylint: disable=no-member + y.__class__.__name__ + for x in pm.post_translation.passes() # pylint: disable=no-member + for y in x["passes"] ] self.assertIn("RemoveResetInZeroState", post_translation_pass_list) From caa9c494d554465e0fdce2c87e0b8f237dadef53 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 2 Sep 2022 09:28:46 -0400 Subject: [PATCH 04/12] Apply suggestions from code review Co-authored-by: Jake Lishman --- qiskit/providers/__init__.py | 2 +- qiskit/transpiler/passmanager_config.py | 1 - .../notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 9ada9625f245..5d88cba9b04f 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -418,7 +418,7 @@ def _define(self): by default (which is the last defined stage in a default compilation). These hook points in a :class:`~.BackendV2` class should only be used if your backend has special requirements for compilation that are not met by the -default backend +default backend. To leverage these hook points you just need to add the methods to your :class:`~.BackendV2` implementation and have them return a diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 23309e053045..2e5420e151f5 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -80,7 +80,6 @@ def __init__( init_method (str): The plugin name for the init stage plugin to use optimization_method (str): The plugin name for the optimization stage plugin to use. - optimization_level (int): The optimization level being used for compilation. post_translation_pm (PassManager): An optional pass manager representing a post-translation stage. """ diff --git a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml index e63bb50d2e85..7895d99d8eb6 100644 --- a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml +++ b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml @@ -8,4 +8,4 @@ features: ``get_scheduling_stage()`` or ``get_post_translation_stage()`` the transpiler will use the returned :class:`~.PassManager` object to run additional custom transpiler passes when targetting that backend. - For more details on how to use this see :ref:`custom_transpiler_backend` + For more details on how to use this see :ref:`custom_transpiler_backend`. From cf6a7ec90ab8b8b150abb92cce15a7935a0b6994 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 21 Sep 2022 17:53:37 -0400 Subject: [PATCH 05/12] Pivot backend transpiler hook points to leverage plugins Since in qiskit-terra 0.22.0 we're adding a plugin interface for transpiler stages already, the more natural fit for enabling backends to inject custom passes into a pass maanger is via that plugin interface. This commit updates the hook points to return a plugin method name that a backend should use by default. This will provide the same level of flexibility but also export any custom stages as standalone methods that users can call into if they need to and also advertise the custom stage methods along with all other installed methods. --- qiskit/compiler/transpiler.py | 17 +- qiskit/providers/__init__.py | 56 ++--- qiskit/providers/backend.py | 20 +- qiskit/transpiler/passmanager_config.py | 12 +- .../transpiler/preset_passmanagers/level0.py | 6 - .../transpiler/preset_passmanagers/level1.py | 7 - .../transpiler/preset_passmanagers/level2.py | 7 - .../transpiler/preset_passmanagers/level3.py | 5 - ...ackend-custom-passes-cddfd05c8704a4b1.yaml | 6 +- .../transpiler/test_preset_passmanagers.py | 195 ++++++++++++++---- 10 files changed, 222 insertions(+), 109 deletions(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 2c2a24206bbd..f220161041b5 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -82,6 +82,7 @@ def transpile( target: Target = None, init_method: str = None, optimization_method: str = None, + ignore_backend_default_methods: bool = False, ) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Transpile one or more circuits, according to some desired transpilation targets. @@ -269,6 +270,11 @@ def callback_func(**kwargs): plugin is not used. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"optimization"`` for the ``stage_name`` argument. + ignore_backend_default_methods: If set to ``True`` any default methods specified by + a backend will be ignored. Some backends specify alternative default methods + to support custom compilation target passes/plugins which support backend + specific compilation techniques. If you'd prefer that these did not run and + use the defaults for a given optimization level this can be used. Returns: The transpiled circuit(s). @@ -336,6 +342,7 @@ def callback_func(**kwargs): target, init_method, optimization_method, + ignore_backend_default_methods, ) # Get transpile_args to configure the circuit transpilation job(s) if coupling_map in unique_transpile_args: @@ -588,6 +595,7 @@ def _parse_transpile_args( target, init_method, optimization_method, + ignore_backend_default_methods, ) -> Tuple[List[Dict], Dict]: """Resolve the various types of args allowed to the transpile() function through duck typing, overriding args, etc. Refer to the transpile() docstring for details on @@ -660,8 +668,11 @@ def _parse_transpile_args( } list_transpile_args = [] - if scheduling_method is None and hasattr(backend, "get_scheduling_stage"): - scheduling_method = backend.get_scheduling_stage() + if not ignore_backend_default_methods: + if scheduling_method is None and hasattr(backend, "get_scheduling_stage_method"): + scheduling_method = backend.get_scheduling_stage_method() + if translation_method is None and hasattr(backend, "get_translation_stage_method"): + translation_method = backend.get_translation_stage_method() for key, value in { "inst_map": inst_map, @@ -694,8 +705,6 @@ def _parse_transpile_args( "pass_manager_config": kwargs, } list_transpile_args.append(transpile_args) - if hasattr(backend, "get_post_translation_stage"): - shared_dict["post_translation_pm"] = backend.get_post_translation_stage() return list_transpile_args, shared_dict diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 5d88cba9b04f..936235fbd628 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -411,46 +411,46 @@ def _define(self): As part of the transpiler there is a provision for backends to provide custom stage implementation to facilitate hardware specific optimizations and circuit transformations. Currently there are two hook points supported, -``get_post_translation_stage()`` which is used for a backend to specify a -:class:`~PassManager` which will be run after basis translation stage in the -compiler and ``get_scheduling_stage()`` which is used for a backend to -specify a :class:`~PassManager` which will be run for the scheduling stage +``get_translation_stage_method()`` which is used for a backend to specify a +string of the translation stage plugin to use in the compiler and +``get_scheduling_stage_method()`` which is used for a backend to +specify a string scheduling method pluging to use for the scheduling stage by default (which is the last defined stage in a default compilation). These hook points in a :class:`~.BackendV2` class should only be used if your backend has special requirements for compilation that are not met by the -default backend. +default backend/:class:`~.Target` interface. Ideally we can expand these +interfaces to cover more details and information to inform the transpiler on +how/when to perform certain steps/optimizations. To leverage these hook points you just need to add the methods to your -:class:`~.BackendV2` implementation and have them return a -:class:`~.PassManager` object. For example:: +:class:`~.BackendV2` implementation and have them return a string method +For example:: - from qiskit.circuit.library import XGate - from qiskit.transpiler.passes import ( - ALAPScheduleAnalysis, - PadDynamicalDecoupling, - ResetAfterMeasureSimplification - ) class Mybackend(BackendV2): - def get_scheduling_stage(self): - dd_sequence = [XGate(), XGate()] - pm = PassManager([ - ALAPScheduleAnalysis(self.instruction_durations), - PadDynamicalDecoupling(self.instruction_durations, dd_sequence) - ]) - return pm + def get_scheduling_stage_method(self): + return "SpecialDD" - def get_post_translation_stage(self): - pm = PassManager([ResetAfterMeasureSimplification()]) - return pm + def get_translation_stage_method(self): + return "BasisTranslatorWithCustom1qOptimization" This snippet of a backend implementation will now have the :func:`~.transpile` -function run a custom stage for scheduling (unless the user manually requests a -different one explicitly) which will insert dynamical decoupling sequences and -also simplify resets after measurements after the basis translation stage. This -way if these two compilation steps are **required** for running on ``Mybackend`` -the transpiler will be able to perform these steps without any manual user input. +function use the ``SpecialDD`` plugin for the scheduling stage and +the ``BasisTranslatorWithCustom1qOptimization`` plugin for the translation +stage by default when the target is set to ``Mybackend`` (unless the user manually +explicitly selects a different method). For this interface to work though transpiler +stage plugins must be implemented for the returned plugin name. You can refer +to :mod:`qiskit.transpiler.preset_passmanagers.plugin` module documentation for +details on how to implement plugins. The typical expectation is that if your backend +requires custom passes as part of a compilation stage the provider package will +include the transpiler stage plugins that use those passes. However, this is not +required and any valid method (from a built-in method or external plugin) can +be used. + +This way if these two compilation steps are **required** for running or providing +efficient output on ``Mybackend`` the transpiler will be able to perform these +custom steps without any manual user input. Run Method ---------- diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 9d82d354fdad..c1b39c6df083 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -289,18 +289,24 @@ class BackendV2(Backend, ABC): defined in this class for backwards compatibility. A backend object can optionally contain methods named - ``get_post_translation_stage`` and ``get_scheduling_stage``. If these + ``get_translation_stage_method`` and ``get_scheduling_stage_method``. If these methods are present on a backend object and this object is used for :func:`~.transpile` or :func:`~.generate_preset_pass_manager` the transpilation process will default to using the output from those methods - as the scheduling stage and the post-translation compilation stage. This - enables a backend which has custom requirements for compilation to transform + as the scheduling stage and the translation compilation stage. This + enables a backend which has custom requirements for compilation to specify a + stage plugin for these stages to enable custom transformation of the circuit the circuit to ensure it is runnable on the backend. These hooks are enabled by default and should only be used to enable extra compilation steps - if they are **required** to ensure a circuit is executable on the backend. - These methods are passed no input arguments and are expected to return - a :class:`~.PassManager` object representing that stage of the transpilation - process. + if they are **required** to ensure a circuit is executable on the backend or + have the expected level of performance. These methods are passed no input + arguments and are expected to return a ``str`` representing the method name + which is typically a stage plugin (see: :mod:`qiskit.transpiler.preset_passmanagers.plugin` + for more details on plugins). The typical expected use case is for a backend + provider to implement a stage plugin for ``translation`` or ``scheduling`` + that contains the custom compilation passes and then for the hook methods on + the backend object to return the plugin name so that :func:`~.transpile` will + use it by default when targetting the backend. """ version = 2 diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 2e5420e151f5..694d761f2e20 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -41,7 +41,6 @@ def __init__( target=None, init_method=None, optimization_method=None, - post_translation_pm=None, ): """Initialize a PassManagerConfig object @@ -80,8 +79,6 @@ def __init__( init_method (str): The plugin name for the init stage plugin to use optimization_method (str): The plugin name for the optimization stage plugin to use. - post_translation_pm (PassManager): An optional pass manager representing a - post-translation stage. """ self.initial_layout = initial_layout self.basis_gates = basis_gates @@ -101,7 +98,6 @@ def __init__( self.unitary_synthesis_method = unitary_synthesis_method self.unitary_synthesis_plugin_config = unitary_synthesis_plugin_config self.target = target - self.post_translation_pm = post_translation_pm @classmethod def from_backend(cls, backend, **pass_manager_options): @@ -153,10 +149,10 @@ def from_backend(cls, backend, **pass_manager_options): if res.target is None: if backend_version >= 2: res.target = backend.target - if res.scheduling_method is None and hasattr(backend, "get_scheduling_stage"): - res.scheduling_method = backend.get_scheduling_stage() - if hasattr(backend, "get_post_translation_stage"): - res.post_translation_pm = backend.get_post_translation_stage() + if res.scheduling_method is None and hasattr(backend, "get_scheduling_stage_method"): + res.scheduling_method = backend.get_scheduling_stage_method() + if res.translation_method is None and hasattr(backend, "get_translation_stage_method"): + res.translation_method = backend.get_translation_stage_method() return res def __str__(self): diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index e755eeb47897..9779ed5dd0d4 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -176,8 +176,6 @@ def _choose_layout_condition(property_set): sched = common.generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map ) - elif isinstance(scheduling_method, PassManager): - sched = scheduling_method else: sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=0 @@ -193,9 +191,6 @@ def _choose_layout_condition(property_set): optimization = plugin_manager.get_passmanager_stage( "optimization", optimization_method, pass_manager_config, optimization_level=0 ) - post_translation = None - if pass_manager_config.post_translation_pm is not None: - post_translation = pass_manager_config.post_translation_pm return StagedPassManager( init=init, @@ -203,7 +198,6 @@ def _choose_layout_condition(property_set): pre_routing=pre_routing, routing=routing, translation=translation, - post_translation=post_translation, pre_optimization=pre_opt, optimization=optimization, scheduling=sched, diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 24088668d636..48a798b62827 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -280,8 +280,6 @@ def _unroll_condition(property_set): sched = common.generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map ) - elif isinstance(scheduling_method, PassManager): - sched = scheduling_method else: sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=1 @@ -293,17 +291,12 @@ def _unroll_condition(property_set): else: init = unroll_3q - post_translation = None - if pass_manager_config.post_translation_pm is not None: - post_translation = pass_manager_config.post_translation_pm - return StagedPassManager( init=init, layout=layout, pre_routing=pre_routing, routing=routing, translation=translation, - post_translation=post_translation, pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 9368918656a6..d1599fd0bc9f 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -255,8 +255,6 @@ def _unroll_condition(property_set): sched = common.generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map ) - elif isinstance(scheduling_method, PassManager): - sched = scheduling_method else: sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=2 @@ -268,17 +266,12 @@ def _unroll_condition(property_set): else: init = unroll_3q - post_translation = None - if pass_manager_config.post_translation_pm is not None: - post_translation = pass_manager_config.post_translation_pm - return StagedPassManager( init=init, layout=layout, pre_routing=pre_routing, routing=routing, translation=translation, - post_translation=post_translation, pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 9fb52f93ae09..b6c9c68b4906 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -315,17 +315,12 @@ def _unroll_condition(property_set): "scheduling", scheduling_method, pass_manager_config, optimization_level=3 ) - post_translation = None - if pass_manager_config.post_translation_pm is not None: - post_translation = pass_manager_config.post_translation_pm - return StagedPassManager( init=init, layout=layout, pre_routing=pre_routing, routing=routing, translation=translation, - post_translation=post_translation, pre_optimization=pre_optimization, optimization=optimization, scheduling=sched, diff --git a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml index 7895d99d8eb6..51f0e77c2e88 100644 --- a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml +++ b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml @@ -5,7 +5,11 @@ features: points enabling backends to inject custom compilation steps as part of :func:`~.transpile` and :func:`~.generate_preset_pass_manager`. If a :class:`~.BackendV2` implementation includes the methods - ``get_scheduling_stage()`` or ``get_post_translation_stage()`` the + ``get_scheduling_stage_method()`` or ``get_translation_stage_method()`` the transpiler will use the returned :class:`~.PassManager` object to run additional custom transpiler passes when targetting that backend. For more details on how to use this see :ref:`custom_transpiler_backend`. + - | + Added a new keyword argument, ``ignore_backend_default_methods``, to the + :func:`~.transpile` function can be used to disable a backend's custom + default method if the target backend has one set. diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index aec12557c5b6..d749d05b09a7 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -46,10 +46,37 @@ from qiskit.circuit.library import GraphState from qiskit.quantum_info import random_unitary from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.transpiler.preset_passmanagers import level0, level1, level2, level3 from qiskit.utils.optionals import HAS_TOQM from qiskit.transpiler.passes import Collect2qBlocks, GatesInBasis +def mock_get_passmanager_stage( + stage_name, + plugin_name, + pm_config, + optimization_level=None, # pylint: disable=unused-argument +) -> PassManager: + """Mock function for get_passmanager_stage.""" + if stage_name == "translation" and plugin_name == "custom_stage_for_test": + pm = PassManager([RemoveResetInZeroState()]) + return pm + + elif stage_name == "scheduling" and plugin_name == "custom_stage_for_test": + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + ALAPScheduleAnalysis(pm_config.instruction_durations), + PadDynamicalDecoupling(pm_config.instruction_durations, dd_sequence), + ] + ) + return pm + elif stage_name == "routing": + return PassManager([]) + else: + raise Exception("Failure, unexpected stage plugin combo for test") + + def emptycircuit(): """Empty circuit""" return QuantumCircuit() @@ -436,28 +463,25 @@ def test_partial_layout_fully_connected_cm(self, level): Layout.from_qubit_list([ancilla[0], ancilla[1], qr[1], ancilla[2], qr[0]]), ) - @data(0, 1, 2, 3) - def test_backend_with_custom_stages(self, optimization_level): + @unittest.mock.patch.object( + level0.PassManagerStagePluginManager, + "get_passmanager_stage", + wraps=mock_get_passmanager_stage, + ) + def test_backend_with_custom_stages(self, _plugin_manager_mock): """Test transpile() executes backend specific custom stage.""" + optimization_level = 1 class TargetBackend(FakeLagosV2): - """Fake Lagos subclass with custom transpiler stages.""" - - def get_scheduling_stage(self): - """Custom scheduling passes.""" - dd_sequence = [XGate(), XGate()] - pm = PassManager( - [ - ALAPScheduleAnalysis(self.instruction_durations), - PadDynamicalDecoupling(self.instruction_durations, dd_sequence), - ] - ) - return pm - - def get_post_translation_stage(self): + """Fake lagos subclass with custom transpiler stages.""" + + def get_scheduling_stage_method(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_method(self): """Custom post translation stage.""" - pm = PassManager([RemoveResetInZeroState()]) - return pm + return "custom_stage_for_test" target = TargetBackend() qr = QuantumRegister(2, "q") @@ -1091,39 +1115,138 @@ def test_invalid_optimization_level(self): with self.assertRaises(ValueError): generate_preset_pass_manager(42) - @data(0, 1, 2, 3) - def test_backend_with_custom_stages(self, optimization_level): + @unittest.mock.patch.object( + level2.PassManagerStagePluginManager, + "get_passmanager_stage", + wraps=mock_get_passmanager_stage, + ) + def test_backend_with_custom_stages_level2(self, _plugin_manager_mock): """Test generated preset pass manager includes backend specific custom stages.""" + optimization_level = 2 class TargetBackend(FakeLagosV2): """Fake lagos subclass with custom transpiler stages.""" - def get_scheduling_stage(self): + def get_scheduling_stage_method(self): """Custom scheduling stage.""" - dd_sequence = [XGate(), XGate()] - pm = PassManager( - [ - ALAPScheduleAnalysis(self.instruction_durations), - PadDynamicalDecoupling(self.instruction_durations, dd_sequence), - ] - ) - return pm - - def get_post_translation_stage(self): + return "custom_stage_for_test" + + def get_translation_stage_method(self): """Custom post translation stage.""" - pm = PassManager([RemoveResetInZeroState()]) - return pm + return "custom_stage_for_test" target = TargetBackend() - pm = generate_preset_pass_manager(optimization_level, target) + pm = generate_preset_pass_manager(optimization_level, backend=target) + self.assertIsInstance(pm, PassManager) + + pass_list = [y.__class__.__name__ for x in pm.passes() for y in x["passes"]] + self.assertIn("PadDynamicalDecoupling", pass_list) + self.assertIn("ALAPScheduleAnalysis", pass_list) + post_translation_pass_list = [ + y.__class__.__name__ + for x in pm.translation.passes() # pylint: disable=no-member + for y in x["passes"] + ] + self.assertIn("RemoveResetInZeroState", post_translation_pass_list) + + @unittest.mock.patch.object( + level1.PassManagerStagePluginManager, + "get_passmanager_stage", + wraps=mock_get_passmanager_stage, + ) + def test_backend_with_custom_stages_level1(self, _plugin_manager_mock): + """Test generated preset pass manager includes backend specific custom stages.""" + optimization_level = 1 + + class TargetBackend(FakeLagosV2): + """Fake lagos subclass with custom transpiler stages.""" + + def get_scheduling_stage_method(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_method(self): + """Custom post translation stage.""" + return "custom_stage_for_test" + + target = TargetBackend() + pm = generate_preset_pass_manager(optimization_level, backend=target) + self.assertIsInstance(pm, PassManager) + + pass_list = [y.__class__.__name__ for x in pm.passes() for y in x["passes"]] + self.assertIn("PadDynamicalDecoupling", pass_list) + self.assertIn("ALAPScheduleAnalysis", pass_list) + post_translation_pass_list = [ + y.__class__.__name__ + for x in pm.translation.passes() # pylint: disable=no-member + for y in x["passes"] + ] + self.assertIn("RemoveResetInZeroState", post_translation_pass_list) + + @unittest.mock.patch.object( + level3.PassManagerStagePluginManager, + "get_passmanager_stage", + wraps=mock_get_passmanager_stage, + ) + def test_backend_with_custom_stages_level3(self, _plugin_manager_mock): + """Test generated preset pass manager includes backend specific custom stages.""" + optimization_level = 3 + + class TargetBackend(FakeLagosV2): + """Fake lagos subclass with custom transpiler stages.""" + + def get_scheduling_stage_method(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_method(self): + """Custom post translation stage.""" + return "custom_stage_for_test" + + target = TargetBackend() + pm = generate_preset_pass_manager(optimization_level, backend=target) + self.assertIsInstance(pm, PassManager) + + pass_list = [y.__class__.__name__ for x in pm.passes() for y in x["passes"]] + self.assertIn("PadDynamicalDecoupling", pass_list) + self.assertIn("ALAPScheduleAnalysis", pass_list) + post_translation_pass_list = [ + y.__class__.__name__ + for x in pm.translation.passes() # pylint: disable=no-member + for y in x["passes"] + ] + self.assertIn("RemoveResetInZeroState", post_translation_pass_list) + + @unittest.mock.patch.object( + level0.PassManagerStagePluginManager, + "get_passmanager_stage", + wraps=mock_get_passmanager_stage, + ) + def test_backend_with_custom_stages_level0(self, _plugin_manager_mock): + """Test generated preset pass manager includes backend specific custom stages.""" + optimization_level = 0 + + class TargetBackend(FakeLagosV2): + """Fake lagos subclass with custom transpiler stages.""" + + def get_scheduling_stage_method(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_method(self): + """Custom post translation stage.""" + return "custom_stage_for_test" + + target = TargetBackend() + pm = generate_preset_pass_manager(optimization_level, backend=target) self.assertIsInstance(pm, PassManager) + pass_list = [y.__class__.__name__ for x in pm.passes() for y in x["passes"]] self.assertIn("PadDynamicalDecoupling", pass_list) self.assertIn("ALAPScheduleAnalysis", pass_list) - self.assertIsNotNone(pm.post_translation) # pylint: disable=no-member post_translation_pass_list = [ y.__class__.__name__ - for x in pm.post_translation.passes() # pylint: disable=no-member + for x in pm.translation.passes() # pylint: disable=no-member for y in x["passes"] ] self.assertIn("RemoveResetInZeroState", post_translation_pass_list) From 3870356cf885381f73ca4c23af0cadf24e745292 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 30 Sep 2022 17:22:02 -0400 Subject: [PATCH 06/12] Apply suggestions from code review Co-authored-by: Kevin Krsulich --- qiskit/providers/__init__.py | 20 +++++++++----------- qiskit/providers/backend.py | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 41311dde4cf3..657cbc5f7cfd 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -410,14 +410,12 @@ def _define(self): Custom Transpiler Passes ^^^^^^^^^^^^^^^^^^^^^^^^ -As part of the transpiler there is a provision for backends to provide custom -stage implementation to facilitate hardware specific optimizations and -circuit transformations. Currently there are two hook points supported, -``get_translation_stage_method()`` which is used for a backend to specify a -string of the translation stage plugin to use in the compiler and -``get_scheduling_stage_method()`` which is used for a backend to -specify a string scheduling method pluging to use for the scheduling stage -by default (which is the last defined stage in a default compilation). These +The transpiler supports the ability for backends to provide custom transpiler +stage implementations to facilitate hardware specific optimizations and +circuit transformations. Currently there are two stages supported, +``get_translation_stage_method()`` and ``get_scheduling_stage_method()`` +which allow a backend to specify string plugin names to be used as the default +translation and scheduling stages, respectively. These hook points in a :class:`~.BackendV2` class should only be used if your backend has special requirements for compilation that are not met by the default backend/:class:`~.Target` interface. Ideally we can expand these @@ -425,7 +423,7 @@ def _define(self): how/when to perform certain steps/optimizations. To leverage these hook points you just need to add the methods to your -:class:`~.BackendV2` implementation and have them return a string method +:class:`~.BackendV2` implementation and have them return a string plugin name. For example:: @@ -440,8 +438,8 @@ def get_translation_stage_method(self): This snippet of a backend implementation will now have the :func:`~.transpile` function use the ``SpecialDD`` plugin for the scheduling stage and the ``BasisTranslatorWithCustom1qOptimization`` plugin for the translation -stage by default when the target is set to ``Mybackend`` (unless the user manually -explicitly selects a different method). For this interface to work though transpiler +stage by default when the target is set to ``Mybackend``. Note that users may override these choices +by explicitly selecting a different plugin name. For this interface to work though transpiler stage plugins must be implemented for the returned plugin name. You can refer to :mod:`qiskit.transpiler.preset_passmanagers.plugin` module documentation for details on how to implement plugins. The typical expectation is that if your backend diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index c1b39c6df083..914365691162 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -295,13 +295,13 @@ class BackendV2(Backend, ABC): transpilation process will default to using the output from those methods as the scheduling stage and the translation compilation stage. This enables a backend which has custom requirements for compilation to specify a - stage plugin for these stages to enable custom transformation of the circuit + stage plugin for these stages to enable custom transformation of the circuit to ensure it is runnable on the backend. These hooks are enabled by default and should only be used to enable extra compilation steps if they are **required** to ensure a circuit is executable on the backend or have the expected level of performance. These methods are passed no input arguments and are expected to return a ``str`` representing the method name - which is typically a stage plugin (see: :mod:`qiskit.transpiler.preset_passmanagers.plugin` + which is should be a stage plugin (see: :mod:`qiskit.transpiler.preset_passmanagers.plugin` for more details on plugins). The typical expected use case is for a backend provider to implement a stage plugin for ``translation`` or ``scheduling`` that contains the custom compilation passes and then for the hook methods on From ccbbe55b0224488600442444add839f0315b9fb7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 30 Sep 2022 17:36:30 -0400 Subject: [PATCH 07/12] Update qiskit/providers/__init__.py Co-authored-by: Kevin Krsulich --- qiskit/providers/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 657cbc5f7cfd..8050393440a6 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -416,11 +416,12 @@ def _define(self): ``get_translation_stage_method()`` and ``get_scheduling_stage_method()`` which allow a backend to specify string plugin names to be used as the default translation and scheduling stages, respectively. These -hook points in a :class:`~.BackendV2` class should only be used if your -backend has special requirements for compilation that are not met by the -default backend/:class:`~.Target` interface. Ideally we can expand these -interfaces to cover more details and information to inform the transpiler on -how/when to perform certain steps/optimizations. +hook points in a :class:`~.BackendV2` class can be used if your +backend has requirements for compilation that are not met by the +current backend/:class:`~.Target` interface. Please also consider +submitting a Github issue describing your use case as there is interest +in improving these interfaces to be able to describe more hardware +architectures in greater depth. To leverage these hook points you just need to add the methods to your :class:`~.BackendV2` implementation and have them return a string plugin name. From e7b2e8bdfa9ebb512fc3e75d10c064d7a48723ff Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 30 Sep 2022 17:31:07 -0400 Subject: [PATCH 08/12] Update release note --- .../notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml index 51f0e77c2e88..e1e1ecd56ed9 100644 --- a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml +++ b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml @@ -6,8 +6,11 @@ features: :func:`~.transpile` and :func:`~.generate_preset_pass_manager`. If a :class:`~.BackendV2` implementation includes the methods ``get_scheduling_stage_method()`` or ``get_translation_stage_method()`` the - transpiler will use the returned :class:`~.PassManager` object to run - additional custom transpiler passes when targetting that backend. + transpiler will use the returned string as the default value for + the ``scheduling_method`` and ``translation_method`` arguments. This enables + backends to run additional custom transpiler passes when targetting that + backend by leveraging the transpiler stage + :mod:`~qiskit.transpiler.preset_passmanagers.plugin` interface. For more details on how to use this see :ref:`custom_transpiler_backend`. - | Added a new keyword argument, ``ignore_backend_default_methods``, to the From f8e8fded30381d8244ed47561734243bfa3095ad Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Oct 2022 12:11:02 -0400 Subject: [PATCH 09/12] Update qiskit/providers/backend.py Co-authored-by: John Lapeyre --- qiskit/providers/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 914365691162..734ca3c83a24 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -301,7 +301,7 @@ class BackendV2(Backend, ABC): if they are **required** to ensure a circuit is executable on the backend or have the expected level of performance. These methods are passed no input arguments and are expected to return a ``str`` representing the method name - which is should be a stage plugin (see: :mod:`qiskit.transpiler.preset_passmanagers.plugin` + which should be a stage plugin (see: :mod:`qiskit.transpiler.preset_passmanagers.plugin` for more details on plugins). The typical expected use case is for a backend provider to implement a stage plugin for ``translation`` or ``scheduling`` that contains the custom compilation passes and then for the hook methods on From d0040806e6ee8315bed57400dc7a216f19c2d41f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Oct 2022 13:50:41 -0400 Subject: [PATCH 10/12] Rename transpile() option and reword doc --- qiskit/compiler/transpiler.py | 16 ++++++++-------- ...d-backend-custom-passes-cddfd05c8704a4b1.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 59bb562ecc56..55e3d02ba717 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -84,7 +84,7 @@ def transpile( hls_config: Optional[HLSConfig] = None, init_method: str = None, optimization_method: str = None, - ignore_backend_default_methods: bool = False, + ignore_backend_supplied_default_methods: bool = False, ) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Transpile one or more circuits, according to some desired transpilation targets. @@ -277,11 +277,11 @@ def callback_func(**kwargs): plugin is not used. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"optimization"`` for the ``stage_name`` argument. - ignore_backend_default_methods: If set to ``True`` any default methods specified by + ignore_backend_supplied_default_methods: If set to ``True`` any default methods specified by a backend will be ignored. Some backends specify alternative default methods - to support custom compilation target passes/plugins which support backend - specific compilation techniques. If you'd prefer that these did not run and - use the defaults for a given optimization level this can be used. + to support custom compilation target specific passes/plugins which support backend + specific compilation techniques. If you'd prefer that these defaults were not used + this option is used to disable those backend specific defaults. Returns: The transpiled circuit(s). @@ -350,7 +350,7 @@ def callback_func(**kwargs): hls_config, init_method, optimization_method, - ignore_backend_default_methods, + ignore_backend_supplied_default_methods, ) # Get transpile_args to configure the circuit transpilation job(s) if coupling_map in unique_transpile_args: @@ -604,7 +604,7 @@ def _parse_transpile_args( hls_config, init_method, optimization_method, - ignore_backend_default_methods, + ignore_backend_supplied_default_methods, ) -> Tuple[List[Dict], Dict]: """Resolve the various types of args allowed to the transpile() function through duck typing, overriding args, etc. Refer to the transpile() docstring for details on @@ -677,7 +677,7 @@ def _parse_transpile_args( } list_transpile_args = [] - if not ignore_backend_default_methods: + if not ignore_backend_supplied_default_methods: if scheduling_method is None and hasattr(backend, "get_scheduling_stage_method"): scheduling_method = backend.get_scheduling_stage_method() if translation_method is None and hasattr(backend, "get_translation_stage_method"): diff --git a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml index e1e1ecd56ed9..3ecf9cb8d42b 100644 --- a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml +++ b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml @@ -13,6 +13,6 @@ features: :mod:`~qiskit.transpiler.preset_passmanagers.plugin` interface. For more details on how to use this see :ref:`custom_transpiler_backend`. - | - Added a new keyword argument, ``ignore_backend_default_methods``, to the + Added a new keyword argument, ``ignore_backend_supplied_default_methods``, to the :func:`~.transpile` function can be used to disable a backend's custom default method if the target backend has one set. From 214d9f75098eca8612bb651e2d689bb879bfd13e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Oct 2022 16:27:42 -0400 Subject: [PATCH 11/12] Rename hook methods backend_ --- qiskit/compiler/transpiler.py | 8 ++++---- qiskit/providers/__init__.py | 6 +++--- qiskit/providers/backend.py | 2 +- qiskit/transpiler/passmanager_config.py | 8 ++++---- ...ackend-custom-passes-cddfd05c8704a4b1.yaml | 2 +- .../transpiler/test_preset_passmanagers.py | 20 +++++++++---------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 55e3d02ba717..aad5ca324360 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -678,10 +678,10 @@ def _parse_transpile_args( list_transpile_args = [] if not ignore_backend_supplied_default_methods: - if scheduling_method is None and hasattr(backend, "get_scheduling_stage_method"): - scheduling_method = backend.get_scheduling_stage_method() - if translation_method is None and hasattr(backend, "get_translation_stage_method"): - translation_method = backend.get_translation_stage_method() + if scheduling_method is None and hasattr(backend, "get_scheduling_stage_plugin"): + scheduling_method = backend.get_scheduling_stage_plugin() + if translation_method is None and hasattr(backend, "get_translation_stage_plugin"): + translation_method = backend.get_translation_stage_plugin() for key, value in { "inst_map": inst_map, diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 8050393440a6..78843cc9edbd 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -413,7 +413,7 @@ def _define(self): The transpiler supports the ability for backends to provide custom transpiler stage implementations to facilitate hardware specific optimizations and circuit transformations. Currently there are two stages supported, -``get_translation_stage_method()`` and ``get_scheduling_stage_method()`` +``get_translation_stage_plugin()`` and ``get_scheduling_stage_plugin()`` which allow a backend to specify string plugin names to be used as the default translation and scheduling stages, respectively. These hook points in a :class:`~.BackendV2` class can be used if your @@ -430,10 +430,10 @@ def _define(self): class Mybackend(BackendV2): - def get_scheduling_stage_method(self): + def get_scheduling_stage_plugin(self): return "SpecialDD" - def get_translation_stage_method(self): + def get_translation_stage_plugin(self): return "BasisTranslatorWithCustom1qOptimization" This snippet of a backend implementation will now have the :func:`~.transpile` diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 734ca3c83a24..39ab6df36f24 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -289,7 +289,7 @@ class BackendV2(Backend, ABC): defined in this class for backwards compatibility. A backend object can optionally contain methods named - ``get_translation_stage_method`` and ``get_scheduling_stage_method``. If these + ``get_translation_stage_plugin`` and ``get_scheduling_stage_plugin``. If these methods are present on a backend object and this object is used for :func:`~.transpile` or :func:`~.generate_preset_pass_manager` the transpilation process will default to using the output from those methods diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index f60ac5297419..6534212c83da 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -154,10 +154,10 @@ def from_backend(cls, backend, **pass_manager_options): if res.target is None: if backend_version >= 2: res.target = backend.target - if res.scheduling_method is None and hasattr(backend, "get_scheduling_stage_method"): - res.scheduling_method = backend.get_scheduling_stage_method() - if res.translation_method is None and hasattr(backend, "get_translation_stage_method"): - res.translation_method = backend.get_translation_stage_method() + if res.scheduling_method is None and hasattr(backend, "get_scheduling_stage_plugin"): + res.scheduling_method = backend.get_scheduling_stage_plugin() + if res.translation_method is None and hasattr(backend, "get_translation_stage_plugin"): + res.translation_method = backend.get_translation_stage_plugin() return res def __str__(self): diff --git a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml index 3ecf9cb8d42b..b6aca2903192 100644 --- a/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml +++ b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml @@ -5,7 +5,7 @@ features: points enabling backends to inject custom compilation steps as part of :func:`~.transpile` and :func:`~.generate_preset_pass_manager`. If a :class:`~.BackendV2` implementation includes the methods - ``get_scheduling_stage_method()`` or ``get_translation_stage_method()`` the + ``get_scheduling_stage_plugin()`` or ``get_translation_stage_plugin()`` the transpiler will use the returned string as the default value for the ``scheduling_method`` and ``translation_method`` arguments. This enables backends to run additional custom transpiler passes when targetting that diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 7aa29d12e1af..39c6529b7444 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -475,11 +475,11 @@ def test_backend_with_custom_stages(self, _plugin_manager_mock): class TargetBackend(FakeLagosV2): """Fake lagos subclass with custom transpiler stages.""" - def get_scheduling_stage_method(self): + def get_scheduling_stage_plugin(self): """Custom scheduling stage.""" return "custom_stage_for_test" - def get_translation_stage_method(self): + def get_translation_stage_plugin(self): """Custom post translation stage.""" return "custom_stage_for_test" @@ -1104,11 +1104,11 @@ def test_backend_with_custom_stages_level2(self, _plugin_manager_mock): class TargetBackend(FakeLagosV2): """Fake lagos subclass with custom transpiler stages.""" - def get_scheduling_stage_method(self): + def get_scheduling_stage_plugin(self): """Custom scheduling stage.""" return "custom_stage_for_test" - def get_translation_stage_method(self): + def get_translation_stage_plugin(self): """Custom post translation stage.""" return "custom_stage_for_test" @@ -1138,11 +1138,11 @@ def test_backend_with_custom_stages_level1(self, _plugin_manager_mock): class TargetBackend(FakeLagosV2): """Fake lagos subclass with custom transpiler stages.""" - def get_scheduling_stage_method(self): + def get_scheduling_stage_plugin(self): """Custom scheduling stage.""" return "custom_stage_for_test" - def get_translation_stage_method(self): + def get_translation_stage_plugin(self): """Custom post translation stage.""" return "custom_stage_for_test" @@ -1172,11 +1172,11 @@ def test_backend_with_custom_stages_level3(self, _plugin_manager_mock): class TargetBackend(FakeLagosV2): """Fake lagos subclass with custom transpiler stages.""" - def get_scheduling_stage_method(self): + def get_scheduling_stage_plugin(self): """Custom scheduling stage.""" return "custom_stage_for_test" - def get_translation_stage_method(self): + def get_translation_stage_plugin(self): """Custom post translation stage.""" return "custom_stage_for_test" @@ -1206,11 +1206,11 @@ def test_backend_with_custom_stages_level0(self, _plugin_manager_mock): class TargetBackend(FakeLagosV2): """Fake lagos subclass with custom transpiler stages.""" - def get_scheduling_stage_method(self): + def get_scheduling_stage_plugin(self): """Custom scheduling stage.""" return "custom_stage_for_test" - def get_translation_stage_method(self): + def get_translation_stage_plugin(self): """Custom post translation stage.""" return "custom_stage_for_test" From 4eb107b19ca07e36ec1b7a9c13245355b4fe4ccf Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Oct 2022 19:47:45 -0400 Subject: [PATCH 12/12] Update qiskit/compiler/transpiler.py Co-authored-by: John Lapeyre --- qiskit/compiler/transpiler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index aad5ca324360..7bb3d0d74f97 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -279,9 +279,9 @@ def callback_func(**kwargs): ``stage_name`` argument. ignore_backend_supplied_default_methods: If set to ``True`` any default methods specified by a backend will be ignored. Some backends specify alternative default methods - to support custom compilation target specific passes/plugins which support backend - specific compilation techniques. If you'd prefer that these defaults were not used - this option is used to disable those backend specific defaults. + to support custom compilation target-specific passes/plugins which support + backend-specific compilation techniques. If you'd prefer that these defaults were + not used this option is used to disable those backend-specific defaults. Returns: The transpiled circuit(s).