Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional with gil and threading #3554

Closed
rth opened this issue Apr 26, 2020 · 6 comments · Fixed by #3556
Closed

Conditional with gil and threading #3554

rth opened this issue Apr 26, 2020 · 6 comments · Fixed by #3556

Comments

@rth
Copy link

rth commented Apr 26, 2020

A nogil block with a loop that uses an unlikely with gil statement e.g. approximately,

cpdef float test_gil(float[:] x):
    cdef int i = 0
    cdef int size = x1.shape[0]
    cdef float out = 0
    with nogil:
        while i < size:
            out_sum += x[i]
            if i > size +1:
                with gil:
                    raise ValueError
   return out 

appears work similarly to the while function not releasing gil, when using in multi-threaded code.

See more complete minimal example in scikit-learn/scikit-learn#17038 (comment)

Naively I though that since the with gil is never executed, performance wise this would be equivalent to the same function without the with gil block (or roughly that threading performance would be directly impacted by the fraction of run time where the gil is released). However it does not seem to be the case.

Is this behavior expected? If so maybe the documentation section https://cython.readthedocs.io/en/latest/src/userguide/external_C_code.html#acquiring-and-releasing-the-gil could be updated to mention conditional acquiring of GIL.

@scoder
Copy link
Contributor

scoder commented Apr 26, 2020

Note that you do not need to write the with gil in this specific example since it's implicit in the raise statement (i.e. you can raise exceptions also from within nogil sections). That generates the same code as with gil, though, so will run into the same problem.

Looking at the C code that Cython generates from the example that you linked to, the difference is that the function that uses with gil has a Python temp variable, which needs to be cleaned up at the end in the exception case. For that, it needs to acquire the GIL. However, it wouldn't need the GIL in the success case, and still acquires it. That makes it uselessly hammer on the GIL in very fast functions like this.

The GIL acquisition is unconditionally inserted here, as a try-finally statement wrapping the whole function body:

def _handle_nogil_cleanup(self, lenv, node):
"Handle cleanup for 'with gil' blocks in nogil functions."
if lenv.nogil and lenv.has_with_gil_block:
# Acquire the GIL for cleanup in 'nogil' functions, by wrapping
# the entire function body in try/finally.
# The corresponding release will be taken care of by
# Nodes.FuncDefNode.generate_function_definitions()
node.body = Nodes.NogilTryFinallyStatNode(
node.body.pos,
body=node.body,
finally_clause=Nodes.EnsureGILNode(node.body.pos),
finally_except_clause=Nodes.EnsureGILNode(node.body.pos))

and gets released here, after generating the function code:

if acquire_gil or (lenv.nogil and lenv.has_with_gil_block):
# release the GIL (note that with-gil blocks acquire it on exit in their EnsureGILNode)
code.put_release_ensured_gil()
code.funcstate.gil_owned = False

I don't know how difficult it would be to detect the needless acquire and avoid it, but in any case, I consider it a bug that this happens unconditionally. in the success case, it should run through the function without touching the GIL at all.

@rth
Copy link
Author

rth commented Apr 26, 2020

Thanks for the detailed answer and investigation @scoder !

@scoder
Copy link
Contributor

scoder commented Apr 26, 2020

I think the best solution is to get rid of the try-finally node and do the cleanup in FuncDefNode.generate_function_definitions(), acquiring the GIL only when necessary for a given exit case (error or success).

scoder added a commit that referenced this issue Apr 26, 2020
…ion exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

Closes #3554.
@scoder
Copy link
Contributor

scoder commented Apr 26, 2020

I think I found a better way to do this. See #3556.

scoder added a commit that referenced this issue Apr 26, 2020
…ion exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

Closes #3554.
@scoder
Copy link
Contributor

scoder commented Apr 26, 2020

@rth @webber26232 could you try #3556 to see if it resolves the performance issue for you? I looked at the generated code and the success code path is free of GIL usage now and should be fast.

scoder added a commit that referenced this issue Apr 27, 2020
Acquire the GIL in nogil functions only when strictly needed on function exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

Closes GH-3554.
@rth
Copy link
Author

rth commented May 2, 2020

Sorry for slow response @scoder. Thank you for finding a solution and for your work on Cython in general! I can confirm that 3.0a3 which includes this fix does resolve the problem on the example in scikit-learn/scikit-learn#17038 (comment)

oleksandr-pavlyk pushed a commit to oleksandr-pavlyk/cython that referenced this issue Mar 28, 2022
Acquire the GIL in nogil functions only when strictly needed on function exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

Closes cythonGH-3554.
scoder pushed a commit that referenced this issue Mar 31, 2022
)

Acquire the GIL in nogil functions only when strictly needed on function exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

Closes GH-3554
Closes GH-4637
scoder added a commit that referenced this issue Apr 16, 2022
… (GH-4703)" (GH-4742)

PR 4703 was an incomplete backport of the changes needed for #3554 and generates incorrect C code.

See #3554
Reverts #4703

This reverts commit d395a56.
oleksandr-pavlyk pushed a commit to oleksandr-pavlyk/cython that referenced this issue Apr 18, 2022
Acquire the GIL in nogil functions only when strictly needed on function exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

Closes cythonGH-3554.
oleksandr-pavlyk pushed a commit to oleksandr-pavlyk/cython that referenced this issue Apr 18, 2022
Acquire the GIL in nogil functions only when strictly needed on function exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

Closes cythonGH-3554.
scoder pushed a commit that referenced this issue May 3, 2022
)

Closes #4637
See See #3556

* Acquire the GIL in nogil functions only when strictly needed on function exit, e.g. for cleaning up temp variables from with-gil blocks or adding tracebacks.

See #3554

* Make the GIL-avoidance in 7d99b0f actually work in nogil functions and not just nogil sections.

See #3558
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants