• Stefy Lanza (nextime / spora )'s avatar
    embeddings: chunk a long input instead of silently keeping its opening (v0.3.72) · 7f598fdb
    Stefy Lanza (nextime / spora ) authored
    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: 's avatarClaude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01HXRMe9ZPUQCCCtBai9uqt3
    7f598fdb
__init__.py 4.21 KB