Fix a bad interation with dask.distributed pickle cache - #1055
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1055 +/- ##
==========================================
- Coverage 94.23% 93.83% -0.41%
==========================================
Files 47 47
Lines 6849 6889 +40
==========================================
+ Hits 6454 6464 +10
- Misses 395 425 +30
Continue to review full report at Codecov.
|
|
@ogrisel in case you did not notice this PR (no pressure for the review, I just have not tagged you yet :)) |
|
The |
|
I pushed a new commit to also run the tests with a more recent version of dask/distributed (namely 2.17) and I can reproduce a failure I observed on my local machine when using recent distributed: This failure disappears with distributed 2.13 (I have not tried other versions) or when running the tests with distributed 2.17 on master. So this related to an interaction of this PR and somewhat recent changes in distributed. Edit: I just realized that on master we have |
|
Maybe |
|
I merged master that has the fix to make AutoBatchingMixin work and improved the test to make it more exhaustive and rewrote some old inline comments that would no longer match the current state of the PR. I think we could add a |
|
Indeed, my conclusion was that somehow, memoization does not happen during the serialization of a function call submitted to So this PR actually changes the behavior of from distributed import Client, LocalCluster
from joblib import Parallel, delayed, parallel_backend
if __name__ == "__main__":
cluster = LocalCluster(n_workers=2)
client = Client(cluster)
def f1(a, b):
a.append(1)
return a, b
def f2(tuple_, b):
a1, a2 = tuple_
a1.append(1)
return (a1, a2), b
a = []
print("f1 using distributed", client.submit(f1, a, a).result())
a = []
print("f1 using joblib+loky", Parallel()(delayed(f1)(a, a) for _ in range(1)))
a = []
with parallel_backend("dask"):
print("f1 using joblib+dask", Parallel()(delayed(f1)(a, a) for _ in range(1)))
a = []
print("f2 using distributed", client.submit(f2, (a, a), a).result())
a = []
print("f2 using joblib+loky", Parallel()(delayed(f2)((a, a), a) for _ in range(1)))
a = []
with parallel_backend("dask"):
print(
"f2 using joblib+dask", Parallel()(delayed(f2)((a, a), a) for _ in range(1))
)gives: on this branch, and on master.
|
That does not seem to be the case: >>> from distributed import LocalCluster, Client
>>> client = Client(LocalCluster(processes=2, threads_per_worker=1))
>>> def physical_identity(a, b):
... return a is b
...
>>> o1 = o2 = object()
>>> client.submit(physical_identity, o1, o2).result()
True |
|
I have no idea what's going on. It seems that objects are memoized but lists are not... |
|
This is probably caused by the fact that top level lists are copied because they can contain future instances. |
In [5]: >>> from distributed import LocalCluster, Client
...: >>> client = Client(LocalCluster(processes=2, threads_per_worker=1))
...: >>> def physical_identity(a, b):
...: ... return a is b, a[0] is b[0]
...: ...
...: >>> o1 = o2 = object()
...: >>> client.submit(physical_identity, l1, l2).result()
Out[4]: (False, True) |
|
For the record, the failure in |
|
The I can reproduce it with import tblib.pickling_support
from loky import ProcessPoolExecutor
tblib.pickling_support.install()
def raise_value_error():
raise ValueError("The message")
def test_exception_cause():
pe = ProcessPoolExecutor(max_workers=2)
f = pe.submit(raise_value_error)
try:
f.result()
except ValueError as e:
assert e.__cause__ is not None
if __name__ == "__main__":
test_exception_cause()Interestingly enough, I cannot reproduce the problem when using concurrent.futures instead of loky. I suspect that this is because I will work on an issue / PR for loky. |
|
Even if I do not fully understand what's going on with pytest and tblib, the fix I suggested above for loky actually works. I opened a PR for loky. In the mean time let's XFAIL this test in joblib. I think we just need to improve |
|
|
|
A simple In [1]: import time
...: from distributed import LocalCluster, get_client, Client, as_completed
...: from joblib import Parallel, delayed, parallel_backend
...: import numpy as np
...:
...: cluster = LocalCluster(n_workers=2)
...: client = Client(cluster)
...:
...: def sleep_and_return(i):
...: time.sleep(0.05)
...: return ithen open http://localhost:8787/status then In [2]: with parallel_backend('dask'):
...: res = Parallel(batch_size=1)(delayed(sleep_and_return)(x) for x in range(100))
...: print(res)here is how the |
|
Note that for some unknown reason, in order to prevent the |
|
Thanks @pierreglaser!
This is fine I think. |
|
Merged! |
ogrisel
left a comment
There was a problem hiding this comment.
Forgot to formally approve :)
|
I already merged master in #1601. |
https://build.opensuse.org/request/show/821624 by user dirkmueller + dimstar_suse - update to 0.16.0 - Fix a problem in the constructors of of Parallel backends classes that inherit from the `AutoBatchingMixin` that prevented the dask backend to properly batch short tasks. joblib/joblib#1062 - Fix a problem in the way the joblib dask backend batches calls that would badly interact with the dask callable pickling cache and lead to wrong results or errors. joblib/joblib#1055 - Prevent a dask.distributed bug from surfacing in joblib's dask backend during nested Parallel calls (due to joblib's auto-scattering feature) joblib/joblib#1061 - Workaround for a race condition after Parallel calls with the dask backend that would cause low level warnings from asyncio

joblib.Parallelobjects submit stateful callables todask's scheduler, which seems goes againstdask's assumption, which is that one should submit pure functions.Because of this assumption,
dasksets up a pickle cache in the workers when deserializing submitted functions. This does not cause any harm in the pure function setting, but it does when one submits two different (e.g different id in the original interpreter), stateful callables with the same pickle bytes representation. In this situation, when deserializing the second callable, the pickle cache is hit, and the deserialized objects points to the first callable. This results in the first callable's state being mutated concurrently, and yields cryptic errors, especially when the callable is something likesklearn'sBaseEstimator.fit.This PR fixes this by ensuring eachjoblibcallable sent todaskhas a unique bytes representationAfter discussion with @ogrisel, we decided to fix this issue by making the
Batchobject stateless.Related issues: #959, dask/distributed#3733