Skip to content

CI Add CPython 3.13 free-threaded test#1589

Merged
tomMoral merged 28 commits intojoblib:mainfrom
lesteve:cpython-free-threaded
Jun 20, 2024
Merged

CI Add CPython 3.13 free-threaded test#1589
tomMoral merged 28 commits intojoblib:mainfrom
lesteve:cpython-free-threaded

Conversation

@lesteve
Copy link
Copy Markdown
Member

@lesteve lesteve commented Jun 4, 2024

Giving it a go to CPython 3.13 free-threaded which is in beta and scheduled to be released in October 2024. Context: scikit-learn/scikit-learn#28978 and more generally https://github.com/Quansight-Labs/free-threaded-compatibility.

Locally and in a quick Github action workflow (see latest runs) there are the following test failures. Note: this is easier to write a Github Action with the action deadsnakes/action which allow to have an easily working Python 3.13 beta.

DeprecationWarning about forking multi-threaded processes

Doing a Parallel with loky followed by a Parallel by multiprocessing yields a DeprecationWarning because I guess loky creates thread and doing a fork of a multi-threaded warning may lead to issues. Maybe we should swallow warning in the test? Not sure if there are use cases where using both loky and multiprocessing and what to do in this case.

from joblib import delayed, Parallel


def square(x):
    return x**2


Parallel(n_jobs=2, backend="loky")(delayed(square)(x) for x in range(3))

Parallel(n_jobs=2, backend="multiprocessing")(delayed(square)(x) for x in range(3))

produces this warning:

/opt/python-free-threading-debug/lib/python3.13/multiprocessing/popen_fork.py:67: DeprecationWarning: This process (pid=802913) is multi-threaded, use of fork() may lead to deadlocks in the child.
  self.pid = os.fork()
/opt/python-free-threading-debug/lib/python3.13/multiprocessing/popen_fork.py:67: DeprecationWarning: This process (pid=802913) is multi-threaded, use of fork() may lead to deadlocks in the child.
  self.pid = os.fork()

The failures are:

FAILED joblib/test/test_parallel.py::test_main_thread_renamed_no_warning[None] - assert 2 == 0
FAILED joblib/test/test_parallel.py::test_main_thread_renamed_no_warning[multiprocessing] - assert 2 == 0
FAILED joblib/test/test_parallel.py::test_main_thread_renamed_no_warning[backend7] - assert 2 == 0

There are some warnings like this where the test expects no warning:

DeprecationWarning('This process (pid=695921) is multi-threaded, use of fork() may lead to deadlocks in the child.')

Fixed: Changes in locals() after exec

I fixed it by tweaking the test and passing a dict to the exec locals argument. I think this still tests interactively defined functions.

FAILED joblib/test/test_memory.py::test_parallel_call_cached_function_defined_in_jupyter[True] - KeyError: 'f'
FAILED joblib/test/test_memory.py::test_parallel_call_cached_function_defined_in_jupyter[False] - KeyError: 'f'

This happens in the test that is trying to test an interactive function in Jupyter by using exec to define a function and expecting locals() to be udpated. This is likely related to CPython 3.13 (not free-threaded related).

Probably related:

@codecov
Copy link
Copy Markdown

codecov bot commented Jun 4, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 95.25%. Comparing base (905b0ca) to head (1a748e1).
Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1589      +/-   ##
==========================================
- Coverage   95.27%   95.25%   -0.03%     
==========================================
  Files          45       45              
  Lines        7708     7715       +7     
==========================================
+ Hits         7344     7349       +5     
- Misses        364      366       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@rgommers
Copy link
Copy Markdown

rgommers commented Jun 5, 2024

I was just looking at the multiprocessing fork method deprecation warning, since they also show up in SciPy tests. Given that the default method will change in Python 3.14 (https://docs.python.org/3.13/whatsnew/3.13.html#pending-removal-in-python-3-14), did you consider always specifying the start method, and/or defaulting to forkserver? It's actually pretty unclear to me (even after reading python/cpython#84559 (comment) and other comments in that issue) whether we're supposed to fix it in libraries or let users deal with it themselves by setting multiprocessing.set_start_method in case they don't want the defaults to switch from under them in 3.14

@lesteve
Copy link
Copy Markdown
Member Author

lesteve commented Jun 5, 2024

Thanks for the pointers, so indeed the change is that on Linux in CPython 3.14, the default multiprocessing start method will change from 'fork' to a better default (I haven't read enough to figure out what the new default is 😅)

To quote the relevant section for more precision:

multiprocessing: The default start method will change to a safer one on Linux, BSDs, and other non-macOS POSIX platforms where 'fork' is currently the default (gh-84559). Adding a runtime warning about this was deemed too disruptive as the majority of code is not expected to care. Use the get_context() or set_start_method() APIs to explicitly specify when your code requires 'fork'. See Contexts and start methods.

In joblib 'multiprocessing' backend case, I am not sure either what we should do. I see broadly three directions:

  1. use multiprocessing with fork i.e. the default until Python 3.13, at least code that was working with joblib before will still work the same, for example interactive functions (i.e. defined in __main__).
  2. let the warning through and add some documentation to tell users to set it explicitly the context depending on their use case.
  3. swallow the warning because we are expecting most users are happy with the default start method even if it changes in Python 3.14. Document the caveat when Python 3.14 is released to say that if you had code that was working before, here is what you need to keep it working.

I would lean slightly towards 3. maybe? Probably not 1. because my understanding is that joblib 'multiprocessing' backend should respect the start method if set by the user.

To be honest I am not that sure how much the joblib 'multiprocessing' backend is used ... but maybe a significant portion of these users do it because they rely on the fork behaviour, hard to tell ...

cc @tomMoral and @ogrisel for informed opinions on this topic

@rgommers
Copy link
Copy Markdown

rgommers commented Jun 5, 2024

I haven't read enough to figure out what the new default is

My understanding is: very likely 'forkserver', but the devs reserved the right to change that decision during the 3.14 alpha phase.

@ogrisel
Copy link
Copy Markdown
Contributor

ogrisel commented Jun 17, 2024

Doing a Parallel with loky followed by a Parallel by multiprocessing yields a DeprecationWarning because I guess loky creates thread and doing a fork of a multi-threaded warning may lead to issues. Maybe we should swallow warning in the test? Not sure if there are use cases where using both loky and multiprocessing and what to do in this case.

I agree we might want the swallow this warning in our test. This is quite a synthetic case that should not happen much in practice, so we can ignore and leave the deprecation warning go through in joblib.

@ogrisel
Copy link
Copy Markdown
Contributor

ogrisel commented Jun 17, 2024

did you consider always specifying the start method, and/or defaulting to forkserver? It's actually pretty unclear to me (even after reading python/cpython#84559 (comment) and other comments in that issue) whether we're supposed to fix it in libraries or let users deal with it themselves by setting multiprocessing.set_start_method in case they don't want the defaults to switch from under them in 3.14

I think I would let the multiprocessing users chose their own start method. I would not change the default because:

  • it would be a behavioral breaking change for people who rely on the semantics of the fork start method;
  • joblib's default loky backend is "better" than multiprocessing + forkserver because it allows to call functions defined interactively in the __main__ module or as local variable under the scope of another function. So I would not push users towards a solution that does not allow that.

Once CPython 3.14 has settled down on its plan, we can adapt joblib accordingly to adapt the tests for CPython 3.14 and later but I would not change joblib's default behavior for earlier Python versions.

@ogrisel
Copy link
Copy Markdown
Contributor

ogrisel commented Jun 17, 2024

  1. swallow the warning because we are expecting most users are happy with the default start method even if it changes in Python 3.14. Document the caveat when Python 3.14 is released to say that if you had code that was working before, here is what you need to keep it working.

+1. I just assume that "swallow the warning" is only done in when running the tests, not in general in joblib.

Copy link
Copy Markdown
Contributor

@ogrisel ogrisel left a comment

Choose a reason for hiding this comment

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

Other than the warning problem and the following, LGTM. Thanks Loïc!

@lesteve
Copy link
Copy Markdown
Member Author

lesteve commented Jun 20, 2024

OK, CI is green and this is ready for another review round!

I have mostly:

  • added a free-threaded Python 3.13 is in Azure rather than Github action (Github action was easier originally to get started since they have a deadsnake/action but this was always the goal to go back to Azure eventually)
  • ignored the fork of multi-threaded process warning in the test

Copy link
Copy Markdown
Contributor

@tomMoral tomMoral left a comment

Choose a reason for hiding this comment

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

This is surprisingly a small diff 😅

thanks for having anticipated so much with support for nogil @lesteve !

@lesteve lesteve force-pushed the cpython-free-threaded branch from 9fa3602 to a4f10c4 Compare June 20, 2024 06:23
@tomMoral tomMoral merged commit fe76ce1 into joblib:main Jun 20, 2024
@tomMoral
Copy link
Copy Markdown
Contributor

Thanks @lesteve !

@lesteve lesteve deleted the cpython-free-threaded branch June 20, 2024 08:06
@lesteve
Copy link
Copy Markdown
Member Author

lesteve commented Jun 20, 2024

This is surprisingly a small diff 😅

I guess the issue are more likely to happen when using free-threaded + joblib multithreading backend + some other library.

Next thing I want to do is run the joblib test suite with default backend switched to threading. I think locally I have seen some errors but I need to investigate more.

After this trying to run the scikit-learn test suite with default backend switched to threading.

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.

4 participants