Skip to content

Add fp16 (float16) vector encoding support - no quantization support#16383

Open
Pulkitg64 wants to merge 5 commits into
apache:mainfrom
Pulkitg64:fp16-encoding
Open

Add fp16 (float16) vector encoding support - no quantization support#16383
Pulkitg64 wants to merge 5 commits into
apache:mainfrom
Pulkitg64:fp16-encoding

Conversation

@Pulkitg64

Copy link
Copy Markdown
Contributor

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

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 msokolov 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.

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;

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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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) {

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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

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.

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()

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.

"IndexedDISI"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

switch (info.getVectorEncoding()) {
case BYTE -> Byte.BYTES;
case FLOAT32 -> Float.BYTES;
case FLOAT16 -> Short.BYTES;

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.

tiny nit, but it would make me happy to list these switch statements in order of size: (BYTE, FLOAT16, FLOAT32); same throughout

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

FLOAT32(4);
FLOAT32(4),

/** Encodes vector using 16 bits of precision per sample in IEEE floating point format. */

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.

maybe "IEEE half-precision floating-point format"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

for (int j = 0; j < v.length; j++) {
v[j] = Float.floatToFloat16(contents[i][j]);
}
doc.add(new KnnFloat16VectorField(field, v, EUCLIDEAN));

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.

why EUCLIDEAN? But the case for float below doesn't specify any function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)

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.

do we really need these float16 values sources? I wonder if we could use Float valuesSources over float16 vector fields?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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??

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.

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");

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.

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();

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.

does randomVectorEncoding now sometimes produce FLOAT16?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 ->

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.

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?

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.

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 danmuzi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

  1. If target is null, NPE is thrown later during search from DefaultFlatVectorScorer.getRandomVectorScorer() when accessing target.length.

  2. If the target contains values such as 0x7C00(+Infinity), 0xFC00(-Infinity), or 0x7E00 (NaN), similarityFunction.compare() may return NaN.
    In TopKnnCollector, 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 assert Float.isFinite(result) check around VectorUtil.dotProduct().
    In production, the issue may remain undetected in production level.

  3. If target.length is 0, KnnFloat16VectorQuery.toString() throws an ArrayIndexOutOfBoundsException because it accesses target[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"));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the next revision

String name,
short[] vector,
VectorSimilarityFunction similarityFunction,
VectorEncoding vectorEncoding) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems not used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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[])}.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Following up on Michael's comment, VectorUtil does not have a l2normalize method for short[].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

  1. When the segment is flushed, the HNSW graph builder computes similarity scores with these values, and the results become NaN.
    NaN breaks the neighbor selection logic, so the graph connections around those nodes end up more or less random.
  2. 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.
  3. No error or warning is raised in production.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto.

throw new IllegalArgumentException(
"value length " + value.length + " must match field dimension " + type.vectorDimension());
}
fieldsData = value;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto.

@jeho-rpls jeho-rpls 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.

I was reading this PR out of interest in the encoding and merge paths and left two small inline comments. Thanks!

Comment on lines +53 to +56
case FLOAT16:
Float16VectorValues val = context.reader().getFloat16VectorValues("field");
vectorScorer = val.scorer(new short[] {1, 2});
break;

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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thanks for noticing, fixed it.

public RandomVectorScorer getRandomVectorScorer(
VectorSimilarityFunction similarityFunction, KnnVectorValues vectorValues, short[] target)
throws IOException {
return null;

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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in next revision

@danmuzi danmuzi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants