[None][perf] executor: memcpy int32 token buffer into tle::Request ctor#15211
Conversation
|
/bot run --disable-fail-fast |
|
PR_Github #53284 [ run ] triggered by Bot. Commit: |
|
PR_Github #53284 [ run ] completed with state |
1684a55 to
28979a1
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #53595 [ run ] triggered by Bot. Commit: |
|
PR_Github #53595 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #53718 [ run ] triggered by Bot. Commit: |
|
PR_Github #53718 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #53762 [ run ] triggered by Bot. Commit: |
|
PR_Github #53762 [ run ] completed with state
|
28979a1 to
a7ebe37
Compare
On the hot proxy->worker RPC-submit path, constructing tle::Request from a list[int] of prompt token-ids costs O(ISL): pickling emits one PyLong frame per token on the wire, and the nanobind ctor does an element-wise cast into VecTokens -- all on the GIL-held submit thread. - request.cpp: add `toVecTokens`, which memcpy's a 1-D contiguous int32 ndarray straight into VecTokens (list[int] falls back to the element-wise cast, so the binding stays backward compatible). - request.py: GenerationRequest.__getstate__/__setstate__ encode token-ids as int32 bytes for the wire (no per-token PyLong frame). On decode the int32 ndarray is stashed in `_prompt_token_ids_i32` for the ctor memcpy and the list is NOT eagerly rebuilt; the `prompt_token_ids` list is materialized lazily-and-cached via a property only if a consumer actually reads it. Implemented as a property (scoped to this one attribute) rather than a class-level __getattr__, which would fire on every missing-attribute access (hasattr/getattr-default probes, pickle/ copy dunder lookups) -- same lazy behavior, zero blast radius on other attributes. - base_worker._enqueue_request: hand the int32 buffer to the ctor when present; prompt_token_ids stays a list[int] for all other consumers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
a3699f2 to
57aaadf
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #54141 [ run ] triggered by Bot. Commit: |
|
PR_Github #54141 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54172 [ run ] triggered by Bot. Commit: |
|
PR_Github #54172 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54290 [ run ] triggered by Bot. Commit: |
|
PR_Github #54290 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54405 [ run ] triggered by Bot. Commit: |
|
PR_Github #54405 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54459 [ run ] triggered by Bot. Commit: |
|
PR_Github #54459 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54534 [ run ] triggered by Bot. Commit: |
|
PR_Github #54534 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54583 [ run ] triggered by Bot. Commit: |
|
PR_Github #54583 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54688 [ run ] triggered by Bot. Commit: |
|
PR_Github #54688 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #54702 [ run ] triggered by Bot. Commit: |
|
PR_Github #54702 [ run ] completed with state |
…or (NVIDIA#15211) Source-Commit: c4cd713 Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
…or (NVIDIA#15211) Source-Commit: c4cd713 Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
…or (NVIDIA#15211) Source-Commit: c4cd713 Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
…event loop
Activates the Phase 1 lazy int32 submit path end to end. The GEN OpenAIServer
now keeps the base64-decoded prompt_token_ids as an int32 ndarray (drops the
O(ISL) `.tolist()`), and the metadata-only placeholder uses `np.full` (a single
C-level fill) instead of `[pad] * N`. The ndarray flows through unchanged:
* inputs.prompt_inputs: new 1-D ndarray branch -> TokensPrompt (no copy).
* LLM._preprocess: the multimodal fast-path guard now tests
`prompt_token_ids is not None` instead of truthiness (a numpy array has an
ambiguous truth value); the text-only path already extracts by key.
* executor.generate_async accepts np.integer[0] (Phase 1) and
GenerationRequest stashes the int32 ndarray as the lazy `_prompt_token_ids_i32`
buffer, memcpy'd into the C++ Request ctor.
Net effect: on the single GEN asyncio event loop -- which serializes every
concurrent request and dominates gen_preprocessing/TTFT at high concurrency -- the
prompt token array is never materialized into a Python list. The list is built
lazily only if a consumer reads the values (prompt logprobs); the plain disagg-gen
forward path never does. Unlike the length-only placeholder this carries the REAL
tokens, so it is correct for all sampling features and composes with the base64
relay (NVIDIA#15698) and the C++ ctor memcpy (NVIDIA#15211): CTX base64 -> orchestrator string
relay -> GEN int32 ndarray (lazy) -> C++ memcpy, no Python list on the path.
`request.prompt_token_ids` (ChatCompletionRequest, OpenAIBaseModel: no
validate_assignment) keeps the ndarray on assignment. Needs a GPU smoke test to
confirm end-to-end TTFT/throughput; CPU unit test covers ndarray preservation
through prompt_inputs + the lazy GenerationRequest path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
…tadata-only + int32 lazy) Reduces the GEN OpenAIServer's per-request front-end cost -- the ISL-sized prompt token array on the single asyncio event loop that dominates gen_preprocessing at high concurrency -- via two opt-in, correctness-guarded paths. 1. gen_tokids_metadata_only: the orchestrator sends the GEN worker only prompt_len; the GEN worker builds a length-only placeholder and generation uses the KV transferred from the context worker (the forward path never dereferences prompt token VALUES, and seq_len/position come from prompt_len). GUARDRAIL: this fast path is taken only when the context worker reported a prompt_len AND the request does not read prompt token VALUES -- sampling penalties (frequency / presence / repetition) or prompt echo -- which the placeholder would silently corrupt; otherwise it falls back to relaying the real tokens. Deployment constraints: text-only, non-harmony, and GEN KV block reuse OFF (block-reuse keys blocks by token-value hash; a placeholder would collide across prompts and reuse the wrong KV). 2. int32-ndarray lazy submit path (correct for ALL requests). The GEN server keeps the base64-decoded prompt_token_ids as an int32 ndarray (drops the O(ISL) .tolist()); prompt_inputs gains a 1-D ndarray branch; LLM._preprocess guards prompt_token_ids with `is not None` (an ndarray truth value is ambiguous); executor.generate_async accepts np.integer; and GenerationRequest stashes the ndarray as the lazy `_prompt_token_ids_i32` buffer, memcpy'd into the C++ Request ctor. The Python list is built lazily only if a consumer reads the values (prompt logprobs); the plain disagg-gen path never does. Unlike the placeholder this carries the REAL tokens, so it is correct under all sampling features and composes with the base64 relay (NVIDIA#15698) and the ctor memcpy (NVIDIA#15211): CTX base64 -> orchestrator string relay -> GEN int32 ndarray (lazy) -> C++ memcpy, with no Python list built anywhere on the path. Both opt-in (default off). Note: the int32 path does not remove the ISL request body from the wire (only metadata-only does) and neither addresses the SSE output-streaming load on the same event loop; the dominant "wait" (queuing) at very high concurrency needs metadata-only body removal for all requests (off-loop real-token delivery) and/or multiple front-end processes. Tested: CPU unit tests (int32 laziness + real-value read + pickle round-trip; prompt_inputs ndarray preservation; the metadata-only guardrail predicate). Needs a GPU disagg smoke test for end-to-end TTFT/throughput/accuracy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
Description
RpcWorker.submit→BaseWorker._enqueue_requestbuilds atle::Requestfromprompt_token_idson a GIL-held thread. With a Pythonlist[int], nanobind casts it tostd::vector<int32>element-by-element (one PyLong read per token) — O(ISL). In the decode phase (short forward steps, host-bound iteration) this stalls the PyExecutor loop on the GIL.This PR makes the token ids cross the proxy→worker boundary as an int32 buffer and
memcpys them into the C++ vector, and avoids rebuilding the Pythonlist[int]on the decode path entirely — while keepingprompt_token_idsa plainlist[int]for every Python consumer that does read it.What it does (3 parts)
request.cpp—tle::Requestctor gains a buffer fast-path (toVecTokens): a 1-D contiguous int32 ndarray ismemcpy'd intoVecTokens;list[int]still works via the default cast (back-compatible).request.py—GenerationRequest.__getstate__/__setstate__encode token-id lists as int32 bytes on the wire (no per-token PyLong frame). On decode they do not eagerly rebuild the list: the int32 ndarray is stashed in a private_prompt_token_ids_i32side-channel for the memcpy ctor, andprompt_token_idsis exposed as a property that materializes thelist[int]lazily (and caches it) only if a consumer actually reads it. Plain decode never reads it → the O(ISL).tolist()never runs.base_worker.py—_enqueue_requesthands_prompt_token_ids_i32to the memcpy ctor when present (no prompt adapter); otherwise the list path.prompt_token_idsalways returnslist[int], so list-assuming consumers (prompt-logprobs concat, tracing truthiness, star-attention, theList[int]result property) are unaffected.Why a lazy property (not ndarray-everywhere, not
__getattr__)prompt_token_idsitself an ndarray breakslist[int]consumers (tokens[1:] + firstsilently broadcasts;if <ndarray>raises). The property keeps theList[int]contract and confines the buffer to the C++ ctor.__setstate__would re-add an O(ISL).tolist()per request for nothing. Materializing on first read avoids it.__getattr__: a__getattr__is invoked by the interpreter on every missing-attribute access on the object (hasattr/getattr(default)probes, copy/pickle dunder lookups, duck-typing), each paying a Python frame + anAttributeErrorraise instead of the C-level miss. A property is scoped to this one attribute → identical lazy behavior, zero blast radius. (microbench, ISL=40k: one attribute miss 0.06 µs with a property vs 0.51 µs with__getattr__.)Measured (nsys, ctx-only, rank0 RPC worker, base vs fix)
rpc_worker_submit(_enqueue_request)rpc_token_preprpc_request_ctorrpc_engine_enqueueConfirmed in the decode-exposed regime (disagg GEN worker, c2880, all 8 ranks):
RpcWorker.submit≈ 146 µs (rank0) and GPU idle between decode iters drops 14.9% → 11.1%, with no regression vs the equivalent eager/__getattr__variants.Test Coverage
__getstate__/__setstate__) validated:prompt_token_idsreturns the originallist[int](lazily),_prompt_token_ids_i32matches, re-pickle of an un-read request round-trips, empty-list andquery_token_idspaths covered, and a missing attribute raises a plainAttributeError(no__getattr__).tests/unittest/bindings/test_executor_bindings.py: add a case constructingRequest(input_token_ids=<int32 ndarray>)and asserting round-trip equivalence withlist[int]+ fallback (empty / non-int32 / non-contiguous). (TODO in this PR.)Notes
feat/deepseek_v4(= f04 + [None][perf] reduce rank-0 GIL contention in disaggregated generation #15133 + [None][perf] disagg: serialize Request input_token_ids as int32 bytes #15134).🤖 Generated with Claude Code