-
Notifications
You must be signed in to change notification settings - Fork 86
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
Migration guide for providers to implement V2 primitives #2496
Open
ElePT
wants to merge
14
commits into
main
Choose a base branch
from
EPT/primitives-migration-for-providers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
fa864d7
Initial content proposal
ElePT aa48513
Test push
beckykd a77b070
Simplify code snippets, edit introduction text, add placeholders for …
ElePT 15a72bb
Merge branch 'EPT/primitives-migration-for-providers' of https://gith…
ElePT f86e387
Add links
beckykd 4941f0d
more links, qiskit bot, TOC
beckykd 11a469a
local links
beckykd 45328ae
edits
beckykd 0edf399
add link
beckykd 38fe250
Apply suggestions from code review
beckykd 1b0ad76
Apply suggestions from code review
beckykd 7d87696
Update docs/migration-guides/external-providers-primitives-v2.mdx
beckykd ebb7b5b
Add comment to explain type checking change in BackendEstimator example
ElePT 55c4a24
Apply Jessie's suggestions for the motivation section
ElePT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
226 changes: 226 additions & 0 deletions
226
docs/migration-guides/external-providers-primitives-v2.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
--- | ||
title: Migrate provider interfaces from backend.run to primitives | ||
description: Migrate to the primitives interface in external providers | ||
|
||
--- | ||
|
||
# Migrate provider interfaces from `backend.run` to primitives | ||
|
||
## Why implement primitives for external providers? | ||
|
||
Similar to the early days of classical computers, when developers had to manipulate CPU registers directly, | ||
the early interface to QPUs simply returned the raw data coming out of the control electronics. | ||
This was not a huge issue when QPUs lived in labs and only allowed direct access by researchers. | ||
When IBM first brought its QPUs to the cloud, we recognized most developers would not and | ||
should not be familiar with distilling such raw data into 0s and 1s. Therefore, | ||
we introduced `backend.run`, our first abstraction for accessing QPUs. This allowed developers | ||
to operate on a data format they were more familiar with and focus on the bigger picture. | ||
|
||
As access to QPUs became more wide spread, and with more quantum algorithms being developed, | ||
we again recognized the need for a higher-level abstraction. This led to the introduction of | ||
the Qiskit [primitives interface](/guides/primitives), which are optimized for two core tasks in quantum algorithm development: | ||
expectation value estimation (Estimator) and circuit sampling (Sampler). The goal is once | ||
again to help developers to focus more on innovation and less on data conversion. | ||
|
||
For backwards compatibility purposes, the `backend.run` interface continues to exist in Qiskit. However, it is deprecated | ||
in Qiskit Runtime, as most of the IBM Quantum users have migrated to V2 primitives due to their improved | ||
usability and efficiency. There is already a collection of migration guides for users to | ||
[transition to the Qiskit Runtime provider](/migration-guides/qiskit-runtime) and | ||
[update their user code to the V2 primitives interface](/migration-guides/v2-primitives) in | ||
time for the upcoming removal of `backend.run` in Qiskit Runtime. | ||
|
||
This migration guide shifts the focus from users and aims to help service providers to migrate from the | ||
`backend.run` interface to primitives, | ||
so their users can also benefit from their improvements. | ||
|
||
Custom primitive implementations can be used to wrap any service provider hardware access function | ||
(for example: `execute_workload(QPU)` or `resource.access()`) or local simulator, | ||
as long as the final inputs and outputs conform to the established standards set by the primitive interfaces. | ||
|
||
## If your provider already implemented `backend.run` | ||
|
||
The Qiskit SDK offers wrappers for `backend.run` that can be easily adapted to a custom primitives workflow | ||
through subclassing; these are the [`BackendEstimatorV2`](/api/qiskit/qiskit.primitives.BackendEstimatorV2) and [`BackendSamplerV2`.](/api/qiskit/qiskit.primitives.BackendSamplerV2) The inputs | ||
to the primitives should follow the Primitive Unified Bloc (PUB) syntax specified in the V2 primitives interface. See the [Overview of PUBs section](/guides/primitive-input-output#pubs) in the Primitive inputs and outputs guide for details. | ||
|
||
An advantage of this strategy is that the wrapper can handle the input and output manipulation, so knowledge of the PUB data | ||
model is not required. However, this might result in a suboptimal runtime, which could be refined through a fully | ||
custom primitives implementation. | ||
|
||
The following snippets show how to create a custom Estimator instance following the strategy described above. | ||
The process is analogous for a custom Sampler, modifying the base class to `BackendSamplerV2`. | ||
|
||
``` python | ||
from qiskit.primitives import BackendEstimatorV2 | ||
|
||
class CustomEstimator(BackendEstimatorV2): | ||
"""Estimator primitive for custom provider.""" | ||
|
||
# This line is for type checking purposes. | ||
# We are changing the type of self._backend from qiskit's | ||
# BackendV1/BackendV2 classes to our custom provider resource. | ||
_backend: CustomProviderResource | ||
beckykd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def __init__( | ||
self, | ||
backend: CustomProviderResource, | ||
options: dict | None = None, | ||
extra_flag_used_in_estimation: bool = True, | ||
another_extra_flag: bool = False, | ||
) -> None: | ||
""" | ||
Args: | ||
backend: custom provider resource to evaluate circuits on. | ||
options: options passed to through to the underlying BackendEstimatorV2. | ||
extra_flag_used_in_estimation: if `False`, do this. | ||
another_extra_flag: if `True`, do that, | ||
""" | ||
|
||
# preprocess arguments if necessary according to custom flags | ||
processed_backend = ... | ||
processed_options = ... | ||
|
||
super().__init__( | ||
processed_backend, | ||
options=processed_options, | ||
) | ||
|
||
@property | ||
def backend(self) -> CustomProviderResource: | ||
"""Computing resource used for circuit evaluation.""" | ||
return self._backend | ||
``` | ||
|
||
## If your provider didn't implement `backend.run` or you prefer a fully custom implementation | ||
|
||
If a new provider is developed that doesn't conform to the legacy `backend.run` interface, the pre-packaged | ||
wrapper might not be the optimal route for implementing the primitives. Instead, you should implement a particular | ||
instance of the abstract base primitive interfaces ([BaseEstimatorV2](/api/qiskit/qiskit.primitives.BaseEstimatorV2) or [BaseSamplerV2](/api/qiskit/qiskit.primitives.BaseSamplerV2)). This process requires an | ||
understanding of the PUB data model for input and output handling. | ||
|
||
The following snippet shows a minimal example of an implementation of a custom Sampler primitive following this strategy. | ||
This example has been extracted and generalized from the [`StatevectorSampler`](/api/qiskit/qiskit.primitives.StatevectorSampler) implementation. It has been simplified | ||
for readability. The full original implementation can be found in the [`StatevectorSampler` source code.](https://github.com/Qiskit/qiskit/blob/stable/1.3/qiskit/primitives/statevector_sampler.py) | ||
|
||
``` python | ||
from qiskit.primitives.base import BaseSamplerV2 | ||
from qiskit.primitives.containers import ( | ||
BitArray, | ||
DataBin, | ||
PrimitiveResult, | ||
SamplerPubResult, | ||
SamplerPubLike, | ||
) | ||
from qiskit.primitives.containers.sampler_pub import SamplerPub | ||
from qiskit.primitives.primitive_job import PrimitiveJob | ||
... | ||
|
||
class CustomStatevectorSampler(BaseSamplerV2): | ||
|
||
... | ||
|
||
def run( | ||
self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None | ||
) -> PrimitiveJob[PrimitiveResult[SamplerPubResult]]: | ||
... | ||
coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs] | ||
job = PrimitiveJob(self._run, coerced_pubs) | ||
job._submit() | ||
return job | ||
|
||
def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[SamplerPubResult]: | ||
results = [self._run_pub(pub) for pub in pubs] | ||
return PrimitiveResult(results, metadata={"version": 2}) | ||
|
||
def _run_pub(self, pub: SamplerPub) -> SamplerPubResult: | ||
|
||
# pre-processing of the sampling inputs to fit the required format | ||
circuit, qargs, meas_info = _preprocess_circuit(pub.circuit) | ||
bound_circuits = pub.parameter_values.bind_all(circuit) | ||
arrays = { | ||
item.creg_name: np.zeros( | ||
bound_circuits.shape + (pub.shots, item.num_bytes), dtype=np.uint8 | ||
) | ||
for item in meas_info | ||
} | ||
for index, bound_circuit in enumerate(bound_circuits): | ||
|
||
# ACCESS PROVIDER RESOURCE HERE | ||
# in this case, we are showing an illustrative implementation | ||
samples_array = ProviderResource.sample(bound_circuit) | ||
|
||
# post-processing of the sampling output to fit the required format | ||
for item in meas_info: | ||
ary = _samples_to_packed_array(samples_array, item.num_bits, item.qreg_indices) | ||
arrays[item.creg_name][index] = ary | ||
|
||
meas = { | ||
item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info | ||
} | ||
return SamplerPubResult( | ||
DataBin(**meas, shape=pub.shape), | ||
metadata={"shots": pub.shots, "circuit_metadata": pub.circuit.metadata}, | ||
) | ||
``` | ||
|
||
The mechanics to implement a custom Estimator are analogous to those for the Sampler, but might require a different pre- or post-processing | ||
step in the `run` method to extract expectation values from samples. Similar to the Sampler | ||
example, this snippet has been modified and simplified for generality and readability. | ||
The full original implementation can be found in the [`StatevectorEstimator` source code.](https://github.com/Qiskit/qiskit/tree/stable/1.3/qiskit/primitives/statevector_estimator.py#L31-L174) | ||
|
||
``` python | ||
from .base import BaseEstimatorV2 | ||
from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult | ||
from .containers.estimator_pub import EstimatorPub | ||
from .primitive_job import PrimitiveJob | ||
... | ||
|
||
class CustomStatevectorEstimator(BaseEstimatorV2): | ||
|
||
... | ||
|
||
def run( | ||
self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None | ||
) -> PrimitiveJob[PrimitiveResult[PubResult]]: | ||
... | ||
coerced_pubs = [EstimatorPub.coerce(pub, precision) for pub in pubs] | ||
|
||
job = PrimitiveJob(self._run, coerced_pubs) | ||
job._submit() | ||
return job | ||
|
||
def _run(self, pubs: list[EstimatorPub]) -> PrimitiveResult[PubResult]: | ||
return PrimitiveResult([self._run_pub(pub) for pub in pubs], metadata={"version": 2}) | ||
|
||
def _run_pub(self, pub: EstimatorPub) -> PubResult: | ||
rng = np.random.default_rng(self._seed) | ||
circuit = pub.circuit | ||
observables = pub.observables | ||
parameter_values = pub.parameter_values | ||
precision = pub.precision | ||
bound_circuits = parameter_values.bind_all(circuit) | ||
bc_circuits, bc_obs = np.broadcast_arrays(bound_circuits, observables) | ||
evs = np.zeros_like(bc_circuits, dtype=np.float64) | ||
stds = np.zeros_like(bc_circuits, dtype=np.float64) | ||
|
||
for index in np.ndindex(*bc_circuits.shape): | ||
# pre-processing of the sampling inputs to fit the required format | ||
bound_circuit = bc_circuits[index] | ||
observable = bc_obs[index] | ||
paulis, coeffs = zip(*observable.items()) | ||
obs = SparsePauliOp(paulis, coeffs) | ||
|
||
# ACCESS PROVIDER RESOURCE HERE | ||
# in this case, we are showing an illustrative implementation | ||
samples_array = ProviderResource.sample(bound_circuit, rng, precision) | ||
|
||
# post-processing of the sampling output to extract expectation value | ||
expectation_value = compute_expectation_value(samples_array, obs) | ||
evs[index] = expectation_value | ||
|
||
data = DataBin(evs=evs, stds=stds, shape=evs.shape) | ||
return PubResult( | ||
data, metadata={"target_precision": precision, "circuit_metadata": pub.circuit.metadata} | ||
) | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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'm a bit confused by this section... this reads like the target audience is people who use 3rd party providers, and not the providers themselves (e.g. users of
IonQProvider
and not IonQ itself).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 am not too set on having this section, to be honest, that's why I mentioned in the PR description that I wanted a second opinion on this approach. I based it on the only kind of external (still mostly IBMers) provider that I know implemented primitives, that is: https://github.com/qiskit-community/qiskit-aqt-provider/tree/master. They based their primitives on the backend primitives as shown in this example, and I thought it might be a good shortcut for other providers too, but I also think it might be too hacky. If you don't think it adds any value, it could be removed.
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 think we can point out backend primitives as an example, but don't explicitly say they should use it. It's also to their benefit to optimize for their hardware instead of using an out-of-box implementation.
This section would actually be quite helpful in the existing
backend.run
migration guide. If, for example, someone is usingbackend.run
across Qiskit Runtime + Provider Foo and want to migrate to primitives. They can use this as an example to write to primitives interface even if Foo hasn't migrated yet.