Skip to content

Improve embedding fidelity (speedkick) - #1587

Merged
edwinyyyu merged 1 commit into
MemMachine:speedkickfrom
edwinyyyu:fix/embedding-chunking-fidelity-speedkick
Sep 8, 2026
Merged

edwinyyyu merged 1 commit into
MemMachine:speedkickfrom
edwinyyyu:fix/embedding-chunking-fidelity-speedkick

Conversation

@edwinyyyu

Copy link
Copy Markdown
Contributor

Copy of #1449 onto speedkick. Cherry-picked cleanly; no conflicts and no changes were needed. Targeted suite server_tests/memmachine_server/common/embedder passes (5 passed).

Purpose of the change

Chunk-merged embeddings (texts longer than the model input limit are split, embedded per chunk, and averaged) reproduce the whole-text embedding more faithfully with greedy max-size chunks and a length-weighted average than with the current balanced chunks and unweighted mean. Measured on three encoders; numbers below.

OpenAI servers no longer error when special tokens are given to text-embedding-3-small, so the special-token sanitizer is removed (probe script and results attached as a PR comment below).

Description

  • Use greedy max-size chunks (chunk_text) instead of balanced chunks (chunk_text_balanced) in the OpenAI, SentenceTransformer, and Amazon Bedrock embedders.
  • Merge chunk embeddings with a length-weighted np.average instead of unweighted np.mean.
  • Remove special token processing.

Measurements

Method: 10 hand-written texts in distinct registers (fiction, technical docs, chat transcript, news, legal, academic, recipe, business memo, product review, encyclopedia), each short enough to embed whole — the whole-text embedding is the observable ground truth. Each text is chunked at limits 800 / 1600 / 3200 chars under each policy, chunks embedded, merged, and compared to the whole-text vector by cosine similarity. Single-chunk cells excluded; 28 cells per configuration per model. Probe script and raw per-cell results are attached as PR comments below (deliberately not committed to the branch).

This PR's configuration (greedy + length-weighted) vs current behavior (balanced + unweighted), paired over the same 28 cells:

Encoder Current mean cos PR mean cos Paired Δ Cell wins t (df=27)
embeddinggemma-300m (local) 0.9578 0.9644 +0.0066 24/28 4.8
text-embedding-3-small 0.9338 0.9394 +0.0055 18/28 2.3
text-embedding-3-large 0.9237 0.9340 +0.0103 20/28 3.3

Full split × weighting grid (mean cosine to whole-text embedding, higher = more faithful):

Configuration gemma-300m 3-small 3-large
greedy + length-weighted (this PR) 0.9644 0.9394 0.9340
balanced + weighted 0.9578 0.9338 0.9237
balanced + unweighted (current) 0.9578 0.9338 0.9237
greedy + unweighted 0.9356 0.9217 0.9161

Two structural checks that this is signal, not noise:

  • Greedy requires the weighting: greedy+unweighted ranks last on all three encoders. The two changes in this PR are a package.
  • Balanced is indifferent to weighting (its two rows agree to ~5 decimals on every encoder), as expected for equal-size chunks — a built-in sanity check on the harness.

Mechanism: to the extent an encoder mean-pools token states, the whole-text vector is approximately a length-weighted average over the text, which a merge approximates best when weights match chunk lengths and most characters sit in maximal-context chunks. The result holds on the OpenAI models even though their pooling is undocumented.

Caveat: per-cell variance is wider on the OpenAI models than on gemma (worst cell −0.019, best +0.048), so this is a consistent aggregate win rather than a uniform per-text one.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • Unit Test
  • Integration Test
  • End-to-end Test
  • Test Script (please provide)
  • Manual verification (list step-by-step instructions)

Test Results: Fidelity tables above; probe scripts and raw results attached as PR comments below; embedder unit tests pass (server_tests/memmachine_server/common/embedder/).

Checklist

  • I have signed the commit(s) within this pull request
  • My code follows the style guidelines of this project (See STYLE_GUIDE.md)
  • I have performed a self-review of my own code
  • I have commented my code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added unit tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Maintainer Checklist

  • Confirmed all checks passed
  • Contributor has signed the commit(s)
  • Reviewed the code
  • Run, Tested, and Verified the change(s) work as expected

🤖 Generated with Claude Code

https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL

@edwinyyyu
edwinyyyu merged commit ea97369 into MemMachine:speedkick Sep 8, 2026
24 of 39 checks passed
edwinyyyu added a commit to edwinyyyu/MemMachine that referenced this pull request Sep 16, 2026
Improve embedding fidelity

Signed-off-by: Edwin Yu <[email protected]>
This was referenced Sep 16, 2026
edwinyyyu added a commit to edwinyyyu/MemMachine that referenced this pull request Sep 17, 2026
Improve embedding fidelity

Signed-off-by: Edwin Yu <[email protected]>
malatewang added a commit that referenced this pull request Sep 18, 2026
* Improve embedding fidelity (speedkick) (#1587)

Improve embedding fidelity

Signed-off-by: Edwin Yu <[email protected]>

* Clamp max_input_length to the per-request cap in the OpenAI embedder

A configured max_input_length above max_total_input_length_per_request
(75,000 code points) can never be honored: no chunk larger than the cap
fits in any request. Before this change such a limit reached cluster_texts
with an oversized chunk and raised ValueError (HTTP 500), the #1298 failure
for that configuration. Greedy chunking widened which input lengths hit it:
at max_input_length=80_000, balanced chunking crashed on 76,000-80,000 and
160,000-char inputs; greedy crashed on everything above 75,000.

The regression test configures max_input_length=80_000 with a 100,000-char
input; it fails before the clamp with the cluster_texts ValueError and
passes after with two requests (75,000 + 25,000 chars).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

---------

Signed-off-by: Edwin Yu <[email protected]>
Co-authored-by: Shu Wang <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
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