Android NDK 序列模型示例(Sequence Sample):用 NNAPI 计算几何级数累加的实现解析
Android NDK 序列模型示例Sequence Sample用 NNAPI 计算几何级数累加的实现解析【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址: https://gitcode.com/gh_mirrors/nd/ndk-samples本篇技术指南以 ndk-samples 仓库中的 nn-samples/sequence 示例为主体深入讲解如何通过 Android NDK 导出的 Neural Networks APINN API构建并执行一个由加法 乘法两个算子组成的序列模型并利用 Android 11 新增的不透明内存Opaque Memory与带依赖的异步执行机制startComputeWithDependencies完成多步链式累加计算。读完本文你将掌握 NNAPI 从模型构建、编译到多步执行的完整调用链理解 ASharedMemory 与 Opaque Memory 的适用场景以及如何在自己的 NDK 工程中落地这套流程。示例概览用一张两步算子图完成几何级数累加Sequence 示例演示了 NNAPI 最基础但完整的使用方式构建一个包含两个运算一次加法、一次乘法的序列模型用于计算几何级数geometric progression累加的单步递推。其计算图如下出自 README.mdsumIn --- --- ADD --- sumOut stateIn --- --- MUL --- stateOut ratio ---其中ratio是定义在模型内的常量张量constant tensor代表训练过程中学习到的权重。在构建模型时它的值被写入共享内存并由 NNAPI 读取对应ANeuralNetworksModel_setOperandValueFromMemory。sumIn、stateIn是模型的输入张量它们的值在每次执行模型时由调用方提供可以随执行不同而变化。单步语义为sumOut sumIn stateInstateOut stateIn × ratio。要求出几何级数的总和就需要把这张图多次执行并将前一次的输出作为后一次的输入链式衔接---------- ---------- ---------- initialSum --| Simple |--| Simple |-- --| Simple |-- sumOut | Sequence | | Sequence | ... | Sequence | initialState --| Model |--| Model |-- --| Model |-- stateOut ---------- ---------- ----------从数学上看设初始值a、公比r执行n步后sumOut a a·r a·r² … a·rⁿ⁻¹正是有限项几何级数stateOut a·rⁿ则是递推过程中不断前进的状态。每次执行只推进一步但通过输入输出链式衔接实现了任意步数的累加——这正是许多时序模型如 RNN 状态递推在 NNAPI 上的典型表达方式。运行环境要求根据 README.md 的 Additional Requirements编译环境需要 Android 11 SDKAPI 30。运行设备需要一台运行 Android 11 的真机或模拟器。注意由于一个已知问题known issue本示例使用了自研的封装源码中直接包含android/NeuralNetworks.h并显式链接neuralnetworks库见 CMakeLists.txt来访问 Android 11 新增的 NNAPI 特性官方计划在下一个 RAndroid 11SDK 版本修复该问题后用正式接口替换这一封装。这意味着如果你要在 Android 11 上使用 Opaque Memory、Memory Descriptor 等新能力需要留意你所用 NDK/SDK 版本的接口可用性。仓库根目录的 nn-samples/README.md 还给出了整个 NN 示例集合的共同前提Android Studio 4.0、NDK r16、Android API 27其中basic模块演示 Android 8API 27时代的基础概念sequence模块则专门演示 Android 11 新增的高级特性。工程结构与构建配置Sequence 模块的核心文件如下文件职责sequence.cppJNI 入口桥接 Java 与 C 模型类sequence_model.hSimpleSequenceModel类声明与成员定义sequence_model.cppNNAPI 模型构建、编译、内存与执行的核心实现CMakeLists.txtCMake 构建脚本MainActivity.javaAndroid UI 与 JNI 调用CMake 构建脚本CMakeLists.txt展示了 NNAPI 工程最基本的链接方式cmake_minimum_required(VERSION 3.22.1) add_library(sequence SHARED sequence.cpp sequence_model.cpp) target_link_libraries(sequence # Link with libneuralnetworks.so for NN API neuralnetworks android log)要点生成名为sequence的动态库对应 Java 侧System.loadLibrary(sequence)见 MainActivity.java。必须链接neuralnetworks即libneuralnetworks.so系统 NNAPI 运行时同时链接android提供ASharedMemory等接口与log提供__android_log_print。JNI 入口三个原生方法Java 层 声明了三个 native 方法构成模型完整的生命周期public native long initModel(float ratio); public native float compute(float initialValue, int steps, long modelHandle); public native void destroyModel(long modelHandle);对应 sequence.cpp 中的实现initModel(ratio)调用SimpleSequenceModel::Create(ratio)完成模型、编译与内存的初始化返回以jlong形式持有的 C 对象指针(jlong)(uintptr_t)model.release()compute(initialValue, steps, modelHandle)把jlong还原为SimpleSequenceModel*调用Compute()执行steps步累加并返回结果destroyModel(modelHandle)delete掉 C 对象释放 NNAPI 资源。MainActivity通过AsyncTask将initModel与compute放到后台线程执行MainActivity.java避免阻塞 UI 线程——这也与 NNAPI 的异步执行模型相匹配。构建模型操作数、算子与输入输出标识模型构建集中在SimpleSequenceModel::CreateModel()sequence_model.cpp是理解 NNAPI C API 的最佳范例。1. 定义操作数类型所有张量操作数都是2 维的ANEURALNETWORKS_TENSOR_FLOAT32形状为dimLength × dimLength源码中dimLength_ 200即200×200的二维浮点张量且不使用任何融合激活函数ANEURALNETWORKS_FUSED_NONEuint32_t dimensions[] {dimLength_, dimLength_}; ANeuralNetworksOperandType float32TensorType{ .type ANEURALNETWORKS_TENSOR_FLOAT32, .dimensionCount sizeof(dimensions) / sizeof(dimensions[0]), .dimensions dimensions, .scale 0.0f, .zeroPoint 0, };外加一个 0 维标量ANEURALNETWORKS_INT32类型用于存放激活函数码FuseCode。2. 按顺序添加操作数操作数隐式地以其加入模型的顺序编号从 0 开始addOperand调用不会返回该索引必须由应用自行记账。源码用opIdx依次登记索引操作数说明0fusedActivationFuncNone常量标量值为ANEURALNETWORKS_FUSED_NONE供 ADD 与 MUL 共用1sumIn用户输入张量执行前确定2stateIn用户输入张量执行前确定3ratio常量张量从共享内存中读取4sumOutADD 输出5stateOutMUL 输出其中常量操作数的值设置分两种方式标量激活码ANeuralNetworksModel_setOperandValue直接写入内存中的值常量张量ratioANeuralNetworksModel_setOperandValueFromMemory(model_, ratio, memoryRatio_, 0, tensorSize_ * sizeof(float))从已创建的ANeuralNetworksMemory中读取——这演示了如何把大块常量数据放进共享内存而非逐元素拷贝。3. 添加算子与标识输入输出// ADDsumIn stateIn fusedActivationFuncNone → sumOut ANeuralNetworksModel_addOperation(model_, ANEURALNETWORKS_ADD, addInputOperands.size(), addInputOperands.data(), 1, sumOut); // MULstateIn × ratio fusedActivationFuncNone → stateOut ANeuralNetworksModel_addOperation(model_, ANEURALNETWORKS_MUL, mulInputOperands.size(), mulInputOperands.data(), 1, stateOut);随后用ANeuralNetworksModel_identifyInputsAndOutputs声明{sumIn, stateIn}为输入、{sumOut, stateOut}为输出最后调用ANeuralNetworksModel_finish结束模型构建。注意finish之后常量操作数的值便不可再修改。编译设置执行偏好CreateCompilation()sequence_model.cpp把构建好的模型编译成可执行形态ANeuralNetworksCompilation_create(model_, compilation_); ANeuralNetworksCompilation_setPreference( compilation_, ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER); ANeuralNetworksCompilation_finish(compilation_);setPreference给运行时与各厂商 driver 一个优化方向提示。本示例选择ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER尽快得到单次结果因为每次执行只做一步递推追求低延迟而不是低功耗。完整的偏好枚举还包括PREFER_LOW_POWER与PREFER_SUSTAINED_SPEED等开发者可根据实际负载权衡。内存管理ASharedMemory 与 Opaque Memory 的配合这是本示例在 Android 11 上的核心看点同一张计算图边界数据用普通共享内存中间数据用 Opaque Memory最大限度减少数据拷贝。ASharedMemory用于边界输入输出CreateSharedMemories()sequence_model.cpp通过ASharedMemory_create分配四块匿名共享内存并调用ANeuralNetworksMemory_createFromFd包装成ANeuralNetworksMemoryinitialState只读PROT_READ保存初始状态值ratio只读填充为常量公比sumIn可读写初始填充 0sumOut可读写保存最终累加结果。填充共享内存的fillMemory辅助函数sequence_model.cpp使用mmapstd::fill写入数据后munmap。注释特别强调真实场景中共享内存区域的值通常由其他模块或进程操纵——这正是 ASharedMemory 的意义所在它天然支持跨进程/跨模块的数据共享。Opaque Memory用于中间状态张量CreateOpaqueMemories()sequence_model.cpp展示了 Android 11 新增的 Memory Descriptor 流程ANeuralNetworksMemoryDesc_create(sumDesc); // 声明该内存将作为 compilation 的第 0 个输入sumIn使用 ANeuralNetworksMemoryDesc_addInputRole(sumDesc, compilation_, 0, 1.0f); // 声明该内存将作为 compilation 的第 0 个输出sumOut使用 ANeuralNetworksMemoryDesc_addOutputRole(sumDesc, compilation_, 0, 1.0f); ANeuralNetworksMemoryDesc_finish(sumDesc); ANeuralNetworksMemory_createFromDesc(sumDesc, memoryOpaqueSumIn_); ANeuralNetworksMemory_createFromDesc(sumDesc, memoryOpaqueSumOut_); ANeuralNetworksMemoryDesc_free(sumDesc);关键点addInputRole/addOutputRole中的索引是相对于identifyInputsAndOutputs声明的输入/输出列表的例如stateDesc使用索引1即输入列表{sumIn, stateIn}中的stateIn、输出列表{sumOut, stateOut}中的stateOutANeuralNetworksMemoryDesc只负责描述用途createFromDesc才真正分配内存描述符在创建完所有内存后即可freeOpaque Memory 适合仅存在于 NNAPI 内部的张量如状态张量、中间结果。使用它可以减少数据拷贝与格式转换的开销——driver 可以直接在自有内存布局上运算示例为 sum 与 state 各创建一对Opaque MemoryIn/Out 各一个每次单步执行后交换两个句柄实现上一轮输出即下一轮输入。多步执行链带依赖的异步计算执行逻辑集中在Compute()sequence_model.cpp与DispatchSingleStep()sequence_model.cpp。初始化与分派fillMemory(sumInFd_, tensorSize_, 0); fillMemory(initialStateFd_, tensorSize_, initialValue);先向共享内存写入初值sumIn填 0、initialState填initialValue。随后按步数创建std::vectorANeuralNetworksEvent* events(steps, nullptr)进入循环。每一步的内存选择逻辑sequence_model.cpp第 0 步sumIn用 ASharedMemorymemorySumIn_、stateIn用 ASharedMemorymemoryInitialState_第 1 步起输入全部改用 Opaque MemorymemoryOpaqueSumIn_、memoryOpaqueStateIn_最后一步sumOut落到 ASharedMemorymemorySumOut_便于 mmap 读回结果其余步骤的输出一律写入 Opaque Memory。注意当把 Opaque Memory 设为执行的输入或输出时offset 与 length 必须为 0表示使用整块内存区域。事件链真正的序列执行DispatchSingleStep的关键在于使用 Android 11 引入的异步带依赖接口const ANeuralNetworksEvent* const* dependencies nullptr; uint32_t numDependencies 0; if (waitFor ! nullptr) { dependencies waitFor; numDependencies 1; } status ANeuralNetworksExecution_startComputeWithDependencies( execution, dependencies, numDependencies, 0, // infinite timeout duration event);startComputeWithDependencies会立即返回一个ANeuralNetworksEvent实际计算在依赖事件完成后才开始。本示例把上一步的 event 作为下一步的依赖传入waitFor i 0 ? nullptr : events[i - 1]从而形成一条流水线式的事件链第i步的计算可以提前入队但会等第i-1步完成后再真正执行硬件有机会重叠计算与数据传输。每步执行后交换 Opaque Memory 句柄std::swap(memoryOpaqueSumIn_, memoryOpaqueSumOut_); std::swap(memoryOpaqueStateIn_, memoryOpaqueStateOut_);收尾与读回结果由于事件是链式串联的只需等待最后一个事件即可保证整条链完成ANeuralNetworksEvent_wait(events.back());随后通过mmap以PROT_READ只读映射sumOutFd_取outputTensorPtr[0]作为最终结果并逐个ANeuralNetworksEvent_free释放事件对象。资源释放析构函数sequence_model.cpp按序释放Compilation、Model、所有ANeuralNetworksMemoryASharedMemory 与 Opaque Memory并close全部文件描述符体现了谁创建、谁释放的完整生命周期管理。UI 与使用流程界面布局见 activity_main.xml交互流程在 MainActivity.java 中在ratio_input输入公比点击Reset按钮销毁旧模型若存在在后台线程initModel(ratio)重建模型并回显 ratio在initial_value_input输入初值、steps_input输入步数点击Compute按钮后台线程compute(initialValue, steps, modelHandle)执行链式累加结果回填到result_text页面销毁onDestroy时调用destroyModel释放原生资源。每次点击 Compute 都复用同一个已编译模型、仅更换输入值——这正是 NNAPI 编译Compilation与执行Execution分离的设计目的模型只编译一次多次执行零重复编译开销。运行与验证构建运行步骤参考 nn-samples/README.md 的 Getting Started用 Android Studio 4.0 打开仓库根目录工程由根目录 settings.gradle 组织sequence 作为独立 app 模块构建确认使用 Android 11 SDKAPI 30编译并准备一台 Android 11 设备执行Tools/Android/Sync Project with Gradle Files同步工程点击Run/Run app部署到设备。验证方式输入公比r 0.5、初值a 1、步数n 5预期结果约为1 0.5 0.25 0.125 0.0625 1.9375步数越多结果越逼近极限a/(1-r) 2从而直观验证链式累加的正确性。小结从本示例可迁移的技术要点模型构建范式Model_create → addOperand按序编号→ setOperandValue/FromMemory → addOperation → identifyInputsAndOutputs → finish常量数据优先走setOperandValueFromMemory放进共享内存编译与执行分离Compilation_create → setPreference → finish编译一次Execution_create → setInputFromMemory/setOutputFromMemory → startComputeWithDependencies → Event_wait重复执行内存选型需要与应用/进程共享或读回的数据用 ASharedMemory仅 NNAPI 内部流转的状态与中间结果用 Opaque MemoryMemory Desc createFromDesc可降低拷贝与变换开销多步时序计算用startComputeWithDependencies把各步事件串成依赖链配合 Opaque Memory 的 In/Out 句柄交换即可高效实现 RNN 式状态递推类模型的逐帧/逐序列执行。如果希望进一步对照 NNAPI 的基础概念可以结合 nn-samples/basic 模块演示 Android 8 的基础用法与本示例对比阅读理解从单次同步执行到Android 11 异步依赖链 Opaque Memory的演进脉络。【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址: https://gitcode.com/gh_mirrors/nd/ndk-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考