Skip to content

Decode DecimalByteParts at the canonical decimal width - #10905

Open
connortsui20 wants to merge 3 commits into
ct/decimal-scalar-factory-widthfrom
ct/dbp-canonical-decode
Open

connortsui20 wants to merge 3 commits into
ct/decimal-scalar-factory-widthfrom
ct/dbp-canonical-decode

Conversation

@connortsui20

@connortsui20 connortsui20 commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Decoding a DecimalByteParts array reuses the MSP buffer at whatever width the MSP has, so a Decimal(18, 2) column with an i32 MSP decodes to an i32-backed DecimalArray. Later steps of #10701 make the canonical width from #10897 (DecimalDType::canonical_values_type()) an invariant of DecimalArray, so decoding has to produce it.

This must not merge before a release publishes the compat fixtures from #10753, because only those fixtures let CI check that old files with narrow MSPs still decode. A local check of fixtures written by the base of this PR passes all 43, including decimal_byte_parts_narrow_msp.vortex.

Changes

assemble_decimal now returns the canonical width. It reuses the MSP buffer when the MSP already has that width, widens a narrower MSP with a copy, and narrows a wider MSP with copy_to_canonical_width, which fails on a valid value outside the precision. One lower part at precision 39 or more assembles straight into i256. The widening copy is a pass that the old reuse didn't make, and the dbp_compute benches from #10752 will show its cost. A TODO(connor) at that branch notes the alternative of returning a Narrow over the MSP if that cost matters.

Every lower part must now be u64, and the number of lower parts is bounded by the precision (none up to precision 18, at most one up to 38), so assembly never needs to narrow. Nothing writes other lower parts today: split_decimal only emits u64 parts, and no edition permits v2. DecimalByteParts::encode now goes through the checked constructor, so a decimal stored wider than its canonical width can't produce more lower parts than its precision allows.

The CUDA decoder follows the same rule. It widens a narrow MSP on the device with decimal_cast, which moves out of the Arrow export into a shared module and gains an i16 output, and it returns an error for an MSP wider than the canonical width. The CUDA code was not built locally, so it needs a review from someone who knows it.

This is stacked on #10903, which is stacked on #10897. For review, read the commits one at a time.

API Changes

DecimalByteParts constructors and the v2 reader reject lower parts that are not u64, and more lower parts than the precision allows. Decoding returns a DecimalArray at the canonical width of its dtype.

@connortsui20 connortsui20 added the changelog/break A breaking API change label Oct 27, 2026
@codspeed

codspeed Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 46.28%

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

⚠️ 4 benchmarks are 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

⚡ 4 improved benchmarks
❌ 17 regressed benchmarks
✅ 2187 untouched benchmarks
⏩ 366 skipped benchmarks1
🗄️ 73 archived benchmarks run2

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ CPU Simulation decode[msp_i64_p38] 20.9 µs 241.5 µs -91.36%
❌ CPU Simulation decode[msp_i32_p18_nullable] 22.1 µs 123.6 µs -82.12%
❌ CPU Simulation decode[msp_i32_p18] 20.9 µs 115.7 µs -81.93%
❌ CPU Simulation decode[gap_msp_i16_p10] 20.8 µs 102.2 µs -79.65%
❌ CPU Simulation min_max[msp_i64_p38] 157.3 µs 416.8 µs -62.26%
❌ CPU Simulation sum[msp_i64_p38] 146.2 µs 344.4 µs -57.53%
❌ CPU Simulation sum[gap_msp_i16_p10] 83.3 µs 167.3 µs -50.21%
❌ CPU Simulation sum[msp_i32_p18] 97.8 µs 181.1 µs -46.02%
❌ CPU Simulation min_max[gap_msp_i16_p10] 109.8 µs 194.4 µs -43.55%
❌ CPU Simulation filter[msp_i64_p38] 155.4 µs 246.9 µs -37.07%
❌ CPU Simulation min_max[msp_i32_p18] 132.2 µs 209.7 µs -36.95%
❌ CPU Simulation sum[msp_i32_p18_nullable] 158.9 µs 237.5 µs -33.1%
❌ CPU Simulation filter[gap_msp_i16_p10] 90.9 µs 127.7 µs -28.85%
❌ CPU Simulation min_max[msp_i32_p18_nullable] 230 µs 310.5 µs -25.92%
❌ CPU Simulation filter[msp_i32_p18] 112.7 µs 152.1 µs -25.92%
❌ CPU Simulation decompress[dbp_bp_i32] 58.6 µs 78.8 µs -25.67%
❌ CPU Simulation filter[msp_i32_p18_nullable] 145 µs 182.7 µs -20.63%
⚡ Walltime threshold_neon[64] 3.8 µs 2.6 µs +46.25%
⚡ Walltime threshold_neon[80] 4.4 µs 3.3 µs +32.44%
⚡ Walltime take_u16_nullable_from_65536_avx2[1048576] 442.5 µs 382.4 µs +15.7%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

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


Comparing ct/dbp-canonical-decode (fca52f8) with ct/decimal-scalar-factory-width (41073b9)

Open in CodSpeed

Footnotes

  1. 366 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. ↩

  2. 73 benchmarks were run, but are now archived. If they were deleted in another branch, consider rebasing to remove them from the report. Instead if they were added back, click here to restore them. ↩

@connortsui20
connortsui20 force-pushed the ct/dbp-canonical-decode branch from 59eca0e to 881fe73 Compare October 27, 2026 22:08
@connortsui20
connortsui20 changed the base branch from ct/decimal-width-owner to ct/decimal-scalar-factory-width October 27, 2026 22:08
@connortsui20
connortsui20 added this pull request to stack #10904 October 27, 2026 22:08
Every lower part of a DecimalByteParts array now has the dtype `u64`.
Only the MSP may use a narrower signed integer dtype. The constructor,
`assemble_decimal` and the v2 reader reject `u8`, `u16` and `u32` lower
parts, and the error names `u64`.

The constructor also bounds the lower-part count, so the parts never
assemble wider than the canonical width of the decimal dtype. Precision
up to 18 allows no lower parts, 19 to 38 at most one, and 39 to 76 up to
three. `assemble_decimal` runs the same validation as the constructor.

`DecimalByteParts::encode` now uses the checked constructor, because a
decimal stored wider than its canonical width splits into more lower
parts than its dtype allows. The unchecked constructor had no other
caller, so it is removed.

Assembly no longer casts lower parts to `u64`. The docs, tests and the
`dbp_assemble_narrowed` bench now use `u64` lower parts.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
`assemble_decimal` now returns a decimal array at the canonical width of
its dtype, whatever the width of the MSP.

With no lower parts, an MSP at the canonical width is reused with no
copy and no scan. A narrower MSP is widened into a copy, and a TODO marks
where a later change can return a `Narrow` instead. A wider MSP is
narrowed with `copy_to_canonical_width`, which checks every valid value
against the precision and writes zeros in the null slots.

One lower part assembles into `i128`, or straight into `i256` at a
precision above 38. Two or three lower parts assemble into `i256`. The
constructor bounds the lower parts by the canonical width, so assembly
never narrows.

Every result is built with the unchecked constructor, because its width
is the canonical width and reads trust the precision.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
The CUDA DecimalByteParts decoder now returns a decimal array at the
canonical width of its dtype, as the CPU decoder does.

An MSP at the canonical width is reused with no copy. A narrower MSP is
widened on the device with the decimal_cast kernels. An MSP wider than
the canonical width returns an error: narrowing it must check the
precision, and that check on the device needs an overflow flag that
synchronizes with the host. The btrblocks scheme never writes such an
MSP. Lower parts still return an error on the GPU.

The device widening moves out of the Arrow export into a crate-private
kernel module, so the export and the decoder share one entry point. That
entry point rejects a narrowing cast. The decimal_cast kernels gain an
i16 output, because an i8 MSP at precision 3 or 4 widens to i16.

The GPU tests now assert the canonical width, and cover an i8 MSP at
precision 3, an i64 MSP at precision 40, an i32 MSP at precision 9 that
is reused, and an i64 MSP at precision 4 that is rejected.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20
connortsui20 force-pushed the ct/dbp-canonical-decode branch from 881fe73 to fca52f8 Compare October 27, 2026 22:18
@connortsui20

connortsui20 commented Oct 28, 2026 •

Copy link
Copy Markdown
Member Author

So the regressions are definitely real, but without narrow decimal encoding there's not much we can do.

I will note though that I think it is good we regress these benchmarks because now it actually shows how expensive a very large decimal is in its fully decoded representation.

@connortsui20
connortsui20 marked this pull request as ready for review October 28, 2026 13:49

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