How to create a new Custom function using vx_amd_custom extension#
When to use Custom OpenVX Node#
The idea of custom Node is to allow the following
To allow the user to plug-in an a kernel which is not yet supported
User wants to write an operator based on a third-party library
User wants to optimize the functionality by providing a their own implementation or wants to fuse functions
In this tutorial, we will walk you through the process of writing, compiling, and loading a vx_amd_custom lib with a new custom function. For demonstration purposes we will provide a CPU and a GPU implementation for the “CustomCopy” kernel. The implementation copies the input data to the output without any modifications.
Prerequisites#
MIVisionX library installed from source
Knowledge of OpenVX and how to add a user defined node
C++ coding
Basic knowledge of CMake
Write the operator definition in custom_lib using predefined APIs in custom_api.h under custom_lib folder#
The Custom shared library (custom_lib) has four main APIs as shown below
// Create Custom Object and retun the handle
customHandle CreateCustom(CustomFunctionType function);
// Setup custom function execution
customStatus_t CustomSetup(customHandle input_handle, customTensorDesc &inputdesc, customTensorDesc &outputdesc, customBackend backend, customStream stream);
// Execute custom function
customStatus_t CustomExecute(customHandle custom_handle, void *input_handle, customTensorDesc &inputdesc, void *output_handle, customTensorDesc &outputdesc);
// Destroy custom execution instance
customStatus_t CustomShutdown(customHandle custom_handle);
In the header file only one function is present which is “Copy” for demonstration.
Write the implementation of the function using the custom_template base class#
custom_base class is an abstract class which exposes the API for the custom_operator implemetation. Basically this has the following declarations
class custom_base
{
protected:
custom_base() {};
public:
virtual ~custom_base() {};
/*!
\param inputdesc => Input tensor desc
\param outputdesc => output tensor desc
\param backend => backend for the impl
\param stream => Output command queue
*/
virtual customStatus_t Setup(customTensorDesc &inputdesc, customTensorDesc &outputdesc, customBackend backend, customStream stream, int num_cpu_threads) = 0;
/*!
\param input_handle => memory handle of input tensor
\param inputdesc => Input tensor desc
\param output_handle => memory handle of output tensor
\param inputdesc => Input tensor desc
*/
virtual customStatus_t Execute(void *input_handle, customTensorDesc &inputdesc, void *output_handle, customTensorDesc &outputdesc) = 0;
//* Shutdown and release resources */
virtual customStatus_t Shutdown() = 0;
};
Add new function types for supporting new custom api#
custom_base * CreateCustomClass(CustomFunctionType function) {
switch(function)
{
case Copy:
return new customCopy();
break;
// todo:: add new custom function types here with corresponding implemetation files
default:
throw std::runtime_error ("Custom function type is unsupported");
return nullptr;
}
}
Provide the implementation of custom function in the derived class for the Operator (here custom_copy_impl.h)#
The function just implements a copy operator which simply copies the content of input_tensor into output_tensor. Below code shows the implementation for both CPU and GPU backend using ROCm HIP implementation.
customStatus_t customCopy::Execute(void *input_handle, customTensorDesc &inputdesc, void *output_handle, customTensorDesc &outputdesc)
{
unsigned size = outputdesc.dims[0] * outputdesc.dims[1] * outputdesc.dims[3] * sizeof(_output_desc.data_type);
unsigned batch_size = outputdesc.dims[3];
if (_backend == customBackend::CPU)
{
int omp_threads = (_cpu_num_threads < batch_size)? _cpu_num_threads: batch_size;
#pragma omp parallel for num_threads(omp_threads)
for (size_t i = 0; i < batch_size; i++) {
unsigned char *src, *dst;
src = (unsigned char *)input_handle + size*i;
dst = (unsigned char *)output_handle + size*i;
memcpy(dst, src, size);
}
}else
{
#if ENABLE_HIP
for (size_t i = 0; i < batch_size; i++) {
unsigned char *src, *dst;
src = (unsigned char *)input_handle + size*i;
dst = (unsigned char *)output_handle + size*i;
hipMemcpy(dst, src, size, hipMemcpyDeviceToDevice);
}
#endif
}
return customStatusSuccess;
}
Link the custom_lib to vx_amd_custom node#
The implementation of vx_amd_custom extension node for OpenVX is pretty staight forward and follows the OpenVX extension specification. More details can be found in the Readme file.
/*! \brief [Graph] Creates a Custom Layer Node.
* \param [in] graph The handle to the graph.
* \param [in] inputs The input tensor data.
* \param [in] function custom funtion enum.
* \param [in] array for user specified custom_parameters.
* \param [out] outputs The output tensor data.
* \return <tt> vx_node</tt>.
* \returns A node reference <tt>\ref vx_node</tt>. Any possible errors preventing a
* successful creation should be checked using <tt>\ref vxGetStatus</tt>.
*/
VX_API_ENTRY vx_node VX_API_CALL vxCustomLayer(vx_graph graph, vx_tensor inputs, vx_enum function, vx_array custom_parameters, vx_tensor outputs);
Adding a new operator type in the custom_lib#
Add enumeration for the new function type in custom_api.h
Add a new class for new custom function in a seperate header file following custom_copy_impl.h
Implement the three functions of the class in a seperate .cpp file following custom_copy_impl.cpp
Invoke the new class by adding it in the CreateCustomClass() custom_api.cpp
Modify CMakeLists.txt to include the implementation file in CUSTOM_LIB_SOURCES
Rebuild MIVisionX with the new custom operator. Voila! The new custom function is ready to work from vx_amd_custom extension
Test the new custom operator using runvx utility#
Sample gdf for runvx using the “Copy” operator can be found under Readme
Modify the gdf approproately to test the new custom functionality