Skip to content
Merged
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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,53 @@ 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:
Comment thread
tlwillke marked this conversation as resolved.

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

Omitting native access can produce restricted-method warnings. If native access is
denied by the runtime or JVM options, automatic selection falls back to Panama;
an explicitly forced native provider fails during initialization. For named-module
deployments, enable native access for the module containing JVector's native code.

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.

Native selection also changes the concrete vector type to `MemorySegmentVectorFloat`.
Use the `VectorFloat` interface instead of casting to `ArrayVectorFloat` or assuming
that `VectorFloat.get()` returns a `float[]`. Both built-in float vector types support
`FloatArray.array()` for array access and `copyFrom` between their storage types.
Native vector operations still require vectors created by the selected provider;
copy external vectors into provider-created vectors before passing them to those
operations. To retain the previous provider and storage choice, disable automatic
native selection with `-Djvector.enable_native_vectorization=false`.

## 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 Expand Up @@ -95,6 +142,9 @@ bash build_native_lib.sh --auto-install-deps
For other distributions the script will print the install commands needed. See
[`jvector-native/src/main/native/README.md`](./jvector-native/src/main/native/README.md) for full build options,
Maven integration, ISA dispatch details, and how to add new kernels.
The [native ABI compatibility guide](./jvector-native/src/main/native/README.md#native-abi-compatibility)
explains the supported ABI set, startup checks, and how to preserve compatibility
when changing native entry points.

---

Expand Down
21 changes: 21 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,21 @@
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.

Successful native selection uses `MemorySegmentVectorFloat` instead of
`ArrayVectorFloat`. Consumers should use `VectorFloat`, or `FloatArray.array()`
when array access is needed; direct casts and assumptions about `get()`'s backing
storage must be updated. `copyFrom` supports both built-in float vector types.
Disable automatic native selection with `-Djvector.enable_native_vectorization=false`
to retain the previous provider choice. Native operations require provider-created
vectors; copying between storage types does not make mixed-storage arithmetic valid.

Pass `--add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED`
for classpath deployments. Missing native access can produce JVM warnings; denied
native access, an unsupported ABI, a missing ABI query, or missing required library
symbols cause automatic selection to fall
back to the Java provider. Explicitly forcing native fails during initialization.

Java requires ABI 1 and checks the native library's explicit supported set before
calling kernels. Compatible native releases keep the same ABI ID. Libraries that
predate the ABI query are rejected; the bundled library supports ABI 1.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;

/**
* VectorFloat implementation backed by an on-heap float array.
Expand Down Expand Up @@ -82,8 +83,15 @@ public VectorFloat<float[]> copy()
@Override
public void copyFrom(VectorFloat<?> src, int srcOffset, int destOffset, int length)
{
ArrayVectorFloat csrc = (ArrayVectorFloat) src;
System.arraycopy(csrc.data, srcOffset, data, destOffset, length);
if (src instanceof FloatArray) {
System.arraycopy(((FloatArray) src).array(), srcOffset, data, destOffset, length);
} else {
Objects.checkFromIndexSize(srcOffset, length, src.length());
Objects.checkFromIndexSize(destOffset, length, data.length);
for (int i = 0; i < length; i++) {
data[destOffset + i] = src.get(srcOffset + i);
}
}
}

@Override
Expand Down Expand Up @@ -130,4 +138,3 @@ public float[] array() {
return get();
}
}

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 @@ -24,6 +24,7 @@
import java.io.IOException;
import java.lang.foreign.MemorySegment;
import java.nio.Buffer;
import java.util.Objects;

/**
* VectorFloat implementation backed by an on-heap MemorySegment.
Expand Down Expand Up @@ -107,9 +108,18 @@ public VectorFloat<MemorySegment> copy()
@Override
public void copyFrom(VectorFloat<?> src, int srcOffset, int destOffset, int length)
{
MemorySegmentVectorFloat csrc = (MemorySegmentVectorFloat) src;
segment.asSlice((long) destOffset * Float.BYTES, (long) length * Float.BYTES)
.copyFrom(csrc.segment.asSlice((long) srcOffset * Float.BYTES, (long) length * Float.BYTES));
if (src instanceof MemorySegmentVectorFloat csrc) {
segment.asSlice((long) destOffset * Float.BYTES, (long) length * Float.BYTES)
.copyFrom(csrc.segment.asSlice((long) srcOffset * Float.BYTES, (long) length * Float.BYTES));
} else if (src instanceof FloatArray csrc) {
System.arraycopy(csrc.array(), srcOffset, array(), destOffset, length);
} else {
Objects.checkFromIndexSize(srcOffset, length, src.length());
Objects.checkFromIndexSize(destOffset, length, length());
for (int i = 0; i < length; i++) {
set(destOffset + i, src.get(srcOffset + i));
}
}
}

@Override
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,32 +16,81 @@

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.cnative.NativeSimdOps;
import io.github.jbellis.jvector.vector.types.VectorTypeSupport;

import java.lang.foreign.SymbolLookup;
import java.lang.reflect.Method;

/**
* 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 static final int REQUIRED_ABI = 1;

private final VectorUtilSupport vectorUtilSupport;
private final VectorTypeSupport vectorTypeSupport;

/// Load the native library and create the provider.
///
/// Throws `UnsupportedOperationException` when the architecture is unsupported, the library
/// can't be loaded, the bindings can't initialize (including when native access is denied),
/// or the loaded library doesn't support the required ABI or export every bound function, so automatic
/// selection falls back to the Java Vector API instead of failing at the first kernel call.
public NativeVectorizationProvider() {
var arch = System.getProperty("os.arch", "");
if (!arch.equals("amd64") && !arch.equals("x86_64") && !arch.equals("aarch64")) {
throw new UnsupportedOperationException("Native SIMD operations are only supported on x86_64 and aarch64.");
}
var libraryLoaded = LibraryLoader.loadJvector();
if (!libraryLoaded) {
throw new UnsupportedOperationException("Failed to load supporting native library.");
String reason = NativeVectorizationProvider.class.getModule().isNativeAccessEnabled() ? ""
: " Native access is not enabled for this module; pass --enable-native-access=ALL-UNNAMED"
+ " (or the jvector module name).";
throw new UnsupportedOperationException("Failed to load supporting native library." + reason);
}
var support = new NativeVectorUtilSupport();
try {
if (SymbolLookup.loaderLookup().find("jvector_supports_abi").isEmpty()) {
throw new UnsupportedOperationException("The loaded jvector native library does not export "
+ "jvector_supports_abi; compatibility with required ABI " + REQUIRED_ABI + " is unknown.");
}
if (NativeSimdOps.jvector_supports_abi(REQUIRED_ABI) != 1) {
throw new UnsupportedOperationException("The loaded jvector native library does not support required ABI "
+ REQUIRED_ABI + "; a different libjvector may be on java.library.path.");
}
String missing = firstMissingSymbol();
if (missing != null) {
throw new UnsupportedOperationException("The loaded jvector native library does not export " + missing
+ "; a different libjvector may be on java.library.path.");
}
support.getActiveIsa();
} catch (IllegalCallerException | LinkageError e) {
throw new UnsupportedOperationException("Native bindings could not initialize.", e);
}
this.vectorUtilSupport = new NativeVectorUtilSupport();
this.vectorUtilSupport = support;
this.vectorTypeSupport = new MemorySegmentVectorProvider();
}

/// Returns the first function bound in [NativeSimdOps] (one per generated `name$address()`
/// accessor) that the libraries loaded by this class loader don't export, or `null` when all
/// are present. Uses a symbol lookup only, so no binding is initialized.
private static String firstMissingSymbol() {
var lookup = SymbolLookup.loaderLookup();
for (Method method : NativeSimdOps.class.getMethods()) {
String name = method.getName();
if (method.getParameterCount() == 0 && name.endsWith("$address")) {
String symbol = name.substring(0, name.length() - "$address".length());
if (lookup.find(symbol).isEmpty()) {
return symbol;
}
}
}
return null;
}

@Override
public VectorUtilSupport getVectorUtilSupport() {
return vectorUtilSupport;
Expand All @@ -51,4 +100,4 @@ public VectorUtilSupport getVectorUtilSupport() {
public VectorTypeSupport getVectorTypeSupport() {
return vectorTypeSupport;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.github.jbellis.jvector.vector.cnative;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

/**
Expand Down Expand Up @@ -48,12 +49,20 @@ static String resourceNameForArch() {
return null;
}

/// Load the jvector native library, from `java.library.path` first and then from the bundled
/// classpath resource.
///
/// Returns `false` when the library is unavailable, native access is denied to this module
/// (`IllegalCallerException`, e.g. with `--illegal-native-access=deny`), or a security policy
/// blocks loading (`SecurityException`). The bundled copy isn't retried after access denial.
public static boolean loadJvector() {
try {
System.loadLibrary("jvector");
return true;
} catch (UnsatisfiedLinkError e) {
// ignore
} catch (IllegalCallerException | SecurityException e) {
return false; // native access or library loading denied
}
try {
// reinventing the wheel instead of picking up deps, so we'll just use the classloader to load the library
Expand All @@ -64,18 +73,25 @@ public static boolean loadJvector() {
}
String baseName = resourceName.substring(1, resourceName.lastIndexOf('.')); // e.g. "libjvector-aarch64"
String ext = resourceName.substring(resourceName.lastIndexOf('.')); // e.g. ".so"
File tmpLibFile = File.createTempFile(baseName, ext);
try (var in = LibraryLoader.class.getResourceAsStream(resourceName);
var out = Files.newOutputStream(tmpLibFile.toPath())) {
if (in != null) {
in.transferTo(out);
out.flush();
} else {
try (var in = LibraryLoader.class.getResourceAsStream(resourceName)) {
if (in == null) {
return false; // couldn't find library
}
File tmpLibFile = File.createTempFile(baseName, ext);
try {
try (var out = Files.newOutputStream(tmpLibFile.toPath())) {
in.transferTo(out);
}
System.load(tmpLibFile.getAbsolutePath());
return true;
} finally {
try {
Files.deleteIfExists(tmpLibFile.toPath());
} catch (IOException | SecurityException e) {
// Best effort: cleanup failure must not invalidate a loaded library.
}
}
}
System.load(tmpLibFile.getAbsolutePath());
return true;
} catch (Exception | UnsatisfiedLinkError e) {
// ignore
}
Expand Down
Loading
Loading