Repository navigation
Support decimal values in Widen - #10946
connortsui20 wants to merge 2 commits into
Conversation
Merging this PR will improve performance by 22.43%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing |
31f7451 to
9577faf
Compare
## Summary - Tracking Issue: #10701 - CD4 (#10946) builds on this PR. `Interleave` accepts decimal values but panics when it executes them, because only the bool and primitive kernels exist. Decimal `Widen` (CD4) falls back to canonical decimals for interleave, so it needs a decimal kernel first. ## Changes Most of the diff is a refactor of the primitive kernel: its gather moves into a `fixed_width` module next to the kernels, so decimals reuse it instead of getting a second copy. Canonical decimals are fixed-width buffers just like primitives, so one set of selector bounds checks and one tuned gather loop can serve both. The gather stays in `interleave` and not in `arrays/fixed_width` because it owns the selector contract of `Interleave` and has no other caller. Decimal sources stored at a non-canonical width are copied to the canonical width before the gather. That copy goes away once CD8a stores every decimal at its canonical width. ## API Changes `Interleave` executes decimal values and returns them at the canonical width of their dtype, where it used to panic. --------- Signed-off-by: Connor Tsui <[email protected]>
WD1 to WD4, CD3, CD3b and the decimal Interleave kernel merged, and CD4, CD5 and WD6 are open drafts. The plan now lists CD3b (#10939) and the Interleave kernel (CD4p, #10940) as dependencies of CD4, and CD3b as a dependency of CD6 and CD7. CD7 merges right after CD5 (#10905). D26 moves to CD6, because CD4 (#10946) adds no serialization lowering: the default writer executes a Widen, and reads of old files are the first ordinary source of one. The CD6 and WR1 briefs now carry that choice. D35 no longer makes WD6 wait for WD5. #10908 joins the follow-up issues, and the plan suggests settling the mechanism of Signed-off-by: Connor Tsui <[email protected]> #10748 before WD5.
9577faf to
29d4a4a
Compare
`widen_values` and `widened_buffer` widened each value with a checked cast and an `expect`, and collected through `BufferMut::extend_iter`, whose write loop has two exits. LLVM vectorized some width pairs, such as `i32` to `i64`, but left others scalar, such as `i64` to `i128`. Build the buffer with `from_trusted_len_iter`, like the Arrow exporter, and fall back to zero instead of panicking. The fallback is dead code when the target is at least as wide as the source, so the loop has one exit and no panic edge. `widened_buffer` now calls `widen_values`. On Apple M4, `i64` to `i128` is about 1.75x faster and `i32` to `i64` is unchanged, but widening into `i256` is 1.24x to 1.7x slower, for a reason we did not find. That widening only happens at precision 39 to 76, and once #10946 and #10905 land, most such columns decode to a `Widen`, so one loop for every width is the simpler trade. A test checks every widening pair against a per-value cast. Signed-off-by: Connor Tsui <[email protected]>
`widen_values` and `widened_buffer` widened each value with a checked cast and an `expect`, and collected through `BufferMut::extend_iter`, whose write loop has two exits. LLVM vectorized some width pairs, such as `i32` to `i64`, but left others scalar, such as `i64` to `i128`. Build the buffer with `from_trusted_len_iter`, like the Arrow exporter, and fall back to zero instead of panicking. The fallback is dead code when the target is at least as wide as the source, so the loop has one exit and no panic edge. `widened_buffer` now calls `widen_values`. A runtime assert checks that the target is at least as wide as the source. Its two sides are constants, so it folds away. A `const` assert does not compile, because the disabled `if const` branches of the callers still name the narrower pairs. On Apple M4, `i64` to `i128` is about 1.75x faster and `i32` to `i64` is unchanged, but widening into `i256` is 1.24x to 1.7x slower, for a reason we did not find. That widening only happens at precision 39 to 76, and once #10946 and #10905 land, most such columns decode to a `Widen`, so one loop for every width is the simpler trade. A test checks every widening pair against a per-value cast, and another checks that a narrower target panics. Signed-off-by: Connor Tsui <[email protected]>
29d4a4a to
9c42fc9
Compare
LLVM left some widening pairs scalar, such as `i64` to `i128`. `widen_values` and `widened_buffer` widened each value with a checked cast and an `expect`, and collected through `BufferMut::extend_iter`, whose write loop has two exits. Build the buffer with `from_trusted_len_iter`, like the Arrow exporter, and fall back to zero instead of panicking. The fallback is dead when the target is at least as wide as the source, so the loop has one exit and no panic edge. `widened_buffer` now calls `widen_values`. A runtime assert guards the widths and folds away. A `const` assert does not compile, because the disabled `if const` branches of the callers still name the narrower pairs. On Apple M4, `i64` to `i128` is about 1.75x faster and `i32` to `i64` is unchanged, but widening into `i256` is 1.24x to 1.7x slower, for a reason we did not find. That widening only happens at precision 39 to 76, and once #10946 and #10905 land, most such columns decode to a `Widen`, so one loop for every width is the simpler trade. Signed-off-by: Connor Tsui <[email protected]>
LLVM left some widening pairs scalar, such as `i64` to `i128`. `widen_values` and `widened_buffer` widened each value with a checked cast and an `expect`, and collected through `BufferMut::extend_iter`, whose write loop has two exits. Build the buffer with `from_trusted_len_iter`, like the Arrow exporter, and fall back to zero instead of panicking. The fallback is dead when the target is at least as wide as the source, so the loop has one exit and no panic edge. `widened_buffer` now calls `widen_values`. A runtime assert guards the widths and folds away. A `const` assert does not compile, because the disabled `if const` branches of the callers still name the narrower pairs. On Apple M4, `i64` to `i128` is about 1.75x faster and `i32` to `i64` is unchanged, but widening into `i256` is 1.24x to 1.7x slower, for a reason we did not find. That widening only happens at precision 39 to 76, and once #10946 and #10905 land, most such columns decode to a `Widen`, so one loop for every width is the simpler trade. Signed-off-by: Connor Tsui <[email protected]>
Tracking Issue: #10701 Bottom of stack #10991. #10989 builds on this PR. ## Summary Every path that needs the canonical decimal array widens its values with `copy_to_canonical_width`: the Arrow and DuckDB imports, interleave, and (after #10946) the execution of a decimal `Widen`. No benchmark covered that widening path. This lands before #10989 so that CodSpeed measures the loop change there against it. ## Changes Adds a `copy_to_canonical_width` bench that copies 16,384 values for `i32` to `i64`, `i64` to `i128` and `i128` to `i256`, with a same-width `i64` copy as the control. Widening never reads the validity, so the cases have no nulls. How the compiler vectorizes the widening loop depends on the target features, so every case carries `#[cpu_features]` and runs on the AVX2, AVX-512 and NEON walltime legs instead of in simulation. ## API Changes None. Signed-off-by: Connor Tsui <[email protected]>
LLVM left some widening pairs scalar, such as `i64` to `i128`. `widen_values` and `widened_buffer` widened each value with a checked cast and an `expect`, and collected through `BufferMut::extend_iter`, whose write loop has two exits. Build the buffer with `from_trusted_len_iter`, like the Arrow exporter, and fall back to zero instead of panicking. The fallback is dead when the target is at least as wide as the source, so the loop has one exit and no panic edge. `widened_buffer` now calls `widen_values`. A runtime assert guards the widths and folds away. A `const` assert does not compile, because the disabled `if const` branches of the callers still name the narrower pairs. On Apple M4, `i64` to `i128` is about 1.75x faster and `i32` to `i64` is unchanged, but widening into `i256` is 1.24x to 1.7x slower, for a reason we did not find. That widening only happens at precision 39 to 76, and once #10946 and #10905 land, most such columns decode to a `Widen`, so one loop for every width is the simpler trade. Signed-off-by: Connor Tsui <[email protected]>
Time compare, sum, min-max, filter, execute and cast on canonical decimal arrays before Widen accepts decimal children, so that the later commits have a baseline to compare their Widen numbers against. Signed-off-by: Connor Tsui <[email protected]>
Decimal values often need far fewer digits than their precision allows, yet the canonical width of that precision makes every compute step read and write the wider type. Widen already keeps integers at a narrower type for this reason. Accept a decimal child with the logical scale and nullability and a strictly narrower canonical width. Execution asks the scheduler for a Decimal child with `require_child!` and copies its values into the logical canonical width, and `encode_decimal` picks the smallest precision that holds the valid values. No kernel serves a decimal child yet. The integer kernels decline one, so compare, between, fill null, cast and the aggregates run on the canonical array. Tests cover the fallbacks of arithmetic, zip, chunked execution and interleave, run the shared conformance suites, and write decimal Widens through serialization, IPC and the flat writer. The `widen_decimal` bench now also times Widen inputs. Signed-off-by: Connor Tsui <[email protected]>
LLVM left some widening pairs scalar, such as `i64` to `i128`. `widen_values` and `widened_buffer` widened each value with a checked cast and an `expect`, and collected through `BufferMut::extend_iter`, whose write loop has two exits. Build the buffer with `from_trusted_len_iter`, like the Arrow exporter, and fall back to zero instead of panicking. The fallback is dead when the target is at least as wide as the source, so the loop has one exit and no panic edge. `widened_buffer` now calls `widen_values`. A runtime assert guards the widths and folds away. A `const` assert does not compile, because the disabled `if const` branches of the callers still name the narrower pairs. On Apple M4, `i64` to `i128` is about 1.75x faster and `i32` to `i64` is unchanged, but widening into `i256` is 1.24x to 1.7x slower, for a reason we did not find. That widening only happens at precision 39 to 76, and once #10946 and #10905 land, most such columns decode to a `Widen`, so one loop for every width is the simpler trade. Signed-off-by: Connor Tsui <[email protected]>
9c42fc9 to
fdf9ac2
Compare
Tracking Issue: #10701
Stacked on #11002. #11003 builds on it, and #10994 stacks on it too. No release gate.
Summary
#10701 gives each decimal precision one canonical width, and that width can be much wider than the values need. A
Decimal(38, 0)array of small counts still stores 16 bytes per value. A decimalWidenkeeps such values in a lower-precision child, so compute can run on the narrow buffer.This PR used to hold the whole decimal
Widenchange. It is now split into #11002, this PR, #11003, #11004, #11005 and #11006. Before #11006, no production code builds a decimalWiden(only tests and benches do), so no layer below it can change production behavior or performance.Changes
Widenaccepts a decimal child with the logical scale and nullability and a strictly narrower canonical width. The child precision must hold every valid value. Execution asks the scheduler for aDecimalchild withrequire_child!and copies its values into the canonical width.encode_decimalpicks the smallest precision that holds the valid values.No kernel serves a decimal child yet, and the integer kernels decline one, so compare, between, fill null, cast and the aggregates execute the canonical array. That is slower, but correct. Tests cover these fallbacks, the shared conformance suites, and writing decimal
Widens through serialization, IPC and the flat writer. Thewiden_decimalbench timesWideninputs next to canonical baselines, so CodSpeed on #11003, #11004 and #11006 measures each kernel against the fallback.Local benchmark results
cargo bench -p vortex-array --bench widen_decimal, Apple M4, medians. The numbers were measured at the top of this stack before the split, so they cover the whole stack, not this layer alone. The baseline is the canonical input, and the candidate is the same values as aWiden(child precision 5 under precision 10, 9 under 18, and 18 under 38). CodSpeed on this PR measures the layer itself.filterexecuteexecuteon aWidenis the widening copy, which the canonical input does not need. The canonicalfiltermedians at p10 and p18 were bimodal across runs, andfilterat p38 is slower on theWiden.API Changes
WidenArray::try_newaccepts decimal values. AddsWidenArray::encode_decimal.