Skip to content

Commit 2e1666d

Browse files
authored
[FFI][CYTHON] Release GIL when calling into long running functions (#11461)
Unlike ctypes, Cython by default do not release GIL when calling into C API functions. This causes problems when the function is long running. As the particular calling thread will block other python threads by holding the GIL. This PR explicitly releases GIL when calling into possible long running functions. It fixes the timeout issue in PopenPool which previously relied on another python thread for timeout. Added a regression test-case by changing sleep to sleep in FFI, which previously will indefinitely block the popen tests.
1 parent 01ee1bc commit 2e1666d

File tree

6 files changed

+73
-29
lines changed

6 files changed

+73
-29
lines changed

python/tvm/_ffi/_cython/base.pxi

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,34 @@ ctypedef int (*TVMPackedCFunc)(
9494

9595
ctypedef void (*TVMPackedCFuncFinalizer)(void* resource_handle)
9696

97+
# NOTE: All of TVM's C API function can be called without gil.
98+
# for API functions that can be run long(e.g. FuncCall)
99+
# we need to explicitly release the GIL as follows.
100+
#
101+
# cdef myfunc():
102+
# cdef int c_api_ret_code
103+
# with nogil:
104+
# c_api_ret_code = TVMAPIFunc(...)
105+
# CHECK_CALL(c_apt_ret_code)
106+
#
107+
# Explicitly releasing the GIL enables other python threads
108+
# to continue running while we are in TVMAPIFunc.
109+
# Not releasing GIL explicitly is OK(and perhaps desirable)
110+
# for short-running functions, as frequent unlocking also takes time,
111+
# the python interpreter will release GIL in a set period.
112+
#
113+
# We mark the possibly long running function as nogil below.
97114
cdef extern from "tvm/runtime/c_runtime_api.h":
98115
void TVMAPISetLastError(const char* msg)
99116
const char *TVMGetLastError()
100117
int TVMFuncGetGlobal(const char* name,
101-
TVMPackedFuncHandle* out);
118+
TVMPackedFuncHandle* out)
102119
int TVMFuncCall(TVMPackedFuncHandle func,
103120
TVMValue* arg_values,
104121
int* type_codes,
105122
int num_args,
106123
TVMValue* ret_val,
107-
int* ret_type_code)
124+
int* ret_type_code) nogil
108125
int TVMFuncFree(TVMPackedFuncHandle func)
109126
int TVMCFuncSetReturn(TVMRetValueHandle ret,
110127
TVMValue* value,
@@ -119,15 +136,15 @@ cdef extern from "tvm/runtime/c_runtime_api.h":
119136
tvm_index_t ndim,
120137
DLDataType dtype,
121138
DLDevice dev,
122-
DLTensorHandle* out)
123-
int TVMArrayFree(DLTensorHandle handle)
139+
DLTensorHandle* out) nogil
140+
int TVMArrayFree(DLTensorHandle handle) nogil
124141
int TVMArrayCopyFromTo(DLTensorHandle src,
125142
DLTensorHandle to,
126-
TVMStreamHandle stream)
143+
TVMStreamHandle stream) nogil
127144
int TVMArrayFromDLPack(DLManagedTensor* arr_from,
128-
DLTensorHandle* out)
145+
DLTensorHandle* out) nogil
129146
int TVMArrayToDLPack(DLTensorHandle arr_from,
130-
DLManagedTensor** out)
147+
DLManagedTensor** out) nogil
131148
void TVMDLManagedTensorCallDeleter(DLManagedTensor* dltensor)
132149
int TVMObjectFree(ObjectHandle obj)
133150
int TVMObjectGetTypeIndex(ObjectHandle obj, unsigned* out_index)
@@ -155,7 +172,8 @@ cdef inline c_str(pystr):
155172
return pystr.encode("utf-8")
156173

157174

158-
cdef inline int CALL(int ret) except -2:
175+
cdef inline int CHECK_CALL(int ret) except -2:
176+
"""Check the return code of the C API function call"""
159177
# -2 brings exception
160178
if ret == -2:
161179
return -2
@@ -193,6 +211,6 @@ cdef _init_env_api():
193211
#
194212
# When the functions are not registered, the signals will be handled
195213
# only when the FFI function returns.
196-
CALL(TVMBackendRegisterEnvCAPI(c_str("PyErr_CheckSignals"), <void*>PyErr_CheckSignals))
214+
CHECK_CALL(TVMBackendRegisterEnvCAPI(c_str("PyErr_CheckSignals"), <void*>PyErr_CheckSignals))
197215

198216
_init_env_api()

python/tvm/_ffi/_cython/ndarray.pxi

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ cdef void _c_dlpack_deleter(object pycaps):
3131
def _from_dlpack(object dltensor):
3232
cdef DLManagedTensor* ptr
3333
cdef DLTensorHandle chandle
34+
cdef int c_api_ret_code
3435
if pycapsule.PyCapsule_IsValid(dltensor, _c_str_dltensor):
3536
ptr = <DLManagedTensor*>pycapsule.PyCapsule_GetPointer(dltensor, _c_str_dltensor)
36-
CALL(TVMArrayFromDLPack(ptr, &chandle))
37+
with nogil:
38+
c_api_ret_code = TVMArrayFromDLPack(ptr, &chandle)
39+
CHECK_CALL(c_api_ret_code)
3740
# set name and destructor to be empty
3841
pycapsule.PyCapsule_SetDestructor(dltensor, NULL)
3942
pycapsule.PyCapsule_SetName(dltensor, _c_str_used_dltensor)
@@ -82,12 +85,18 @@ cdef class NDArrayBase:
8285
self.c_is_view = is_view
8386

8487
def __dealloc__(self):
88+
cdef int c_api_ret_code
8589
if self.c_is_view == 0:
86-
CALL(TVMArrayFree(self.chandle))
90+
with nogil:
91+
c_api_ret_code = TVMArrayFree(self.chandle)
92+
CHECK_CALL(c_api_ret_code)
8793

8894
def _copyto(self, target_nd):
8995
"""Internal function that implements copy to target ndarray."""
90-
CALL(TVMArrayCopyFromTo(self.chandle, (<NDArrayBase>target_nd).chandle, NULL))
96+
cdef int c_api_ret_code
97+
with nogil:
98+
c_api_ret_code = TVMArrayCopyFromTo(self.chandle, (<NDArrayBase>target_nd).chandle, NULL)
99+
CHECK_CALL(c_api_ret_code)
91100
return target_nd
92101

93102
def to_dlpack(self):
@@ -98,9 +107,12 @@ cdef class NDArrayBase:
98107
dlpack : DLPack tensor view of the array data
99108
"""
100109
cdef DLManagedTensor* dltensor
110+
cdef int c_api_ret_code
101111
if self.c_is_view != 0:
102112
raise ValueError("to_dlpack do not work with memory views")
103-
CALL(TVMArrayToDLPack(self.chandle, &dltensor))
113+
with nogil:
114+
c_api_ret_code = TVMArrayToDLPack(self.chandle, &dltensor)
115+
CHECK_CALL(c_api_ret_code)
104116
return pycapsule.PyCapsule_New(dltensor, _c_str_dltensor, _c_dlpack_deleter)
105117

106118

python/tvm/_ffi/_cython/object.pxi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ cdef inline object make_ret_object(void* chandle):
3838
cdef object handle
3939
object_type = OBJECT_TYPE
4040
handle = ctypes_handle(chandle)
41-
CALL(TVMObjectGetTypeIndex(chandle, &tindex))
41+
CHECK_CALL(TVMObjectGetTypeIndex(chandle, &tindex))
4242

4343
if tindex < len(OBJECT_TYPE):
4444
cls = OBJECT_TYPE[tindex]
@@ -101,7 +101,7 @@ cdef class ObjectBase:
101101
self._set_handle(value)
102102

103103
def __dealloc__(self):
104-
CALL(TVMObjectFree(self.chandle))
104+
CHECK_CALL(TVMObjectFree(self.chandle))
105105

106106
def __init_handle_by_constructor__(self, fconstructor, *args):
107107
"""Initialize the handle by calling constructor function.

python/tvm/_ffi/_cython/packed_func.pxi

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ cdef int tvm_callback(TVMValue* args,
4646
tcode == kTVMNDArrayHandle or
4747
tcode == kTVMObjectRefArg or
4848
tcode > kTVMExtBegin):
49-
CALL(TVMCbArgToReturn(&value, &tcode))
49+
CHECK_CALL(TVMCbArgToReturn(&value, &tcode))
5050

5151
if tcode != kTVMDLTensorHandle:
5252
pyargs.append(make_ret(value, tcode))
@@ -64,7 +64,7 @@ cdef int tvm_callback(TVMValue* args,
6464
raise ValueError("PackedFunction can only support one return value")
6565
temp_args = []
6666
make_arg(rv, &value, &tcode, temp_args)
67-
CALL(TVMCFuncSetReturn(ret, &value, &tcode, 1))
67+
CHECK_CALL(TVMCFuncSetReturn(ret, &value, &tcode, 1))
6868
return 0
6969

7070

@@ -90,10 +90,10 @@ def convert_to_tvm_func(object pyfunc):
9090
"""
9191
cdef TVMPackedFuncHandle chandle
9292
Py_INCREF(pyfunc)
93-
CALL(TVMFuncCreateFromCFunc(tvm_callback,
94-
<void*>(pyfunc),
95-
tvm_callback_finalize,
96-
&chandle))
93+
CHECK_CALL(TVMFuncCreateFromCFunc(tvm_callback,
94+
<void*>(pyfunc),
95+
tvm_callback_finalize,
96+
&chandle))
9797
return make_packed_func(chandle, False)
9898

9999

@@ -243,15 +243,20 @@ cdef inline int FuncCall3(void* chandle,
243243
temp_args = []
244244
for i in range(nargs):
245245
make_arg(args[i], &values[i], &tcodes[i], temp_args)
246-
CALL(TVMFuncCall(chandle, &values[0], &tcodes[0],
247-
nargs, ret_val, ret_tcode))
246+
247+
with nogil:
248+
c_api_ret_code = TVMFuncCall(chandle, &values[0], &tcodes[0],
249+
nargs, ret_val, ret_tcode)
250+
251+
CHECK_CALL(c_api_ret_code)
248252
return 0
249253

250254
cdef inline int FuncCall(void* chandle,
251255
tuple args,
252256
TVMValue* ret_val,
253257
int* ret_tcode) except -1:
254258
cdef int nargs
259+
cdef int c_api_ret_code
255260
nargs = len(args)
256261
if nargs <= 3:
257262
FuncCall3(chandle, args, nargs, ret_val, ret_tcode)
@@ -264,8 +269,11 @@ cdef inline int FuncCall(void* chandle,
264269
temp_args = []
265270
for i in range(nargs):
266271
make_arg(args[i], &values[i], &tcodes[i], temp_args)
267-
CALL(TVMFuncCall(chandle, &values[0], &tcodes[0],
268-
nargs, ret_val, ret_tcode))
272+
273+
with nogil:
274+
c_api_ret_code = TVMFuncCall(chandle, &values[0], &tcodes[0],
275+
nargs, ret_val, ret_tcode)
276+
CHECK_CALL(c_api_ret_code)
269277
return 0
270278

271279

@@ -314,7 +322,7 @@ cdef class PackedFuncBase:
314322

315323
def __dealloc__(self):
316324
if self.is_global == 0:
317-
CALL(TVMFuncFree(self.chandle))
325+
CHECK_CALL(TVMFuncFree(self.chandle))
318326

319327
def __call__(self, *args):
320328
cdef TVMValue ret_val
@@ -326,7 +334,7 @@ cdef class PackedFuncBase:
326334

327335
def _get_global_func(name, allow_missing):
328336
cdef TVMPackedFuncHandle chandle
329-
CALL(TVMFuncGetGlobal(c_str(name), &chandle))
337+
CHECK_CALL(TVMFuncGetGlobal(c_str(name), &chandle))
330338
if chandle != NULL:
331339
return make_packed_func(chandle, True)
332340

python/tvm/testing/popen_pool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
# under the License.
1717
# pylint: disable=invalid-name, missing-function-docstring
1818
"""Common functions for popen_pool test cases"""
19-
import time
2019
import tvm
20+
from . import _ffi_api
2121

2222
TEST_GLOBAL_STATE_1 = 0
2323
TEST_GLOBAL_STATE_2 = 0
@@ -72,4 +72,4 @@ def slow_summation(n):
7272

7373

7474
def timeout_job(n):
75-
time.sleep(n * 1.5)
75+
_ffi_api.sleep_in_ffi(n * 1.5)

src/support/ffi_testing.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <tvm/te/tensor.h>
2929
#include <tvm/tir/expr.h>
3030

31+
#include <chrono>
3132
#include <thread>
3233

3334
namespace tvm {
@@ -159,4 +160,9 @@ runtime::Module NewFrontendTestModule() {
159160

160161
TVM_REGISTER_GLOBAL("testing.FrontendTestModule").set_body_typed(NewFrontendTestModule);
161162

163+
TVM_REGISTER_GLOBAL("testing.sleep_in_ffi").set_body_typed([](double timeout) {
164+
std::chrono::duration<int64_t, std::nano> duration(static_cast<int64_t>(timeout * 1e9));
165+
std::this_thread::sleep_for(duration);
166+
});
167+
162168
} // namespace tvm

0 commit comments

Comments
 (0)