Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/cudf/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ def on_missing_reference(app, env, node, contnode):
("py:class", "typing_extensions.Self"),
("py:class", "np.uint32"),
("py:class", "np.uint64"),
("py:class", "ArrowLike"),
]


Expand Down
9 changes: 9 additions & 0 deletions python/pylibcudf/pylibcudf/_interop_helpers.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ class ArrowLike(metaclass=_ArrowLikeMeta):
pass


class _ObjectWithArrowMetadata:
def __init__(self, obj, metadata=None):
self.obj = obj
self.metadata = metadata

def __arrow_c_array__(self, requested_schema=None):
return self.obj._to_schema(self.metadata), self.obj._to_host_array()


@dataclass
class ColumnMetadata:
"""Metadata associated with a column.
Expand Down
1 change: 1 addition & 0 deletions python/pylibcudf/pylibcudf/column.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class Column:
def from_rmm_buffer(
buff: DeviceBuffer, dtype: DataType, size: int, children: list[Column]
) -> Column: ...
def to_arrow(self, metadata: list | str | None = None) -> ArrowLike: ...
@staticmethod
def from_arrow(
obj: ArrowLike, dtype: DataType | None = None
Expand Down
38 changes: 36 additions & 2 deletions python/pylibcudf/pylibcudf/column.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,22 @@ from ._interop_helpers cimport (
from .utils cimport _get_stream

from .gpumemoryview import _datatype_from_dtype_desc
from ._interop_helpers import ArrowLike, ColumnMetadata
from ._interop_helpers import ArrowLike, ColumnMetadata, _ObjectWithArrowMetadata

import array
from itertools import accumulate
import functools
import operator
from typing import Iterable

try:
import pyarrow as pa
pa_err = None
except ImportError as e:
pa = None
pa_err = e


__all__ = ["Column", "ListColumnView", "is_c_contiguous"]


Expand Down Expand Up @@ -336,12 +344,38 @@ cdef class Column:
self._children = children
self._num_children = len(children)

def to_arrow(
self,
metadata: ColumnMetadata | str | None = None
) -> ArrowLike:
"""Create a PyArrow array from a pylibcudf column.

Parameters
----------
metadata : list
The metadata to attach to the columns of the table.

Returns
-------
pyarrow.Array
"""
if pa_err is not None:
raise RuntimeError(
"pyarrow was not found on your system. Please "
"pip install pylibcudf with the [pyarrow] extra for a "
"compatible pyarrow version."
) from pa_err
# TODO: Once the arrow C device interface registers more
# types that it supports, we can call pa.array(self) if
# no metadata is passed.
return pa.array(_ObjectWithArrowMetadata(self, metadata))
Comment thread
mroeschke marked this conversation as resolved.

@staticmethod
def from_arrow(
obj: ArrowLike,
dtype: DataType | None = None,
Stream stream=None
) -> Column:
) -> ArrowLike:
"""
Create a Column from an Arrow-like object using the Arrow C Data Interface.

Expand Down
72 changes: 14 additions & 58 deletions python/pylibcudf/pylibcudf/interop.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ from rmm.pylibrmm.stream cimport Stream
from .column cimport Column
from .scalar cimport Scalar
from .table cimport Table
from .types cimport DataType, type_id
from .types import LIBCUDF_TO_ARROW_TYPES
from .types cimport DataType
from .utils cimport _get_stream
from ._interop_helpers import ColumnMetadata

Expand Down Expand Up @@ -71,7 +70,7 @@ def from_arrow(pyarrow_object, *, DataType data_type=None):


@singledispatch
def to_arrow(plc_object, metadata=None):
def to_arrow(plc_object, **kwargs):
"""Convert to a PyArrow object.

Parameters
Expand Down Expand Up @@ -105,79 +104,36 @@ if pa is not None:
return Table.from_arrow(pyarrow_object, dtype=data_type)

@from_arrow.register(pa.Scalar)
def _from_arrow_scalar(pyarrow_object, *, DataType data_type=None):
return Scalar.from_arrow(pyarrow_object, dtype=data_type)
def _from_arrow_scalar(
pyarrow_object,
*,
DataType data_type=None,
Stream stream = None
):
return Scalar.from_arrow(pyarrow_object, dtype=data_type, stream=stream)

@from_arrow.register(pa.Array)
def _from_arrow_column(pyarrow_object, *, DataType data_type=None):
return Column.from_arrow(pyarrow_object, dtype=data_type)

@to_arrow.register(DataType)
def _to_arrow_datatype(plc_object, **kwargs):
"""
Convert a datatype to arrow.

Translation of some types requires extra information as a keyword
argument. Specifically:

- When translating a decimal type, provide ``precision``
- When translating a struct type, provide ``fields``
- When translating a list type, provide the wrapped ``value_type``
"""
if plc_object.id() in {
type_id.DECIMAL32,
type_id.DECIMAL64,
type_id.DECIMAL128
}:
if not (precision := kwargs.get("precision")):
raise ValueError(
"Precision must be provided for decimal types"
)
# no pa.decimal32 or pa.decimal64
return pa.decimal128(precision, -plc_object.scale())
elif plc_object.id() == type_id.STRUCT:
if not (fields := kwargs.get("fields")):
raise ValueError(
"Fields must be provided for struct types"
)
return pa.struct(fields)
elif plc_object.id() == type_id.LIST:
if not (value_type := kwargs.get("value_type")):
raise ValueError(
"Value type must be provided for list types"
)
return pa.list_(value_type)
else:
try:
return LIBCUDF_TO_ARROW_TYPES[plc_object.id()]
except KeyError:
raise TypeError(
f"Unable to convert {plc_object.id()} to arrow datatype"
)

class _ObjectWithArrowMetadata:
def __init__(self, obj, metadata=None):
self.obj = obj
self.metadata = metadata

def __arrow_c_array__(self, requested_schema=None):
return self.obj._to_schema(self.metadata), self.obj._to_host_array()
"""Convert a datatype to arrow."""
return plc_object.to_arrow(**kwargs)

@to_arrow.register(Table)
def _to_arrow_table(plc_object, metadata=None):
"""Create a PyArrow table from a pylibcudf table."""
return pa.table(_ObjectWithArrowMetadata(plc_object, metadata))
return plc_object.to_arrow(metadata=metadata)

@to_arrow.register(Column)
def _to_arrow_array(plc_object, metadata=None):
"""Create a PyArrow array from a pylibcudf column."""
return pa.array(_ObjectWithArrowMetadata(plc_object, metadata))
return plc_object.to_arrow(metadata=metadata)

@to_arrow.register(Scalar)
def _to_arrow_scalar(plc_object, metadata=None):
# Note that metadata for scalars is primarily important for preserving
# information on nested types since names are otherwise irrelevant.
return to_arrow(Column.from_scalar(plc_object, 1), metadata=metadata)[0]
return plc_object.to_arrow(metadata=metadata)


cpdef Table from_dlpack(object managed_tensor, Stream stream=None):
Expand Down
5 changes: 5 additions & 0 deletions python/pylibcudf/pylibcudf/scalar.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ from pylibcudf.types import DataType

NpGeneric = type[Any]

PaScalar = type[Any]

class Scalar:
def __init__(self): ...
def type(self) -> DataType: ...
def is_valid(self) -> bool: ...
@staticmethod
def empty_like(column: Column, stream: Stream | None = None) -> Scalar: ...
def to_arrow(
self, metadata: list | str | None = None, stream: Stream | None = None
) -> PaScalar: ...
@staticmethod
def from_arrow(
pa_val: Any,
Expand Down
24 changes: 24 additions & 0 deletions python/pylibcudf/pylibcudf/scalar.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ from .traits cimport is_floating_point
from .types cimport DataType
from .utils cimport _get_stream
from functools import singledispatch
from ._interop_helpers import ArrowLike, ColumnMetadata

try:
import pyarrow as pa
Expand Down Expand Up @@ -150,6 +151,29 @@ cdef class Scalar:
"""True if the scalar is valid, false if not"""
return self.get().is_valid()

def to_arrow(
self,
metadata: list[ColumnMetadata] | str | None = None,
stream: Stream = None,
) -> ArrowLike:
"""Create a PyArrow array from a pylibcudf scalar.

Parameters
----------
metadata : list
The metadata to attach to the columns of the table.
stream : Stream | None
CUDA stream on which to perform the operation.

Returns
-------
pyarrow.Scalar
"""
stream = _get_stream(stream)
# Note that metadata for scalars is primarily important for preserving
# information on nested types since names are otherwise irrelevant.
return Column.from_scalar(self, 1, stream).to_arrow(metadata=metadata)[0]

@staticmethod
def from_arrow(
pa_val,
Expand Down
1 change: 1 addition & 0 deletions python/pylibcudf/pylibcudf/table.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Table:
def num_rows(self) -> int: ...
def shape(self) -> tuple[int, int]: ...
def columns(self) -> list[Column]: ...
def to_arrow(self, metadata: list) -> ArrowLike: ...
@staticmethod
def from_arrow(
arrow_like: ArrowLike, dtype: DataType | None = None
Expand Down
26 changes: 25 additions & 1 deletion python/pylibcudf/pylibcudf/table.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,15 @@ from pylibcudf._interop_helpers cimport (
_release_device_array,
_metadata_to_libcudf,
)
from ._interop_helpers import ArrowLike, ColumnMetadata
from ._interop_helpers import ArrowLike, ColumnMetadata, _ObjectWithArrowMetadata

try:
import pyarrow as pa
pa_err = None
except ImportError as e:
pa = None
pa_err = e


__all__ = ["Table"]

Expand All @@ -61,6 +69,22 @@ cdef class Table:
raise ValueError("All columns must be pylibcudf Column objects")
self._columns = columns

def to_arrow(
self,
metadata: list[ColumnMetadata | str] | None = None
) -> ArrowLike:
"""Create a PyArrow table from a pylibcudf table."""
if pa_err is not None:
raise RuntimeError(
"pyarrow was not found on your system. Please "
"pip install pylibcudf with the [pyarrow] extra for a "
"compatible pyarrow version."
) from pa_err
# TODO: Once the arrow C device interface registers more
# types that it supports, we can call pa.array(self) if
# no metadata is passed.
Comment on lines +83 to +86

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.

What is this comment referring to? I don't understand I'm afraid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's a typo that should be pa.table

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I mean is that once arrow can consume more kinds of device arrays, we can call pa.table(self) directly. The following example fails today

In [1]: import pylibcudf as plc

In [2]: import pyarrow as pa

In [3]: pa.table(plc.Table([plc.Column.from_iterable_of_py([1,2])]))
---------------------------------------------------------------------------
ArrowKeyError                             Traceback (most recent call last)
Cell In[3], line 1
----> 1 pa.table(plc.Table([plc.Column.from_iterable_of_py([1,2])]))

File ~/.conda/envs/rapids/lib/python3.13/site-packages/pyarrow/table.pxi:6235, in pyarrow.lib.table()

File ~/.conda/envs/rapids/lib/python3.13/site-packages/pyarrow/table.pxi:6054, in pyarrow.lib.record_batch()

File ~/.conda/envs/rapids/lib/python3.13/site-packages/pyarrow/table.pxi:4044, in pyarrow.lib.RecordBatch._import_from_c_device_capsule()

File ~/.conda/envs/rapids/lib/python3.13/site-packages/pyarrow/error.pxi:155, in pyarrow.lib.pyarrow_internal_check_status()

File ~/.conda/envs/rapids/lib/python3.13/site-packages/pyarrow/error.pxi:92, in pyarrow.lib.check_status()

ArrowKeyError: Device type 2is not registered

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.

Oh I see. Yeah... I'm not sure that'll ever be supported, but if it is then great. The best bet might be backing pyarrow with pylibcudf for GPU bits!

return pa.table(_ObjectWithArrowMetadata(self, metadata))

@staticmethod
def from_arrow(obj: ArrowLike, dtype: DataType | None = None) -> Table:
"""
Expand Down
1 change: 1 addition & 0 deletions python/pylibcudf/pylibcudf/types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class DataType:
def __init__(self, type_id: TypeId, scale: int = 0): ...
def id(self) -> TypeId: ...
def scale(self) -> int: ...
def to_arrow(self, **kwargs) -> PyarrowDataType: ...
@staticmethod
def from_arrow(pa_typ: PyarrowDataType) -> DataType: ...
def from_py(self, type: type) -> DataType: ...
Expand Down
61 changes: 61 additions & 0 deletions python/pylibcudf/pylibcudf/types.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ from pylibcudf.libcudf.types import sorted as Sorted # no-cython-lint, isort:sk

from functools import cache

try:
import pyarrow as pa
pa_err = None
except ImportError as e:
pa = None
pa_err = e

try:
import pyarrow as pa

Expand Down Expand Up @@ -189,6 +196,60 @@ cdef class DataType:
ret.c_obj = dt
return ret

def to_arrow(self, **kwargs):
"""
Convert a datatype to arrow.

Returns
-------
pyarrow.DataType

Notes
-----
Translation of some types requires extra information as a keyword
argument. Specifically:

- When translating a decimal type, provide ``precision``
- When translating a struct type, provide ``fields``
- When translating a list type, provide the wrapped ``value_type``
"""
if pa_err is not None:
raise RuntimeError(
"pyarrow was not found on your system. Please "
"pip install pylibcudf with the [pyarrow] extra for a "
"compatible pyarrow version."
) from pa_err
if self.id() in {
type_id.DECIMAL32,
type_id.DECIMAL64,
type_id.DECIMAL128
}:
if not (precision := kwargs.get("precision")):
raise ValueError(
"Precision must be provided for decimal types"
)
# no pa.decimal32 or pa.decimal64
return pa.decimal128(precision, -self.scale())
elif self.id() == type_id.STRUCT:
if not (fields := kwargs.get("fields")):
raise ValueError(
"Fields must be provided for struct types"
)
return pa.struct(fields)
elif self.id() == type_id.LIST:
if not (value_type := kwargs.get("value_type")):
raise ValueError(
"Value type must be provided for list types"
)
return pa.list_(value_type)
else:
try:
return LIBCUDF_TO_ARROW_TYPES[self.id()]
except KeyError:
raise TypeError(
f"Unable to convert {self.id()} to arrow datatype"
)

@staticmethod
def from_arrow(pa_typ) -> DataType:
"""
Expand Down
Loading