Skip to content

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

Merged
connortsui20 merged 6 commits into
developfrom
ct/decimal-read-plugin
Nov 5, 2026
Merged

connortsui20 merged 6 commits into
developfrom
ct/decimal-read-plugin

Conversation

@connortsui20

@connortsui20 connortsui20 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Every decimal leaf must now have the canonical width of its precision, which makes legacy vortex.decimal nodes at other widths invalid as leaves. Those nodes still exist: old files store a decimal at whatever width held its values, and WidenPlugin writes a decimal Widen as a legacy node, since vortex.widen has no wire form yet. Reading a legacy node into the canonical array or a decimal Widen keeps it readable.

Changes

The reader is a DecimalPlugin because a vtable's deserialize must return its own array type, while a plugin can return a Widen. It picks the child with WidenArray::from_stored_decimal, like DecimalByteParts decoding (#10905), so both paths give the same values the same precision. The plugin also owns the serializer, so the Decimal vtable has no serde, as with FoR. The plugin checks the wire layout first, so a malformed node returns an error instead of panicking.

Known cost: each legacy read scans its valid values once, because stored statistics are attached only after the plugin returns. This also applies to IPC and writes without a compressor, not only to old files.

Choosing the child precision of a legacy node scans its valid values on the CPU, so a legacy node in device memory now returns an error instead of being copied to the host. Known regression: a CUDA read of a legacy node, which worked before, fails until the CUDA executor reads legacy decimal nodes itself.

Follow-up: a legacy node can now read as a Widen, which has no Python subclass.

API Changes

The default session registers the new public DecimalPlugin in place of Decimal. The Decimal vtable no longer serializes or deserializes, so a session that registers Decimal directly cannot read or write decimal arrays.

@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 degrade performance by 18.08%

2 warnings

⚠️ 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

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

❌ 1 regressed benchmark
✅ 1913 untouched benchmarks
⏩ 47 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ CPU Simulation missing_stats[16] 75.8 µs 92.6 µs -18.08%

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 (0dcf448) with develop (f0b14cd)

Open in CodSpeed

Footnotes

  1. 47 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from bad5407 to d2a4285 Compare October 29, 2026 20:24
@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
@connortsui20
connortsui20 force-pushed the ct/decimal-widen-stored branch from eb6bfc5 to b3b006c Compare October 30, 2026 17:50
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from 3cfb191 to 852affb Compare October 30, 2026 17:50
@connortsui20
connortsui20 force-pushed the ct/decimal-widen-stored branch from b3b006c to e50341e Compare October 30, 2026 19:28
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from 852affb to c6f1a4c Compare October 30, 2026 19:28
@connortsui20
connortsui20 force-pushed the ct/decimal-widen-stored branch from e50341e to d030931 Compare November 2, 2026 13:50
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from c6f1a4c to 6ec1784 Compare November 2, 2026 16:57
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from 6ec1784 to ac9bc1e Compare November 2, 2026 17:08
@connortsui20
connortsui20 force-pushed the ct/decimal-widen-stored branch 2 times, most recently from 73a9038 to bf6bc81 Compare November 2, 2026 17:24
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from ac9bc1e to 1c710a6 Compare November 2, 2026 17:24
@connortsui20
connortsui20 force-pushed the ct/decimal-widen-stored branch from bf6bc81 to 0892933 Compare November 2, 2026 19:24
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from 1c710a6 to 05e0b63 Compare November 2, 2026 19:24
@connortsui20
connortsui20 force-pushed the ct/decimal-widen-stored branch from 0892933 to a1837f2 Compare November 2, 2026 19:52
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch 2 times, most recently from 1e28863 to fbb5f7e Compare November 2, 2026 20:25
@connortsui20
connortsui20 force-pushed the ct/decimal-widen-stored branch from 0ed981b to 280c0a8 Compare November 2, 2026 21:29
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch 2 times, most recently from 4951602 to 7b2c2f1 Compare November 3, 2026 13:20
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch 2 times, most recently from 107a5b0 to 73da43c Compare November 4, 2026 21:38
@connortsui20 connortsui20 changed the title Read old vortex.decimal nodes into the canonical array or a Widen Read legacy vortex.decimal nodes into the canonical array or a Widen Nov 4, 2026
@mhk197

mhk197 commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

IIUC, i dont understand why this isn't decimal.v2 or similar since we want to enforce a new invariant? would also simplify plugin logic

@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from 73da43c to f6d0e12 Compare November 5, 2026 14:45
@connortsui20
connortsui20 requested a review from mhk197 November 5, 2026 14:46
@robert3005

Copy link
Copy Markdown
Contributor

I think if we can avoid introducing new serialized ids we should, this maximizes compatibility. The old decimal let you do whatever you wanted - this is tightening of the bounds so old readers can always read the current values and new readers use Widen to get the desired behaviour

@mhk197

mhk197 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

This LGTM now but would get someone with CUDA integration experience to review the CUDA part?

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
Serde is moving out of vtables, so the Decimal vtable now returns None from serialize and bails
in deserialize, as FoR and BitPacked do. DecimalPlugin owns the metadata, the serializer, and
StoredDecimal, and the vtable serde tests move into the plugin tests.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
StoredDecimal now keeps the row count from the containing format, so into_array_parts uses it
directly instead of deriving it from the buffer. The byte length check stays, because a legacy
read still takes its row count from the buffer.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20
connortsui20 force-pushed the ct/decimal-read-plugin branch from a345eac to a865895 Compare November 5, 2026 17:42
@connortsui20

Copy link
Copy Markdown
Member Author

oh wait I can make a cosmetic change first (store len in the stored decimal struct so we dont recalculate it / keep it close to the rest of the metadata)

/// A device buffer that holds its bytes on the host. A copy to the host succeeds unless
/// `fails_host_copy` is set.
#[derive(Debug, PartialEq, Eq, Hash)]
struct TestDeviceBuffer {

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.

just remove these tests and the copy to host logic - we need to add handling for this in cuda executor not here

Choosing the child precision of a legacy node scans its valid values on the CPU. Copying device
buffers to the host for that scan mixes host and device execution, so from_stored_decimal now
returns an error for noncanonical values or validity in device memory. The CUDA executor can
read legacy nodes itself later. A canonical node still keeps its device buffer.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20
connortsui20 enabled auto-merge (squash) November 5, 2026 18:46
@connortsui20
connortsui20 merged commit 114500b into develop Nov 5, 2026
73 of 75 checks passed
@connortsui20
connortsui20 deleted the ct/decimal-read-plugin branch November 5, 2026 19:08
mhk197 added a commit that referenced this pull request Nov 6, 2026
## Summary

Removes the blanket `impl<V: VTable> ArrayPlugin for V` and the
`serialize` / `deserialize` methods on `VTable`. Every serializable
encoding now implements `ArrayPlugin` explicitly on its vtable type, in
a `plugin.rs` child module next to its `impl VTable`. Redo of #10111 on
current develop.

## Changes

- `VTable` covers only in-memory behaviour; serde is `impl ArrayPlugin
for X`.
- Serde bodies are ported verbatim, so wire formats are unchanged.
- `read_ids` / `write_ids` default to `vec![self.id()]`, what the
blanket impl declared. Plugins with one wire ID no longer spell them
out, including `ALPPatchedPlugin`, `ScalarFnArrayPlugin` and the Python
stub.
- Inside an impl the wire ID is `ArrayPlugin::id(self)`; only the `id`
method itself delegates to `VTable::id`.
- `session.arrays().register(X)` is unchanged for every encoding that
was serializable.
- In-memory-only vtables lose the two stub methods and get no plugin:
Filter, Slice, Interleave, ScalarFn, ScalarFnValidity, ForeignArray,
PiecewiseSequence, Widen, Shared, test-harness vtables.
- `DecimalPlugin` (#10995) becomes `impl ArrayPlugin for Decimal`: one
wire ID, same legacy-node reads, no separate type. Its module,
`StoredDecimal` and tests stay where they are.
- Multi-ID plugins (`BitPackedPlugin`, `FoRPlugin`,
`DecimalBytePartsPlugin`) and `WidenPlugin` are untouched;
`ALPPatchedPlugin` now delegates to `ArrayPlugin::deserialize(&ALP,
..)`.
- `PythonVTable` keeps a stub plugin so the Python `register` API still
compiles.
- Vtable files that gained a `plugin.rs` child are directory modules
(`array.rs` becomes `array/mod.rs`, 14 pure renames). ALP's own impl and
`ALPPatchedPlugin` share `alp/array/plugin.rs`.
- Tests that called the vtable serde directly (ALP, ALPRD, Dict, List,
RunEnd, VarBin, RLE, serde.rs, unary, zstd buffers) go through
`ArrayPlugin` instead.

| | count |
|---|---|
| new `impl ArrayPlugin` (vortex-array) | 20 |
| new `impl ArrayPlugin` (encodings + python) | 17 |
| vtables that only lose the stub methods | 14 |

## API changes

- `VTable` loses `serialize` and `deserialize`. An external vtable that
was registered through the blanket impl must now `impl ArrayPlugin` to
be registrable.
- `ArrayPlugin::read_ids` / `write_ids` gain defaults of
`vec![self.id()]`.
- `vortex_array::arrays::DecimalPlugin` is removed; register `Decimal`
instead.

## Behaviour changes

- `PiecewiseSequence` is no longer registered by
`ArraySession::default()`; it never serialized, and its error is now "is
not registered for serialization".
- The writer seeds the footer's encoding list from the session registry,
so dropping `vortex.piecewise-sequence` makes every default-session file
48 bytes smaller. `test_stock_ticker_file_size` expectations move
accordingly (70060 → 70012, 69996 → 69948, 55144 → 55096).
- `DecimalByteParts`, `BitPacked` and `FoR` can no longer be registered
bare, so `bare_vtable_refuses_serde` and `vtable_serde_requires_plugin`
are removed.

## Testing

- `cargo clippy --all-targets --all-features -- -D warnings` and `cargo
+nightly-2026-09-10 fmt --check` on every touched crate (vortex-array,
vortex-file, vortex-layout, vortex-python and the 13 encodings crates).
- `cargo nextest run` on all touched crates plus vortex-ipc and
vortex-scan: 8981 passed, 0 failed; `cargo test --doc` on vortex-array,
fastlanes, decimal-byte-parts and alp.
- `cargo nextest run -p vortex-file -E
'test(test_stock_ticker_file_size)'` with and without `--features zstd`,
to derive the new sizes.

## Follow-up

- Pre-existing, unchanged here: the Patched, RLE, OnPair, ALPRD, FSST
and Zstd deserializers index children or buffers before checking their
counts, so a malformed node panics in `SerializedArray::child` instead
of returning an error. Worth hoisting the count checks in a separate PR.
- Fold `FoRPlugin` / `BitPackedPlugin` / `DecimalBytePartsPlugin` into
`impl ArrayPlugin for FoR` etc. if wanted.
- Overlaps with #11298, which makes the same change with inline impls
and trait defaults for `read_ids` / `write_ids`.

---------

Signed-off-by: Matt Katz <mhkatz97@gmail.com>
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.

3 participants