Skip to content

feat(bitpacking): Add batched index unpacking - #190

Merged
lwwmanning merged 10 commits into
developfrom
wm/batch-unpack-single
Sep 1, 2026
Merged

feat(bitpacking): Add batched index unpacking#190
lwwmanning merged 10 commits into
developfrom
wm/batch-unpack-single

Conversation

@lwwmanning

@lwwmanning lwwmanning commented Aug 27, 2026

Copy link
Copy Markdown
Member

Add unpack_indices and unchecked_unpack_indices to extract selected values without a complete 1,024-value unpack.

Use unpack_indices when the bit width and packed length are compile-time constants. Use unchecked_unpack_indices when the bit width is available only at runtime. It dispatches the runtime width once per batch.

Both methods write into MaybeUninit output. Use unpack_single for one selected value. Use full unpack above the recommended threshold.

One private #[inline(always)] helper implements single-value extraction. Public unpack_single keeps an out-of-line boundary. The runtime dispatcher and batch path inline the helper after width dispatch. This keeps width-specific expansion inside library code.

Performance

Native Apple M1 benchmarks use Rust 1.91.0 and -C target-cpu=native.

At 8 and 32 selected values, batch extraction is 2.0–3.4× faster than repeated unpack_single calls.

The table gives a conservative dispatch policy across the sampled packed widths.

Physical type Use batch when Use full unpack when
u8 n <= 16 n > 16
u16 n <= 32 n > 32
u32 n <= 64 n > 64
u64 n <= 160 n > 160

The grid covers widths 1, 4, and 7 for u8. It covers widths 1, 3, 8, and 15 for u16.

It covers widths 1, 8, 16, 24, and 31 for u32. It covers widths 1, 16, 32, 48, and 63 for u64.

The limiting measured crossovers were 18–19, 32–34, 68–72, and 176–184. The rounded thresholds leave margin for other hardware.

Packed-width effects were non-monotonic, so the policy uses only the physical type and selected-value count.

cargo asm confirms one runtime-width dispatch per batch. The generated u16 batch function contains no per-index unpack_single calls.

Code size

The release rlib grows from 3,750,536 bytes on develop to 3,942,416 bytes, a 5.1% increase. Object text grows from 419,814 bytes to 447,917 bytes, a 6.7% increase.

Verification

Tests cover every integer type and runtime width. They also cover empty, duplicate, unordered, full-block, zero-width, and invalid inputs.

🤖 Generated with Codex

Signed-off-by: Will Manning <will@willmanning.io>
@codspeed-hq

codspeed-hq Bot commented Aug 27, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 41.77%

⚡ 4 improved benchmarks
✅ 154 untouched benchmarks
🆕 36 new benchmarks
⏩ 135 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation throughput_compress 2.3 ms 1.5 ms +55.37%
Simulation unfor_pack_16_from_3_stack 3.2 µs 2.2 µs +41.04%
Simulation for_pack_16_to_3_stack 3.4 µs 2.5 µs +36.54%
Simulation unchecked_unfor_pack_16_from_3_stack 3.3 µs 2.5 µs +35.02%
🆕 Simulation batched[1] N/A 1.2 µs N/A
🆕 Simulation batched[128] N/A 6.2 µs N/A
🆕 Simulation batched[32] N/A 3.9 µs N/A
🆕 Simulation batched[8] N/A 2.2 µs N/A
🆕 Simulation full_unpack_then_gather[1] N/A 4.3 µs N/A
🆕 Simulation full_unpack_then_gather[128] N/A 6.4 µs N/A
🆕 Simulation full_unpack_then_gather[32] N/A 4.8 µs N/A
🆕 Simulation full_unpack_then_gather[8] N/A 4.4 µs N/A
🆕 Simulation repeated_single[1] N/A 816.7 ns N/A
🆕 Simulation repeated_single[128] N/A 6.1 µs N/A
🆕 Simulation repeated_single[32] N/A 3.6 µs N/A
🆕 Simulation repeated_single[8] N/A 1.8 µs N/A
🆕 Simulation batched[1] N/A 1.2 µs N/A
🆕 Simulation batched[128] N/A 5.2 µs N/A
🆕 Simulation batched[32] N/A 2.1 µs N/A
🆕 Simulation batched[8] N/A 1.4 µs N/A
... ... ... ... ... ...

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

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing wm/batch-unpack-single (efbe1cb) with develop (6e2aaa6)

Open in CodSpeed

Footnotes

  1. 135 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.

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 marked this pull request as ready for review August 28, 2026 16:09
@lwwmanning
lwwmanning enabled auto-merge (squash) August 28, 2026 16:09

@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: 28f12cd2d9

ℹ️ 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".

Comment thread src/bitpacking.rs Outdated
Signed-off-by: Will Manning <will@willmanning.io>
Comment thread src/bitpacking.rs
@joseph-isaacs

joseph-isaacs commented Aug 28, 2026

Copy link
Copy Markdown
Member

Do we know how much this will increase compile time and code gen size?

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

@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: ce09c988c7

ℹ️ 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".

Comment thread src/bitpacking.rs
Signed-off-by: Will Manning <will@willmanning.io>
@lwwmanning
lwwmanning disabled auto-merge August 28, 2026 18:05
@lwwmanning

Copy link
Copy Markdown
Member Author

Do we know how much this will increase compile time and code gen size?

added ~5% to rlib size, increased release build time by < 1 second

Signed-off-by: Will Manning <will@willmanning.io>
Signed-off-by: Will Manning <will@willmanning.io>
@lwwmanning
lwwmanning enabled auto-merge (squash) August 28, 2026 19:49
Comment thread src/bitpacking.rs Outdated
Comment thread src/bitpacking.rs Outdated
@robert3005

Copy link
Copy Markdown
Member

I think you get 99% of this feature if you remove the private #[inline(always)] helper and add #[inline] on unpack_single and uncheck_unpack_single. This would lead to slightly larger rlib size but it's just 0.5% more

@robert3005

Copy link
Copy Markdown
Member

also if we document to the callers that if they want to loop over indices they should use

for (&index, output) in indices.iter().zip(outputs) {
      output.write(T::unpack_single::<W, B>(packed, index));
  }

instead of

for index in indices {
      T::unchecked_unpack_single(width, packed, index);
  }

then they get the benefits of this pr with minimal changes here

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T19:59:47.930993Z efbe1cb New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@lwwmanning

Copy link
Copy Markdown
Member Author

also if we document to the callers that if they want to loop over indices they should use

for (&index, output) in indices.iter().zip(outputs) {
      output.write(T::unpack_single::<W, B>(packed, index));
  }

instead of

for index in indices {
      T::unchecked_unpack_single(width, packed, index);
  }

then they get the benefits of this pr with minimal changes here

That is true when the packed width is a compile-time constant (i.e., unpack_indices doesn't have much benefit over calling T::unpack_single::<W, B> in a loop).

Vortex's bit_width comes from array metadata at runtime. A loop over unchecked_unpack_single(bit_width, ...) retains runtime dispatch per index, which is ~2x slower than unchecked_unpack_indices across batches of 8–128 indices.

Vortex can avoid that cost only by duplicating the complete width match outside its loop... which is literally just implementing unchecked_unpack_indices in Vortex. We could do that, the only downside there is that the inlining policy across crates is less likely & more brittle (though is now possible with the change I just pushed adding #[inline] to unpack_single).

@robert3005 robert3005 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ok, we want unpack_indices because we cannot guarantee that unpack_single will be inlined across crates

@lwwmanning
lwwmanning merged commit 069f29d into develop Sep 1, 2026
5 checks passed
@lwwmanning
lwwmanning deleted the wm/batch-unpack-single branch September 1, 2026 20:20
@github-actions github-actions Bot mentioned this pull request Sep 1, 2026
lwwmanning added a commit that referenced this pull request Sep 1, 2026
The benchmarks from #190 did not compare the three paths fairly. The
compiler treated the bit width as a constant for some paths and removed
work that real callers still do. This PR gives every path the same
runtime bit width and covers all four integer types near the candidate
cutoffs.

It also restores extraneous code comment removals in #190 and makes the
new tests fail safely if an unpack method skips an output.
lwwmanning pushed a commit that referenced this pull request Sep 1, 2026
## 🤖 New release

* `fastlanes`: 0.7.0 -> 0.7.1 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

##
[0.7.1](v0.7.0...v0.7.1) -
2026-09-01

### Added

- *(bitpacking)* Add batched index unpacking
([#190](#190))

### Other

- *(bitpacking)* more representative unpack_indices benchmarks
([#194](#194))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants