Skip to content

Add int8 / byte-vector similarity support - #709

Draft
r-devulap wants to merge 9 commits into
mainfrom
int8-support
Draft

Add int8 / byte-vector similarity support#709
r-devulap wants to merge 9 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.

@github-actions

github-actions Bot commented Aug 11, 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

Copy link
Copy Markdown
Contributor Author

To do:

  • Update all the docs/README to reflect these changes
  • Add release notes
  • Add BenchYAML results

@jshook jshook left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR needs to be updated.

  1. merge conflicts need to be addressed
  2. the base commit this branch depends on should probably determine the target of the merge, rather than main, since there are direct dependencies.

…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.
…raph

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.
ScalarQuantizer implements VectorCompressor<ByteSequence<?>> with
per-dimension float32→int8 quantization. fit() computes per-dimension
min/max from a base RAVV; encode()/encodeTo() map vectors linearly into
[-128, 127]; encodeAll() returns an SQVectors in parallel; write()/load()
serialize the dimMin/dimMax arrays so the quantizer survives an index
reload from disk; dequantize() and quantizeAll() remain as helpers for
graph construction and reranking.

SQVectors implements CompressedVectors: holds the ByteSequence[] sidecar,
provides precomputedScoreFunctionFor/diversityFunctionFor via
ByteVectorSimilarityFunction, and has its own write()/load() pair for
sidecar persistence.

SQExample is a new tutorial that loads siftsmall fvecs, fits a
ScalarQuantizer, builds an int8 graph in one parallel shot, saves the
graph with original float32 InlineVectors for full-fidelity reranking,
and searches every query vector. TutorialRunner gains a "sq" entry point.
Wire type: SQ in YAML into the full Grid benchmark path:

CompressorParameters.SQParameters is a sentinel that Grid detects with
an instanceof check before the normal float32 compressor path, routing
to a dedicated buildInt8InMemory() method.

buildInt8InMemory() fits a ScalarQuantizer on the base RAVV, quantizes
all base vectors to int8, builds the graph via
GraphIndexBuilder(RandomAccessByteVectorValues, ByteVectorSimilarityFunction),
embeds the ScalarQuantizer in the index header via SQFeature (so it is
recoverable at search time without access to base vectors), and writes
original float32 vectors as INLINE_VECTORS for full-fidelity reranking.

ConfiguredSystem gains a byteRavv field and an SQ-aware constructor.
scoreProviderFor() detects SQ_QUANTIZER in the feature set, recovers the
ScalarQuantizer from the OnDiskGraphIndex header, encodes the query on-
the-fly, uses byte×byte scoring as the ApproximateScoreFunction for graph
traversal, and falls back to the float INLINE_VECTORS reranker for final
topK selection.

Also adds sift-128-euclidean-int8.yml benchmark config and a
corresponding datasets.yml entry.
@r-devulap

Copy link
Copy Markdown
Contributor Author

This PR needs to be updated.

  1. merge conflicts need to be addressed

Fixed, it is now rebased with the latest changes in main.

  1. the base commit this branch depends on should probably determine the target of the merge, rather than main, since there are direct dependencies.

This is no longer a problem since it includes the commits in #708

@r-devulap
r-devulap marked this pull request as draft August 18, 2026 07:51
@r-devulap

r-devulap commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Marking this as draft until I resolve the question of leveraging NVQ for INT8.

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

2 participants