[Datasets] Add support for string tensor columns in ArrowTensorArray and ArrowVariableShapedTensorArray - #32143
Conversation
Signed-off-by: Scott Lee <sjl@anyscale.com>
Signed-off-by: Scott Lee <sjl@anyscale.com>
Signed-off-by: Scott Lee <sjl@anyscale.com>
| raw_values = data.values | ||
| offset = raw_values.offset | ||
| if pa.types.is_boolean(raw_values.type): | ||
| # Arrow boolean array buffers are bit-packed, with 8 entries per byte, | ||
| # and are accessed via bit offsets. | ||
| buffer_item_width = raw_values.type.bit_width | ||
| else: | ||
| # We assume all other array types are accessed via byte array | ||
| # offsets. | ||
| buffer_item_width = raw_values.type.bit_width // 8 | ||
| data_buffer = raw_values.buffers()[1] | ||
| data_offset = buffer_item_width * offset | ||
|
|
||
| if pa.types.is_boolean(raw_values.type): | ||
| # Special handling for boolean arrays, since Arrow | ||
| # bit-packs boolean arrays while NumPy does not. | ||
| # Cast as uint8 array and let NumPy unpack into a boolean view. | ||
| # Offset into uint8 array, where each element is | ||
| # a bucket for 8 booleans. | ||
| byte_bucket_offset = data_offset // 8 | ||
| # Offset for a specific boolean, within a uint8 array element. | ||
| bool_offset = data_offset % 8 | ||
| # The number of uint8 array elements (buckets) that our slice spans. | ||
| # Note that, due to the offset for a specific boolean, | ||
| # the slice can span byte boundaries even if it contains | ||
| # less than 8 booleans. | ||
| num_boolean_byte_buckets = 1 + ((bool_offset + np.prod(shape) - 1) // 8) | ||
| # Construct the uint8 array view on the buffer. | ||
| arr = np.ndarray( | ||
| (num_boolean_byte_buckets,), | ||
| dtype=np.uint8, | ||
| buffer=data_buffer, | ||
| offset=byte_bucket_offset, | ||
| ) | ||
| # Unpack into a byte per boolean, using LSB bit-packed ordering. | ||
| arr = np.unpackbits(arr, bitorder="little") | ||
| # Interpret buffer as boolean array. | ||
| return np.ndarray(shape, dtype=np.bool_, buffer=arr, offset=bool_offset) | ||
|
|
||
| # Special handling of ragged string tensors | ||
| if pa.types.is_fixed_size_binary(raw_values.type): | ||
| offset = raw_values.offset | ||
| buffer_item_width = raw_values.type.bit_width // 8 | ||
| ext_dtype = np.dtype( | ||
| f"<U{raw_values.type.byte_width // NUM_BYTES_PER_UNICODE_CHAR}" | ||
| ) | ||
| data_offset = buffer_item_width * offset | ||
| data_buffer = raw_values.buffers()[1] | ||
| return np.ndarray( | ||
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | ||
| ) | ||
| ext_dtype = raw_values.type.to_pandas_dtype() | ||
| return np.ndarray( | ||
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | ||
| ) |
There was a problem hiding this comment.
Part 2 of new code from this PR (as opposed to changes from previous PR)
| # TODO(Clark): Construct ndarray view directly on tensor element buffer to | ||
| # ensure reliable zero-copy semantics. | ||
| flat_ndarray = scalar.value.values.to_numpy(zero_copy_only=False) | ||
| return flat_ndarray.reshape(self.shape) | ||
| shape = scalar.type.shape | ||
| raw_values = scalar.value.values | ||
| offset = raw_values.offset | ||
| if pa.types.is_boolean(raw_values.type): | ||
| # Arrow boolean array buffers are bit-packed, with 8 entries per byte, | ||
| # and are accessed via bit offsets. | ||
| buffer_item_width = raw_values.type.bit_width | ||
| else: | ||
| # We assume all other array types are accessed via byte array | ||
| # offsets. | ||
| buffer_item_width = raw_values.type.bit_width // 8 | ||
| data_buffer = raw_values.buffers()[1] | ||
| data_offset = buffer_item_width * offset | ||
| if pa.types.is_boolean(raw_values.type): | ||
| # Special handling for boolean arrays, since Arrow bit-packs | ||
| # boolean arrays while NumPy does not. | ||
| # Cast as uint8 array and let NumPy unpack into a boolean view. | ||
| # Offset into uint8 array, where each element is | ||
| # a bucket for 8 booleans. | ||
| byte_bucket_offset = offset // 8 | ||
| # Offset for a specific boolean, within a uint8 array element. | ||
| bool_offset = offset % 8 | ||
| # The number of uint8 array elements (buckets) that our slice spans. | ||
| # Note that, due to the offset for a specific boolean, | ||
| # the slice can span byte boundaries even if it contains | ||
| # less than 8 booleans. | ||
| num_boolean_byte_buckets = 1 + ((bool_offset + np.prod(shape) - 1) // 8) | ||
| # Construct the uint8 array view on the buffer. | ||
| arr = np.ndarray( | ||
| (num_boolean_byte_buckets,), | ||
| dtype=np.uint8, | ||
| buffer=data_buffer, | ||
| offset=byte_bucket_offset, | ||
| ) | ||
| # Unpack into a byte per boolean, using LSB bit-packed ordering. | ||
| arr = np.unpackbits(arr, bitorder="little") | ||
| # Interpret buffer as boolean array. | ||
| return np.ndarray(shape, dtype=np.bool_, buffer=arr, offset=bool_offset) | ||
| # Special handling of ragged string tensors | ||
| if pa.types.is_fixed_size_binary(raw_values.type): | ||
| offset = raw_values.offset | ||
| buffer_item_width = raw_values.type.bit_width // 8 | ||
| ext_dtype = np.dtype( | ||
| f"<U{raw_values.type.byte_width // NUM_BYTES_PER_UNICODE_CHAR}" | ||
| ) | ||
| data_offset = buffer_item_width * offset | ||
| data_buffer = raw_values.buffers()[1] | ||
| return np.ndarray( | ||
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | ||
| ) | ||
| ext_dtype = raw_values.type.to_pandas_dtype() | ||
| return np.ndarray( | ||
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | ||
| ) |
There was a problem hiding this comment.
Part 1 of new code from this PR (as opposed to changes from previous PR)
| "int64": np.int64, | ||
| "int64_list": object, | ||
| "float": np.float, | ||
| "float": np.float_, |
There was a problem hiding this comment.
This came up when I was testing locally with Arrow 8.0+. np.float is deprecated starting numpy 1.20.0, so I have replaced it with np.float_.
| offset = raw_values.offset | ||
| buffer_item_width = raw_values.type.bit_width // 8 |
There was a problem hiding this comment.
Duplicated definitions of above
| offset = raw_values.offset | |
| buffer_item_width = raw_values.type.bit_width // 8 |
| data_offset = buffer_item_width * offset | ||
| data_buffer = raw_values.buffers()[1] |
There was a problem hiding this comment.
Same here
| data_offset = buffer_item_width * offset | |
| data_buffer = raw_values.buffers()[1] |
| return np.ndarray( | ||
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | ||
| ) |
There was a problem hiding this comment.
Actually, could we fallthrough to the below np.ndarray() constructor after overriding ext_dtype? Should just need to move the below ext_dtype to the top of this function to let the string override take place.
| return np.ndarray( | |
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | |
| ) |
| if pa.types.is_fixed_size_binary(raw_values.type): | ||
| offset = raw_values.offset | ||
| buffer_item_width = raw_values.type.bit_width // 8 | ||
| ext_dtype = np.dtype( | ||
| f"<U{raw_values.type.byte_width // NUM_BYTES_PER_UNICODE_CHAR}" | ||
| ) | ||
| data_offset = buffer_item_width * offset | ||
| data_buffer = raw_values.buffers()[1] | ||
| return np.ndarray( | ||
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | ||
| ) |
There was a problem hiding this comment.
Same here
| if pa.types.is_fixed_size_binary(raw_values.type): | |
| offset = raw_values.offset | |
| buffer_item_width = raw_values.type.bit_width // 8 | |
| ext_dtype = np.dtype( | |
| f"<U{raw_values.type.byte_width // NUM_BYTES_PER_UNICODE_CHAR}" | |
| ) | |
| data_offset = buffer_item_width * offset | |
| data_buffer = raw_values.buffers()[1] | |
| return np.ndarray( | |
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | |
| ) | |
| if pa.types.is_fixed_size_binary(raw_values.type): | |
| ext_dtype = np.dtype( | |
| f"<U{raw_values.type.byte_width // NUM_BYTES_PER_UNICODE_CHAR}" | |
| ) |
Signed-off-by: Scott Lee <sjl@anyscale.com>
| ) | ||
| return np.ndarray( | ||
| shape, dtype=ext_dtype, buffer=data_buffer, offset=data_offset | ||
| ) |
There was a problem hiding this comment.
Btw, it appears that this and ArrowVariableShapedTensorType._extension_scalar_to_ndarray() are the same after the definitions of shape and raw_values, could we consolidate most of this logic into a single utility function that lives at the bottom of this module?
There was a problem hiding this comment.
I generalized ArrowTensorType._extension_scalar_to_ndarray as well (which had a few variables that were slightly different). LMK what you think
Signed-off-by: Scott Lee <sjl@anyscale.com>
clarkzinzow
left a comment
There was a problem hiding this comment.
Awesome, great consolidation of that common ndarray building code! It looks like we could also subsume most of ArrowTensorArray._to_numpy() into that helper as well:
ray/python/ray/air/util/tensor_extensions/arrow.py
Lines 392 to 447 in f21a9e6
I was looking into this as well, but I think that method has some extra logic that's not present in the other methods, for handling the offsets for a single element: ray/python/ray/air/util/tensor_extensions/arrow.py Lines 406 to 415 in f21a9e6 I could further generalize the helper method to work for this, but I think it could be overgeneralized with this new addition. Thoughts? @clarkzinzow |
|
@scottjlee Ah good point, I missed that on the skim! In that case, this looks good to merge! |
…` and `ArrowVariableShapedTensorArray` (ray-project#32143) Add support for creating ArrowTensorArrays and ArrowVariableShapedTensorArray with string typed columns. The previous PR ray-project#31817 had CI test failures which were not run at PR-review time. This PR replicates the functionality of the previous PR, and additionally addresses the test failures (which only occur for Arrow 8.0+). Signed-off-by: Scott Lee <sjl@anyscale.com> Signed-off-by: Edward Oakes <ed.nmi.oakes@gmail.com>
Signed-off-by: Scott Lee sjl@anyscale.com
Why are these changes needed?
Add support for creating
ArrowTensorArraysandArrowVariableShapedTensorArraywith string typed columns. The previous PR #31817 had CI test failures which were not run at PR-review time. This PR replicates the functionality of the previous PR, and additionally addresses the test failures (which only occur for Arrow 8.0+).Related issue number
Based off of PR #31817, with additional implementation on top. I have marked new sections of code with GitHub comments.
Checks
git commit -s) in this PR.scripts/format.shto lint the changes in this PR.