Skip to content
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

Fix backend primitives on backends with job size limit (backport #8955) #9039

Merged
merged 1 commit into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 23 additions & 10 deletions qiskit/primitives/backend_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
from qiskit.quantum_info import Pauli, PauliList
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.result import Counts, Result
from qiskit.tools.monitor import job_monitor
from qiskit.transpiler import PassManager

from .base import BaseEstimator, EstimatorResult
Expand All @@ -39,7 +38,6 @@
def _run_circuits(
circuits: QuantumCircuit | list[QuantumCircuit],
backend: BackendV1 | BackendV2,
monitor: bool = False,
**run_options,
) -> tuple[Result, list[dict]]:
"""Remove metadata of circuits and run the circuits on a backend.
Expand All @@ -57,11 +55,29 @@ def _run_circuits(
for circ in circuits:
metadata.append(circ.metadata)
circ.metadata = {}
if isinstance(backend, BackendV1):
max_circuits = backend.configuration().max_experiments
elif isinstance(backend, BackendV2):
max_circuits = backend.max_circuits
if max_circuits:
jobs = [
backend.run(circuits[pos : pos + max_circuits], **run_options)
for pos in range(0, len(circuits), max_circuits)
]
result = [x.result() for x in jobs]
else:
result = [backend.run(circuits, **run_options).result()]
return result, metadata


job = backend.run(circuits, **run_options)
if monitor:
job_monitor(job)
return job.result(), metadata
def _prepare_counts(results):
counts = []
for res in results:
count = res.get_counts()
if not isinstance(count, list):
count = [count]
counts.extend(count)
return counts


class BackendEstimator(BaseEstimator):
Expand Down Expand Up @@ -338,10 +354,7 @@ def _postprocessing(
"""
Postprocessing for evaluation of expectation value using pauli rotation gates.
"""

counts = result.get_counts()
if not isinstance(counts, list):
counts = [counts]
counts = _prepare_counts(result)
expval_list = []
var_list = []
shots_list = []
Expand Down
10 changes: 3 additions & 7 deletions qiskit/primitives/backend_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .base import BaseSampler, SamplerResult
from .primitive_job import PrimitiveJob
from .utils import _circuit_key
from .backend_estimator import _run_circuits, _prepare_counts


class BackendSampler(BaseSampler):
Expand Down Expand Up @@ -151,16 +152,11 @@ def _call(
bound_circuits = self._bound_pass_manager_run(bound_circuits)

# Run
result = self._backend.run(bound_circuits, **run_options).result()

result, _metadata = _run_circuits(bound_circuits, self._backend, **run_options)
return self._postprocessing(result, bound_circuits)

def _postprocessing(self, result: Result, circuits: list[QuantumCircuit]) -> SamplerResult:

counts = result.get_counts()
if not isinstance(counts, list):
counts = [counts]

counts = _prepare_counts(result)
shots = sum(counts[0].values())

probabilies = []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
fixes:
- |
Fixed an issue with the primitive classes, :class:`~.BackendSampler` and
:class:`~.BackendEstimator` when running on backends that have a limited
number of circuits in each job. Not all backends support an unlimited
batch size (most hardware backends do not) and previously the backend
primitive classes would have potentially incorrectly sent more circuits
than the backend supported. This has been corrected so that
:class:`~.BackendSampler` and :class:`~.BackendEstimator` will chunk the
circuits into multiple jobs if the backend has a limited number of
circuits per job.
- |
Fixed an issue with the :class:`~.BackendEstimator` class where previously
setting a run option named ``monitor`` to a value that evaluated as
``True`` would have incorrectly triggered a job monitor that only
worked on backends from the ``qiskit-ibmq-provider`` package. This
has been removed so that you can use a ``monitor`` run option if needed
without causing any issues.
43 changes: 43 additions & 0 deletions test/python/primitives/test_backend_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,49 @@ def test_options(self, backend):
self.assertIsInstance(result, EstimatorResult)
np.testing.assert_allclose(result.values, [-1.307397243478641], rtol=0.1)

def test_job_size_limit_v2(self):
"""Test BackendEstimator respects job size limit"""

class FakeNairobiLimitedCircuits(FakeNairobiV2):
"""FakeNairobiV2 with job size limit."""

@property
def max_circuits(self):
return 1

backend = FakeNairobiLimitedCircuits()
backend.set_options(seed_simulator=123)
qc = QuantumCircuit(1)
qc2 = QuantumCircuit(1)
qc2.x(0)
backend.set_options(seed_simulator=123)
qc = RealAmplitudes(num_qubits=2, reps=2)
op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)])
k = 5
params_array = np.random.rand(k, qc.num_parameters)
params_list = params_array.tolist()
estimator = BackendEstimator(backend=backend)
with unittest.mock.patch.object(backend, "run") as run_mock:
estimator.run([qc] * k, [op] * k, params_list).result()
self.assertEqual(run_mock.call_count, 10)

def test_job_size_limit_v1(self):
"""Test BackendEstimator respects job size limit"""
backend = FakeNairobi()
config = backend.configuration()
config.max_experiments = 1
backend._configuration = config
backend.set_options(seed_simulator=123)
qc = RealAmplitudes(num_qubits=2, reps=2)
op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)])
k = 5
params_array = np.random.rand(k, qc.num_parameters)
params_list = params_array.tolist()
estimator = BackendEstimator(backend=backend)
with unittest.mock.patch.object(backend, "run") as run_mock:
estimator.run([qc] * k, [op] * k, params_list).result()
self.assertEqual(run_mock.call_count, 10)


if __name__ == "__main__":
unittest.main()
42 changes: 42 additions & 0 deletions test/python/primitives/test_backend_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,48 @@ def test_primitive_job_status_done(self, backend):
job = sampler.run(circuits=[bell])
self.assertEqual(job.status(), JobStatus.DONE)

def test_primitive_job_size_limit_backend_v2(self):
"""Test primitive respects backend's job size limit."""

class FakeNairobiLimitedCircuits(FakeNairobiV2):
"""FakeNairobiV2 with job size limit."""

@property
def max_circuits(self):
return 1

qc = QuantumCircuit(1)
qc.measure_all()
qc2 = QuantumCircuit(1)
qc2.x(0)
qc2.measure_all()
sampler = BackendSampler(backend=FakeNairobiLimitedCircuits())
result = sampler.run([qc, qc2]).result()
self.assertIsInstance(result, SamplerResult)
self.assertEqual(len(result.quasi_dists), 2)

self.assertDictAlmostEqual(result.quasi_dists[0], {0: 1}, 0.1)
self.assertDictAlmostEqual(result.quasi_dists[1], {1: 1}, 0.1)

def test_primitive_job_size_limit_backend_v1(self):
"""Test primitive respects backend's job size limit."""
backend = FakeNairobi()
config = backend.configuration()
config.max_experiments = 1
backend._configuration = config
qc = QuantumCircuit(1)
qc.measure_all()
qc2 = QuantumCircuit(1)
qc2.x(0)
qc2.measure_all()
sampler = BackendSampler(backend=backend)
result = sampler.run([qc, qc2]).result()
self.assertIsInstance(result, SamplerResult)
self.assertEqual(len(result.quasi_dists), 2)

self.assertDictAlmostEqual(result.quasi_dists[0], {0: 1}, 0.1)
self.assertDictAlmostEqual(result.quasi_dists[1], {1: 1}, 0.1)


if __name__ == "__main__":
unittest.main()