Skip to content

feat(parquet): add ALP encoding for FLOAT and DOUBLE columns - #1289

Open
prtkgaur wants to merge 12 commits into
apache:mainfrom
prtkgaur:alp-go-encoding
Open

prtkgaur wants to merge 12 commits into
apache:mainfrom
prtkgaur:alp-go-encoding

Conversation

@prtkgaur

@prtkgaur prtkgaur commented Sep 4, 2026 •

Copy link
Copy Markdown

Co-Author: @arnav.balyan

Supersedes #704 (this PR started from it and addresses the review there).

Rationale for this change

ALP (Adaptive Lossless floating-Point) is the ALP = 10 encoding for FLOAT and DOUBLE columns in parquet-format (in Preview since 2026-08-01). It scales values by powers of ten into integers, then stores them frame-of-reference + bit-packed, keeping values that don't round-trip as exceptions. Arrow C++ already implements it.

What changes are included in this PR?

Encoding (parquet/internal/encoding)

  • alp_encoding.go: generic alpEncoder[T]/alpDecoder[T] for float32 and float64, following the spec's page layout (7-byte header, offset array, self-describing vectors).
    • The encoder buffers one vector at a time and writes a page per FlushValues.
    • The decoder decodes lazily, one vector at a time, honours the page's log_vector_size (8–32768), and bounds-checks every size and offset read from the page.
    • Exception values keep their exact IEEE 754 bits, NaN payloads included.
  • alp_scaling.go: the normative decode encoded * 10^f * 10^-e with the spec's literal power-of-ten tables. alpScaling[T] looks the constants up once per vector instead of once per value.
  • alp_params.go: picks (exponent, factor) by estimated encoded size, using the paper's sampling scheme: 256 samples per vector, a full search over the first 8 vectors, then the 5 best combinations.
  • Reuses utils.BitWriter/BitReader for bit packing.

Writer/reader integration

  • parquet.Encodings.ALP, and Encoding_ALP in the generated thrift code.
  • The column reader only builds an ALP decoder for FLOAT/DOUBLE columns.
  • Writing is opt-in, as the spec recommends for Preview encodings:
    • WithALPEncoding / WithALPEncodingFor / WithALPEncodingPath allow ALP, and WithEncoding*(Encodings.ALP) selects it.
    • Selecting ALP without allowing it panics in NewWriterProperties.
    • A dictionary takes precedence, as for every other encoding.
  • WithALPVectorSize / WithALPVectorSizeFor / WithALPVectorSizePath set the vector size: a power of two from 8 to 32768, default 1024, matching the alp_vector_size property of the Arrow C++ implementation.

Are these changes tested?

Yes.

  • Unit tests for scaling, parameter search, bit packing, every vector size, partial vectors, exceptions (NaN payloads, ±Inf, −0, subnormals, out-of-range values), partial reads and discards, multi-page encoders, and corrupt or truncated pages.
  • File-level round trips: V1 and V2 data pages, nulls, all-null pages, many pages, and per-column vector sizes. Also a pqarrow round trip.
  • TestReadALPConformanceFile reads alp_extended.zstd.parquet from apache/parquet-testing, written by Arrow C++ with 32-, 1024- and 4096-value vectors. It bit-compares every ALP column against its PLAIN copy. The parquet-testing submodule is bumped to include that file.
  • Interop in the other direction, checked manually: Arrow C++ (pyarrow 27.0.0.dev59) read a Go-written file with float/double/nullable columns at vector sizes 8, 1024 and 32768. Every value was bit-identical to the PLAIN columns, NaN payloads included.
  • Benchmarks: BenchmarkALP{En,De}codingFloat{32,64}, plus batched decoding.

Are there any user-facing changes?

Yes, all opt-in:

  • parquet.Encodings.ALP
  • WithALPEncoding* and WithALPVectorSize* writer properties, with the matching getters
  • DefaultALPEnabled and DefaultALPVectorSize

Reading ALP needs no option.

@prtkgaur
prtkgaur force-pushed the alp-go-encoding branch 2 times, most recently from 22e6bde to 44ffbd8 Compare October 4, 2026 02:39
sfc-gh-pgaur and others added 5 commits October 7, 2026 05:01
ALP turns a float into an integer by multiplying it by 10^exponent and
then by 10^-factor. This adds that arithmetic and the search that picks
the pair for each vector. The search estimates the size of every pair
on a sample of 256 values, counting the packed width of the values that
convert and the full cost of those that do not, and breaks ties toward
the larger exponent as the ALP paper does. After eight vectors it only
tries the five pairs those vectors chose most often.

The arithmetic follows the reference implementation's rounding and
order of multiplication exactly. Rounding decides which values become
exceptions, so a writer that differs by one bit writes a different page.
This adds the ALP page format for FLOAT and DOUBLE columns. A page has a
seven-byte header, one offset per vector, and vectors of 1024 values.
Each vector stores its scaling, a frame of reference, the bit-packed
differences from that frame, and the values that did not survive the
scaling as exceptions patched in by position. Packing uses the existing
BitWriter and BitReader. The differences are computed in unsigned
arithmetic, so a DOUBLE vector spanning most of the int64 range packs at
64 bits instead of overflowing.

The decoder accepts any vector size a page declares, from 8 to 32768
values, so it reads pages from writers that do not use 1024. It checks
every offset, bit width and exception position against the page before
reading, and returns an error when the page holds fewer values than the
page header claims instead of a short read the column reader would
retry forever.

ALP takes encoding id 10. The generated Thrift bindings have no
checked-in source, so the new value is added to them by hand.

Co-authored-by: Prateek Gaur <prateek.gaur@snowflake.com>
parquet-format marks ALP as a preview encoding, and a reader without it
cannot open a file that uses it. Writing ALP therefore takes an explicit
opt-in, WithALPEncoding for every column or WithALPEncodingFor for one,
and NewWriterProperties panics when a column asks for ALP without it, as
it does for any unusable encoding. The opt-in permits ALP but does not
select it. A column still chooses it with WithEncoding or
WithEncodingFor, and needs dictionary encoding off, because a writer
that abandons its dictionary falls back to PLAIN.

The column reader returns an error for an ALP page on a column that is
not FLOAT or DOUBLE, where the decoder traits would otherwise panic. The
tests write and read columns of several pages with and without nulls,
under both data page versions.
A round trip through one implementation cannot show that it reads what
other writers produce. This moves parquet-testing to the commit that
adds alp_extended.zstd.parquet, whose ALP columns use vectors of 32,
1024 and 4096 values next to PLAIN copies of the same data, and checks
that every ALP value decodes to the bits of its PLAIN copy.

A second test writes an Arrow table with ALP columns and reads it back
across compression codecs, several pages and row groups, nulls, and
values that can only be stored as exceptions.
The exception rate drives what ALP costs, so the benchmarks encode and
decode FLOAT and DOUBLE columns at several sizes with no exceptions, one
exception in ten values, and nothing but exceptions. A batched DOUBLE
decode reads 137 values at a time, so batches end partway through a
vector the way column reader calls do.
# Conflicts:
#	parquet/file/file_reader_test.go
- Look the power-of-ten constants up once per vector (alpScaling) instead
  of once per value; encode is ~35-40% and decode ~30-44% faster.
- Add the frame of reference back in the encoded integer's width, as the
  spec requires for FLOAT.
- Reuse scratch for PutSpaced and the page header.
- Add WithALPVectorSize/WithALPVectorSizeFor/WithALPVectorSizePath
  (power of two, 8-32768, default 1024) and apply it in the float column
  writers.
- Document ALP in the package docs.
@zeroshade zeroshade changed the title [WIP] Add support for ALP encoding feat(parquet): add ALP encoding for FLOAT and DOUBLE columns Oct 29, 2026
- Treat a FLOAT value whose rounded integer leaves int32 as an exception.
  Rounding a scaled value just under 2^31 could reach 2^31, which readers
  rebuild in int32 with the sign flipped (or a 33-bit packing width).
- Avoid int overflow on 32-bit platforms when counting vectors and
  bounds-checking vector offsets in the decoder.
- List the encodings the data pages actually used in the column chunk
  metadata when there is no dictionary, so a column that asked for ALP
  but fell back to PLAIN is not labelled ALP.
- Point the docs at per-column ALP selection, make ALPEncodingEnabled
  report the default like the other getters, and add ALPVectorSize.
- Count vector offsets in EstimatedDataEncodedSize; fix stale comments.
- Cover V2 pages with nulls and compression, and the spaced write/read
  paths, which the existing tests named but did not reach.
- Prove pages are ALP via data-page encoding stats, and check the
  many-pages test really writes several pages.
- Merge the float32/float64 copy-paste round trips into one generic
  table test; drop tests that only restated types or duplicated the
  shared encoding suite; move reader-error tests to column_reader_test.go
  on testutils.MockPageReader; gather file-level ALP tests in alp_test.go.
- Use checked allocators, and report allocations in the benchmarks.
EstimatedDataEncodedSize counted buffered values at their raw width, so a
vector of NaNs or infinities (each stored with a 2-byte position, plus the
vector header and offset) was underestimated and pages could overshoot the
page size.
A vector can pack every slot at full width and also store exceptions, so
sizing the pending vector as all exceptions still undercounted mixed
vectors of NaNs and wide integers. Count both.
@zeroshade
zeroshade marked this pull request as ready for review October 29, 2026 17:54
@zeroshade
zeroshade self-requested a review as a code owner October 29, 2026 17:54
@zeroshade
zeroshade requested review from kou, lidavidm and wgtmac October 29, 2026 17:54
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.

4 participants