-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-36699][Core] Reuse compatible executors for stage-level scheduling #33941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 23 commits
3a7d060
96fa103
8f9d0fe
5a4067a
05ad5e9
9b5b604
9b27088
540c8e2
b28b295
c1355ea
9a11d28
5af3390
1c08b44
9ec2615
ba93652
0465af3
67d2835
3698463
97a32cf
c22277e
266c31d
437a1ab
9155338
8deb499
5fab51c
2abef37
379fbea
b2d16b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,6 +110,8 @@ private[spark] class ExecutorAllocationManager( | |
|
|
||
| import ExecutorAllocationManager._ | ||
|
|
||
| private val reuseExecutors = conf.get(SCHEDULER_REUSE_COMPATIBLE_EXECUTORS) | ||
|
|
||
| // Lower and upper bounds on the number of executors. | ||
| private val minNumExecutors = conf.get(DYN_ALLOCATION_MIN_EXECUTORS) | ||
| private val maxNumExecutors = conf.get(DYN_ALLOCATION_MAX_EXECUTORS) | ||
|
|
@@ -297,11 +299,13 @@ private[spark] class ExecutorAllocationManager( | |
| val numRunningOrPendingTasks = pendingTask + pendingSpeculative + running | ||
| val rp = resourceProfileManager.resourceProfileFromId(rpId) | ||
| val tasksPerExecutor = rp.maxTasksPerExecutor(conf) | ||
| logDebug(s"max needed for rpId: $rpId numpending: $numRunningOrPendingTasks," + | ||
| s" tasksperexecutor: $tasksPerExecutor") | ||
|
|
||
| val maxNeeded = math.ceil(numRunningOrPendingTasks * executorAllocationRatio / | ||
| tasksPerExecutor).toInt | ||
|
|
||
| logDebug(s"max needed for rpId: $rpId numpending: $numRunningOrPendingTasks," + | ||
| s" tasksperexecutor: $tasksPerExecutor = $maxNeeded") | ||
|
|
||
| val maxNeededWithSpeculationLocalityOffset = | ||
| if (tasksPerExecutor > 1 && maxNeeded == 1 && pendingSpeculative > 0) { | ||
| // If we have pending speculative tasks and only need a single executor, allocate one more | ||
|
|
@@ -490,6 +494,11 @@ private[spark] class ExecutorAllocationManager( | |
| numExecutorsTargetPerResourceProfileId(rpId) - oldNumExecutorsTarget | ||
| } | ||
|
|
||
| private def numExecutorsTargetsCompatibleProfiles(rpId: Int): Int = { | ||
| val compatibleProfileIds = resourceProfileManager.getCompatibleProfileIds(rpId) | ||
| compatibleProfileIds.map { numExecutorsTargetPerResourceProfileId(_) }.sum | ||
| } | ||
|
|
||
| /** | ||
| * Update the target number of executors and figure out how many to add. | ||
| * If the cap on the number of executors is reached, give up and reset the | ||
|
|
@@ -502,14 +511,27 @@ private[spark] class ExecutorAllocationManager( | |
| */ | ||
| private def addExecutors(maxNumExecutorsNeeded: Int, rpId: Int): Int = { | ||
| val oldNumExecutorsTarget = numExecutorsTargetPerResourceProfileId(rpId) | ||
| // Do not request more executors if it would put our target over the upper bound | ||
| // this is doing a max check per ResourceProfile | ||
| if (oldNumExecutorsTarget >= maxNumExecutors) { | ||
| logDebug("Not adding executors because our current target total " + | ||
| s"is already ${oldNumExecutorsTarget} (limit $maxNumExecutors)") | ||
| numExecutorsToAddPerResourceProfileId(rpId) = 1 | ||
| return 0 | ||
|
|
||
| if (!reuseExecutors) { | ||
| // Do not request more executors if it would put our target over the upper bound | ||
| // this is doing a max check per ResourceProfile | ||
| if (oldNumExecutorsTarget >= maxNumExecutors) { | ||
| logDebug("Not adding executors because our current target total " + | ||
| s"is already ${oldNumExecutorsTarget} (limit $maxNumExecutors)") | ||
| numExecutorsToAddPerResourceProfileId(rpId) = 1 | ||
| return 0 | ||
| } | ||
| } else { | ||
| val numCompatibleExecutors = numExecutorsTargetsCompatibleProfiles(rpId) | ||
| if (oldNumExecutorsTarget + numCompatibleExecutors >= maxNumExecutors) { | ||
|
xwu-intel marked this conversation as resolved.
Outdated
|
||
| logDebug("Not adding executors because our current target total is already " + | ||
| s"${oldNumExecutorsTarget} (limit: max $maxNumExecutors - " + | ||
| s"reused $numCompatibleExecutors)") | ||
|
xwu-intel marked this conversation as resolved.
Outdated
|
||
| numExecutorsToAddPerResourceProfileId(rpId) = 1 | ||
| return 0 | ||
| } | ||
| } | ||
|
|
||
| // There's no point in wasting time ramping up to the number of executors we already have, so | ||
| // make sure our target is at least as much as our current allocation: | ||
| var numExecutorsTarget = math.max(numExecutorsTargetPerResourceProfileId(rpId), | ||
|
|
@@ -518,11 +540,25 @@ private[spark] class ExecutorAllocationManager( | |
| numExecutorsTarget += numExecutorsToAddPerResourceProfileId(rpId) | ||
| // Ensure that our target doesn't exceed what we need at the present moment: | ||
| numExecutorsTarget = math.min(numExecutorsTarget, maxNumExecutorsNeeded) | ||
| // Ensure that our target fits within configured bounds: | ||
| numExecutorsTarget = math.max(math.min(numExecutorsTarget, maxNumExecutors), minNumExecutors) | ||
| numExecutorsTarget = if (!reuseExecutors) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to look in more detail perhaps on how to do this, I don't really like haven't to add conditionals in so many places
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pls let me know if there is better way. |
||
| // Ensure that our target fits within configured bounds: | ||
| math.max(math.min(numExecutorsTarget, maxNumExecutors), minNumExecutors) | ||
| } else { | ||
| // Ensure that our target fits within adjusted bounds: | ||
| val numCompatibleExecutors = numExecutorsTargetsCompatibleProfiles(rpId) | ||
| val adjustedMinNumExecutors = math.max(0, minNumExecutors - numCompatibleExecutors) | ||
| val adjustedMaxNumExecutors = math.max(1, maxNumExecutors - numCompatibleExecutors) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this part doesn't make sense to me on initial reading. If we set the min and max and our target should still fit within those and not those adjusted. I get that you are trying to say including the compatible ones keep it in that limit but I think this is hard to read to understand that. this would also get more complicated if the reuse policy could change within an application.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, what I mean is the new min and max executor numbers if there are some executors reused. What is your suggestion to get this more easy ? |
||
|
|
||
| math.max(math.min(numExecutorsTarget, adjustedMaxNumExecutors), | ||
| adjustedMinNumExecutors) | ||
| } | ||
|
|
||
| val delta = numExecutorsTarget - oldNumExecutorsTarget | ||
| numExecutorsTargetPerResourceProfileId(rpId) = numExecutorsTarget | ||
|
|
||
| logDebug("new numExecutorsTargetPerResourceProfileId: " + | ||
| numExecutorsTargetPerResourceProfileId.mkString(", ")) | ||
|
|
||
| // If our target has not changed, do not send a message | ||
| // to the cluster manager and reset our exponential growth | ||
| if (delta == 0) { | ||
|
|
@@ -712,7 +748,13 @@ private[spark] class ExecutorAllocationManager( | |
|
|
||
| if (!numExecutorsTargetPerResourceProfileId.contains(profId)) { | ||
| numExecutorsTargetPerResourceProfileId.put(profId, initialNumExecutors) | ||
| if (initialNumExecutors > 0) { | ||
| val adjustedinitialNumExecutors = if (reuseExecutors) { | ||
| val numCompatibleExecutors = numExecutorsTargetsCompatibleProfiles(profId) | ||
| math.max(0, initialNumExecutors - numCompatibleExecutors) | ||
| } else { | ||
| initialNumExecutors | ||
| } | ||
| if (adjustedinitialNumExecutors > 0) { | ||
| logDebug(s"requesting executors, rpId: $profId, initial number is $initialNumExecutors") | ||
| // we need to trigger a schedule since we add an initial number here. | ||
| client.requestTotalExecutors( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,7 @@ import org.apache.spark.partial.CountEvaluator | |
| import org.apache.spark.partial.GroupedCountEvaluator | ||
| import org.apache.spark.partial.PartialResult | ||
| import org.apache.spark.resource.ResourceProfile | ||
| import org.apache.spark.resource.ResourceProfileCompatiblePolicy.ResourceProfileCompatiblePolicy | ||
| import org.apache.spark.storage.{RDDBlockId, StorageLevel} | ||
| import org.apache.spark.util.{BoundedPriorityQueue, Utils} | ||
| import org.apache.spark.util.collection.{ExternalAppendOnlyMap, OpenHashMap, | ||
|
|
@@ -1809,6 +1810,22 @@ abstract class RDD[T: ClassTag]( | |
| this | ||
| } | ||
|
|
||
| /** | ||
| * Specify a ResourceProfile and reuse existing compatible executors to use when calculating | ||
| * this RDD. | ||
| * @param reuseResourceNames specify what resource should be checked when reusing executors | ||
| * @param reusePolicy specify executor reuse policy | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather see ResourceProfileCompatiblePolicy as a public interface that user could implement their own policy. We can provide a couple very basic ones, equals and AllGreater or something like that. |
||
| */ | ||
| @Experimental | ||
| @Since("3.3.0") | ||
| def withResources(rp: ResourceProfile, | ||
| reuseResourceNames: Set[String], reusePolicy: ResourceProfileCompatiblePolicy): this.type = { | ||
| resourceProfile = Option(rp) | ||
| sc.resourceProfileManager.updateResourceReusePolicy(reuseResourceNames, reusePolicy) | ||
| sc.resourceProfileManager.addResourceProfile(resourceProfile.get) | ||
| this | ||
| } | ||
|
|
||
| /** | ||
| * Get the ResourceProfile specified with this RDD or null if it wasn't specified. | ||
| * @return the user specified ResourceProfile or null (for Java compatibility) if | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,9 @@ import scala.collection.mutable.HashMap | |
| import org.apache.spark.{SparkConf, SparkException} | ||
| import org.apache.spark.annotation.Evolving | ||
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.internal.config.SCHEDULER_REUSE_COMPATIBLE_EXECUTORS | ||
| import org.apache.spark.internal.config.Tests._ | ||
| import org.apache.spark.resource.ResourceProfileCompatiblePolicy.{EQUAL_RESOURCES, ResourceProfileCompatiblePolicy} | ||
| import org.apache.spark.scheduler.{LiveListenerBus, SparkListenerResourceProfileAdded} | ||
| import org.apache.spark.util.Utils | ||
| import org.apache.spark.util.Utils.isTesting | ||
|
|
@@ -40,6 +42,9 @@ private[spark] class ResourceProfileManager(sparkConf: SparkConf, | |
| listenerBus: LiveListenerBus) extends Logging { | ||
| private val resourceProfileIdToResourceProfile = new HashMap[Int, ResourceProfile]() | ||
|
|
||
| private val reuseExecutors = sparkConf.get(SCHEDULER_REUSE_COMPATIBLE_EXECUTORS) | ||
| private val resourceProfileIdToCompatibleResourceProfileIds = new HashMap[Int, Set[Int]]() | ||
|
|
||
| private val (readLock, writeLock) = { | ||
| val lock = new ReentrantReadWriteLock() | ||
| (lock.readLock(), lock.writeLock()) | ||
|
|
@@ -50,6 +55,9 @@ private[spark] class ResourceProfileManager(sparkConf: SparkConf, | |
|
|
||
| def defaultResourceProfile: ResourceProfile = defaultProfile | ||
|
|
||
| var reuseResourceNames: Set[String] = Set("cores") | ||
| var reusePolicy = EQUAL_RESOURCES | ||
|
|
||
| private val dynamicEnabled = Utils.isDynamicAllocationEnabled(sparkConf) | ||
| private val master = sparkConf.getOption("spark.master") | ||
| private val isYarn = master.isDefined && master.get.equals("yarn") | ||
|
|
@@ -77,13 +85,27 @@ private[spark] class ResourceProfileManager(sparkConf: SparkConf, | |
| true | ||
| } | ||
|
|
||
| def updateResourceReusePolicy(resourceNames: Set[String], | ||
| policy: ResourceProfileCompatiblePolicy): Unit = { | ||
| writeLock.lock() | ||
| try { | ||
| reuseResourceNames = resourceNames | ||
| reusePolicy = policy | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I must be missing where this is used? Or you wanted feedback first?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added an extra param to specify policy for RDD.withResource. The code is to save reuse policy. def withResources(rp: ResourceProfile, But as you suggested setting a global policy is enough, I am thinking how to do that.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I didn't change removeExecutors logic, the executors are only removed when they timeout. |
||
| } finally { | ||
| writeLock.unlock() | ||
| } | ||
| } | ||
|
|
||
| def addResourceProfile(rp: ResourceProfile): Unit = { | ||
| isSupported(rp) | ||
| var putNewProfile = false | ||
| writeLock.lock() | ||
| try { | ||
| if (!resourceProfileIdToResourceProfile.contains(rp.id)) { | ||
| val prev = resourceProfileIdToResourceProfile.put(rp.id, rp) | ||
| if (reuseExecutors) { | ||
| calculateCompatibleProfileIds() | ||
| } | ||
| if (prev.isEmpty) putNewProfile = true | ||
| } | ||
| } finally { | ||
|
|
@@ -94,7 +116,8 @@ private[spark] class ResourceProfileManager(sparkConf: SparkConf, | |
| // force the computation of maxTasks and limitingResource now so we don't have cost later | ||
| rp.limitingResource(sparkConf) | ||
| logInfo(s"Added ResourceProfile id: ${rp.id}") | ||
| listenerBus.post(SparkListenerResourceProfileAdded(rp)) | ||
| listenerBus.post(SparkListenerResourceProfileAdded(rp, | ||
| resourceProfileIdToCompatibleResourceProfileIds.get(rp.id))) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -127,4 +150,48 @@ private[spark] class ResourceProfileManager(sparkConf: SparkConf, | |
| readLock.unlock() | ||
| } | ||
| } | ||
|
|
||
| private [spark] def getCompatibleProfileIds(rpId: Int): Set[Int] = { | ||
| readLock.lock() | ||
| try { | ||
| resourceProfileIdToCompatibleResourceProfileIds.getOrElse(rpId, Set()) | ||
| } finally { | ||
| readLock.unlock() | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * Calculate and cache all compatible profile IDs excluding itself | ||
| * This operation is done when new ResourceProfile added | ||
| */ | ||
| private def calculateCompatibleProfileIds(): Unit = { | ||
| resourceProfileIdToResourceProfile.foreach { | ||
| case (id: Int, rpEntry: ResourceProfile) => | ||
| val resourceProfile = resourceProfileFromId(id) | ||
| val compatibleIdSet = resourceProfileIdToResourceProfile.filter { | ||
| case (otherId: Int, _: ResourceProfile) => | ||
| id != otherId && rpEntry.resourcesCompatible(resourceProfile) | ||
| }.map(_._1).toSet | ||
| resourceProfileIdToCompatibleResourceProfileIds.put(id, compatibleIdSet) | ||
| } | ||
| } | ||
|
|
||
| private [spark] def dumpResourceProfiles(): Unit = { | ||
| readLock.lock() | ||
| try { | ||
| logDebug("dump resource profiles:") | ||
| resourceProfileIdToResourceProfile.foreach { case (id: Int, rpEntry: ResourceProfile) => | ||
| logDebug("-- rpId " + id + ": " + rpEntry) | ||
| } | ||
| if (reuseExecutors) { | ||
| logDebug("dump compatible resource profile ids:") | ||
| resourceProfileIdToCompatibleResourceProfileIds.foreach { | ||
| case (id: Int, compatibleIds: Set[Int]) => | ||
| logDebug("-- rpId " + id + ": " + compatibleIds) | ||
| } | ||
| } | ||
| } finally { | ||
| readLock.unlock() | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -283,8 +283,9 @@ case class SparkListenerApplicationEnd(time: Long) extends SparkListenerEvent | |
| case class SparkListenerLogStart(sparkVersion: String) extends SparkListenerEvent | ||
|
|
||
| @DeveloperApi | ||
| @Since("3.1.0") | ||
| case class SparkListenerResourceProfileAdded(resourceProfile: ResourceProfile) | ||
| @Since("3.3.0") | ||
| case class SparkListenerResourceProfileAdded(resourceProfile: ResourceProfile, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer not to change this interface as it gets used by people. I'd rather create a new one if needed but need to look at how its all used. If its just for environment page to show compatible, I'm not sure its worth it. WE can come back to this once figure out main logic
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's for event logging and for showing compatible info in UI. I think it's a natural extension for ResourceProfileAdded event. We can leave it right now and pls let me know if we want a seperate event to address event with compatible info.
|
||
| compatibleResourceProfileIds: Option[Set[Int]]) | ||
| extends SparkListenerEvent | ||
|
|
||
| /** | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.