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.26k stars 2.37k forks source link

Combining two transpilations on a real backend #11282

Open alejomonbar opened 12 months ago

alejomonbar commented 12 months ago

Environment

What is happening?

Classical shadows is an efficient method for constructing an approximate classical description of a quantum state. There is a problem when trying to implement it on a real device, it requires the transpilation of two different layers, one that is the same for all the circuits, and one random with single-qubit operations. If I try to join the two layers and do the transpilation afterwards it takes too long. If I try to transpile the first and the second separately to reuse the first layer, I didn't find a way for my second layer to be aware of the first transpiled layer. This is a follow-up to our discussion @1ucian0.

How can we reproduce the issue?

The code below could help to understand what I found:


from qiskit import QuantumCircuit, ClassicalRegister
from qiskit_ibm_provider import IBMProvider
from qiskit.circuit.library import EfficientSU2
from qiskit import transpile
import numpy as np

provider = IBMProvider()

ibm_brisbane = provider.get_backend("ibm_brisbane")

num_qubits = 7
qc = QuantumCircuit(num_qubits)
for i in range(0, num_qubits-1, 2):
    qc_su2 = EfficientSU2(2, reps=1)
    params_i = np.pi * np.random.rand(qc_su2.num_parameters)
    qc_su2 = qc_su2.assign_parameters(params_i)
    qc = qc.compose(qc_su2, [i,i+1])
    qc = transpile(qc, ibm_brisbane)

# Adding the classical shadows layer with a random projection set
n_shadow = 1
unitary_ids = np.random.randint(0, 3, size=(n_shadow, num_qubits))
qcs = []
for ns in range(n_shadow):
    qc_shadow = QuantumCircuit(num_qubits) # Classical Shadows layer circuit
    for i in range(num_qubits):
        if unitary_ids[ns, i] == 0:
            qc_shadow.h(i)
        elif unitary_ids[ns, i] == 1:
            qc_shadow.sdg(i)
            qc_shadow.h(i) 
        elif unitary_ids[ns, i] == 2:
            pass
    # How do I transpile the circuit below with the same routing map of the previous circuit qc? 
    qc_shadow = transpile(qc_shadow, ibm_brisbane)
    circuit = qc.compose(qc_shadow, range(num_qubits))
    circuit.add_register(ClassicalRegister(num_qubits))
    circuit.measure(range(num_qubits), range(num_qubits))
    qcs.append(circuit)

What should happen?

Been able to combine the two circuits while keeping the same logical qubits.

Any suggestions?

No response

jakelishman commented 12 months ago

What calls are you making that it seems like transpilation is taking a long time? The first call to transpile with a new IBM Runtime backend may take a couple of seconds to initialise the backend fully, but after that, transpilation should be fast. In your example, if I delay the transpilation until after the whole circuit is constructed, the transpilation takes 37ms on my machine.

You can set the initial virtual->physical mapping for subsequent transpilations by using the initial_layout argument to transpile.

alejomonbar commented 12 months ago

What calls are you making that it seems like transpilation is taking a long time? The first call to transpile with a new IBM Runtime backend may take a couple of seconds to initialise the backend fully, but after that, transpilation should be fast. In your example, if I delay the transpilation until after the whole circuit is constructed, the transpilation takes 37ms on my machine.

You can set the initial virtual->physical mapping for subsequent transpilations by using the initial_layout argument to transpile.

Thank you for the quick reply, @jakelishman. For me this code is broken, isn't it for you? When I said it takes too long is as if the whole transpilation is made after combining the two layers. Maybe it does not take too long for small circuits but the idea is to make it for 127 qubits with a number of shadows around 10.000.

jakelishman commented 10 months ago

Sorry for the long delay between now and then. The code block exactly as presented is broken, but I was talking about how long transpile took if you build the circuits completely and then transpile, so I used this tweaked code-block, with the only changes indicated:

from qiskit import QuantumCircuit, ClassicalRegister
from qiskit_ibm_provider import IBMProvider
from qiskit.circuit.library import EfficientSU2
from qiskit import transpile
import numpy as np

provider = IBMProvider()

ibm_brisbane = provider.get_backend("ibm_brisbane")

num_qubits = 7
qc = QuantumCircuit(num_qubits)
for i in range(0, num_qubits-1, 2):
    qc_su2 = EfficientSU2(2, reps=1)
    params_i = np.pi * np.random.rand(qc_su2.num_parameters)
    qc_su2 = qc_su2.assign_parameters(params_i)
    qc = qc.compose(qc_su2, [i,i+1])
# I think this `transpile` line should have been outside the loop anyway:
# REMOVED: qc = transpile(qc, ibm_brisbane)

# Adding the classical shadows layer with a random projection set
n_shadow = 1
unitary_ids = np.random.randint(0, 3, size=(n_shadow, num_qubits))
qcs = []
for ns in range(n_shadow):
    qc_shadow = QuantumCircuit(num_qubits) # Classical Shadows layer circuit
    for i in range(num_qubits):
        if unitary_ids[ns, i] == 0:
            qc_shadow.h(i)
        elif unitary_ids[ns, i] == 1:
            qc_shadow.sdg(i)
            qc_shadow.h(i)
        elif unitary_ids[ns, i] == 2:
            pass
    # REMOVED: qc_shadow = transpile(qc_shadow, ibm_brisbane)
    circuit = qc.compose(qc_shadow, range(num_qubits))
    circuit.add_register(ClassicalRegister(num_qubits))
    circuit.measure(range(num_qubits), range(num_qubits))
    qcs.append(circuit)

At the end of this, calling

transpile(qcs, ibm_brisbane)

was timed at 36.8(6)ms on my machine, which doesn't seem excessive. Are you working at scales where you absolutely must pre-transpile the initial setup of the circuit?

In principle, the way you would do the transpile(qc_shadow, ibm_brisbane) call using the final layout map of a pre-transpiled circuit is to find the positions that virtual qubits $0, \dots, n-1$ end up in, and set that as the initial_layout argument to the second transpile call. That looks something like:

qc_base_transpiled = transpile(qc_base, ibm_brisbane)
base_final_layout = qc_base_transpiled.layout.final_index_layout()
qc_shadow = <build your base shadow>
# This transpilation ensures the virtual qubits in `qc_shadow` start in the places that the
# same-numbered virtual qubits in `qc_base` finished on.
qc_shadow_transpiled = transpile(qc_shadow, ibm_brisbane, initial_layout=base_final_layout)
# We don't need to remap the qubits in the composition, because both circuits are in terms of
# the same physical qubits.
circuit = qc_base.compose(qc_shadow_transpiled)

I should note that you should add the measurements to the second circuit before you transpile it, to ensure that the remappings and any subsequent inserted swaps are respected.