Skip to main contentIBM Quantum Documentation Preview
This is a preview build of IBM Quantumâ„¢ documentation. Refer to docs.quantum.ibm.com for the official documentation.

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 widespread, 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, 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 backward compatibility, 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 and update their user code to the V2 primitives interface 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 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 or 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 implementation. It has been simplified for readability. The full original implementation can be found in the StatevectorSampler source code.

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.

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}
        )