Skip to content

Prefer Panama for small vectors - #791

Open
r-devulap wants to merge 2 commits into
mainfrom
panama-threshold
Open

r-devulap wants to merge 2 commits into
mainfrom
panama-threshold

Conversation

@r-devulap

@r-devulap r-devulap commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #782 and #771

Summary

Native query latency and index construction performs worser than Panama by ~15% - 25% on datasets with smaller dimensions (ex: nytimes-256-angular, sift-128-euclidean, etc.). This PR fixes two independent causes.

1. Per-visit cost of lazy MemorySegment creation (from #761)

#761 made MemorySegmentVectorFloat(float[]) keep the raw array and create its MemorySegment on the first native call. That helps vectors that stay in scalar Java code, but OnDiskGraphIndex.getVector(node) creates a fresh vector per visited node and uses it for one native call. Each visit therefore paid a synchronized block, a volatile publication and a VarHandle read, with nothing to spread the cost over.

  • MemorySegmentVectorFloat now keeps the float[] in a final field and caches the segment view in a plain field. The view is created lock-free. Racing threads may each build one, which is harmless because heap segments only have final fields and address the same array.
  • fromVectorFloat/intoVectorFloat and the MemorySegmentVectorProvider read/write paths use FloatArray.array() directly, so disk I/O never materializes a MemorySegment.

2. FFI boundary cost on low-dimension vectors

For small vectors the FFI call costs more than the native kernel saves. NativeVectorUtilSupport now delegates 1-to-1 dotProduct, cosine and squareDistance to the Panama Vector API when the vector length is <= NATIVE_DIMENSION_THRESHOLD (default 512, override with -Djvector.native_vector_threshold).

Also adds the NativeThresholdBenchmark JMH benchmark, which sweeps dimension to find the crossover.

Results

Setup: x8i.4xlarge (Intel GNR), full-precision build, main vs this branch, mean of 2 reps. Query latency is for an index built with 16 threads.

Native mode: mean query latency (ms)

Dataset Dim Overquery 1 (main → PR) Overquery 10 (main → PR)
lastfm-64-dot 64 0.066 → 0.043 (−34%) 0.179 → 0.138 (−23%)
sift-128-euclidean 128 0.152 → 0.110 (−28%) 0.806 → 0.591 (−27%)
nytimes-256-angular 256 0.152 → 0.112 (−26%) 0.811 → 0.589 (−27%)
e5-small-v2-100k 384 0.113 → 0.089 (−21%) 0.559 → 0.445 (−20%)

Latency drops 20–34% across every overquery value I checked (1, 2, 5, 10) on all four datasets. Recall@10 is unchanged apart from run-to-run noise of ≤0.01.

Native mode: index build time (s)

Dataset 1 thread 16 threads
lastfm-64-dot 85.0 → 73.6 (1.15×) 7.8 → 7.3 (1.07×)
sift-128-euclidean 762.6 → 543.9 (1.40×) 62.1 → 45.4 (1.37×)
nytimes-256-angular 207.5 → 159.2 (1.30×) 17.4 → 13.2 (1.32×)
e5-small-v2-100k 70.1 → 55.3 (1.27×) 6.1 → 5.1 (1.20×)

Panama mode (control)

Panama-mode runs should not change, and they don't. Query latency moves by −4% to +7% (the +7% is sift-128-euclidean at overquery 10) and build time by 0.93–1.06×, which is within run-to-run noise for 2 reps.

Plots

build_time latency_recall

r-devulap and others added 2 commits November 5, 2026 08:34
…r small vectors

Native query latency regressed (~15% on nytimes-256-angular) relative to
the pre-#761 baseline. There were two independent sources, both fixed here.

1. Per-visit cost of lazy MemorySegment creation (introduced by #761)

   #761 made MemorySegmentVectorFloat(float[]) keep the raw array and defer
   MemorySegment creation until the first native call, to help vectors that
   stay in scalar Java code (e.g. the KMeans distance array). But
   OnDiskGraphIndex.getVector(node) creates a fresh vector per node visited
   during traversal and hands it straight to NativeVectorUtilSupport
   cosine/dotProduct, which calls get(). These vectors are ephemeral (one
   native call, then discarded), so every visit paid a synchronized block,
   volatile publication and a VarHandle access with nothing to amortize.

   - MemorySegmentVectorFloat now holds the float[] in a final field and
     caches its MemorySegment view in a plain field. The view is created
     lock-free on first get(); racing threads may each build one, which is
     harmless because heap segments only have final fields and address the
     same array. The synchronized block, volatile store and VarHandle are
     gone, and get(int) is a plain array read.
   - fromVectorFloat/intoVectorFloat in NativeVectorUtilSupport and the
     MemorySegmentVectorProvider read/write methods use FloatArray.array()
     directly, so disk I/O no longer materializes a MemorySegment at all.

2. FFI boundary cost on low-dimension vectors

   For small vectors the cost of crossing the FFI boundary exceeds the gain
   from the native kernels. NativeVectorUtilSupport now delegates 1-to-1
   dotProduct, cosine and squareDistance to the Panama Vector API when the
   vector length is at or below NATIVE_DIMENSION_THRESHOLD (default 512,
   override with -Djvector.native_vector_threshold).

LazyMemorySegmentTest is updated for the new lazy behavior.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Measures 1-to-1 dotProduct, cosine and squareDistance across vector
dimensions for the native and Panama implementations, to help choose
the NATIVE_DIMENSION_THRESHOLD crossover point.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Before you submit for review:

  • Does your PR follow guidelines from CONTRIBUTIONS.md?
  • Did you summarize what this PR does clearly and concisely?
  • Did you include performance data for changes which may be performance impacting?
  • Did you include useful docs for any user-facing changes or features?
  • Did you include useful javadocs for developer oriented changes, explaining new concepts or key changes?
  • Did you rebase your branch onto the latest main for regression testing and PR submission?
  • Did you trigger regression testing via Run Bench Main and review results?
  • Did you adhere to the code formatting guidelines (TBD)
  • Did you group your changes for easy review, providing meaningful descriptions for each commit?
  • Did you ensure that all files contain the correct copyright header?
  • Did you add documentation for this feature to the release notes directory?

If you did not complete any of these, then please explain below.

@r-devulap r-devulap changed the title Panama threshold Prefer Panama for small vectors Nov 5, 2026
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.

NativeVectorUtilSupport: lazy MemorySegment promotion adds synchronized overhead on per-node-visit vectors

1 participant