Skip to content

Add filter-and-scatter RowFn execution - #9521

Merged
connortsui20 merged 1 commit into
developfrom
ct/row-fn-filter-scatter
Aug 26, 2026
Merged

connortsui20 merged 1 commit into
developfrom
ct/row-fn-filter-scatter

Conversation

@connortsui20

@connortsui20 connortsui20 commented Aug 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a private correctness fallback for partially valid RowFn batches. Direct skip-invalid execution remains the first choice. If an input decoder cannot safely decode the original null rows, or an output sink cannot initialize skipped rows, batch execution filters every input to the jointly valid rows, runs the ordinary dense kernel, and scatters the compact output back to the original row domain.

[A, null, B] -> filter -> [A, B] -> execute -> [x, y] -> scatter -> [x, null, y]

Why filter and scatter

The direct path already invokes the row function only for valid rows. However, it must create each Rust-native input view and the full-length output sink before entering that loop. Some representations cannot create a safe view over unspecified payloads behind nulls. Some sinks also have no meaningful value with which to initialize a skipped row. In either case, simply changing the loop to visit rows one at a time does not provide the missing input or output representation.

Calling the general scalar API once per valid row would work, but it would repeat array execution and scalar construction in the hot loop. A prepared, type-specific selected-row view is a better possible fast path for types that can support one. It is not a complete generic fallback because it does not solve skipped output initialization and needs a fallible per-row access contract for representations such as geometry.

Filter and scatter is not treated as an optimization. Filtering can materialize nested or compressed inputs, and scattering allocates original-length take indices. The executor uses it only after direct execution declines.

Directional ARM strategy comparison

Measured on an Apple M4 Max with Rust 1.98.0, target-cpu=native, the bench profile, and 100 Divan samples. Values are two-run average medians in microseconds. Lower is better.

Workload Existing geometry placeholder Generic filter/scatter Prepared selected-row prototype
Nullable point distance 20.11 17.85 15.16
Nullable polygon contains 40.15 44.71 49.09
90% null polygon contains 11.47 12.12 7.67
Two 50%-valid operands 13.77 14.66 7.63
Dense polygon contains 32.62 33.31 48.25

The prototype confirms that selected-row decoding can win for sparse validity. It is also 45% slower than filter/scatter on the dense polygon control and 10% slower on the ordinary nullable polygon case. This supports keeping selected-row decoding as an optional direct capability instead of making it the general execution model.

These are directional ARM results, not x86 runtime measurements.

Validation

  • Covers invalid payloads behind nulls, including row zero.
  • Covers sinks without skipped-row initialization.
  • Covers dense retry, runtime sink parameters, zero-width fixed-size lists, and logical extension output dtypes.
  • Preserves the direct valid-row fast path.
  • Passes the RowFn test suite, vortex-array doctests, formatting, and workspace clippy with all targets and features.

@codspeed

codspeed Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 4 improved benchmarks
❌ 1 regressed benchmark
✅ 2107 untouched benchmarks
🆕 18 new benchmarks
⏩ 106 skipped benchmarks1
🗄️ 4 archived benchmarks run2

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Simulation compact[(2048, 90)] 1.4 µs 1.5 µs -10.71%
⚡ WallTime arrow_checked_add_u32_neon[16384] 20.3 µs 13.5 µs +50.03%
⚡ WallTime arrow_checked_add_u32_avx512[16384] 21.2 µs 17.6 µs +20.55%
⚡ WallTime arrow_checked_add_u32_avx2[16384] 21.3 µs 17.7 µs +20.54%
⚡ WallTime words_gather_scalar_avx2[65536] 9.4 µs 8.2 µs +13.82%
🆕 WallTime infallible_bool_neon[i32, ConstantPerRow] N/A 4.5 µs N/A
🆕 WallTime infallible_bool_neon[i32, PerRowConstant] N/A 4.4 µs N/A
🆕 WallTime infallible_bool_neon[i32, PerRowPerRow] N/A 3.7 µs N/A
🆕 WallTime infallible_bool_neon[i64, ConstantPerRow] N/A 5.7 µs N/A
🆕 WallTime infallible_bool_neon[i64, PerRowConstant] N/A 5.7 µs N/A
🆕 WallTime infallible_bool_neon[i64, PerRowPerRow] N/A 5.6 µs N/A
🆕 WallTime infallible_bool_avx2[i32, ConstantPerRow] N/A 3.8 µs N/A
🆕 WallTime infallible_bool_avx2[i32, PerRowConstant] N/A 3.7 µs N/A
🆕 WallTime infallible_bool_avx2[i32, PerRowPerRow] N/A 2.8 µs N/A
🆕 WallTime infallible_bool_avx2[i64, ConstantPerRow] N/A 9.9 µs N/A
🆕 WallTime infallible_bool_avx2[i64, PerRowConstant] N/A 9.9 µs N/A
🆕 WallTime infallible_bool_avx2[i64, PerRowPerRow] N/A 10.3 µs N/A
🆕 WallTime infallible_bool_avx512[i32, ConstantPerRow] N/A 3.3 µs N/A
🆕 WallTime infallible_bool_avx512[i32, PerRowConstant] N/A 3.2 µs N/A
🆕 WallTime infallible_bool_avx512[i32, PerRowPerRow] N/A 2.3 µs N/A
... ... ... ... ... ...

ℹ️ 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/row-fn-filter-scatter (2cb8ddb) with develop (0a128b6)3

Open in CodSpeed

Footnotes

  1. 106 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. 4 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. ↩

  3. No successful run was found on develop (28f7cf0) during the generation of this report, so 0a128b6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@connortsui20
connortsui20 force-pushed the ct/row-fn-filter-scatter branch 4 times, most recently from 2268a7d to 84b879d Compare August 20, 2026 20:50
@connortsui20
connortsui20 marked this pull request as ready for review August 20, 2026 20:51
@connortsui20
connortsui20 force-pushed the ct/row-fn-filter-scatter branch from 84b879d to b6c09be Compare August 20, 2026 20:57
@joseph-isaacs

Copy link
Copy Markdown
Contributor

Can you merge this with the next pr so we can see if this is the best approach

@connortsui20

Copy link
Copy Markdown
Member Author

@joseph-isaacs this is not about being a better approach, it's that we literally cannot support spatial with rowfn without this

let me update the pr description to include more detail

Comment thread vortex-array/src/scalar_fn/unstable/row/batch/execute/filter_scatter.rs Outdated
@connortsui20
connortsui20 marked this pull request as draft August 21, 2026 16:03
@connortsui20

Copy link
Copy Markdown
Member Author

im actually going to hold off on this until we can figure out how we want to deal with @HarukiMoriarty's work

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20

Copy link
Copy Markdown
Member Author

The downstream consumer is now visible in #9623, which is stacked directly on this PR and contains only spatial changes. GeometryRow declines direct valid-row decoding, so this fallback handles the partially valid case without placeholder geometries or an InputElement API expansion. The PR body also includes the comparison with both the placeholder implementation and the prepared selected-row prototype.

@connortsui20
connortsui20 merged commit 8aeeac3 into develop Aug 26, 2026
113 of 114 checks passed
@connortsui20
connortsui20 deleted the ct/row-fn-filter-scatter branch August 26, 2026 17:13
"valid-only execution requires direct valid-row support; {} selected an unsupported signature",
self.id,
)
self.filter_and_scatter(kernel, &valid_rows, ctx)

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.

how is this the default here?

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.

the alternative is much more complicated (per-row decode needs to be defined by each input type). That can maybe be added in the future. This is just the easy way to do it

DenseWithRetry,

/// Execute only valid rows over the original inputs.
/// Execute only valid rows, filtering inputs if direct execution is unavailable.

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 is likely MUCH slower for most operations

@connortsui20 connortsui20 Aug 26, 2026 •

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.

yes, but its simple and doesnt require adding another 2k LoC (probably more)

connortsui20 pushed a commit that referenced this pull request Aug 26, 2026
## Overview

Depends on #9521.

Moves the remaining Rust `geo`-backed spatial scalar functions onto
RowFn. This removes the bespoke geometry execution adapters and leaves
constant handling, strict null propagation, input decoding, output
construction, and validity restoration with the shared executor.

`GeometryRow` now provides only its ordinary fallible decoder. It does
not manufacture placeholder geometries for null rows or expand the
shared `InputElement` API. On a partially valid batch, direct valid-row
execution declines and #9521 filters every input to the jointly valid
rows before geometry decoding.

## Implemented

- Migrates area, contains, intersects, convex hull, and length to RowFn.
- Preserves constant bounding-box prechecks for contains and intersects.
- Keeps length on native LineString and MultiLineString storage.
- Adds native MultiLineString support to ST_Length.
- Removes the bespoke geo-types execution layer.

## Execution choice

A prepared selected-row geometry view was prototyped separately. It was
substantially faster for sparse validity, but slower for ordinary
nullable predicates and about 45% slower than filter-and-scatter on the
dense polygon control. It also needs fallible per-row access for empty
geometry representations. The generic fallback is therefore the
correctness baseline, while a selected-row view remains a possible
optional fast path. The comparison and full rationale are documented in
#9521.

## Validation

- `cargo nextest run -p vortex-spatial`: 245 passed.
- `cargo clippy -p vortex-spatial --all-targets --all-features -- -D
warnings`.
- `cargo test --doc -p vortex-spatial`.
- `cargo +nightly fmt --all -- --check`.

---------

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
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.

3 participants