Skip to content

Commit a61ebfa

Browse files
jbower-fbfacebook-github-bot
authored andcommitted
Add "free-list" for JIT generator objects
Summary: Redo of D77572569. The original had a bug where upon shutdown the module state was free'd but we still go down the `jit_dealloc_gen()` path which tries to make use of it. We now swap back the original dealloc methods for the system generators on shutdown. We also move the free list data to the module state and bump the module state's refcount so we don't free memory from underneath outstanding JIT generators. Reviewed By: alexmalyshev Differential Revision: D78516909 fbshipit-source-id: 5ff870968d90873bedd1b4a34edae132133f4ee9
1 parent cdfd31d commit a61ebfa

13 files changed

Lines changed: 337 additions & 31 deletions

cinderx/Common/log.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ std::string repr(BorrowedRef<> obj);
7979
} \
8080
}
8181

82+
#define JIT_CHECK_ONCE(COND, ...) \
83+
{ \
84+
static bool checked = false; \
85+
if (!checked) { \
86+
JIT_CHECK(COND, __VA_ARGS__); \
87+
} else { \
88+
checked = true; \
89+
} \
90+
}
91+
8292
#define JIT_ABORT(...) \
8393
{ \
8494
fmt::print(stderr, "JIT: {}:{} -- Abort\n", __FILE__, __LINE__); \
@@ -97,6 +107,7 @@ std::string repr(BorrowedRef<> obj);
97107
#ifdef Py_DEBUG
98108
#define JIT_DABORT(...) JIT_ABORT(__VA_ARGS__)
99109
#define JIT_DCHECK(COND, ...) JIT_CHECK((COND), __VA_ARGS__)
110+
#define JIT_DCHECK_ONCE(COND, ...) JIT_CHECK_ONCE((COND), __VA_ARGS__)
100111
#else
101112
#define JIT_DABORT(...) \
102113
if (0) { \
@@ -106,6 +117,10 @@ std::string repr(BorrowedRef<> obj);
106117
if (0) { \
107118
JIT_CHECK((COND), __VA_ARGS__); \
108119
}
120+
#define JIT_DCHECK_ONCE(COND, ...) \
121+
if (0) { \
122+
JIT_CHECK_ONCE((COND), __VA_ARGS__); \
123+
}
109124
#endif
110125

111126
} // namespace jit

cinderx/Common/util.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ constexpr T roundUp(T x, size_t n) {
128128
return roundDown(x + n - 1, n);
129129
}
130130

131+
template <typename T1, typename T2>
132+
requires std::is_integral_v<T1> && std::is_integral_v<T2>
133+
constexpr std::common_type_t<T1, T1> ceilDiv(T1 a, T2 b) {
134+
return (a + b - 1) / b;
135+
}
136+
131137
constexpr int kCoFlagsAnyGenerator =
132138
CO_ASYNC_GENERATOR | CO_COROUTINE | CO_GENERATOR | CO_ITERABLE_COROUTINE;
133139

cinderx/Jit/codegen/gen_asm.cpp

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,13 +1376,10 @@ void NativeGenerator::generateResumeEntry() {
13761376
#if PY_VERSION_HEX < 0x030C0000
13771377
auto gi_jit_data_offset = offsetof(PyGenObject, gi_jit_data);
13781378
#else
1379-
int python_frame_data_bytes =
1380-
_PyFrame_NumSlotsForCodeObject(GetFunction()->code) *
1381-
cinderx::getModuleState()->genType()->tp_itemsize;
1382-
Py_ssize_t gi_jit_data_offset =
1383-
cinderx::getModuleState()->genType()->tp_basicsize +
1384-
python_frame_data_bytes;
1385-
1379+
Py_ssize_t python_frame_slots =
1380+
_PyFrame_NumSlotsForCodeObject(GetFunction()->code);
1381+
Py_ssize_t gi_jit_data_offset = _PyObject_VAR_SIZE(
1382+
cinderx::getModuleState()->genType(), python_frame_slots);
13861383
#endif
13871384
as_->mov(jit_data_r, x86::ptr(x86::rdi, gi_jit_data_offset));
13881385

cinderx/Jit/generators_mm.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
#include "cinderx/python.h"
4+
5+
#if PY_VERSION_HEX >= 0x030C0000
6+
7+
#include "internal/pycore_object.h"
8+
9+
#include "cinderx/Common/log.h"
10+
#include "cinderx/Common/util.h"
11+
#include "cinderx/Jit/generators_mm.h"
12+
#include "cinderx/module_state.h"
13+
#if PY_VERSION_HEX >= 0x030E0000
14+
#include "internal/pycore_interpframe.h"
15+
#endif
16+
17+
namespace jit {
18+
19+
JitGenFreeList:: // NOLINT(cppcoreguidelines-pro-type-member-init)
20+
JitGenFreeList() {
21+
Entry* next = nullptr;
22+
for (size_t i = 0; i < kGenFreeListEntries; ++i) {
23+
entries_[i].next = next;
24+
next = &entries_[i];
25+
}
26+
head_ = next;
27+
}
28+
29+
void* JitGenFreeList::rawAllocate() {
30+
JIT_DCHECK(head_, "No free generator entries");
31+
Entry* entry = head_;
32+
head_ = entry->next;
33+
// The memory for the free-list is backed by the module state, so bump the
34+
// reference count to prevent it being free'd before all free-listed
35+
// generators are.
36+
Py_INCREF(cinderx::getModuleState()->module());
37+
return entry->data;
38+
}
39+
40+
bool JitGenFreeList::fromThisArena(void* ptr) {
41+
return ptr >= &entries_ && ptr < &entries_[kGenFreeListEntries - 1] + 1;
42+
}
43+
44+
void JitGenFreeList::free(PyObject* ptr) {
45+
if (!fromThisArena(ptr)) {
46+
PyObject_GC_Del(ptr);
47+
return;
48+
}
49+
// Note we assert in allocate() that the "presize" of data is
50+
// sizeof(PyGC_HEAD)
51+
Entry* entry = reinterpret_cast<Entry*>( // NOLINT(performance-no-int-to-ptr)
52+
reinterpret_cast<uintptr_t>(ptr) - sizeof(PyGC_Head));
53+
JIT_DCHECK(
54+
(reinterpret_cast<uintptr_t>(entry) -
55+
reinterpret_cast<uintptr_t>(&entries_[0])) %
56+
kGenFreeListEntrySize ==
57+
0,
58+
"Incorrect pointer calculation");
59+
entry->next = head_;
60+
head_ = entry;
61+
// See comment in rawAllocate()
62+
Py_DECREF(cinderx::getModuleState()->module());
63+
}
64+
65+
std::pair<JitGenObject*, size_t> JitGenFreeList::allocate(
66+
BorrowedRef<PyCodeObject> code,
67+
uint64_t jit_data_size) {
68+
BorrowedRef<PyTypeObject> gen_tp = cinderx::getModuleState()->genType();
69+
// We *assume* these assertions hold in free().
70+
JIT_DCHECK_ONCE(
71+
_PyType_PreHeaderSize(gen_tp) == sizeof(PyGC_Head) &&
72+
!_PyType_HasFeature(gen_tp, Py_TPFLAGS_PREHEADER),
73+
"Unexpected pre-header setup");
74+
75+
// A "slot" is the size of PyObject* and we assume this just means 64 bits for
76+
// purposes of sizing allocation to cover JIT data.
77+
static_assert(sizeof(uint64_t) == sizeof(PyObject*));
78+
79+
// +1 for the pointer to JIT data (GenDataFooter*)
80+
size_t slots =
81+
_PyFrame_NumSlotsForCodeObject(code) + 1 + ceilDiv(jit_data_size, 8);
82+
// All the generator types should be the same size.
83+
size_t size = _PyObject_VAR_SIZE(gen_tp, slots);
84+
size_t total_size = sizeof(PyGC_Head) + size;
85+
86+
bool is_coro = !!(code->co_flags & CO_COROUTINE);
87+
88+
if (!head_ || total_size > kGenFreeListEntrySize) {
89+
JitGenObject* gen = is_coro
90+
? reinterpret_cast<JitGenObject*>(PyObject_GC_NewVar(
91+
PyCoroObject, cinderx::getModuleState()->coroType(), slots))
92+
: reinterpret_cast<JitGenObject*>(
93+
PyObject_GC_NewVar(PyGenObject, gen_tp, slots));
94+
// See comment in allocate_and_link_interpreter_frame about failure.
95+
JIT_CHECK(gen != nullptr, "Failed to allocate JitGenObject");
96+
return {gen, size};
97+
}
98+
99+
void* raw = rawAllocate();
100+
// Zero the pre-header, which in this case is the GC header. The
101+
// The reference for this is gc_alloc() + _PyObject_GC_Link(). It
102+
// would be nice if the latter were public so we could custom
103+
// allocate GC'able objects.
104+
// Note we are NOT bumping the GC's young generation counter here as
105+
// _PyObject_GC_Link would. I argue we're not actually increasing memory
106+
// pressure so this is not needed.
107+
(reinterpret_cast<PyObject**>(raw))[0] = nullptr;
108+
(reinterpret_cast<PyObject**>(raw))[1] = nullptr;
109+
PyVarObject* op =
110+
reinterpret_cast<PyVarObject*>( // NOLINT(performance-no-int-to-ptr)
111+
reinterpret_cast<uintptr_t>(raw) + sizeof(PyGC_Head));
112+
113+
PyTypeObject* tp = is_coro ? cinderx::getModuleState()->coroType() : gen_tp;
114+
_PyObject_InitVar(op, tp, slots);
115+
116+
return {reinterpret_cast<JitGenObject*>(op), size};
117+
}
118+
119+
} // namespace jit
120+
121+
#endif // PY_VERSION_HEX >= 0x030C0000

cinderx/Jit/generators_mm.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#pragma once
3+
4+
#include <Python.h>
5+
6+
#if PY_VERSION_HEX >= 0x030C0000
7+
8+
#include "cinderx/Common/ref.h"
9+
#include "cinderx/Jit/generators_mm_iface.h"
10+
11+
#include <array>
12+
13+
namespace jit {
14+
15+
struct JitGenObject;
16+
17+
// These values were determined experimentally on IG's webservers by utilizing
18+
// the stats above. The number of outstanding requests seems to burst up to ~60k
19+
// on startup but then quickly settles down to around 1-2k, so 2048 entries
20+
// should be enough. The average size seems to be ~400 bytes with the max being
21+
// about 10x that. Performance experiments showed a size of 512 was a greater
22+
// improvement compared to 1024. Presumably the trade off in extra fixed memory
23+
// allocation cost on workers isn't worth it for greater sizes.
24+
constexpr size_t kGenFreeListEntries = 2048;
25+
constexpr size_t kGenFreeListEntrySize = 512;
26+
27+
// Basically a free-list but the backing memory is pre-allocated in a single
28+
// block. This makes it possible to determine if the storage is from this pool
29+
// even after deopt by just examining a generator's pointer value.
30+
class JitGenFreeList : public IJitGenFreeList {
31+
public:
32+
JitGenFreeList();
33+
~JitGenFreeList() override = default;
34+
35+
std::pair<JitGenObject*, size_t> allocate(
36+
BorrowedRef<PyCodeObject> code,
37+
uint64_t jit_spill_words) override;
38+
void free(PyObject* ptr) override;
39+
40+
private:
41+
void* rawAllocate();
42+
bool fromThisArena(void* ptr);
43+
44+
struct Entry {
45+
union {
46+
uint8_t data[kGenFreeListEntrySize];
47+
Entry* next;
48+
};
49+
};
50+
51+
std::array<Entry, kGenFreeListEntries> entries_;
52+
Entry* head_;
53+
};
54+
55+
} // namespace jit
56+
57+
#endif // PY_VERSION_HEX >= 0x030C0000

cinderx/Jit/generators_mm_iface.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
3+
#pragma once
4+
5+
#include <Python.h>
6+
7+
#include "cinderx/Common/ref.h"
8+
9+
namespace jit {
10+
11+
struct JitGenObject;
12+
13+
class IJitGenFreeList {
14+
public:
15+
IJitGenFreeList() = default;
16+
virtual ~IJitGenFreeList() = default;
17+
18+
virtual std::pair<JitGenObject*, size_t> allocate(
19+
BorrowedRef<PyCodeObject> code,
20+
uint64_t jit_spill_words) = 0;
21+
virtual void free(PyObject* ptr) = 0;
22+
};
23+
24+
} // namespace jit

cinderx/Jit/generators_rt.cpp

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
#include "cinderx/Jit/deopt.h"
1313
#include "cinderx/Jit/frame.h"
1414
#include "cinderx/Jit/generators_borrowed.h"
15+
#include "cinderx/Jit/generators_mm.h"
16+
#include "cinderx/Jit/generators_rt.h"
1517
#include "cinderx/Jit/runtime.h"
18+
#include "cinderx/UpstreamBorrow/borrowed.h"
1619
#include "cinderx/module_state.h"
1720

1821
#include <string_view>
@@ -32,11 +35,57 @@ PyObject* JitGenObject::yieldFrom() {
3235

3336
namespace {
3437

35-
void jitgen_dealloc(PyObject* obj) {
36-
if (!deopt_jit_gen(obj)) {
38+
const destructor original_gen_dealloc = PyGen_Type.tp_dealloc;
39+
const destructor original_coro_dealloc = PyCoro_Type.tp_dealloc;
40+
41+
// This is mostly a copy of gen_dealloc from genobject.c but with a deopt at the
42+
// start, using our own memory manager for free at the end, some minor
43+
// tweaks for C++, and to use public APIs.
44+
void jitgen_dealloc(PyObject* self) {
45+
if (!deopt_jit_gen(self)) {
3746
JIT_ABORT("Tried to dealloc a running JIT generator");
3847
}
39-
return Py_TYPE(obj)->tp_dealloc(obj);
48+
49+
PyGenObject* gen = reinterpret_cast<PyGenObject*>(self);
50+
51+
PyObject_GC_UnTrack(gen);
52+
53+
if (gen->gi_weakreflist != nullptr) {
54+
PyObject_ClearWeakRefs(self);
55+
}
56+
57+
PyObject_GC_Track(self);
58+
59+
if (PyObject_CallFinalizerFromDealloc(self)) {
60+
return; /* resurrected. :( */
61+
}
62+
63+
PyObject_GC_UnTrack(self);
64+
if (PyAsyncGen_CheckExact(gen)) {
65+
/* We have to handle this case for asynchronous generators
66+
right here, because this code has to be between UNTRACK
67+
and GC_Del. */
68+
Py_CLEAR(reinterpret_cast<PyAsyncGenObject*>(gen)->ag_origin_or_finalizer);
69+
}
70+
if (gen->gi_frame_state < FRAME_CLEARED) {
71+
_PyInterpreterFrame* frame = generatorFrame(gen);
72+
gen->gi_frame_state = FRAME_CLEARED;
73+
frame->previous = nullptr;
74+
_PyFrame_ClearExceptCode(frame);
75+
}
76+
PyCodeObject* code = frameCode(generatorFrame(gen));
77+
if (code->co_flags & CO_COROUTINE) {
78+
Py_CLEAR(reinterpret_cast<PyCoroObject*>(gen)->cr_origin_or_finalizer);
79+
}
80+
Py_DECREF(code);
81+
Py_CLEAR(gen->gi_name);
82+
Py_CLEAR(gen->gi_qualname);
83+
_PyErr_ClearExcState(&gen->gi_exc_state);
84+
#if PY_VERSION_HEX < 0x030E0000
85+
Py_CLEAR(gen->gi_ci_awaiter);
86+
#endif
87+
88+
cinderx::getModuleState()->jitGenFreeList()->free(self);
4089
}
4190

4291
int jitgen_traverse(PyObject* obj, visitproc visit, void* arg) {
@@ -715,6 +764,10 @@ void init_jit_genobject_type() {
715764
auto copy_getset = [](PyGetSetDef* src, PyGetSetDef* target) {
716765
int i;
717766
for (i = 0; src[i].name != nullptr; ++i) {
767+
JIT_CHECK(
768+
target[i].name != nullptr,
769+
"Missing getter/setter on JIT generator: {}",
770+
src[i].name);
718771
JIT_CHECK(
719772
std::string_view(target[i].name) == src[i].name,
720773
"Name mismatch: {} != {}",
@@ -744,6 +797,10 @@ void init_jit_genobject_type() {
744797
auto copy_methods = [](PyMethodDef* src, PyMethodDef* target) {
745798
int i;
746799
for (i = 0; src[i].ml_name != nullptr; ++i) {
800+
JIT_CHECK(
801+
target[i].ml_name != nullptr,
802+
"Missing method on JIT generator: {}",
803+
src[i].ml_name);
747804
JIT_CHECK(
748805
std::string_view(target[i].ml_name) == src[i].ml_name,
749806
"Name mismatch: {} != {}",
@@ -766,6 +823,16 @@ void init_jit_genobject_type() {
766823
copy_methods(PyGen_Type.tp_methods, gen_type->tp_methods);
767824
copy_methods(PyCoro_Type.tp_methods, coro_type->tp_methods);
768825

826+
cinderx::getModuleState()->setJitGenFreeList(new JitGenFreeList());
827+
828+
// Override dealloc so we can use a "free-list" for our objects.
829+
JIT_CHECK(
830+
PyGen_Type.tp_dealloc == original_gen_dealloc &&
831+
PyCoro_Type.tp_dealloc == original_coro_dealloc,
832+
"PyGen/Coro_Type already overridden");
833+
PyGen_Type.tp_dealloc = reinterpret_cast<destructor>(jitgen_dealloc);
834+
PyCoro_Type.tp_dealloc = reinterpret_cast<destructor>(jitgen_dealloc);
835+
769836
#ifdef ENABLE_GENERATOR_AWAITER
770837
JIT_CHECK(
771838
PyCoro_Type.tp_flags & Ci_TPFLAGS_HAVE_AM_EXTRA,
@@ -776,6 +843,11 @@ void init_jit_genobject_type() {
776843
#endif
777844
}
778845

846+
void shutdown_jit_genobject_type() {
847+
PyGen_Type.tp_dealloc = original_gen_dealloc;
848+
PyCoro_Type.tp_dealloc = original_coro_dealloc;
849+
}
850+
779851
static PyMethodDef anextawaitable_methods[] = {
780852
{"send", (PyCFunction)Ci_anextawaitable_send, METH_O, ""},
781853
{"throw", (PyCFunction)Ci_anextawaitable_throw, METH_VARARGS, ""},

0 commit comments

Comments
 (0)