-
Notifications
You must be signed in to change notification settings - Fork 29k
[SPARK-16929] Improve performance when check speculatable tasks. #16867
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
Conversation
|
@kayousterhout @squito |
squito
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is to go from an O(n log n) operation at each call to checkSpeculatableTasks to an O(lg n) operation at the completion of each task? Just in terms of complexity, there isn't a clear benefit. And I pointed out that the implementation would need to be a little more complicated (duplicate times) -- enough that I'm not sure the actual performance will be better. I feel like this needs a benchmark.
If nothing else, your change to only look at running tasks is a good one which seems like a simple, clear improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can't be a set, since multiple tasks might be running for the same amount of time. you could change it to a set of (time, count) pairs, which would make the update logic a bit more complicated, or just keep a list of all runtimes.
also the name should indicate that its the durations, eg. successfulTaskDurations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are some other things you could do to make the original code more efficient:
a) instead of .values.filter.map, use a foreach to directly add into an ArrayBuffer. That will avoid all the intermediate collections that scala would create otherwise.
b) store an approximate distribution of the runtimes, eg. using a tdigest.
aside: what pain that there is no quick way to get the middle element of a TreeSet -- I couldn't find anything efficient in either the java or scala libs :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, good find, this alone looks like a worthwhile fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Echoing what Imran said -- I'm definitely +1 on merging this simple change. The other changes in this PR add a bunch of complexity, so I'd need to see measurements demonstrating a significant improvement in performance to be convinced that we should merge them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kayousterhout
Thanks a lot for your comments :)
I will keep this simple change in this pr. For time complexity improvement, I will make another pr and try add some measurements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@squito @kayousterhout @jinxing64 Just to add, the other change does look interesting - and I can definitely see potential value in it. Would be good to see actual impact of it.
For example, when I was running jobs with 200k - 400k tasks, this never came up (though probably my config's were different from yours) - would be good to see actual impact of the other changes.
|
Jenkins, ok to test |
|
Test build #73230 has finished for PR 16867 at commit
|
|
@squito
I get the median duration by
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
taskInfos(<slice_output_above>).duration
|
Test build #73514 has finished for PR 16867 at commit
|
|
This looks like a real test failure resulting from this change |
|
Test build #73562 has finished for PR 16867 at commit
|
|
Test build #73563 has finished for PR 16867 at commit
|
cd16008 to
9778b67
Compare
|
Test build #73575 has started for PR 16867 at commit |
9778b67 to
0abcceb
Compare
|
Test build #73580 has finished for PR 16867 at commit
|
|
@jinxing64 thanks for updating this to be just the simpler fix. Since the original jira has a bit of a longer discussion on it, do you mind opening a new jira for this change, and linking it to the other one? Then we can continue discussion of your other change when you have a new pr, still on SPARK-16929 |
|
other than a bit of jira re-organization, lgtm |
|
LGTM and @squito's JIRA re-reorging sounds perfect |
|
@kayousterhout @squito |
6825bd7 to
4f2fc48
Compare
|
@kayousterhout @squito @mridulm I added a measurement for this pr in #17112 . Results are as below, newAlgorithm indicates whether we use
if
if
As we can see, new algorithm(TreeSet) has better performance than old algorithm(Arrays.sort). When tasksNum=100000, Arrays.sort costs over 100ms every time, while in new algorithm all below 20ms. |
|
Test build #73658 has finished for PR 16867 at commit
|
|
Test build #73660 has finished for PR 16867 at commit
|
|
I'm a little on the fence about this because of the added complexity, but it does seem to be a significant time improvement. Did you consider implementing this as a median heap (see the last post here: http://stackoverflow.com/questions/15319561/how-to-implement-a-median-heap/15319593). As long as this is solely to improve performance, it seems like we should do the most efficient implementation, and the heap implementation is O(1) for all operations (whereas this is logN to insert and I think similar to do the slice). |
|
Also, thanks for doing the timing measurements! |
|
Test build #74640 has finished for PR 16867 at commit
|
|
Test build #74649 has finished for PR 16867 at commit
|
| } catch { | ||
| case e: NoSuchElementException => | ||
| valid = true | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scalatest has a simpler pattern for this:
intercept[NoSuchElementException] {
medianHeap.median
}http://www.scalatest.org/user_guide/using_assertions
(I guess you could use assertThrows in this case, but I tend to always use intercept since it also lets you inspect the thrown exception.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the recommendation :)
| assert(medianHeap.median === (array(4))) | ||
| } | ||
|
|
||
| test("Size of Median should be correct though there are duplicated numbers inside.") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd remove "Size of" from the name, this is testing more than the size.
| Arrays.sort(array) | ||
| assert(medianHeap.size === 10) | ||
| assert(medianHeap.median === ((array(4) + array(5)) / 2.0)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know Kay asked for tests with a some hardcoded data, but I think these tests are too simplistic. All of these tests insert data in order, and none have significant skew.
Can you add a test case which does something like:
- inserts 10 elements with the same value (eg. 5), check the median
- insert 100 elements with a larger value (eg 10) check the median
- insert 1000 elements with an even smaller value (eg 0), check the median
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I added this change.
|
Test build #74673 has finished for PR 16867 at commit
|
|
Test build #74674 has finished for PR 16867 at commit
|
|
Test build #74675 has finished for PR 16867 at commit
|
|
@jinxing64 would you mind repeating your performance experiments with the lastest version? Both for |
|
@squito
|
|
LGTM @kayousterhout , @squito. |
|
@mridulm |
squito
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, very minor suggestions (which might just be my personal style preferences ...)
|
|
||
| // Stores all the numbers less than the current median in a smallerHalf, | ||
| // i.e median is the maximum, at the root | ||
| private[this] var smallerHalf = PriorityQueue.empty[Double](ord) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very minor -- could you make this comment a doc with /**? Even though its private, I find that helpful as that is useful in IDEs where they'll show this text w/ a hover on a reference
| * return the average of the two top values of heaps. Otherwise we return the top of the | ||
| * heap which has one more element. | ||
| */ | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: delete this blank line
| } | ||
|
|
||
| // Returns the median of the numbers. | ||
| def median: Double = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: I find comments which basically just restate the method name to be pretty pointless. I'd only include them if they add something else, eg. preconditions, or complexity, etc. Mostly I'd say they're not necessary for any of the methods here.
|
@squito |
|
Test build #74858 has started for PR 16867 at commit |
|
Test build #74870 has finished for PR 16867 at commit
|
|
@kayousterhout more comments? |
kayousterhout
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates @jinxing64! I left a few last very minor comments for the tests.
| } | ||
|
|
||
| test("Median should be correct though there are duplicated numbers inside.") { | ||
| val array = Array(0, 0, 1, 1, 2, 2, 3, 3, 4, 4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change this to something like:
Array(0, 0, 1, 1, 2, 3, 4)?
Otherwise the median heap could be handling the duplicates wrong (e.g., by not actually inserting duplicates), and the assertion at the bottom would still old. Then the check at the end can be medianHeap.median === 1.
| val medianHeap = new MedianHeap() | ||
| array.foreach(medianHeap.insert(_)) | ||
| assert(medianHeap.size() === 10) | ||
| assert(medianHeap.median === ((array(4) + array(5)) / 2.0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of indexing into the array, I think it would be clearer here to just hard-code 4.5 (it's easier to see that the median is 4.5 than to reason about the indices in the array)
|
|
||
| package org.apache.spark.util.collection | ||
|
|
||
| import java.util.Arrays |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit: can you combine these into one import (import java.util.{Arrays, NoSuchElementException})
| val medianHeap = new MedianHeap() | ||
| array.foreach(medianHeap.insert(_)) | ||
| assert(medianHeap.size() === 9) | ||
| assert(medianHeap.median === (array(4))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly here -- just medianHeap.median === 4
| assert(medianHeap.median === ((array(4) + array(5)) / 2.0)) | ||
| } | ||
|
|
||
| test("Median should be correct when skew situations.") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"when skew situations" --> "when input data is skewed"
|
Test build #75137 has finished for PR 16867 at commit
|
|
@kayousterhout |
|
Merged this to master -- thanks for all of the quick updates here @jinxing64! |
|
@kayousterhout @squito @mridulm |
What changes were proposed in this pull request?
Use a MedianHeap to record durations of successful tasks. When check speculatable tasks, we can get the median duration with O(1) time complexity.
checkSpeculatableTaskswill synchronizeTaskSchedulerImpl. IfcheckSpeculatableTasksdoesn't finish with 100ms, then the possibility exists for that thread to release and then immediately re-acquire the lock. ChangescheduleAtFixedRateto bescheduleWithFixedDelaywhen call method ofcheckSpeculatableTasks.How was this patch tested?
Added MedianHeapSuite.