在网心云OES A311D 上跑通人脸识别NPU:实战折腾经验分享
这篇教程整理自我在 OES A311D 设备上跑通人脸识别 NPU 的完整过程。
核心结论先放在前面:真正起作用的不是某个神奇的预编译库,而是一条组合路线——Etnaviv 接住 NPU,Teflon 把 TFLite 图送进去,模型转换和后端量化互相匹配,旧资源监控单独做兼容层。
这套方法最适合 A311D 一类、NPU 已经由 Etnaviv 接管的 OES。换到别的芯片、别的 VIP core 或别的内核版本,不要直接复制库,先重新做基线和 ABI 核对。
1. 先说最后跑通的样子
我最后接受的状态,不是“日志里出现了 NPU”这么简单,而是下面几件事同时成立。少一件,都只能算中间状态。
- NPU 设备明确绑定到 Etnaviv,Teflon 能创建上下文。
- 检测图可以委托给 Teflon,识别图在 full NPU 模式下由一个完整 delegate 分区覆盖。
- 识别初始化日志打印模型摘要、delegate 摘要、图条目数和输入 dtype,服务子进程使用的环境与父进程一致。
- 真实 NPU scheduler 能看到目标设备的 job run 和 job done,提交数和完成数一致,重复调用输出稳定。
- RPC 能返回固定形状的 512 维 embedding,服务和数据库没有因为切换而异常。
- NPU 状态卡不再因为旧 galcore 路径不存在而显示为零,监控数据在硬件作业停止后回到零。
识别核心调用在设备上大约是几十毫秒级,但相册整轮任务仍可能要几分钟。照片读取、解码、对齐、RPC、数据库和聚类都在这条链路上,所以不要拿整轮相册耗时直接反推 NPU 有没有工作。
2. 第一步不是改代码,而是确认硬件链路
网页上显示的 NPU 卡片只能当线索,不能当硬件执行证据。下面这些检查应该在任何改动前做一次,并保存输出。
uname -a
readlink -f /sys/bus/platform/devices/ff100000.npu/driver
ls -l /dev/galcore /dev/dma_heap /dev/ion 2>/dev/null
systemctl show ai_manager oes-etnaviv.service resmon_service -p ActiveState -p MainPID -p NRestarts
df -P / /tmp
要确认的点:
- NPU 节点的 driver link 指向
etnaviv-gpu,而不是只看某个旧模块名字在不在。 - 记录内核版本、VIP/NPU core 版本、TFLite runtime 版本和 Teflon 构建版本。后面所有库和模型都要与这组版本配套。
- 确认根分区和
/tmp有足够空间。模型转换、Mesa 编译和日志 trace 会同时占空间,空间不足时最容易出现误判。 - 在开始改动前备份 systemd 环境、当前模型、当前 delegate 和人脸入口,确保可以回滚。
3. 为什么选这套公开代码组合
我实际对照过几类公开代码。最后选的不是某一个神奇的预编译包,而是把几个方向的长处拼成一条能解释、能重建、能回滚的路线。
| 公开项目或代码方向 | 我从里面拿到什么 | 为什么放进最终组合 |
|---|---|---|
| funnymdzz/mesa | Teflon 前端、Etnaviv NPU 后端和 Mesa 的构建方式 | 直接对应 A311D 的 Etnaviv/Teflon 路线,适合作为后端修复的代码基础 |
| leegao/mesa | 同一类 Teflon/Etnaviv 改动的另一份实现和结构对照 | 用来交叉看资源管理、构建选项和服务接入,避免只相信一个分支的写法 |
| tflite-vx-delegate | TFLite delegate 接口、量化模型接入和解释器调用方式 | 保留接口层经验,但不把它当作当前设备的直接运行库;驱动和运行时必须以当前 Etnaviv 链为准 |
| OES/PCDN 公开代码和镜像资料 | 设备软件分层、启动服务和基础运行环境的线索 | 只用来确认平台背景和恢复边界,不把旧平台的二进制直接搬到新链路 |
最后形成的组合是:
Mesa Teflon/Etnaviv 后端 + A311D VIP v7 量化模型和权重编码 + 一个只负责输入输出和环境隔离的适配器 + 一层面向旧监控接口的 telemetry 兼容。
这样选的原因很实际:每一层都能单独测,出了问题能知道是模型、后端、适配器还是监控;而且不需要把某个陌生的整包二进制直接写进系统。
公开项目只能提供方向,不能替代本机 ABI 核对。真正决定能不能跑的是芯片 core 版本、内核绑定、Teflon 编译选项、模型量化规则和子进程环境是否一致。
4. 这条链路到底由什么组成
| 层 | 作用 | 我会检查什么 |
|---|---|---|
| 硬件与内核 | 提供 NPU、DRM 和 scheduler | 设备绑定、版本、作业事件 |
| Etnaviv | 把用户态 Teflon 作业送到 NPU | 上下文、提交、完成、fault |
| Teflon delegate | 把 TFLite 算子降低成 VIP v7 作业 | 量化、布局、权重和 tensor view |
| TFLite 模型 | 定义检测和识别图及量化元数据 | dtype、scale、zero point、图覆盖 |
| Python/服务适配器 | 把照片输入转成模型输入并返回 embedding | 环境作用域、输出形状、ready 日志 |
| 资源监控 | 向 UI 提供 NPU 数量和忙碌率 | 数据源、过期保护、服务隔离 |
这几层必须按顺序验收。模型没委托完整时不要先调监控,监控显示恢复也不能证明识别算子正确。
5. 真正需要改的地方
5.1 模型要为目标 NPU 量化
我没有把浮点模型直接丢给 Teflon。识别图需要整数量化,输入输出 dtype、scale 和 zero point 必须由模型和后端共同约定。模型转换时要保存每层量化审计信息,至少包括输入、权重和输出的 scale、zero point、张量布局和算子版本。
要点:
- 输入预处理要固定:颜色顺序、尺寸、均值、标准差和量化公式不能在 CPU 侧和模型侧各做一套。
- VIP v7 的 signed int8 权重不能未经处理就按硬件 unsigned 系数使用。转换、转置、重排和重新量化完成后,再做硬件需要的符号转换和零点补偿。
- 多层模型里,一层的小误差会被后面的 SUB、ADD 和 requantization 放大。不要只测第一层就宣布模型正确。
- 模型最后要用固定输入做 CPU/TFLite 与 NPU 输出对照,同时检查重复调用是否一致。
5.2 后端最容易出错的是量化和资源视图
我在后端追到的关键问题不是“设备打不开”,而是设备收到了作业,却拿到了错误的权重、错误的 bias 或错误长度的输入。下面几条是最值得回到正式代码里的修复。
// 伪代码:双输入打包后仍要保留逻辑 view
packed = create_resource(input0_size + input1_size)
tensor0.resource = packed
tensor0.offset = 0
tensor0.size = input0_size // 不能改成 packed_size
tensor1.resource = packed
tensor1.offset = input0_size
tensor1.size = input1_size
upload(tensor0, input0_pointer, tensor0.size)
upload(tensor1, input1_pointer, tensor1.size)
要点:
- ADD/SUB 等双输入算子如果共用一个物理 resource,第一输入和第二输入的逻辑 size、offset、上传指针必须分开。
- bias correction 要按实际编码后的权重通道数计算。虚拟的 2×2×1 加法系数不能沿用原始 tensor 的通道数。
- depthwise、pointwise 和普通 Conv2D 的权重布局不同,不能为了省事把所有层走同一个 pack 路径。
- 每次修改只针对一个明确的算子或一类布局,先跑最小模型,再跑完整识别图。不要同时改十几个量化参数。
5.3 适配器要明确区分检测和识别
检测和识别经常在同一个进程里初始化。如果环境变量是全局的,检测器的限制很容易污染识别器,或者父进程的旧设置被子进程继承。我的做法是让适配器在创建每个 interpreter 时保存并恢复环境,并让 full NPU 识别单独走一套严格检查。
export OES_FACE_BACKEND=teflon
export OES_FACE_MODEL=/opt/oes-accel/vx-test/det-int8.tflite
export OES_FACE_DELEGATE=/opt/oes-accel/teflon-det/lib/libteflon.so
export OES_REC_MODEL=/opt/oes-accel/vx-test/rec-int8-full.tflite
export OES_REC_DELEGATE=/opt/oes-accel/teflon-rec/lib/libteflon.so
export OES_REC_FULL_NPU=1
# 识别 full NPU 时不能保留会限制委托范围的变量
unset ETNA_ML_CONV_ONLY
export ETNA_ML_INIT_EVERY_CONTEXT=1
export ETNA_ML_NN_NO_ZRL=1
初始化后必须检查 delegate 分区数。full NPU 模式下,分区数不等于 1 就直接报错;同时打印模型摘要、delegate 摘要、图条目数和输入 dtype。这样即使相册任务还没启动,也能先知道加载的是不是正确版本。
5.4 监控不能再硬读旧监控接口
旧资源监控库通常会检查 /dev/galcore 或 galcore debugfs。Etnaviv 接管后,这些路径不存在是正常的,结果就是 NPU 数量被解析成零,前端把 NPU 卡片隐藏。
建议做法:
- 单独运行一个 telemetry 进程,只过滤目标 NPU 设备的 scheduler job run/job done 事件。
- 用“窗口内仍有未完成硬件作业的时间 ÷ 窗口时间”计算 busy percent,并同时记录 submitted、completed、outstanding 和 clients。
- 把统计结果写入私有运行时目录,写临时文件后 rename,文件过期或 trace 丢事件时返回无效。
- 只对
resmon_service加载兼容层,不能给ai_manager、face 进程或全系统设置全局LD_PRELOAD。
这个指标是 NPU 调度忙碌率,不是 NPU 内部每个计算单元的精确利用率,也不是整条相册任务的 CPU 使用率。显示文字要把这个含义讲清楚。
6. 我踩过的坑
6.1 只看服务状态
- 现象:服务 active 只能说明进程还活着,不能说明识别图已经委托给 NPU,更不能说明输出正确。
- 原因:有时模型加载日志和服务状态都正常,但实际图只有一部分在硬件上,或者硬件作业根本没有完成。
- 处理:把 delegate 分区、硬件 job run/job done、固定输入读回和 fault 增量放在同一个验收门里。
6.2 把限制变量写成 0
- 现象:以为把某个限制变量设成 0 就等于关闭限制,结果 full NPU 仍然只委托了卷积。
- 原因:这类后端有的按变量是否存在判断,不解析变量的字符串值,所以
0仍然算开启。 - 处理:full NPU 识别初始化前直接
unset,并在初始化后检查分区数;检测器的限制只在检测器自己的环境作用域里设置。
6.3 双输入打包覆盖逻辑长度
- 现象:ADD 或 SUB 的输出像随机数,单输入卷积却正常。
- 原因:两个逻辑输入共用一个物理 resource 时,把 resource 总长度误当成了第一输入长度,上传循环因此越界或把第二段填成了无效值。
- 处理:保留每个 view 的 logical size 和 offset,上传时按各自的 size 读原始输入;修完先跑双输入最小图,再跑整图。
6.4 signed int8 和硬件编码不一致
- 现象:第一层差得不多,越往后 embedding 越偏。
- 原因:TFLite 的 signed int8 权重、zero point、bias correction 和 VIP v7 的 unsigned 系数编码没有在同一阶段对齐。多层 requantization 会把小误差放大。
- 处理:在布局重排和重新量化完成后再做符号转换,重新计算 bias correction,并为每层保存量化审计数据。
6.5 父服务和子进程用的不是同一套模型
- 现象:父服务环境看起来是新模型,但真正处理请求的 face 子进程仍然打印旧模型摘要。
- 原因:内部启动逻辑覆盖了 systemd 传下去的变量,或者检测器初始化留下的环境污染了识别器。
- 处理:由统一入口显式导出变量,在创建每个 interpreter 时保存并恢复环境,并把子进程 ready 日志作为验收证据。
6.6 监控仍然显示 NPU 为零
总结
我这次的路线核心就一句话:
先证明硬件链路,再逐层修正模型和后端,最后才接服务和 UI。顺序反过来,很容易得到一个看起来正常、实际没有跑对的系统。
如果你也在 A311D 或类似 Etnaviv 接管的 OES 上做人脸识别 NPU,希望这篇整理能帮你少走一点弯路。

💬 评论区 0