Repository navigation
Batch native Highway operations to reduce Java/native overhead - #761
Conversation
This reverts commit 6c51c1f2d0fae0b13f2944d1ac295261d3f86216.
|
Before you submit for review:
If you did not complete any of these, then please explain below. |
MarkWolters
left a comment
There was a problem hiding this comment.
Approved, although take the comment into consideration as a possible change before merging.
jshook
left a comment
There was a problem hiding this comment.
PR #761 review: Batch native Highway operations
Reviewed at 50d7d50d against origin/main eba7bd4e. The equivalence claims hold. There is
one speed regression on 128-bit vector platforms, plus API and test gaps.
Verification
- Full reactor suite with the native provider forced (JDK 24, avx3_dl): 496 tests, 0 failures,
5 skipped (unrelated). jvector-nativesuite: 43/43 pass atJVECTOR_MAX_ISA=avx2+-XX:MaxVectorSize=32, and at
sse42+MaxVectorSize=16.
Findings
-
The 8-dimension KMeans++ path is 3.6–5× slower at 128 bits
(NativeKMeansInitialization.java:37).-
SPECIES_256falls back to unaccelerated Java code wherever there's no 256-bit hardware.
Main used Highway'seuclidean_f32here, which adapts to the ISA. -
Timing of one seeding pass (32K points, 255 updates), against main's native loop in the
same JVM. Results were identical in every row.Width PR main native PR ÷ main 512 (avx3_dl) 5.3 ns/dist 14.1 0.38× 256 (avx2, MaxVectorSize=32) 5.1 14.0 0.37× 128 (sse42, MaxVectorSize=16) 55–77 15.4 3.6–5.0× -
The 128-bit row is simulated on x86. NEON, 128-bit SVE2 (Graviton4) and SSE4.2-capped x86
are expected to match. arm64 CI checks correctness only. -
Suggested fix: use this path only when
SPECIES_PREFERRED.vectorBitSize() >= 256;
otherwise returnfalse. -
The comment "Main's exact-width path" describes Panama. For the native provider these
distances now differ from main, within tolerance.
-
-
NVQ-only copies of generic operations in the public API (
VectorUtil.java:253,
VectorUtilSupport.java:210).nvqMin,nvqScaleand twonvqSuboverloads compute the same thing asmin,scale
andsub. They only select a different implementation per call site (see 1899795,
82cd218).- With the
try*methods andsupportsNvqCosine8bitPacked, that's eight new public entry
points, most without javadoc. - Prefer keeping them internal to the native provider, or choosing by length inside
NativeVectorUtilSupport. If they stay public, document them.
-
Unneeded
volatileand locking in the lazy view (MemorySegmentVectorFloat.java:37,
:67).- Both states reference the same array, so a race only produces two equivalent views. A heap
segment's state lives in final fields, which should make a plain field safe (worth
confirming). - As written, every
get(i)/set(i)pays for a volatile read and a type check that the JIT
can't move out of loops; thedistanceValuesworkaround inKMeansPlusPlusClusterer
suggests this cost.synchronized (this)also locks on a public object. - After a vector's first native use, scalar reads take
getAtIndexplus an extra bounds
check.
- Both states reference the same array, so a race only produces two equivalent views. A heap
-
encodeTolost its length check (NVQuantization.java:209). A longer vector is encoded
silently using only its prefix; a shorter one fails with an index error instead of an
argument error. -
Assertions double the closest-centroid cost under
-ea(VectorUtil.java:111). This is
deliberate, but worth noting in the PR description.
Verified correct:
closest_centroid_f32: same comparison, ties and NaN behavior.nvq_quantize_8bit:DemoteTosaturation matches the old clamp, with a bounded tail store.nvq_shuffle_query_in_place_8bit: theLoadInterleaved4order matches the old permutation,
and the in-place write is safe.nvq_growth_rate: same candidates, increments, starting value and comparison ascompute.subtract_addandscale_to: arithmetic unchanged, overlap checked before any write.- ABI 1: no released library is affected, and the missing-symbol fallback works.
Test coverage
The correctness oracles are strong: bitwise comparisons with the replaced code, scalar oracles,
ties, non-finite values, tails, aliasing and concurrency. CI runs them, and the C++ tests, at
every ISA cap on both architectures. Gaps:
- No performance check below 256 bits. All benchmarks ran on one Xeon. A Graviton run of
PQ training would cover finding 1. KMeansInitializationTestuses the wrong baseline. It compares with Panama, not the
native path it replaces. Seeding is where codebooks can now differ from main, and only recall
benchmarks cover that.- x86 CI never caps the Java vector width. The Java-side paths run below 256 bits only on
arm64. Adding-XX:MaxVectorSize=16to thesse42job would fix that. - No direct test of the packed
NVQScorercosine path against the unpacked one. - Minor:
- No test for the
encodeTolength case. KMeansInitializationTestlacksrequireNativeProvider(), so it errors rather than skips
where the native library can't load.
- No test for the
Reproduce the 128-bit timing (a throwaway JUnit class in jvector-native tests, available on
request):
JVECTOR_MAX_ISA=sse42 JAVA_TOOL_OPTIONS=-XX:MaxVectorSize=16 mvn -B test -pl jvector-native -am -Dtest=<class> -Dsurefire.failIfNoSpecifiedTests=false.
JAVA_TOOL_OPTIONS is needed because the module's surefire config sets its own argLine.
|
Thanks @MarkWolters and @jshook. Pushed
The 11 focused tests passed in all six JDK 22/25 × ISA/width configurations, without skips. The original Java-only KMeans test needs no native-library guard; the new native-reference test has one. The 128-bit specialization remains unpublished while I verify that it preserves 256/512-bit performance on both JDKs. I’m retaining synchronized lazy-view publication for this PR; removing it warrants separate concurrency and performance validation. |
|
Thanks for the detailed reviews @MarkWolters and @jshook. The follow-up commits address the findings:
All 20 CI checks pass, including ARM and capped x86 configurations. Narrow-width performance measurements used capped x86. |
jshook
left a comment
There was a problem hiding this comment.
All known gaps have been closed
Goal and changes
Reduce Java/native call overhead and temporary allocation in the Google Highway backend, especially during PQ training and encoding. Builds on the native kernels and provider selection already merged in #753 and #751.
Performance results
Both arms use native Highway AVX-512. Each cell shows main → this PR; bold marks the winning measurement.
PQ training is approximately 1.9–3.2× faster, and encoding 2.3–3.5× faster. Total construction is lower in every comparison. Single-thread query throughput improves in seven of eight dataset/JDK combinations. I confirmed that recall@10 is essentially unchanged.
Construction — seconds, lower is better
Queries — QPS, higher is better
Graph/write remains slower in several cases. The fresh Cohere/JDK22 repeat shows 7.6% higher graph/write time, with 10.1% lower total construction time; its cause is not established. Cohere/JDK22 single-thread QPS is 2.3–4.5% lower.
Method: BenchYAML, JDK22/25, four nominal-1M datasets, 128 GiB heap, 48 construction workers, deferred batch writer, FusedPQ width8/K256 uncentered/unweighted, NVQ with two 8-bit subvectors, V6 M32/ef100, hierarchy/refinement enabled, pruning and assertions disabled. Every construction uses a fresh codebook and graph. Queries use resident graphs, pinned 1-/8-thread execution, top10 at 1×/2× overquery, and three warmup/measurement rounds.
† Fresh A-B-B-A medians for main
eba7bd4eversus latest PR headcebb94f8, replacing the earlier Cohere/JDK22 construction row.Other rows compare historical main
eba7bd4emeasurements with thef65c91f8rerun on the same server. The latest scalar fix received separate correctness/microbenchmark validation and the Cohere construction repeat; the full query sweep was not repeated after it.Test and methodology