Skip to content

fix(embedding): re-normalize per-item index after batch split - #5240

Merged
qinxuye merged 1 commit into
xorbitsai:mainfrom
m199369309:fix/embedding-batch-index
Jul 25, 2026
Merged

fix(embedding): re-normalize per-item index after batch split#5240
qinxuye merged 1 commit into
xorbitsai:mainfrom
m199369309:fix/embedding-batch-index

Conversation

@m199369309

Copy link
Copy Markdown
Collaborator

Problem

Under concurrency, the /v1/embeddings endpoint intermittently returns responses whose data[].index is out of range: a 2-input request can come back with index = [1, 2] instead of [0, 1]. The HTTP response is otherwise correct (200, correct count and dimensions, vectors still in input order) — only the index labels are wrong. This violates the OpenAI-compatible contract that data[].index must cover [0, n), and clients that rely on index to map vectors back to their inputs will silently mismatch them.

Root cause

EmbeddingModel.create_embedding.batch (xoscar batching) works in four steps:

  1. Group concurrent calls by kwargs and concatenate their inputs into one list (recording per-call offsets).
  2. Call _create_embedding once on the whole concatenated list.
  3. Engines number EmbeddingData.index with enumerate over the entire batch (global 0..N-1), e.g. sentence_transformers/core.py.
  4. Split the result back per call: data[offset:offset+n].

Step 4 slices the correct vectors for each caller, but every EmbeddingData.index still holds the global batch value — the splitter never re-normalizes index to a per-call [0, n). So any call whose inputs land at offset > 0 leaks the global index ([1, 2] instead of [0, 1]).

The defect is in the shared EmbeddingModel base, so it affects every batched embedding engine (sentence_transformers, flag, vllm, llama_cpp). It is intermittent: it only triggers when ≥2 concurrent requests land inside the batch window (XINFERENCE_BATCH_INTERVAL, default 3 ms; allow_batch=True by default) and one sits at a non-zero offset.

Fix

After slicing, rebuild each EmbeddingData with a local index of 0..n-1, preserving object and embedding verbatim. Minimal, localized change — no effect on grouping, sorting, the single-request (non-batch) path, vectors, count, dimensions, or usage. Works for both dense (List[float]) and sparse (Dict[str, float]) embeddings.

 for (offset, n), idx in zip(offsets, indices):
     data = embedding_list["data"][offset : offset + n]
+    # Re-normalize the per-item index to [0, n) for this caller.
+    data = [
+        EmbeddingData(
+            index=j, object=item["object"], embedding=item["embedding"]
+        )
+        for j, item in enumerate(data)
+    ]
     result = Embedding(...)

Test

Adds xinference/model/embedding/tests/test_embedding_batch.py: a stub EmbeddingModel whose _create_embedding reproduces the real engines' global-enumerate behaviour, with no model download. It calls create_embedding.batch(...) directly to deterministically place one call at a non-zero offset:

  • test_create_embedding_batch_normalizes_index_per_request — a multi-input call at offset 1 must return [0, 1] (not [1, 2]), and the vectors must be unchanged.
  • test_create_embedding_batch_single_input_at_nonzero_offset — a single-input call batched behind another must still return [0].

Both fail on unfixed code ([1, 2] != [0, 1]) and pass after the fix; runs in ~0.01 s.

Compatibility

Backward compatible: only the index label is corrected. No changes to request/response schemas, model registration, CLI flags, or the non-batch path.

🤖 Generated with Claude Code

@XprobeBot XprobeBot added the bug Something isn't working label Jul 25, 2026
@XprobeBot XprobeBot added this to the v3.x milestone Jul 25, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses an issue where concurrent batched embedding requests leak global batch indices to individual callers instead of returning normalized indices starting from 0. It resolves this by re-normalizing the per-item index to [0, n) for each caller and adds corresponding unit tests. The review feedback suggests optimizing this process by updating the indices in-place rather than reconstructing the EmbeddingData objects, which also eliminates the need to import EmbeddingData.

Comment thread xinference/model/embedding/core.py Outdated
Comment thread xinference/model/embedding/core.py Outdated
`create_embedding.batch` concatenates inputs from concurrent calls, runs
`_create_embedding` once over the full list, then slices the result back
per call. Engines number `EmbeddingData.index` over the *entire*
concatenated batch (global enumerate), but the splitter only sliced the
vectors and never re-normalized `index`. A call whose inputs land at a
non-zero offset therefore leaks the global batch index (e.g. `[1, 2]`
instead of `[0, 1]`), violating the OpenAI-compatible contract that
`data[].index` covers `[0, n)`.

Triggered whenever >=2 concurrent embedding requests fall inside the
batch window (default `XINFERENCE_BATCH_INTERVAL=0.003`,
`allow_batch=True`) and one lands at offset > 0, so it surfaces
intermittently under real load.

Fix: after slicing, set each item's `index` in place to `0..n-1` (the
throw-away batch result and disjoint per-caller slices make mutation
safe), leaving all other fields verbatim — covers both dense and sparse
embeddings. Minimal, engine-agnostic change in the shared
`EmbeddingModel` base; all batched embedding engines
(sentence_transformers / flag / vllm / llama_cpp) benefit.

Adds a white-box regression test with a stub model that reproduces the
global-enumerate behaviour deterministically (no model download); it
fails on unfixed code (`[1, 2] != [0, 1]`) and passes after the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@m199369309
m199369309 force-pushed the fix/embedding-batch-index branch from 3ecd724 to 6edbf41 Compare July 25, 2026 11:44

@qinxuye qinxuye left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@qinxuye
qinxuye merged commit 38c2cdc into xorbitsai:main Jul 25, 2026
11 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants