Skip to content

qpy fails to serialize certain parametrized instructions  #10735

@ElePT

Description

@ElePT

Environment

  • Qiskit Terra version: 0.25.1
  • Python version: 3.9
  • Operating system: Mac OS

What is happening?

This issue about the IAE algorithm showed different results when running on the runtime Sampler vs Aer Sampler. After some investigation, the issue seems to be caused by the serialization of the multi-controlled RY gate that is used to build the state preparation circuit.

After discussing it with @Cryoris, we had the theory that the parameters were not properly stored, but I am not sure, because the printed before/after circuits look the same (see here circuit for the example presented below)


        ┌───┐         ┌────────────┐              ░ ┌─┐      
   q_0: ┤ H ├────■────┤ Ry(1.5635) ├──────■───────░─┤M├──────
        ├───┤    │    └─────┬──────┘┌─────┴─────┐ ░ └╥┘┌─┐   
   q_1: ┤ H ├────■──────────■───────┤ Ry(3.127) ├─░──╫─┤M├───
        ├───┤┌───┴───┐      │       └─────┬─────┘ ░  ║ └╥┘┌─┐
   q_2: ┤ H ├┤ Ry(0) ├──────■─────────────■───────░──╫──╫─┤M├
        └───┘└───────┘                            ░  ║  ║ └╥┘
meas: 3/═════════════════════════════════════════════╩══╩══╩═
                                                     0  1  2 

[Edit] After further inspection, the parameters are stored but the circuit instructions are not using them properly.
Before serializing the circuit shown in the example, circ.data contains:


[CircuitInstruction(operation=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 0),), clbits=()), CircuitInstruction(operation=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 1),), clbits=()), CircuitInstruction(operation=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 2),), clbits=()), CircuitInstruction(operation=Instruction(name='ccry', num_qubits=3, num_clbits=0, params=[0.0]), qubits=(Qubit(QuantumRegister(3, 'q'), 0), Qubit(QuantumRegister(3, 'q'), 1), Qubit(QuantumRegister(3, 'q'), 2)), clbits=()), CircuitInstruction(operation=Instruction(name='ccry', num_qubits=3, num_clbits=0, params=[1.563487]), qubits=(Qubit(QuantumRegister(3, 'q'), 1), Qubit(QuantumRegister(3, 'q'), 2), Qubit(QuantumRegister(3, 'q'), 0)), clbits=()), CircuitInstruction(operation=Instruction(name='ccry', num_qubits=3, num_clbits=0, params=[3.126974]), qubits=(Qubit(QuantumRegister(3, 'q'), 2), Qubit(QuantumRegister(3, 'q'), 0), Qubit(QuantumRegister(3, 'q'), 1)), clbits=()), CircuitInstruction(operation=Instruction(name='barrier', num_qubits=3, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 0), Qubit(QuantumRegister(3, 'q'), 1), Qubit(QuantumRegister(3, 'q'), 2)), clbits=()), CircuitInstruction(operation=Instruction(name='measure', num_qubits=1, num_clbits=1, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 0),), clbits=(Clbit(ClassicalRegister(3, 'meas'), 0),)), CircuitInstruction(operation=Instruction(name='measure', num_qubits=1, num_clbits=1, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 1),), clbits=(Clbit(ClassicalRegister(3, 'meas'), 1),)), CircuitInstruction(operation=Instruction(name='measure', num_qubits=1, num_clbits=1, params=[]), qubits=(Qubit(QuantumRegister(3, 'q'), 2),), clbits=(Clbit(ClassicalRegister(3, 'meas'), 2),))]

Where, before serializing:

circ.data[4].operation.definition.draw()
Out[11]: 
                                                    
control_0: ─────────────────■────────────────────■──
                            │                    │  
control_1: ─────────────────■────────────────────■──
           ┌─────────────┐┌─┴─┐┌──────────────┐┌─┴─┐
   target: ┤ Ry(0.78174) ├┤ X ├┤ Ry(-0.78174) ├┤ X ├
           └─────────────┘└───┘└──────────────┘└───┘

circ.data[5].operation.definition.draw()
Out[12]: 
                                                  
control_0: ────────────────■───────────────────■──
                           │                   │  
control_1: ────────────────■───────────────────■──
           ┌────────────┐┌─┴─┐┌─────────────┐┌─┴─┐
   target: ┤ Ry(1.5635) ├┤ X ├┤ Ry(-1.5635) ├┤ X ├
           └────────────┘└───┘└─────────────┘└───┘

And after serializing:

circ2[0].data[4].operation.definition.draw()
Out[5]: 
                                       
control_0: ───────────■─────────────■──
                      │             │  
control_1: ───────────■─────────────■──
           ┌───────┐┌─┴─┐┌───────┐┌─┴─┐
   target: ┤ Ry(0) ├┤ X ├┤ Ry(0) ├┤ X ├
           └───────┘└───┘└───────┘└───┘
circ2[0].data[5].operation.definition.draw()
Out[6]: 
                                       
control_0: ───────────■─────────────■──
                      │             │  
control_1: ───────────■─────────────■──
           ┌───────┐┌─┴─┐┌───────┐┌─┴─┐
   target: ┤ Ry(0) ├┤ X ├┤ Ry(0) ├┤ X ├
           └───────┘└───┘└───────┘└───┘

So it looks like qpy is only reading the initial parameter (0) and applying it to all gates.

How can we reproduce the issue?

Let's say we have a circuit with multi-controlled RY gates (the issue is more evident the larger the amount of gates, but I wanted to keep the example small):

from qiskit import qpy
from qiskit import QuantumCircuit
from qiskit.circuit.library import RYGate
from qiskit import transpile

circ = QuantumCircuit(3)

circ.h(circ.qubits)
for i in range(3):
    c2ry = RYGate(i*1.563487).control(2)
    circ.append(c2ry, [i % 3, (i+1) % 3, (i+2) % 3])

# circ = transpile(circ, basis_gates=['sx', 'x', 'rz', 'cx'])
circ.measure_all()

Let's now dump and load the circuit again:

with open('circuit.qpy', 'wb') as fd:
    qpy.dump(circ, fd)

with open('circuit.qpy', 'rb') as fd:
    circ2 = qpy.load(fd)

And run an exact simulation using the reference Sampler:

from qiskit.primitives import Sampler as RefSampler

print("Before serialization")
print("--------------------")
ref_sampler = RefSampler(options={"shots": 2000, "seed": 10})
result = ref_sampler.run(circ).result()
print("ref", result.quasi_dists)

print("\nAfter serialization")
print("--------------------")
# primitives cache circuits -> reusing primitive changes the result
ref_sampler = RefSampler(options={"shots": 2000, "seed": 10})
result = ref_sampler.run(circ2).result()
print("ref", result.quasi_dists)

(I am not re-using the primitive instance because it caches the circuits).
The results should be identical, and yet, they are not:

Before serialization
--------------------
ref [{0: 0.129, 1: 0.1335, 2: 0.1185, 3: 0.127, 4: 0.1245, 5: 0.249, 7: 0.1185}]

After serialization
--------------------
ref [{0: 0.129, 1: 0.1335, 2: 0.1185, 3: 0.127, 4: 0.1245, 5: 0.116, 6: 0.118, 7: 0.1335}]

Similarly, if we try the Aer Sampler:

from qiskit_aer.primitives import Sampler as AerSampler

print("Before serialization")
print("--------------------")
aer_sampler = AerSampler(run_options={"shots": 2000, "seed": 10})
result = aer_sampler.run(circ).result()
print("aer", result.quasi_dists)

print("\nAfter serialization")
print("--------------------")
# primitives cache circuits -> reusing primitive changes the result
aer_sampler = AerSampler(run_options={"shots": 2000, "seed": 10})
result = aer_sampler.run(circ2).result()
print("aer", result.quasi_dists)
Before serialization
--------------------
aer [{5: 0.245, 4: 0.124, 1: 0.123, 0: 0.124, 2: 0.1245, 7: 0.142, 3: 0.1175}]

After serialization
--------------------
aer [{5: 0.1335, 6: 0.114, 4: 0.124, 1: 0.123, 0: 0.124, 2: 0.1245, 7: 0.1395, 3: 0.1175}]

If we configure the runtime sampler to match the Aer Sampler settings, we get that in both cases the runtime results match the "after serialization" aer results:

from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options

service = QiskitRuntimeService(channel='ibm_quantum')
backend = service.backend("ibmq_qasm_simulator")
options = Options()
options.execution.shots = 2000
options.optimization_level = 0
options.resilience_level = 0
options.simulator.seed_simulator = 10

print("Before serialization")
print("--------------------")
runtime_sampler = Sampler(backend=backend, options=options)
result = runtime_sampler.run(circ).result()
print("runtime", result.quasi_dists)

print("\nAfter serialization")
print("--------------------")
# primitives cache circuits -> reusing primitive changes the result
runtime_sampler = Sampler(backend=backend, options=options)
result = runtime_sampler.run(circ2).result()
print("runtime", result.quasi_dists)
Before serialization
--------------------
runtime [{5: 0.1335, 1: 0.123, 2: 0.1245, 6: 0.114, 4: 0.124, 0: 0.124, 3: 0.1175, 7: 0.1395}]

After serialization
--------------------
runtime [{5: 0.1335, 1: 0.123, 2: 0.1245, 6: 0.114, 4: 0.124, 0: 0.124, 3: 0.1175, 7: 0.1395}]

Finally, if we transpile the circuit locally (uncommenting the line in the first snippet), all results match the "before serialization" aer/reference ones (which is the expected outcome in any case).

What should happen?

Serializing the circuit should not change the result of the sampler.

Any suggestions?

See edit on top.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmod: qpyRelated to QPY serialization

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions