Repository navigation
[PD] Keep ingesting requests while a prefill forward result is pending - #42035
Conversation
|
This port clears our bar on gate 18✓✗1✂1 lint✓ build✓ A✓ B✓ C 0/10⏳ other✓ · accel ✗amd,npu @merrymercy @Jialin — could one of you take a look and merge it if you are happy? Nothing else is waiting on our side. |
The prefill overlap loop used to block in copy_done.synchronize() for the oldest forward before it could receive anything else, so new requests, bootstrap arrivals, aborts and pause/continue sat in the socket and the second forward slot stayed empty. Behind SGLANG_DISAGG_PREFILL_CONTINUOUS_INPUT_POLLING (default off), the loop now polls the oldest result with a TP-wide MIN all-reduce over copy_done.query(), keeps ingesting while it is unfinished and launches a second batch when one is ready. A pause received during the wait is held with everything behind it until the result is processed; timeout aborts are not re-emitted for a request whose abort is already pending; inflight inventory includes batches still in result_queue; storage prefetch retry deadlines advance per scheduling step, not per poll; and retract pauses on a prefill instance set the chunk send boundary and leave an optimistic chunk in place while a middle chunk is still in flight. Only non-speculative autoregressive generation with attention DP = CP = 1 enables the polling loop; other shapes and the flag's default keep the blocking behaviour. Co-authored-by: Jialin <Jialin@users.noreply.github.com>
0249d58 to
f4d659d
Compare
|
This port clears our bar on gate✓ lint✓ build✓ A✓ B✓ C 0/10⏳ other✓ · accel ✗amd,npu @merrymercy @Jialin — could one of you take a look and merge it if you are happy? Nothing else is waiting on our side. |
… poll interval Rename SGLANG_DISAGG_PREFILL_CONTINUOUS_INPUT_POLLING to SGLANG_ENABLE_DISAGG_PREFILL_CONTINUOUS_INPUT_POLLING so the new feature toggle follows the env-var naming convention (SGLANG_ prefix + verb category; an opt-in knob is ENABLE_FOO = EnvBool(False)). The flag is introduced by this pull request, so no alias is needed. Also state next to PREFILL_INPUT_POLL_INTERVAL_S what the 100 microsecond TP=1 yield is: an initial heuristic that gives the bootstrap, transfer and cache threads a reliable chance at the GIL when the input socket is empty, not a tuned or measured optimum.
|
This port clears our bar on gate 20✓✗1 lint✓ build✓ A✓ B✓ C 0/10⏳ other✓ · accel ✗amd,npu,xpu @merrymercy @Jialin — could one of you take a look and merge it if you are happy? Nothing else is waiting on our side. |
Test-only; no production code changes. - test_parity: list PR107's Python-only lora_backfill_paths alongside lora_path and lora_id; the strict set equality is unchanged. - test_abort_request_prefix: the scheduler fake also binds the real _collect_inflight_batches, which collect_inflight_reqs calls since #42035, with polling off and an empty result queue, as test_weight_versions does. - test_pool_configurator: the retained draft-budget case uses SWARequestCapPoolConfigurator, the name #43202 gave the capped SWA configurator. - test_beam_nvfp4: pin the CUDA capability to SM100 with the Blackwell platform override; compressed-tensors scheme selection queries it directly since #40022. - test_fused_moe_common_utils: the Beam case also expects the tuner's num_layers and dense_layers keys from #28723.
Based on
main. The new test follows thecheckpoint_disagg_prefill/checkpoint_kv_cachenames from #41520 and the release-timeis_insert=Falsefrom #42194.Motivation
In the overlap event loop of a disaggregated prefill instance,
process_batch_resultblocks incopy_done.synchronize()until the oldest forward's sampled tokens have been copied to the host. While the scheduler thread sits in that wait it receives nothing: new requests, bootstrapped requests, aborts, pause/continue and weight updates all queue up in ZMQ, and the second forward slot stays empty even when work is ready. With long prefills this turns intake into a stop-and-go pattern and leaves the GPU idle between forwards.This change lets the prefill overlap loop keep ingesting requests while a forward's result is still pending, so a second batch can be prepared and launched behind the first one, and results are processed as soon as they are complete on every TP rank.
Modifications
event_loop_overlap_disagg_prefillkeeps at most two batches in flight. On each pass it ingests requests, tries to prepare a batch if a slot is free, and then checks whether the oldest queued result is ready (is_disagg_prefill_batch_result_ready, a non-blockingcopy_done.query()reduced withMINover the TP CPU group so all ranks agree). If it is not ready, the loop goes back to intake instead of blocking.A failed preparation while a result is pending sets a marker so the loop does not retry admission on every poll; new input or newly bootstrapped requests clear it immediately, and it is always cleared once a result finishes. Storage prefetch retry deadlines only advance on scheduling steps, not on polls, and
process_prefill_chunkruns once per launched batch (ScheduleBatch.prefill_chunk_processed).ingest_requests(stop_at_pause=...)keeps receiving and broadcasting on every pass but, while a result is pending, retains aPauseGenerationReqInputand everything behind it until the next scheduling boundary, so a pause cannot retract a batch whose result has not been processed. All TP ranks retain the same suffix. Timeout aborts are not re-polled while dispatch is held, and_poll_timeout_abortsskips requests whose abort is already pending (req.to_finish).collect_inflight_reqsincludes batches queued inresult_queuewhile polling is enabled, so aborts, running timeouts and weight-version tracking see a forward that has been launched but is not yetlast_batch.Retract pauses on a prefill instance set the chunked request's
tmp_end_idxbefore draining the pending result, and an optimistic prefill chunk is released and requeued only when no middle chunk is still in flight; otherwise the retract drains every result and nothing would requeue it.The feature is gated by a new environment variable,
SGLANG_ENABLE_DISAGG_PREFILL_CONTINUOUS_INPUT_POLLING(default off; named with theENABLE_verb per the env-var naming convention in.agents/skills/env-var-conventions), introduced by this pull request inenviron.py. It is only enabled for non-speculative autoregressive generation with attention DP = CP = 1; other shapes log a warning and keep the blocking loop. With the flag off, result handling stays exactly as before. At TP = 1 a boundedtime.sleep(PREFILL_INPUT_POLL_INTERVAL_S)per polling pass lets bootstrap, transfer and cache threads reacquire the GIL; the 100 µs value is an initial heuristic (documented at the constant), not a measured optimum._dispatch_event_loop_onceresets the flag so a PD role switch that selects a different loop does not inherit it.Accuracy Tests
The change reorders scheduler work within a prefill instance and does not touch the model forward or sampling. As an end-to-end check, a flag-on vs flag-off PD serving comparison was run with public components only: one node with four NVIDIA GB300 GPUs (the shape of the
4-gpu-gb300CI runner),Qwen/Qwen3-8B,--disaggregation-transfer-backend nixl, the Python mini load balancer and default server arguments, as TP1 (prefill on GPU 0, decode on GPU 1) and TP2 (prefill on GPUs 0-1, decode on GPUs 2-3) stacks, each freshly started. Procedure: GSM8K throughsglang.test.run_eval(200 examples, greedy);json_schemarequests racing plain long-prompt requests; streaming requests disconnected mid-prefill and requests aborted through/abort_requeston both servers while forwards were pending;/pause_generationinin_placeand inretractmode on both servers, issued into a burst of long requests and followed by/continue_generation; and a check that the prefill queues drain afterwards. The figures from that run are not reproduced in this description.Speed Tests and Profiling
The pending-result wait is replaced by non-blocking
query()polls plus one smallMINall-reduce on the TP CPU group per pass; with the flag off there is no new work. The same stacks were benchmarked withpython -m sglang.bench_serving --backend sglang --dataset-name random --random-range-ratio 1.0in a burst, a long chunked-prefill, a steady-rate and a light-rate scenario, with the prefill scheduler's CPU use sampled at idle and under load andpy-spystack samples of the prefill scheduler taken under load in both modes. The figures from those runs are not reproduced in this description.Checklist
scripts/lint/check_registered_tests.pypass on the changed files.test/registered/unit/disaggregation/test_prefill_result_polling.pydrives the real prefill overlap loop with controlled intake and copy events (two-slot pipelining, deferred pause in both modes, shutdown during a wait, storage-retry pacing, chunked and optimistic prefill hand-offs, unsupported shapes falling back, and a two-process Gloo check that both ranks agree on FIFO completion). Existing scheduler tests gain cases for pending-abort suppression, inflight inventory with a queued result, idle-step accounting under polling, and single-result retract. The changed test files and the unchanged callers of the touched scheduler methods (mlx/test_scheduler_mixin.py,mlx/test_attention_patching.py,test_batch_result_processor_mamba_boundary.py,test_priority_scheduling_disaggregation.py,test_scheduler_hicache_events.py,test_scheduler_weight_version_tracking.py,test_decode_queue_cleanup.py,test_pd_role_switch.py) pass locally on CPU, one process per file.environ.py.CI States
Latest PR Test (Base): 🚫 Run #37129843019
Latest PR Test (Extra): ❌ Run #37129842972
Latest PR Test (AMD ROCm 10): ❌ Run #37129843035