-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add request limiter when polling for results #6774
Changes from 4 commits
c668323
4365313
354a2ed
a189bbe
1bbc3ea
49acf02
d301604
31c03c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,11 +14,24 @@ | |
"""Abstract base class for things sampling quantum circuits.""" | ||
|
||
import collections | ||
from typing import Dict, FrozenSet, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union | ||
from itertools import islice | ||
from typing import ( | ||
Any, | ||
Dict, | ||
FrozenSet, | ||
Iterator, | ||
List, | ||
Optional, | ||
Sequence, | ||
Tuple, | ||
TYPE_CHECKING, | ||
Union, | ||
) | ||
|
||
import duet | ||
import pandas as pd | ||
|
||
|
||
from cirq import ops, protocols, study, value | ||
from cirq.work.observable_measurement import ( | ||
measure_observables, | ||
|
@@ -34,6 +47,11 @@ | |
class Sampler(metaclass=value.ABCMetaImplementAnyOneOf): | ||
"""Something capable of sampling quantum circuits. Simulator or hardware.""" | ||
|
||
# Users have a rate limit of 1000 QPM for read/write requests to | ||
# the Quantum Engine. 1000/60 ~= 16 QPS. So requests are sent | ||
# in chunks of size 16 per second. | ||
CHUNK_SIZE: int = 16 | ||
|
||
def run( | ||
self, | ||
program: 'cirq.AbstractCircuit', | ||
|
@@ -294,9 +312,26 @@ async def run_batch_async( | |
See docs for `cirq.Sampler.run_batch`. | ||
""" | ||
params_list, repetitions = self._normalize_batch_args(programs, params_list, repetitions) | ||
return await duet.pstarmap_async( | ||
self.run_sweep_async, zip(programs, params_list, repetitions) | ||
) | ||
if len(programs) <= self.CHUNK_SIZE: | ||
return await duet.pstarmap_async( | ||
self.run_sweep_async, zip(programs, params_list, repetitions) | ||
) | ||
|
||
results = [] | ||
for program_chunk, params_chunk, reps_chunk in zip( | ||
_chunked(programs, self.CHUNK_SIZE), | ||
_chunked(params_list, self.CHUNK_SIZE), | ||
_chunked(repetitions, self.CHUNK_SIZE), | ||
): | ||
# Run_sweep_async for the current chunk | ||
await duet.sleep(1) # Delay for 1 second between chunk | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this delay needed? If we wait for completion of the chunk (as it appears we do), then we should be able to immediately begin another chunk. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, without it we still run into the quota exception when polling for results (e.g for 100+ jobs with relatively shallow circuits). Empirically, I found that sleeping for 1s removes the error for both shallow and deep circuits |
||
results.extend( | ||
await duet.pstarmap_async( | ||
self.run_sweep_async, zip(program_chunk, params_chunk, reps_chunk) | ||
) | ||
) | ||
|
||
return results | ||
|
||
def _normalize_batch_args( | ||
self, | ||
|
@@ -449,3 +484,8 @@ def _get_measurement_shapes( | |
) | ||
num_instances[key] += 1 | ||
return {k: (num_instances[k], qid_shape) for k, qid_shape in qid_shapes.items()} | ||
|
||
|
||
def _chunked(iterable: Sequence[Any], n: int) -> Iterator[tuple[Any, ...]]: # pragma: no cover | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's a private method so it shouldn't be explicitly tested, but it is implicitly tested in the added tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The typing allows the function to take a https://mypy.readthedocs.io/en/stable/generics.html#generic-functions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
it = iter(iterable) # pragma: no cover | ||
return iter(lambda: tuple(islice(it, n)), ()) # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FMI - how was this value chosen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a comment:
Users have a rate limit of 1000 QPM for read/write requests to the Quantum Engine. 1000/60 ~= 16 QPS. So requests are sent in chunks of size 16 per second.
.And empirically I verified chunk sizes over 16 lead to Quota exceeded exceptions for large batches.