Skip to content

Commit 7349d4f

Browse files
author
Ankur Goel
committed
Move C code to native module and integrate Java code under java21
1 parent 3cb723c commit 7349d4f

24 files changed

Lines changed: 925 additions & 116 deletions

gradle/java/javac.gradle

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ allprojects { project ->
2424

2525
// Use 'release' flag instead of 'source' and 'target'
2626
tasks.withType(JavaCompile) {
27-
options.compilerArgs += ["--release", rootProject.minJavaVersion.toString(), "--enable-preview"]
28-
}
29-
30-
tasks.withType(Test) {
31-
jvmArgs += "--enable-preview"
27+
options.compilerArgs += ["--release", rootProject.minJavaVersion.toString()]
3228
}
3329

3430
// Configure warnings.
@@ -76,19 +72,17 @@ allprojects { project ->
7672
"-Xdoclint:-accessibility"
7773
]
7874

79-
if (project.path == ":lucene:benchmark-jmh" ) {
75+
if (project.path == ":lucene:benchmark-jmh") {
8076
// JMH benchmarks use JMH preprocessor and incubating modules.
8177
} else {
8278
// proc:none was added because of LOG4J2-1925 / JDK-8186647
8379
options.compilerArgs += [
8480
"-proc:none"
8581
]
8682

87-
/**
8883
if (propertyOrDefault("javac.failOnWarnings", true).toBoolean()) {
8984
options.compilerArgs += "-Werror"
9085
}
91-
*/
9286
}
9387
}
9488
}

gradle/testing/defaults-tests.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ allprojects {
139139
":lucene:test-framework"
140140
] ? 'ALL-UNNAMED' : 'org.apache.lucene.core')
141141

142-
jvmArgs '-Djava.library.path=' + file("${buildDir}/libs/dotProduct/shared").absolutePath
143-
144142
def loggingConfigFile = layout.projectDirectory.file("${resources}/logging.properties")
145143
def tempDir = layout.projectDirectory.dir(testsTmpDir.toString())
146144
jvmArgumentProviders.add(

gradle/testing/randomization/policies/tests.policy

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ grant {
5252
// Needed for DirectIODirectory to retrieve block size
5353
permission java.lang.RuntimePermission "getFileStoreAttributes";
5454

55+
// Needed to load native library containing optimized dot product implementation
56+
permission java.lang.RuntimePermission "loadLibrary.dotProduct";
57+
5558
// TestLockFactoriesMultiJVM opens a random port on 127.0.0.1 (port 0 = ephemeral port range):
5659
permission java.net.SocketPermission "127.0.0.1:0", "accept,listen,resolve";
5760
// Replicator tests connect to ephemeral ports
@@ -104,7 +107,10 @@ grant codeBase "file:${gradle.worker.jar}" {
104107
};
105108

106109
grant {
107-
permission java.security.AllPermission;
110+
// Allow reading gradle worker JAR.
111+
permission java.io.FilePermission "${gradle.worker.jar}", "read";
112+
// Allow reading from classpath JARs (resources).
113+
permission java.io.FilePermission "${gradle.user.home}${/}-", "read";
108114
};
109115

110116
// Grant permissions to certain test-related JARs (https://github.com/apache/lucene/pull/13146)

lucene/benchmark-jmh/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ tasks.matching { it.name == "forbiddenApisMain" }.configureEach {
3838
])
3939
}
4040

41+
4142
// Skip certain infrastructure tasks that we can't use or don't care about.
4243
tasks.matching { it.name in [
4344
// Turn off JMH dependency checksums and licensing (it's GPL w/ classpath exception

lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/VectorUtilBenchmark.java

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
*/
1717
package org.apache.lucene.benchmark.jmh;
1818

19-
import java.lang.foreign.Arena;
20-
import java.lang.foreign.MemorySegment;
21-
import java.lang.foreign.ValueLayout;
19+
import java.lang.invoke.MethodHandle;
20+
import java.lang.invoke.MethodHandles;
21+
import java.lang.invoke.MethodType;
2222
import java.util.concurrent.ThreadLocalRandom;
2323
import java.util.concurrent.TimeUnit;
2424
import org.apache.lucene.util.VectorUtil;
@@ -52,12 +52,11 @@ static void compressBytes(byte[] raw, byte[] compressed) {
5252
private float[] floatsB;
5353
private int expectedhalfByteDotProduct;
5454

55-
private MemorySegment nativeBytesA;
55+
private Object nativeBytesA;
56+
private Object nativeBytesB;
5657

57-
private MemorySegment nativeBytesB;
58-
59-
// @Param({"1", "128", "207", "256", "300", "512", "702", "1024"})
60-
@Param({"768"})
58+
/** private Object nativeBytesA; private Object nativeBytesB; */
59+
@Param({"1", "128", "207", "256", "300", "512", "702", "1024"})
6160
int size;
6261

6362
@Setup(Level.Iteration)
@@ -92,20 +91,76 @@ public void init() {
9291
floatsA[i] = random.nextFloat();
9392
floatsB[i] = random.nextFloat();
9493
}
95-
96-
Arena offHeap = Arena.ofAuto();
97-
nativeBytesA = offHeap.allocate(size, ValueLayout.JAVA_BYTE.byteAlignment());
98-
nativeBytesB = offHeap.allocate(size, ValueLayout.JAVA_BYTE.byteAlignment());
99-
for (int i = 0; i < size; ++i) {
100-
nativeBytesA.set(ValueLayout.JAVA_BYTE, i, (byte) random.nextInt(128));
101-
nativeBytesA.set(ValueLayout.JAVA_BYTE, i, (byte) random.nextInt(128));
94+
// Java 21+ specific initialization
95+
final int runtimeVersion = Runtime.version().feature();
96+
if (runtimeVersion >= 21) {
97+
// Reflection based code to eliminate the use of Preview classes in JMH benchmarks
98+
try {
99+
final Class<?> vectorUtilSupportClass = VectorUtil.getVectorUtilSupportClass();
100+
final var className = "org.apache.lucene.internal.vectorization.PanamaVectorUtilSupport";
101+
if (vectorUtilSupportClass.getName().equals(className) == false) {
102+
nativeBytesA = null;
103+
nativeBytesB = null;
104+
} else {
105+
MethodHandles.Lookup lookup = MethodHandles.lookup();
106+
final var MemorySegment = "java.lang.foreign.MemorySegment";
107+
final var methodType =
108+
MethodType.methodType(lookup.findClass(MemorySegment), byte[].class);
109+
MethodHandle nativeMemorySegment =
110+
lookup.findStatic(vectorUtilSupportClass, "nativeMemorySegment", methodType);
111+
byte[] a = new byte[size];
112+
byte[] b = new byte[size];
113+
for (int i = 0; i < size; ++i) {
114+
a[i] = (byte) random.nextInt(128);
115+
b[i] = (byte) random.nextInt(128);
116+
}
117+
nativeBytesA = nativeMemorySegment.invoke(a);
118+
nativeBytesB = nativeMemorySegment.invoke(b);
119+
}
120+
} catch (Throwable e) {
121+
throw new RuntimeException(e);
122+
}
123+
/*
124+
Arena offHeap = Arena.ofAuto();
125+
nativeBytesA = offHeap.allocate(size, ValueLayout.JAVA_BYTE.byteAlignment());
126+
nativeBytesB = offHeap.allocate(size, ValueLayout.JAVA_BYTE.byteAlignment());
127+
for (int i = 0; i < size; ++i) {
128+
nativeBytesA.set(ValueLayout.JAVA_BYTE, i, (byte) random.nextInt(128));
129+
nativeBytesB.set(ValueLayout.JAVA_BYTE, i, (byte) random.nextInt(128));
130+
}*/
102131
}
103132
}
104133

134+
/**
135+
* High overhead (lower score) from using NATIVE_DOT_PRODUCT.invoke(nativeBytesA, nativeBytesB).
136+
* Both nativeBytesA and nativeBytesB are offHeap MemorySegments created by invoking the method
137+
* PanamaVectorUtilSupport.nativeMemorySegment(byte[]) which allocated these segments and copies
138+
* bytes from the supplied byte[] to offHeap memory. The benchmark output below shows
139+
* significantly more overhead. <b>NOTE:</b> Return type of dots8s() was set to void for the
140+
* benchmark run to avoid boxing/unboxing overhead.
141+
*
142+
* <pre>
143+
* Benchmark (size) Mode Cnt Score Error Units
144+
* VectorUtilBenchmark.dot8s 768 thrpt 15 36.406 ± 0.496 ops/us
145+
* </pre>
146+
*
147+
* Much lower overhead was observed when preview APIs were used directly in JMH benchmarking code
148+
* and exact method invocation was made as shown below <b>return (int)
149+
* VectorUtil.NATIVE_DOT_PRODUCT.invokeExact(nativeBytesA, nativeBytesB);</b>
150+
*
151+
* <pre>
152+
* Benchmark (size) Mode Cnt Score Error Units
153+
* VectorUtilBenchmark.dot8s 768 thrpt 15 43.662 ± 0.818 ops/us
154+
* </pre>
155+
*/
105156
@Benchmark
106157
@Fork(jvmArgsPrepend = {"--add-modules=jdk.incubator.vector"})
107-
public int dot8s() {
108-
return VectorUtil.dot8s(nativeBytesA, nativeBytesB, size);
158+
public void dot8s() {
159+
try {
160+
VectorUtil.NATIVE_DOT_PRODUCT.invoke(nativeBytesA, nativeBytesB);
161+
} catch (Throwable e) {
162+
throw new RuntimeException(e);
163+
}
109164
}
110165

111166
@Benchmark

lucene/core/build.gradle

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,63 +14,22 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
plugins {
18-
id "c"
19-
}
2017

2118
apply plugin: 'java-library'
22-
apply plugin: 'c'
2319

2420
description = 'Lucene core library'
25-
model {
26-
binaries {
27-
all {
28-
cCompiler.args "--shared", "-O3", "-march=native", "-funroll-loops"
29-
}
30-
}
31-
32-
toolChains {
33-
gcc(Gcc) {
34-
target("linux_aarch64") {
35-
cCompiler.executable = System.getenv("CC")
36-
}
37-
}
38-
clang(Clang) {
39-
target("osx_aarch64"){
40-
cCompiler.executable = System.getenv("CC")
41-
}
42-
}
43-
}
44-
45-
components {
46-
dotProduct(NativeLibrarySpec) {
47-
sources {
48-
c {
49-
source {
50-
srcDir 'src/c' // Path to your C source files
51-
include "**/*.c"
52-
}
53-
exportedHeaders {
54-
srcDir "src/c"
55-
include "**/*.h"
56-
}
57-
}
58-
}
59-
}
60-
}
61-
62-
}
63-
64-
test.dependsOn 'dotProductSharedLibrary'
6521

6622
dependencies {
6723
moduleTestImplementation project(':lucene:codecs')
6824
moduleTestImplementation project(':lucene:test-framework')
6925
}
7026

7127
test {
28+
build {
29+
dependsOn ':lucene:native:build'
30+
}
7231
systemProperty(
7332
"java.library.path",
74-
file("${buildDir}/libs/dotProduct/shared").absolutePath
33+
project(":lucene:native").layout.buildDirectory.get().asFile.absolutePath + "/libs/dotProduct/shared"
7534
)
7635
}

lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsFormat.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.lucene.codecs.hnsw.FlatVectorScorerUtil;
2323
import org.apache.lucene.codecs.hnsw.FlatVectorsFormat;
2424
import org.apache.lucene.codecs.hnsw.FlatVectorsReader;
25+
import org.apache.lucene.codecs.hnsw.FlatVectorsScorer;
2526
import org.apache.lucene.codecs.hnsw.FlatVectorsWriter;
2627
import org.apache.lucene.index.SegmentReadState;
2728
import org.apache.lucene.index.SegmentWriteState;
@@ -70,7 +71,8 @@ public class Lucene99ScalarQuantizedVectorsFormat extends FlatVectorsFormat {
7071

7172
final byte bits;
7273
final boolean compress;
73-
final Lucene99ScalarQuantizedVectorScorer flatVectorScorer;
74+
// final Lucene99ScalarQuantizedVectorScorer flatVectorScorer;
75+
final FlatVectorsScorer flatVectorScorer;
7476

7577
/** Constructs a format using default graph construction parameters */
7678
public Lucene99ScalarQuantizedVectorsFormat() {
@@ -117,8 +119,11 @@ public Lucene99ScalarQuantizedVectorsFormat(
117119
this.bits = (byte) bits;
118120
this.confidenceInterval = confidenceInterval;
119121
this.compress = compress;
120-
this.flatVectorScorer =
121-
new Lucene99ScalarQuantizedVectorScorer(DefaultFlatVectorScorer.INSTANCE);
122+
FlatVectorsScorer scorer = FlatVectorScorerUtil.getLucene99FlatVectorsScorer();
123+
if (scorer == DefaultFlatVectorScorer.INSTANCE) {
124+
scorer = new Lucene99ScalarQuantizedVectorScorer(DefaultFlatVectorScorer.INSTANCE);
125+
}
126+
this.flatVectorScorer = scorer;
122127
}
123128

124129
public static float calculateDefaultConfidenceInterval(int vectorDimension) {

lucene/core/src/java/org/apache/lucene/codecs/lucene99/OffHeapQuantizedByteVectorValues.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public float getScoreCorrectionConstant(int targetOrd) throws IOException {
146146
}
147147
slice.seek(((long) targetOrd * byteSize) + numBytes);
148148
slice.readFloats(scoreCorrectionConstant, 0, 1);
149+
lastOrd = targetOrd;
149150
return scoreCorrectionConstant[0];
150151
}
151152

lucene/core/src/java/org/apache/lucene/util/Constants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ private static boolean is64Bit() {
100100
/** true iff we know VFMA has faster throughput than separate vmul/vadd. */
101101
public static final boolean HAS_FAST_VECTOR_FMA = hasFastVectorFMA();
102102

103+
// TODO: <below condition> && Boolean.parseBoolean(getSysProp("lucene.useNativeDotProduct",
104+
// "False")
105+
public static final boolean NATIVE_DOT_PRODUCT_ENABLED = OS_ARCH.equalsIgnoreCase("aarch64");
106+
103107
/** true iff we know FMA has faster throughput than separate mul/add. */
104108
public static final boolean HAS_FAST_SCALAR_FMA = hasFastScalarFMA();
105109

lucene/core/src/java21/org/apache/lucene/internal/vectorization/Lucene99MemorySegmentByteVectorScorer.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
package org.apache.lucene.internal.vectorization;
1818

1919
import java.io.IOException;
20+
import java.lang.foreign.Arena;
2021
import java.lang.foreign.MemorySegment;
22+
import java.lang.foreign.ValueLayout;
2123
import java.util.Optional;
2224
import org.apache.lucene.index.ByteVectorValues;
2325
import org.apache.lucene.index.KnnVectorValues;
2426
import org.apache.lucene.index.VectorSimilarityFunction;
2527
import org.apache.lucene.store.FilterIndexInput;
2628
import org.apache.lucene.store.IndexInput;
2729
import org.apache.lucene.store.MemorySegmentAccessInput;
30+
import org.apache.lucene.util.Constants;
2831
import org.apache.lucene.util.hnsw.RandomVectorScorer;
2932

3033
abstract sealed class Lucene99MemorySegmentByteVectorScorer
@@ -34,6 +37,8 @@ abstract sealed class Lucene99MemorySegmentByteVectorScorer
3437
final MemorySegmentAccessInput input;
3538
final MemorySegment query;
3639
byte[] scratch;
40+
MemorySegment offHeapScratch;
41+
MemorySegment offHeapQuery;
3742

3843
/**
3944
* Return an optional whose value, if present, is the scorer. Otherwise, an empty optional is
@@ -49,7 +54,10 @@ public static Optional<Lucene99MemorySegmentByteVectorScorer> create(
4954
checkInvariants(values.size(), values.getVectorByteLength(), input);
5055
return switch (type) {
5156
case COSINE -> Optional.of(new CosineScorer(msInput, values, queryVector));
52-
case DOT_PRODUCT -> Optional.of(new DotProductScorer(msInput, values, queryVector));
57+
case DOT_PRODUCT ->
58+
Constants.NATIVE_DOT_PRODUCT_ENABLED == false
59+
? Optional.of(new DotProductScorer(msInput, values, queryVector))
60+
: Optional.of(new NativeDotProductScorer(msInput, values, queryVector));
5361
case EUCLIDEAN -> Optional.of(new EuclideanScorer(msInput, values, queryVector));
5462
case MAXIMUM_INNER_PRODUCT ->
5563
Optional.of(new MaxInnerProductScorer(msInput, values, queryVector));
@@ -64,6 +72,20 @@ public static Optional<Lucene99MemorySegmentByteVectorScorer> create(
6472
this.query = MemorySegment.ofArray(queryVector);
6573
}
6674

75+
final MemorySegment getNativeSegment(int ord) throws IOException {
76+
long byteOffset = (long) ord * vectorByteSize;
77+
MemorySegment seg = input.segmentSliceOrNull(byteOffset, vectorByteSize);
78+
if (seg == null) {
79+
if (offHeapScratch == null) {
80+
offHeapScratch =
81+
Arena.ofAuto().allocate(vectorByteSize, ValueLayout.JAVA_BYTE.byteAlignment());
82+
}
83+
input.readBytes(byteOffset, offHeapScratch, 0, vectorByteSize);
84+
seg = offHeapScratch;
85+
}
86+
return seg;
87+
}
88+
6789
final MemorySegment getSegment(int ord) throws IOException {
6890
checkOrdinal(ord);
6991
long byteOffset = (long) ord * vectorByteSize;
@@ -103,6 +125,27 @@ public float score(int node) throws IOException {
103125
}
104126
}
105127

128+
static final class NativeDotProductScorer extends Lucene99MemorySegmentByteVectorScorer {
129+
130+
NativeDotProductScorer(
131+
MemorySegmentAccessInput input, KnnVectorValues values, byte[] queryVector) {
132+
super(input, values, queryVector);
133+
if (offHeapQuery == null) {
134+
offHeapQuery =
135+
Arena.ofAuto().allocate(vectorByteSize, ValueLayout.JAVA_BYTE.byteAlignment());
136+
}
137+
offHeapQuery.copyFrom(query);
138+
}
139+
140+
@Override
141+
public float score(int node) throws IOException {
142+
checkOrdinal(node);
143+
// divide by 2 * 2^14 (maximum absolute value of product of 2 signed bytes) * len
144+
int raw = PanamaVectorUtilSupport.nativeDotProduct(offHeapQuery, getNativeSegment(node));
145+
return 0.5f + raw / (float) (query.byteSize() * (1 << 15));
146+
}
147+
}
148+
106149
static final class DotProductScorer extends Lucene99MemorySegmentByteVectorScorer {
107150
DotProductScorer(MemorySegmentAccessInput input, KnnVectorValues values, byte[] query) {
108151
super(input, values, query);

0 commit comments

Comments
 (0)