Skip to content

Conversation

jakelishman
Copy link
Member

@jakelishman jakelishman commented Aug 26, 2023

Summary

SabreLayout in particular previously had a very confused notion of virtual and physical qubits in Rust space; on each iteration, it rewrote the SabreDAG to "apply" a layout. This is not logically what SabreDAG represents, though; that defines the circuit purely in terms of virtual qubits, which by definition do not change as the mapping of
them to physical qubits changes.

This commit modifies that internal logic so that there is a clear distinction between the virtual DAG and the changing layout. This significantly simplies the logic within the Sabre layout Rust component. This commit is RNG-compatible with its parent.

The Python entry points to the Rust components of both layout and routing are modified to return a final permutation, rather than a final layout, as this is what TranspileLayout.final_layout actually is.

The application of the Sabre result to a DAG is also updated to reflect the correct virtual/physical relationship. With the roles of everything clarified, this now means that in the Sabre layout case, it automatically does the job of ApplyLayout and the subsequent swap-mapping in one iteration, rather than rewriting the DAG once with ApplyLayout only to rewrite it immediately after with the swap-mapped form; the initial layout is after all only valid as far as the first inserted swap.

The Sabre routing inputs are slightly tweaked, so the clone of the layout that the routing pass mutates to track its state as it progresses happens internally. Technically, there could have been situations where it was preferable for the caller in Rust-space to give the output object (if it didn't care about losing the initial layout), but in practice, we always cloned on entry. The cost of the layout clone is not high even in the worst case, and this makes the ownership, mutation and return-value interfaces clearer.

Details and comments

I came across this when trying to move more of the layout application behaviour from Sabre into Rust space; at the moment, there's a lot of quite split logic that means that the Rust component of the Sabre algorithm doesn't have all the information it needs, despite appearances. This refactor now means that a call to build_swap_map_inner supplies it with all the information needed to apply the layout to the original DAG as well, and simplifies a lot of the data.

In practice, it would be far easier for reconstruction if the Rust Sabre components output its swaps in terms of physical qubits rather than virtual ones - we're applying swaps to the physical circuit not the virtual one. This would avoid a lot of faffing around that we have to do during the reconstruction at the moment to handle the ancilla allocations correctly; there's no reason in principle that SabreSwap actually needs its input DAG to be full width other than it's difficult to apply the virtual swaps without it. This especially hurts SabreLayout on disjoint coupling maps, since the ancilla allocation doesn't happen until after the disjoint layout has been done, so we have to run around trying to work out which ancillas were effectively mapped to each component.

Performance:

  • Technically there's now quite a bit less data cloning / rewriting in the Sabre layout algorithm, though in practice I don't expect it to have a notable impact
  • We now elide the call to ApplyLayout from SabreLayout when doing both the layout and routing together, which may be a small performance boost for large circuits that are already close to routed (the relative impact is smaller if we insert many swaps)
  • The DAGOpNode transforming logic in the Sabre reconstruction is now simpler and involves less copying just to throw a node away again, so there might be a little win there.

I'll run some actual benchmarks a bit later, but I'm not worried if this has a neutral effect on performance; this PR is just meant to be clarifying all the logic ahead of a complete rewrite of the Sabre routing reconstruction that's actually intended to be more performant, and to reduce memory usage in the output. edit: I ran the whole suite, and it was neutral in benchmarking.

@jakelishman jakelishman added Changelog: None Do not include in changelog mod: transpiler Issues and PRs related to Transpiler labels Aug 26, 2023
@jakelishman jakelishman requested a review from a team as a code owner August 26, 2023 02:29
@qiskit-bot
Copy link
Collaborator

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

@jakelishman jakelishman force-pushed the sabre/dag-is-logical branch from 5250ae6 to d473402 Compare August 26, 2023 02:33
@coveralls
Copy link

coveralls commented Aug 26, 2023

Pull Request Test Coverage Report for Build 5984811233

  • 166 of 167 (99.4%) changed or added relevant lines in 5 files are covered.
  • 11 unchanged lines in 4 files lost coverage.
  • Overall coverage increased (+0.01%) to 87.268%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/transpiler/passes/routing/sabre_swap.py 48 49 97.96%
Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/sabre_swap/mod.rs 1 97.56%
crates/qasm2/src/expr.rs 1 93.76%
crates/qasm2/src/lex.rs 3 91.39%
crates/accelerate/src/nlayout.rs 6 76.39%
Totals Coverage Status
Change from base Build 5978384848: 0.01%
Covered Lines: 74237
Relevant Lines: 85068

💛 - Coveralls

`SabreLayout` in particular previously had a very confused notion of
virtual and physical qubits in Rust space; on each iteration, it rewrote
the `SabreDAG` to "apply" a layout.  This is not logically what
`SabreDAG` represents, though; that defines the circuit purely in terms
of virtual qubits, which by definition do not change as the mapping of
them to physical qubits changes.

This commit modifies that internal logic so that there is a clear
distinction between the virtual DAG and the changing layout.  This
significantly simplies the logic within the Sabre layout Rust component.
This commit is RNG-compatible with its parent.

The Python entry points to the Rust components of both layout and
routing are modified to return a final permutation, rather than a final
layout, as this is what `TranspileLayout.final_layout` actually is.

The application of the Sabre result to a DAG is also updated to reflect
the correct virtual/physical relationship.  With the roles of everything
clarified, this now means that in the Sabre layout case, it
automatically does the job of `ApplyLayout` _and_ the subsequent
swap-mapping in one iteration, rather than rewriting the DAG once with
`ApplyLayout` only to rewrite it immediately after with the swap-mapped
form; the initial layout is after all only valid as far as the first
inserted swap.

The Sabre routing inputs are slightly tweaked, so the clone of the
layout that the routing pass mutates to track its state as it progresses
happens internally.  _Technically_, there could have been situations
where it was preferable for the caller in Rust-space to give the output
object (if it didn't care about losing the initial layout), but in
practice, we always cloned on entry.  The cost of the layout clone is
not high even in the worst case, and this makes the ownership,
mutation and return-value interfaces clearer.
@jakelishman jakelishman force-pushed the sabre/dag-is-logical branch from d473402 to a09d212 Compare August 26, 2023 12:35
@jakelishman
Copy link
Member Author

The benchmark suite currently doesn't really capture any performance changes from this PR, but locally I've found that it is definitely an improvement in the reconstruction speed of the DAG, particularly after Sabre layout, just by nature of skipping some of the copy and rewrite handling when outputting the swap gates onto the DAG. For this circuit:

from qiskit.circuit.library import QuantumVolume
from qiskit.transpiler import CouplingMap

cm115 = CouplingMap.from_heavy_hex(7)
qc = QuantumVolume(100, seed=0).decompose()

when instrumented with cProfile, I found that during the call transpile(qc, coupling_map=cm115, seed_transpiler=0), Sabre layout went from 4.02(7)s to 3.02(4)s, of which the DAG reconstruction went from 2.03(7)s to 1.25(4)s; it's a fairly sizeable improvement. That said, those numbers are skewed a bit in this pass's favour; the older version makes more small function calls during DAG reconstruction, which means it also accrues more overhead from cProfile's timing hooks. I found the whole transpile call timed with timeit to go from 3.77(4)s to 3.32(9)s (the numbers are lower than before because of the lack of instrumentation), which is more indicative of the time save.

A ~12% improvement in layout performance is pretty good going here, considering this PR isn't even meant to be the performance focussed one. This is just the effect of tidying up all the logic; we need to do less to unpick the confused layouts during reconstruction now, so it's all easier.

@mtreinish mtreinish self-assigned this Aug 28, 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.

Overall this LGTM, this is big improvement in legibility of the code and it's easier to follow the transforms that are being made. Just a few inline comments, but nothing major.

@@ -39,16 +51,18 @@ impl SabreDAG {
num_clbits: usize,
nodes: Vec<(usize, Vec<usize>, HashSet<usize>)>,
node_blocks: HashMap<usize, Vec<SabreDAG>>,
) -> PyResult<Self> {
) -> Self {
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, nothing was returning an error before?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's no detection in here of failure, though I guess Rust would panic if fed "qubits" or "clbits" that are out of range. If we're worried about that, we could (possibly should) add proper error handling, but maybe in a separate PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

See #10724 for a possible change here.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I wasn't necessarily suggesting we should be returning an error here. I was more just surprised that nothing complained about us having a result return without an error anywhere in the function. I was assuming clippy or something would see we only returned Ok(..) and would have suggested dropping the result (although maybe PyResult isn't tracked like that)

Copy link
Member Author

@jakelishman jakelishman Aug 29, 2023

Choose a reason for hiding this comment

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

If it's a public function, I guess clippy couldn't complain about the return value because it could be that it's a defined part of the interface that you (for example) can use the function object as having a particular type in a different crate. But also the (spelling of the) ! type to make infallible Result<_, !> types is relatively new as well, so it may well be that clippy just doesn't care about unnecessary result wrapping.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also about the error: I feels a shade nicer to check and error, but I don't feel strongly about it. We can either merge or close #10724 as people prefer.

swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]]
apply_gate(
mapped_dag,
DAGOpNode(op=SwapGate(), qargs=swap_qargs),
Copy link
Member

Choose a reason for hiding this comment

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

I'm glad to see this oddity disappear. There wasn't ever a real reason we should have been manually creating dag nodes.

Copy link
Contributor

@kevinhartman kevinhartman left a comment

Choose a reason for hiding this comment

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

LGTM overall as well. I remember wondering why it was necessary to walk the SabreDAG to manually adjust the indices inside SabreLayout while adding control flow support. These changes make the separation a lot clearer.

I also like that you got rid of the oddness of us moving a layout into swap_map_trial and then out of it on return, which previously felt like it was obfuscating the semantics of what was really going on.

let layout_dag = apply_layout(&dag_forward, &initial_layout);
let mut pass_final_layout = NLayout::generate_trivial_layout(num_physical_qubits);
build_swap_map_inner(
for dag in [&dag_no_control_forward, &dag_no_control_reverse] {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this :D

jakelishman and others added 2 commits August 29, 2023 11:38
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: Kevin Hartman <kevin@hart.mn>
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, thanks for the quick updates.

@@ -97,7 +96,7 @@ pub fn sabre_layout_and_routing(
};
(
res.0,
res.1,
PyArray::from_vec(py, res.1).into(),
Copy link
Member

Choose a reason for hiding this comment

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

Hah I didn't even know this method or from_iter() existed. Looking at the rust-numpy source it looks like it just calls into_pyarray() internally. I wonder if this just my knowledge of the library is several years out of date and these didn't exist when I started using it. It's a little bit more ergonomic looking than the .into_pyarray() I used everywhere else to do a no copy numpy array creation.

@mtreinish mtreinish added this pull request to the merge queue Aug 29, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 29, 2023
@jakelishman jakelishman added this pull request to the merge queue Aug 30, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 30, 2023
@jakelishman jakelishman added this pull request to the merge queue Aug 30, 2023
Merged via the queue into Qiskit:main with commit dbc4506 Aug 30, 2023
@jakelishman jakelishman deleted the sabre/dag-is-logical branch August 30, 2023 10:11
SameerD-phys pushed a commit to SameerD-phys/qiskit-terra that referenced this pull request Sep 7, 2023
* Fix virtual/physical-qubit distinction in Sabre

`SabreLayout` in particular previously had a very confused notion of
virtual and physical qubits in Rust space; on each iteration, it rewrote
the `SabreDAG` to "apply" a layout.  This is not logically what
`SabreDAG` represents, though; that defines the circuit purely in terms
of virtual qubits, which by definition do not change as the mapping of
them to physical qubits changes.

This commit modifies that internal logic so that there is a clear
distinction between the virtual DAG and the changing layout.  This
significantly simplies the logic within the Sabre layout Rust component.
This commit is RNG-compatible with its parent.

The Python entry points to the Rust components of both layout and
routing are modified to return a final permutation, rather than a final
layout, as this is what `TranspileLayout.final_layout` actually is.

The application of the Sabre result to a DAG is also updated to reflect
the correct virtual/physical relationship.  With the roles of everything
clarified, this now means that in the Sabre layout case, it
automatically does the job of `ApplyLayout` _and_ the subsequent
swap-mapping in one iteration, rather than rewriting the DAG once with
`ApplyLayout` only to rewrite it immediately after with the swap-mapped
form; the initial layout is after all only valid as far as the first
inserted swap.

The Sabre routing inputs are slightly tweaked, so the clone of the
layout that the routing pass mutates to track its state as it progresses
happens internally.  _Technically_, there could have been situations
where it was preferable for the caller in Rust-space to give the output
object (if it didn't care about losing the initial layout), but in
practice, we always cloned on entry.  The cost of the layout clone is
not high even in the worst case, and this makes the ownership,
mutation and return-value interfaces clearer.

* Fix out-of-date types

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: Kevin Hartman <kevin@hart.mn>

* Improve efficiency of interfaces

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: Kevin Hartman <kevin@hart.mn>
jakelishman added a commit to jakelishman/qiskit-terra that referenced this pull request Apr 7, 2025
This has been a bug since Qiskitgh-10712, but wasn't caught by the integration
test in the test suite because that constructs a backend with all-to-all
connectivity, so the layout stage was short-circuiting out after VF2.
jakelishman added a commit to jakelishman/qiskit-terra that referenced this pull request Apr 7, 2025
This has been a bug since Qiskitgh-10712, but wasn't caught by the integration
test in the test suite because the circuit has a perfect layout, so the
layout stage was short-circuiting out after VF2.
github-merge-queue bot pushed a commit that referenced this pull request Apr 7, 2025
This has been a bug since gh-10712, but wasn't caught by the integration
test in the test suite because the circuit has a perfect layout, so the
layout stage was short-circuiting out after VF2.
mergify bot pushed a commit that referenced this pull request Apr 7, 2025
This has been a bug since gh-10712, but wasn't caught by the integration
test in the test suite because the circuit has a perfect layout, so the
layout stage was short-circuiting out after VF2.

(cherry picked from commit 2504bde)
mergify bot pushed a commit that referenced this pull request Apr 7, 2025
This has been a bug since gh-10712, but wasn't caught by the integration
test in the test suite because the circuit has a perfect layout, so the
layout stage was short-circuiting out after VF2.

(cherry picked from commit 2504bde)

# Conflicts:
#	qiskit/transpiler/passes/layout/sabre_layout.py
github-merge-queue bot pushed a commit that referenced this pull request Apr 7, 2025
This has been a bug since gh-10712, but wasn't caught by the integration
test in the test suite because the circuit has a perfect layout, so the
layout stage was short-circuiting out after VF2.

(cherry picked from commit 2504bde)

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
github-merge-queue bot pushed a commit that referenced this pull request Apr 9, 2025
…14190)

* Fix circuit-metadata propagation in `SabreLayout` (#14186)

This has been a bug since gh-10712, but wasn't caught by the integration
test in the test suite because the circuit has a perfect layout, so the
layout stage was short-circuiting out after VF2.

(cherry picked from commit 2504bde)

# Conflicts:
#	qiskit/transpiler/passes/layout/sabre_layout.py

* Fix merge conflicts

* Adjust plugins used in testing for 1.4.x

---------

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: None Do not include in changelog mod: transpiler Issues and PRs related to Transpiler
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants