sunchao commented on code in PR #57329:
URL: https://github.com/apache/spark/pull/57329#discussion_r3648372078


##########
core/src/main/scala/org/apache/spark/scheduler/TaskSetManager.scala:
##########
@@ -125,6 +126,30 @@ private[spark] class TaskSetManager(
   val successful = new Array[Boolean](numTasks)
   private val numFailures = new Array[Int](numTasks)
 
+  // For each task, tracks the number of times it has failed due to 
out-of-memory. Used to grow
+  // the number of CPUs allocated to a retry (see 
spark.task.oomRetryCpusIncrement), which lowers
+  // the executor's concurrent task count and thus increases the retry's 
execution-memory share.
+  // Reset to 0 when the task succeeds, mirroring numFailures.
+  private[scheduler] val numOomRetries = new Array[Int](numTasks)
+  private val oomRetryCpusIncrement = conf.get(config.OOM_RETRY_CPUS_INCREMENT)
+  // Executor exit codes treated as OOM (see 
spark.task.oomRetryExecutorExitCodes); when an
+  // executor exits with one of these, the tasks it was running have their OOM 
retry count bumped.
+  private val oomRetryExecutorExitCodes = 
conf.get(config.OOM_RETRY_EXECUTOR_EXIT_CODES).toSet
+  // The executor total cores of this TaskSet's ResourceProfile. A 
TaskSetManager is tied to a
+  // single ResourceProfile, so this is constant for its lifetime and caps the 
OOM retry cpus.
+  private val executorCoresLimit: Int = {
+    val rp = 
sched.sc.resourceProfileManager.resourceProfileFromId(taskSet.resourceProfileId)
+    rp.getExecutorCores.getOrElse(conf.get(EXECUTOR_CORES))

Review Comment:
   [P1] The known ResourceProfile cap can still permanently strand retries on 
the current head.
   
   The `None` fix addresses unknown executor capacity, but a known profile or 
configured capacity can still exceed the executor's *actual registered total*. 
Spark's existing Kubernetes OOM recovery demonstrates this without synthetic 
offers: exit `52` is described as `(JVM OOM)`, 
`KubernetesClusterSchedulerBackend.doRemoveExecutor` enables recovery, 
`BasicExecutorFeatureStep` sets `SPARK_EXECUTOR_CORES` to `spark.task.cpus`, 
and the Kubernetes entrypoint passes that reduced value as the executor's 
registered `--cores`. With a four-core executor profile, `spark.task.cpus=1`, 
and `spark.task.oomRetryCpusIncrement=1`, the replacement registers **one** 
core while `executorCoresLimit` remains **four**. The pending OOM retry 
therefore requires **two** cores, rejects every offer forever, and never gets 
another attempt or reaches `maxTaskFailures`.
   
   The same failure is reproducible locally with `local[2,2]`, 
`spark.executor.cores=4`, and increment `2`: the retry requests three CPUs from 
an executor whose actual total is two. The added `FailureSuite` only tests 
matching configured and actual capacities.
   
   Please cap against each eligible executor's actual **registered total** 
rather than profile/configured cores or currently free cores, and add coverage 
for Kubernetes OOM recovery and unequal configured/actual local capacity.



##########
core/src/main/scala/org/apache/spark/scheduler/TaskSetManager.scala:
##########
@@ -1247,6 +1318,11 @@ private[spark] class TaskSetManager(
           // that the task is not running, and it is NetworkFailure rather 
than TaskFailure.
           case _ => !info.launching
         }
+        // Grow the CPUs of the retry when a running task's executor died of a 
JVM heap OOM. See
+        // the SparkOutOfMemoryError branch in handleFailedTask for the 
non-fatal counterpart.
+        if (oomRetryCpusIncrement > 0 && !isBarrier && isOomExit && 
exitCausedByApp) {

Review Comment:
   [P2] The dropped-cause limitation is reachable from Spark's own 
execution-memory spill path.
   
   This is not limited to an unusual application-defined exception. 
`TaskMemoryManager.spillConsumer` constructs 
`SparkOutOfMemoryError("SPILL_OUT_OF_MEMORY", new HashMap<String, String>() {{ 
... }})` using a double-brace anonymous map. Inspecting the compiled 
`TaskMemoryManager$1` confirms that the map captures both its nonserializable 
`TaskMemoryManager` and `MemoryConsumer`. When 
`FileFormatDataWriter.enrichWriteError` wraps that OOM in a `SparkException`, 
`Executor.TaskRunner` hits `NotSerializableException` while serializing 
`ExceptionFailure` and explicitly retries with `preserveCause=false`.
   
   The driver consequently receives `exception=None` and the top-level class 
name `org.apache.spark.SparkException`; neither OOM fallback matches. Since 
`SparkOutOfMemoryError` is deliberately nonfatal, there is no executor-exit 
fallback either, and the retry retains the same CPU allocation until the stage 
fails. The existing wrapped-OOM test bypasses executor serialization, so it 
cannot detect this real in-tree case.
   
   Please preserve an explicit OOM classification before discarding the 
throwable, or otherwise transport a serializable marker, and add a regression 
that exercises the actual executor failure-serialization fallback.



-- 
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