Skip to content

GH-533: Add ALP (Adaptive Lossless floating-Point) encoding specification - #557

Merged
alamb merged 15 commits into
apache:masterfrom
prtkgaur:alpEncoding
Jul 30, 2026
Merged

alamb merged 15 commits into
apache:masterfrom
prtkgaur:alpEncoding

Conversation

@prtkgaur

@prtkgaur prtkgaur commented Mar 11, 2026 •

Copy link
Copy Markdown
Contributor

Add the encoding specification for ALP (encoding value 10) to Encodings.md. ALP compresses FLOAT and DOUBLE columns by converting values to integers via decimal scaling, then applying Frame of Reference encoding and bit-packing. Values that cannot be losslessly round-tripped are stored as exceptions.

See rendered preview here: https://github.com/prtkgaur/parquet-format/blob/alpEncoding/Encodings.md#adaptive-lossless-floating-point-alp--10

The spec covers:

  • Page layout: 7-byte header, offset array, compressed vectors
  • Vector format: AlpInfo, ForInfo, packed values, exception data
  • Encoding math: two-step multiplication for cross-language consistency
  • Parameter selection, exception detection, and decoding steps

Based on the paper "ALP: Adaptive Lossless floating-Point Compression" (Afroozeh and Boncz, SIGMOD 2024). Wire format matches the C++ Arrow and Java parquet-java implementations.

Rationale for this change

What changes are included in this PR?

Do these changes have PoC implementations?

@alamb alamb changed the title Add ALP (Adaptive Lossless floating-Point) encoding specification GH-533: Add ALP (Adaptive Lossless floating-Point) encoding specification Mar 11, 2026
Comment thread Encodings.md Resolved
Comment thread Encodings.md Outdated

##### Header (7 bytes)

All multi-byte values are little-endian.

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.

Suggested change
All multi-byte values are little-endian.
All multi-byte values are stored in little-endian order.

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.

Done

Comment thread Encodings.md Outdated
|--------|-------|------|------|-------------|
| 0 | compression_mode | 1 byte | uint8 | Compression mode (must be 0 = ALP) |
| 1 | integer_encoding | 1 byte | uint8 | Integer encoding (must be 0 = FOR + bit-packing) |
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in \[3, 15\]. Default: 10 (vector size 1024) |

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.

Suggested change
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in \[3, 15\]. Default: 10 (vector size 1024) |
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in \[3, 15\]. Recommended default: 10 (vector size 1024) |

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.

done

Comment thread Encodings.md Outdated

| Offset | Field | Size | Type | Description |
|--------|-------|------|------|-------------|
| 0 | compression_mode | 1 byte | uint8 | Compression mode (must be 0 = ALP) |

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.

we think ALP-RD fits in well here? I forget what the extension point is and why we were OK keeping this field, but not version.

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.

changing the code to read ALP's newer laid out bits would be difficult to apply across all implementations. but AlpRD is a completely newer implementations.

Added a note to the field description.

Comment thread Encodings.md Outdated
|--------|-------|------|------|-------------|
| 0 | compression_mode | 1 byte | uint8 | Compression mode (must be 0 = ALP) |
| 1 | integer_encoding | 1 byte | uint8 | Integer encoding (must be 0 = FOR + bit-packing) |
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in \[3, 15\]. Default: 10 (vector size 1024) |

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.

Suggested change
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in \[3, 15\]. Default: 10 (vector size 1024) |
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in the inclusive range: \[3, 15\]. Default: 10 (vector size 1024) |

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.

done

Comment thread Encodings.md Outdated
**Note:** The number of elements per vector and the packed data size are NOT stored
in the header. They are derived:
* Elements per vector: `vector_size` for all vectors except the last, which may be smaller.
* Packed data size: `ceil(num_elements_in_vector * bit_width / 8)`.

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.

bit_width isn't in the header either, so it is a little strange to call ths out here?

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.

I also hihglighted this as confusing when reading the spec. I recommend removing this sentence as the packed data size is covered in the "Vector Format" section below

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.

comment regarding packed data size has been removed.

Comment thread Encodings.md Outdated

**Note:** The number of elements per vector and the packed data size are NOT stored
in the header. They are derived:
* Elements per vector: `vector_size` for all vectors except the last, which may be smaller.

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.

This seems like a little bit of a strange callout since it is covered on line 457 explicitly. and log_vector_size is stored in the header?

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.

I agree this is redundant -- I think leaving the note is valuable clarification

Note: The number of elements per vector and the packed data size are NOT stored
in the header, they are derived

However the other items are not

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.

Removed the packed data size bullet, kept the elements-per-vector clarification.

Comment thread Encodings.md Outdated
values. Each offset gives the byte position of the corresponding vector's data,
measured from the start of the offset array itself.

The first offset equals `num_vectors * 4` (pointing just past the offset array).

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.

Suggested change
The first offset equals `num_vectors * 4` (pointing just past the offset array).
The first offset always equals `num_vectors * 4` (pointing just past the offset array).

Lets be explicitly here that we don't support padding.

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.

done

Comment thread Encodings.md Outdated
Data section sizes:
| Section | Size Formula | Description |
|---------------------|-----------------------------|------------------------------|
| PackedValues | ceil(N * bit\_width / 8) | Bit-packed delta values |

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.

Suggested change
| PackedValues | ceil(N * bit\_width / 8) | Bit-packed delta values |
| PackedValues | ceil(`vector_size` * bit\_width / 8) | Bit-packed delta values |

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.

done

Comment thread Encodings.md Outdated
|---------------------|-----------------------------|------------------------------|
| PackedValues | ceil(N * bit\_width / 8) | Bit-packed delta values |
| ExceptionPositions | num\_exceptions * 2 bytes | uint16 indices of exceptions |
| ExceptionValues | num\_exceptions * sizeof(T) | Original float/double values |

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.

Suggested change
| ExceptionValues | num\_exceptions * sizeof(T) | Original float/double values |
| ExceptionValues | num\_exceptions * sizeof(encoded type) (float=4 and double=8) | Original float/double values |

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.

done

Comment thread Encodings.md Outdated

The FOR-encoded deltas, bit-packed into `ceil(num_elements_in_vector * bit_width / 8)` bytes.
Values are packed from the least significant bit of each byte to the most significant bit,
in groups of 8 values, using the same bit-packing order as the

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.

Where does the group of 8 values come in? Wouldn't this messup the number of bytes math?

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.

Good catch — removed 'groups of 8' phrasing. Now simply references the same LSB-first packing order as RLE/Bit-Packing Hybrid.

(I think RleBitPackHybrid was in my mind at that point :) )

Comment thread Encodings.md Outdated
The encoding uses two separate multiplications (not a single multiplication by
`10^(e-f)`, and not division) to ensure that implementations produce identical
floating-point rounding across languages. The powers of 10 MUST be stored as
precomputed floating-point constants (i.e., literal values like `1e-3f`), not

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.

Why can't they be precomputed at runtime?

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.

You're right — this shouldn't mandate literals vs runtime computation. Reworded to require that encoder and decoder use identical power-of-10 values, without prescribing how they're obtained.

Comment thread Encodings.md Outdated
| Type | Magic Number | Formula |
|--------|-----------------------------------|----------------------------------|
| FLOAT | 2^22 + 2^23 = 12,582,912 | `(int)((value + magic) - magic)` |
| DOUBLE | 2^51 + 2^52 = 6,755,399,441,055,744 | `(long)((value + magic) - magic)` |

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.

Suggested change
| DOUBLE | 2^51 + 2^52 = 6,755,399,441,055,744 | `(long)((value + magic) - magic)` |
| DOUBLE | 2^51 + 2^52 = 6,755,399,441,055,744 | `(int64_t)((value + magic) - magic)` |

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.

done

Comment thread Encodings.md Outdated

| Type | Magic Number | Formula |
|--------|-----------------------------------|----------------------------------|
| FLOAT | 2^22 + 2^23 = 12,582,912 | `(int)((value + magic) - magic)` |

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.

Suggested change
| FLOAT | 2^22 + 2^23 = 12,582,912 | `(int)((value + magic) - magic)` |
| FLOAT | 2^22 + 2^23 = 12,582,912 | `(int32_t)((value + magic) - magic)` |

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.

done

Comment thread Encodings.md Outdated
```
+-------------------------------------------------------------------+
| |
| encoded = round( value * 10^e * 10^(-f) ) |

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.

Suggested change
| encoded = round( value * 10^e * 10^(-f) ) |
| encoded = fast_round( value * 10^e * 10^(-f) ) |

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.

done

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

Thank you @prtkgaur and @emkornfield -- I started going through this proposal as well. I haven't made it through the entire thing, but I left commands on what I have made it through first.

@prtkgaur how would you like to address feedback on this PR? Would you like to process the comments? I would also be happy to make a PR with proposed edits to your branch if that would be better. Please just let me know

Comment thread Encodings.md Outdated
Comment on lines +376 to +380
ALP works by converting floating-point values to integers using decimal scaling,
then applying Frame of Reference (FOR) encoding and bit-packing. Values that
cannot be losslessly converted are stored as exceptions. The encoding achieves
high compression for decimal-like floating-point data (e.g., monetary values,
sensor readings) while remaining fully lossless.

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.

You have more summary at the end of the encoding, but I think a few more sentences in the intro would help people understand this more easily

Suggested change
ALP works by converting floating-point values to integers using decimal scaling,
then applying Frame of Reference (FOR) encoding and bit-packing. Values that
cannot be losslessly converted are stored as exceptions. The encoding achieves
high compression for decimal-like floating-point data (e.g., monetary values,
sensor readings) while remaining fully lossless.
ALP works by converting floating-point values to integers using decimal scaling,
then applying Frame of Reference (FOR) encoding and bit-packing. Values that
cannot be losslessly converted are stored as exceptions. The encoding achieves
high compression for decimal-like floating-point data (e.g., monetary values,
sensor readings) while remaining fully lossless. Values do not depend on
each other, which enables quick random access and parallel encode/decode.

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.

Agreed — added sentence about value independence enabling random access and parallel encode/decode.

Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Outdated

**Note:** The number of elements per vector and the packed data size are NOT stored
in the header. They are derived:
* Elements per vector: `vector_size` for all vectors except the last, which may be smaller.

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.

I agree this is redundant -- I think leaving the note is valuable clarification

Note: The number of elements per vector and the packed data size are NOT stored
in the header, they are derived

However the other items are not

Comment thread Encodings.md Outdated
**Note:** The number of elements per vector and the packed data size are NOT stored
in the header. They are derived:
* Elements per vector: `vector_size` for all vectors except the last, which may be smaller.
* Packed data size: `ceil(num_elements_in_vector * bit_width / 8)`.

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.

I also hihglighted this as confusing when reading the spec. I recommend removing this sentence as the packed data size is covered in the "Vector Format" section below

prtkgaur pushed a commit to prtkgaur/parquet-format that referenced this pull request Apr 29, 2026
Incorporate review comments from emkornfield and alamb on PR apache#557:
@alamb

alamb commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

I expect to have a second round of feedback tomorrow

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

Ok, I made it through the spec again. I think it is really quite close

Let me know what you think about the comments and what you think abotu the idea of starting to draft a Blog post.

Comment thread Encodings.md Outdated
| ------------------------------------- | -------------- |
| [Bit-packed (Deprecated)](#BITPACKED) | BIT_PACKED = 4 |

| Encoding | ID | Supported Types |

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.

Something seems strange to me about this chart. The version on main

### Supported Encodings
For details on current implementation status, see the [Implementation Status](https://parquet.apache.org/docs/file-format/implementationstatus/#encodings) page.
| Encoding type | Encoding enum | Supported Types |
| ------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------- |
| [Plain](#PLAIN) | PLAIN = 0 | All Physical Types |
| [Dictionary Encoding](#DICTIONARY) | PLAIN_DICTIONARY = 2 (Deprecated) <br> RLE_DICTIONARY = 8 | All Physical Types |
| [Run Length Encoding / Bit-Packing Hybrid](#RLE) | RLE = 3 | BOOLEAN, Dictionary Indices |
| [Delta Encoding](#DELTAENC) | DELTA_BINARY_PACKED = 5 | INT32, INT64 |
| [Delta-length byte array](#DELTALENGTH) | DELTA_LENGTH_BYTE_ARRAY = 6 | BYTE_ARRAY |
| [Delta Strings](#DELTASTRING) | DELTA_BYTE_ARRAY = 7 | BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY |
| [Byte Stream Split](#BYTESTREAMSPLIT) | BYTE_STREAM_SPLIT = 9 | INT32, INT64, FLOAT, DOUBLE, FIXED_LEN_BYTE_ARRAY |

Doesn't appear in this document.

I would expect this PR that adds the new ALP encoding to add a new entry to the existing table, rather than add an entirely new table.

Maybe we need to merge up from main to this branch (I am happy to do this and I think I have the permissions to push to your branch, but I don't want to mess things up on you)

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.

You're right, this branch was forked before the summary table was added in #550. Rebased onto upstream/master and added ALP as a row to the existing table.

Comment thread Encodings.md Outdated

##### Fast Rounding

The rounding function uses a "magic number" technique for branchless rounding:

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.

I think it would help to use consistent terminology here to above.

Suggested change
The rounding function uses a "magic number" technique for branchless rounding:
The `fast_round` function uses a "magic number" technique for branchless rounding.
`fast_round(value)` is defined as follows:

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.

Done. Updated to use fast_round consistently and added "is defined as follows".

Comment thread Encodings.md Outdated
| FLOAT | 2^22 + 2^23 = 12,582,912 | `(int32_t)((value + magic) - magic)` |
| DOUBLE | 2^51 + 2^52 = 6,755,399,441,055,744 | `(int64_t)((value + magic) - magic)` |

For negative values, the signs are reversed: `(int32_t)((value - magic) + magic)` for FLOAT, `(int64_t)((value - magic) + magic)` for DOUBLE.

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.

I don't understand this sentence. The C++ implementation does not seem to change sign order from what I can tell:

https://github.com/apache/arrow/pull/48345/changes#diff-f9ab708cab94060b4067fff0a6739e9c3751b450422115663b2bd0badfcc748bR487-R490

 /// \brief Convert a float to an int without rounding
  static inline auto FastRound(T n) -> SignedExactType {
    n = n + Constants::kMagicNumber - Constants::kMagicNumber;
    return static_cast<SignedExactType>(n);
  }

Also the "Fast Rounding" section of the ALP paper doesn't mention sign reversal that I can find

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.

You're right -- the sign reversal was incorrect. The C++ implementation uses a single formula for both positive and negative values (n + magic - magic). Removed the paragraph.

Comment thread Encodings.md Outdated
(the encoded integer of the first non-exception value, or 0 if all values
are exceptions) before FOR encoding. This keeps the FOR range tight.

##### Frame of Reference and Bit-Packing

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.

This section has a worked example of FOR / bitpacking so perhaps we could write it like

Suggested change
##### Frame of Reference and Bit-Packing
##### Example: Frame of Reference and Bit-Packing

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.

Updated

Comment thread Encodings.md Outdated

##### Frame of Reference and Bit-Packing

After decimal encoding and exception substitution:

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.

Suggested change
After decimal encoding and exception substitution:
Given the following data after decimal encoding and exception substitution:

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.

Updated

Comment thread Encodings.md Outdated
+ sum(vector_bytes for each vector) // all vectors
```

#### Constants Reference

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.

This appears to be C/++ implementation details -- I am not sure it adds any value to the spec

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.

removed

Comment thread Encodings.md Outdated
encoded size. However, ALP and Byte Stream Split can be complementary: ALP
exploits decimal structure while Byte Stream Split exploits byte-level correlation.

#### Size Calculations

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.

I don't think we need to add add to the Parquet spec -- it isn't required to implement the encoder/decoder for ALP.

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.

removed

Comment thread Encodings.md Outdated
converted are stored separately as *exceptions*. The encoding achieves high
compression for decimal-like floating-point data (e.g., monetary values, sensor
readings) while remaining fully lossless. Each value is encoded independently,
enabling random access to individual vectors and parallel encode/decode.

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.

Suggested change
enabling random access to individual vectors and parallel encode/decode.
enabling random access to individual values and parallel encode/decode.

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.

done

Comment thread Encodings.md Outdated
Comment on lines +592 to +593
floating-point rounding across languages. Implementations must ensure that the
encoder and decoder use identical power-of-10 values for a given exponent.

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.

I don't understand what "use identical power-of-10 values" means. ALL encoders and decoders must to use the exact same values (and floating point arithmetic) as I understand it.

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.

Yes, what you said. Reworded to: "All implementations MUST use the exact same floating-point arithmetic and power-of-10 constants to guarantee cross-language interoperability."

Comment thread Encodings.md Outdated
Comment on lines +616 to +619
To avoid the cost of exhaustive search on every vector, implementations
SHOULD use sampling to select up to 5 candidate (exponent, factor)
combinations (the "encoding preset") at the start of each column chunk.
Each vector then searches only those 5 candidates.

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.

I think SHOULD is too strong a word here. Maybe we could soften it to a suggestion

Suggested change
To avoid the cost of exhaustive search on every vector, implementations
SHOULD use sampling to select up to 5 candidate (exponent, factor)
combinations (the "encoding preset") at the start of each column chunk.
Each vector then searches only those 5 candidates.
To avoid the cost of exhaustive search on every vector, implementations
can use a sampling approach. One such approach, described in the paper, is to
select up to 5 candidate (exponent, factor) combinations (the "encoding preset")
at the start of each column chunk, and when encoding each vector,
test each of the 5 candidates for the fewest exceptions.

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.

Done

Comment thread Encodings.md
readings) while remaining fully lossless. Each value is encoded independently,
enabling random access to individual vectors and parallel encode/decode.

#### Overview

@pitrou pitrou May 5, 2026 •

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 algorithm description is so long that I think it should be moved to a separate file that we would link to here. In this file we would just keeping the description that is before the Overview.

@prtkgaur prtkgaur May 6, 2026 •

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.

Yes this makes sense. I too thought it became long and having a separate file would be good.
Let me take a stab at it after I address the above comments and get approval. Else the comment threads on github will get lost.

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.

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.

I also think it would be ok to if we moved the content to a separate file as a follow on PR (after the spec change is approved) as in my mind moving the content to a separate file does not affect the content of the spec

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.

It seems we should do this as a follow on PR. I can do so after this PR is merged

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.

Got it. Let's address it in a follow up PR

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.

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.

Add the encoding specification for ALP (encoding value 10) to Encodings.md.
ALP compresses FLOAT and DOUBLE columns by converting values to integers via
decimal scaling, then applying Frame of Reference encoding and bit-packing.
Values that cannot be losslessly round-tripped are stored as exceptions.

The spec covers:
- Page layout: 7-byte header, offset array, compressed vectors
- Vector format: AlpInfo, ForInfo, packed values, exception data
- Encoding math: two-step multiplication for cross-language consistency
- Parameter selection, exception detection, and decoding steps

Based on the paper "ALP: Adaptive Lossless floating-Point Compression"
(Afroozeh and Boncz, SIGMOD 2024). Wire format matches the C++ Arrow
and Java parquet-java implementations.
Incorporate review comments from emkornfield and alamb on PR apache#557:
Comment thread Encodings.md
| [Delta-length byte array](#DELTALENGTH) | DELTA_LENGTH_BYTE_ARRAY = 6 | BYTE_ARRAY |
| [Delta Strings](#DELTASTRING) | DELTA_BYTE_ARRAY = 7 | BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY |
| [Byte Stream Split](#BYTESTREAMSPLIT) | BYTE_STREAM_SPLIT = 9 | INT32, INT64, FLOAT, DOUBLE, FIXED_LEN_BYTE_ARRAY |
| [ALP](#ALP) | ALP = 10 | FLOAT, DOUBLE |

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've raised the same question in the google doc earlier. FP16 and FP8 have fewer bits so the benefit may not be that significant compared to regular float and double. But I agree that we can discuss these in the future to not expand and thus block current spec design.

Comment thread Encodings.md Outdated
converted are stored separately as *exceptions*. The encoding achieves high
compression for decimal-like floating-point data (e.g., monetary values, sensor
readings) while remaining fully lossless. Each value is encoded independently,
enabling random access to individual values and parallel encode/decode.

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.

Suggested change
enabling random access to individual values and parallel encode/decode.
enabling random access to individual values and parallel encoding/decoding.

Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md
+-------------+------+------+-----+---------+----------+----------+-----+----------+
```

The compression pipeline below describes *one* way to produce a conforming

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.

IIUC, the informative compression description should be move to a separate doc later in the followup PR because here we just need necessary description for decoding a ALP-encoded page (i.e. the Page Layout section).

cc @alamb

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.

Ack. Makes sense. We can do this in a follow up PR

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.

Comment thread Encodings.md Outdated
Comment on lines +478 to +479
Byte: 0 1 2 3 4 5 6
+----------------+---------------+--------------+----+----+----+----+

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.

Suggested change
Byte: 0 1 2 3 4 5 6
+----------------+---------------+--------------+----+----+----+----+
Byte: 0 1 2 3 4 5 6
+----------------+---------------+--------------+----+----+----+----+

Let's adjust the alignment here.

Comment thread Encodings.md
|--------|-------|------|------|-------------|
| 0 | compression_mode | 1 byte | uint8 | Compression mode (0 = ALP). Reserved for future variants (e.g., ALP-RD). |
| 1 | integer_encoding | 1 byte | uint8 | Integer encoding (must be 0 = FOR + bit-packing) |
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in the inclusive range \[3, 15\]. Recommended default: 10 (vector size 1024) |

@wgtmac wgtmac Jul 22, 2026 •

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.

If the minimum value is 3, why not defining it as log_vector_size_minus_three so it can start with 0? Not a strong opinion since this anyway consume a byte which is large enough.

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'd like to keep log_vector_size, maps directly to 1 << log_vector_size.

Comment thread Encodings.md Outdated
v
+----------------------------------------------------------+
| 1. CHOOSE PARAMETERS |
| Select (exponent, factor) pair for this vector |

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.

Suggested change
| Select (exponent, factor) pair for this vector |
| Select (exponent, factor) pair for this array |

Be consistent with Input: float/double array above.

Comment thread Encodings.md Outdated
Each subsequent offset equals the previous offset plus the stored size of the
previous vector. No padding is inserted between vectors.

A vector's absolute byte position within the page is

@wgtmac wgtmac Jul 22, 2026 •

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.

This is incorrect when pages are compressed or have rep/def levels. I would suggest removing this sentence. If we want to emphasize random access of ALP, perhaps we should replace page_data_start with a more precise start point after decompressed.

Comment thread Encodings.md Outdated
| 0 | compression_mode | 1 byte | uint8 | Compression mode (0 = ALP). Reserved for future variants (e.g., ALP-RD). |
| 1 | integer_encoding | 1 byte | uint8 | Integer encoding (must be 0 = FOR + bit-packing) |
| 2 | log_vector_size | 1 byte | uint8 | log2(vector\_size). Must be in the inclusive range \[3, 15\]. Recommended default: 10 (vector size 1024) |
| 3 | num_elements | 4 bytes | int32 | Total number of floating-point values in the page |

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.

Suggested change
| 3 | num_elements | 4 bytes | int32 | Total number of floating-point values in the page |
| 3 | num_elements | 4 bytes | int32 | Total number of non-null floating-point values in the page |

Comment thread Encodings.md
values. Each offset gives the byte position of the corresponding vector's data,
measured from the start of the offset array itself.

The first offset always equals `num_vectors * 4` (pointing just past the offset array).

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.

Why num_vectors * 4 but not num_vectors * 4 + 7 to include the header? Usually the whole buffer is stored including header, so implementations need to manually add 7 each time to get an absolute offset to each vector.

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.

I don't see any particular reason for one or the other. Given that we have used the num_vectors * 4 in the prototype implementation I think it is ok to leave this as is

Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Outdated

This encoding is adapted from the paper
["ALP: Adaptive Lossless floating-Point Compression"](https://dl.acm.org/doi/10.1145/3626717)
by Afroozeh and Boncz (SIGMOD 2024).

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.

It seems that the paper has THREE authors but we have only listed TWO.

Comment thread Encodings.md Resolved
Comment thread Encodings.md
+----------------------------------------------------------+
| 3. FRAME OF REFERENCE (FOR) |
| min_val = min(encoded[:]) |
| delta[i] = encoded[i] - min_val |

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.

We need to clarify that delta[i] is an unsigned integer to avoid signed overflow.

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.

Thanks. Updated.

State explicitly that the magic-number add/subtract are floating-point
operations (only the final cast is integer), and that the constants are
written as integers merely because they are exact. Add a MUST requiring
FLOAT fast_round to be evaluated in single precision and DOUBLE in double
precision, since a promotion of the FLOAT path to double drops the value
into a binade with ULP below 1.0, defeating the rounding entirely.
The 1.5*2^n midpoint constant exists to make the magic-number rounding
branchless, covering a symmetric range in a single formula. Since this
spec branches on sign, the midpoint is the wrong choice: with a branch,
2^n doubles the usable domain (to +/-2^23 for FLOAT and +/-2^52 for
DOUBLE) and makes the branch actually necessary, whereas with the
midpoint the branch was redundant. Update the constants, the domain
description, and the prose (the technique is no longer branchless).
Resolve open review comments on PR apache#557 (apacheGH-533):

- parquet.thrift: add missing Encoding.ALP = 10 (thread 50). The enum
  value was never added; prose and the summary table said "ALP = 10"
  but the machine-readable contract lacked it.
- Fix paper authorship to three authors: Afroozeh, Kuffo, Boncz (62).
- "For each data page..." intro, drop misleading "page-level header" (16/49).
- num_elements is the non-null value count (56).
- Correct vector offset base for compressed / rep-def pages: use
  alp_data_start (decoded page data) instead of page_data_start+7 (55).
- Note Vector Format applies to compression_mode=0 / integer_encoding=0 (59).
- Label vector header vs data section; forward-ref num_exceptions/bit_width (60/61).
- Clarify deltas are non-negative/unsigned with no sign extension (64).
- Exceptions stored as raw IEEE 754 bits, MUST NOT canonicalize NaN (63).
- Header byte-diagram alignment and "parallel encoding/decoding" wording (49/52).

Leaves the contested fast_round sign-branching (threads 31/47/48)
untouched pending reviewer consensus.
Follow-up to review comments on PR apache#557 (apacheGH-533):

- Pipeline step 1 wording: "for this array" to match the diagram's
  "Input: float/double array" (thread 54).
- Clarify FOR deltas are computed and stored as unsigned integers,
  explicitly to avoid signed overflow when max-min exceeds the encoded
  type's signed maximum; no sign extension on unpack (thread 64).
- Annotate the Vector Format diagram with a bracket row marking the
  vector header vs the data section (thread 61).
@alamb
alamb requested a review from wgtmac July 29, 2026 06:46
@alamb

alamb commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

SInce we have had a successful vote, I suggest we merge this PR and make additional changes as follow on PRs:

From what I can see, it looks like @prtkgaur resolved all blocking comments from @wgtmac 's most recent review (e.g. parquet.thrift change). There are a few more style/clarification suggestions from @wgtmac 's I think that we could do as a follow on.

Thus, @wgtmac could you confirm you are happy with merging this PR as is (perhaps by approving the PR) and if so I'll merge this PR in and we can continue to iterate in follow on PRs

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

I agree with @alamb's suggestion and fine with current PR. Thanks @prtkgaur and @alamb!

Comment thread Encodings.md Resolved
Comment thread Encodings.md
readings) while remaining fully lossless. Each value is encoded independently,
enabling random access to individual vectors and parallel encode/decode.

#### Overview

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.

Got it. Let's address it in a follow up PR

Comment thread Encodings.md
+-------------+------+------+-----+---------+----------+----------+-----+----------+
```

The compression pipeline below describes *one* way to produce a conforming

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.

Ack. Makes sense. We can do this in a follow up PR

Comment thread Encodings.md

This encoding is adapted from the paper
["ALP: Adaptive Lossless floating-Point Compression"](https://dl.acm.org/doi/10.1145/3626717)
by Afroozeh, Kuffo, and Boncz (SIGMOD 2024).

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.

Fixed

Comment thread Encodings.md
+----------------------------------------------------------+
| 3. FRAME OF REFERENCE (FOR) |
| min_val = min(encoded[:]) |
| delta[i] = encoded[i] - min_val |

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.

Thanks. Updated.

Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Resolved
Comment thread Encodings.md Outdated
| FLOAT | 2^22 + 2^23 = 12,582,912 | `(int32_t)((value + magic) - magic)` | `(int32_t)((value - magic) + magic)` |
| DOUBLE | 2^51 + 2^52 = 6,755,399,441,055,744 | `(int64_t)((value + magic) - magic)` | `(int64_t)((value - magic) + magic)` |

The sign branching is necessary because the technique relies on `value ± magic`

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 over indexed on Ryan's example and updated the wording, thanks Andrew for the correction.
Given the examples that Andrew has brought up i'll keep the wording but instead of "encoder must do it", I can change it to "one of the ways of doing it is..."

Comment thread Encodings.md
ratio and exception count, never correctness or what a reader decodes. The
`fast_round` technique below is one recommended implementation.

##### Fast Rounding (informative)

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.

@alamb do you recommend I simplify the language here or remove part of the section?

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'm going for simplification for now

@alamb

alamb commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

I just had a brief sync with @prtkgaur - I think he plans to simplify a few things and then we'll merge this one in and iterate in follow on PRs

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

I took a look at the most recent edits and I think they look good. I will merge this in and we can continue iterating as follow on PRs

Comment thread Encodings.md Outdated
example, places `8,388,608` in a binade whose ULP is far below 1.0 and no rounding
occurs.

Each form has a limited valid domain — roughly (-2^23, 2^23) for floats and

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.

I like this wording

@alamb
alamb merged commit 2076361 into apache:master Jul 30, 2026
4 checks passed
alamb added a commit to apache/arrow-rs that referenced this pull request Sep 4, 2026
# Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax.
-->

- Part of #10541
- Closes #8748 .
- Related spec PR: apache/parquet-format#557

# Rationale for this change
check issue
<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->

# What changes are included in this PR?

<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->

# Are these changes tested?

<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code

If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
-->

# Are there any user-facing changes?

<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.

If there are any breaking changes to public APIs, please call them out.
-->

---------

Co-authored-by: Andrew Lamb <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Rich-T-kid pushed a commit to Rich-T-kid/arrow-rs that referenced this pull request Sep 18, 2026
# Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax.
-->

- Part of apache#10541
- Closes apache#8748 .
- Related spec PR: apache/parquet-format#557

# Rationale for this change
check issue
<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->

# What changes are included in this PR?

<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->

# Are these changes tested?

<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code

If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
-->

# Are there any user-facing changes?

<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.

If there are any breaking changes to public APIs, please call them out.
-->

---------

Co-authored-by: Andrew Lamb <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Rich-T-kid pushed a commit to Rich-T-kid/arrow-rs that referenced this pull request Sep 21, 2026
# Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax.
-->

- Part of apache#10541
- Closes apache#8748 .
- Related spec PR: apache/parquet-format#557

# Rationale for this change
check issue
<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->

# What changes are included in this PR?

<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->

# Are these changes tested?

<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code

If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
-->

# Are there any user-facing changes?

<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.

If there are any breaking changes to public APIs, please call them out.
-->

---------

Co-authored-by: Andrew Lamb <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposal] Add ALP encoding support in parquet file format

9 participants