Skip to content

Add int8 / byte-vector similarity support - #709

Open
r-devulap wants to merge 16 commits into
mainfrom
int8-support
Open

Add int8 / byte-vector similarity support#709
r-devulap wants to merge 16 commits into
mainfrom
int8-support

Conversation

@r-devulap

@r-devulap r-devulap commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Add int8 / byte-vector similarity support

Extends JVector with end-to-end support for quantized int8 (byte) vectors — from core data types and graph indexing through to SIMD-accelerated similarity kernels and benchmarks.

Note: This branch contains commits from and depends on the simd-testing branch (see PR #708). It is recommended to merge that PR first, or target this PR against simd-testing until it lands.


Foundation

  • b5d3c53: Add RandomAccessByteVectorValues interface and ListRandomAccessByteVectorValues
    Byte-vector parallel to RandomAccessVectorValues, keeping the HNSW pipeline fully type-safe on ByteSequence<?> without any float32 round-trip. A separate interface is used because ByteSequence<?> and VectorFloat<?> share no common parent and mixing them would cause silent dequantization or runtime casts.

  • 0e6c317: Add byte-similarity methods to VectorUtilSupport / VectorUtil / DefaultVectorUtilSupport
    Adds dotProduct, squareDistance, and cosine for signed int8 vectors to the provider dispatch layer. Scalar implementations land in DefaultVectorUtilSupport; PanamaVectorUtilSupport gets stubs so the module compiles ahead of the SIMD work.

  • a7508ba: Add ByteVectorSimilarityFunction enum
    EUCLIDEAN, DOT_PRODUCT, and COSINE variants normalised to [0,1], mirroring VectorSimilarityFunction conventions for byte vectors.

Graph indexing

  • e93df41 : Add BuildScoreProvider.byteVectorScoreProvider factory
    Wires RandomAccessByteVectorValues + ByteVectorSimilarityFunction into the builder's score-provider abstraction, keeping all scoring byte×byte with no float conversion during graph construction.

  • e1050bd: GraphIndexBuilder byte-vector constructor, build(), and addGraphNode() overloads
    Convenience entry points for building and incrementally updating a graph over byte vectors, reusing all existing graph-construction logic without touching VectorFloat.

  • 1d00f35: Fix ambiguous GraphIndexBuilder constructor call in TestVectorGraph
    Adds explicit casts to resolve the overload ambiguity introduced by the new byte-vector constructor.

SIMD acceleration

  • fd499b4: Vectorize ByteSequence similarity metrics with Panama SIMD and native AVX-512/AVX2 kernels
    Replaces scalar stubs with Panama Vector API implementations (B2I widening, 512/256/128-bit dispatch) and native Highway kernels (dot_product_i8, euclidean_i8, cosine_i8) wired through JNI. Includes C++ and Java unit tests cross-checked against the scalar baseline.

Testing & benchmarks

  • 660e806: Add Int8IndexBuild end-to-end example
    Walks the full pipeline: load .bvec data, build a byte-vector graph, query with on-the-fly quantization, and report recall.

  • 39ba02d: Add SiftLoader.readBvecs for loading .bvec files
    Adds a reader for the binary bvec format along with siftsmall dataset files for tests and examples.

  • 9ee1486: Add INT8 benchmark pipeline via SQ compression type in BenchYAML
    Adds ScalarQuantizer, wires a new SQ compression type into the benchmark grid, and adds a sift-128-euclidean-int8.yml config. Queries are quantized on-the-fly; float inline vectors are stored for final reranking.

Covers cosine_f32, dot_product_f32, euclidean_f32, elementwise ops, and
CPU-feature dispatch across SSE42/AVX2/AVX512 lane widths and tail paths.
- New benchmarks/bench_similarity_f32.cpp: parameterised benchmarks for
  cosine_f32, dot_product_f32, and euclidean_f32 over array sizes
  128, 256, 512, 1024, 1536, and 3072.
- meson.build: add optional google-benchmark dependency (required: false);
  builds bench_simd_kernels executable only when the library is present.
- Install libgtest-dev alongside meson/ninja so GTest is found by meson.
- Build test_simd_kernels with 'ninja -C build test_simd_kernels' after
  checkout (before the JDK setup, since it is a pure C++ step).
- Run the binary once per matrix.max_isa leg:
    avx512f  → no JVECTOR_MAX_ISA cap (auto-detects best ISA on runner)
    avx2     → JVECTOR_MAX_ISA=avx2
    sse42    → JVECTOR_MAX_ISA=sse42
…to the jar

replace all hardcoded relative ../../../../ paths with variables rooted
at $(git rev-parse --show-toplevel), so the script works correctly regardless
of the working directory it is invoked from.
@github-actions

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.

…ctorValues

Introduce a byte-vector parallel to RandomAccessVectorValues so the HNSW
pipeline can operate natively on int8 (ByteSequence<?>) vectors without any
float32 round-trip.

Why new classes instead of overloading ListRandomAccessVectorValues:
- RandomAccessVectorValues.getVector() is contractually fixed to return
  VectorFloat<?>. Adding a constructor that accepts List<ByteSequence<?>>
  would leave getVector() unable to return the stored bytes without either
  a ClassCastException at runtime or silent dequantization on every access —
  both defeating the purpose of a native int8 path.
- ByteSequence<?> and VectorFloat<?> are unrelated types with no common
  parent, so Java's type system offers no return-type covariance that could
  make a single getVector() work for both.
- A separate interface (RandomAccessByteVectorValues) keeps the byte-vector
  path type-safe end-to-end and ensures that no existing RandomAccessVectorValues
  consumer can accidentally receive a ByteSequence where it expects a VectorFloat.

New files:
- RandomAccessByteVectorValues: interface mirroring RandomAccessVectorValues
  but with getVector() -> ByteSequence<?>; includes the same threadLocalSupplier()
  default with shared/non-shared logic (shared=false => returns this, shared=true
  => wraps in ExplicitThreadLocal).
- ListRandomAccessByteVectorValues: List<ByteSequence<?>>-backed implementation;
  isValueShared()==false, copy() returns this.
…ltVectorUtilSupport

Add three fundamental signed int8 vector similarity operations to the
vectorization support layer so they participate in the same provider dispatch
as float similarity. Bytes are treated as signed int8 (Java byte range -128..127).

VectorUtilSupport — three new abstract methods:
  float dotProduct(ByteSequence<?> a, ByteSequence<?> b)
  float squareDistance(ByteSequence<?> a, ByteSequence<?> b)
  float cosine(ByteSequence<?> a, ByteSequence<?> b)

VectorUtil — three new public static delegates:
  dotProduct(ByteSequence<?>, ByteSequence<?>) -> impl.dotProduct
  squareL2Distance(ByteSequence<?>, ByteSequence<?>) -> impl.squareDistance
  cosine(ByteSequence<?>, ByteSequence<?>) -> impl.cosine

DefaultVectorUtilSupport — scalar loop implementations:
  dotProduct: accumulate (int)a.get(i) * (int)b.get(i), return as float.
  squareDistance: accumulate (diff * diff) for each signed byte difference.
  cosine: dot / sqrt(normA * normB) using per-element float promotion.

PanamaVectorUtilSupport — scalar stub overrides identical to Default,
  so jvector-twenty compiles without requiring a SIMD implementation now.
  SIMD optimisation of byte similarity is a future concern.
New enum in jvector-base/.../vector/ parallel to VectorSimilarityFunction but
operating on ByteSequence<?>, delegating to the VectorUtil byte methods from
Sub-Task 2.

Three variants with return values normalised to [0,1] matching VectorSimilarityFunction
conventions (higher = more similar):

  EUCLIDEAN:   1 / (1 + squaredL2 / (n * 255^2))
    Normalises by the maximum possible squared distance between two signed int8
    vectors (255^2 per dimension) so the result stays in (0,1] regardless of
    dimension.

  DOT_PRODUCT: (1 + dot / (n * 127^2)) / 2
    Normalises by the maximum possible dot product magnitude (127^2 per dimension)
    before applying the (1+x)/2 mapping so the result stays in [0,1] regardless
    of dimension or whether vectors are unit-norm. For already unit-norm int8
    vectors (e.g. Cohere, OpenAI reduced-precision) prefer COSINE.

  COSINE:      (1 + cosine(v1, v2)) / 2
    Cosine is inherently bounded to [-1,1] so no extra normalisation is needed.
Wire RandomAccessByteVectorValues + ByteVectorSimilarityFunction into the
existing BuildScoreProvider abstraction so GraphIndexBuilder can build a graph
over byte vectors without any change to the builder core.

New static factory: byteVectorScoreProvider(RandomAccessByteVectorValues, ByteVectorSimilarityFunction)

The returned BuildScoreProvider:
- isExact() -> true; all scoring stays byte×byte with no float round-trip.
- approximateCentroid(): sums byte elements cast to float then divides by
  ravv.size() — acceptable one-time cost per the plan.
- searchProviderFor(VectorFloat<?>): throws UnsupportedOperationException with
  a clear message; float queries not supported on the byte-only build path.
- searchProviderFor(int node1): captures ravv.getVector(node1) as 'v', builds
  an ExactScoreFunction lambda node2 -> bvsf.compare(v, ravv.getVector(node2)),
  returns a DefaultSearchScoreProvider wrapping it.
- diversityProviderFor(int node1): delegates to searchProviderFor(node1).
- diversityScoreFunctionFor(int node1): same lambda pattern, returned directly
  as a ScoreFunction.ExactScoreFunction.

Uses the same threadLocalSupplier() pattern as randomAccessScoreProvider() so
concurrent graph builds are thread-safe: two independent supplier handles
(vectors + vectorsCopy) are created so diversity comparisons don't collide.
…) overloads

convenience constructor:
  GraphIndexBuilder(RandomAccessByteVectorValues vectorValues,
                    ByteVectorSimilarityFunction similarityFunction,
                    int M, int beamWidth, float neighborOverflow, float alpha,
                    boolean addHierarchy)
  Delegates to the BuildScoreProvider constructor via
  byteVectorScoreProvider(vectorValues, similarityFunction), then stores
  byteVectorValues and byteVectorSimilarityFunction as nullable instance fields
  for use by addGraphNode(int, ByteSequence<?>).

build(RandomAccessByteVectorValues) overload:
  Parallel build loop that calls scoreProvider.searchProviderFor(node) directly
  for each node ordinal, keeping all scoring byte×byte throughout construction.
  Does not call getVector() and never touches VectorFloat.

addGraphNode(int, ByteSequence<?>):
  Incremental ingest path for byte vectors. Builds an ExactScoreFunction lambda
  node2 -> bvsf.compare(vector, byteVectorValues.getVector(node2)) and delegates
  to the existing addGraphNode(int, SearchScoreProvider) — no duplication of
  graph-construction logic.
  Guards against misuse (called on a float-only builder) with a clear
  UnsupportedOperationException pointing to the correct constructor.

Two new nullable fields added to GraphIndexBuilder:
  RandomAccessByteVectorValues byteVectorValues
  ByteVectorSimilarityFunction byteVectorSimilarityFunction
Both are null when the builder is constructed via a float-vector constructor
and non-null only when the byte-vector convenience constructor is used.
Adding GraphIndexBuilder(RandomAccessByteVectorValues, ByteVectorSimilarityFunction, ...)
made the existing null,null test call ambiguous since null satisfies both the
float and byte-vector overloads. Cast the arguments to
(RandomAccessVectorValues) and (VectorSimilarityFunction) to pin the call to
the float constructor and restore unambiguous compilation.
Replace scalar loop implementations of dotProduct, squareDistance, and
cosine for ByteSequence with Panama Vector API implementations in
PanamaVectorUtilSupport, and native AVX-512/AVX2 kernels wired through
NativeVectorUtilSupport -> NativeSimdOps JNI bindings.

Java (Panama) path:
- Widen signed bytes to int32 via B2I conversion, accumulate products
  in IntVector lanes, then reduce.
- Dispatch on PREFERRED_BIT_SIZE:
    512-bit: load 16 bytes (SPECIES_128) -> IntVector.SPECIES_512
    256-bit: load  8 bytes (SPECIES_64)  -> IntVector.SPECIES_256
    128-bit: scalar fallback
- cosine variants accumulate dot/norm products in long after reduction
  to avoid int32 overflow on large vectors.

Native path (C++):
- New kernels in jvector_simd_kernels.cpp and
  jvector_avx3_dl_kernels.cpp: dot_product_i8, euclidean_i8,
  cosine_i8 using Highway SIMD (AVX-512 / AVX2 dispatch).
- Registered in jvector_simd_kernel_list.h and exported via
  jvector_simd.cpp.
- Microbenchmarks added in bench_similarity_i8.cpp.
- C++ unit tests added in test_similarity_i8.cpp using a prime-length
  (107-element) vector to exercise tail handling.

Java tests:
- TestVectorizationProvider.testSimilarityMetricsByte cross-checks
  SIMD results against scalar DefaultVectorUtilSupport baseline.
- Add ScalarQuantizer to jvector-base quantization package: per-dimension
  min/max fitting, quantize (float32 → int8), quantizeAll, and dequantize
  (int8 → float32) with correct signed-byte inverse mapping (b+128, not b&0xFF)

- Wire type: SQ into YAML compression system: SQParameters sentinel in
  CompressorParameters, case SQ in Compression.java

- Add buildInt8InMemory() in Grid: fits ScalarQuantizer, builds graph with
  GraphIndexBuilder(RandomAccessByteVectorValues, ByteVectorSimilarityFunction),
  writes INLINE_VECTORS with dequantized float32 for reranking

- Add INT8 ConfiguredSystem constructor: stores sq + byteRavv; scoreProviderFor()
  quantizes query on-the-fly, uses byte scores as ApproximateScoreFunction for
  graph traversal, float INLINE_VECTORS reranker for final topK selection

- Add sift-128-euclidean-int8.yml benchmark config and datasets.yml entry
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.

Native INT8 (byte vector) HNSW build + search API

1 participant