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
137 changes: 137 additions & 0 deletions .github/workflows/unit-tests-arm64.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Unit Test CI — ARM64 (NEON / SVE)

on:
workflow_dispatch:
pull_request:
push:
branches:
- main
paths:
- .github/workflows/unit-tests-arm64.yaml
- '**.java'
- '**/pom.xml'

jobs:
build-arm64:
concurrency:
group: arm64-${{ matrix.max_isa }}-${{ matrix.jdk }}
cancel-in-progress: false
strategy:
matrix:
jdk: [ 24 ]
# Three ISA tiers in ascending capability order, mirroring avx512f/avx2/sse42.
# GitHub-hosted ubuntu-24.04-arm is a Neoverse-N1 (Graviton 2): NEON only, no SVE.
# The sve/sve2 matrix entries still exercise the JVECTOR_MAX_ISA cap path and
# compile all three ISA variants; the native kernel tests that require actual SVE
# hardware are gated on the runtime feature check below.
max_isa: [ neon, sve, sve2 ]
runs-on: ubuntu-24.04-arm
steps:
- name: Report ARM64 ISA capabilities
id: cpu-features
run: |
# Parse the "Features" line from /proc/cpuinfo — the kernel only exposes a
# token here when the OS has set up context-switch support for it, so this is
# the same authority as getauxval(AT_HWCAP / AT_HWCAP2).
# "asimd" is the NEON token; "sve"/"sve2"/"sveaes" appear on Graviton 3/4.
flags="$(grep '^Features' /proc/cpuinfo | head -1 | cut -d: -f2)"
has_neon=false; has_sve=false; has_sve2=false
[[ " $flags " == *" asimd "* ]] && has_neon=true
[[ " $flags " == *" sve "* ]] && has_sve=true
[[ " $flags " == *" sve2 "* ]] && has_sve2=true
printf "NEON=%s SVE=%s SVE2=%s\n" "$has_neon" "$has_sve" "$has_sve2"
if [[ "$has_neon" != "true" ]]; then
echo "ERROR: NEON (asimd) not found in /proc/cpuinfo — not a valid AArch64 runner"
exit 2
fi
# Expose as step outputs for conditional steps below.
echo "has_neon=$has_neon" >> "$GITHUB_OUTPUT"
echo "has_sve=$has_sve" >> "$GITHUB_OUTPUT"
echo "has_sve2=$has_sve2" >> "$GITHUB_OUTPUT"

- name: Set up GCC
run: |
sudo apt install -y gcc g++

- name: Install Meson, Ninja, and GTest
run: |
sudo apt update && sudo apt install -y meson ninja-build pkg-config libgtest-dev

- uses: actions/checkout@v4

- name: Initialize Git Submodules
run: git submodule update --init

- name: Build test_simd_kernels (native C++)
# Meson detects aarch64 and compiles all three ISA variants (neon/sve/sve2)
# regardless of what the host CPU supports at runtime.
working-directory: jvector-native/src/main/native
run: |
meson setup build --wipe
ninja -C build test_simd_kernels

- name: Run test_simd_kernels — no ISA cap (auto-detect, neon job)
if: matrix.max_isa == 'neon'
working-directory: jvector-native/src/main/native
run: ./build/test_simd_kernels

- name: Run test_simd_kernels — capped at neon (sve job, host may lack SVE)
if: matrix.max_isa == 'sve'
working-directory: jvector-native/src/main/native
env:
JVECTOR_MAX_ISA: neon
run: ./build/test_simd_kernels

- name: Run test_simd_kernels — no ISA cap on SVE hardware (sve job)
if: matrix.max_isa == 'sve' && steps.cpu-features.outputs.has_sve == 'true'
working-directory: jvector-native/src/main/native
run: ./build/test_simd_kernels

- name: Run test_simd_kernels — capped at neon (sve2 job baseline check)
if: matrix.max_isa == 'sve2'
working-directory: jvector-native/src/main/native
env:
JVECTOR_MAX_ISA: neon
run: ./build/test_simd_kernels

- name: Run test_simd_kernels — no ISA cap on SVE2 hardware (sve2 job)
if: matrix.max_isa == 'sve2' && steps.cpu-features.outputs.has_sve2 == 'true'
working-directory: jvector-native/src/main/native
run: ./build/test_simd_kernels

- name: Set up JDK ${{ matrix.jdk }}
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.jdk }}
distribution: temurin
cache: maven

- name: Verify native-access vector support (JDK ${{ matrix.jdk }})
env:
JVECTOR_MAX_ISA: ${{ matrix.max_isa }}
run: >-
mvn -B -Punix-amd64-profile -pl jvector-tests -am test
-DTest_RequireSpecificVectorizationProvider=NativeVectorizationProvider
-Dsurefire.failIfNoSpecifiedTests=false
-Dtest=TestVectorizationProvider

- name: Test full suite with native vectorization (JDK ${{ matrix.jdk }})
env:
JVECTOR_MAX_ISA: ${{ matrix.max_isa }}
run: >-
mvn -B -Punix-amd64-profile test
-DTest_RequireSpecificVectorizationProvider=NativeVectorizationProvider

- name: Test Summary for (ARM64/max:${{ matrix.max_isa }},JDK${{ matrix.jdk }})
if: always()
uses: test-summary/action@v2
with:
paths: |
**/target/surefire-reports/TEST-*.xml

- name: Upload Surefire Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: surefire-results--arm64-${{ matrix.max_isa }}-${{ matrix.jdk }}
path: "**/target/surefire-reports/**"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ git clone --recurse-submodules <repo-url>
### Building native libraries

The native SIMD library (`libjvector.so`) is built with [Meson](https://mesonbuild.com/) + [Ninja](https://ninja-build.org/)
and requires **g++ 11+**. The entry-point script is
`jvector-native/src/main/native/jextract_vector_simd.sh`. Run it from that directory:
and requires **g++ 11+**. Supported platforms: **Linux x86-64** (SSE4.2, AVX2, AVX-512) and **Linux AArch64** (NEON, SVE, SVE2).
The entry-point script is `jvector-native/src/main/native/jextract_vector_simd.sh`. Run it from that directory:

```bash
cd jvector-native/src/main/native
Expand Down
1 change: 0 additions & 1 deletion jvector-native/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@
<activation>
<os>
<family>unix</family>
<arch>amd64</arch>
</os>
</activation>
<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public class NativeVectorizationProvider extends VectorizationProvider {

public NativeVectorizationProvider() {
var arch = System.getProperty("os.arch", "");
if (!arch.equals("amd64") && !arch.equals("x86_64")) {
throw new UnsupportedOperationException("Native SIMD operations are only supported on x86_64.");
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) {
Expand Down
47 changes: 33 additions & 14 deletions jvector-native/src/main/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ backend that accelerates vector operations in JVector via the Java Foreign
Function & Memory (FFM) API.

> **Platform support:** Currently enabled on **Linux x86-64** (SSE4.2, AVX2,
> and AVX-512). Windows and macOS are not yet supported. Support for **ARM**
> (NEON and SVE) is planned for the near future; the
> [Google Highway](https://github.com/google/highway) library used for SIMD
> portability already targets both AArch64 targets, which will make the
> extension straightforward.
> AVX-512) and **Linux AArch64** (NEON, SVE, SVE2). Windows and macOS are not
> yet supported.

---

Expand Down Expand Up @@ -242,19 +239,32 @@ JVECTOR_MAX_ISA=sse42 ../../../target/meson-build/bench_simd_kernels

## How it is integrated into JVector

**x86-64:**
```
Java caller
└─ NativeVectorUtilSupport (jvector-native/.../vector/)
└─ NativeSimdOps (jvector-native/.../vector/cnative/ — FFM glue, generated by jextract)
└─ libjvector.so (this library, loaded at runtime by LibraryLoader)
└─ jvector_simd.cpp — dispatches to the best ISA vtable
├─ AVX3_SPR::* (compiled with -march=skylake-avx512 -mavx512fp16 …, Sapphire Rapids)
├─ AVX3_DL::* (compiled with -march=skylake-avx512 -mavx512vnni …, Ice Lake)
├─ AVX3_SPR::* (compiled with -march=sapphirerapids)
├─ AVX3_DL::* (compiled with -march=icelake-server)
├─ AVX3::* (compiled with -march=skylake-avx512)
├─ AVX2::* (compiled with -march=haswell)
└─ SSE42::* (compiled with -msse4.2, scalar fallback)
```

**AArch64:**
```
Java caller
└─ NativeVectorUtilSupport (jvector-native/.../vector/)
└─ NativeSimdOps (jvector-native/.../vector/cnative/ — FFM glue, generated by jextract)
└─ libjvector.so (this library, loaded at runtime by LibraryLoader)
└─ jvector_simd.cpp — dispatches to the best ISA vtable
├─ SVE2::* (compiled with -march=armv9-a+sve2, scalable VL)
├─ SVE::* (compiled with -march=armv8.4-a+sve, scalable VL)
└─ NEON::* (compiled with -march=armv8-a+crypto, baseline)
```

### Load sequence

1. `NativeVectorizationProvider` calls `LibraryLoader.loadJvector()` at startup.
Expand All @@ -268,9 +278,10 @@ Java caller

Dispatch happens **once** at C++ static-init time (before `main()`):

1. `populate_cpu_features()` issues CPUID / XGETBV and fills a feature array.
2. `dispatch_kernels()` checks the feature array in descending capability order
(`AVX3` ⊃ `AVX2` ⊃ `SSE42`) and returns a copy of the matching `KernelVTable`.
1. `populate_cpu_features()` issues CPUID / XGETBV (x86) or `getauxval` (AArch64) and fills a feature array.
2. `dispatch_kernels()` checks the feature array in descending capability order and returns a copy of the matching `KernelVTable`:
- x86-64: `AVX3_SPR` ⊃ `AVX3_DL` ⊃ `AVX3` ⊃ `AVX2` ⊃ `SSE42`
- AArch64: `SVE2` ⊃ `SVE` ⊃ `NEON`
3. All public API functions are one-liner wrappers that call through
`kernels.<fn>`.

Expand All @@ -279,15 +290,23 @@ Dispatch happens **once** at C++ static-init time (before `main()`):
Set the `JVECTOR_MAX_ISA` environment variable before starting the JVM to cap
the selected ISA without recompiling:

**x86-64:**
```bash
JVECTOR_MAX_ISA=avx3_spr java ... # use Sapphire-Rapids FP16 tier
JVECTOR_MAX_ISA=avx3_dl java ... # use Ice Lake tier
JVECTOR_MAX_ISA=avx3 java ... # use AVX-512 even if a higher tier is available
JVECTOR_MAX_ISA=avx2 java ... # use AVX2 even on an AVX-512 machine
JVECTOR_MAX_ISA=sse42 java ... # force scalar/SSE4.2 fallback
JVECTOR_MAX_ISA=avx3 java ... # use AVX-512 even if a higher tier is available
JVECTOR_MAX_ISA=avx2 java ... # use AVX2 even on an AVX-512 machine
JVECTOR_MAX_ISA=sse42 java ... # force scalar/SSE4.2 fallback
```

**AArch64:**
```bash
JVECTOR_MAX_ISA=sve2 java ... # use SVE2 tier (Graviton 4 / Neoverse V2/N2)
JVECTOR_MAX_ISA=sve java ... # use SVE tier (Graviton 3 / Neoverse V1)
JVECTOR_MAX_ISA=neon java ... # force NEON baseline
```

Accepted values (case-sensitive): `avx3_spr`, `avx3_dl`, `avx3`, `avx2`, `sse42`.
Accepted values (case-sensitive): `avx3_spr`, `avx3_dl`, `avx3`, `avx2`, `sse42` (x86-64); `sve2`, `sve`, `neon` (AArch64).
An unrecognised value is silently ignored and full CPU detection is used.

### Updating the Java bindings
Expand Down
Loading
Loading