diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index e6442ecfb44..fd21ae6a75f 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -133,7 +133,7 @@ TokenSwapperSynthesisPermutation """ -from typing import Optional, Union, List, Tuple +from typing import Optional, Union, List, Tuple, Callable import numpy as np import rustworkx as rx @@ -227,16 +227,34 @@ class HLSConfig: :ref:`using-high-level-synthesis-plugins`. """ - def __init__(self, use_default_on_unspecified=True, **kwargs): + def __init__( + self, + use_default_on_unspecified: bool = True, + plugin_selection: str = "sequential", + plugin_evaluation_fn: Optional[Callable[[QuantumCircuit], int]] = None, + **kwargs, + ): """Creates a high-level-synthesis config. Args: - use_default_on_unspecified (bool): if True, every higher-level-object without an + use_default_on_unspecified: if True, every higher-level-object without an explicitly specified list of methods will be synthesized using the "default" algorithm if it exists. + plugin_selection: if set to ``"sequential"`` (default), for every higher-level-object + the synthesis pass will consider the specified methods sequentially, stopping + at the first method that is able to synthesize the object. If set to ``"all"``, + all the specified methods will be considered, and the best synthesized circuit, + according to ``plugin_evaluation_fn`` will be chosen. + plugin_evaluation_fn: a callable that evaluates the quality of the synthesized + quantum circuit; a smaller value means a better circuit. If ``None``, the + quality of the circuit its size (i.e. the number of gates that it contains). kwargs: a dictionary mapping higher-level-objects to lists of synthesis methods. """ self.use_default_on_unspecified = use_default_on_unspecified + self.plugin_selection = plugin_selection + self.plugin_evaluation_fn = ( + plugin_evaluation_fn if plugin_evaluation_fn is not None else lambda qc: qc.size() + ) self.methods = {} for key, value in kwargs.items(): @@ -248,9 +266,6 @@ def set_methods(self, hls_name, hls_methods): self.methods[hls_name] = hls_methods -# ToDo: Do we have a way to specify optimization criteria (e.g., 2q gate count vs. depth)? - - class HighLevelSynthesis(TransformationPass): """Synthesize higher-level objects and unroll custom definitions. @@ -500,6 +515,9 @@ def _synthesize_op_using_plugins( else: methods = [] + best_decomposition = None + best_score = np.inf + for method in methods: # There are two ways to specify a synthesis method. The more explicit # way is to specify it as a tuple consisting of a synthesis algorithm and a @@ -538,11 +556,22 @@ def _synthesize_op_using_plugins( ) # The synthesis methods that are not suited for the given higher-level-object - # will return None, in which case the next method in the list will be used. + # will return None. if decomposition is not None: - return decomposition - - return None + if self.hls_config.plugin_selection == "sequential": + # In the "sequential" mode the first successful decomposition is + # returned. + best_decomposition = decomposition + break + + # In the "run everything" mode we update the best decomposition + # discovered + current_score = self.hls_config.plugin_evaluation_fn(decomposition) + if current_score < best_score: + best_decomposition = decomposition + best_score = current_score + + return best_decomposition def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: """ diff --git a/releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml b/releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml new file mode 100644 index 00000000000..2ab34c61fb3 --- /dev/null +++ b/releasenotes/notes/add-run-all-plugins-option-ba8806a269e5713c.yaml @@ -0,0 +1,51 @@ +--- +features: + - | + The :class:`~.HLSConfig` now has two additional optional arguments. The argument + ``plugin_selection`` can be set either to ``"sequential"`` or to ``"all"``. + If set to "sequential" (default), for every higher-level-object + the :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass will consider the + specified methods sequentially, in the order they appear in the list, stopping + at the first method that is able to synthesize the object. If set to "all", + all the specified methods will be considered, and the best synthesized circuit, + according to ``plugin_evaluation_fn`` will be chosen. The argument + ``plugin_evaluation_fn`` is an optional callable that evaluates the quality of + the synthesized quantum circuit; a smaller value means a better circuit. When + set to ``None``, the quality of the circuit is its size (i.e. the number of gates + that it contains). + + The following example illustrates the new functionality:: + + from qiskit import QuantumCircuit + from qiskit.circuit.library import LinearFunction + from qiskit.synthesis.linear import random_invertible_binary_matrix + from qiskit.transpiler.passes import HighLevelSynthesis, HLSConfig + + # Create a circuit with a linear function + mat = random_invertible_binary_matrix(7, seed=37) + qc = QuantumCircuit(7) + qc.append(LinearFunction(mat), [0, 1, 2, 3, 4, 5, 6]) + + # Run different methods with different parameters, + # choosing the best result in terms of depth. + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ("pmh", {"section_size": 1}), + ("pmh", {"section_size": 3}), + ("kms", {}), + ("kms", {"use_inverted": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda circuit: circuit.depth(), + ) + + # synthesize + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + + In the example, we run multiple synthesis methods with different parameters, + choosing the best circuit in terms of depth. Note that optimizing + ``circuit.size()`` instead would pick a different circuit. diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 5ab78af8f58..0f074865f41 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -65,6 +65,7 @@ ) from test import QiskitTestCase # pylint: disable=wrong-import-order + # In what follows, we create two simple operations OpA and OpB, that potentially mimic # higher-level objects written by a user. # For OpA we define two synthesis methods: @@ -586,6 +587,78 @@ def test_invert_and_transpose(self): self.assertEqual(qct.size(), 6) self.assertEqual(qct.depth(), 6) + def test_plugin_selection_all(self): + """Test setting plugin_selection to all.""" + + linear_function = LinearFunction(self.construct_linear_circuit(7)) + qc = QuantumCircuit(7) + qc.append(linear_function, [0, 1, 2, 3, 4, 5, 6]) + + with self.subTest("sequential"): + # In the default "run sequential" mode, we stop as soon as a plugin + # in the list returns a circuit. + # For this specific example the default options lead to a suboptimal circuit. + hls_config = HLSConfig(linear_function=[("pmh", {}), ("pmh", {"use_inverted": True})]) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 12) + self.assertEqual(qct.depth(), 8) + + with self.subTest("all"): + # In the non-default "run all" mode, we examine all plugins in the list. + # For this specific example we get the better result for the second plugin in the list. + hls_config = HLSConfig( + linear_function=[("pmh", {}), ("pmh", {"use_inverted": True})], + plugin_selection="all", + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 6) + self.assertEqual(qct.depth(), 6) + + def test_plugin_selection_all_with_metrix(self): + """Test setting plugin_selection to all and specifying different evaluation functions.""" + + # The seed is chosen so that we get different best circuits depending on whether we + # want to minimize size or depth. + mat = random_invertible_binary_matrix(7, seed=37) + qc = QuantumCircuit(7) + qc.append(LinearFunction(mat), [0, 1, 2, 3, 4, 5, 6]) + + with self.subTest("size_fn"): + # We want to minimize the "size" (aka the number of gates) in the circuit + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda qc: qc.size(), + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 20) + self.assertEqual(qct.depth(), 15) + + with self.subTest("depth_fn"): + # We want to minimize the "depth" (aka the number of layers) in the circuit + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda qc: qc.depth(), + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 23) + self.assertEqual(qct.depth(), 12) + class TestKMSSynthesisLinearFunctionPlugin(QiskitTestCase): """Tests for the KMSSynthesisLinearFunction plugin for synthesizing linear functions."""