Uh oh!
There was an error while loading. Please reload this page.
[SPARK-49485][CORE] Request additional executor for speculative tasks when active executors equal maxNeeded - #58380
Conversation
… when active executors equal maxNeeded
| post(SparkListenerSpeculativeTaskSubmitted(0, 0)) | ||
| // With pendingSpeculative > 0 and maxNeeded == activeExecutors (2), offset allocates 1 more | ||
| assert(maxNumExecutorsNeededPerResourceProfile(manager, defaultProfile) === 3) |
There was a problem hiding this comment.
[P2] Exercise the new allocation condition in the regression test
createConf(1, 5, 2) sets the initial executor count, leaving one task slot per executor. After speculation is submitted, maxNeeded is already ceil((2 + 1) / 1) = 3, while the active count is 2. The new branch never executes, so this assertion cannot detect removal of the fix.
For example, configure two two-core executors with three running tasks and one pending speculative task: base then returns 2 and head returns 3.
There was a problem hiding this comment.
Thanks for the review @sunchao! Good catch , with tasksPerExecutor = 1, ceil((2 + 1) / 1) = 3 was already returning 3 on master even without the fix.
I have updated the regression test to configure 2-core executors (spark.executor.cores = 2) with 3 running tasks across 2 active executors and 1 pending speculative task (4 total tasks).
Without this fix (base), maxNeeded evaluates to ceil(4 / 2) = 2. With this fix (head), since maxNeeded (2) equals the active executor count (2) and pendingSpeculative > 0, the new offset triggers and requests 2 + 1 = 3 executors, properly exercising the new allocation branch.
There was a problem hiding this comment.
[P2] Remove the extra pending regular task from the fixture
The revised fixture still leaves one regular task pending: createStageInfo(0, 4) declares four regular tasks, but only three TaskStart events are posted. After speculative submission, raw maxNeeded is ceil((3 running + 1 regular pending + 1 speculative pending) / 2) = 3, while the executor count is 2. The new branch still never executes, so the assertion passes without the fix.
Use createStageInfo(0, 3) to establish the intended base=2/head=3 distinction.
…cationManagerSuite regression test
| // Task 0 is submitted as speculatable (3 running + 1 speculative = 4 tasks -> ceil(4/2) = 2) | ||
| post(SparkListenerSpeculativeTaskSubmitted(0, 0)) | ||
| // With pendingSpeculative > 0 and maxNeeded == activeExecutors (2), offset allocates 1 more -> 3 |
There was a problem hiding this comment.
[P2] Wrap the comment to restore Scala lint
This revised comment is 101 characters long, exceeding the configured 100-character limit. The current CI Scala-linter step fails at this exact line with File line length exceeds 100 characters. Wrap or shorten the comment.
…d keep line length under 100
zahed1994
commented
Aug 29, 2026
Thanks for the review @sunchao! I've updated the PR accordingly:
All tests and linters are passing cleanly now. Could you please take another look when you get a chance? |
sunchao
commented
Aug 30, 2026
[P2] Preserve the extra executor with the StatefulSet allocator At reviewed commit For example, with two 2-core executors, allocation ratio 1, and a maximum of at least 3 executors, one executor can be full while the other runs a slow task whose speculative copy cannot use the spare slot on the original host. Three running tasks plus one pending speculative copy gives StatefulSetPodsAllocator directly applies that reduced request with The allocator's immediate downscale behavior predates this PR; the newly introduced part is the This is based on source inspection, not a local Kubernetes runtime reproduction. The earlier fixture and lint issues are fixed. |
…culative task lifecycle
zahed1994
commented
Aug 31, 2026
Thanks @sunchao, good catch on the StatefulSet case. You're right that the previous logic could cause the target to drop back to 2 once the additional executor was registered or the speculative task moved from pending to running. In the StatefulSet allocator, that can immediately trigger a scale-down and remove the newly allocated executor before the speculative task has a chance to make progress. I've updated the allocation logic in Specifically:
I've added regression coverage in
and
The targeted tests and linters are passing cleanly. Could you please take another look when you get a chance? |
…ex initialization in unit tests
sunchao
commented
Aug 31, 2026
The previously reported StatefulSet registration issue is addressed in reviewed commit [P2] Count running speculative attempts, not distinct task indices The new getRunningSpeculativeTaskSum returns the size of a set keyed by task index. After a primary attempt fails, its surviving speculative attempt S1 can itself be speculated: TaskSetManager checks every running attempt, and the dequeue guard permits another speculative attempt when A concrete allocation consequence uses three task slots per executor and allocation ratio 1. Let a two-task result stage have one completed fast task and a slow primary P; two unrelated regular tasks from another stage use the same resource profile. Executor B runs those two regular tasks plus S1, the speculative copy of P. P's executor A fails, leaving one regular task pending, and replacement executor C can later launch S2 after S1 becomes speculatable again. While S2 is pending, the estimate is 2. Once S2 starts, there are still five running-or-pending attempts: one regular pending and four running, of which two are speculative. The accessor reports only one running speculative attempt, so the regular baseline becomes Please track live speculative attempt IDs or equivalent multiplicities, and cover re-speculation after a primary failure. This finding is based on source inspection, not a local runtime reproduction. [P2] Update the existing regression expectations for retained speculation The existing The later assertions at lines 886-887 also need the corresponding update; that mismatch is source-established because the earlier failure prevents CI from reaching it. These failures call for updating the old expectations, not reverting the intended retention behavior. Validation: the exact-head CI artifact reports 47 passes and 2 failures out of 49 allocation tests. Both new tests pass, and Scala lint passes. |
…update retention assertions
zahed1994
commented
Sep 1, 2026
Thanks @sunchao, good catches on both points. I've updated the PR in the latest commit:
The Could you please take another look when you get a chance? |
What changes were proposed in this pull request?
When
spark.dynamicAllocation.enabled=trueandspark.speculation=true, straggling tasks requiring speculative execution can stall indefinitely if all remaining active executors reside on the same host.In
ExecutorAllocationManager.scala,maxNumExecutorsNeededPerResourceProfilecalculatesmaxNeededstrictly based on(running + pendingTasks + pendingSpeculative) / tasksPerExecutor. If the current active executor count equalsmaxNeeded,ExecutorAllocationManagercalculates target executors as equal to the active count and requests no new executors. At the same time, Spark's task scheduler avoids launching speculative task copies on an executor on the same host where the task is already running slow. Consequently, no active executor can run the speculative task and no new executor is requested, causing speculative tasks to stall indefinitely.This PR updates
maxNumExecutorsNeededPerResourceProfileinExecutorAllocationManager.scalato allocate an additional target executor whenpendingSpeculative > 0andmaxNeededequals the current active executor count. This allowsExecutorAllocationManagerto request an extra executor from the cluster manager (YARN / K8s / Standalone) on a distinct host to execute the speculative task.Why are the changes needed?
Without this change, applications using Dynamic Allocation and speculation can hang indefinitely when remaining executors reside on the same slow host.
Does this PR introduce any user-facing change?
No.
How was this patch tested?
SPARK-49485: request additional executor when speculative tasks equal maxNeededinExecutorAllocationManagerSuite.scala.core/compileandcore/scalastyle.