Repository: cython/cython

Issue:
Fatal Python error: PyEval_SaveThread when creating ufunc with nogil [BUG] 
### Describe the bug

I'm generating ufuncs for my fortran library by the new version 3.0.0b1 of cython, using the @cython.ufunc function decorator.
I declare the function as nogil. and call it in python script. It works fine when the input array length is less than 500. When the input array length is greater than 500, it stops and throws a thread error.
```
Fatal Python error: PyEval_SaveThread: the function must be called with the GIL held, but the GIL is released (the current Python thread state is NULL)
Python runtime state: initialized
```
I'm using the mingw-w64 compiler provided by msys2 on windows, but that doesn't seem to be the cause of the error. I transferred the program to an ubuntu machine and found the same error.

### Code to reproduce the behaviour:

```cython
# test_ufunc.pyx
cimport cython

@cython.ufunc
cdef double add_one(double x) nogil:
    return x+1
```

```python
import test_ufunc
import numpy as np

x=np.linspace(-1,1,501)
x_add=test_ufunc.add_one(x)
print(x_add)
```

### Expected behaviour

No errors, result returns successfully.

### OS

Ubuntu and Windows

### Python version

3.10.6 (Ubuntu), 3.11.2 (Windows)

### Cython version

3.0.0b1

### Additional context

_No response_


Discussion:
So it looks like I misunderstood ufuncs and the GIL. I'd assumed that it was up to the user to release the GIL in a ufunc (on the basis that they'd documented macros to do so https://numpy.org/doc/stable/reference/c-api/ufunc.html#macros). But it looks like Numpy releases it automatically. It only becomes an issue for large arrays though.

It also looks like I wasn't the only one to find this confusing https://github.com/numpy/numpy/issues/2067.

Should be fairly easy to fix I think.

Thanks for the report


Resolving patch:
diff --git a/Cython/Compiler/UFuncs.py b/Cython/Compiler/UFuncs.py
--- a/Cython/Compiler/UFuncs.py
+++ b/Cython/Compiler/UFuncs.py
@@ -138,6 +138,9 @@ def generate_cy_utility_code(self):
 
         ufunc_cname = self.global_scope.next_id(self.node.entry.name + "_ufunc_def")
 
+        will_be_called_without_gil = not (any(t.is_pyobject for t in arg_types) or
+            any(t.is_pyobject for t in out_types))
+
         context = dict(
             func_cname=ufunc_cname,
             in_types=arg_types,
@@ -145,6 +148,7 @@ def generate_cy_utility_code(self):
             inline_func_call=self.node.entry.cname,
             inline_func_declaration=inline_func_decl,
             nogil=self.node.entry.type.nogil,
+            will_be_called_without_gil=will_be_called_without_gil,
         )
 
         code = CythonUtilityCode.load(


Test patch:
diff --git a/tests/run/ufunc.pyx b/tests/run/ufunc.pyx
--- a/tests/run/ufunc.pyx
+++ b/tests/run/ufunc.pyx
@@ -10,25 +10,34 @@ int_arr_1d = np.arange(20, dtype=int)[::4]
 int_arr_2d = np.arange(500, dtype=int).reshape((50, -1))[5:8, 6:8]
 double_arr_1d = int_arr_1d.astype(np.double)
 double_arr_2d = int_arr_2d.astype(np.double)
+# Numpy has a cutoff at about 500 where it releases the GIL, so test some large arrays
+large_int_arr_1d = np.arange(1500, dtype=int)
+large_int_arr_2d = np.arange(1500*600, dtype=int).reshape((1500, -1))
+large_double_arr_1d = large_int_arr_1d.astype(np.double)
+large_double_arr_2d = large_int_arr_2d.astype(np.double)
 
 # it's fairly hard to test that nogil results in the GIL actually
 # being released unfortunately
 @cython.ufunc
-cdef double tripple_it(long x) nogil:
-    """tripple_it doc"""
+cdef double triple_it(long x) nogil:
+    """triple_it doc"""
     return x*3.
 
-def test_tripple_it():
+def test_triple_it():
     """
     Ufunc also generates a signature so just look at the end
-    >>> tripple_it.__doc__.endswith('tripple_it doc')
+    >>> triple_it.__doc__.endswith('triple_it doc')
     True
-    >>> tripple_it(int_arr_1d)
+    >>> triple_it(int_arr_1d)
     array([ 0., 12., 24., 36., 48.])
-    >>> tripple_it(int_arr_2d)
+    >>> triple_it(int_arr_2d)
     array([[168., 171.],
            [198., 201.],
            [228., 231.]])
+
+    Treat the large arrays just as a "don't crash" test
+    >>> _ = triple_it(large_int_arr_1d)
+    >>> _ = triple_it(large_int_arr_2d)
     """
 
 @cython.ufunc
@@ -41,6 +50,8 @@ def test_to_the_power():
     True
     >>> np.allclose(to_the_power(1., double_arr_2d), np.ones_like(double_arr_2d))
     True
+    >>> _ = to_the_power(large_double_arr_1d, -large_double_arr_1d)
+    >>> _ = to_the_power(large_double_arr_2d, -large_double_arr_2d)
     """
 
 @cython.ufunc
@@ -56,6 +67,7 @@ def test_py_return_value():
     >>> py_return_value(double_arr_1d).dtype
     dtype('O')
     >>> py_return_value(-1.)  # returns None
+    >>> _ = py_return_value(large_double_arr_1d)
     """
 
 @cython.ufunc
@@ -66,6 +78,7 @@ def test_py_arg():
     """
     >>> py_arg(np.array([1, "2.0", 3.0], dtype=object))
     array([1., 2., 3.])
+    >>> _ = py_arg(np.array([1]*1200, dtype=object))
     """
 
 @cython.ufunc
@@ -156,3 +169,31 @@ def test_nested_function():
     >>> nested_function(-1.)
     -2.0
     """
+
+@cython.ufunc
+cdef double can_throw(double x):
+    if x<0:
+        raise RuntimeError
+    return x
+
+def test_can_throw():
+    """
+    >>> arr = double_arr_1d.copy()
+    >>> arr[1] = -1.
+    >>> can_throw(arr)
+    Traceback (most recent call last):
+    ...
+    RuntimeError
+    >>> large_arr = large_double_arr_1d.copy()
+    >>> large_arr[-4] = -2.
+    >>> can_throw(large_arr)
+    Traceback (most recent call last):
+    ...
+    RuntimeError
+    >>> large_arr2d = large_double_arr_2d.copy()
+    >>> large_arr2d[100, 200] = -1.
+    >>> can_throw(large_arr2d)
+    Traceback (most recent call last):
+    ...
+    RuntimeError
+    """