This is an automated email from the ASF dual-hosted git repository.
mawiesne pushed a commit to branch opennlp-2.x
in repository https://gitbox.apache.org/repos/asf/opennlp.git
The following commit(s) were added to refs/heads/opennlp-2.x by this push:
new 078377921 [2.x]: GH-1390: Let SentenceVectorsDL run on a GPU (#1496)
078377921 is described below
commit 078377921e0220613a085e636bf9bd6a34b89518
Author: Richard Zowalla <[email protected]>
AuthorDate: Sun Oct 18 17:06:24 2026 +0200
[2.x]: GH-1390: Let SentenceVectorsDL run on a GPU (#1496)
---
.gitignore | 2 +
opennlp-dl-gpu/pom.xml | 31 +++++++
.../java/opennlp/dl/vectors/SentenceVectorsDL.java | 25 +++++-
.../SentenceVectorsDLInferenceOptionsTest.java | 90 +++++++++++++++++++++
.../opennlp/dl/vectors/gen_tiny_vectors_model.py | 57 +++++++++++++
.../resources/opennlp/dl/vectors/tiny-vectors.onnx | Bin 0 -> 373 bytes
rat-excludes | 3 +
7 files changed, 207 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 965000f40..e976fdc77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,6 @@ nb-configuration.xml
*.DS_Store
.checkstyle
*.onnx
+# Committed test fixture, generated once by the script beside it, not build
output.
+!opennlp-dl/src/test/resources/opennlp/dl/vectors/tiny-vectors.onnx
vocab.txt
diff --git a/opennlp-dl-gpu/pom.xml b/opennlp-dl-gpu/pom.xml
index e66f0f62b..e617868b7 100644
--- a/opennlp-dl-gpu/pom.xml
+++ b/opennlp-dl-gpu/pom.xml
@@ -69,6 +69,13 @@
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
+ <!-- Keep the CPU onnxruntime off the test classpath; this module uses
onnxruntime_gpu. -->
+ <exclusions>
+ <exclusion>
+ <groupId>com.microsoft.onnxruntime</groupId>
+ <artifactId>onnxruntime</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
</dependencies>
@@ -86,7 +93,31 @@
</plugins>
</build>
+ <!-- onnxruntime_gpu ships native libraries for linux-x64 and win-x64 only,
unlike the
+ CPU onnxruntime jar, which also covers macOS and Linux aarch64. The
tests cannot run there. -->
<profiles>
+ <profile>
+ <id>no-onnxruntime-gpu-on-mac</id>
+ <activation>
+ <os>
+ <family>mac</family>
+ </os>
+ </activation>
+ <properties>
+ <skipTests>true</skipTests>
+ </properties>
+ </profile>
+ <profile>
+ <id>no-onnxruntime-gpu-on-aarch64</id>
+ <activation>
+ <os>
+ <arch>aarch64</arch>
+ </os>
+ </activation>
+ <properties>
+ <skipTests>true</skipTests>
+ </properties>
+ </profile>
<profile>
<id>eval-tests</id>
<properties>
diff --git a/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
b/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
index 06ba5cbd6..93658b8d6 100644
--- a/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
+++ b/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
@@ -30,6 +30,7 @@ import ai.onnxruntime.OrtException;
import ai.onnxruntime.OrtSession;
import opennlp.dl.AbstractDL;
+import opennlp.dl.InferenceOptions;
import opennlp.dl.Tokens;
import opennlp.tools.tokenize.Tokenizer;
@@ -62,9 +63,31 @@ public class SentenceVectorsDL extends AbstractDL {
*/
public SentenceVectorsDL(final File model, final File vocabulary)
throws OrtException, IOException {
+ this(model, vocabulary, new InferenceOptions());
+ }
+
+ /**
+ * Instantiates a {@link SentenceVectorsDL sentence vector generator} using
ONNX models.
+ * With {@link InferenceOptions#isGpu()} set, the model runs on the CUDA
device
+ * {@link InferenceOptions#getGpuDeviceId()}; the other options are not used.
+ *
+ * @param model The file name of a sentence vectors ONNX model.
+ * @param vocabulary The file name of the vocabulary file for the model.
+ * @param inferenceOptions {@link InferenceOptions} to control the inference.
+ *
+ * @throws OrtException Thrown if the {@code model} cannot be loaded, or if
a GPU is requested
+ * and the CUDA execution provider or the device is not available.
+ * @throws IOException Thrown if errors occurred loading the {@code model}
or {@code vocabulary}.
+ */
+ public SentenceVectorsDL(final File model, final File vocabulary,
+ final InferenceOptions inferenceOptions) throws OrtException,
IOException {
env = OrtEnvironment.getEnvironment();
- session = env.createSession(model.getPath(), new
OrtSession.SessionOptions());
+ final OrtSession.SessionOptions sessionOptions = new
OrtSession.SessionOptions();
+ if (inferenceOptions.isGpu()) {
+ sessionOptions.addCUDA(inferenceOptions.getGpuDeviceId());
+ }
+ session = env.createSession(model.getPath(), sessionOptions);
vocab = loadVocab(vocabulary);
tokenizer = createTokenizer(vocab);
diff --git
a/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLInferenceOptionsTest.java
b/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLInferenceOptionsTest.java
new file mode 100644
index 000000000..44928fa31
--- /dev/null
+++
b/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLInferenceOptionsTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 opennlp.dl.vectors;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import ai.onnxruntime.OrtException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import opennlp.dl.InferenceOptions;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests the {@link InferenceOptions} constructor of {@link SentenceVectorsDL}
with
+ * {@code tiny-vectors.onnx} (see {@code gen_tiny_vectors_model.py}), whose
output vector of a
+ * token is its vocabulary id times {@code [0.5, -1, 2]}.
+ */
+class SentenceVectorsDLInferenceOptionsTest {
+
+ /** A CUDA device id that no machine has. */
+ private static final int UNUSABLE_DEVICE_ID = 99;
+
+ private static final String SENTENCE = "the dog";
+
+ @TempDir
+ Path dir;
+
+ /** Without a GPU request the options constructor computes the vectors of
the default one. */
+ @Test
+ void testCpuOptionsMatchTheDefaultConstructor() throws IOException,
OrtException {
+ final File model = copy("/opennlp/dl/vectors/tiny-vectors.onnx",
"tiny-vectors.onnx");
+ final File vocab = copy("/opennlp/dl/vocab-plain.txt", "vocab.txt");
+ try (SentenceVectorsDL defaults = new SentenceVectorsDL(model, vocab);
+ SentenceVectorsDL options = new SentenceVectorsDL(model, vocab, new
InferenceOptions())) {
+ final float[] expected = defaults.getVectors(SENTENCE);
+ assertEquals(3, expected.length);
+ assertArrayEquals(expected, options.getVectors(SENTENCE));
+ }
+ }
+
+ /** A GPU request fails when CUDA or the device is not available instead of
using the CPU. */
+ @Test
+ void testGpuRequestOnAnUnusableDeviceFails() throws IOException {
+ final File model = copy("/opennlp/dl/vectors/tiny-vectors.onnx",
"tiny-vectors.onnx");
+ final File vocab = copy("/opennlp/dl/vocab-plain.txt", "vocab.txt");
+ final InferenceOptions options = new InferenceOptions();
+ options.setGpu(true);
+ options.setGpuDeviceId(UNUSABLE_DEVICE_ID);
+ assertThrows(OrtException.class, () -> new SentenceVectorsDL(model, vocab,
options).close());
+ }
+
+ /**
+ * Copies a test resource into the temporary directory.
+ *
+ * @param resource The classpath resource.
+ * @param name The file name in the temporary directory.
+ * @return The copied file.
+ * @throws IOException Thrown if the resource cannot be read or written.
+ */
+ private File copy(final String resource, final String name) throws
IOException {
+ final Path target = dir.resolve(name);
+ try (InputStream in = getClass().getResourceAsStream(resource)) {
+ Files.copy(in, target);
+ }
+ return target.toFile();
+ }
+}
diff --git
a/opennlp-dl/src/test/resources/opennlp/dl/vectors/gen_tiny_vectors_model.py
b/opennlp-dl/src/test/resources/opennlp/dl/vectors/gen_tiny_vectors_model.py
new file mode 100644
index 000000000..d2d79b4ad
--- /dev/null
+++ b/opennlp-dl/src/test/resources/opennlp/dl/vectors/gen_tiny_vectors_model.py
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# Generates tiny-vectors.onnx, the deterministic model behind
SentenceVectorsDLInferenceOptionsTest.
+#
+# The graph computes output[b][t][d] = float(input_ids[b][t]) * W[0][d] with
+# W = [[0.5, -1.0, 2.0]], so the vector at any token position is that token's
vocabulary id
+# times W, hand-computable in the test. It declares the same three inputs a
BERT-style
+# encoder declares (input_ids, attention_mask, token_type_ids; the latter two
are accepted
+# and ignored) and one output of shape [batch, tokens, 3] so the hidden
dimension is static
+# in the model metadata.
+#
+# Regenerate with: python3 gen_tiny_vectors_model.py (requires the onnx
package)
+
+import numpy as np
+import onnx
+from onnx import TensorProto, helper, numpy_helper
+
+W = np.array([[0.5, -1.0, 2.0]], dtype=np.float32)
+
+cast = helper.make_node("Cast", ["input_ids"], ["ids_float"],
to=TensorProto.FLOAT)
+unsqueeze = helper.make_node("Unsqueeze", ["ids_float", "axes"], ["ids_3d"])
+matmul = helper.make_node("MatMul", ["ids_3d", "w"], ["last_hidden_state"])
+
+
+def encoder_input(name):
+ return helper.make_tensor_value_info(name, TensorProto.INT64, ["batch",
"tokens"])
+
+
+graph = helper.make_graph(
+ [cast, unsqueeze, matmul],
+ "tiny-vectors",
+ [encoder_input("input_ids"), encoder_input("attention_mask"),
+ encoder_input("token_type_ids")],
+ [helper.make_tensor_value_info(
+ "last_hidden_state", TensorProto.FLOAT, ["batch", "tokens", 3])],
+ [numpy_helper.from_array(np.array([2], dtype=np.int64), name="axes"),
+ numpy_helper.from_array(W, name="w")],
+)
+
+model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
+model.ir_version = 8
+onnx.checker.check_model(model)
+onnx.save(model, "tiny-vectors.onnx")
+print("wrote tiny-vectors.onnx,", len(model.SerializeToString()), "bytes")
diff --git a/opennlp-dl/src/test/resources/opennlp/dl/vectors/tiny-vectors.onnx
b/opennlp-dl/src/test/resources/opennlp/dl/vectors/tiny-vectors.onnx
new file mode 100644
index 000000000..7d63c9132
Binary files /dev/null and
b/opennlp-dl/src/test/resources/opennlp/dl/vectors/tiny-vectors.onnx differ
diff --git a/rat-excludes b/rat-excludes
index 3ad3ae4df..ed5b4bd2d 100644
--- a/rat-excludes
+++ b/rat-excludes
@@ -58,3 +58,6 @@ src/test/resources/*.info
<!-- other licence -->
src/main/java/opennlp/tools/stemmer/snowball/*.java
+
+<!-- opennlp-dl tiny ONNX test model (project-generated binary, see
gen_tiny_vectors_model.py next to it) -->
+src/test/resources/opennlp/dl/vectors/tiny-vectors.onnx