-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HLS with coupling map #9250
HLS with coupling map #9250
Conversation
Thank you for opening a new pull request. Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient. While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone. One or more of the the following people are requested to review this: |
def _hamiltonian_paths( | ||
coupling_map: CouplingMap, cutoff: Union[None | int] = None | ||
) -> List[List[int]]: | ||
"""Returns a list of all Hamiltonian paths in ``coupling_map`` (stopping the enumeration when | ||
the number of already discovered paths exceeds the ``cutoff`` value, when specified). | ||
In particular, returns an empty list if there are no Hamiltonian paths. | ||
""" | ||
|
||
# This is a temporary function, the plan is to move it to rustworkx | ||
|
||
def should_stop(): | ||
return cutoff is not None and len(all_paths) >= cutoff | ||
|
||
def _recurse(current_node): | ||
current_path.append(current_node) | ||
if len(current_path) == coupling_map.size(): | ||
# Discovered a new Hamiltonian path | ||
all_paths.append(current_path.copy()) | ||
|
||
if should_stop(): | ||
return | ||
|
||
unvisited_neighbors = [ | ||
node for node in coupling_map.neighbors(current_node) if node not in current_path | ||
] | ||
for node in unvisited_neighbors: | ||
_recurse(node) | ||
if should_stop(): | ||
return | ||
|
||
current_path.pop() | ||
|
||
all_paths = [] | ||
current_path = [] | ||
qubits = coupling_map.physical_qubits | ||
|
||
for qubit in qubits: | ||
_recurse(qubit) | ||
if should_stop(): | ||
break | ||
return all_paths |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be more efficient to use rustworkx's dfs_search()
method to do this traversal: https://qiskit.org/documentation/retworkx/apiref/rustworkx.dfs_search.html#rustworkx.dfs_search which lets you build this as an event driven iterator on the rust side. So you define a visitor class which has hook points in the dfs and rustworkx calls the visitor methods based on it's dfs traversal.
But we definitely should do this in rustworkx natively for the next release because this algorithm can be implemented in parallel fairly easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I did not know about the rustworkx's dfs_search
method. Though, here we need something akin to "DFS with backtracking", and I was not quite able to figure out if dfs_search
can be used for that; that is, can this be implemented with some appropriate visitor? In any case, this function is only temporary as I am planning to follow your suggestion of adding it to rustworkx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be something that can implement with a specially crafted visitor, but I'm not sure. It's at least not obvious to me quickly scanning the docs and the code. Maybe @georgios-ts knows (as he wrote the functions), but yeah as a temporary step this is fine while waiting on a dedicated function in rustworkx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dfs_search
cannot be used since it does not support backtracking as @alexanderivrii correctly pointed out. But all_pairs_all_simple_paths
might be a viable option:
res = rustworkx.all_pairs_all_simple_paths(graph, min_depth=graph.num_nodes())
all_hamiltonian_paths = []
for paths_from_node in res.values():
for paths_from_node_to_target in paths_from_node.values():
all_hamiltonian_paths.extend(map(list, paths_from_node_to_target))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, @georgios-ts! I have already noticed the min-depth option, see my comment here: #9250 (comment). What scares me is what if there is an exponential number of hamiltonian paths, is there a way to limit rustworkx to compute only some fixed number of them?
Pull Request Test Coverage Report for Build 5520254013
💛 - Coveralls |
Two relevant points that we discussed during the weekly meeting. First, @ajavadia has suggested to support an alternative form of specifying synthesis methods for given high-level-objects. In addition to the already supported format
we should also support the format
This is done in 243027c. Though, the implementation still uses generic Second, there was a very interesting suggestion to analyze the coupling map before layout/routing, and based on this analysis to automatically choose the best resynthesis algorithm. One concrete idea was to avoid using the LNN-based KMS method if the coupling map does not have a path of a sufficient length. I really like these suggestions, however I am afraid that it would require a large number of changes to make the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a first pass through the code and had some inline comments. I feel like we need to split this PR into 2 pieces. One that adds the coupling map to the plugin interface and pass, the second which adds an alternative construction mechanism using the raw classes. They're really 2 separate logical changes and bundling makes it hard to review, bisect, and also test the features.
Other than that the only piece I'm a bit concerned about is the interface for how we specify the plugin interface around coupling map. I feel like it needs to be made a bit more explicit for plugin authors that coupling_map
is something that will be available to all plugins (assuming the information is provided to the pass) and not something that needs to be manually included as part of the custom settings dictionary.
@@ -0,0 +1,50 @@ | |||
--- | |||
features: | |||
- | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have a separate entry here for the permute()
method on the LinearFunction
class too.
@@ -89,9 +96,20 @@ class HighLevelSynthesis(TransformationPass): | |||
``default`` methods for all other high-level objects, including ``op_a``-objects. | |||
""" | |||
|
|||
def __init__(self, hls_config=None): | |||
def __init__(self, coupling_map: CouplingMap = None, hls_config=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For backwards compatibility I would reverse these arguments. Python will let you specify keyword arguments positionally by default so putting coupling_map
first will break users doing HighLevelSynthesis(HLSConfig(...))
which was valid way to instantiate the pass before.
coupling_map (CouplingMap): the coupling map of the backend | ||
in case synthesis is done on a physical circuit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also add a Target
argument here, similar to what I do in #9263 ? Basically I'm trying to unify our internal transpiler model around the target. My plan for 0.24.0 is to hopefully have the preset pass manager only use a target internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mtreinish, so should right now HighLevelSynthesis
accept both coupling_map
and target
, or just target
? I guess I am asking whether generate_translation_passmanager
(or code further upstream) updates target
with coupling_map
information if only the latter is specified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now it needs to accept both, there is still a code path in transpile
where a target will not be generated (I was hoping to change this for 0.24, so it was only target, but it won't make it). You can do it with a second argument or what we did in #9263 is had a single argument take either a coupling map or a target.
def __init__(self, **options): | ||
self._options = options |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a huge fan of this for things that are not directly user specified via the HLSConfig
. I'm thinking coupling_map
is different here because it's a target constraint and just bundling it via manually specified options by the user (in the HLSConfig
) isn't the best interface for plugins. I feel like we really need something like what unitary synthesis is doing that defines the interface options vs the user specified configuration. Right now if I wanted to write a hardware aware synthesis plugin I wouldn't know what is a standard feature of the interface and might end up diverging from the standard interface.
exists a hamiltonian path through the qubits over which this linear function is | ||
defined. When the coupling map is not ``None`` and the hamiltonian path does | ||
not exist, this plugin returns ``None``. | ||
upgrade: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This second note is really a feature note too, but as I mentioned above I feel like this needs to be a separate PR.
upgrade: |
I completely agree that A related question, in But what about The interface of |
The only concern I have with doing it explicitly like that in
I guess it depends on where/how we're running
This is why I like the
I think that's entirely sensible and a good idea. As long as there is a fallback to try other plugins if a synthesis plugin can return |
After some delay, I am back thinking about this PR. The alternative form of specifying the HLS config has already been merged in #9413. Other than that, this PR consists of two parts: finalizing the high-level-synthesis plugins interface in the presence of The more important discussion is on how the plugin interface should look like. Note that @mtreinish has excellent points #9250 (comment) and #9250 (comment). So, a few thoughts: Personally, I prefer passing I am not sure that the interface mechanism of unitary synthesis is sufficient for general synthesis methods. For instance, unitary synthesis has methods like Another thought is that given a huge variety of potential use-cases, we will not be able to choose a single set of options suitable for all possible synthesis routines, i.e. each synthesis algorithm may come with some of its dedicated options that make no sense in other cases. For instance, the options for synthesizing a Clifford might be very different from options for synthesizing an MCX-gate, and then some of specific MCX-synthesis algorithms will have their own set of options like I don't think that we should pass the whole I agree that it might be nice to separate plugin synthesis options into two sets, things like
|
Is #10477 superseding this one? |
To some extent, yes. There is still one piece left of extending some of the Clifford / LinearFunction / Permutation synthesis plugins to work with the coupling map (somewhat similarly to #10657), but at this point it's best to delegate this to a separate clean PR and to close this one. |
Summary
This PR adds the argument
coupling_map
to theHighLevelSynthesis
transpiler pass and fully implements two high-level-synthesis plugins forLinearFunctions
,PMHSynthesisLinearFunction
andKMSSynthesisLinearFunction
, both based on the already implemented Patel-Markov-Hayes (PMH) and Kutin-Moulton-Smithline (KMS) synthesis algorithms for linear functions.The underlying PMH algorithm does not take the coupling map into account, thus the corresponding
PMHSynthesisLinearFunction
plugin does not do anything when the coupling map is notNone
. This allows to effectively use this method when synthesizing linear functions on the virtual quantum circuit (that is, before layout and routing take place), but not on physical quantum circuits (after layout and routing).The underlying KMS algorithm synthesizes linear functions for the linear-nearest-neighbor (LNN) architecture. This allows to use this method on the virtual quantum circuit, and also on the physical quantum circuit, provided that the (placed and routed) linear function is defined over a set of qubit connected by a path. More precisely, this means a hamiltonian path for the coupling map reduced to the set of qubits involved in the linear function. When such a path does not exist, the KMS algorithm does not apply and the
KMSSynthesisLinearFunction
does not do anything, otherwise the linear function is synthesized over this hamiltonian path.Each of the two plugins described above also have the option to choose the best implementation when several implementations are available, including picking the best circuit when considering
A
,A^t
,A^{-1}
andA^t^{-1}
(whereA
is the matrix representing the linear function), and also to choose the original circuit describing the linear function when present (for instance, whenLinearFunction
is obtained viaCollectLinearFunctions
transpiler pass). For the LNN-based synthesis plugin, we can also consider multiple hamiltonian paths and choose the best definition.This PR does not change the preset pass managers with the now available functionality to collect and to resynthesize linear functions. This requires more thorough benchmarking, but see the (simple) example below illustrating what one can currently do.
Example
This is a very simple example consisting of a single randomly-generated quantum circuit with 6 qubits and 40 CX-gates (and no other gates) that we want to transpile onto the 6-qubit LNN architecture.
[1] First, consider the default run. Initially:
#cx = 40, depth = 28
. After routing (Sabre):#cx = 40, #swaps = 28, depth = 54
(thoughdepth
here is a bit misleading as SWAPs are also counted as having depth 1). After basis translation, we have#cx = 124, depth = 108
. The optimization loop (unitary resynthesis of 2-qubit blocks) manages to reduce the numbers a little bit, resulting in#cx = 118, #u = 4, depth = 106
.[2] Now let's modify the
init
plugin manager stage to additionally resynthesize linear functions using KMS, as followsThe idea is that we want to apply the LNN-based synthesis method on the abstract circuit, so that a priori
VF2
can find a perfect layout, and no extra swaps would be inserted. Indeed, after resynthesis we have#cx = 57, depth = 27
(Kutin's method tends to introduce quite many gates). No extra swaps are inserted during routing. Optimization loop does not do anything.[3] Let's instead modify the
init
plugin manager stage to additionally resynthesize linear functions using PMH. The idea is that the resynthesis algorithm may reduce the number of CX-gates, and hence routing would still be needed, but smaller circuits require fewer SWAPs. We have: after resynthesis#cx = 13, depth = 10
(we got lucky), after routing#cx = 13, #swap = 6, depth = 12
, after basis translation#cx = 31, depth = 20
.For each of the above cases, we can also resynthesize linear functions after routing (making sure to keep the pre-synthesized definition not to make things worse). It is important to resynthesize linear functions before resynthesizng 2q blocks using unitary synthesis (or else the CX structure of the circuit will be messed up), this can be achieved by further modifying the pass manager's
optimization
stage:[4] Recall that after routing and basis translation in [1] we had
#cx = 124, depth = 108
. The first linear functions resynthesis in the optimization loop results in#cx = 49, depth = 23
. At the very end we have#cx = 43, #u = 7, depth = 22
.[5] Recall that after routing and basis translation in [2] we had
#cx = 57, depth = 27
. The second time that we resynthesize this using KMS does not do anything, so at the end we have#cx = 57, depth = 27
[6] Recall that after routing and basis translation in [2] we had
#cx = 31, depth = 20
. The KMS-based resynthesis produce worse functions, so the pre-synthesized circuit is kept, and at the end we have#cx = 31, depth = 20
.Feedback wanted
Which other synthesis schemes should be considered now or in the long run?
What is the best way to properly benchmark various options, based on which possibly deciding to adjust preset pass managers?
What is the right way to compare two linear quantum circuits (and how to properly handle SWAP gates).
Different HLS plugins have their own set of
kwargs
(we actually wanted that from the start). But how to properly document this explaining which options exactly are available?