Converting from Qiskit Runtime Programs

This tutorial will be a demonstation of converting your custom Qiskit Runtime Program into a Qiskit Serverless QiskitPattern.

If you were using Qiskit Runtime Programs before, your code probably looks similar to the following example:

"""A sample runtime program that submits random circuits for user-specified iterations."""

import random

from qiskit import transpile
from qiskit.circuit.random import random_circuit


def prepare_circuits(backend):
    circuit = random_circuit(
        num_qubits=5, depth=4, measure=True, seed=random.randint(0, 1000)
    )
    return transpile(circuit, backend)


def main(backend, user_messenger, **kwargs):
    """Main entry point of the program.

    Args:
        backend: Backend to submit the circuits to.
        user_messenger: Used to communicate with the program consumer.
        kwargs: User inputs.
    """
    iterations = kwargs.pop("iterations", 5)
    for it in range(iterations):
        qc = prepare_circuits(backend)
        result = backend.run(qc).result()
        user_messenger.publish({"iteration": it, "counts": result.get_counts()})

    return "Hello, World!"

All Qiskit Runtime Programs have a main method which accepts backend, user_messenger and **kwargs. This method is not required for Qiskit Serverless patterns.

Qiskit Serverless handles backends, logging, and input arguments a bit differently than Qiskit Runtime:

  • backend. For Qiskit Serverless programs you are not limited to single backend for a program. You can call any number of backends from single program. Since Backend.run is deprecated, we will be using Qiskit Primitives to do our calculation.

  • user_messenger were used in Qiskit Runtime Programs to facilitate retrieving logs from the program. Qiskit Serverless does not require passing such an object. Instead, all contents of stdout (e.g. print statements, logging messages) will be provided to the user via the Qiskit Serverless job handler.

  • **kwargs was a variable used to capture program inputs from the user. Users should now input their arguments to the ServerlessProvider.run method, and the arguments should be retrieved within the pattern using the get_arguments function from Qiskit Serverless.

  • To save the results of a pattern, the save_result function should be used. It accepts a python dictionary and can be accessed via the job handler.

Let’s use the guidelines above to transform the above Qiskit Runtime Program into a Qiskit Serverless QiskitPattern.

# migrated_pattern.py
"""A sample runtime pattern that submits random circuits for user-specified iterations."""

import random

from qiskit import transpile
from qiskit.circuit.random import random_circuit
from qiskit.primitives import Sampler

from qiskit_serverless import get_arguments, save_result


def prepare_circuits():
    circuit = random_circuit(
        num_qubits=5, depth=4, measure=True, seed=random.randint(0, 1000)
    )
    return transpile(circuit)


arguments = get_arguments()
iterations = arguments.get("iterations", 5)

for it in range(iterations):
    qc = prepare_circuits()
    result = Sampler.run(qc).result()
    print({"iteration": it, "dists": result.quasi_dists})

save_result({"result": "Hello, World!"})

Let’s save this code as ./src/migrated_pattern.py and execute it using the QiskitPattern class from the qiskit_serverless package.

[1]:
from qiskit_serverless import QiskitPattern

pattern = QiskitPattern(
    title="migrated-pattern", entrypoint="migrated_pattern.py", working_dir="./src/"
)
[2]:
from qiskit_serverless import ServerlessProvider
import os

serverless = ServerlessProvider(
    token=os.environ.get("GATEWAY_TOKEN", "awesome_token"),
    host=os.environ.get("GATEWAY_HOST", "http://localhost:8000"),
)
serverless
[2]:
<ServerlessProvider: gateway-provider>
[3]:
serverless.upload(pattern)
[3]:
'migrated-pattern'

While Qiskit Runtime programs required users to upload their program and call it in two separate steps, the QiskitPattern class allows users to send a job for remote execution in a single step.

[4]:
job = serverless.run("migrated-pattern", arguments={"iterations": 3})
job
[4]:
<Job | 0ae02e24-859a-43c1-a9e9-038dcb7dc296>
[5]:
job.result()
[5]:
{'result': 'Hello, World!'}
[6]:
print(job.logs())
OpenBLAS WARNING - could not determine the L2 cache size on this system, assuming 256k
OpenBLAS WARNING - could not determine the L2 cache size on this system, assuming 256k
{'iteration': 0, 'dists': [{0: 0.01953568379784, 1: 0.003434588839427, 2: 0.000962772320948, 3: 0.022007500316319, 4: 0.018613812117662, 5: 0.003272513622768, 6: 0.000917339944671, 7: 0.020968985795759, 8: 0.089342856396779, 9: 0.100647254570651, 10: 0.004403062114611, 11: 0.015707460288483, 12: 0.085126845839344, 13: 0.095897799438366, 14: 0.004195285498675, 15: 0.014966239097696, 16: 0.018613812117662, 17: 0.003272513622768, 18: 0.007093750939097, 19: 0.014792574801333, 20: 0.01953568379784, 21: 0.003434588839427, 22: 0.007445077580607, 23: 0.01552519505666, 24: 0.085126845839344, 25: 0.067651119863038, 26: 0.032441965074003, 27: 0.014966239097696, 28: 0.089342856396779, 29: 0.071001623840398, 30: 0.034048692844864, 31: 0.015707460288483}]}
{'iteration': 1, 'dists': [{0: 0.179254126288168, 1: 0.382666923582299, 4: 0.031208803971925, 5: 0.066623721595232, 16: 0.092444479011949, 17: 0.197348005974499, 20: 0.016094924471265, 21: 0.034359015104663}]}
{'iteration': 2, 'dists': [{0: 0.003593062771524, 2: 0.037032118667734, 4: 0.032043767985561, 6: 0.330261031899324, 8: 0.005805731220397, 10: 0.059837119799466, 12: 0.002779551551986, 14: 0.028647616104007, 16: 0.003593062771524, 18: 0.037032118667734, 20: 0.032043767985561, 22: 0.330261031899324, 24: 0.005805731220397, 26: 0.059837119799466, 28: 0.002779551551986, 30: 0.028647616104007}]}