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 01bcdc7ca2.

---------

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: Kevin Hartman <kevin@hart.mn>
This commit is contained in:
Julien Gacon 2024-09-11 13:55:41 +02:00 committed by GitHub
parent dd145b54be
commit f2ca9205e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 8 deletions

View File

@ -70,7 +70,10 @@ impl FrontLayer {
pub fn remove(&mut self, index: &NodeIndex) {
// The actual order in the indexmap doesn't matter as long as it's reproducible.
// Swap-remove is more efficient than a full shift-remove.
let [a, b] = self.nodes.swap_remove(index).unwrap();
let [a, b] = self
.nodes
.swap_remove(index)
.expect("Tried removing index that does not exist.");
self.qubits[a.index()] = None;
self.qubits[b.index()] = None;
}

View File

@ -27,6 +27,7 @@ use rustworkx_core::petgraph::prelude::*;
use rustworkx_core::petgraph::visit::EdgeRef;
use rustworkx_core::shortest_path::dijkstra;
use rustworkx_core::token_swapper::token_swapper;
use smallvec::{smallvec, SmallVec};
use crate::getenv_use_multiple_threads;
use crate::nlayout::{NLayout, PhysicalQubit};
@ -286,7 +287,7 @@ impl<'a, 'b> RoutingState<'a, 'b> {
fn force_enable_closest_node(
&mut self,
current_swaps: &mut Vec<[PhysicalQubit; 2]>,
) -> NodeIndex {
) -> SmallVec<[NodeIndex; 2]> {
let (&closest_node, &qubits) = {
let dist = &self.target.distance;
self.front_layer
@ -328,7 +329,32 @@ impl<'a, 'b> RoutingState<'a, 'b> {
current_swaps.push([shortest_path[end], shortest_path[end - 1]]);
}
current_swaps.iter().for_each(|&swap| self.apply_swap(swap));
closest_node
// If we apply a single swap it could be that we route 2 nodes; that is a setup like
// A - B - A - B
// and we swap the middle two qubits. This cannot happen if we apply 2 or more swaps.
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]
.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]
}
}
/// 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(
}
if routable_nodes.is_empty() {
// If we exceeded the max number of heuristic-chosen swaps without making progress,
// unwind to the last progress point and greedily swap to bring a ndoe together.
// unwind to the last progress point and greedily swap to bring a node together.
// Efficiency doesn't matter much; this path never gets taken unless we're unlucky.
current_swaps
.drain(..)
.rev()
.for_each(|swap| state.apply_swap(swap));
let force_routed = state.force_enable_closest_node(&mut current_swaps);
routable_nodes.push(force_routed);
routable_nodes.extend(force_routed);
}
state.update_route(&routable_nodes, current_swaps);
if state.heuristic.decay.is_some() {

View File

@ -0,0 +1,7 @@
---
fixes:
- |
Fixed an edge case in :class:`.SabreLayout`, where in rare cases on large
devices and challenging circuits, the routing would fail. This was due to the
release valve making more than one two-qubit gate routable, where only one was expected.
Fixed `#13081 <https://github.com/Qiskit/qiskit/issues/13081>`__.

View File

@ -18,16 +18,16 @@ import math
from qiskit import QuantumRegister, QuantumCircuit
from qiskit.circuit.classical import expr, types
from qiskit.circuit.library import EfficientSU2
from qiskit.circuit.library import EfficientSU2, QuantumVolume
from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager
from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap
from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap, Unroll3qOrMore
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.converters import circuit_to_dag
from qiskit.compiler.transpiler import transpile
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler.passes.layout.sabre_pre_layout import SabrePreLayout
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from test import QiskitTestCase # pylint: disable=wrong-import-order
from test import QiskitTestCase, slow_test # pylint: disable=wrong-import-order
from ..legacy_cmaps import ALMADEN_CMAP, MUMBAI_CMAP
@ -315,6 +315,25 @@ barrier q18585[5],q18585[2],q18585[8],q18585[3],q18585[6];
layout = pass_.property_set["layout"]
self.assertEqual([layout[q] for q in qc.qubits], [2, 3, 4, 1, 5])
@slow_test
def test_release_valve_routes_multiple(self):
"""Test Sabre works if the release valve routes more than 1 operation.
Regression test of #13081.
"""
qv = QuantumVolume(500, seed=42)
qv.measure_all()
qc = Unroll3qOrMore()(qv)
cmap = CouplingMap.from_heavy_hex(21)
pm = PassManager(
[
SabreLayout(cmap, swap_trials=20, layout_trials=20, max_iterations=4, seed=100),
]
)
_ = pm.run(qc)
self.assertIsNotNone(pm.property_set.get("layout"))
class DensePartialSabreTrial(AnalysisPass):
"""Pass to run dense layout as a sabre trial."""