Skip to content

Commit 5b507c4

Browse files
sbrandhsnmergify[bot]
authored andcommitted
'Peephole' optimization - or: collecting and optimizing two-qubit blocks - before routing (#12727)
* init * up * up * Update builtin_plugins.py * Update builtin_plugins.py * reno * Update builtin_plugins.py * Update builtin_plugins.py * Update peephole-before-routing-c3d184b740bb7a8b.yaml * neko check * check neko * Update builtin_plugins.py * test neko * Update builtin_plugins.py * Update builtin_plugins.py * Update builtin_plugins.py * lint * tests and format * remove FakeTorino test * Update peephole-before-routing-c3d184b740bb7a8b.yaml * Apply suggestions from code review Co-authored-by: Matthew Treinish <mtreinish@kortar.org> * comments from code review * fix precision * up * up * update * up * . * cyclic import * cycl import * cyl import * . * circular import * . * lint * Include new pass in docs * Fix Split2QUnitaries dag manipulation This commit fixes the dag handling to do the 1q unitary insertion. Previously the dag manipulation was being done manually using the insert_node_on_in_edges() rustworkx method. However as the original node had 2 incoming edges for each qubit this caused the dag after running the pass to become corrupted. Each of the new 1q unitary nodes would end up with 2 incident edges and they would be in a sequence. This would result in later passes not being able to correctly understand the state of the circuit correctly. This was causing the unit tests to fail. This commit fixes this by just using `substitute_node_with_dag()` to handle the node substition, while doing it manually to avoid the overhead of checking is probably possible, the case where a unitary is the product of two 1q gates is not very common so optimizing it isn't super critical. * Update releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml * stricter check for doing split2q * Update qiskit/transpiler/preset_passmanagers/builtin_plugins.py Co-authored-by: Matthew Treinish <mtreinish@kortar.org> * code review * Update qiskit/transpiler/passes/optimization/split_2q_unitaries.py Co-authored-by: Matthew Treinish <mtreinish@kortar.org> * new tests * typo * lint * lint --------- Co-authored-by: Matthew Treinish <mtreinish@kortar.org> (cherry picked from commit 1214d51)
1 parent 9120b8d commit 5b507c4

File tree

8 files changed

+436
-1
lines changed

8 files changed

+436
-1
lines changed

qiskit/transpiler/passes/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
ElidePermutations
9292
NormalizeRXAngle
9393
OptimizeAnnotated
94+
Split2QUnitaries
9495
9596
Calibration
9697
=============
@@ -244,6 +245,7 @@
244245
from .optimization import ElidePermutations
245246
from .optimization import NormalizeRXAngle
246247
from .optimization import OptimizeAnnotated
248+
from .optimization import Split2QUnitaries
247249

248250
# circuit analysis
249251
from .analysis import ResourceEstimation

qiskit/transpiler/passes/optimization/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@
3838
from .elide_permutations import ElidePermutations
3939
from .normalize_rx_angle import NormalizeRXAngle
4040
from .optimize_annotated import OptimizeAnnotated
41+
from .split_2q_unitaries import Split2QUnitaries
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2017, 2024.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
"""Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error."""
13+
from typing import Optional
14+
15+
from qiskit.transpiler.basepasses import TransformationPass
16+
from qiskit.circuit.quantumcircuitdata import CircuitInstruction
17+
from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode
18+
from qiskit.circuit.library.generalized_gates import UnitaryGate
19+
from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition
20+
21+
22+
class Split2QUnitaries(TransformationPass):
23+
"""Attempt to splits two-qubit gates in a :class:`.DAGCircuit` into two single-qubit gates
24+
25+
This pass will analyze all the two qubit gates in the circuit and analyze the gate's unitary
26+
matrix to determine if the gate is actually a product of 2 single qubit gates. In these
27+
cases the 2q gate can be simplified into two single qubit gates and this pass will
28+
perform this optimization and will replace the two qubit gate with two single qubit
29+
:class:`.UnitaryGate`.
30+
"""
31+
32+
def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16):
33+
"""Split2QUnitaries initializer.
34+
35+
Args:
36+
fidelity (float): Allowed tolerance for splitting two-qubit unitaries and gate decompositions
37+
"""
38+
super().__init__()
39+
self.requested_fidelity = fidelity
40+
41+
def run(self, dag: DAGCircuit):
42+
"""Run the Split2QUnitaries pass on `dag`."""
43+
for node in dag.topological_op_nodes():
44+
# skip operations without two-qubits and for which we can not determine a potential 1q split
45+
if (
46+
len(node.cargs) > 0
47+
or len(node.qargs) != 2
48+
or node.matrix is None
49+
or node.is_parameterized()
50+
):
51+
continue
52+
53+
decomp = TwoQubitWeylDecomposition(node.op, fidelity=self.requested_fidelity)
54+
if (
55+
decomp._inner_decomposition.specialization
56+
== TwoQubitWeylDecomposition._specializations.IdEquiv
57+
):
58+
new_dag = DAGCircuit()
59+
new_dag.add_qubits(node.qargs)
60+
61+
ur = decomp.K1r
62+
ur_node = DAGOpNode.from_instruction(
63+
CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=new_dag
64+
)
65+
66+
ul = decomp.K1l
67+
ul_node = DAGOpNode.from_instruction(
68+
CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=new_dag
69+
)
70+
new_dag._apply_op_node_back(ur_node)
71+
new_dag._apply_op_node_back(ul_node)
72+
new_dag.global_phase = decomp.global_phase
73+
dag.substitute_node_with_dag(node, new_dag)
74+
elif (
75+
decomp._inner_decomposition.specialization
76+
== TwoQubitWeylDecomposition._specializations.SWAPEquiv
77+
):
78+
# TODO maybe also look into swap-gate-like gates? Things to consider:
79+
# * As the qubit mapping may change, we'll always need to build a new dag in this pass
80+
# * There may not be many swap-gate-like gates in an arbitrary input circuit
81+
# * Removing swap gates from a user-routed input circuit here is unexpected
82+
pass
83+
return dag

qiskit/transpiler/preset_passmanagers/builtin_plugins.py

+65
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import os
1616

17+
from qiskit.circuit import Instruction
18+
from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries
1719
from qiskit.transpiler.passmanager import PassManager
1820
from qiskit.transpiler.exceptions import TranspilerError
1921
from qiskit.transpiler.passes import BasicSwap
@@ -64,12 +66,23 @@
6466
CYGate,
6567
SXGate,
6668
SXdgGate,
69+
get_standard_gate_name_mapping,
6770
)
6871
from qiskit.utils.parallel import CPU_COUNT
6972
from qiskit import user_config
7073

7174
CONFIG = user_config.get_config()
7275

76+
_discrete_skipped_ops = {
77+
"delay",
78+
"reset",
79+
"measure",
80+
"switch_case",
81+
"if_else",
82+
"for_loop",
83+
"while_loop",
84+
}
85+
7386

7487
class DefaultInitPassManager(PassManagerStagePlugin):
7588
"""Plugin class for default init stage."""
@@ -160,6 +173,58 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
160173
)
161174
)
162175
init.append(CommutativeCancellation())
176+
# skip peephole optimization before routing if target basis gate set is discrete,
177+
# i.e. only consists of Cliffords that an user might want to keep
178+
# use rz, sx, x, cx as basis, rely on physical optimziation to fix everything later one
179+
stdgates = get_standard_gate_name_mapping()
180+
181+
def _is_one_op_non_discrete(ops):
182+
"""Checks if one operation in `ops` is not discrete, i.e. is parameterizable
183+
Args:
184+
ops (List(Operation)): list of operations to check
185+
Returns
186+
True if at least one operation in `ops` is not discrete, False otherwise
187+
"""
188+
found_one_continuous_gate = False
189+
for op in ops:
190+
if isinstance(op, str):
191+
if op in _discrete_skipped_ops:
192+
continue
193+
op = stdgates.get(op, None)
194+
195+
if op is not None and op.name in _discrete_skipped_ops:
196+
continue
197+
198+
if op is None or not isinstance(op, Instruction):
199+
return False
200+
201+
if len(op.params) > 0:
202+
found_one_continuous_gate = True
203+
return found_one_continuous_gate
204+
205+
target = pass_manager_config.target
206+
basis = pass_manager_config.basis_gates
207+
# consolidate gates before routing if the user did not specify a discrete basis gate, i.e.
208+
# * no target or basis gate set has been specified
209+
# * target has been specified, and we have one non-discrete gate in the target's spec
210+
# * basis gates have been specified, and we have one non-discrete gate in that set
211+
do_consolidate_blocks_init = target is None and basis is None
212+
do_consolidate_blocks_init |= target is not None and _is_one_op_non_discrete(
213+
target.operations
214+
)
215+
do_consolidate_blocks_init |= basis is not None and _is_one_op_non_discrete(basis)
216+
217+
if do_consolidate_blocks_init:
218+
init.append(Collect2qBlocks())
219+
init.append(ConsolidateBlocks())
220+
# If approximation degree is None that indicates a request to approximate up to the
221+
# error rates in the target. However, in the init stage we don't yet know the target
222+
# qubits being used to figure out the fidelity so just use the default fidelity parameter
223+
# in this case.
224+
if pass_manager_config.approximation_degree is not None:
225+
init.append(Split2QUnitaries(pass_manager_config.approximation_degree))
226+
else:
227+
init.append(Split2QUnitaries())
163228
else:
164229
raise TranspilerError(f"Invalid optimization level {optimization_level}")
165230
return init
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
features_transpiler:
3+
- |
4+
Added a new pass :class:`.Split2QUnitaries` that iterates over all two-qubit gates or unitaries in a
5+
circuit and replaces them with two single-qubit unitaries, if possible without introducing errors, i.e.
6+
the two-qubit gate/unitary is actually a (kronecker) product of single-qubit unitaries.
7+
- |
8+
The passes :class:`.Collect2qBlocks`, :class:`.ConsolidateBlocks` and :class:`.Split2QUnitaries` have been
9+
added to the ``init`` stage of the preset pass managers with optimization level 2 and optimization level 3.
10+
The modification of the `init` stage should allow for a more efficient routing for quantum circuits that either:
11+
12+
* contain two-qubit unitaries/gates that are actually a product of single-qubit gates
13+
* contain multiple two-qubit gates in a continuous block of two-qubit gates.
14+
15+
In the former case, the routing of the two-qubit gate can simply be skipped as no real interaction
16+
between a pair of qubits occurs. In the latter case, the lookahead space of routing algorithms is not
17+
'polluted' by superfluous two-qubit gates, i.e. for routing it is sufficient to only consider one single
18+
two-qubit gate per continuous block of two-qubit gates. These passes are not run if the pass
19+
managers target a :class:`.Target` that has a discrete basis gate set, i.e. all basis gates have are not
20+
parameterized.

test/python/compiler/test_transpiler.py

+38
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass
8585
from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget
8686
from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout
87+
from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries
88+
8789
from qiskit.transpiler.passmanager_config import PassManagerConfig
8890
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager
8991
from qiskit.transpiler.target import (
@@ -862,6 +864,42 @@ def test_do_not_run_gatedirection_with_symmetric_cm(self):
862864
transpile(circ, coupling_map=coupling_map, initial_layout=layout)
863865
self.assertFalse(mock_pass.called)
864866

867+
def tests_conditional_run_split_2q_unitaries(self):
868+
"""Tests running `Split2QUnitaries` when basis gate set is (non-) discrete"""
869+
qc = QuantumCircuit(3)
870+
qc.sx(0)
871+
qc.t(0)
872+
qc.cx(0, 1)
873+
qc.cx(1, 2)
874+
875+
orig_pass = Split2QUnitaries()
876+
with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass:
877+
basis = ["t", "sx", "cx"]
878+
backend = GenericBackendV2(3, basis_gates=basis)
879+
transpile(qc, backend=backend)
880+
transpile(qc, basis_gates=basis)
881+
transpile(qc, target=backend.target)
882+
self.assertFalse(mock_pass.called)
883+
884+
orig_pass = Split2QUnitaries()
885+
with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass:
886+
basis = ["rz", "sx", "cx"]
887+
backend = GenericBackendV2(3, basis_gates=basis)
888+
transpile(qc, backend=backend, optimization_level=2)
889+
self.assertTrue(mock_pass.called)
890+
mock_pass.called = False
891+
transpile(qc, basis_gates=basis, optimization_level=2)
892+
self.assertTrue(mock_pass.called)
893+
mock_pass.called = False
894+
transpile(qc, target=backend.target, optimization_level=2)
895+
self.assertTrue(mock_pass.called)
896+
mock_pass.called = False
897+
transpile(qc, backend=backend, optimization_level=3)
898+
self.assertTrue(mock_pass.called)
899+
mock_pass.called = False
900+
transpile(qc, basis_gates=basis, optimization_level=3)
901+
self.assertTrue(mock_pass.called)
902+
865903
def test_optimize_to_nothing(self):
866904
"""Optimize gates up to fixed point in the default pipeline
867905
See https://github.com/Qiskit/qiskit-terra/issues/2035

test/python/transpiler/test_preset_passmanagers.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import unittest
1616

17+
1718
from test import combine
1819
from ddt import ddt, data
1920

@@ -279,7 +280,7 @@ def counting_callback_func(pass_, dag, time, property_set, count):
279280
callback=counting_callback_func,
280281
translation_method="synthesis",
281282
)
282-
self.assertEqual(gates_in_basis_true_count + 1, collect_2q_blocks_count)
283+
self.assertEqual(gates_in_basis_true_count + 2, collect_2q_blocks_count)
283284

284285

285286
@ddt

0 commit comments

Comments
 (0)