rzo1 commented on code in PR #1468:
URL: https://github.com/apache/opennlp/pull/1468#discussion_r4242497766
##########
opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java:
##########
@@ -73,14 +75,23 @@
* {@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
+ * {@value #MAX_BATCH_TOKEN_POSITIONS} token positions is split into several
inferences.</p>
*/
@ThreadSafe
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.
+ */
+ static final int MAX_BATCH_TOKEN_POSITIONS = 16384;
Review Comment:
Please state in the Javadoc why the bound is 16384 (32 rows at the BERT
length of 512).
##########
opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java:
##########
@@ -223,9 +278,8 @@ public float[] embed(final CharSequence text) {
/**
* {@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 {@value #MAX_BATCH_TOKEN_POSITIONS} token
positions.</p>
Review Comment:
The previous Javadoc stated that every row is computed from the tensors its
single-input call would have used. Callers rely on this guarantee; please keep
the sentence.
##########
opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLBatchLimitTest.java:
##########
@@ -0,0 +1,278 @@
+/*
+ * 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.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.TINY_POOLED;
+import static opennlp.dl.vectors.TinyVectorModels.TINY_VECTORS;
+import static opennlp.dl.vectors.TinyVectorModels.gpuOrAbort;
+import static opennlp.dl.vectors.TinyVectorModels.model;
+import static opennlp.dl.vectors.TinyVectorModels.scale;
+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;
+
+ private static final float DELTA = 1e-5f;
+
+ private static final String HELLO = "hello";
+
+ private static final String HELLO_WORLD = "hello world";
+
+ private static final float[] UNIT = scale(1 / (float) Math.sqrt(5.25));
+
+ /**
+ * {@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 {@code pairs} times "hello world" followed by "hello"}
+ *
+ * @param pairs The number of pairs.
+ */
+ private 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;
+ }
+
+ /**
+ * {@return the length, the count and the expected batch sizes at the edges
of the row count}
+ */
+ static Stream<Arguments> rowCountBoundaries() {
+ return Stream.of(
+ Arguments.of(4, 4096, List.of(4096)),
+ Arguments.of(4, 4097, List.of(4096, 1)),
+ Arguments.of(4, 4095, List.of(4095)),
+ Arguments.of(2, 8192, List.of(8192)),
+ Arguments.of(2, 8193, List.of(8192, 1)),
+ Arguments.of(3, 5461, List.of(5461)),
+ Arguments.of(3, 5462, List.of(5461, 1)),
+ Arguments.of(3, 10922, List.of(5461, 5461)),
+ Arguments.of(3, 10923, List.of(5461, 5461, 1)),
+ Arguments.of(1, 16384, List.of(16384)),
+ Arguments.of(1, 16385, List.of(16384, 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, and every row
+ * equals the vector of its 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);
+ for (int i = 0; i < texts.size(); i++) {
+ 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, scale(19 / 4f),
scale(14 / 3f)),
+ Arguments.of(TINY_VECTORS, Pooling.CLS, false, scale(7), scale(7)),
+ Arguments.of(TINY_VECTORS, Pooling.CLS, true, UNIT, UNIT),
+ Arguments.of(TINY_VECTORS, Pooling.MEAN, true, UNIT, UNIT),
+ Arguments.of(TINY_POOLED, Pooling.CLS, false, scale(19), scale(14)),
+ Arguments.of(TINY_POOLED, Pooling.MEAN, true, UNIT, UNIT));
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ /**
+ * {@return the model, pooling and unit length combinations, one per path
after the inference}
+ */
+ static Stream<Arguments> gpuCases() {
+ return pathCases().map(arguments ->
Arguments.of(Arrays.copyOf(arguments.get(), 3)));
+ }
+
+ /**
+ * Checks that a split group gives the same vectors on CUDA device {@code 0}
as on the CPU,
+ * where a CUDA device exists; skipped elsewhere.
+ */
+ @ParameterizedTest
+ @MethodSource("gpuCases")
+ void testGpuMatchesTheCpuForASplitGroup(final String model, final Pooling
pooling,
Review Comment:
This repeats the GPU-against-CPU structure of
`SentenceVectorsDLInferenceOptionsTest.testGpuVectorsMatchTheCpu` from #1462
(same `gpuOrAbort`, row loop and model/pooling cases). Please extend the
existing test with a split group instead of adding a second one.
##########
opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLBatchLimitTest.java:
##########
@@ -0,0 +1,278 @@
+/*
+ * 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.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.TINY_POOLED;
+import static opennlp.dl.vectors.TinyVectorModels.TINY_VECTORS;
+import static opennlp.dl.vectors.TinyVectorModels.gpuOrAbort;
+import static opennlp.dl.vectors.TinyVectorModels.model;
+import static opennlp.dl.vectors.TinyVectorModels.scale;
+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;
+
+ private static final float DELTA = 1e-5f;
+
+ private static final String HELLO = "hello";
+
+ private static final String HELLO_WORLD = "hello world";
+
+ private static final float[] UNIT = scale(1 / (float) Math.sqrt(5.25));
+
+ /**
+ * {@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 {@code pairs} times "hello world" followed by "hello"}
+ *
+ * @param pairs The number of pairs.
+ */
+ private 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;
+ }
+
+ /**
+ * {@return the length, the count and the expected batch sizes at the edges
of the row count}
+ */
+ static Stream<Arguments> rowCountBoundaries() {
+ return Stream.of(
+ Arguments.of(4, 4096, List.of(4096)),
Review Comment:
These values (4096, 8192, 5461, ...) are derived from 16384. Please compute
them from `LIMIT` (e.g. `LIMIT / 4`), otherwise a change of the constant
silently changes what the cases test.
##########
opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLBatchLimitTest.java:
##########
@@ -0,0 +1,278 @@
+/*
+ * 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.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.TINY_POOLED;
+import static opennlp.dl.vectors.TinyVectorModels.TINY_VECTORS;
+import static opennlp.dl.vectors.TinyVectorModels.gpuOrAbort;
+import static opennlp.dl.vectors.TinyVectorModels.model;
+import static opennlp.dl.vectors.TinyVectorModels.scale;
+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;
+
+ private static final float DELTA = 1e-5f;
Review Comment:
`DELTA`, `HELLO`, `HELLO_WORLD` and the expected vectors (`UNIT`, `scale(19
/ 4f)`, ...) also exist in `SentenceVectorsDLEmbedderTest`. Please move them to
`TinyVectorModels`, together with the fixture points from the #1462 review.
##########
opennlp-core/opennlp-ml/opennlp-dl/src/test/java/opennlp/dl/vectors/SentenceVectorsDLBatchLimitTest.java:
##########
@@ -0,0 +1,278 @@
+/*
+ * 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.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.TINY_POOLED;
+import static opennlp.dl.vectors.TinyVectorModels.TINY_VECTORS;
+import static opennlp.dl.vectors.TinyVectorModels.gpuOrAbort;
+import static opennlp.dl.vectors.TinyVectorModels.model;
+import static opennlp.dl.vectors.TinyVectorModels.scale;
+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;
+
+ private static final float DELTA = 1e-5f;
+
+ private static final String HELLO = "hello";
+
+ private static final String HELLO_WORLD = "hello world";
+
+ private static final float[] UNIT = scale(1 / (float) Math.sqrt(5.25));
+
+ /**
+ * {@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 {@code pairs} times "hello world" followed by "hello"}
+ *
+ * @param pairs The number of pairs.
+ */
+ private 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;
+ }
+
+ /**
+ * {@return the length, the count and the expected batch sizes at the edges
of the row count}
+ */
+ static Stream<Arguments> rowCountBoundaries() {
+ return Stream.of(
+ Arguments.of(4, 4096, List.of(4096)),
+ Arguments.of(4, 4097, List.of(4096, 1)),
+ Arguments.of(4, 4095, List.of(4095)),
+ Arguments.of(2, 8192, List.of(8192)),
+ Arguments.of(2, 8193, List.of(8192, 1)),
+ Arguments.of(3, 5461, List.of(5461)),
+ Arguments.of(3, 5462, List.of(5461, 1)),
+ Arguments.of(3, 10922, List.of(5461, 5461)),
+ Arguments.of(3, 10923, List.of(5461, 5461, 1)),
+ Arguments.of(1, 16384, List.of(16384)),
+ Arguments.of(1, 16385, List.of(16384, 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, and every row
+ * equals the vector of its own text.
+ */
+ @Test
+ void testMixedGroupsThroughTheSession(@TempDir final Path dir) throws
Exception {
Review Comment:
This compares about 4100 rows with single `embed` calls. Comparing a few
rows of each batch would be sufficient.
##########
opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/vectors/SentenceVectorsDL.java:
##########
@@ -73,14 +75,23 @@
* {@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
Review Comment:
`MAX_BATCH_TOKEN_POSITIONS` is package-private but referenced via `{@value}`
in the public class and method Javadoc. Either it is part of the contract, then
it should be public, or the Javadoc should state the number.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]