Qiskit / qiskit

Qiskit is an open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives.
https://www.ibm.com/quantum/qiskit
Apache License 2.0
5.11k stars 2.35k forks source link

OptimizeSwapBeforeMeasure pass drops Swap gate (even if there is NO measure after it) #7642

Open MattePalte opened 2 years ago

MattePalte commented 2 years ago

Environment

What is happening?

I define a a sub-circuit without any measurement (since it is intended as sub-component); this sub-circuit ends with a swap gate, then I optimize the sub-circuit (optimization level=3) before using it as building block for a larger circuit/algorithm. But this optimization drops my final swap gate and leaves me with a semantically different sub-circuit with respect to the initial one I wrote. Thus the optimization is modifying the semantic in this context.

How can we reproduce the issue?

I transpile with level 0 and I get the expected result:

from qiskit import QuantumCircuit, transpile, Aer, execute
from qiskit.circuit.library.standard_gates import *
qc = QuantumCircuit(3)
qc.x(0)
qc.swap(0,1)
qc = transpile(qc, optimization_level=0)
qc.measure_all()
qc.draw(fold=-1)
counts = execute(qc, backend=Aer.get_backend('qasm_simulator'), shots=1024).result().get_counts(qc)
        ┌───┐    ░ ┌─┐      
   q_0: ┤ X ├─X──░─┤M├──────
        └───┘ │  ░ └╥┘┌─┐   
   q_1: ──────X──░──╫─┤M├───
                 ░  ║ └╥┘┌─┐
   q_2: ─────────░──╫──╫─┤M├
                 ░  ║  ║ └╥┘
meas: 3/════════════╩══╩══╩═
                    0  1  2 
{'010': 1024}

Running it with level 3 instead, drops my swap and gives a semantically different circuit, with different result.

from qiskit import QuantumCircuit, transpile, Aer, execute
from qiskit.circuit.library.standard_gates import *
qc = QuantumCircuit(3)
qc.x(0)
qc.swap(0,1)
import pdb
pdb.set_trace()
qc = transpile(qc, optimization_level=3)
qc.measure_all()
qc.draw(fold=-1)
counts = execute(qc, backend=Aer.get_backend('qasm_simulator'), shots=1024).result().get_counts(qc)
        ┌───┐ ░ ┌─┐      
   q_0: ┤ X ├─░─┤M├──────
        └───┘ ░ └╥┘┌─┐   
   q_1: ──────░──╫─┤M├───
              ░  ║ └╥┘┌─┐
   q_2: ──────░──╫──╫─┤M├
              ░  ║  ║ └╥┘
meas: 3/═════════╩══╩══╩═
                 0  1  2 
{'001': 1024}

What should happen?

I would have expected the optimization to preserve the semantic of the input circuit.

Any suggestions?

Via interactive debugging I nailed down the problem to the optimization pass OptimizeSwapBeforeMeasure. This removes the swap before measurement and the swaps which are before final nodes DAGOutNode (aka end of the circuit). But in this way we are assuming that there will be never be any measurement, which in case of sub-component this is not true. Thus I suggest to remove this optimization for the swap followed by DAGOutNode nodes, because we cannot guarantee a perfectly semantically equivalent circuit.

https://github.com/Qiskit/qiskit-terra/blob/fcec842f1de9fd12120e30a1bf73bf7c52b1bf81/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py#L45

        swaps = dag.op_nodes(SwapGate)
        for swap in swaps[::-1]:
            if swap.op.condition is not None:
                continue
            final_successor = []
            for successor in dag.successors(swap):
                final_successor.append(
                    isinstance(successor, DAGOutNode)
                    or (isinstance(successor, DAGOpNode) and isinstance(successor.op, Measure))
                )
            if all(final_successor):
                # the node swap needs to be removed and, if a measure follows, needs to be adapted
                swap_qargs = swap.qargs
                measure_layer = DAGCircuit()
                for qreg in dag.qregs.values():
                    measure_layer.add_qreg(qreg)
                for creg in dag.cregs.values():
                    measure_layer.add_creg(creg)
                for successor in list(dag.successors(swap)):
                    if isinstance(successor, DAGOpNode) and isinstance(successor.op, Measure):
                        # replace measure node with a new one, where qargs is set with the "other"
                        # swap qarg.
                       ...
                dag.compose(measure_layer)
                dag.remove_op_node(swap)

Going back to my bug-triggering code, if the measurement would have been included in the circuit before the transpile call it would have worked perfectly, but sometimes we might not know how this sub-circuit will be reused in other parts of our algorithm.

jakelishman commented 2 years ago

Right now, transpile is purely for complete quantum programs; you say you want to use it on a subcomponent, but right now that's not a supported use. In the context of an entire program, there is no semantic violation here. Our documentation on this expectation could certainly be better, though.

Compilation of subcomponents is something that has come up a couple of times before, and it may be something we want to support in the future. It's not immediately obvious as to how this would work, though; various things like routing, layout and scheduling need to know the exact register allocations, and a lot of optimisations need to know what swaps/couplings will be in place in order to rearrange gates. Even if we did the initial basis translation on a subcomponent, it couldn't actually be re-used safely unless it was laid out onto specific qubits and then only applied to that exact same set, and even then it would need to be scheduled before it could run.