Skip to content

[feat] tiled fixed size list array (PDX-style) - #9118

Open
lwwmanning wants to merge 54 commits into
developfrom
codex/tiled-fixed-size-list
Open

[feat] tiled fixed size list array (PDX-style)#9118
lwwmanning wants to merge 54 commits into
developfrom
codex/tiled-fixed-size-list

Conversation

@lwwmanning

@lwwmanning lwwmanning commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

Many vector workloads, including vector indices, benefit from storing fixed-size primitive lists in a two-dimensional, PDX-style layout so kernels can process tiles directly instead of transposing rows during scoring. This adds an experimental tiled fixed-size-list encoding behind unstable_encodings; it does not carry a stable wire-format guarantee.

What changes are included in this PR?

Adds the vortex-tiled-fsl crate with configurable row and dimension tiles, checked geometry, scalar access, canonical take fallback, file round trips, benchmarks, and differential fuzzing against canonical fixed-size lists. Full-width slices are O(1) row views at the TiledFSL layer and retain at most the selected rows plus two boundary tiles; a local slice-reduce benchmark measured 42.05 ns at 1,024 rows and 43.04 ns at 1,000,000 rows. Aligned multi-slab slices use one PiecewiseSequenceArray run per dimension slab. Unaligned multi-slab slices remain lazy and, when executed, issue a bounded slice to each child slab before canonical assembly; child-internal work remains encoding-dependent.

Element validity remains encapsulated by the nullable primitive elements child, while TiledFSL owns only outer-row validity. Serialized row views carry their logical offset and retained backing extent, including nullable FastLanes-bitpacked children. Tensor and TiledFSL registration remain available without the files feature and initialize only once in default sessions.

The default compressor does not select or preserve this encoding automatically. Callers that persist tiled arrays must choose an appropriate layout strategy for the relevant field. Coverage includes canonical conformance, malformed serde inputs, file round trips, deterministic stateful fuzzing, a 10,000-run libFuzzer pass, and structural scaling checks.

What APIs are changed? Are there any user-facing changes?

vortex::encodings::tiled_fsl is available behind unstable_encodings, and the unstable edition permits vortex.tiled_fsl. The experimental API exposes tiled array construction, TileGeometry, tile-bound iteration, encoding from primitive fixed-size lists, and access to the physical child and row-view metadata.

🤖 Generated with Codex

Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
…write

Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
@lwwmanning
lwwmanning requested a review from connortsui20 August 1, 2026 21:30
@lwwmanning
lwwmanning marked this pull request as ready for review August 3, 2026 00:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4dfc88bfc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if let (Some(source_validity), Some(output_validity)) =
(&source_validity, &mut output_validity)
{
output_validity.set_to(physical, source_validity.value(canonical));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid bit-at-a-time validity transposition

When nullable primitive elements resolve to Mask::Values, encoding performs a BitBuffer::value lookup and branch for every scalar; the inverse path repeats the same pattern at line 179. For large vector arrays this makes validity transposition a bit-at-a-time hot loop and discards the bitmap word-level fast paths, so transpose validity by u64 chunks with all-set/all-unset handling (or iterate set indices into an unset destination) and cover the nullable case in the transpose benchmark.

AGENTS.md reference: AGENTS.md:L185-L190

Useful? React with 👍 / 👎.

Comment on lines +7 to +9

pub const UNSTABLE_2026_07_0: EditionId = EditionId::new("unstable", 2026, 7, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the new public edition declarations

Add API documentation for UNSTABLE_2026_07_0 and DECLARATION, as well as module-level documentation for this publicly exported edition module. Every neighboring unstable-edition module documents these public definitions, while this new module exposes them without any doc comments, contrary to the repository requirement for every new public API definition.

AGENTS.md reference: AGENTS.md:L146-L146

Useful? React with 👍 / 👎.

Signed-off-by: Will Manning <will@willmanning.io>
Comment thread fuzz/src/tiled_fsl.rs
}

#[expect(clippy::result_large_err)]
fn fuzz<T>(result: VortexResult<T>) -> VortexFuzzResult<T> {

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 function is terrible, it's just a map_err, inline it everywhere and a lot of weird code will go away imho

Comment thread fuzz/src/tiled_fsl.rs
tiled.encoding_id()
);
Ok(())
})())?;

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.

We run reduce operations so I don't think you will have a slice here always, looks like this sometimes will end up with just the tiled array

use vortex_fuzz::run_tiled_fsl;

fuzz_target!(|input: FuzzTiledFsl| -> Corpus {
match run_tiled_fsl(input) {

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.

I think you can unwrap the error here and use vortexresult throughout the module. I only have VortexFuzzResult so that we have the necessary debug information for fuzzing

.collect::<VortexResult<Buffer<u64>>>()?;
let row = array
.elements()
.take(PrimitiveArray::from_iter(indices).into_array())?

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 not need from_iter, you already have a Buffer

Mask::Values(validity) => values
.iter()
.copied()
.zip(validity.bit_buffer().iter())

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.

bit_buffer().for_each_set_index?

}
let elements = elements.finish();
let decoded = decode_visible_elements(
elements.as_::<Primitive>(),

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 do we know this is primitive?

let span_elements = array
.elements()
.slice(span)?
.execute::<PrimitiveArray>(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.

this is weird

Comment on lines +75 to +81
let mut starts = Vec::<u64>::with_capacity(dimension_slab_count);
let mut lengths = Vec::<u64>::with_capacity(dimension_slab_count);

for span in spans {
starts.push(u64::try_from(span.start)?);
lengths.push(u64::try_from(span.len())?);
}

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.

build buffers directly

let validity = self
.as_ref()
.slots()
.get(TiledFixedSizeListSlots::VALIDITY)

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.

You will get TiledFixedSizeListSlotsExt which will give you a method so you can make this be just self.validity(). See StructArray

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