-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Description
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:
-
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 userun()
for local simulation, small remote simulations, and typical transpilation.run_async()
can be used for device runs or large remote transpilation jobs. -
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:
- Qobj -> moves into the provider and is the role of the provider to handle serialization
- Results class is removed and it is the role of the provider to return the service object (circuit, counts, etc)
- Path to removing execuite and encouraging the use the service
backend.run(circuits)
- Speed up the local simulator but doing less work to make it look like a remote service
- Also removes the assembler