Skip to content

Read old vortex.decimal nodes into the canonical array or a Widen - #10995

Draft
connortsui20 wants to merge 3 commits into
ct/decimal-widen-storedfrom
ct/decimal-read-plugin
Draft

connortsui20 wants to merge 3 commits into
ct/decimal-widen-storedfrom
ct/decimal-read-plugin

Conversation

@connortsui20

@connortsui20 connortsui20 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Tracking Issue: #10701

Stacked on #11005, the layer of the decimal Widen stack that adds from_stored_decimal. It does not need #11006.

Summary

Older writers stored a decimal dtype at any width that held its values, so old files contain vortex.decimal nodes at a width other than the canonical width of their dtype. Once every DecimalArray must have the canonical width, these nodes can't read back as stored, so they have to convert on read. They convert through WidenArray::from_stored_decimal, the same path that DecimalByteParts decoding uses (#10905), so every kind of old decimal data reads as the canonical array or a decimal Widen.

Do not merge until 0.91.0 publishes the CD2 compat fixtures. Then run uv run vortex-test/compat-gen/scripts/compat.py check --mode last on this branch. The aim is to ship this in the same release as #10905 and #10992.

Changes

DecimalPlugin takes over the vortex.decimal ID in the default session. A node at the canonical width reads as before, with no copy and no scan, and its buffer stays on the device. Any other node reads into the canonical array or a Widen over the smallest precision that holds its valid values. The plugin can't use stored statistics (the deserializer attaches them after the plugin returns), so an old node costs one scan of its values.

Old nodes are rare, since the compressor has put every decimal that fits in 64 bits into DecimalByteParts since 2025-05. So instead of adding a device path (CUDA has no Widen kernel), the plugin copies the buffers and validity of an old node to the host. The new crate-private ArrayRef::try_into_host_sync copies the validity tree, and a failed copy returns an error. Open question for a CUDA reviewer: does a GPU scan over such a column still work?

Decimal::deserialize now rejects a non-canonical width. Both paths decode through a shared StoredDecimal, which checks the buffer length, alignment and validity against the dtype, so a malformed node returns an error instead of panicking.

Follow-up: in Python, an old narrow node now reads as a decimal Widen, which has no Python subclass, so it isn't a DecimalArray there.

API Changes

Reading an old vortex.decimal node returns the canonical array or a decimal Widen instead of a DecimalArray at the stored width. Adds vortex_array::arrays::decimal::DecimalPlugin, which the default ArraySession registers in place of Decimal. A session that registers only Decimal rejects old nodes.

@connortsui20 connortsui20 added the changelog/break A breaking API change label Oct 29, 2026
@connortsui20 connortsui20 changed the title ct/decimal read plugin Read old vortex.decimal nodes into the canonical array or a Widen Oct 29, 2026
@codspeed

codspeed Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

1 warning

⚠️ Walltime results may be unreliable

Some benchmarks used the Walltime instrument outside CodSpeed Macro Runners, which can make the results inconsistent.

Learn more about Macro Runners

See affected benchmarks

⚡ 12 improved benchmarks
❌ 2 regressed benchmarks
✅ 2346 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ CPU Simulation missing_stats[16] 67.9 µs 85.9 µs -21.02%
❌ CPU Simulation random_i8[0.01] 98.8 µs 112.2 µs -11.99%
⚡ Walltime mul_u64_nonnull_neon 39.7 µs 20.4 µs +94.58%
⚡ Walltime filter_neon[64] 2.9 µs 1.9 µs +53.95%
⚡ Walltime threshold_neon[64] 3.7 µs 2.6 µs +39.69%
⚡ Walltime filter_neon[80] 3.7 µs 2.7 µs +36.81%
⚡ CPU Simulation filter_powerlaw_by_random[10000] 700.5 ns 513.4 ns +36.44%
⚡ CPU Simulation bench_compare_varbin[(50000, 64)] 163.6 µs 133.9 µs +22.19%
⚡ CPU Simulation bench_compare_varbin[(50000, 2)] 161.4 µs 132.2 µs +22.09%
⚡ Walltime threshold_neon[80] 4.3 µs 3.5 µs +21.93%
⚡ CPU Simulation bench_compare_sliced_dict_varbinview[(7500, 10000)] 227.1 µs 199 µs +14.11%
⚡ CPU Simulation bench_compare_sliced_dict_varbinview[(10000, 10000)] 233.3 µs 205.2 µs +13.71%
⚡ CPU Simulation bench_compare_sliced_dict_varbinview[(1000, 10000)] 89.2 µs 78.5 µs +13.56%
⚡ CPU Simulation take_filter_primitive_small_uncached_random_mask_random_indices[4096, 10] 24.3 µs 21.4 µs +13.45%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing ct/decimal-read-plugin (3cfb191) with ct/decimal-widen-stored (eb6bfc5)

Open in CodSpeed

A vortex.decimal node from an older writer can store its values at a
width other than the canonical width of its dtype. The read plugin of
the next commit converts such a node, and it must never panic on a
malformed one.

Move the decoding of a node into StoredDecimal, which checks the
metadata, the buffer count, the dtype, the alignment and the validity
in one place. It also checks that the buffer holds exactly len values
of the stored width. Before, only the array validation after decoding
caught a buffer of another length, and a read that converts the node
needs the check before it reads any value.

Signed-off-by: Connor Tsui <[email protected]>
Older writers stored a decimal dtype at any width that held its values,
so old files hold vortex.decimal nodes at a non-canonical width. After
this commit, no read of a vortex.decimal node returns a decimal leaf at
a non-canonical width.

DecimalPlugin takes over the vortex.decimal ID. A canonical node reads
as before, with no copy and no scan, and a device buffer stays on the
device. Any other node goes through WidenArray::from_stored_decimal,
which returns the canonical array or a decimal Widen. Nodes wider than
the canonical width follow the same rule, so they can also become a
Widen. from_stored_decimal executes the validity on the host, so the
plugin first copies it there with the new ArrayRef::try_into_host_sync.
CUDA has no Widen kernel, so old nodes read on the host even on CUDA
paths.

The Decimal vtable now rejects a non-canonical width with an error that
names DecimalPlugin, and the default session registers the plugin in
its place.

Signed-off-by: Connor Tsui <[email protected]>
The legacy decimal fixtures write vortex.decimal columns at widths
other than the canonical width. Their round-trip test asserted that a
read keeps the stored width, which DecimalPlugin no longer does.

The test now asserts the form of each read column. A column stored at
the canonical width reads unchanged, with its raw slots. Any other
column reads as a Widen over the smallest precision that holds its
valid values, or as the canonical array when that precision has the
canonical width. A slow reference function gives that precision. The
values are compared with the canonical expected arrays.

Signed-off-by: Connor Tsui <[email protected]>
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from d2a4285 to 3cfb191 Compare October 29, 2026 21:33
@connortsui20
connortsui20 changed the base branch from ct/decimal-widen to ct/decimal-widen-stored October 29, 2026 21:33

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/break A breaking API change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant