This is an automated email from the ASF dual-hosted git repository.

apitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 6c326db6a5 GH-33984: [C++][Python] DLPack implementation for Arrow 
Arrays (producer) (#38472)
6c326db6a5 is described below

commit 6c326db6a5686a78bc77be662b61236ddbfc66dc
Author: Alenka Frim <[email protected]>
AuthorDate: Tue Dec 19 19:58:29 2023 +0100

    GH-33984: [C++][Python] DLPack implementation for Arrow Arrays (producer) 
(#38472)
    
    ### Rationale for this change
    
    DLPack is selected for Array API protocol so it is important to have it 
implemented for Arrow/PyArrow Arrays also. This is possible for primitive type 
arrays (int, uint and float) with no validity buffer. Device support is not in 
scope of this PR (CPU only).
    
    ### What changes are included in this PR?
    
    - `ExportArray` and `ExportDevice` methods on Arrow C++ Arrays
    - `__dlpack__` method on the base PyArrow Array class exposing 
`ExportArray` method
    -  `__dlpack_device__` method on the base PyArrow Array class exposing 
`ExportDevice` method
    
    ### Are these changes tested?
    
    Yes, tests are added to `dlpack_test.cc` and `test_array.py`.
    
    ### Are there any user-facing changes?
    
    No.
    
    * Closes: #33984
    
    Lead-authored-by: AlenkaF <[email protected]>
    Co-authored-by: Alenka Frim <[email protected]>
    Co-authored-by: Antoine Pitrou <[email protected]>
    Co-authored-by: Joris Van den Bossche <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/CMakeLists.txt                |   1 +
 cpp/src/arrow/c/CMakeLists.txt              |   1 +
 cpp/src/arrow/c/dlpack.cc                   | 133 ++++++++++++
 cpp/src/arrow/c/dlpack.h                    |  51 +++++
 cpp/src/arrow/c/dlpack_abi.h                | 321 ++++++++++++++++++++++++++++
 cpp/src/arrow/c/dlpack_test.cc              | 129 +++++++++++
 dev/release/rat_exclude_files.txt           |   1 +
 docs/source/python/dlpack.rst               |  93 ++++++++
 docs/source/python/index.rst                |   1 +
 docs/source/python/interchange_protocol.rst |   6 +-
 python/pyarrow/_dlpack.pxi                  |  46 ++++
 python/pyarrow/array.pxi                    |  38 ++++
 python/pyarrow/includes/libarrow.pxd        |  19 ++
 python/pyarrow/lib.pyx                      |   3 +
 python/pyarrow/tests/test_dlpack.py         | 142 ++++++++++++
 15 files changed, 982 insertions(+), 3 deletions(-)

diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt
index 46a7aa9106..00947c6275 100644
--- a/cpp/src/arrow/CMakeLists.txt
+++ b/cpp/src/arrow/CMakeLists.txt
@@ -192,6 +192,7 @@ set(ARROW_SRCS
     type_traits.cc
     visitor.cc
     c/bridge.cc
+    c/dlpack.cc
     io/buffered.cc
     io/caching.cc
     io/compressed.cc
diff --git a/cpp/src/arrow/c/CMakeLists.txt b/cpp/src/arrow/c/CMakeLists.txt
index 3765477ba0..81a81cd3f1 100644
--- a/cpp/src/arrow/c/CMakeLists.txt
+++ b/cpp/src/arrow/c/CMakeLists.txt
@@ -16,6 +16,7 @@
 # under the License.
 
 add_arrow_test(bridge_test PREFIX "arrow-c")
+add_arrow_test(dlpack_test)
 
 add_arrow_benchmark(bridge_benchmark)
 
diff --git a/cpp/src/arrow/c/dlpack.cc b/cpp/src/arrow/c/dlpack.cc
new file mode 100644
index 0000000000..13ee2761b0
--- /dev/null
+++ b/cpp/src/arrow/c/dlpack.cc
@@ -0,0 +1,133 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "arrow/c/dlpack.h"
+
+#include "arrow/array/array_base.h"
+#include "arrow/c/dlpack_abi.h"
+#include "arrow/device.h"
+#include "arrow/type.h"
+#include "arrow/type_traits.h"
+
+namespace arrow::dlpack {
+
+namespace {
+
+Result<DLDataType> GetDLDataType(const DataType& type) {
+  DLDataType dtype;
+  dtype.lanes = 1;
+  dtype.bits = type.bit_width();
+  switch (type.id()) {
+    case Type::INT8:
+    case Type::INT16:
+    case Type::INT32:
+    case Type::INT64:
+      dtype.code = DLDataTypeCode::kDLInt;
+      return dtype;
+    case Type::UINT8:
+    case Type::UINT16:
+    case Type::UINT32:
+    case Type::UINT64:
+      dtype.code = DLDataTypeCode::kDLUInt;
+      return dtype;
+    case Type::HALF_FLOAT:
+    case Type::FLOAT:
+    case Type::DOUBLE:
+      dtype.code = DLDataTypeCode::kDLFloat;
+      return dtype;
+    case Type::BOOL:
+      // DLPack supports byte-packed boolean values
+      return Status::TypeError("Bit-packed boolean data type not supported by 
DLPack.");
+    default:
+      return Status::TypeError("DataType is not compatible with DLPack spec: ",
+                               type.ToString());
+  }
+}
+
+struct ManagerCtx {
+  std::shared_ptr<ArrayData> array;
+  DLManagedTensor tensor;
+};
+
+}  // namespace
+
+Result<DLManagedTensor*> ExportArray(const std::shared_ptr<Array>& arr) {
+  // Define DLDevice struct nad check if array type is supported
+  // by the DLPack protocol at the same time. Raise TypeError if not.
+  // Supported data types: int, uint, float with no validity buffer.
+  ARROW_ASSIGN_OR_RAISE(auto device, ExportDevice(arr))
+
+  // Define the DLDataType struct
+  const DataType& type = *arr->type();
+  std::shared_ptr<ArrayData> data = arr->data();
+  ARROW_ASSIGN_OR_RAISE(auto dlpack_type, GetDLDataType(type));
+
+  // Create ManagerCtx that will serve as the owner of the DLManagedTensor
+  std::unique_ptr<ManagerCtx> ctx(new ManagerCtx);
+
+  // Define the data pointer to the DLTensor
+  // If array is of length 0, data pointer should be NULL
+  if (arr->length() == 0) {
+    ctx->tensor.dl_tensor.data = NULL;
+  } else {
+    const auto data_offset = data->offset * type.byte_width();
+    ctx->tensor.dl_tensor.data =
+        const_cast<uint8_t*>(data->buffers[1]->data() + data_offset);
+  }
+
+  ctx->tensor.dl_tensor.device = device;
+  ctx->tensor.dl_tensor.ndim = 1;
+  ctx->tensor.dl_tensor.dtype = dlpack_type;
+  ctx->tensor.dl_tensor.shape = const_cast<int64_t*>(&data->length);
+  ctx->tensor.dl_tensor.strides = NULL;
+  ctx->tensor.dl_tensor.byte_offset = 0;
+
+  ctx->array = std::move(data);
+  ctx->tensor.manager_ctx = ctx.get();
+  ctx->tensor.deleter = [](struct DLManagedTensor* self) {
+    delete reinterpret_cast<ManagerCtx*>(self->manager_ctx);
+  };
+  return &ctx.release()->tensor;
+}
+
+Result<DLDevice> ExportDevice(const std::shared_ptr<Array>& arr) {
+  // Check if array is supported by the DLPack protocol.
+  if (arr->null_count() > 0) {
+    return Status::TypeError("Can only use DLPack on arrays with no nulls.");
+  }
+  const DataType& type = *arr->type();
+  if (type.id() == Type::BOOL) {
+    return Status::TypeError("Bit-packed boolean data type not supported by 
DLPack.");
+  }
+  if (!is_integer(type.id()) && !is_floating(type.id())) {
+    return Status::TypeError("DataType is not compatible with DLPack spec: ",
+                             type.ToString());
+  }
+
+  // Define DLDevice struct
+  DLDevice device;
+  if (arr->data()->buffers[1]->device_type() == DeviceAllocationType::kCPU) {
+    device.device_id = 0;
+    device.device_type = DLDeviceType::kDLCPU;
+    return device;
+  } else {
+    return Status::NotImplemented(
+        "DLPack support is implemented only for buffers on CPU device.");
+  }
+}
+
+}  // namespace arrow::dlpack
diff --git a/cpp/src/arrow/c/dlpack.h b/cpp/src/arrow/c/dlpack.h
new file mode 100644
index 0000000000..d11ccfc1fd
--- /dev/null
+++ b/cpp/src/arrow/c/dlpack.h
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include "arrow/array/array_base.h"
+#include "arrow/c/dlpack_abi.h"
+
+namespace arrow::dlpack {
+
+/// \brief Export Arrow array as DLPack tensor.
+///
+/// DLMangedTensor is produced as defined by the DLPack protocol,
+/// see https://dmlc.github.io/dlpack/latest/.
+///
+/// Data types for which the protocol is supported are
+/// integer and floating-point data types.
+///
+/// DLPack protocol only supports arrays with one contiguous
+/// memory region which means Arrow Arrays with validity buffers
+/// are not supported.
+///
+/// \param[in] arr Arrow array
+/// \return DLManagedTensor struct
+ARROW_EXPORT
+Result<DLManagedTensor*> ExportArray(const std::shared_ptr<Array>& arr);
+
+/// \brief Get DLDevice with enumerator specifying the
+/// type of the device data is stored on and index of the
+/// device which is 0 by default for CPU.
+///
+/// \param[in] arr Arrow array
+/// \return DLDevice struct
+ARROW_EXPORT
+Result<DLDevice> ExportDevice(const std::shared_ptr<Array>& arr);
+
+}  // namespace arrow::dlpack
diff --git a/cpp/src/arrow/c/dlpack_abi.h b/cpp/src/arrow/c/dlpack_abi.h
new file mode 100644
index 0000000000..4af557a7ed
--- /dev/null
+++ b/cpp/src/arrow/c/dlpack_abi.h
@@ -0,0 +1,321 @@
+// Taken from:
+// 
https://github.com/dmlc/dlpack/blob/ca4d00ad3e2e0f410eeab3264d21b8a39397f362/include/dlpack/dlpack.h
+/*!
+ *  Copyright (c) 2017 by Contributors
+ * \file dlpack.h
+ * \brief The common header of DLPack.
+ */
+#ifndef DLPACK_DLPACK_H_
+#define DLPACK_DLPACK_H_
+
+/**
+ * \brief Compatibility with C++
+ */
+#ifdef __cplusplus
+#define DLPACK_EXTERN_C extern "C"
+#else
+#define DLPACK_EXTERN_C
+#endif
+
+/*! \brief The current major version of dlpack */
+#define DLPACK_MAJOR_VERSION 1
+
+/*! \brief The current minor version of dlpack */
+#define DLPACK_MINOR_VERSION 0
+
+/*! \brief DLPACK_DLL prefix for windows */
+#ifdef _WIN32
+#ifdef DLPACK_EXPORTS
+#define DLPACK_DLL __declspec(dllexport)
+#else
+#define DLPACK_DLL __declspec(dllimport)
+#endif
+#else
+#define DLPACK_DLL
+#endif
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*!
+ * \brief The DLPack version.
+ *
+ * A change in major version indicates that we have changed the
+ * data layout of the ABI - DLManagedTensorVersioned.
+ *
+ * A change in minor version indicates that we have added new
+ * code, such as a new device type, but the ABI is kept the same.
+ *
+ * If an obtained DLPack tensor has a major version that disagrees
+ * with the version number specified in this header file
+ * (i.e. major != DLPACK_MAJOR_VERSION), the consumer must call the deleter
+ * (and it is safe to do so). It is not safe to access any other fields
+ * as the memory layout will have changed.
+ *
+ * In the case of a minor version mismatch, the tensor can be safely used as
+ * long as the consumer knows how to interpret all fields. Minor version
+ * updates indicate the addition of enumeration values.
+ */
+typedef struct {
+  /*! \brief DLPack major version. */
+  uint32_t major;
+  /*! \brief DLPack minor version. */
+  uint32_t minor;
+} DLPackVersion;
+
+/*!
+ * \brief The device type in DLDevice.
+ */
+#ifdef __cplusplus
+typedef enum : int32_t {
+#else
+typedef enum {
+#endif
+  /*! \brief CPU device */
+  kDLCPU = 1,
+  /*! \brief CUDA GPU device */
+  kDLCUDA = 2,
+  /*!
+   * \brief Pinned CUDA CPU memory by cudaMallocHost
+   */
+  kDLCUDAHost = 3,
+  /*! \brief OpenCL devices. */
+  kDLOpenCL = 4,
+  /*! \brief Vulkan buffer for next generation graphics. */
+  kDLVulkan = 7,
+  /*! \brief Metal for Apple GPU. */
+  kDLMetal = 8,
+  /*! \brief Verilog simulator buffer */
+  kDLVPI = 9,
+  /*! \brief ROCm GPUs for AMD GPUs */
+  kDLROCM = 10,
+  /*!
+   * \brief Pinned ROCm CPU memory allocated by hipMallocHost
+   */
+  kDLROCMHost = 11,
+  /*!
+   * \brief Reserved extension device type,
+   * used for quickly test extension device
+   * The semantics can differ depending on the implementation.
+   */
+  kDLExtDev = 12,
+  /*!
+   * \brief CUDA managed/unified memory allocated by cudaMallocManaged
+   */
+  kDLCUDAManaged = 13,
+  /*!
+   * \brief Unified shared memory allocated on a oneAPI non-partititioned
+   * device. Call to oneAPI runtime is required to determine the device
+   * type, the USM allocation type and the sycl context it is bound to.
+   *
+   */
+  kDLOneAPI = 14,
+  /*! \brief GPU support for next generation WebGPU standard. */
+  kDLWebGPU = 15,
+  /*! \brief Qualcomm Hexagon DSP */
+  kDLHexagon = 16,
+} DLDeviceType;
+
+/*!
+ * \brief A Device for Tensor and operator.
+ */
+typedef struct {
+  /*! \brief The device type used in the device. */
+  DLDeviceType device_type;
+  /*!
+   * \brief The device index.
+   * For vanilla CPU memory, pinned memory, or managed memory, this is set to 
0.
+   */
+  int32_t device_id;
+} DLDevice;
+
+/*!
+ * \brief The type code options DLDataType.
+ */
+typedef enum {
+  /*! \brief signed integer */
+  kDLInt = 0U,
+  /*! \brief unsigned integer */
+  kDLUInt = 1U,
+  /*! \brief IEEE floating point */
+  kDLFloat = 2U,
+  /*!
+   * \brief Opaque handle type, reserved for testing purposes.
+   * Frameworks need to agree on the handle data type for the exchange to be 
well-defined.
+   */
+  kDLOpaqueHandle = 3U,
+  /*! \brief bfloat16 */
+  kDLBfloat = 4U,
+  /*!
+   * \brief complex number
+   * (C/C++/Python layout: compact struct per complex number)
+   */
+  kDLComplex = 5U,
+  /*! \brief boolean */
+  kDLBool = 6U,
+} DLDataTypeCode;
+
+/*!
+ * \brief The data type the tensor can hold. The data type is assumed to 
follow the
+ * native endian-ness. An explicit error message should be raised when 
attempting to
+ * export an array with non-native endianness
+ *
+ *  Examples
+ *   - float: type_code = 2, bits = 32, lanes = 1
+ *   - float4(vectorized 4 float): type_code = 2, bits = 32, lanes = 4
+ *   - int8: type_code = 0, bits = 8, lanes = 1
+ *   - std::complex<float>: type_code = 5, bits = 64, lanes = 1
+ *   - bool: type_code = 6, bits = 8, lanes = 1 (as per common array library 
convention,
+ * the underlying storage size of bool is 8 bits)
+ */
+typedef struct {
+  /*!
+   * \brief Type code of base types.
+   * We keep it uint8_t instead of DLDataTypeCode for minimal memory
+   * footprint, but the value should be one of DLDataTypeCode enum values.
+   * */
+  uint8_t code;
+  /*!
+   * \brief Number of bits, common choices are 8, 16, 32.
+   */
+  uint8_t bits;
+  /*! \brief Number of lanes in the type, used for vector types. */
+  uint16_t lanes;
+} DLDataType;
+
+/*!
+ * \brief Plain C Tensor object, does not manage memory.
+ */
+typedef struct {
+  /*!
+   * \brief The data pointer points to the allocated data. This will be CUDA
+   * device pointer or cl_mem handle in OpenCL. It may be opaque on some device
+   * types. This pointer is always aligned to 256 bytes as in CUDA. The
+   * `byte_offset` field should be used to point to the beginning of the data.
+   *
+   * Note that as of Nov 2021, multiply libraries (CuPy, PyTorch, TensorFlow,
+   * TVM, perhaps others) do not adhere to this 256 byte aligment requirement
+   * on CPU/CUDA/ROCm, and always use `byte_offset=0`.  This must be fixed
+   * (after which this note will be updated); at the moment it is recommended
+   * to not rely on the data pointer being correctly aligned.
+   *
+   * For given DLTensor, the size of memory required to store the contents of
+   * data is calculated as follows:
+   *
+   * \code{.c}
+   * static inline size_t GetDataSize(const DLTensor* t) {
+   *   size_t size = 1;
+   *   for (tvm_index_t i = 0; i < t->ndim; ++i) {
+   *     size *= t->shape[i];
+   *   }
+   *   size *= (t->dtype.bits * t->dtype.lanes + 7) / 8;
+   *   return size;
+   * }
+   * \endcode
+   */
+  void* data;
+  /*! \brief The device of the tensor */
+  DLDevice device;
+  /*! \brief Number of dimensions */
+  int32_t ndim;
+  /*! \brief The data type of the pointer*/
+  DLDataType dtype;
+  /*! \brief The shape of the tensor */
+  int64_t* shape;
+  /*!
+   * \brief strides of the tensor (in number of elements, not bytes)
+   *  can be NULL, indicating tensor is compact and row-majored.
+   */
+  int64_t* strides;
+  /*! \brief The offset in bytes to the beginning pointer to data */
+  uint64_t byte_offset;
+} DLTensor;
+
+/*!
+ * \brief C Tensor object, manage memory of DLTensor. This data structure is
+ *  intended to facilitate the borrowing of DLTensor by another framework. It 
is
+ *  not meant to transfer the tensor. When the borrowing framework doesn't need
+ *  the tensor, it should call the deleter to notify the host that the resource
+ *  is no longer needed.
+ *
+ * \note This data structure is used as Legacy DLManagedTensor
+ *       in DLPack exchange and is deprecated after DLPack v0.8
+ *       Use DLManagedTensorVersioned instead.
+ *       This data structure may get renamed or deleted in future versions.
+ *
+ * \sa DLManagedTensorVersioned
+ */
+typedef struct DLManagedTensor {
+  /*! \brief DLTensor which is being memory managed */
+  DLTensor dl_tensor;
+  /*! \brief the context of the original host framework of DLManagedTensor in
+   *   which DLManagedTensor is used in the framework. It can also be NULL.
+   */
+  void* manager_ctx;
+  /*!
+   * \brief Destructor - this should be called
+   * to destruct the manager_ctx  which backs the DLManagedTensor. It can be
+   * NULL if there is no way for the caller to provide a reasonable destructor.
+   * The destructors deletes the argument self as well.
+   */
+  void (*deleter)(struct DLManagedTensor* self);
+} DLManagedTensor;
+
+// bit masks used in in the DLManagedTensorVersioned
+
+/*! \brief bit mask to indicate that the tensor is read only. */
+#define DLPACK_FLAG_BITMASK_READ_ONLY (1UL << 0UL)
+
+/*!
+ * \brief A versioned and managed C Tensor object, manage memory of DLTensor.
+ *
+ * This data structure is intended to facilitate the borrowing of DLTensor by
+ * another framework. It is not meant to transfer the tensor. When the 
borrowing
+ * framework doesn't need the tensor, it should call the deleter to notify the
+ * host that the resource is no longer needed.
+ *
+ * \note This is the current standard DLPack exchange data structure.
+ */
+struct DLManagedTensorVersioned {
+  /*!
+   * \brief The API and ABI version of the current managed Tensor
+   */
+  DLPackVersion version;
+  /*!
+   * \brief the context of the original host framework.
+   *
+   * Stores DLManagedTensorVersioned is used in the
+   * framework. It can also be NULL.
+   */
+  void* manager_ctx;
+  /*!
+   * \brief Destructor.
+   *
+   * This should be called to destruct manager_ctx which holds the
+   * DLManagedTensorVersioned. It can be NULL if there is no way for the 
caller to provide
+   * a reasonable destructor. The destructors deletes the argument self as 
well.
+   */
+  void (*deleter)(struct DLManagedTensorVersioned* self);
+  /*!
+   * \brief Additional bitmask flags information about the tensor.
+   *
+   * By default the flags should be set to 0.
+   *
+   * \note Future ABI changes should keep everything until this field
+   *       stable, to ensure that deleter can be correctly called.
+   *
+   * \sa DLPACK_FLAG_BITMASK_READ_ONLY
+   */
+  uint64_t flags;
+  /*! \brief DLTensor which is being memory managed */
+  DLTensor dl_tensor;
+};
+
+#ifdef __cplusplus
+}  // DLPACK_EXTERN_C
+#endif
+#endif  // DLPACK_DLPACK_H_
diff --git a/cpp/src/arrow/c/dlpack_test.cc b/cpp/src/arrow/c/dlpack_test.cc
new file mode 100644
index 0000000000..3136506bf3
--- /dev/null
+++ b/cpp/src/arrow/c/dlpack_test.cc
@@ -0,0 +1,129 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include "arrow/array/array_base.h"
+#include "arrow/c/dlpack.h"
+#include "arrow/c/dlpack_abi.h"
+#include "arrow/memory_pool.h"
+#include "arrow/testing/gtest_util.h"
+
+namespace arrow::dlpack {
+
+class TestExportArray : public ::testing::Test {
+ public:
+  void SetUp() {}
+};
+
+void CheckDLTensor(const std::shared_ptr<Array>& arr,
+                   const std::shared_ptr<DataType>& arrow_type,
+                   DLDataTypeCode dlpack_type, int64_t length) {
+  ASSERT_OK_AND_ASSIGN(auto dlmtensor, arrow::dlpack::ExportArray(arr));
+  auto dltensor = dlmtensor->dl_tensor;
+
+  const auto byte_width = arr->type()->byte_width();
+  const auto start = arr->offset() * byte_width;
+  ASSERT_OK_AND_ASSIGN(auto sliced_buffer,
+                       SliceBufferSafe(arr->data()->buffers[1], start));
+  ASSERT_EQ(sliced_buffer->data(), dltensor.data);
+
+  ASSERT_EQ(0, dltensor.byte_offset);
+  ASSERT_EQ(NULL, dltensor.strides);
+  ASSERT_EQ(length, dltensor.shape[0]);
+  ASSERT_EQ(1, dltensor.ndim);
+
+  ASSERT_EQ(dlpack_type, dltensor.dtype.code);
+
+  ASSERT_EQ(arrow_type->bit_width(), dltensor.dtype.bits);
+  ASSERT_EQ(1, dltensor.dtype.lanes);
+  ASSERT_EQ(DLDeviceType::kDLCPU, dltensor.device.device_type);
+  ASSERT_EQ(0, dltensor.device.device_id);
+
+  ASSERT_OK_AND_ASSIGN(auto device, arrow::dlpack::ExportDevice(arr));
+  ASSERT_EQ(DLDeviceType::kDLCPU, device.device_type);
+  ASSERT_EQ(0, device.device_id);
+
+  dlmtensor->deleter(dlmtensor);
+}
+
+TEST_F(TestExportArray, TestSupportedArray) {
+  const std::vector<std::pair<std::shared_ptr<DataType>, DLDataTypeCode>> 
cases = {
+      {int8(), DLDataTypeCode::kDLInt},
+      {uint8(), DLDataTypeCode::kDLUInt},
+      {
+          int16(),
+          DLDataTypeCode::kDLInt,
+      },
+      {uint16(), DLDataTypeCode::kDLUInt},
+      {
+          int32(),
+          DLDataTypeCode::kDLInt,
+      },
+      {uint32(), DLDataTypeCode::kDLUInt},
+      {
+          int64(),
+          DLDataTypeCode::kDLInt,
+      },
+      {uint64(), DLDataTypeCode::kDLUInt},
+      {float16(), DLDataTypeCode::kDLFloat},
+      {float32(), DLDataTypeCode::kDLFloat},
+      {float64(), DLDataTypeCode::kDLFloat}};
+
+  const auto allocated_bytes = arrow::default_memory_pool()->bytes_allocated();
+
+  for (auto [arrow_type, dlpack_type] : cases) {
+    const std::shared_ptr<Array> array =
+        ArrayFromJSON(arrow_type, "[1, 0, 10, 0, 2, 1, 3, 5, 1, 0]");
+    CheckDLTensor(array, arrow_type, dlpack_type, 10);
+    ASSERT_OK_AND_ASSIGN(auto sliced_1, array->SliceSafe(1, 5));
+    CheckDLTensor(sliced_1, arrow_type, dlpack_type, 5);
+    ASSERT_OK_AND_ASSIGN(auto sliced_2, array->SliceSafe(0, 5));
+    CheckDLTensor(sliced_2, arrow_type, dlpack_type, 5);
+    ASSERT_OK_AND_ASSIGN(auto sliced_3, array->SliceSafe(3));
+    CheckDLTensor(sliced_3, arrow_type, dlpack_type, 7);
+  }
+
+  ASSERT_EQ(allocated_bytes, arrow::default_memory_pool()->bytes_allocated());
+}
+
+TEST_F(TestExportArray, TestErrors) {
+  const std::shared_ptr<Array> array_null = ArrayFromJSON(null(), "[]");
+  ASSERT_RAISES_WITH_MESSAGE(TypeError,
+                             "Type error: DataType is not compatible with 
DLPack spec: " +
+                                 array_null->type()->ToString(),
+                             arrow::dlpack::ExportArray(array_null));
+
+  const std::shared_ptr<Array> array_with_null = ArrayFromJSON(int8(), "[1, 
100, null]");
+  ASSERT_RAISES_WITH_MESSAGE(TypeError,
+                             "Type error: Can only use DLPack on arrays with 
no nulls.",
+                             arrow::dlpack::ExportArray(array_with_null));
+
+  const std::shared_ptr<Array> array_string =
+      ArrayFromJSON(utf8(), R"(["itsy", "bitsy", "spider"])");
+  ASSERT_RAISES_WITH_MESSAGE(TypeError,
+                             "Type error: DataType is not compatible with 
DLPack spec: " +
+                                 array_string->type()->ToString(),
+                             arrow::dlpack::ExportArray(array_string));
+
+  const std::shared_ptr<Array> array_boolean = ArrayFromJSON(boolean(), 
"[true, false]");
+  ASSERT_RAISES_WITH_MESSAGE(
+      TypeError, "Type error: Bit-packed boolean data type not supported by 
DLPack.",
+      arrow::dlpack::ExportDevice(array_boolean));
+}
+
+}  // namespace arrow::dlpack
diff --git a/dev/release/rat_exclude_files.txt 
b/dev/release/rat_exclude_files.txt
index ce637bf839..4f86a12afe 100644
--- a/dev/release/rat_exclude_files.txt
+++ b/dev/release/rat_exclude_files.txt
@@ -12,6 +12,7 @@ ci/etc/*.patch
 ci/vcpkg/*.patch
 CHANGELOG.md
 cpp/CHANGELOG_PARQUET.md
+cpp/src/arrow/c/dlpack_abi.h
 cpp/src/arrow/io/mman.h
 cpp/src/arrow/util/random.h
 cpp/src/arrow/status.cc
diff --git a/docs/source/python/dlpack.rst b/docs/source/python/dlpack.rst
new file mode 100644
index 0000000000..f612ebabde
--- /dev/null
+++ b/docs/source/python/dlpack.rst
@@ -0,0 +1,93 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+.. or more contributor license agreements.  See the NOTICE file
+.. distributed with this work for additional information
+.. regarding copyright ownership.  The ASF licenses this file
+.. to you under the Apache License, Version 2.0 (the
+.. "License"); you may not use this file except in compliance
+.. with the License.  You may obtain a copy of the License at
+
+..   http://www.apache.org/licenses/LICENSE-2.0
+
+.. Unless required by applicable law or agreed to in writing,
+.. software distributed under the License is distributed on an
+.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+.. KIND, either express or implied.  See the License for the
+.. specific language governing permissions and limitations
+.. under the License.
+
+.. _pyarrow-dlpack:
+
+The DLPack Protocol
+===================
+
+`The DLPack Protocol <https://github.com/dmlc/dlpack>`_
+is a stable in-memory data structure that allows exchange
+between major frameworks working with multidimensional
+arrays or tensors. It is designed for cross hardware
+support meaning it allows exchange of data on devices other
+than the CPU (e.g. GPU).
+
+DLPack protocol had been
+`selected as the Python array API standard 
<https://data-apis.org/array-api/latest/design_topics/data_interchange.html#dlpack-an-in-memory-tensor-structure>`_
+by the
+`Consortium for Python Data API Standards <https://data-apis.org/>`_
+in order to enable device aware data interchange between array/tensor
+libraries in the Python ecosystem. See more about the standard
+in the
+`protocol documentation <https://data-apis.org/array-api/latest/index.html>`_
+and more about DLPack in the
+`Python Specification for DLPack 
<https://dmlc.github.io/dlpack/latest/python_spec.html#python-spec>`_.
+
+Implementation of DLPack in PyArrow
+-----------------------------------
+
+The producing side of the DLPack Protocol is implemented for ``pa.Array``
+and can be used to interchange data between PyArrow and other tensor
+libraries. Supported data types are integer, unsigned integer and float. The
+protocol has no missing data support meaning PyArrow arrays with
+missing values cannot be transferred through the DLPack
+protocol. Currently, the Arrow implementation of the protocol only supports
+data on a CPU device.
+
+Data interchange syntax of the protocol includes
+
+1. ``from_dlpack(x)``: consuming an array object that implements a
+   ``__dlpack__`` method and creating a new array while sharing the
+   memory.
+
+2. ``__dlpack__(self, stream=None)`` and ``__dlpack_device__``:
+   producing a PyCapsule with the DLPack struct which is called from
+   within ``from_dlpack(x)``.
+
+PyArrow implements the second part of the protocol
+(``__dlpack__(self, stream=None)`` and ``__dlpack_device__``) and can
+thus be consumed by libraries implementing ``from_dlpack``.
+
+Example
+-------
+
+Convert a PyArrow CPU array to NumPy array:
+
+.. code-block::
+
+    >>> import pyarrow as pa
+    >>> array = pa.array([2, 0, 2, 4])
+    <pyarrow.lib.Int64Array object at 0x121fd4880>
+    [
+    2,
+    0,
+    2,
+    4
+    ]
+
+    >>> import numpy as np
+    >>> np.from_dlpack(array)
+    array([2, 0, 2, 4])
+
+Convert a PyArrow CPU array to PyTorch tensor:
+
+.. code-block::
+
+    >>> import torch
+    >>> torch.from_dlpack(array)
+    tensor([2, 0, 2, 4])    
diff --git a/docs/source/python/index.rst b/docs/source/python/index.rst
index 6a3de3d42b..08939bc760 100644
--- a/docs/source/python/index.rst
+++ b/docs/source/python/index.rst
@@ -53,6 +53,7 @@ files into Arrow structures.
    numpy
    pandas
    interchange_protocol
+   dlpack
    timestamps
    orc
    csv
diff --git a/docs/source/python/interchange_protocol.rst 
b/docs/source/python/interchange_protocol.rst
index c354541a67..2a5ec8afed 100644
--- a/docs/source/python/interchange_protocol.rst
+++ b/docs/source/python/interchange_protocol.rst
@@ -37,7 +37,7 @@ libraries in the Python ecosystem. See more about the
 standard in the
 `protocol documentation 
<https://data-apis.org/dataframe-protocol/latest/index.html>`_.
 
-From pyarrow to other libraries: ``__dataframe__()`` method
+From PyArrow to other libraries: ``__dataframe__()`` method
 -----------------------------------------------------------
 
 The ``__dataframe__()`` method creates a new exchange object that
@@ -54,7 +54,7 @@ This is meant to be used by the consumer library when calling
 the ``from_dataframe()`` function and is not meant to be used manually
 by the user.
 
-From other libraries to pyarrow: ``from_dataframe()``
+From other libraries to PyArrow: ``from_dataframe()``
 -----------------------------------------------------
 
 With the ``from_dataframe()`` function, we can construct a 
:class:`pyarrow.Table`
@@ -63,7 +63,7 @@ from any dataframe object that implements the
 protocol.
 
 We can for example take a pandas dataframe and construct a
-pyarrow table with the use of the interchange protocol:
+PyArrow table with the use of the interchange protocol:
 
 .. code-block::
 
diff --git a/python/pyarrow/_dlpack.pxi b/python/pyarrow/_dlpack.pxi
new file mode 100644
index 0000000000..c2f4cff640
--- /dev/null
+++ b/python/pyarrow/_dlpack.pxi
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+cimport cpython
+from cpython.pycapsule cimport PyCapsule_New
+
+
+cdef void dlpack_pycapsule_deleter(object dltensor) noexcept:
+    cdef DLManagedTensor* dlm_tensor
+    cdef PyObject* err_type
+    cdef PyObject* err_value
+    cdef PyObject* err_traceback
+
+    # Do nothing if the capsule has been consumed
+    if cpython.PyCapsule_IsValid(dltensor, "used_dltensor"):
+        return
+
+    # An exception may be in-flight, we must save it in case
+    # we create another one
+    cpython.PyErr_Fetch(&err_type, &err_value, &err_traceback)
+
+    dlm_tensor = <DLManagedTensor*>cpython.PyCapsule_GetPointer(dltensor, 
'dltensor')
+    if dlm_tensor == NULL:
+        cpython.PyErr_WriteUnraisable(dltensor)
+    # The deleter can be NULL if there is no way for the caller
+    # to provide a reasonable destructor
+    elif dlm_tensor.deleter:
+        dlm_tensor.deleter(dlm_tensor)
+        assert (not cpython.PyErr_Occurred())
+
+    # Set the error indicator from err_type, err_value, err_traceback
+    cpython.PyErr_Restore(err_type, err_value, err_traceback)
diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi
index 789e30d3e9..74a196002b 100644
--- a/python/pyarrow/array.pxi
+++ b/python/pyarrow/array.pxi
@@ -1779,6 +1779,44 @@ cdef class Array(_PandasConvertible):
 
         return pyarrow_wrap_array(array)
 
+    def __dlpack__(self, stream=None):
+        """Export a primitive array as a DLPack capsule.
+
+        Parameters
+        ----------
+        stream : int, optional
+            A Python integer representing a pointer to a stream. Currently not 
supported.
+            Stream is provided by the consumer to the producer to instruct the 
producer
+            to ensure that operations can safely be performed on the array.
+
+        Returns
+        -------
+        capsule : PyCapsule
+            A DLPack capsule for the array, pointing to a DLManagedTensor.
+        """
+        if stream is None:
+            dlm_tensor = GetResultValue(ExportToDLPack(self.sp_array))
+
+            return PyCapsule_New(dlm_tensor, 'dltensor', 
dlpack_pycapsule_deleter)
+        else:
+            raise NotImplementedError(
+                "Only stream=None is supported."
+            )
+
+    def __dlpack_device__(self):
+        """
+        Return the DLPack device tuple this arrays resides on.
+
+        Returns
+        -------
+        tuple : Tuple[int, int]
+            Tuple with index specifying the type of the device (where
+            CPU = 1, see cpp/src/arrow/c/dpack_abi.h) and index of the
+            device which is 0 by default for CPU.
+        """
+        device = GetResultValue(ExportDevice(self.sp_array))
+        return device.device_type, device.device_id
+
 
 cdef _array_like_to_pandas(obj, options, types_mapper):
     cdef:
diff --git a/python/pyarrow/includes/libarrow.pxd 
b/python/pyarrow/includes/libarrow.pxd
index 403846a38f..bad5ec606c 100644
--- a/python/pyarrow/includes/libarrow.pxd
+++ b/python/pyarrow/includes/libarrow.pxd
@@ -1199,6 +1199,25 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
     shared_ptr[CScalar] MakeNullScalar(shared_ptr[CDataType] type)
 
 
+cdef extern from "arrow/c/dlpack_abi.h" nogil:
+    ctypedef enum DLDeviceType:
+        kDLCPU = 1
+
+    ctypedef struct DLDevice:
+        DLDeviceType device_type
+        int32_t device_id
+
+    ctypedef struct DLManagedTensor:
+        void (*deleter)(DLManagedTensor*)
+
+
+cdef extern from "arrow/c/dlpack.h" namespace "arrow::dlpack" nogil:
+    CResult[DLManagedTensor*] ExportToDLPack" arrow::dlpack::ExportArray"(
+        const shared_ptr[CArray]& arr)
+
+    CResult[DLDevice] ExportDevice(const shared_ptr[CArray]& arr)
+
+
 cdef extern from "arrow/builder.h" namespace "arrow" nogil:
 
     cdef cppclass CArrayBuilder" arrow::ArrayBuilder":
diff --git a/python/pyarrow/lib.pyx b/python/pyarrow/lib.pyx
index 57fb0f42e3..29a0bed559 100644
--- a/python/pyarrow/lib.pyx
+++ b/python/pyarrow/lib.pyx
@@ -176,6 +176,9 @@ include "table.pxi"
 # Tensors
 include "tensor.pxi"
 
+# DLPack
+include "_dlpack.pxi"
+
 # File IO
 include "io.pxi"
 
diff --git a/python/pyarrow/tests/test_dlpack.py 
b/python/pyarrow/tests/test_dlpack.py
new file mode 100644
index 0000000000..7cf3f4acdb
--- /dev/null
+++ b/python/pyarrow/tests/test_dlpack.py
@@ -0,0 +1,142 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import ctypes
+from functools import wraps
+import pytest
+
+import numpy as np
+
+import pyarrow as pa
+from pyarrow.vendored.version import Version
+
+
+def PyCapsule_IsValid(capsule, name):
+    return ctypes.pythonapi.PyCapsule_IsValid(ctypes.py_object(capsule), name) 
== 1
+
+
+def check_dlpack_export(arr, expected_arr):
+    DLTensor = arr.__dlpack__()
+    assert PyCapsule_IsValid(DLTensor, b"dltensor") is True
+
+    result = np.from_dlpack(arr)
+    np.testing.assert_array_equal(result, expected_arr, strict=True)
+
+    assert arr.__dlpack_device__() == (1, 0)
+
+
+def check_bytes_allocated(f):
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        allocated_bytes = pa.total_allocated_bytes()
+        try:
+            return f(*args, **kwargs)
+        finally:
+            assert pa.total_allocated_bytes() == allocated_bytes
+    return wrapper
+
+
+@check_bytes_allocated
[email protected](
+    ('value_type', 'np_type'),
+    [
+        (pa.uint8(), np.uint8),
+        (pa.uint16(), np.uint16),
+        (pa.uint32(), np.uint32),
+        (pa.uint64(), np.uint64),
+        (pa.int8(), np.int8),
+        (pa.int16(), np.int16),
+        (pa.int32(), np.int32),
+        (pa.int64(), np.int64),
+        (pa.float16(), np.float16),
+        (pa.float32(), np.float32),
+        (pa.float64(), np.float64),
+    ]
+)
+def test_dlpack(value_type, np_type):
+    if Version(np.__version__) < Version("1.24.0"):
+        pytest.skip("No dlpack support in numpy versions older than 1.22.0, "
+                    "strict keyword in assert_array_equal added in numpy 
version "
+                    "1.24.0")
+
+    expected = np.array([1, 2, 3], dtype=np_type)
+    arr = pa.array(expected, type=value_type)
+    check_dlpack_export(arr, expected)
+
+    arr_sliced = arr.slice(1, 1)
+    expected = np.array([2], dtype=np_type)
+    check_dlpack_export(arr_sliced, expected)
+
+    arr_sliced = arr.slice(0, 1)
+    expected = np.array([1], dtype=np_type)
+    check_dlpack_export(arr_sliced, expected)
+
+    arr_sliced = arr.slice(1)
+    expected = np.array([2, 3], dtype=np_type)
+    check_dlpack_export(arr_sliced, expected)
+
+    arr_zero = pa.array([], type=value_type)
+    expected = np.array([], dtype=np_type)
+    check_dlpack_export(arr_zero, expected)
+
+
+def test_dlpack_not_supported():
+    if Version(np.__version__) < Version("1.22.0"):
+        pytest.skip("No dlpack support in numpy versions older than 1.22.0.")
+
+    arr = pa.array([1, None, 3])
+    with pytest.raises(TypeError, match="Can only use DLPack "
+                       "on arrays with no nulls."):
+        np.from_dlpack(arr)
+
+    arr = pa.array(
+        [[0, 1], [3, 4]],
+        type=pa.list_(pa.int32())
+    )
+    with pytest.raises(TypeError, match="DataType is not compatible with 
DLPack spec"):
+        np.from_dlpack(arr)
+
+    arr = pa.array([])
+    with pytest.raises(TypeError, match="DataType is not compatible with 
DLPack spec"):
+        np.from_dlpack(arr)
+
+    # DLPack doesn't support bit-packed boolean values
+    arr = pa.array([True, False, True])
+    with pytest.raises(TypeError, match="Bit-packed boolean data type "
+                       "not supported by DLPack."):
+        np.from_dlpack(arr)
+
+
+def test_dlpack_cuda_not_supported():
+    cuda = pytest.importorskip("pyarrow.cuda")
+
+    schema = pa.schema([pa.field('f0', pa.int16())])
+    a0 = pa.array([1, 2, 3], type=pa.int16())
+    batch = pa.record_batch([a0], schema=schema)
+
+    cbuf = cuda.serialize_record_batch(batch, cuda.Context(0))
+    cbatch = cuda.read_record_batch(cbuf, batch.schema)
+    carr = cbatch["f0"]
+
+    # CudaBuffers not yet supported
+    with pytest.raises(NotImplementedError, match="DLPack support is 
implemented "
+                       "only for buffers on CPU device."):
+        np.from_dlpack(carr)
+
+    with pytest.raises(NotImplementedError, match="DLPack support is 
implemented "
+                       "only for buffers on CPU device."):
+        carr.__dlpack_device__()


Reply via email to