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

feat: free-threaded Python (3.13.0+) support #165

Merged
merged 24 commits into from
Nov 7, 2024

Conversation

jmao-denver
Copy link
Contributor

Fixes #163

@jmao-denver jmao-denver added the enhancement New feature or request label Sep 17, 2024
@jmao-denver jmao-denver self-assigned this Sep 17, 2024
@jmao-denver jmao-denver force-pushed the 163-free-threaed-port branch 2 times, most recently from 404fb91 to a8008de Compare September 23, 2024 16:36
@jmao-denver jmao-denver force-pushed the 163-free-threaed-port branch 7 times, most recently from 0efa657 to b5b17ab Compare October 23, 2024 02:02
@jmao-denver jmao-denver force-pushed the 163-free-threaed-port branch 2 times, most recently from 6e49f85 to 6299c87 Compare October 24, 2024 16:56
@jmao-denver jmao-denver force-pushed the 163-free-threaed-port branch from 4045833 to 6a0f8fe Compare October 24, 2024 19:36
@jmao-denver jmao-denver marked this pull request as ready for review October 25, 2024 15:21
.github/workflows/build.yml Outdated Show resolved Hide resolved
.github/workflows/build.yml Outdated Show resolved Hide resolved
.github/workflows/build.yml Show resolved Hide resolved
.github/workflows/build.yml Outdated Show resolved Hide resolved
.github/workflows/build.yml Show resolved Hide resolved
Comment on lines 30 to 37
#ifdef Py_GIL_DISABLED
typedef struct {
PyMutex lock;
PyThreadState* owner;
int recursion_level;
} ReentrantLock;

static void acquire_lock(ReentrantLock* self) {
Copy link
Member

Choose a reason for hiding this comment

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

Is this a pattern that you found suggested by C python, or elsewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

C Python doesn't offer an reentrant lock primitive. This is a pattern I found somewhere else.

src/main/c/jpy_jtype.c Outdated Show resolved Hide resolved
src/main/java/org/jpy/PyObjectReferences.java Outdated Show resolved Hide resolved
src/main/java/org/jpy/PyObject.java Show resolved Hide resolved
jpyutil.py Show resolved Hide resolved
jpyutil.py Outdated Show resolved Hide resolved
src/main/c/jni/org_jpy_PyLib.c Outdated Show resolved Hide resolved
src/main/c/jpy_jtype.c Show resolved Hide resolved
src/main/c/jpy_jtype.c Show resolved Hide resolved
@@ -71,7 +71,7 @@ public static int cleanup() {
PyObject(long pointer, boolean fromJNI) {
state = new PyObjectState(pointer);
if (fromJNI) {
if (CLEANUP_ON_INIT && PyLib.hasGil()) {
if (CLEANUP_ON_INIT) {
REFERENCES.cleanupOnlyUseFromGIL(); // only performs *one* cleanup
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are no longer using the GIL, then we should rename this function and instead have comments on why it is correct. If we are on 3.12 mode why are we still correct?

Copy link
Contributor Author

@jmao-denver jmao-denver Oct 31, 2024

Choose a reason for hiding this comment

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

This change is made to fix a bug based on the following two observations:

  1. Because we now release the GIL whenever we cross into Java from Python (by @niloc132), the PyLib.hasGIL() test will always fail in Python GIL enabled mode, which results in that the cleanup method will never get called anymore. (BTW, in the GIL disabled mode, PyLib.hasGIL() returns true).
  2. As part of the same enhancement change by @niloc132, he made sure that the cleanup is done under GIL.
    private int cleanupOnlyUseFromGIL(long[] buffer) {
        return PyLib.ensureGil(() -> {
            int index = 0;
            while (index < buffer.length) {
                final Reference<? extends PyObject> reference = referenceQueue.poll();
                if (reference == null) {
   ...

Copy link
Contributor

Choose a reason for hiding this comment

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

@jmao-denver is right, there are several layers of questionable here, and the latest round here is my fault.

  • latest stable release: hasGil is always false here, so this block never executes
  • prior to the "release gil while in Java" change: hasGil is always true, so this always runs.

Why did it even test to begin with? Why does it clean up all other object any time any PyObject is about to be passed in to Java (instead of just once either just before or just after calling Java)? Couldn't say. I think removing the check is correct (either just before the ft patch, or as part of it), and we should reevaluate what is even happening here separately.

Agreed on renaming cleanupOnlyUseFromGIL.

Copy link
Member

Choose a reason for hiding this comment

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

This was an oversight on my part during the "drop GIL" review.

I made the argument during the review that we might prefer to have some way to keep the GIL during "quick" java calls, or in other cases where we explicitly want to keep the GIL. I might argue that our call from JType_CreateJavaPyObject into the constructor of PyObject should keep the GIL.

The reason we need this to only be called into the context of the GIL (historically) is because we need the GIL when calling PyLib.decRef; I'm assuming in the case of free-threaded (/GIL-less) builds, Py_DECREF is thread safe. And it's not only that Py_DECREF that needs to be thread-safe, the arbitrary python deallocation code that can be executed as part of a decRef to 0 needs to be thread-safe.

In the context of free-threaded / GIL-less builds, maybe the functions need to be renamed to be something like PyLib.isThreadSafe / PyLib.ensureThreadSafe, or something like that.

@@ -29,3 +29,32 @@ jobs:

- name: Run Test
run: python setup.py test

test-free-threaded:
Copy link
Member

Choose a reason for hiding this comment

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

I see windows-specific and mac-specific code, but I don't see CI checks for the platforms. There do appear to be windows runners. https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners
It isn't the highest priority, but I am noting it.

src/main/c/jpy_jobj.c Outdated Show resolved Hide resolved
fail-fast: false
matrix:
info:
- { machine: 'ubuntu-20.04', python: '3.13t', arch: 'amd64', cmd: '.github/env/Linux/bdist-wheel.sh' }
Copy link
Member

Choose a reason for hiding this comment

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

Noting that the latest ubuntu LTS is 24.04.1. Not sure how much it matters here.

fail-fast: false
matrix:
info:
- { machine: 'ubuntu-20.04', python: '3.13t', arch: 'aarch64', cmd: '.github/env/Linux/bdist-wheel.sh' }
Copy link
Member

Choose a reason for hiding this comment

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

see note about the latest LTS version

Copy link
Contributor

Choose a reason for hiding this comment

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

20.04 goes EOL 4/23/2025. We may consider bumping to at least 22.04 which.

Copy link
Contributor Author

@jmao-denver jmao-denver Oct 29, 2024

Choose a reason for hiding this comment

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

@devinrsmith is almost certain changing to 22.04 will make the wheels not installation due to some incompatibility in the GLIBC. Will file a follow-up ticket.

Copy link
Member

Choose a reason for hiding this comment

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

We are currently targeting manylinux_2_17, which makes it compatible with GLIBC version 2.17+; afaict, the building of these artifacts needs to happen on a system w/ GLIBC version 2.17, and updating to 22.04 or later breaks this. We should have a ticket about GLIBC version support, and when we will move on past 2.17.

Copy link
Member

Choose a reason for hiding this comment

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

pypa/cibuildwheel#1772 has relevant upstream comments about the topic.

.github/workflows/build.yml Outdated Show resolved Hide resolved
src/test/python/jpy_mt_eval_exec_test.py Outdated Show resolved Hide resolved
src/test/python/jpy_mt_eval_exec_test.py Show resolved Hide resolved
src/test/python/jpy_mt_eval_exec_test.py Show resolved Hide resolved
src/main/c/jpy_jtype.c Outdated Show resolved Hide resolved
src/main/c/jpy_jtype.c Outdated Show resolved Hide resolved
src/main/c/jpy_jobj.c Show resolved Hide resolved
src/main/c/jpy_module.c Show resolved Hide resolved
.github/workflows/build.yml Outdated Show resolved Hide resolved
source .venv/bin/activate
uv pip install pip
echo $JAVA_HOME
echo PATH=$PATH >> $GITHUB_ENV
Copy link
Member

Choose a reason for hiding this comment

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

This looks like it is still using GITHUB_ENV?

src/main/c/jni/org_jpy_PyLib.c Outdated Show resolved Hide resolved
jpyutil.py Outdated Show resolved Hide resolved
Comment on lines +334 to +335

debug_build = sysconfig.get_config_var('Py_DEBUG')
Copy link
Member

Choose a reason for hiding this comment

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

This is not correct in the context of how you are using it. It needs to be checked if it's set to "1"; at least in my builds, it's explicitly set to "0":

python3.8 -m sysconfig | grep Py_DEBUG    
        Py_DEBUG = "0"

After some more reading, I actually think that sys.abiflags is the more appropriate place to check, since it's specifically mentioned as being set for debug builds, https://docs.python.org/3.10/using/configure.html#python-debug-build:

Effects of a debug build:

Copy link
Member

Choose a reason for hiding this comment

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

Sorry for my flip-flopping; it actually looks like .get_config_var is correct, since it seems to return an int when used like this. That said, maybe we should still prefer sys.abiflags since it's more explicitly documented?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

image

Py_DEBUG is also explicitly mentioned there. Also a quick ChatGPT inquiry yields this;

image

I think we should be comfortable with using the value of Py_DEBUG in sysconfig

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 not confident that Py_DEBUG and Py_REF_DEBUG macros are guaranteed to be equivalent to "config vars"; that said, it looks like sys.abiflags doesn't work on Windows, so I guess get_config_var is the best way for now.

It looks like there was a patch many years ago to add an official way to look up if it was built with debug, but it was not merged. python/cpython#69443

@@ -71,8 +71,8 @@ public static int cleanup() {
PyObject(long pointer, boolean fromJNI) {
state = new PyObjectState(pointer);
if (fromJNI) {
if (CLEANUP_ON_INIT && PyLib.hasGil()) {
REFERENCES.cleanupOnlyUseFromGIL(); // only performs *one* cleanup
if (CLEANUP_ON_INIT) {
Copy link
Member

Choose a reason for hiding this comment

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

Although originally unintentional (I think), this is a change in behavior vs the current release. I might suggest Colin or I follow up with a PR, unless this is newly tested in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It isn't explicitly tested. But the code path does get executed implicitly in jpy_typeconv_test_pyobj.py.

@jmao-denver jmao-denver merged commit 1d1a3b1 into jpy-consortium:master Nov 7, 2024
70 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ENH: Support free-threaded python build
5 participants