jerrypeng commented on PR #56055:
URL: https://github.com/apache/spark/pull/56055#issuecomment-4898223172

   # Pipelined Shuffle Dependency & Concurrent Stage Scheduling
   
   A spec for running data-dependent stages of a single job concurrently, 
connected by a shuffle the
   consumer reads incrementally.
   
   ---
   
   ## 1. Motivation
   
   Today a multi-stage job runs one stage at a time: each shuffle is fully 
materialized before the next
   stage starts. Some workloads need the stages of a single job to run 
**concurrently**, connected by a
   shuffle whose consumer reads the producer's output **as it is produced** 
rather than after the
   producer finishes. This spec introduces the scheduler primitives to express 
and run that.
   
   "Run these stages concurrently" and "the connecting shuffle is incremental" 
are the same decision
   seen from two sides: co-scheduling a producer and consumer is only useful if 
the edge is readable
   before the producer completes.
   
   ---
   
   ## 2. Primitives
   
   ### 2.1 Pipelined shuffle dependency (PSD)
   
   A shuffle dependency declared **incrementally readable**: a consumer stage 
may begin reading its
   output while the producer stage is still running.
   
   - It is a shuffle dependency (has a `shuffleId`, partitioner, map/reduce 
sides); the *pipelined*
     property is a binding part of the scheduler contract, not an advisory hint.
   - The property is set during **physical planning** (an execution concern, 
not a logical-plan one)
     and carried into the `ShuffleDependency` the `DAGScheduler` reads at 
stage-creation time.
   
   A **regular shuffle dependency (RSD)** is an ordinary shuffle dependency: 
its output must be fully
   materialized before any consumer reads it.
   
   ### 2.2 Pipelined group (G)
   
   The set of stages connected to one another through pipelined edges — the 
connected component of the
   stage DAG when only pipelined edges are considered.
   
   - A stage with no incident pipelined edge is a **singleton group** and 
behaves exactly as a normal
     stage today.
   - The group — not the edge or the individual stage — is the unit of 
**admission**, **slot
     checking**, **completion**, and **failure**.
   
   **External input of G:** a regular shuffle dependency whose consumer is in G 
and whose producer is
   not — i.e. a normal materialized parent of the group.
   
   ---
   
   ## 3. Group formation
   
   - **Stage decomposition is unchanged.** A pipelined dependency introduces a 
shuffle boundary exactly
     as a regular one does; the set of stages and their partitioning are 
identical. The pipelined
     property changes only *when* stages run relative to one another, never 
*how the plan is cut into
     stages*.
   - **Group = connected component over pipelined edges.** As stages are 
created, two stages joined by a
     pipelined edge are placed in the same group; the group is the transitive 
closure.
   - **Every stage belongs to exactly one group** (singletons included). Group 
membership is fixed at
     stage-creation time.
   
   ---
   
   ## 4. Scheduling & admission
   
   - **A pipelined edge is non-sequencing.** The consumer of a pipelined 
dependency does not wait for
     its producer to materialize. (A regular-dependency consumer still waits — 
the default behavior.)
   - **Group readiness.** A group is ready to be admitted when every external 
input of the group (its
     regular materialized parents) is available — the same precondition a 
normal stage has today, lifted
     to the group. Pipelined parents inside the group impose no readiness 
precondition.
   - **Gang admission (all-or-nothing).** A group is admitted only if the 
cluster can currently run all
     tasks of all member stages **concurrently**; admission then submits every 
member stage at once.
     There is no partial admission — a group is never left with some members 
running while others wait
     on slots the running members occupy.
   - **Slot check.** The group's aggregate concurrent-task demand — the sum of 
`numTasks` over member
     stages — is compared against the number of available slots in the cluster 
(a slot is one task's
     worth of capacity, so this is the maximum number of tasks that can run at 
once). If the group
     needs more slots than the cluster can offer, the submission fails fast, 
since the group could
     never become fully co-resident.
   - **Co-residency.** Once admitted, all member stages of a group are 
simultaneously running.
   - **Single ownership.** A pipelined group belongs to exactly one job; a 
pipelined producer is not
     shared across jobs.
   
   ---
   
   ## 5. Completion
   
   - **Group-observable completion.** A member stage is not job-observably 
finished until **all** member
     stages of its group have completed successfully. A result stage's job 
therefore completes only when
     its entire group has, and a member that finishes ahead of the others 
cannot advance job or stage
     completion until the group does.
   - **Defer the finish decision, not per-task work.** When a member finishes 
before its group, only its
     stage-finish / job-finish transition is deferred until group completion. 
Per-task side effects that
     must always run — accumulator updates, output-commit coordination, 
task-end listener events — run
     immediately.
   - **Replay window.** There is a window between a member finishing and group 
completion. A failure in
     that window is a group failure (§6): the deferred finish transitions are 
dropped and the group
     reruns.
   
   ### 5.1 Observable completion events (listener bus)
   
   The listener bus is an external contract that monitoring tools depend on, so 
it is worth stating
   exactly when each event is delivered. The rule follows directly from atomic 
commit: **task-level
   events flow in real time, but stage-completion and job-completion events are 
held until the group
   commits — so a listener never observes a member as *successfully completed* 
before the group as a
   whole has.**
   
   | Event | Timing | Rationale |
   |-------|--------|-----------|
   | `SparkListenerTaskStart` / `SparkListenerTaskEnd` | Real time, as they 
occur | Per-task facts are true when they happen; a group's members genuinely 
run concurrently. Deferring these would freeze a member's live progress and 
metrics for the whole group's duration. Note a successful `TaskEnd` means "this 
task finished," not "its output is committed" — already true in Spark, since a 
stage attempt can later be discarded. |
   | `SparkListenerStageSubmitted` | Real time, at group admission | All member 
stages are submitted together (§4); a monitor should show them active 
simultaneously. |
   | `SparkListenerStageCompleted` | Deferred to group commit; on group 
failure, emitted with a failure reason | "Completed" should track commit, which 
is atomic at the group level. A member whose tasks finish early is reported as 
still running until the group commits — which matches the truth that its 
results are not usable until then. This avoids emitting a success-shaped 
completion for an attempt that a later group failure would discard. |
   | `SparkListenerJobEnd(JobSucceeded)` | At group commit only | Job 
completion delivers results to the caller and cancels sibling stages; emitting 
it before the group commits risks double/inconsistent result delivery if the 
group later fails. Non-negotiable. |
   | `SparkListenerJobEnd(JobFailed)` | On group failure | Group-atomic failure 
(§6): buffered success transitions are dropped, never replayed as success. |
   
   Failure-path consequence: because stage- and job-completion are deferred 
(never emitted early as
   success), a group failure needs only to emit them as *failure* — there is no 
premature success event
   to retract. Already-emitted task events stand as-is (they were true), 
consistent with how Spark
   treats task events from a stage attempt that is later discarded.
   ---
   
   ## 6. Failure
   
   - **Failure is group-atomic.** Any task failure in any member stage — 
including a fetch failure —
     fails the whole group. There is no independent single-stage failure within 
a group. Two mechanisms
     make this hold:
     - *No single-stage resubmit inside a group.* The ordinary "resubmit one 
failed stage" path is
       invalid for a group member: the transient pipelined shuffle cannot be 
re-read, and members are
       co-scheduled. A member failure routes to group failure instead.
     - *Teardown is by group membership, not producer availability.* At the end 
of a batch the producer
       finishes and registers all its map outputs — becoming "available" — 
while the consumer is still
       draining the remainder, so a 
producer-available-while-consumer-still-running window is normal,
       not rare. For a transient pipelined shuffle, "available" does not mean 
the consumer is safe: the
       data is not durably stored and the consumer is still actively reading 
it, so a producer failure
       in that window still strands the consumer. Failure propagation that keys 
off producer
       availability would miss the consumer here; teardown is therefore keyed 
on group membership, so a
       producer failure always tears down its co-scheduled consumers regardless 
of the producer's
       availability at the failure instant.
   - **Recovery.** Group failure tears down all member stages atomically and 
discards their deferred
     completions. Because the shuffle is transient it cannot be partially 
recovered; the job fails and
     the caller re-runs it.
   
   ---
   
   ## 7. Interaction with regular dependencies
   
   - **Regular shuffle into a group — supported.** A regular dependency from 
outside the group to a
     member is a normal materialized parent (an external input); the group 
waits for it.
   - **Regular shuffle out of a group — supported.** A regular dependency from 
a member to a stage
     outside the group produces a materialized output that a downstream group 
consumes with normal
     sequencing; the downstream waits for the group to complete.
   - **Regular shuffle internal to a group — fail-fast / unsupported.** If a 
regular dependency's
     producer and consumer both fall in the same group, the group would 
co-schedule them concurrently
     while the regular edge demands the producer be materialized first — a 
contradiction. Reject at
     stage creation.
   - Pipelined edges are intra-group by construction, so they never cross a 
group boundary.
   
   ---
   
   ## 8. Activation
   
   - **Activation is by group membership.** Every behavioral change — 
admission, completion, and
     failure handling — keys off a single question: is this stage a member of a 
pipelined group? There
     is no separate enable flag and no half-activated state.
   - **Opt-in is expressed by the presence of a pipelined dependency**, which a 
physical-planning rule
     sets on the relevant exchanges. The scheduler primitive is generic; any 
feature can write such a
     rule to opt a job into concurrent stage scheduling over an incremental 
shuffle.
   
   ---
   
   ## 9. Fail-fast on unsupported idioms
   
   A pipelined group that involves any of the following is rejected at 
stage/group creation with a clear
   error, rather than silently mis-scheduled. Each is a documented limitation 
of the first version.
   
   The rejected idioms fall into two kinds. Some are **moot** under the group 
failure model (§6):
   Spark's stage-level recompute/rollback mechanisms never fire inside a group, 
because any failure
   aborts the whole group and the caller reruns from scratch — so a mechanism 
whose only job is to
   recompute or roll back a stage after a partial failure is never reached. We 
reject these rather than
   carry dead, self-contradictory machinery. The rest are **incompatible** with 
concurrent execution
   itself and would corrupt or hang a group that never fails.
   
   | Rejected condition | Kind | Why rejected |
   |--------------------|------|--------------|
   | Barrier execution in a member stage | incompatible | Barrier exposes 
output only after a global sync, contradicting concurrent partial reads. |
   | Dynamic resource allocation | incompatible | Gang admission needs a stable 
slot set; reclaiming executors from a pinned-open group can deadlock it. |
   | Speculative execution | incompatible | A speculative producer copy races a 
consumer already reading partial output; no commit barrier protects the read. |
   | Push-based shuffle merge | incompatible | Push-based shuffle merges each 
partition's blocks into large pre-sealed files on merger services, and those 
files become readable only after a post-map-completion "finalize" step — the 
exact opposite of a pipelined shuffle, whose consumer reads before the producer 
finishes, so the two can never apply to the same edge. |
   | Statically-indeterminate producer | moot | Its recovery is stage 
rollback-and-recompute, which a group never performs (§6: any failure aborts 
the group). The mechanism is never reached; rejected so the inapplicability is 
explicit rather than latent. (A producer whose output RDD is classified 
`INDETERMINATE` — a rerun can yield different data, not just a different order 
— determinable from the RDD graph at stage-creation time.) |
   | Checksum-mismatch full retry | moot | The runtime counterpart to static 
indeterminism: it checksums each map task's output and, on a cross-attempt 
mismatch, rolls back and re-runs the succeeding stages. A group never keeps 
succeeding stages across a retry (§6), so this never fires; rejected to keep 
the inapplicability explicit. |
   | Cached/persisted RDD in a member's within-stage chain | incompatible | 
Would capture partial output read mid-run and serve it as a complete result on 
reuse. Scoped to the member's within-stage (narrow-dependency) chain: a cached 
*complete* input reached via a broadcast variable or across a materialized 
shuffle — e.g. the static side of a stream-static join — is outside that chain 
and is unaffected. |
   | Pipelined producer shared across jobs | incompatible | Breaks single 
ownership: a failure must map to exactly one job's group. |
   | Regular shuffle internal to a group | incompatible | Concurrent 
co-scheduling contradicts materialize-before-read (§7). |
   | Adaptive Query Execution over a pipelined exchange | incompatible | AQE 
reshapes exchanges from complete map-output statistics, which are unavailable 
while the shuffle is read incrementally. Enforced where exchanges are marked 
pipelined. |
   
   The AQE row forbids AQE's *intra-batch, mid-stage* replanning — reading a 
running producer's statistics
   within one execution and reshaping its consumers — not statistics-driven 
adaptivity in general. A
   streaming consumer may still reshape a later batch's plan from an earlier 
batch's completed statistics:
   those statistics are final (the earlier batch fully materialized), and the 
reshaping happens at planning
   time, before the later batch's stages are co-scheduled — so it composes with 
pipelining rather than
   contradicting it. That cross-batch feedback is the compatible way to get 
AQE-like benefits (partition
   coalescing, skew handling, join-strategy selection) in a pipelined streaming 
query.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to