Skip to content

Fix concurrent execution of compiled NumExpr objects - #570

Open
xuu33030 wants to merge 2 commits into
pydata:masterfrom
xuu33030:fix/compiled-expression-thread-safety
Open

xuu33030 wants to merge 2 commits into
pydata:masterfrom
xuu33030:fix/compiled-expression-thread-safety

Conversation

@xuu33030

Copy link
Copy Markdown

Summary

  • serialize calls to the same compiled NumExpr object with a per-object lock
  • release the GIL while waiting for that lock
  • add a synchronized regression test that forces NumExpr's single-threaded execution path

Problem

A compiled NumExpr object stores mutable execution buffers on the object. When the same object is called concurrently while NUMEXPR_NUM_THREADS=1, multiple Python threads can enter the serial interpreter path at once and overwrite that shared state, causing incorrect results or heap corruption.

The existing global parallel_mutex only protects the multi-threaded interpreter path and therefore does not cover this case.

Verification

  • Before the fix, the new regression test aborts with SIGABRT on macOS arm64 / Python 3.13 / NumPy 2.5.3.
  • python -m pytest numexpr/tests/test_numexpr.py::test_threading::test_shared_compiled_expression -q — 1 passed
  • python -m pytest -q — 6043 passed
  • issue reproducer with NUMEXPR_NUM_THREADS=1 — prints OK
  • git diff --check — clean

Closes #569

@kif

kif commented Sep 20, 2026

Copy link
Copy Markdown

I confirm this PR addresses the bug.

@FrancescAlted

Copy link
Copy Markdown
Contributor

It looks like some test is stalled with this PR. Any hint?

@kif

kif commented Sep 20, 2026

Copy link
Copy Markdown

All stall processes are when testing 3.14t, i.e. the free-threaded version.

@xuu33030

Copy link
Copy Markdown
Author

I reproduced the stall locally on Python 3.14t with the CI test command. The new regression test passed on its own, but under pytest --parallel-threads=4 multiple copies of that test ran concurrently; the thread dump showed one copy waiting in set_num_threads() while others were joining workers. That test changes NumExpr’s global worker-pool size, so I marked it thread_unsafe (the suite’s existing convention) while retaining its own concurrent-expression workers. With that one-line change, the full --parallel-threads=4 --pyargs numexpr suite passed locally on 3.14t (6043 tests), as did repeated focused runs. The previous CI hang is consistent with this interaction; it does not establish a deadlock in the per-object lock. The new Build workflow currently requires maintainer approval to run on this fork PR.

@kif

kif commented Sep 23, 2026

Copy link
Copy Markdown

Apparently the fact the tests dead-lock in thread-free version of python is a different issue related to the set_num_threads() function which is not properly protected in this flavor of python. It is only by chance (or thanks to the GIL) that it did not show off earlier.

I can propose some locking to protect this function if needed, but maybe @xuu33030 prefers doing it on his own.

@xuu33030

Copy link
Copy Markdown
Author

Thanks for offering, @kif. Would you like to take the set_num_threads() investigation? The current 3.14t CI is green after isolating the regression test, but that does not establish that concurrent set_num_threads() calls are safe. Would the maintainers prefer to track this separately in a new issue/PR, or keep it within #570?

@kif

kif commented Sep 26, 2026

Copy link
Copy Markdown

I think I will submit a separated PR ... maybe even open another issue since what we see here is just highlighting an issue already pre-existing.

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.

Heap corruption when a compiled NumExpr object is called from several threads with NUMEXPR_NUM_THREADS=1

3 participants