Skip to content

Commit f2ca920

Browse files
Cryorisjakelishmankevinhartman
authored
Fix an edge case in Sabre's release valve (#13114)
* Fix edge case in Sabre release valve If a Sabre trial does not find a set of Swaps to route nodes, the "release valve" adds Swaps to route the two-qubit gate in between the the closest two qubits. In rare cases, this leads to _more_ than one gate being routable, which was not handled correctly previously. Co-authored-by: Jake Lishman <jake.lishman@ibm.com> * add reno and test * Use sensible syntax Co-authored-by: Kevin Hartman <kevin@hart.mn> * Use sensible grammar Co-authored-by: Kevin Hartman <kevin@hart.mn> * clippy * Check if the new test breaks CI * Revert "Check if the new test breaks CI" This reverts commit 01bcdc7. --------- Co-authored-by: Jake Lishman <jake.lishman@ibm.com> Co-authored-by: Kevin Hartman <kevin@hart.mn>
1 parent dd145b5 commit f2ca920

File tree

4 files changed

+63
-8
lines changed

4 files changed

+63
-8
lines changed

crates/accelerate/src/sabre/layer.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ impl FrontLayer {
7070
pub fn remove(&mut self, index: &NodeIndex) {
7171
// The actual order in the indexmap doesn't matter as long as it's reproducible.
7272
// Swap-remove is more efficient than a full shift-remove.
73-
let [a, b] = self.nodes.swap_remove(index).unwrap();
73+
let [a, b] = self
74+
.nodes
75+
.swap_remove(index)
76+
.expect("Tried removing index that does not exist.");
7477
self.qubits[a.index()] = None;
7578
self.qubits[b.index()] = None;
7679
}

crates/accelerate/src/sabre/route.rs

+30-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use rustworkx_core::petgraph::prelude::*;
2727
use rustworkx_core::petgraph::visit::EdgeRef;
2828
use rustworkx_core::shortest_path::dijkstra;
2929
use rustworkx_core::token_swapper::token_swapper;
30+
use smallvec::{smallvec, SmallVec};
3031

3132
use crate::getenv_use_multiple_threads;
3233
use crate::nlayout::{NLayout, PhysicalQubit};
@@ -286,7 +287,7 @@ impl<'a, 'b> RoutingState<'a, 'b> {
286287
fn force_enable_closest_node(
287288
&mut self,
288289
current_swaps: &mut Vec<[PhysicalQubit; 2]>,
289-
) -> NodeIndex {
290+
) -> SmallVec<[NodeIndex; 2]> {
290291
let (&closest_node, &qubits) = {
291292
let dist = &self.target.distance;
292293
self.front_layer
@@ -328,7 +329,32 @@ impl<'a, 'b> RoutingState<'a, 'b> {
328329
current_swaps.push([shortest_path[end], shortest_path[end - 1]]);
329330
}
330331
current_swaps.iter().for_each(|&swap| self.apply_swap(swap));
331-
closest_node
332+
333+
// If we apply a single swap it could be that we route 2 nodes; that is a setup like
334+
// A - B - A - B
335+
// and we swap the middle two qubits. This cannot happen if we apply 2 or more swaps.
336+
if current_swaps.len() > 1 {
337+
smallvec![closest_node]
338+
} else {
339+
// check if the closest node has neighbors that are now routable -- for that we get
340+
// the other physical qubit that was swapped and check whether the node on it
341+
// is now routable
342+
let mut possible_other_qubit = current_swaps[0]
343+
.iter()
344+
// check if other nodes are in the front layer that are connected by this swap
345+
.filter_map(|&swap_qubit| self.front_layer.qubits()[swap_qubit.index()])
346+
// remove the closest_node, which we know we already routed
347+
.filter(|(node_index, _other_qubit)| *node_index != closest_node)
348+
.map(|(_node_index, other_qubit)| other_qubit);
349+
350+
// if there is indeed another candidate, check if that gate is routable
351+
if let Some(other_qubit) = possible_other_qubit.next() {
352+
if let Some(also_routed) = self.routable_node_on_qubit(other_qubit) {
353+
return smallvec![closest_node, also_routed];
354+
}
355+
}
356+
smallvec![closest_node]
357+
}
332358
}
333359

334360
/// Return the swap of two virtual qubits that produces the best score of all possible swaps.
@@ -573,14 +599,14 @@ pub fn swap_map_trial(
573599
}
574600
if routable_nodes.is_empty() {
575601
// If we exceeded the max number of heuristic-chosen swaps without making progress,
576-
// unwind to the last progress point and greedily swap to bring a ndoe together.
602+
// unwind to the last progress point and greedily swap to bring a node together.
577603
// Efficiency doesn't matter much; this path never gets taken unless we're unlucky.
578604
current_swaps
579605
.drain(..)
580606
.rev()
581607
.for_each(|swap| state.apply_swap(swap));
582608
let force_routed = state.force_enable_closest_node(&mut current_swaps);
583-
routable_nodes.push(force_routed);
609+
routable_nodes.extend(force_routed);
584610
}
585611
state.update_route(&routable_nodes, current_swaps);
586612
if state.heuristic.decay.is_some() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed an edge case in :class:`.SabreLayout`, where in rare cases on large
5+
devices and challenging circuits, the routing would fail. This was due to the
6+
release valve making more than one two-qubit gate routable, where only one was expected.
7+
Fixed `#13081 <https://github.com/Qiskit/qiskit/issues/13081>`__.

test/python/transpiler/test_sabre_layout.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@
1818

1919
from qiskit import QuantumRegister, QuantumCircuit
2020
from qiskit.circuit.classical import expr, types
21-
from qiskit.circuit.library import EfficientSU2
21+
from qiskit.circuit.library import EfficientSU2, QuantumVolume
2222
from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager
23-
from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap
23+
from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap, Unroll3qOrMore
2424
from qiskit.transpiler.exceptions import TranspilerError
2525
from qiskit.converters import circuit_to_dag
2626
from qiskit.compiler.transpiler import transpile
2727
from qiskit.providers.fake_provider import GenericBackendV2
2828
from qiskit.transpiler.passes.layout.sabre_pre_layout import SabrePreLayout
2929
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
30-
from test import QiskitTestCase # pylint: disable=wrong-import-order
30+
from test import QiskitTestCase, slow_test # pylint: disable=wrong-import-order
3131

3232
from ..legacy_cmaps import ALMADEN_CMAP, MUMBAI_CMAP
3333

@@ -315,6 +315,25 @@ def test_support_var_with_explicit_routing_pass(self):
315315
layout = pass_.property_set["layout"]
316316
self.assertEqual([layout[q] for q in qc.qubits], [2, 3, 4, 1, 5])
317317

318+
@slow_test
319+
def test_release_valve_routes_multiple(self):
320+
"""Test Sabre works if the release valve routes more than 1 operation.
321+
322+
Regression test of #13081.
323+
"""
324+
qv = QuantumVolume(500, seed=42)
325+
qv.measure_all()
326+
qc = Unroll3qOrMore()(qv)
327+
328+
cmap = CouplingMap.from_heavy_hex(21)
329+
pm = PassManager(
330+
[
331+
SabreLayout(cmap, swap_trials=20, layout_trials=20, max_iterations=4, seed=100),
332+
]
333+
)
334+
_ = pm.run(qc)
335+
self.assertIsNotNone(pm.property_set.get("layout"))
336+
318337

319338
class DensePartialSabreTrial(AnalysisPass):
320339
"""Pass to run dense layout as a sabre trial."""

0 commit comments

Comments
 (0)