Skip to content
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

Merged
merged 20 commits into from
Oct 16, 2023

Conversation

alexanderivrii
Copy link
Contributor

@alexanderivrii alexanderivrii commented Sep 13, 2023

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 of d 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 is EfficientSU2(num_qubits=16, entanglement='circular', reps=6, flatten=True) and the coupling map is CouplingMap.from_heavy_hex(15).

image

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.

@alexanderivrii alexanderivrii requested a review from a team as a code owner September 13, 2023 11:03
@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

  • @Qiskit/terra-core

@coveralls
Copy link

coveralls commented Sep 13, 2023

Pull Request Test Coverage Report for Build 6534170170

  • 87 of 88 (98.86%) changed or added relevant lines in 3 files are covered.
  • 7 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+0.01%) to 87.06%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/transpiler/passes/layout/sabre_pre_layout.py 85 86 98.84%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 93.76%
crates/qasm2/src/lex.rs 6 91.67%
Totals Coverage Status
Change from base Build 6529876136: 0.01%
Covered Lines: 74517
Relevant Lines: 85593

💛 - Coveralls

@alexanderivrii alexanderivrii changed the title [WIP] starting layout analysis pass for sabre Starting layout analysis pass for sabre Sep 15, 2023
@mtreinish mtreinish self-assigned this Sep 19, 2023
@mtreinish mtreinish added the Changelog: New Feature Include in the "Added" section of the changelog label Sep 19, 2023
@mtreinish mtreinish added this to the 0.45.0 milestone Sep 19, 2023
Copy link
Member

@mtreinish mtreinish left a 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.

Comment on lines 35 to 36
target=None,
coupling_map=None,
Copy link
Member

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:

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).

Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just thinking it'd be good for consistency, but I also don't feel super strongly (I preferred the two args myself, but moved to the shared for approach based on review feedback in #9263 ). Personally I just want to implement #9256 so we don't have to worry about this dual path issue anymore.

Copy link
Contributor Author

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)
Copy link
Member

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:

Suggested change
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?

Copy link
Contributor Author

@alexanderivrii alexanderivrii Sep 29, 2023

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.

Copy link
Member

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 :)

Copy link
Contributor Author

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).

qiskit/transpiler/passes/layout/sabre_pre_layout.py Outdated Show resolved Hide resolved
qiskit/transpiler/passes/layout/sabre_pre_layout.py Outdated Show resolved Hide resolved
qiskit/transpiler/passes/layout/sabre_pre_layout.py Outdated Show resolved Hide resolved
@mtreinish
Copy link
Member

I think this is basically ready to go when resolve the question on the heuristic error for the added edges.

@alexanderivrii
Copy link
Contributor Author

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: error_rate(d) = 1 - ( (1 - self.error_rate)^d ), though both this and Matthew's "lazy" heuristic error_rate(d) = min(0.9999, self.error_rate * distance) gave identical results. However now that I have merged main into the branch I see that the transpiler results became very different and much-much better due to other changes in Sabre (though I still don't quite understand the reason).


if self.coupling_map is None:
raise TranspilerError(
"SabrePreLayout requires either target or coupling_map to be provided."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"SabrePreLayout requires either target or coupling_map to be provided."
"SabrePreLayout requires coupling_map to be used with either a "
"CouplingMap or Target."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5e6d3a5.

Comment on lines 56 to 57
target (Target): A target representing the backend device. If specified, it will
supersede a set value for ``coupling_map``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
target (Target): A target representing the backend device. If specified, it will
supersede a set value for ``coupling_map``.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. Fixed in 5e6d3a5.

Comment on lines 58 to 59
coupling_map (Union[CouplingMap, Target]): directed graph representing the
original coupling map.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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).

Copy link
Contributor Author

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)
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

mtreinish
mtreinish previously approved these changes Oct 16, 2023
Copy link
Member

@mtreinish mtreinish left a 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

qiskit/transpiler/passes/layout/sabre_pre_layout.py Outdated Show resolved Hide resolved
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
@mtreinish mtreinish added this pull request to the merge queue Oct 16, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Oct 16, 2023
@jakelishman jakelishman added this pull request to the merge queue Oct 16, 2023
@jakelishman
Copy link
Member

Test failure in the merge queue was a network timeout with installing the dependencies - not a true failure, and hopefully just ephemeral.

Merged via the queue into Qiskit:main with commit df9eae4 Oct 16, 2023
14 checks passed
@alexanderivrii alexanderivrii deleted the initial-layout-for-sabre branch October 23, 2023 07:23
mtreinish added a commit to mtreinish/qiskit-core that referenced this pull request May 22, 2024
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
mtreinish added a commit to mtreinish/qiskit-core that referenced this pull request May 22, 2024
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
github-merge-queue bot pushed a commit that referenced this pull request May 22, 2024
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
ElePT pushed a commit to ElePT/qiskit that referenced this pull request May 31, 2024
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants