-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Starting layout analysis pass for sabre #10829
Starting layout analysis pass for sabre #10829
Conversation
One or more of the the following people are requested to review this:
|
Pull Request Test Coverage Report for Build 6534170170
💛 - Coveralls |
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 for making the updates, I just had a couple of other comments inline after taking another pass at review.
target=None, | ||
coupling_map=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.
Could we combine these args (and just use target). We did something similar in SabreLayout
:
qiskit/qiskit/transpiler/passes/layout/sabre_layout.py
Lines 164 to 169 in 18a9a2b
if isinstance(coupling_map, Target): | |
self.target = coupling_map | |
self.coupling_map = self.target.build_coupling_map() | |
else: | |
self.target = None | |
self.coupling_map = coupling_map |
(but used coupling_map
as the name for backwards compatibility).
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 guess we could do this, but this feels like this is somewhat abusing notation. I don't feel very strongly one way or another.
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.
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.
Sure, it's better to be consistent across different passes, done in 96b1056.
d = self.coupling_map.distance(x, y) | ||
if 1 < d <= distance: | ||
augmented_coupling_map.add_edge(x, y) | ||
augmented_error_map.add_error((x, y), self.error_rate) |
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 know we were talking offline about scaling the error rate based on the edge distance. Do you think doing something like:
augmented_error_map.add_error((x, y), self.error_rate) | |
augmented_error_map.add_error((x, y), self.error_rate * distance) |
Or were you thinking of something more complicated?
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 think something like this is a good idea, except that if error_rate * distance
becomes larger than 1
, then the final fidelity will be 0
, regardless of the layout (because the scoring code multiplies all the fidelities). So either we can live with this (in the worst case we will get a crappy starting layout that Sabre will eventually not pick), or to automatically decrease error_rate
to something like 1 / (2 * distance)
, or to consider some arctan
-like function f(distance)
such that f(1) = 0
(real edges have no error) and f(infinity)=1
(100% error), and everything else would be in-between.
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 like the asymptotic error rate approach that seems like the right fit for this use case. Although, I'd probably just be lazy and do something like min(0.9999, self.error_rate * distance)
to approximate it :)
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 actually works well in practice, though at the end I have gone with an also simple asymptotic approach which resembles score calculations in sabre error_rate(d) = 1 - ( (1 - self.error_rate)^d )
(but interestingly gives the same results as this "lazy" heuristic).
I didn't know this existed :) Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
I think this is basically ready to go when resolve the question on the heuristic error for the added edges. |
…qiskit-terra into initial-layout-for-sabre
I experimented quite a bit with mapping the BV circuit over 16 qubits to the heavy-hex(n) coupling for increasing values of n, and saw that (i) this PreSabreLayout helps even if the smallest d for which VF2 finds the layout is 4 (i.e. we need to add distance-4 edges to the coupling graph to find an embedding), (ii) additionally increasing error rates for longer-distance edges helps for larger values of n. In the end I have went with the heuristic that closely resembles computation of errors in sabre scoring: |
…y with other passes
|
||
if self.coupling_map is None: | ||
raise TranspilerError( | ||
"SabrePreLayout requires either target or coupling_map to be provided." |
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.
"SabrePreLayout requires either target or coupling_map to be provided." | |
"SabrePreLayout requires coupling_map to be used with either a " | |
"CouplingMap or 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.
Fixed in 5e6d3a5.
target (Target): A target representing the backend device. If specified, it will | ||
supersede a set value for ``coupling_map``. |
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.
target (Target): A target representing the backend device. If specified, it will | |
supersede a set value for ``coupling_map``. |
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.
Oops. Fixed in 5e6d3a5.
coupling_map (Union[CouplingMap, Target]): directed graph representing the | ||
original coupling map. |
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.
coupling_map (Union[CouplingMap, Target]): directed graph representing the | |
original coupling map. | |
coupling_map (Union[CouplingMap, Target]): directed graph representing the | |
original coupling map or a target modelling the backend (including its | |
connectivity). |
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.
Done in 5e6d3a5.
def _find_layout(self, dag, edges): | ||
"""Checks if there is a layout for a given set of edges.""" | ||
cm = CouplingMap(edges) | ||
pass_ = VF2Layout(cm, seed=0, max_trials=1, call_limit=self.call_limit_vf2) |
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 feel like this is a bit heavy weight we're basically we just need to call rustworkx.is_subgraph_isomorphic(cm_graph, im_graph, id_order=True, induced=False, call_limit=self.call_limit_vf2)
here without all the pass machinery to determine if a reduced edge list is valid or not.
For a first implementation I think this is fine, because there is probably some larger refinement we'll want to do since we do need a layout with the minimized edge list.
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 am leaving this as is for now, but sure this is something to rethink in the future.
extra_edges_unprocessed_set = self._get_extra_edges_used(dag, layout).difference( | ||
set(extra_edges_necessary) | ||
) | ||
best_layout = layout |
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.
If we minimize the edge list we don't take into account noise really and just return the first edge found? Since _find_layout
sets max_trials=1
. Is there value in running multiple trials on the output of minimization to take into account error rates?
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 is a good question. On the one hand, I believe that some part of the effort of looking for extra solutions at this stage would be wasted, as the top-level algorithm would proceed to removing more edges and calling this function again. On the other hand, it does seem a good idea to take the noise into account here as well. However, I do think that the effort here should be smaller than the effort in the main call. A possible solution would be to add yet another argument to the run
function, something like improve_layout_max_trials_vf2
(and, while we are at this, also improve_layout_call_limit_vf2
). Please tell me if you are agree with this.
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.
Yeah, I'm not sure either way. In general for sabre I don't think noise awareness buys us very much because we have VF2PostLayout
which factors in the whole circuit after routing. The place where this is different is to try and avoid the extra edges we've added to the coupling map. But I think for right now this is probably fine, we can always refine the pass more and add extra options in a follow up.
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 LGTM, just one tiny inline nit (sorry I didn't catch it before) and then I think this is good to merge
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Test failure in the merge queue was a network timeout with installing the dependencies - not a true failure, and hopefully just ephemeral. |
Building on the work done in Qiskit#10829, Qiskit#10721, and Qiskit#12104 this commit adds a new trial to all runs of `SabreLayout` that runs the dense layout pass. In general the sabre layout algorithm starts from a random layout and then runs a routing algorithm to permute that layout virtually where swaps would be inserted to select a layout that would result in fewer swaps. As was discussed in Qiskit#10721 and Qiskit#10829 that random starting point is often not ideal especially for larger targets where the distance between qubits can be quite far. Especially when the circuit qubit count is low relative to the target's qubit count this can result it poor layouts as the distance between the qubits is too large. In qiskit we have an existing pass, `DenseLayout`, which tries to find the most densely connected n qubit subgraph of a connectivity graph. This algorithm necessarily will select a starting layout where the qubits are near each other and for those large backends where the random starting layout doesn't work well this can improve the output quality. As the overhead of `DenseLayout` is quite low and the core algorithm is written in rust already this commit adds a default trial that uses DenseLayout as a starting point on top of the random trials (and any input starting points). For example if the user specifies to run SabreLayout with 20 layout trials this will run 20 random trials and one trial with `DenseLayout` as the starting point. This is all done directly in the sabre layout rust code for efficiency. The other difference between the standalone `DenseLayout` pass is that in the standalone pass a sparse matrix is built and a reverse Cuthill-McKee permutation is run on the densest subgraph qubits to pick the final layout. This permutation is skipped because in the case of Sabre's use of dense layout we're relying on the sabre algorithm to perform the permutation. Depends on: Qiskit#12104
Building on the work done in Qiskit#10829, Qiskit#10721, and Qiskit#12104 this commit adds a new trial to all runs of `SabreLayout` that runs the dense layout pass. In general the sabre layout algorithm starts from a random layout and then runs a routing algorithm to permute that layout virtually where swaps would be inserted to select a layout that would result in fewer swaps. As was discussed in Qiskit#10721 and Qiskit#10829 that random starting point is often not ideal especially for larger targets where the distance between qubits can be quite far. Especially when the circuit qubit count is low relative to the target's qubit count this can result it poor layouts as the distance between the qubits is too large. In qiskit we have an existing pass, `DenseLayout`, which tries to find the most densely connected n qubit subgraph of a connectivity graph. This algorithm necessarily will select a starting layout where the qubits are near each other and for those large backends where the random starting layout doesn't work well this can improve the output quality. As the overhead of `DenseLayout` is quite low and the core algorithm is written in rust already this commit adds a default trial that uses DenseLayout as a starting point on top of the random trials (and any input starting points). For example if the user specifies to run SabreLayout with 20 layout trials this will run 20 random trials and one trial with `DenseLayout` as the starting point. This is all done directly in the sabre layout rust code for efficiency. The other difference between the standalone `DenseLayout` pass is that in the standalone pass a sparse matrix is built and a reverse Cuthill-McKee permutation is run on the densest subgraph qubits to pick the final layout. This permutation is skipped because in the case of Sabre's use of dense layout we're relying on the sabre algorithm to perform the permutation. Depends on: Qiskit#12104
Building on the work done in #10829, #10721, and #12104 this commit adds a new trial to all runs of `SabreLayout` that runs the dense layout pass. In general the sabre layout algorithm starts from a random layout and then runs a routing algorithm to permute that layout virtually where swaps would be inserted to select a layout that would result in fewer swaps. As was discussed in #10721 and #10829 that random starting point is often not ideal especially for larger targets where the distance between qubits can be quite far. Especially when the circuit qubit count is low relative to the target's qubit count this can result it poor layouts as the distance between the qubits is too large. In qiskit we have an existing pass, `DenseLayout`, which tries to find the most densely connected n qubit subgraph of a connectivity graph. This algorithm necessarily will select a starting layout where the qubits are near each other and for those large backends where the random starting layout doesn't work well this can improve the output quality. As the overhead of `DenseLayout` is quite low and the core algorithm is written in rust already this commit adds a default trial that uses DenseLayout as a starting point on top of the random trials (and any input starting points). For example if the user specifies to run SabreLayout with 20 layout trials this will run 20 random trials and one trial with `DenseLayout` as the starting point. This is all done directly in the sabre layout rust code for efficiency. The other difference between the standalone `DenseLayout` pass is that in the standalone pass a sparse matrix is built and a reverse Cuthill-McKee permutation is run on the densest subgraph qubits to pick the final layout. This permutation is skipped because in the case of Sabre's use of dense layout we're relying on the sabre algorithm to perform the permutation. Depends on: #12104
Building on the work done in Qiskit#10829, Qiskit#10721, and Qiskit#12104 this commit adds a new trial to all runs of `SabreLayout` that runs the dense layout pass. In general the sabre layout algorithm starts from a random layout and then runs a routing algorithm to permute that layout virtually where swaps would be inserted to select a layout that would result in fewer swaps. As was discussed in Qiskit#10721 and Qiskit#10829 that random starting point is often not ideal especially for larger targets where the distance between qubits can be quite far. Especially when the circuit qubit count is low relative to the target's qubit count this can result it poor layouts as the distance between the qubits is too large. In qiskit we have an existing pass, `DenseLayout`, which tries to find the most densely connected n qubit subgraph of a connectivity graph. This algorithm necessarily will select a starting layout where the qubits are near each other and for those large backends where the random starting layout doesn't work well this can improve the output quality. As the overhead of `DenseLayout` is quite low and the core algorithm is written in rust already this commit adds a default trial that uses DenseLayout as a starting point on top of the random trials (and any input starting points). For example if the user specifies to run SabreLayout with 20 layout trials this will run 20 random trials and one trial with `DenseLayout` as the starting point. This is all done directly in the sabre layout rust code for efficiency. The other difference between the standalone `DenseLayout` pass is that in the standalone pass a sparse matrix is built and a reverse Cuthill-McKee permutation is run on the densest subgraph qubits to pick the final layout. This permutation is skipped because in the case of Sabre's use of dense layout we're relying on the sabre algorithm to perform the permutation. Depends on: Qiskit#12104
Summary
This PR augments #10721 with a specific strategy to create a starting sabre layout, implemented as
SabrePreLayout
analysis pass.The pass works by augmenting the coupling map with more and more "extra" edges until VF2 succeeds to find a perfect graph isomorphism. More precisely, the augmented coupling map contains edges between nodes that are within a given distance
d
in the original coupling map, and the value ofd
is increased until an isomorphism is found.Intuitively, a better layout involves fewer extra edges. The pass also optionally minimizes the number of extra edges involved in the layout until a local minimum is found. This involves removing extra edges and running VF2 to see if an isomorphism still exists.
Details and comments
Matthew (@mtreinish) had the idea to mitigate the problem of the transpiler yielding less optimal circuits on larger topologies (described in #10160) by creating "better" starting layouts for Sabre and using these in addition to the fully random layouts (see #10169 and #10721). This particular strategy of creating a "better" layout is heavily inspired by #10169.
Here are a few experiments on the
EfficientSU2
example with circular entanglement from #10160. The quantum circuit isEfficientSU2(num_qubits=16, entanglement='circular', reps=6, flatten=True)
and the coupling map isCouplingMap.from_heavy_hex(15)
.Running Sabre yields the layout on the left. Note that certain pairs of qubits that are connected in the abstract circuit get quite separated in the physical circuit (shown using black arrows). Using SabrePreLayout before Sabre yields the layout in the middle. In this case there is a way to embed the abstract circuit such that all connected nodes will be at most distance-2 apart in the physical map, and Sabre ends up choosing this starting layout over the random layouts. Note however that this layout is still visually non-optimal, containing both a qubit away from the "figure-8" (circled pair of red qubits) and gaps in the "figure-8" (circles blue qubits). Using the additional optional minimization in SabrePreLayout yields the layout on the right, which is the best out of the 3.
Note that heavy-hex architectures a bit funny, in that they also contain heavy pentagons in addition to heavy hexagons (the pentagons are at the border of the coupling map). An even better layout would be a "figure-8" around a heavy-hexagon and a heavy-pentagon, but the proposed method is not guaranteed to find it.
Limitations of the approach
The proposed method is expected to work only for a niche set of applications, where a perfect layout does not exist, but a "close-to-perfect" layout does. For example, it would not be useful for abstract circuits where every pair of qubits is connected. However, the way we have organized the transpiler flow is that
SabrePreLayout
is just an analysis pass that creates additional layouts for Sabre, if these turn out to be not useful, Sabre will end up simply ignoring these.