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

Tier two memory and reference "leaks" #114695

Closed
Eclips4 opened this issue Jan 29, 2024 · 4 comments
Closed

Tier two memory and reference "leaks" #114695

Eclips4 opened this issue Jan 29, 2024 · 4 comments
Assignees
Labels
3.13 new features, bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) tests Tests in the Lib/test dir

Comments

@Eclips4
Copy link
Member

Eclips4 commented Jan 29, 2024

Bug report

Bug description:

CPython was built with ./configure --enable-experimental-jit --with-pydebug

Full trace
trace.txt

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

@Eclips4 Eclips4 added the type-bug An unexpected behavior, bug, or error label Jan 29, 2024
@brandtbucher
Copy link
Member

Thanks for the report, I'll take a look tomorrow!

@brandtbucher brandtbucher self-assigned this Jan 29, 2024
@brandtbucher brandtbucher changed the title A lot of reference leaks JIT reference leaks Jan 29, 2024
@brandtbucher
Copy link
Member

So, these aren't related to the JIT, and they aren't "real" reference/memory leaks (since the memory is indeed cleaned up with the interpreter is finalized).

The problem here is tier two in general... since it needs to "warm up" to actually start running, the normal refleak stabilization period of 3 runs isn't enough. It appears we're "leaking" two things: the executors (reference and memory block "leaks") and the arrays of executors managed by code objects (memory block "leaks").

So there are a few different routes we can take here... I'd like to hear others' thoughts (@gvanrossum, and Mark when he's back).

A: Crank up the number of warmups we do for refleak runs

This is probably the "right" thing to do, but it's fragile and could slow our buildbots to a crawl. With local experiments, it appears that using 9 warmups instead of 3 (-R 9:3) is what's required to pass the whole test suite without leaks. So refleak runs will take roughly twice as long as they do now, and it could get worse.

B: Disable the optimizer during refleak runs

Not a horrible option, but it does mean that we have no refleak coverage for all of tier two.

C: Expose a helper function to wipe out all executors

This is similar to how internal caches get around false "refleaks". Unfortunately, this doesn't really work for us the way things are set up now: while we have ways of globally invalidating executors, this doesn't immediately remove them from the bytecode or touch their refcounts. Getting this to work correctly would be tricky, and likely introduce nasty reference cycles between code objects and executors, or between different executors (once we have trace stitching).

It's a heavy lift with a real runtime cost just to appease the (arguably broken) refleak runners... but there's a chance somebody could want this functionality someday anyways. Currently an executor needs to be hit again in order to be removed after invalidation, but I could certainly imagine use cases for immediately purging JIT'ed code that isn't currently running (and may not ever run again).

D: Hack around the problem

Manually tweak the global refcount total when creating and destroying executors, so the initial reference in the code object isn't reflected (other refcount operations on executors will be tracked normally). This isn't pretty, but it works as expected.

However, this doesn't solve the memory leak problem.

diff --git a/Python/optimizer.c b/Python/optimizer.c
index 0d04b09fef..c33bd87354 100644
--- a/Python/optimizer.c
+++ b/Python/optimizer.c
@@ -2,6 +2,7 @@
 #include "opcode.h"
 #include "pycore_interp.h"
 #include "pycore_bitutils.h"        // _Py_popcount32()
+#include "pycore_object.h"
 #include "pycore_opcode_metadata.h" // _PyOpcode_OpName[]
 #include "pycore_opcode_utils.h"  // MAX_REAL_OPCODE
 #include "pycore_optimizer.h"     // _Py_uop_analyze_and_optimize()
@@ -1103,6 +1104,9 @@ _Py_ExecutorInit(_PyExecutorObject *executor, _PyBloomFilter *dependency_set)
         executor->vm_data.bloom.bits[i] = dependency_set->bits[i];
     }
     link_executor(executor);
+#ifdef Py_REF_DEBUG
+    _Py_DecRefTotal(_PyInterpreterState_GET());
+#endif
 }
 
 /* This must be called by executors during dealloc */
@@ -1110,6 +1114,9 @@ void
 _Py_ExecutorClear(_PyExecutorObject *executor)
 {
     unlink_executor(executor);
+#ifdef Py_REF_DEBUG
+    _Py_IncRefTotal(_PyInterpreterState_GET());
+#endif
 }
 
 void

@brandtbucher brandtbucher added tests Tests in the Lib/test dir interpreter-core (Objects, Python, Grammar, and Parser dirs) 3.13 new features, bugs and security fixes and removed type-bug An unexpected behavior, bug, or error labels Jan 30, 2024
@brandtbucher brandtbucher changed the title JIT reference leaks Tier two memory and reference "leaks" Jan 30, 2024
@gvanrossum
Copy link
Member

So IIUC the solution is to have a non-refcounted backreference from an executor to its code object, which is cleared when the code object releases the executor (i.e., removes it from its list of executors).

@brandtbucher
Copy link
Member

Yep (we decided offline that that that's a not-too-difficult way to implement option C).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.13 new features, bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) tests Tests in the Lib/test dir
Projects
None yet
Development

No branches or pull requests

3 participants