Qiskit / qiskit-ibm-runtime

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

Using pulse gates with Runtime #655

Closed cyberteej closed 1 year ago

cyberteej commented 1 year ago

Describe the bug Hi all, it seems to me that using custom pulse gates in conjunction with IBM Runtime is a no-go at the moment.

Steps to reproduce If I run this script, I get sensible results.

from qiskit.quantum_info import SparsePauliOp
from qiskit import QuantumCircuit

service = QiskitRuntimeService()
backend = service.backend('ibm_oslo')
z_op = SparsePauliOp('Z')

circ = QuantumCircuit(1,1)
circ.h(0)
circ.measure(0,0)

with Session(service=service, backend=backend) as session:
    estimator = Estimator(session=session)
    res = estimator.run(circ, z_op)

print(res.result())

On the other hand, if I run this code:

from qiskit_ibm_runtime import QiskitRuntimeService, Estimator, Session
from qiskit.pulse import Gaussian
from qiskit.quantum_info import SparsePauliOp
from qiskit import QuantumCircuit
from qiskit.circuit import Gate

service = QiskitRuntimeService()
backend = service.backend('ibm_oslo')
z_op = SparsePauliOp('Z')

circ = QuantumCircuit(1,1)
circ.h(0)
custom_gate = Gate('custom_gate', 1, [])
circ.append(custom_gate, [0])
circ.measure(0,0)

with pulse.build(backend=backend) as sched:
    pulse.play(Gaussian(duration=64, amp=0.2, sigma=8), pulse.drive_channel(0))

circ.add_calibration('custom_gate', [0], sched, [])

with Session(service=service, backend=backend) as session:
    estimator = Estimator(session=session)
    res = estimator.run(circ, z_op)

print(res.result())

I get an error which boils down to TypeError: __new__() missing 3 required positional arguments: 'duration', 'amp', and 'sigma'.

Expected behavior Both of these pieces of code work if you simply run them directly on a backend, but one fails if you use Runtime. I would've expected both to work

Suggested solutions 🤷‍♂️

Additional Information The same is true for Sampler

jyu00 commented 1 year ago

This looks like an issue with qpy that was fixed in terra but not picked up by ibm-runtime. For example, if I do

import io
from qiskit import qpy

with io.BytesIO() as buff:
    qpy.dump(circ, buff)    # Using circ from earlier example
    buff.seek(0)
    restored = qpy.load(buff)
    restored[0].copy()    # Used by the estimator which raised the exception 

The code above works, but if I switch to ibm-runtime's qpy this fails with

TypeError: __new__() missing 3 required positional arguments: 'duration', 'amp', and 'sigma'
wshanks commented 1 year ago

I don't get the __new__() error. I get the behavior in #657. I am not sure why the behavior is different. As a workaround, you can try converting your pulses to Waveform so replacing Gaussian(duration=64, amp=0.2, sigma=8) with Gaussian(duration=64, amp=0.2, sigma=8).get_waveform() because I think it is only parametric pulses that are broken rather than all pulse gates.

Edit: I do get the __new__() error if I use the example code given in this issue. It seems like when there are multiple pulse gates defined on a qubit you can get a different error to trigger first, but the underlying cause (qpy mismatch) is the same.

Edit: Another workaround is to overwrite the _pulse_type attribute on the pulses to something that is not a builtin pulse name (Gaussian, GaussianSquare, Drag, Constant).

I tried both workarounds and ended up getting qiskit.exceptions.QiskitError: 'Cannot apply Operation: custom_gate'. I tried a different job with pulse gates and it worked, so I think these workarounds work and there might be another issue with the example, which I guess is still pulse gate specific.

cyberteej commented 1 year ago

This looks like an issue with qpy that was fixed in terra but not picked up by ibm-runtime. For example, if I do

import io
from qiskit import qpy

with io.BytesIO() as buff:
    qpy.dump(circ, buff)    # Using circ from earlier example
    buff.seek(0)
    restored = qpy.load(buff)
    restored[0].copy()    # Used by the estimator which raised the exception 

The code above works, but if I switch to ibm-runtime's qpy this fails with

TypeError: __new__() missing 3 required positional arguments: 'duration', 'amp', and 'sigma'

Could you clarify what you mean by the code above works? If I replace the circuit in the estimator with new_circ as defined by restored[0].copy(), I get the same __new__() error (qiskit 0.22.3, runtime 0.8.0).

wshanks commented 1 year ago

Could you clarify what you mean by the code above works?

Here "works" just means that the code example does not produce the error locally (without submitting to the runtime), not that it produces a circuit that can be submitted to the runtime. For comparison, this code does reproduce the error locally:

import json

from qiskit import QuantumCircuit, assemble, pulse
from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder

with pulse.builder.build(name="x") as sched:
    pulse.play(pulse.Gaussian(100, 0.8, 10), pulse.DriveChannel(0))

qc = QuantumCircuit(1)
qc.x(0)
qc.add_calibration("x", (0,), sched)

dumped = json.dumps(qc, cls=RuntimeEncoder)
loaded = json.loads(dumped, cls=RuntimeDecoder)

loaded.copy()

The difference is that this code uses the runtime's version of qpy to dump and load rather than terra's version.

wshanks commented 1 year ago

Here is a modified version of your example that gets past the pulse problem (an illustration of my second edited workaround above):

from qiskit_ibm_runtime import QiskitRuntimeService, Estimator, Session
from qiskit.pulse import Gaussian
from qiskit.quantum_info import SparsePauliOp
from qiskit import QuantumCircuit
from qiskit.circuit import Gate

service = QiskitRuntimeService()
backend = service.backend('ibm_oslo')
z_op = SparsePauliOp('Z')

circ = QuantumCircuit(1,1)
circ.h(0)
custom_gate = Gate('custom_gate', 1, [])
circ.append(custom_gate, [0])
circ.measure(0,0)

gauss = Gaussian(duration=64, amp=0.2, sigma=8)
gauss._pulse_type = "custom_gaussian"
with pulse.build(backend=backend) as sched:
    pulse.play(gauss, pulse.drive_channel(0))

circ.add_calibration('custom_gate', [0], sched, [])

with Session(service=service, backend=backend) as session:
    estimator = Estimator(session=session)
    res = estimator.run(circ, z_op)

print(res.result())

For me, this gives:

2022-12-16T15:06:27.524698748Z     pass_.run(FencedDAGCircuit(dag))
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/transpiler/passes/optimization/commutation_analysis.py", line 75, in run
2022-12-16T15:06:27.524698748Z     does_commute = self.comm_checker.commute(
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/circuit/commutation_checker.py", line 135, in commute
2022-12-16T15:06:27.524698748Z     operator_1 = Operator(op1, input_dims=(2,) * len(qarg1), output_dims=(2,) * len(qarg1))
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/quantum_info/operators/operator.py", line 85, in __init__
2022-12-16T15:06:27.524698748Z     self._data = self._init_instruction(data).data
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/quantum_info/operators/operator.py", line 520, in _init_instruction
2022-12-16T15:06:27.524698748Z     op._append_instruction(instruction)
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/quantum_info/operators/operator.py", line 564, in _append_instruction
2022-12-16T15:06:27.524698748Z     raise QiskitError(f"Cannot apply Operation: {obj.name}")
2022-12-16T15:06:27.524698748Z qiskit.exceptions.QiskitError: 'Cannot apply Operation: custom_gate'
Pauliver90 commented 1 year ago

Here is a modified version of your example that gets past the pulse problem (an illustration of my second edited workaround above):

from qiskit_ibm_runtime import QiskitRuntimeService, Estimator, Session
from qiskit.pulse import Gaussian
from qiskit.quantum_info import SparsePauliOp
from qiskit import QuantumCircuit
from qiskit.circuit import Gate

service = QiskitRuntimeService()
backend = service.backend('ibm_oslo')
z_op = SparsePauliOp('Z')

circ = QuantumCircuit(1,1)
circ.h(0)
custom_gate = Gate('custom_gate', 1, [])
circ.append(custom_gate, [0])
circ.measure(0,0)

gauss = Gaussian(duration=64, amp=0.2, sigma=8)
gauss._pulse_type = "custom_gaussian"
with pulse.build(backend=backend) as sched:
    pulse.play(gauss, pulse.drive_channel(0))

circ.add_calibration('custom_gate', [0], sched, [])

with Session(service=service, backend=backend) as session:
    estimator = Estimator(session=session)
    res = estimator.run(circ, z_op)

print(res.result())

For me, this gives:

2022-12-16T15:06:27.524698748Z     pass_.run(FencedDAGCircuit(dag))
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/transpiler/passes/optimization/commutation_analysis.py", line 75, in run
2022-12-16T15:06:27.524698748Z     does_commute = self.comm_checker.commute(
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/circuit/commutation_checker.py", line 135, in commute
2022-12-16T15:06:27.524698748Z     operator_1 = Operator(op1, input_dims=(2,) * len(qarg1), output_dims=(2,) * len(qarg1))
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/quantum_info/operators/operator.py", line 85, in __init__
2022-12-16T15:06:27.524698748Z     self._data = self._init_instruction(data).data
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/quantum_info/operators/operator.py", line 520, in _init_instruction
2022-12-16T15:06:27.524698748Z     op._append_instruction(instruction)
2022-12-16T15:06:27.524698748Z   File "/opt/app-root/lib64/python3.9/site-packages/qiskit/quantum_info/operators/operator.py", line 564, in _append_instruction
2022-12-16T15:06:27.524698748Z     raise QiskitError(f"Cannot apply Operation: {obj.name}")
2022-12-16T15:06:27.524698748Z qiskit.exceptions.QiskitError: 'Cannot apply Operation: custom_gate'

I tried my own version of the waveform approach you mentioned and it gave me a different error, not sure why. The error says trying to find more "in" edges ? I don't know, nothing I try seems to work.

from qiskit import IBMQ
IBMQ.load_account()
provider= IBMQ.get_provider(hub= "provider here")
import qiskit
from qiskit_ibm_runtime import QiskitRuntimeService, Session, Options
from qiskit_ibm_runtime import Estimator as my_runtime_estimator
service = QiskitRuntimeService()
real_backend_manila= provider.get_backend('ibmq_manila')
service_real_backend_waveform_manila = service.backend('ibmq_manila')

from qiskit import pulse
from qiskit.circuit import Gate
from qiskit import QuantumCircuit
from qiskit.circuit.library import standard_gates
from qiskit.pulse.library import Gaussian, Drag

pulse_x_gate = Gate(name='pulse_x_gate', label='p_x_g', num_qubits=2, params=[])

qc_pulse_gate_circuit_waveform_manila = QuantumCircuit(2, 0)
qc_pulse_gate_circuit_waveform_manila.append(pulse_x_gate,[0,1])

sched_waveform_x_manila = Gaussian(duration=64, amp=0.2, sigma=8).get_waveform()

with pulse.build(real_backend_manila, name='custom_pulse_sched_waveform_manila') as custom_pulse_schedule_waveform_manila:
    pulse.play(sched_waveform_x_manila, pulse.drive_channel(0))

qc_pulse_gate_circuit_waveform_manila.add_calibration('pulse_x_gate', [0,1],custom_pulse_schedule_waveform_manila)

options_manila = Options()
options_manila.transpilation.skip_transpilation = True

from qiskit.quantum_info import SparsePauliOp
z_op = SparsePauliOp('ZZ')

with Session(service=service, backend=service_real_backend_waveform_manila) as session:
    estimator = my_runtime_estimator(session=session,options=options_manila)
    res = estimator.run(qc_pulse_gate_circuit_waveform_manila, z_op)

print(res.result())

This gives (part of the long error message):

Started server process [7]
Waiting for application startup.
Application startup complete.
--- Logging error ---
raceback (most recent call last):
File "/provider/programruntime/program_starter_wrapper.py", line 88, in execute
final_result = self.main(backend, self.messenger, **self.user_params)
File "/code/./program.py", line 1399, in main
result = estimator.run(
File "/code/./program.py", line 209, in run
transpiled_circuits = self.transpiled_circuits
File "/code/./program.py", line 392, in transpiled_circuits
self._split_transpile()
File "/code/./program.py", line 435, in _split_transpile
self._transpiled_circuits += self._combine(
File "/code/./program.py", line 449, in _combine
transpiled_circuit.compose(diff_circuit, inplace=True)
File "/opt/app-root/lib64/python3.9/site-packages/qiskit/circuit/quantumcircuit.py", line 931, in compose
raise CircuitError(
qiskit.circuit.exceptions.CircuitError: "Trying to compose with another QuantumCircuit which has more 'in' edges."
wshanks commented 1 year ago

@Pauliver90 I haven't tried to get your example to work, but it seems like the problem is related to transpilation. You might try removing your skip_transpilation option or keep it but do an explicit qiskit.transpile() call on qc_pulse_gate_circuit_waveform_manila before passing it to estimator.run(). My guess is that the estimator program is composing your input circuit with a circuit for the expectation value measurement that has been transpiled for the full number of qubits in the backend. The error seems to be related to the number of qubits not matching between the two circuits.

kt474 commented 1 year ago

@tylerjones1 are you still having this issue? We just updated qpy on the server side

cyberteej commented 1 year ago

Hi @kt474, this issue seems resolved, my snippet runs into an unrelated error. Thanks!