Skip to content

Commit

Permalink
Remove backend cache from QiskitRuntimeService (#1732)
Browse files Browse the repository at this point in the history
* wip, remove cache

* fix test failure

* add release note

* remove early return

* remove discover_backends

* cache configuration

* remove upgrade note

* mypy

* add release note

---------

Co-authored-by: Jessie Yu <[email protected]>
  • Loading branch information
nkanazawa1989 and jyu00 authored Jun 11, 2024
1 parent 00d14da commit e0aed70
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 132 deletions.
7 changes: 6 additions & 1 deletion qiskit_ibm_runtime/api/clients/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(
**params.connection_parameters(),
)
self._api = Runtime(self._session)
self._configuration_registry: Dict[str, Dict[str, Any]] = {}

def program_run(
self,
Expand Down Expand Up @@ -301,7 +302,11 @@ def backend_configuration(self, backend_name: str) -> Dict[str, Any]:
Returns:
Backend configuration.
"""
return self._api.backend(backend_name).configuration()
if backend_name not in self._configuration_registry:
self._configuration_registry[backend_name] = self._api.backend(
backend_name
).configuration()
return self._configuration_registry[backend_name].copy()

def backend_status(self, backend_name: str) -> Dict[str, Any]:
"""Return the status of the IBM backend.
Expand Down
207 changes: 79 additions & 128 deletions qiskit_ibm_runtime/qiskit_runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
from qiskit.providers.backend import BackendV2 as Backend
from qiskit.providers.exceptions import QiskitBackendNotFoundError
from qiskit.providers.providerutils import filter_backends
from qiskit.providers.models import (
PulseBackendConfiguration,
QasmBackendConfiguration,
)

from qiskit_ibm_runtime import ibm_backend
from .proxies import ProxyConfiguration
Expand All @@ -47,7 +43,7 @@
from .utils.result_decoder import ResultDecoder
from .runtime_job import RuntimeJob
from .runtime_job_v2 import RuntimeJobV2
from .utils import RuntimeDecoder, RuntimeEncoder, to_python_identifier
from .utils import RuntimeDecoder, RuntimeEncoder
from .api.client_parameters import ClientParameters
from .runtime_options import RuntimeOptions
from .ibm_backend import IBMBackend
Expand Down Expand Up @@ -146,16 +142,12 @@ def __init__(

self._channel_strategy = channel_strategy or self._account.channel_strategy
self._channel = self._account.channel
self._backends: Dict[str, "ibm_backend.IBMBackend"] = {}
self._backend_configs: Dict[str, Any] = {}
self._backend_allowed_list: List[str] = []

if self._channel == "ibm_cloud":
self._api_client = RuntimeClient(self._client_params)
# TODO: We can make the backend discovery lazy
self._backends = self._discover_cloud_backends()
QiskitRuntimeService.global_service = self
self._backend_allowed_list = self._discover_cloud_backends()
self._validate_channel_strategy()
return
else:
auth_client = self._authenticate_ibm_quantum_account(self._client_params)
# Update client parameters to use authenticated values.
Expand All @@ -170,21 +162,15 @@ def __init__(
"Please check if the service is in maintenance mode "
"https://docs.quantum.ibm.com/announcements/service-alerts."
)

for hgp in self._hgps.values():
for backend_name in hgp.backends:
if backend_name not in self._backends:
self._backends[backend_name] = None
self._backend_allowed_list = sorted(
set(sum([hgp.backends for hgp in self._hgps.values()], []))
)
self._current_instance = self._account.instance
if not self._current_instance:
self._current_instance = self._get_hgp().name
logger.info("Default instance: %s", self._current_instance)
QiskitRuntimeService.global_service = self

# TODO - it'd be nice to allow some kind of autocomplete, but `service.ibmq_foo`
# just seems wrong since backends are not runtime service instances.
# self._discover_backends()

def _discover_account(
self,
token: Optional[str] = None,
Expand Down Expand Up @@ -289,27 +275,13 @@ def _validate_channel_strategy(self) -> None:
"To use this instance, set channel_strategy='q-ctrl'."
)

def _discover_cloud_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]:
def _discover_cloud_backends(self) -> List[str]:
"""Return the remote backends available for this service instance.
Returns:
A dict of the remote backend instances, keyed by backend name.
A list of the remote backend names.
"""
ret = OrderedDict() # type: ignore[var-annotated]
backends_list = self._api_client.list_backends(channel_strategy=self._channel_strategy)
for backend_name in backends_list:
raw_config = self._api_client.backend_configuration(backend_name=backend_name)
config = configuration_from_server_data(
raw_config=raw_config, instance=self._account.instance
)
if not config:
continue
ret[config.backend_name] = ibm_backend.IBMBackend(
configuration=config,
service=self,
api_client=self._api_client,
)
return ret
return self._api_client.list_backends(channel_strategy=self._channel_strategy)

def _resolve_crn(self, account: Account) -> None:
account.resolve_crn()
Expand Down Expand Up @@ -477,15 +449,6 @@ def _get_hgp(

raise QiskitBackendNotFoundError(error_message)

def _discover_backends(self) -> None:
"""Discovers the remote backends for this account, if not already known."""
for backend in self._backends.values():
backend_name = to_python_identifier(backend.name)
# Append _ if duplicate
while backend_name in self.__dict__:
backend_name += "_"
setattr(self, backend_name, backend)

# pylint: disable=arguments-differ
def backends(
self,
Expand Down Expand Up @@ -551,57 +514,41 @@ def backends(
"Currently fractional_gates and dynamic_circuits feature cannot be "
"simulutaneously enabled. Consider disabling one or the other."
)

backends: List[IBMBackend] = []
instance_filter = instance if instance else self._account.instance
if self._channel == "ibm_quantum":
instance_filter = instance if instance else self._account.instance
if name:
if name not in self._backends:
if name not in self._backend_allowed_list:
raise QiskitBackendNotFoundError("No backend matches the criteria.")
if not self._backends[name] or instance_filter != self._backends[name]._instance:
self._set_backend_config(name)
self._backends[name] = self._create_backend_obj(
self._backend_configs[name],
instance_filter,
)
if self._backends[name]:
backends.append(self._backends[name])
backend_names = [name]
hgp = instance_filter
elif instance_filter:
hgp = self._get_hgp(instance=instance_filter)
for backend_name in hgp.backends:
if (
not self._backends[backend_name]
or instance_filter != self._backends[backend_name]._instance
):
self._set_backend_config(backend_name, instance_filter)
self._backends[backend_name] = self._create_backend_obj(
self._backend_configs[backend_name], instance_filter
)
if self._backends[backend_name]:
backends.append(self._backends[backend_name])
backend_names = self._get_hgp(instance=instance_filter).backends
hgp = instance_filter
else:
for backend_name, backend_config in self._backends.items():
if not backend_config:
self._set_backend_config(backend_name)
self._backends[backend_name] = self._create_backend_obj(
self._backend_configs[backend_name]
)
if self._backends[backend_name]:
backends.append(self._backends[backend_name])

backend_names = self._backend_allowed_list
hgp = None
for backend_name in backend_names:
if backend := self._create_backend_obj(backend_name, instance=hgp):
backends.append(backend)
else:
if instance:
raise IBMInputValueError(
"The 'instance' keyword is only supported for ``ibm_quantum`` runtime."
)
backends = list(self._backends.values())
for backend_name in self._backend_allowed_list:
if backend := self._create_backend_obj(
backend_name, instance=self._account.instance
):
backends.append(backend)

if name:
kwargs["backend_name"] = name
if min_num_qubits:
backends = list(
filter(lambda b: b.configuration().n_qubits >= min_num_qubits, backends)
)

if dynamic_circuits is not None:
backends = list(
filter(
Expand All @@ -616,58 +563,60 @@ def backends(
backend.options.use_fractional_gates = use_fractional_gates
return filter_backends(backends, filters=filters, **kwargs)

def _set_backend_config(self, backend_name: str, instance: Optional[str] = None) -> None:
"""Retrieve backend configuration and add to backend_configs.
Args:
backend_name: backend name that will be returned.
instance: the current h/g/p.
"""
if backend_name not in self._backend_configs:
raw_config = self._api_client.backend_configuration(backend_name)
config = configuration_from_server_data(raw_config=raw_config, instance=instance)
self._backend_configs[backend_name] = config

def _create_backend_obj(
self,
config: Union[QasmBackendConfiguration, PulseBackendConfiguration],
backend_name: str,
instance: Optional[str] = None,
) -> IBMBackend:
"""Given a backend configuration return the backend object.
Args:
config: backend configuration.
backend_name: Name of backend to instantiate.
instance: the current h/g/p.
Returns:
A backend object.
Raises:
QiskitBackendNotFoundError: if the backend is not in the hgp passed in.
"""
if config:
if not instance:
for hgp in list(self._hgps.values()):
if config.backend_name in hgp.backends:
instance = to_instance_format(hgp._hub, hgp._group, hgp._project)
break

elif config.backend_name not in self._get_hgp(instance=instance).backends:
hgps_with_backend = []
for hgp in list(self._hgps.values()):
if config.backend_name in hgp.backends:
hgps_with_backend.append(
to_instance_format(hgp._hub, hgp._group, hgp._project)
)
raise QiskitBackendNotFoundError(
f"Backend {config.backend_name} is not in "
f"{instance}. Please try a different instance. "
f"{config.backend_name} is in the following instances you have access to: "
f"{hgps_with_backend}"
if config := configuration_from_server_data(
raw_config=self._api_client.backend_configuration(backend_name),
instance=instance,
):
if self._channel == "ibm_quantum":
if not instance:
for hgp in list(self._hgps.values()):
if config.backend_name in hgp.backends:
instance = to_instance_format(hgp._hub, hgp._group, hgp._project)
break

elif config.backend_name not in self._get_hgp(instance=instance).backends:
hgps_with_backend = []
for hgp in list(self._hgps.values()):
if config.backend_name in hgp.backends:
hgps_with_backend.append(
to_instance_format(hgp._hub, hgp._group, hgp._project)
)
raise QiskitBackendNotFoundError(
f"Backend {config.backend_name} is not in "
f"{instance}. Please try a different instance. "
f"{config.backend_name} is in the following instances you have access to: "
f"{hgps_with_backend}"
)
return ibm_backend.IBMBackend(
instance=instance,
configuration=config,
service=self,
api_client=self._api_client,
)
else:
# cloud backend doesn't set hgp instance
return ibm_backend.IBMBackend(
configuration=config,
service=self,
api_client=self._api_client,
)

return ibm_backend.IBMBackend(
instance=instance,
configuration=config,
service=self,
api_client=self._api_client,
)
return None

def active_account(self) -> Optional[Dict[str, str]]:
Expand Down Expand Up @@ -893,17 +842,21 @@ def run(

qrt_options.validate(channel=self.channel)

hgp_name = None
if self._channel == "ibm_quantum":
# Find the right hgp
hgp = self._get_hgp(
hgp_name = self._get_hgp(
instance=qrt_options.instance, backend_name=qrt_options.get_backend_name()
)
hgp_name = hgp.name
).name
if hgp_name != self._current_instance:
self._current_instance = hgp_name
logger.info("Instance selected: %s", self._current_instance)
backend = self.backend(name=qrt_options.get_backend_name(), instance=hgp_name)
else:
hgp_name = None
backend = qrt_options.backend
if isinstance(backend, str) or (
hgp_name and isinstance(backend, IBMBackend) and backend._instance != hgp_name
):
backend = self.backend(name=qrt_options.get_backend_name(), instance=hgp_name)
status = backend.status()
if status.operational is True and status.status_msg != "active":
warnings.warn(
Expand Down Expand Up @@ -938,11 +891,9 @@ def run(
if ex.status_code == 404:
raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None
raise IBMRuntimeError(f"Failed to run program: {ex}") from None
backend = (
self.backend(name=response["backend"], instance=hgp_name)
if response["backend"]
else qrt_options.get_backend_name()
)

if response["backend"] and response["backend"] != qrt_options.get_backend_name():
backend = self.backend(name=response["backend"], instance=hgp_name)

if version == 2:
job = RuntimeJobV2(
Expand Down
5 changes: 5 additions & 0 deletions release-notes/unreleased/1732.upgrade.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The :meth:`.QiskitRuntimeService.backends` now always returns
new :class:`IBMBackend` instance even for the same query.
The backend properties and defaults data are retrieved from the server
for every instance when they are accessed for the first time,
while the configuration data is cached internally in the service instance.
12 changes: 9 additions & 3 deletions test/unit/mock/fake_runtime_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,9 @@ def list_backends(

def backend_configuration(self, backend_name: str) -> Dict[str, Any]:
"""Return the configuration a backend."""
return self._find_backend(backend_name).configuration
if ret := self._find_backend(backend_name).configuration:
return ret.copy()
return None

def backend_status(self, backend_name: str) -> Dict[str, Any]:
"""Return the status of a backend."""
Expand All @@ -451,11 +453,15 @@ def backend_properties(self, backend_name: str, datetime: Any = None) -> Dict[st
"""Return the properties of a backend."""
if datetime:
raise NotImplementedError("'datetime' is not supported.")
return self._find_backend(backend_name).properties
if ret := self._find_backend(backend_name).properties:
return ret.copy()
return None

def backend_pulse_defaults(self, backend_name: str) -> Dict[str, Any]:
"""Return the pulse defaults of a backend."""
return self._find_backend(backend_name).defaults
if ret := self._find_backend(backend_name).defaults:
return ret.copy()
return None

# pylint: disable=unused-argument
def create_session(
Expand Down
Loading

0 comments on commit e0aed70

Please sign in to comment.