Skip to content

Conversation

@aditya-1112
Copy link
Contributor

@aditya-1112 aditya-1112 commented Jun 25, 2025

Enable integer exception checking for performance improvement. This also resolves Asyncio segfaults with free-threaded Python.

Takes care of Cython performance hint:

performance hint: src/python/grpcio/grpc/_cython/_cygrpc/aio/completion_queue.pyx.pxi:126:22: Exception check after calling '_poll' will always require the GIL to be acquired. Possible solutions:	1. Declare '_poll' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.	2. Use an 'int' return type on '_poll' to allow an error code to be returned. 

Details in https://cython.readthedocs.io/en/3.1.x/src/userguide/language_basics.html#error-return-values:

There’s a known performance pitfall when combining nogil and except * @cython.exceptval(check=True). In this case Cython must always briefly re-acquire the GIL after a function call to check if an exception has been raised. This can commonly happen with a function returning nothing (C void). Simple workarounds are to mark the function as noexcept if you’re certain that exceptions cannot be thrown, or to change the return type to int and just let Cython use the return value as an error flag (by default, -1 triggers the exception check).

@sreenithi sreenithi requested a review from asheshvidyut July 16, 2025 09:47
@sreenithi sreenithi added the release notes: no Indicates if PR should not be in release notes label Jul 16, 2025
@aditya-1112 aditya-1112 changed the title [Python] Resolve aio segfaults for Nogil mode support [Python] Make nogil function as noexcept for performance improvement and resolving Asyncio segfaults with free-threaded Python Jul 16, 2025
self._loops[loop] = _BoundEventLoop(loop, self._read_socket, self._handle_events)

cdef void _poll(self) nogil:
cdef int _poll(self) noexcept nogil:
Copy link
Member

@sergiitk sergiitk Jul 19, 2025

Choose a reason for hiding this comment

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

I saw we call _handle_events below, which is aclassic python def method. Can we guarantee that it's not going to throw? Or noexcept only indicates that we explicitly raise from _poll itself? Not familiar with enough with Cython yet.

# instead of delegate to any thread, the polling thread
# should handle the distribution of the event.
self._handle_events(None)
return 0
Copy link
Member

Choose a reason for hiding this comment

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

Hm. I think I just found a better explanation to the normal compiler warning we're getting:

There’s a known performance pitfall when combining nogil and except * @cython.exceptval(check=True). In this case Cython must always briefly re-acquire the GIL after a function call to check if an exception has been raised. This can commonly happen with a function returning nothing (C void). Simple workarounds are to mark the function as noexcept if you’re certain that exceptions cannot be thrown, or to change the return type to int and just let Cython use the return value as an error flag (by default, -1 triggers the exception check).
https://cython.readthedocs.io/en/3.1.x/src/userguide/language_basics.html

I think unlike the compile regular warning, this doc implies that we shouldn't do either one or another, not both. Compare with the compile warning warning:

Exception check after calling '_poll' will always require the GIL to be acquired. Possible solutions:	1. Declare '_poll' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.	2. Use an 'int' return type on '_poll' to allow an error code to be returned. 

The important part is

or to change the return type to int and just let Cython use the return value as an error flag (by default, -1 triggers the exception check).

Do I understand this correctly? Here's how I interpret this:

If we JUST change the return type from void to int, we'll still be able to throw the exception as before - without changing anything other then the method signature and adding return 1 on this line to indicate success.
Then Cython doesn't have this performance penalty because it now translate the exception we raised to exception to return -1. So later it know to acquire gil and handle the exception because the return is -1.

@sreenithi Does this make sense? Again, I have virtually no experience with Cython, so please verify this for me

Copy link
Contributor

Choose a reason for hiding this comment

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

ya this makes sense. We just tried it and verified that it works too. So he has changed the function accordingly. This also solves the problem with self._handle_events raising any errors. Because though it doesn't explicitly raise an error, there may be any run-time errors that might get missed out. Doing this will propagate that error successfully too.

Thanks for the suggestion!

Copy link
Contributor

Choose a reason for hiding this comment

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

However, I was thinking return 0 still makes sense as the last return value in case of success. Both in C/C++ or shell scripts, 0 is the usual convention for a successful return code right. So I was thinking it makes sense to keep it 0.

Copy link
Member

Choose a reason for hiding this comment

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

I'm ok with returning 0 here, especially since we're not checking the return value. I'd argue what to return gets a bit ambiguous in the cython context, which is in between C and python.

@aditya-1112
Copy link
Contributor Author

Removed the 'noexcept' and declared the function as 'except -1' with integer return type, which signals an error via a -1 return value without the unsafe GIL acquisition on the return path, also allowing Python exceptions to be directly raised within the _poll function

@aditya-1112 aditya-1112 changed the title [Python] Make nogil function as noexcept for performance improvement and resolving Asyncio segfaults with free-threaded Python [Python] Enable integer exception checking for performance improvement and resolving Asyncio segfaults with free-threaded Python Jul 21, 2025
@sergiitk sergiitk removed the release notes: no Indicates if PR should not be in release notes label Jul 22, 2025
@sergiitk sergiitk added the release notes: yes Indicates if PR needs to be in release notes label Jul 22, 2025
@sergiitk sergiitk changed the title [Python] Enable integer exception checking for performance improvement and resolving Asyncio segfaults with free-threaded Python [Python] Async grpcio: Improve CompletionQueue polling performance Jul 22, 2025
@sergiitk sergiitk changed the title [Python] Async grpcio: Improve CompletionQueue polling performance [Python] gRPC AsyncIO: Improve CompletionQueue polling performance Jul 22, 2025
asheshvidyut pushed a commit to asheshvidyut/grpc that referenced this pull request Aug 22, 2025
…rpc#39993) Enable integer exception checking for performance improvement. This also resolves Asyncio segfaults with free-threaded Python. Takes care of Cython performance hint: ``` performance hint: src/python/grpcio/grpc/_cython/_cygrpc/aio/completion_queue.pyx.pxi:126:22: Exception check after calling '_poll' will always require the GIL to be acquired. Possible solutions:	1. Declare '_poll' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.	2. Use an 'int' return type on '_poll' to allow an error code to be returned. ``` Details in https://cython.readthedocs.io/en/3.1.x/src/userguide/language_basics.html#error-return-values: > There’s a known performance pitfall when combining `nogil` and `except *` `@cython.exceptval(check=True)`. In this case Cython must always briefly re-acquire the GIL after a function call to check if an exception has been raised. This can commonly happen with a function returning nothing (C `void`). Simple workarounds are to mark the function as `noexcept` if you’re certain that exceptions cannot be thrown, or to change the return type to `int` and just let Cython use the return value as an error flag (by default, `-1` triggers the exception check). Closes grpc#39993 COPYBARA_INTEGRATE_REVIEW=grpc#39993 from aditya-1112:aio-segfault-fix bdc3310 PiperOrigin-RevId: 796464140
paulosjca pushed a commit to paulosjca/grpc that referenced this pull request Aug 23, 2025
…rpc#39993) Enable integer exception checking for performance improvement. This also resolves Asyncio segfaults with free-threaded Python. Takes care of Cython performance hint: ``` performance hint: src/python/grpcio/grpc/_cython/_cygrpc/aio/completion_queue.pyx.pxi:126:22: Exception check after calling '_poll' will always require the GIL to be acquired. Possible solutions:	1. Declare '_poll' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.	2. Use an 'int' return type on '_poll' to allow an error code to be returned. ``` Details in https://cython.readthedocs.io/en/3.1.x/src/userguide/language_basics.html#error-return-values: > There’s a known performance pitfall when combining `nogil` and `except *` `@cython.exceptval(check=True)`. In this case Cython must always briefly re-acquire the GIL after a function call to check if an exception has been raised. This can commonly happen with a function returning nothing (C `void`). Simple workarounds are to mark the function as `noexcept` if you’re certain that exceptions cannot be thrown, or to change the return type to `int` and just let Cython use the return value as an error flag (by default, `-1` triggers the exception check). Closes grpc#39993 COPYBARA_INTEGRATE_REVIEW=grpc#39993 from aditya-1112:aio-segfault-fix bdc3310 PiperOrigin-RevId: 796464140
asheshvidyut pushed a commit to asheshvidyut/grpc that referenced this pull request Sep 12, 2025
…rpc#39993) Enable integer exception checking for performance improvement. This also resolves Asyncio segfaults with free-threaded Python. Takes care of Cython performance hint: ``` performance hint: src/python/grpcio/grpc/_cython/_cygrpc/aio/completion_queue.pyx.pxi:126:22: Exception check after calling '_poll' will always require the GIL to be acquired. Possible solutions:	1. Declare '_poll' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.	2. Use an 'int' return type on '_poll' to allow an error code to be returned. ``` Details in https://cython.readthedocs.io/en/3.1.x/src/userguide/language_basics.html#error-return-values: > There’s a known performance pitfall when combining `nogil` and `except *` `@cython.exceptval(check=True)`. In this case Cython must always briefly re-acquire the GIL after a function call to check if an exception has been raised. This can commonly happen with a function returning nothing (C `void`). Simple workarounds are to mark the function as `noexcept` if you’re certain that exceptions cannot be thrown, or to change the return type to `int` and just let Cython use the return value as an error flag (by default, `-1` triggers the exception check). Closes grpc#39993 COPYBARA_INTEGRATE_REVIEW=grpc#39993 from aditya-1112:aio-segfault-fix bdc3310 PiperOrigin-RevId: 796464140
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment