Skip to content

Commit

Permalink
feat: Add verbose mode for Remote provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxence Drutel committed Apr 24, 2024
1 parent ab9e771 commit 844e442
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 16 deletions.
13 changes: 13 additions & 0 deletions qiskit_alice_bob_provider/remote/api/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ def get_job(client: ApiClient, job_id: str) -> dict:
return client.get(f'v1/jobs/{job_id}').json()


def get_job_metrics(client: ApiClient, job_id: str) -> dict:
"""Get exposed metrics about a job in the Alice & Bob API
Args:
client (ApiClient): a client for the Alice & Bob API
job_id (str): the ID of the job in the Alice & Bob API
Returns:
dict: the API response object, containing the recorded metrics.
"""
return client.get(f'v1/jobs/{job_id}/metrics').json()


def cancel_job(client: ApiClient, job_id: str) -> None:
"""Cancel a job in the Alice & Bob API
Expand Down
10 changes: 8 additions & 2 deletions qiskit_alice_bob_provider/remote/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self, api_client: ApiClient, target_description: Dict):
self._target = ab_target_to_qiskit_target(target_description)
self._options = _options_from_ab_target(target_description)
self._translation_plugin = _determine_translation_plugin(self._target)
self._verbose = True

def __repr__(self) -> str:
return f'<AliceBobRemoteBackend(name={self.name})>'
Expand All @@ -69,7 +70,10 @@ def get_translation_stage_plugin(self):
This function is called before we start the circuit translation,
and we therefore also use it to inform that we are starting this step.
"""
write_current_line('Translating circuit to supported operations...')
if self._verbose:
write_current_line(
'Translating circuit to supported operations...'
)
return self._translation_plugin

def update_options(self, option_updates: Dict[str, Any]) -> Options:
Expand All @@ -96,7 +100,8 @@ def run(self, run_input: QuantumCircuit, **kwargs) -> AliceBobRemoteJob:
Wait for the results by calling
:func:`AliceBobRemoteJob.result`.
"""
write_current_line('Sending circuit to the API...')
if self._verbose:
write_current_line('Sending circuit to the API...')
options = self.update_options(kwargs)
input_params = _ab_input_params_from_options(options)
job = jobs.create_job(self._api_client, self.name, input_params)
Expand All @@ -109,6 +114,7 @@ def run(self, run_input: QuantumCircuit, **kwargs) -> AliceBobRemoteJob:
api_client=self._api_client,
job_id=job['id'],
circuit=run_input,
verbose=self._verbose,
)


Expand Down
75 changes: 63 additions & 12 deletions qiskit_alice_bob_provider/remote/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import time
from dataclasses import dataclass
from io import StringIO
from typing import Callable, Dict, Optional
from typing import Any, Callable, Dict, Optional

from qiskit import QuantumCircuit
from qiskit.providers import JobStatus, JobV1
Expand Down Expand Up @@ -53,12 +53,14 @@ class _DownloadedFile:
class AliceBobRemoteJob(JobV1):
"""A Qiskit job referencing a job executed in the Alice & Bob API"""

# pylint: disable=too-many-arguments
def __init__(
self,
backend: BackendV2,
api_client: ApiClient,
job_id: str,
circuit: QuantumCircuit,
verbose: bool,
):
"""A job should not be instantiated manually but created by calling
:func:`AliceBobRemoteBackend.run` or :func:`qiskit.execute`.
Expand All @@ -75,11 +77,13 @@ def __init__(
self._backend_v2 = backend
self._api_client = api_client
self._circuit = circuit
self._verbose = verbose
self._last_response: Optional[Dict] = None
self._ab_status: Optional[AliceBobEventType] = None
self._status: Optional[JobStatus] = None
self._counts: Optional[Dict[str, int]] = None
self._files: Dict[str, _DownloadedFile] = {}
self._metrics: Dict[str, Any] = {}

def _refresh(self) -> None:
"""If the job status is not final, refresh the description of the API
Expand Down Expand Up @@ -151,31 +155,43 @@ def _get_counts(self) -> Dict[str, int]:
self._counts[hex(int(row['memory'], 2))] = int(row['count'])
return self._counts

def _get_metrics(self) -> Dict[str, Any]:
self._metrics = jobs.get_job_metrics(self._api_client, self.job_id())
return self._metrics

def _monitor_state(
self, timeout: Optional[float] = None, wait: float = 2
) -> None:
start_time = time.time()
status = self.status()

# TODO: add timestamp
while status not in JOB_FINAL_STATES:
if self._ab_status == AliceBobEventType.INPUT_READY:
if (
self._verbose
and self._ab_status == AliceBobEventType.INPUT_READY
):
write_current_line(
f'\rJob {self.job_id()} is waiting to be compiled.'
f'Job {self.job_id()} is waiting to be compiled.'
)
if self._ab_status in {
if self._verbose and self._ab_status in {
AliceBobEventType.COMPILING,
AliceBobEventType.TRANSPILING,
}:
# We take the shortcut of assuming compilation + transpilation
# are the same things at the moment.
write_current_line(f'\rJob {self.job_id()} is being compiled.')
if self._ab_status == AliceBobEventType.TRANSPILED:
write_current_line(f'Job {self.job_id()} is being compiled.')
if (
self._verbose
and self._ab_status == AliceBobEventType.TRANSPILED
):
write_current_line(
f'\rJob {self.job_id()} is waiting to be executed.'
f'Job {self.job_id()} is waiting to be executed.'
)
if self._ab_status == AliceBobEventType.EXECUTING:
write_current_line(f'\rJob {self.job_id()} is being executed.')
if (
self._verbose
and self._ab_status == AliceBobEventType.EXECUTING
):
write_current_line(f'Job {self.job_id()} is being executed.')

elapsed_time = time.time() - start_time
if timeout is not None and elapsed_time >= timeout:
Expand All @@ -186,8 +202,25 @@ def _monitor_state(
time.sleep(wait)
status = self.status()

if self._ab_status == AliceBobEventType.SUCCEEDED:
write_current_line(f'\rJob {self.job_id()} finished successfully.')
if self._verbose and self._ab_status == AliceBobEventType.SUCCEEDED:
metrics = self._get_metrics()
success_msg = f'Job {self.job_id()} finished successfully.'
if 'qpu_duration_ns' in metrics and isinstance(
metrics['qpu_duration_ns'], int
):
success_msg += (
' Time spent executing on QPU: '
f'{_format_nanoseconds(metrics["qpu_duration_ns"])}.'
)
elif 'simulation_duration_ns' in metrics and isinstance(
metrics['simulation_duration_ns'], int
):
success_msg += (
' Time spent simulating: '
f'{_format_nanoseconds(metrics["simulation_duration_ns"])}'
'.'
)
write_current_line(success_msg)
# For the other final states, Qiskit is already displaying the error
# with the relevant details.

Expand Down Expand Up @@ -236,3 +269,21 @@ def status(self) -> JobStatus:
"""Return the status of the job, among the values of ``JobStatus``."""
self._refresh()
return self._status


def _format_nanoseconds(time_ns: int) -> str:
"""Format a given number of nanoseconds to a string containing a unit
and the time value converted to this unit in the most readable way.
This will convert the nanoseconds either to microseconds, milliseconds,
seconds or minutes."""
if time_ns < 1e3: # < 1us
return f'{time_ns}ns'
elif time_ns < 1e6: # < 1ms
return f'{(time_ns / 1e3):.2f}us'
elif time_ns < 1e9: # < 1s
return f'{(time_ns / 1e6):.2f}ms'
elif time_ns < 60e9: # < 1min
return f'{(time_ns / 1e9):.2f}s'
else: # min & seconds format
minutes, seconds = divmod(time_ns / 1e9, 60)
return f'{minutes}min {seconds:.0f}s'
20 changes: 19 additions & 1 deletion qiskit_alice_bob_provider/remote/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,30 @@ def __init__(
for ab_target in list_targets(client):
self._backends.append(AliceBobRemoteBackend(client, ab_target))

def get_backend(self, name=None, **kwargs) -> AliceBobRemoteBackend:
def get_backend(
self, name=None, verbose=True, **kwargs
) -> AliceBobRemoteBackend:
"""Return a single backend matching by its name.
Args:
name (str): name of the backend
verbose (bool): if True, will display information about the jobs
execution.
Defaults to True.
**kwargs: additional backend parameters
(see targets documentation).
Returns:
AliceBobRemoteBackend: backend matching the given name.
"""
backend = super().get_backend(name)
# We allow to set the options when getting the backend,
# to align with what we do in the local provider.
if kwargs:
backend.update_options(kwargs)

# pylint: disable=protected-access
backend._verbose = verbose
return backend

def backends(
Expand Down
9 changes: 8 additions & 1 deletion qiskit_alice_bob_provider/remote/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from datetime import datetime


def write_current_line(text: str) -> None:
"""Write a given text on the same line as the previous call of this
function."""
# We use a padding of 80 characters to be sure everything is flushed out
# when we do the next call to this function.
print(f'{text:<80}', end='\r', flush=True)
print(
f'[{datetime.now().strftime("%H:%M:%S")}] {text:<80}',
end='\r',
flush=True,
)
25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ def successful_job(mocked_targets: Mocker) -> Mocker:
f'/v1/jobs/{job_id}/output',
text='11,12\n10,474\n01,6\n00,508\n',
)
mocked_targets.register_uri(
'GET',
f'/v1/jobs/{job_id}/metrics',
json={},
)
return mocked_targets


Expand Down Expand Up @@ -500,6 +505,11 @@ def failed_transpilation_job(mocked_targets: Mocker) -> Mocker:
}
},
)
mocked_targets.register_uri(
'GET',
f'/v1/jobs/{job_id}/metrics',
json={},
)
return mocked_targets


Expand Down Expand Up @@ -620,6 +630,11 @@ def failed_execution_job(mocked_targets: Mocker) -> Mocker:
}
},
)
mocked_targets.register_uri(
'GET',
f'/v1/jobs/{job_id}/metrics',
json={},
)
return mocked_targets


Expand Down Expand Up @@ -767,6 +782,11 @@ def cancellable_job(mocked_targets: Mocker) -> Mocker:
}
},
)
mocked_targets.register_uri(
'GET',
f'/v1/jobs/{job_id}/metrics',
json={},
)
return mocked_targets


Expand Down Expand Up @@ -857,4 +877,9 @@ def failed_validation_job(mocked_targets: Mocker) -> Mocker:
}
},
)
mocked_targets.register_uri(
'GET',
f'/v1/jobs/{job_id}/metrics',
json={},
)
return mocked_targets

0 comments on commit 844e442

Please sign in to comment.