Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,39 @@ Introductory tutorials for JVector are available in [docs/tutorials](./docs/tuto
The older step-by-step guide for JV can be found [here](./docs/legacy/jvector-step-by-step.md). New users should start with the tutorials mentioned earler, but the step-by-step guide contains useful commentary for advanced users.


## SIMD provider selection

On Java 22+ with the Vector API enabled, JVector automatically prefers its Google
Highway native provider when the native library is available. Native libraries are
bundled for Linux x86-64 (SSE4.2 or newer) and Linux AArch64. Use these JVM options:

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.

Just a quick note: native x86 support spent at least one release as an experimental feature before being enabled in production. In contrast, for AArch64 we're enabling it by default in production in the same release that introduces the feature.


```text
--add-modules jdk.incubator.vector
--enable-native-access=ALL-UNNAMED
```

Operations implemented by Highway use native kernels; remaining operations use the
inherited Panama/Java Vector API implementation with native vector storage. Native
ISA selection remains automatic and respects the existing `JVECTOR_MAX_ISA` cap.

| JVM property | Effect |
| --- | --- |
| No provider property | Prefer native on Java 22+; use Panama when native is unavailable |
| `-Djvector.enable_native_vectorization=false` | Disable automatic native selection |
| `-Djvector.vectorization_provider=panama` | Explicitly select Panama |
| `-Djvector.vectorization_provider=native` | Explicitly require native; fail if it cannot load |
| `-Djvector.vectorization_provider=default` | Explicitly select the scalar provider |

The explicit `jvector.vectorization_provider` property takes precedence over the
native-enable property. The former
`jvector.experimental.enable_native_vectorization` name is a deprecated compatibility
alias; the new `jvector.enable_native_vectorization` name takes precedence when both
are supplied. Both names default to `true` when neither is supplied. Provider
selection happens once per JVM. Java 20/21 uses
Panama when available. Older runtimes, or runtimes without the Vector API module,
retain the scalar fallback. Neither Panama nor native is enabled by these defaults
when the existing runtime compatibility checks reject SIMD support.

## The research behind the algorithms

* Foundational work: [HNSW](https://ieeexplore.ieee.org/abstract/document/8594636) and [DiskANN](https://suhasjs.github.io/files/diskann_neurips19.pdf) papers, and [a higher level explainer](https://www.datastax.com/guides/hierarchical-navigable-small-worlds)
Expand Down
3 changes: 3 additions & 0 deletions docs/release notes/4.0.2/751.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Google Highway native SIMD is no longer experimental and is preferred by default
on supported Java 22+ runtimes. `jvector.enable_native_vectorization` is the supported
flag; the former name remains a deprecated compatibility alias.
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ static VectorizationProvider lookup(boolean testMode) {
return new DefaultVectorizationProvider();
}

// check for jvector.experimental.enable_native_vectorization
// this is an experimental feature subject to change
if (Boolean.getBoolean("jvector.experimental.enable_native_vectorization")) {
// Native uses Java 22 FFM. The new setting takes precedence over its deprecated alias.
if (runtimeVersion >= 22 && Boolean.parseBoolean(System.getProperty(
"jvector.enable_native_vectorization",
System.getProperty("jvector.experimental.enable_native_vectorization", "true")))) {
try {
Class<?> clazz = Class.forName("io.github.jbellis.jvector.vector.NativeVectorizationProvider");
Constructor<?> ctor = clazz.getConstructor();
Expand All @@ -141,7 +142,7 @@ static VectorizationProvider lookup(boolean testMode) {
} catch (UnsupportedOperationException uoe) {
LOG.warning("Native vector API was not enabled. " + uoe.getMessage());
} catch (ClassNotFoundException e) {
LOG.warning("Java version does not support native vector API");
LOG.fine("Native vector provider is not on the classpath; trying the Java Vector API.");
} catch (InvocationTargetException e) {
// unwrap, check for UnsupportedOperationException
if (e.getCause() instanceof UnsupportedOperationException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import java.lang.foreign.MemorySegment;
import java.nio.ByteOrder;

import io.github.jbellis.jvector.annotations.Experimental;
import io.github.jbellis.jvector.vector.cnative.NativeSimdOps;
import io.github.jbellis.jvector.vector.types.ByteSequence;
import io.github.jbellis.jvector.vector.types.VectorFloat;
Expand All @@ -29,10 +28,8 @@
import jdk.incubator.vector.VectorSpecies;

/**
* Experimental!
* VectorUtilSupport implementation that prefers native/Panama SIMD.
* VectorUtilSupport using Highway kernels and inherited Panama operations.
*/
@Experimental
final class NativeVectorUtilSupport extends PanamaVectorUtilSupport
{
public NativeVectorUtilSupport() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@

package io.github.jbellis.jvector.vector;

import io.github.jbellis.jvector.annotations.Experimental;
import io.github.jbellis.jvector.vector.cnative.LibraryLoader;
import io.github.jbellis.jvector.vector.types.VectorTypeSupport;

/**
* Experimental!
* VectorizationProvider implementation that uses MemorySegment vectors and prefers native/Panama SIMD.
* VectorizationProvider using MemorySegment vectors, Highway kernels, and Panama
* for operations without native implementations.
*/
@Experimental
public class NativeVectorizationProvider extends VectorizationProvider {
private final VectorUtilSupport vectorUtilSupport;
private final VectorTypeSupport vectorTypeSupport;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ public void testAssembleAndSum() {
* -ea
* --add-modules jdk.incubator.vector
* --enable-native-access=ALL-UNNAMED
* -Djvector.experimental.enable_native_vectorization=true
* }</pre>
*
* To run with panama support, use
* <pre>{@code
* --ea
* -ea
* --add-modules jdk.incubator.vector
* -Djvector.vectorization_provider=panama
* }</pre>
*
* If <pre>{@code -DTest_RequireSpecificVectorizationProvider=<simplename>}</pre>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* 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.vector;

import org.junit.Assume;
import org.junit.Test;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

/** Fresh JVMs exercise the real singleton without leaking provider properties between tests. */
public class TestVectorizationProviderSelection {
private static final String ENABLE_NATIVE = "-Djvector.enable_native_vectorization=";
private static final String LEGACY_ENABLE_NATIVE = "-Djvector.experimental.enable_native_vectorization=";
private static final boolean HAS_VECTOR_MODULE = VectorizationProvider.vectorModulePresentAndReadable();

private static String classpath() {
return System.getProperty("surefire.test.class.path", System.getProperty("java.class.path"));
}

private static String withoutNativeArtifact() {
return Arrays.stream(classpath().split(java.util.regex.Pattern.quote(File.pathSeparator)))
.filter(entry -> !entry.replace('\\', '/').contains("/jvector-native/"))
.filter(entry -> !(Path.of(entry).getFileName().toString().startsWith("jvector-native-")
&& entry.endsWith(".jar")))
.collect(Collectors.joining(File.pathSeparator));
}

private static boolean nativeAvailable() throws Exception {
if (Runtime.version().feature() < 22 || !HAS_VECTOR_MODULE
|| !System.getProperty("os.name").startsWith("Linux")) return false;
String arch = System.getProperty("os.arch");
String resource = arch.equals("aarch64") ? "/libjvector-aarch64.so"
: (arch.equals("amd64") || arch.equals("x86_64")) ? "/libjvector-x86_64.so" : null;
if (resource == null) return false;
try {
return Class.forName("io.github.jbellis.jvector.vector.NativeVectorizationProvider", false,
TestVectorizationProviderSelection.class.getClassLoader()).getResource(resource) != null;
} catch (ClassNotFoundException e) { return false; }
}

private static String javaProvider() {
return HAS_VECTOR_MODULE ? "PanamaVectorizationProvider" : "DefaultVectorizationProvider";
}

private static String probe(String cp, boolean vectorModule, boolean success, String... properties) throws Exception {
var tmp = Files.createTempDirectory("provider-selection-");
Process child = null;
try {
var command = new ArrayList<String>();
command.add(Path.of(System.getProperty("java.home"), "bin", "java").toString());
if (vectorModule) command.add("--add-modules=jdk.incubator.vector");
if (Runtime.version().feature() >= 22) command.add("--enable-native-access=ALL-UNNAMED");
command.add("-Djava.io.tmpdir=" + tmp);
command.addAll(Arrays.asList(properties));
command.add("-cp"); command.add(cp); command.add(Probe.class.getName());
var process = new ProcessBuilder(command).redirectErrorStream(true).start();
child = process;
assertTrue("Provider probe timed out", process.waitFor(30, TimeUnit.SECONDS));
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
if (success) assertEquals(output, 0, process.exitValue());
else assertTrue(output, process.exitValue() != 0);
return output;
} finally {
if (child != null && child.isAlive()) { child.destroyForcibly(); child.waitFor(); }
try (var files = Files.walk(tmp)) {
for (var file : files.sorted(Comparator.reverseOrder()).collect(Collectors.toList()))
Files.delete(file);
}
}
}

private static void assertProvider(String expected, String cp, String... properties) throws Exception {
String output = probe(cp, HAS_VECTOR_MODULE, true, properties);
assertTrue(output, output.contains("PROVIDER=" + expected));
// The native distance and inherited sum must both work with the selected vector storage.
assertTrue(output, output.contains("DOT=14.0 SUM=6.0"));
}

@Test public void automaticSelectionPrefersAvailableNative() throws Exception {
assertProvider(nativeAvailable() ? "NativeVectorizationProvider" : javaProvider(), classpath());
}

@Test public void explicitFalseDisablesAutomaticNative() throws Exception {
assertProvider(javaProvider(), classpath(), ENABLE_NATIVE + "false");
}

@Test public void explicitTrueStillEnablesNative() throws Exception {
assertProvider(nativeAvailable() ? "NativeVectorizationProvider" : javaProvider(), classpath(), ENABLE_NATIVE + "true");
}

@Test public void deprecatedFalseAliasStillDisablesNative() throws Exception {
assertProvider(javaProvider(), classpath(), LEGACY_ENABLE_NATIVE + "false");
}

@Test public void deprecatedTrueAliasStillEnablesNative() throws Exception {
assertProvider(nativeAvailable() ? "NativeVectorizationProvider" : javaProvider(), classpath(),
LEGACY_ENABLE_NATIVE + "true");
}

@Test public void newFalseOverridesDeprecatedTrue() throws Exception {
assertProvider(javaProvider(), classpath(), ENABLE_NATIVE + "false", LEGACY_ENABLE_NATIVE + "true");
}

@Test public void newTrueOverridesDeprecatedFalse() throws Exception {
assertProvider(nativeAvailable() ? "NativeVectorizationProvider" : javaProvider(), classpath(),
LEGACY_ENABLE_NATIVE + "false", ENABLE_NATIVE + "true");
}

@Test public void explicitScalarProviderTakesPrecedence() throws Exception {
assertProvider("DefaultVectorizationProvider", classpath(), ENABLE_NATIVE + "true", LEGACY_ENABLE_NATIVE + "false",
"-Djvector.vectorization_provider=default");
}

@Test public void explicitPanamaProviderTakesPrecedence() throws Exception {
Assume.assumeTrue(HAS_VECTOR_MODULE);
assertProvider("PanamaVectorizationProvider", classpath(), ENABLE_NATIVE + "true", LEGACY_ENABLE_NATIVE + "false",
"-Djvector.vectorization_provider=panama");
}

@Test public void explicitNativeProviderTakesPrecedenceOverFalse() throws Exception {
Assume.assumeTrue(nativeAvailable());
assertProvider("NativeVectorizationProvider", classpath(), ENABLE_NATIVE + "false", LEGACY_ENABLE_NATIVE + "true",
"-Djvector.vectorization_provider=native");
}

@Test public void absentNativeArtifactUsesJavaProvider() throws Exception {
assertProvider(javaProvider(), withoutNativeArtifact());
}

@Test public void unsupportedArchitectureUsesJavaProvider() throws Exception {
assertProvider(javaProvider(), classpath(), "-Dos.arch=unsupported-provider-test");
}

@Test public void explicitNativeFailureDoesNotSilentlyFallBack() throws Exception {
String output = probe(classpath(), HAS_VECTOR_MODULE, false,
"-Djvector.vectorization_provider=native", "-Dos.arch=unsupported-provider-test");
assertTrue(output, output.contains("Failed to load forced NativeVectorizationProvider"));
}

@Test public void absentVectorModuleUsesScalar() throws Exception {
String output = probe(classpath(), false, true);
assertTrue(output, output.contains("PROVIDER=DefaultVectorizationProvider"));
}

public static class Probe {
public static void main(String[] args) {
var provider = VectorizationProvider.getInstance();
var vector = provider.getVectorTypeSupport().createFloatVector(new float[] {1f, 2f, 3f});
var support = provider.getVectorUtilSupport();
System.out.println("PROVIDER=" + provider.getClass().getSimpleName());
System.out.println("DOT=" + support.dotProduct(vector, vector) + " SUM=" + support.sum(vector));
}
}
}
Loading