mccullocht commented on code in PR #16030:
URL: https://github.com/apache/lucene/pull/16030#discussion_r4040014579
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorsWriter.java:
##########
@@ -320,39 +346,129 @@ private QuantizedByteVectorValues
mergedQuantizedVectorValues(
return new QuantizedFloatVectorValues(vectorValues, quantizer, encoding,
centroid);
}
+ /**
+ * Returns a view that quantizes a single segment's float vectors against
{@code centroid} using
+ * this writer's encoding, without consulting any quantized bytes the
segment may already store.
+ */
+ private QuantizedFloatVectorValues quantizeFromFloats(
+ KnnVectorsReader reader, FieldInfo fieldInfo, float[] centroid) throws
IOException {
+ OptimizedScalarQuantizer quantizer =
+ new OptimizedScalarQuantizer(fieldInfo.getVectorSimilarityFunction());
+ FloatVectorValues vectorValues =
+ fieldInfo.getVectorEncoding() == VectorEncoding.FLOAT16
+ ? new
Float16AsFloatVectorValues(reader.getFloat16VectorValues(fieldInfo.name))
+ : reader.getFloatVectorValues(fieldInfo.name);
+ if (fieldInfo.getVectorSimilarityFunction() == COSINE) {
+ vectorValues = new NormalizedFloatVectorValues(vectorValues);
+ }
+ return new QuantizedFloatVectorValues(vectorValues, quantizer, encoding,
centroid);
+ }
+
@Override
public void mergeOneFlatVectorField(FieldInfo fieldInfo, MergeState
mergeState)
throws IOException {
- // Don't need access to the random vectors, we can just use the merged
- rawVectorDelegate.mergeOneFlatVectorField(fieldInfo, mergeState);
if (fieldInfo.getVectorEncoding().isFloatingPoint() == false) {
+ rawVectorDelegate.mergeOneFlatVectorField(fieldInfo, mergeState);
return;
}
- final float[] centroid;
+ if (enableCentering) {
+ mergeOneFlatVectorFieldCentered(fieldInfo, mergeState);
+ } else {
+ mergeOneFlatVectorFieldDataBlind(fieldInfo, mergeState);
+ }
+ }
+
+ private void mergeOneFlatVectorFieldCentered(FieldInfo fieldInfo, MergeState
mergeState)
+ throws IOException {
+ // Don't need access to the random vectors, we can just use the merged
+ rawVectorDelegate.mergeOneFlatVectorField(fieldInfo, mergeState);
final float[] mergedCentroid = new float[fieldInfo.getVectorDimension()];
int vectorCount = mergeAndRecalculateCentroids(mergeState, fieldInfo,
mergedCentroid);
- centroid = mergedCentroid;
if (segmentWriteState.infoStream.isEnabled(QUANTIZED_VECTOR_COMPONENT)) {
segmentWriteState.infoStream.message(
QUANTIZED_VECTOR_COMPONENT, "Vectors' count:" + vectorCount);
}
QuantizedByteVectorValues quantizedVectorValues =
- mergedQuantizedVectorValues(fieldInfo, mergeState, centroid);
+ mergedQuantizedVectorValues(fieldInfo, mergeState, mergedCentroid);
long vectorDataOffset = vectorData.alignFilePointer(Float.BYTES);
DocsWithFieldSet docsWithField = writeVectorData(vectorData,
quantizedVectorValues);
long vectorDataLength = vectorData.getFilePointer() - vectorDataOffset;
float centroidDp =
- docsWithField.cardinality() > 0 ? VectorUtil.dotProduct(centroid,
centroid) : 0;
+ docsWithField.cardinality() > 0 ?
VectorUtil.dotProduct(mergedCentroid, mergedCentroid) : 0;
writeMeta(
fieldInfo,
segmentWriteState.segmentInfo.maxDoc(),
vectorDataOffset,
vectorDataLength,
- centroid,
+ mergedCentroid,
centroidDp,
docsWithField);
}
+ private void mergeOneFlatVectorFieldDataBlind(FieldInfo fieldInfo,
MergeState mergeState)
+ throws IOException {
+ float[] zeroCentroid = new float[fieldInfo.getVectorDimension()];
+ // Build one merged view where, per contributing segment, either its
existing quantized bytes
+ // are passed through or its float vectors are quantized fresh. Inputs
already quantized to
+ // {@code encoding} against a zero centroid (data-blind segments) are
copied directly; they are
+ // never dequantized and re-quantized, which would only add loss. Segments
with raw floats are
+ // quantized fresh, as their stored bytes live in a different (centered)
quantization space.
+ List<QuantizedByteVectorValuesSub> subs = new ArrayList<>();
+ for (int i = 0; i < mergeState.knnVectorsReaders.length; i++) {
+ KnnVectorsReader reader = mergeState.knnVectorsReaders[i];
+ if (reader == null) {
+ continue;
+ }
+ QuantizedByteVectorValues values;
+ if (hasRawVectorValues(reader, fieldInfo)) {
+ // Segment stored full-precision floats; quantize them against the
zero centroid.
+ values = quantizeFromFloats(reader, fieldInfo, zeroCentroid);
+ } else {
+ QuantizedByteVectorValues qvv = getQuantizedVectorValues(reader,
fieldInfo.name);
+ if (qvv == null || qvv.size() == 0) {
+ continue;
+ }
+ if (qvv.getScalarEncoding() != encoding) {
+ // Re-quantization from raw floats would be required, which is not
possible when raw
+ // floats were never written.
+ throw new IllegalStateException(
Review Comment:
Changed it so that we disallow merging back into centered vectors if the
float vectors are missing.
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorsWriter.java:
##########
@@ -456,8 +610,10 @@ static int mergeAndRecalculateCentroids(
float[] centroid = getCentroid(knnVectorsReader, fieldInfo.name);
totalVectorCount += vectorCount;
// If there aren't centroids, or previously clustered with more than one
cluster
- // or if there are deleted docs, we must recalculate the centroid
- if (centroid == null || mergeState.liveDocs[i] != null) {
+ // or if there are deleted docs, we must recalculate the centroid. An
all-zero centroid
+ // indicates a data-blind segment (no centering was done); it can't be
combined with the
+ // others, so recompute from the (possibly dequantized) vectors.
+ if (centroid == null || isAllZero(centroid) || mergeState.liveDocs[i] !=
null) {
Review Comment:
Removed isAllZero() and replaced it with a mode check; isAllZero was a
vestige of the previous approach.
##########
lucene/CHANGES.txt:
##########
@@ -393,6 +393,10 @@ New Features
translates each term into a SpanTermQuery within a {@link SpanOrQuery}, but
retains only the most frequent terms so it
will not overflow the boolean max clause count. (Alan Woodward, Nishant
Mehta, David Smiley)
+* GITHUB#16029: Scalar quantization option to disable centering and writing of
float vectors. This
+ reduces vector storage costs by 4x or more but also reduces quantization
accuracy.
+ (Trevor McCulloch)
Review Comment:
Done
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorsWriter.java:
##########
@@ -740,6 +936,148 @@ long quantizationOverheadBytesUsed() {
}
}
+ /** In-memory storage for fp32 vectors used in data-blind mode; nothing is
written to disk. */
+ private static class InMemoryFloatFieldWriter extends
FlatFieldVectorsWriter<float[]> {
Review Comment:
Collapsed the implementations using generics.
I don't think the duplication is a huge issue, at least not yet, so I'm not
going to expose this the Lucene99 writers (it would need to be public)
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorsFormat.java:
##########
@@ -85,19 +89,27 @@
* <li><b>vlong</b> the length of the vector data in the .veq file
* <li><b>vint</b> the number of vectors
* <li><b>vint</b> the wire number for ScalarEncoding
- * <li><b>[float]</b> the centroid
- * <li><b>float</b> the centroid square magnitude
+ * <li><b>[float]</b> the centroid (omitted when the metadata version
indicates data-blind mode)
+ * <li><b>float</b> the centroid square magnitude (omitted when the metadata
version indicates
+ * data-blind mode)
* <li>The sparse vector information, if required, mapping vector ordinal to
doc ID
* </ul>
*
+ * <p>{@code enableCentering} manifests in the version: when true we write
version 0 and when false
Review Comment:
Yes, this is to keep this backwards compatible.
This is definitely unconventional -- typically this kind of encoding change
is a one way door even if it's just a VERSION bump within the codec. TBH this
better matches my priors -- a consumer of Lucene could upgrade, wait for new
version of Lucene to pass their rollback window, then allow disable centering.
IIRC in a previous review iteration I was asked to push everything back into
Lucene104 so I'd prefer to leave this as-is. If the changes get much more
involved we should create a new codec. I think next time I'd consider being
more mechanical: a PR that simply branches the existing format, then a second
PR that makes changes to make review tractable.
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/OffHeapScalarQuantizedFloatVectorValues.java:
##########
@@ -93,7 +93,7 @@ abstract class OffHeapScalarQuantizedFloatVectorValues
extends FloatVectorValues
this.byteBuffer = ByteBuffer.allocate(docPackedLength);
this.vectorValue = new float[dimension];
this.byteValue = byteBuffer.array();
- this.unpackedByteVectorValue = new byte[dimension];
+ this.unpackedByteVectorValue = new
byte[encoding.getDiscreteDimensions(dimension)];
Review Comment:
This is an independent change -- it would occasionally compute the wrong
size and tests would fail.
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorsFormat.java:
##########
@@ -44,6 +44,10 @@
* quantized vectors in the index.
* <li>Transforming the half-byte quantized query vectors in such a way that
the comparison with
* single bit vectors can be done with bit arithmetic.
+ * <li>Data blind mode: vectors are quantized without centering and float
vectors are discarded.
Review Comment:
Switched the parameter to an `enum Mode` that flips between the existing
centered approach, and data blind with/without source vectors. The argument for
allowing you to keep float vectors is low fidelity (1, 2, maybe 4) bit
quantization where you may wish to use the float vectors to re-rank the results
of the ANN search.
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorsWriter.java:
##########
@@ -320,39 +346,129 @@ private QuantizedByteVectorValues
mergedQuantizedVectorValues(
return new QuantizedFloatVectorValues(vectorValues, quantizer, encoding,
centroid);
}
+ /**
+ * Returns a view that quantizes a single segment's float vectors against
{@code centroid} using
+ * this writer's encoding, without consulting any quantized bytes the
segment may already store.
+ */
+ private QuantizedFloatVectorValues quantizeFromFloats(
+ KnnVectorsReader reader, FieldInfo fieldInfo, float[] centroid) throws
IOException {
+ OptimizedScalarQuantizer quantizer =
+ new OptimizedScalarQuantizer(fieldInfo.getVectorSimilarityFunction());
+ FloatVectorValues vectorValues =
+ fieldInfo.getVectorEncoding() == VectorEncoding.FLOAT16
+ ? new
Float16AsFloatVectorValues(reader.getFloat16VectorValues(fieldInfo.name))
+ : reader.getFloatVectorValues(fieldInfo.name);
+ if (fieldInfo.getVectorSimilarityFunction() == COSINE) {
+ vectorValues = new NormalizedFloatVectorValues(vectorValues);
+ }
+ return new QuantizedFloatVectorValues(vectorValues, quantizer, encoding,
centroid);
+ }
+
@Override
public void mergeOneFlatVectorField(FieldInfo fieldInfo, MergeState
mergeState)
throws IOException {
- // Don't need access to the random vectors, we can just use the merged
- rawVectorDelegate.mergeOneFlatVectorField(fieldInfo, mergeState);
if (fieldInfo.getVectorEncoding().isFloatingPoint() == false) {
+ rawVectorDelegate.mergeOneFlatVectorField(fieldInfo, mergeState);
return;
}
- final float[] centroid;
+ if (enableCentering) {
+ mergeOneFlatVectorFieldCentered(fieldInfo, mergeState);
+ } else {
+ mergeOneFlatVectorFieldDataBlind(fieldInfo, mergeState);
+ }
+ }
+
+ private void mergeOneFlatVectorFieldCentered(FieldInfo fieldInfo, MergeState
mergeState)
+ throws IOException {
+ // Don't need access to the random vectors, we can just use the merged
+ rawVectorDelegate.mergeOneFlatVectorField(fieldInfo, mergeState);
final float[] mergedCentroid = new float[fieldInfo.getVectorDimension()];
int vectorCount = mergeAndRecalculateCentroids(mergeState, fieldInfo,
mergedCentroid);
- centroid = mergedCentroid;
if (segmentWriteState.infoStream.isEnabled(QUANTIZED_VECTOR_COMPONENT)) {
segmentWriteState.infoStream.message(
QUANTIZED_VECTOR_COMPONENT, "Vectors' count:" + vectorCount);
}
QuantizedByteVectorValues quantizedVectorValues =
- mergedQuantizedVectorValues(fieldInfo, mergeState, centroid);
+ mergedQuantizedVectorValues(fieldInfo, mergeState, mergedCentroid);
long vectorDataOffset = vectorData.alignFilePointer(Float.BYTES);
DocsWithFieldSet docsWithField = writeVectorData(vectorData,
quantizedVectorValues);
long vectorDataLength = vectorData.getFilePointer() - vectorDataOffset;
float centroidDp =
- docsWithField.cardinality() > 0 ? VectorUtil.dotProduct(centroid,
centroid) : 0;
+ docsWithField.cardinality() > 0 ?
VectorUtil.dotProduct(mergedCentroid, mergedCentroid) : 0;
writeMeta(
fieldInfo,
segmentWriteState.segmentInfo.maxDoc(),
vectorDataOffset,
vectorDataLength,
- centroid,
+ mergedCentroid,
centroidDp,
docsWithField);
}
+ private void mergeOneFlatVectorFieldDataBlind(FieldInfo fieldInfo,
MergeState mergeState)
+ throws IOException {
+ float[] zeroCentroid = new float[fieldInfo.getVectorDimension()];
+ // Build one merged view where, per contributing segment, either its
existing quantized bytes
+ // are passed through or its float vectors are quantized fresh. Inputs
already quantized to
+ // {@code encoding} against a zero centroid (data-blind segments) are
copied directly; they are
+ // never dequantized and re-quantized, which would only add loss. Segments
with raw floats are
+ // quantized fresh, as their stored bytes live in a different (centered)
quantization space.
+ List<QuantizedByteVectorValuesSub> subs = new ArrayList<>();
+ for (int i = 0; i < mergeState.knnVectorsReaders.length; i++) {
+ KnnVectorsReader reader = mergeState.knnVectorsReaders[i];
+ if (reader == null) {
+ continue;
+ }
+ QuantizedByteVectorValues values;
+ if (hasRawVectorValues(reader, fieldInfo)) {
+ // Segment stored full-precision floats; quantize them against the
zero centroid.
+ values = quantizeFromFloats(reader, fieldInfo, zeroCentroid);
+ } else {
+ QuantizedByteVectorValues qvv = getQuantizedVectorValues(reader,
fieldInfo.name);
+ if (qvv == null || qvv.size() == 0) {
+ continue;
+ }
+ if (qvv.getScalarEncoding() != encoding) {
+ // Re-quantization from raw floats would be required, which is not
possible when raw
+ // floats were never written.
+ throw new IllegalStateException(
+ "Cannot merge field \""
+ + fieldInfo.name
+ + "\" from data-blind segment with encoding "
+ + qvv.getScalarEncoding()
+ + " into data-blind format with encoding "
+ + encoding
+ + ": re-quantization requires raw float vectors");
+ }
+ float[] centroid = getCentroid(reader, fieldInfo.name);
+ if (centroid != null && isAllZero(centroid)) {
+ // Quantized-only segment whose bytes already match the output
format (encoding and zero
+ // centroid): copy them directly.
+ values = qvv;
+ } else {
+ // Bytes were produced against a non-zero (or unknown) centroid, so
they cannot be passed
+ // through into the zero-centroid output; re-quantize from floats.
+ values = quantizeFromFloats(reader, fieldInfo, zeroCentroid);
+ }
+ }
+ subs.add(new QuantizedByteVectorValuesSub(mergeState.docMaps[i],
values));
+ }
+ long vectorDataOffset = vectorData.alignFilePointer(Float.BYTES);
+ MergedQuantizedByteVectorValues mergedQBVV =
+ MergedQuantizedByteVectorValues.merge(mergeState, zeroCentroid,
encoding, subs);
+ DocsWithFieldSet docsWithField = writeVectorData(vectorData, mergedQBVV);
+ long vectorDataLength = vectorData.getFilePointer() - vectorDataOffset;
+ // centroidDp is 0 (zero centroid); the data-blind metadata omits it and
the centroid.
+ writeMeta(
+ fieldInfo,
+ segmentWriteState.segmentInfo.maxDoc(),
+ vectorDataOffset,
+ vectorDataLength,
+ zeroCentroid,
Review Comment:
Done
##########
lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104ScalarQuantizedVectorsWriter.java:
##########
@@ -420,6 +536,44 @@ static float[] getCentroid(KnnVectorsReader vectorsReader,
String fieldName) {
return null;
}
+ static QuantizedByteVectorValues getQuantizedVectorValues(
+ KnnVectorsReader vectorsReader, String fieldName) throws IOException {
+ vectorsReader = vectorsReader.unwrapReaderForField(fieldName);
+ if (vectorsReader instanceof Lucene104ScalarQuantizedVectorsReader reader)
{
+ return reader.getQuantizedVectorValues(fieldName);
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the segment stores full-precision vectors for this field,
or null when the
Review Comment:
Done
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]