Skip to content
This repository has been archived by the owner on Jul 24, 2024. It is now read-only.

Initial support for session #675

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5b423b0
Initial support for session
merav-aharoni Jul 18, 2023
cb02578
Merge branch 'main' into support_session
merav-aharoni Jul 20, 2023
cc1f28f
Added test for session. Cleaned up some of the unnecessary code
merav-aharoni Jul 20, 2023
c9ca298
Added max_execution_time parameter when calling program_run
merav-aharoni Jul 20, 2023
f8890bf
Added hms_to_seconds
merav-aharoni Jul 20, 2023
59de266
Placeholder for session close
merav-aharoni Jul 20, 2023
6135081
Merge branch 'main' into support_session
merav-aharoni Jul 21, 2023
a715bf1
Updated example in documentation. Removed Session._circuits_map.
merav-aharoni Jul 23, 2023
766c36e
Merge branch 'support_session' of github.com:merav-aharoni/qiskit-ibm…
merav-aharoni Jul 23, 2023
79607f8
New test file for session in unit test
merav-aharoni Jul 23, 2023
9107e8f
More unit tests
merav-aharoni Jul 23, 2023
5a4e9f0
Added more tests, unit and integration
merav-aharoni Jul 23, 2023
0dc9756
Changed tests to use FakeProvider
merav-aharoni Jul 23, 2023
91c6f0a
Added support to closing a session
merav-aharoni Jul 24, 2023
ee7f100
lint and black
merav-aharoni Jul 24, 2023
d9ab78a
Adding runtime_session.py
merav-aharoni Jul 24, 2023
f6c864a
Moved tests from unit to integration
merav-aharoni Jul 24, 2023
43b2bd3
lint
merav-aharoni Jul 24, 2023
366c9f0
Updated documentation
merav-aharoni Jul 24, 2023
bd74605
Added test
merav-aharoni Jul 24, 2023
92efec9
Merge branch 'main' into support_session
merav-aharoni Jul 27, 2023
9f97104
Added Session to __init__
merav-aharoni Jul 27, 2023
577e3f9
Fixed tab of documentation
merav-aharoni Jul 27, 2023
bf19d18
Added newline
merav-aharoni Jul 27, 2023
320e9ca
Merge branch 'main' into support_session
merav-aharoni Aug 3, 2023
5fc999f
Added test for session as a parameter
merav-aharoni Aug 3, 2023
a2fe052
Merge branch 'support_session' of github.com:merav-aharoni/qiskit-ibm…
merav-aharoni Aug 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions qiskit_ibm_provider/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
IBMProvider
IBMBackend
IBMBackendService
Session

Exceptions
==========
Expand All @@ -80,6 +81,7 @@

from .ibm_provider import IBMProvider
from .ibm_backend import IBMBackend
from .session import Session
from .job.ibm_job import IBMJob
from .exceptions import *
from .ibm_backend_service import IBMBackendService
Expand Down
8 changes: 8 additions & 0 deletions qiskit_ibm_provider/api/clients/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,11 @@ def update_tags(self, job_id: str, tags: list) -> Response:
API Response.
"""
return self._api.program_job(job_id).update_tags(tags)

def close_session(self, session_id: str) -> None:
"""Close session

Args:
session_id (str): the id of the session to close
"""
self._api.runtime_session(session_id=session_id).close()
12 changes: 12 additions & 0 deletions qiskit_ibm_provider/api/rest/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .backend import Backend
from .base import RestAdapterBase
from .program_job import ProgramJob
from .runtime_session import RuntimeSession
from ...utils import RuntimeEncoder
from ...utils.converters import local_to_utc

Expand Down Expand Up @@ -56,6 +57,17 @@ def program_job(self, job_id: str) -> "ProgramJob":
"""
return ProgramJob(self.session, job_id)

def runtime_session(self, session_id: str) -> "RuntimeSession":
"""Return an adapter for the session.

Args:
session_id: Job ID of the first job in a session.

Returns:
The session adapter.
"""
return RuntimeSession(self.session, session_id)

def program_run(
self,
program_id: str,
Expand Down
42 changes: 42 additions & 0 deletions qiskit_ibm_provider/api/rest/runtime_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Runtime Session REST adapter."""


from qiskit_ibm_provider.api.rest.base import RestAdapterBase
from ..session import RetrySession


class RuntimeSession(RestAdapterBase):
"""Rest adapter for session related endpoints."""

URL_MAP = {
"close": "/close",
}

def __init__(
self, session: RetrySession, session_id: str, url_prefix: str = ""
) -> None:
"""Job constructor.

Args:
session: RetrySession to be used in the adapter.
session_id: Job ID of the first job in a runtime session.
url_prefix: Prefix to use in the URL.
"""
super().__init__(session, "{}/sessions/{}".format(url_prefix, session_id))

def close(self) -> None:
"""Close this session."""
url = self.get_url("close")
self.session.delete(url)
17 changes: 17 additions & 0 deletions qiskit_ibm_provider/ibm_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,13 +529,28 @@ def _runtime_run(
) -> IBMCircuitJob:
"""Runs the runtime program and returns the corresponding job object"""
hgp_name = self._instance or self.provider._get_hgp().name

session = self.provider._session
if session:
if not session.active:
raise RuntimeError(f"The session {session.session_id} is closed.")
session_id = session.session_id
max_execution_time = session._max_time
else:
session_id = None
max_execution_time = None
start_session = session_id is None

try:
response = self.provider._runtime_client.program_run(
program_id=program_id,
backend_name=backend_name,
params=inputs,
hgp=hgp_name,
job_tags=job_tags,
session_id=session_id,
start_session=start_session,
max_execution_time=max_execution_time,
image=image,
)
except RequestsApiError as ex:
Expand All @@ -556,6 +571,8 @@ def _runtime_run(
"when submitting job: {}".format(str(err))
) from err
Publisher().publish("ibm.job.start", job)
if self.provider._session and self.provider._session.session_id is None:
self.provider._session._session_id = job.job_id()
return job

def _get_run_config(self, program_id: str, **kwargs: Any) -> Dict:
Expand Down
9 changes: 9 additions & 0 deletions qiskit_ibm_provider/ibm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .ibm_backend import IBMBackend # pylint: disable=cyclic-import
from .ibm_backend_service import IBMBackendService # pylint: disable=cyclic-import
from .job import IBMJob # pylint: disable=cyclic-import
from .session import Session
from .proxies.configuration import ProxyConfiguration
from .utils.hgp import to_instance_format, from_instance_format

Expand Down Expand Up @@ -131,6 +132,7 @@ def __init__(
instance: Optional[str] = None,
proxies: Optional[dict] = None,
verify: Optional[bool] = None,
session: Optional[Session] = None,
) -> None:
"""IBMProvider constructor

Expand Down Expand Up @@ -180,6 +182,7 @@ def __init__(

self._hgps = self._initialize_hgps(self._auth_client)
self._initialize_services()
self._session = session

@staticmethod
def _discover_account(
Expand Down Expand Up @@ -662,5 +665,11 @@ def get_backend(
raise QiskitBackendNotFoundError("No backend matches the criteria")
return backends[0]

def close_session(self) -> None:
"""Close session"""
self._session.close()
if self._session.session_id:
self._runtime_client.close_session(self._session.session_id)

def __repr__(self) -> str:
return "<{}>".format(self.__class__.__name__)
144 changes: 144 additions & 0 deletions qiskit_ibm_provider/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Qiskit Runtime flexible session."""

from typing import Optional, Type, Union
from types import TracebackType
from contextvars import ContextVar

from qiskit_ibm_provider.utils.converters import hms_to_seconds


class Session:
"""Class for creating a flexible Qiskit Runtime session.

A Qiskit Runtime ``session`` allows you to group a collection of iterative calls to
the quantum computer. A session is started when the first job within the session
is started. Subsequent jobs within the session are prioritized by the scheduler.
Data used within a session, such as transpiled circuits, is also cached to avoid
unnecessary overhead.

You can open a Qiskit Runtime session using this ``Session`` class
and submit one or more jobs.

For example::

from qiskit.test.reference_circuits import ReferenceCircuits
from qiskit_ibm_provider import IBMProvider
from qiskit_ibm_provider.session import Session

circ = ReferenceCircuits.bell()
backend = "ibmq_qasm_simulator"
with Session(backend_name=backend) as session:
provider = IBMProvider(session=session)
job = provider.get_backend(name=backend).run(circ)
print(f"Job ID: {job.job_id()}")
print(f"Result: {job.result()}")
# Close the session only if all jobs are finished and
# you don't need to run more in the session.
provider.close_session()

"""

def __init__(
self,
backend_name: Optional[str] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent with runtime, we should probably use backend here instead of backend_name

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but I wanted to stress that here it is only a string and not an actual backend. In runtime, it can be either.

max_time: Optional[Union[int, str]] = None,
):
"""Session constructor.

Args:
backend_name: string name of backend.
If not specified, a backend will be selected automatically
by the IBMProvider(IBM Cloud channel only).

max_time: (EXPERIMENTAL setting, can break between releases without warning)
Maximum amount of time, a runtime session can be open before being
forcibly closed. Can be specified as seconds (int) or a string like "2h 30m 40s".
This value must be in between 300 seconds and the
`system imposed maximum
<https://qiskit.org/documentation/partners/qiskit_ibm_runtime/faqs/max_execution_time.html>`_.

Raises:
ValueError: If an input value is invalid.
"""
self._instance = None
self._backend = backend_name
self._session_id: Optional[str] = None
self._active = True

self._max_time = (
max_time
if max_time is None or isinstance(max_time, int)
else hms_to_seconds(max_time, "Invalid max_time value: ")
)

def backend(self) -> Optional[str]:
"""Return backend for this session.

Returns:
Backend for this session. None if unknown.
"""
return self._backend

@property
def session_id(self) -> str:
"""Return the session ID.

Returns:
Session ID. None until a job runs in the session.
"""
return self._session_id

@property
def active(self) -> bool:
"""Return the status of the session.

Returns:
True if the session is active, False otherwise.
"""
return self._active

def close(self) -> None:
"""Set the session._active status to False"""
self._active = False

def __enter__(self) -> "Session":
set_cm_session(self)
return self

def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
set_cm_session(None)


# Default session
_DEFAULT_SESSION: ContextVar[Optional[Session]] = ContextVar(
"_DEFAULT_SESSION", default=None
)
_IN_SESSION_CM: ContextVar[bool] = ContextVar("_IN_SESSION_CM", default=False)


def set_cm_session(session: Optional[Session]) -> None:
"""Set the context manager session."""
_DEFAULT_SESSION.set(session)
_IN_SESSION_CM.set(session is not None)


def get_cm_session() -> Session:
"""Return the context managed session."""
return _DEFAULT_SESSION.get()
36 changes: 31 additions & 5 deletions qiskit_ibm_provider/utils/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
from math import ceil
from typing import Union, Tuple, Any, Optional

import dateutil.parser
from dateutil import tz
from dateutil import tz, parser

from qiskit_ibm_provider.exceptions import IBMInputValueError


def utc_to_local(utc_dt: Union[datetime, str]) -> datetime:
Expand All @@ -33,7 +34,7 @@ def utc_to_local(utc_dt: Union[datetime, str]) -> datetime:
TypeError: If the input parameter value is not valid.
"""
if isinstance(utc_dt, str):
utc_dt = dateutil.parser.parse(utc_dt)
utc_dt = parser.parse(utc_dt)
if not isinstance(utc_dt, datetime):
raise TypeError("Input `utc_dt` is not string or datetime.")
utc_dt = utc_dt.replace(tzinfo=timezone.utc) # type: ignore[arg-type]
Expand All @@ -54,7 +55,7 @@ def local_to_utc(local_dt: Union[datetime, str]) -> datetime:
TypeError: If the input parameter value is not valid.
"""
if isinstance(local_dt, str):
local_dt = dateutil.parser.parse(local_dt)
local_dt = parser.parse(local_dt)
if not isinstance(local_dt, datetime):
raise TypeError("Input `local_dt` is not string or datetime.")

Expand Down Expand Up @@ -131,7 +132,7 @@ def str_to_utc(utc_dt: Optional[str]) -> Optional[datetime]:
"""
if not utc_dt:
return None
parsed_dt = dateutil.parser.isoparse(utc_dt)
parsed_dt = parser.isoparse(utc_dt)
return parsed_dt.replace(tzinfo=timezone.utc)


Expand Down Expand Up @@ -184,3 +185,28 @@ def duration_difference(date_time: datetime) -> str:
elif time_tuple[3]:
time_str += "{} sec".format(time_tuple[3])
return time_str


def hms_to_seconds(hms: str, msg_prefix: str = "") -> int:
"""Convert duration specified as hours minutes seconds to seconds.

Args:
hms: The string input duration (in hours minutes seconds). Ex: 2h 10m 20s
msg_prefix: Additional message to prefix the error.

Returns:
Total seconds (int) in the duration.

Raises:
IBMInputValueError: when the given hms string is in an invalid format
"""
try:
date_time = parser.parse(hms)
hours = date_time.hour
minutes = date_time.minute
seconds = date_time.second
return int(
timedelta(hours=hours, minutes=minutes, seconds=seconds).total_seconds()
)
except parser.ParserError as parser_error:
raise IBMInputValueError(msg_prefix + str(parser_error))
Loading