perf(embedding): build embedding base64 directly from the tensor (numpy.tobytes) - #10221
Closed
tzulingk wants to merge 2 commits into
Closed
perf(embedding): build embedding base64 directly from the tensor (numpy.tobytes)#10221tzulingk wants to merge 2 commits into
tzulingk wants to merge 2 commits into
Conversation
… path
Stacks on the SHM branch. The SHM fast path already passes raw f32 bytes
(no base64), so opt-2's remaining target is the base64 fallback used when
DYN_EMBEDDING_SHM is off: build the base64 straight from the pooling tensor
via torch -> numpy.tobytes, dropping the per-embedding Python float-list
materialization and the struct.pack("<{N}f", *floats) varargs (DIS-2154 #3).
Output bytes are unchanged on little-endian hosts.
Net effect:
- DYN_EMBEDDING_SHM=1: identical to the SHM branch (raw f32 over /dev/shm).
- DYN_EMBEDDING_SHM=0: faster base64 serialization than the SHM branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Tzu-Ling <tzulingk@nvidia.com>
…64 helpers Factor the detach/cpu/flatten/float32 step into `_flatten_pooling_tensor`, reused by both `_pooling_output_to_list` and `_pooling_output_to_base64` so the tensor-prep isn't duplicated. The fast base64 path still avoids `.tolist()` (it goes tensor -> numpy.tobytes), so no per-element Python work is reintroduced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Tzu-Ling <tzulingk@nvidia.com>
Contributor
Author
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.
Important
Recommended embedding fix. Captures the full −30–35% latency win cross-node with a one-line, behavior-preserving change. Benchmarks (below) show it matches the shared-memory path (#10220) without that PR's same-node constraint or extra failure modes, so it — not SHM — is the one to ship for text embeddings. (Complements the already-merged base64 wire-format change in #10139: that made shipping the payload cheap; this makes producing it cheap.)
Note: this branch is currently stacked on #10220; to merge standalone it should be rebased onto
main(thenumpy.tobyteschange is independent of the shared-memory code).Overview:
Speeds up embedding-response serialization on the worker by building the base64 payload directly from the pooling tensor via
numpy.tobytes, instead of going through a Python float list +struct.packvarargs expansion.The current path is
tensor.detach().cpu().flatten().tolist()→struct.pack("<{N}f", *floats)→ base64. For a batch-15 × 3072-dim response that's 46,080 Python float objects materialized and then unpacked as 46,080 positional args intostruct.pack— an O(N) interpreter-level pass over every element. This PR replaces it withtensor → numpy.tobytes() → base64, a single C-level copy. The emitted base64 bytes are identical on little-endian hosts.Measured on GB200 (Qwen3-Embedding-0.6B, dim 3072): −30% at batch 15 (124 → 87 ms) and −35% at batch 64 (470 → 307 ms) per-request latency. The win grows with batch size, since it scales with the number of floats serialized. It also works cross-node (no shared memory required) and complements the base64-wire-format change in #10139 (this PR makes producing that base64 cheaper; #10139 made shipping it cheaper).
Details:
components/src/dynamo/vllm/handlers.py: new_pooling_output_to_base64()builds base64 straight from the tensor. Shared tensor-prep (detach().cpu().flatten().to(float32)) is factored into a small helper reused by_pooling_output_to_list, so there's no duplicated tensor handling. A list/tuple fallback preserves behavior for non-tensor pooling outputs..to(torch.float32)makes bf16/fp16 pooling outputs match thestruct.pack("<f")width, keeping the emitted bytes identical.Benchmark (GB200, Qwen3-Embedding-0.6B, dim 3072, ISL 80):
Per-request latency (ms, avg). Small batches: rate-limited (no queueing), 200/100 reqs. Large batches: closed-loop
--concurrency 1, 40/25 reqs.tolist+struct.pack)n/m = not measured — the baseline's
struct.pack("<{N}f", *floats)becomes pathological at these batch sizes (millions of positional args). This PR gives −30% (batch 15) to −35% (batch 64) vs baseline and tracks SHM within noise across all sizes; SHM's ~2–5% nominal edge is partly cross-node measurement noise and shrinks against the client-facing float-JSON response, which neither change touches.Where should the reviewer start?
components/src/dynamo/vllm/handlers.py—_pooling_output_to_base64and its use in the embedding response loop.🤖 Generated with Claude Code