Skip to content

⚡ Bolt: Use einsum for squared norms - #185

Open
stffns wants to merge 3 commits into
mainfrom
bolt/einsum-squared-norms-18326153558401829078
Open

⚡ Bolt: Use einsum for squared norms#185
stffns wants to merge 3 commits into
mainfrom
bolt/einsum-squared-norms-18326153558401829078

Conversation

@stffns

@stffns stffns commented Aug 11, 2026

Copy link
Copy Markdown
Owner

💡 What
Replaced row-wise squared Euclidean norm calculations ((X ** 2).sum(axis)) with np.einsum equivalents across snapvec/_kmeans.py, snapvec/_ivfpq.py, and snapvec/_pq.py. Emulated keepdims=True behavior by appending [:, None].

🎯 Why
Calculating (X ** 2) or (X * X) creates a large intermediate NumPy array equal to the size of X before performing the sum. For large N and D configurations (e.g., during IVFPQ codebook routing, K-means updates), this consumes significant memory bandwidth and incurs unnecessary allocations.

📊 Impact
Prevents large temporary array allocations during critical scoring, clustering, and routing operations. Benchmarks indicate a ~3x performance improvement in the respective computational blocks compared to the original .sum() approach.

🔬 Measurement
Review tests passing and verify performance via isolated python benchmarking scripts matching the logic implemented. Expected result is ~3x speedup on operations such as kmeans_pp_init, assign_l2, IVFPQ code additions, and PQ updates.


PR created automatically by Jules for task 18326153558401829078 started by @stffns

Summary by CodeRabbit

  • Performance Improvements

    • Improved memory efficiency and processing speed for vector indexing, search, clustering, and product quantization workloads.
    • Optimized squared-distance and norm calculations while preserving numerical results.
  • Bug Fixes

    • No functional behavior changes are expected; optimized calculations continue to produce equivalent results.
  • Tests

    • Added performance benchmarks to compare optimized and existing calculations and verify matching outputs.

Replaced instances of (X ** 2).sum(axis) with np.einsum('ij,ij->i', X, X) across the codebase (specifically in kmeans, ivfpq, and pq submodules) to avoid allocating large intermediate arrays and improve overall computational throughput by approximately 3-4x.

Co-authored-by: stffns <70039235+stffns@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbiteu

coderabbiteu Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@stffns, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a1beb14f-b500-4067-afef-c2f36cfdb056

📥 Commits

Reviewing files that changed from the base of the PR and between b182ba3 and 4fe6d75.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • snapvec/__init__.py
  • snapvec/_fast.pyi
  • snapvec/_file_format.py
  • snapvec/_index.py
  • snapvec/_residual.py
  • tests/test_adversarial.py
  • tests/test_file_format.py
  • tests/test_properties.py
  • tests/test_snapvec.py
📝 Walkthrough

Walkthrough

Changes

The PR replaces repeated elementwise squared-norm reductions with np.einsum in IVFPQ, PQ, and k-means calculations. It adds benchmarks that compare execution time and numerical results.

NumPy norm computation optimization

Layer / File(s) Summary
Core norm calculation replacements
snapvec/_ivfpq.py, snapvec/_kmeans.py, snapvec/_pq.py
Squared-norm and distance calculations now use np.einsum. Existing distance, assignment, probing, and encoding behavior remains unchanged. Type annotations use direct postponed-evaluation references.
Benchmark and equivalence validation
.jules/bolt.md, test_perf*.py
Benchmark scripts compare elementwise calculations with np.einsum, report timings, and verify numerical equivalence with np.allclose. Documentation records the row-wise norm pattern.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing squared-norm calculations with einsum.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bolt/einsum-squared-norms-18326153558401829078

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@stffns, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aab5d117-eec3-4c7a-9540-fac5dee4c831

📥 Commits

Reviewing files that changed from the base of the PR and between 66cbe33 and 4fe6d75.

📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • .jules/bolt.md
  • snapvec/__init__.py
  • snapvec/_fast.pyi
  • snapvec/_file_format.py
  • snapvec/_index.py
  • snapvec/_ivfpq.py
  • snapvec/_kmeans.py
  • snapvec/_pq.py
  • snapvec/_residual.py
  • tests/test_adversarial.py
  • tests/test_file_format.py
  • tests/test_properties.py
  • tests/test_snapvec.py
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bolt/einsum-squared-norms-18326153558401829078

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbiteu coderabbiteu 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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
test_perf_3.py (1)

11-26: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise the iterative kmeans_pp_init update.

This benchmark computes only the first-center distance. It does not execute the changed np.minimum(d2, ...) update for subsequent centers. Add multiple center updates and compare the final distance array and selected indices between implementations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test_perf_3.py` around lines 11 - 26, Extend the benchmark loops in
test_perf_3.py beyond the initial center so they exercise iterative
kmeans_pp_init distance updates using the existing d2 and d2_e arrays. Apply
equivalent np.minimum updates for multiple selected centers in both
implementations, then compare the final distance arrays and the selected center
indices, not just the initial distances.
test_perf.py (1)

1-24: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Guard benchmark execution from imports and explicit collection.

The repository’s default pytest configuration searches only tests, so it does not collect these root-level files by default. The files still execute all benchmark loops when imported or explicitly collected. Add a main() guard or rename the files to a benchmark-only pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test_perf.py` around lines 1 - 24, Wrap the benchmark setup, timing loops,
and output in a main() function, then invoke it only under an if __name__ ==
"__main__" guard in test_perf.py. Keep imports and constant definitions safe for
collection without executing benchmark work.
snapvec/_kmeans.py (1)

31-41: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add assignment-level equivalence tests.

ndarray.sum and np.einsum can produce different float32 reductions. Near ties or very small values, this can change argmin, k-means++ center selection, probe ranking, or PQ codes. Compare the optimized paths with the original formulas using near-tie, tiny-value, and non-contiguous float32 inputs. Cover snapvec/_kmeans.py, snapvec/_pq.py, and snapvec/_ivfpq.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@snapvec/_kmeans.py` around lines 31 - 41, Add assignment-level equivalence
tests for the optimized distance and reduction paths in _kmeans.py, _pq.py, and
_ivfpq.py, comparing them against the original ndarray.sum-based formulas.
Include non-contiguous float32 inputs plus near-tie and tiny-value cases, and
assert identical argmin, center-selection, probe-ranking, and PQ-code
assignments where applicable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test_perf.py`:
- Line 24: Replace the boolean-only print validation around cb_norms1 and
cb_norms2 with an assertion or equivalent non-zero exit when np.allclose returns
false, while preserving successful completion for matching results. Apply the
same validation behavior in test_perf_2.py through test_perf_7.py.

---

Nitpick comments:
In `@snapvec/_kmeans.py`:
- Around line 31-41: Add assignment-level equivalence tests for the optimized
distance and reduction paths in _kmeans.py, _pq.py, and _ivfpq.py, comparing
them against the original ndarray.sum-based formulas. Include non-contiguous
float32 inputs plus near-tie and tiny-value cases, and assert identical argmin,
center-selection, probe-ranking, and PQ-code assignments where applicable.

In `@test_perf_3.py`:
- Around line 11-26: Extend the benchmark loops in test_perf_3.py beyond the
initial center so they exercise iterative kmeans_pp_init distance updates using
the existing d2 and d2_e arrays. Apply equivalent np.minimum updates for
multiple selected centers in both implementations, then compare the final
distance arrays and the selected center indices, not just the initial distances.

In `@test_perf.py`:
- Around line 1-24: Wrap the benchmark setup, timing loops, and output in a
main() function, then invoke it only under an if __name__ == "__main__" guard in
test_perf.py. Keep imports and constant definitions safe for collection without
executing benchmark work.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 812edc64-ca81-4516-8672-a2bf737a59d2

📥 Commits

Reviewing files that changed from the base of the PR and between 66cbe33 and b182ba3.

📒 Files selected for processing (11)
  • .jules/bolt.md
  • snapvec/_ivfpq.py
  • snapvec/_kmeans.py
  • snapvec/_pq.py
  • test_perf.py
  • test_perf_2.py
  • test_perf_3.py
  • test_perf_4.py
  • test_perf_5.py
  • test_perf_6.py
  • test_perf_7.py

Comment thread test_perf.py Outdated
t1 = time.time()
print(f"Time einsum: {t1 - t0}")

print(np.allclose(cb_norms1, cb_norms2))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail the validation when results differ.

print(np.allclose(cb_norms1, cb_norms2)) reports False but still exits successfully. If this script is used for equivalence validation, a regression can pass automation. Raise an AssertionError or exit with a non-zero status. Apply the same change to test_perf_2.py through test_perf_7.py.

Proposed fix
-print(np.allclose(cb_norms1, cb_norms2))
+if not np.allclose(cb_norms1, cb_norms2):
+    raise AssertionError("Norm implementations differ")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(np.allclose(cb_norms1, cb_norms2))
if not np.allclose(cb_norms1, cb_norms2):
raise AssertionError("Norm implementations differ")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test_perf.py` at line 24, Replace the boolean-only print validation around
cb_norms1 and cb_norms2 with an assertion or equivalent non-zero exit when
np.allclose returns false, while preserving successful completion for matching
results. Apply the same validation behavior in test_perf_2.py through
test_perf_7.py.

google-labs-jules Bot and others added 2 commits August 11, 2026 18:18
Applied ruff automatic fixes and resolved lint/type issues discovered in CI:
- Removed quotes from type annotations for self-referential types and updated to use Python 3.10+ class names as references or `__future__` imports
- Formatted `__all__` arrays
- Sorted imports via isort
- Replaced redundant `dict()` usage in parametrization arrays in tests
- Correctly combined `with` statements where possible.

Co-authored-by: stffns <70039235+stffns@users.noreply.github.com>
Recent numpy releases (>=2.5.0) use the `type` statement in `__init__.pyi`, which mypy currently fails to parse when its configured `python_version` is below 3.12 (it is 3.10 in this repo). Pinned numpy in the GitHub Actions `lint` job so it successfully downloads an older numpy before running `mypy --strict`.

Co-authored-by: stffns <70039235+stffns@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.

1 participant