Skip to content

BERT on the DSL path: complete bertNetwork(), BertEncoderRuntime (eager + traced), one-call BertEmbeddingModel, legacy runtime removed#222

Open
michalharakal wants to merge 6 commits into
developfrom
feature/bert-dsl-embeddings
Open

BERT on the DSL path: complete bertNetwork(), BertEncoderRuntime (eager + traced), one-call BertEmbeddingModel, legacy runtime removed#222
michalharakal wants to merge 6 commits into
developfrom
feature/bert-dsl-embeddings

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Completes the BERT migration to the DSL philosophy (network definition → eager DIRECT execution or traced/optimized ComputeGraph → StableHLO export), adds a one-call consumption API, and removes the deprecated hand-coded eager stack in the same release.

The gap this closes

BertRuntime was deprecated pointing at a migration guide that was never written and a ReplaceWith factory that doesn't exist, while the "new path" was half-built: bertNetwork() omitted position/token-type embeddings (numerically incomplete on its own), OptimizedLLMRuntime is generation-shaped (no full-sequence hidden states, no pooling), and SkaiNetEmbeddingModel + KBertJava were still hard-wired to the deprecated class.

What's new

  • BertEmbeddings — complete embeddings block (word gather + position rows via narrow(table, 0, 0, L) + token-type row 0 broadcast, LayerNorm). Index-free position/type lookups keep the traced graph at exactly one non-parameter leaf. Single-segment by design.
  • BertEncoderRuntime — full-sequence encoder + masked mean pooling + optional 2_Dense projection + L2 norm. DIRECT (default) runs the module tree eagerly; OPTIMIZED executes a traced, LLM-pipeline-optimized ComputeGraph, shape-specialized per sequence length (small LRU). exportTape(...) feeds the StableHLO path.
  • BertEmbeddingModel.fromHuggingFace("MongoDB/mdbr-leaf-mt") / fromSafeTensors(dir) (llm-providers) — one-call factories behind the neutral EmbeddingModel SPI, with built-in Hub download via the engine's skainet-data-source (hf:// URIs, HF_TOKEN-aware, .part + atomic rename, offline-safe after first run). kbert-cli accepts a repo id directly.
  • Consumers retargeted: SkaiNetEmbeddingModel (adapter), KBertJava/KBertSession (Java method surface unchanged — smoke test passes untouched), kbert-cli.

Bugs found and fixed on the way

  1. Post-norm residual wiring — the transformer blocks' residual rule (segment starts after the previous ResidualAdd) fits pre-norm decoders; BERT is post-norm, so the FFN residual grabbed the pre-LayerNorm value. Each encoder layer is now two blocks (attn/ffn).
  2. Bias-free 2_Dense heads were silently dropped — the legacy runtime required weight AND bias; LEAF ships bias=false, so it returned unprojected 384-dim vectors while advertising 1024. The new runtime applies bias-free projections; KBertJava picks up heads it previously ignored.
  3. Graph replay dropped permute axes (engine executor bug, decode never hits it) — worked around here with an axes-aware "permute" handler in LLMFusedOpHandlers; the proper engine fix is fix(compile-dag): ComputeGraphExecutor replays permute with its recorded axes SKaiNET#803, and the override gets dropped when that release is consumed.

Removed (BREAKING)

BertRuntime, BertRuntimeWeights/BertLayerWeights, loadBertWeights, BertWeightMapper, BertTensorNames, BertIngestion. Migration: createBertEncoderRuntime(config, tensors, ctx) or BertEmbeddingModel.from*(...). BertModelConfig + MDBR_LEAF_IR_CONFIG moved file (same package — imports unaffected). BCV dumps regenerated; CHANGELOG carries the full BREAKING notes.

Verification

  • Parity vs the PyTorch-validated legacy runtime on real MongoDB/mdbr-leaf-mt: hidden states ≤ 2.2e-6, embeddings ≤ 9e-8 (bridge test lived until the deletion commit).
  • Projected pipeline verified ≤ 6e-8 against independent plain-Kotlin recomputation.
  • DIRECT vs OPTIMIZED bit-exact (synthetic + real model); StableHLO export smoke-tested (gather/dot/add survive lowering).
  • BertLeafReferenceSmokeTest (Java surface) passes unmodified; new factory tests incl. Hub download; full jvm suites + apiCheck green.
  • Downstream leaf-cli e2e: same 56-chunk corpus indexes in 44.5 s vs 676.9 s on the deleted eager runtime — 15× faster with identical embeddings.

First commit regenerates two stale BCV dumps that pre-exist on develop (transformer-core RoPE, kgemma FunctionGemma) so the branch starts from a green apiCheck.

🤖 Generated with Claude Code

michalharakal and others added 6 commits July 9, 2026 15:47
…ithCosSin, kgemma FunctionGemma)

Pre-existing on develop: both were merged without running apiDump, so
apiCheck fails on a pristine checkout. Isolated commit so the BERT work
that follows starts from a green gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bertNetwork()

BertEmbeddings module: word gather + position rows via narrow(table, 0, 0, L)
+ token-type row 0 broadcast ([L,dim]+[dim], the Linear-bias pattern), then
LayerNorm. Index-free position/type lookups keep the traced graph at exactly
one non-parameter leaf. Single-segment by design.

bertNetwork() now returns a numerically complete tokens→hidden-states encoder.
BertSafeTensorsNameResolver (llm-core copy) gains position/token_type cases;
BertNetworkLoader rewritten: loadWeightTensors(loaders) collection, bare-name→
bert.-prefix normalization, fromWeightTensors with strict mapped==total and
shape-fallback off. bert module gains compile-dag/opt deps for the upcoming
traced encoder runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion/L2, post-norm fix

BertEncoderRuntime wraps bertNetwork(): full-sequence forward, masked mean
pooling, optional sentence-transformers dense projection, L2 norm — encode()
keeps the legacy contract. createBertEncoderRuntime maps checkpoint tensors
strictly (shape-fallback off) and pulls the 2_Dense projection pair.

Two correctness fixes the old stack hid:
- bertNetwork()'s single-block encoder layer wired the FFN residual to the
  pre-LayerNorm value (the blocks' residual rule fits pre-norm decoders).
  BERT is post-norm; each layer is now two blocks so every residual segment
  starts at the right value. Verified against the PyTorch-validated eager
  runtime on real mdbr-leaf-mt: hidden-state parity ≤2.2e-6.
- LEAF's 2_Dense head is bias-free; the legacy runtime required weight AND
  bias and silently skipped the projection (returning 384-dim vectors while
  advertising 1024). The new runtime applies bias-free projection, verified
  ≤6e-8 against independent recomputation.

BertModelConfig + MDBR_LEAF_IR_CONFIG moved to BertConfig.kt (same package)
with a regex BertConfigParser (config.json + 2_Dense/config.json). Resolver
learns sub-block layer paths (encoder.layer.N.attn/.ffn).
BertNumericalAccuracyTest retargeted to the DSL path; BertLegacyParityTest
bridges old→new until BertRuntime is deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…O export gate

BertEncoderRuntime.OPTIMIZED traces the encoder (tape → toComputeGraph →
LLM optimization pipeline → ComputeGraphExecutor), shape-specialized per
sequence length in a small LRU cache; DIRECT stays the default. Graph
leaves split by module-parameter identity; the token input is the unique
Int32 leaf (BertEmbeddings' index-free position/type lookups guarantee
that), other non-param leaves (eager weight transposes, attention
constants) ride along as trace-time statics.

Fixes a latent executor replay bug this path exposed: the builtin
dispatch replayed 'permute' as a last-two-dims transpose, dropping the
recorded axes — wrong for MHA's [1,0,2] heads/seq swap on any multi-token
trace (decode's seqLen-1 shortcut never hits it). LLMFusedOpHandlers now
registers an axes-aware permute handler (registry precedes the builtin);
remove once the engine executor honors 'axes' upstream.

Gates: DIRECT vs OPTIMIZED bit-exact on real mdbr-leaf-mt and synthetic
models (BertTraceParityTest); bertNetwork lowers to StableHLO with
gather/dot/add intact (BertStableHloExportTest, IREE execution explicitly
out of scope).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s + fromHuggingFace

BertEmbeddingModel.fromSafeTensors(dir) auto-detects weights file, config,
2_Dense projection head, and tokenizer (vocab.txt → tokenizer.json), builds
the DSL encoder, and returns the neutral EmbeddingModel SPI.
fromHuggingFace(repoId) downloads missing snapshot files via the engine's
skainet-data-source (hf:// URIs, HF_TOKEN-aware) into
~/.cache/skainet/models/<owner>_<repo> — streamed, .part + atomic rename,
optional files (bias-free heads) skip gracefully — then delegates.
llm-providers gains data-source + kotlinx-io deps (BOM-aligned).

Consumers retargeted off the deprecated eager runtime:
- SkaiNetEmbeddingModel now adapts BertEncoderRuntime (suppression gone).
- KBertJava/KBertSession: method surface unchanged (Java smoke test passes
  untouched); internals on the DSL path — also picks up 2_Dense heads the
  legacy facade silently ignored.
- kbert-cli Main/Demo rewritten onto the factory; Main accepts an HF repo
  id directly (kbert MongoDB/mdbr-leaf-mt "query" "doc").

Gates: BertEmbeddingModelTest (semantic ranking + HF download) on real
mdbr-leaf-mt; BertLeafReferenceSmokeTest green on rewritten KBertJava;
CLI e2e produces 1024-dim projected embeddings, paraphrase cosine 0.92.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deleted: BertRuntime, BertRuntimeWeights/BertLayerWeights, loadBertWeights/
BertWeightMapper/BertTensorNames (BertWeightLoader.kt), BertIngestion, and
their tests (BertRuntimeTest superseded by BertEncoderRuntimeTest;
BertLegacyParityTest served as the migration bridge and dies with the class
it validated). BertModelConfig + MDBR_LEAF_IR_CONFIG live on in
BertConfig.kt, same package. PooledExecutionContext and
HuggingFaceTokenizer stay.

BCV dumps regenerated (jvm via apiDump; android reconciled by the same
class-level delta — commonMain-only changes, verified structurally).
CHANGELOG gains the full Unreleased entry incl. the BREAKING migration
notes. README BERT row upgraded to 'verified'. Docs pages
(tutorials/embeddings, explanation/embeddings, dsl-vs-handcoded,
specs/spring-ai-adapter) rewritten onto the real API — the previous
examples referenced loaders that never existed (loadGGUF, fromGguf).

git grep BertRuntime now hits only historical prose in llm-core KDoc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@michalharakal

Copy link
Copy Markdown
Contributor Author

Engine-side permute fix SKaiNET-developers/SKaiNET#803 is now merged into the engine's develop (merge commit 9cb2ee8, includes regression tests).

No change needed in this PR: it builds against the released engine 0.35.0, where the executor bug still exists, so the axes-aware permute override in LLMFusedOpHandlers remains load-bearing here. Once this repo bumps to the first engine release containing the fix, the override (handler object + one registerFusedOp line in llm-core/.../graph/LLMFusedOpHandlers.kt) can be dropped — the code comment and CHANGELOG entry already flag it.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant