dfe4cebbda
* [RUNTIME] Improve signal handling in python env. Python execution environment handles the signal by caching the signal a state and invokes the handler when execution goes into the python interpreter. This model can cause problem when runnning a long running c++ function. As keyboard interrupt can only be caught in the end. Additionally, because python registered special signal handlers. Socket operations can return EINTR that needs to be explicitly retried when the interrupt is not a KeyboardInterrupt. This PR adds the following changes to resolve these problems. - Allow execution env(python) to register CheckSignals function to the TVM runtime. - Add runtime::EnvCheckSignals to check the signal error. - Add retry when EINTR is encountered in socket. - Register the python C API functions in cython mode. To testout the EnvCheckSignals, run the following code ```python import tvm.testing tvm.testing.run_check_signal(10) ``` Note that the C API functions are only registered in cython FFI mode because ctypes have problems invoking these functions. This however won't affect the correctness, but will defer the interrupt handling to function return sites. Co-authored-by: Andrew Reusch <areusch@octoml.ai> Co-authored-by: Robert Kimball <bobkimball@gmail.com> * Address comments * Alternative implementation that preserves python exception. * Address comments * Update check signals Co-authored-by: Andrew Reusch <areusch@octoml.ai> Co-authored-by: Robert Kimball <bobkimball@gmail.com>
349 lines
9.0 KiB
Python
349 lines
9.0 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
# coding: utf-8
|
|
# pylint: disable=invalid-name, import-outside-toplevel
|
|
"""Base library for TVM FFI."""
|
|
import sys
|
|
import os
|
|
import ctypes
|
|
import numpy as np
|
|
from . import libinfo
|
|
|
|
# ----------------------------
|
|
# library loading
|
|
# ----------------------------
|
|
string_types = (str,)
|
|
integer_types = (int, np.int32)
|
|
numeric_types = integer_types + (float, np.float32)
|
|
|
|
# this function is needed for python3
|
|
# to convert ctypes.char_p .value back to python str
|
|
if sys.platform == "win32":
|
|
|
|
def _py_str(x):
|
|
try:
|
|
return x.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
encoding = "cp" + str(ctypes.cdll.kernel32.GetACP())
|
|
return x.decode(encoding)
|
|
|
|
py_str = _py_str
|
|
else:
|
|
py_str = lambda x: x.decode("utf-8")
|
|
|
|
|
|
def _load_lib():
|
|
"""Load libary by searching possible path."""
|
|
lib_path = libinfo.find_lib_path()
|
|
# The dll search path need to be added explicitly in
|
|
# windows after python 3.8
|
|
if sys.platform.startswith("win32") and sys.version_info >= (3, 8):
|
|
for path in libinfo.get_dll_directories():
|
|
os.add_dll_directory(path)
|
|
lib = ctypes.CDLL(lib_path[0], ctypes.RTLD_GLOBAL)
|
|
lib.TVMGetLastError.restype = ctypes.c_char_p
|
|
return lib, os.path.basename(lib_path[0])
|
|
|
|
|
|
try:
|
|
# The following import is needed for TVM to work with pdb
|
|
import readline # pylint: disable=unused-import
|
|
except ImportError:
|
|
pass
|
|
|
|
# version number
|
|
__version__ = libinfo.__version__
|
|
# library instance
|
|
_LIB, _LIB_NAME = _load_lib()
|
|
|
|
# Whether we are runtime only
|
|
_RUNTIME_ONLY = "runtime" in _LIB_NAME
|
|
|
|
# The FFI mode of TVM
|
|
_FFI_MODE = os.environ.get("TVM_FFI", "auto")
|
|
|
|
|
|
# ----------------------------
|
|
# helper function in ctypes.
|
|
# ----------------------------
|
|
def c_str(string):
|
|
"""Create ctypes char * from a python string
|
|
Parameters
|
|
----------
|
|
string : string type
|
|
python string
|
|
|
|
Returns
|
|
-------
|
|
str : c_char_p
|
|
A char pointer that can be passed to C API
|
|
"""
|
|
return ctypes.c_char_p(string.encode("utf-8"))
|
|
|
|
|
|
def c_array(ctype, values):
|
|
"""Create ctypes array from a python array
|
|
|
|
Parameters
|
|
----------
|
|
ctype : ctypes data type
|
|
data type of the array we want to convert to
|
|
|
|
values : tuple or list
|
|
data content
|
|
|
|
Returns
|
|
-------
|
|
out : ctypes array
|
|
Created ctypes array
|
|
"""
|
|
return (ctype * len(values))(*values)
|
|
|
|
|
|
def decorate(func, fwrapped):
|
|
"""A wrapper call of decorator package, differs to call time
|
|
|
|
Parameters
|
|
----------
|
|
func : function
|
|
The original function
|
|
|
|
fwrapped : function
|
|
The wrapped function
|
|
"""
|
|
import decorator
|
|
|
|
return decorator.decorate(func, fwrapped)
|
|
|
|
|
|
# -----------------------------------------
|
|
# Base code for structured error handling.
|
|
# -----------------------------------------
|
|
# Maps error type to its constructor
|
|
ERROR_TYPE = {}
|
|
|
|
|
|
class TVMError(RuntimeError):
|
|
"""Default error thrown by TVM functions.
|
|
|
|
TVMError will be raised if you do not give any error type specification,
|
|
"""
|
|
|
|
|
|
def register_error(func_name=None, cls=None):
|
|
"""Register an error class so it can be recognized by the ffi error handler.
|
|
|
|
Parameters
|
|
----------
|
|
func_name : str or function or class
|
|
The name of the error function.
|
|
|
|
cls : function
|
|
The function to create the class
|
|
|
|
Returns
|
|
-------
|
|
fregister : function
|
|
Register function if f is not specified.
|
|
|
|
Examples
|
|
--------
|
|
.. code-block:: python
|
|
|
|
@tvm.error.register_error
|
|
class MyError(RuntimeError):
|
|
pass
|
|
|
|
err_inst = tvm.error.create_ffi_error("MyError: xyz")
|
|
assert isinstance(err_inst, MyError)
|
|
"""
|
|
if callable(func_name):
|
|
cls = func_name
|
|
func_name = cls.__name__
|
|
|
|
def register(mycls):
|
|
"""internal register function"""
|
|
err_name = func_name if isinstance(func_name, str) else mycls.__name__
|
|
ERROR_TYPE[err_name] = mycls
|
|
return mycls
|
|
|
|
if cls is None:
|
|
return register
|
|
return register(cls)
|
|
|
|
|
|
def _valid_error_name(name):
|
|
"""Check whether name is a valid error name."""
|
|
return all(x.isalnum() or x in "_." for x in name)
|
|
|
|
|
|
def _find_error_type(line):
|
|
"""Find the error name given the first line of the error message.
|
|
|
|
Parameters
|
|
----------
|
|
line : str
|
|
The first line of error message.
|
|
|
|
Returns
|
|
-------
|
|
name : str The error name
|
|
"""
|
|
if sys.platform == "win32":
|
|
# Stack traces aren't logged on Windows due to a DMLC limitation,
|
|
# so we should try to get the underlying error another way.
|
|
# DMLC formats errors "[timestamp] file:line: ErrorMessage"
|
|
# ErrorMessage is usually formatted "ErrorType: message"
|
|
# We can try to extract the error type using the final ":"
|
|
end_pos = line.rfind(":")
|
|
if end_pos == -1:
|
|
return None
|
|
start_pos = line.rfind(":", 0, end_pos)
|
|
if start_pos == -1:
|
|
err_name = line[:end_pos].strip()
|
|
else:
|
|
err_name = line[start_pos + 1 : end_pos].strip()
|
|
if _valid_error_name(err_name):
|
|
return err_name
|
|
return None
|
|
|
|
end_pos = line.find(":")
|
|
if end_pos == -1:
|
|
return None
|
|
err_name = line[:end_pos]
|
|
if _valid_error_name(err_name):
|
|
return err_name
|
|
return None
|
|
|
|
|
|
def c2pyerror(err_msg):
|
|
"""Translate C API error message to python style.
|
|
|
|
Parameters
|
|
----------
|
|
err_msg : str
|
|
The error message.
|
|
|
|
Returns
|
|
-------
|
|
new_msg : str
|
|
Translated message.
|
|
|
|
err_type : str
|
|
Detected error type.
|
|
"""
|
|
arr = err_msg.split("\n")
|
|
if arr[-1] == "":
|
|
arr.pop()
|
|
err_type = _find_error_type(arr[0])
|
|
trace_mode = False
|
|
stack_trace = []
|
|
message = []
|
|
for line in arr:
|
|
if trace_mode:
|
|
if line.startswith(" "):
|
|
stack_trace[-1] += "\n" + line
|
|
elif line.startswith(" "):
|
|
stack_trace.append(line)
|
|
else:
|
|
trace_mode = False
|
|
if not trace_mode:
|
|
if line.startswith("Stack trace"):
|
|
trace_mode = True
|
|
else:
|
|
message.append(line)
|
|
out_msg = ""
|
|
if stack_trace:
|
|
out_msg += "Traceback (most recent call last):\n"
|
|
out_msg += "\n".join(reversed(stack_trace)) + "\n"
|
|
out_msg += "\n".join(message)
|
|
return out_msg, err_type
|
|
|
|
|
|
def py2cerror(err_msg):
|
|
"""Translate python style error message to C style.
|
|
|
|
Parameters
|
|
----------
|
|
err_msg : str
|
|
The error message.
|
|
|
|
Returns
|
|
-------
|
|
new_msg : str
|
|
Translated message.
|
|
"""
|
|
arr = err_msg.split("\n")
|
|
if arr[-1] == "":
|
|
arr.pop()
|
|
trace_mode = False
|
|
stack_trace = []
|
|
message = []
|
|
for line in arr:
|
|
if trace_mode:
|
|
if line.startswith(" "):
|
|
stack_trace.append(line)
|
|
else:
|
|
trace_mode = False
|
|
if not trace_mode:
|
|
if line.find("Traceback") != -1:
|
|
trace_mode = True
|
|
else:
|
|
message.append(line)
|
|
# Remove the first error name if there are two of them.
|
|
# RuntimeError: MyErrorName: message => MyErrorName: message
|
|
head_arr = message[0].split(":", 3)
|
|
if len(head_arr) >= 3 and _valid_error_name(head_arr[1].strip()):
|
|
head_arr[1] = head_arr[1].strip()
|
|
message[0] = ":".join(head_arr[1:])
|
|
# reverse the stack trace.
|
|
out_msg = "\n".join(message)
|
|
if stack_trace:
|
|
out_msg += "\nStack trace:\n"
|
|
out_msg += "\n".join(reversed(stack_trace)) + "\n"
|
|
return out_msg
|
|
|
|
|
|
def get_last_ffi_error():
|
|
"""Create error object given result of TVMGetLastError.
|
|
|
|
Returns
|
|
-------
|
|
err : object
|
|
The error object based on the err_msg
|
|
"""
|
|
c_err_msg = py_str(_LIB.TVMGetLastError())
|
|
py_err_msg, err_type = c2pyerror(c_err_msg)
|
|
if err_type is not None and err_type.startswith("tvm.error."):
|
|
err_type = err_type[10:]
|
|
return ERROR_TYPE.get(err_type, TVMError)(py_err_msg)
|
|
|
|
|
|
def check_call(ret):
|
|
"""Check the return value of C API call
|
|
|
|
This function will raise exception when error occurs.
|
|
Wrap every API call with this function
|
|
|
|
Parameters
|
|
----------
|
|
ret : int
|
|
return value from API calls
|
|
"""
|
|
if ret != 0:
|
|
raise get_last_ffi_error()
|