Repository: Qiskit/qiskit

Issue:
SabreLayout does not terminate in some cases
### Environment

Qiskit version: master
Python version: 3.8.8
OS: Mac OS 12.2

### What is happening?

SabreSwap gets stuck in some specific cases and does not make forward progress.



### How can we reproduce the issue?

here's an example that runs indefinitely for me. Note that even though I run SabreLayout, internally it is calling SabreSwap and that's where it gets stuck.

```py
from qiskit.circuit.library import QuantumVolume
from qiskit.transpiler import PassManager, CouplingMap
from qiskit.transpiler.passes import SabreLayout

n = 30
coupling_map = CouplingMap.from_line(n)
basis_gates=['cx', 'u']
qv = QuantumVolume(n, seed=983).decompose()
pm = PassManager(SabreLayout(coupling_map, seed=1))
pm.run(qv)
```

### What should happen?

map the circuit and terminate

### Any suggestions?

I think the SabreSwap algorithm has a way to score different swap options and to make forward progress, so not sure what's going on.


Discussion:
@ajavadia I can take a look at this.
@ajavadia Just an initial report. I turned debug logging on for the `SabreSwap `runs and have determined that there are 6 calls to `SabreSwap.run`. The first 4 and the sixth runs work, but the 5th one gets in a state where it can't make progress. The `extended_set `is the same 4 qubits and the `best_swap `is the same 2 qubits.

Next step is to work back to see where this begins and to see why `qubits_decay` doesn't get the run out of the loop. Will report any progress.
@ajavadia Here's a rundown of what I've found so far.
- There are 6 calls to SabreSwap from SabreLayout. Given the seeds in your test case, 5 of the calls terminate after about 2500 internal loops. The other one does not terminate.
- For the run that doesn't terminate, on loop 2451, a new front_layer appears and a new extended_set. From that point on, the extended_set does not change.
- On loop 2483, a final new front_layer appears.
- Beyond that point, the best_swap changes regularly, but there is no longer any progress made on that front_layer.
- If I adjust any of the 4 global parameters at the top of sabre_swap.py, either up or down, by arbitrary amounts, all the calls terminate.

I have uploaded a log file
[sabre_bad_run1.txt](https://github.com/Qiskit/qiskit-terra/files/8215919/sabre_bad_run1.txt)
 that goes from loop 2300 to loop 2600 for that non-terminating loop if anyone else wants to look at that. Search for 
'count: 2451' to locate the start of the issue.

I'm not sure why the process gets stuck, but if you can point me in a direction, I'm willing to pursue this further.
Thanks for looking into it @enavarro51.  The extended_set here is the set of node pairs in the DAG that we must make neighbors through insertion of swaps. Since `best_swap` gets picked at every step but `extended_set` does not change, it looks like it is just swapping qubits around without ever actually making progress on resolving the extended_set (i.e. it goes in circles, sometimes those nodes get closer and sometimes get further). Maybe we can draw this on paper to see which swaps get inserted and why they never converge.
This might not be the only case of an infinite loop in the algorithm, but I'm confident that I understand how it happens here, and I've produced a minimal circuit for which `SabreSwap` immediately enters an infinite loop from which it can provably never escape no matter how the pass is seeded:
```text
 q_0: ─■────────────────
       │
 q_1: ─┼──■─────────────
       │  │
 q_2: ─┼──┼──■──────────
       │  │  │
 q_3: ─┼──┼──┼──■───────
       │  │  │  │
 q_4: ─┼──┼──┼──┼─────■─
       │  │  │  │     │
 q_5: ─┼──┼──┼──┼──■──■─
       │  │  │  │  │
 q_6: ─┼──┼──┼──┼──┼────
       │  │  │  │  │
 q_7: ─┼──┼──┼──┼──■──■─
       │  │  │  │     │
 q_8: ─┼──┼──┼──┼─────■─
       │  │  │  │
 q_9: ─┼──┼──┼──■───────
       │  │  │
q_10: ─┼──┼──■──────────
       │  │
q_11: ─┼──■─────────────
       │
q_12: ─■────────────────
```
The relevant coupling map is a line in the same order the qubits are displayed in the drawing.  The first five gates are the front layer, and the second two are the extended set.  The algorithm will get stuck making random swaps from the set `[(0, 1), (1, 2), (2, 3), (9, 10), (10, 11), (11, 12)]` (i.e. the outermost qubits), even though the only way it could possibly progress (and would do immediately) would be to swap `(5, 6)` or `(6, 7)`.

It gets stuck because it's in a stable local minimum of the (non-basic) heuristics.  The only way it can advance is by making a swap that temporarily increases the score from the "lookahead" components.  I've written a routine that produces a circuit with arbitrarily many layouts in the stable local minimum region (increase `additional_local_minimum_gates`), and requires arbitrarily many swaps to be made "uphill" against the heuristic (increase `uphill_swaps`) before progress can be made:
```python
from qiskit.circuit import QuantumCircuit

def looping_circuit(uphill_swaps=1, additional_local_minimum_gates=0):
    """Create a circuit that is arbitrarily hard for the advanced SABRE
    heuristics to route.

    The circuit is in a stable local minimum of the "lookahead" and "decay"
    SABRE heuristics, where you need to increase the values of those heuristics
    before you can make progress.  This minimum is stable no matter what
    weighting is attached to the cost of the gates in the extended set (except
    if they're weighted as zero).  If the decay reset interval is set
    sufficiently high, it _can_ become possible to escape the minimum, but you
    need to get arbitrarily lucky as the two inputs to this function are
    increased.
    
    Draw the circuit output from this function to see the hill structure.

    Args:
        uphill_swaps (int): how many swaps SABRE will need to make that increase
            the total score of the layout before it can make further progress
        additional_local_minimum_gates (int): how many additional gates to add
            in the front layer that produces the local minumum that the routing
            pass gets stuck in.  This increases the number of layouts that all
            have the same score that the pass will get stuck switching between.
    """
    outers = 4 + additional_local_minimum_gates
    n_qubits = 2*outers + 4 + uphill_swaps
    # This is (most of) the front layer, which is a bunch of outer qubits in the
    # coupling map.
    outer_pairs = [(i, n_qubits - i - 1) for i in range(outers)]
    inner_heuristic_peak = [
        # This gate is completely "inside" all the others in the front layer in
        # terms of the coupling map, so it's the only one that we can in theory
        # make progress towards without making the others worse.
        (outers + 1, outers + 2 + uphill_swaps),
        # These are the only two gates in the extended set, and they both get
        # further apart if you make a swap to bring the above gate closer
        # together, which is the trick that creates the "heuristic hill".
        (outers, outers + 1),
        (outers + 2 + uphill_swaps, outers + 3 + uphill_swaps),
    ]
    qc = QuantumCircuit(n_qubits)
    for pair in outer_pairs + inner_heuristic_peak:
        qc.cz(*pair)
    return qc
```

If you want to run the infinite loop, the extra bit to demonstrate it is:
```python
from qiskit.converters import circuit_to_dag
from qiskit.transpiler import CouplingMap
from qiskit.transpiler.passes import SabreSwap

def infinite_loop(circuit, method="lookahead"):
    if method == "basic":
        raise Exception("the 'basic' heuristic can't get stuck")
    coupling_map = CouplingMap.from_line(circuit.num_qubits)
    # If called on the output of `looping_circuit`, this will never return.
    return SabreSwap(coupling_map, method).run(circuit_to_dag(circuit))
```
I have several potential modifications to the heuristics that would always allow it to progress eventually, but with the extra parameters in `looping_circuit` above, I can make all of them either take factorial (I think) amounts of memory to detect or require exponential time in the number of qubits in the circuit.
Oh, I'm actually slightly wrong in the docstring of `looping_circuit` above - it's not _a priori_ stable against the weighting of the extended set heuristic additions, but with a suitably large `additional_local_minimum_gates` you can make it stable against a weighting that becomes arbitrarily close to zero.

The requirement to have _four_ outer gates right now is because of our particular choice of `EXTENDED_SET_WEIGHT = 0.5`.  If we instead set that to 1, we'd only need three outer gates to make it work, so we could reduce the minimal reproducer by two qubits. In general, you can eventually escape the loop if you satisfy `1 / (n_outer_gates + 1) >= EXTENDED_SET_WEIGHT / 2` in this configuration.

Resolving patch:
diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py
--- a/qiskit/transpiler/passes/routing/sabre_swap.py
+++ b/qiskit/transpiler/passes/routing/sabre_swap.py
@@ -15,7 +15,9 @@
 import logging
 from collections import defaultdict
 from copy import copy, deepcopy
+
 import numpy as np
+import retworkx
 
 from qiskit.circuit.library.standard_gates import SwapGate
 from qiskit.transpiler.basepasses import TransformationPass
@@ -64,7 +66,13 @@ class SabreSwap(TransformationPass):
     `arXiv:1809.02573 <https://arxiv.org/pdf/1809.02573.pdf>`_
     """
 
-    def __init__(self, coupling_map, heuristic="basic", seed=None, fake_run=False):
+    def __init__(
+        self,
+        coupling_map,
+        heuristic="basic",
+        seed=None,
+        fake_run=False,
+    ):
         r"""SabreSwap initializer.
 
         Args:
@@ -153,6 +161,14 @@ def run(self, dag):
         if len(dag.qubits) > self.coupling_map.size():
             raise TranspilerError("More virtual qubits exist than physical.")
 
+        max_iterations_without_progress = 10 * len(dag.qubits)  # Arbitrary.
+        ops_since_progress = []
+        extended_set = None
+
+        # Normally this isn't necessary, but here we want to log some objects that have some
+        # non-trivial cost to create.
+        do_expensive_logging = logger.isEnabledFor(logging.DEBUG)
+
         self.dist_matrix = self.coupling_map.distance_matrix
 
         rng = np.random.default_rng(self.seed)
@@ -169,7 +185,7 @@ def run(self, dag):
 
         # A decay factor for each qubit used to heuristically penalize recently
         # used qubits (to encourage parallelism).
-        self.qubits_decay = {qubit: 1 for qubit in dag.qubits}
+        self.qubits_decay = dict.fromkeys(dag.qubits, 1)
 
         # Start algorithm from the front layer and iterate until all gates done.
         num_search_steps = 0
@@ -178,10 +194,12 @@ def run(self, dag):
         for _, input_node in dag.input_map.items():
             for successor in self._successors(input_node, dag):
                 self.applied_predecessors[successor] += 1
+
         while front_layer:
             execute_gate_list = []
 
             # Remove as many immediately applicable gates as possible
+            new_front_layer = []
             for node in front_layer:
                 if len(node.qargs) == 2:
                     v0, v1 = node.qargs
@@ -191,13 +209,24 @@ def run(self, dag):
                         current_layout._v2p[v0], current_layout._v2p[v1]
                     ):
                         execute_gate_list.append(node)
+                    else:
+                        new_front_layer.append(node)
                 else:  # Single-qubit gates as well as barriers are free
                     execute_gate_list.append(node)
+            front_layer = new_front_layer
+
+            if not execute_gate_list and len(ops_since_progress) > max_iterations_without_progress:
+                # Backtrack to the last time we made progress, then greedily insert swaps to route
+                # the gate with the smallest distance between its arguments.  This is a release
+                # valve for the algorithm to avoid infinite loops only, and should generally not
+                # come into play for most circuits.
+                self._undo_operations(ops_since_progress, mapped_dag, current_layout)
+                self._add_greedy_swaps(front_layer, mapped_dag, current_layout, canonical_register)
+                continue
 
             if execute_gate_list:
                 for node in execute_gate_list:
                     self._apply_gate(mapped_dag, node, current_layout, canonical_register)
-                    front_layer.remove(node)
                     for successor in self._successors(node, dag):
                         self.applied_predecessors[successor] += 1
                         if self._is_resolved(successor):
@@ -207,27 +236,33 @@ def run(self, dag):
                         self._reset_qubits_decay()
 
                 # Diagnostics
-                logger.debug(
-                    "free! %s",
-                    [
-                        (n.name if isinstance(n, DAGOpNode) else None, n.qargs)
-                        for n in execute_gate_list
-                    ],
-                )
-                logger.debug(
-                    "front_layer: %s",
-                    [(n.name if isinstance(n, DAGOpNode) else None, n.qargs) for n in front_layer],
-                )
-
+                if do_expensive_logging:
+                    logger.debug(
+                        "free! %s",
+                        [
+                            (n.name if isinstance(n, DAGOpNode) else None, n.qargs)
+                            for n in execute_gate_list
+                        ],
+                    )
+                    logger.debug(
+                        "front_layer: %s",
+                        [
+                            (n.name if isinstance(n, DAGOpNode) else None, n.qargs)
+                            for n in front_layer
+                        ],
+                    )
+
+                ops_since_progress = []
+                extended_set = None
                 continue
 
             # After all free gates are exhausted, heuristically find
             # the best swap and insert it. When two or more swaps tie
             # for best score, pick one randomly.
-            extended_set = self._obtain_extended_set(dag, front_layer)
-            swap_candidates = self._obtain_swaps(front_layer, current_layout)
-            swap_scores = dict.fromkeys(swap_candidates, 0)
-            for swap_qubits in swap_scores:
+            if extended_set is None:
+                extended_set = self._obtain_extended_set(dag, front_layer)
+            swap_scores = {}
+            for swap_qubits in self._obtain_swaps(front_layer, current_layout):
                 trial_layout = current_layout.copy()
                 trial_layout.swap(*swap_qubits)
                 score = self._score_heuristic(
@@ -238,9 +273,14 @@ def run(self, dag):
             best_swaps = [k for k, v in swap_scores.items() if v == min_score]
             best_swaps.sort(key=lambda x: (self._bit_indices[x[0]], self._bit_indices[x[1]]))
             best_swap = rng.choice(best_swaps)
-            swap_node = DAGOpNode(op=SwapGate(), qargs=best_swap)
-            self._apply_gate(mapped_dag, swap_node, current_layout, canonical_register)
+            swap_node = self._apply_gate(
+                mapped_dag,
+                DAGOpNode(op=SwapGate(), qargs=best_swap),
+                current_layout,
+                canonical_register,
+            )
             current_layout.swap(*best_swap)
+            ops_since_progress.append(swap_node)
 
             num_search_steps += 1
             if num_search_steps % DECAY_RESET_INTERVAL == 0:
@@ -250,23 +290,23 @@ def run(self, dag):
                 self.qubits_decay[best_swap[1]] += DECAY_RATE
 
             # Diagnostics
-            logger.debug("SWAP Selection...")
-            logger.debug("extended_set: %s", [(n.name, n.qargs) for n in extended_set])
-            logger.debug("swap scores: %s", swap_scores)
-            logger.debug("best swap: %s", best_swap)
-            logger.debug("qubits decay: %s", self.qubits_decay)
+            if do_expensive_logging:
+                logger.debug("SWAP Selection...")
+                logger.debug("extended_set: %s", [(n.name, n.qargs) for n in extended_set])
+                logger.debug("swap scores: %s", swap_scores)
+                logger.debug("best swap: %s", best_swap)
+                logger.debug("qubits decay: %s", self.qubits_decay)
 
         self.property_set["final_layout"] = current_layout
-
         if not self.fake_run:
             return mapped_dag
         return dag
 
     def _apply_gate(self, mapped_dag, node, current_layout, canonical_register):
-        if self.fake_run:
-            return
         new_node = _transform_gate_for_layout(node, current_layout, canonical_register)
-        mapped_dag.apply_operation_back(new_node.op, new_node.qargs, new_node.cargs)
+        if self.fake_run:
+            return new_node
+        return mapped_dag.apply_operation_back(new_node.op, new_node.qargs, new_node.cargs)
 
     def _reset_qubits_decay(self):
         """Reset all qubit decay factors to 1 upon request (to forget about
@@ -332,9 +372,20 @@ def _obtain_swaps(self, front_layer, current_layout):
                     virtual_neighbor = current_layout[neighbor]
                     swap = sorted([virtual, virtual_neighbor], key=lambda q: self._bit_indices[q])
                     candidate_swaps.add(tuple(swap))
-
         return candidate_swaps
 
+    def _add_greedy_swaps(self, front_layer, dag, layout, qubits):
+        """Mutate ``dag`` and ``layout`` by applying greedy swaps to ensure that at least one gate
+        can be routed."""
+        layout_map = layout._v2p
+        target_node = min(
+            front_layer,
+            key=lambda node: self.dist_matrix[layout_map[node.qargs[0]], layout_map[node.qargs[1]]],
+        )
+        for pair in _shortest_swap_path(tuple(target_node.qargs), self.coupling_map, layout):
+            self._apply_gate(dag, DAGOpNode(op=SwapGate(), qargs=pair), layout, qubits)
+            layout.swap(*pair)
+
     def _compute_cost(self, layer, layout):
         cost = 0
         layout_map = layout._v2p
@@ -369,13 +420,38 @@ def _score_heuristic(self, heuristic, front_layer, extended_set, layout, swap_qu
 
         raise TranspilerError("Heuristic %s not recognized." % heuristic)
 
+    def _undo_operations(self, operations, dag, layout):
+        """Mutate ``dag`` and ``layout`` by undoing the swap gates listed in ``operations``."""
+        if dag is None:
+            for operation in reversed(operations):
+                layout.swap(*operation.qargs)
+        else:
+            for operation in reversed(operations):
+                dag.remove_op_node(operation)
+                p0 = self._bit_indices[operation.qargs[0]]
+                p1 = self._bit_indices[operation.qargs[1]]
+                layout.swap(p0, p1)
+
 
 def _transform_gate_for_layout(op_node, layout, device_qreg):
     """Return node implementing a virtual op on given layout."""
     mapped_op_node = copy(op_node)
+    mapped_op_node.qargs = [device_qreg[layout._v2p[x]] for x in op_node.qargs]
+    return mapped_op_node
 
-    premap_qargs = op_node.qargs
-    mapped_qargs = map(lambda x: device_qreg[layout._v2p[x]], premap_qargs)
-    mapped_op_node.qargs = list(mapped_qargs)
 
-    return mapped_op_node
+def _shortest_swap_path(target_qubits, coupling_map, layout):
+    """Return an iterator that yields the swaps between virtual qubits needed to bring the two
+    virtual qubits in ``target_qubits`` together in the coupling map."""
+    v_start, v_goal = target_qubits
+    start, goal = layout._v2p[v_start], layout._v2p[v_goal]
+    # TODO: remove the list call once using retworkx 0.12, as the return value can be sliced.
+    path = list(retworkx.dijkstra_shortest_paths(coupling_map.graph, start, target=goal)[goal])
+    # Swap both qubits towards the "centre" (as opposed to applying the same swaps to one) to
+    # parallelise and reduce depth.
+    split = len(path) // 2
+    forwards, backwards = path[1:split], reversed(path[split:-1])
+    for swap in forwards:
+        yield v_start, layout._p2v[swap]
+    for swap in backwards:
+        yield v_goal, layout._p2v[swap]


Test patch:
diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py
--- a/test/python/transpiler/test_sabre_swap.py
+++ b/test/python/transpiler/test_sabre_swap.py
@@ -13,13 +13,79 @@
 """Test the Sabre Swap pass"""
 
 import unittest
-from qiskit.circuit.library import CCXGate, HGate, Measure
+
+import ddt
+
+from qiskit.circuit.library import CCXGate, HGate, Measure, SwapGate
 from qiskit.transpiler.passes import SabreSwap
 from qiskit.transpiler import CouplingMap, PassManager
 from qiskit import QuantumRegister, QuantumCircuit
 from qiskit.test import QiskitTestCase
-
-
+from qiskit.utils import optionals
+
+
+def looping_circuit(uphill_swaps=1, additional_local_minimum_gates=0):
+    """A circuit that causes SabreSwap to loop infinitely.
+
+    This looks like (using cz gates to show the symmetry, though we actually output cx for testing
+    purposes):
+
+    .. parsed-literal::
+
+         q_0: ─■────────────────
+               │
+         q_1: ─┼──■─────────────
+               │  │
+         q_2: ─┼──┼──■──────────
+               │  │  │
+         q_3: ─┼──┼──┼──■───────
+               │  │  │  │
+         q_4: ─┼──┼──┼──┼─────■─
+               │  │  │  │     │
+         q_5: ─┼──┼──┼──┼──■──■─
+               │  │  │  │  │
+         q_6: ─┼──┼──┼──┼──┼────
+               │  │  │  │  │
+         q_7: ─┼──┼──┼──┼──■──■─
+               │  │  │  │     │
+         q_8: ─┼──┼──┼──┼─────■─
+               │  │  │  │
+         q_9: ─┼──┼──┼──■───────
+               │  │  │
+        q_10: ─┼──┼──■──────────
+               │  │
+        q_11: ─┼──■─────────────
+               │
+        q_12: ─■────────────────
+
+    where `uphill_swaps` is the number of qubits separating the inner-most gate (representing how
+    many swaps need to be made that all increase the heuristics), and
+    `additional_local_minimum_gates` is how many extra gates to add on the outside (these increase
+    the size of the region of stability).
+    """
+    outers = 4 + additional_local_minimum_gates
+    n_qubits = 2 * outers + 4 + uphill_swaps
+    # This is (most of) the front layer, which is a bunch of outer qubits in the
+    # coupling map.
+    outer_pairs = [(i, n_qubits - i - 1) for i in range(outers)]
+    inner_heuristic_peak = [
+        # This gate is completely "inside" all the others in the front layer in
+        # terms of the coupling map, so it's the only one that we can in theory
+        # make progress towards without making the others worse.
+        (outers + 1, outers + 2 + uphill_swaps),
+        # These are the only two gates in the extended set, and they both get
+        # further apart if you make a swap to bring the above gate closer
+        # together, which is the trick that creates the "heuristic hill".
+        (outers, outers + 1),
+        (outers + 2 + uphill_swaps, outers + 3 + uphill_swaps),
+    ]
+    qc = QuantumCircuit(n_qubits)
+    for pair in outer_pairs + inner_heuristic_peak:
+        qc.cx(*pair)
+    return qc
+
+
+@ddt.ddt
 class TestSabreSwap(QiskitTestCase):
     """Tests the SabreSwap pass."""
 
@@ -124,6 +190,53 @@ def test_do_not_reorder_measurements(self):
         self.assertEqual(h_qubits, first_measure_qubits)
         self.assertNotEqual(h_qubits, second_measure_qubits)
 
+    # The 'basic' method can't get stuck in the same way.
+    @ddt.data("lookahead", "decay")
+    def test_no_infinite_loop(self, method):
+        """Test that the 'release value' mechanisms allow SabreSwap to make progress even on
+        circuits that get stuck in a stable local minimum of the lookahead parameters."""
+        qc = looping_circuit(3, 1)
+        qc.measure_all()
+        coupling_map = CouplingMap.from_line(qc.num_qubits)
+        routing_pass = PassManager(SabreSwap(coupling_map, method))
+
+        n_swap_gates = 0
+
+        def leak_number_of_swaps(cls, *args, **kwargs):
+            nonlocal n_swap_gates
+            n_swap_gates += 1
+            if n_swap_gates > 1_000:
+                raise Exception("SabreSwap seems to be stuck in a loop")
+            # pylint: disable=bad-super-call
+            return super(SwapGate, cls).__new__(cls, *args, **kwargs)
+
+        with unittest.mock.patch.object(SwapGate, "__new__", leak_number_of_swaps):
+            routed = routing_pass.run(qc)
+
+        routed_ops = routed.count_ops()
+        del routed_ops["swap"]
+        self.assertEqual(routed_ops, qc.count_ops())
+        couplings = {
+            tuple(routed.find_bit(bit).index for bit in qargs)
+            for _, qargs, _ in routed.data
+            if len(qargs) == 2
+        }
+        # Asserting equality to the empty set gives better errors on failure than asserting that
+        # `couplings <= coupling_map`.
+        self.assertEqual(couplings - set(coupling_map.get_edges()), set())
+
+        # Assert that the same keys are produced by a simulation - this is a test that the inserted
+        # swaps route the qubits correctly.
+        if not optionals.HAS_AER:
+            return
+
+        from qiskit import Aer
+
+        sim = Aer.get_backend("aer_simulator")
+        in_results = sim.run(qc, shots=4096).result().get_counts()
+        out_results = sim.run(routed, shots=4096).result().get_counts()
+        self.assertEqual(set(in_results), set(out_results))
+
 
 if __name__ == "__main__":
     unittest.main()