Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix a bad interation with dask.distributed pickle cache - #1055

Merged
ogrisel merged 25 commits into
joblib:masterfrom
pierreglaser:joblib-batch-uuid
Jul 1, 2020
Merged

Fix a bad interation with dask.distributed pickle cache#1055
ogrisel merged 25 commits into
joblib:masterfrom
pierreglaser:joblib-batch-uuid

Conversation

@pierreglaser

@pierreglaser pierreglaser commented May 23, 2020

Copy link
Copy Markdown
Contributor

joblib.Parallel objects submit stateful callables to dask's scheduler, which seems goes against dask's assumption, which is that one should submit pure functions.

Because of this assumption, dask sets 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 like sklearn 's BaseEstimator.fit.

This PR fixes this by ensuring each joblib callable sent to dask has a unique bytes representation
After discussion with @ogrisel, we decided to fix this issue by making the Batch object stateless.

Related issues: #959, dask/distributed#3733

@codecov

codecov Bot commented May 23, 2020

Copy link
Copy Markdown

Codecov Report

Merging #1055 into master will decrease coverage by 0.40%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            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     
Impacted Files Coverage Δ
joblib/_dask.py 93.90% <100.00%> (-0.99%) ⬇️
joblib/test/test_dask.py 98.79% <100.00%> (+0.64%) ⬆️
joblib/test/test_parallel.py 96.92% <100.00%> (-0.10%) ⬇️
joblib/backports.py 44.73% <0.00%> (-39.48%) ⬇️
joblib/_memmapping_reducer.py 94.33% <0.00%> (-2.27%) ⬇️
joblib/test/test_memmapping.py 97.33% <0.00%> (-1.91%) ⬇️
joblib/pool.py 86.17% <0.00%> (-1.63%) ⬇️
joblib/disk.py 90.47% <0.00%> (-1.59%) ⬇️
joblib/logger.py 85.52% <0.00%> (-1.32%) ⬇️
joblib/func_inspect.py 90.41% <0.00%> (-1.20%) ⬇️
... and 9 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dfc11fe...360e8d9. Read the comment docs.

@pierreglaser

Copy link
Copy Markdown
Contributor Author

@ogrisel in case you did not notice this PR (no pressure for the review, I just have not tagged you yet :))

@ogrisel ogrisel left a comment

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 do not really understand how this is related to #959.

I still get the error when trying the reproducer of #959 with this branch:

TypeError: 'CancelledError' object is not iterable

Comment thread joblib/test/test_dask.py
@pierreglaser

pierreglaser commented May 27, 2020

Copy link
Copy Markdown
Contributor Author

#959 is not fixed by this PR only, we also need to tweak the scattering properties of dask, but this is orthogonal to this PR. However, the pattern that this PR fixes appeared in #959.

@ogrisel

ogrisel commented Jun 15, 2020

Copy link
Copy Markdown
Contributor

The linux_pypy3 failure is an instance of the random failure of test_nested_exception_dispatch[multiprocessing] already tracked in #1034 as is unrelated to the dask backend.

@ogrisel

ogrisel commented Jun 15, 2020

Copy link
Copy Markdown
Contributor

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:

_____________________________ test_manual_scatter ______________________________

loop = <tornado.platform.asyncio.AsyncIOLoop object at 0x7f93ae9cce50>

                             f(z, z, x, d=z, e=y)]
                    expected = [func(*args, **kwargs)
                                for func, args, kwargs in tasks]
                    results = Parallel()(tasks)
    
                # Scatter must take a list/tuple
                with pytest.raises(TypeError):
                    with parallel_backend('dask', loop=loop, scatter=1):
                        pass
    
        assert results == expected
    
        # Scattered variables only serialized once
        assert x.count == 1
        assert y.count == 1
>       assert z.count == 6
E       assert 4 == 6
E         +4
E         -6

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 assert z.count == 4 instead.

@ogrisel

ogrisel commented Jun 15, 2020

Copy link
Copy Markdown
Contributor

Maybe z is sometimes memoized in a Batch?

@ogrisel

ogrisel commented Jun 15, 2020

Copy link
Copy Markdown
Contributor

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 __repr__ to the Batch class to display the name of the wrapped function name and the number of batched tasks to make it more informative when using the dask dashboard.

@pierreglaser

pierreglaser commented Jun 17, 2020

Copy link
Copy Markdown
Contributor Author

Indeed, my conclusion was that somehow, memoization does not happen during the serialization of a function call submitted to distributed's Client.

So this PR actually changes the behavior of joblib when the user submits a function that modifies its inputs:

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:

f1 using distributed ([1], [])
f1 using joblib+loky [([1], [1])]
f1 using joblib+dask [([1], [])]
f2 using distributed (([1], []), [])
f2 using joblib+loky [(([1], [1]), [1])]
f2 using joblib+dask [(([1], []), [])]

on this branch, and

f1 using distributed ([1], [])
f1 using joblib+loky [([1], [1])]
f1 using joblib+dask [([1], [1])]
f2 using distributed (([1], []), [])
f2 using joblib+loky [(([1], [1]), [1])]
f2 using joblib+dask [(([1], [1]), [1])]

on master.

f2 highlights the fact that there is no memoization happening at all, even when serializing only one attribute (one could imagine that distribubuted separates the serialization of each args of a submitted functions).

@ogrisel

ogrisel commented Jun 18, 2020

Copy link
Copy Markdown
Contributor

one could imagine that distribubuted separates the serialization of each args of a submitted functions

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

@ogrisel

ogrisel commented Jun 18, 2020

Copy link
Copy Markdown
Contributor

I have no idea what's going on. It seems that objects are memoized but lists are not...

@ogrisel

ogrisel commented Jun 18, 2020

Copy link
Copy Markdown
Contributor

This is probably caused by the fact that top level lists are copied because they can contain future instances.

Comment thread azure-pipelines.yml Outdated
@ogrisel

ogrisel commented Jun 18, 2020

Copy link
Copy Markdown
Contributor
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)

@ogrisel

ogrisel commented Jun 19, 2020

Copy link
Copy Markdown
Contributor

For the record, the failure in test_nested_exception_dispatch[loky] is caused by a side effect of the import of the distributed package in test_parallel.py but only in Python 3.8. Still investigating.

@ogrisel

ogrisel commented Jun 19, 2020

Copy link
Copy Markdown
Contributor

The test_nested_exception_dispatch[loky] failure is caused by tblib.pickling_support.install() used in distributed.

I can reproduce it with loky and tblib (without joblib and distributed) but only under a pytest session:

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 _ExceptionWithTraceback inherits from BaseException in loky but not in concurrent.futures and that causes a bad interaction with pytest and tblib for some reason I do not quite understand.

I will work on an issue / PR for loky.

@ogrisel

ogrisel commented Jun 19, 2020

Copy link
Copy Markdown
Contributor

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 Batch.__repr__ to be able to merge this PR.

@ogrisel

ogrisel commented Jun 21, 2020

Copy link
Copy Markdown
Contributor

test_thread_bomb_mitigation[loky] used to be a random failure but now seems to fail 100% on the time on this PR. This is possibly again because of a side effect of tblib: will need to investigate.

Comment thread joblib/test/test_dask.py Outdated
@pierreglaser

pierreglaser commented Jun 30, 2020

Copy link
Copy Markdown
Contributor Author

A simple joblib+dask example to showcase how joblib's Batch function now appear:

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 i

then 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 dask dashboard looks:

image

@pierreglaser

Copy link
Copy Markdown
Contributor Author

Note that for some unknown reason, in order to prevent the Batch name to be truncated in the dashboard, I had to use underscores and not dashes in the repr of the Batch to separate words. Weird but I don't have time to investigate.

@ogrisel

ogrisel commented Jul 1, 2020

Copy link
Copy Markdown
Contributor

Thanks @pierreglaser!

I had to use underscores and not dashes in the repr of the Batch to separate words. Weird but I don't have time to investigate.

This is fine I think.

@ogrisel
ogrisel merged commit 9d389e0 into joblib:master Jul 1, 2020
@ogrisel

ogrisel commented Jul 1, 2020

Copy link
Copy Markdown
Contributor

Merged!

@ogrisel ogrisel left a comment

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.

Forgot to formally approve :)

@pierreglaser

Copy link
Copy Markdown
Contributor Author

Great! Upon rebasing, #1061 should now fully fix #959.

@ogrisel

ogrisel commented Jul 1, 2020

Copy link
Copy Markdown
Contributor

I already merged master in #1601.

bmwiedemann added a commit to bmwiedemann/openSUSE that referenced this pull request Jul 18, 2020
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants