diff --git a/docs/batched-vector-access.md b/docs/batched-vector-access.md index 0a3c585cb..c31673018 100644 --- a/docs/batched-vector-access.md +++ b/docs/batched-vector-access.md @@ -87,12 +87,14 @@ useDatasetPrefetching: true ``` Omitted or `false` preserves the resident path. The existing VM option -`-Djvector.dataset_loader=buffered` is an explicit override; `preload` overrides the -flag in the other direction. Neither runner changes a user's YAML files. +`-Djvector.dataset_loader=buffered` explicitly overrides the YAML flag; `preload` +overrides the flag in the other direction. Neither runner changes a user's YAML files. The harness uses file input for bulk consumption in quantized construction and existing disk storage. Uncompressed construction, or a search configuration that requires raw input rather than stored vectors/codes, still uses resident input. +The VM option does not bypass this resident-input routing. If an explicit +`buffered` request requires resident input, the harness logs the reason for using preload. A mixed configuration requiring resident input remains resident; split those cases into separate configurations to measure dataset prefetching independently. This is source routing, not a restriction on the vector-access API. @@ -267,6 +269,8 @@ reads and payload allocation, not physical device I/O. - Close cursors with try-with-resources, stop consumers, then close the root source. - Copies share the root's storage and I/O executor. Closing a copy affects that view only; closing the root invalidates all copies and releases the shared resources. +- If point reads are still running when the root closes, close waits for each read + in progress to finish; later point reads fail with `IllegalStateException`. - Direct payload buffers become eligible for JVM reclamation after references are released; closure does not promise immediate native-memory reclamation. diff --git a/docs/release notes/4.0.2/744.feature.md b/docs/release notes/4.0.2/744.feature.md index 9efe1e844..750d2cf28 100644 --- a/docs/release notes/4.0.2/744.feature.md +++ b/docs/release notes/4.0.2/744.feature.md @@ -6,7 +6,9 @@ of the original base dataset; existing vector sources remain supported. BenchYAML and AutoBenchYAML support `useDatasetPrefetching: true` in their index configuration. The flag defaults to false, preserving resident loading and scoring. Comparison-heavy uncompressed configurations retain resident input. The existing -`jvector.dataset_loader` VM option remains an explicit override. +`jvector.dataset_loader` VM option overrides the flag, while configurations +requiring resident raw vectors still preload; an explicit buffered request logs +the reason for that fallback. See [batched vector access](../../batched-vector-access.md) for ownership, resource limits, examples and the benchmark opt-in. Encoded vectors, training diff --git a/jvector-base/src/main/java/io/github/jbellis/jvector/disk/FvecFileVectorValues.java b/jvector-base/src/main/java/io/github/jbellis/jvector/disk/FvecFileVectorValues.java index 64b27d30c..8a1815d94 100644 --- a/jvector-base/src/main/java/io/github/jbellis/jvector/disk/FvecFileVectorValues.java +++ b/jvector-base/src/main/java/io/github/jbellis/jvector/disk/FvecFileVectorValues.java @@ -55,7 +55,9 @@ * has its own borrowed vector. * *
Close all cursors and stop consumers before closing the source. Closing the root
- * releases all resources, including resources used by copies. No memory mapping or
+ * releases all resources, including resources used by copies. A point read already in
+ * progress finishes before its channel is closed; later point reads, including ones
+ * waiting for a reader, fail with {@link IllegalStateException}. No memory mapping or
* OS cache eviction is performed. Retained training vectors are caller-owned memory.
*/
public final class FvecFileVectorValues implements BatchedVectorValues, AutoCloseable {
@@ -463,6 +465,8 @@ void readPointInto(int ordinal, VectorFloat> destination, int offset) {
PointReader reader = null;
try {
reader = borrowPointReader();
+ // Close waits for each lease, so a closed source can't reopen this reader's channel.
+ if (closed) throw new IllegalStateException("Vector source is closed");
pointHint.set(reader);
var input = pointChannel(reader);
var buffer = reader.buffer;
@@ -626,10 +630,12 @@ void close() throws IOException {
}
IOException failure = null;
for (PointReader reader : pointReaders) {
+ // Wait for an in-flight point read to finish; readers that lease afterwards see closed.
+ reader.permit.acquireUninterruptibly();
try { if (reader.channel != null) reader.channel.close(); }
catch (IOException error) {
if (failure == null) failure = error; else failure.addSuppressed(error);
- } finally { reader.channel = null; reader.buffer = null; }
+ } finally { reader.channel = null; reader.buffer = null; reader.permit.release(); }
}
if (pointHint != null) pointHint.remove();
try { channel.close(); }
diff --git a/jvector-base/src/main/java/io/github/jbellis/jvector/graph/similarity/BuildScoreProvider.java b/jvector-base/src/main/java/io/github/jbellis/jvector/graph/similarity/BuildScoreProvider.java
index bda700d04..8e1f91cf8 100644
--- a/jvector-base/src/main/java/io/github/jbellis/jvector/graph/similarity/BuildScoreProvider.java
+++ b/jvector-base/src/main/java/io/github/jbellis/jvector/graph/similarity/BuildScoreProvider.java
@@ -110,11 +110,23 @@ static BuildScoreProvider randomAccessScoreProvider(RandomAccessVectorValues rav
var vectorsCopy = ravv.threadLocalSupplier();
return new BuildScoreProvider() {
+ /// Exact scores against `node1`.
+ ///
+ /// For a value-shared source the anchor is read on the first comparison and kept as
+ /// a private copy, so creating a score function reads nothing, later reads through the
+ /// same views can't overwrite the anchor, and each comparison reads only `node2`.
private ScoreFunction.ExactScoreFunction scoreFunctionFor(int node1) {
if (ravv.isValueShared()) {
- // Resolve both ordinals using separate borrowed views; no anchor escapes a read.
- return node2 -> similarityFunction.compare(vectors.get().getVector(node1),
- vectorsCopy.get().getVector(node2));
+ return new ScoreFunction.ExactScoreFunction() {
+ private VectorFloat> anchor;
+
+ @Override
+ public float similarityTo(int node2) {
+ var a = anchor;
+ if (a == null) anchor = a = vectors.get().getVector(node1).copy();
+ return similarityFunction.compare(a, vectorsCopy.get().getVector(node2));
+ }
+ };
}
var vector = vectors.get().getVector(node1);
var vc = vectorsCopy.get();
diff --git a/jvector-examples/pom.xml b/jvector-examples/pom.xml
index 088f60d8e..843c8ef25 100644
--- a/jvector-examples/pom.xml
+++ b/jvector-examples/pom.xml
@@ -15,6 +15,7 @@
> getGroundTruth(){return List.of(List.of(0));}
};
+ var cacheFile = Path.of("pq_cache").resolve(new CompressorParameters.PQParameters(4,16,false,-1f).idStringFor(ds));
var work = Files.createTempDirectory("bulk-grid-wiring");
try {
Grid.runOneGraph(OnDiskGraphIndexCache.initialize(work.resolve("saved")), List.of(EnumSet.of(FeatureId.INLINE_VECTORS),EnumSet.of(FeatureId.NVQ_VECTORS)),
@@ -139,7 +142,10 @@ private MultiConfig config(String build, String search, boolean fused, String re
}
assertEquals(2,seen);
} finally {
- try(var paths=Files.walk(work)){for(var path:paths.sorted(Comparator.reverseOrder()).collect(java.util.stream.Collectors.toList()))Files.delete(path);}
+ try { Files.deleteIfExists(cacheFile); }
+ finally {
+ try(var paths=Files.walk(work)){for(var path:paths.sorted(Comparator.reverseOrder()).collect(java.util.stream.Collectors.toList()))Files.delete(path);}
+ }
}
}
}
diff --git a/jvector-tests/src/test/java/io/github/jbellis/jvector/graph/TestBuildScoreProviderAnchorReads.java b/jvector-tests/src/test/java/io/github/jbellis/jvector/graph/TestBuildScoreProviderAnchorReads.java
new file mode 100644
index 000000000..da55e3a26
--- /dev/null
+++ b/jvector-tests/src/test/java/io/github/jbellis/jvector/graph/TestBuildScoreProviderAnchorReads.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.github.jbellis.jvector.graph;
+
+import io.github.jbellis.jvector.disk.FvecFileVectorValues;
+import io.github.jbellis.jvector.graph.similarity.BuildScoreProvider;
+import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.Assert.assertTrue;
+
+/// A full-precision build score function compares one fixed anchor against many nodes. For a
+/// value-shared file source it should read the anchor once, not once per comparison.
+public class TestBuildScoreProviderAnchorReads {
+ private static final int ROWS = 2000, DIMENSIONS = 16, COMPARISONS = 1000;
+
+ private static Path writeFvecs() throws Exception {
+ var file = Files.createTempFile("anchor-reads", ".fvecs");
+ var bytes = ByteBuffer.allocate(ROWS * (DIMENSIONS + 1) * 4).order(ByteOrder.LITTLE_ENDIAN);
+ for (int row = 0; row < ROWS; row++) {
+ bytes.putInt(DIMENSIONS);
+ for (int d = 0; d < DIMENSIONS; d++) bytes.putFloat(row + d / 32f);
+ }
+ Files.write(file, bytes.array());
+ return file;
+ }
+
+ private static void assertAnchorReadOnce(String kind, long reads) {
+ // One read per compared node, plus one for the anchor and a little slack.
+ long limit = COMPARISONS + 8;
+ assertTrue(kind + " made " + reads + " source reads for " + COMPARISONS
+ + " comparisons against one anchor; expected at most " + limit, reads <= limit);
+ }
+
+ @Test public void diversityScoreFunctionReadsAnchorOnce() throws Exception {
+ var file = writeFvecs();
+ try (var source = FvecFileVectorValues.open(file)) {
+ var bsp = BuildScoreProvider.randomAccessScoreProvider(source, VectorSimilarityFunction.EUCLIDEAN);
+ var sf = bsp.diversityScoreFunctionFor(0);
+ long before = source.statistics().reads;
+ for (int node = 1; node <= COMPARISONS; node++) sf.similarityTo(node);
+ assertAnchorReadOnce("diversityScoreFunctionFor", source.statistics().reads - before);
+ } finally {
+ Files.delete(file);
+ }
+ }
+
+ @Test public void searchProviderReadsAnchorOnce() throws Exception {
+ var file = writeFvecs();
+ try (var source = FvecFileVectorValues.open(file)) {
+ var bsp = BuildScoreProvider.randomAccessScoreProvider(source, VectorSimilarityFunction.EUCLIDEAN);
+ var sf = bsp.searchProviderFor(0).scoreFunction();
+ long before = source.statistics().reads;
+ for (int node = 1; node <= COMPARISONS; node++) sf.similarityTo(node);
+ assertAnchorReadOnce("searchProviderFor", source.statistics().reads - before);
+ } finally {
+ Files.delete(file);
+ }
+ }
+}