[None][perf] executor: batch RPC submit to relieve rank-0 GIL contention - #15109
Closed
lancelly wants to merge 1 commit into
Closed
[None][perf] executor: batch RPC submit to relieve rank-0 GIL contention#15109lancelly wants to merge 1 commit into
lancelly wants to merge 1 commit into
Conversation
On the RPC executor path, rank 0 is the sole RPC ingress for the whole instance and also runs the co-located rank-0 worker. Every request therefore costs one pickle/HMAC + ZMQ round-trip + run_in_executor dispatch + GIL acquisition on rank 0, all competing with the executor loop. That per-request overhead scales with concurrency and starves the loop at high load (seen in nsys as RpcWorker.submit contending with the worker loop at high concurrency, e.g. c2048). Add an opt-in batched-submit path that coalesces requests on the proxy into a single submit_batch RPC, collapsing those fixed per-request costs by ~the batch size: - RpcWorkerMixin.submit_batch(): enqueue a list of requests in one RPC. The C++ enqueue releases the GIL, so the loop yields between requests. - RpcExecutorMixin: buffer requests, flush on size (TLLM_RPC_SUBMIT_BATCH_MAX) or after a short delay (TLLM_RPC_SUBMIT_BATCH_DELAY_MS). The time-triggered flush runs on the proxy main loop and uses remote_future to avoid blocking it. - The GenerationResult is registered before sending so a response can never race ahead of result tracking. Disabled by default (TLLM_RPC_SUBMIT_BATCH_MAX=1); no behavior change unless opted in. Signed-off-by: Lance Liao <108499334+lancelly@users.noreply.github.com>
4 tasks
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
On the RPC executor path (
GenerationExecutorRpcProxy/RpcWorker), rank 0 is the sole RPC ingress for the whole instance (RpcWorkerMixin.start_rpc_serverbinds the server only onrank == 0) and also runs the co-located rank-0 model worker. Every request therefore pays, on rank 0:pickle+ HMAC of theGenerationRequest(incl.prompt_token_ids) on send and the matchingpickle.loadson receive,run_in_executordispatch on the RPC server thread pool,all competing with the executor loop on the same GIL. This per-request overhead scales with concurrency, so at high load the executor loop (worker0) gets starved. In nsys this shows up as
RpcWorker.submitholding the GIL while the worker loop waits — mild at low concurrency (e.g. c400) but severe at high concurrency (e.g. c2048).#14889 (avoid
deepcopyofprompt_token_idson enqueue) cut a per-request constant; it does not change how the cost scales with concurrency.Change
Add an opt-in batched-submit path that coalesces requests on the proxy into a single
submit_batchRPC, collapsing the fixed per-request costs (RPC framing, pickle/HMAC call, ZMQ op, dispatch, GIL acquisition count) by ~the batch size:RpcWorkerMixin.submit_batch(requests)— enqueue a list of requests in one RPC. The C++ enqueue insidesuper().submit()releases the GIL, so the loop yields naturally between requests.RpcExecutorMixin— buffer requests and flush on size (TLLM_RPC_SUBMIT_BATCH_MAX) or after a short delay (TLLM_RPC_SUBMIT_BATCH_DELAY_MS). The time-triggered flush runs on the proxy main loop and usesremote_future(non-blocking) so it never blocks the loop or the response path.GenerationResultis now registered before sending, so a response can never arrive for an untrackedclient_id.This is orthogonal to (and stacks with) shrinking the per-request payload (e.g. shipping
prompt_token_idsas a raw ZMQ frame instead of pickling) — batching cuts the count-based overheads, payload work cuts the bytes-based ones.Knobs (env)
TLLM_RPC_SUBMIT_BATCH_MAX1(disabled)>1enables batchingTLLM_RPC_SUBMIT_BATCH_DELAY_MS0.5Disabled by default — no behavior change unless opted in.
Test plan
python-gil— fraction of time worker0 spends in GIL-wait per decode step, with batching off vs on; sweepTLLM_RPC_SUBMIT_BATCH_MAX∈ {8,16,32,64}.