Skip to content

Commit

Permalink
HLSConfig option to run multiple plugins and to choose the best decom…
Browse files Browse the repository at this point in the history
…position (#12108)

* fix docstring

* import

* exposing additional plugin arguments

* tests

* lint

* release notes

* HLS config option to run all specified plugins + tests

* lint"

* removing todo

* release notes

* fixes

---------

Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
  • Loading branch information
alexanderivrii and ElePT committed May 1, 2024
1 parent a65c9e6 commit cd03721
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 10 deletions.
49 changes: 39 additions & 10 deletions qiskit/transpiler/passes/synthesis/high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]:
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
73 changes: 73 additions & 0 deletions test/python/transpiler/test_high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit cd03721

Please sign in to comment.