Skip to content

Commit

Permalink
Add ElidePermutations transpiler pass (#9523)
Browse files Browse the repository at this point in the history
* Add ElideSwaps transpiler pass

This commit adds a new transpiler pass ElideSwaps which is a logical
optimization pass designed to run prior to layout or any other physical
embedding steps in the transpiler pipeline. It traverses the DAG in
topological order and when a swap gate is encountered it removes that
gate and instead permutes the logical qubits for any subsequent gates
in the DAG. This will eliminate any swaps in a circuit not caused by
routing. Additionally, this pass is added to the preset pass manager for
optimization level 3, we can consider adding it to other levels too if
we think it makes sense (there is little overhead, and almost 0 if there
are no swaps).

One thing to note is that this pass does not recurse into control flow
blocks at all, it treats them as black box operations. So if any control
flow blocks contain swap gates those will not be optimized away. This
change was made because mapping the permutation outside and inside any
control flow block was larger in scope than what the intent for this
pass was. That type of work is better suited for the routing passes
which already have to reason about this.

* Update tests with optimization level 3

* Pass final layout from ElideSwap to output

The new ElideSwap pass is causing an output permutation just as a
routing pass would. This information needs to be passed through to the
output in case there is no layout or routing run. In those cases the
information about the output permutation caused by the swap elision will
be lost and doing layout aware operations like Operator.from_circuit()
will not be able to reason about the permutation. This commit fixes this
by inserting the original layout and qubit mapping into the property set
along with the final layout. Then the base pass class and pass manager
class are updated to use the original layout as the initial layout if
one isn't set. In cases where we run layout and routing the output
metadata from those passes will superscede these new metadata fields.

* Move pass in opt level 3 earlier in stage and skip with explicit layout

This commit fixes 2 issues in the execution of the new ElideSwaps pass
as part of optimization level 3. First we were running it too late in
the process both after high level synthesis (which isn't relavant yet,
but will be soon when this is expanded to elide all permutations not
just swaps) and also after reset diagonal before measurement. The second
issue is that if the user is specifying to run with a manual layout set
we should not run this pass, as it will interfere with the user intent.

* Doc and copy paste fixes

* Expand test coverage

* Update permutation tracking

There were 2 issues with the permutation tracking done in an earlier
commit. First, it conflicted with the final_layout property set via
routing (or internally by the routing done in the combined sabre
layout). This was breaking conditional checks in the preset pass manager
around embedding. To fix this a different property is used and set as
the output final layout if no final layout is set. The second issue was
the output layout object was not taking into account a set initial
layout which will permute the qubits and cause the output to not be up
to date. This is fixed by updating apply layout to apply the initial
layout to the elision_final_layout in the property set.

* Generalize pass to support PermutationGate too

This commit generalizes the pass from just considering swap gates to all
permutations (via the PermutationGate class). This enables the pass to
elide additional circuit permutations, not just the special case of a
swap gate. The pass and module are renamed accordingly to
ElidePermutations and elide_permutations respectively.

* Fix permutation handling

This commit fixes the recently added handling of the PermutationGate so
that it correctly is mapping the qubits. The previous iteration of this
messed up the mapping logic so it wasn't valid.

* Fix formatting

* Fix final layout handling for no initial layout

* Improve documentation and log a warning if run post layout

* Fix final layout handling with no ElideSwaps being run

* Fix docs build

* Fix release note

* Fix typo

* Add test for routing and elide permutations

* Apply suggestions from code review

Co-authored-by: Jim Garrison <jim@garrison.cc>

* Rename test file to test_elide_permutations.py

* Apply suggestions from code review

Co-authored-by: Kevin Hartman <kevin@hart.mn>

* Fix test import after rebase

* fixing failing test cases

this should pass CI after merging #12057

* addresses kehas comments - thx

* Adding FinalyzeLayouts pass to pull the virtual circuit permutation from ElidePermutations to the final layout

* formatting

* partial rebase on top of 12057 + tests

* also need test_operator for partial rebase

* removing from transpiler flow for now; reworking elide tests

* also adding permutation gate to the example

* also temporarily reverting test_transpiler.py

* update to release notes

* minor fixes

* Apply suggestions from code review

* Fix lint

* Update qiskit/transpiler/passes/optimization/elide_permutations.py

* Add test to test we skip after layout

* Integrate FinalizeLayouts into the PassManager harness

This commit integrates the function that finalize layouts was performing
into the passmanager harnesss. We'll always need to run the equivalent
of finalize layout if any passes are setting a virtual permutation so
using a standalone pass that can be forgotten is potentially error
prone. This inlines the logic as part of the passmanager's output
preparation stage so we always finalize the layout.

* Compose a potential existing virtual_permutation_layout

* Remove unused import

---------

Co-authored-by: Jim Garrison <jim@garrison.cc>
Co-authored-by: Kevin Hartman <kevin@hart.mn>
Co-authored-by: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com>
Co-authored-by: AlexanderIvrii <alexi@il.ibm.com>
Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
  • Loading branch information
6 people committed May 2, 2024
1 parent 469c989 commit 122c64e
Show file tree
Hide file tree
Showing 9 changed files with 631 additions and 4 deletions.
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
EchoRZXWeylDecomposition
ResetAfterMeasureSimplification
OptimizeCliffords
ElidePermutations
NormalizeRXAngle
OptimizeAnnotated
Expand Down Expand Up @@ -236,6 +237,7 @@
from .optimization import CollectCliffords
from .optimization import ResetAfterMeasureSimplification
from .optimization import OptimizeCliffords
from .optimization import ElidePermutations
from .optimization import NormalizeRXAngle
from .optimization import OptimizeAnnotated

Expand Down
1 change: 0 additions & 1 deletion qiskit/transpiler/passes/layout/apply_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ def run(self, dag):
raise TranspilerError("The 'layout' must be full (with ancilla).")

post_layout = self.property_set["post_layout"]

q = QuantumRegister(len(layout), "q")

new_dag = DAGCircuit()
Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/layout/set_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def run(self, dag):
layout = None
else:
raise InvalidLayoutError(
f"SetLayout was intialized with the layout type: {type(self.layout)}"
f"SetLayout was initialized with the layout type: {type(self.layout)}"
)
self.property_set["layout"] = layout
return dag
1 change: 1 addition & 0 deletions qiskit/transpiler/passes/optimization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@
from .reset_after_measure_simplification import ResetAfterMeasureSimplification
from .optimize_cliffords import OptimizeCliffords
from .collect_cliffords import CollectCliffords
from .elide_permutations import ElidePermutations
from .normalize_rx_angle import NormalizeRXAngle
from .optimize_annotated import OptimizeAnnotated
112 changes: 112 additions & 0 deletions qiskit/transpiler/passes/optimization/elide_permutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023
#
# 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.


"""Remove any swap gates in the circuit by pushing it through into a qubit permutation."""

import logging

from qiskit.circuit.library.standard_gates import SwapGate
from qiskit.circuit.library.generalized_gates import PermutationGate
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.layout import Layout

logger = logging.getLogger(__name__)


class ElidePermutations(TransformationPass):
r"""Remove permutation operations from a pre-layout circuit
This pass is intended to be run before a layout (mapping virtual qubits
to physical qubits) is set during the transpilation pipeline. This
pass iterates over the :class:`~.DAGCircuit` and when a :class:`~.SwapGate`
or :class:`~.PermutationGate` are encountered it permutes the virtual qubits in
the circuit and removes the swap gate. This will effectively remove any
:class:`~SwapGate`\s or :class:`~PermutationGate` in the circuit prior to running
layout. If this pass is run after a layout has been set it will become a no-op
(and log a warning) as this optimization is not sound after physical qubits are
selected and there are connectivity constraints to adhere to.
For tracking purposes this pass sets 3 values in the property set if there
are any :class:`~.SwapGate` or :class:`~.PermutationGate` objects in the circuit
and the pass isn't a no-op.
* ``original_layout``: The trivial :class:`~.Layout` for the input to this pass being run
* ``original_qubit_indices``: The mapping of qubit objects to positional indices for the state
of the circuit as input to this pass.
* ``virtual_permutation_layout``: A :class:`~.Layout` object mapping input qubits to the output
state after eliding permutations.
These three properties are needed for the transpiler to track the permutations in the out
:attr:`.QuantumCircuit.layout` attribute. The elision of permutations is equivalent to a
``final_layout`` set by routing and all three of these attributes are needed in the case
"""

def run(self, dag):
"""Run the ElidePermutations pass on ``dag``.
Args:
dag (DAGCircuit): the DAG to be optimized.
Returns:
DAGCircuit: the optimized DAG.
"""
if self.property_set["layout"] is not None:
logger.warning(
"ElidePermutations is not valid after a layout has been set. This indicates "
"an invalid pass manager construction."
)
return dag

op_count = dag.count_ops(recurse=False)
if op_count.get("swap", 0) == 0 and op_count.get("permutation", 0) == 0:
return dag

new_dag = dag.copy_empty_like()
qubit_mapping = list(range(len(dag.qubits)))

def _apply_mapping(qargs):
return tuple(dag.qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs)

for node in dag.topological_op_nodes():
if not isinstance(node.op, (SwapGate, PermutationGate)):
new_dag.apply_operation_back(
node.op, _apply_mapping(node.qargs), node.cargs, check=False
)
elif getattr(node.op, "condition", None) is not None:
new_dag.apply_operation_back(
node.op, _apply_mapping(node.qargs), node.cargs, check=False
)
elif isinstance(node.op, SwapGate):
index_0 = dag.find_bit(node.qargs[0]).index
index_1 = dag.find_bit(node.qargs[1]).index
qubit_mapping[index_1], qubit_mapping[index_0] = (
qubit_mapping[index_0],
qubit_mapping[index_1],
)
elif isinstance(node.op, PermutationGate):
starting_indices = [qubit_mapping[dag.find_bit(qarg).index] for qarg in node.qargs]
pattern = node.op.params[0]
pattern_indices = [qubit_mapping[idx] for idx in pattern]
for i, j in zip(starting_indices, pattern_indices):
qubit_mapping[i] = j
input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)}
self.property_set["original_layout"] = Layout(input_qubit_mapping)
if self.property_set["original_qubit_indices"] is None:
self.property_set["original_qubit_indices"] = input_qubit_mapping

new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)})
if current_layout := self.property_set["virtual_permutation_layout"] is not None:
self.property_set["virtual_permutation_layout"] = current_layout.compose(new_layout)
else:
self.property_set["virtual_permutation_layout"] = new_layout
return new_dag
45 changes: 44 additions & 1 deletion qiskit/transpiler/passmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from qiskit.passmanager.exceptions import PassManagerError
from .basepasses import BasePass
from .exceptions import TranspilerError
from .layout import TranspileLayout
from .layout import TranspileLayout, Layout

_CircuitsT = Union[List[QuantumCircuit], QuantumCircuit]

Expand Down Expand Up @@ -69,6 +69,7 @@ def _passmanager_backend(
) -> QuantumCircuit:
out_program = dag_to_circuit(passmanager_ir, copy_operations=False)

self._finalize_layouts(passmanager_ir)
out_name = kwargs.get("output_name", None)
if out_name is not None:
out_program.name = out_name
Expand Down Expand Up @@ -96,6 +97,48 @@ def _passmanager_backend(

return out_program

def _finalize_layouts(self, dag):
if (virtual_permutation_layout := self.property_set["virtual_permutation_layout"]) is None:
return

self.property_set.pop("virtual_permutation_layout")

# virtual_permutation_layout is usually created before extending the layout with ancillas,
# so we extend the permutation to be identity on ancilla qubits
original_qubit_indices = self.property_set.get("original_qubit_indices", None)
for oq in original_qubit_indices:
if oq not in virtual_permutation_layout:
virtual_permutation_layout[oq] = original_qubit_indices[oq]

t_qubits = dag.qubits

if (t_initial_layout := self.property_set.get("layout", None)) is None:
t_initial_layout = Layout(dict(enumerate(t_qubits)))

if (t_final_layout := self.property_set.get("final_layout", None)) is None:
t_final_layout = Layout(dict(enumerate(t_qubits)))

# Ordered list of original qubits
original_qubits_reverse = {v: k for k, v in original_qubit_indices.items()}
original_qubits = []
for i in range(len(original_qubits_reverse)):
original_qubits.append(original_qubits_reverse[i])

virtual_permutation_layout_inv = virtual_permutation_layout.inverse(
original_qubits, original_qubits
)

t_initial_layout_inv = t_initial_layout.inverse(original_qubits, t_qubits)

# ToDo: this can possibly be made simpler
new_final_layout = t_initial_layout_inv
new_final_layout = new_final_layout.compose(virtual_permutation_layout_inv, original_qubits)
new_final_layout = new_final_layout.compose(t_initial_layout, original_qubits)
new_final_layout = new_final_layout.compose(t_final_layout, t_qubits)

self.property_set["layout"] = t_initial_layout
self.property_set["final_layout"] = new_final_layout

def append(
self,
passes: Task | list[Task],
Expand Down
1 change: 0 additions & 1 deletion qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
)
)
init.append(CommutativeCancellation())

else:
raise TranspilerError(f"Invalid optimization level {optimization_level}")
return init
Expand Down
41 changes: 41 additions & 0 deletions releasenotes/notes/add-elide-swaps-b0a4c373c9af1efd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
features:
- |
Added a new optimization transpiler pass, :class:`~.ElidePermutations`,
which is designed to run prior to the :ref:`layout_stage` and will
optimize away any :class:`~.SwapGate`\s and
:class:`~qiskit.circuit.library.PermutationGate`\s
in a circuit by permuting virtual
qubits. For example, taking a circuit with :class:`~.SwapGate`\s:
.. plot::
from qiskit.circuit import QuantumCircuit
qc = QuantumCircuit(3)
qc.h(0)
qc.swap(0, 1)
qc.swap(2, 0)
qc.cx(1, 0)
qc.measure_all()
qc.draw("mpl")
will remove the swaps when the pass is run:
.. plot::
:include-source:
from qiskit.transpiler.passes import ElidePermutations
from qiskit.circuit import QuantumCircuit
qc = QuantumCircuit(3)
qc.h(0)
qc.swap(0, 1)
qc.swap(2, 0)
qc.cx(1, 0)
qc.measure_all()
ElidePermutations()(qc).draw("mpl")
The pass also sets the ``virtual_permutation_layout`` property set, storing
the permutation of the virtual qubits that was optimized away.
Loading

0 comments on commit 122c64e

Please sign in to comment.