Skip to content

Commit 0b4a62a

Browse files
authored
Fix AVX-512 fpclass mask to classify negative infinity as negative in MIP scoring. (#142514)
Fix AVX-512 fpclass mask to classify negative infinity as negative in MIP scoring. The AVX-512 implementation of scaleMaxInnerProductScore in score_2.cpp used _mm512_fpclass_ps_mask(res, 0x40) to identify negative values. The mask 0x40 (bit 6) only matches "Negative Finite", missing negative infinity (bit 4, 0x10). When the raw score was -Infinity, the code took the wrong branch (1 + res = -Infinity) instead of the negative branch (1/(1 - res) = 0), diverging from both the AVX2 path (_CMP_LT_OQ correctly treats -Infinity < 0 as true) and the Java scalar scaleMaxInnerProductScore. The fix changes the mask from 0x40 to 0x50 to also match negative infinity. A targeted regression test is added that forces raw scores to -Infinity via queryAdditionalCorrection and asserts scalar vs vectorized agreement.
1 parent 023637e commit 0b4a62a

File tree

6 files changed

+105
-6
lines changed

6 files changed

+105
-6
lines changed

docs/changelog/142514.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
area: Vector Search
2+
issues:
3+
- 142289
4+
pr: 142514
5+
summary: Fix AVX-512 fpclass mask to classify negative infinity as negative in MIP
6+
scoring
7+
type: bug

libs/native/libraries/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ configurations {
1919
}
2020

2121
var zstdVersion = "1.5.7"
22-
var vecVersion = "1.0.40"
22+
var vecVersion = "1.0.41"
2323

2424
repositories {
2525
exclusiveContent {

libs/simdvec/native/publish_vec_binaries.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ if [ -z "$ARTIFACTORY_API_KEY" ]; then
2020
exit 1;
2121
fi
2222

23-
VERSION="1.0.40"
23+
VERSION="1.0.41"
2424
ARTIFACTORY_REPOSITORY="${ARTIFACTORY_REPOSITORY:-https://artifactory.elastic.dev/artifactory/elasticsearch-native/}"
2525
TEMP=$(mktemp -d)
2626

libs/simdvec/native/src/vec/c/amd64/score_2.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ EXPORT f32_t diskbbq_apply_corrections_maximum_inner_product_bulk_2(
105105
_mm512_set1_ps(queryAdditionalCorrection - centroidDp)
106106
);
107107

108-
__mmask16 is_neg_mask = _mm512_fpclass_ps_mask(res, 0x40);
108+
__mmask16 is_neg_mask = _mm512_fpclass_ps_mask(res, 0x50);
109109
__m512 negative_scaled = _mm512_rcp14_ps(_mm512_fnmadd_ps(_mm512_set1_ps(1.0f), res, _mm512_set1_ps(1.0f)));
110110
__m512 positive_scaled = _mm512_add_ps(_mm512_set1_ps(1.0f), res);
111111

libs/simdvec/src/test/java/org/elasticsearch/simdvec/internal/vectorization/ESNextOSQVectorsScorerTests.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,101 @@ private void doTestScoreBulk(int bulkSize) throws Exception {
354354
}
355355
}
356356

357+
/**
358+
* Regression test: verifies that the vectorized scorer correctly handles -Infinity raw scores
359+
* for MAXIMUM_INNER_PRODUCT. Passing Float.NEGATIVE_INFINITY as queryAdditionalCorrection
360+
* (with all-zero corrections) forces every element's raw score to -Infinity before
361+
* scaleMaxInnerProductScore is applied. The correct result is 0.0 for all elements.
362+
*
363+
* This catches the AVX-512 bug where {@code _mm512_fpclass_ps_mask(res, 0x40)} (Negative Finite)
364+
* failed to classify -Infinity as negative, causing the positive branch ({@code 1 + res = -Infinity})
365+
* to be used instead of the negative branch ({@code 1/(1 - res) = 0}).
366+
*/
367+
public void testScoreBulkWithNegativeInfinityScore() throws Exception {
368+
final int dimensions = 768;
369+
final int bulkSize = ESNextOSQVectorsScorer.BULK_SIZE;
370+
371+
final int length = switch (indexBits) {
372+
case 1 -> ESNextDiskBBQVectorsFormat.QuantEncoding.ONE_BIT_4BIT_QUERY.getDocPackedLength(dimensions);
373+
case 2 -> ESNextDiskBBQVectorsFormat.QuantEncoding.TWO_BIT_4BIT_QUERY.getDocPackedLength(dimensions);
374+
case 4 -> ESNextDiskBBQVectorsFormat.QuantEncoding.FOUR_BIT_SYMMETRIC.getDocPackedLength(dimensions);
375+
default -> throw new IllegalArgumentException("Unsupported bits: " + indexBits);
376+
};
377+
final int queryBytes = length * (queryBits / indexBits);
378+
379+
try (Directory dir = newParametrizedDirectory()) {
380+
try (IndexOutput out = dir.createOutput("testNegInf.bin", IOContext.DEFAULT)) {
381+
byte[] vector = new byte[length];
382+
for (int i = 0; i < bulkSize; i++) {
383+
random().nextBytes(vector);
384+
out.writeBytes(vector, 0, length);
385+
}
386+
// All-zero corrections: zero bytes are interpreted identically regardless of byte order
387+
byte[] zeroCorrections = new byte[16 * bulkSize];
388+
out.writeBytes(zeroCorrections, 0, zeroCorrections.length);
389+
CodecUtil.writeFooter(out);
390+
}
391+
392+
byte[] query = new byte[queryBytes];
393+
random().nextBytes(query);
394+
395+
float[] scoresDefault = new float[bulkSize];
396+
float[] scoresPanama = new float[bulkSize];
397+
398+
try (IndexInput in = dir.openInput("testNegInf.bin", IOContext.DEFAULT)) {
399+
final long dataLength = (long) bulkSize * length + 16L * bulkSize;
400+
final IndexInput slice = in.slice("test", 0, dataLength);
401+
final var defaultScorer = defaultProvider().newESNextOSQVectorsScorer(
402+
slice,
403+
queryBits,
404+
indexBits,
405+
dimensions,
406+
length,
407+
bulkSize
408+
);
409+
final var panamaScorer = maybePanamaProvider().newESNextOSQVectorsScorer(
410+
in,
411+
queryBits,
412+
indexBits,
413+
dimensions,
414+
length,
415+
bulkSize
416+
);
417+
418+
// Pass Float.NEGATIVE_INFINITY as queryAdditionalCorrection.
419+
// With all-zero corrections and zero query intervals, the base score is zero,
420+
// and adding -Infinity makes every element's total raw score -Infinity.
421+
float defaultMaxScore = defaultScorer.scoreBulk(
422+
query,
423+
0f,
424+
0f,
425+
0,
426+
Float.NEGATIVE_INFINITY,
427+
similarityFunction,
428+
0f,
429+
scoresDefault
430+
);
431+
float panamaMaxScore = panamaScorer.scoreBulk(
432+
query,
433+
0f,
434+
0f,
435+
0,
436+
Float.NEGATIVE_INFINITY,
437+
similarityFunction,
438+
0f,
439+
scoresPanama
440+
);
441+
442+
assertEquals(defaultMaxScore, panamaMaxScore, 1e-2f);
443+
for (int j = 0; j < bulkSize; j++) {
444+
assertEquals("score mismatch at index " + j, scoresDefault[j], scoresPanama[j], 1e-2f);
445+
}
446+
assertEquals(dataLength, slice.getFilePointer());
447+
assertEquals(dataLength, in.getFilePointer());
448+
}
449+
}
450+
}
451+
357452
private static void writeCorrections(OptimizedScalarQuantizer.QuantizationResult[] corrections, IndexOutput out) throws IOException {
358453
for (OptimizedScalarQuantizer.QuantizationResult correction : corrections) {
359454
out.writeInt(Float.floatToIntBits(correction.lowerInterval()));

muted-tests.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,6 @@ tests:
344344
- class: org.elasticsearch.xpack.ml.integration.ClassificationIT
345345
method: testWithOnlyTrainingRowsAndTrainingPercentIsFifty_DependentVariableIsKeyword
346346
issue: https://github.com/elastic/elasticsearch/issues/142282
347-
- class: org.elasticsearch.benchmark.vector.scorer.VectorScorerOSQBenchmarkTests
348-
method: testBulkScalarVsVectorized {p0=1024 p1=2 p2=MMAP p3=MAXIMUM_INNER_PRODUCT}
349-
issue: https://github.com/elastic/elasticsearch/issues/142289
350347
- class: org.elasticsearch.xpack.analytics.cumulativecardinality.CumulativeCardinalityAggregatorTests
351348
method: testSimple
352349
issue: https://github.com/elastic/elasticsearch/issues/142408

0 commit comments

Comments
 (0)