Skip to content
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

'Peephole' optimization - or: collecting and optimizing two-qubit blocks - before routing #12727

Merged
merged 51 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9965ce3
init
sbrandhsn Jul 5, 2024
f3af45a
Merge branch 'Qiskit:main' into peephole-opt
sbrandhsn Jul 5, 2024
c650503
up
sbrandhsn Jul 5, 2024
092257c
up
sbrandhsn Jul 5, 2024
3befb83
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
a8c5b8c
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
90e725c
reno
sbrandhsn Jul 8, 2024
fd988e3
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
e765285
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
1805cb8
Update peephole-before-routing-c3d184b740bb7a8b.yaml
sbrandhsn Jul 8, 2024
bce3fcd
neko check
sbrandhsn Jul 8, 2024
e6f0b39
check neko
sbrandhsn Jul 8, 2024
6f7445c
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
d3a45e7
test neko
sbrandhsn Jul 8, 2024
dec91de
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
45912f8
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
5404822
Update builtin_plugins.py
sbrandhsn Jul 8, 2024
ed790c1
lint
sbrandhsn Jul 8, 2024
1236cd6
tests and format
sbrandhsn Jul 8, 2024
db8aa2d
remove FakeTorino test
sbrandhsn Jul 8, 2024
3e28ec6
Update peephole-before-routing-c3d184b740bb7a8b.yaml
sbrandhsn Jul 9, 2024
d064c66
Apply suggestions from code review
sbrandhsn Jul 12, 2024
b88e3f0
comments from code review
sbrandhsn Jul 12, 2024
a655001
fix precision
sbrandhsn Jul 16, 2024
8aa570d
up
sbrandhsn Jul 26, 2024
c84fa16
up
sbrandhsn Jul 26, 2024
b8fea91
update
sbrandhsn Jul 26, 2024
6137daa
up
sbrandhsn Jul 26, 2024
a6ea05b
.
sbrandhsn Jul 26, 2024
c6f4530
cyclic import
sbrandhsn Jul 28, 2024
6ec02fc
cycl import
sbrandhsn Jul 29, 2024
44f2f38
cyl import
sbrandhsn Jul 29, 2024
22bb156
Merge branch 'main' into peephole-opt
sbrandhsn Jul 29, 2024
df0897f
.
sbrandhsn Jul 29, 2024
66d0a22
circular import
sbrandhsn Jul 29, 2024
e2074b6
Merge branch 'main' into peephole-opt
sbrandhsn Jul 29, 2024
aff157b
.
sbrandhsn Jul 29, 2024
c333f5b
lint
sbrandhsn Jul 30, 2024
1bdf860
Include new pass in docs
mtreinish Jul 31, 2024
a28a564
Fix Split2QUnitaries dag manipulation
mtreinish Jul 31, 2024
6eaed5d
Update releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml
mtreinish Jul 31, 2024
254ea38
Merge branch 'main' into peephole-opt
mtreinish Jul 31, 2024
9964d65
stricter check for doing split2q
sbrandhsn Aug 1, 2024
b90f9f6
Update qiskit/transpiler/preset_passmanagers/builtin_plugins.py
sbrandhsn Aug 1, 2024
a7d3594
code review
sbrandhsn Aug 1, 2024
89ac7dc
Update qiskit/transpiler/passes/optimization/split_2q_unitaries.py
sbrandhsn Aug 1, 2024
229b028
new tests
sbrandhsn Aug 1, 2024
d8fba88
Merge branch 'peephole-opt' of https://github.com/sbrandhsn/qiskit in…
sbrandhsn Aug 1, 2024
48eacd7
typo
sbrandhsn Aug 1, 2024
edde611
lint
sbrandhsn Aug 1, 2024
5404bbc
lint
sbrandhsn Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/optimization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@
from .elide_permutations import ElidePermutations
from .normalize_rx_angle import NormalizeRXAngle
from .optimize_annotated import OptimizeAnnotated
from .split_2q_unitaries import Split2QUnitaries
49 changes: 49 additions & 0 deletions qiskit/transpiler/passes/optimization/split_2q_unitaries.py
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."""
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved

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.
Show resolved Hide 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.
Show resolved Hide resolved
):
continue

# check if the node can be represented by single-qubit gates
nmat = node.op.to_matrix()
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved
if np.all(two_qubit_local_invariants(nmat) == [1, 0, 3]):
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved
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.
Show resolved Hide resolved
dag_node.global_phase += phase
dag.substitute_node_with_dag(node, dag_node)
Copy link
Member

Choose a reason for hiding this comment

The 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:

https://github.com/Qiskit/qiskit/blob/main/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py#L244-L248

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, thanks. I will modify this.


return dag
48 changes: 48 additions & 0 deletions qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"""Built-in transpiler stage plugins for preset pass managers."""

from qiskit.circuit import Instruction
from qiskit.transpiler.passmanager import PassManager
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes import BasicSwap
Expand Down Expand Up @@ -39,6 +40,7 @@
Collect2qBlocks,
ConsolidateBlocks,
InverseCancellation,
Split2QUnitaries,
)
from qiskit.transpiler.passes import Depth, Size, FixedPoint, MinimumPoint
from qiskit.transpiler.passes.utils.gates_basis import GatesInBasis
Expand All @@ -62,8 +64,11 @@
CYGate,
SXGate,
SXdgGate,
get_standard_gate_name_mapping,
)

_discrete_skipped_ops = {"delay", "reset", "measure"}


class DefaultInitPassManager(PassManagerStagePlugin):
"""Plugin class for default init stage."""
Expand Down Expand Up @@ -154,6 +159,49 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
)
)
init.append(CommutativeCancellation())
# skip peephole optimization before routing if target basis gate set is discrete,
# i.e. only consists of Cliffords that an user might want to keep
# use rz, sx, x, cx as basis, rely on physical optimziation to fix everything later one
stdgates = get_standard_gate_name_mapping()

def _is_one_op_non_discrete(ops):
"""Checks if one operation in `ops` is not discrete, i.e. is parameterizable
Args:
ops (List(Operation)): list of operations to check
Returns
True if at least one operation in `ops` is not discrete, False otherwise
"""
for op in ops:
if isinstance(op, str):
op = stdgates.get(op, None)

if (
op is None
or not isinstance(op, Instruction)
or op.name in _discrete_skipped_ops
):
continue

if len(op.params) > 0:
return True
return False
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

target = pass_manager_config.target
bases = pass_manager_config.basis_gates
# consolidate gates before routing if the user did not specify a discrete basis gate, i.e.
# * no target or basis gate set has been specified
# * target has been specified, and we have one non-discrete gate in the target's spec
# * basis gates have been specified, and we have one non-discrete gate in that set
do_consolidate_blocks_init = target is None and bases is None
do_consolidate_blocks_init |= target is not None and _is_one_op_non_discrete(
target.operations
)
do_consolidate_blocks_init |= bases is not None and _is_one_op_non_discrete(bases)

if do_consolidate_blocks_init:
init.append(Collect2qBlocks())
init.append(ConsolidateBlocks())
init.append(Split2QUnitaries())
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved
else:
raise TranspilerError(f"Invalid optimization level {optimization_level}")
return init
Expand Down
16 changes: 16 additions & 0 deletions releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
features:
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved
- |
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.
Copy link
Member

Choose a reason for hiding this comment

The 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.

48 changes: 48 additions & 0 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -109,6 +111,16 @@ def _define(self):
self._definition = QuantumCircuit(2)
self._definition.cx(0, 1)

def to_matrix(self) -> np.ndarray:
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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."""
Expand Down Expand Up @@ -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.
Show resolved Hide 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
Expand Down
17 changes: 14 additions & 3 deletions test/python/transpiler/test_preset_passmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import unittest


from test import combine
from ddt import ddt, data

Expand Down Expand Up @@ -126,14 +127,14 @@ def test_layout_3239(self, level=3):
qc.measure(range(4), range(4))
result = transpile(
qc,
basis_gates=["u1", "u2", "u3", "cx"],
basis_gates=["h", "z", "cx"],
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved
layout_method="trivial",
optimization_level=level,
)

dag = circuit_to_dag(result)
op_nodes = [node.name for node in dag.topological_op_nodes()]
self.assertNotIn("u1", op_nodes) # Check if the diagonal Z-Gates (u1) were removed
self.assertNotIn("z", op_nodes) # Check if the diagonal Z-Gates (u1) were removed

@combine(level=[0, 1, 2, 3], name="level{level}")
def test_no_basis_gates(self, level):
Expand Down Expand Up @@ -262,7 +263,7 @@ def counting_callback_func(pass_, dag, time, property_set, count):
callback=counting_callback_func,
translation_method="synthesis",
)
self.assertEqual(gates_in_basis_true_count + 1, collect_2q_blocks_count)
self.assertEqual(gates_in_basis_true_count + 2, collect_2q_blocks_count)
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved


@ddt
Expand Down Expand Up @@ -1455,6 +1456,16 @@ def _define(self):
self._definition = QuantumCircuit(2)
self._definition.cx(0, 1)

def to_matrix(self) -> np.ndarray:
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],
]
)
sbrandhsn marked this conversation as resolved.
Show resolved Hide resolved

circuit = QuantumCircuit(6, 1)
circuit.h(0)
circuit.measure(0, 0)
Expand Down
78 changes: 78 additions & 0 deletions test/python/transpiler/test_split_2q_unitaries.py
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.
Show resolved Hide 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
)
)
Loading