news 2026/9/24 13:46:40

Android NDK 序列模型示例(Sequence Sample):用 NNAPI 计算几何级数累加的实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 API(NN API)构建并执行一个由"加法 + 乘法"两个算子组成的序列模型,并利用 Android 11 新增的不透明内存(Opaque Memory)与带依赖的异步执行机制(startComputeWithDependencies)完成多步链式累加计算。读完本文,你将掌握 NNAPI 从模型构建、编译到多步执行的完整调用链,理解 ASharedMemory 与 Opaque Memory 的适用场景,以及如何在自己的 NDK 工程中落地这套流程。

示例概览:用一张"两步算子图"完成几何级数累加

Sequence 示例演示了 NNAPI 最基础但完整的使用方式:构建一个包含两个运算(一次加法、一次乘法)的序列模型,用于计算几何级数(geometric progression)累加的单步递推。其计算图如下(出自 README.md):

sumIn ---+ +--- 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 SDK(API 30)。
  • 运行设备:需要一台运行 Android 11 的真机或模拟器。

注意:由于一个已知问题(known issue),本示例使用了自研的封装(源码中直接包含<android/NeuralNetworks.h>并显式链接neuralnetworks库,见 CMakeLists.txt)来访问 Android 11 新增的 NNAPI 特性;官方计划在下一个 R(Android 11)SDK 版本修复该问题后,用正式接口替换这一封装。这意味着如果你要在 Android 11 上使用 Opaque Memory、Memory Descriptor 等新能力,需要留意你所用 NDK/SDK 版本的接口可用性。

仓库根目录的 nn-samples/README.md 还给出了整个 NN 示例集合的共同前提:Android Studio 4.0+、NDK r16+、Android API 27+;其中basic模块演示 Android 8(API 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通过AsyncTaskinitModelcompute放到后台线程执行(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_NONE):

uint32_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. 添加算子与标识输入输出

// ADD:sumIn + stateIn + fusedActivationFuncNone → sumOut ANeuralNetworksModel_addOperation(model_, ANEURALNETWORKS_ADD, addInputOperands.size(), addInputOperands.data(), 1, &sumOut); // MUL:stateIn × 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_POWERPREFER_SUSTAINED_SPEED等,开发者可根据实际负载权衡。

内存管理:ASharedMemory 与 Opaque Memory 的配合

这是本示例在 Android 11 上的核心看点:同一张计算图,边界数据用普通共享内存,中间数据用 Opaque Memory,最大限度减少数据拷贝。

ASharedMemory:用于"边界"输入输出

CreateSharedMemories()(sequence_model.cpp)通过ASharedMemory_create分配四块匿名共享内存,并调用ANeuralNetworksMemory_createFromFd包装成ANeuralNetworksMemory

  • initialState:只读(PROT_READ),保存初始状态值;
  • ratio:只读,填充为常量公比;
  • sumIn:可读写,初始填充 0;
  • sumOut:可读写,保存最终累加结果。

填充共享内存的fillMemory辅助函数(sequence_model.cpp)使用mmap+std::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}中的stateOut
  • ANeuralNetworksMemoryDesc只负责描述用途createFromDesc才真正分配内存;描述符在创建完所有内存后即可free
  • Opaque Memory 适合仅存在于 NNAPI 内部的张量(如状态张量、中间结果)。使用它可以减少数据拷贝与格式转换的开销——driver 可以直接在自有内存布局上运算;
  • 示例为 sum 与 state 各创建一对Opaque Memory(In/Out 各一个),每次单步执行后交换两个句柄,实现"上一轮输出即下一轮输入"。

多步执行链:带依赖的异步计算

执行逻辑集中在Compute()(sequence_model.cpp)与DispatchSingleStep()(sequence_model.cpp)。

初始化与分派

fillMemory(sumInFd_, tensorSize_, 0); fillMemory(initialStateFd_, tensorSize_, initialValue);

先向共享内存写入初值:sumIn填 0、initialStateinitialValue。随后按步数创建std::vector<ANeuralNetworksEvent*> events(steps, nullptr),进入循环。

每一步的内存选择逻辑(sequence_model.cpp):

  • 第 0 步:sumIn用 ASharedMemory(memorySumIn_)、stateIn用 ASharedMemory(memoryInitialState_);
  • 第 1 步起:输入全部改用 Opaque Memory(memoryOpaqueSumIn_memoryOpaqueStateIn_);
  • 最后一步:sumOut落到 ASharedMemory(memorySumOut_),便于 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());

随后通过mmapPROT_READ只读映射sumOutFd_,取outputTensorPtr[0]作为最终结果,并逐个ANeuralNetworksEvent_free释放事件对象。

资源释放

析构函数(sequence_model.cpp)按序释放CompilationModel、所有ANeuralNetworksMemory(ASharedMemory 与 Opaque Memory),并close全部文件描述符,体现了"谁创建、谁释放"的完整生命周期管理。

UI 与使用流程

界面布局见 activity_main.xml,交互流程在 MainActivity.java 中:

  1. ratio_input输入公比,点击Reset按钮:销毁旧模型(若存在),在后台线程initModel(ratio)重建模型,并回显 ratio;
  2. initial_value_input输入初值、steps_input输入步数,点击Compute按钮:后台线程compute(initialValue, steps, modelHandle)执行链式累加,结果回填到result_text
  3. 页面销毁(onDestroy)时调用destroyModel释放原生资源。

每次点击 Compute 都复用同一个已编译模型、仅更换输入值——这正是 NNAPI 编译(Compilation)与执行(Execution)分离的设计目的:模型只编译一次,多次执行零重复编译开销。

运行与验证

构建运行步骤(参考 nn-samples/README.md 的 Getting Started):

  1. 用 Android Studio 4.0+ 打开仓库根目录(工程由根目录 settings.gradle 组织,sequence 作为独立 app 模块构建);
  2. 确认使用 Android 11 SDK(API 30)编译,并准备一台 Android 11 设备;
  3. 执行Tools/Android/Sync Project with Gradle Files同步工程;
  4. 点击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 Memory(Memory 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 13:46:36

Flask 扩展 Moment 本地化日期和时间

Web 应用中时间显示是一个常见需求,而本地化展示时间更是提升用户体验的关键细节。不同地区的用户希望看到符合其文化习惯的时间格式,比如“2025年4月7日 上午10:30”这样的格式对中文用户更友好,而美国用户则更习惯“April 7, 2025, 10:30 AM”。 Flask-Moment 是 Flask 的…

作者头像 李华
网站建设 2026/9/24 13:46:31

Flask Cookies 本地数据

在Web开发中,Cookies 是实现用户会话管理、偏好设置保存以及简易身份识别的重要手段。Flask作为一个轻量级的Python Web框架,提供了简单直观的方式来处理Cookies。掌握Cookies的用法不仅有助于构建更智能的Web应用,也是在构建用户体验、处理用户状态以及提高系统安全性方面的…

作者头像 李华
网站建设 2026/9/24 13:46:31

Flask 扩展 SQLalchemy 操作数据库

Flask 本身是一个轻量级框架,默认并不内置 ORM 功能。为了提供数据库支持,可以通过扩展集成 SQLAlchemy。SQLAlchemy 是 Python 中功能强大的数据库工具,具备 ORM(对象关系映射)和 SQL 表达式语言两种能力,能够让代码更贴近对象操作的思维方式,同时也不失对底层 SQL 的控…

作者头像 李华
网站建设 2026/9/24 13:45:30

魔百盒CM311-1救砖:S905L3短接maskrom线刷全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华