Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions core/src/main/scala/org/apache/spark/scheduler/Pool.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,29 @@ private[spark] class Pool(
parent.decreaseRunningTasks(taskNum)
}
}

override def updateAvailableSlots(numSlots: Float): Unit = {
schedulingMode match {
case SchedulingMode.FAIR =>
val usableWeights = schedulableQueue.asScala
.map(s => if (s.getSortedTaskSetQueue.nonEmpty) (s, s.weight) else (s, 0))
val totalWeights = usableWeights.map(_._2).sum
usableWeights.foreach { case (schedulable, usableWeight) =>
schedulable.updateAvailableSlots(
Math.max(numSlots * usableWeight / totalWeights, schedulable.minShare))
}
// for FIFO, allocate all slots to the first taskset in the queue
case SchedulingMode.FIFO =>
val sortedSchedulableQueue =
schedulableQueue.asScala.toSeq.sortWith(taskSetSchedulingAlgorithm.comparator)
var isFirst = true
for (schedulable <- sortedSchedulableQueue) {
Comment thread
cloud-fan marked this conversation as resolved.
schedulable.updateAvailableSlots(if (isFirst) numSlots else 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I think this is only first because only one rootPool with FIFO, maybe add comment here, I assume not really needed since sortedScheduleQueeu should just have 1 thing, but here just in case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sortedSchedulableQueue can have many tasksets even with FIFO. Its just all active tasks in the order they were submitted -- which may be multiple tasksets from a single job with a branch, or from concurrent jobs.

I do think a comment is necessary here to explain this. If I understood right, the idea here is that all other tasksets follow the old logic on resetting locality level -- for every single task that is scheduled, the locality timer gets reset. But for the first taskset, it only resets when the taskset is using all slots

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@squito is correct here.
The idea is for FIFO we assign all "available slots" to the first taskset/pool in the queue.
For FAIR we divide based on total slots based on weights, not counting weights with pools with no tasksets, and ensuring we always give at least minShare slot availability.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this makes sense. by default we run FIFO and we can have multiple tasksets at the same time with a single Job. so if we have plenty of slots to fit both tasksets in we could end up with same broken behavior we have now in the second taskset. Its perhaps slightly better as when first tasket finishes then it does go more quickly but in the mean time you could have wasted a lot of time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for pointing this out!
Instead of giving all slots to the first schedulable, I will change it so that if a schedulable has n tasks, it will only give n slots, and proceed to the next until no slots remain.
I will try to apply similar logic to the FAIR case by including the num of tasks in calculation and distributing any "unused" slots to the remaining tasks based on weight.
What are your thoughts here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so something like that is definitely better. I think you have to make sure that n tasks is the number of tasks running or pending, because a taskSet could have 10000 tasks and if 9999 of them are finished it really only needs 1 and the rest should go to the other tasksets. I'm not sure that is easily accessible right now from this location.

It kind of feels to me that FIFO should just have some boolean that is passed to the TSM to tell if it there are remaining slots. with FIFO, if I have multiple tasks set, they do go in priority order, but if the higher priority one passes slots up there is nothing keeping the other tasksets from using it. So I don't know why we should make the locality preference skewed. its definitely not any worse then we have now and I'm not sure how you would really get away from this without going to something more like the node delay vs task delay.

The fair scheduler side I need to think about more. I think it has similar issue with the totalSlots vs how many tasks it actually has left to run. For instance you have 2 pools with 1 taskset each, both with equal fair shares, the first one has run enough where it only has 2 tasks left so do you send the rest of the free slots to the second tsm so it can use more of the cluster. I would generally say yes and I think that is the way it acts now without this patch. The shares for the Fair scheduler just appear to sort the tasksets and they are visited in that order but there doesn't appear to be any real hard enforcement of the limits.

isFirst = false
}
case _ =>
val msg = s"Unsupported scheduling mode: $schedulingMode. Use FAIR or FIFO instead."
throw new IllegalArgumentException(msg)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ private[spark] trait Schedulable {
def stageId: Int
def name: String

def updateAvailableSlots(numSlots: Float): Unit
Comment thread
viirya marked this conversation as resolved.
def addSchedulable(schedulable: Schedulable): Unit
def removeSchedulable(schedulable: Schedulable): Unit
def getSchedulableByName(name: String): Schedulable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import org.apache.spark.rpc.RpcEndpoint
import org.apache.spark.scheduler.SchedulingMode.SchedulingMode
import org.apache.spark.scheduler.TaskLocality.TaskLocality
import org.apache.spark.storage.BlockManagerId
import org.apache.spark.util.{AccumulatorV2, SystemClock, ThreadUtils, Utils}
import org.apache.spark.util.{AccumulatorV2, Clock, SystemClock, ThreadUtils, Utils}

/**
* Schedules tasks for multiple types of clusters by acting through a SchedulerBackend.
Expand All @@ -61,7 +61,8 @@ import org.apache.spark.util.{AccumulatorV2, SystemClock, ThreadUtils, Utils}
private[spark] class TaskSchedulerImpl(
val sc: SparkContext,
val maxTaskFailures: Int,
isLocal: Boolean = false)
isLocal: Boolean = false,
clock: Clock = new SystemClock)
extends TaskScheduler with Logging {

import TaskSchedulerImpl._
Expand Down Expand Up @@ -125,10 +126,12 @@ private[spark] class TaskSchedulerImpl(

protected val hostsByRack = new HashMap[String, HashSet[String]]

protected val executorIdToCores = new HashMap[String, Int]
protected var totalSlots = 0

protected val executorIdToHost = new HashMap[String, String]

private val abortTimer = new Timer(true)
private val clock = new SystemClock
// Exposed for testing
val unschedulableTaskSetToExpiryTime = new HashMap[TaskSetManager, Long]

Expand Down Expand Up @@ -403,6 +406,8 @@ private[spark] class TaskSchedulerImpl(
if (!executorIdToRunningTaskIds.contains(o.executorId)) {
hostToExecutors(o.host) += o.executorId
executorAdded(o.executorId, o.host)
executorIdToCores(o.executorId) = o.cores
totalSlots += o.cores / CPUS_PER_TASK
executorIdToHost(o.executorId) = o.host
executorIdToRunningTaskIds(o.executorId) = HashSet[Long]()
newExecAvail = true
Expand Down Expand Up @@ -439,6 +444,8 @@ private[spark] class TaskSchedulerImpl(
}
}

rootPool.updateAvailableSlots(totalSlots)

// Take each TaskSet in our scheduling order, and then offer it each node in increasing order
// of locality levels so that it gets a chance to launch local tasks on all of them.
// NOTE: the preferredLocality order: PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY
Expand Down Expand Up @@ -808,6 +815,7 @@ private[spark] class TaskSchedulerImpl(
// happen below in the rootPool.executorLost() call.
taskIds.foreach(cleanupTaskState)
}
executorIdToCores.remove(executorId).foreach(totalSlots -= _ / CPUS_PER_TASK)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that rather than calling rootPool.updateAvailableSlots() on every resourceOffer, it would be more efficient to just call it when the total number has changed -- here and in the resourceOffer branch where its updated. Might not matter much, hopefully updateAvailableSlots() isn't too expensive, but still I think that would be better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateAvailableSlots is dependent on both the total number of slots + what tasksets/pools are present. Will take a look to see how feasible doing the update incrementally is.


val host = executorIdToHost(executorId)
val execs = hostToExecutors.getOrElse(host, new HashSet)
Expand Down
30 changes: 20 additions & 10 deletions core/src/main/scala/org/apache/spark/scheduler/TaskSetManager.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ private[spark] class TaskSetManager(
.map { case (t, idx) => t.partitionId -> idx }.toMap
val numTasks = tasks.length
val copiesRunning = new Array[Int](numTasks)
var numAvailableSlot: Float = 0

val speculationEnabled = conf.get(SPECULATION_ENABLED)
// Quantile of tasks at which to start speculation
Expand Down Expand Up @@ -200,10 +201,13 @@ private[spark] class TaskSetManager(
private[scheduler] var localityWaits = myLocalityLevels.map(getLocalityWait)

// Delay scheduling variables: we keep track of our current locality level and the time we
// last launched a task at that level, and move up a level when localityWaits[curLevel] expires.
// last launched a task at that level and were also fully utilizing all slots

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: period at end of sentence.

I'd also reference here that the idea of "all slots" is dependent on the scheduling mode and more details are in Pool.updateAvailableSlots() (and as mentioned previously we need more comments there).

// Move up a level when localityWaits[curLevel] expires.
// We then move down if we manage to launch a "more local" task.
private var currentLocalityIndex = 0 // Index of our current locality level in validLocalityLevels
private var lastLaunchTime = clock.getTimeMillis() // Time we last launched a task at this level
// Time we last launched a task at this level
// and number of running tasks was greater than or equal to numAvailableSlot
private var lastFullUtilizationTime = clock.getTimeMillis()

override def schedulableQueue: ConcurrentLinkedQueue[Schedulable] = null

Expand Down Expand Up @@ -410,11 +414,13 @@ private[spark] class TaskSetManager(
execId, host, taskLocality, speculative)
taskInfos(taskId) = info
taskAttempts(index) = info :: taskAttempts(index)
// Update our locality level for delay scheduling
// Update our locality level for delay scheduling if all allocated slots are being utilized
// NO_PREF will not affect the variables related to delay scheduling
if (maxLocality != TaskLocality.NO_PREF) {
// Adding 1 to runningTasks, since runningTasks doesn't include this task yet
val utilizingAllSlots = runningTasks + 1 >= numAvailableSlot
if (maxLocality != TaskLocality.NO_PREF && utilizingAllSlots) {
currentLocalityIndex = getLocalityIndex(taskLocality)
lastLaunchTime = curTime
lastFullUtilizationTime = curTime
}
// Serialize and return the task
val serializedTask: ByteBuffer = try {
Expand Down Expand Up @@ -534,14 +540,14 @@ private[spark] class TaskSetManager(
// This is a performance optimization: if there are no more tasks that can
// be scheduled at a particular locality level, there is no point in waiting
// for the locality wait timeout (SPARK-4939).
lastLaunchTime = curTime
lastFullUtilizationTime = curTime
logDebug(s"No tasks for locality level ${myLocalityLevels(currentLocalityIndex)}, " +
s"so moving to locality level ${myLocalityLevels(currentLocalityIndex + 1)}")
currentLocalityIndex += 1
} else if (curTime - lastLaunchTime >= localityWaits(currentLocalityIndex)) {
// Jump to the next locality level, and reset lastLaunchTime so that the next locality
// wait timer doesn't immediately expire
lastLaunchTime += localityWaits(currentLocalityIndex)
} else if (curTime - lastFullUtilizationTime >= localityWaits(currentLocalityIndex)) {
// Jump to the next locality level, and reset lastFullUtilizationTime
// so that the next locality wait timer doesn't immediately expire
lastFullUtilizationTime += localityWaits(currentLocalityIndex)
logDebug(s"Moving to ${myLocalityLevels(currentLocalityIndex + 1)} after waiting for " +
s"${localityWaits(currentLocalityIndex)}ms")
currentLocalityIndex += 1
Expand Down Expand Up @@ -1054,6 +1060,10 @@ private[spark] class TaskSetManager(
def executorAdded(): Unit = {
recomputeLocality()
}

override def updateAvailableSlots(numSlots: Float): Unit = {
numAvailableSlot = numSlots
}
}

private[spark] object TaskSetManager {
Expand Down
62 changes: 62 additions & 0 deletions core/src/test/scala/org/apache/spark/scheduler/PoolSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,68 @@ class PoolSuite extends SparkFunSuite with LocalSparkContext {
assert(nextTaskSetToSchedule.get.stageId === expectedStageId)
}

test("validate FIFO slot distributions") {
Comment thread
viirya marked this conversation as resolved.
sc = new SparkContext(LOCAL, APP_NAME)
val taskScheduler = new TaskSchedulerImpl(sc)

val rootPool = new Pool("", FIFO, 0, 0)
val schedulableBuilder = new FIFOSchedulableBuilder(rootPool)

val taskSetManager0 = createTaskSetManager(0, 2, taskScheduler)
val taskSetManager1 = createTaskSetManager(1, 2, taskScheduler)
val taskSetManager2 = createTaskSetManager(2, 2, taskScheduler)
schedulableBuilder.addTaskSetManager(taskSetManager0, null)
schedulableBuilder.addTaskSetManager(taskSetManager1, null)
schedulableBuilder.addTaskSetManager(taskSetManager2, null)

rootPool.updateAvailableSlots(9)
assert(taskSetManager0.numAvailableSlot === 9)
assert(taskSetManager1.numAvailableSlot === 0)
assert(taskSetManager2.numAvailableSlot === 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also check its right after you remove tasksets and add more.

}

test("validate FAIR slot distributions") {
val xmlPath = getClass.getClassLoader.getResource("fairscheduler-with-valid-data.xml").getFile()
val conf = new SparkConf().set(SCHEDULER_ALLOCATION_FILE, xmlPath)
sc = new SparkContext(LOCAL, APP_NAME, conf)
val taskScheduler = new TaskSchedulerImpl(sc)

val rootPool = new Pool("", FAIR, 0, 0)
val schedulableBuilder = new FairSchedulableBuilder(rootPool, sc.conf)
schedulableBuilder.buildPools()

val taskSetManager0 = createTaskSetManager(0, 2, taskScheduler)
val taskSetManager1 = createTaskSetManager(1, 2, taskScheduler)
sc.setLocalProperty(SparkContext.SPARK_SCHEDULER_POOL, "pool1")
var localProperties = sc.getLocalProperties
schedulableBuilder.addTaskSetManager(taskSetManager0, localProperties)
schedulableBuilder.addTaskSetManager(taskSetManager1, localProperties)

sc.setLocalProperty(SparkContext.SPARK_SCHEDULER_POOL, "pool2")
localProperties = sc.getLocalProperties
val taskSetManager2 = createTaskSetManager(2, 2, taskScheduler)
val taskSetManager3 = createTaskSetManager(3, 2, taskScheduler)
schedulableBuilder.addTaskSetManager(taskSetManager2, localProperties)
schedulableBuilder.addTaskSetManager(taskSetManager3, localProperties)


rootPool.updateAvailableSlots(12)
// pool 1 weight is 1, pool 2 weight is 2, so they get 4 and 8 slots respectively
// since pool 1 is FIFO TSM 0 gets all 4 slots of pool 1
// since pool 2 is FAIR TSM 3 and 4 each get half (4) of pool 2's 8 slots
assert(taskSetManager0.numAvailableSlot === 4)
assert(taskSetManager1.numAvailableSlot === 0)
assert(taskSetManager2.numAvailableSlot === 4)
assert(taskSetManager3.numAvailableSlot === 4)

rootPool.updateAvailableSlots(6)
// similar to above but pool 1 has 3 slots due to having minShare of 3
assert(taskSetManager0.numAvailableSlot === 3)
assert(taskSetManager1.numAvailableSlot === 0)
assert(taskSetManager2.numAvailableSlot === 2)
assert(taskSetManager3.numAvailableSlot === 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, expand this to include removing and adding tasksets

}

test("FIFO Scheduler Test") {
sc = new SparkContext(LOCAL, APP_NAME)
val taskScheduler = new TaskSchedulerImpl(sc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,63 @@ class TaskSchedulerImplSuite extends SparkFunSuite with LocalSparkContext with B
assert(!failedTaskSet)
}

test("executors should not sit idle for too long") {
val LOCALITY_WAIT_MS = 3000
val clock = new ManualClock
val conf = new SparkConf()
sc = new SparkContext("local", "TaskSchedulerImplSuite", conf)
val taskScheduler = new TaskSchedulerImpl(sc,
sc.conf.get(config.TASK_MAX_FAILURES),
clock = clock) {
override def createTaskSetManager(taskSet: TaskSet, maxTaskFailures: Int): TaskSetManager = {
new TaskSetManager(this, taskSet, maxTaskFailures, blacklistTrackerOpt, clock)
}
override def shuffleOffers(offers: IndexedSeq[WorkerOffer]): IndexedSeq[WorkerOffer] = {
// Don't shuffle the offers around for this test. Instead, we'll just pass in all
// the permutations we care about directly.
offers
}
}
// Need to initialize a DAGScheduler for the taskScheduler to use for callbacks.
new DAGScheduler(sc, taskScheduler) {
override def taskStarted(task: Task[_], taskInfo: TaskInfo): Unit = {}

override def executorAdded(execId: String, host: String): Unit = {}
}
taskScheduler.initialize(new FakeSchedulerBackend)
val taskSet = FakeTask.createTaskSet(5, 1, 1,
Seq(TaskLocation("host1", "exec1")),
Seq(TaskLocation("host1", "exec1")),
Seq(TaskLocation("host1", "exec1")),
Seq(TaskLocation("host1", "exec1")),
Seq(TaskLocation("host1", "exec1"))
)
taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1)))
taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1)))
taskScheduler.submitTasks(taskSet)

// First offer host2, exec2: no task should be chosen due to bad data locality
assert(taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec2", "host2", 1)))
.flatten.isEmpty)
val task0 = taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1)))
.flatten.head
assert(task0.index === 0)
taskScheduler.statusUpdate(task0.taskId, TaskState.FINISHED, ByteBuffer.allocate(0))

clock.advance(LOCALITY_WAIT_MS * 2)
val task1 = taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1)))
.flatten.head
assert(task1.index === 1)
val task2 = taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec2", "host2", 1)))
.flatten.head

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before the fix, this would fail here, with a NoSuchElementException, rather than at the assert. Would be a little bit more clear to assert its nonempty. Also a comment here explaining that this is the most important check -- even though we've just scheduled something task local, we don't delay our use of exec2.

assert(task2.index === 2)
taskScheduler.statusUpdate(task1.taskId, TaskState.FINISHED, ByteBuffer.allocate(0))
assert(taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1)))
.flatten.head.index === 3)
assert(taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec2", "host2", 1)))
.flatten.isEmpty)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoudl add some tests with more executors getting added & removed.

}

test("Scheduler does not crash when tasks are not serializable") {
val taskCpus = 2
val taskScheduler = setupSchedulerWithMaster(
Expand Down Expand Up @@ -910,6 +967,7 @@ class TaskSchedulerImplSuite extends SparkFunSuite with LocalSparkContext with B
test("SPARK-16106 locality levels updated if executor added to existing host") {
val taskScheduler = setupScheduler()

taskScheduler.resourceOffers(IndexedSeq(new WorkerOffer("executor0", "host0", 1)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note of why this line was necessary.

The current locality level (currentLocalityIndex) of a TSM is defaulted to the current "most local" level when a taskset is submitted. Before this line was added, that level was ANY as the scheduler was not aware of any resources.

Locality levels are recomputed whenever a new executor is detected, but the current locality level remains the same. Previously the test was passing because the first task that began running was NODE_LOCAL, hence the locality level was set to node local, causing the second task not to run.

Since this PR only resets locality level when all slots are utilized, this locality level reset not happening.
This new line makes it such that the starting locality level is not ANY

taskScheduler.submitTasks(FakeTask.createTaskSet(2, stageId = 0, stageAttemptId = 0,
(0 until 2).map { _ => Seq(TaskLocation("host0", "executor2")) }: _*
))
Expand Down