# Sample App Tutorial ## Introduction This tutorial describes how to build a C++ application using QNN APIs that can execute models created using one of the qnn converters on a Linux host or an Android device, and it describes the working of *qnn-sample-app*. Warning The qnn-sample-app is subject to change without notice. *qnn-sample-app* is an example C++ application available with the SDK at **${QNN\_SDK\_ROOT}/examples/QNN/SampleApp**. Where QNN\_SDK\_ROOT is the path to extracted QNN SDK. This tutorial navigates through the source code of *qnn-sample-app* showing the workflow of usage of QNN APIs to execute a model. For creating a C++ application based on QNN APIs, we prescribe the below pattern: 1. [Loading pre-requisite shared libraries](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#loading-pre-requisite-shared-libraries) 2. [Usage of QNN APIs](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#usage-of-qnn-apis) > > > 1. [Use QNN Interface to obtain function pointers](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#use-qnn-interface-to-obtain-function-pointers) > 2. [Set up logging](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#set-up-logging) > 3. [Initialize backend](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#initialize-backend) > 4. [Initialize profiling](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#initialize-profiling) > 5. [Create device](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#create-device) > 6. [Register op packages](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#register-op-packages) > 7. [Create context](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#create-context) > 8. [Prepare graphs](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#prepare-graphs) > 9. [Finalize graphs](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#finalize-graphs) > 10. [Save context into a Binary](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#save-context-into-a-binary) > 11. [Load context from a cached binary](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#load-context-from-a-cached-binary) > 12. [Execute graphs](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#execute-graphs) > 13. [Free context](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#free-context) > 14. [Terminate backend](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#terminate-backend) 3. [Building and running qnn-sample-app](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#building-and-running-qnn-sample-app) > > > 1. [Build](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#build) > 2. [Run](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#run) 4. [Building and running LPAI qnn-sample-app](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#building-and-running-lpai-qnn-sample-app) > > > 1. [Compilation](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#compilation) > 2. [Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#execution) ### Guide for navigating through qnn-sample-app source code - Things to note before we proceed to navigate through the source code: - 1. Any return statement returning a value of type StatusCode::xxxx is a reference to enums named StatusCode containing appropriate return codes. 2. QNN\_xxxx macros are used for logging various debug messages. ## Loading pre-requisite shared libraries QNN SDK provides various shared libraries to access backends, and applications have to load them as needed to execute a network. - A network in QNN can be created in two ways: - 1. It can be directly built into the user application by using QNN APIs. 2. QNN converters can be used to produce a shared library of a QNN network. *qnn-sample-app* makes use of the second option above. This network can be produced using one of the QNN converters available in the SDK, and further be compiled into a shared library using general/tools:qnn-model-lib-generator. Note For Windows users, please replace all ‘.so’ files with the analogous ‘.dll’ file in the following sections. Please refer to Platform Differences for more details. ### Loading a backend Shared libraries for various backends including CPU, GPU, HTP, and DSP are available in the QNN SDK. Every backend that implements QNN APIs exposes all necessary symbols that can be accessed using dynamic loading mechanism. Let’s consider a sample backend shared library named *libQnnSampleBackend.so*, which can be dynamically loaded as shown below: 1 void* libBackendHandle = pal::dynamicloading::dlOpen( 2 "libQnnSampleBackend.so", pal::dynamicloading::DL_NOW | pal::dynamicloading::DL_LOCAL); 3 4 if (nullptr == libBackendHandle) { 5 QNN_ERROR("Unable to load backend. pal::dynamicloading::dlError(): %s", 6 pal::dynamicloading::dlError()); 7 return StatusCode::FAIL_LOAD_BACKEND; 8 } Copy to clipboard To load a model as a shared library, let’s consider a sample model shared library named *libQnnSampleModel.so*, which can be dynamically loaded as shown below: 1 void* libModelHandle = pal::dynamicloading::dlOpen( 2 "libQnnSampleModel.so", pal::dynamicloading::DL_NOW | pal::dynamicloading::DL_LOCAL); 3 4 if (nullptr == libModelHandle) { 5 QNN_ERROR("Unable to load model. pal::dynamicloading::dlError(): %s", 6 pal::dynamicloading::dlError()); 7 return StatusCode::FAIL_LOAD_MODEL; 8 } Copy to clipboard Optionally, to create a context from a cached binary and execute graphs, applications can make use of QnnSystem API to retrieve metadata associated with the context. QnnSystem API can be accessed by loading the *libQnnSystem.so* shared library as shown below: 1 void* systemLibraryHandle = pal::dynamicloading::dlOpen( 2 "libQnnSystem.so", pal::dynamicloading::DL_NOW | pal::dynamicloading::DL_LOCAL); 3 4 if (nullptr == systemLibraryHandle) { 5 QNN_ERROR("Unable to load system library. pal::dynamicloading::dlError(): %s", 6 pal::dynamicloading::dlError()); 7 return StatusCode::FAIL_LOAD_SYSTEM_LIB; 8 } Copy to clipboard ### Resolving symbols in shared libraries After the shared libraries are successfully loaded, we can proceed to resolve all necessary symbols to access QNN APIs. The below code snippet shows a template to resolve a symbol in a shared library: 1 // A generic function to resolve symbols in a library 2 template 3 static inline T resolveSymbol(void* libHandle, const char* symName) { 4 T ptr = (T)pal::dynamicloading::dlSym(libHandle, symName); 5 if (ptr == nullptr) { 6 QNN_ERROR("Unable to access symbol [%s]. pal::dynamicloading::dlError(): %s", symName, pal::dynamicloading::dlError()); 7 } 8 return ptr; 9 } 10 11 // Template for resolving a function of type SampleFnHandleType_t 12 typedef ReturnType_t (*SampleFnHandleType_t)(FunctionParameterTypes_t ...); 13 SampleFnHandleType_t sampleFn = nullptr; 14 sampleFnHandle = resolveSymbol(libBackendHandle, "QnnSample_API"); 15 if (nullptr == sampleFnHandle) { 16 // Error code indicating failure in symbol resolution 17 return StatusCode::FAIL_SYM_FUNCTION; 18 } Copy to clipboard The below code snippet shows an example of how to resolve an actual QNN API: 1 /* Resolve the symbol for Qnn_ErrorHandle_t QnnInterface_getProviders(const QnnInterface_t*** providerList, 2 uint32_t* numProviders) 3 API */ 4 typedef Qnn_ErrorHandle_t (*QnnInterfaceGetProvidersFn_t)(const QnnInterface_t*** providerList, 5 uint32_t* numProviders); 6 7 8 QnnInterfaceGetProvidersFn_t getInterfaceProviders {nullptr}; 9 10 getInterfaceProviders = 11 resolveSymbol(libBackendHandle, "QnnInterface_getProviders"); 12 if (nullptr == getInterfaceProviders) { 13 return StatusCode::FAIL_SYM_FUNCTION; 14 } Copy to clipboard In *qnn-sample-app* source code, all necessary symbols are resolved and stored in a struct of type QnnFunctionPointers shown below: 1typedef struct QnnFunctionPointers { 2 // APIs from model output from converters 3 // QnnModel_composeGraphs 4 ComposeGraphsFnHandleType_t composeGraphsFnHandle; 5 // QnnModel_freeGraphsInfo 6 FreeGraphInfoFnHandleType_t freeGraphInfoFnHandle; 7 // QNN Interface function table containing pointers to all necessary QNN APIs 8 // in a backend 9 QNN_INTERFACE_VER_TYPE qnnInterface; 10 // QNN System Interface function table containing pointers to all QNN System APIs 11 QNN_SYSTEM_INTERFACE_VER_TYPE qnnSystemInterface; 12} QnnFunctionPointers; Copy to clipboard The above structure can be found in **${QNN\_SDK\_ROOT}/examples/QNN/SampleApp/SampleApp/src/SampleApp.hpp**. The rest of the tutorial will assume a variable named *m\_qnnFunctionPointers* of type *QnnFunctionPointers* that contains valid function pointers. ## Usage of QNN APIs This section demonstrates the usage of QNN APIs in a client application. ### Use QNN Interface to obtain function pointers QNN Interface mechanism can be used to set up a table of function pointers to QNN APIs in the backend instead of manually resolving symbols to each and every API, which makes resolving symbols easy. QNN Interface can be used as below: 1QnnInterface_t** interfaceProviders{nullptr}; 2uint32_t numProviders{0}; 3// Query for al available interfaces 4if (QNN_SUCCESS != 5 getInterfaceProviders((const QnnInterface_t***)&interfaceProviders, &numProviders)) { 6 QNN_ERROR("Failed to get interface providers."); 7 return StatusCode::FAIL_GET_INTERFACE_PROVIDERS; 8} 9// Check for validity of returned interfaces 10if (nullptr == interfaceProviders) { 11 QNN_ERROR("Failed to get interface providers: null interface providers received."); 12 return StatusCode::FAIL_GET_INTERFACE_PROVIDERS; 13} 14if (0 == numProviders) { 15 QNN_ERROR("Failed to get interface providers: 0 interface providers."); 16 return StatusCode::FAIL_GET_INTERFACE_PROVIDERS; 17} 18bool foundValidInterface{false}; 19// Loop through all available interface providers and pick the one that suits the current API 20// version 21for (size_t pIdx = 0; pIdx < numProviders; pIdx++) { 22 if (QNN_API_VERSION_MAJOR == interfaceProviders[pIdx]->apiVersion.coreApiVersion.major && 23 QNN_API_VERSION_MINOR <= interfaceProviders[pIdx]->apiVersion.coreApiVersion.minor) { 24 foundValidInterface = true; 25 m_qnnFunctionPointers.qnnInterface = interfaceProviders[pIdx]->QNN_INTERFACE_VER_NAME; 26 break; 27 } 28} 29if (!foundValidInterface) { 30 QNN_ERROR("Unable to find a valid interface."); 31 libBackendHandle = nullptr; 32 return StatusCode::FAIL_GET_INTERFACE_PROVIDERS; 33} Copy to clipboard QNN System Interface can be used to resolve all symbols related to QNN System APIs as shown below: 1 typedef Qnn_ErrorHandle_t (*QnnSystemInterfaceGetProvidersFn_t)( 2 const QnnSystemInterface_t*** providerList, uint32_t* numProviders); 3 4 QnnSystemInterfaceGetProvidersFn_t getSystemInterfaceProviders{nullptr}; 5 getSystemInterfaceProviders = resolveSymbol( 6 systemLibraryHandle, "QnnSystemInterface_getProviders"); 7 if (nullptr == getSystemInterfaceProviders) { 8 return StatusCode::FAIL_SYM_FUNCTION; 9 } 10 QnnSystemInterface_t** systemInterfaceProviders{nullptr}; 11 uint32_t numProviders{0}; 12 if (QNN_SUCCESS != getSystemInterfaceProviders( 13 (const QnnSystemInterface_t***)&systemInterfaceProviders, &numProviders)) { 14 QNN_ERROR("Failed to get system interface providers."); 15 return StatusCode::FAIL_GET_INTERFACE_PROVIDERS; 16 } 17 if (nullptr == systemInterfaceProviders) { 18 QNN_ERROR("Failed to get system interface providers: null interface providers received."); 19 return StatusCode::FAIL_GET_INTERFACE_PROVIDERS; 20 } 21 if (0 == numProviders) { 22 QNN_ERROR("Failed to get interface providers: 0 interface providers."); 23 return StatusCode::FAIL_GET_INTERFACE_PROVIDERS; 24 } 25 bool foundValidSystemInterface{false}; 26 for (size_t pIdx = 0; pIdx < numProviders; pIdx++) { 27 if (QNN_SYSTEM_API_VERSION_MAJOR == systemInterfaceProviders[pIdx]->systemApiVersion.major && 28 QNN_SYSTEM_API_VERSION_MINOR <= systemInterfaceProviders[pIdx]->systemApiVersion.minor) { 29 foundValidSystemInterface = true; 30 m_qnnFunctionPointers->qnnSystemInterface = 31 systemInterfaceProviders[pIdx]->QNN_SYSTEM_INTERFACE_VER_NAME; 32 break; 33 } 34 } Copy to clipboard ### Set up logging Logging can be set up before a backed is initialized and after a backend shared library has been dynamically loaded. To initialize logging, a callback of type *QnnLog\_Callback\_t* has to be defined. An example is defined below: 1 void logStdoutCallback(const char* fmt, 2 QnnLog_Level_t level, 3 uint64_t timestamp, 4 va_list argp) { 5 const char* levelStr = ""; 6 switch (level) { 7 case QNN_LOG_LEVEL_ERROR: 8 levelStr = " ERROR "; 9 break; 10 case QNN_LOG_LEVEL_WARN: 11 levelStr = "WARNING"; 12 break; 13 case QNN_LOG_LEVEL_INFO: 14 levelStr = " INFO "; 15 break; 16 case QNN_LOG_LEVEL_DEBUG: 17 levelStr = " DEBUG "; 18 break; 19 case QNN_LOG_LEVEL_VERBOSE: 20 levelStr = "VERBOSE"; 21 break; 22 case QNN_LOG_LEVEL_MAX: 23 levelStr = "UNKNOWN"; 24 break; 25 } 26 fprintf(stdout, "%8.1fms [%-7s] ", ms, levelStr); 27 vfprintf(stdout, fmt, argp); 28 fprintf(stdout, "\n"); 29 } Copy to clipboard The above callback can be registered with the backend along with a maximum log level. Sample code to initialize with a max log level of QNN\_LOG\_LEVEL\_INFO: 1 Qnn_LogHandle_t logHandle; 2 if (QNN_SUCCESS != 3 m_qnnFunctionPointers.qnnInterface.logCreate(logStdoutCallback, QNN_LOG_LEVEL_INFO, &logHandle)) { 4 QNN_ERROR("Unable to initialize logging in the backend."); 5 return StatusCode::FAILURE; 6 } Copy to clipboard ### Initialize backend Once logging has been successfully initialized, backend can be initialized as shown below: 1 Qnn_BackendHandle_t backendHandle; 2 const QnnBackend_Config_t* backendConfigs; 3 /* Set up any necessary backend configurations */ 4 if (QNN_BACKEND_NO_ERROR != m_qnnFunctionPointers.qnnInterface.backendCreate(logHandle, 5 &backendConfigs, 6 &backendHandle)) { 7 QNN_ERROR("Could not initialize backend"); 8 return StatusCode::FAILURE; 9 } Copy to clipboard ### Initialize Profiling If profiling is desired, after the backend is initialized, a profile handle can be set up. This profile handle can be used at a later point in any API that supports profiling. A profile handle can be created in the backend with basic profiling level as shown below: 1 Qnn_ProfileHandle_t profileHandle; 2 if (QNN_PROFILE_NO_ERROR != m_qnnFunctionPointers.qnnInterface.profileCreate( 3 backendHandle, QNN_PROFILE_LEVEL_BASIC, &profileHandle)) { 4 QNN_WARN("Unable to create profile handle in the backend."); 5 return StatusCode::FAILURE; 6 } Copy to clipboard ### Create device Device can be created as shown below: 1Qnn_DeviceHandle_t deviceHandle {nullptr}; 2const QnnDevice_Config_t* devConfigArray[] = {&devConfig, nullptr}; 3Qnn_ErrorHandle_t ret = m_qnnFunctionPointers.qnnInterface.deviceCreate(logHandle, devConfigArray, &deviceHandle); 4 if (QNN_SUCCESS != ret) { 5 QNN_ERROR("Failed to create device: %u", qnnStatus); 6 return StatusCode::FAILURE; 7 } Copy to clipboard Set **devConfig** as defined here in QNN HTP Backend API ### Register op packages Op packages are way to supply libraries containing ops to backends. They can be registered as shown below: 1 uint32_t opPackageCount; 2 char* opPackagePath[opPackageCount]; 3 char* opPackageInterfaceProvider[opPackageCount]; 4 /* Set up required op package paths and interface providers as necessary */ 5 for(uint32_t idx = 0; idx < opPackageCount; idx++) { 6 if (QNN_BACKEND_NO_ERROR != 7 m_qnnFunctionPointers.qnnInterface.backendRegisterOpPackage(backendHandle, 8 opPackagePath[idx], 9 opPackageInterfaceProvider[idx])) { 10 QNN_ERROR("Could not register Op Package: %s and interface provider: %s", 11 opPackagePath[idx], 12 opPackageInterfaceProvider[idx]); 13 return StatusCode::FAILURE; 14 } 15 } Copy to clipboard ### Create context A context can be created in a backend as shown below: 1 Qnn_ContextHandle_t context; 2 Qnn_DeviceHandle_t deviceHandle {nullptr}; 3 const QnnContext_Config_t* contextConfigs; 4 /* Set up any context configs that are necessary */ 5 if (QNN_CONTEXT_NO_ERROR != 6 m_qnnFunctionPointers.qnnInterface.contextCreate(backendHandle, 7 deviceHandle, 8 &contextConfigs, 9 &context)) { 10 QNN_ERROR("Could not create context"); 11 return StatusCode::FAILURE; 12 } Copy to clipboard ### Prepare graphs *qnn-sample-app* relies on the output from one of the converters to create a QNN network in the backend. *composeGraphsFnHandle* is mapped to *QnnModel\_composeGraphs* API in the model shared library, which takes *qnn\_wrapper\_api::GraphInfo\_t\*\*\** as one of the parameters. The function *composeGraphsFnHandle* will make necessary calls to the backend to create a network(s). It also writes all necessary information, like information about input and output tensors related to the graph, required to execute a graph into the structure *graphsInfo* as shown in the following code block: 1 /* Structure to retrieve information about graphs, like graph name, 2 details about input and output tensors preset in libQnnSampleModel.so */ 3 qnn_wrapper_api::GraphInfo_t** graphsInfo; 4 // No. of graphs present in libQnnSampleModel.so 5 uint32_t graphsCount; 6 // true to enable intermediate outputs, false for network outputs only 7 bool debug; 8 if (qnn_wrapper_api::ModelError_t::MODEL_NO_ERROR != 9 m_qnnFunctionPointers.composeGraphsFnHandle(backendHandle, 10 m_qnnFunctionPointers.qnnInterface, 11 context, 12 &graphsInfo, 13 &graphsCount, 14 debug)) { 15 QNN_ERROR("Failed in composeGraphs()"); 16 return StatusCode::FAILURE; 17 } Copy to clipboard At this point, the context will contain all the graphs that were present in *libQnnSampleModel.so*. ### Finalize Graphs Graphs that were added in the previous step can be finalized as shown below: 1 // information about graphs obtained in the previous step 2 qnn_wrapper_api::GraphInfo_t** graphsInfo; 3 // No. of graphs obtained in the previous step 4 uint32_t graphsCount; 5 /* A valid profile handle if profiling is desired, 6 nullptr if profiling is not needed */ 7 Qnn_ProfileHandle_t profileHandle; 8 9 for (size_t graphIdx = 0; graphIdx < m_graphsCount; graphIdx++) { 10 if (QNN_GRAPH_NO_ERROR != 11 m_qnnFunctionPointers.qnnInterface.graphFinalize( 12 (*graphsInfo)[graphIdx].graph, profileBackendHandle, nullptr)) { 13 return StatusCode::FAILURE; 14 } 15 /* Extract profiling information if desired and if a valid handle was supplied to finalize 16 graphs API */ 17 } Copy to clipboard ### Save context into a binary After all the graphs in a context are finalized, the user application may choose to save the context into a binary for future use. The advantage of saving a context is that it can be retrieved in the future for execution of graphs contained within it without having to finalize them again. This will save considerable time for initialization during execution of a network. The context can be saved as shown below: 1 // Get the expected size of the buffer from the backend in which the context can be saved 2 if (QNN_CONTEXT_NO_ERROR != 3 m_qnnFunctionPointers.qnnInterface.contextGetBinarySize(context, &requiredBufferSize)) { 4 QNN_ERROR("Could not get the required binary size."); 5 return StatusCode::FAILURE; 6 } 7 8 // Allocate a buffer of the required size 9 saveBuffer = (uint8_t*)malloc(requiredBufferSize * sizeof(uint8_t)); 10 if (nullptr == saveBuffer) { 11 QNN_ERROR("Could not allocate buffer to save binary."); 12 return StatusCode::FAILURE; 13 } 14 15 auto status = StatusCode::SUCCESS; 16 uint32_t writtenBufferSize{0}; 17 // Pass the allocated buffer and obtain a copy of the context binary written into the buffer 18 if (QNN_CONTEXT_NO_ERROR != 19 m_qnnFunctionPointers.qnnInterface.contextGetBinary(context, 20 reinterpret_cast(saveBuffer), 21 requiredBufferSize, 22 &writtenBufferSize)) { 23 QNN_ERROR("Could not get binary."); 24 status = StatusCode::FAILURE; 25 } 26 27 // Check if the supplied buffer size is at least as big as the amount of data witten by the backend 28 if (requiredBufferSize < writtenBufferSize) { 29 QNN_ERROR( 30 "Illegal written buffer size [%d] bytes. Cannot exceed allocated memory of [%d] bytes", 31 writtenBufferSize, 32 requiredBufferSize); 33 status = StatusCode::FAILURE; 34 } 35 36 // Use caching utility to save metadata along with the binary buffer from the backend 37 if (status == StatusCode::SUCCESS && 38 tools::datautil::StatusCode::SUCCESS != tools::datautil::writeBinaryToFile(outputPath, 39 saveBinaryName + ".bin", 40 (uint8_t*)saveBuffer, 41 writtenBufferSize)) { 42 QNN_ERROR("Could not serialize to file."); 43 status = StatusCode::FAILURE; 44 } Copy to clipboard ### Load context from a cached binary A context that was saved into a binary, like in the previous step, can be loaded as an alternative to creating a new context every time. The code snippet below demonstrates this step: 1 auto returnStatus = StatusCode::SUCCESS; 2 std::shared_ptr buffer{nullptr}; 3 uint32_t graphsCount {0}; 4 buffer = std::shared_ptr(new uint8_t[bufferSize], std::default_delete()); 5 if (!buffer) { 6 QNN_ERROR("Failed to allocate memory."); 7 return StatusCode::FAILURE; 8 } 9 10 if (tools::datautil::StatusCode::SUCCESS != 11 tools::datautil::readBinaryFromFile( 12 cachedBinaryPath, reinterpret_cast(buffer.get()), bufferSize) 13 QNN_ERROR("Failed to read binary file."); 14 returnStatus = StatusCode::FAILURE; 15 } 16 17 /* Create a QnnSystemContext handle to access system context APIs. */ 18 QnnSystemContext_Handle_t sysCtxHandle{nullptr}; 19 if (QNN_SUCCESS != m_qnnFunctionPointers.qnnSystemInterface.systemContextCreate(&sysCtxHandle)) { 20 QNN_ERROR("Could not create system handle."); 21 returnStatus = StatusCode::FAILURE; 22 } 23 24 /* Retrieve metadata from the context binary through QNN System Context API. */ 25 QnnSystemContext_BinaryInfo_t* binaryInfo{nullptr}; 26 uint32_t binaryInfoSize{0}; 27 if (StatusCode::SUCCESS == returnStatus && 28 QNN_SUCCESS != m_qnnFunctionPointers.qnnSystemInterface.systemContextGetBinaryInfo( 29 sysCtxHandle, 30 static_cast(buffer.get()), 31 bufferSize, 32 &binaryInfo, 33 &binaryInfoSize)) { 34 QNN_ERROR("Failed to get context binary info"); 35 returnStatus = StatusCode::FAILURE; 36 } 37 38 qnn_wrapper_api::GraphInfo_t** graphsInfo; 39 /* Make a copy of the metadata. */ 40 if (StatusCode::SUCCESS == returnStatus && 41 !copyMetadataToGraphsInfo(binaryInfo, graphsInfo, graphsCount)) { 42 QNN_ERROR("Failed to copy metadata."); 43 returnStatus = StatusCode::FAILURE; 44 } 45 46 /* Release resources associated with previously created QnnSystemContext handle. */ 47 m_qnnFunctionPointers.qnnSystemInterface.systemContextFree(sysCtxHandle); 48 sysCtxHandle = nullptr; 49 50 /* readBuffer contains the binary data that was previously obtained from a backend. Pass this 51 cached binary data to the backend to recreate the same context. */ 52 if (StatusCode::SUCCESS == returnStatus && 53 m_qnnFunctionPointers.qnnInterface.contextCreateFromBinary(backendHandle, 54 deviceHandle, 55 (const QnnContext_Config_t**)&contextConfig, 56 reinterpret_cast(readBuffer), 57 bufferSize, 58 &context, 59 profileBackendHandle)) { 60 QNN_ERROR("Could not create context from binary."); 61 returnStatus = StatusCode::FAILURE; 62 } 63 64 // Optionally, extract profiling numbers if desired 65 if (ProfilingLevel::OFF != m_profilingLevel) { 66 extractBackendProfilingInfo(profileBackendHandle); 67 } 68 69 /* Obtain and save graph handles for each graph present in the context based on the saved graph 70 names in the metadata */ 71 if (StatusCode::SUCCESS == returnStatus) { 72 for (size_t graphIdx = 0; graphIdx < m_graphsCount; graphIdx++) { 73 if (QNN_SUCCESS != 74 m_qnnFunctionPointers.qnnInterface.graphRetrieve( 75 context, (*graphsInfo)[graphIdx].graphName, &((*graphsInfo)[graphIdx].graph))) { 76 QNN_ERROR("Unable to retrieve graph handle for graph Idx: %d", graphIdx); 77 returnStatus = StatusCode::FAILURE; 78 } 79 } 80 } Copy to clipboard ### Execute graphs After a context has been created, graphs have been added and finalized, or alternatively, after a context has been retrieved from a binary, one or more graphs in the context can be executed. Executing a graph involves: > > > 1. Setting up input and output tensors. > 2. Populating input data into input tensors. > 3. Calling the execute method in the backend. > 4. Obtaining outputs and saving them. This is demonstrated using the code snippet below: 1 // Select a graph from graphsInfo if there are more than one graph in this context 2 uint32_t graphIdx; 3 QNN_DEBUG("Starting execution for graphIdx: %d", graphIdx); 4 Qnn_Tensor_t* inputs = nullptr; 5 Qnn_Tensor_t* outputs = nullptr; 6 // IOTensor utility is used to set up input and output tensor structures 7 if (iotensor::StatusCode::SUCCESS != 8 ioTensor.setupInputAndOutputTensors(&inputs, &outputs, (*graphsInfo)[graphIdx])) { 9 QNN_ERROR("Error in setting up Input and output Tensors for graphIdx: %d", graphIdx); 10 returnStatus = StatusCode::FAILURE; 11 break; 12 } 13 14 // Grab input raw file paths to read input data 15 auto inputFileList = inputFileLists[graphIdx]; 16 auto graphInfo = (*graphsInfo)[graphIdx]; 17 if (!inputFileList.empty()) { 18 /* *qnn-sample-app* reads data based on the batch size until the whole buffer is filled. 19 If there isn't sufficient data, it pads the rest with zeroes. */ 20 size_t totalCount = inputFileList[0].size(); 21 while (!inputFileList[0].empty()) { 22 size_t startIdx = (totalCount - inputFileList[0].size()); 23 24 // IOTensor utility is used to populate input tensors with input data 25 if (iotensor::StatusCode::SUCCESS != 26 m_ioTensor.populateInputTensors( 27 graphIdx, inputFileList, inputs, graphInfo, inputDataType)) { 28 returnStatus = StatusCode::FAILURE; 29 } 30 31 if (StatusCode::SUCCESS == returnStatus) { 32 // Execute the graph in the backend with optional profile handle 33 QNN_DEBUG("Successfully populated input tensors for graphIdx: %d", graphIdx); 34 Qnn_ErrorHandle_t executeStatus = QNN_GRAPH_NO_ERROR; 35 executeStatus = m_qnnFunctionPointers.qnnInterface.graphExecute(graphInfo.graph, 36 inputs, 37 graphInfo.numInputTensors, 38 outputs, 39 graphInfo.numOutputTensors, 40 profileBackendHandle, 41 nullptr); 42 if (QNN_GRAPH_NO_ERROR != executeStatus) { 43 returnStatus = StatusCode::FAILURE; 44 } 45 if (StatusCode::SUCCESS == returnStatus) { 46 QNN_DEBUG("Successfully executed graphIdx: %d ", graphIdx); 47 // IOTensor utility is used to write output tensors to raw files 48 if (iotensor::StatusCode::SUCCESS != 49 ioTensor.writeOutputTensors(graphIdx, 50 startIdx, 51 graphInfo.graphName, 52 outputs, 53 graphInfo.outputTensors, 54 graphInfo.numOutputTensors, 55 outputDataType, 56 graphsCount, 57 outputPath)) { 58 returnStatus = StatusCode::FAILURE; 59 } 60 } 61 } 62 if (StatusCode::SUCCESS != returnStatus) { 63 QNN_ERROR("Execution of Graph: %d failed!", graphIdx); 64 break; 65 } 66 } 67 } 68 69 // Clean up all the tensors after execution is completed 70 ioTensor.tearDownInputAndOutputTensors( 71 inputs, outputs, graphInfo.numInputTensors, graphInfo.numOutputTensors); 72 inputs = nullptr; 73 outputs = nullptr; 74 if (StatusCode::SUCCESS != returnStatus) { 75 break; 76 } 77 } Copy to clipboard IOTensor is a utility provided with the source code at **${QNN\_SDK\_ROOT}/examples/QNN/SampleApp/SampleApp/src/Utils/IOTensor.cpp**. It exposes a few methods that help with the execution of a graph, which were used in the previous code snippet: > > > 1. *setupInputAndOutputTensors* to set up structures related to input and output tensors. > 2. *populateInputTensors* to copy input data into input tensor structures. > 3. *tearDownInputAndOutputTensors* to clean up resources associated with input and output tensors. Refer to the IOTensor source code for more details about these APIs. ### Free context After all the execution is completed, the context can be freed as shown below: 1 if (QNN_CONTEXT_NO_ERROR != 2 m_qnnFunctionPointers.qnnInterface.contextFree(context, profileBackendHandle)) { 3 QNN_ERROR("Could not free context"); 4 return StatusCode::FAILURE; 5 } Copy to clipboard ### Terminate backend Backend can be terminated as shown below: 1 if (QNN_BACKEND_NO_ERROR != m_qnnFunctionPointers.qnnInterface.backendFree(backendHandle)) { 2 QNN_ERROR("Could not free backend"); 3 return StatusCode::FAILURE; 4 } Copy to clipboard ## Building and running qnn-sample-app ### Setup **Linux** - Building *qnn-sample-app* has two external dependencies: - 1. clang compiler 2. ndk-build (for Android targets only) If the clang compiler is not available in your system **PATH**, the script **${QNN\_SDK\_ROOT}/bin/check-linux-dependency.sh** provided with the SDK can be used to install and prepare your environment. Alternatively, you could install these dependencies and make them available in your PATH. Command to automatically install required dependencies: 1 $ sudo bash ${QNN_SDK_ROOT}/bin/check-linux-dependency.sh Copy to clipboard For the second dependency to be satisfied, ndk-build needs to be set using general/setup:Compiler Toolchains 1 $ ${QNN_SDK_ROOT}/bin/envcheck -n Copy to clipboard *Note: qnn-sample-app has been verified to work with Android NDK version r25c.* **GCC Toolchain** For building *qnn-sample-app* to run on devices with Yocto based OS, gcc compiler is needed. To support Yocto Kirkstone based devices, the SDK libraries are compiled with GCC-11.2. Following section provides steps to acquire the toolchain taking Yocto Kirkstone as an example. If the required compiler is not available in your system **PATH**, please use the below steps to install the dependency and make them available in your PATH. Please follow Qualcomm build guide to generate the eSDK that contains cross compiler toolchain (qcom-wayland-x86\_64-qcom-console-image-armv8-2a-qcm6490-toolchain-ext-0.0.sh) required to build sample application. > > > 1. Steps to build the eSDK are available at [https://docs.qualcomm.com/bundle/publicresource/topics/80-70020-254/how_to.html#generate-an-esdk](https://docs.qualcomm.com/bundle/publicresource/topics/80-70020-254/how_to.html#generate-an-esdk) > > 1. After building the eSDK, qcom-wayland-x86_64-qcom-console-image-armv8-2a-qcm6490-toolchain-ext-0.0.sh will > be generated at <WORKSPACE DIR>/build-qcom-wayland/tmp-glibc/deploy/sdk. > 2. Extract toolchain using ./qcom-wayland-x86_64-qcom-console-image-armv8-2a-qcm6490-toolchain-ext-0.0.sh **Windows** The tutorial assumes general setup instructions have been followed at [Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/general_setup.html). Please use “Developer PowerShell for VS 2022” in the following steps. **Hexagon** - Building *libQnnSampleApp.so* has one external dependencies: - 1. hexagon sdk HEXAGON\_SDK\_ROOT is the path of the Hexagon SDK installation. Refer HTP and DSP to setup hexagon sdk. To setup environment: 1 $ source ${HEXAGON_SDK_ROOT}/setup_sdk_env.source Copy to clipboard ### Build Once the setup is complete, *qnn-sample-app* can be built as follows: **Linux** 1 $ cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp 2 $ make all_x86 all_android Copy to clipboard After executing make from above, you should be able to see two new folders in the same directory: > > > 1. bin: contains *qnn-sample-app* binaries for each platform within respective directories. > 2. obj: contains all the object files that were used for building and linking the executable. To delete all the artifacts that were generated in the above step, run: 1 $ cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp 2 $ make clean Copy to clipboard **Linux (Yocto Based)** For those devices which have Yocto based Linux OS, GCC compiler needs to be used to build the sample source code. To support Yocto Kirkstone based devices, libraries are compiled with gcc11.2. Please refer below steps for building QNN sample app: 1 $ cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp/ 2 $ export QNN_AARCH64_LINUX_OE_GCC_112=/path/to/extracted/toolchain 3 $ make CXX="/tmp/sysroots/x86_64/usr/bin/aarch64-qcom-linux/aarch64-qcom-linux-g++ 4 --sysroot=/tmp/sysroots/qcm6490" all_linux_oe_aarch64_gcc112 Copy to clipboard After executing make from above, you should be able to see two new folders in the same directory: > > > 1. bin: contains *qnn-sample-app* binaries for each platform within respective directories. > 2. obj: contains all the object files that were used for building and linking the executable. To delete all the artifacts that were generated in the above step, run: 1 $ cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp 2 $ make clean Copy to clipboard **Windows** Warning AsyncExecution and MultiCore features are not supported on the Windows platform. Running sample apps with these features might fail at runtime. 1 $ cd $QNN_SDK_ROOT/examples/QNN/SampleApp/SampleApp 2 $ mkdir build 3 $ cd build 4 $ cmake ../ -A [x64, ARM64] 5 $ cmake --build ./ --config Release Copy to clipboard After executing commands from above, you should be able to see $QNN\_SDK\_ROOT/examples/QNN/SampleApp/SampleApp/build/src/Release/qnn-sample-app.exe **Hexagon** Assuming user desired Hexagon architecture version is v69. To build QnnSampleApp for hexagon use below command 1 $ cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp 2 $ make hexagon V=v69 Copy to clipboard After executing make from above, you should be able to see two new folders in the same directory: > > > 1. bin: contains *libQnnSampleAppv69.so* shared library in hexagon directory. > 2. obj: contains all the object files that were used for building and linking the executable. To delete the artifacts that were generated in the above step, run: 1 $ cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp 2 $ make clean_hexagon Copy to clipboard ### Run **Linux** *qnn-sample-app* executable generated in the build step can be used to execute a model using any QNN backend available for *linux-x86\_64* and *aarch64-android*. It is very similar to executing *qnn-net-run*, except when retrieving a context from a cached binary. To retrieve a cached context, qnn-sample-app additionally needs the QNN System library (*libQnnSystem.so*) to extract metadata, and it can be provided through the *–system\_library* option. *libQnnSystem.so* can be found in the SDK for a particular target under *lib/<target>* folder. Refer to further documentation on *qnn-net-run* here. For example, let’s consider execution of the shallow model on CPU backend on a Linux host from [Tutorial 1](https://docs.qualcomm.com/doc/80-63442-10/topic/tutorial1.html). Replacing *qnn-net-run* with *qnn-sample-app* should produce same results: 1$ cd ${QNN_SDK_ROOT}/examples/QNN/converter/models # access input data 2$ ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp/bin/x86_64-linux-clang/qnn-sample-app \ 3 --backend ${QNN_SDK_ROOT}/lib/x86_64-linux-clang/libQnnCpu.so \ 4 --model ${QNN_SDK_ROOT}/examples/QNN/example_libs/x86_64-linux-clang/libqnn_model_float.so \ 5 --input_list ${QNN_SDK_ROOT}/examples/QNN/converter/models/input_list_float.txt \ 6 --op_packages ${QNN_SDK_ROOT}/examples/QNN/OpPackage/CPU/libs/x86_64-linux-clang/libQnnCpuOpPackageExample.so:QnnOpPackage_interfaceProvider Copy to clipboard For more tool help, run: 1 $ qnn-sample-app --help Copy to clipboard **Linux (Yocto Based)** *qnn-sample-app* executable generated in the build step can be used to execute a model using any QNN backend. To support Yocto kirkstone based devices, backends are available for *aarch64-oe-linux-gcc11.2*. To run the executable, please refer same steps as **LINUX** section above. For more tool help, run: 1 $ qnn-sample-app --help Copy to clipboard **Windows** Warning AsyncExecution and MultiCore features are not supported on the Windows platform. Running sample apps with these features might fail at runtime. *qnn-sample-app.exe* executable generated in the build step can be used to execute a model using any QNN backend available for windows-x86\_64 and aarch64-windows platforms. It is very similar to executing qnn-net-run. Refer to the general QNN documentation available at here to see how to run qnn-net-run. Simply replacing qnn-net-run with *qnn-sample-app.exe* in the tutorials should help. For example, let’s consider execution of the Inception\_v3 model on CPU backend on a Windows host from Converting and executing a CNN model with QNN. Replacing *qnn-net-run* with *qnn-sample-app.exe* should produce same results: 1$ & "/bin/envsetup.ps1" 2$ cd $QNN_SDK_ROOT/examples/QNN/converter/models 3$ qnn-sample-app.exe \ 4 --backend QnnCpu.dll \ 5 --model Inception_v3.dll \ 6 --input_list $QNN_SDK_ROOT/examples/QNN/converter/models/input_list_float.txt Copy to clipboard For more tool help, run: 1 $ qnn-sample-app.exe --help Copy to clipboard **Hexagon** *libQnnSampleApp69.so* shared library generated in the build step can be used to execute a model using QNN backend available for same hexagon architecture. It is executed using *run\_main\_on\_hexagon* on device. DEVICE\_PATH refer to path on device where required files are pushed. Push required file on device (Below command are for android device only) 1$ adb push ${HEXAGON_SDK_ROOT}/libs/run_main_on_hexagon/ship/android_aarch64/run_main_on_hexagon /vendor/bin/run_main_on_hexagon 2$ adb push ${HEXAGON_SDK_ROOT}/libs/run_main_on_hexagon/ship/hexagon_toolv87_v69/librun_main_on_hexagon_skel.so /vendor/lib/rfsa/adsp/ 3$ adb push ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp/bin/hexagon/libQnnSampleAppv69.so ${DEVICE_PATH} 4$ adb push ${QNN_SDK_ROOT}/lib/hexagon-v69/unsigned/libQnnHtpV69.so ${DEVICE_PATH} 5$ adb push ${QNN_SDK_ROOT}/lib/hexagon-v69/unsigned/libQnnSystem.so ${DEVICE_PATH} 6$ adb push qnnmodel.serialized.bin ${DEVICE_PATH} 7$ adb push input_list.txt ${DEVICE_PATH} Copy to clipboard Run below command to execute QnnSampleApp on device Note run\_main\_on\_hexagon require specification of the DSP domain on which to offload the program, in Hexagon QnnSampleApp case will use cDSP domain which is expressed by numeric domain id 3 1$ cd /vendor/bin 2$ ./run_main_on_hexagon 3 ${DEVICE_PATH}/libQnnSampleAppv69.so \ 3 --backend ${DEVICE_PATH}/libQnnHtpV69.so \ 4 --system_library ${DEVICE_PATH}/libQnnSystem.so \ 5 --retrieve_context ${DEVICE_PATH}/qnnmodel.serialized.bin \ 6 --input_list ${DEVICE_PATH}/input_list.txt Copy to clipboard ## Building and Running LPAI qnn-sample-app - [Overview](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#overview) - [Reference Implementation](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#reference-implementation) - [Prerequisites](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#prerequisites) - [Environment Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#environment-setup) - [Linux Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#linux-setup) - [GCC Toolchain (Yocto)](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#gcc-toolchain-yocto) - [Android Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#android-setup) - [Hexagon Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#hexagon-setup) - [Compilation](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#compilation) - [Linux Build](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#linux-build) - [Linux OE Build (Yocto)](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#linux-oe-build-yocto) - [Android Build](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#android-build) - [Hexagon Build](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#hexagon-build) - [Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#execution) - [Linux Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#linux-execution) - [Linux OE Execution (Yocto)](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#linux-oe-execution-yocto) - [Android Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#android-execution) - [Hexagon Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#hexagon-execution) ### [Overview](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) This guide provides step-by-step instructions for building and running the qnn-sample-app-lpai using the LPAI backend. The sample demonstrates how to integrate and execute the QNN LPAI backend on both Linux and Hexagon DSP platforms. It includes environment setup, compilation, and execution procedures tailored for different targets. The LPAI backend supports multiple execution modes. For details on each, see: - [ARM FastRPC Backend Type](https://docs.qualcomm.com/doc/80-63442-10/topic/lpai_backend.html#qnn-lpai-fastrpc-backend-type) - [Native aDSP Direct Mode Backend Type](https://docs.qualcomm.com/doc/80-63442-10/topic/lpai_backend.html#qnn-lpai-direct-mode-backend-type) - [Simulation Backend](https://docs.qualcomm.com/doc/80-63442-10/topic/lpai_backend.html#qnn-lpai-sim-execution) ### [Reference Implementation](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) The reference implementation is located at: `${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI` This directory contains source code and build scripts for the sample application. It serves as a practical example of how to use the LPAI backend with QNN SDK. ### [Prerequisites](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) Before proceeding, ensure the following prerequisites are met: **General Requirements** - QNN SDK installed and QNN_SDK_ROOT environment variable set. - Hexagon SDK installed and HEXAGON_SDK_ROOT environment variable set. - Android NDK (required for Android targets). - GCC toolchain (required for Yocto-based Linux targets). - Clang compiler (required for x86 Linux builds). - adb tool (for pushing binaries to Android devices). - Root access on target device (required for Android ARM and Hexagon execution). ### [Environment Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) #### [Linux Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) To build qnn-sample-app-lpai on Linux, you need: 1. **Clang Compiler** 2. **ndk-build** (only for Android targets) If Clang is not available in your system PATH, use the SDK-provided script to install it: $ sudo bash ${QNN_SDK_ROOT}/bin/check-linux-dependency.sh Copy to clipboard To verify the environment setup: $ ${QNN_SDK_ROOT}/bin/envcheck -n Copy to clipboard Note qnn-sample-app-lpai has been verified with Android NDK version **r25c**. #### [GCC Toolchain (Yocto)](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) For Yocto-based Linux targets, the GCC compiler is required. The QNN SDK libraries are compiled using GCC 11.2 to support Yocto Kirkstone. To obtain the correct toolchain: 1. Follow Qualcomm’s build guide: [https://docs.qualcomm.com/bundle/publicresource/topics/80-70020-254/how_to.html#generate-an-esdk](https://docs.qualcomm.com/bundle/publicresource/topics/80-70020-254/how_to.html#generate-an-esdk) 2. After building the eSDK, locate the toolchain script: <WORKSPACE DIR>/build-qcom-wayland/tmp-glibc/deploy/sdk/qcom-wayland-x86_64-qcom-console-image-armv8-2a-qcm6490-toolchain-ext-0.0.sh 3. Extract the toolchain: ./qcom-wayland-x86_64-qcom-console-image-armv8-2a-qcm6490-toolchain-ext-0.0.sh Copy to clipboard #### [Android Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) To build qnn-sample-app-lpai for Android, set `ANDROID_NDK_ROOT` to the Android NDK installation path: $ export ANDROID_NDK_ROOT=/path/to/android-ndk Copy to clipboard #### [Hexagon Setup](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) To build for Hexagon DSP, the Hexagon SDK must be installed and configured. Set up the environment: $ source ${HEXAGON_SDK_ROOT}/setup_sdk_env.source Copy to clipboard Refer to general/setup/linux\_setup:LPAI (Low Power AI) for detailed setup instructions. ### [Compilation](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) #### [Linux Build](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) To build for x86 Linux: cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI make all_x86 Copy to clipboard #### [Linux OE Build (Yocto)](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) To build for Yocto Kirkstone based devices (aarch64 with GCC 11.2): cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI/ export QNN_AARCH64_LINUX_OE_GCC_112=/path/to/extracted/toolchain make CXX="/tmp/sysroots/x86_64/usr/bin/aarch64-qcom-linux/aarch64-qcom-linux-g++ \ --sysroot=/tmp/sysroots/qcm6490" all_linux_oe_aarch64_gcc112 Copy to clipboard After executing make from above, you should be able to see two new folders in the same directory: > > > 1. bin: contains *qnn-sample-app-lpai* binary in `aarch64-oe-linux-gcc11.2` directory. > 2. obj: contains all the object files that were used for building and linking the executable. To delete the artifacts that were generated in the above step, run: cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI make clean Copy to clipboard #### [Android Build](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) To build for Android (aarch64): cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI make all_android Copy to clipboard After executing make from above, you should be able to see two new folders in the same directory: > > > 1. bin: contains *qnn-sample-app-lpai* binary in `aarch64-android` directory. > 2. obj: contains all the object files that were used for building and linking the executable. To delete the artifacts that were generated in the above step, run: cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI make clean Copy to clipboard #### [Hexagon Build](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) To build for Hexagon DSP (e.g., v79 architecture): cd ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI make hexagon V=v79 Copy to clipboard ### [Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) #### [Linux Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) Set the library path and run the sample app: export LD_LIBRARY_PATH=${QNN_SDK_ROOT}/lib/x86_64-linux-clang ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI/bin/x86_64-linux-clang/qnn-sample-app-lpai \ --backend ${QNN_SDK_ROOT}/lib/x86_64-linux-clang/libQnnLpai.so \ --retrieve_context qnnmodel.serialized.bin \ --systemlib ${QNN_SDK_ROOT}/lib/x86_64-linux-clang/libQnnSystem.so Copy to clipboard #### [Linux OE Execution (Yocto)](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) The LPAI ARM backend on Yocto-based devices also uses **FastRPC** to communicate with the aDSP, requiring both the ARM-side stub and the signed Hexagon skel library on the DSP side. `DEVICE_PATH` refers to the path on the device where required files are copied. `HW_VER` refers to the LPAI hardware version (e.g., `v6`, `v7`). Note `${QNN_SDK_ROOT}/lib/lpai-${HW_VER}/unsigned/libQnnLpaiSkel.so` must be signed before deploying to the device. Copy required files to the target device (e.g., via `scp`): scp ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI/bin/aarch64-oe-linux-gcc11.2/qnn-sample-app-lpai ${DEVICE_PATH} scp ${QNN_SDK_ROOT}/lib/aarch64-oe-linux-gcc11.2/libQnnLpai.so ${DEVICE_PATH} scp ${QNN_SDK_ROOT}/lib/aarch64-oe-linux-gcc11.2/libQnnLpaiStub.so ${DEVICE_PATH} scp ${QNN_SDK_ROOT}/lib/aarch64-oe-linux-gcc11.2/libQnnSystem.so ${DEVICE_PATH} scp ${QNN_SDK_ROOT}/lib/lpai-${HW_VER}/unsigned/libQnnLpaiSkel.so ${DEVICE_PATH}/adsp scp qnnmodel.serialized.bin ${DEVICE_PATH} Copy to clipboard Run the sample app on the device: export LD_LIBRARY_PATH=${DEVICE_PATH}:${DEVICE_PATH}/adsp export ADSP_LIBRARY_PATH=${DEVICE_PATH}/adsp ${DEVICE_PATH}/qnn-sample-app-lpai \ --backend ${DEVICE_PATH}/libQnnLpai.so \ --retrieve_context ${DEVICE_PATH}/qnnmodel.serialized.bin \ --systemlib ${DEVICE_PATH}/libQnnSystem.so Copy to clipboard For more tool help, run: $ qnn-sample-app-lpai --help Copy to clipboard #### [Android Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) The LPAI ARM backend executes via **FastRPC**: the ARM CPU communicates with the aDSP over IPC. This requires both an ARM-side stub library and a Hexagon skeleton library on the DSP side. Note To execute the LPAI backend on an Android device, the following conditions must be met: 1. `${QNN_SDK_ROOT}/lib/lpai-${HW_VER}/unsigned/libQnnLpaiSkel.so` must be signed by the client before pushing to the device. Use the Qualcomm `sectools` signing utility or the `sign_hexagon.py` helper script from the Hexagon SDK. 2. `qnn-sample-app-lpai` must be executed with root permissions. `DEVICE_PATH` refers to the path on the device where required files are pushed. `HW_VER` refers to the LPAI hardware version (e.g., `v6`, `v7`). Push required files to the device: adb push ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleAppLPAI/bin/aarch64-android/qnn-sample-app-lpai ${DEVICE_PATH} adb push ${QNN_SDK_ROOT}/lib/aarch64-android/libQnnLpai.so ${DEVICE_PATH} adb push ${QNN_SDK_ROOT}/lib/aarch64-android/libQnnLpaiStub.so ${DEVICE_PATH} adb push ${QNN_SDK_ROOT}/lib/aarch64-android/libQnnSystem.so ${DEVICE_PATH} # LPAI FastRPC backend requires the signed Hexagon skel library adb push ${QNN_SDK_ROOT}/lib/lpai-${HW_VER}/unsigned/libQnnLpaiSkel.so ${DEVICE_PATH}/adsp adb push qnnmodel.serialized.bin ${DEVICE_PATH} Copy to clipboard Run the sample app on the device: adb shell export LD_LIBRARY_PATH=${DEVICE_PATH}:${DEVICE_PATH}/adsp export ADSP_LIBRARY_PATH=${DEVICE_PATH}/adsp ${DEVICE_PATH}/qnn-sample-app-lpai \ --backend ${DEVICE_PATH}/libQnnLpai.so \ --retrieve_context ${DEVICE_PATH}/qnnmodel.serialized.bin \ --systemlib ${DEVICE_PATH}/libQnnSystem.so Copy to clipboard #### [Hexagon Execution](https://docs.qualcomm.com/doc/80-63442-10/topic/sample_app.html#contents) Note The run_main_on_hexagon utility is used to offload execution to the Hexagon DSP. In this case, the aDSP domain is used, which corresponds to domain ID 0. The unsigned_pd=0 argument specifies the use of a signed PD. Note To execute the LPAI backend on an Android device, the following conditions must be met: 1. The following Hexagon artifacts must be signed by the client: - `${HEXAGON_SDK_ROOT}/libs/run_main_on_hexagon/ship/hexagon_toolv19_v79/librun_main_on_hexagon_skel.so` - `${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp/bin/hexagon/libQnnSampleAppv79.so` - `${QNN_SDK_ROOT}/lib/lpai-v6/unsigned/libQnnLpai.so` - `${QNN_SDK_ROOT}/lib/hexagon-v79/unsigned/libQnnSystem.so` Note The `run_main_on_hexagon` binary from the Hexagon SDK should not be signed. 2. `run_main_on_hexagon` must be executed with root permissions. Steps to Execute: DEVICE_PATH - refers to path on device where required files are pushed. 1. Push binaries to the device: adb push ${HEXAGON_SDK_ROOT}/libs/run_main_on_hexagon/ship/android_aarch64/run_main_on_hexagon /vendor/bin/run_main_on_hexagon adb push ${HEXAGON_SDK_ROOT}/libs/run_main_on_hexagon/ship/hexagon_toolv19_v79/librun_main_on_hexagon_skel.so /vendor/lib/rfsa/adsp/ adb push ${QNN_SDK_ROOT}/examples/QNN/SampleApp/SampleApp/bin/hexagon/libQnnSampleAppv79.so ${DEVICE_PATH} adb push ${QNN_SDK_ROOT}/lib/lpai-v6/unsigned/libQnnLpai.so ${DEVICE_PATH} adb push ${QNN_SDK_ROOT}/lib/hexagon-v79/unsigned/libQnnSystem.so ${DEVICE_PATH} adb push qnnmodel.serialized.bin ${DEVICE_PATH} Copy to clipboard 2. Run the sample app: cd /vendor/bin ./run_main_on_hexagon 0 ${DEVICE_PATH}/libQnnSampleAppv79.so \ unsigned_pd=0 \ --backend ${DEVICE_PATH}/libQnnLpai.so \ --systemlib ${DEVICE_PATH}/libQnnSystem.so \ --retrieve_context ${DEVICE_PATH}/qnnmodel.serialized.bin Copy to clipboard Last Published: Aug 06, 2026 [Previous Topic Multicore Device Creation and NSP Selection](https://docs.qualcomm.com/bundle/publicresource/80-63442-10/topics/tutorial_nsp_selection.md) [Next Topic Saver Tutorial: Save execution sequence with Saver and replay on a backend](https://docs.qualcomm.com/bundle/publicresource/80-63442-10/topics/saver_tutorial.md)