diff --git a/README.md b/README.md index 3d7bb2546..0e0675d5e 100644 --- a/README.md +++ b/README.md @@ -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: + +```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) 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..b415e36fa --- /dev/null +++ b/docs/release notes/4.0.2/751.feature.md @@ -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. 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..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(); } - // 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(); @@ -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) { 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/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..7919f8e6b --- /dev/null +++ b/jvector-tests/src/test/java/io/github/jbellis/jvector/vector/TestVectorizationProviderSelection.java @@ -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(); + 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)); + } + } +}