Skip to content

Collapse Sabre nodes with no routing implications#14607

Merged
mtreinish merged 2 commits intoQiskit:mainfrom
jakelishman:sabre/compress
Aug 13, 2025
Merged

Collapse Sabre nodes with no routing implications#14607
mtreinish merged 2 commits intoQiskit:mainfrom
jakelishman:sabre/compress

Conversation

@jakelishman
Copy link
Copy Markdown
Member

Summary

Not all nodes in the SabreDAG actually imply additional routing constraints. For example, a 2q gate immediately following a prior 2q gate on the same qubits doesn't need to be routed; it's automatically valid as soon as its predecessor is. Similarly, 1q gates (without additional classical wires) never impact routing and can be folded into previous instructions.

More formally, a Synchronize node whose incoming edges are all from the same predecessor (or are at the start of the circuit) does not actually synchronise anything, and can be folded into any preceding node. A TwoQ node whose incoming edges are all from the same predecessor can be folded into that predecessor if it is another TwoQ node (even if there were already intermediate Synchronize nodes folded into that TwoQ node). ControlFlow nodes always have to be considered by the router, because they need to be recursed into.

Collapsing these nodes as part of the SabreDAG construction has a few benefits:

  • In layout, when we keep multiple versions of a SabreDAG around, we have to store less memory, especially because we can entirely throw away all tracking of the collapsed DAG nodes, since we never need to rebuild afterwards.

  • In routing, when we look ahead to populate the extended set, we no longer need to have the separate visit_now queue because all the nodes (and more!) that we would "look through" to make a DFS over the 2q gates are now automatically folded into the preceding 2q gate already. The behaviour is not entirely identical (the handling of wide Synchronize items can be slightly different), but the spirit is still there, and the handling is much easier.

  • When populating the extended set, runs of 2q gates can no longer be multiply counted. Previously, a run could saturate the extended set, biasing the algorithm towards that particular pair, even though the routing constraint was no stronger than any other single gate.

Details and comments

Based on #14317.

@jakelishman jakelishman added this to the 2.2.0 milestone Jun 15, 2025
@jakelishman jakelishman added the on hold Can not fix yet label Jun 15, 2025
@jakelishman jakelishman requested a review from a team as a code owner June 15, 2025 22:06
@jakelishman jakelishman added performance Rust This PR or issue is related to Rust code in the repository mod: transpiler Issues and PRs related to Transpiler labels Jun 15, 2025
@qiskit-bot
Copy link
Copy Markdown
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@coveralls
Copy link
Copy Markdown

coveralls commented Jun 15, 2025

Pull Request Test Coverage Report for Build 16883428058

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 65 of 68 (95.59%) changed or added relevant lines in 2 files are covered.
  • 2105 unchanged lines in 58 files lost coverage.
  • Overall coverage decreased (-0.07%) to 87.693%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/transpiler/src/passes/sabre/dag.rs 44 45 97.78%
crates/transpiler/src/passes/sabre/route.rs 21 23 91.3%
Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/circuit_library/pauli_evolution.rs 1 98.98%
crates/synthesis/src/linear_phase/mod.rs 1 90.91%
qiskit/circuit/commutation_checker.py 1 94.74%
qiskit/circuit/parameterexpression.py 1 98.08%
qiskit/passmanager/passmanager.py 1 94.32%
qiskit/primitives/containers/observables_array.py 1 97.14%
qiskit/transpiler/passes/optimization/elide_permutations.py 1 91.3%
qiskit/transpiler/passes/optimization/split_2q_unitaries.py 1 91.3%
qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py 1 98.21%
qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py 1 97.3%
Totals Coverage Status
Change from base Build 16145526249: -0.07%
Covered Lines: 82419
Relevant Lines: 93986

💛 - Coveralls

@raynelfss raynelfss assigned jakelishman and unassigned jakelishman Jul 3, 2025
Not all nodes in the `SabreDAG` actually imply additional routing
constraints.  For example, a 2q gate immediately following a prior 2q
gate on the same qubits doesn't need to be routed; it's automatically
valid as soon as its predecessor is.  Similarly, 1q gates (without
additional classical wires) never impact routing and can be folded into
previous instructions.

More formally, a `Synchronize` node whose incoming edges are all from
the same predecessor (or are at the start of the circuit) does not
actually synchronise anything, and can be folded into any preceding
node.  A `TwoQ` node whose incoming edges are all from the same
predecessor can be folded into that predecessor if it is another `TwoQ`
node (even if there were already intermediate `Synchronize` nodes folded
into that `TwoQ` node).  `ControlFlow` nodes always have to be
considered by the router, because they need to be recursed into.

Collapsing these nodes as part of the `SabreDAG` construction has a few
benefits:

- In layout, when we keep multiple versions of a `SabreDAG`
  around, we have to store less memory, especially because we can
  entirely throw away all tracking of the collapsed DAG nodes, since we
  never need to rebuild afterwards.

- In routing, when we look ahead to populate the extended set, we no
  longer need to have the separate `visit_now` queue because all the
  nodes (and more!) that we would "look through" to make a DFS over the
  2q gates are now automatically folded into the preceding 2q gate
  already.  The behaviour is not entirely identical (the handling of
  wide `Synchronize` items can be slightly different), but the spirit is
  still there, and the handling is much easier.

- When populating the extended set, runs of 2q gates can no longer be
  multiply counted.  Previously, a run _could_ saturate the extended
  set, biasing the algorithm towards that particular pair, even though
  the routing constraint was no stronger than any other single gate.
@jakelishman jakelishman removed the on hold Can not fix yet label Jul 8, 2025
@jakelishman
Copy link
Copy Markdown
Member Author

Now rebased over #14317.

mtreinish
mtreinish previously approved these changes Aug 6, 2025
Copy link
Copy Markdown
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, it's fairly straightforward and is a nice optimization that I think is really neat. I had one nit inline. Feel free to enqueue if you don't want to change it otherwise I'll circle back with a re-approval if you do change it.

InteractionKind::Synchronize => {
initial.push(dag_node);
}
kind => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There a so many weird match syntax things I don't know about. I really didn't know this worked until just now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this is just the root case of pattern-match syntax you're already familiar with? You use

match <expr> {
  InteractionKind::ControlFlow(num_blocks) => (),
}

happily, where num_blocks assigns a name to a partial destructuring. The highlighted line here is the same, there's just no destructuring happening; it's matching the root pattern. It's the same thing as a _ => () line, but I gave the pattern a name rather than throwing it away.

Comment thread crates/transpiler/src/passes/sabre/route.rs Outdated
@jakelishman jakelishman requested a review from mtreinish August 11, 2025 21:53
@raynelfss raynelfss added the Changelog: None Do not include in the GitHub Release changelog. label Aug 13, 2025
@mtreinish mtreinish added this pull request to the merge queue Aug 13, 2025
Merged via the queue into Qiskit:main with commit e364cd9 Aug 13, 2025
27 checks passed
@jakelishman jakelishman deleted the sabre/compress branch August 13, 2025 22:45
littlebullGit pushed a commit to littlebullGit/qiskit that referenced this pull request Sep 5, 2025
* Collapse Sabre nodes with no routing implications

Not all nodes in the `SabreDAG` actually imply additional routing
constraints.  For example, a 2q gate immediately following a prior 2q
gate on the same qubits doesn't need to be routed; it's automatically
valid as soon as its predecessor is.  Similarly, 1q gates (without
additional classical wires) never impact routing and can be folded into
previous instructions.

More formally, a `Synchronize` node whose incoming edges are all from
the same predecessor (or are at the start of the circuit) does not
actually synchronise anything, and can be folded into any preceding
node.  A `TwoQ` node whose incoming edges are all from the same
predecessor can be folded into that predecessor if it is another `TwoQ`
node (even if there were already intermediate `Synchronize` nodes folded
into that `TwoQ` node).  `ControlFlow` nodes always have to be
considered by the router, because they need to be recursed into.

Collapsing these nodes as part of the `SabreDAG` construction has a few
benefits:

- In layout, when we keep multiple versions of a `SabreDAG`
  around, we have to store less memory, especially because we can
  entirely throw away all tracking of the collapsed DAG nodes, since we
  never need to rebuild afterwards.

- In routing, when we look ahead to populate the extended set, we no
  longer need to have the separate `visit_now` queue because all the
  nodes (and more!) that we would "look through" to make a DFS over the
  2q gates are now automatically folded into the preceding 2q gate
  already.  The behaviour is not entirely identical (the handling of
  wide `Synchronize` items can be slightly different), but the spirit is
  still there, and the handling is much easier.

- When populating the extended set, runs of 2q gates can no longer be
  multiply counted.  Previously, a run _could_ saturate the extended
  set, biasing the algorithm towards that particular pair, even though
  the routing constraint was no stronger than any other single gate.

* Make conditional binding clearer
aaryav-3 pushed a commit to aaryav-3/qiskit that referenced this pull request Oct 21, 2025
* Collapse Sabre nodes with no routing implications

Not all nodes in the `SabreDAG` actually imply additional routing
constraints.  For example, a 2q gate immediately following a prior 2q
gate on the same qubits doesn't need to be routed; it's automatically
valid as soon as its predecessor is.  Similarly, 1q gates (without
additional classical wires) never impact routing and can be folded into
previous instructions.

More formally, a `Synchronize` node whose incoming edges are all from
the same predecessor (or are at the start of the circuit) does not
actually synchronise anything, and can be folded into any preceding
node.  A `TwoQ` node whose incoming edges are all from the same
predecessor can be folded into that predecessor if it is another `TwoQ`
node (even if there were already intermediate `Synchronize` nodes folded
into that `TwoQ` node).  `ControlFlow` nodes always have to be
considered by the router, because they need to be recursed into.

Collapsing these nodes as part of the `SabreDAG` construction has a few
benefits:

- In layout, when we keep multiple versions of a `SabreDAG`
  around, we have to store less memory, especially because we can
  entirely throw away all tracking of the collapsed DAG nodes, since we
  never need to rebuild afterwards.

- In routing, when we look ahead to populate the extended set, we no
  longer need to have the separate `visit_now` queue because all the
  nodes (and more!) that we would "look through" to make a DFS over the
  2q gates are now automatically folded into the preceding 2q gate
  already.  The behaviour is not entirely identical (the handling of
  wide `Synchronize` items can be slightly different), but the spirit is
  still there, and the handling is much easier.

- When populating the extended set, runs of 2q gates can no longer be
  multiply counted.  Previously, a run _could_ saturate the extended
  set, biasing the algorithm towards that particular pair, even though
  the routing constraint was no stronger than any other single gate.

* Make conditional binding clearer
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 the GitHub Release changelog. mod: transpiler Issues and PRs related to Transpiler performance Rust This PR or issue is related to Rust code in the repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants