embeddings: chunk over-long GGUF inputs instead of truncating

Inputs beyond the context window are split into context-sized token
windows, each embedded, and combined with a token-count-weighted mean
(then normalized) — no content dropped. Single-chunk inputs keep the
direct path. Pairs with raising the GGUF embedders' n_ctx in config.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
parent 4ace5612
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.1.39" __version__ = "0.1.40"
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even # expandable_segments lets the allocator return freed pages to the driver even
......
...@@ -446,20 +446,47 @@ def _embed_texts(model_obj, texts: List[str], dimensions=None) -> List[List[floa ...@@ -446,20 +446,47 @@ def _embed_texts(model_obj, texts: List[str], dimensions=None) -> List[List[floa
_tok_limit = max(16, model.n_ctx() - 8) _tok_limit = max(16, model.n_ctx() - 8)
except Exception: except Exception:
_tok_limit = 2040 _tok_limit = 2040
def _embed_one(chunk_text):
emb = model.embed(chunk_text)
if emb and isinstance(emb[0], (list, tuple)):
n = len(emb)
emb = [sum(col) / n for col in zip(*emb)]
return emb
for t in texts: for t in texts:
# Truncate to the context window — an over-long input would abort # An input longer than the context window would abort llama.cpp
# llama.cpp (GGML_ASSERT) and take the whole engine down with it. # (GGML_ASSERT) and take the whole engine down. Instead of cutting
# the text, CHUNK it into context-sized token windows, embed each,
# and combine with a token-count-weighted mean — no content is
# dropped, and single-chunk inputs take the direct path.
_chunks = [(t, 1)]
try: try:
_toks = model.tokenize(t.encode('utf-8', 'ignore'), _toks = model.tokenize(t.encode('utf-8', 'ignore'),
add_bos=True, special=False) add_bos=True, special=False)
if len(_toks) > _tok_limit: if len(_toks) > _tok_limit:
t = model.detokenize(_toks[:_tok_limit]).decode('utf-8', 'ignore') _chunks = []
for _i in range(0, len(_toks), _tok_limit):
_w = _toks[_i:_i + _tok_limit]
_chunks.append(
(model.detokenize(_w).decode('utf-8', 'ignore'),
len(_w)))
except Exception: except Exception:
pass pass
emb = model.embed(t) if len(_chunks) == 1:
if emb and isinstance(emb[0], (list, tuple)): emb = _embed_one(_chunks[0][0])
n = len(emb) else:
emb = [sum(col) / n for col in zip(*emb)] _acc = None
_tot = 0
for _ct, _cw in _chunks:
_e = _embed_one(_ct)
if _acc is None:
_acc = [x * _cw for x in _e]
else:
for _j, _x in enumerate(_e):
_acc[_j] += _x * _cw
_tot += _cw
emb = [x / _tot for x in _acc]
norm = math.sqrt(sum(x * x for x in emb)) or 1.0 norm = math.sqrt(sum(x * x for x in emb)) or 1.0
results.append([x / norm for x in emb]) results.append([x / norm for x in emb])
elif backend == 'vision': elif backend == 'vision':
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment