From 500b1a0de5b8c3b18d0a173c6ce61bb2273b607f Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Tue, 21 Feb 2023 19:58:59 +0000 Subject: [PATCH 1/7] Add versioning support to DLPack APIs xref https://github.com/dmlc/dlpack/issues/116 --- src/array_api_stubs/_draft/array_object.py | 83 ++++++++++++++----- .../_draft/creation_functions.py | 6 ++ 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/array_api_stubs/_draft/array_object.py b/src/array_api_stubs/_draft/array_object.py index cf6adcf3c..28d82eade 100644 --- a/src/array_api_stubs/_draft/array_object.py +++ b/src/array_api_stubs/_draft/array_object.py @@ -278,7 +278,9 @@ def __complex__(self: array, /) -> complex: """ def __dlpack__( - self: array, /, *, stream: Optional[Union[int, Any]] = None + self: array, /, *, + max_version: Optional[tuple[int, int]] = None, + stream: Optional[Union[int, Any]] = None ) -> PyCapsule: """ Exports the array for consumption by :func:`~array_api.from_dlpack` as a DLPack capsule. @@ -287,21 +289,20 @@ def __dlpack__( ---------- self: array array instance. + max_version: Optional[tuple[int, int]] + The maximum DLPack version that the consumer (i.e., the caller of + ``__dlpack__``) supports, in the form ``(major, minor)``. + This method may return that maximum version (recommended if it does + support that), or a different version. stream: Optional[Union[int, Any]] for CUDA and ROCm, a Python integer representing a pointer to a stream, on devices that support streams. ``stream`` is provided by the consumer to the producer to instruct the producer to ensure that operations can safely be performed on the array (e.g., by inserting a dependency between streams via "wait for event"). The pointer must be a positive integer or ``-1``. If ``stream`` is ``-1``, the value may be used by the consumer to signal "producer must not perform any synchronization". The ownership of the stream stays with the consumer. On CPU and other device types without streams, only ``None`` is accepted. For other device types which do have a stream, queue or similar synchronization mechanism, the most appropriate type to use for ``stream`` is not yet determined. E.g., for SYCL one may want to use an object containing an in-order ``cl::sycl::queue``. This is allowed when libraries agree on such a convention, and may be standardized in a future version of this API standard. + .. note:: + Support for a ``stream`` value other than ``None`` is optional and implementation-dependent. - .. note:: - Support for a ``stream`` value other than ``None`` is optional and implementation-dependent. - - - Device-specific notes: - - - .. admonition:: CUDA - :class: note + Device-specific values of ``stream`` for CUDA: - ``None``: producer must assume the legacy default stream (default). - ``1``: the legacy default stream. @@ -309,24 +310,21 @@ def __dlpack__( - ``> 2``: stream number represented as a Python integer. - ``0`` is disallowed due to its ambiguity: ``0`` could mean either ``None``, ``1``, or ``2``. - - .. admonition:: ROCm - :class: note + Device-specific values of ``stream`` for ROCm: - ``None``: producer must assume the legacy default stream (default). - ``0``: the default stream. - ``> 2``: stream number represented as a Python integer. - Using ``1`` and ``2`` is not supported. + .. admonition:: Tip + :class: important - .. admonition:: Tip - :class: important - - It is recommended that implementers explicitly handle streams. If - they use the legacy default stream, specifying ``1`` (CUDA) or ``0`` - (ROCm) is preferred. ``None`` is a safe default for developers who do - not want to think about stream handling at all, potentially at the - cost of more synchronization than necessary. + It is recommended that implementers explicitly handle streams. If + they use the legacy default stream, specifying ``1`` (CUDA) or ``0`` + (ROCm) is preferred. ``None`` is a safe default for developers who do + not want to think about stream handling at all, potentially at the + cost of more synchronization than necessary. Returns ------- @@ -343,9 +341,52 @@ def __dlpack__( Notes ----- + Major DLPack versions represent ABI breaks, minor versions represent + ABI-compatible additions (e.g., new enum values for new data types or + device types). + + The ``max_version`` keyword was introduced in v2023.12, and goes + together with the ``DLManagedTensorVersioned`` struct added in DLPack + 1.0. This keyword may not be used by consumers for some time after + introduction. It is recommended to use this logic in the implementation + of ``__dlpack__``: + + .. code:: python + + if max_version is None: + # Keep and use the DLPack 0.X implementation + # Note: in >= 2 years from now (but ideally as late as + # possible), it's okay to raise BufferError here + else: + # We get to produce `DLManagedTensorVersioned` now + if max_version >= our_own_dlpack_version: + # Consumer understands us, just return a Capsule with our max version + elif max_version[0] == our_own_dlpack_version[0]: + # major versions match, we should still be fine here - + # return our own max version + else: + # if we're at a higher major version internally, did we + # keep an implementation of the older major version around? + # If so, use that. Else, just return our max + # version and let the consumer deal with it. + + And this logic for the producer (i.e., in ``from_dlpack``): + + .. code:: python + + try: + x.__dlpack__(max_version=(1, 0)) + # if it succeeds, store info about capsule name being "dltensor_versioned", + # and needing to set the capsule name to "used_dltensor_versioned" + # when we're done + except TypeError: + x.__dlpack__() .. versionchanged:: 2022.12 Added BufferError. + + .. versionchanged:: 2023.12 + Added the ``max_version`` keyword. """ def __dlpack_device__(self: array, /) -> Tuple[Enum, int]: diff --git a/src/array_api_stubs/_draft/creation_functions.py b/src/array_api_stubs/_draft/creation_functions.py index 8736efc29..7e4262b8f 100644 --- a/src/array_api_stubs/_draft/creation_functions.py +++ b/src/array_api_stubs/_draft/creation_functions.py @@ -212,6 +212,12 @@ def from_dlpack(x: object, /) -> array: :class: note The returned array may be either a copy or a view. See :ref:`data-interchange` for details. + + Notes + ----- + See :meth:`array.__dlpack__` for implementation suggestions for `from_dlpack` in + order to handle DLPack versioning correctly. + """ From 612b99e9b7cdce77b979d13a9f77db1616963d2d Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Tue, 21 Mar 2023 18:30:16 +0000 Subject: [PATCH 2/7] Address review comment, replace ">=2 years" by "from March 2025" --- src/array_api_stubs/_draft/array_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/array_api_stubs/_draft/array_object.py b/src/array_api_stubs/_draft/array_object.py index 28d82eade..0c888b30d 100644 --- a/src/array_api_stubs/_draft/array_object.py +++ b/src/array_api_stubs/_draft/array_object.py @@ -355,7 +355,7 @@ def __dlpack__( if max_version is None: # Keep and use the DLPack 0.X implementation - # Note: in >= 2 years from now (but ideally as late as + # Note: from March 2025 onwards (but ideally as late as # possible), it's okay to raise BufferError here else: # We get to produce `DLManagedTensorVersioned` now From 8e208c98d9f08c9c204fed8a8d73c35783e665ac Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Wed, 7 Feb 2024 11:38:25 -0500 Subject: [PATCH 3/7] nit: re-order --- src/array_api_stubs/_draft/array_object.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/array_api_stubs/_draft/array_object.py b/src/array_api_stubs/_draft/array_object.py index d9f087056..d7f037fed 100644 --- a/src/array_api_stubs/_draft/array_object.py +++ b/src/array_api_stubs/_draft/array_object.py @@ -289,8 +289,8 @@ def __complex__(self: array, /) -> complex: def __dlpack__( self: array, /, *, - max_version: Optional[tuple[int, int]] = None, - stream: Optional[Union[int, Any]] = None + stream: Optional[Union[int, Any]] = None, + max_version: Optional[tuple[int, int]] = None ) -> PyCapsule: """ Exports the array for consumption by :func:`~array_api.from_dlpack` as a DLPack capsule. @@ -299,11 +299,6 @@ def __dlpack__( ---------- self: array array instance. - max_version: Optional[tuple[int, int]] - The maximum DLPack version that the consumer (i.e., the caller of - ``__dlpack__``) supports, in the form ``(major, minor)``. - This method may return that maximum version (recommended if it does - support that), or a different version. stream: Optional[Union[int, Any]] for CUDA and ROCm, a Python integer representing a pointer to a stream, on devices that support streams. ``stream`` is provided by the consumer to the producer to instruct the producer to ensure that operations can safely be performed on the array (e.g., by inserting a dependency between streams via "wait for event"). The pointer must be a positive integer or ``-1``. If ``stream`` is ``-1``, the value may be used by the consumer to signal "producer must not perform any synchronization". The ownership of the stream stays with the consumer. On CPU and other device types without streams, only ``None`` is accepted. @@ -335,6 +330,11 @@ def __dlpack__( (ROCm) is preferred. ``None`` is a safe default for developers who do not want to think about stream handling at all, potentially at the cost of more synchronization than necessary. + max_version: Optional[tuple[int, int]] + The maximum DLPack version that the consumer (i.e., the caller of + ``__dlpack__``) supports, in the form ``(major, minor)``. + This method may return that maximum version (recommended if it does + support that), or a different version. Returns ------- From 1db17179be7ee9196f3bfbeda5165eb950a04a75 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Wed, 7 Feb 2024 12:47:25 -0500 Subject: [PATCH 4/7] improvements & fixes --- src/array_api_stubs/_draft/array_object.py | 47 +++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/array_api_stubs/_draft/array_object.py b/src/array_api_stubs/_draft/array_object.py index d7f037fed..1d9e627a7 100644 --- a/src/array_api_stubs/_draft/array_object.py +++ b/src/array_api_stubs/_draft/array_object.py @@ -300,9 +300,9 @@ def __dlpack__( self: array array instance. stream: Optional[Union[int, Any]] - for CUDA and ROCm, a Python integer representing a pointer to a stream, on devices that support streams. ``stream`` is provided by the consumer to the producer to instruct the producer to ensure that operations can safely be performed on the array (e.g., by inserting a dependency between streams via "wait for event"). The pointer must be a positive integer or ``-1``. If ``stream`` is ``-1``, the value may be used by the consumer to signal "producer must not perform any synchronization". The ownership of the stream stays with the consumer. On CPU and other device types without streams, only ``None`` is accepted. + for CUDA and ROCm, a Python integer representing a pointer to a stream, on devices that support streams. ``stream`` is provided by the consumer to the producer to instruct the producer to ensure that operations can safely be performed on the array (e.g., by inserting a dependency between streams via "wait for event"). The pointer must be an integer larger than or equal to ``-1`` (see below for allowed values on each platform). If ``stream`` is ``-1``, the value may be used by the consumer to signal "producer must not perform any synchronization". The ownership of the stream stays with the consumer. On CPU and other device types without streams, only ``None`` is accepted. - For other device types which do have a stream, queue or similar synchronization mechanism, the most appropriate type to use for ``stream`` is not yet determined. E.g., for SYCL one may want to use an object containing an in-order ``cl::sycl::queue``. This is allowed when libraries agree on such a convention, and may be standardized in a future version of this API standard. + For other device types which do have a stream, queue, or similar synchronization/ordering mechanism, the most appropriate type to use for ``stream`` is not yet determined. E.g., for SYCL one may want to use an object containing an in-order ``cl::sycl::queue``. This is allowed when libraries agree on such a convention, and may be standardized in a future version of this API standard. .. note:: Support for a ``stream`` value other than ``None`` is optional and implementation-dependent. @@ -329,12 +329,12 @@ def __dlpack__( they use the legacy default stream, specifying ``1`` (CUDA) or ``0`` (ROCm) is preferred. ``None`` is a safe default for developers who do not want to think about stream handling at all, potentially at the - cost of more synchronization than necessary. + cost of more synchronizations than necessary. max_version: Optional[tuple[int, int]] - The maximum DLPack version that the consumer (i.e., the caller of - ``__dlpack__``) supports, in the form ``(major, minor)``. - This method may return that maximum version (recommended if it does - support that), or a different version. + The maximum DLPack version that the *consumer* (i.e., the caller of + ``__dlpack__``) supports, in the form of a 2-tuple ``(major, minor)``. + This method may return a capsule of version ``max_version`` (recommended + if it does support that), or of a different version. Returns ------- @@ -351,14 +351,17 @@ def __dlpack__( Notes ----- - Major DLPack versions represent ABI breaks, minor versions represent - ABI-compatible additions (e.g., new enum values for new data types or - device types). + The DLPack version scheme is SemVer, where the major DLPack versions + represent ABI breaks, and minor versions represent ABI-compatible additions + (e.g., new enum values for new data types or device types). The ``max_version`` keyword was introduced in v2023.12, and goes together with the ``DLManagedTensorVersioned`` struct added in DLPack - 1.0. This keyword may not be used by consumers for some time after - introduction. It is recommended to use this logic in the implementation + 1.0. This keyword may not be used by consumers until a later time after + introduction, because producers may implement the support at a different + point in time. + + It is recommended for the producer to use this logic in the implementation of ``__dlpack__``: .. code:: python @@ -368,7 +371,10 @@ def __dlpack__( # Note: from March 2025 onwards (but ideally as late as # possible), it's okay to raise BufferError here else: - # We get to produce `DLManagedTensorVersioned` now + # We get to produce `DLManagedTensorVersioned` now. Note that + # our_own_dlpack_version is the max version that the *producer* + # supports and fills in the `DLManagedTensorVersioned::version` + # field if max_version >= our_own_dlpack_version: # Consumer understands us, just return a Capsule with our max version elif max_version[0] == our_own_dlpack_version[0]: @@ -377,18 +383,21 @@ def __dlpack__( else: # if we're at a higher major version internally, did we # keep an implementation of the older major version around? - # If so, use that. Else, just return our max - # version and let the consumer deal with it. + # For example, if the producer is on DLPack 1.x and the consumer + # is 0.y, can the producer still export a capsule containing + # DLManagedTensor and not DLManagedTensorVersioned? + # If so, use that. Else, the producer should raise a BufferError + # here to tell users that the consumer's max_version is too + # old to allow the data exchange to happen. - And this logic for the producer (i.e., in ``from_dlpack``): + And this logic for the consumer in ``from_dlpack``: .. code:: python try: x.__dlpack__(max_version=(1, 0)) - # if it succeeds, store info about capsule name being "dltensor_versioned", - # and needing to set the capsule name to "used_dltensor_versioned" - # when we're done + # if it succeeds, store info from the capsule named "dltensor_versioned", + # and need to set the name to "used_dltensor_versioned" when we're done except TypeError: x.__dlpack__() From 1648db5f6206d46a07b8d02a48ce3ef77b3cbe61 Mon Sep 17 00:00:00 2001 From: Athan Date: Thu, 8 Feb 2024 05:01:39 -0800 Subject: [PATCH 5/7] Satisfy linter --- src/array_api_stubs/_draft/array_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/array_api_stubs/_draft/array_object.py b/src/array_api_stubs/_draft/array_object.py index 1d9e627a7..d5515d16b 100644 --- a/src/array_api_stubs/_draft/array_object.py +++ b/src/array_api_stubs/_draft/array_object.py @@ -290,7 +290,7 @@ def __complex__(self: array, /) -> complex: def __dlpack__( self: array, /, *, stream: Optional[Union[int, Any]] = None, - max_version: Optional[tuple[int, int]] = None + max_version: Optional[tuple[int, int]] = None, ) -> PyCapsule: """ Exports the array for consumption by :func:`~array_api.from_dlpack` as a DLPack capsule. From 7909b3c336cf810286dc7a30e646bd04299ffbb5 Mon Sep 17 00:00:00 2001 From: Athan Date: Thu, 8 Feb 2024 05:04:17 -0800 Subject: [PATCH 6/7] Satisfy linter --- src/array_api_stubs/_draft/array_object.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/array_api_stubs/_draft/array_object.py b/src/array_api_stubs/_draft/array_object.py index d5515d16b..b27bdbb0d 100644 --- a/src/array_api_stubs/_draft/array_object.py +++ b/src/array_api_stubs/_draft/array_object.py @@ -288,7 +288,9 @@ def __complex__(self: array, /) -> complex: """ def __dlpack__( - self: array, /, *, + self: array, + /, + *, stream: Optional[Union[int, Any]] = None, max_version: Optional[tuple[int, int]] = None, ) -> PyCapsule: From 437526b5802aa9a23f1d6a74dcfa869db8b64ba8 Mon Sep 17 00:00:00 2001 From: Leo Fang Date: Thu, 8 Feb 2024 11:04:34 -0500 Subject: [PATCH 7/7] Update src/array_api_stubs/_draft/array_object.py Co-authored-by: Sebastian Berg --- src/array_api_stubs/_draft/array_object.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/array_api_stubs/_draft/array_object.py b/src/array_api_stubs/_draft/array_object.py index b27bdbb0d..9f8be2479 100644 --- a/src/array_api_stubs/_draft/array_object.py +++ b/src/array_api_stubs/_draft/array_object.py @@ -337,6 +337,8 @@ def __dlpack__( ``__dlpack__``) supports, in the form of a 2-tuple ``(major, minor)``. This method may return a capsule of version ``max_version`` (recommended if it does support that), or of a different version. + This means the consumer must verify the version even when + `max_version` is passed. Returns -------