Kristian Rickert created OPENNLP-1877:
-----------------------------------------

             Summary: Pure JVM Embeddings - 
opennlp-extensions/opennlp-embeddings module
                 Key: OPENNLP-1877
                 URL: https://issues.apache.org/jira/browse/OPENNLP-1877
             Project: OpenNLP
          Issue Type: New Feature
    Affects Versions: 3.0.0-M4
            Reporter: Kristian Rickert
            Assignee: Kristian Rickert
             Fix For: 3.0.0-M5


h2. Summary

OpenNLP's static word-vector support ends at the GloVe loader in 
{{opennlp.tools.util.wordvector}}. The static-embedding field moved in 
2024/2025: distillation tooling (Model2Vec and compatible) compresses a full 
sentence transformer into a flat per-token vector table, the same artifact 
shape as word2vec/GloVe, carrying the teacher model's semantics. Inference over 
such a table is tokenize, gather, weight, mean-pool, normalize: no forward 
pass, no GPU, no native runtime. That is a pure-JVM sweet spot, and this ticket 
adds the engine for it as a new {{opennlp-extensions/opennlp-embeddings}} 
module.

The module ships code only. Users point it at a table they downloaded (the 
{{vocab.txt}} plus {{model.safetensors}} file pair such distillations publish); 
nothing is fetched at build or run time and no model weights enter the source 
tree or release artifacts. The pooling semantics were verified against the 
reference implementations (the Model2Vec Python package and its official Rust 
port), not assumed: {{[CLS]}}/{{[SEP]}} are never pooled, unknown tokens are 
dropped from both the sum and the denominator, an optional per-row {{weights}} 
tensor multiplies each token's vector before pooling, the denominator is the 
pooled-token count (not the sum of weights), and normalization uses an epsilon 
floor so token-less input yields a zero vector rather than NaN.

Measured with JMH at the scale of a current published table (29,528 rows by 256 
dimensions): about 766,000 short-sentence embeds per second on one core, and 
the full-vocabulary top-10 similarity scan at 649 per second on one core 
scaling to about 9,200 per second at 32 threads. Instances are immutable and 
annotated {{@ThreadSafe}}, with a concurrency test comparing every concurrent 
result against the single-threaded reference.

h2. Scope

* {{SafetensorsFile}}: a reader for the safetensors tensor format (8-byte 
little-endian header length, JSON header, raw tensor bytes) with a 
purpose-built cursor parser for the header, no third-party JSON dependency. 
Only the F32 decode path is implemented. Unlike pickle-based checkpoint 
formats, safetensors carries no executable content, so loading is safe by 
construction. The embedding matrix is found as the single 2-D F32 tensor, 
failing loud and listing candidates when that is ambiguous, rather than 
guessing a key-name convention.
* {{WordPieceVocabulary}}: a BERT-style {{vocab.txt}} reader; the line number 
is the token's row id into the embedding matrix.
* {{StaticEmbeddingModel}}: loads the file pair, embeds text through the 
existing {{BertTokenizer}}/{{WordpieceTokenizer}} (reused as is, no new 
tokenizer code), and applies the verified pooling formula. Per-row L2 norms and 
the special-token mask are precomputed at load.
* Convenience surface: {{similarity(text1, text2)}}, {{mostSimilar(text, 
topK)}} (brute-force scan with a bounded top-K selection), and {{analogy(a, b, 
c, topK)}} (vector arithmetic, with the input terms excluded by folding them 
through the model's own tokenizer, so exclusion is case- and accent-consistent 
with embedding).
* A JMH benchmark behind the same opt-in {{jmh}} Maven profile pattern 
{{opennlp-runtime}} already uses; the default build is unaffected.

h2. Acceptance criteria

* Loads the file pair of a current published distilled table and matches the 
reference pooling semantics; tests pin the formula details (denominator, 
unknown-token handling, weight application, normalization epsilon, zero-row NaN 
guard).
* No bundled model data, no new dependencies, nothing fetched at build or run 
time.
* Immutable and thread-safe; a concurrency test compares concurrent results 
against the single-threaded reference.
* {{mvn verify}} green including checkstyle and forbiddenapis; JMH numbers 
recorded in the PR.

h2. Out of scope

* Bundling any model weights as a zero-config default. That requires 
per-release license diligence on the weights (the distillation tool being MIT 
does not settle the weight license) and is a separable later decision.
* An approximate-nearest-neighbor index for {{mostSimilar}}. Brute force is the 
documented v1 posture at this vocabulary scale; an index is a follow-up.
* The gRPC {{EmbeddingProvider}} backend in opennlp-sandbox that serves this 
engine. That is a companion change in the sandbox repository after this lands.
* Interpreting the Hugging Face {{tokenizer.json}} pipeline format. The v1 
contract is {{vocab.txt}} plus the existing WordPiece tokenizer, which the 
targeted table family publishes.
* Training or distillation. Producing tables stays in the upstream tooling, the 
same posture opennlp-dl takes for ONNX models.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to