Skip to content

Add an in-memory Widen encoding for integers - #10864

Merged
connortsui20 merged 3 commits into
developfrom
ct/narrow-in-memory
Oct 28, 2026
Merged

connortsui20 merged 3 commits into
developfrom
ct/narrow-in-memory

Conversation

@connortsui20

@connortsui20 connortsui20 commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Read the tracking issue for context.

Note that this does not add Decimal support yet, though it will later.

Replaces #10223 and #10332, which are closed.

Changes

WidenArray::encode stores an integer array at the smallest width with the same signedness that holds its valid values. Slice, filter, mask, cast, fill null, take and compare run on the narrow values. Everything else, including aggregates and arithmetic until the next PRs in the stack, decodes through a cast, so results match the canonical array. Take executes eagerly instead of rewriting lazily, so that a Dict stays at the root, where the DuckDB exporter and the dictionary layout writer look for it.

Widen has no wire form yet, so ArraySession does not register it until the wire format lands, and serializing an array that holds a Widen fails like it does for any other unregistered encoding. Registering it now would also add a reader entry that could only fail.

WidenArray::encode_values is the deprecated variant that changes the dtype, for internal children such as list offsets and dictionary codes. The deprecation marks each future caller for its own decision.

For review, read the commits one at a time.

@connortsui20 connortsui20 added the changelog/feature A new feature label Oct 27, 2026
connortsui20 added a commit that referenced this pull request Oct 27, 2026
A registered VTable also gets a reader entry under its wire ID. For
Narrow, which has no wire form yet, that entry can only fail, so it
hides the allow_unknown foreign fallback and lists vortex.narrow in
footers written without editions. NR1 (#10864) therefore leaves Narrow
out of ArraySession, lowers every Narrow before the serializer lookup,
and makes Narrow::serialize panic with a TODO. WR1 adds the
registration together with the working deserializer.

D26, the NR1 brief and the wire brief now say so. The NR1 brief also
gets the final deprecation note of encode_values.

Signed-off-by: Connor Tsui <[email protected]>
connortsui20 added a commit that referenced this pull request Oct 27, 2026
NR1 (#10864) no longer lowers a Narrow in ArraySerializationTree. In
NR1 nothing outside the tests creates a Narrow, so the branch protected
no real path, and a branch specific to Narrow does not belong in the
generic serializer while nothing needs it. Serializing a Narrow now
fails like it does for any other unregistered in-memory encoding.

D26 is now deferred to CD4, the first PR where a Narrow reaches writers
in ordinary use. CD4 chooses between a lowering specific to Narrow, a
generic lowering for arrays that cannot be written, and no lowering.
The NR1 brief, the CD4 and WR1 briefs, the design and the plan now say
so.

Signed-off-by: Connor Tsui <[email protected]>
@codspeed

codspeed Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 4 benchmarks

3 warnings, 1 note

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

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

ℹ️ This run is excluded from the branch history

This commit was force-pushed, so this run no longer appears in the branch's performance history.

⚡ 8 improved benchmarks
❌ 4 regressed benchmarks
✅ 2212 untouched benchmarks
⏩ 370 skipped benchmarks1
🗄️ 30 archived benchmarks run2

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ CPU Simulation filter_powerlaw_by_random[10000] 889.4 ns 1,477.5 ns -39.8%
❌ CPU Simulation take_filter_primitive_small_uncached_random_mask_random_indices[4096, 10] 20.4 µs 24.7 µs -17.44%
❌ Walltime threshold_avx2[80] 2.6 µs 3 µs -11.42%
❌ Walltime add_constant_shapes_neon[(32768, PerRowConstant)] 14.2 µs 15.8 µs -10.2%
⚡ Walltime mul_u64_nonnull_neon 39.6 µs 20.6 µs +92.05%
⚡ CPU Simulation bench_compare_varbin[(50000, 2)] 164.5 µs 135.5 µs +21.42%
⚡ CPU Simulation bench_compare_varbin[(50000, 64)] 166.2 µs 137.4 µs +21.02%
⚡ CPU Simulation encode_primitives[u8, (2000, 4)] 163.7 µs 135.9 µs +20.47%
⚡ CPU Simulation bench_compare_sliced_dict_varbinview[(7500, 10000)] 230 µs 196.5 µs +17.04%
⚡ CPU Simulation bench_compare_sliced_dict_varbinview[(10000, 10000)] 236.1 µs 208.7 µs +13.16%
⚡ CPU Simulation bench_compare_sliced_dict_varbinview[(1000, 10000)] 92.2 µs 81.7 µs +12.89%
⚡ Walltime compare_u8_neon 1.7 µs 1.6 µs +10.44%

Tip

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


Comparing ct/narrow-in-memory (75c7f4a) with develop (c45aa5d)

Open in CodSpeed

Footnotes

  1. 370 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. 30 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. ↩

Comment thread vortex-array/src/arrays/narrow/array.rs Outdated
{
(*values_ptype, *logical_ptype)
}
_ => vortex_bail!("Narrow requires integer dtypes, got {values} values for {logical}"),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that in the future this will also check for Decimal

connortsui20 added a commit that referenced this pull request Oct 27, 2026
NR1 (#10864) gave vortex-array/src/arrays/narrow/ the layout of the
other encodings: one file per operation in compute/, and the vtable in
vtable/. The integer Narrow brief now describes that layout, and the
NR3, NR4 and NR5 file tables put their code there. The CD4 and wire
briefs point to the same section.

Signed-off-by: Connor Tsui <[email protected]>
Comment thread vortex-array/src/arrays/narrow/mod.rs Outdated
Comment on lines +17 to +18
//! Take executes its gather instead of leaving a lazy [`Dict`] under a `Narrow`, because code such
//! as the DuckDB exporter and the dict layout writer looks for a `Dict` at the root of an array.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to #10748

Widen stores an integer array in a strictly narrower integer type with
the same signedness and nullability. The values child holds the
validity and can have any encoding. try_new and a reduce rule keep
Widen arrays from nesting. Execution casts the values to the logical
dtype, and probe_scalar casts each child scalar.

WidenArray::encode stores a PrimitiveArray at the smallest integer type
of the same signedness that holds its valid values. The deprecated
encode_values returns the same narrower values without the Widen, for
internal children whose parent chooses their dtype, so that each future
caller is marked for its own decision.

Widen has no wire form yet, so ArraySession does not register it, and
serializing an array that holds a Widen fails as it does for any other
encoding that is not registered. A registered Widen would add a reader
entry that can only fail, which hides the allow_unknown foreign
fallback and lists vortex.widen in footers written without editions.

Signed-off-by: Connor Tsui <[email protected]>
Slice, filter, mask, cast and fill null run on the values and wrap the
result in a Widen again, with the nullability of the result. A cast to
a wider integer target of the same signedness keeps the values at their
own type, so a cast to a non-nullable target still fails on a null. A
cast to a float or to the other signedness, and a fill value outside
the range of the values, fall back to canonical execution.

Take is an execution kernel for the Dict parent. It executes the gather
of the values, so that no lazy Dict sits under a Widen, where the
DuckDB exporter and the dict layout writer would not find it.

The compare kernel compares two Widen operands at the wider value
type, and a constant inside the range of the values with the values. A
constant outside that range gives the same answer for every valid row,
so the kernel returns that answer with the validity of the values, as a
ConstantArray when there are no nulls.

The compute conformance suites run on each shared fixture.

Signed-off-by: Connor Tsui <[email protected]>
@connortsui20
connortsui20 marked this pull request as ready for review October 27, 2026 15:38
connortsui20 added a commit that referenced this pull request Oct 27, 2026
A check of the briefs against NR1 (#10864) found nine mismatches. The
NR3 kernels need a pub(crate) compute module, and the decode function
moves to array.rs, because the vtable modules are private. The
fastlanes test directory does not exist yet. There is no
ZipFalseReduce trait, so NR4 writes that rule by hand, and it reuses
integer_value instead of copying it. Narrow is not registered in
ArraySession. The other fixes update moved line numbers and the NR2
session recipe.

Signed-off-by: Connor Tsui <[email protected]>
The cast conformance suite casts an integer array only to integer
types, so this test is the only one that covers the cast rule declining
a float target, including the integer guard for unsigned values. It
keeps one signed and one unsigned case.

Signed-off-by: Connor Tsui <[email protected]>

@robert3005 robert3005 left a comment

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.

This should be called Widen and should be a ScalarFn

@connortsui20

Copy link
Copy Markdown
Member Author

once someone fixes everything in here we can make this a scalar fn too: #9156

@connortsui20
connortsui20 added this pull request to stack #10892 October 27, 2026 17:49
@connortsui20
connortsui20 enabled stacked auto-merge (squash) October 28, 2026 14:42
stacked auto-merge was automatically disabled October 28, 2026 14:43

Replaced by a newer auto-merge request

@connortsui20
connortsui20 enabled stacked auto-merge (squash) October 28, 2026 14:43
@connortsui20 connortsui20 changed the title Add an in-memory Narrow encoding for integers Add an in-memory Widen encoding for integers Oct 28, 2026
stacked auto-merge was automatically disabled October 28, 2026 16:16

Auto-merge was disabled for another pull request in the stack

@connortsui20
connortsui20 merged commit 6005d19 into develop Oct 28, 2026
110 of 112 checks passed
@connortsui20
connortsui20 deleted the ct/narrow-in-memory branch October 28, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants