GH-39295: [C++][Python] ConsumingDLPack on Arrays and Tensor - #51122
Conversation
|
|
8d1eca2 to
2fe859d
Compare
There was a problem hiding this comment.
🟡 Changes recommended
There is a likely C++ compile-breaking scoping issue in cpp/src/arrow/tensor.cc around the stride helper functions, plus several doc/API consistency issues to resolve.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds DLPack consumer support to Arrow Arrays/Tensors in C++ and exposes new Python factory APIs (Array.from_dlpack, Tensor.from_dlpack), enabling zero-copy (or requested-copy) imports from DLPack-producing libraries (e.g., NumPy, PyTorch).
Changes:
- Add C++ DLPack consumers:
arrow::dlpack::ImportArrayVersionedandarrow::dlpack::ImportTensorVersioned, plus supporting utilities and tests. - Expose Python APIs for importing via versioned DLPack capsules, and bind
FixedShapeTensorArray::FromTensor. - Add Python test coverage for tensor/array import, copy vs zero-copy behavior, and unsupported cases.
File summaries
| File | Description |
|---|---|
| python/pyarrow/tests/test_dlpack.py | Adds Python tests for Tensor.from_dlpack / Array.from_dlpack, including copy semantics and unsupported multidim array import. |
| python/pyarrow/tensor.pxi | Implements Tensor.from_dlpack and wires it to C++ ImportTensorVersioned. |
| python/pyarrow/includes/libarrow.pxd | Declares DLPack version struct/constant and new C++ import APIs for Cython bindings; adds FixedShapeTensorArray::FromTensor. |
| python/pyarrow/array.pxi | Implements Array.from_dlpack and adds FixedShapeTensorArray.from_tensor binding. |
| cpp/src/arrow/tensor.h | Declares ComputeTensorSize helper for stride/shape-based size computation. |
| cpp/src/arrow/tensor.cc | Defines ComputeTensorSize and adjusts internal stride helper placement/calls. |
| cpp/src/arrow/c/dlpack.h | Exposes compiled DLPack version constant and declares new import APIs. |
| cpp/src/arrow/c/dlpack.cc | Implements DLPack version constant and consumer-side import logic for arrays/tensors. |
| cpp/src/arrow/c/dlpack_test.cc | Adds comprehensive C++ tests for importing (ownership, copy vs share, dtype coverage, errors). |
Review details
Suppressed comments (1)
cpp/src/arrow/c/dlpack.h:132
- The parameter doc for
ImportTensorVersionedsaysrawis an "Arrow array", but the function takes aDLManagedTensorVersioned*(a DLPack tensor).
/// \param[in] raw Arrow array
- Files reviewed: 9/9 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
There are correctness and robustness issues in the new DLPack consumer path (notably error type mismatch for unsupported array layouts, plus missing input validation that can crash on malformed tensors).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 4
- Review effort level: Lite
c940592 to
625aab0
Compare
There was a problem hiding this comment.
🟡 Changes recommended
There is a confirmed C++ compilation issue in tensor.cc, and the Python copy= contract isn’t enforced by the current from_dlpack implementations.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
cpp/src/arrow/tensor.cc:563
Tensor::is_row_major()/is_column_major()callinternal::IsTensorStridesRowMajor/ColumnMajor, but those helpers are defined inside an anonymous namespace nested underarrow::internal(in this translation unit), soarrow::internal::IsTensorStridesRowMajordoesn't exist. This will fail to compile. Consider computing the expected row/column-major strides directly in these methods (or moving the helpers out of the anonymous namespace).
bool Tensor::is_row_major() const {
return internal::IsTensorStridesRowMajor(type_, shape_, strides_);
}
- Files reviewed: 9/9 changed files
- Comments generated: 3
- Review effort level: Lite
625aab0 to
496604c
Compare
There was a problem hiding this comment.
🟡 Changes recommended
There are compile- and correctness-blocking issues in the C++ tensor stride helper linkage and in the Cython from_dlpack implementations (nogil assignment and unchecked capsule consumption marking).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
cpp/src/arrow/tensor.cc:227
Tensor::is_row_major()/is_column_major()callinternal::IsTensorStridesRowMajor/ColumnMajor, but those functions are currently only defined in an anonymous namespace (notarrow::internal), so this won’t compile. DefineIsTensorStridesRowMajorandIsTensorStridesColumnMajorinnamespace internal(next toIsTensorStridesContiguous) or revert the qualified calls.
} // namespace
} // namespace internal
namespace internal {
- Files reviewed: 9/9 changed files
- Comments generated: 3
- Review effort level: Lite
| {data, nbytes}, default_cpu_memory_manager())); | ||
| } else { | ||
| const bool readonly = dl.is_readonly(); | ||
| // Trick to keep DLPack data alive taken from `Buffer::FromVector`. |
There was a problem hiding this comment.
A new Buffer subclass would be cleaner than this IMHO.
There was a problem hiding this comment.
That said, I understand that the subclassing strategy might not be scalable once we want to support non-CPU devices.
There was a problem hiding this comment.
I does seem a bit overkill to subclass for that purpose.
What about adding Buffer::TakeOwnership(T&& container, uint8_t const* data, int64_t nbytes); (where data points into container)? I can see it being universally useful to a C++ user.
| /// producer. | ||
| /// \return An Arrow Array | ||
| ARROW_EXPORT | ||
| Result<std::shared_ptr<Array>> ImportArrayVersioned(DLManagedTensorVersioned* raw, |
There was a problem hiding this comment.
Is the "Versioned" suffix useful? We're not exposing any non-versioned API, and even then, we could just rely on function overloading.
There was a problem hiding this comment.
Mostly to match the producer side ExportArrayVersioned, I feel it easier to match the importing and exporting functions that way.
| } else if (copy) { | ||
| ARROW_ASSIGN_OR_RAISE(buffer, MutableBuffer::CopyNonOwned( | ||
| {data, nbytes}, default_cpu_memory_manager())); | ||
| } else { |
There was a problem hiding this comment.
Presumably, a caller setting copy wants to get an exclusive mutable copy of the tensor.
Should we then inspect the DLPack flags for DLPACK_FLAG_BITMASK_IS_COPIED? In that case, a new copy doesn't need to be made, I think?
(also check that DLPACK_FLAG_BITMASK_READ_ONLY is not set?)
There was a problem hiding this comment.
@pitrou I've been thinking with this and:
- First we do not need the copy parameter in
ImportArraybecause we already can passcopyto the producer (which is what we do in Python). - Then without
copyI believe we have no use forDLPACK_FLAG_BITMASK_IS_COPIED.
What do you think, should we keep our copy parameter (currently unused) or always rely on the producer to copy?
There was a problem hiding this comment.
If it's currently unused then let's remove it for now?
On the Python side, as you say, __dlpack__ is called first.
If there are some direct C/C++ uses of DLPack, then we may want to revisit later.
| x : object | ||
| The input object containing array data, following the DLPack | ||
| protocol (has a ``__dlpack__`` method). | ||
| device : tuple[enum.Enum, int], optional |
There was a problem hiding this comment.
PyArrow also has its own Device class, can we add a TODO and/or open an issue to support it here?
>>> mm = pa.default_cpu_memory_manager()
>>> mm.device
<pyarrow.Device: CPUDevice()>
>>> mm.device.device_type
<DeviceAllocationType.CPU: 1>
>>> import pyarrow.cuda as cu
>>> ctx = cu.Context()
>>> ctx.device
<pyarrow.Device: CudaDevice(device_number=0, name="NVIDIA GeForce GT 1030")>
>>> ctx.device.device_type
<DeviceAllocationType.CUDA: 2>There was a problem hiding this comment.
That would be outside the DLPack spec. Do you think its reasonable?
There was a problem hiding this comment.
Well NumPy supports a string, PyTorch supports a torch.device, JAX supports a _jax.Device. I think PyArrow can support a PyArrow device :)
b9eb12e to
381e5ea
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new C++ DLPack consumer treats ndim == 0 tensors as having 0 elements, which breaks scalar tensor imports (should be 1 element).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
There is at least one confirmed build-breaking issue in cpp/src/arrow/tensor.cc plus a couple of missing error-handling checks that could lead to undefined behavior or double-deletion in DLPack import paths.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
cpp/src/arrow/tensor.cc:566
Tensor::is_row_major()/Tensor::is_column_major()callinternal::IsTensorStridesRowMajor/ColumnMajor, but those helpers are not defined inarrow::internal(they are currently in an unnamed namespace), so this won’t compile. Implement these checks inline usinginternal::ComputeRowMajorStrides/internal::ComputeColumnMajorStrides(or alternatively move the helpers out of the unnamed namespace intoarrow::internal).
return internal::IsTensorStridesRowMajor(type_, shape_, strides_);
}
bool Tensor::is_column_major() const {
return internal::IsTensorStridesColumnMajor(type_, shape_, strides_);
python/pyarrow/array.pxi:2319
PyCapsule_SetName()can fail (returns non-zero and sets an exception). If that happens, this code will still proceed to call the C++ importer with a pending Python error and with the capsule possibly not marked as consumed, which risks double-deletion when the capsule is later GC’d. Check the return value and abort on failure.
# Mark the capsule as consumed so its destructor does not also invoke the deleter.
# ImportArrayVersionedFromDLPack will take ownership even if it errors (calling
# the deleter in that case).
PyCapsule_SetName(pycapsule, "used_dltensor_versioned")
with nogil:
python/pyarrow/tensor.pxi:348
PyCapsule_SetName()can fail (returns non-zero and sets an exception). If that happens, this code will still proceed to call the C++ importer with a pending Python error and with the capsule possibly not marked as consumed, which risks double-deletion when the capsule is later GC’d. Check the return value and abort on failure.
# Mark the capsule as consumed so its destructor does not also invoke the deleter.
# ImportTensorVersionedFromDLPack will take ownership even if it errors (calling
# the deleter in that case).
PyCapsule_SetName(pycapsule, "used_dltensor_versioned")
with nogil:
- Files reviewed: 14/14 changed files
- Comments generated: 1
- Review effort level: Lite
|
@AntoinePrv I see the last comment from @pitrou has been addressed and feel this is ready to be merged if I am not missing anything. |
pitrou
left a comment
There was a problem hiding this comment.
Thanks for the updates @AntoinePrv ! This LGTM, we can merge if CI is green.
|
CI failures are unrelated. |
Rationale for this change
There are already utilities to import tensor data from NumPy.
With DLPack standard, it will work across many tensor providers.
What changes are included in this PR?
FixedShapedTensorArray::FromTensorin PythonImportArrayVersionedFromDLPackandImportTensorVersionedFromDLPackin C++Array.from_dlpackandTensor.from_dlpackSimilar to the export, multidimensional tensor import require an explicit step through
Tensor.Array::FromTensoror evenFixedSizeListArray::FromTensor. IMHO it does not feel as necessary in this direction but open to anyone's take one it.Are these changes tested?
Yes.
Are there any user-facing changes?
Yes, new APIs.