Skip to content

Commit

Permalink
'Peephole' optimization - or: collecting and optimizing two-qubit blo…
Browse files Browse the repository at this point in the history
…cks - before routing (#12727) (#12881)

* 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)

Co-authored-by: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com>
  • Loading branch information
mergify[bot] and sbrandhsn authored Aug 1, 2024
1 parent 9120b8d commit f4cb741
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 1 deletion.
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
ElidePermutations
NormalizeRXAngle
OptimizeAnnotated
Split2QUnitaries
Calibration
=============
Expand Down Expand Up @@ -244,6 +245,7 @@
from .optimization import ElidePermutations
from .optimization import NormalizeRXAngle
from .optimization import OptimizeAnnotated
from .optimization import Split2QUnitaries

# circuit analysis
from .analysis import ResourceEstimation
Expand Down
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
83 changes: 83 additions & 0 deletions qiskit/transpiler/passes/optimization/split_2q_unitaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 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."""
from typing import Optional

from qiskit.transpiler.basepasses import TransformationPass
from qiskit.circuit.quantumcircuitdata import CircuitInstruction
from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode
from qiskit.circuit.library.generalized_gates import UnitaryGate
from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition


class Split2QUnitaries(TransformationPass):
"""Attempt to splits two-qubit gates in a :class:`.DAGCircuit` into two single-qubit gates
This pass will analyze all the two qubit gates in the circuit and analyze the gate's unitary
matrix to determine if the gate is actually a product of 2 single qubit gates. In these
cases the 2q gate can be simplified into two single qubit gates and this pass will
perform this optimization and will replace the two qubit gate with two single qubit
:class:`.UnitaryGate`.
"""

def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16):
"""Split2QUnitaries initializer.
Args:
fidelity (float): Allowed tolerance for splitting two-qubit unitaries and gate decompositions
"""
super().__init__()
self.requested_fidelity = fidelity

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
or len(node.qargs) != 2
or node.matrix is None
or node.is_parameterized()
):
continue

decomp = TwoQubitWeylDecomposition(node.op, fidelity=self.requested_fidelity)
if (
decomp._inner_decomposition.specialization
== TwoQubitWeylDecomposition._specializations.IdEquiv
):
new_dag = DAGCircuit()
new_dag.add_qubits(node.qargs)

ur = decomp.K1r
ur_node = DAGOpNode.from_instruction(
CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=new_dag
)

ul = decomp.K1l
ul_node = DAGOpNode.from_instruction(
CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=new_dag
)
new_dag._apply_op_node_back(ur_node)
new_dag._apply_op_node_back(ul_node)
new_dag.global_phase = decomp.global_phase
dag.substitute_node_with_dag(node, new_dag)
elif (
decomp._inner_decomposition.specialization
== TwoQubitWeylDecomposition._specializations.SWAPEquiv
):
# TODO maybe also look into swap-gate-like gates? Things to consider:
# * As the qubit mapping may change, we'll always need to build a new dag in this pass
# * There may not be many swap-gate-like gates in an arbitrary input circuit
# * Removing swap gates from a user-routed input circuit here is unexpected
pass
return dag
65 changes: 65 additions & 0 deletions qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import os

from qiskit.circuit import Instruction
from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries
from qiskit.transpiler.passmanager import PassManager
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes import BasicSwap
Expand Down Expand Up @@ -64,12 +66,23 @@
CYGate,
SXGate,
SXdgGate,
get_standard_gate_name_mapping,
)
from qiskit.utils.parallel import CPU_COUNT
from qiskit import user_config

CONFIG = user_config.get_config()

_discrete_skipped_ops = {
"delay",
"reset",
"measure",
"switch_case",
"if_else",
"for_loop",
"while_loop",
}


class DefaultInitPassManager(PassManagerStagePlugin):
"""Plugin class for default init stage."""
Expand Down Expand Up @@ -160,6 +173,58 @@ 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
"""
found_one_continuous_gate = False
for op in ops:
if isinstance(op, str):
if op in _discrete_skipped_ops:
continue
op = stdgates.get(op, None)

if op is not None and op.name in _discrete_skipped_ops:
continue

if op is None or not isinstance(op, Instruction):
return False

if len(op.params) > 0:
found_one_continuous_gate = True
return found_one_continuous_gate

target = pass_manager_config.target
basis = 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 basis is None
do_consolidate_blocks_init |= target is not None and _is_one_op_non_discrete(
target.operations
)
do_consolidate_blocks_init |= basis is not None and _is_one_op_non_discrete(basis)

if do_consolidate_blocks_init:
init.append(Collect2qBlocks())
init.append(ConsolidateBlocks())
# If approximation degree is None that indicates a request to approximate up to the
# error rates in the target. However, in the init stage we don't yet know the target
# qubits being used to figure out the fidelity so just use the default fidelity parameter
# in this case.
if pass_manager_config.approximation_degree is not None:
init.append(Split2QUnitaries(pass_manager_config.approximation_degree))
else:
init.append(Split2QUnitaries())
else:
raise TranspilerError(f"Invalid optimization level {optimization_level}")
return init
Expand Down
20 changes: 20 additions & 0 deletions releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
features_transpiler:
- |
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.
- |
The passes :class:`.Collect2qBlocks`, :class:`.ConsolidateBlocks` and :class:`.Split2QUnitaries` have been
added to the ``init`` stage of the preset pass managers with optimization level 2 and optimization level 3.
The modification of the `init` stage should allow for a more efficient routing for quantum circuits that either:
* contain two-qubit unitaries/gates that are actually a product of single-qubit gates
* 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. These passes are not run if the pass
managers target a :class:`.Target` that has a discrete basis gate set, i.e. all basis gates have are not
parameterized.
38 changes: 38 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 Down Expand Up @@ -862,6 +864,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)
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
3 changes: 2 additions & 1 deletion 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 @@ -279,7 +280,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)


@ddt
Expand Down
Loading

0 comments on commit f4cb741

Please sign in to comment.