blob: f613fda5291ab3a196cb15c642b671f84bf5ee0c [file]
# 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.
import ctypes
import threading
import os
from numbers import Integral, Real
from typing import Any, Callable
if os.environ.get("TVM_FFI_BUILD_DOCS", "0") == "0":
try:
# optionally import torch and setup torch related utils
import torch
except ImportError:
torch = None
try:
# optionally import numpy
import numpy
except ImportError:
numpy = None
try:
from cuda.bindings import driver as cuda_driver
except ImportError:
cuda_driver = None
else:
torch = None
numpy = None
cuda_driver = None
cdef int _RELEASE_GIL_BY_DEFAULT = int(
os.environ.get("TVM_FFI_RELEASE_GIL_BY_DEFAULT", "1")
)
cdef inline object make_ret_small_str(TVMFFIAny result):
"""convert small string to return value."""
cdef TVMFFIByteArray bytes
bytes = TVMFFISmallBytesGetContentByteArray(&result)
return bytearray_to_str(&bytes)
cdef inline object make_ret_small_bytes(TVMFFIAny result):
"""convert small bytes to return value."""
cdef TVMFFIByteArray bytes
bytes = TVMFFISmallBytesGetContentByteArray(&result)
return bytearray_to_bytes(&bytes)
cdef inline object make_ret(TVMFFIAny result, const DLPackExchangeAPI* c_ctx_dlpack_api = NULL):
"""convert result to return value."""
cdef int32_t type_index
type_index = result.type_index
if type_index == kTVMFFITensor:
# specially handle Tensor as it needs a special dltensor field
return make_tensor_from_any(result, c_ctx_dlpack_api)
elif type_index == kTVMFFIOpaquePyObject:
return make_ret_opaque_object(result)
elif type_index >= kTVMFFIStaticObjectBegin:
obj = make_ret_object(result)
if c_ctx_dlpack_api != NULL and isinstance(obj, CContainerBase):
(<CContainerBase>obj)._dlpack_exchange_api = c_ctx_dlpack_api
return obj
# the following code should be optimized to switch case
if type_index == kTVMFFINone:
return None
elif type_index == kTVMFFIBool:
return bool(result.v_int64)
elif type_index == kTVMFFIInt:
return result.v_int64
elif type_index == kTVMFFIFloat:
return result.v_float64
elif type_index == kTVMFFISmallStr:
return make_ret_small_str(result)
elif type_index == kTVMFFISmallBytes:
return make_ret_small_bytes(result)
elif type_index == kTVMFFIOpaquePtr:
return ctypes_handle(result.v_ptr)
elif type_index == kTVMFFIDataType:
return make_ret_dtype(result)
elif type_index == kTVMFFIDevice:
return make_ret_device(result)
elif type_index == kTVMFFIDLTensorPtr:
return make_ret_dltensor(result)
elif type_index == kTVMFFIObjectRValueRef:
raise ValueError("Return value cannot be ObjectRValueRef")
elif type_index == kTVMFFIByteArrayPtr:
raise ValueError("Return value cannot be ByteArrayPtr")
elif type_index == kTVMFFIRawStr:
raise ValueError("Return value cannot be RawStr")
raise ValueError("Unhandled type index %d" % type_index)
# ----------------------------------------------------------------------------
# Helper to simplify calling constructor
# ----------------------------------------------------------------------------
cdef inline int ConstructorCall(void* constructor_handle,
PyObject* py_arg_tuple,
void** handle,
TVMFFIPyCallContext* parent_ctx) except -1:
"""Call contructor of a handle function"""
cdef TVMFFIAny result
cdef int c_api_ret_code
# IMPORTANT: caller need to initialize result->type_index to kTVMFFINone
result.type_index = kTVMFFINone
result.v_int64 = 0
TVMFFIPyConstructorCall(
constructor_handle, py_arg_tuple, &result, &c_api_ret_code,
parent_ctx
)
CHECK_CALL(c_api_ret_code)
handle[0] = result.v_ptr
return 0
# ----------------------------------------------------------------------------
# Implementation of setters using same naming style as TVMFFIPyArgSetterXXX_
# ----------------------------------------------------------------------------
cdef int TVMFFIPyArgSetterTensor_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* arg, TVMFFIAny* out
) except -1:
if (<CObject>arg).chandle != NULL:
out.type_index = kTVMFFITensor
out.v_ptr = (<Tensor>arg).chandle
else:
out.type_index = kTVMFFIDLTensorPtr
out.v_ptr = (<Tensor>arg).cdltensor
return 0
cdef int TVMFFIPyArgSetterObject_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* arg, TVMFFIAny* out
) except -1:
out.type_index = TVMFFIObjectGetTypeIndex((<CObject>arg).chandle)
out.v_ptr = (<CObject>arg).chandle
return 0
cdef int TVMFFIPyArgSetterContainerObject_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* arg, TVMFFIAny* out
) except -1:
"""Setter for container objects (Array, List, Map, Dict).
Propagates DLPack exchange API tag and scans for stream context.
"""
cdef TVMFFIAny scan_args[1]
cdef TVMFFIAny scan_result
cdef void* stream = NULL
out.type_index = TVMFFIObjectGetTypeIndex((<CObject>arg).chandle)
out.v_ptr = (<CObject>arg).chandle
cdef const DLPackExchangeAPI* api = (<CContainerBase>arg)._dlpack_exchange_api
if api != NULL:
if ctx.dlpack_c_exchange_api == NULL:
ctx.dlpack_c_exchange_api = api
if ctx.device_type == -1 and api.current_work_stream != NULL:
# Call C++ to find the first non-CPU tensor device in one shot.
scan_args[0].type_index = out.type_index
scan_args[0].v_obj = <TVMFFIObject*>(<CObject>arg).chandle
scan_result.type_index = kTVMFFINone
scan_result.v_int64 = 0
CHECK_CALL(TVMFFIFunctionCall(
(<CObject>_FFI_CONTAINER_FIND_FIRST_NON_CPU_DEVICE).chandle,
scan_args, 1, &scan_result))
if scan_result.type_index == kTVMFFIDevice and scan_result.v_device.device_type != kDLCPU:
ctx.device_type = scan_result.v_device.device_type
ctx.device_id = scan_result.v_device.device_id
api.current_work_stream(
scan_result.v_device.device_type,
scan_result.v_device.device_id,
&stream)
ctx.stream = <TVMFFIStreamHandle>stream
return 0
cdef int TVMFFIPyArgSetterDLPackExchangeAPI_(
TVMFFIPyArgSetter* this, TVMFFIPyCallContext* ctx,
PyObject* arg, TVMFFIAny* out
) except -1:
cdef DLManagedTensorVersioned* temp_managed_tensor
cdef TVMFFIObjectHandle temp_chandle
cdef void* current_stream = NULL
cdef const DLPackExchangeAPI* exchange_api = this.dlpack_c_exchange_api
# Set the exchange API in context
ctx.dlpack_c_exchange_api = exchange_api
# Convert PyObject to DLPack using the struct's function pointer
if exchange_api.managed_tensor_from_py_object_no_sync(arg, &temp_managed_tensor) != 0:
return -1
# Query current stream from producer if device is not CPU
if temp_managed_tensor.dl_tensor.device.device_type != kDLCPU:
if ctx.device_type == -1 and exchange_api.current_work_stream != NULL:
# First time seeing a device, query the stream
if exchange_api.current_work_stream(
temp_managed_tensor.dl_tensor.device.device_type,
temp_managed_tensor.dl_tensor.device.device_id,
&current_stream
) == 0:
ctx.stream = <TVMFFIStreamHandle>current_stream
ctx.device_type = temp_managed_tensor.dl_tensor.device.device_type
ctx.device_id = temp_managed_tensor.dl_tensor.device.device_id
# Convert to TVM Tensor
if TVMFFITensorFromDLPackVersioned(temp_managed_tensor, 0, 0, &temp_chandle) != 0:
# recycle the managed tensor to avoid leak
if temp_managed_tensor.deleter != NULL:
temp_managed_tensor.deleter(temp_managed_tensor)
raise BufferError("Failed to convert DLManagedTensorVersioned to ffi.Tensor")
out.type_index = kTVMFFITensor
out.v_ptr = temp_chandle
TVMFFIPyPushTempFFIObject(ctx, temp_chandle)
return 0
cdef int TorchManagedTensorToPyObjectNoSyncFallback_(
DLManagedTensorVersioned* dltensor, void** py_obj_out
) except -1:
# a bit convoluted but ok as a fallback
cdef TVMFFIObjectHandle temp_chandle
if TVMFFITensorFromDLPackVersioned(dltensor, 0, 0, &temp_chandle) != 0:
return -1
tensor = make_tensor_from_chandle(temp_chandle)
torch_tensor = torch.from_dlpack(tensor)
Py_INCREF(torch_tensor)
py_obj_out[0] = <void*>(<PyObject*>torch_tensor)
return 0
cdef inline const DLPackExchangeAPI* GetTorchFallbackExchangeAPI() noexcept:
global _torch_fallback_exchange_api
_torch_fallback_exchange_api.header.version.major = DLPACK_MAJOR_VERSION
_torch_fallback_exchange_api.header.version.minor = DLPACK_MINOR_VERSION
_torch_fallback_exchange_api.header.prev_api = NULL
_torch_fallback_exchange_api.managed_tensor_allocator = NULL
_torch_fallback_exchange_api.managed_tensor_from_py_object_no_sync = NULL
_torch_fallback_exchange_api.managed_tensor_to_py_object_no_sync = (
TorchManagedTensorToPyObjectNoSyncFallback_
)
_torch_fallback_exchange_api.dltensor_from_py_object_no_sync = NULL
_torch_fallback_exchange_api.current_work_stream = NULL
return &_torch_fallback_exchange_api
# Static storage for the fallback exchange API
cdef DLPackExchangeAPI _torch_fallback_exchange_api
cdef int TVMFFIPyArgSetterTorchFallback_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Current setter for torch.Tensor, go through python and not as fast as c exporter"""
# TODO(tqchen): remove this once torch always support fast DLPack importer
cdef object arg = <object>py_arg
cdef long long temp_ptr
is_cuda = arg.is_cuda
arg = from_dlpack(torch.utils.dlpack.to_dlpack(arg))
out.type_index = kTVMFFITensor
out.v_ptr = (<Tensor>arg).chandle
temp_dltensor = TVMFFITensorGetDLTensorPtr((<Tensor>arg).chandle)
ctx.dlpack_c_exchange_api = GetTorchFallbackExchangeAPI()
# record the stream and device for torch context
if is_cuda and ctx.device_type == -1:
ctx.device_type = temp_dltensor.device.device_type
ctx.device_id = temp_dltensor.device.device_id
# This is an API that dynamo and other uses to get the raw stream from torch
temp_ptr = torch._C._cuda_getCurrentRawStream(temp_dltensor.device.device_id)
ctx.stream = <TVMFFIStreamHandle>temp_ptr
# push to temp and clear the handle
TVMFFIPyPushTempPyObject(ctx, <PyObject*>arg)
return 0
cdef int TVMFFIPyArgSetterDLPack_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for __dlpack__ mechanism through python, not as fast as c exporter"""
cdef TVMFFIObjectHandle temp_chandle
cdef object arg = <object>py_arg
_from_dlpack_universal(arg, 0, 0, &temp_chandle)
out.type_index = kTVMFFITensor
out.v_ptr = temp_chandle
# record the stream from the source framework context when possible
temp_dltensor = TVMFFITensorGetDLTensorPtr(temp_chandle)
if (temp_dltensor.device.device_type != kDLCPU and ctx.device_type != -1):
# __tvm_ffi_env_stream__ returns the expected stream that should be set
# through TVMFFIEnvSetStream when calling a TVM FFI function
if hasattr(arg, "__tvm_ffi_env_stream__"):
# Ideally projects should directly setup their stream context API
# write through by also calling TVMFFIEnvSetStream
# so we do not need this protocol to do exchange
ctx.device_type = temp_dltensor.device.device_type
ctx.device_id = temp_dltensor.device.device_id
temp_ptr= arg.__tvm_ffi_env_stream__()
ctx.stream = <TVMFFIStreamHandle>temp_ptr
TVMFFIPyPushTempFFIObject(ctx, temp_chandle)
return 0
cdef int TVMFFIPyArgSetterIntegral_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for Integral"""
cdef object arg = <object>py_arg
out.type_index = kTVMFFIInt
# keep it in cython so it will also check for fallback cases
# where the arg is not exactly the int class
out.v_int64 = <long long>arg
return 0
cdef int TVMFFIPyArgSetterReal_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for Real"""
cdef object arg = <object>py_arg
out.type_index = kTVMFFIFloat
# keep it in cython so it will also check for fallback cases
# where the arg is not exactly the float class
out.v_float64 = <double>arg
return 0
cdef int TVMFFIPyArgSetterFFIObjectProtocol_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for objects that implement the `__tvm_ffi_object__` protocol."""
cdef object arg = <object>py_arg
cdef TVMFFIObjectHandle temp_chandle
cdef CObject obj = arg.__tvm_ffi_object__()
cdef long ref_count = Py_REFCNT(obj)
temp_chandle = obj.chandle
out.type_index = TVMFFIObjectGetTypeIndex(temp_chandle)
out.v_ptr = temp_chandle
if ref_count == 1:
# keep alive the tensor, since the tensor is temporary
# and will be freed after we exit here
TVMFFIObjectIncRef(temp_chandle)
TVMFFIPyPushTempFFIObject(ctx, temp_chandle)
return 0
cdef int TVMFFIPyArgSetterCUDAStreamProtocol_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for cuda stream protocol"""
cdef object arg = <object>py_arg
# cuda stream is a subclass of str, so this check occur before str
cdef tuple cu_stream_tuple = arg.__cuda_stream__()
cdef long long long_ptr = <long long>cu_stream_tuple[1]
out.type_index = kTVMFFIOpaquePtr
out.v_ptr = <void*>long_ptr
return 0
cdef int TVMFFIPyArgSetterCUDADriverStreamFallback_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for cuda.bindings.driver.CUstream as a fallback without __cuda_stream__ protocol"""
cdef object arg = <object>py_arg
# call driver stream
cdef long long long_ptr = int(arg)
out.type_index = kTVMFFIOpaquePtr
out.v_ptr = <void*>long_ptr
return 0
cdef int TVMFFIPyArgSetterDType_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for dtype"""
cdef object arg = <object>py_arg
# dtype is a subclass of str, so this check occur before str
arg = arg._tvm_ffi_dtype
out.type_index = kTVMFFIDataType
out.v_dtype = (<DataType>arg).cdtype
return 0
cdef int TVMFFIPyArgSetterDevice_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for device"""
cdef object arg = <object>py_arg
out.type_index = kTVMFFIDevice
out.v_device = (<Device>arg).cdevice
return 0
cdef int TVMFFIPyArgSetterDLPackDeviceProtocol_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for dlpack device protocol"""
cdef object arg = <object>py_arg
cdef tuple dlpack_device = arg.__dlpack_device__()
out.type_index = kTVMFFIDevice
out.v_device = TVMFFIDLDeviceFromIntPair(
<int32_t>dlpack_device[0],
<int32_t>dlpack_device[1]
)
return 0
cdef int TVMFFIPyArgSetterStr_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for str"""
cdef object arg = <object>py_arg
cdef bytes tstr = arg.encode("utf-8")
cdef char* data
cdef Py_ssize_t size
cdef TVMFFIByteArray cdata
PyBytes_AsStringAndSize(tstr, &data, &size)
cdata.data = data
cdata.size = size
CHECK_CALL(TVMFFIStringFromByteArray(&cdata, out))
if out.type_index >= kTVMFFIStaticObjectBegin:
TVMFFIPyPushTempFFIObject(ctx, out.v_ptr)
return 0
cdef int TVMFFIPyArgSetterPyNativeObjectStr_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Specially handle String as its _tvm_ffi_cached_object may be empty"""
cdef object arg = <object>py_arg
# need to check if the arg is a large string returned from ffi
if arg._tvm_ffi_cached_object is not None:
arg = arg._tvm_ffi_cached_object
out.type_index = TVMFFIObjectGetTypeIndex((<CObject>arg).chandle)
out.v_ptr = (<CObject>arg).chandle
return 0
return TVMFFIPyArgSetterStr_(handle, ctx, py_arg, out)
cdef int TVMFFIPyArgSetterBytes_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for bytes"""
cdef object arg = <object>py_arg
if isinstance(arg, bytearray):
arg = bytes(arg)
cdef char* data
cdef Py_ssize_t size
cdef TVMFFIByteArray cdata
PyBytes_AsStringAndSize(arg, &data, &size)
cdata.data = data
cdata.size = size
CHECK_CALL(TVMFFIBytesFromByteArray(&cdata, out))
if out.type_index >= kTVMFFIStaticObjectBegin:
TVMFFIPyPushTempFFIObject(ctx, out.v_ptr)
return 0
cdef int TVMFFIPyArgSetterPyNativeObjectBytes_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Specially handle Bytes as its _tvm_ffi_cached_object may be empty"""
cdef object arg = <object>py_arg
# need to check if the arg is a large bytes returned from ffi
if arg._tvm_ffi_cached_object is not None:
arg = arg._tvm_ffi_cached_object
out.type_index = TVMFFIObjectGetTypeIndex((<CObject>arg).chandle)
out.v_ptr = (<CObject>arg).chandle
return 0
return TVMFFIPyArgSetterBytes_(handle, ctx, py_arg, out)
cdef int TVMFFIPyArgSetterPyNativeObjectGeneral_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Specially handle Object as its _tvm_ffi_cached_object may be empty"""
cdef object arg = <object>py_arg
if arg._tvm_ffi_cached_object is None:
raise ValueError(f"_tvm_ffi_cached_object is None for {type(arg)}")
assert arg._tvm_ffi_cached_object is not None
arg = arg._tvm_ffi_cached_object
out.type_index = TVMFFIObjectGetTypeIndex((<CObject>arg).chandle)
out.v_ptr = (<CObject>arg).chandle
return 0
cdef int TVMFFIPyArgSetterCtypesVoidPtr_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for ctypes.c_void_p"""
out.type_index = kTVMFFIOpaquePtr
out.v_ptr = c_handle(<object>py_arg)
return 0
cdef int TVMFFIPyArgSetterFFIOpaquePtrCompatible_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for objects that implement the `__tvm_ffi_opaque_ptr__` protocol."""
cdef object arg = <object>py_arg
cdef long long long_ptr = <long long>arg.__tvm_ffi_opaque_ptr__()
out.type_index = kTVMFFIOpaquePtr
out.v_ptr = <void*>long_ptr
return 0
cdef int TVMFFIPyArgSetterObjectRValueRef_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for ObjectRValueRef"""
cdef object arg = <object>py_arg
out.type_index = kTVMFFIObjectRValueRef
out.v_ptr = &((<CObject>(arg.obj)).chandle)
return 0
cdef int TVMFFIPyArgSetterCallable_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for Callable"""
cdef TVMFFIObjectHandle chandle
CHECK_CALL(TVMFFIPyConvertPyCallback(py_arg, NULL, &chandle))
out.type_index = TVMFFIObjectGetTypeIndex(chandle)
out.v_ptr = chandle
TVMFFIPyPushTempFFIObject(ctx, chandle)
return 0
cdef int TVMFFIPyArgSetterException_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for Exception"""
cdef object arg = <object>py_arg
arg = _convert_to_ffi_error(arg)
out.type_index = TVMFFIObjectGetTypeIndex((<CObject>arg).chandle)
out.v_ptr = (<CObject>arg).chandle
TVMFFIPyPushTempPyObject(ctx, <PyObject*>arg)
return 0
cdef int TVMFFIPyArgSetterTuple_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for Tuple"""
# recursively construct a new tuple
cdef TVMFFIObjectHandle chandle
ConstructorCall(_CONSTRUCTOR_ARRAY.chandle, py_arg, &chandle, ctx)
out.type_index = TVMFFIObjectGetTypeIndex(chandle)
out.v_ptr = chandle
TVMFFIPyPushTempFFIObject(ctx, chandle)
return 0
cdef int TVMFFIPyArgSetterTupleLike_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for TupleLike"""
# recursively construct a new tuple
cdef tuple tuple_arg = tuple(<object>py_arg)
cdef TVMFFIObjectHandle chandle
ConstructorCall(_CONSTRUCTOR_ARRAY.chandle, <PyObject*>tuple_arg, &chandle, ctx)
out.type_index = TVMFFIObjectGetTypeIndex(chandle)
out.v_ptr = chandle
TVMFFIPyPushTempFFIObject(ctx, chandle)
return 0
cdef int TVMFFIPyArgSetterMap_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for Map"""
# recursively construct a new map
cdef dict dict_arg = <dict>py_arg
cdef list list_kvs = []
for k, v in dict_arg.items():
list_kvs.append(k)
list_kvs.append(v)
cdef tuple_arg_kvs = tuple(list_kvs)
cdef TVMFFIObjectHandle chandle
ConstructorCall(_CONSTRUCTOR_MAP.chandle, <PyObject*>tuple_arg_kvs, &chandle, ctx)
out.type_index = TVMFFIObjectGetTypeIndex(chandle)
out.v_ptr = chandle
TVMFFIPyPushTempFFIObject(ctx, chandle)
return 0
cdef int TVMFFIPyArgSetterObjectConvertible_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for ObjectConvertible"""
# recursively construct a new map
cdef object arg = <object>py_arg
arg = arg.asobject()
out.type_index = TVMFFIObjectGetTypeIndex((<CObject>arg).chandle)
out.v_ptr = (<CObject>arg).chandle
TVMFFIPyPushTempPyObject(ctx, <PyObject*>arg)
cdef int TVMFFIPyArgSetterFallback_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Fallback setter for all other types"""
cdef object arg = <object>py_arg
cdef TVMFFIObjectHandle chandle
_convert_to_opaque_object_handle(arg, &chandle)
out.type_index = kTVMFFIOpaquePyObject
out.v_ptr = chandle
TVMFFIPyPushTempFFIObject(ctx, chandle)
cdef int TVMFFIPyArgSetterDTypeFromTorch_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for torch dtype"""
cdef py_obj = <object>py_arg
if py_obj not in TORCH_DTYPE_TO_DL_DATA_TYPE:
raise ValueError("Unsupported torch dtype: ", py_obj)
out.type_index = kTVMFFIDataType
out.v_dtype = TORCH_DTYPE_TO_DL_DATA_TYPE[py_obj]
return 0
cdef int TVMFFIPyArgSetterDTypeFromNumpy_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for torch dtype"""
cdef py_obj = <object>py_arg
if py_obj not in NUMPY_DTYPE_TO_DL_DATA_TYPE:
raise ValueError("Unsupported numpy or ml_dtypes dtype: ", py_obj)
out.type_index = kTVMFFIDataType
out.v_dtype = NUMPY_DTYPE_TO_DL_DATA_TYPE[py_obj]
return 0
cdef int TVMFFIPyArgSetterDLPackDataTypeProtocol_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for dtype protocol"""
cdef object arg = <object>py_arg
cdef tuple dltype_data_type = arg.__dlpack_data_type__()
out.type_index = kTVMFFIDataType
out.v_dtype.code = <long long>dltype_data_type[0]
out.v_dtype.bits = <long long>dltype_data_type[1]
out.v_dtype.lanes = <long long>dltype_data_type[2]
return 0
cdef int TVMFFIPyArgSetterIntProtocol_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for class with __tvm_ffi_int__() method"""
cdef object arg = <object>py_arg
out.type_index = kTVMFFIInt
out.v_int64 = <long long>(arg.__tvm_ffi_int__())
return 0
cdef int TVMFFIPyArgSetterFloatProtocol_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for class with __tvm_ffi_float__() method"""
cdef object arg = <object>py_arg
out.type_index = kTVMFFIFloat
out.v_float64 = <double>(arg.__tvm_ffi_float__())
return 0
cdef int TVMFFIPyArgSetterFFIValueProtocol_(
TVMFFIPyArgSetter* handle, TVMFFIPyCallContext* ctx,
PyObject* py_arg, TVMFFIAny* out
) except -1:
"""Setter for class with __tvm_ffi_value__() method"""
cdef object arg = <object>py_arg
cdef object ffi_value_py_obj = arg.__tvm_ffi_value__()
cdef PyObject* ffi_value_py_obj_ptr = <PyObject*>ffi_value_py_obj
# keep alive the python object since this is a temporary object
# we must push to extra temp py objects stack to avoid overflow the temp py objects stack
TVMFFIPyPushExtraTempPyObject(ctx, ffi_value_py_obj_ptr)
return TVMFFIPySetArgumentGenericDispatcher(ctx, ffi_value_py_obj_ptr, out)
cdef _DISPATCH_TYPE_KEEP_ALIVE = set()
cdef _DISPATCH_TYPE_KEEP_ALIVE_LOCK = threading.Lock()
cdef public int TVMFFICyArgSetterFactory(PyObject* value, TVMFFIPyArgSetter* out) except -1:
"""
Factory function that creates an argument setter for a given Python argument type.
"""
# NOTE: the order of checks matter here
# becase each argument may satisfy multiple checks
# priortize native types over external types
cdef object arg = <object>value
cdef long long temp_ptr
# The C++ dispatcher dispatches the argument passing by TYPE(obj) pointer which
# is non-owning. This means that there is the following edge case:
# - type A is registered through dispatcher
# - type A gets garbage collected (because it is a local type)
# - type B is created and uses the same memory address as type A
#
# Then when we pass in type B, it will mistakenly use the dispatch function for type A
#
# To prevent this, we keep alive the types that are registered through dispatcher
# by adding them to _DISPATCH_TYPE_KEEP_ALIVE
#
# NOTE that the total number of types that are registered through dispatcher is expected
# to be limited in practice so we can afford to keep them alive
# Lock is used to ensure thread-safety for future thread-free python case
with _DISPATCH_TYPE_KEEP_ALIVE_LOCK:
_DISPATCH_TYPE_KEEP_ALIVE.add(type(arg))
if arg is None:
out.func = TVMFFIPyArgSetterNone_
return 0
if isinstance(arg, Tensor):
out.func = TVMFFIPyArgSetterTensor_
return 0
if isinstance(arg, CContainerBase):
out.func = TVMFFIPyArgSetterContainerObject_
return 0
if isinstance(arg, CObject):
out.func = TVMFFIPyArgSetterObject_
return 0
if isinstance(arg, ObjectRValueRef):
out.func = TVMFFIPyArgSetterObjectRValueRef_
return 0
arg_class = type(arg)
if hasattr(arg_class, "__tvm_ffi_object__"):
# can directly map to tvm ffi object
# usually used for solutions that takes subclass of ffi.Object
# as a member variable
out.func = TVMFFIPyArgSetterFFIObjectProtocol_
return 0
if os.environ.get("TVM_FFI_SKIP_DLPACK_C_EXCHANGE_API", "0") != "1":
# Check for DLPackExchangeAPI struct (new approach)
# This is checked on the CLASS, not the instance
if hasattr(arg_class, "__dlpack_c_exchange_api__"):
out.func = TVMFFIPyArgSetterDLPackExchangeAPI_
_get_dlpack_exchange_api(arg_class.__dlpack_c_exchange_api__, &(out.dlpack_c_exchange_api))
return 0
if hasattr(arg_class, "__cuda_stream__"):
# cuda stream protocol
out.func = TVMFFIPyArgSetterCUDAStreamProtocol_
return 0
if cuda_driver is not None and isinstance(arg, cuda_driver.CUstream):
# TODO(tqchen): remove this once cuda-python supports __cuda_stream__ protocol
out.func = TVMFFIPyArgSetterCUDADriverStreamFallback_
return 0
if torch is not None and isinstance(arg, torch.Tensor):
out.func = TVMFFIPyArgSetterTorchFallback_
return 0
if hasattr(arg_class, "__dlpack__"):
out.func = TVMFFIPyArgSetterDLPack_
return 0
if isinstance(arg, bool):
# A python `bool` is a subclass of `int`, so this check
# must occur before `Integral`.
out.func = TVMFFIPyArgSetterBool_
return 0
if isinstance(arg, Integral):
# must occur before Real check
# cannot simply use TVMFFIPyArgSetterInt
# because Integral may not be exactly the int class
out.func = TVMFFIPyArgSetterIntegral_
return 0
if isinstance(arg, Real):
# cannot simply use TVMFFIPyArgSetterFloat
# because Real may not be exactly the float class
out.func = TVMFFIPyArgSetterReal_
return 0
# dtype is a subclass of str, so this check must occur before str
if isinstance(arg, _CLASS_DTYPE):
out.func = TVMFFIPyArgSetterDType_
return 0
if isinstance(arg, _CLASS_DEVICE):
out.func = TVMFFIPyArgSetterDevice_
return 0
if isinstance(arg, PyNativeObject):
# check for PyNativeObject
# this check must happen before str/bytes/tuple
if isinstance(arg, str):
out.func = TVMFFIPyArgSetterPyNativeObjectStr_
return 0
if isinstance(arg, bytes):
out.func = TVMFFIPyArgSetterPyNativeObjectBytes_
return 0
out.func = TVMFFIPyArgSetterPyNativeObjectGeneral_
return 0
if isinstance(arg, str):
out.func = TVMFFIPyArgSetterStr_
return 0
if isinstance(arg, (bytes, bytearray)):
out.func = TVMFFIPyArgSetterBytes_
return 0
if isinstance(arg, tuple):
out.func = TVMFFIPyArgSetterTuple_
return 0
if isinstance(arg, list):
out.func = TVMFFIPyArgSetterTupleLike_
return 0
if isinstance(arg, dict):
out.func = TVMFFIPyArgSetterMap_
return 0
if isinstance(arg, ctypes.c_void_p):
out.func = TVMFFIPyArgSetterCtypesVoidPtr_
return 0
if hasattr(arg_class, "__tvm_ffi_opaque_ptr__"):
out.func = TVMFFIPyArgSetterFFIOpaquePtrCompatible_
return 0
if callable(arg):
out.func = TVMFFIPyArgSetterCallable_
return 0
if torch is not None and isinstance(arg, torch.dtype):
out.func = TVMFFIPyArgSetterDTypeFromTorch_
return 0
if numpy is not None and isinstance(arg, numpy.dtype):
out.func = TVMFFIPyArgSetterDTypeFromNumpy_
return 0
if hasattr(arg_class, "__dlpack_data_type__"):
# prefer dlpack as it covers all DLDataType struct
out.func = TVMFFIPyArgSetterDLPackDataTypeProtocol_
return 0
if hasattr(arg_class, "__dlpack_device__") and not hasattr(arg_class, "__dlpack__"):
# if a class have __dlpack_device__ but not __dlpack__
# then it is a DLPack device protocol
out.func = TVMFFIPyArgSetterDLPackDeviceProtocol_
return 0
if hasattr(arg_class, "__tvm_ffi_int__"):
out.func = TVMFFIPyArgSetterIntProtocol_
return 0
if hasattr(arg_class, "__tvm_ffi_float__"):
out.func = TVMFFIPyArgSetterFloatProtocol_
return 0
if hasattr(arg_class, "__tvm_ffi_value__"):
out.func = TVMFFIPyArgSetterFFIValueProtocol_
return 0
if isinstance(arg, Exception):
out.func = TVMFFIPyArgSetterException_
return 0
if isinstance(arg, ObjectConvertible):
out.func = TVMFFIPyArgSetterObjectConvertible_
return 0
# default to opaque object
out.func = TVMFFIPyArgSetterFallback_
return 0
# ---------------------------------------------------------------------------------------------
# Implementation of function calling
# ---------------------------------------------------------------------------------------------
cdef class Function(CObject):
"""Callable wrapper around a TVM FFI function.
Instances are obtained by converting Python callables with
:func:`tvm_ffi.convert`, or by looking up globally-registered FFI
functions using :func:`tvm_ffi.get_global_func`.
Examples
--------
.. code-block:: python
@tvm_ffi.register_global_func("my.add")
def add(a, b):
return a + b
f = tvm_ffi.get_global_func("my.add")
assert isinstance(f, tvm_ffi.Function)
assert f(1, 2) == 3
See Also
--------
:py:func:`tvm_ffi.register_global_func`
Register a Python callable as a global FFI function.
:py:func:`tvm_ffi.get_global_func`
Look up a previously registered global FFI function by name.
"""
cdef int c_release_gil
cdef dict __dict__
def __cinit__(self) -> None:
self.c_release_gil = _RELEASE_GIL_BY_DEFAULT
property release_gil:
"""Whether calls release the Python GIL while executing."""
def __get__(self) -> bool:
return self.c_release_gil != 0
def __set__(self, value: bool) -> None:
self.c_release_gil = value
def __call__(self, *args: Any) -> Any:
"""Invoke the wrapped FFI function with ``args``."""
cdef TVMFFIAny result
cdef int c_api_ret_code
cdef const DLPackExchangeAPI* c_ctx_dlpack_api = NULL
# IMPORTANT: caller need to initialize result->type_index to kTVMFFINone
result.type_index = kTVMFFINone
result.v_int64 = 0
TVMFFIPyFuncCall(
(<CObject>self).chandle, <PyObject*>args,
&result,
&c_api_ret_code,
self.release_gil,
&c_ctx_dlpack_api
)
# NOTE: logic is same as check_call
# directly inline here to simplify the resulting trace
if c_api_ret_code == 0:
return make_ret(result, c_ctx_dlpack_api)
# backward compact with error already set case
# TODO(tqchen): remove after we move beyond a few versions.
if c_api_ret_code == -2:
raise raise_existing_error()
# epecial handle env error already set
error = move_from_last_error()
if error.kind == "EnvErrorAlreadySet":
raise raise_existing_error()
raise error.py_error()
@staticmethod
def __from_extern_c__(
c_symbol: int,
*,
keep_alive_object: object | None = None
) -> Function:
"""Convert a function from extern C address.
Parameters
----------
c_symbol : int
Function pointer to the safe call function.
The function pointer must ignore the first argument,
which is the function handle.
keep_alive_object : object
Optional object to be captured and kept alive.
Usually this can be the execution engine that JIT-compiled the function
to ensure we keep the execution environment alive
as long as the function is alive.
Returns
-------
Function
The function object.
"""
cdef TVMFFIObjectHandle chandle
# must first convert to int64_t
cdef int64_t c_symbol_as_long_long = c_symbol
cdef void* safe_call_addr_ptr = <void*>c_symbol_as_long_long
cdef PyObject* closure_py_obj = <PyObject*>keep_alive_object
cdef int ret_code
if keep_alive_object is None:
ret_code = TVMFFIFunctionCreate(
NULL, <TVMFFISafeCallType>safe_call_addr_ptr, NULL,
&chandle
)
else:
# otherwise, we use Python object
Py_INCREF(keep_alive_object)
ret_code = TVMFFIFunctionCreate(
closure_py_obj, <TVMFFISafeCallType>safe_call_addr_ptr, TVMFFIPyObjectDeleter,
&chandle
)
if ret_code != 0:
# cleanup during error handling
Py_DECREF(keep_alive_object)
CHECK_CALL(ret_code)
func = Function.__new__(Function)
(<CObject>func).chandle = chandle
return func
@staticmethod
def __from_mlir_packed_safe_call__(
mlir_packed_symbol: int,
*,
keep_alive_object: object | None = None
) -> Function:
"""Convert a function from MLIR packed safe call function pointer.
Parameters
----------
mlir_packed_symbol : int
Function pointer to the MLIR packed call function
that represents a safe call function.
keep_alive_object : object
Optional object to be captured and kept alive.
Usually this can be the execution engine that JIT-compiled the function
to ensure we keep the execution environment alive
as long as the function is alive.
Returns
-------
Function
The function object.
"""
cdef TVMFFIObjectHandle chandle
# must first convert to int64_t
cdef int64_t c_symbol_as_long_long = mlir_packed_symbol
cdef void* packed_call_addr_ptr = <void*>c_symbol_as_long_long
cdef PyObject* keepalive_py_obj
if keep_alive_object is None:
keepalive_py_obj = NULL
else:
keepalive_py_obj = <PyObject*>keep_alive_object
cdef void* mlir_packed_safe_call = TVMFFIPyMLIRPackedSafeCallCreate(
<void (*)(void**) noexcept>packed_call_addr_ptr,
keepalive_py_obj
)
cdef int ret_code
ret_code = TVMFFIFunctionCreate(
mlir_packed_safe_call,
TVMFFIPyMLIRPackedSafeCallInvoke,
TVMFFIPyMLIRPackedSafeCallDeleter,
&chandle
)
if ret_code != 0:
# cleanup during error handling
TVMFFIPyMLIRPackedSafeCallDeleter(mlir_packed_safe_call)
CHECK_CALL(ret_code)
func = Function.__new__(Function)
(<CObject>func).chandle = chandle
return func
def _register_global_func(name: str, pyfunc: Callable[..., Any] | Function, override: bool) -> Function:
cdef TVMFFIObjectHandle chandle
cdef int c_api_ret_code
cdef int ioverride = override
cdef ByteArrayArg name_arg = ByteArrayArg(c_str(name))
if not isinstance(pyfunc, Function):
pyfunc = _convert_to_ffi_func(pyfunc)
CHECK_CALL(TVMFFIFunctionSetGlobal(name_arg.cptr(), (<CObject>pyfunc).chandle, ioverride))
return pyfunc
def _get_global_func(name: str, allow_missing: bool):
cdef TVMFFIObjectHandle chandle
cdef ByteArrayArg name_arg = ByteArrayArg(c_str(name))
CHECK_CALL(TVMFFIFunctionGetGlobal(name_arg.cptr(), &chandle))
if chandle != NULL:
ret = Function.__new__(Function)
(<CObject>ret).chandle = chandle
return ret
if allow_missing:
return None
raise ValueError("Cannot find global function %s" % name)
cdef inline int _convert_to_opaque_object_handle(
object pyobject, TVMFFIObjectHandle* out_handle
) except -1:
"""Convert a python object to TVM FFI opaque object handle"""
Py_INCREF(pyobject)
CHECK_CALL(TVMFFIObjectCreateOpaque(
<void*>(pyobject),
kTVMFFIOpaquePyObject,
TVMFFIPyObjectDeleter,
out_handle))
return 0
def _convert_to_opaque_object(object pyobject: Any) -> OpaquePyObject:
"""Convert a python object to TVM FFI opaque object"""
cdef TVMFFIObjectHandle chandle
_convert_to_opaque_object_handle(pyobject, &chandle)
ret = OpaquePyObject.__new__(OpaquePyObject)
(<CObject>ret).chandle = chandle
return ret
cdef extern from *:
"""
static void TVMFFITestingCallDeleterWithoutThreadState(void* py_obj) {
PyThreadState* thread_state = PyEval_SaveThread();
TVMFFIPyObjectDeleter(py_obj);
PyEval_RestoreThread(thread_state);
}
"""
void TVMFFITestingCallDeleterWithoutThreadState(void* py_obj)
def _testing_drop_last_ref_without_thread_state() -> None:
"""Drop the last Python ref from a detached-thread-state region."""
cdef object pyobject = {}
cdef PyObject* py_obj = <PyObject*>pyobject
Py_INCREF(pyobject)
pyobject = None
TVMFFITestingCallDeleterWithoutThreadState(<void*>py_obj)
def _print_debug_info() -> None:
"""Get the size of the arg dispatch map"""
cdef size_t size = TVMFFIPyGetArgDispatchMapSize()
print(f"TVMFFIPyGetArgDispatchMapSize: {size}")
cdef Function _OBJECT_FROM_JSON_GRAPH_STR = _get_global_func("ffi.FromJSONGraphString", True)
cdef Function _OBJECT_TO_JSON_GRAPH_STR = _get_global_func("ffi.ToJSONGraphString", True)
cdef Function _CONSTRUCTOR_ARRAY = _get_global_func("ffi.Array", True)
cdef Function _CONSTRUCTOR_MAP = _get_global_func("ffi.Map", True)
cdef Function _FFI_CONTAINER_FIND_FIRST_NON_CPU_DEVICE = _get_global_func("ffi.ContainerFindFirstNonCPUDevice", True)
cdef Function _MAKE_FILED_GETTER = _get_global_func("ffi.MakeFieldGetter", True)
cdef Function _MAKE_FIELD_SETTER = _get_global_func("ffi.MakeFieldSetter", True)
cdef Function _PYCLS_REGISTER = _get_global_func("ffi._PyClassRegisterTypeAttrColumns", True)
MISSING = _get_global_func("ffi.GetInvalidObject", False)()
KWARGS = _get_global_func("ffi.GetKwargsObject", False)()