embeddings: chunk a long input instead of silently keeping its opening (v0.3.72)
Only the GGUF path ever chunked. Every other backend passed `truncation=True`
and returned a confident UNIT vector of roughly the first page of a long
document, with nothing in the response to say so:
truncation=True, max_length=1800 # GME-Qwen2-VL, a hardcoded ceiling
truncation=True, max_length=max_length # bge-m3 native
model.encode(texts, ...) # sentence-transformers, its own cap
Not an error -- a wrong answer that looks right, which is the failure mode this
codebase keeps paying for. A caller had no way to tell an embedding of a
document from an embedding of its first page.
Now a long input is windowed to the model's real limit, every window of every
input goes to the backend in ONE batch (a 40-page document must not become 40
round trips), and the chunk vectors are mean-pooled AND RE-NORMALIZED: mean
pooling unit vectors does not give a unit vector, and cosine similarity on a
non-unit vector is quietly wrong, so the renormalization is part of the contract
rather than a tidy-up.
Tunable per-model -> env -> default, the convention the admission gate already
uses:
embed_long_input chunk | truncate | error (default chunk)
embed_chunk_pool mean | first (default mean)
`truncate` stays reachable and is now DELIBERATE: for a short-text encoder a
40-page input is usually a caller bug, and an operator may prefer the old
behaviour to a vector pooled from forty windows. `error` says so out loud.
AND IT IS REPORTED: resp.chunked = {"windows": [...], "pool": ...} whenever any
input needed more than one. Replacing a silent truncation with a silent pooling
would have been the same bug with a better answer.
The GGUF path's hard-won guard now covers every backend: each window is
RE-ENCODED and shrunk until it verifiably fits, because a detokenize->retokenize
round trip is not token-count-stable (specials and BOS are re-added) and a
window cut at exactly the limit can re-inflate past it -- which is how that path
first tripped llama.cpp's GGML_ASSERT.
Wired: sentence-transformers, and GME-Qwen2-VL (windowed BEFORE the chat prompt
is built around the text, since the processor's own truncation would cut the
template's closing tokens as readily as the body). GGUF already chunked.
NOT wired, on purpose: the CLIP family (clip, geoclip, vpr) takes 77 tokens BY
DESIGN -- those encoders embed captions, not documents, and pooling forty
windows of a contract into a CLIP vector would be meaningless. A test asserts
this so "chunk everywhere" is not later read as "chunk where it makes no sense".
NOT wired, needs a decision: bge-m3's native multi-vector path returns dense
PLUS sparse and colbert. Dense mean-pools fine, but sparse would need
max-merging per token id across windows and colbert would concatenate per-token
vectors. Those are retrieval semantics, not a mechanical change, and improvising
them would degrade search in a way that looks like a model problem.
26 tests. Suite at the 25 pre-existing failures.
Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXRMe9ZPUQCCCtBai9uqt3
Showing
This diff is collapsed.
Please
register
or
sign in
to comment