This Epic is for implementing narrow integer encoding, an encoding that keeps an array's logical dtype while storing its values in a narrower integer child.
For example, an I64 array with values between [-128, 127] can be stored as an I8 child and still read as I64. This reduces the storage and memory footprint significantly, and compute kernels may be able to run directly on the narrow child without widening first.
Status
Proposed.
Goal
The goal is to store integer data at only the necessary width as far as possible through compute. This seems to be a low-hanging compression and compute win for integer types, and may be applicable to decimal types in the future as well.
Today, you can can shrink an integer array in two ways:
- Cast it to a narrower type. This is a schema change and changes overflow behavior, so it is a non-starter.
- Compress it (e.g. with bitpacking). Bitpacking minimizes storage, but compute decodes values to logical width.
Narrowing would be a relatively low-cost intermediate between casting and bitpacking. The logical dtype of a narrowed array would stay the same, but compute would run over the narrowed values when possible. Narrower values -> more values per SIMD lane -> higher throughput.
Design
NarrowArray is a wrapper array with no buffers. It has a logical integer dtype and one child slot.
NarrowArray(dtype = Primtive(I64)
└ values: PrimitiveArray(ptype = I8, _))
We call the dtype of the NarrowArray original_dtype, since it is the original dtype of the child before narrowing. We call the dtype of the child narrow_dtype.
For now, we can enforce the following invariants:
original_dtype and narrow_dtype are both integers
sgn(original_dtype) == sgn(narrow_dtype)
width(original_dtype) > width(narrow_dtype)
- Validity lives in the child. The parent derives it.
- The child is a canonical primitive integer array (for now). The child could in principle could be any array tree with a logical integer dtype narrower than the original dtype.
Encoding
NarrowArray::encode(primitive) computes the non-null min/max and picks the smallest fitting type of the same signedness. It casts the values once or returns the input unchanged if nothing smaller fits.
Canonicalization
NarrowArray::execute() simply the child to the original_dtype while preserving its validity. This is guaranteed to be lossless, since sgn(original_dtype) == sgn(narrow_dtype) and width(original_dtype) > width(narrow_dtype).
Compute
One of the main benefits of this encoding is that compute over the narrowed child should be more performant in many cases than compute over the original array.
Unresolved questions
This Epic is for implementing narrow integer encoding, an encoding that keeps an array's logical dtype while storing its values in a narrower integer child.
For example, an
I64array with values between[-128, 127]can be stored as anI8child and still read asI64. This reduces the storage and memory footprint significantly, and compute kernels may be able to run directly on the narrow child without widening first.Status
Proposed.
Goal
The goal is to store integer data at only the necessary width as far as possible through compute. This seems to be a low-hanging compression and compute win for integer types, and may be applicable to decimal types in the future as well.
Today, you can can shrink an integer array in two ways:
Narrowing would be a relatively low-cost intermediate between casting and bitpacking. The logical dtype of a narrowed array would stay the same, but compute would run over the narrowed values when possible. Narrower values -> more values per SIMD lane -> higher throughput.
Design
NarrowArrayis a wrapper array with no buffers. It has a logical integer dtype and one child slot.We call the dtype of the
NarrowArrayoriginal_dtype, since it is the original dtype of the child before narrowing. We call the dtype of the childnarrow_dtype.For now, we can enforce the following invariants:
original_dtypeandnarrow_dtypeare both integerssgn(original_dtype) == sgn(narrow_dtype)width(original_dtype) > width(narrow_dtype)Encoding
NarrowArray::encode(primitive)computes the non-null min/max and picks the smallest fitting type of the same signedness. It casts the values once or returns the input unchanged if nothing smaller fits.Canonicalization
NarrowArray::execute()simply the child to theoriginal_dtypewhile preserving its validity. This is guaranteed to be lossless, sincesgn(original_dtype) == sgn(narrow_dtype)andwidth(original_dtype) > width(narrow_dtype).Compute
One of the main benefits of this encoding is that compute over the narrowed child should be more performant in many cases than compute over the original array.
Unresolved questions