From 757e794e9dfd26b3fa5f7da274a4f15b2f08ef6d Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 31 Oct 2022 10:58:14 +0100 Subject: [PATCH 1/5] Fix default batching in variational algorithms --- qiskit/algorithms/eigensolvers/vqd.py | 12 ++++++++ .../minimum_eigensolvers/sampling_vqe.py | 12 ++++++++ qiskit/algorithms/minimum_eigensolvers/vqe.py | 12 ++++++++ qiskit/algorithms/optimizers/optimizer.py | 9 ++++-- ...vqe-default-batching-eb08e6ce17907da3.yaml | 7 +++++ .../minimum_eigensolvers/test_vqe.py | 29 +++++++++++++++++++ 6 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-vqe-default-batching-eb08e6ce17907da3.yaml diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py index a3e4020338d0..6306cda57b65 100644 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ b/qiskit/algorithms/eigensolvers/vqd.py @@ -264,10 +264,22 @@ def compute_eigenvalues( fun=energy_evaluation, x0=initial_point, bounds=bounds ) else: + # We always want to submit as many estimations per job as possible for minimal + # overhead on the hardware. The minimum is set to 50 to cover the commonly used SPSA + # calibration or 2 * num_parameters to cover finite difference gradients, + # and we cap at 1000 parameter evaluations at once. + max_batchsize = getattr(self.optimizer, "_max_evals_grouped", None) + if max_batchsize is None: + default_batchsize = min(1000, max(50, 2 * self.ansatz.num_parameters)) + self.optimizer.set_max_evals_grouped(default_batchsize) + opt_result = self.optimizer.minimize( fun=energy_evaluation, x0=initial_point, bounds=bounds ) + # reset to original value + self.optimizer.set_max_evals_grouped(max_batchsize) + eval_time = time() - start_time self._update_vqd_result(result, opt_result, eval_time, self.ansatz.copy()) diff --git a/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py b/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py index cfa19eb7b6cf..122b31e3cd29 100755 --- a/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py @@ -208,10 +208,22 @@ def compute_minimum_eigenvalue( # pylint: disable=not-callable optimizer_result = self.optimizer(fun=evaluate_energy, x0=initial_point, bounds=bounds) else: + # We always want to submit as many estimations per job as possible for minimal + # overhead on the hardware. The minimum is set to 50 to cover the commonly used SPSA + # calibration or 2 * num_parameters to cover finite difference gradients, + # and we cap at 1000 parameter evaluations at once. + max_batchsize = getattr(self.optimizer, "_max_evals_grouped", None) + if max_batchsize is None: + default_batchsize = min(1000, max(50, 2 * self.ansatz.num_parameters)) + self.optimizer.set_max_evals_grouped(default_batchsize) + optimizer_result = self.optimizer.minimize( fun=evaluate_energy, x0=initial_point, bounds=bounds ) + # reset to original value + self.optimizer.set_max_evals_grouped(max_batchsize) + optimizer_time = time() - start_time logger.info( diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index 4c01ddc26191..e14466f2a87d 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -181,10 +181,22 @@ def compute_minimum_eigenvalue( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) else: + # We always want to submit as many estimations per job as possible for minimal + # overhead on the hardware. The minimum is set to 50 to cover the commonly used SPSA + # calibration or 2 * num_parameters to cover finite difference gradients, + # and we cap at 1000 parameter evaluations at once. + max_batchsize = getattr(self.optimizer, "_max_evals_grouped", None) + if max_batchsize is None: + default_batchsize = min(1000, max(50, 2 * self.ansatz.num_parameters)) + self.optimizer.set_max_evals_grouped(default_batchsize) + optimizer_result = self.optimizer.minimize( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) + # reset to original value + self.optimizer.set_max_evals_grouped(max_batchsize) + optimizer_time = time() - start_time logger.info( diff --git a/qiskit/algorithms/optimizers/optimizer.py b/qiskit/algorithms/optimizers/optimizer.py index 6f2e4e1077f9..3ade5b08fb3a 100644 --- a/qiskit/algorithms/optimizers/optimizer.py +++ b/qiskit/algorithms/optimizers/optimizer.py @@ -180,7 +180,7 @@ def __init__(self): self._bounds_support_level = self.get_support_level()["bounds"] self._initial_point_support_level = self.get_support_level()["initial_point"] self._options = {} - self._max_evals_grouped = 1 + self._max_evals_grouped = None @abstractmethod def get_support_level(self): @@ -205,7 +205,7 @@ def set_options(self, **kwargs): # pylint: disable=invalid-name @staticmethod - def gradient_num_diff(x_center, f, epsilon, max_evals_grouped=1): + def gradient_num_diff(x_center, f, epsilon, max_evals_grouped=None): """ We compute the gradient with the numeric differentiation in the parallel way, around the point x_center. @@ -214,11 +214,14 @@ def gradient_num_diff(x_center, f, epsilon, max_evals_grouped=1): x_center (ndarray): point around which we compute the gradient f (func): the function of which the gradient is to be computed. epsilon (float): the epsilon used in the numeric differentiation. - max_evals_grouped (int): max evals grouped + max_evals_grouped (int): max evals grouped, defaults to 1 (i.e. no batching). Returns: grad: the gradient computed """ + if max_evals_grouped is None: # no batching by default + max_evals_grouped = 1 + forig = f(*((x_center,))) grad = [] ei = np.zeros((len(x_center),), float) diff --git a/releasenotes/notes/fix-vqe-default-batching-eb08e6ce17907da3.yaml b/releasenotes/notes/fix-vqe-default-batching-eb08e6ce17907da3.yaml new file mode 100644 index 000000000000..8ea2178ed722 --- /dev/null +++ b/releasenotes/notes/fix-vqe-default-batching-eb08e6ce17907da3.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a performance bug where the new primitive-based variational algorithms + :class:`.minimum_eigensolvers.VQE`, :class:`.eigensolvers.VQD` and :class:`.SamplingVQE` + did not batch energy evaluations per default, which resulted in a significant slowdown + if a hardware backend was used. \ No newline at end of file diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index c3c0b89d9cce..ea81d83c4f1d 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -300,6 +300,35 @@ def run_check(): vqe.optimizer = L_BFGS_B() run_check() + def test_default_batch_evaluation(self): + """Test the default batching works.""" + ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") + + wrapped_estimator = Estimator() + inner_estimator = Estimator() + + callcount = {"estimator": 0} + + def wrapped_estimator_run(*args, **kwargs): + kwargs["callcount"]["estimator"] += 1 + return inner_estimator.run(*args, **kwargs) + + wrapped_estimator.run = partial(wrapped_estimator_run, callcount=callcount) + + spsa = SPSA(maxiter=5) + + vqe = VQE(wrapped_estimator, ansatz, spsa) + _ = vqe.compute_minimum_eigenvalue(Pauli("ZZ")) + + # 1 calibration + 5 loss + 1 return loss + expected_estimator_runs = 1 + 5 + 1 + + with self.subTest(msg="check callcount"): + self.assertEqual(callcount["estimator"], expected_estimator_runs) + + with self.subTest(msg="check reset to original max evals grouped"): + self.assertIsNone(spsa.get_max_evals_grouped()) + def test_batch_evaluate_with_qnspsa(self): """Test batch evaluating with QNSPSA works.""" ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") From ac33d049e23ff432b09c8980720c130caa43118e Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 31 Oct 2022 11:29:07 +0100 Subject: [PATCH 2/5] fix test --- test/python/algorithms/minimum_eigensolvers/test_vqe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index ea81d83c4f1d..2c3f7d6261e2 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -327,7 +327,7 @@ def wrapped_estimator_run(*args, **kwargs): self.assertEqual(callcount["estimator"], expected_estimator_runs) with self.subTest(msg="check reset to original max evals grouped"): - self.assertIsNone(spsa.get_max_evals_grouped()) + self.assertIsNone(spsa._max_evals_grouped) def test_batch_evaluate_with_qnspsa(self): """Test batch evaluating with QNSPSA works.""" From 44f86a67975f882d13eed072f5d1e2cd54d47b7e Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 1 Nov 2022 08:55:04 +0100 Subject: [PATCH 3/5] reduce batching to only SPSA --- qiskit/algorithms/eigensolvers/vqd.py | 17 ++++++------ .../minimum_eigensolvers/sampling_vqe.py | 17 ++++++------ qiskit/algorithms/minimum_eigensolvers/vqe.py | 17 ++++++------ qiskit/algorithms/utils/set_batching.py | 27 +++++++++++++++++++ .../minimum_eigensolvers/test_vqe.py | 2 +- 5 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 qiskit/algorithms/utils/set_batching.py diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py index 6306cda57b65..3a8151c76766 100644 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ b/qiskit/algorithms/eigensolvers/vqd.py @@ -38,6 +38,9 @@ from ..exceptions import AlgorithmError from ..observables_evaluator import estimate_observables +# private function as we expect this to be updated in the next released +from ..utils.set_batching import _set_default_batchsize + logger = logging.getLogger(__name__) @@ -264,21 +267,17 @@ def compute_eigenvalues( fun=energy_evaluation, x0=initial_point, bounds=bounds ) else: - # We always want to submit as many estimations per job as possible for minimal - # overhead on the hardware. The minimum is set to 50 to cover the commonly used SPSA - # calibration or 2 * num_parameters to cover finite difference gradients, - # and we cap at 1000 parameter evaluations at once. - max_batchsize = getattr(self.optimizer, "_max_evals_grouped", None) - if max_batchsize is None: - default_batchsize = min(1000, max(50, 2 * self.ansatz.num_parameters)) - self.optimizer.set_max_evals_grouped(default_batchsize) + # we always want to submit as many estimations per job as possible for minimal + # overhead on the hardware + was_updated = _set_default_batchsize(self.optimizer) opt_result = self.optimizer.minimize( fun=energy_evaluation, x0=initial_point, bounds=bounds ) # reset to original value - self.optimizer.set_max_evals_grouped(max_batchsize) + if was_updated: + self.optimizer.set_max_evals_grouped(None) eval_time = time() - start_time diff --git a/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py b/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py index 122b31e3cd29..711f93e60a4c 100755 --- a/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/sampling_vqe.py @@ -39,6 +39,9 @@ from ..observables_evaluator import estimate_observables from ..utils import validate_initial_point, validate_bounds +# private function as we expect this to be updated in the next released +from ..utils.set_batching import _set_default_batchsize + logger = logging.getLogger(__name__) @@ -208,21 +211,17 @@ def compute_minimum_eigenvalue( # pylint: disable=not-callable optimizer_result = self.optimizer(fun=evaluate_energy, x0=initial_point, bounds=bounds) else: - # We always want to submit as many estimations per job as possible for minimal - # overhead on the hardware. The minimum is set to 50 to cover the commonly used SPSA - # calibration or 2 * num_parameters to cover finite difference gradients, - # and we cap at 1000 parameter evaluations at once. - max_batchsize = getattr(self.optimizer, "_max_evals_grouped", None) - if max_batchsize is None: - default_batchsize = min(1000, max(50, 2 * self.ansatz.num_parameters)) - self.optimizer.set_max_evals_grouped(default_batchsize) + # we always want to submit as many estimations per job as possible for minimal + # overhead on the hardware + was_updated = _set_default_batchsize(self.optimizer) optimizer_result = self.optimizer.minimize( fun=evaluate_energy, x0=initial_point, bounds=bounds ) # reset to original value - self.optimizer.set_max_evals_grouped(max_batchsize) + if was_updated: + self.optimizer.set_max_evals_grouped(None) optimizer_time = time() - start_time diff --git a/qiskit/algorithms/minimum_eigensolvers/vqe.py b/qiskit/algorithms/minimum_eigensolvers/vqe.py index e14466f2a87d..266637253911 100644 --- a/qiskit/algorithms/minimum_eigensolvers/vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/vqe.py @@ -35,6 +35,9 @@ from ..observables_evaluator import estimate_observables from ..utils import validate_initial_point, validate_bounds +# private function as we expect this to be updated in the next released +from ..utils.set_batching import _set_default_batchsize + logger = logging.getLogger(__name__) @@ -181,21 +184,17 @@ def compute_minimum_eigenvalue( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) else: - # We always want to submit as many estimations per job as possible for minimal - # overhead on the hardware. The minimum is set to 50 to cover the commonly used SPSA - # calibration or 2 * num_parameters to cover finite difference gradients, - # and we cap at 1000 parameter evaluations at once. - max_batchsize = getattr(self.optimizer, "_max_evals_grouped", None) - if max_batchsize is None: - default_batchsize = min(1000, max(50, 2 * self.ansatz.num_parameters)) - self.optimizer.set_max_evals_grouped(default_batchsize) + # we always want to submit as many estimations per job as possible for minimal + # overhead on the hardware + was_updated = _set_default_batchsize(self.optimizer) optimizer_result = self.optimizer.minimize( fun=evaluate_energy, x0=initial_point, jac=evaluate_gradient, bounds=bounds ) # reset to original value - self.optimizer.set_max_evals_grouped(max_batchsize) + if was_updated: + self.optimizer.set_max_evals_grouped(None) optimizer_time = time() - start_time diff --git a/qiskit/algorithms/utils/set_batching.py b/qiskit/algorithms/utils/set_batching.py new file mode 100644 index 000000000000..225f50a6fed8 --- /dev/null +++ b/qiskit/algorithms/utils/set_batching.py @@ -0,0 +1,27 @@ +# 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. + +"""Set default batch sizes for the optimizers.""" + +from qiskit.algorithms.optimizers import Optimizer, SPSA + + +def _set_default_batchsize(optimizer: Optimizer) -> bool: + """Set the default batchsize, if None is set and return whether it was updated or not.""" + if isinstance(optimizer, SPSA): + updated = optimizer._max_evals_grouped is None + if updated: + optimizer.set_max_evals_grouped(50) + else: # we only set a batchsize for SPSA + updated = False + + return updated diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 2c3f7d6261e2..31a55d27d438 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -300,7 +300,7 @@ def run_check(): vqe.optimizer = L_BFGS_B() run_check() - def test_default_batch_evaluation(self): + def test_default_batch_evaluation_on_spsa(self): """Test the default batching works.""" ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz") From e0e683776a997a1df3e805551fd0290a9272fafc Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 1 Nov 2022 09:25:26 +0100 Subject: [PATCH 4/5] fix tests --- qiskit/algorithms/optimizers/spsa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/optimizers/spsa.py b/qiskit/algorithms/optimizers/spsa.py index a7f4f93efdaa..ac70f8a0a6fe 100644 --- a/qiskit/algorithms/optimizers/spsa.py +++ b/qiskit/algorithms/optimizers/spsa.py @@ -719,7 +719,7 @@ def _batch_evaluate(function, points, max_evals_grouped, unpack_points=False): """ # if the function cannot handle lists of points as input, cover this case immediately - if max_evals_grouped == 1: + if max_evals_grouped is None or max_evals_grouped == 1: # support functions with multiple arguments where the points are given in a tuple return [ function(*point) if isinstance(point, tuple) else function(point) for point in points From 7f6ae239a9f7c915d27c3febebfa45c71f0848ce Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 1 Nov 2022 15:43:49 +0100 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Matthew Treinish --- qiskit/algorithms/eigensolvers/vqd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py index 3a8151c76766..caf1113e3b9e 100644 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ b/qiskit/algorithms/eigensolvers/vqd.py @@ -38,7 +38,7 @@ from ..exceptions import AlgorithmError from ..observables_evaluator import estimate_observables -# private function as we expect this to be updated in the next released +# private function as we expect this to be updated in the next release from ..utils.set_batching import _set_default_batchsize logger = logging.getLogger(__name__)