chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,798 @@
|
||||
# arrow_serialization.py must resides outside of ray.data, otherwise
|
||||
# it causes circular dependency issues for AsyncActors due to
|
||||
# ray.data's lazy import.
|
||||
# see https://github.com/ray-project/ray/issues/30498 for more context.
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple
|
||||
|
||||
from ray._private.utils import is_in_test
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pyarrow
|
||||
|
||||
RAY_DISABLE_CUSTOM_ARROW_JSON_OPTIONS_SERIALIZATION = (
|
||||
"RAY_DISABLE_CUSTOM_ARROW_JSON_OPTIONS_SERIALIZATION"
|
||||
)
|
||||
RAY_DISABLE_CUSTOM_ARROW_DATA_SERIALIZATION = (
|
||||
"RAY_DISABLE_CUSTOM_ARROW_DATA_SERIALIZATION"
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Whether we have already warned the user about bloated fallback serialization.
|
||||
_serialization_fallback_set = set()
|
||||
|
||||
|
||||
def _register_custom_datasets_serializers(serialization_context):
|
||||
try:
|
||||
import pyarrow as pa # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
# No pyarrow installed so not using Arrow, so no need for custom serializers.
|
||||
return
|
||||
|
||||
# Register all custom serializers required by Datasets.
|
||||
_register_arrow_data_serializer(serialization_context)
|
||||
_register_arrow_json_readoptions_serializer(serialization_context)
|
||||
_register_arrow_json_parseoptions_serializer(serialization_context)
|
||||
|
||||
|
||||
# Register custom Arrow JSON ReadOptions serializer to workaround it not being picklable
|
||||
# in Arrow < 8.0.0.
|
||||
def _register_arrow_json_readoptions_serializer(serialization_context):
|
||||
if (
|
||||
os.environ.get(
|
||||
RAY_DISABLE_CUSTOM_ARROW_JSON_OPTIONS_SERIALIZATION,
|
||||
"0",
|
||||
)
|
||||
== "1"
|
||||
):
|
||||
return
|
||||
|
||||
import pyarrow.json as pajson
|
||||
|
||||
serialization_context._register_cloudpickle_serializer(
|
||||
pajson.ReadOptions,
|
||||
custom_serializer=lambda opts: (opts.use_threads, opts.block_size),
|
||||
custom_deserializer=lambda args: pajson.ReadOptions(*args),
|
||||
)
|
||||
|
||||
|
||||
def _register_arrow_json_parseoptions_serializer(serialization_context):
|
||||
if (
|
||||
os.environ.get(
|
||||
RAY_DISABLE_CUSTOM_ARROW_JSON_OPTIONS_SERIALIZATION,
|
||||
"0",
|
||||
)
|
||||
== "1"
|
||||
):
|
||||
return
|
||||
|
||||
import pyarrow.json as pajson
|
||||
|
||||
serialization_context._register_cloudpickle_serializer(
|
||||
pajson.ParseOptions,
|
||||
custom_serializer=lambda opts: (
|
||||
opts.explicit_schema,
|
||||
opts.newlines_in_values,
|
||||
opts.unexpected_field_behavior,
|
||||
),
|
||||
custom_deserializer=lambda args: pajson.ParseOptions(*args),
|
||||
)
|
||||
|
||||
|
||||
# Register custom Arrow data serializer to work around zero-copy slice pickling bug.
|
||||
# See https://issues.apache.org/jira/browse/ARROW-10739.
|
||||
def _register_arrow_data_serializer(serialization_context):
|
||||
"""Custom reducer for Arrow data that works around a zero-copy slicing pickling
|
||||
bug by using the Arrow IPC format for the underlying serialization.
|
||||
|
||||
Background:
|
||||
Arrow has both array-level slicing and buffer-level slicing; both are zero-copy,
|
||||
but the former has a serialization bug where the entire buffer is serialized
|
||||
instead of just the slice, while the latter's serialization works as expected
|
||||
and only serializes the slice of the buffer. I.e., array-level slicing doesn't
|
||||
propagate the slice down to the buffer when serializing the array.
|
||||
|
||||
We work around this by registering a custom cloudpickle reducers for Arrow
|
||||
Tables that delegates serialization to the Arrow IPC format; thankfully, Arrow's
|
||||
IPC serialization has fixed this buffer truncation bug.
|
||||
|
||||
See https://issues.apache.org/jira/browse/ARROW-10739.
|
||||
"""
|
||||
if os.environ.get(RAY_DISABLE_CUSTOM_ARROW_DATA_SERIALIZATION, "0") == "1":
|
||||
return
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
serialization_context._register_cloudpickle_reducer(pa.Table, _arrow_table_reduce)
|
||||
serialization_context._register_cloudpickle_reducer(pa.Schema, _arrow_schema_reduce)
|
||||
|
||||
|
||||
def _arrow_schema_reduce(
|
||||
schema: "pyarrow.Schema",
|
||||
) -> Tuple[Callable[["bytes"], "pyarrow.Schema"], Tuple[bytes]]:
|
||||
"""Custom reducer for Arrow Schema that uses IPC serialization for performance.
|
||||
|
||||
Arrow's native IPC serialization for schemas is significantly faster than
|
||||
cloudpickle (10-20x for serialization, 2-3x for deserialization), making
|
||||
this optimization particularly valuable for workloads with large schemas.
|
||||
"""
|
||||
# Use Arrow's native IPC serialization which is much faster than cloudpickle
|
||||
return _restore_schema_from_ipc, (schema.serialize().to_pybytes(),)
|
||||
|
||||
|
||||
def _restore_schema_from_ipc(buf: bytes) -> "pyarrow.Schema":
|
||||
"""Restore an Arrow Schema serialized to Arrow IPC format."""
|
||||
import pyarrow as pa
|
||||
|
||||
return pa.ipc.read_schema(pa.BufferReader(buf))
|
||||
|
||||
|
||||
def _arrow_table_reduce(t: "pyarrow.Table"):
|
||||
"""Custom reducer for Arrow Tables that works around a zero-copy slice pickling bug.
|
||||
Background:
|
||||
Arrow has both array-level slicing and buffer-level slicing; both are zero-copy,
|
||||
but the former has a serialization bug where the entire buffer is serialized
|
||||
instead of just the slice, while the latter's serialization works as expected
|
||||
and only serializes the slice of the buffer. I.e., array-level slicing doesn't
|
||||
propagate the slice down to the buffer when serializing the array.
|
||||
All that these copy methods do is, at serialization time, take the array-level
|
||||
slicing and translate them to buffer-level slicing, so only the buffer slice is
|
||||
sent over the wire instead of the entire buffer.
|
||||
See https://issues.apache.org/jira/browse/ARROW-10739.
|
||||
"""
|
||||
global _serialization_fallback_set
|
||||
|
||||
# Reduce the ChunkedArray columns.
|
||||
reduced_columns = []
|
||||
for column_name in t.column_names:
|
||||
column = t[column_name]
|
||||
try:
|
||||
# Delegate to ChunkedArray reducer.
|
||||
reduced_column = _arrow_chunked_array_reduce(column)
|
||||
except Exception as e:
|
||||
if not _is_dense_union(column.type) and is_in_test():
|
||||
# If running in a test and the column is not a dense union array
|
||||
# (which we expect to need a fallback), we want to raise the error,
|
||||
# not fall back.
|
||||
raise e from None
|
||||
if type(column.type) not in _serialization_fallback_set:
|
||||
logger.warning(
|
||||
"Failed to complete optimized serialization of Arrow Table, "
|
||||
f"serialization of column '{column_name}' of type {column.type} "
|
||||
"failed, so we're falling back to Arrow IPC serialization for the "
|
||||
"table. Note that this may result in slower serialization and more "
|
||||
"worker memory utilization. Serialization error:",
|
||||
exc_info=True,
|
||||
)
|
||||
_serialization_fallback_set.add(type(column.type))
|
||||
# Fall back to Arrow IPC-based workaround for the entire table.
|
||||
return _arrow_table_ipc_reduce(t)
|
||||
else:
|
||||
# Column reducer succeeded, add reduced column to list.
|
||||
reduced_columns.append(reduced_column)
|
||||
return _reconstruct_table, (reduced_columns, t.schema)
|
||||
|
||||
|
||||
def _reconstruct_table(
|
||||
reduced_columns: List[Tuple[List["pyarrow.Array"], "pyarrow.DataType"]],
|
||||
schema: "pyarrow.Schema",
|
||||
) -> "pyarrow.Table":
|
||||
"""Restore a serialized Arrow Table, reconstructing each reduced column."""
|
||||
import pyarrow as pa
|
||||
|
||||
# Reconstruct each reduced column.
|
||||
columns = []
|
||||
for chunks_payload, type_ in reduced_columns:
|
||||
columns.append(_reconstruct_chunked_array(chunks_payload, type_))
|
||||
|
||||
return pa.Table.from_arrays(columns, schema=schema)
|
||||
|
||||
|
||||
def _arrow_chunked_array_reduce(
|
||||
ca: "pyarrow.ChunkedArray",
|
||||
) -> Tuple[List["PicklableArrayPayload"], "pyarrow.DataType"]:
|
||||
"""Custom reducer for Arrow ChunkedArrays that works around a zero-copy slice
|
||||
pickling bug. This reducer does not return a reconstruction function, since it's
|
||||
expected to be reconstructed by the Arrow Table reconstructor.
|
||||
"""
|
||||
# Convert chunks to serialization payloads.
|
||||
chunk_payloads = []
|
||||
for chunk in ca.chunks:
|
||||
chunk_payload = PicklableArrayPayload.from_array(chunk)
|
||||
chunk_payloads.append(chunk_payload)
|
||||
return chunk_payloads, ca.type
|
||||
|
||||
|
||||
def _reconstruct_chunked_array(
|
||||
chunks: List["PicklableArrayPayload"], type_: "pyarrow.DataType"
|
||||
) -> "pyarrow.ChunkedArray":
|
||||
"""Restore a serialized Arrow ChunkedArray from chunks and type."""
|
||||
import pyarrow as pa
|
||||
|
||||
# Reconstruct chunks from serialization payloads.
|
||||
chunks = [chunk.to_array() for chunk in chunks]
|
||||
return pa.chunked_array(chunks, type_)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PicklableArrayPayload:
|
||||
"""Picklable array payload, holding data buffers and array metadata.
|
||||
|
||||
This is a helper container for pickling and reconstructing nested Arrow Arrays while
|
||||
ensuring that the buffers that underly zero-copy slice views are properly truncated.
|
||||
"""
|
||||
|
||||
# Array type.
|
||||
type: "pyarrow.DataType"
|
||||
# Length of array.
|
||||
length: int
|
||||
# Underlying data buffers.
|
||||
buffers: List["pyarrow.Buffer"]
|
||||
# Cached null count.
|
||||
null_count: int
|
||||
# Slice offset into base array.
|
||||
offset: int
|
||||
# Serialized array payloads for nested (child) arrays.
|
||||
children: List["PicklableArrayPayload"]
|
||||
|
||||
@classmethod
|
||||
def from_array(self, a: "pyarrow.Array") -> "PicklableArrayPayload":
|
||||
"""Create a picklable array payload from an Arrow Array.
|
||||
|
||||
This will recursively accumulate data buffer and metadata payloads that are
|
||||
ready for pickling; namely, the data buffers underlying zero-copy slice views
|
||||
will be properly truncated.
|
||||
"""
|
||||
return _array_to_array_payload(a)
|
||||
|
||||
def to_array(self) -> "pyarrow.Array":
|
||||
"""Reconstruct an Arrow Array from this picklable payload."""
|
||||
return _array_payload_to_array(self)
|
||||
|
||||
|
||||
def _array_payload_to_array(payload: "PicklableArrayPayload") -> "pyarrow.Array":
|
||||
"""Reconstruct an Arrow Array from a possibly nested PicklableArrayPayload."""
|
||||
import pyarrow as pa
|
||||
|
||||
children = [child_payload.to_array() for child_payload in payload.children]
|
||||
|
||||
if pa.types.is_dictionary(payload.type):
|
||||
# Dedicated path for reconstructing a DictionaryArray, since
|
||||
# Array.from_buffers() doesn't work for DictionaryArrays.
|
||||
assert len(children) == 2, len(children)
|
||||
indices, dictionary = children
|
||||
return pa.DictionaryArray.from_arrays(
|
||||
indices,
|
||||
dictionary,
|
||||
ordered=payload.type.ordered, # Explicitly pass the ordered flag to from_arrays() to prevent dropping it as ordered=False by default
|
||||
)
|
||||
elif pa.types.is_map(payload.type) and len(children) > 1:
|
||||
# In pyarrow<7.0.0, the underlying map child array is not exposed, so we work
|
||||
# with the key and item arrays.
|
||||
assert len(children) == 3, len(children)
|
||||
offsets, keys, items = children
|
||||
return pa.MapArray.from_arrays(offsets, keys, items)
|
||||
elif isinstance(payload.type, pa.BaseExtensionType):
|
||||
assert len(children) == 1, len(children)
|
||||
storage = children[0]
|
||||
return payload.type.wrap_array(storage)
|
||||
else:
|
||||
# Common case: use Array.from_buffers() to construct an array of a certain type.
|
||||
return pa.Array.from_buffers(
|
||||
type=payload.type,
|
||||
length=payload.length,
|
||||
buffers=payload.buffers,
|
||||
null_count=payload.null_count,
|
||||
offset=payload.offset,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def _array_to_array_payload(a: "pyarrow.Array") -> "PicklableArrayPayload":
|
||||
"""Serialize an Arrow Array to an PicklableArrayPayload for later pickling.
|
||||
|
||||
This function's primary purpose is to dispatch to the handler for the input array
|
||||
type.
|
||||
"""
|
||||
import pyarrow as pa
|
||||
|
||||
if _is_dense_union(a.type):
|
||||
# Dense unions are not supported.
|
||||
# TODO(Clark): Support dense unions.
|
||||
raise NotImplementedError(
|
||||
"Custom slice view serialization of dense union arrays is not yet "
|
||||
"supported."
|
||||
)
|
||||
|
||||
# Dispatch to handler for array type.
|
||||
if pa.types.is_null(a.type):
|
||||
return _null_array_to_array_payload(a)
|
||||
elif _is_primitive(a.type):
|
||||
return _primitive_array_to_array_payload(a)
|
||||
elif _is_binary(a.type):
|
||||
return _binary_array_to_array_payload(a)
|
||||
elif pa.types.is_list(a.type) or pa.types.is_large_list(a.type):
|
||||
return _list_array_to_array_payload(a)
|
||||
elif pa.types.is_fixed_size_list(a.type):
|
||||
return _fixed_size_list_array_to_array_payload(a)
|
||||
elif pa.types.is_struct(a.type):
|
||||
return _struct_array_to_array_payload(a)
|
||||
elif pa.types.is_union(a.type):
|
||||
return _union_array_to_array_payload(a)
|
||||
elif pa.types.is_dictionary(a.type):
|
||||
return _dictionary_array_to_array_payload(a)
|
||||
elif pa.types.is_map(a.type):
|
||||
return _map_array_to_array_payload(a)
|
||||
elif isinstance(a.type, pa.BaseExtensionType):
|
||||
return _extension_array_to_array_payload(a)
|
||||
else:
|
||||
raise ValueError("Unhandled Arrow array type:", a.type)
|
||||
|
||||
|
||||
def _is_primitive(type_: "pyarrow.DataType") -> bool:
|
||||
"""Whether the provided Array type is primitive (boolean, numeric, temporal or
|
||||
fixed-size binary)."""
|
||||
import pyarrow as pa
|
||||
|
||||
return (
|
||||
pa.types.is_integer(type_)
|
||||
or pa.types.is_floating(type_)
|
||||
or pa.types.is_decimal(type_)
|
||||
or pa.types.is_boolean(type_)
|
||||
or pa.types.is_temporal(type_)
|
||||
or pa.types.is_fixed_size_binary(type_)
|
||||
)
|
||||
|
||||
|
||||
def _is_binary(type_: "pyarrow.DataType") -> bool:
|
||||
"""Whether the provided Array type is a variable-sized binary type."""
|
||||
import pyarrow as pa
|
||||
|
||||
return (
|
||||
pa.types.is_string(type_)
|
||||
or pa.types.is_large_string(type_)
|
||||
or pa.types.is_binary(type_)
|
||||
or pa.types.is_large_binary(type_)
|
||||
)
|
||||
|
||||
|
||||
def _null_array_to_array_payload(a: "pyarrow.NullArray") -> "PicklableArrayPayload":
|
||||
"""Serialize null array to PicklableArrayPayload."""
|
||||
# Buffer scheme: [None]
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[None], # Single null buffer is expected.
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=[],
|
||||
)
|
||||
|
||||
|
||||
def _primitive_array_to_array_payload(a: "pyarrow.Array") -> "PicklableArrayPayload":
|
||||
"""Serialize primitive (numeric, temporal, boolean) arrays to
|
||||
PicklableArrayPayload.
|
||||
"""
|
||||
assert _is_primitive(a.type), a.type
|
||||
# Buffer scheme: [bitmap, data]
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) == 2, len(buffers)
|
||||
|
||||
# Copy bitmap buffer, if needed.
|
||||
bitmap_buf = buffers[0]
|
||||
if a.null_count > 0:
|
||||
bitmap_buf = _copy_bitpacked_buffer_if_needed(bitmap_buf, a.offset, len(a))
|
||||
else:
|
||||
bitmap_buf = None
|
||||
|
||||
# Copy data buffer, if needed.
|
||||
data_buf = buffers[1]
|
||||
if data_buf is not None:
|
||||
data_buf = _copy_buffer_if_needed(buffers[1], a.type, a.offset, len(a))
|
||||
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[bitmap_buf, data_buf],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=[],
|
||||
)
|
||||
|
||||
|
||||
def _binary_array_to_array_payload(a: "pyarrow.Array") -> "PicklableArrayPayload":
|
||||
"""Serialize binary (variable-sized binary, string) arrays to
|
||||
PicklableArrayPayload.
|
||||
"""
|
||||
assert _is_binary(a.type), a.type
|
||||
# Buffer scheme: [bitmap, value_offsets, data]
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) == 3, len(buffers)
|
||||
|
||||
# Copy bitmap buffer, if needed.
|
||||
if a.null_count > 0:
|
||||
bitmap_buf = _copy_bitpacked_buffer_if_needed(buffers[0], a.offset, len(a))
|
||||
else:
|
||||
bitmap_buf = None
|
||||
|
||||
# Copy offset buffer, if needed.
|
||||
offset_buf = buffers[1]
|
||||
offset_buf, data_offset, data_length = _copy_offsets_buffer_if_needed(
|
||||
offset_buf, a.type, a.offset, len(a)
|
||||
)
|
||||
data_buf = buffers[2]
|
||||
data_buf = _copy_buffer_if_needed(data_buf, None, data_offset, data_length)
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[bitmap_buf, offset_buf, data_buf],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=[],
|
||||
)
|
||||
|
||||
|
||||
def _list_array_to_array_payload(a: "pyarrow.Array") -> "PicklableArrayPayload":
|
||||
"""Serialize list (regular and large) arrays to PicklableArrayPayload."""
|
||||
# Dedicated path for ListArrays. These arrays have a nested set of bitmap and
|
||||
# offset buffers, eventually bottoming out on a data buffer.
|
||||
# Buffer scheme:
|
||||
# [bitmap, offsets, bitmap, offsets, ..., bitmap, data]
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) > 1, len(buffers)
|
||||
|
||||
# Copy bitmap buffer, if needed.
|
||||
if a.null_count > 0:
|
||||
bitmap_buf = _copy_bitpacked_buffer_if_needed(buffers[0], a.offset, len(a))
|
||||
else:
|
||||
bitmap_buf = None
|
||||
|
||||
# Copy offset buffer, if needed.
|
||||
offset_buf = buffers[1]
|
||||
offset_buf, child_offset, child_length = _copy_offsets_buffer_if_needed(
|
||||
offset_buf, a.type, a.offset, len(a)
|
||||
)
|
||||
|
||||
# Propagate slice to child.
|
||||
child = a.values.slice(child_offset, child_length)
|
||||
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[bitmap_buf, offset_buf],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=[_array_to_array_payload(child)],
|
||||
)
|
||||
|
||||
|
||||
def _fixed_size_list_array_to_array_payload(
|
||||
a: "pyarrow.FixedSizeListArray",
|
||||
) -> "PicklableArrayPayload":
|
||||
"""Serialize fixed size list arrays to PicklableArrayPayload."""
|
||||
# Dedicated path for fixed-size lists.
|
||||
# Buffer scheme:
|
||||
# [bitmap, values_bitmap, values_data, values_subbuffers...]
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) >= 1, len(buffers)
|
||||
|
||||
# Copy bitmap buffer, if needed.
|
||||
if a.null_count > 0:
|
||||
bitmap_buf = _copy_bitpacked_buffer_if_needed(buffers[0], a.offset, len(a))
|
||||
else:
|
||||
bitmap_buf = None
|
||||
|
||||
# Propagate slice to child.
|
||||
child_offset = a.type.list_size * a.offset
|
||||
child_length = a.type.list_size * len(a)
|
||||
child = a.values.slice(child_offset, child_length)
|
||||
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[bitmap_buf],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=[_array_to_array_payload(child)],
|
||||
)
|
||||
|
||||
|
||||
def _struct_array_to_array_payload(a: "pyarrow.StructArray") -> "PicklableArrayPayload":
|
||||
"""Serialize struct arrays to PicklableArrayPayload."""
|
||||
# Dedicated path for StructArrays.
|
||||
# StructArrays have a top-level bitmap buffer and one or more children arrays.
|
||||
# Buffer scheme: [bitmap, None, child_bitmap, child_data, ...]
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) >= 1, len(buffers)
|
||||
|
||||
# Copy bitmap buffer, if needed.
|
||||
if a.null_count > 0:
|
||||
bitmap_buf = _copy_bitpacked_buffer_if_needed(buffers[0], a.offset, len(a))
|
||||
else:
|
||||
bitmap_buf = None
|
||||
|
||||
# Get field children payload.
|
||||
# Offsets and truncations are already propagated to the field arrays, so we can
|
||||
# serialize them as-is.
|
||||
children = [_array_to_array_payload(a.field(i)) for i in range(a.type.num_fields)]
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[bitmap_buf],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def _union_array_to_array_payload(a: "pyarrow.UnionArray") -> "PicklableArrayPayload":
|
||||
"""Serialize union arrays to PicklableArrayPayload."""
|
||||
import pyarrow as pa
|
||||
|
||||
# Dedicated path for UnionArrays.
|
||||
# UnionArrays have a top-level bitmap buffer and type code buffer, and one or
|
||||
# more children arrays.
|
||||
# Buffer scheme: [None, typecodes, child_bitmap, child_data, ...]
|
||||
assert not _is_dense_union(a.type)
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) > 1, len(buffers)
|
||||
|
||||
bitmap_buf = buffers[0]
|
||||
assert bitmap_buf is None, bitmap_buf
|
||||
|
||||
# Copy type code buffer, if needed.
|
||||
type_code_buf = buffers[1]
|
||||
type_code_buf = _copy_buffer_if_needed(type_code_buf, pa.int8(), a.offset, len(a))
|
||||
|
||||
# Get field children payload.
|
||||
# Offsets and truncations are already propagated to the field arrays, so we can
|
||||
# serialize them as-is.
|
||||
children = [_array_to_array_payload(a.field(i)) for i in range(a.type.num_fields)]
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[bitmap_buf, type_code_buf],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def _dictionary_array_to_array_payload(
|
||||
a: "pyarrow.DictionaryArray",
|
||||
) -> "PicklableArrayPayload":
|
||||
"""Serialize dictionary arrays to PicklableArrayPayload."""
|
||||
# Dedicated path for DictionaryArrays.
|
||||
# Buffer scheme: [indices_bitmap, indices_data] (dictionary stored separately)
|
||||
indices_payload = _array_to_array_payload(a.indices)
|
||||
dictionary_payload = _array_to_array_payload(a.dictionary)
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=[indices_payload, dictionary_payload],
|
||||
)
|
||||
|
||||
|
||||
def _map_array_to_array_payload(a: "pyarrow.MapArray") -> "PicklableArrayPayload":
|
||||
"""Serialize map arrays to PicklableArrayPayload."""
|
||||
import pyarrow as pa
|
||||
|
||||
# Dedicated path for MapArrays.
|
||||
# Buffer scheme: [bitmap, offsets, child_struct_array_buffers, ...]
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) > 0, len(buffers)
|
||||
|
||||
# Copy bitmap buffer, if needed.
|
||||
if a.null_count > 0:
|
||||
bitmap_buf = _copy_bitpacked_buffer_if_needed(buffers[0], a.offset, len(a))
|
||||
else:
|
||||
bitmap_buf = None
|
||||
|
||||
new_buffers = [bitmap_buf]
|
||||
|
||||
# Copy offsets buffer, if needed.
|
||||
offset_buf = buffers[1]
|
||||
offset_buf, data_offset, data_length = _copy_offsets_buffer_if_needed(
|
||||
offset_buf, a.type, a.offset, len(a)
|
||||
)
|
||||
|
||||
if isinstance(a, pa.lib.ListArray):
|
||||
# Map arrays directly expose the one child struct array in pyarrow>=7.0.0, which
|
||||
# is easier to work with than the raw buffers.
|
||||
new_buffers.append(offset_buf)
|
||||
children = [_array_to_array_payload(a.values.slice(data_offset, data_length))]
|
||||
else:
|
||||
# In pyarrow<7.0.0, the child struct array is not exposed, so we work with the
|
||||
# key and item arrays.
|
||||
buffers = a.buffers()
|
||||
assert len(buffers) > 2, len(buffers)
|
||||
# Reconstruct offsets array.
|
||||
offsets = pa.Array.from_buffers(
|
||||
pa.int32(), len(a) + 1, [bitmap_buf, offset_buf]
|
||||
)
|
||||
# Propagate slice to keys.
|
||||
keys = a.keys.slice(data_offset, data_length)
|
||||
# Propagate slice to items.
|
||||
items = a.items.slice(data_offset, data_length)
|
||||
children = [
|
||||
_array_to_array_payload(offsets),
|
||||
_array_to_array_payload(keys),
|
||||
_array_to_array_payload(items),
|
||||
]
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=new_buffers,
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def _extension_array_to_array_payload(
|
||||
a: "pyarrow.ExtensionArray",
|
||||
) -> "PicklableArrayPayload":
|
||||
storage_payload = _array_to_array_payload(a.storage)
|
||||
return PicklableArrayPayload(
|
||||
type=a.type,
|
||||
length=len(a),
|
||||
buffers=[],
|
||||
null_count=a.null_count,
|
||||
offset=0,
|
||||
children=[storage_payload],
|
||||
)
|
||||
|
||||
|
||||
def _copy_buffer_if_needed(
|
||||
buf: "pyarrow.Buffer",
|
||||
type_: Optional["pyarrow.DataType"],
|
||||
offset: int,
|
||||
length: int,
|
||||
) -> "pyarrow.Buffer":
|
||||
"""Copy buffer, if needed."""
|
||||
import pyarrow as pa
|
||||
|
||||
if type_ is not None and pa.types.is_boolean(type_):
|
||||
# Arrow boolean array buffers are bit-packed, with 8 entries per byte,
|
||||
# and are accessed via bit offsets.
|
||||
buf = _copy_bitpacked_buffer_if_needed(buf, offset, length)
|
||||
else:
|
||||
type_bytewidth = type_.bit_width // 8 if type_ is not None else 1
|
||||
buf = _copy_normal_buffer_if_needed(buf, type_bytewidth, offset, length)
|
||||
return buf
|
||||
|
||||
|
||||
def _copy_normal_buffer_if_needed(
|
||||
buf: "pyarrow.Buffer",
|
||||
byte_width: int,
|
||||
offset: int,
|
||||
length: int,
|
||||
) -> "pyarrow.Buffer":
|
||||
"""Copy buffer, if needed."""
|
||||
byte_offset = offset * byte_width
|
||||
byte_length = length * byte_width
|
||||
if offset > 0 or byte_length < buf.size:
|
||||
# Array is a zero-copy slice, so we need to copy to a new buffer before
|
||||
# serializing; this slice of the underlying buffer (not the array) will ensure
|
||||
# that the buffer is properly copied at pickle-time.
|
||||
buf = buf.slice(byte_offset, byte_length)
|
||||
return buf
|
||||
|
||||
|
||||
def _copy_bitpacked_buffer_if_needed(
|
||||
buf: "pyarrow.Buffer",
|
||||
offset: int,
|
||||
length: int,
|
||||
) -> "pyarrow.Buffer":
|
||||
"""Copy bit-packed binary buffer, if needed."""
|
||||
bit_offset = offset % 8
|
||||
byte_offset = offset // 8
|
||||
byte_length = _bytes_for_bits(bit_offset + length) // 8
|
||||
if offset > 0 or byte_length < buf.size:
|
||||
buf = buf.slice(byte_offset, byte_length)
|
||||
if bit_offset != 0:
|
||||
# Need to manually shift the buffer to eliminate the bit offset.
|
||||
buf = _align_bit_offset(buf, bit_offset, byte_length)
|
||||
return buf
|
||||
|
||||
|
||||
def _copy_offsets_buffer_if_needed(
|
||||
buf: "pyarrow.Buffer",
|
||||
arr_type: "pyarrow.DataType",
|
||||
offset: int,
|
||||
length: int,
|
||||
) -> Tuple["pyarrow.Buffer", int, int]:
|
||||
"""Copy the provided offsets buffer, returning the copied buffer and the
|
||||
offset + length of the underlying data.
|
||||
"""
|
||||
import pyarrow as pa
|
||||
import pyarrow.compute as pac
|
||||
|
||||
if (
|
||||
pa.types.is_large_list(arr_type)
|
||||
or pa.types.is_large_string(arr_type)
|
||||
or pa.types.is_large_binary(arr_type)
|
||||
or pa.types.is_large_unicode(arr_type)
|
||||
):
|
||||
offset_type = pa.int64()
|
||||
else:
|
||||
offset_type = pa.int32()
|
||||
# Copy offset buffer, if needed.
|
||||
buf = _copy_buffer_if_needed(buf, offset_type, offset, length + 1)
|
||||
# Reconstruct the offset array so we can determine the offset and length
|
||||
# of the child array.
|
||||
offsets = pa.Array.from_buffers(offset_type, length + 1, [None, buf])
|
||||
child_offset = offsets[0].as_py()
|
||||
child_length = offsets[-1].as_py() - child_offset
|
||||
# Create new offsets aligned to 0 for the copied data buffer slice.
|
||||
offsets = pac.subtract(offsets, child_offset)
|
||||
if pa.types.is_int32(offset_type):
|
||||
# We need to cast the resulting Int64Array back down to an Int32Array.
|
||||
offsets = offsets.cast(offset_type, safe=False)
|
||||
buf = offsets.buffers()[1]
|
||||
return buf, child_offset, child_length
|
||||
|
||||
|
||||
def _bytes_for_bits(n: int) -> int:
|
||||
"""Round up n to the nearest multiple of 8.
|
||||
This is used to get the byte-padded number of bits for n bits.
|
||||
"""
|
||||
return (n + 7) & (-8)
|
||||
|
||||
|
||||
def _align_bit_offset(
|
||||
buf: "pyarrow.Buffer",
|
||||
bit_offset: int,
|
||||
byte_length: int,
|
||||
) -> "pyarrow.Buffer":
|
||||
"""Align the bit offset into the buffer with the front of the buffer by shifting
|
||||
the buffer and eliminating the offset.
|
||||
"""
|
||||
import pyarrow as pa
|
||||
|
||||
bytes_ = buf.to_pybytes()
|
||||
bytes_as_int = int.from_bytes(bytes_, sys.byteorder)
|
||||
bytes_as_int >>= bit_offset
|
||||
bytes_ = bytes_as_int.to_bytes(byte_length, sys.byteorder)
|
||||
return pa.py_buffer(bytes_)
|
||||
|
||||
|
||||
def _arrow_table_ipc_reduce(table: "pyarrow.Table"):
|
||||
"""Custom reducer for Arrow Table that works around a zero-copy slicing pickling
|
||||
bug by using the Arrow IPC format for the underlying serialization.
|
||||
|
||||
This is currently used as a fallback for unsupported types (or unknown bugs) for
|
||||
the manual buffer truncation workaround, e.g. for dense unions.
|
||||
"""
|
||||
from pyarrow.ipc import RecordBatchStreamWriter
|
||||
from pyarrow.lib import BufferOutputStream
|
||||
|
||||
output_stream = BufferOutputStream()
|
||||
with RecordBatchStreamWriter(output_stream, schema=table.schema) as wr:
|
||||
wr.write_table(table)
|
||||
# NOTE: output_stream.getvalue() materializes the serialized table to a single
|
||||
# contiguous bytestring, resulting in a few copy. This adds 1-2 extra copies on the
|
||||
# serialization side, and 1 extra copy on the deserialization side.
|
||||
return _restore_table_from_ipc, (output_stream.getvalue(),)
|
||||
|
||||
|
||||
def _restore_table_from_ipc(buf: bytes) -> "pyarrow.Table":
|
||||
"""Restore an Arrow Table serialized to Arrow IPC format."""
|
||||
from pyarrow.ipc import RecordBatchStreamReader
|
||||
|
||||
with RecordBatchStreamReader(buf) as reader:
|
||||
return reader.read_all()
|
||||
|
||||
|
||||
def _is_dense_union(type_: "pyarrow.DataType") -> bool:
|
||||
"""Whether the provided Arrow type is a dense union."""
|
||||
import pyarrow as pa
|
||||
|
||||
return pa.types.is_union(type_) and type_.mode == "dense"
|
||||
Reference in New Issue
Block a user