Skip to content

Qiskit Services API #4105

@jaygambetta

Description

@jaygambetta

With the introduction of more IQX services, such as transpiler-as-a-service, we are moving towards a model where there are local and remote executions of the same task (e.g. simulation, transpilation), and additionally remote executions can be low- or high- latency (e.g. simulator vs. device run).

In order to make it easy for users to switch between different types of executions, we would like to create a uniform API between local vs. remote executions, and additionally accomodate both low- and high-latency executions in a natural way.

The proposal is as follows:

  1. Introduce two methods for explicit sync and async runs, and allow the user to choose one that suits their specific run. run() waits for results and returns the result. run_async() returns a job handle that can be later queried for result. One would use run() for local simulation, small remote simulations, and typical transpilation. run_async() can be used for device runs or large remote transpilation jobs.

  2. Remove Result as wrapper of a job result, which has to be queried again for the things you care about. Instead, directly return the thing that the run was intended for, be it counts, memory, unitary, statevector, or a circuit.

So a generic service API will look like:

from qiskit.providers.ibmq import IBMQProvider
provider = IBMQProvider(hub, group, project)
service = provider.service.service_instance
service.setup(service_config)

# run option 1
service_job = service.run_async(service_input)
service_output = service_job.result()

# run option 2
service_output = service.run(service_input)

# run option 3 (for future)
service_stream = service.run_stream(service_input)

Concrete examples:

# local simulation
counts = qasm_sim.run(circuit)
statevector = statevector_sim.run(circuit)
unitary = unitary_sim.run(circuit)

# remote device
backend.setup(run_config)
job = backend.run_async(circuit)  # JobResult
counts = job.result()

# remote device but want per-shot readouts
run_config.memory=True
backend.setup(run_config)
job = backend.run_async(circuit)  # JobResult
memory = job.result()

# local passmanager
from qiskit.transpiler.preset_pass_managers import local_pass_manager
pm = local_pass_manager(passmanager_config)
compiled_circuit = local_pass_manager.run(circuit)

# remote passmanager
pm = remote_pass_manager()
pm.setup(passmanager_config)  # sets up remote service
compiled_circuit = remote_pass_manager.run(circuit)
# or if there are many large circuits
job = remote_pass_manager.run_async(circuit)  # PassManagerJob
compiled_circuit = job.result()

Questions:

Backwards compatibility

We need to keep a deprecation period for the old style to still work. This could be like:

job = backend.run(service_input=qobj)  # +warning, instruct to use job = backend.run_async(service_input=circuit) or result = backend.run(service_input=circuit)
depending on operational.

execute is edited to default to use run_async() so that it works in the same way but add a message that it will be depreicated.

execute(circuit, backend) # deprecate warining and when backend.run(circuit) exists. will default to .run_async() during the deprecation period

"Setup"

Can we do the setup in a Pythonic way for remote services as well? i.e. instead of

pm = remote_pass_manager()
pm.setup(passmanager_config)  # sets up remote service

doing

pm = remote_pass_manager(passmanager_config)

and have the config persist across multiple remote runs.

This would be be my preference as this allows the user to see what the service provider can set up

Configurations

In the above, the setup is done with a configuration, currently in the form of PassManagerConfig or RunConfig objects. Should we remove these classes and just have pm.setup() and backend.setup() accept these kwargs directly?

class PassManagerConfig:
    """Pass Manager Configuration.
    """

    def __init__(self,
                 initial_layout=None,
                 basis_gates=None,
                 coupling_map=None,
                 layout_method=None,
                 routing_method=None,
                 backend_properties=None,
                 seed_transpiler=None):

class RunConfig:
    """Run Configuration.
    """

    def __init__(self,
                 shots=None,
                 memory=None,
                 parameter_binds=None,
                 max_credits=None,
                 seed_simulator=None):

objects removed:

  1. Qobj -> moves into the provider and is the role of the provider to handle serialization
  2. Results class is removed and it is the role of the provider to return the service object (circuit, counts, etc)
  3. Path to removing execuite and encouraging the use the service
backend.run(circuits)
  1. Speed up the local simulator but doing less work to make it look like a remote service
  2. Also removes the assembler

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions