From 50441174b4450e3c800fe4e65ebd4cfab6562ebc Mon Sep 17 00:00:00 2001 From: --get-all Date: Thu, 26 Oct 2023 07:44:31 -0400 Subject: [PATCH 01/10] Initial writeup --- 0016-base-primitive-unification.md | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 0016-base-primitive-unification.md diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md new file mode 100644 index 0000000..8d8691e --- /dev/null +++ b/0016-base-primitive-unification.md @@ -0,0 +1,115 @@ +# Base Primitive and Units of Primitive Work + +| **Status** | **Proposed/Accepted/Deprecated** | +|:------------------|:---------------------------------------------| +| **RFC #** | 0016 | +| **Authors** | Lev Bishop (lsbishop@us.ibm.com) | +| | Ian Hincks (ian.hincks@ibm.com) | +| | Blake Johnson (blake.johnson@ibm.com) | +| | Chris Wood (cjwood@us.ibm.com) | +| **Submitted** | 2023-10-26 | +| **Updated** | YYYY-MM-DD + +## Summary + +The primitives form an execution framework for QPUs and their simulators. +Each primitive specializes at some computational task. +For example, the `Estimator` specializes at computing expectation values of user-provider observables. + +Different types of primitives are implemented as different abstract subclasses of `BasePrimitive` (e.g. `BaseEstimator`), and different primitive implementations as further subclasses (e.g. `qiskit_aer.primitives.Estimator`). +Because the constructor of these subclasses is reserved for implementation-specific setup, the main entry-point to execution is the `.run()` method of a particular class. +However, as it stands, `BasePrimitive` does not provide `.run()` as an abstraction; each type of primitive gets to define its own signature. + +This RFC proposes an abstraction for `BasePrimitive.run()` as well as structures for its inputs and outputs. + +## Motivation + +The central motivation of this RFC is to more clearly emphasize that primitives are a well-defined execution framework on top of which applications can be built. +We aim to force each category of primitive to clearly state what units of quantum work (tasks) it is able perform, and what outputs can be expected. +It is understood that having the proposed abstract base method alone will not necessarily improve the day-to-day life of a typical user of the primitives, and neither will it necessarily improve the ease of implementing new primitive types or implementations: this change is mainly about clarifying the shared nature of the primitives, and reflecting view this in abstractions. + +## User Benefit + +One class of benefited users are application frameworks that are built on top of the primitives, which, following this proposal, will be able to exploit the promise of a homogenous experience across all primitives. +As an example, `qiskit_experiments` defines a suite of common diagnostic, calibration, and characterization experiments. +Currently, it is designed around the `Backend.run` execution interface. +One could imagine that, for example, instead of each `Experiment` providing a `circuits` attribute specifying the circuits to run through `Backend.run`, they could rather each specify a `tasks` attribute indicating which primitive units of work need performed. +The `qiskit_experiments` execution and analysis machinery could then rely on the abstraction of `BasePrimitive.run` when reasoning about dispatching and collecting results. + +## Design Proposal + +We propose the following abstraction, where descriptions of all types can be found in the detailed design section. + +```python +In = TypeVar("In", base=Task) +Out = TypeVar("Out", base=TaskResult) +Shape = Tuple[int, ...] + +class BasePrimitive(ABC, Generic[In, Out]): + @abstractmethod + def run(self, tasks: In | Iterable[In]) -> Job[PrimitiveResult[Out]]: + """Run one or more input tasks and return a result for each one.""" + + @classmethod + def result_type(cls, task: In) -> DataBundle: + """Return the typed data namespace that can be expected if the given + task were to be run.""" + # concrete implementation in terms of cls._output_fields(task) + + @classmethod + @abstractmethod + def _output_fields(cls, task: In) -> Iterable[Tuple[str, type, Shape]]: + """Prescription of field names, types, and shapes for the task.""" +``` + +## Detailed Design + +### Task + +We propose the concept of a _task_, which we define as _a single circuit along with auxiliary data required to execute the circuit relative to the primitive in question_. This concept is general enough that it can be used for all primitive types, current and future, where we stress that what the “auxiliary data” is can vary between primitive types. + +```python +# prototype implementation for the RFC +@dataclass(frozen=True) +class Task: + circuit: QuantumCircuit +``` + +Different primitive types (such as `Estimator`) are intented to subclass this type + +### DataBundle + +A data bundle is a namespace for storing data. +The fields in the namespace, in general, depend on the task that was executed. +For example, a `Sampler` will have fields for each output (as defined by the OpenQASM 3 spec, but in Qiskit, you can currently take "output" to mean the names of the classical registers) of the task's circuit. +The value of each field will store the corresponding data. +The names, types, and shapes of each field in the namespace can be queried before execution via the abstract class method `BasePrimitive.result_type(Task) -> DataBundle`. + +All primitives will store their data in `DataBundle`s. +There will not be subclassing to the effect of `SamplerDataBundle < DataBundle`; look instead to `TaskResult` for such needs. + +### TaskResult + +A `TaskResult` is the result of running a single `Task` and does three things: + + #. Stores a `DataBundle` instance bcontaining the data from execution. + #. Stores the metadata that is possibly implementation-specific, and always specific to the executed task. + #. (subclasses) Contains methods to help transform the data into standard formats, including migration helpers. + +We generally expect each primitive type to define its own subclass of `TaskResult` to accomodate the third item, though `TaskResult` has no methods that need to be abstract. + +We elect to have a special container for the data (`DataBundle`) so as not to pollute the `TaskResult` namespace with task-specific names, to enable a clean `BasePrimitive.result_type` formalism, and to keep data quite distinct from metadata. + +### PrimitiveResult + +`PrimitiveResult` is the type returned by `Job.result()` and is primarily a `Sequence[TaskResult]`, with one entry for each `Task` input into the primitive's run method. + +Secondarily, it has a metadata field for those metadata which are not specific to any single task. + +## Alternative Approaches + +The alternate approach is what currently exists: to not have a common abstraction. + +## Future Extensions + +The future extensions are primarily in the choices that particular primitive types would make to satisfy this proposal. \ No newline at end of file From 9b38ea875e5203483bfa33abfbb9db4c0a0602e9 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 26 Oct 2023 08:36:20 -0400 Subject: [PATCH 02/10] Update 0016-base-primitive-unification.md Co-authored-by: Blake Johnson --- 0016-base-primitive-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index 8d8691e..3bf2e1c 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -26,7 +26,7 @@ This RFC proposes an abstraction for `BasePrimitive.run()` as well as structures The central motivation of this RFC is to more clearly emphasize that primitives are a well-defined execution framework on top of which applications can be built. We aim to force each category of primitive to clearly state what units of quantum work (tasks) it is able perform, and what outputs can be expected. -It is understood that having the proposed abstract base method alone will not necessarily improve the day-to-day life of a typical user of the primitives, and neither will it necessarily improve the ease of implementing new primitive types or implementations: this change is mainly about clarifying the shared nature of the primitives, and reflecting view this in abstractions. +It is understood that having the proposed abstract base method alone will not necessarily improve the day-to-day life of a typical user of the primitives, and neither will it necessarily improve the ease of implementing new primitive types or implementations: this change is mainly about clarifying the shared nature of the primitives, and reflecting this view in abstractions. ## User Benefit From 5bfb392ea26ddf0c98093f90f62e97403e381b13 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 26 Oct 2023 08:36:26 -0400 Subject: [PATCH 03/10] Update 0016-base-primitive-unification.md Co-authored-by: Blake Johnson --- 0016-base-primitive-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index 3bf2e1c..cf924b9 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -92,7 +92,7 @@ There will not be subclassing to the effect of `SamplerDataBundle < DataBundle`; A `TaskResult` is the result of running a single `Task` and does three things: - #. Stores a `DataBundle` instance bcontaining the data from execution. + #. Stores a `DataBundle` instance containing the data from execution. #. Stores the metadata that is possibly implementation-specific, and always specific to the executed task. #. (subclasses) Contains methods to help transform the data into standard formats, including migration helpers. From 76dbfc53bc726d6455edf5b45267f3f769b5fb8e Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 26 Oct 2023 08:36:32 -0400 Subject: [PATCH 04/10] Update 0016-base-primitive-unification.md Co-authored-by: Blake Johnson --- 0016-base-primitive-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index cf924b9..e90cf16 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -75,7 +75,7 @@ class Task: circuit: QuantumCircuit ``` -Different primitive types (such as `Estimator`) are intented to subclass this type +Different primitive types (such as `Estimator`) are intended to subclass this type ### DataBundle From 9766e13e506348bf54f89a56e81c1b0dd647e814 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Fri, 27 Oct 2023 07:00:02 -0400 Subject: [PATCH 05/10] Update 0016-base-primitive-unification.md Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- 0016-base-primitive-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index e90cf16..a4eb931 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -25,7 +25,7 @@ This RFC proposes an abstraction for `BasePrimitive.run()` as well as structures ## Motivation The central motivation of this RFC is to more clearly emphasize that primitives are a well-defined execution framework on top of which applications can be built. -We aim to force each category of primitive to clearly state what units of quantum work (tasks) it is able perform, and what outputs can be expected. +We aim to force each category of primitive to clearly state what units of quantum work (tasks) it is able to perform, and what outputs can be expected. It is understood that having the proposed abstract base method alone will not necessarily improve the day-to-day life of a typical user of the primitives, and neither will it necessarily improve the ease of implementing new primitive types or implementations: this change is mainly about clarifying the shared nature of the primitives, and reflecting this view in abstractions. ## User Benefit From bfce062058ea1ef29fe70b19596ff6098cfe5627 Mon Sep 17 00:00:00 2001 From: --get-all Date: Fri, 27 Oct 2023 07:02:06 -0400 Subject: [PATCH 06/10] correct bullet formatting --- 0016-base-primitive-unification.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index a4eb931..c77e752 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -92,9 +92,9 @@ There will not be subclassing to the effect of `SamplerDataBundle < DataBundle`; A `TaskResult` is the result of running a single `Task` and does three things: - #. Stores a `DataBundle` instance containing the data from execution. - #. Stores the metadata that is possibly implementation-specific, and always specific to the executed task. - #. (subclasses) Contains methods to help transform the data into standard formats, including migration helpers. + * Stores a `DataBundle` instance containing the data from execution. + * Stores the metadata that is possibly implementation-specific, and always specific to the executed task. + * (subclasses) Contains methods to help transform the data into standard formats, including migration helpers. We generally expect each primitive type to define its own subclass of `TaskResult` to accomodate the third item, though `TaskResult` has no methods that need to be abstract. From a317b81de74c881784ec0df6fcd84a75a3b5aba8 Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 6 Nov 2023 09:05:43 -0500 Subject: [PATCH 07/10] Update to make the signature a suggestion/convention, rather than a base abstraction --- 0016-base-primitive-unification.md | 122 ++++++++++++++++++----------- 1 file changed, 76 insertions(+), 46 deletions(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index c77e752..ad1e129 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -17,49 +17,56 @@ Each primitive specializes at some computational task. For example, the `Estimator` specializes at computing expectation values of user-provider observables. Different types of primitives are implemented as different abstract subclasses of `BasePrimitive` (e.g. `BaseEstimator`), and different primitive implementations as further subclasses (e.g. `qiskit_aer.primitives.Estimator`). -Because the constructor of these subclasses is reserved for implementation-specific setup, the main entry-point to execution is the `.run()` method of a particular class. -However, as it stands, `BasePrimitive` does not provide `.run()` as an abstraction; each type of primitive gets to define its own signature. - -This RFC proposes an abstraction for `BasePrimitive.run()` as well as structures for its inputs and outputs. +This RFC proposes the structure and format of various container types that can be used by base primitives to store input and output values. ## Motivation -The central motivation of this RFC is to more clearly emphasize that primitives are a well-defined execution framework on top of which applications can be built. -We aim to force each category of primitive to clearly state what units of quantum work (tasks) it is able to perform, and what outputs can be expected. -It is understood that having the proposed abstract base method alone will not necessarily improve the day-to-day life of a typical user of the primitives, and neither will it necessarily improve the ease of implementing new primitive types or implementations: this change is mainly about clarifying the shared nature of the primitives, and reflecting this view in abstractions. +The central motivation of this RFC is to more clearly emphasize that primitives are an execution framework on top of which applications (not just algorithms) can be built. +We aim to help each type of primitive to clearly state what units of quantum work it is able to perform. +We do this by proposing a set of containers that various `run()` can use (possibly subclassed) as input and output types. +In doing so, we are also proposing a set of names that can help us standardize the way we talk about execution in documentation, and throughout the stack. ## User Benefit -One class of benefited users are application frameworks that are built on top of the primitives, which, following this proposal, will be able to exploit the promise of a homogenous experience across all primitives. -As an example, `qiskit_experiments` defines a suite of common diagnostic, calibration, and characterization experiments. -Currently, it is designed around the `Backend.run` execution interface. -One could imagine that, for example, instead of each `Experiment` providing a `circuits` attribute specifying the circuits to run through `Backend.run`, they could rather each specify a `tasks` attribute indicating which primitive units of work need performed. -The `qiskit_experiments` execution and analysis machinery could then rely on the abstraction of `BasePrimitive.run` when reasoning about dispatching and collecting results. +We expect that users will generally benefit from a more homogenous experience across the primitives, in terms of how they would submit jobs, and in how they would collect the data out of their results. +These containers would also make it possible for base primitives to have multiple, named output types. +This would enable, for example, primitives to have separate output fields for each output variable of a particular circuit, or to take standard error/confidence intervals out of the metadata and put it right alongside the first moment data. ## Design Proposal -We propose the following abstraction, where descriptions of all types can be found in the detailed design section. +We propose to encourage base primitives, like `BaseSampler` and `BaseEstimator`, to have a `run` method typed as ```python -In = TypeVar("In", base=Task) -Out = TypeVar("Out", base=TaskResult) -Shape = Tuple[int, ...] - -class BasePrimitive(ABC, Generic[In, Out]): - @abstractmethod - def run(self, tasks: In | Iterable[In]) -> Job[PrimitiveResult[Out]]: +class BaseFoo: + def run(self, tasks: FooTaskLike | Iterable[FooTaskLike]) -> Job[PrimitiveResult[FooTaskResult]]: """Run one or more input tasks and return a result for each one.""" +``` + +where `FooTask` is equal to or derives from `Task`, `FooTaskLike` is a union type that is easily coercible into a `FooTask` via the static method `FooTask.coerce`, and where `FooTaskResult` derives from `TaskResult`. + +The base containers `PrimitiveResult,`, `Task`, and `TaskResult` are described in the next sections. +`Job[T]` is any `qiskit.provider.JobV1` whose result method returns type `T`. + +Any primitive following the above pattern + +```python +# instantiate the primitive +foo = Foo() + +# runt he primitive with three tasks +job = foo.run([task0, task1, task2]) + +# block for results +result = job.result() - @classmethod - def result_type(cls, task: In) -> DataBundle: - """Return the typed data namespace that can be expected if the given - task were to be run.""" - # concrete implementation in terms of cls._output_fields(task) +# get data from second task +result1 = result[1] - @classmethod - @abstractmethod - def _output_fields(cls, task: In) -> Iterable[Tuple[str, type, Shape]]: - """Prescription of field names, types, and shapes for the task.""" +# get particular data from this result (the available fields depend on the primitive type and task, +# and this example is not proposing any specific names) +alpha_data = result1.data.alpha +beta_data = result1.data.beta +expectation_values = result1.data.expectation_values ``` ## Detailed Design @@ -75,41 +82,64 @@ class Task: circuit: QuantumCircuit ``` -Different primitive types (such as `Estimator`) are intended to subclass this type +Different primitive types (such as `Estimator`) are intended to subclass this class, adding auxiliary fields as +required. -### DataBundle +### DataBin -A data bundle is a namespace for storing data. -The fields in the namespace, in general, depend on the task that was executed. +A data bin is a namespace for storing data. +The fields in the namespace, in general, depend on the task that was executed (not the `type` of the task). For example, a `Sampler` will have fields for each output (as defined by the OpenQASM 3 spec, but in Qiskit, you can currently take "output" to mean the names of the classical registers) of the task's circuit. The value of each field will store the corresponding data. -The names, types, and shapes of each field in the namespace can be queried before execution via the abstract class method `BasePrimitive.result_type(Task) -> DataBundle`. -All primitives will store their data in `DataBundle`s. -There will not be subclassing to the effect of `SamplerDataBundle < DataBundle`; look instead to `TaskResult` for such needs. +All primitives will store their data in `DataBin`s. +There will not be subclassing to the effect of `SamplerDataBin < DataBin`; look instead to `TaskResult` for such needs. + +```python +# Make a new DataBin class. make_data_bin() is analagous to dataclasses.make_dataclass(). +# The shape is an optional argument which indicates that all values are to share the same leading shape. +# To put the following generic example into context, if it helps, imagine that this code lives in the +# BaseSampler implementation, and a data bin class is being created to store the results from +# particular SamplerTask instance whose circuit has two output registers named alpha and beta, and +# that the task itself has shape (5, 4). +data_bin_cls = make_data_bin({"alpha": NDArray[np.float], "beta": NDArray[np.uint8]}, shape=(5, 4)) + +# make an instance with particular data +data_bin = data_bin_cls( + alpha=np.empty((5, 4, 1024), dtype=np.float), + beta=np.empty((5, 4, 1024, 127)) +) + +# access items as attributes +alpha_data = data_bin.alpha + +# access items with subscripts +alpha_data = data_bin["alpha"] +``` ### TaskResult A `TaskResult` is the result of running a single `Task` and does three things: - * Stores a `DataBundle` instance containing the data from execution. + * Stores a `DataBin` instance containing the data from execution. * Stores the metadata that is possibly implementation-specific, and always specific to the executed task. * (subclasses) Contains methods to help transform the data into standard formats, including migration helpers. We generally expect each primitive type to define its own subclass of `TaskResult` to accomodate the third item, though `TaskResult` has no methods that need to be abstract. -We elect to have a special container for the data (`DataBundle`) so as not to pollute the `TaskResult` namespace with task-specific names, to enable a clean `BasePrimitive.result_type` formalism, and to keep data quite distinct from metadata. - -### PrimitiveResult +We elect to have a special container for the data (`DataBin`) so as not to pollute the `TaskResult` namespace with task-specific names, and to keep data quite distinct from metadata. -`PrimitiveResult` is the type returned by `Job.result()` and is primarily a `Sequence[TaskResult]`, with one entry for each `Task` input into the primitive's run method. +```python +# return a DataBin +task_result.data -Secondarily, it has a metadata field for those metadata which are not specific to any single task. +# return a metadata dictionary +task_result.metadata +``` -## Alternative Approaches -The alternate approach is what currently exists: to not have a common abstraction. +### PrimitiveResult -## Future Extensions +`PrimitiveResult` is the type returned by `Job.result()` and is primarily a `Sequence[TaskResult]`, with one entry for each `Task` input into the primitive's run method. -The future extensions are primarily in the choices that particular primitive types would make to satisfy this proposal. \ No newline at end of file +Secondarily, it has a metadata field for those metadata which are not specific to any single task. \ No newline at end of file From a791530b952989528fb76f53bcb8ce3f7122a54c Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Mon, 6 Nov 2023 11:16:23 -0500 Subject: [PATCH 08/10] Update 0016-base-primitive-unification.md Co-authored-by: Blake Johnson --- 0016-base-primitive-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index ad1e129..676e367 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -53,7 +53,7 @@ Any primitive following the above pattern # instantiate the primitive foo = Foo() -# runt he primitive with three tasks +# run the primitive with three tasks job = foo.run([task0, task1, task2]) # block for results From 073f48599266127452859c143b0d47efb6ed5af2 Mon Sep 17 00:00:00 2001 From: --get-all Date: Mon, 6 Nov 2023 11:37:51 -0500 Subject: [PATCH 09/10] fix unfinished sentence --- 0016-base-primitive-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index 676e367..22f7b33 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -47,7 +47,7 @@ where `FooTask` is equal to or derives from `Task`, `FooTaskLike` is a union typ The base containers `PrimitiveResult,`, `Task`, and `TaskResult` are described in the next sections. `Job[T]` is any `qiskit.provider.JobV1` whose result method returns type `T`. -Any primitive following the above pattern +Any primitive following the above pattern could be used as follows: ```python # instantiate the primitive From a1e4827c1741c3d02e2e04cdf85cc39df551aa0e Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Mon, 6 Nov 2023 16:58:48 -0500 Subject: [PATCH 10/10] Update 0016-base-primitive-unification.md Co-authored-by: Matthew Treinish --- 0016-base-primitive-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0016-base-primitive-unification.md b/0016-base-primitive-unification.md index 22f7b33..aeaed15 100644 --- a/0016-base-primitive-unification.md +++ b/0016-base-primitive-unification.md @@ -23,7 +23,7 @@ This RFC proposes the structure and format of various container types that can b The central motivation of this RFC is to more clearly emphasize that primitives are an execution framework on top of which applications (not just algorithms) can be built. We aim to help each type of primitive to clearly state what units of quantum work it is able to perform. -We do this by proposing a set of containers that various `run()` can use (possibly subclassed) as input and output types. +We do this by proposing a set of containers that various `run()` methods can use (possibly subclassed) as input and output types. In doing so, we are also proposing a set of names that can help us standardize the way we talk about execution in documentation, and throughout the stack. ## User Benefit