Qiskit / qiskit-ibm-runtime

IBM Client for Qiskit Runtime
https://docs.quantum.ibm.com/api/qiskit-ibm-runtime
Apache License 2.0
149 stars 154 forks source link

Circuit parameters can't be bound by Runtime Primitives #809

Closed adekusar-drl closed 8 months ago

adekusar-drl commented 1 year ago

Describe the bug Runtime Estimator fails when a complex circuit that is made of instructions created from other circuit is executed. The same circuit runs perfectly well on the reference primitives locally. The script below reproduces the problem. It may look artificial, but a lot of code has been removed to make the script shorter. I do believe the same problem will occur with Sampler.

Steps to reproduce Add a TOKEN definition to the script and run it.

import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.primitives import Estimator
from qiskit.quantum_info import Pauli
from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator as RuntimeEstimator

def cloud_execute(qc):
    observable = Pauli("Z" * qc.num_qubits)

    service = QiskitRuntimeService(channel="ibm_quantum", token=TOKEN)
    backend_name = "ibmq_qasm_simulator"
    with Session(service=service, backend=backend_name) as session:
        estimator = RuntimeEstimator(session=session)
        result = estimator.run(qc, observable, np.random.random(qc.num_parameters)).result()
        print(result)

def local_execute(qc):
    observable = Pauli("Z" * qc.num_qubits)
    estimator = Estimator()
    result = estimator.run(qc, observable, np.random.random(qc.num_parameters)).result()
    print(result)

def create_layer(num_qubits, params):
    qc = QuantumCircuit(num_qubits, name="L")

    param_index = 0
    for qubit1 in range(0, num_qubits - 1, 2):
        qubit2 = qubit1 + 1
        unit_params = params[param_index: param_index + 1]
        add_unit(qc, qubit1, qubit2, unit_params)
        param_index += 1

    return qc.to_instruction()

def add_unit(qc: QuantumCircuit, qubit1, qubit2, params):
    u = QuantumCircuit(2, name="U")
    u.rz(params[0], 0)

    qc.compose(u.to_instruction(), [qubit1, qubit2], inplace=True)

num_qubits = 4
qc = QuantumCircuit(num_qubits)

num_params = 2
params = ParameterVector("u", num_params)
layer = create_layer(num_qubits, params)

qc.compose(layer, list(range(num_qubits)), inplace=True)

print(qc.decompose(reps=2))

local_execute(qc)
cloud_execute(qc)

Exception is raised qiskit.circuit.exceptions.CircuitError: \'Cannot bind parameters (u[1]) not present in the circuit.\'

Full log:

Traceback (most recent call last):
  File "... __decompose_bug.py", line 61, in <module>
    cloud_execute(qc)
  File "... __decompose_bug.py", line 18, in cloud_execute
    result = estimator.run(qc, observable, np.random.random(qc.num_parameters)).result()
  File "... lib/python3.8/site-packages/qiskit_ibm_runtime/runtime_job.py", line 226, in result
    raise RuntimeJobFailureError(
qiskit_ibm_runtime.exceptions.RuntimeJobFailureError: 'Unable to retrieve job result. Job cgv6cgj3sd207kpt424g has failed:\nTraceback (most recent call last):\n2023-04-18T09:43:44.024738294Z   File "/provider/server/main.py", line 90, in execute_program\n2023-04-18T09:43:44.024757792Z     starter.execute()\n2023-04-18T09:43:44.024778195Z   File "/provider/programruntime/program_starter_wrapper.py", line 97, in execute\n2023-04-18T09:43:44.024797212Z     raise ex\n2023-04-18T09:43:44.024815487Z   File "/provider/programruntime/program_starter_wrapper.py", line 84, in execute\n2023-04-18T09:43:44.024832275Z     final_result = self.main(backend, self.messenger, **self.user_params)\n2023-04-18T09:43:44.024849954Z   File "/code/program.py", line 1464, in main\n2023-04-18T09:43:44.024867719Z     result = estimator.run(\n2023-04-18T09:43:44.024885527Z   File "/code/program.py", line 270, in run\n2023-04-18T09:43:44.024904502Z     bound_circuits = [\n2023-04-18T09:43:44.024923817Z   File "/code/program.py", line 273, in <listcomp>\n2023-04-18T09:43:44.024940911Z     else transpiled_circuits[circuit_index].bind_parameters(p)\n2023-04-18T09:43:44.024959235Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/circuit/quantumcircuit.py", line 2735, in bind_parameters\n2023-04-18T09:43:44.024977216Z     return self.assign_parameters(values)\n2023-04-18T09:43:44.024993295Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/circuit/quantumcircuit.py", line 2687, in assign_parameters\n2023-04-18T09:43:44.025027333Z     raise CircuitError(\n2023-04-18T09:43:44.025043583Z qiskit.circuit.exceptions.CircuitError: \'Cannot bind parameters (u[1]) not present in the circuit.\'\n2023-04-18T09:43:44.071122732Z /pod-data/ CLOSE_WRITE,CLOSE terminated\n2023-04-18T09:43:44.077314378Z Termination marker file found. Kill process (7).\n'

Expected behavior No exception is thrown

Suggested solutions There's a workaround. Replace cloud_execute(qc) with cloud_execute(qc.decompose(reps=3) and the problem is solved, but it is inconvenient.

Additional Information

mberna commented 1 year ago

This seems to be an issue with the qpy serialization/deserialization.

This example constructs the same qc provided above in the original description of the issue, prints it, then serializes and deserializes it, and prints it again. The original circuit qc differs from new circuit new_qc: in the new circuit the param u[1] does not appear.

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector

def create_layer(num_qubits, params):
    qc = QuantumCircuit(num_qubits, name="L")

    param_index = 0
    for qubit1 in range(0, num_qubits - 1, 2):
        qubit2 = qubit1 + 1
        unit_params = params[param_index: param_index + 1]
        add_unit(qc, qubit1, qubit2, unit_params)
        param_index += 1

    return qc.to_instruction()

def add_unit(qc: QuantumCircuit, qubit1, qubit2, params):
    u = QuantumCircuit(2, name="U")
    u.rz(params[0], 0)

    qc.compose(u.to_instruction(), [qubit1, qubit2], inplace=True)

num_qubits = 4
qc = QuantumCircuit(num_qubits)

num_params = 2
params = ParameterVector("u", num_params)
layer = create_layer(num_qubits, params)

qc.compose(layer, list(range(num_qubits)), inplace=True)

# print original circuit
print(qc.decompose(reps=2))

# convert to qpy and back to QuantumCircuit
from qiskit import qpy
with open('temp.qpy', 'wb') as fd:
    qpy.dump(qc, fd)

with open('temp.qpy', 'rb') as fd:
    new_qc = qpy.load(fd)[0]

# print new circuit
print(new_qc.decompose(reps=2))

Output: qc:

     ┌──────────┐
q_0: ┤ Rz(u[0]) ├
     └──────────┘
q_1: ────────────
     ┌──────────┐
q_2: ┤ Rz(u[1]) ├
     └──────────┘
q_3: ────────────

new_qc:


     ┌──────────┐
q_0: ┤ Rz(u[0]) ├
     └──────────┘
q_1: ────────────
     ┌──────────┐
q_2: ┤ Rz(u[0]) ├
     └──────────┘
q_3: ────────────
fvarchon commented 1 year ago

Hi. @mberna In order to update the client, is there any news on that issue?

ElePT commented 1 year ago

Hi @fvarchon , this case actually hits a limitation of qpy that has an easy fix on the user side.

qpy relies on the operation name for serializing/deserializing. This allows it to only store the definition for custom gates once and then re-use it for all instances of the gate. In this examples, the add_unit method is hard-coding the name "U" on two units that are actually different, as one is parametrized with u[0], and the other one with u[1]. There is no way for qpy to currently check if the first "U" is different to the second "U", and this is why they are both deserialized as the first "definition" of "U". Making sure both layers have different names (u = QuantumCircuit(2, name=f"U_{id}")) will lead to the correct serialization:

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector

def create_layer(num_qubits, params):
    qc = QuantumCircuit(num_qubits, name="L")

    param_index = 0
    for qubit1 in range(0, num_qubits - 1, 2):
        qubit2 = qubit1 + 1
        unit_params = params[param_index: param_index + 1]
        add_unit(qc, qubit1, qubit2, unit_params, qubit1)
        param_index += 1

    return qc.to_instruction()

def add_unit(qc: QuantumCircuit, qubit1, qubit2, params, id):
    u = QuantumCircuit(2, name=f"U_{id}")
    u.rz(params[0], 0)

    qc.compose(u.to_instruction(), [qubit1, qubit2], inplace=True)

num_qubits = 4
qc = QuantumCircuit(num_qubits)

num_params = 2
params = ParameterVector("u", num_params)
layer = create_layer(num_qubits, params)

qc.compose(layer, list(range(num_qubits)), inplace=True)

# print original circuit
print(qc.decompose(reps=2))

# convert to qpy and back to QuantumCircuit
from qiskit import qpy
with open('temp.qpy', 'wb') as fd:
    qpy.dump(qc, fd)

with open('temp.qpy', 'rb') as fd:
    new_qc = qpy.load(fd)[0]

# print new circuit
print(new_qc.decompose(reps=2))

Output: qc:

     ┌──────────┐
q_0: ┤ Rz(u[0]) ├
     └──────────┘
q_1: ────────────
     ┌──────────┐
q_2: ┤ Rz(u[1]) ├
     └──────────┘
q_3: ────────────

qc2:

     ┌──────────┐
q_0: ┤ Rz(u[0]) ├
     └──────────┘
q_1: ────────────
     ┌──────────┐
q_2: ┤ Rz(u[1]) ├
     └──────────┘
q_3: ────────────

While I agree that using the gate name is clearly not "bullet-proof", it's the mechanism we currently have, and modifying it could have memory or runtime costs that need to be considered carefully. However, as you can see, it's easy to fix once the issue is known. Do you think this could be enough to help the client? (and @adekusar-drl)

adekusar-drl commented 1 year ago

@ElePT thanks for the update, I overcame the issue long time ago, honestly. I realized that it is better not use nested circuits/instructions and so on despite the fact they are very handy for complex circuits. But the issue is really annoying. The proposed solution is okay, but nevertheless the issue to be fixed. It is very common to debug on reference primitives and then move to cloud. And then fixing the issue becomes crucial when you queued for ages and then you get a nasty error in your circuit.

jyu00 commented 8 months ago

Closing since this is a duplicate of https://github.com/Qiskit/qiskit/issues/8941