Skip to content

Commit d5aca74

Browse files
committed
Support leading segment merge with PQ
Signed-off-by: Andriy Redko <drreta@gmail.com>
1 parent 6834ca0 commit d5aca74

1 file changed

Lines changed: 120 additions & 56 deletions

File tree

src/main/java/org/opensearch/knn/index/codec/jvector/JVectorWriter.java

Lines changed: 120 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,10 @@ class RandomAccessMergedFloatVectorValues implements RandomAccessVectorValues {
697697
// Ordinal sparsity has a memory cost (in terms map memory usage)
698698
// during leading segment merge.
699699
private static final double MIN_HEAP_GRAPH_ORDINAL_DENSITY = 0.4;
700+
// a number in [0.0, 1.0] that indicates how much leading segment live vectors dominate
701+
// over live vectors across all other segments, this is relevant when PQ is triggered since
702+
// only leading segment vectors will be taken into account
703+
private static final double MAX_PQ_LEADING_SEGMENT_LIVE_VECTOR_FACTOR = 0.1;
700704

701705
// Array of sub-readers
702706
private final KnnVectorsReader[] readers;
@@ -1077,78 +1081,83 @@ private void mergePQ(RemappedRandomAccessVectorValues compactRavv) throws IOExce
10771081
PerFieldKnnVectorsFormat.FieldsReader fieldsReader = (PerFieldKnnVectorsFormat.FieldsReader) readers[LEADING_READER_IDX];
10781082
JVectorReader leadingReader = (JVectorReader) fieldsReader.getFieldReader(fieldName);
10791083

1080-
// Check if the leading reader has pre-existing PQ codebooks and if so, refine them with the remaining vectors
1081-
if (leadingReader.getProductQuantizationForField(fieldInfo.name).isEmpty()) {
1082-
// No pre-existing codebooks, check if we have enough vectors to trigger quantization
1083-
log.info(
1084-
"No Pre-existing PQ codebooks found in this merge for field {} in segment {}, will check if a new codebooks is necessary",
1085-
fieldName,
1086-
mergeState.segmentInfo.name
1087-
);
1088-
if (totalLiveVectorsCount >= minimumBatchSizeForQuantization) {
1084+
final ProductQuantization leadingCompressor;
1085+
if (leadingReader.getProductQuantizationForField(fieldName).isEmpty() == false) {
1086+
final long start = Clock.systemDefaultZone().millis();
1087+
leadingCompressor = leadingReader.getProductQuantizationForField(fieldName).get();
1088+
final long end = Clock.systemDefaultZone().millis();
1089+
final long trainingTime = end - start;
1090+
log.info("Refined PQ codebooks for field {}, in {} millis", fieldName, trainingTime);
1091+
KNNCounter.KNN_QUANTIZATION_TRAINING_TIME.add(trainingTime);
1092+
} else {
1093+
leadingCompressor = null;
1094+
}
1095+
1096+
boolean ok = tryLeadingSegmentMerge(leadingCompressor);
1097+
if (!ok) {
1098+
// Check if the leading reader has pre-existing PQ codebooks and if so, refine them with the remaining vectors
1099+
if (leadingCompressor == null) {
1100+
// No pre-existing codebooks, check if we have enough vectors to trigger quantization
10891101
log.info(
1090-
"Calculating new codebooks and compressed vectors for field: {}, with totalVectorCount: {}, above minimumBatchSizeForQuantization: {}",
1102+
"No Pre-existing PQ codebooks found in this merge for field {} in segment {}, will check if a new codebooks is necessary",
10911103
fieldName,
1092-
totalVectorsCount,
1093-
minimumBatchSizeForQuantization
1094-
);
1095-
compactPqVectors = JVectorIndexQuantization.computePqVectors(
1096-
compactRavv,
1097-
getVectorSimilarityFunction(fieldInfo),
1098-
quantization.numSubspaces(compactRavv.dimension()),
1099-
simdPoolMerge
1104+
mergeState.segmentInfo.name
11001105
);
1106+
if (totalLiveVectorsCount >= minimumBatchSizeForQuantization) {
1107+
log.info(
1108+
"Calculating new codebooks and compressed vectors for field: {}, with totalVectorCount: {}, above minimumBatchSizeForQuantization: {}",
1109+
fieldName,
1110+
totalVectorsCount,
1111+
minimumBatchSizeForQuantization
1112+
);
1113+
compactPqVectors = JVectorIndexQuantization.computePqVectors(
1114+
compactRavv,
1115+
getVectorSimilarityFunction(fieldInfo),
1116+
quantization.numSubspaces(compactRavv.dimension()),
1117+
simdPoolMerge
1118+
);
1119+
} else {
1120+
log.info(
1121+
"Not enough vectors found for field: {}, totalVectorCount: {}, is below minimumBatchSizeForQuantization: {}",
1122+
fieldName,
1123+
totalVectorsCount,
1124+
minimumBatchSizeForQuantization
1125+
);
1126+
compactPqVectors = null;
1127+
}
11011128
} else {
11021129
log.info(
1103-
"Not enough vectors found for field: {}, totalVectorCount: {}, is below minimumBatchSizeForQuantization: {}",
1130+
"Pre-existing PQ codebooks found in this merge for field {} in segment {}, will refine the codebooks from the leading reader with the remaining vectors",
11041131
fieldName,
1105-
totalVectorsCount,
1106-
minimumBatchSizeForQuantization
1132+
mergeState.segmentInfo.name
11071133
);
1108-
compactPqVectors = null;
1134+
compactPqVectors = PQVectors.encodeAndBuild(leadingCompressor, compactRavv.size(), compactRavv, simdPoolMerge);
11091135
}
1110-
} else {
1111-
log.info(
1112-
"Pre-existing PQ codebooks found in this merge for field {} in segment {}, will refine the codebooks from the leading reader with the remaining vectors",
1113-
fieldName,
1114-
mergeState.segmentInfo.name
1115-
);
1116-
final long start = Clock.systemDefaultZone().millis();
1117-
ProductQuantization leadingCompressor = leadingReader.getProductQuantizationForField(fieldName).get();
1118-
// We are not refining PQ codes on merge presently.
1119-
// See https://github.com/opensearch-project/opensearch-jvector/issues/661
1120-
final long end = Clock.systemDefaultZone().millis();
1121-
final long trainingTime = end - start;
1122-
log.info("Refined PQ codebooks for field {}, in {} millis", fieldName, trainingTime);
1123-
KNNCounter.KNN_QUANTIZATION_TRAINING_TIME.add(trainingTime);
1124-
compactPqVectors = PQVectors.encodeAndBuild(leadingCompressor, compactRavv.size(), compactRavv, simdPoolMerge);
1125-
}
11261136

1127-
if (compactPqVectors == null) {
1128-
final String segmentName = segmentWriteState.segmentInfo.name;
1129-
log.info("No PQ codebooks found, will merge with full-precision vectors: field {} in segment {}", fieldName, segmentName);
1130-
1131-
boolean ok = tryLeadingSegmentMerge();
1132-
if (!ok) {
1137+
if (compactPqVectors == null) {
11331138
// leading segment merge was skipped
11341139
log.info(
11351140
"Merging segments by building graph from scratch (skipping leading segment merge) for segment {}, on field {}",
1136-
segmentName,
1141+
segmentWriteState.segmentInfo.name,
11371142
fieldName
11381143
);
11391144
var bsp = BuildScoreProvider.randomAccessScoreProvider(compactRavv, getVectorSimilarityFunction(fieldInfo));
11401145
var graph = getGraph(bsp, compactRavv, fieldInfo, segmentWriteState.segmentInfo.name, simdPoolMerge);
11411146
writeField(fieldInfo, compactRavv, compactOrdToDocMap, graph);
1147+
} else {
1148+
log.info("PQ codebooks found, building graph from scratch with PQ vectors");
1149+
// We're building from scratch, so we can use the "compact" ordinal space directly
1150+
var buildScoreProvider = BuildScoreProvider.pqBuildScoreProvider(
1151+
getVectorSimilarityFunction(fieldInfo),
1152+
compactPqVectors
1153+
);
1154+
// Pre-init the diversity provider here to avoid doing it lazily (as it could block the SIMD threads)
1155+
buildScoreProvider.diversityProviderFor(0);
1156+
var graph = getGraph(buildScoreProvider, compactRavv, fieldInfo, segmentWriteState.segmentInfo.name, simdPoolMerge);
1157+
writeField(fieldInfo, compactRavv, compactPqVectors, compactOrdToDocMap, graph);
11421158
}
1143-
} else {
1144-
log.info("PQ codebooks found, building graph from scratch with PQ vectors");
1145-
// We're building from scratch, so we can use the "compact" ordinal space directly
1146-
var buildScoreProvider = BuildScoreProvider.pqBuildScoreProvider(getVectorSimilarityFunction(fieldInfo), compactPqVectors);
1147-
// Pre-init the diversity provider here to avoid doing it lazily (as it could block the SIMD threads)
1148-
buildScoreProvider.diversityProviderFor(0);
1149-
var graph = getGraph(buildScoreProvider, compactRavv, fieldInfo, segmentWriteState.segmentInfo.name, simdPoolMerge);
1150-
writeField(fieldInfo, compactRavv, compactPqVectors, compactOrdToDocMap, graph);
11511159
}
1160+
11521161
}
11531162

11541163
/**
@@ -1163,7 +1172,7 @@ private void mergePQ(RemappedRandomAccessVectorValues compactRavv) throws IOExce
11631172
*
11641173
* @return a boolean value indicating if leading segment merge was performed
11651174
*/
1166-
private boolean tryLeadingSegmentMerge() throws IOException {
1175+
private boolean tryLeadingSegmentMerge(ProductQuantization leadingCompressor) throws IOException {
11671176
if (leadingSegmentMergeDisabled) {
11681177
log.info("Leading segment merge is disabled, skipping");
11691178
return false;
@@ -1221,6 +1230,20 @@ private boolean tryLeadingSegmentMerge() throws IOException {
12211230
return false;
12221231
}
12231232

1233+
if (leadingCompressor != null) {
1234+
var leadingSegmentLiveVectorsFactor = totalLiveVectorsInOtherReaders / (double) totalLiveVectorsInLeadingReader;
1235+
if (leadingSegmentLiveVectorsFactor > MAX_PQ_LEADING_SEGMENT_LIVE_VECTOR_FACTOR) {
1236+
log.warn(
1237+
"Leading segment does not contain sufficient live vectors to preserve the recall ({} / {}). "
1238+
+ "Will skip leading segment merge. (totalLiveVectors={})",
1239+
totalLiveVectorsInOtherReaders,
1240+
totalLiveVectorsInLeadingReader,
1241+
totalLiveVectorsCount
1242+
);
1243+
return false;
1244+
}
1245+
}
1246+
12241247
log.info(
12251248
"Starting leading segment merge for segment {} on field {}",
12261249
segmentWriteState.segmentInfo.name,
@@ -1285,9 +1308,46 @@ private boolean tryLeadingSegmentMerge() throws IOException {
12851308
throw new IllegalStateException("failed to fill one of the maps, this is a bug");
12861309
}
12871310

1311+
PQVectors compactPqVectors = null;
1312+
BuildScoreProvider leadingBsp = null;
12881313
var heapRavv = new RemappedRandomAccessVectorValues(this, heapToGlobalRavvOrds);
1314+
if (leadingCompressor != null) {
1315+
compactPqVectors = PQVectors.encodeAndBuild(leadingCompressor, heapRavv.size(), new RandomAccessVectorValues() {
1316+
@Override
1317+
public int size() {
1318+
return heapRavv.size();
1319+
}
12891320

1290-
var leadingBsp = BuildScoreProvider.randomAccessScoreProvider(heapRavv, getVectorSimilarityFunction(fieldInfo));
1321+
@Override
1322+
public int dimension() {
1323+
return heapRavv.dimension();
1324+
}
1325+
1326+
@Override
1327+
public VectorFloat<?> getVector(int nodeId) {
1328+
// PQVectors implementations does not deal with "holes" (deleted vectors)
1329+
final int remapped = heapToGlobalRavvOrds[nodeId];
1330+
if (remapped == GraphNodeIdToDocMap.NO_VECTOR_OR_DELETED_DOC) {
1331+
return null; /* no vector */
1332+
} else {
1333+
return heapRavv.getVector(nodeId);
1334+
}
1335+
}
1336+
1337+
@Override
1338+
public boolean isValueShared() {
1339+
return heapRavv.isValueShared();
1340+
}
1341+
1342+
@Override
1343+
public RandomAccessVectorValues copy() {
1344+
return heapRavv.copy();
1345+
}
1346+
}, simdPoolMerge);
1347+
leadingBsp = BuildScoreProvider.pqBuildScoreProvider(getVectorSimilarityFunction(fieldInfo), compactPqVectors);
1348+
} else {
1349+
leadingBsp = BuildScoreProvider.randomAccessScoreProvider(heapRavv, getVectorSimilarityFunction(fieldInfo));
1350+
}
12911351

12921352
// we left this uninitialized earlier, but we're ready to set it up now
12931353
// just in time to mutate the graph
@@ -1334,7 +1394,11 @@ private boolean tryLeadingSegmentMerge() throws IOException {
13341394
// Note that the ordinals for the OnDiskGraphIndex will automatically be compacted
13351395
// But the OnHeapGraphIndex will not
13361396
var finalOrdToDocMap = new GraphNodeIdToDocMap(finalOrdToDocId);
1337-
writeField(fieldInfo, heapRavv, finalOrdToDocMap, graph);
1397+
if (compactPqVectors != null) {
1398+
writeField(fieldInfo, heapRavv, compactPqVectors, finalOrdToDocMap, graph);
1399+
} else {
1400+
writeField(fieldInfo, heapRavv, finalOrdToDocMap, graph);
1401+
}
13381402
return true;
13391403
}
13401404
}

0 commit comments

Comments
 (0)