Repository navigation
Release the GIL around oneMKL VM calls - #3156
Conversation
On a CPU device oneMKL VM waits on the dependent events and computes on the calling thread. On an in-order queue those events are ordered after the host tasks dpnp submits to keep arguments alive, which acquire the GIL, so a VM call made with the GIL held deadlocks.
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are recorded for the CPU in-order queue deadlock fix.
No open issues 🎉
What changed in this PR
This PR prevents CPU in-order queue deadlocks by releasing Python’s global interpreter lock (GIL) around oneMKL vector math calls. It extends the approach already used by dpnp’s BLAS, LAPACK, and FFT extensions.
Changes:
- Releases the GIL around all 45 oneMKL VM calls, after type and offset validation.
- Adds a subprocess regression test with a timeout to detect deadlocks safely.
- Records the fix in the changelog.
| File | Description |
|---|---|
dpnp/tests/test_vm_gil.py |
Adds timeout-protected CPU deadlock regression coverage. |
dpnp/libtensor/extensions/vm/trunc.cpp |
Releases the GIL around trunc. |
dpnp/libtensor/extensions/vm/tanh.cpp |
Releases the GIL around tanh. |
dpnp/libtensor/extensions/vm/tan.cpp |
Releases the GIL around tan. |
dpnp/libtensor/extensions/vm/sub.cpp |
Releases the GIL around sub. |
dpnp/libtensor/extensions/vm/sqrt.cpp |
Releases the GIL around sqrt. |
dpnp/libtensor/extensions/vm/sqr.cpp |
Releases the GIL around sqr. |
dpnp/libtensor/extensions/vm/sinh.cpp |
Releases the GIL around sinh. |
dpnp/libtensor/extensions/vm/sin.cpp |
Releases the GIL around sin. |
dpnp/libtensor/extensions/vm/rint.cpp |
Releases the GIL around rint. |
dpnp/libtensor/extensions/vm/pow.cpp |
Releases the GIL around pow. |
dpnp/libtensor/extensions/vm/nextafter.cpp |
Releases the GIL around nextafter. |
dpnp/libtensor/extensions/vm/mul.cpp |
Releases the GIL around mul. |
dpnp/libtensor/extensions/vm/modf.cpp |
Releases the GIL around modf. |
dpnp/libtensor/extensions/vm/log2.cpp |
Releases the GIL around log2. |
dpnp/libtensor/extensions/vm/log1p.cpp |
Releases the GIL around log1p. |
dpnp/libtensor/extensions/vm/log10.cpp |
Releases the GIL around log10. |
dpnp/libtensor/extensions/vm/ln.cpp |
Releases the GIL around ln. |
dpnp/libtensor/extensions/vm/inv.cpp |
Releases the GIL around inv. |
dpnp/libtensor/extensions/vm/i0.cpp |
Releases the GIL around i0. |
dpnp/libtensor/extensions/vm/hypot.cpp |
Releases the GIL around hypot. |
dpnp/libtensor/extensions/vm/fmod.cpp |
Releases the GIL around fmod. |
dpnp/libtensor/extensions/vm/fmin.cpp |
Releases the GIL around fmin. |
dpnp/libtensor/extensions/vm/fmax.cpp |
Releases the GIL around fmax. |
dpnp/libtensor/extensions/vm/floor.cpp |
Releases the GIL around floor. |
dpnp/libtensor/extensions/vm/expm1.cpp |
Releases the GIL around expm1. |
dpnp/libtensor/extensions/vm/exp2.cpp |
Releases the GIL around exp2. |
dpnp/libtensor/extensions/vm/exp.cpp |
Releases the GIL around exp. |
dpnp/libtensor/extensions/vm/erf_funcs.cpp |
Releases the GIL in macro-generated error-function calls. |
dpnp/libtensor/extensions/vm/div.cpp |
Releases the GIL around div. |
dpnp/libtensor/extensions/vm/cosh.cpp |
Releases the GIL around cosh. |
dpnp/libtensor/extensions/vm/cos.cpp |
Releases the GIL around cos. |
dpnp/libtensor/extensions/vm/copysign.cpp |
Releases the GIL around copysign. |
dpnp/libtensor/extensions/vm/conj.cpp |
Releases the GIL around conj. |
dpnp/libtensor/extensions/vm/ceil.cpp |
Releases the GIL around ceil. |
dpnp/libtensor/extensions/vm/cbrt.cpp |
Releases the GIL around cbrt. |
dpnp/libtensor/extensions/vm/atanh.cpp |
Releases the GIL around atanh. |
dpnp/libtensor/extensions/vm/atan2.cpp |
Releases the GIL around atan2. |
dpnp/libtensor/extensions/vm/atan.cpp |
Releases the GIL around atan. |
dpnp/libtensor/extensions/vm/asinh.cpp |
Releases the GIL around asinh. |
dpnp/libtensor/extensions/vm/asin.cpp |
Releases the GIL around asin. |
dpnp/libtensor/extensions/vm/arg.cpp |
Releases the GIL around arg. |
dpnp/libtensor/extensions/vm/add.cpp |
Releases the GIL around add. |
dpnp/libtensor/extensions/vm/acosh.cpp |
Releases the GIL around acosh. |
dpnp/libtensor/extensions/vm/acos.cpp |
Releases the GIL around acos. |
dpnp/libtensor/extensions/vm/abs.cpp |
Releases the GIL around abs. |
CHANGELOG.md |
Documents the CPU in-order queue deadlock fix. |
Review effort: Balanced
| // On a CPU device oneMKL VM waits on `depends` and computes on the | ||
| // calling thread, so release the GIL for host tasks that need it |
There was a problem hiding this comment.
Nit: Could all of these comments be pared down into a README or a piece of documentation (i.e., a docstring on some common code) which informs users more effectively and in a less verbose way?
There was a problem hiding this comment.
Done: the comment now lives once, on a vm_gil_release alias in vm/common.hpp, and each call site is just vm_gil_release lock{};.
ndgrigorian
left a comment
There was a problem hiding this comment.
Aside from that nit LGTM
ndgrigorian
left a comment
There was a problem hiding this comment.
LGTM, this bug appeared in the dpnp.random rework PR so getting it in would be nice
On a CPU device oneMKL VM is synchronous: it waits on the `depends` events with `sycl::event::wait_and_throw`, computes on the calling thread and returns an empty event (`src/vml/_src/onemkl_vm/source/core/cpu/intel/usm/linear/v21.hpp` and the other CPU variants). The VM extension calls it with the GIL held. On an in-order queue the `depends` events are ordered after the host tasks dpnp submits to keep arguments alive, and these host tasks acquire the GIL, so the VM call never returns. The PR releases the GIL right before each of the 45 oneMKL VM calls, after the type and offset checks that can throw, as the BLAS, LAPACK and FFT extensions already do (#3027, #3040). Reproducer, hangs on master and finishes in about a second with the PR: ```python import dpctl, dpnp q = dpctl.SyclQueue("cpu", property="in_order") xp = dpnp.linspace(10.0, 0.0, 5, sycl_queue=q) for _ in range(100): xs = xp[dpnp.argsort(xp)] # submits host tasks keeping arguments alive xs[-1:] - xs[:1] # oneMKL VM sub q.wait() ``` `dpnp.interp` with `period` hangs the same way, e.g. `test_sycl_queue.py::test_interp[180.0-None-None-opencl:cpu:0]` when the default queue is in-order. With IntelPython/dpctl#2299, which makes `submitted_events` of an in-order queue return the queue's last event (the keep-alive host task), the full `test_sycl_queue.py` hung in 5 of 5 runs with in-order queues; with this PR it passes in 6 of 6. `dpnp/tests/test_vm_gil.py` runs the reproducer in a subprocess with a timeout, since a deadlocked call holds the GIL and can't be interrupted from the test process. It fails after the 120 s timeout on master and passes in under a second with the PR. Tested on Panther Lake H (Arc B390 iGPU) with DPC++ 2026.2: `test_umath.py`, `test_mathematical.py`, `test_special.py`, `test_sycl_queue.py`, `test_usm_type.py`, `test_vm_gil.py` and `third_party/cupy/math_tests` pass on Level Zero GPU and OpenCL CPU. The time of a VM `add` on 1024 elements is unchanged: 16.5-17.1 us on master vs 16.9-17.0 us with the PR on GPU, 13.1-13.2 us vs 13.3-13.4 us on CPU. 90622cd
On a CPU device oneMKL VM is synchronous: it waits on the
dependsevents withsycl::event::wait_and_throw, computes on the calling thread and returns an empty event (src/vml/_src/onemkl_vm/source/core/cpu/intel/usm/linear/v21.hppand the other CPU variants). The VM extension calls it with the GIL held. On an in-order queue thedependsevents are ordered after the host tasks dpnp submits to keep arguments alive, and these host tasks acquire the GIL, so the VM call never returns.The PR releases the GIL right before each of the 45 oneMKL VM calls, after the type and offset checks that can throw, as the BLAS, LAPACK and FFT extensions already do (#3027, #3040).
Reproducer, hangs on master and finishes in about a second with the PR:
dpnp.interpwithperiodhangs the same way, e.g.test_sycl_queue.py::test_interp[180.0-None-None-opencl:cpu:0]when the default queue is in-order. With IntelPython/dpctl#2299, which makessubmitted_eventsof an in-order queue return the queue's last event (the keep-alive host task), the fulltest_sycl_queue.pyhung in 5 of 5 runs with in-order queues; with this PR it passes in 6 of 6.dpnp/tests/test_vm_gil.pyruns the reproducer in a subprocess with a timeout, since a deadlocked call holds the GIL and can't be interrupted from the test process. It fails after the 120 s timeout on master and passes in under a second with the PR.Tested on Panther Lake H (Arc B390 iGPU) with DPC++ 2026.2:
test_umath.py,test_mathematical.py,test_special.py,test_sycl_queue.py,test_usm_type.py,test_vm_gil.pyandthird_party/cupy/math_testspass on Level Zero GPU and OpenCL CPU. The time of a VMaddon 1024 elements is unchanged: 16.5-17.1 us on master vs 16.9-17.0 us with the PR on GPU, 13.1-13.2 us vs 13.3-13.4 us on CPU.