Skip to content

GH-38868: [C++][Python] Add Array::ToTensor and fixed size list support - #50929

Open
AntoinePrv wants to merge 24 commits into
apache:mainfrom
AntoinePrv:dl-to-tensor
Open

GH-38868: [C++][Python] Add Array::ToTensor and fixed size list support#50929
AntoinePrv wants to merge 24 commits into
apache:mainfrom
AntoinePrv:dl-to-tensor

Conversation

@AntoinePrv

@AntoinePrv AntoinePrv commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Rationale for this change

Enable multidimensional DLPack support for Array via to_tensor.

What changes are included in this PR?

  • Add virtual Array::ToTensor
  • Add NumericArray::ToTensor for 1D arrays
  • Add FixedSizeListArray::ToTensor for multidimensional arrays
  • Add DLPack error suggestiong tensor convertion
  • Add DLPack tests with arr.to_tensor().__dlpack__()

Note: Nulls are explicitly supported in to_tensor as unspecified data. This was the current behaviour.

Are these changes tested?

Yes

Are there any user-facing changes?

New public Array function.

Copilot AI lite review requested due to automatic review settings August 20, 2026 14:23
@github-actions github-actions Bot added the awaiting review Awaiting review label Aug 20, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #38868 has been automatically assigned in GitHub to PR creator.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new public Array::ToTensor API (C++ and Python) to enable exporting multidimensional array-like data as Tensor, and updates DLPack export paths and tests to use to_tensor() for multidimensional support (notably nested FixedSizeListArray and FixedShapeTensorArray).

Changes:

  • Add virtual Array::ToTensor plus concrete implementations for 1D numeric arrays and (nested) fixed-size list arrays; route FixedShapeTensorArray::ToTensor through the base virtual.
  • Refactor tensor stride utilities (row-major stride computation) and simplify DLPack device handling; update DLPack type errors to suggest Tensor conversion.
  • Add/extend C++ and Python test coverage for to_tensor().__dlpack__() on multidimensional inputs.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
python/pyarrow/tests/test_dlpack.py Adds multidimensional array-to-tensor DLPack export tests via arr.to_tensor()
python/pyarrow/includes/libarrow.pxd Exposes Array::ToTensor() at the Cython API layer
python/pyarrow/array.pxi Adds Array.to_tensor() Python API and routes FixedShapeTensorArray.to_tensor() through it
cpp/src/arrow/tensor.h Updates stride utilities API and adds std::span overload for row-major strides
cpp/src/arrow/tensor.cc Refactors row-major stride computation implementation
cpp/src/arrow/extension/fixed_shape_tensor.h Makes FixedShapeTensorArray::ToTensor() override the new virtual
cpp/src/arrow/extension/fixed_shape_tensor.cc Updates ToTensor() signature to match override
cpp/src/arrow/c/dlpack.cc Refactors DLPack export (device factoring, type checks, array offset/length handling) and updates type errors
cpp/src/arrow/c/dlpack_test.cc Updates DLPack tests to validate shape/strides and revised ExportDevice behavior
cpp/src/arrow/array/array_test.cc Adds C++ unit tests for Array::ToTensor() on primitive arrays
cpp/src/arrow/array/array_primitive.h Implements NumericArray::ToTensor() for 1D numeric arrays
cpp/src/arrow/array/array_nested.h Declares FixedSizeListArray::ToTensor() API
cpp/src/arrow/array/array_nested.cc Implements FixedSizeListArray::ToTensor() with nested fixed-size list support
cpp/src/arrow/array/array_list_test.cc Adds tests for FixedSizeListArray::ToTensor() including nesting, slicing, and null handling
cpp/src/arrow/array/array_base.h Declares new virtual Array::ToTensor() API
cpp/src/arrow/array/array_base.cc Provides default Array::ToTensor() NotImplemented behavior

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cpp/src/arrow/c/dlpack_test.cc
Comment thread cpp/src/arrow/c/dlpack.cc Outdated
Comment thread cpp/src/arrow/array/array_primitive.h Outdated
Comment thread cpp/src/arrow/array/array_nested.cc
Comment thread python/pyarrow/tests/test_dlpack.py Outdated
Copilot AI review requested due to automatic review settings August 20, 2026 14:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

python/pyarrow/tests/test_dlpack.py:169

  • The numpy version guard checks < 1.24.0, but the skip message says "No dlpack support ... older than 1.22.0". This is confusing when diagnosing test skips; update the message to reflect the actual minimum version (and optionally mention why 1.24 is required).
    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")

Comment thread cpp/src/arrow/c/dlpack.cc
Comment thread cpp/src/arrow/array/array_nested.cc Outdated
Copilot AI review requested due to automatic review settings August 20, 2026 15:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (3)

python/pyarrow/tests/test_dlpack.py:169

  • The skip condition is numpy < 1.24.0, but the message says "older than 1.22.0". This is misleading when diagnosing CI skips; align the message with the actual version gate (or explain both requirements explicitly).
    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")

cpp/src/arrow/array/array_test.cc:1234

  • Two of the EXPECT_EQ assertions are no-ops (they compare shape/strides to literals that exactly match those variables), so this test isn't actually verifying the tensor shape/strides beyond the later checks. Removing them makes the intent clearer and avoids false confidence in coverage.
  EXPECT_EQ(int32(), tensor->type());
  EXPECT_EQ(shape, std::vector<int64_t>{5});
  EXPECT_EQ(strides, std::vector<int64_t>{sizeof(int32_t)});
  EXPECT_EQ(shape, tensor->shape());
  EXPECT_EQ(strides, tensor->strides());

cpp/src/arrow/extension/fixed_shape_tensor.h:48

  • Docstring grammar: "where this array null entries" is missing a verb. This is a public header comment, so it's worth fixing for clarity.
  /// Nulls are ignored, leaving the output tensor with unspecified values where this
  /// array null entries.

Copilot AI review requested due to automatic review settings August 20, 2026 16:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

cpp/src/arrow/array/array_base.h:257

  • Public API comment has a couple of grammatical issues ("Example include" / "where this array null entries"), which can be confusing in generated docs.
  /// Example include NumericArray, FixedShapeTensorArray, nested FixedSizeListArray.
  /// Nulls are ignored, leaving the output tensor with unspecified values where this
  /// array null entries.

cpp/src/arrow/array/array_nested.h:653

  • Doc comment contains grammatical issues ("number of element", "fixed sized list", "where this array null entries"). Since this is a public override, it will show up in generated docs.
  /// The output tensor has a row major layout with the number of element as the first
  /// dimension and the fixed sized list as the remaining one (possibly nested).
  /// Nulls are ignored, leaving the output tensor with unspecified values where this
  /// array null entries.

python/pyarrow/tests/test_dlpack.py:169

  • The skip condition is numpy < 1.24.0, but the message says "No dlpack support ... older than 1.22.0". This is misleading for numpy 1.22/1.23 where dlpack exists but the test still needs 1.24 due to strict=True.
    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")

Comment thread cpp/src/arrow/array/array_nested.cc
Comment thread cpp/src/arrow/array/array_primitive.h Outdated
Copilot AI review requested due to automatic review settings August 31, 2026 09:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

cpp/src/arrow/array/array_primitive.h:149

  • NumericArray::ToTensor() can pass a null Buffer into Tensor::Make when the values buffer is nullptr (e.g., an empty array built from ArrayData with buffers {nullptr, nullptr}). Tensor::Make rejects null data, so ToTensor() will fail for those empty arrays. Consider creating a non-null 0-byte Buffer when length()==0 and no buffer is present.
      ARROW_ASSIGN_OR_RAISE(buffer, SliceBufferSafe(data_->buffers[1], boffset, blength));
    }
    return Tensor::Make(type(), std::move(buffer), {length()});
  }

cpp/src/arrow/array/array_nested.cc:1046

  • FixedSizeListArray::ToTensor() will fail for empty arrays if the leaf values buffer is nullptr (it forwards a null Buffer to Tensor::Make, which returns Invalid("Null data is supplied")). For length==0, it should be safe to use a non-null 0-byte Buffer instead so empty fixed-size-list tensors can still be created.
    ARROW_ASSIGN_OR_RAISE(buffer, SliceBufferSafe(buf, boffset, blength));
  }

  return Tensor::Make(std::move(type), std::move(buffer), std::move(shape));
}

cpp/src/arrow/c/dlpack.cc:54

  • GetDLDataType() is used for both Array and Tensor exports (ExportArrayImpl and ExportTensorImpl). The current error text suggests "try converting to a Tensor", which is confusing when the caller is already exporting a Tensor. Consider rewording the message so it remains accurate in both contexts (or move the hint to the array-only path).
    return Status::TypeError(
        "DataType is not compatible with DLPack spec: ", type.ToString(),
        ", try converting to a Tensor for multi dimensional data support");
  }

cpp/src/arrow/c/dlpack_test.cc:174

  • These assertions hard-code the exact DLPack type-compatibility error message. If GetDLDataType() is reworded to avoid implying callers should convert Tensors to tensors (since it is used by both array and tensor export paths), update both expected strings here to match the new wording.
  ASSERT_RAISES_WITH_MESSAGE(TypeError,
                             "Type error: DataType is not compatible with DLPack spec: " +
                                 array_null->type()->ToString() +
                                 ", try converting to a Tensor for multi"
                                 " dimensional data support",

@AntoinePrv

Copy link
Copy Markdown
Collaborator Author

Thank you @AlenkaF, I reverted the unnecessary tensor changes.

Comment thread cpp/src/arrow/tensor.cc Outdated
remaining /= shape[i];
strides->push_back(remaining);
// The outermost dimension is never a factor of the strides, so a shape whose total
// number of elements overflows can still have valid strides.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strides would be valid, but computing an actual element address would overflow, so is it useful to allow this?

Comment thread cpp/src/arrow/array/array_primitive.h Outdated
Comment thread cpp/src/arrow/array/array_base.h Outdated
/// Examples include NumericArray, FixedShapeTensorArray, nested FixedSizeListArray.
/// Nulls are ignored, leaving the output tensor with unspecified values where this
/// array has null entries.
virtual Result<std::shared_ptr<Tensor>> ToTensor() const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API nit, but I think it would make more sense to expose Tensor facilities only in the corresponding headers, therefore have Tensor::FromArray rather than Array::ToTensor.

It would also mirror FixedShapeTensorArray::FromTensor.

@AntoinePrv AntoinePrv Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, note however that in Python there is already array.to_tensor().
Also FixedShapeTensorArray::ToTensor is also an existing method.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, and I see we also have Table::ToTensor. So perhaps that ship has already sailed...

Comment thread cpp/src/arrow/array/array_list_test.cc Outdated
Comment thread cpp/src/arrow/array/array_nested.cc Outdated
Comment thread cpp/src/arrow/array/array_test.cc
Copilot AI review requested due to automatic review settings September 1, 2026 09:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

cpp/src/arrow/array/array_primitive.h:156

  • Tensor::Make rejects a null Buffer ("Null data is supplied"), but this implementation passes a null buffer when data_->buffers[1] is NULL (which can be valid for empty fixed-width arrays). This makes ToTensor unexpectedly fail for some valid empty arrays.
    return Tensor::Make(type(), std::move(buffer), {length()});

cpp/src/arrow/array/array_nested.cc:1040

  • Tensor::Make rejects a null Buffer, but this code leaves buffer as nullptr when the leaf values buffer is NULL (which can be valid when the computed leaf value count is 0). This can make ToTensor fail for some valid empty fixed-size-list arrays.
  return Tensor::Make(std::move(type), std::move(buffer), std::move(shape));

Comment thread python/pyarrow/array.pxi Outdated
Copilot AI review requested due to automatic review settings September 1, 2026 09:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

python/pyarrow/array.pxi:1856

  • Docstring says allow_nulls defaults to True, but the function signature defaults to False. This mismatch will confuse users and docs generation.
        allow_nulls: bool, default `True`

cpp/src/arrow/array/array_nested.cc:1035

  • If the leaf values buffer is null for an empty FixedSizeListArray (possible for length==0 ArrayData), this passes a null data buffer to Tensor::Make, which fails validation. Consider creating an explicit 0-byte Buffer when the computed tensor size is 0.
  std::shared_ptr<Buffer> buffer = nullptr;
  if (const auto& buf = data->buffers[1]; buf != NULLPTR) {
    const int64_t byte_width = type->byte_width();
    // Buffer guarantees this fits into an int64_t.
    const int64_t byte_offset = offset * byte_width;

cpp/src/arrow/array/array_primitive.h:152

  • If a (valid) empty NumericArray has a null values buffer (buffers[1] == nullptr), this method passes a null data buffer into Tensor::Make, which fails validation even though the tensor has zero elements. Consider materializing an explicit 0-byte Buffer in that case.
    std::shared_ptr<Buffer> buffer;
    if (data_->buffers[1] != NULLPTR) {
      // Array guarantees this will not overflow.
      const int64_t byte_offset = data_->offset * byte_width;
      const int64_t byte_length = length() * byte_width;

Comment thread python/pyarrow/array.pxi Outdated
Comment thread cpp/src/arrow/c/dlpack.cc
Copilot AI review requested due to automatic review settings September 1, 2026 11:53
@AntoinePrv

Copy link
Copy Markdown
Collaborator Author

@pitrou I changed Array::ToTensor(bool allow_nulls = false); that now calls a virtual protected Array::ToTensorWithNulls;.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

python/pyarrow/array.pxi:1856

  • allow_nulls is documented as defaulting to True, but the signature defaults to False. This makes the docstring misleading and contradicts the behavior tested elsewhere (nulls rejected unless explicitly allowed).
        Parameters
        ----------
        allow_nulls: bool, default `True`
            When true, nulls are ignored, leaving the output tensor with

python/pyarrow/array.pxi:4995

  • This override of FixedShapeTensorArray.to_tensor() shadows Array.to_tensor(allow_nulls=...) but doesn't accept an allow_nulls argument. As a result, arr.to_tensor(allow_nulls=True) will raise TypeError for fixed-shape tensor arrays, breaking the new null-handling API and the added tests.
        """

        return Array.to_tensor(self)

Comment thread cpp/src/arrow/tensor.h
Copilot AI review requested due to automatic review settings September 1, 2026 12:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

python/pyarrow/array.pxi:1856

  • The docstring says allow_nulls defaults to True, but the Python signature defaults to False (and the C++ default is also false). This is user-facing API documentation, so it should match the actual default behavior.
        allow_nulls: bool, default `True`

python/pyarrow/array.pxi:4995

  • This delegation uses Array.to_tensor(self) without exposing the new allow_nulls parameter. For FixedShapeTensorArray instances, arr.to_tensor(allow_nulls=True) will raise a Python TypeError because this override’s signature is to_tensor(self) only.
        return Array.to_tensor(self)

Comment thread cpp/src/arrow/c/dlpack.cc
Copilot AI review requested due to automatic review settings September 1, 2026 13:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (2)

cpp/src/arrow/c/dlpack.cc:54

  • Typo/wording: "multi dimensional" should be hyphenated as "multi-dimensional" in this user-facing error message (and update test expectations accordingly).
    return Status::TypeError(
        "DataType is not compatible with DLPack spec: ", type.ToString(),
        ", try converting to a Tensor for multi dimensional data support");
  }

cpp/src/arrow/array/array_nested.cc:1023

  • offset = offset * list_size + ... and length = length * list_size can trigger signed overflow (UB) on large arrays before SliceBufferSafe has a chance to validate bounds. Use overflow-checked arithmetic to keep this safe even on malformed/unvalidated inputs.
    // Overflow cannot happen on a valid array (its data needs to fit in memory,
    // therefore be smaller than INT64_MAX)
    offset = offset * fsl->list_size() + data->offset;
    length = length * fsl->list_size();
    shape.push_back(fsl->list_size());

@pitrou pitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch of nits and minor suggestions, but LGTM in general!


namespace arrow {

class Tensor;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can include arrow/type_fwd.h to avoid adding declarations like this.

Comment on lines +1890 to +1891
// Offsets accumulate at every level: the innermost values are offset by 2, the
// middle lists by 3 * 2 and the outer lists by 1 * 3 * 2.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The middle lists do not seem sliced below?

// Nulls are ignored, leaving unspecified values in the output tensor.
ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor(/* allow_nulls= */ true));
ASSERT_OK(tensor->Validate());
ASSERT_EQ(std::vector<int64_t>({3, 2}), tensor->shape());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably check the 3 non-null tensor values?

Comment on lines +1921 to +1925
NotImplemented,
ArrayFromJSON(fixed_size_list(utf8(), 1), R"([["a"], ["b"]])")->ToTensor());
ASSERT_RAISES(
Invalid,
ArrayFromJSON(fixed_size_list(boolean(), 2), "[[true, false]]")->ToTensor());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we get NotImplemented and Invalid? Ideally this should return TypeError.

ASSERT_OK_AND_ASSIGN(auto tensor, array->ToTensor(/* allow_nulls= */ true));
ASSERT_OK(tensor->Validate());

EXPECT_EQ(shape, tensor->shape());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also check the non-null values here?

Comment thread cpp/src/arrow/tensor.h

namespace arrow {

class Array;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: can include arrow/type_fwd.h instead.

Comment thread python/pyarrow/array.pxi
array = array.copy()
return array

def to_tensor(self, allow_nulls=False):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to make the boolean flag allow_nulls keyword-only to discourage bad practices:

Suggested change
def to_tensor(self, allow_nulls=False):
def to_tensor(self, *, allow_nulls=False):

Comment thread python/pyarrow/array.pxi
return self.to_tensor().to_numpy()

def to_tensor(self):
def to_tensor(self, allow_nulls=False):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: make it keyword-only?

Suggested change
def to_tensor(self, allow_nulls=False):
def to_tensor(self, *, allow_nulls=False):

Comment thread python/pyarrow/array.pxi
with nogil:
ctensor = ext_array.ToTensor()
return pyarrow_wrap_tensor(GetResultValue(ctensor))
return Array.to_tensor(self, allow_nulls=allow_nulls)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just inherit this method or do you keep it for the more concrete docstring?

# A Tensor sharing an Array buffer is immutable, so it can only be exported
# through the versioned DLPack protocol.
assert not tensor.is_mutable
result = np.from_dlpack(DLPackForwarder(tensor, max_version=(1, 0)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to also call np.from_dlpack(arr) or is that not possible yet?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants