Repository: Qiskit/qiskit — pull request #14617 (python)
Title: Fix Sabre extended-set tracking

<!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\n\r\nThis fixes the logic bug in extended-set tracking where the "other" qubit of a gate touched by a swap would not have its tracking data updated during an `apply_swap` operation.\r\n\r\nThis change has the effect of making the extended-set update operation now linear in the size of the extended set in all cases, whereas previously it was only worst-case linear and average-case constant. This can be offset in the future by modifications to how the "lookahead" gates are tracked at all, if it is needed.\r\n\r\n\r\n\r\n### Details and comments\r\n\r\nFix #14244.  I'm tagging for 2.2.0 because it's not a correctness bug, so imo it's not worth the risk of changing behaviour in the backport, and it _does_ have performance implications.\r\n\r\nThis does make it more costly to update a swap in the `ExtendedSet`, and we probably should revisit the struct to make it cheaper.  The ASV benchmarks below (with options `-b UtilityScale -b sabre` and `--only-changed`) show that it doesn't seem to have a critical impact on runtime performance, though, at the scale of fixed 20-gate size:\r\n\r\n```text\r\n| Change   | Before [3e19a7ad]    | After [801295b5]    |   Ratio | Benchmark (Parameter)                                                                        |\r\n|----------|----------------------|---------------------|---------|----------------------------------------------------------------------------------------------|\r\n| -        | 148497               | 122160              |    0.82 | qft.LargeQFTMappingTrackBench.track_depth_sabre_swap(1081, 'lookahead')                      |\r\n| -        | 3358                 | 2827                |    0.84 | qft.LargeQFTMappingTrackBench.track_depth_sabre_swap(115, 'lookahead')                       |\r\n| -        | 31891                | 27395               |    0.86 | qft.LargeQFTMappingTrackBench.track_depth_sabre_swap(409, 'lookahead')                       |\r\n| -        | 6360                 | 5337                |    0.84 | quantum_volume.LargeQuantumVolumeMappingTrackBench.track_depth_sabre_swap(1081, 10, 'decay') |\r\n| -        | 203±10ms             | 157±7ms             |    0.77 | queko.QUEKOTranspilerBench.time_transpile_bss(3, 'sabre')                                    |\r\n| -        | 666                  | 592                 |    0.89 | queko.QUEKOTranspilerBench.track_depth_bntf_optimal_depth_25(0, 'sabre')                     |\r\n| -        | 230                  | 198                 |    0.86 | queko.QUEKOTranspilerBench.track_depth_bntf_optimal_depth_25(2, 'sabre')                     |\r\n| -        | 197                  | 165                 |    0.84 | queko.QUEKOTranspilerBench.track_depth_bntf_optimal_depth_25(3, 'sabre')                     |\r\n| -        | 765                  | 671                 |    0.88 | queko.QUEKOTranspilerBench.track_depth_bss_optimal_depth_100(0, 'sabre')                     |\r\n| -        | 505                  | 392                 |    0.78 | queko.QUEKOTranspilerBench.track_depth_bss_optimal_depth_100(1, 'sabre')                     |\r\n| Change   | Before [3e19a7ad]    | After [801295b5]    |   Ratio | Benchmark (Parameter)                                                                       |\r\n|----------|----------------------|---------------------|---------|---------------------------------------------------------------------------------------------|\r\n| +        | 428±4ms              | 479±3ms             |    1.12 | mapping_passes.PassBenchmarks.time_sabre_layout(5, 1024)                                    |\r\n| +        | 3239                 | 3807                |    1.18 | qft.LargeQFTMappingTrackBench.track_depth_sabre_swap(115, 'decay')                          |\r\n| +        | 1.35±0.01s           | 1.61±0.01s          |    1.19 | quantum_volume.LargeQuantumVolumeMappingTimeBench.time_sabre_swap(1081, 10, 'decay')        |\r\n| +        | 1396                 | 1667                |    1.19 | quantum_volume.LargeQuantumVolumeMappingTrackBench.track_depth_sabre_swap(409, 10, 'decay') |\r\n| +        | 262                  | 330                 |    1.26 | queko.QUEKOTranspilerBench.track_depth_bss_optimal_depth_100(2, 'sabre')                    |\r\n| +        | 2053                 | 2321                |    1.13 | utility_scale.UtilityScaleBenchmarks.track_qft_depth('cx')                                  |\r\n| +        | 2053                 | 2321                |    1.13 | utility_scale.UtilityScaleBenchmarks.track_qft_depth('cz')                                  |\r\n| +        | 2053                 | 2320                |    1.13 | utility_scale.UtilityScaleBenchmarks.track_qft_depth('ecr')                                 |\r\n```

Resolved issues:
title: ExtendSet.apply_swap can break bidirectional consistency of qubit connections
body: ### Environment\n\n- **Qiskit version**: Tested on 1.2.4, should still be the case in the lastest versions given that the concerned functions have not changed since.\n- **Python version**: 3.10.12 \n\n\n### What is happening?\n\nThe apply_swap method of the ExtendedSet structure (defined in crates/accelerate/src/sabre/layer.rs)  currently only updates the adjacency lists for the two qubits being swapped. It does not update references to these qubits elsewhere in the data structure, which can result in inconsistent or broken bidirectional connections.\n\n### How can we reproduce the issue?\n\nAny call the the apply_set method will have this error. Simply printing the ExtendedSet right before and right after the call to this method allow to see the bug. \nDown below is a copy of the ExtendedSet struct from crates/accelerate/src/sabre/layer.rs reduced to only the necessary methods and simplied using simple datatype (u32 rather than PhysicalQubit) for simplicity. Compiling and executing this code show the error as ExtendedSet after the apply_swap call doesn't respect the bidirectional connection rule.\n\n```\npub struct ExtendedSetDebug {\n    qubits: Vec<Vec<usize>>,\n}\n\nimpl ExtendedSetDebug {\n    pub fn new(num_qubits: u32) -> Self {\n        ExtendedSetDebug {\n            qubits: vec![Vec::new(); num_qubits as usize],\n        }\n    }\n\tpub fn qubits(&self) -> &[Vec<usize>] {\n\t&self.qubits\n    }\n\t\n\t/// Add a node and its active qubits to the extended set.\n    pub fn push(&mut self, qubits: [usize; 2]) {\n        let [a, b] = qubits;\n        self.qubits[a].push(b);\n        self.qubits[b].push(a);\n    }\n\n\tpub fn apply_swap(&mut self, swap: [usize; 2]) {\n\t\tlet [a, b] = swap;\n\t\t\n\t\tfor other in self.qubits[a].iter_mut() {\n\t\t\tif *other == b {\n\t\t\t\t*other = a;\n\t\t\t}\n\t\t}\n\t\t\n\t\tfor other in self.qubits[b].iter_mut() {\n\t\t\tif *other == a {\n\t\t\t\t*other = b;\n\t\t\t}\n\t\t}\n\t\t\n\t\tself.qubits.swap(a, b);\n\t}\n}\n\nfn main() {\n    println!("Hello, world!");\n\tlet mut L = ExtendedSetDebug::new(3);\n\tL.push([0,1]);\n\tL.push([0,2]);\n\t\n\tprintln!("{:?}",L.qubits());\t\n\tL.apply_swap([0,2]);\t\n\tprintln!("{:?}",L.qubits());\t\t\n}\n```\n\n### What should happen?\n\nLet's take the example of the previous code:\nApplying the swap [0,2] to qubits = [[1,2],[0],[0]] leads to qubits = [[2],[0],[1,0]]. Bidirectional connections is broken since this result say that qubit 0 interacts only with qubit 2 (qubits[0] = [2]) but it also says that qubit 1 interacts with qubit 0 (qubits[1] = [0]). \nInstead, the result should be [[2], [2], [1,0]] which preserves the bidirectional connections of qubit interactions.\n\nNote that this happens for pretty much any call to apply_swap and that this is only one example.\n\n### Any suggestions?\n\nUpdate the implementation to iterate through all qubit adjacency lists and update every reference to a or b, not just those of a and b.\n\n```\npub fn apply_swap(&mut self, swap: [PhysicalQubit; 2]) {\n        let [a, b] = swap;\n        let a_idx = a.index();\n        let b_idx = b.index();\n\n        // Iterate through *all* qubits and update references\n        for neighbors in self.qubits.iter_mut() {\n            for qubit in neighbors.iter_mut() {\n                if *qubit == a {\n                    *qubit = b;\n                } else if *qubit == b {\n                    *qubit = a;\n                }\n            }\n        }\n        self.qubits.swap(a_idx, b_idx);\n    }\n```

Hints:

Fix patch:
diff --git a/crates/transpiler/src/passes/sabre/layer.rs b/crates/transpiler/src/passes/sabre/layer.rs\nindex 328ca4341fd3..243f10958a6c 100644\n--- a/crates/transpiler/src/passes/sabre/layer.rs\n+++ b/crates/transpiler/src/passes/sabre/layer.rs\n@@ -236,14 +236,14 @@ impl ExtendedSet {\n     /// Apply a physical swap to the current layout data structure.\n     pub fn apply_swap(&mut self, swap: [PhysicalQubit; 2]) {\n         let [a, b] = swap;\n-        for other in self.qubits[a.index()].iter_mut() {\n-            if *other == b {\n-                *other = a\n-            }\n-        }\n-        for other in self.qubits[b.index()].iter_mut() {\n+        // This effectively iterates twice through each element of the extended set, which is a\n+        // complexity bottleneck if the extended set scales with the size of the circuit.  We should\n+        // find a new structure that makes these updates more efficient.\n+        for other in self.qubits.iter_mut().flat_map(|others| others.iter_mut()) {\n             if *other == a {\n-                *other = b\n+                *other = b;\n+            } else if *other == b {\n+                *other = a;\n             }\n         }\n         self.qubits.swap(a.index(), b.index());\ndiff --git a/releasenotes/notes/sabre-extended-set-fix-0b168b2e643a77af.yaml b/releasenotes/notes/sabre-extended-set-fix-0b168b2e643a77af.yaml\nnew file mode 100644\nindex 000000000000..88aefbc01711\n--- /dev/null\n+++ b/releasenotes/notes/sabre-extended-set-fix-0b168b2e643a77af.yaml\n@@ -0,0 +1,7 @@\n+---\n+fixes:\n+  - |\n+    A logic bug in the extended-set tracking for the ``lookahead`` heuristic in Sabre routing\n+    previously causing its tracking to become self-inconsistent has been fixed.  This did not affect\n+    the correctness of the compiler, but caused the heuristic to not measure what it was intended to\n+    measure.  See `#14244 <https://github.com/Qiskit/qiskit/issues/14244>`__ for more detail.\n

Test patch:
diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py\nindex 61407d4c4c8d..a5aaa5687a85 100644\n--- a/test/python/transpiler/test_sabre_layout.py\n+++ b/test/python/transpiler/test_sabre_layout.py\n@@ -270,7 +270,7 @@ def test_layout_many_search_trials(self):\n         self.assertIsInstance(res, QuantumCircuit)\n         layout = res._layout.initial_layout\n         self.assertEqual(\n-            [layout[q] for q in qc.qubits], [0, 12, 7, 8, 6, 3, 1, 10, 4, 9, 2, 11, 13, 5]\n+            [layout[q] for q in qc.qubits], [26, 19, 16, 23, 14, 15, 25, 20, 22, 17, 24, 21, 13, 18]\n         )\n \n     def test_support_var_with_rust_fastpath(self):\n@@ -519,7 +519,7 @@ def test_integration_with_pass_manager(self):\n         qct_initial_layout = qct.layout.initial_layout\n         self.assertEqual(\n             [qct_initial_layout[q] for q in self.circuit.qubits],\n-            [8, 7, 12, 13, 18, 19, 17, 16, 11, 10, 5, 6, 1, 2, 3, 9],\n+            [2, 3, 8, 7, 12, 13, 14, 18, 17, 16, 15, 11, 10, 5, 6, 1],\n         )\n \n \n