[https://nvbugs/6194812][fix] Restore non-blocking PP send-handle progress in broadcast loop#14850
[https://nvbugs/6194812][fix] Restore non-blocking PP send-handle progress in broadcast loop#14850tensorrt-cicd wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThis PR prevents MPI stalls and deadlocks during PyExecutor shutdown by adding bounded polling of outstanding pipeline-parallel (PP) send handles. When the broadcast queue is empty and about to block, the thread now polls pending MPI sends instead of relying solely on the main loop to advance them. ChangesMPI Progress Polling During Shutdown
🎯 2 (Simple) | ⏱️ ~8 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…st_sample_state_loop`, but routed it t Signed-off-by: Chenfei Zhang <chenfeiz@nvidia.com>
1fc0fe9 to
0005c08
Compare
| # next stage's recv_sample_state stalls (NVBugs/6194812). | ||
| # Use non-blocking test() (bounded poll) instead of wait() to | ||
| # keep the shutdown-safety property from NVBugs/6050489. | ||
| if self.executed_batch_queue.empty(): |
There was a problem hiding this comment.
Looks like that should be under some kind of lock
| def progress_pp_send_handles(self, | ||
| send_handles, | ||
| microbatch_id, | ||
| max_polls: int = 64): |
|
@bo-nv @pcastonguay @Tabrizian FYI since this is a follow-up to #12888 |
|
|
I don't understand why #12888 causes the regression, pr12888 just reverted #12528, which I wrongly introduced. Maybe the baseline is wrong, we need perf data before karen-sy@9a24d1a CC @chenfeiz0326 |
|
/bot run --disable-fail-fast --stage-list "*GB200*PerfSanity*,*GB300*PerfSanity*" |
|
PR_Github #52498 [ run ] triggered by Bot. Commit: |
|
PR_Github #52498 [ run ] completed with state
|
Summary
Restores the per-iteration MPI-progress driver in
_broadcast_sample_state_loopthat was removed by PR #12888 (which fixed a separate PP shutdown hang, NVBugs/6050489).The pipeline-parallel sample-state ring broadcast forwards the sampled token/state between PP ranks with a non-blocking
isend_object(mpi4py pkl5 / UCX rendezvous). For a rendezvous transfer the bulk payload is only pushed when the sender drives the progress engine (ucp_worker_progress), and in the default build that happens only when the application calls into MPI — there is no background progress thread.d1731cb5) removed theif self.executed_batch_queue.empty(): self.wait_on_pp_send_handles(...)flush from_broadcast_sample_state_loopintensorrt_llm/_torch/pyexecutor/py_executor.py. That flush was also the broadcast thread's only MPI-progress driver during the idle gap between decode steps. Without it, after posting theisendthe loop spins in Python and makes no MPI calls, so the sender's UCX worker is never pumped, the payload sits parked, and the next PP stage'srecv_sample_statestalls until the sender incidentally re-enters MPI on the following step. Same kernels and same kernel time — the regression is a host-side PP-bubble dilation (NVBugs/6194812).progress_pp_send_handleshelper that uses a non-blocking, boundedRequest.test()poll (max 64) instead of the original blockingwait().test()pumps the sender's MPI/UCX progress so the parked send flushes immediately, while the bounded non-blocking form cannot block forever if the receiver has already exited — so it stays safe against the shutdown hang from NVBugs/6050489. Surgical change, single file.Test case
The combination of properties is what makes this case a sensitive detector:
pp:4— after the last stage samples, the sampled state is ring-broadcast around all 4 ranks (rank3 → rank0 → rank1 → rank2) so every rank updates its request/KV bookkeeping and rank0 gets the next input token. This ring sits on the per-token critical path, and the 3 hops are a strict sequential chain (each rank forwards the bytes it just received).con:1— a single request in flight; no other microbatch work to overlap, so any broadcast stall is a fully-exposed GPU idle bubble instead of being hidden.input_output_len 8000,1000— 1000 output tokens = 1000 serial decode steps, each paying the stall once. The smallqwen3 0.6bbfloat16model makes per-step compute cheap, so the host-side comm stall dominates step time.Small model + single request + 4-way PP + long decode ⇒ the sample-state broadcast latency is the bottleneck, multiplied over 1000 steps.
What
handle.test()doeshandleis theMPI.Requestreturned byisend_object.handle.test()returns(flag, status);[0]is the boolean completion flag. Each call does two things:It is non-blocking and bounded, so it restores the per-step progress cadence (the perf win) without re-introducing the blocking
wait()that #12888 removed to fix the shutdown hang.Why it's effective
The ring is a 3-hop sequential chain — this fix does not parallelize the hops. What it removes is the per-hop dead wait: in the regressed build each hop's stall is almost entirely "waiting for the sender to re-enter MPI", not data movement (the payload is small; real transfer is ~1 ms). nsys shows
recv_sample_stategoing from ~3 ms → 269 ms average (1.85 s worst case) in the regressed build, with GPU kernels byte-for-byte identical (same FMHA, GEMM, NCCL kernels and durations).Legend:
#GPU forward ·Ssample ·.GPU idle / transfer parked (no progress) ·>transfer moving (pumped) ·R*recv completesBefore (with #12888) — each ring hop parks:
After (this PR) —
test()pumps each sender, ~1 ms/hop:The GPU work (
#widths) is identical in both; only the ring row differs — parked vs moving. Removing the per-hop dead wait collapses the broadcast from ~3×269 ms of bubble to ~3 ms, recovering essentially all of the throughput back to the good-commit baseline.Perf
qwen3_0.6b-bench-pytorch-bfloat16-pp:4-gpus:4(GB300, lyris)total_output_throughput, higher-is-better. Fix recovers full perf (+52.8% over bad commit, +0.5% over good commit, within run-to-run noise).Independent reproduction on GB300 (lyris),
total_token_throughput, plain run (no profiler), building the wheel from source for each variant:Wall-clock for the benchmark drops 28:26 → 19:05. nsys confirms identical GPU kernels across all three; the only delta is host-side
recv_sample_statelatency.Test plan
Links
/lustre/fsw/coreai_comparch_trtllm/chenfeiz/agent/20260525-120524-bug-triage-6194812/🤖 Generated with repair-bot
Summary by CodeRabbit