Use concurrent.futures in local scheduler#6322
Conversation
|
Cool. It's nice to see how little had to change here. |
|
Thanks! Was pleased as well. Didn't take long to do either. There is still a bit of debugging to figure out some exception handling cases. I think |
We would want the same behavior as before. I think that apply_async/get have the same semantics as submit/result, so this shouldn't be hard to acheive I hope. |
|
Are we wanting to still support |
Is performance roughly the same between the old way and this PR? If so then I don't see a reason to keep the old way around. |
|
I haven't compared yet. Since Part of the reason I ask about legacy support is we do have some tests using pools instead, which would need to change somehow. Also in the docs, we mention configuring a Sorry for laboring on this point so much. Just trying to be transparent 🙂 |
|
Personally I'm comfortable breaking the pool argument.
I recommend that we hold off of process differences between
concurrent.futures and pools until we observe a meaningful difference.
…On Tue, Jun 16, 2020 at 12:00 PM jakirkham ***@***.***> wrote:
I haven't compared yet.
Since concurrent.futures is designed to allow threads/processes to spin
up during use and spin down when unused, the performance could differ on
initial runs or workflows with large gaps between runs. Is this something
we care about? If not, it is simple to benchmark in a loop with sufficient
iterations.
Part of the reason I ask about legacy support is we do have some tests
using pools instead, which would need to change somehow. Also in the docs,
we mention configuring a pool
<https://docs.dask.org/en/latest/scheduling.html#configuration>. Though
the docs do state, "some of the scheduler support other keyword arguments".
So maybe this is not guaranteed to work? IDK how common this use case is
the wild either. Users may just be choosing "threads" or "processes"
instead.
Sorry for laboring on this point so much. Just trying to be transparent 🙂
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#6322 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACKZTDT434YFUQDPAD36IDRW66LDANCNFSM4N57EISQ>
.
|
Thanks Matt 🙂
I didn't quite follow this bit. Can you please clarify? |
|
I'm referring to this:
Since concurrent.futures is designed to allow threads/processes to spin
up during use and spin down when unused, the performance could differ on
initial runs or workflows with large gaps between runs. Is this something
we care about? If not, it is simple to benchmark in a loop with sufficient
iterations.
My recommendation is to try things out first. I'm guessing that an initial
performance test won't take more than 30-60 minutes to run, and that seems
like a better use of time than engaging many people in a technical
discussion.
…On Tue, Jun 16, 2020 at 12:35 PM jakirkham ***@***.***> wrote:
Personally I'm comfortable breaking the pool argument.
Thanks Matt 🙂
I recommend that we hold off of process differences between
concurrent.futures and pools until we observe a meaningful difference.
I didn't quite follow this bit. Can you please clarify?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#6322 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACKZTCULJEATK3C6IEON5LRW7CRJANCNFSM4N57EISQ>
.
|
jakirkham
left a comment
There was a problem hiding this comment.
Noticed some issues trying to query the num_workers with these concurrent.futures use case as shown below. This seems to be used by some logic in get_async to avoid oversubscription. Given concurrent.futures already has its own logic for avoiding oversubscription, am going to try removing our logic and the num_workers parameter and see if that works. Not expecting any issues. Just trying to document this for transparency.
|
So I think this is in a better state now. Remaining things...
|
a1fac00 to
bcc318c
Compare
Yeah, this looks great! It's cool to see how cleanly this turned out.
Hrm, that's odd.
We could also hold off until we drop Python 3.6, which may not be so long. |
|
The next question I would have would be about performance. I'm curious how many trivial tasks per second we can push through this relative to the old method. I suspect that there are some benchmarks in dask-benchmarks for this, but if not this might also be a good opportunity to add them if you found that interesting. |
Yeah agreed. I'll try to look closer, but would certainly appreciate pointers if anything looks odd here.
For context, in Python 3.7 What we have done is switch to using The My guess is these workarounds have captured the common use cases on Python 3.6. So it's now mostly a matter of updating the tests accordingly.
It's certainly a fair question. However I want to hold off on this until we have something that has the behavior we expect. As it stands the order test failures worry me. So would not feel comfortable measuring performance until those are addressed. Otherwise we are merely how fast we can do the wrong thing 😉 |
|
It's certainly a fair question. However I want to hold off on this until
we have something that has the behavior we expect. As it stands the order
test failures worry me. So would not feel comfortable measuring performance
until those are addressed. Otherwise we are merely how fast we can do the
wrong thing 😉
The counter-argument here is that if it's going to be slower then we're
going to abandon this approach anyway. Assuming that someone is paying for
your time they may want to get some signal that this is a worthwhile
endeavor before making sure that everything is just right.
…On Mon, Jun 22, 2020 at 11:13 AM jakirkham ***@***.***> wrote:
A couple of order tests fail
Hrm, that's odd.
Yeah agreed. I'll try to look closer, but would certainly appreciate
pointers if anything looks odd here.
Python 3.6 remains an odd case (may need to skip some tests there)
We could also hold off until we drop Python 3.6, which may not be so long.
For context, in Python 3.7 concurrent.futures added support for an
mp_context (so multiprocessing.get_context(...) can be used) and
initializer (so new processes can run an initialization function). We use
both of these features today, which would not work out-of-the-box on Python
3.6.
What we have done is switch to using multiprocessing.set_start_method(...)
on Python 3.6 only, which means we can only set the context once in that
case. Though that is probably good enough in most practical cases where
users just pick spawn or some flavor fork and stick with it (feel free to
let me know examples if not). Though there are tests that change the
multiprocessing method repeatedly in a session, which are then broken on
Python 3.6. I'm thinking we just skip these there.
The initializer part was easier to address, which we have done by just
wrapping user function calls to make sure initialization happens on Python
3.6. We can probably tweak this a bit if needed. Though it seems to get the
right behavior.
My guess is these workarounds have captured the common use cases on Python
3.6. So it's now mostly a matter of updating the tests accordingly.
The next question I would have would be about performance.
It's certainly a fair question. However I want to hold off on this until
we have something that has the behavior we expect. As it stands the order
test failures worry me. So would not feel comfortable measuring performance
until those are addressed. Otherwise we are merely how fast we can do the
wrong thing 😉
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#6322 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACKZTASVSVXJ7X6WOGJEMDRX6NLFANCNFSM4N57EISQ>
.
|
Provides a simple synchronous `Executor` for use with `concurrent.futures`. Simply executes any function once submitted and stores the result or exception in a `Future` returned to the user. This matches closely with our current implementation of synchronous execution. Also it is simpler to implement than say delaying execution until the `Future`'s `.result(...)` is queried. Besides as we already collect and deduplicate all work to do in the local scheduler before submitting it, there is no material difference between computing immediately or when the result is requested.
As any `Exception` (or subclass thereof) that is raised by the callback will be logged instead of allowed to propagate through, we don't get the excepted exception handling behavior with `concurrent.futures` when grabbing the result here. So pass the `Future` instead so that this is delayed until the `Future`'s result is later retrieved. Should allow the exception to be handled in the proper context.
We can just handle this with the `callback` directly. So drop the wrapper function. It was mostly for debugging anyways.
|
Have added some logic to make batching optional. Though users can configure this through an argument to By default it is off for the |
ab4a3d9 to
8810902
Compare
Choose a small, but non-`1` value for batching with multiprocessing. It's helpful to have some batching here. Though `5` is a rough guess based on some benchmarking (and we may want to change that later).
8810902 to
cc61931
Compare
As the `process` case uses a `chunksize=6`, this test will wind up putting all of the work in 1 job. To fix that, update the test to use a `chunksize=1` to ensure each task goes to a new worker.
|
There's a few other lingering places where we still use |
|
@dask/maintenance it would be good to get some thoughts on this when someone has a chance 🙂 |
concurrent.futures in local scheduler
|
Planning on merging EOD Friday if no comments |
Now that the local scheduler uses `concurrent.futures`, change this test to use a `concurrent.futures` based `Executor` as well. While the `multiprocessing.Pool` would still work, this is the recommended way to go in the future.
|
Sorry I'm late to seeing this. Does this break the old |
|
No worries. Figured people might stop by later and am generally happy to deal with any issues that come up 🙂 Yep this was designed to be backwards compatible. So setting a Please let us know if you have any other questions |
| Defaults to 5 as some batching is helpful. | ||
| If -1, will be computed to evenly divide ready work across workers. | ||
| """ | ||
| chunksize = chunksize or config.get("chunksize", 6) |
There was a problem hiding this comment.
Thanks for flagging this @jakirkham -- it made it a lot easier to track down where this came from when users started encountering a "lack of parallelism" with trivial flows. I'll be maintaining the Dask executors in Prefect now and would appreciate similar pings in the future if you remember :)
The main debug of this issue can be seen in #7583. I went through the entire series of steps for this where I started with no knowledge of how this worked to now coming up with ideas for how this could perform better 😉 ### The Issues The issues fixed here were introduced in #6322. They can be tested by monitoring/profiling the memory usage from this example: ```python from dask.diagnostics import ResourceProfiler, Profiler, CacheProfiler, visualize import dask.array as da from datetime import datetime if __name__ == "__main__": with Profiler() as prof, ResourceProfiler(dt=0.5) as rprof, CacheProfiler() as cprof: d = da.random.random((5000, 250000), chunks=(1, -1)).sum() d.compute(optimize_graph=False) #visualize( # [rprof, prof, cprof], # file_path=f'/tmp/profile_{datetime.utcnow():%Y%m%d_%H%M%S}.html', # show=False # ) print(max(x.mem for x in rprof.results)) ``` If you uncomment the visualize call then you can see the full profile output for it (see the related issue for screenshots). Ever since #6322 the memory for this example is huge at ~10GB max. Prior to that PR the max memory was ~220MB. ### Fixes This PR mostly reverts us to the behavior before the concurrent.futures PR, but keeps the concurrent.futures API.
This adds support for
concurrent.futures.Executors in the local scheduler. It also swaps out the existing synchronous, multithreaded, and multiprocessing execution engines forconcurrent.futures.Executor-based implementations. Though legacy support for using the oldmultiprocessing.Poolexecution engines remains. Both cases are covered in the threading and multiprocessing tests.For the synchronous case, we have rolled our own
SynchronousExecutor, which simply runs a function immediately whensubmitis called (storing the result or exception in aFuture). This matches the current behavior we have for the synchronous case. It is also easier to implement/maintain than alternatives.To provide support for the legacy
multiprocessing.Poolexecution engines, aMultiprocessingPoolExecutoris provided. Whenever amultiprocessing.Poolinstance is encountered either in the threading or multiprocessing case, it is wrapped with aMultiprocessingPoolExecutor. TheMultiprocessingPoolExecutorprovides a very thin layer that does what is necessary to wrapapply_asyncwith asubmitcall as well as store the result or exception in aFuture.The multithreading and multiprocessing cases are updated to just use the corresponding
ThreadPoolExecutorandProcessPoolExecutor. Docs are also updated to reflect this pattern. Also tests are updated to this usage model as well.Internally the
get_asyncfunction looks largely the same, but now takes anExecutor.submitmethod (though any function that looks like it should also work). Passing anapply_asyncfunction to it no longer works. User looking to still do that should consider usingMultiprocessingPoolExecutororget_apply_async, which will help wrap calls toapply_asyncin aconcurrent.futurescompatible manner. Longer term users should consider migrating toconcurrent.futuresdirectly.In addition to these changes, the local scheduler now batches several tasks per
submitcall. This is important for cases like multiprocessing where there can be a fair bit of overhead to setting up a process and communicate to/from it. Though this strategy is used in all cases. This results in comparable (if not slightly better) performance in all cases.black dask/flake8 daskCloses #6220