Skip to content

Tier two memory and reference "leaks" #114695

Description

@Eclips4

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

Activity

  1. brandtbucher commented on Jan 29, 2024

    @brandtbucher
    Member

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

  2. self-assigned this
    on Jan 29, 2024
  3. changed the title [-]A lot of reference leaks[/-] [+]JIT reference leaks[/+] on Jan 29, 2024
  4. brandtbucher commented on Jan 30, 2024

    @brandtbucher
    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
  5. added
    testsTests in the Lib/test dir
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    3.13only security fixes
    and removed
    type-bugAn unexpected behavior, bug, or error
    on Jan 30, 2024
  6. changed the title [-]JIT reference leaks[/-] [+]Tier two memory and reference "leaks"[/+] on Jan 30, 2024
  7. gvanrossum commented on Jan 31, 2024

    @gvanrossum
    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).

  8. brandtbucher commented on Jan 31, 2024

    @brandtbucher
    Member

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

  9. added a commit that references this issue on Feb 12, 2024
  10. added a commit that references this issue on Feb 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.13only security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)testsTests in the Lib/test dir

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions