Skip to content

Commit 338ec17

Browse files
committed
Enabling use of graph only for binary quantization for gpus. Search is
expected to fail
1 parent 7f19fa7 commit 338ec17

File tree

11 files changed

+147
-12
lines changed

11 files changed

+147
-12
lines changed

jni/external/faiss

Submodule faiss updated from 5616caa to 9107354

jni/external/nmslib

jni/src/faiss_wrapper.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,16 @@ jlong knn_jni::faiss_wrapper::LoadIndexWithStream(faiss::IOReader* ioReader) {
460460
| faiss::IO_FLAG_PQ_SKIP_SDC_TABLE
461461
| faiss::IO_FLAG_SKIP_PRECOMPUTE_TABLE);
462462

463+
// Graph-only indexes have null flat storage and cannot be searched via JNI.
464+
auto* idMap = dynamic_cast<faiss::IndexIDMap*>(indexReader);
465+
if (idMap != nullptr) {
466+
auto* hnsw = dynamic_cast<faiss::IndexHNSW*>(idMap->index);
467+
if (hnsw != nullptr && hnsw->storage == nullptr) {
468+
delete indexReader;
469+
throw std::runtime_error("Cannot load a graph-only index: flat vector storage is null");
470+
}
471+
}
472+
463473
return (jlong) indexReader;
464474
}
465475
jlong knn_jni::faiss_wrapper::LoadIndexWithStreamADCParams(faiss::IOReader* ioReader, knn_jni::JNIUtilInterface * jniUtil, JNIEnv * env, jobject methodParamsJ) {

remote-index-build-client/src/main/java/org/opensearch/remoteindexbuild/constants/KNNRemoteConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ public class KNNRemoteConstants {
5050
public static final String DIMENSION = "dimension";
5151
public static final String VECTOR_DATA_TYPE_FIELD = "data_type";
5252
public static final String KNN_ENGINE = "engine";
53+
public static final String GRAPH_ONLY = "graph_only";
5354
}

remote-index-build-client/src/main/java/org/opensearch/remoteindexbuild/model/RemoteBuildRequest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.TENANT_ID;
2323
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.VECTOR_DATA_TYPE_FIELD;
2424
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.VECTOR_PATH;
25+
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.GRAPH_ONLY;
2526

2627
/**
2728
* Request object for sending build requests to the remote build service, encapsulating all the required parameters
@@ -40,6 +41,7 @@ public class RemoteBuildRequest implements ToXContentObject {
4041
protected String vectorDataType;
4142
protected String engine;
4243
protected RemoteIndexParameters indexParameters;
44+
protected boolean graphOnly;
4345

4446
@Override
4547
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
@@ -54,6 +56,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
5456
builder.field(VECTOR_DATA_TYPE_FIELD, vectorDataType);
5557
builder.field(KNN_ENGINE, engine);
5658
builder.field(INDEX_PARAMETERS, indexParameters);
59+
builder.field(GRAPH_ONLY, graphOnly);
5760
builder.endObject();
5861
return builder;
5962
}

remote-index-build-client/src/test/java/org/opensearch/remoteindexbuild/model/RemoteBuildRequestTests.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.VECTOR_BLOB_FILE_EXTENSION;
4343
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.VECTOR_DATA_TYPE_FIELD;
4444
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.VECTOR_PATH;
45+
import static org.opensearch.remoteindexbuild.constants.KNNRemoteConstants.GRAPH_ONLY;
4546

4647
public class RemoteBuildRequestTests extends OpenSearchSingleNodeTestCase {
4748
public static final String MOCK_FULL_PATH = "vectors/1_1_25/SIRKos4rOWlMA62PX2p75m_vectors/SIRKos4rOWlMA62PX2p75m_target_field__3l";
@@ -190,7 +191,10 @@ public String getMockExpectedJson() {
190191
+ METHOD_PARAMETER_M
191192
+ "\":14"
192193
+ "}"
193-
+ "}"
194+
+ "},"
195+
+ "\""
196+
+ GRAPH_ONLY
197+
+ "\":false"
194198
+ "}";
195199
}
196200
}

src/main/java/org/opensearch/knn/index/codec/nativeindex/remote/RemoteIndexBuildStrategy.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.opensearch.knn.index.remote.RemoteIndexWaiter;
2525
import org.opensearch.knn.index.remote.RemoteIndexWaiterFactory;
2626
import org.opensearch.knn.index.vectorvalues.KNNVectorValues;
27-
import org.opensearch.knn.index.vectorvalues.QuantizedKNNBinaryVectorValues;
2827
import org.opensearch.remoteindexbuild.client.RemoteIndexClient;
2928
import org.opensearch.remoteindexbuild.client.RemoteIndexClientFactory;
3029
import org.opensearch.remoteindexbuild.model.RemoteBuildRequest;
@@ -186,12 +185,14 @@ public void buildAndWriteIndex(BuildIndexParams indexInfo) throws IOException {
186185
private void writeToRepository(RepositoryContext repositoryContext, BuildIndexParams indexInfo) {
187186
VectorRepositoryAccessor vectorRepositoryAccessor = repositoryContext.vectorRepositoryAccessor;
188187
boolean success = false;
188+
// For the BQ quantization path, send raw fp32 vectors instead of quantized binary
189+
final VectorDataType writeDataType = isGraphOnlyBuild(indexInfo) ? VectorDataType.FLOAT : indexInfo.getVectorDataType();
189190
metrics.startRepositoryWriteMetrics();
190191
try {
191192
vectorRepositoryAccessor.writeToRepository(
192193
repositoryContext.blobName,
193194
indexInfo.getTotalLiveDocs(),
194-
indexInfo.getVectorDataType(),
195+
writeDataType,
195196
decorateVectorValuesSupplier(indexInfo)
196197
);
197198
success = true;
@@ -203,13 +204,19 @@ private void writeToRepository(RepositoryContext repositoryContext, BuildIndexPa
203204
}
204205

205206
private static Supplier<KNNVectorValues<?>> decorateVectorValuesSupplier(final BuildIndexParams indexInfo) {
206-
if (indexInfo.getVectorDataType() == VectorDataType.BINARY && indexInfo.getQuantizationState() != null) {
207-
return () -> new QuantizedKNNBinaryVectorValues(indexInfo.getKnnVectorValuesSupplier().get(), indexInfo);
208-
}
209-
207+
// When quantization is present, skip wrapping — send raw fp32 vectors for graph-only build
210208
return indexInfo.getKnnVectorValuesSupplier();
211209
}
212210

211+
/**
212+
* Returns true when the build should produce a graph-only index (no flat vector storage).
213+
* Currently applies to the binary quantization path where fp32 vectors are quantized to binary.
214+
* TODO: Add integration tests for graph-only build once search-side support is implemented.
215+
*/
216+
private static boolean isGraphOnlyBuild(final BuildIndexParams indexInfo) {
217+
return indexInfo.getVectorDataType() == VectorDataType.BINARY && indexInfo.getQuantizationState() != null;
218+
}
219+
213220
/**
214221
* Submits a remote build request to the remote index build service
215222
* @return RemoteBuildResponse containing the response from the remote service
@@ -364,6 +371,15 @@ static RemoteBuildRequest buildRemoteBuildRequest(
364371

365372
final String vectorDataType = determineVectorDataType(indexInfo.getVectorDataType(), parameters);
366373

374+
// For the binary quantization path (fp32 vectors quantized to binary), skip quantization
375+
// and send raw fp32 vectors with graph_only=true. The GPU builds the CAGRA graph from fp32
376+
// and returns only the graph structure without flat vector storage.
377+
final boolean graphOnly = isGraphOnlyBuild(indexInfo);
378+
final String resolvedVectorDataType = graphOnly ? VectorDataType.FLOAT.getValue() : vectorDataType;
379+
if (graphOnly) {
380+
log.info("Graph-only build: sending fp32 vectors with graph_only=true for field [{}]", indexInfo.getFieldName());
381+
}
382+
367383
KNNVectorValues<?> vectorValues = decorateVectorValuesSupplier(indexInfo).get();
368384
initializeVectorValues(vectorValues);
369385
assert (vectorValues.dimension() > 0);
@@ -376,9 +392,10 @@ static RemoteBuildRequest buildRemoteBuildRequest(
376392
.tenantId(indexSettings.getSettings().get(ClusterName.CLUSTER_NAME_SETTING.getKey()))
377393
.dimension(vectorValues.dimension())
378394
.docCount(indexInfo.getTotalLiveDocs())
379-
.vectorDataType(vectorDataType)
395+
.vectorDataType(resolvedVectorDataType)
380396
.engine(indexInfo.getKnnEngine().getName())
381397
.indexParameters(indexInfo.getKnnEngine().createRemoteIndexingParameters(parameters))
398+
.graphOnly(graphOnly)
382399
.build();
383400
}
384401
}

src/main/java/org/opensearch/knn/index/store/IndexInputWithBuffer.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ private long remainingBytes() {
4545
return contentLength - indexInput.getFilePointer();
4646
}
4747

48+
/**
49+
* Reads the first 4 bytes (fourcc) of the index file and resets the read position.
50+
* Used to determine the index file type (binary vs non-binary) at load time,
51+
* rather than relying on index metadata which may not reflect the actual file contents
52+
* (e.g. graph-only builds produce non-binary files for binary-quantized indexes).
53+
*/
54+
public String peekFourcc() throws IOException {
55+
byte[] fourcc = new byte[4];
56+
indexInput.readBytes(fourcc, 0, 4);
57+
indexInput.seek(0);
58+
return new String(fourcc);
59+
}
60+
4861
@Override
4962
public String toString() {
5063
return "{indexInput=" + indexInput + ", len(buffer)=" + buffer.length + "}";

src/main/java/org/opensearch/knn/jni/JNIService.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.opensearch.knn.index.store.IndexOutputWithBuffer;
2121
import org.opensearch.knn.index.util.IndexUtil;
2222

23+
import java.io.IOException;
2324
import java.util.Locale;
2425
import java.util.Map;
2526

@@ -195,8 +196,16 @@ public static void createIndexFromTemplate(
195196
*/
196197
public static long loadIndex(IndexInputWithBuffer readStream, Map<String, Object> parameters, KNNEngine knnEngine) {
197198
if (KNNEngine.FAISS == knnEngine) {
198-
if (IndexUtil.isBinaryIndex(knnEngine, parameters)) {
199-
return FaissService.loadBinaryIndexWithStream(readStream);
199+
// Use the file's fourcc to determine binary vs non-binary loader.
200+
// Graph-only builds for binary quantization produce fp32 index files,
201+
// so we can't rely on index metadata to make this decision.
202+
try {
203+
String fourcc = readStream.peekFourcc();
204+
if (isBinaryFourcc(fourcc)) {
205+
return FaissService.loadBinaryIndexWithStream(readStream);
206+
}
207+
} catch (IOException e) {
208+
throw new RuntimeException("Failed to peek at index file fourcc", e);
200209
}
201210

202211
if (IndexUtil.isADCEnabled(knnEngine, parameters)) {
@@ -213,6 +222,10 @@ public static long loadIndex(IndexInputWithBuffer readStream, Map<String, Object
213222
);
214223
}
215224

225+
private static boolean isBinaryFourcc(String fourcc) {
226+
return fourcc != null && fourcc.startsWith("IB");
227+
}
228+
216229
/**
217230
* Determine if index contains shared state. Currently, we cannot do this in the plugin because we do not store the
218231
* model definition anywhere. Only faiss supports indices that have shared state. So for all other engines it will

src/test/java/org/opensearch/knn/index/codec/nativeindex/remote/RemoteIndexBuildStrategyTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
import org.opensearch.knn.common.exception.TerminalIOException;
1818
import org.opensearch.knn.index.KNNSettings;
1919
import org.opensearch.knn.index.VectorDataType;
20+
import org.opensearch.knn.index.codec.nativeindex.model.BuildIndexParams;
21+
import org.opensearch.knn.index.engine.KNNEngine;
2022
import org.opensearch.knn.plugin.stats.KNNRemoteIndexBuildValue;
23+
import org.opensearch.knn.quantization.models.quantizationState.QuantizationState;
2124
import org.opensearch.remoteindexbuild.model.RemoteBuildRequest;
2225
import org.opensearch.repositories.RepositoriesService;
2326
import org.opensearch.repositories.RepositoryMissingException;
@@ -196,6 +199,32 @@ public void testBuildRequest() throws IOException {
196199
assertEquals(TEST_CLUSTER, request.getTenantId());
197200
assertEquals(3, request.getDocCount());
198201
assertEquals(2, request.getDimension());
202+
assertFalse(request.isGraphOnly());
203+
}
204+
205+
public void testBuildRequestGraphOnly() throws IOException {
206+
// Build params with BINARY vectorDataType + quantizationState to trigger graph-only
207+
BuildIndexParams bqBuildParams = BuildIndexParams.builder()
208+
.indexOutputWithBuffer(indexOutputWithBuffer)
209+
.knnEngine(KNNEngine.FAISS)
210+
.vectorDataType(VectorDataType.BINARY)
211+
.parameters(Map.of("index", "param"))
212+
.knnVectorValuesSupplier(knnVectorValuesSupplier)
213+
.totalLiveDocs((int) knnVectorValues.totalLiveDocs())
214+
.segmentWriteState(segmentWriteState)
215+
.quantizationState(mock(QuantizationState.class))
216+
.isFlush(randomBoolean())
217+
.build();
218+
219+
RemoteBuildRequest request = RemoteIndexBuildStrategy.buildRemoteBuildRequest(
220+
createTestIndexSettings(),
221+
bqBuildParams,
222+
createTestRepositoryMetadata(),
223+
MOCK_FULL_PATH,
224+
getMockParameterMap()
225+
);
226+
assertTrue(request.isGraphOnly());
227+
assertEquals(VectorDataType.FLOAT.getValue(), request.getVectorDataType());
199228
}
200229

201230
public Map<String, Object> getMockParameterMap() {

0 commit comments

Comments
 (0)