Skip to content

Conversation

@ngoldbaum
Copy link
Collaborator

Fixes #56

I'm not sure whether it makes more sense to use functools.cache and just not worry about memory overhead. lru_cache seemed to be sufficient to speed up the example from sciki-learn in #56.

@ngoldbaum
Copy link
Collaborator Author

For the test_continuous example from SciPy, test collection goes from 16.19s to 0.55s with this PR.

@rgommers
Copy link
Member

Does it fix the performance regression completely, or if not what's the gain you are seeing?

@ngoldbaum
Copy link
Collaborator Author

I just tried comparing with pytest-run-parallel 0.3.1 on the scipy example and it looks like this PR completely fixes the regression, within measurement noise.

@rgommers
Copy link
Member

I can confirm that test_continuous is fast. However the full SciPy test suite is rather unhappy:

INTERNALERROR>     identify_thread_unsafe_nodes(
INTERNALERROR>     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
INTERNALERROR>         child_fn, self.skip_set, self.level + 1
INTERNALERROR>         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>     )
INTERNALERROR>     ^
INTERNALERROR> TypeError: unhashable type: 'MarkDecorator'
Full traceback:
platform linux -- Python 3.13.3, pytest-8.3.5, pluggy-1.5.0
rootdir: /home/rgommers/code/pixi-dev-scipystack/scipy/scipy
configfile: pytest.ini
plugins: run-parallel-0.4.3.dev0, hypothesis-6.131.0
collected 2072 items                                                                             
Collected 136 items to run in parallel
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 283, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>                          ~~~~^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 336, in _main
INTERNALERROR>     config.hook.pytest_collection(session=session)
INTERNALERROR>     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_hooks.py", line 513, in __call__
INTERNALERROR>     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
INTERNALERROR>            ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_manager.py", line 120, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>            ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR>     raise exception.with_traceback(exception.__traceback__)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_callers.py", line 122, in _multicall
INTERNALERROR>     teardown.throw(exception)  # type: ignore[union-attr]
INTERNALERROR>     ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/logging.py", line 790, in pytest_collection
INTERNALERROR>     return (yield)
INTERNALERROR>             ^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_callers.py", line 122, in _multicall
INTERNALERROR>     teardown.throw(exception)  # type: ignore[union-attr]
INTERNALERROR>     ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/warnings.py", line 121, in pytest_collection
INTERNALERROR>     return (yield)
INTERNALERROR>             ^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_callers.py", line 122, in _multicall
INTERNALERROR>     teardown.throw(exception)  # type: ignore[union-attr]
INTERNALERROR>     ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/config/__init__.py", line 1417, in pytest_collection
INTERNALERROR>     return (yield)
INTERNALERROR>             ^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_callers.py", line 103, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 347, in pytest_collection
INTERNALERROR>     session.perform_collect()
INTERNALERROR>     ~~~~~~~~~~~~~~~~~~~~~~~^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 809, in perform_collect
INTERNALERROR>     self.items.extend(self.genitems(node))
INTERNALERROR>     ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 975, in genitems
INTERNALERROR>     yield from self.genitems(subnode)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 975, in genitems
INTERNALERROR>     yield from self.genitems(subnode)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 975, in genitems
INTERNALERROR>     yield from self.genitems(subnode)
INTERNALERROR>   [Previous line repeated 3 more times]
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/_pytest/main.py", line 963, in genitems
INTERNALERROR>     node.ihook.pytest_itemcollected(item=node)
INTERNALERROR>     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_hooks.py", line 513, in __call__
INTERNALERROR>     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
INTERNALERROR>            ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_manager.py", line 120, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>            ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR>     raise exception.with_traceback(exception.__traceback__)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pluggy/_callers.py", line 103, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pytest_run_parallel/plugin.py", line 178, in pytest_itemcollected
INTERNALERROR>     thread_unsafe, thread_unsafe_reason = identify_thread_unsafe_nodes(
INTERNALERROR>                                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
INTERNALERROR>         item.obj, skipped_functions
INTERNALERROR>         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>     )
INTERNALERROR>     ^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pytest_run_parallel/utils.py", line 130, in identify_thread_unsafe_nodes
INTERNALERROR>     visitor.visit(tree)
INTERNALERROR>     ~~~~~~~~~~~~~^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/ast.py", line 428, in visit
INTERNALERROR>     return visitor(node)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/ast.py", line 436, in generic_visit
INTERNALERROR>     self.visit(item)
INTERNALERROR>     ~~~~~~~~~~^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/ast.py", line 428, in visit
INTERNALERROR>     return visitor(node)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/ast.py", line 436, in generic_visit
INTERNALERROR>     self.visit(item)
INTERNALERROR>     ~~~~~~~~~~^^^^^^
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/ast.py", line 428, in visit
INTERNALERROR>     return visitor(node)
INTERNALERROR>   File "/home/rgommers/code/pixi-dev-scipystack/scipy/.pixi/envs/free-threading/lib/python3.13t/site-packages/pytest_run_parallel/utils.py", line 98, in visit_Call
INTERNALERROR>     identify_thread_unsafe_nodes(
INTERNALERROR>     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
INTERNALERROR>         child_fn, self.skip_set, self.level + 1
INTERNALERROR>         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>     )
INTERNALERROR>     ^
INTERNALERROR> TypeError: unhashable type: 'MarkDecorator'

@ngoldbaum
Copy link
Collaborator Author

I'm not able to reproduce that.

That said, I think this is coming from the change to use a frozenset. I can avoid that if I move the lru_cache to a new function that accepts a filename and returns the parsed AST and it looks like that gives a similar speedup.

I wanted to add a test but since I can't reproduce the issue Ralf was seeing it's hard to do that confidently.

@ngoldbaum
Copy link
Collaborator Author

This approach is a bit slower than the initial approach, collection takes 2s instead of 0.55s on scipy's test_continuous.py with the latest version of this PR.

@ngoldbaum
Copy link
Collaborator Author

Are you doing anything special to trigger that MarkDecorator error besides e.g. spin test -- --parallel-threads=2 --iterations=1 --collect-only in a checkout of scipy?

@rgommers
Copy link
Member

Are you doing anything special to trigger that MarkDecorator error besides e.g. spin test -- --parallel-threads=2 --iterations=1 --collect-only in a checkout of scipy?

Not really. It's reproducible on macOS as well, with test_stats rather than test_continuous. It looks like it's failing on one of the array API compat decorators/fixtures (xp or make_xp_test_case).

It shouldn't matter that I use pixi over direct spin/pytest, but for completeness: I'm using https://github.com/rgommers/pixi-dev-scipystack/ with this one change to test this branch:

@@ -232,7 +233,12 @@ pytest = "*"
 hypothesis = "*"
 threadpoolctl = "*"
 pooch = "*"
-pytest-run-parallel = ">=0.3.0"
+
+[feature.free-threading.pypi-dependencies]
+pytest-run-parallel = { path = "../../tmp/pytest-run-parallel", editable = true  }
 
 [feature.free-threading.tasks]
 build-nogil = { cmd = "spin build --build-dir=build-nogil -C-Dblas=blas -C-Dlapack=lapack -C-Duse-g77-abi=true", cwd = "scipy", env = { CC = "ccache $CC", CXX = "ccache $CXX", FC = "ccache $FC" } }

And then:

pixi r test-nogil -t scipy.stats.tests.test_stats -- --parallel-threads=2 --collect-only

@rgommers
Copy link
Member

The last commit seems to work, while still helping performance (just less so). On macOS arm64 I'm seeing collection times for the whole SciPy test suite of:

  • v0.3.1: 54 sec
  • v0.4.2: 1138 sec (21x slower)
  • this PR at commit dca74b8: 314 sec (6x slower)

@ngoldbaum
Copy link
Collaborator Author

Thanks for your patience - it turns out I was testing an old scipy commit. I can reproduce after a git pull upstream main.

@ngoldbaum
Copy link
Collaborator Author

I spent a little time this morning trying to make a test case that triggers this so I can add a regression test but couldn't manage to trigger it. The skip_xp_backends fixture in SciPy is doing something to trigger it.

@ogrisel
Copy link

ogrisel commented May 20, 2025

I checked out this branch and tried with scikit-learn by running:

pytest --parallel-threads=2 --collect-only sklearn/tests/test_common.py

It runs in 19 to 21s both for 214f68f of this branch and 3d65bf7 of the current main.

For reference, not using the plugin:

pytest --collect-only sklearn/tests/test_common.py

It runs in 4 to 5s.

So I don't see any significant effect of this PR w.r.t. the use case described in #56. Did I miss anything?

@ngoldbaum
Copy link
Collaborator Author

@ogrisel thank you so much for testing. This is why it's a bad idea to push code close to mignight!

The try/except obscured that I was causing a cache miss on every call - moving to a frozenset fixes that.

@ngoldbaum
Copy link
Collaborator Author

I see 12 seconds on main and 2.3 seconds on this PR for the command @ogrisel suggested using the scikit-learn tests.

@ogrisel
Copy link

ogrisel commented May 20, 2025

@ngoldbaum I confirm it works for me as well with the latest commit of this branch: 4s which is very close to the collection duration with the plugin disabled.

Good job!

Copy link
Member

@rgommers rgommers left a comment

Choose a reason for hiding this comment

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

LGTM now - works on the SciPy test suite, fully fixes the performance issue, and looks pretty clean.

Thanks @ngoldbaum and @ogrisel!

@rgommers rgommers merged commit 4e9cd0b into Quansight-Labs:main May 21, 2025
9 checks passed
@rgommers
Copy link
Member

I think we should release this as 0.4.3 - let's discuss later today @ngoldbaum

@ogrisel
Copy link

ogrisel commented May 26, 2025

I confirm that this much nicer to use pytest-run-parallel to iterative find and fix thread-safety problems in our test suite with this cache.

@ngoldbaum
Copy link
Collaborator Author

ngoldbaum commented May 26, 2025

Thanks again for the report - I'll try not to implement any accidentally O(N!) (I think?) algorithms anytime soon...

We'll try to get a bugfix release out this week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pytest collection becomes 5x slower with--parallel-threads=2 or more

3 participants