C API Guidelines¶
The C API uses a handle based approach for accessing the Qualcomm® Neural Processing SDK API classes.
Handle Creation
The user can generate a handle for a specific class using the Create function call. The Create call is similar to creating an object in C++ by passing arguments to a constructor. In the below code excerpt, the Snpe_SNPEBuilder_Create function call creates a snpe builder object and returns back a handle, which can then be used by the user to invoke the builder object.
Snpe_SNPEBuilder_Handle_t snpeBuilderHandle = Snpe_SNPEBuilder_Create(containerHandle);
Handle Deletion
The lifecycle of handles created and returned to the user needs to be managed by the user. i.e Every handle created by the user or returned to the user by value needs to be deleted explicitly by the user. The following code excerpt, shows how the Delete call can be invoked by the user
Snpe_IUserBuffer_Handle_t userBufferEncodingFloatHandle = Snpe_UserBufferEncodingFloat_Create();
Snpe_UserBufferEncodingFloat_Delete(userBufferEncodingFloatHandle);
Types of handles
There are three types of handles in the C API:
Handle Created by User: The User can invoke the Create method to generate a handle to access the object for a specific class. The handles generated using the create call needs to be deleted explicitly by the user by calling the corresponding Delete API call.
Snpe_RuntimeList_Handle_t runtimeListHandle = Snpe_RuntimeList_Create(); Snpe_RuntimeList_Delete(runtimeListHandle);
Handle returned by Value: When an API returns back a handle of a different type, the lifecycle of the handle is to be managed by the user. The following excerpt demonstrates the return by value scenario.
Snpe_StringList_Handle_t sl = Snpe_UserBufferMap_GetUserBufferNames(ubMapHandle); Snpe_StringList_Delete(sl);
Handle returned by Reference: When a handle is returned by an API as reference, the handle is implicitly deleted when the parent handle is deleted. So the user does not have to manage the lifecycle in this case. The API returning a handle by ref is denoted by _Ref. In the following code excerpt the ubHandle is deleted when the Delete API is called on the ubMapHandle.
Snpe_IUserBuffer_Handle_t ubHandle = Snpe_UserBufferMap_GetUserBuffer_Ref(ubMapHandle, name); Snpe_UserBufferMap_Delete(ubMapHandle);
Porting C++ APIs to C
The C API functions follow the pattern “Snpe_<ClassName>_<function>(<objectHandle>, …)”. As the C API uses a handle based approach, while translating the C++ APIs, the handle to access the object needs to be passed as an argument. The following code snippets demonstrates the translation of Qualcomm® Neural Processing SDK C++ API to C API.
C++ API calls to create a userbuffer Map:
void createUserBuffer(zdl::DlSystem::UserBufferMap& userBufferMap,
std::unordered_map<std::string, std::vector<uint8_t>>& applicationBuffers,
std::vector<std::unique_ptr<zdl::DlSystem::IUserBuffer>>& snpeUserBackedBuffers,
std::unique_ptr<zdl::SNPE::SNPE>& snpe,
const char * name)
{
// get attributes of buffer by name
auto bufferAttributesOpt = snpe->getInputOutputBufferAttributes(name);
if (!bufferAttributesOpt) throw std::runtime_error(std::string("Error obtaining attributes for input tensor ") + name);
// calculate the size of buffer required by the input tensor
const zdl::DlSystem::TensorShape& bufferShape = (*bufferAttributesOpt)->getDims();
// Calculate the stride based on buffer strides, assuming tightly packed.
// Note: Strides = Number of bytes to advance to the next element in each dimension.
// For example, if a float tensor of dimension 2x4x3 is tightly packed in a buffer of 96 bytes, then the strides would be (48,12,4)
// Note: Buffer stride is usually known and does not need to be calculated.
std::vector<size_t> strides(bufferShape.rank());
strides[strides.size() - 1] = sizeof(float);
size_t stride = strides[strides.size() - 1];
for (size_t i = bufferShape.rank() - 1; i > 0; i--)
{
stride *= bufferShape[i];
strides[i-1] = stride;
}
const size_t bufferElementSize = (*bufferAttributesOpt)->getElementSize();
size_t bufSize = calcSizeFromDims(bufferShape.getDimensions(), bufferShape.rank(), bufferElementSize);
// set the buffer encoding type
zdl::DlSystem::UserBufferEncodingFloat userBufferEncodingFloat;
// create user-backed storage to load input data onto it
applicationBuffers.emplace(name, std::vector<uint8_t>(bufSize));
// create Qualcomm (R) Neural Processing SDK user buffer from the user-backed buffer
zdl::DlSystem::IUserBufferFactory& ubFactory = zdl::SNPE::SNPEFactory::getUserBufferFactory();
snpeUserBackedBuffers.push_back(ubFactory.createUserBuffer(applicationBuffers.at(name).data(),
bufSize,
strides,
&userBufferEncodingFloat));
// add the user-backed buffer to the inputMap, which is later on fed to the network for execution
userBufferMap.add(name, snpeUserBackedBuffers.back().get());
C API calls to create a userbuffer Map:
void createUserBuffer(Snpe_UserBufferMap_Handle_t userBufferMapHandle,
std::unordered_map<std::string, std::vector<uint8_t>>& applicationBuffers,
std::vector<Snpe_IUserBuffer_Handle_t> snpeUserBackedBuffersHandle,
Snpe_SNPE_Handle_t snpeHandle,
const char * name)
{
// get attributes of buffer by name
Snpe_IBufferAttributes_Handle_t bufferAttributesOptHandle = Snpe_SNPE_GetInputOutputBufferAttributes(snpeHandle, name);
if (bufferAttributesOptHandle == nullptr) throw std::runtime_error(std::string("Error obtaining attributes for input tensor ") + name);
// calculate the size of buffer required by the input tensor
Snpe_TensorShape_Handle_t bufferShapeHandle = Snpe_IBufferAttributes_GetDims(bufferAttributesOptHandle);
// Calculate the stride based on buffer strides, assuming tightly packed.
// Note: Strides = Number of bytes to advance to the next element in each dimension.
// For example, if a float tensor of dimension 2x4x3 is tightly packed in a buffer of 96 bytes, then the strides would be (48,12,4)
// Note: Buffer stride is usually known and does not need to be calculated.
std::vector<size_t> strides(Snpe_TensorShape_Rank(bufferShapeHandle));
strides[strides.size() - 1] = sizeof(float);
size_t stride = strides[strides.size() - 1];
for (size_t i = Snpe_TensorShape_Rank(bufferShapeHandle) - 1; i > 0; i--)
{
stride *= Snpe_TensorShape_At(bufferShapeHandle, i);
strides[i-1] = stride;
}
Snpe_TensorShape_Handle_t stridesHandle = Snpe_TensorShape_CreateDimsSize(strides.data(), Snpe_TensorShape_Rank(bufferShapeHandle));
size_t bufferElementSize = Snpe_IBufferAttributes_GetElementSize(bufferAttributesOptHandle);
size_t bufSize = calcSizeFromDims(Snpe_TensorShape_GetDimensions(bufferShapeHandle), Snpe_TensorShape_Rank(bufferShapeHandle), bufferElementSize);
// set the buffer encoding type
Snpe_UserBufferEncoding_Handle_t userBufferEncodingFloatHandle = Snpe_UserBufferEncodingFloat_Create();
// create user-backed storage to load input data onto it
applicationBuffers.emplace(name, std::vector<uint8_t>(bufSize));
// create Qualcomm (R) Neural Processing SDK user buffer from the user-backed buffer
ubsHandle.push_back(Snpe_Util_CreateUserBuffer(applicationBuffers.at(name).data(),
bufSize,
stridesHandle,
userBufferEncodingFloatHandle));
// add the user-backed buffer to the inputMap, which is later on fed to the network for execution
Snpe_UserBufferMap_Add(userBufferMapHandle, name, snpeUserBackedBuffersHandle.back());
// clean up created handles
Snpe_IBufferAttributes_Delete(bufferAttributesOptHandle);
Snpe_UserBufferEncodingFloat_Delete(userBufferEncodingFloatHandle);
Snpe_TensorShape_Delete(bufferShapeHandle);