-
Notifications
You must be signed in to change notification settings - Fork 3k
'Peephole' optimization - or: collecting and optimizing two-qubit blocks - before routing #12727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 21 commits
9965ce3
f3af45a
c650503
092257c
3befb83
a8c5b8c
90e725c
fd988e3
e765285
1805cb8
bce3fcd
e6f0b39
6f7445c
d3a45e7
dec91de
45912f8
5404822
ed790c1
1236cd6
db8aa2d
3e28ec6
d064c66
b88e3f0
a655001
8aa570d
c84fa16
b8fea91
6137daa
a6ea05b
c6f4530
6ec02fc
44f2f38
22bb156
df0897f
66d0a22
e2074b6
aff157b
c333f5b
1bdf860
a28a564
6eaed5d
254ea38
9964d65
b90f9f6
a7d3594
89ac7dc
229b028
d8fba88
48eacd7
edde611
5404bbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # This code is part of Qiskit. | ||
| # | ||
| # (C) Copyright IBM 2017, 2024. | ||
| # | ||
| # This code is licensed under the Apache License, Version 2.0. You may | ||
| # obtain a copy of this license in the LICENSE.txt file in the root directory | ||
| # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
| # | ||
| # Any modifications or derivative works of this code must retain this | ||
| # copyright notice, and modified files need to carry a notice indicating | ||
| # that they have been altered from the originals. | ||
| """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" | ||
|
|
||
| import numpy as np | ||
| from qiskit.circuit.library import UnitaryGate | ||
| from qiskit.dagcircuit import DAGCircuit | ||
| from qiskit.synthesis.two_qubit.local_invariance import two_qubit_local_invariants | ||
| from qiskit.synthesis.two_qubit.two_qubit_decompose import decompose_two_qubit_product_gate | ||
| from qiskit.transpiler import TransformationPass | ||
|
|
||
|
|
||
| class Split2QUnitaries(TransformationPass): | ||
| """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" | ||
|
|
||
| def run(self, dag: DAGCircuit): | ||
| """Run the Split2QUnitaries pass on `dag`.""" | ||
| for node in dag.topological_op_nodes(): | ||
| # skip operations without two-qubits and for which we can not determine a potential 1q split | ||
| if ( | ||
| len(node.cargs) > 0 | ||
|
mtreinish marked this conversation as resolved.
|
||
| or len(node.qargs) != 2 | ||
| or not (hasattr(node.op, "to_matrix") and hasattr(node.op, "__array__")) | ||
| or (hasattr(node.op, "is_parameterized") and node.op.is_parameterized()) | ||
|
sbrandhsn marked this conversation as resolved.
Outdated
|
||
| ): | ||
| continue | ||
|
|
||
| # check if the node can be represented by single-qubit gates | ||
| nmat = node.op.to_matrix() | ||
|
sbrandhsn marked this conversation as resolved.
Outdated
|
||
| if np.all(two_qubit_local_invariants(nmat) == [1, 0, 3]): | ||
|
sbrandhsn marked this conversation as resolved.
Outdated
|
||
| ul, ur, phase = decompose_two_qubit_product_gate(nmat) | ||
| dag_node = DAGCircuit() | ||
| dag_node.add_qubits(node.qargs) | ||
|
|
||
| dag_node.apply_operation_back(UnitaryGate(ur), qargs=(node.qargs[0],)) | ||
| dag_node.apply_operation_back(UnitaryGate(ul), qargs=(node.qargs[1],)) | ||
|
sbrandhsn marked this conversation as resolved.
Outdated
|
||
| dag_node.global_phase += phase | ||
| dag.substitute_node_with_dag(node, dag_node) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While we can use substitute node with dag here, it'll probably be a bit more performant to do something like: Where we just insert the decomposition gate before the 2q gate and then just remove the node. This should be a bit faster because it avoids all the extra complexity in the dag substitution code.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, thanks. I will modify this. |
||
|
|
||
| return dag | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| --- | ||
| features: | ||
|
sbrandhsn marked this conversation as resolved.
Outdated
|
||
| - | | ||
| Added a new pass :class:`.Split2QUnitaries` that iterates over all two-qubit gates or unitaries in a | ||
| circuit and replaces them with two single-qubit unitaries, if possible without introducing errors, i.e. | ||
| the two-qubit gate/unitary is actually a (kronecker) product of single-qubit unitaries. In addition, | ||
| the passes :class:`.Collect2qBlocks`, :class:`.ConsolidateBlocks` and :class:`.Split2QUnitaries` are | ||
| added to the `init` stage of the preset pass managers with optimization level 2 and optimization level 3 | ||
| if the pass managers target a :class:`.Target` that has a discrete basis gate set, i.e. all basis gates | ||
| have are not parameterized. The modification of the `init` stage should allow for a more efficient routing | ||
| for quantum circuits that (a) contain two-qubit unitaries/gates that are actually a product of single-qubit gates | ||
| or (b) contain multiple two-qubit gates in a continuous block of two-qubit gates. In the former case, | ||
| the routing of the two-qubit gate can simply be skipped as no real interaction between a pair of qubits | ||
| occurs. In the latter case, the lookahead space of routing algorithms is not 'polluted' by superfluous | ||
| two-qubit gates, i.e. for routing it is sufficient to only consider one single two-qubit gate per | ||
| continuous block of two-qubit gates. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would split this into two release notes (with separate bullet points under features). One documenting the new pass, the other documenting the modifications to the preset pass manager. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,6 +84,8 @@ | |
| from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass | ||
| from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget | ||
| from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout | ||
| from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries | ||
|
|
||
| from qiskit.transpiler.passmanager_config import PassManagerConfig | ||
| from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager | ||
| from qiskit.transpiler.target import ( | ||
|
|
@@ -109,6 +111,16 @@ def _define(self): | |
| self._definition = QuantumCircuit(2) | ||
| self._definition.cx(0, 1) | ||
|
|
||
| def to_matrix(self) -> np.ndarray: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed? Should this skip the gate if the matrix is empty?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking into this - a couple of tests were failing... |
||
| return np.asarray( | ||
| [ | ||
| [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], | ||
| [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j], | ||
| [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j], | ||
| [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], | ||
| ] | ||
| ) | ||
|
|
||
|
|
||
| def connected_qubits(physical: int, coupling_map: CouplingMap) -> set: | ||
| """Get the physical qubits that have a connection to this one in the coupling map.""" | ||
|
|
@@ -862,6 +874,42 @@ def test_do_not_run_gatedirection_with_symmetric_cm(self): | |
| transpile(circ, coupling_map=coupling_map, initial_layout=layout) | ||
| self.assertFalse(mock_pass.called) | ||
|
|
||
| def tests_conditional_run_split_2q_unitaries(self): | ||
| """Tests running `Split2QUnitaries` when basis gate set is (non-) discrete""" | ||
| qc = QuantumCircuit(3) | ||
| qc.sx(0) | ||
| qc.t(0) | ||
| qc.cx(0, 1) | ||
| qc.cx(1, 2) | ||
|
|
||
| orig_pass = Split2QUnitaries() | ||
| with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: | ||
| basis = ["t", "sx", "cx"] | ||
| backend = GenericBackendV2(3, basis_gates=basis) | ||
| transpile(qc, backend=backend) | ||
| transpile(qc, basis_gates=basis) | ||
| transpile(qc, target=backend.target) | ||
|
sbrandhsn marked this conversation as resolved.
|
||
| self.assertFalse(mock_pass.called) | ||
|
|
||
| orig_pass = Split2QUnitaries() | ||
| with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: | ||
| basis = ["rz", "sx", "cx"] | ||
| backend = GenericBackendV2(3, basis_gates=basis) | ||
| transpile(qc, backend=backend, optimization_level=2) | ||
| self.assertTrue(mock_pass.called) | ||
| mock_pass.called = False | ||
| transpile(qc, basis_gates=basis, optimization_level=2) | ||
| self.assertTrue(mock_pass.called) | ||
| mock_pass.called = False | ||
| transpile(qc, target=backend.target, optimization_level=2) | ||
| self.assertTrue(mock_pass.called) | ||
| mock_pass.called = False | ||
| transpile(qc, backend=backend, optimization_level=3) | ||
| self.assertTrue(mock_pass.called) | ||
| mock_pass.called = False | ||
| transpile(qc, basis_gates=basis, optimization_level=3) | ||
| self.assertTrue(mock_pass.called) | ||
|
|
||
| def test_optimize_to_nothing(self): | ||
| """Optimize gates up to fixed point in the default pipeline | ||
| See https://github.com/Qiskit/qiskit-terra/issues/2035 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # This code is part of Qiskit. | ||
| # | ||
| # (C) Copyright IBM 2017, 2024. | ||
| # | ||
| # This code is licensed under the Apache License, Version 2.0. You may | ||
| # obtain a copy of this license in the LICENSE.txt file in the root directory | ||
| # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
| # | ||
| # Any modifications or derivative works of this code must retain this | ||
| # copyright notice, and modified files need to carry a notice indicating | ||
| # that they have been altered from the originals. | ||
|
|
||
| """ | ||
| Tests for the Split2QUnitaries transpiler pass. | ||
| """ | ||
| from test import QiskitTestCase | ||
|
|
||
| from qiskit import QuantumCircuit | ||
| from qiskit.circuit.library import UnitaryGate | ||
| from qiskit.quantum_info import Operator | ||
| from qiskit.transpiler import PassManager | ||
| from qiskit.quantum_info.operators.predicates import matrix_equal | ||
| from qiskit.transpiler.passes import Collect2qBlocks, ConsolidateBlocks | ||
| from qiskit.transpiler.passes.optimization import Split2QUnitaries | ||
|
|
||
|
|
||
| class TestSplit2QUnitaries(QiskitTestCase): | ||
|
sbrandhsn marked this conversation as resolved.
|
||
| """ | ||
| Tests to verify that splitting two-qubit unitaries into two single-qubit unitaries works correctly. | ||
| """ | ||
|
|
||
| def test_splits(self): | ||
| """Test that the kronecker product of matrices is correctly identified by the pass and that the | ||
| global phase is set correctly.""" | ||
| qc = QuantumCircuit(2) | ||
| qc.x(0) | ||
| qc.z(1) | ||
| qc.global_phase += 1.2345 | ||
| qc_split = QuantumCircuit(2) | ||
| qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) | ||
|
|
||
| pm = PassManager() | ||
| pm.append(Collect2qBlocks()) | ||
| pm.append(ConsolidateBlocks()) | ||
| pm.append(Split2QUnitaries()) | ||
| qc_split = pm.run(qc_split) | ||
|
|
||
| self.assertTrue(Operator(qc).equiv(qc_split)) | ||
| self.assertTrue( | ||
| matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) | ||
| ) | ||
|
|
||
| def test_no_split(self): | ||
| """Test that the pass does not split a non-local two-qubit unitary.""" | ||
| qc = QuantumCircuit(2) | ||
| qc.cx(0, 1) | ||
| qc.global_phase += 1.2345 | ||
|
|
||
| qc_split = QuantumCircuit(2) | ||
| qc_split.append(UnitaryGate(Operator(qc)), [0, 1]) | ||
|
|
||
| pm = PassManager() | ||
| pm.append(Collect2qBlocks()) | ||
| pm.append(ConsolidateBlocks()) | ||
| pm.append(Split2QUnitaries()) | ||
| qc_split = pm.run(qc_split) | ||
|
|
||
| self.assertTrue(Operator(qc).equiv(qc_split)) | ||
| self.assertTrue( | ||
| matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) | ||
| ) | ||
| # either not a unitary gate, or the unitary has been consolidated to a 2q-unitary by another pass | ||
| self.assertTrue( | ||
| all( | ||
| op.name != "unitary" or (op.name == "unitary" and len(op.qubits) > 1) | ||
| for op in qc_split.data | ||
| ) | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.