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

Transpilation time of qc.prepare_state instruction shows unexpected peaks #12726

Closed jfraxanet closed 2 months ago

jfraxanet commented 3 months ago

Environment

What is happening?

We realized that the transpilation time using the AerSimulator for the instructions

qc = QuantumCircuit(num_qubits)
qc.reset(range(num_qubits))
qc.prepare_state(amplitudes)

takes much longer than

qc = QuantumCircuit(num_qubits)
qc.initialize(amplitudes)

even though the two instructions should be equivalent. This is specially relevant for iterative processes such as variational algorithms, in which the circuit is built several times, since we see structured peaks in the transpilation time for each iterative step.

Captura de pantalla 2024-07-05 a las 11 07 00

When comparing the two classes, we realized that for the qc.prepare_state()instruction, the qc.append(StatePreparation) is called with "copy=False" while for qc.initialize(), the default value is "copy=True". Even though it seems that qc.prepare_state()might be slower because it creates a circuit from scratch each time, we were not able to reproduce the results using qc.initialize()differently, and this would not explain the peaks.

How can we reproduce the issue?

import time
import numpy as np
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_aer import AerSimulator
import matplotlib.pyplot as plt

def run_circuit(num_qubits, backend, initialize=False):

    amplitudes = np.ones(2**num_qubits)/np.sqrt(2**num_qubits)

    qc = QuantumCircuit(num_qubits)
    if initialize == True:
        qc.initialize(amplitudes)
    else:
        qc.reset(range(num_qubits))
        qc.prepare_state(amplitudes)

    pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
    t0 = time.time()
    transpiled_qc = pm.run(qc)
    t1 = time.time()

    return t1-t0

num_qubits = 5
backend = AerSimulator()

plt.figure(figsize=(7, 5))

for initialize in [True, False]:

    time_array = []

    for it in range(100):

        t_diff = run_circuit(num_qubits, backend, initialize)
        time_array.append(t_diff)

    plt.plot(time_array, '.-', label="Initialize = {}".format(initialize))

plt.legend()
plt.xlabel('Iteration')
plt.ylabel ('Time (s)')

What should happen?

The following instructions should be equivalent:

qc = QuantumCircuit(num_qubits)
qc.reset(range(num_qubits))
qc.prepare_state(amplitudes)
qc = QuantumCircuit(num_qubits)
qc.initialize(amplitudes)

Any suggestions?

Probably qc.prepare_state()is implementing some step which is not needed, or redefining the circuit at some point.

jakelishman commented 3 months ago

The difference between prepare_state and initialize is because Aer supports initialize natively as an instruction, so transpile doesn't need to do anything to it. Aer can short-circuit evaluation of initialize to simply set the statevector to the correct value (including relevant partial traces, etc), but prepare_state is a unitary operation that has a defined action on the $\lvert 0\rangle$ state, and undefined but still fixed actions on all other basis states, so in general, it can't. Obviously here, if you reset the qubits first, the action will be the same, but we don't have a peephole optimisation pass that combines these.

The point of prepare_state is that you can take the adjoint of it, so you can use it as a kernel to implement arbitrary $\lvert \psi\rangle \to \lvert \phi\rangle$ transitions by applying $U\phi U\psi^\dagger$ to a state.

I'm not sure what's causing peaks in the measured runtime - off the top of my head, I can't think of what it might be in the transpiler, though it's possible that those are garbage-collection spikes.

jfraxanet commented 3 months ago

I see, thank you so much for the quick response and the clarification! It's good to know, since using qc.prepare_state() in an optimization workflow can increase the time to a point in which it becomes difficult to get a converged result, but by switching to qc.initialize() it is easily solved!

jakelishman commented 2 months ago

Thanks! I'll close this issue now, but feel free to re-open if there's more to discuss.