Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
182 commits
Select commit Hold shift + click to select a range
6a71703
OPENNLP-1885: Add opennlp-subword: pure-Java SentencePiece inference …
krickert Jul 10, 2026
fe4abdd
OPENNLP-1885: Speed up the encode path 2.3x, parity-checked at every …
krickert Jul 10, 2026
db1c371
OPENNLP-1885: Move the subword contract into opennlp-api
krickert Jul 11, 2026
f71db4d
OPENNLP-1885: Add WordpieceEncoder and fold the unreleased BertTokeni…
krickert Jul 12, 2026
abbf3e6
OPENNLP-1885: Document the hand-rolled protobuf reader rationale and …
krickert Jul 12, 2026
5c78b97
OPENNLP-1885: Trim commentary and tighten javadoc per review conventions
krickert Jul 12, 2026
dea3031
OPENNLP-1885: Tighten javadoc to contracts and document helpers and o…
krickert Jul 13, 2026
1f3883a
OPENNLP-1885: Declare serialVersionUID on SentencePieceTokenizer
krickert Jul 13, 2026
976326a
OPENNLP-1885: Trim residual commentary per review conventions
krickert Jul 13, 2026
bf3cacf
OPENNLP-1885: Document subword tokenization in the manual
krickert Jul 14, 2026
81b1581
OPENNLP-1885: Make the tokenizer graph serializable with computed UID…
krickert Jul 16, 2026
e430109
OPENNLP-1885: Guard tokenizer deserialization with an allow-listing O…
krickert Jul 17, 2026
b843cf7
OPENNLP-1885: Cite the SentencePiece usage example test in the manual
krickert Jul 20, 2026
f55fbec
OPENNLP-1885: Align null contracts and annotations with the review co…
krickert Jul 21, 2026
8ebca7e
OPENNLP-1885: Address review: checked InvalidFormatException for malf…
krickert Jul 21, 2026
80029ca
OPENNLP-1885: Address review: validation message style, shared test f…
krickert Jul 28, 2026
397c9ec
OPENNLP-1885: Deprecate BertTokenizer as a shim over WordpieceEncoder…
krickert Jul 29, 2026
632d996
OPENNLP-1885: Document fixture regeneration and Utf8Text span mapping
krickert Aug 6, 2026
9670c34
OPENNLP-1885: Attribute the double-array literature in DoubleArrayTrie
krickert Aug 6, 2026
d8b0449
OPENNLP-1885: Expand fixtures README into a validation tutorial and l…
krickert Aug 6, 2026
dcc33bd
OPENNLP-1885: Pin InvalidFormatException on a malformed precompiled c…
krickert Aug 8, 2026
f512cec
OPENNLP-1885: Fix loader exception type and wire opennlp-subword into…
krickert Aug 8, 2026
45db421
OPENNLP-1885: Reference the fixtures README by its in-tree path
krickert Aug 9, 2026
2d95887
Add opennlp-embeddings module with a safetensors reader
krickert Jul 7, 2026
dc3fa77
Add StaticEmbeddingModel: tokenize, gather, weight, mean-pool, normalize
krickert Jul 7, 2026
cffed1f
Add word similarity and analogy convenience API
krickert Jul 7, 2026
b13f849
Add a JMH benchmark for StaticEmbeddingModel, matching the perf-1850-…
krickert Jul 7, 2026
c9381b8
Review pass on opennlp-embeddings: analogy exclusion fixes, faster ne…
krickert Jul 7, 2026
a919ceb
Stream safetensors tensor data with positional reads
krickert Jul 9, 2026
795734b
Unit-test SafetensorsHeaderParser directly, reject trailing header ga…
krickert Jul 9, 2026
dcd15e9
Add StaticEmbeddingModel.load(modelDirectory) resolving switches from…
krickert Jul 9, 2026
e25ff87
Document opennlp-embeddings: Dev Manual chapter and module README
krickert Jul 9, 2026
c7610de
OPENNLP-1877: Address review: value-safe TensorInfo, checked IO, type…
krickert Jul 10, 2026
78414d9
OPENNLP-1877: Register opennlp-embeddings in the root dependencyManag…
krickert Jul 10, 2026
4cb03e8
OPENNLP-1877: Rename WordPieceVocabulary to WordpieceVocabulary
krickert Jul 11, 2026
e76555e
OPENNLP-1877: Add the TextEmbedder seam; both engines implement it
krickert Jul 11, 2026
d43f6e1
OPENNLP-1877: Give the embeddings module its own tokenization pipeline
krickert Jul 12, 2026
4aea42b
OPENNLP-1877: Load the tiny-vectors test model from the classpath stream
krickert Jul 12, 2026
b7ca1b6
OPENNLP-1877: Trim commentary and tighten javadoc per review conventions
krickert Jul 12, 2026
3f9bead
OPENNLP-1877: Rewrite the opennlp-embeddings README with diagrams and…
krickert Jul 12, 2026
99538d3
OPENNLP-1877: Document implementation-defined empty-input behavior on…
krickert Jul 12, 2026
110613c
OPENNLP-1877: Read F16 and BF16 safetensors tensors, not just F32
krickert Jul 12, 2026
78a3fd9
OPENNLP-1877: Support SentencePiece models through the SubwordTokeniz…
krickert Jul 13, 2026
d423eb2
OPENNLP-1877: Document helpers and validate record and parser boundaries
krickert Jul 13, 2026
9f7ea98
OPENNLP-1877: Let the embeddings JMH benchmark load real model direct…
krickert Jul 13, 2026
48e81e5
OPENNLP-1877: Add a distillation tutorial and real two-model benchmar…
krickert Jul 13, 2026
6c2a938
OPENNLP-1877: Load WordPiece tables that dropped the frame tokens
krickert Jul 13, 2026
2ae4912
OPENNLP-1877: Add the AssembleModel CLI to complete and verify distil…
krickert Jul 13, 2026
8b2c075
OPENNLP-1877: Ship the distillation script and the Python parity harness
krickert Jul 13, 2026
dfe5834
OPENNLP-1877: Trim residual commentary per review conventions
krickert Jul 13, 2026
0349b1f
OPENNLP-1877: Name format constants, document throws, and source or r…
krickert Jul 16, 2026
1fd2f68
OPENNLP-1877: Cite the static embedding usage example test in the manual
krickert Jul 20, 2026
9aa534c
OPENNLP-1877: Align dev helper placement and test literals with the r…
krickert Jul 21, 2026
d3cf3ff
OPENNLP-1877: Add a Java model distiller and address the review comments
krickert Jul 28, 2026
2501af1
OPENNLP-1877: Fix distiller correctness bugs and cover the untested s…
krickert Jul 28, 2026
1bc1928
OPENNLP-1877: Pin and verify teacher downloads
krickert Jul 28, 2026
7694191
OPENNLP-1877: Batch same-length inputs into one ONNX run in embedAll
krickert Aug 2, 2026
742148c
OPENNLP-1877: Mark public API experimental and reject bare minus headers
krickert Aug 6, 2026
595959d
OPENNLP-1877: Address review: complete experimental markers, hrefs, a…
krickert Aug 8, 2026
71b16b3
OPENNLP-1877: Throw InvalidFormatException for malformed model content
krickert Aug 9, 2026
7212e40
OPENNLP-1877: Demote test-only SafetensorsFile accessors to package-p…
krickert Aug 9, 2026
ddb1992
OPENNLP-1877: Document the DistillModel and AssembleModel tools in th…
krickert Aug 9, 2026
d78300e
OPENNLP-1877: Fail loud on a config.json declaring non-mean pooling
krickert Aug 10, 2026
b42043c
OPENNLP-1877: Clamp the neighbor scan's topK to the vocabulary size
krickert Aug 10, 2026
86963bc
OPENNLP-1877: Reject non-finite embedding matrix values at load time
krickert Aug 10, 2026
52ac814
OPENNLP-1877: Exercise Casing.CASED through the directory loader
krickert Aug 10, 2026
b0ae5e7
OPENNLP-1877: Mirror the manual's explicit-overload listing with a test
krickert Aug 10, 2026
851196f
OPENNLP-1877: Document semantic search in the manual with a mirrored …
krickert Aug 10, 2026
0e1b0fa
OPENNLP-1877: Parse teacher references and hex digests with cursor scans
krickert Aug 16, 2026
5cc79e3
OPENNLP-1877: Distill whole terms as extra rows matched before subwor…
krickert Aug 16, 2026
18d4d00
OPENNLP-1895: Quantized embedding matrix core: seeded rotation, Lloyd…
krickert Jul 24, 2026
b5813b2
OPENNLP-1895: Load quantized matrices in StaticEmbeddingModel and add…
krickert Jul 24, 2026
add1add
OPENNLP-1895: Reject ambiguous matrix source, widen the seed, documen…
krickert Jul 24, 2026
b6bee0a
OPENNLP-1895: Reject dimensions whose padded bit count overflows an int
krickert Jul 24, 2026
fe18a2d
OPENNLP-1895: Pin the checked loader contract for malformed quantized…
krickert Aug 9, 2026
21ce369
OPENNLP-1895: Fail malformed quantized matrices with InvalidFormatExc…
krickert Aug 9, 2026
c22616f
OPENNLP-1895: Follow the checked loader contract through the quantize…
krickert Aug 9, 2026
b8d1333
OPENNLP-1895: Pin InvalidFormatException for every malformed-content …
krickert Aug 10, 2026
c45b8df
OPENNLP-1895: Fail every malformed-content path with InvalidFormatExc…
krickert Aug 10, 2026
ded6805
OPENNLP-1895: Correct the quantized size numbers for power-of-two pad…
krickert Aug 10, 2026
f6f1bef
OPENNLP-1877: load self-contained Model2Vec Unigram tokenizers
krickert Aug 21, 2026
3388e41
OPENNLP-1895: merge latest embedding foundation
krickert Aug 22, 2026
05398bb
OPENNLP-1895: adapt Unigram loading to embedding tables
krickert Aug 22, 2026
228fd7e
OPENNLP-1885: Add opennlp-subword: pure-Java SentencePiece inference …
krickert Jul 10, 2026
8fe701c
OPENNLP-1885: Speed up the encode path 2.3x, parity-checked at every …
krickert Jul 10, 2026
28e6456
OPENNLP-1885: Move the subword contract into opennlp-api
krickert Jul 11, 2026
5b6be0f
OPENNLP-1885: Add WordpieceEncoder and fold the unreleased BertTokeni…
krickert Jul 12, 2026
878e897
OPENNLP-1885: Document the hand-rolled protobuf reader rationale and …
krickert Jul 12, 2026
dfc43f7
OPENNLP-1885: Trim commentary and tighten javadoc per review conventions
krickert Jul 12, 2026
2d57fb6
OPENNLP-1885: Tighten javadoc to contracts and document helpers and o…
krickert Jul 13, 2026
b8aaeb5
OPENNLP-1885: Declare serialVersionUID on SentencePieceTokenizer
krickert Jul 13, 2026
eab8f5d
OPENNLP-1885: Trim residual commentary per review conventions
krickert Jul 13, 2026
8c89cdd
OPENNLP-1885: Document subword tokenization in the manual
krickert Jul 14, 2026
294ccf0
OPENNLP-1885: Make the tokenizer graph serializable with computed UID…
krickert Jul 16, 2026
efacca1
OPENNLP-1885: Guard tokenizer deserialization with an allow-listing O…
krickert Jul 17, 2026
ef6edb6
OPENNLP-1885: Cite the SentencePiece usage example test in the manual
krickert Jul 20, 2026
a5bb0b4
OPENNLP-1885: Align null contracts and annotations with the review co…
krickert Jul 21, 2026
0912464
OPENNLP-1885: Address review: checked InvalidFormatException for malf…
krickert Jul 21, 2026
6f27789
OPENNLP-1885: Address review: validation message style, shared test f…
krickert Jul 28, 2026
eeb829b
OPENNLP-1885: Deprecate BertTokenizer as a shim over WordpieceEncoder…
krickert Jul 29, 2026
2a60b37
OPENNLP-1885: Document fixture regeneration and Utf8Text span mapping
krickert Aug 6, 2026
3d82635
OPENNLP-1885: Attribute the double-array literature in DoubleArrayTrie
krickert Aug 6, 2026
40bb365
OPENNLP-1885: Expand fixtures README into a validation tutorial and l…
krickert Aug 6, 2026
c24cd7f
OPENNLP-1885: Pin InvalidFormatException on a malformed precompiled c…
krickert Aug 8, 2026
d15b010
OPENNLP-1885: Fix loader exception type and wire opennlp-subword into…
krickert Aug 8, 2026
db498e0
OPENNLP-1885: Reference the fixtures README by its in-tree path
krickert Aug 9, 2026
447178f
OPENNLP-1885: Document where the subword API lives and what a piece c…
krickert Sep 1, 2026
154b789
Add opennlp-embeddings module with a safetensors reader
krickert Jul 7, 2026
333da58
Add StaticEmbeddingModel: tokenize, gather, weight, mean-pool, normalize
krickert Jul 7, 2026
c3d6589
Add word similarity and analogy convenience API
krickert Jul 7, 2026
a5b2508
Add a JMH benchmark for StaticEmbeddingModel, matching the perf-1850-…
krickert Jul 7, 2026
cf7bd72
Review pass on opennlp-embeddings: analogy exclusion fixes, faster ne…
krickert Jul 7, 2026
2df6e63
Stream safetensors tensor data with positional reads
krickert Jul 9, 2026
9e247a1
Unit-test SafetensorsHeaderParser directly, reject trailing header ga…
krickert Jul 9, 2026
bc741a2
Add StaticEmbeddingModel.load(modelDirectory) resolving switches from…
krickert Jul 9, 2026
0a98852
Document opennlp-embeddings: Dev Manual chapter and module README
krickert Jul 9, 2026
85b6882
OPENNLP-1877: Address review: value-safe TensorInfo, checked IO, type…
krickert Jul 10, 2026
7aef651
OPENNLP-1877: Register opennlp-embeddings in the root dependencyManag…
krickert Jul 10, 2026
b9043d7
OPENNLP-1877: Rename WordPieceVocabulary to WordpieceVocabulary
krickert Jul 11, 2026
ad625f9
OPENNLP-1877: Add the TextEmbedder seam; both engines implement it
krickert Jul 11, 2026
b9fb977
OPENNLP-1877: Give the embeddings module its own tokenization pipeline
krickert Jul 12, 2026
d5f0391
OPENNLP-1877: Load the tiny-vectors test model from the classpath stream
krickert Jul 12, 2026
01549dd
OPENNLP-1877: Trim commentary and tighten javadoc per review conventions
krickert Jul 12, 2026
610af68
OPENNLP-1877: Rewrite the opennlp-embeddings README with diagrams and…
krickert Jul 12, 2026
c874771
OPENNLP-1877: Document implementation-defined empty-input behavior on…
krickert Jul 12, 2026
566c0da
OPENNLP-1877: Read F16 and BF16 safetensors tensors, not just F32
krickert Jul 12, 2026
826c8b4
OPENNLP-1877: Support SentencePiece models through the SubwordTokeniz…
krickert Jul 13, 2026
213c0ea
OPENNLP-1877: Document helpers and validate record and parser boundaries
krickert Jul 13, 2026
1fb5d66
OPENNLP-1877: Let the embeddings JMH benchmark load real model direct…
krickert Jul 13, 2026
0d3306d
OPENNLP-1877: Add a distillation tutorial and real two-model benchmar…
krickert Jul 13, 2026
0941962
OPENNLP-1877: Load WordPiece tables that dropped the frame tokens
krickert Jul 13, 2026
d2d1f6a
OPENNLP-1877: Add the AssembleModel CLI to complete and verify distil…
krickert Jul 13, 2026
7463c47
OPENNLP-1877: Ship the distillation script and the Python parity harness
krickert Jul 13, 2026
064f981
OPENNLP-1877: Trim residual commentary per review conventions
krickert Jul 13, 2026
f33cc6d
OPENNLP-1877: Name format constants, document throws, and source or r…
krickert Jul 16, 2026
0451d0a
OPENNLP-1877: Cite the static embedding usage example test in the manual
krickert Jul 20, 2026
2b83deb
OPENNLP-1877: Align dev helper placement and test literals with the r…
krickert Jul 21, 2026
3bcac53
OPENNLP-1877: Add a Java model distiller and address the review comments
krickert Jul 28, 2026
231537f
OPENNLP-1877: Fix distiller correctness bugs and cover the untested s…
krickert Jul 28, 2026
3a1084f
OPENNLP-1877: Pin and verify teacher downloads
krickert Jul 28, 2026
45e1c47
OPENNLP-1877: Batch same-length inputs into one ONNX run in embedAll
krickert Aug 2, 2026
36d3f8f
OPENNLP-1877: Mark public API experimental and reject bare minus headers
krickert Aug 6, 2026
201d284
OPENNLP-1877: Address review: complete experimental markers, hrefs, a…
krickert Aug 8, 2026
75ba701
OPENNLP-1877: Throw InvalidFormatException for malformed model content
krickert Aug 9, 2026
3db9afc
OPENNLP-1877: Demote test-only SafetensorsFile accessors to package-p…
krickert Aug 9, 2026
4f15498
OPENNLP-1877: Document the DistillModel and AssembleModel tools in th…
krickert Aug 9, 2026
337caa7
OPENNLP-1877: Fail loud on a config.json declaring non-mean pooling
krickert Aug 10, 2026
2bb3f41
OPENNLP-1877: Clamp the neighbor scan's topK to the vocabulary size
krickert Aug 10, 2026
1f70611
OPENNLP-1877: Reject non-finite embedding matrix values at load time
krickert Aug 10, 2026
ac2b012
OPENNLP-1877: Exercise Casing.CASED through the directory loader
krickert Aug 10, 2026
9e72bc8
OPENNLP-1877: Mirror the manual's explicit-overload listing with a test
krickert Aug 10, 2026
a41ac4b
OPENNLP-1877: Document semantic search in the manual with a mirrored …
krickert Aug 10, 2026
000abb9
OPENNLP-1877: Parse teacher references and hex digests with cursor scans
krickert Aug 16, 2026
322da72
OPENNLP-1877: Distill whole terms as extra rows matched before subwor…
krickert Aug 16, 2026
6d52da2
OPENNLP-1877: load self-contained Model2Vec Unigram tokenizers
krickert Aug 21, 2026
d2c216e
OPENNLP-1885: Add subword API and WordPiece encoder
krickert Sep 3, 2026
6591f8f
Merge remote-tracking branch 'upstream/main' into OPENNLP-1877-static…
krickert Sep 3, 2026
18b3426
Merge API-only OPENNLP-1885 into static embeddings
krickert Sep 3, 2026
1469ddd
OPENNLP-1895: Quantized embedding matrix core: seeded rotation, Lloyd…
krickert Jul 24, 2026
e10c814
OPENNLP-1895: Load quantized matrices in StaticEmbeddingModel and add…
krickert Jul 24, 2026
b909d10
OPENNLP-1895: Reject ambiguous matrix source, widen the seed, documen…
krickert Jul 24, 2026
fefb346
OPENNLP-1895: Reject dimensions whose padded bit count overflows an int
krickert Jul 24, 2026
2992f5f
OPENNLP-1895: Pin the checked loader contract for malformed quantized…
krickert Aug 9, 2026
4f87ddc
OPENNLP-1895: Fail malformed quantized matrices with InvalidFormatExc…
krickert Aug 9, 2026
4795e4e
OPENNLP-1895: Follow the checked loader contract through the quantize…
krickert Aug 9, 2026
4fd9bc6
OPENNLP-1895: Pin InvalidFormatException for every malformed-content …
krickert Aug 10, 2026
29b5e69
OPENNLP-1895: Fail every malformed-content path with InvalidFormatExc…
krickert Aug 10, 2026
53e819c
OPENNLP-1895: Correct the quantized size numbers for power-of-two pad…
krickert Aug 10, 2026
16e4ab4
OPENNLP-1895: adapt Unigram loading to embedding tables
krickert Aug 22, 2026
6491446
OPENNLP-1885: Add subword API and WordPiece encoder
krickert Sep 3, 2026
c7ebfce
OPENNLP-1885: Cover WordPiece parity and model ids
krickert Sep 4, 2026
141160e
OPENNLP-1885: Align WordPiece API and DL callers
krickert Sep 4, 2026
ff83d94
OPENNLP-1877: Harden static embedding loading
krickert Sep 5, 2026
fd1f4f3
OPENNLP-1895: Harden quantized embedding models
krickert Sep 5, 2026
d130e1c
OPENNLP-1885: Merge current main and resource installer
krickert Sep 5, 2026
9d9dcb0
OPENNLP-1877: Reconcile published embeddings history
krickert Sep 5, 2026
f8228d9
OPENNLP-1877: Merge reviewed subword API and current main
krickert Sep 5, 2026
2dc70c1
OPENNLP-1895: Reconcile published quantization history
krickert Sep 5, 2026
7e55830
OPENNLP-1895: Merge updated embeddings and preserve wide analogy queries
krickert Sep 5, 2026
9f187e0
OPENNLP-1877: Add failing test for relative parent-segment teacher paths
krickert Sep 5, 2026
00082f1
OPENNLP-1877: Reject relative teacher paths with parent segments
krickert Sep 5, 2026
b2de78f
Merge branch 'OPENNLP-1877-static-embeddings' into OPENNLP-1895-turbo…
krickert Sep 5, 2026
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
38 changes: 38 additions & 0 deletions dev/embeddings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!--
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.
-->

# Embeddings scripts

Developer scripts around the static embeddings module. None of them are part of the build; they
make the module's numbers and its worked example reproducible from a checkout.

## `distill_bge_m3.py`

The runnable form of the TRAINING.md worked example: distills the multilingual bge-m3 teacher
into a 256-dimension static table with Model2Vec. Needs a Python environment with
`model2vec[distill]` installed; the script's header shows the setup. After it finishes, copy the
teacher's `sentencepiece.bpe.model` next to the output and verify with the `AssembleModel`
command.

## `parity/`

The parity and single-thread speed comparison between this module and the model2vec Python
reference: the same model and the same multilingual sentences on both sides, the two vector sets
checked against each other, and both throughputs measured with the same fixed-duration
methodology. `sh run.sh` after building the project; see the script header for the environment
overrides. A run passes only when the vectors agree within float tolerance, so the two speeds it
prints are for implementations producing the same answer.
52 changes: 52 additions & 0 deletions dev/embeddings/distill_bge_m3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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.

"""Distills the multilingual bge-m3 teacher into a static embedding table.

This is the worked example from opennlp-extensions/opennlp-embeddings/TRAINING.md as a
runnable script. It needs a Python environment with model2vec's distill extra installed:

uv venv .venv-distill
uv pip install --python .venv-distill "model2vec[distill]"
.venv-distill/bin/python distill_bge_m3.py [output-dir]

After it finishes, copy the teacher's trained SentencePiece file
(sentencepiece.bpe.model on the model hub) into the output directory and run the
AssembleModel command to verify the directory loads:

opennlp-embeddings AssembleModel -modelDir <output-dir>

256 dimensions is the deliberate default: distilling the same teacher at 512 gives the
same cross-lingual similarity within noise while doubling the matrix and halving embed
throughput, because PCA to 256 already captures the useful variance.
"""

import os
import sys

from model2vec.distill import distill

out = sys.argv[1] if len(sys.argv) > 1 else "bge-m3-static"

static = distill("BAAI/bge-m3", pca_dims=256)
static.save_pretrained(out)
print("SAVED:", out, "dim:", static.dim)

print("=== output files ===")
for name in sorted(os.listdir(out)):
path = os.path.join(out, name)
print(f" {os.path.getsize(path):>12} {name}")
print("Now copy the teacher's sentencepiece.bpe.model into", out,
"and run: opennlp-embeddings AssembleModel -modelDir", out)
92 changes: 92 additions & 0 deletions dev/embeddings/parity/EmbedBenchM3.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.
*/

import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import opennlp.embeddings.StaticEmbeddingModel;

/**
* The JVM half of the parity and speed comparison (see run.sh). Loads the static table, writes
* one vector per input sentence for the parity check, then measures single-thread embed
* throughput with the same fixed-duration, warmup-discarded methodology the Python side uses.
*
* <p>Args: modelDir sentencesFile vectorsOut warmupSeconds measureSeconds</p>
*/
public final class EmbedBenchM3 {

/** Not instantiable. */
private EmbedBenchM3() {
}

/**
* Runs the parity dump and the single-thread throughput measurement.
*
* @param args modelDir, sentencesFile, vectorsOut, warmupSeconds, measureSeconds.
* @throws Exception Thrown if a file cannot be read or written.
*/
public static void main(String[] args) throws Exception {
final Path modelDir = Path.of(args[0]);
final List<String> sentences = Files.readAllLines(Path.of(args[1]), StandardCharsets.UTF_8)
.stream().map(String::strip).filter(s -> !s.isEmpty()).toList();
final Path vectorsOut = Path.of(args[2]);
final int warmupSeconds = Integer.parseInt(args[3]);
final int measureSeconds = Integer.parseInt(args[4]);

final long loadStart = System.nanoTime();
final StaticEmbeddingModel model = StaticEmbeddingModel.load(modelDir);
final double loadMs = (System.nanoTime() - loadStart) / 1e6;

// One vector per sentence, so the Python side can diff them for parity.
try (BufferedWriter writer = Files.newBufferedWriter(vectorsOut, StandardCharsets.UTF_8)) {
for (final String sentence : sentences) {
final float[] vector = model.embed(sentence);
final StringBuilder line = new StringBuilder();
for (int i = 0; i < vector.length; i++) {
if (i > 0) {
line.append(' ');
}
line.append(Float.toString(vector[i]));
}
writer.write(line.toString());
writer.newLine();
}
}

final long warmupEnd = System.nanoTime() + warmupSeconds * 1_000_000_000L;
int index = 0;
while (System.nanoTime() < warmupEnd) {
model.embed(sentences.get(index++ % sentences.size()));
}

long embedded = 0;
final long measureStart = System.nanoTime();
final long measureEnd = measureStart + measureSeconds * 1_000_000_000L;
index = 0;
while (System.nanoTime() < measureEnd) {
model.embed(sentences.get(index++ % sentences.size()));
embedded++;
}
final double seconds = (System.nanoTime() - measureStart) / 1e9;

System.out.printf("JVM load %.0f ms | %,.0f texts/s single-thread (%d embeds in %.1fs)%n",
loadMs, embedded / seconds, embedded, seconds);
}
}
93 changes: 93 additions & 0 deletions dev/embeddings/parity/parity_speed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 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.

"""The Python half of the parity and speed comparison, plus the final parity check.

Loads the same static table with model2vec, writes one vector per sentence, measures
single-thread throughput with the same fixed-duration warmup-discarded loop the JVM side
uses, then loads the JVM's vectors (written first by run.sh) and reports the parity between
them.

The point is not to declare a winner; it is to show that both implementations produce the
same vectors, and to let anyone reproduce both numbers on their own hardware.

Usage: parity_speed.py <model-dir> [sentences-file] [jvm-vectors-file]
"""

import sys
import time

import numpy as np
from model2vec import StaticModel

WARMUP_SECONDS = 3
MEASURE_SECONDS = 5


def read_sentences(path):
with open(path, encoding="utf-8") as handle:
return [line.strip() for line in handle if line.strip()]


def main():
model_dir = sys.argv[1]
sentences_file = sys.argv[2] if len(sys.argv) > 2 else "sentences.txt"
jvm_vectors_file = sys.argv[3] if len(sys.argv) > 3 else "jvm_vectors.tsv"
sentences = read_sentences(sentences_file)

load_start = time.time()
model = StaticModel.from_pretrained(model_dir)
load_ms = (time.time() - load_start) * 1000.0

python_vectors = np.array([model.encode(s) for s in sentences], dtype=np.float32)

end = time.time() + WARMUP_SECONDS
i = 0
while time.time() < end:
model.encode(sentences[i % len(sentences)])
i += 1

embedded = 0
i = 0
start = time.time()
end = start + MEASURE_SECONDS
while time.time() < end:
model.encode(sentences[i % len(sentences)])
embedded += 1
i += 1
seconds = time.time() - start
print(f"Python load {load_ms:.0f} ms | {embedded / seconds:,.0f} texts/s single-thread "
f"({embedded} embeds in {seconds:.1f}s)")

jvm_vectors = np.loadtxt(jvm_vectors_file, dtype=np.float32)
if jvm_vectors.shape != python_vectors.shape:
print(f"PARITY FAIL: shape mismatch {jvm_vectors.shape} vs {python_vectors.shape}")
sys.exit(1)

max_abs_diff = float(np.abs(python_vectors - jvm_vectors).max())
cosines = [
float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
for a, b in zip(python_vectors, jvm_vectors)
]
print(f"Parity max abs diff {max_abs_diff:.2e} | min cosine {min(cosines):.6f} "
f"over {len(sentences)} sentences in {python_vectors.shape[1]} dims")
if min(cosines) < 0.9999:
print("PARITY FAIL: vectors diverge")
sys.exit(1)
print("Parity OK: the JVM and Python vectors are the same within float tolerance")


if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions dev/embeddings/parity/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/sh
# 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.

# Reproduces the parity and single-thread speed comparison between opennlp-embeddings and the
# model2vec Python reference: the same model and the same sentences on both sides, with the
# vector sets checked against each other. Run from this directory after building the project
# (mvn install, or at least mvn compile from the repository root).
#
# Environment overrides:
# MODEL_DIR the static model directory (default: bge-m3-static in this directory;
# see ../distill_bge_m3.py and opennlp-extensions/opennlp-embeddings/TRAINING.md
# to produce one)
# PYTHON a Python interpreter with model2vec installed (default: python3)
set -e

MODEL_DIR="${MODEL_DIR:-bge-m3-static}"
PYTHON="${PYTHON:-python3}"

# The repository root is three levels above this script.
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
CP="$ROOT/opennlp-api/target/classes:$ROOT/opennlp-core/opennlp-runtime/target/classes:$ROOT/opennlp-extensions/opennlp-subword/target/classes:$ROOT/opennlp-extensions/opennlp-embeddings/target/classes"

echo "Model: $MODEL_DIR"
echo "Sentences: $(grep -c . sentences.txt) lines, multilingual"
echo

# JVM side first: it writes jvm_vectors.tsv, which the Python side then diffs.
javac -cp "$CP" -d . EmbedBenchM3.java
java -cp "$CP:." EmbedBenchM3 "$MODEL_DIR" sentences.txt jvm_vectors.tsv 3 5

# Python side: prints its own rate, then reports parity against the JVM's vectors.
"$PYTHON" parity_speed.py "$MODEL_DIR"
20 changes: 20 additions & 0 deletions dev/embeddings/parity/sentences.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
The weather is beautiful today and the sky is clear.
Machine learning models turn text into vectors.
I would like a cup of coffee with milk please.
The quarterly financial results disappointed investors.
Das Wetter ist heute wunderschoen und der Himmel ist klar.
Maschinelles Lernen verwandelt Text in Vektoren.
Le temps est magnifique aujourd'hui et le ciel est degage.
Los modelos de aprendizaje automatico convierten texto en vectores.
今天天气很好,天空很晴朗。
机器学习模型把文本转换成向量。
今日はとても良い天気で空が澄んでいます。
機械学習モデルはテキストをベクトルに変換します。
Сегодня прекрасная погода и ясное небо.
Модели машинного обучения превращают текст в векторы.
La retrieval semantica trova documenti per significato non per parole.
Natural language processing is a field of artificial intelligence.
A quick brown fox jumps over the lazy dog near the river.
Embeddings place similar sentences close together in space.
Coffee, tea, and espresso are all popular hot drinks.
The library opens at nine in the morning on weekdays.
Loading
Loading