This is an automated email from the ASF dual-hosted git repository.
mawiesne pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opennlp.git
The following commit(s) were added to refs/heads/main by this push:
new 95e990636 GH-1391: Cap the token positions of one
SentenceVectorsDL.embedAll inference (#1468)
95e990636 is described below
commit 95e9906364baba21625234dc30f198c7140bbc0c
Author: Kristian Rickert <[email protected]>
AuthorDate: Wed Oct 21 15:24:24 2026 -0400
GH-1391: Cap the token positions of one SentenceVectorsDL.embedAll
inference (#1468)
TextEmbedder.embedAll and the manual say the implementation forms its
own batches, so a caller may pass a list of any size. SentenceVectorsDL
grouped the texts by tokenized length and ran each group as one
inference, however many texts it had. Each group is now split so that no
inference takes more than 16384 token positions (rows times row length),
32 rows at the BERT length of 512. An input longer than that runs in a
batch of its own. Groups run in the
order their length first appears in the call.
Vectors do not change: batches are not padded, so every row is computed
from the same tensors as its single-input call.
The grouping moved into a package-private batches method so tests can
check the inference plan. SentenceVectorsDLBatchLimitTest covers row
count boundaries (exact fit, one over, one under, lengths that do and do
not divide the cap), a length at and over the cap, an empty call, a mix
where only one group splits, input order with alternating lengths, CLS
and mean pooling, the pooled-output model and unit length scaling
through a split group. The GPU parity test also runs a split group, and
the tiny model texts and expected vectors are shared in TinyVectorModels.
Signed-off-by: Kristian Rickert <[email protected]>
Co-authored-by: Claude Code 2.1.296 (Opus 5.5) <[email protected]>
---
.../java/opennlp/dl/vectors/SentenceVectorsDL.java | 49 ++++-
.../vectors/SentenceVectorsDLBatchLimitTest.java | 240 +++++++++++++++++++++
.../dl/vectors/SentenceVectorsDLEmbedderTest.java | 17 +-
.../SentenceVectorsDLInferenceOptionsTest.java | 20 +-
.../java/opennlp/dl/vectors/TinyVectorModels.java | 40 ++++
opennlp-docs/src/docbkx/machine-learning.xml | 4 +-
6 files changed, 343 insertions(+), 27 deletions(-)
diff --git
a/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
b/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
index 08e114d83..fa4902fe3 100644
---
a/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
+++
b/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java
@@ -23,6 +23,7 @@ import java.io.UncheckedIOException;
import java.nio.LongBuffer;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -74,7 +75,8 @@ import opennlp.tools.util.ParamChecks;
* {@code close()} with inference methods.</p>
*
* <p>{@link #embedAll(List)} runs one batched session per distinct tokenized
length, so a batch
- * of same-length inputs costs one inference instead of one per input.</p>
+ * of same-length inputs costs one inference instead of one per input. A group
larger than
+ * 16384 token positions is split into several inferences.</p>
*/
@ThreadSafe
public class SentenceVectorsDL extends AbstractDL implements TextEmbedder {
@@ -82,6 +84,15 @@ public class SentenceVectorsDL extends AbstractDL implements
TextEmbedder {
/** The maximum number of tokens per input if none is given, the BERT
position limit. */
public static final int DEFAULT_MAX_LENGTH = 512;
+ /**
+ * The upper bound on the token positions of one inference, its row count
times its row length.
+ * {@link #embedAll(List)} splits a larger group into consecutive
inferences, so the tensors of
+ * one inference stay bounded however many texts a call has. A single input
longer than the
+ * bound still runs on its own. The bound is 32 rows at the BERT length of
+ * {@value #DEFAULT_MAX_LENGTH}.
+ */
+ static final int MAX_BATCH_TOKEN_POSITIONS = 16384;
+
private static final String SENTENCE_EMBEDDING = "sentence_embedding";
private static final int POOLED_RANK = 2;
private static final int TOKEN_RANK = 3;
@@ -268,9 +279,9 @@ public class SentenceVectorsDL extends AbstractDL
implements TextEmbedder {
/**
* {@inheritDoc}
*
- * <p>The inputs are tokenized up front, grouped by tokenized length, and
each group runs
- * through the session once with shape {@code [group size, length]}. A batch
never pads, so
- * every row is computed from the tensors its single-input call would have
used.</p>
+ * <p>The inputs are grouped by tokenized length, never padded, and each
group runs in
+ * inferences of at most 16384 token positions. Every row is computed from
the tensors its
+ * single-input call would have used.</p>
*/
@Override
public float[][] embedAll(final List<? extends CharSequence> texts) {
@@ -282,15 +293,13 @@ public class SentenceVectorsDL extends AbstractDL
implements TextEmbedder {
throw new IllegalArgumentException("texts[" + i + "] must not be
null");
}
}
- final Map<Integer, List<Integer>> byLength = new HashMap<>();
final Tokens[] encoded = new Tokens[checked.length];
for (int i = 0; i < checked.length; i++) {
encoded[i] = encode(checked[i]);
- byLength.computeIfAbsent(encoded[i].ids().length, length -> new
ArrayList<>()).add(i);
}
final float[][] vectors = new float[checked.length][];
try {
- for (final List<Integer> group : byLength.values()) {
+ for (final List<Integer> group : batches(encoded)) {
final Tokens[] batch = new Tokens[group.size()];
for (int b = 0; b < batch.length; b++) {
batch[b] = encoded[group.get(b)];
@@ -306,6 +315,32 @@ public class SentenceVectorsDL extends AbstractDL
implements TextEmbedder {
return vectors;
}
+ /**
+ * Splits the indices of a call into the batches that run as single
inferences. A batch contains
+ * indices of one tokenized length, in call order, and no more rows than fit
in
+ * {@value #MAX_BATCH_TOKEN_POSITIONS} token positions, but always at least
one, so a row
+ * wider than the bound runs alone.
+ *
+ * @param encoded The encodings of the call, in call order. An encoding
contains at least
+ * the start and end tokens, so no length is zero.
+ * @return The batches, each a list of indices into {@code encoded}.
+ */
+ static List<List<Integer>> batches(final Tokens[] encoded) {
+ final Map<Integer, List<Integer>> byLength = new LinkedHashMap<>();
+ for (int i = 0; i < encoded.length; i++) {
+ byLength.computeIfAbsent(encoded[i].ids().length, length -> new
ArrayList<>()).add(i);
+ }
+ final List<List<Integer>> batches = new ArrayList<>();
+ for (final Map.Entry<Integer, List<Integer>> group : byLength.entrySet()) {
+ final int rows = Math.max(1, MAX_BATCH_TOKEN_POSITIONS / group.getKey());
+ final List<Integer> indices = group.getValue();
+ for (int from = 0; from < indices.size(); from += rows) {
+ batches.add(indices.subList(from, Math.min(from + rows,
indices.size())));
+ }
+ }
+ return batches;
+ }
+
/**
* {@inheritDoc}
*
diff --git
a/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLBatchLimitTest.java
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLBatchLimitTest.java
new file mode 100644
index 000000000..e55fc0e89
--- /dev/null
+++
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLBatchLimitTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import opennlp.dl.Tokens;
+
+import static opennlp.dl.vectors.TinyVectorModels.CLS_VECTOR;
+import static opennlp.dl.vectors.TinyVectorModels.DELTA;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_MEAN;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_SUM;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_WORLD;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_WORLD_MEAN;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_WORLD_SUM;
+import static opennlp.dl.vectors.TinyVectorModels.TINY_POOLED;
+import static opennlp.dl.vectors.TinyVectorModels.TINY_VECTORS;
+import static opennlp.dl.vectors.TinyVectorModels.UNIT_VECTOR;
+import static opennlp.dl.vectors.TinyVectorModels.alternating;
+import static opennlp.dl.vectors.TinyVectorModels.model;
+import static opennlp.dl.vectors.TinyVectorModels.vocab;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests the split of {@link SentenceVectorsDL#embedAll(List)} into inferences
of at most
+ * {@value SentenceVectorsDL#MAX_BATCH_TOKEN_POSITIONS} token positions: the
boundaries of the
+ * batches, and the vectors that come back through a split group. "hello"
encodes to 3 tokens and
+ * "hello world" to 4.
+ */
+class SentenceVectorsDLBatchLimitTest {
+
+ private static final int LIMIT = SentenceVectorsDL.MAX_BATCH_TOKEN_POSITIONS;
+
+ /**
+ * {@return a session on the CPU over the given tiny model}
+ *
+ * @param dir The directory to copy the model into.
+ * @param model {@link TinyVectorModels#TINY_VECTORS} or {@link
TinyVectorModels#TINY_POOLED}.
+ * @param pooling The pooling.
+ * @param normalize {@code true} to scale every vector to unit length.
+ * @param maxLength The maximum number of tokens per input.
+ */
+ private static SentenceVectorsDL vectors(final Path dir, final String model,
+ final Pooling pooling, final boolean normalize, final int maxLength)
throws Exception {
+ return new SentenceVectorsDL(model(dir, model), vocab(dir), true, pooling,
normalize,
+ maxLength);
+ }
+
+ /**
+ * {@return an encoding of {@code length} tokens; only its length matters
for batching}
+ *
+ * @param length The number of tokens.
+ */
+ private static Tokens tokens(final int length) {
+ return new Tokens(new String[length], new long[length], new long[length],
new long[length]);
+ }
+
+ /**
+ * {@return the sizes of the batches of {@code count} encodings of {@code
length} tokens}
+ *
+ * @param length The number of tokens of every encoding.
+ * @param count The number of encodings.
+ */
+ private static List<Integer> sizes(final int length, final int count) {
+ final Tokens[] encoded = Collections.nCopies(count,
tokens(length)).toArray(new Tokens[0]);
+ final List<Integer> sizes = new ArrayList<>();
+ for (final List<Integer> batch : SentenceVectorsDL.batches(encoded)) {
+ sizes.add(batch.size());
+ }
+ return sizes;
+ }
+
+ /**
+ * {@return the length, the count and the expected batch sizes at the edges
of the row count}
+ */
+ static Stream<Arguments> rowCountBoundaries() {
+ final int rows4 = LIMIT / 4;
+ final int rows2 = LIMIT / 2;
+ final int rows3 = LIMIT / 3;
+ return Stream.of(
+ Arguments.of(4, rows4, List.of(rows4)),
+ Arguments.of(4, rows4 + 1, List.of(rows4, 1)),
+ Arguments.of(4, rows4 - 1, List.of(rows4 - 1)),
+ Arguments.of(2, rows2, List.of(rows2)),
+ Arguments.of(2, rows2 + 1, List.of(rows2, 1)),
+ Arguments.of(3, rows3, List.of(rows3)),
+ Arguments.of(3, rows3 + 1, List.of(rows3, 1)),
+ Arguments.of(3, 2 * rows3, List.of(rows3, rows3)),
+ Arguments.of(3, 2 * rows3 + 1, List.of(rows3, rows3, 1)),
+ Arguments.of(1, LIMIT, List.of(LIMIT)),
+ Arguments.of(1, LIMIT + 1, List.of(LIMIT, 1)));
+ }
+
+ /**
+ * Checks a group that fills one inference exactly, one row more, one row
fewer, and lengths
+ * that do or do not divide the cap evenly.
+ */
+ @ParameterizedTest
+ @MethodSource("rowCountBoundaries")
+ void testRowCountBoundaries(final int length, final int count, final
List<Integer> expected) {
+ assertEquals(expected, sizes(length, count));
+ }
+
+ /** An input as long as the cap, or longer, runs in a batch of its own. */
+ @ParameterizedTest
+ @ValueSource(ints = {LIMIT, LIMIT + 1})
+ void testLengthAtAndOverTheLimitRunsOneRowPerBatch(final int length) {
+ assertEquals(List.of(1, 1, 1), sizes(length, 3));
+ }
+
+ /** An empty call has no batches. */
+ @Test
+ void testEmptyCallHasNoBatches() {
+ assertTrue(SentenceVectorsDL.batches(new Tokens[0]).isEmpty());
+ }
+
+ /** Inputs of different lengths run one batch per length, in the order each
length appears. */
+ @Test
+ void testGroupsByLengthInCallOrder() {
+ assertEquals(List.of(List.of(0, 3), List.of(1, 2, 4)),
SentenceVectorsDL.batches(
+ new Tokens[] {tokens(4), tokens(3), tokens(3), tokens(4), tokens(3)}));
+ }
+
+ /**
+ * Checks that only the group that is too large is split: the 4-token inputs
need two
+ * inferences, the 3-token inputs between them stay in one.
+ */
+ @Test
+ void testOnlyTheLargeGroupIsSplit() {
+ final Tokens[] encoded = new Tokens[LIMIT / 4 + 3];
+ Arrays.fill(encoded, tokens(4));
+ encoded[1] = tokens(3);
+ encoded[2] = tokens(3);
+ final List<List<Integer>> batches = SentenceVectorsDL.batches(encoded);
+ assertEquals(3, batches.size());
+ assertEquals(LIMIT / 4, batches.get(0).size());
+ assertEquals(List.of(0, 3), batches.get(0).subList(0, 2));
+ assertEquals(1, batches.get(1).size());
+ assertEquals(List.of(1, 2), batches.get(2));
+ }
+
+ /**
+ * Runs the same mix through the session: one group splits, the other does
not. The rows at
+ * the start and end of each inference, and the two short texts, equal the
vector of their own
+ * text.
+ */
+ @Test
+ void testMixedGroupsThroughTheSession(@TempDir final Path dir) throws
Exception {
+ final List<String> texts = new ArrayList<>(Collections.nCopies(LIMIT / 4 +
1, HELLO_WORLD));
+ texts.add(1, HELLO);
+ texts.add(7, HELLO);
+ try (SentenceVectorsDL v = vectors(dir, TINY_VECTORS, Pooling.MEAN, false,
+ SentenceVectorsDL.DEFAULT_MAX_LENGTH)) {
+ final float[][] batch = v.embedAll(texts);
+ assertEquals(texts.size(), batch.length);
+ // The first inference ends with the (LIMIT / 4)-th "hello world", at
index LIMIT / 4 + 1.
+ final int lastOfFirst = LIMIT / 4 + 1;
+ for (final int i : new int[] {0, 1, 2, 7, lastOfFirst, lastOfFirst + 1,
texts.size() - 1}) {
+ assertArrayEquals(v.embed(texts.get(i)), batch[i], DELTA, "row " + i);
+ }
+ }
+ }
+
+ /** An input as wide as the cap runs next to short inputs and comes back in
call order. */
+ @Test
+ void testInputAtTheLimitThroughTheSession(@TempDir final Path dir) throws
Exception {
+ final String wide = String.join(" ", Collections.nCopies(LIMIT - 2,
HELLO));
+ try (SentenceVectorsDL v = vectors(dir, TINY_VECTORS, Pooling.MEAN, false,
LIMIT)) {
+ final List<String> texts = List.of(HELLO, wide, HELLO_WORLD, wide);
+ final float[][] batch = v.embedAll(texts);
+ for (int i = 0; i < texts.size(); i++) {
+ assertArrayEquals(v.embed(texts.get(i)), batch[i], DELTA, "row " + i);
+ }
+ }
+ }
+
+ /**
+ * {@return the model, pooling, unit length, and the expected vectors for
"hello world" and
+ * "hello"}
+ */
+ static Stream<Arguments> pathCases() {
+ return Stream.of(
+ Arguments.of(TINY_VECTORS, Pooling.MEAN, false, HELLO_WORLD_MEAN,
HELLO_MEAN),
+ Arguments.of(TINY_VECTORS, Pooling.CLS, false, CLS_VECTOR, CLS_VECTOR),
+ Arguments.of(TINY_VECTORS, Pooling.CLS, true, UNIT_VECTOR,
UNIT_VECTOR),
+ Arguments.of(TINY_VECTORS, Pooling.MEAN, true, UNIT_VECTOR,
UNIT_VECTOR),
+ Arguments.of(TINY_POOLED, Pooling.CLS, false, HELLO_WORLD_SUM,
HELLO_SUM),
+ Arguments.of(TINY_POOLED, Pooling.MEAN, true, UNIT_VECTOR,
UNIT_VECTOR));
+ }
+
+ /**
+ * Checks every path after the inference through split groups of alternating
lengths, with the
+ * results in input order.
+ */
+ @ParameterizedTest
+ @MethodSource("pathCases")
+ void testPathsThroughASplitGroup(final String model, final Pooling pooling,
+ final boolean normalize, final float[] helloWorld, final float[] hello,
+ @TempDir final Path dir) throws Exception {
+ final List<String> texts = alternating(LIMIT / 3 + 1);
+ try (SentenceVectorsDL v = vectors(dir, model, pooling, normalize,
+ SentenceVectorsDL.DEFAULT_MAX_LENGTH)) {
+ final float[][] batch = v.embedAll(texts);
+ assertEquals(texts.size(), batch.length);
+ for (int i = 0; i < batch.length; i++) {
+ assertArrayEquals(i % 2 == 0 ? helloWorld : hello, batch[i], DELTA,
"row " + i);
+ }
+ }
+ }
+}
diff --git
a/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLEmbedderTest.java
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLEmbedderTest.java
index 01d2fe470..6d3356330 100644
---
a/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLEmbedderTest.java
+++
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLEmbedderTest.java
@@ -39,9 +39,14 @@ import static
opennlp.dl.vectors.OnnxTextEmbedderProvider.MAX_LENGTH_OPTION;
import static opennlp.dl.vectors.OnnxTextEmbedderProvider.NORMALIZE_OPTION;
import static opennlp.dl.vectors.OnnxTextEmbedderProvider.POOLING_OPTION;
import static opennlp.dl.vectors.OnnxTextEmbedderProvider.VOCABULARY_OPTION;
+import static opennlp.dl.vectors.TinyVectorModels.CLS_VECTOR;
import static opennlp.dl.vectors.TinyVectorModels.DELTA;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_MEAN;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_WORLD_MEAN;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_WORLD_SUM;
import static opennlp.dl.vectors.TinyVectorModels.TINY_POOLED;
import static opennlp.dl.vectors.TinyVectorModels.TINY_VECTORS;
+import static opennlp.dl.vectors.TinyVectorModels.UNIT_VECTOR;
import static opennlp.dl.vectors.TinyVectorModels.model;
import static opennlp.dl.vectors.TinyVectorModels.scale;
import static opennlp.dl.vectors.TinyVectorModels.vocab;
@@ -63,18 +68,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
class SentenceVectorsDLEmbedderTest {
- // W / |W|, the unit-length vector of every input of the tiny models.
- private static final float[] UNIT_VECTOR = scale(1 / (float)
Math.sqrt(5.25));
-
- // "hello world" = [CLS]=7 hello=4 world=5 [SEP]=3
- private static final float[] HELLO_WORLD_MEAN = scale(19 / 4f);
- private static final float[] HELLO_WORLD_SUM = scale(19);
-
- // "hello" = [CLS]=7 hello=4 [SEP]=3
- private static final float[] HELLO_MEAN = scale(14 / 3f);
-
- private static final float[] CLS_VECTOR = scale(7);
-
// The vocabulary above as a tokenizer.json of an uncased WordPiece model.
private static File tokenizerJson(final Path dir, final boolean lowercase)
throws IOException {
final Path file = dir.resolve("tokenizer.json");
diff --git
a/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLInferenceOptionsTest.java
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLInferenceOptionsTest.java
index da8cb3a70..a19b4fc88 100644
---
a/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLInferenceOptionsTest.java
+++
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLInferenceOptionsTest.java
@@ -35,8 +35,10 @@ import opennlp.dl.InferenceOptions;
import static opennlp.dl.OnnxTestSupport.UNUSABLE_DEVICE_ID;
import static opennlp.dl.OnnxTestSupport.gpu;
import static opennlp.dl.vectors.TinyVectorModels.DELTA;
+import static opennlp.dl.vectors.TinyVectorModels.HELLO_WORLD;
import static opennlp.dl.vectors.TinyVectorModels.TINY_POOLED;
import static opennlp.dl.vectors.TinyVectorModels.TINY_VECTORS;
+import static opennlp.dl.vectors.TinyVectorModels.alternating;
import static opennlp.dl.vectors.TinyVectorModels.gpuOrAbort;
import static opennlp.dl.vectors.TinyVectorModels.model;
import static opennlp.dl.vectors.TinyVectorModels.scale;
@@ -50,10 +52,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
*/
class SentenceVectorsDLInferenceOptionsTest {
- private static final String HELLO = "hello";
-
- private static final String TEXT = "hello world";
-
private static final String LONGER_TEXT = "world hello world";
/**
@@ -83,13 +81,21 @@ class SentenceVectorsDLInferenceOptionsTest {
SentenceVectorsDL cpu = new SentenceVectorsDL(model, vocab, true,
pooling, normalize,
SentenceVectorsDL.DEFAULT_MAX_LENGTH, new InferenceOptions())) {
assertEquals(cpu.dimension(), gpu.dimension());
- assertArrayEquals(cpu.embed(TEXT), gpu.embed(TEXT), DELTA);
- final List<String> texts = List.of(TEXT, LONGER_TEXT, "");
+ assertArrayEquals(cpu.embed(HELLO_WORLD), gpu.embed(HELLO_WORLD), DELTA);
+ final List<String> texts = List.of(HELLO_WORLD, LONGER_TEXT, "");
final float[][] cpuBatch = cpu.embedAll(texts);
final float[][] gpuBatch = gpu.embedAll(texts);
for (int i = 0; i < cpuBatch.length; i++) {
assertArrayEquals(cpuBatch[i], gpuBatch[i], DELTA, "row " + i);
}
+ // Both length groups exceed the token position cap and are split.
+ final List<String> split =
alternating(SentenceVectorsDL.MAX_BATCH_TOKEN_POSITIONS / 3 + 1);
+ final float[][] cpuSplit = cpu.embedAll(split);
+ final float[][] gpuSplit = gpu.embedAll(split);
+ assertEquals(split.size(), gpuSplit.length);
+ for (int i = 0; i < cpuSplit.length; i++) {
+ assertArrayEquals(cpuSplit[i], gpuSplit[i], DELTA, "split row " + i);
+ }
}
}
@@ -170,7 +176,7 @@ class SentenceVectorsDLInferenceOptionsTest {
SentenceVectorsDL.DEFAULT_MAX_LENGTH);
SentenceVectorsDL withOptions = new SentenceVectorsDL(model, vocab,
true, Pooling.MEAN,
false, SentenceVectorsDL.DEFAULT_MAX_LENGTH, new
InferenceOptions())) {
- assertArrayEquals(plain.embed(TEXT), withOptions.embed(TEXT), DELTA);
+ assertArrayEquals(plain.embed(HELLO_WORLD),
withOptions.embed(HELLO_WORLD), DELTA);
}
}
diff --git
a/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/TinyVectorModels.java
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/TinyVectorModels.java
index d48d4ae16..9fe03f057 100644
---
a/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/TinyVectorModels.java
+++
b/opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/TinyVectorModels.java
@@ -20,6 +20,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.List;
import ai.onnxruntime.OrtException;
@@ -47,6 +48,30 @@ public final class TinyVectorModels {
/** The tolerance for comparing vectors of the tiny models. */
public static final float DELTA = 1e-5f;
+ /** A text that encodes to 3 tokens: {@code [CLS]=7 hello=4 [SEP]=3}. */
+ public static final String HELLO = "hello";
+
+ /** A text that encodes to 4 tokens: {@code [CLS]=7 hello=4 world=5
[SEP]=3}. */
+ public static final String HELLO_WORLD = "hello world";
+
+ /** {@code W / |W|}, the unit-length vector of every input. */
+ public static final float[] UNIT_VECTOR = scale(1 / (float) Math.sqrt(5.25));
+
+ /** The mean pooled vector of {@link #HELLO_WORLD}. */
+ public static final float[] HELLO_WORLD_MEAN = scale(19 / 4f);
+
+ /** The summed vector of {@link #HELLO_WORLD}, the {@code
sentence_embedding} of the pooled model. */
+ public static final float[] HELLO_WORLD_SUM = scale(19);
+
+ /** The mean pooled vector of {@link #HELLO}. */
+ public static final float[] HELLO_MEAN = scale(14 / 3f);
+
+ /** The summed vector of {@link #HELLO}, the {@code sentence_embedding} of
the pooled model. */
+ public static final float[] HELLO_SUM = scale(14);
+
+ /** The {@code [CLS]} pooled vector of every input. */
+ public static final float[] CLS_VECTOR = scale(7);
+
private static final String VECTORS_RESOURCES = "/opennlp/dl/vectors/";
private TinyVectorModels() {
@@ -88,6 +113,21 @@ public final class TinyVectorModels {
return new float[] {0.5f * factor, -1f * factor, 2f * factor};
}
+ /**
+ * {@return {@code pairs} times {@link #HELLO_WORLD} followed by {@link
#HELLO}, so the two
+ * lengths alternate}
+ *
+ * @param pairs The number of pairs.
+ */
+ public static List<String> alternating(final int pairs) {
+ final List<String> texts = new ArrayList<>(2 * pairs);
+ for (int i = 0; i < pairs; i++) {
+ texts.add(HELLO_WORLD);
+ texts.add(HELLO);
+ }
+ return texts;
+ }
+
/**
* Opens a session on CUDA device {@code 0}, or aborts the calling test
where the ONNX Runtime
* on the classpath has no CUDA execution provider or the device is not
usable.
diff --git a/opennlp-docs/src/docbkx/machine-learning.xml
b/opennlp-docs/src/docbkx/machine-learning.xml
index 69c3f72d3..dc1f0f07a 100644
--- a/opennlp-docs/src/docbkx/machine-learning.xml
+++ b/opennlp-docs/src/docbkx/machine-learning.xml
@@ -157,7 +157,9 @@ org-start wc=ic=1.0 w&c=apache,ic=2.0 p1wc=lc=3.0]]>
<para>
An embedder is safe for concurrent use until it is
closed. <code>embedAll</code> checks
every text before embedding any of them and forms its
own batches, so it accepts a list
- of any size. Inference failures are reported as
<code>EmbeddingException</code>.
+ of any size. No inference of
<code>SentenceVectorsDL</code> exceeds 16,384 token
+ positions, except that a single text whose encoding is
longer runs as an inference of
+ its own. Inference failures are reported as
<code>EmbeddingException</code>.
</para>
<programlisting language="java"><![CDATA[ProviderSpec spec =
ProviderSpec.of(Path.of("model.onnx"),
Map.of("vocabulary", "vocab.txt"));