From 141259d95291721c6c6df5399fbd7eef8ce04e5c Mon Sep 17 00:00:00 2001 From: Ted Willke Date: Mon, 12 Oct 2026 22:34:37 +0000 Subject: [PATCH 1/3] Prefer available native SIMD by default --- README.md | 29 ++++ .../jvector/vector/VectorizationProvider.java | 8 +- .../vector/TestVectorizationProvider.java | 4 +- .../TestVectorizationProviderSelection.java | 160 ++++++++++++++++++ 4 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java diff --git a/README.md b/README.md index 3d7bb2546..73b0f217c 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,35 @@ 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: + +```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.experimental.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. 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) diff --git a/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java b/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java index c25458b67..36b9c3926 100644 --- a/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java +++ b/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java @@ -120,9 +120,9 @@ 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 the Java 22 FFM API. An explicit false keeps the Panama path. + if (runtimeVersion >= 22 && Boolean.parseBoolean( + System.getProperty("jvector.experimental.enable_native_vectorization", "true"))) { try { Class clazz = Class.forName("io.github.jbellis.jvector.vector.NativeVectorizationProvider"); Constructor ctor = clazz.getConstructor(); @@ -141,7 +141,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) { diff --git a/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProvider.java b/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProvider.java index 81a99aafc..b8d0b5c3e 100644 --- a/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProvider.java +++ b/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProvider.java @@ -98,13 +98,13 @@ public void testAssembleAndSum() { * -ea * --add-modules jdk.incubator.vector * --enable-native-access=ALL-UNNAMED - * -Djvector.experimental.enable_native_vectorization=true * } * * To run with panama support, use *
{@code
-     *   --ea
+     *   -ea
      *   --add-modules jdk.incubator.vector
+     *   -Djvector.vectorization_provider=panama
      * }
* * If
{@code -DTest_RequireSpecificVectorizationProvider=}
diff --git a/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java b/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java new file mode 100644 index 000000000..4d008e28e --- /dev/null +++ b/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java @@ -0,0 +1,160 @@ +/* + * 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.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(); + 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 explicitScalarProviderTakesPrecedence() throws Exception { + assertProvider("DefaultVectorizationProvider", classpath(), ENABLE_NATIVE + "true", + "-Djvector.vectorization_provider=default"); + } + + @Test public void explicitPanamaProviderTakesPrecedence() throws Exception { + Assume.assumeTrue(HAS_VECTOR_MODULE); + assertProvider("PanamaVectorizationProvider", classpath(), ENABLE_NATIVE + "true", + "-Djvector.vectorization_provider=panama"); + } + + @Test public void explicitNativeProviderTakesPrecedenceOverFalse() throws Exception { + Assume.assumeTrue(nativeAvailable()); + assertProvider("NativeVectorizationProvider", classpath(), ENABLE_NATIVE + "false", + "-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)); + } + } +} From 915e7832085e4ec967b32204a9daa91d5138acc3 Mon Sep 17 00:00:00 2001 From: Ted Willke Date: Mon, 12 Oct 2026 22:34:58 +0000 Subject: [PATCH 2/3] Add native-default provider release note --- docs/release notes/4.0.2/751.feature.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/release notes/4.0.2/751.feature.md diff --git a/docs/release notes/4.0.2/751.feature.md b/docs/release notes/4.0.2/751.feature.md new file mode 100644 index 000000000..ade3ba508 --- /dev/null +++ b/docs/release notes/4.0.2/751.feature.md @@ -0,0 +1,4 @@ +JVector now prefers Google Highway native SIMD automatically on supported Java 22+ +runtimes. Explicit provider overrides and the native-enable opt-out remain available. +Operations without native kernels retain Panama implementations; unavailable native +support retains the existing Panama/scalar fallback. From 599478a183504bca5f33296bb8ad4daa960adf28 Mon Sep 17 00:00:00 2001 From: Ted Willke Date: Mon, 12 Oct 2026 22:43:25 +0000 Subject: [PATCH 3/3] Graduate native provider and retain deprecated option alias --- README.md | 8 ++++-- docs/release notes/4.0.2/751.feature.md | 7 +++-- .../jvector/vector/VectorizationProvider.java | 7 ++--- .../vector/NativeVectorUtilSupport.java | 5 +--- .../vector/NativeVectorizationProvider.java | 6 ++--- .../TestVectorizationProviderSelection.java | 27 ++++++++++++++++--- 6 files changed, 39 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 73b0f217c..0e0675d5e 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,17 @@ 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.experimental.enable_native_vectorization=false` | Disable automatic native selection | +| `-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. Provider selection happens once per JVM. Java 20/21 uses +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. diff --git a/docs/release notes/4.0.2/751.feature.md b/docs/release notes/4.0.2/751.feature.md index ade3ba508..b415e36fa 100644 --- a/docs/release notes/4.0.2/751.feature.md +++ b/docs/release notes/4.0.2/751.feature.md @@ -1,4 +1,3 @@ -JVector now prefers Google Highway native SIMD automatically on supported Java 22+ -runtimes. Explicit provider overrides and the native-enable opt-out remain available. -Operations without native kernels retain Panama implementations; unavailable native -support retains the existing Panama/scalar fallback. +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. diff --git a/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java b/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java index 36b9c3926..c29f092b8 100644 --- a/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java +++ b/jvector-base/src/main/java/io/github/jbellis/jvector/vector/VectorizationProvider.java @@ -120,9 +120,10 @@ static VectorizationProvider lookup(boolean testMode) { return new DefaultVectorizationProvider(); } - // Native uses the Java 22 FFM API. An explicit false keeps the Panama path. - if (runtimeVersion >= 22 && Boolean.parseBoolean( - System.getProperty("jvector.experimental.enable_native_vectorization", "true"))) { + // 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(); diff --git a/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorUtilSupport.java b/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorUtilSupport.java index 4b627a244..9230c2f9a 100644 --- a/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorUtilSupport.java +++ b/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorUtilSupport.java @@ -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; @@ -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() {} diff --git a/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorizationProvider.java b/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorizationProvider.java index 213e53f9a..4d5e68182 100644 --- a/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorizationProvider.java +++ b/jvector-native/src/main/java/io/github/jbellis/jvector/vector/NativeVectorizationProvider.java @@ -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; diff --git a/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java b/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java index 4d008e28e..7919f8e6b 100644 --- a/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java +++ b/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java @@ -34,7 +34,8 @@ /** Fresh JVMs exercise the real singleton without leaking provider properties between tests. */ public class TestVectorizationProviderSelection { - private static final String ENABLE_NATIVE = "-Djvector.experimental.enable_native_vectorization="; + 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() { @@ -112,20 +113,38 @@ private static void assertProvider(String expected, String cp, String... propert 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", + 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", + 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", + assertProvider("NativeVectorizationProvider", classpath(), ENABLE_NATIVE + "false", LEGACY_ENABLE_NATIVE + "true", "-Djvector.vectorization_provider=native"); }