Add fp16 (float16) vector encoding support - no quantization support#16383
Add fp16 (float16) vector encoding support - no quantization support#16383Pulkitg64 wants to merge 5 commits into
Conversation
Introduce a new FLOAT16 VectorEncoding that stores vectors at half precision (16 bits per dimension, represented as short[]). Adds Float16VectorValues, KnnFloat16VectorField, KnnFloat16VectorQuery, short[] similarity and VectorUtil operations, codec reader/writer plumbing across the KNN vectors stack, and function-query value sources, together with tests. This introduces the raw fp16 encoding only; scalar quantization of fp16 vectors is intentionally excluded and will be added separately. GITHUB#15549
msokolov
left a comment
There was a problem hiding this comment.
Looks good! I have a bunch of tiny comments; mostly just double checking a few things and some javadoc stuff
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.lucene.codecs.lucene95; |
There was a problem hiding this comment.
it seems kind of funny to be adding to the lucene95 codec, but I guess that's where the other OffHeap*VectorValues continue to live?
There was a problem hiding this comment.
Yes, that was the motivation of creating the new OffHeapFloat16VectorValues but I am not sure about the best practice here. Should I move this new class to latest codec 104?
| long vectorDataLength, | ||
| IndexInput vectorData) | ||
| throws IOException { | ||
| if (configuration.docsWithFieldOffset == -2 || vectorEncoding != VectorEncoding.FLOAT16) { |
There was a problem hiding this comment.
seems weird that we respond to requests for other vectorEncoding by returning an empty values rather than raising an error -- is this consistent with the other encodings?
There was a problem hiding this comment.
Yes this is consistent with other encodings as I copied this from OffHeapFloatVectorValues only. References:
| } | ||
|
|
||
| /** | ||
| * Dense vector values that are stored off-heap. This is the most common case when every doc has a |
There was a problem hiding this comment.
maybe "This is the case" -- strike "most common" because it's the only one?
| private static class SparseOffHeapVectorValues extends OffHeapFloat16VectorValues { | ||
| private final DirectMonotonicReader ordToDoc; | ||
| private final IndexedDISI disi; | ||
| // dataIn was used to init a new IndexedDIS for #randomAccess() |
| switch (info.getVectorEncoding()) { | ||
| case BYTE -> Byte.BYTES; | ||
| case FLOAT32 -> Float.BYTES; | ||
| case FLOAT16 -> Short.BYTES; |
There was a problem hiding this comment.
tiny nit, but it would make me happy to list these switch statements in order of size: (BYTE, FLOAT16, FLOAT32); same throughout
| FLOAT32(4); | ||
| FLOAT32(4), | ||
|
|
||
| /** Encodes vector using 16 bits of precision per sample in IEEE floating point format. */ |
There was a problem hiding this comment.
maybe "IEEE half-precision floating-point format"?
| for (int j = 0; j < v.length; j++) { | ||
| v[j] = Float.floatToFloat16(contents[i][j]); | ||
| } | ||
| doc.add(new KnnFloat16VectorField(field, v, EUCLIDEAN)); |
There was a problem hiding this comment.
why EUCLIDEAN? But the case for float below doesn't specify any function?
There was a problem hiding this comment.
Actually the float case is also using EUCLIDEAN.
public KnnFloatVectorField(String name, float[] vector) {
this(name, vector, VectorSimilarityFunction.EUCLIDEAN);
}
Let me check what happens if I use random Encoding.
| } | ||
|
|
||
| @Override | ||
| public FunctionValues getValues(Map<Object, Object> context, LeafReaderContext readerContext) |
There was a problem hiding this comment.
do we really need these float16 values sources? I wonder if we could use Float valuesSources over float16 vector fields?
There was a problem hiding this comment.
do we really need these float16 values sources?
This was needed just for benchmark support in lucene util for exact scoring path(example ref: link). This is not needed in production path. Should we remove this change to make PR more simpler?
I wonder if we could use Float valuesSources over float16 vector fields?
I think we can do that and drop the ConstKnnFloat16ValueSource completely. It's just there to be consistent with other classes ConstKnnFloatValueSource and ConstKnnByteValueSource. But I don't have any strong opinion on this. Should we drop this??
There was a problem hiding this comment.
OK I guess we support vector-valued values sources, which seems a bit weird to me -- what would consume them? But we may as well follow the existing pattern
|
|
||
| @Override | ||
| public Float16VectorValues getFloat16VectorValues(String field) throws IOException { | ||
| throw new UnsupportedOperationException("Float16 vectors not supported"); |
There was a problem hiding this comment.
I wonder if it could support it? Not for this PR, but maybe it already has such a thing?
|
|
||
| @Before | ||
| public void init() { | ||
| vectorEncoding = randomVectorEncoding(); |
There was a problem hiding this comment.
does randomVectorEncoding now sometimes produce FLOAT16?
There was a problem hiding this comment.
For all the backward codec, it doesn't because we have overridden this function with something like this:
@Override
protected VectorEncoding randomVectorEncoding() {
return random().nextBoolean() ? VectorEncoding.BYTE : VectorEncoding.FLOAT32;
}
For for latest codecs we are producing Float16 encoding sometimes:
protected VectorEncoding randomVectorEncoding() {
return VectorEncoding.values()[random().nextInt(VectorEncoding.values().length)];
}
| beamWidth, | ||
| infoStream, | ||
| tinySegmentsThreshold); | ||
| case FLOAT16 -> |
There was a problem hiding this comment.
OK I just realized -- we should not be adding a writer method to an existing codec, because this will create a scenario where we can write an index with an older codec version (104?) that cannot be read by other versions of Lucene that have the 104 readers without fp16 support. When we add a new file format, we need a new Codec version, don't we?
There was a problem hiding this comment.
after discussing and thinking about this some more, I realized this was wrong: Lucene doesn't expect to provide any forwards-compatibility guarantee. Even though the Codec version does not change, older releases are not expected to be able to read indexes produced by newer ones.
danmuzi
left a comment
There was a problem hiding this comment.
Thanks for your contribution! :)
I have a few comments.
| public KnnFloat16VectorQuery( | ||
| String field, short[] target, int k, Query filter, KnnSearchStrategy searchStrategy) { | ||
| super(field, k, filter, searchStrategy); | ||
| this.target = target; |
There was a problem hiding this comment.
The float32 implementation validates the target vector as follows:
this.target = VectorUtil.checkFinite(Objects.requireNonNull(target, "target"));This class does not have any validation for target, so it can cause several issues.
-
If
targetis null, NPE is thrown later during search fromDefaultFlatVectorScorer.getRandomVectorScorer()when accessingtarget.length. -
If the
targetcontains values such as 0x7C00(+Infinity), 0xFC00(-Infinity), or 0x7E00 (NaN),similarityFunction.compare()may returnNaN.
InTopKnnCollector, comparisons involving NaN return false, so the search may silently return incorrect top-K results without throwing an exception.
This is only caught when assertions are enabled, by the assertFloat.isFinite(result)check aroundVectorUtil.dotProduct().
In production, the issue may remain undetected in production level. -
If
target.lengthis 0,KnnFloat16VectorQuery.toString()throws anArrayIndexOutOfBoundsExceptionbecause it accessestarget[0].
So I think adding VectorUtil.checkFiniteFloat16() would be a reasonable solution.
// VectorUtil.java
public static short[] checkFiniteFloat16(short[] v) {
for (int i = 0; i < v.length; i++) {
if ((v[i] & 0x7C00) == 0x7C00) {
throw new IllegalArgumentException( "non-finite float16 value at vector[" + i + "]=" + Float.float16ToFloat(v[i]));
}
}
return v;
}
// KnnFloat16VectorQuery.java
this.target = VectorUtil.checkFiniteFloat16(Objects.requireNonNull(target, "target"));There was a problem hiding this comment.
Fixed in the next revision
| String name, | ||
| short[] vector, | ||
| VectorSimilarityFunction similarityFunction, | ||
| VectorEncoding vectorEncoding) { |
There was a problem hiding this comment.
Yeah, I already removed it in my next revision. Thanks for noticing it.
| * Creates a numeric vector field. Fields are single-valued: each document has either one value or | ||
| * no value. Vectors of a single field share the same dimension and similarity function. Note that | ||
| * some vector similarities (like {@link VectorSimilarityFunction#DOT_PRODUCT}) require values to | ||
| * be unit-length, which can be enforced using {@link VectorUtil#l2normalize(float[])}. |
There was a problem hiding this comment.
Following up on Michael's comment, VectorUtil does not have a l2normalize method for short[].
There was a problem hiding this comment.
I have removed the comment for now, even float32 is not exercising this path (l2normalize) in without quantization path i.e based on the code it looks like its the user responsibility to l2normalize the vectors. Once we add the quantization support for float16, i will again add the support for l2 normalize
| public KnnFloat16VectorField( | ||
| String name, short[] vector, VectorSimilarityFunction similarityFunction) { | ||
| super(name, createType(vector, similarityFunction)); | ||
| fieldsData = vector; // null check done above |
There was a problem hiding this comment.
This should also validate the vector using the VectorUtil.checkFiniteFloat16() suggested in the comment on KnnFloat16VectorQuery.
fieldsData = VectorUtil.checkFiniteFloat16(vector);Without this validation, the following issues may occur:
- When the segment is flushed, the HNSW graph builder computes similarity scores with these values, and the results become
NaN.
NaNbreaks the neighbor selection logic, so the graph connections around those nodes end up more or less random. - Once these values are written into a segment, every later merge reuses them to rebuild the graph, so the damage spreads to new segments as well.
- No error or warning is raised in production.
There was a problem hiding this comment.
Thanks for pointing it out. I have fixed it in next revision.
| VectorSimilarityFunction similarityFunction, | ||
| VectorEncoding vectorEncoding) { | ||
| super(name, createType(vector, similarityFunction)); | ||
| fieldsData = vector; // null check done above |
There was a problem hiding this comment.
These functions were unused so I have removed them in the next revision.
| throw new IllegalArgumentException( | ||
| "The number of vector dimensions does not match the field type"); | ||
| } | ||
| fieldsData = vector; |
| throw new IllegalArgumentException( | ||
| "value length " + value.length + " must match field dimension " + type.vectorDimension()); | ||
| } | ||
| fieldsData = value; |
jeho-rpls
left a comment
There was a problem hiding this comment.
I was reading this PR out of interest in the encoding and merge paths and left two small inline comments. Thanks!
| case FLOAT16: | ||
| Float16VectorValues val = context.reader().getFloat16VectorValues("field"); | ||
| vectorScorer = val.scorer(new short[] {1, 2}); | ||
| break; |
There was a problem hiding this comment.
If I am reading this right, these shorts are interpreted as fp16 bit patterns, so {1, 2} would decode to a nearly zero vector rather than {1.0, 2.0} like the byte and float cases.
I think the test still passes because it only counts the matched docs and never checks scores. Would it make sense to use like Float.floatToFloat16(1f) and Float.floatToFloat16(2f) here instead?
There was a problem hiding this comment.
thanks for noticing, fixed it.
| public RandomVectorScorer getRandomVectorScorer( | ||
| VectorSimilarityFunction similarityFunction, KnnVectorValues vectorValues, short[] target) | ||
| throws IOException { | ||
| return null; |
There was a problem hiding this comment.
Minor one, but returning null here could surface as an NPE at some caller far from the cause.
I noticed Lucene102BinaryQuantizedVectorsReader in this package throws UnsupportedOperationException for the same situation. Would it be more
consistent to throw here as well?
There was a problem hiding this comment.
Fixed in next revision
Description
Introduce a new FLOAT16 VectorEncoding that stores vectors at half precision (16 bits per dimension, represented as short[]). This introduces the raw fp16 encoding only; scalar quantization of fp16 vectors is intentionally excluded for simplifying the PR and reviewing process. Will raise another PR with quantization support.
Initital full PR with both new encoding and quantization support: #15549