fix(embeddings): truncate oversized input, isolate per-chunk failures - #68
Open
vvooki-sys wants to merge 1 commit into
Open
fix(embeddings): truncate oversized input, isolate per-chunk failures#68vvooki-sys wants to merge 1 commit into
vvooki-sys wants to merge 1 commit into
Conversation
The local ONNX embedder built tensors at the tokenized length, but the
BERT/XLM-R checkpoints bake 512 position embeddings into the graph, so a
longer chunk failed in model.run() with "Invalid range 0..N for slicing
1,512". embed_batch aborted at the first `?` and flush_batch cleared the
whole batch on Err, so one oversized chunk cost up to 50 chunks their
embedding. Measured on the real model: a 5130-char chunk in a batch of
five yielded 0 embeddings for all five. That is the mechanism behind the
"buried entity" incident.
Two layers, because the first without the second leaves the bomb armed:
- LocalEmbedder::load now sets TruncationParams { max_length: 512 } on
the tokenizer unless the checkpoint declares its own truncation. Doing
it on the tokenizer keeps the post-processor's special tokens correct
(max_length counts them) instead of slicing tensors by hand.
- embed_batch_lenient returns one Result per input, so a failing chunk
costs only itself. flush_batch pairs requests to results by index and
logs each failure with its chunk_id; the old drain().zip() silently
dropped the tail whenever the embedder returned fewer vectors than
requests.
The OpenAI path (llm::embed_batch) is deliberately untouched — same class
of problem, separate scope. flush_batch no longer makes it worse: a
failed call is expanded into one reported error per chunk instead of a
silent batch clear.
Regression tests (ignored, need LOOMEM_TEST_EMBED_MODEL): batch of five
with one 5130-char member now stores 5/5 embeddings through flush_batch
against a real RocksDB, and a 10k-char text embeds to a 384-dim vector.
Signed-off-by: Łukasz Gumowski <lukasz.gumowski@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Comment on lines
+61
to
+68
| if tokenizer.get_truncation().is_none() { | ||
| tokenizer | ||
| .with_truncation(Some(TruncationParams { | ||
| max_length: DEFAULT_MAX_SEQUENCE_TOKENS, | ||
| ..Default::default() | ||
| })) | ||
| .map_err(|e| anyhow::anyhow!("Failed to set tokenizer truncation: {}", e))?; | ||
| } |
There was a problem hiding this comment.
Configured tokenizer limit can exceed model capacity
load applies the 512-token cap only when the tokenizer has no truncation configuration. A checkpoint whose tokenizer.json already declares, for example, max_length: 1024 keeps that value, and embed forwards the resulting sequence length into the ONNX inputs. For models with the documented 512-position window, long chunks then fail in model.run rather than being truncated. Clamp existing tokenizer truncation to the model-supported maximum before encoding.
Artifacts
- The authored executable reads the production source and exercises the existing-versus-absent tokenizer truncation branches, showing the existing 1024-token value is preserved above the 512-token model capacity.
- The executed Python check reports that an existing 1024-token truncation survives load, exceeds 512, and proceeds to tensor construction and model run, confirming the defect.
- The attempted ignored local-embedding regression test compiled dependencies but stopped at clang-sys because llvm-config and libclang were unavailable, so a real ONNX inference failure could not be captured.
- The executed git diff check returned exit code 0 for local_embeddings.rs, confirming the validation did not modify production code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mechanism
LocalEmbedder::embedtokenized the text and built tensors at the tokenized length. The ONNX checkpoints in the BERT / XLM-R family — including the defaultmultilingual-e5-small— bake 512 learned position embeddings into the graph, so a longer sequence fails insidemodel.run():Two amplifiers turned that into data loss:
embed_batchdidresults.push(self.embed(text)?)— the first error aborted the whole loop.flush_batchtreated theErrbranch as fatal for the batch:warn!+batch.clear(), withbatch_size = 50.So one oversized chunk cost up to 49 healthy neighbours their embedding. Verified earlier on the live v0.5.3 engine: 5 chunks including one 4830-char member →
tantivy_docs +5,embeddings +0. This is the mechanism behind the "buried entity" incident.Measurement (real model,
multilingual-e5-small)A throwaway probe against the real ONNX model, before any code change:
After the fix, the same batch: 5 embeddings for 5 chunks, and end-to-end through
flush_batchagainst a real RocksDB, 5/5 chunk ids have a 384-dim vector.Change
local_embeddings.rsloadsetsTruncationParams { max_length: 512 }on the tokenizer, but only when the checkpoint'stokenizer.jsondeclares no truncation of its own (ours doesn't). Truncating on the tokenizer rather than slicing tensors by hand keeps the post-processor's[CLS]/[SEP]correct — HF subtracts the added special tokens frommax_lengthinpost_process.const DEFAULT_MAX_SEQUENCE_TOKENS = 512with a comment on where the number comes from. Reading the limit from the model was considered and rejected: the ONNX inputs are dynamic (batch, seq), the limit lives in the positional weights, and the model dir ships noconfig.jsonwithmax_position_embeddings— any read would be guesswork.embed_batch_lenient(&self, texts: &[String]) -> Vec<Result<Vec<f32>>>in bothimplbranches (feature and stub). Failures are returned, not logged — the caller owns thechunk_id.embed_batch(all-or-nothing) is left in place; the/v1/embed-missingbackfill handler still uses it.embedding_queue.rsflush_batchis now per-element. A new privateembed_allreturnsVec<Result<Vec<f32>>>with exactly one entry per request on both paths.batch.drain(..).zip(embeddings)silently dropped the tail whenever the embedder returned fewer vectors than requests.pad_tofills missing slots with explicitErrs, so a "lost" chunk is now reported rather than swallowed.Errbranch no longer callsbatch.clear(). Each failure is onewarn!carrying itschunk_id; the aggregate counter stays as a summary alongside, not instead.Tests
New,
#[ignore]d because they need the real model — run with:embed_truncates_oversized_input— 10 320 chars → 384-dim, L2-normalized vector (wasErr).lenient_batch_survives_oversized_member— 5 texts, one 5130 chars → 5/5Ok.flush_stores_every_chunk_despite_oversized_member— end-to-end: realLocalEmbedder+RocksDbStorein a tempdir, assertsget_embeddingfor all five chunk ids.Plus three plain unit tests for the index-alignment helpers (
pad_to,all_failed) that need no model and run in CI.Deliberately out of scope
llm::embed_batch) is untouched. Same class of problem — one failed call means no vectors for the whole request — but the constraint there is API-side, not our tensor, and it deserves its own change.flush_batchno longer deepens it: a failed call is expanded into one reported error per chunk instead of a silent batch clear.handlers/admin.rs(/v1/embed-missing) is untouched. It is a god file needing its ownCritical file rationale:, and after the truncation fix the backfill stops failing on long chunks anyway, becauseembedno longer returnsErrfor them.Gate
cargo fmt --check— greencargo clippy --workspace -- -D warnings— greencargo test --workspace— 0 failedcargo check -p loomem-core --no-default-features— green (stub branch)polish_semantic_similarity) — still passing; truncation does not touch short textsGreptile Summary
This change truncates oversized local-embedding inputs and lets queued chunks retain successful embeddings when another chunk fails. Validation confirmed that
loomem-core/src/local_embeddings.rsstill permits a custom tokenizer configuration with a limit above the ONNX model's 512-token positional window, so sufficiently long text can reach inference and fail instead of being truncated.Confidence Score: 4/5
The change is not ready to merge for custom local embedding checkpoints until configured tokenizer limits are constrained to the ONNX model capacity.
A focused executable check confirmed that an existing 1024-token tokenizer limit remains in effect despite the 512-token model window, and the resulting sequence length is forwarded into ONNX inference.
Files Needing Attention:
loomem-core/src/local_embeddings.rsneeds to validate or clamp existing tokenizer truncation settings before embedding input is encoded.What T-Rex did
Comments Outside Diff (1)
General comment
loomem-core/src/local_embeddings.rs:61-68, the 512-token truncation is installed only whentokenizer.get_truncation()isNone. A customtokenizer.jsonthat already specifiesmax_length: 1024is retained.embeduses the encodedseq_lendirectly for all input tensors (:94-100) and passes them tomodel.run(:102-109). Thus an input above a 512-position ONNX model's capacity can reach inference and fail instead of being capped.max_lengthagainst the fixedDEFAULT_MAX_SEQUENCE_TOKENSmodel window.Reviews (1): Last reviewed commit: "fix(embeddings): truncate long input, is..." | Re-trigger Greptile