Skip to content

Commit baa5387

Browse files
AlenkaFrok
andauthored
apacheGH-39294: [C++][Python] DLPack on Tensor class (apache#42118)
### Rationale for this change Producing part of DLPack protocol [has been added to Arrow Arrays](apache#33984) but is missing on Arrow Tensor class. ### What changes are included in this PR? This PR adds support for producing DLPack struct and bindings to it in Python: - `ExportTensor` and `ExportTensorDevice` methods on Arrow C++ Tensor class - `__dlpack__` method on PyArrow Tensor class exposing ExportTensor method - `__dlpack_device__` method on PyArrow Tensor class exposing ExportTensorDevice method ### Are these changes tested? Yes ### Are there any user-facing changes? No * GitHub Issue: apache#39294 Lead-authored-by: AlenkaF <frim.alenka@gmail.com> Co-authored-by: Alenka Frim <AlenkaF@users.noreply.github.com> Co-authored-by: Rok Mihevc <rok@mihevc.org> Signed-off-by: AlenkaF <frim.alenka@gmail.com>
1 parent 8f30636 commit baa5387

7 files changed

Lines changed: 240 additions & 12 deletions

File tree

cpp/src/arrow/c/dlpack.cc

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "arrow/array/array_base.h"
2121
#include "arrow/c/dlpack_abi.h"
2222
#include "arrow/device.h"
23+
#include "arrow/tensor.h"
2324
#include "arrow/type.h"
2425
#include "arrow/type_traits.h"
2526

@@ -66,7 +67,7 @@ struct ManagerCtx {
6667
} // namespace
6768

6869
Result<DLManagedTensor*> ExportArray(const std::shared_ptr<Array>& arr) {
69-
// Define DLDevice struct nad check if array type is supported
70+
// Define DLDevice struct and check if array type is supported
7071
// by the DLPack protocol at the same time. Raise TypeError if not.
7172
// Supported data types: int, uint, float with no validity buffer.
7273
ARROW_ASSIGN_OR_RAISE(auto device, ExportDevice(arr))
@@ -77,7 +78,7 @@ Result<DLManagedTensor*> ExportArray(const std::shared_ptr<Array>& arr) {
7778
ARROW_ASSIGN_OR_RAISE(auto dlpack_type, GetDLDataType(type));
7879

7980
// Create ManagerCtx that will serve as the owner of the DLManagedTensor
80-
std::unique_ptr<ManagerCtx> ctx(new ManagerCtx);
81+
auto ctx = std::make_unique<ManagerCtx>();
8182

8283
// Define the data pointer to the DLTensor
8384
// If array is of length 0, data pointer should be NULL
@@ -130,4 +131,71 @@ Result<DLDevice> ExportDevice(const std::shared_ptr<Array>& arr) {
130131
}
131132
}
132133

134+
struct TensorManagerCtx {
135+
std::shared_ptr<Tensor> t;
136+
std::vector<int64_t> strides;
137+
std::vector<int64_t> shape;
138+
DLManagedTensor tensor;
139+
};
140+
141+
Result<DLManagedTensor*> ExportTensor(const std::shared_ptr<Tensor>& t) {
142+
// Define the DLDataType struct
143+
const DataType& type = *t->type();
144+
ARROW_ASSIGN_OR_RAISE(auto dlpack_type, GetDLDataType(type));
145+
146+
// Define DLDevice struct
147+
ARROW_ASSIGN_OR_RAISE(auto device, ExportDevice(t))
148+
149+
// Create TensorManagerCtx that will serve as the owner of the DLManagedTensor
150+
auto ctx = std::make_unique<TensorManagerCtx>();
151+
152+
// Define the data pointer to the DLTensor
153+
// If tensor is of length 0, data pointer should be NULL
154+
if (t->size() == 0) {
155+
ctx->tensor.dl_tensor.data = NULL;
156+
} else {
157+
ctx->tensor.dl_tensor.data = t->raw_mutable_data();
158+
}
159+
160+
ctx->tensor.dl_tensor.device = device;
161+
ctx->tensor.dl_tensor.ndim = t->ndim();
162+
ctx->tensor.dl_tensor.dtype = dlpack_type;
163+
ctx->tensor.dl_tensor.byte_offset = 0;
164+
165+
std::vector<int64_t>& shape_arr = ctx->shape;
166+
shape_arr.reserve(t->ndim());
167+
for (auto i : t->shape()) {
168+
shape_arr.emplace_back(i);
169+
}
170+
ctx->tensor.dl_tensor.shape = shape_arr.data();
171+
172+
std::vector<int64_t>& strides_arr = ctx->strides;
173+
strides_arr.reserve(t->ndim());
174+
auto byte_width = t->type()->byte_width();
175+
for (auto i : t->strides()) {
176+
strides_arr.emplace_back(i / byte_width);
177+
}
178+
ctx->tensor.dl_tensor.strides = strides_arr.data();
179+
180+
ctx->t = std::move(t);
181+
ctx->tensor.manager_ctx = ctx.get();
182+
ctx->tensor.deleter = [](struct DLManagedTensor* self) {
183+
delete reinterpret_cast<TensorManagerCtx*>(self->manager_ctx);
184+
};
185+
return &ctx.release()->tensor;
186+
}
187+
188+
Result<DLDevice> ExportDevice(const std::shared_ptr<Tensor>& t) {
189+
// Define DLDevice struct
190+
DLDevice device;
191+
if (t->data()->device_type() == DeviceAllocationType::kCPU) {
192+
device.device_id = 0;
193+
device.device_type = DLDeviceType::kDLCPU;
194+
return device;
195+
} else {
196+
return Status::NotImplemented(
197+
"DLPack support is implemented only for buffers on CPU device.");
198+
}
199+
}
200+
133201
} // namespace arrow::dlpack

cpp/src/arrow/c/dlpack.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ namespace arrow::dlpack {
3939
ARROW_EXPORT
4040
Result<DLManagedTensor*> ExportArray(const std::shared_ptr<Array>& arr);
4141

42+
ARROW_EXPORT
43+
Result<DLManagedTensor*> ExportTensor(const std::shared_ptr<Tensor>& t);
44+
4245
/// \brief Get DLDevice with enumerator specifying the
4346
/// type of the device data is stored on and index of the
4447
/// device which is 0 by default for CPU.
@@ -48,4 +51,7 @@ Result<DLManagedTensor*> ExportArray(const std::shared_ptr<Array>& arr);
4851
ARROW_EXPORT
4952
Result<DLDevice> ExportDevice(const std::shared_ptr<Array>& arr);
5053

54+
ARROW_EXPORT
55+
Result<DLDevice> ExportDevice(const std::shared_ptr<Tensor>& t);
56+
5157
} // namespace arrow::dlpack

cpp/src/arrow/c/dlpack_test.cc

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "arrow/c/dlpack.h"
2222
#include "arrow/c/dlpack_abi.h"
2323
#include "arrow/memory_pool.h"
24+
#include "arrow/tensor.h"
2425
#include "arrow/testing/gtest_util.h"
2526

2627
namespace arrow::dlpack {
@@ -48,7 +49,6 @@ void CheckDLTensor(const std::shared_ptr<Array>& arr,
4849
ASSERT_EQ(1, dltensor.ndim);
4950

5051
ASSERT_EQ(dlpack_type, dltensor.dtype.code);
51-
5252
ASSERT_EQ(arrow_type->bit_width(), dltensor.dtype.bits);
5353
ASSERT_EQ(1, dltensor.dtype.lanes);
5454
ASSERT_EQ(DLDeviceType::kDLCPU, dltensor.device.device_type);
@@ -126,4 +126,93 @@ TEST_F(TestExportArray, TestErrors) {
126126
arrow::dlpack::ExportDevice(array_boolean));
127127
}
128128

129+
class TestExportTensor : public ::testing::Test {
130+
public:
131+
void SetUp() {}
132+
};
133+
134+
void CheckDLTensor(const std::shared_ptr<Tensor>& t,
135+
const std::shared_ptr<DataType>& tensor_type,
136+
DLDataTypeCode dlpack_type, std::vector<int64_t> shape,
137+
std::vector<int64_t> strides) {
138+
ASSERT_OK_AND_ASSIGN(auto dlmtensor, arrow::dlpack::ExportTensor(t));
139+
auto dltensor = dlmtensor->dl_tensor;
140+
141+
ASSERT_EQ(t->data()->data(), dltensor.data);
142+
ASSERT_EQ(t->ndim(), dltensor.ndim);
143+
ASSERT_EQ(0, dltensor.byte_offset);
144+
for (int i = 0; i < t->ndim(); i++) {
145+
ASSERT_EQ(shape.data()[i], dltensor.shape[i]);
146+
ASSERT_EQ(strides.data()[i], dltensor.strides[i]);
147+
}
148+
149+
ASSERT_EQ(dlpack_type, dltensor.dtype.code);
150+
ASSERT_EQ(tensor_type->bit_width(), dltensor.dtype.bits);
151+
ASSERT_EQ(1, dltensor.dtype.lanes);
152+
ASSERT_EQ(DLDeviceType::kDLCPU, dltensor.device.device_type);
153+
ASSERT_EQ(0, dltensor.device.device_id);
154+
155+
ASSERT_OK_AND_ASSIGN(auto device, arrow::dlpack::ExportDevice(t));
156+
ASSERT_EQ(DLDeviceType::kDLCPU, device.device_type);
157+
ASSERT_EQ(0, device.device_id);
158+
159+
dlmtensor->deleter(dlmtensor);
160+
}
161+
162+
TEST_F(TestExportTensor, TestTensor) {
163+
const std::vector<std::pair<std::shared_ptr<DataType>, DLDataTypeCode>> cases = {
164+
{int8(), DLDataTypeCode::kDLInt},
165+
{uint8(), DLDataTypeCode::kDLUInt},
166+
{
167+
int16(),
168+
DLDataTypeCode::kDLInt,
169+
},
170+
{uint16(), DLDataTypeCode::kDLUInt},
171+
{
172+
int32(),
173+
DLDataTypeCode::kDLInt,
174+
},
175+
{uint32(), DLDataTypeCode::kDLUInt},
176+
{
177+
int64(),
178+
DLDataTypeCode::kDLInt,
179+
},
180+
{uint64(), DLDataTypeCode::kDLUInt},
181+
{float16(), DLDataTypeCode::kDLFloat},
182+
{float32(), DLDataTypeCode::kDLFloat},
183+
{float64(), DLDataTypeCode::kDLFloat}};
184+
185+
const auto allocated_bytes = arrow::default_memory_pool()->bytes_allocated();
186+
187+
for (auto [arrow_type, dlpack_type] : cases) {
188+
std::vector<int64_t> shape = {3, 6};
189+
std::vector<int64_t> dlpack_strides = {6, 1};
190+
std::shared_ptr<Tensor> tensor = TensorFromJSON(
191+
arrow_type, "[1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9]", shape);
192+
193+
CheckDLTensor(tensor, arrow_type, dlpack_type, shape, dlpack_strides);
194+
}
195+
196+
ASSERT_EQ(allocated_bytes, arrow::default_memory_pool()->bytes_allocated());
197+
}
198+
199+
TEST_F(TestExportTensor, TestTensorStrided) {
200+
std::vector<int64_t> shape = {2, 2, 2};
201+
std::vector<int64_t> strides = {sizeof(float) * 4, sizeof(float) * 2,
202+
sizeof(float) * 1};
203+
std::vector<int64_t> dlpack_strides = {4, 2, 1};
204+
std::shared_ptr<Tensor> tensor =
205+
TensorFromJSON(float32(), "[1, 2, 3, 4, 5, 6, 1, 1]", shape, strides);
206+
207+
CheckDLTensor(tensor, float32(), DLDataTypeCode::kDLFloat, shape, dlpack_strides);
208+
209+
std::vector<int64_t> f_strides = {sizeof(float) * 1, sizeof(float) * 2,
210+
sizeof(float) * 4};
211+
std::vector<int64_t> f_dlpack_strides = {1, 2, 4};
212+
std::shared_ptr<Tensor> f_tensor =
213+
TensorFromJSON(float32(), "[1, 2, 3, 4, 5, 6, 1, 1]", shape, f_strides);
214+
215+
CheckDLTensor(f_tensor, float32(), DLDataTypeCode::kDLFloat, shape, f_dlpack_strides);
216+
}
217+
129218
} // namespace arrow::dlpack

python/pyarrow/array.pxi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2144,7 +2144,8 @@ cdef class Array(_PandasConvertible):
21442144
return pyarrow_wrap_array(array)
21452145

21462146
def __dlpack__(self, stream=None):
2147-
"""Export a primitive array as a DLPack capsule.
2147+
"""
2148+
Export a primitive array as a DLPack capsule.
21482149
21492150
Parameters
21502151
----------
@@ -2159,7 +2160,7 @@ cdef class Array(_PandasConvertible):
21592160
A DLPack capsule for the array, pointing to a DLManagedTensor.
21602161
"""
21612162
if stream is None:
2162-
dlm_tensor = GetResultValue(ExportToDLPack(self.sp_array))
2163+
dlm_tensor = GetResultValue(ExportArrayToDLPack(self.sp_array))
21632164

21642165
return PyCapsule_New(dlm_tensor, 'dltensor', dlpack_pycapsule_deleter)
21652166
else:

python/pyarrow/includes/libarrow.pxd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1452,10 +1452,13 @@ cdef extern from "arrow/c/dlpack_abi.h" nogil:
14521452

14531453

14541454
cdef extern from "arrow/c/dlpack.h" namespace "arrow::dlpack" nogil:
1455-
CResult[DLManagedTensor*] ExportToDLPack" arrow::dlpack::ExportArray"(
1455+
CResult[DLManagedTensor*] ExportArrayToDLPack" arrow::dlpack::ExportArray"(
14561456
const shared_ptr[CArray]& arr)
1457+
CResult[DLManagedTensor*] ExportTensorToDLPack" arrow::dlpack::ExportTensor"(
1458+
const shared_ptr[CTensor]& tensor)
14571459

14581460
CResult[DLDevice] ExportDevice(const shared_ptr[CArray]& arr)
1461+
CResult[DLDevice] ExportDevice(const shared_ptr[CTensor]& tensor)
14591462

14601463

14611464
cdef extern from "arrow/builder.h" namespace "arrow" nogil:

python/pyarrow/tensor.pxi

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,45 @@ strides: {self.strides}"""
300300
buffer.strides = <Py_ssize_t *> cp.PyBytes_AsString(self._ssize_t_strides)
301301
buffer.suboffsets = NULL
302302

303+
def __dlpack__(self, stream=None):
304+
"""
305+
Export a Tensor as a DLPack capsule.
306+
307+
Parameters
308+
----------
309+
stream : int, optional
310+
A Python integer representing a pointer to a stream. Currently not supported.
311+
Stream is provided by the consumer to the producer to instruct the producer
312+
to ensure that operations can safely be performed on the array.
313+
314+
Returns
315+
-------
316+
capsule : PyCapsule
317+
A DLPack capsule for the tensor, pointing to a DLManagedTensor.
318+
"""
319+
if stream is None:
320+
dlm_tensor = GetResultValue(ExportTensorToDLPack(self.sp_tensor))
321+
322+
return PyCapsule_New(dlm_tensor, 'dltensor', dlpack_pycapsule_deleter)
323+
else:
324+
raise NotImplementedError(
325+
"Only stream=None is supported."
326+
)
327+
328+
def __dlpack_device__(self):
329+
"""
330+
Return the DLPack device tuple this tensor resides on.
331+
332+
Returns
333+
-------
334+
tuple : Tuple[int, int]
335+
Tuple with index specifying the type of the device (where
336+
CPU = 1, see cpp/src/arrow/c/dpack_abi.h) and index of the
337+
device which is 0 by default for CPU.
338+
"""
339+
device = GetResultValue(ExportDevice(self.sp_tensor))
340+
return device.device_type, device.device_id
341+
303342

304343
ctypedef CSparseCOOIndex* _CSparseCOOIndexPtr
305344

python/pyarrow/tests/test_dlpack.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,13 @@
2020
import gc
2121
import pytest
2222

23-
try:
24-
import numpy as np
25-
except ImportError:
26-
np = None
27-
2823
import pyarrow as pa
2924
from pyarrow.vendored.version import Version
3025

31-
3226
# Marks all of the tests in this module
3327
# Ignore these with pytest ... -m 'not numpy'
3428
pytestmark = pytest.mark.numpy
29+
np = pytest.importorskip("numpy")
3530

3631

3732
def PyCapsule_IsValid(capsule, name):
@@ -87,6 +82,9 @@ def test_dlpack(value_type, np_type_str):
8782
arr = pa.array(expected, type=value_type)
8883
check_dlpack_export(arr, expected)
8984

85+
t = pa.Tensor.from_numpy(expected)
86+
check_dlpack_export(t, expected)
87+
9088
arr_sliced = arr.slice(1, 1)
9189
expected = np.array([2], dtype=np.dtype(np_type_str))
9290
check_dlpack_export(arr_sliced, expected)
@@ -103,6 +101,30 @@ def test_dlpack(value_type, np_type_str):
103101
expected = np.array([], dtype=np.dtype(np_type_str))
104102
check_dlpack_export(arr_zero, expected)
105103

104+
t = pa.Tensor.from_numpy(expected)
105+
check_dlpack_export(t, expected)
106+
107+
108+
@check_bytes_allocated
109+
@pytest.mark.parametrize('np_type',
110+
[np.uint8, np.uint16, np.uint32, np.uint64,
111+
np.int8, np.int16, np.int32, np.int64,
112+
np.float16, np.float32, np.float64,])
113+
def test_tensor_dlpack(np_type):
114+
if Version(np.__version__) < Version("1.24.0"):
115+
pytest.skip("No dlpack support in numpy versions older than 1.22.0, "
116+
"strict keyword in assert_array_equal added in numpy version "
117+
"1.24.0")
118+
119+
arr = np.array([1, 2, 3, 4, 5, 6, 1, 1])
120+
expected = np.array(arr, dtype=np_type).reshape((2, 2, 2), order='C')
121+
t = pa.Tensor.from_numpy(expected)
122+
check_dlpack_export(t, expected)
123+
124+
expected = np.array(arr, dtype=np_type).reshape((2, 2, 2), order='F')
125+
t = pa.Tensor.from_numpy(expected)
126+
check_dlpack_export(t, expected)
127+
106128

107129
def test_dlpack_not_supported():
108130
if Version(np.__version__) < Version("1.22.0"):

0 commit comments

Comments
 (0)