fix(embedding): re-normalize per-item index after batch split - #5240
Merged
Conversation
Contributor
There was a problem hiding this comment.
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.
`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
force-pushed
the
fix/embedding-batch-index
branch
from
July 25, 2026 11:44
3ecd724 to
6edbf41
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Under concurrency, the
/v1/embeddingsendpoint intermittently returns responses whosedata[].indexis out of range: a 2-input request can come back withindex = [1, 2]instead of[0, 1]. The HTTP response is otherwise correct (200, correct count and dimensions, vectors still in input order) — only theindexlabels are wrong. This violates the OpenAI-compatible contract thatdata[].indexmust cover[0, n), and clients that rely onindexto map vectors back to their inputs will silently mismatch them.Root cause
EmbeddingModel.create_embedding.batch(xoscar batching) works in four steps:_create_embeddingonce on the whole concatenated list.EmbeddingData.indexwithenumerateover the entire batch (global0..N-1), e.g.sentence_transformers/core.py.data[offset:offset+n].Step 4 slices the correct vectors for each caller, but every
EmbeddingData.indexstill holds the global batch value — the splitter never re-normalizesindexto a per-call[0, n). So any call whose inputs land atoffset > 0leaks the global index ([1, 2]instead of[0, 1]).The defect is in the shared
EmbeddingModelbase, 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=Trueby default) and one sits at a non-zero offset.Fix
After slicing, rebuild each
EmbeddingDatawith a localindexof0..n-1, preservingobjectandembeddingverbatim. Minimal, localized change — no effect on grouping, sorting, the single-request (non-batch) path, vectors, count, dimensions, orusage. 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 stubEmbeddingModelwhose_create_embeddingreproduces the real engines' global-enumerate behaviour, with no model download. It callscreate_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
indexlabel is corrected. No changes to request/response schemas, model registration, CLI flags, or the non-batch path.🤖 Generated with Claude Code