Skip to content

Refactor Sabre state into separately mutable components#14909

Merged
alexanderivrii merged 3 commits intoQiskit:mainfrom
jakelishman:sabre/layers/1
Mar 29, 2026
Merged

Refactor Sabre state into separately mutable components#14909
alexanderivrii merged 3 commits intoQiskit:mainfrom
jakelishman:sabre/layers/1

Conversation

@jakelishman
Copy link
Copy Markdown
Member

This commit separates out the three logical components of the Sabre routing tracking into three separate structs, where each struct groups objects that have the same mutation tendency.

The previous Sabre state stored its problem description, internal tracking, and output tracking altogether in the same flat structure. Those three components have different tendencies to mutate: the problem description never mutates, the internal tracking frequently does, and the output tracking only occasionally does and has a lifetime validity tied to that of the problem description.

Putting them together made it impossible to call methods that mutated the state while passing an object that borrowed from the problem description, such as when recursing into control-flow operations, because the borrow checker could not validate it. This applied interface pressure to inline more into the same method, which made code-reuse of separate concerns harder.

@jakelishman jakelishman requested a review from a team as a code owner August 14, 2025 15:10
@jakelishman jakelishman added this to the 2.2.0 milestone Aug 14, 2025
@jakelishman jakelishman added the mod: transpiler Issues and PRs related to Transpiler label Aug 14, 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

Pull Request Test Coverage Report for Build 16969141168

Details

  • 101 of 110 (91.82%) changed or added relevant lines in 4 files are covered.
  • 40 unchanged lines in 5 files lost coverage.
  • Overall coverage decreased (-0.03%) to 88.229%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/transpiler/src/passes/sabre/route.rs 97 100 97.0%
crates/circuit/src/nlayout.rs 0 6 0.0%
Files with Coverage Reduction New Missed Lines %
crates/transpiler/src/passes/sabre/dag.rs 1 96.97%
crates/qasm2/src/lex.rs 3 92.27%
crates/transpiler/src/passes/sabre/route.rs 3 93.96%
crates/circuit/src/parameter/symbol_expr.rs 9 74.33%
crates/qasm2/src/parse.rs 24 95.68%
Totals Coverage Status
Change from base Build 16967470304: -0.03%
Covered Lines: 87812
Relevant Lines: 99527

💛 - Coveralls

This commit separates out the three logical components of the Sabre
routing tracking into three separate structs, where each struct groups
objects that have the same mutation tendency.

The previous Sabre state stored its problem description, internal
tracking, and output tracking altogether in the same flat structure.
Those three components have different tendencies to mutate: the problem
description never mutates, the internal tracking frequently does, and
the output tracking only occasionally does and has a lifetime validity
tied to that of the problem description.

Putting them together made it impossible to call methods that mutated
the state while passing an object that borrowed from the problem
description, such as when recursing into control-flow operations,
because the borrow checker could not validate it.  This applied
interface pressure to inline more into the same method, which made
code-reuse of separate concerns harder.
@jakelishman jakelishman removed the on hold Can not fix yet label Mar 24, 2026
@jakelishman
Copy link
Copy Markdown
Member Author

Uncoupled from its parents, so no longer on hold.

Copy link
Copy Markdown
Member

@alexanderivrii alexanderivrii 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 split into components makes sense to me. Most of the changes are more or less mechanical and easy to follow (and I had a question about one particular code simplification).

Comment thread crates/transpiler/src/passes/sabre/route.rs Outdated
pub fn rebuild(&self) -> PyResult<DAGCircuit> {
let num_swaps = self.swap_count();
let dag = self.dag.physical_empty_like_with_capacity(
let num_swaps = self.order.swap_count();
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.

self.swap_count() would also work, correct?

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.

Yeah. Some of the slightly weird diff here might be from rebases / reworking the chain a couple of times.

Comment thread crates/transpiler/src/passes/sabre/route.rs
Comment on lines +662 to +700
if current_swaps.len() > 1 {
smallvec![closest_node]
} else {
// check if the closest node has neighbors that are now routable -- for that we get
// the other physical qubit that was swapped and check whether the node on it
// is now routable
let mut possible_other_qubit = current_swaps[0]
match current_swaps.as_slice() {
[swap] => swap
.iter()
// check if other nodes are in the front layer that are connected by this swap
.filter_map(|&swap_qubit| self.front_layer.qubits()[swap_qubit.index()])
// remove the closest_node, which we know we already routed
.filter(|(node_index, _other_qubit)| *node_index != closest_node)
.map(|(_node_index, other_qubit)| other_qubit);

// if there is indeed another candidate, check if that gate is routable
if let Some(other_qubit) = possible_other_qubit.next() {
if let Some(also_routed) = self.routable_node_on_qubit(other_qubit) {
return smallvec![closest_node, also_routed];
}
}
smallvec![closest_node]
.filter_map(|q| self.routable_node_on_qubit(problem, *q))
.collect(),
_ => smallvec![closest_node],
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.

I have not looked at this change closely yet, but it removes quite a number of lines. Can you comment as to what is going on?

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.

Normally in the release valve, we expect that there's only one newly routable gate. If there's more than one swap in the chain, it's only possible for there to be only one routable gate. In the special case of the release valve needing only a single swap (which needs a very particular set of heuristics and awkwardness in the layer structure), you can get something like A - B - A - B where the middle two qubits get swapped, and now two gates are routed.

The old code for this (see #13114) was kind of complex, as you see - its logic was something like:

  • for each qubit in involved in the swap...
  • find the gate (if any) that it is involved in
  • ignore that gate if it's closest_node
  • get the other qubit involved in that gate
  • get the routable node on that qubit (if any, except we already know that there is one)
  • emit both closest_node and the other node.

Apparently I changed this as part of this PR (presumably I had to modify the self.front_layer and went overboard?), but the new logic is just:

  • ignore closest_node - we'll find it again
  • for each qubit in the swap...
  • emit the routable gate on it, if any

which is obviously way simpler, and is still guaranteed to include closest_node and not duplicate it: the two qubits can't both be in closest_node or we wouldn't have entered the release valve; and we know closest_node is now routable because we took the Dijkstra path specifically to it.

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.

Thanks for the explanation -- the code simplification makes sense. My only mild concern is that this should ideologically belong to a separate PR.

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.

No worries, reverted in 607d159 and I can open a new PR once this merges.

Copy link
Copy Markdown
Member

@alexanderivrii alexanderivrii left a comment

Choose a reason for hiding this comment

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

Thanks Jake. This looks good to me, modulo possibly splitting the release valve mechanism simplification into a separate PR. Or at least mentioning this simplification in the PR summary.

Copy link
Copy Markdown
Member

@alexanderivrii alexanderivrii left a comment

Choose a reason for hiding this comment

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

Thanks!

@alexanderivrii alexanderivrii added this pull request to the merge queue Mar 29, 2026
Merged via the queue into Qiskit:main with commit 2941838 Mar 29, 2026
25 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in Qiskit 2.5 Mar 29, 2026
@jakelishman jakelishman deleted the sabre/layers/1 branch March 29, 2026 11:02
@ShellyGarion ShellyGarion added the Changelog: None Do not include in the GitHub Release changelog. label Apr 17, 2026
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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

8 participants