Skip to content

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

Open
AntoinePrv wants to merge 17 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 17 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
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
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
@AntoinePrv
AntoinePrv marked this pull request as ready for review August 21, 2026 08:04
Copilot AI review requested due to automatic review settings August 21, 2026 08:04

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 checks NumPy < 1.24.0, but the message says “older than 1.22.0”. This is confusing when diagnosing CI skips; update the message to match the actual version gate (and/or split the reasons: dlpack support vs strict= support).
    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/c/dlpack.cc:54

  • The new TypeError message for unsupported DLPack dtypes always suggests converting to a Tensor, but that advice isn’t applicable to many unsupported types (e.g., strings). Consider qualifying the suggestion (e.g., “if the array represents multi-dimensional numeric data”) or mentioning the concrete API (Array::ToTensor / to_tensor).
    return Status::TypeError("Bit-packed boolean data type not supported by DLPack.");
  } else {
    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.cc:126

  • For the null-count failure path, the error is still correct, but it now misses the new recommended workaround introduced in this PR: Array::ToTensor supports nulls (as unspecified values) and can then be exported via DLPack. Consider updating this error message to mention converting to a Tensor as well, for consistency with the new unsupported-type guidance.
Result<DT*> ExportArrayImpl(const std::shared_ptr<Array>& arr, bool copy) {
  if (arr->null_count() > 0) {
    return Status::TypeError("Can only use DLPack on arrays with no nulls.");
  }

@AntoinePrv

Copy link
Copy Markdown
Collaborator Author

@AlenkaF this is ready, I think the remaining failures are unrelated.

@AntoinePrv

Copy link
Copy Markdown
Collaborator Author

@rok too :)

@AlenkaF

AlenkaF commented Aug 26, 2026

Copy link
Copy Markdown
Member

Hi @AntoinePrv, will do the initial review today!

@AlenkaF AlenkaF 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.

Went through the PR, amazing work!
I only have two questions regarding the tensor files, other is a nit.

Comment thread cpp/src/arrow/tensor.cc Outdated

if (remaining == 0) {
strides->assign(shape.size(), byte_width);
// An empty dimension makes the whole tensor empty, so any stride is as good.

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 changes in tensor.h/.cc were not clear to me so I helped myself with Claude to better understand. Is the reason behind the change (order and code change) meant to catch an empty dimension up front?

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.

Outdated now.

Comment thread cpp/src/arrow/tensor.h Outdated
/// Pass `elem_size=1` to get the strides in number of elements, or the element size in
/// bytes to get them in bytes. On error, the contents of `strides` are unspecified.
ARROW_EXPORT
Status ComputeRowMajorStrides(std::span<const int64_t> shape, int64_t elem_size,

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 there a specific reason why the API changed (new signature added, the old becoming a thin wrapper)? Is this meant to serve a purpose for DLPack or is this a general improvement? cc @rok

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.

That is a leftover from when I initially implemented tensor support for fixed size list directly in DLPack (so I needed to compute the strides internally).
Reverting now.

Comment on lines +47 to +48
/// Nulls are ignored, leaving the output tensor with unspecified values where this
/// array has null entries.

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.

👍

Comment thread cpp/src/arrow/array/array_list_test.cc Outdated
@github-actions github-actions Bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Aug 26, 2026
Copilot AI review requested due to automatic review settings August 31, 2026 09:04

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 (1)

python/pyarrow/tests/test_dlpack.py:169

  • The skip condition checks for NumPy < 1.24.0, but the message says "older than 1.22.0". This is misleading when diagnosing CI skips; update the message to match the actual version gate (or adjust the gate if 1.22 is really sufficient).
    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")

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 on lines +142 to +145
if (internal::MultiplyWithOverflow(data_->offset, byte_width, &boffset) ||
internal::MultiplyWithOverflow(length(), byte_width, &blength)) {
return Status::Invalid("Array byte size does not fit in an int64");
}

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.

That can't happen for a valid array, so we needn't check for this.

/// 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.

}

TEST_F(TestFixedSizeListArray, ToTensorNulls) {
// Nulls are ignored, leaving unspecified values in the output 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.

Hmm... can we perhaps have an option to control that?
For example Tensor::FromArray(bool allow_nulls = false) or Array::ToTensor(bool allow_nulls = false)?

Comment on lines +1020 to +1022
if (internal::MultiplyWithOverflow(offset, int64_t{fsl->list_size()}, &offset) ||
internal::AddWithOverflow(offset, data->offset, &offset) ||
internal::MultiplyWithOverflow(length, int64_t{fsl->list_size()}, &length)) {

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.

I think the overflow checks are not necessary here either. Overflow cannot happen on a valid array (because its data needs to fit in memory, therefore be smaller than INT64_MAX).

}

TEST(TestPrimitiveArray, ToTensorNulls) {
// Nulls are ignored, leaving unspecified values in the output 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.

Same comment as in array_list_test.cc.

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