Skip to content

Commit e8d5c3c

Browse files
marianotepperjknitlwillke
authored
Non-uniform vector quantization (#374)
Adds support for Non-uniform Vector Quantization (NVQ, pronounced as "new vec"). This new technique quantizes the values in each vector with high accuracy by first applying a nonlinear transformation that is individually fit to each vector. These nonlinearities are designed to be lightweight and have a negligible impact on distance computation performance. --------- Co-authored-by: Joel Knighton <joel.knighton@datastax.com> Co-authored-by: Ted Willke <ted.willke@datastax.com>
1 parent 431538e commit e8d5c3c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2810
-87
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The graph is represented by an on-disk adjacency list per node, with additional
2727

2828
The second pass can be performed with
2929
* Full resolution float32 vectors
30+
* NVQ, which uses a non-uniform technique to quantize vectors with high-accuracy
3031

3132
[This two-pass design reduces memory usage and reduces latency while preserving accuracy](https://thenewstack.io/why-vector-size-matters/).
3233

UPGRADING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
`CompressedVectors` directly from `encodeAll()`.
1717
- `PQVectors::getProductQuantization` is removed; it duplicated `CompressedVectors::getCompressor` unnecessarily
1818

19+
## New features
20+
- Support for Non-uniform Vector Quantization (NVQ, pronounced as "new vec"). This new technique quantizes the values
21+
in each vector with high accuracy by first applying a nonlinear transformation that is individually fit to each
22+
vector. These nonlinearities are designed to be lightweight and have a negligible impact on distance computation
23+
performance.
24+
1925
# Upgrading from 2.0.x to 3.0.x
2026

2127
## Critical API changes

jvector-base/src/main/java/io/github/jbellis/jvector/graph/GraphIndexBuilder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,20 +211,20 @@ public static GraphIndexBuilder rescore(GraphIndexBuilder other, BuildScoreProvi
211211
var neighbors = other.graph.getNeighbors(i);
212212
var sf = newProvider.searchProviderFor(i).scoreFunction();
213213
var newNeighbors = new NodeArray(neighbors.size());
214-
214+
215215
// Copy edges, compute new scores
216216
for (var it = neighbors.iterator(); it.hasNext(); ) {
217217
int neighbor = it.nextInt();
218218
// since we're using a different score provider, use insertSorted instead of addInOrder
219219
newNeighbors.insertSorted(neighbor, sf.similarityTo(neighbor));
220220
}
221-
221+
222222
newBuilder.graph.addNode(i, newNeighbors);
223223
}
224224

225225
// Set the entry node
226226
newBuilder.graph.updateEntryNode(other.graph.entry());
227-
227+
228228
return newBuilder;
229229
}
230230

jvector-base/src/main/java/io/github/jbellis/jvector/graph/disk/FeatureId.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
*/
3232
public enum FeatureId {
3333
INLINE_VECTORS(InlineVectors::load),
34-
FUSED_ADC(FusedADC::load);
34+
FUSED_ADC(FusedADC::load),
35+
NVQ_VECTORS(NVQ::load);
3536

3637
public static final Set<FeatureId> ALL = Collections.unmodifiableSet(EnumSet.allOf(FeatureId.class));
3738

jvector-base/src/main/java/io/github/jbellis/jvector/graph/disk/FusedADC.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
import io.github.jbellis.jvector.disk.RandomAccessReader;
2020
import io.github.jbellis.jvector.graph.GraphIndex;
2121
import io.github.jbellis.jvector.graph.similarity.ScoreFunction;
22-
import io.github.jbellis.jvector.pq.FusedADCPQDecoder;
23-
import io.github.jbellis.jvector.pq.PQVectors;
24-
import io.github.jbellis.jvector.pq.ProductQuantization;
22+
import io.github.jbellis.jvector.quantization.FusedADCPQDecoder;
23+
import io.github.jbellis.jvector.quantization.PQVectors;
24+
import io.github.jbellis.jvector.quantization.ProductQuantization;
2525
import io.github.jbellis.jvector.util.ExplicitThreadLocal;
2626
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
2727
import io.github.jbellis.jvector.vector.VectorizationProvider;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright DataStax, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.github.jbellis.jvector.graph.disk;
18+
19+
import io.github.jbellis.jvector.disk.RandomAccessReader;
20+
import io.github.jbellis.jvector.graph.similarity.ScoreFunction;
21+
import io.github.jbellis.jvector.quantization.NVQScorer;
22+
import io.github.jbellis.jvector.quantization.NVQuantization;
23+
import io.github.jbellis.jvector.quantization.NVQuantization.QuantizedVector;
24+
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
25+
import io.github.jbellis.jvector.vector.types.VectorFloat;
26+
27+
import java.io.DataOutput;
28+
import java.io.IOException;
29+
import java.io.UncheckedIOException;
30+
31+
/**
32+
* Implements the storage of NuVeQ vectors in an on-disk graph index. These can be used for reranking.
33+
*/
34+
public class NVQ implements Feature {
35+
private final NVQuantization nvq;
36+
private final NVQScorer scorer;
37+
private final ThreadLocal<QuantizedVector> reusableQuantizedVector;
38+
39+
public NVQ(NVQuantization nvq) {
40+
this.nvq = nvq;
41+
scorer = new NVQScorer(this.nvq);
42+
reusableQuantizedVector = ThreadLocal.withInitial(() -> NVQuantization.QuantizedVector.createEmpty(nvq.subvectorSizesAndOffsets, nvq.bitsPerDimension));
43+
}
44+
45+
@Override
46+
public FeatureId id() {
47+
return FeatureId.NVQ_VECTORS;
48+
}
49+
50+
@Override
51+
public int headerSize() {
52+
return nvq.compressorSize();
53+
}
54+
55+
@Override
56+
public int inlineSize() { return nvq.compressedVectorSize();}
57+
58+
public int dimension() {
59+
return nvq.globalMean.length();
60+
}
61+
62+
static NVQ load(CommonHeader header, RandomAccessReader reader) {
63+
try {
64+
return new NVQ(NVQuantization.load(reader));
65+
} catch (IOException e) {
66+
throw new UncheckedIOException(e);
67+
}
68+
}
69+
70+
@Override
71+
public void writeHeader(DataOutput out) throws IOException {
72+
nvq.write(out, OnDiskGraphIndex.CURRENT_VERSION);
73+
}
74+
75+
@Override
76+
public void writeInline(DataOutput out, Feature.State state_) throws IOException {
77+
var state = (NVQ.State) state_;
78+
state.vector.write(out);
79+
}
80+
81+
public static class State implements Feature.State {
82+
public final QuantizedVector vector;
83+
84+
public State(QuantizedVector vector) {
85+
this.vector = vector;
86+
}
87+
}
88+
89+
ScoreFunction.ExactScoreFunction rerankerFor(VectorFloat<?> queryVector,
90+
VectorSimilarityFunction vsf,
91+
FeatureSource source) {
92+
var function = scorer.scoreFunctionFor(queryVector, vsf);
93+
94+
return node2 -> {
95+
try {
96+
var reader = source.inlineReaderForNode(node2, FeatureId.NVQ_VECTORS);
97+
QuantizedVector.loadInto(reader, reusableQuantizedVector.get());
98+
} catch (IOException e) {
99+
throw new RuntimeException(e);
100+
}
101+
return function.similarityTo(reusableQuantizedVector.get());
102+
};
103+
}
104+
}

jvector-base/src/main/java/io/github/jbellis/jvector/graph/disk/OnDiskGraphIndex.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,13 @@ public void close() throws IOException {
262262

263263
@Override
264264
public ScoreFunction.ExactScoreFunction rerankerFor(VectorFloat<?> queryVector, VectorSimilarityFunction vsf) {
265-
if (!features.containsKey(FeatureId.INLINE_VECTORS)) {
266-
throw new UnsupportedOperationException("No inline vectors in this graph");
265+
if (features.containsKey(FeatureId.INLINE_VECTORS)) {
266+
return RandomAccessVectorValues.super.rerankerFor(queryVector, vsf);
267+
} else if (features.containsKey(FeatureId.NVQ_VECTORS)) {
268+
return ((NVQ) features.get(FeatureId.NVQ_VECTORS)).rerankerFor(queryVector, vsf, this);
269+
} else {
270+
throw new UnsupportedOperationException("No reranker available for this graph");
267271
}
268-
return RandomAccessVectorValues.super.rerankerFor(queryVector, vsf);
269272
}
270273

271274
@Override

jvector-base/src/main/java/io/github/jbellis/jvector/graph/disk/OnDiskGraphIndexWriter.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,10 @@ public OnDiskGraphIndexWriter build() throws IOException {
324324
int dimension;
325325
if (features.containsKey(FeatureId.INLINE_VECTORS)) {
326326
dimension = ((InlineVectors) features.get(FeatureId.INLINE_VECTORS)).dimension();
327+
} else if (features.containsKey(FeatureId.NVQ_VECTORS)) {
328+
dimension = ((NVQ) features.get(FeatureId.NVQ_VECTORS)).dimension();
327329
} else {
328-
throw new IllegalArgumentException("Inline vectors must be provided.");
330+
throw new IllegalArgumentException("Inline or NVQ vectors must be provided.");
329331
}
330332

331333
if (ordinalMapper == null) {

jvector-base/src/main/java/io/github/jbellis/jvector/graph/similarity/BuildScoreProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
package io.github.jbellis.jvector.graph.similarity;
1818

1919
import io.github.jbellis.jvector.graph.RandomAccessVectorValues;
20-
import io.github.jbellis.jvector.pq.BQVectors;
21-
import io.github.jbellis.jvector.pq.PQVectors;
20+
import io.github.jbellis.jvector.quantization.BQVectors;
21+
import io.github.jbellis.jvector.quantization.PQVectors;
2222
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
2323
import io.github.jbellis.jvector.vector.VectorUtil;
2424
import io.github.jbellis.jvector.vector.VectorizationProvider;

jvector-base/src/main/java/io/github/jbellis/jvector/graph/similarity/CachingVectorValues.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package io.github.jbellis.jvector.graph.similarity;
1818

1919
import io.github.jbellis.jvector.graph.RandomAccessVectorValues;
20-
import io.github.jbellis.jvector.pq.PQVectors;
20+
import io.github.jbellis.jvector.quantization.PQVectors;
2121
import io.github.jbellis.jvector.vector.types.VectorFloat;
2222
import org.agrona.collections.Int2ObjectHashMap;
2323

0 commit comments

Comments
 (0)