[Data] Prevent output backpressure from starving lineage reconstruction - #64805
[Data] Prevent output backpressure from starving lineage reconstruction#64805dragongu wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to prioritize and drain streaming generator tasks queued for resubmission by lineage reconstruction under backpressure. It exposes the queued resubmit task IDs from the core worker task submitters up to the Python streaming executor, where these tasks are prioritized and granted a one-block lane to prevent starvation and deadlock. Feedback suggests adding defensive guards in streaming_executor_state.py to prevent potential AttributeError exceptions when checking the first ready task's ID and type.
40a1751 to
6be8d89
Compare
ayushk7102
left a comment
There was a problem hiding this comment.
Thanks for your PR!
Main feedback: should we fold this as part of the existing BackpressureGuard?
Also, should we should characterize this as a fix addressing stall/performance drop than deadlocking, because we are still making progress
On preemptible clusters, Ray Data pipelines relying on lineage reconstruction can collapse to near-zero goodput after object loss. The generator that reconstruction is waiting on is starved by its own operator's output backpressure, which is in turn caused by the very downstream task blocked on that reconstruction. The existing escape hatch nudges the cycle forward, but slowly (~1 block per 10s idle detection) and blindly (serving any ready task, not the generator replay is queued behind), so losses can pile up faster than recovery drains. Give Ray Data a bounded feedback path from Ray Core's reconstruction state: expose the locally-submitted generators queued for resubmit, then have OutputBackpressureGuard order them ahead of ordinary tasks and vend a per-operator ReconstructionBypassLane worth lineage_reconstruction_backpressure_bypass_blocks blocks. The lane reads one block at a time, stops early when nothing is ready, and never touches the operator's byte budget, so ordinary tasks stay fully backpressured. Both exceptions to output backpressure now live in the guard. The lane stays separate from should_unblock because eligibility is task-scoped: widening the operator byte budget would let every task on the operator read past backpressure, not just the generator reconstruction needs. Signed-off-by: dragongu <andrewgu@vip.qq.com>
6be8d89 to
fd8c09e
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
Reviewed by Cursor Bugbot for commit fd8c09e. Configure here.
- Move the pure-logic OutputBackpressureGuard tests (task ordering, bypass lane) into tests/unit/test_output_backpressure_guard.py, where the unit conftest enforces no Ray runtime / sleep. The two tests that drive process_completed_tasks (and need ray.wait) stay in the integration file. - Hoist the shared FakeDataOpTask fake and the mock-op factory into tests/util.py so both the unit and integration tests reuse one helper. - Add Doxygen \return tags to the new CoreWorker / task-submitter accessors, matching the neighboring QueueGeneratorForResubmit declarations. Signed-off-by: dragongu <andrewgu@vip.qq.com>

Description
On preemptible / elastic clusters (spot workers preempted often), Ray Data
pipelines that rely on lineage reconstruction can collapse to near-zero
goodput after a recoverable object loss. The streaming generator that
reconstruction is waiting on is starved by its own operator's output
backpressure — which is in turn caused by the very downstream task that is
blocked on that reconstruction.
Consider a typical elastic-resource pipeline (worker pods can be preempted at
any time):
The starvation cycle
A_task_k's original attempt is still running, so Ray Core queues it forresubmit (replay) and waits for the current attempt to return. For that attempt
to return, Ray Data must keep reading
A_task_k's streaming output. But RayData applies output read budgets per operator, and under backpressure the
budget for operator
Ais0:This is not a permanent deadlock in the general case: Ray Data's existing
output-backpressure escape hatch (
OutputBackpressureGuard.should_unblock)periodically releases a single block. But its forward progress is only nominal —
it fires on a ~10s idle-detection interval, and it releases the budget to any
ready task, not specifically the generator replay is queued behind. How badly
this bites depends on the resource regime:
reconstruct, but ~1 block / 10s loses the race against sustained preemption —
goodput collapses.
preempted, with none free to reschedule on): the released block is often not
the reconstruction-critical generator, and even when it is, that block has
nowhere to go — surviving GPU actors are starved waiting for inputs and no
new actors can be scheduled. The released blocks instead pile up in the object
store, which tightens the very backpressure starving the generator. Here the
"one block per round" safety net produces no real forward progress and the
pipeline effectively hangs — the same cycle, surfacing as a full halt.
Root cause
Ray Data computes output read budgets at the operator level and treats all
of an operator's ready tasks identically. It has no way to tell that one
specific streaming generator is the one Ray Core is waiting on for
reconstruction, so under backpressure that generator is starved along with
everything else. Ray Core cannot resolve this alone either — it does not observe
or control Ray Data's per-operator output-read policy.
Fix
Give Ray Data a minimal, bounded feedback path from Ray Core's reconstruction
state. All of it lands in
OutputBackpressureGuard, which already owns the oneexisting exception to output backpressure:
Expose the signal from Core. Add
CoreWorker::GetLocalQueuedGeneratorResubmitTaskIds()(aggregating newGetQueuedGeneratorResubmitTaskIds()accessors on the normal and actor tasksubmitters), surfaced to Python via
CoreWorker.get_local_queued_generator_resubmit_task_ids(). It returns thelocally-submitted streaming generators reconstruction has queued for resubmit.
Local (no RPC) but takes the submitter lock, so it is only queried when at
least one operator has a finite output read budget.
Prioritize the reconstruction-critical generator.
OutputBackpressureGuard.order_ready_tasksorders an operator's ready tasksso queued generators come first, then by task index (deterministic; falls
back to plain task-index order when nothing is queued).
Grant a bounded bypass lane. For a fully-backpressured operator the guard
vends a
ReconstructionBypassLaneworthDataContext.lineage_reconstruction_backpressure_bypass_blocksblocks (envRAY_DATA_LINEAGE_RECONSTRUCTION_BACKPRESSURE_BYPASS_BLOCKS, default1,0disables), shared across that operator's queued generators. The lane reads one
block at a time, stops early once the generator has nothing ready, and
never touches the operator's byte budget — so ordinary tasks on the same
operator stay fully backpressured.
This breaks the cycle:
A_task_kis drained a bounded number of blocks perround, its current attempt can finish, replay proceeds,
A1and thenB1arereconstructed, and recovery keeps pace with the preemption rate.
Keeping this in the guard (rather than the scheduling loop) puts both
backpressure exceptions in one place. The lane stays a separate mechanism
from
should_unblockbecause eligibility is task-scoped: widening theoperator-level byte budget would let every task on the operator read past
backpressure, not just the one generator reconstruction is blocked on.