Status: Phase 1 in progress · Owner: @michalharakal · Created: 2026-07-13
Extend the BERT embedding stack beyond LEAF to the two most-used compact retrieval
models, establishing the model matrix for the upcoming embedding performance
benchmarks (llm-performance will compare models × execution modes on one harness):
| Target model | Dims | Size | Blocking gaps |
|---|---|---|---|
BAAI/bge-small-en-v1.5 |
384 | 130 MB | CLS pooling; query instruction prefix |
intfloat/multilingual-e5-small |
384 | 118 MB | Unigram (XLM-R) tokenizer; query:/passage: prefixes |
Verified facts (HF configs, 2026-07-13): both are plain 12-layer BERTs (hidden 384,
heads 12, FFN 1536, absolute positions, type_vocab_size 2, no 2_Dense head,
modules Transformer → Pooling → Normalize). bertNetwork() runs both unchanged —
every gap is peripheral (tokenizer, pooling, text prefixes).
BertEncoderRuntime hard-codes masked mean pooling. Add:
public enum class BertPooling { MEAN, CLS }- Constructor/
createBertEncoderRuntimeparameter, defaultMEAN(source- and behavior-compatible; BCV dumps regenerate for the new signatures). encode():CLStakes hidden-state row 0;MEANkeeps the existing masked mean. Pooling stays outside the traced encoder graph (unchanged export/OPTIMIZED path).- Detection: sentence-transformers
1_Pooling/config.json(pooling_mode_cls_token/pooling_mode_mean_tokens) parsed next toBertConfigParser; both factories wire it through. Absent file →MEAN(status quo).
Retrieval models embed queries and documents differently (instruction prefixes).
- SPI (additive):
EmbeddingModelgainsembedQuery(text)andembedDocument(text)/embedDocuments(texts)with defaults delegating toembed(...)— existing implementations and consumers are untouched. PrefixedEmbeddingModeldecorator (llm-providers): prepends per-role prefixes;embed(...)uses the document role (indexing is the common bulk path).EmbeddingModelProfilesregistry keyed by repo-id prefix:intfloat/multilingual-e5-*→("query: ", "passage: ");BAAI/bge-*-en-*→("Represent this sentence for searching relevant passages: ", "").fromHuggingFaceapplies the profile automatically;fromSafeTensorsaccepts explicit prefix parameters (no repo id to key on).
HuggingFaceTokenizer is WordPiece-only with hard-coded [CLS]/[SEP]. E5 needs
SentencePiece Unigram (Viterbi over a 250k scored vocab) plus XLM-R special tokens
(<s>/</s>, ids 0/2).
- Source of truth:
tokenizer.json(embeds the scored vocab + post-processor — no protobuf.modelparsing). - Normalizer: XLM-R uses a
Precompiledcharsmap; we approximate with NFKC and quantify drift with a multilingual parity corpus vs HFtokenizers(CP-3). - Placement decision at CP-3 (see checkpoints): transformers-side
(
llm-inference/bert, no engine release needed) vs engine (sk.ainet.io.tokenizer, where SentencePiece-BPE lives, requires engine release). Default: transformers-side first, upstream to the engine later.
Max/sqrt-len pooling modes, two-segment (cross-encoder) inputs, matryoshka dims, serving-side batching.
Gitflow: feature/* → develop → release/x.y.z → tag (publishes to Maven Central).
| ID | Work item | Repo / branch | Release | Status |
|---|---|---|---|---|
| E5BGE-0 | This design doc | SK-tr feature/embedding-pooling-profiles |
0.36.1 | done |
| E5BGE-1 | BertPooling (MEAN/CLS) + 1_Pooling detection + tests |
SK-tr, same branch | 0.36.1 | done |
| E5BGE-2 | SPI embedQuery/embedDocument + PrefixedEmbeddingModel + profiles |
SK-tr, same branch | 0.36.1 | done |
| E5BGE-3 | BGE end-to-end verification (CLS, 384 dims, prefix) + smoke entry | SK-tr, same branch | 0.36.1 | pending |
| E5BGE-4 | Unigram tokenizer spike → placement decision | SK-tr (or engine per CP-3) | next release | pending |
| E5BGE-5 | Unigram impl + multilingual parity + special-token generalization | per CP-3 | next release | pending |
| E5BGE-6 | E5 end-to-end verification + docs + benchmark matrix entry | SK-tr | next release | pending |
| E5BGE-7 | leaf-cli: embedQuery/embedDocument wiring + dim recorded in index |
SK-leaf feature/* → develop |
after 0.36.1 | pending |
- CP-1 — Phase 1 code complete (E5BGE-1..2): full build +
apiCheckgreen, pooling/prefix unit tests pass. Engine changes required: none. - CP-2 — BGE verified (E5BGE-3):
fromHuggingFace("BAAI/bge-small-en-v1.5")produces 384-dim CLS-pooled embeddings; parity vs sentence-transformers reference vectors; PR todevelop; ship as 0.36.1. Gate: confirm no engine release is on the critical path — if anything surfaced, stop and file engine issues first. Gate outcome (2026-07-14): one engine gap surfaced —SafeTensorsParametersLoadercannot skip BGE's persisted I64embeddings.position_idsbuffer (SKaiNET#822). Bridged by the interimFloatSafeTensorsLoaderin llm-providers (permute-handler pattern from 0.36.0: local, documented, dropped when the engine fix ships). No engine release on the 0.36.1 critical path — which is why this ships as a patch on the 0.36.0 engine line rather than as 0.37.0. - CP-3 — Tokenizer placement decision (E5BGE-4): spike Unigram-from-tokenizer.json; measure normalizer drift on a multilingual corpus. Decision recorded here: transformers-side vs engine-side. This is the explicit "are upstream-engine changes required?" gate for Phase 2.
- CP-4 — E5 verified (E5BGE-5..6): parity ≤ 1e-5 vs sentence-transformers on the parity corpus (drift from the normalizer approximation documented), smoke green; ship as 0.38.0.
Once CP-2 lands, the embedding benchmark matrix becomes
{mdbr-leaf-ir, bge-small-en-v1.5} × {DIRECT, OPTIMIZED}, extended by
multilingual-e5-small at CP-4 — measured with the existing llm-performance
harness plus the PhaseProfile diagnostic (branch perf/llama-packed-load-memory)
for the non-matmul tail.