diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 0e6f51a705c0..7bb3d0d74f97 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -84,6 +84,7 @@ def transpile( hls_config: Optional[HLSConfig] = None, init_method: str = None, optimization_method: str = None, + ignore_backend_supplied_default_methods: bool = False, ) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Transpile one or more circuits, according to some desired transpilation targets. @@ -276,6 +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_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. Returns: The transpiled circuit(s). @@ -344,6 +350,7 @@ def callback_func(**kwargs): hls_config, init_method, optimization_method, + ignore_backend_supplied_default_methods, ) # Get transpile_args to configure the circuit transpilation job(s) if coupling_map in unique_transpile_args: @@ -426,7 +433,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) @@ -597,6 +604,7 @@ def _parse_transpile_args( hls_config, init_method, optimization_method, + 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 @@ -669,6 +677,12 @@ 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_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, "coupling_map": coupling_map, diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index e96d5635fe00..78843cc9edbd 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -406,6 +406,53 @@ def _define(self): transpiler will ensure that it continues to be well supported by Qiskit moving forward. +.. _custom_transpiler_backend: + +Custom Transpiler Passes +^^^^^^^^^^^^^^^^^^^^^^^^ +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_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 +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. +For example:: + + + class Mybackend(BackendV2): + + def get_scheduling_stage_plugin(self): + return "SpecialDD" + + def get_translation_stage_plugin(self): + return "BasisTranslatorWithCustom1qOptimization" + +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``. 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 +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 5506043db868..39ab6df36f24 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -287,6 +287,26 @@ 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_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 + 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 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 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 + 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 c54eead039e5..6534212c83da 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -42,7 +42,6 @@ def __init__( hls_config=None, init_method=None, optimization_method=None, - optimization_level=None, ): """Initialize a PassManagerConfig object @@ -84,7 +83,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. """ self.initial_layout = initial_layout self.basis_gates = basis_gates @@ -105,7 +103,6 @@ def __init__( self.unitary_synthesis_plugin_config = unitary_synthesis_plugin_config self.target = target self.hls_config = hls_config - self.optimization_level = optimization_level @classmethod def from_backend(cls, backend, **pass_manager_options): @@ -157,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_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/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index 3f3f5cd4227f..a9518daa7bb3 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -205,7 +205,6 @@ def generate_preset_pass_manager( hls_config=hls_config, init_method=init_method, optimization_method=optimization_method, - optimization_level=optimization_level, ) if backend is not None: diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index da7a102a338b..609c8da360a1 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -94,11 +94,6 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config target = pass_manager_config.target hls_config = pass_manager_config.hls_config - # 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) @@ -314,14 +309,13 @@ 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=3 ) - # Restore PassManagerConfig optimization_level override - pass_manager_config.optimization_level = optimization_level - return StagedPassManager( init=init, layout=layout, 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..b6aca2903192 --- /dev/null +++ b/releasenotes/notes/add-backend-custom-passes-cddfd05c8704a4b1.yaml @@ -0,0 +1,18 @@ +--- +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_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 + 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_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. 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 eadd82edb38a..39c6529b7444 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, QuantumVolume +from qiskit.transpiler.passes import ( + ALAPScheduleAnalysis, + PadDynamicalDecoupling, + RemoveResetInZeroState, +) +from qiskit.circuit.library import U2Gate, U3Gate, XGate, QuantumVolume from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import ( FakeBelem, @@ -41,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() @@ -431,6 +463,36 @@ def test_partial_layout_fully_connected_cm(self, level): Layout.from_qubit_list([ancilla[0], ancilla[1], qr[1], ancilla[2], qr[0]]), ) + @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_plugin(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_plugin(self): + """Custom post translation stage.""" + return "custom_stage_for_test" + + target = TargetBackend() + 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): @@ -1029,3 +1091,139 @@ def test_invalid_optimization_level(self): """Assert we fail with an invalid optimization_level.""" with self.assertRaises(ValueError): generate_preset_pass_manager(42) + + @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_plugin(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_plugin(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( + 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_plugin(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_plugin(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_plugin(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_plugin(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_plugin(self): + """Custom scheduling stage.""" + return "custom_stage_for_test" + + def get_translation_stage_plugin(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)