✕
开启精彩搜索
热门搜索
emby EMBY 解析 免流 源码 IPV6 阿里 阿里云 青龙 音乐 网心 网心云 webdav 阿里云盘 unraid 洗白 Rclone 刮削 云梦

在网心云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 为零

  • 现象:人脸任务能跑,资源监控却把 NPU 数量显示成零,前端不渲染 NPU 卡片。
  • 原因:旧监控库硬编码读取 galcore 设备节点和 debugfs,而当前 NPU 已经由 Etnaviv 提供。
  • 处理:单独订阅目标 NPU 的 scheduler 事件,生成新鲜的运行时统计,再只对 resmon_service 做兼容映射;不要伪造全局设备节点。

6.7 网上下载的库能加载但 ABI 对不上

  • 现象:动态链接器能打开库,初始化却失败,或者硬件作业出现不可解释的错误。
  • 原因:芯片代际、内核 ABI、Teflon 编译选项和模型权重编码必须成套匹配,文件能加载不代表协议正确。
  • 处理:只把公开项目当源码和方法参考,先在本机做版本和硬件核对,再从明确的源码提交构建。

6.8 重启后修复消失

  • 现象:当前 shell 里能跑,重启后服务回到旧模型,或者监控服务没有自动恢复。
  • 原因:改动只存在临时目录、当前进程环境或手工启动命令,systemd 没有固定版本和回滚记录。
  • 处理:模型、delegate、适配器、wrapper、systemd drop-in 和 telemetry 都做版本化安装,重启前后对比摘要,失败就自动恢复旧配置。

7. 最小验证流程

建议按下面的顺序验证,别一上来就跑整个相册。每一步都能把问题范围缩小一点。

顺序 做什么
先跑一个最小算子图 用已知输入和已知输出的 Conv、ABS、SUB、ADD、depthwise 小模型,确认解释器能创建 delegate,重复执行稳定。
再跑真实第一段 从真实识别图截出前几个算子,用同一份 CPU 中间 tensor 作为 NPU 输入,定位第一个偏差发生在哪个算子。
再跑整张识别图 确认一个完整 delegate 分区,输出是有限的 512 维向量,重复调用结果一致。
再接适配器 让 Rec.get_feat 和 Rec.get 使用同一模型、同一 delegate,确认输出形状和 embedding 写回。
再接真实 RPC 用固定测试图片走 Unix socket,检查 HTTP 状态、业务码、返回形状和服务日志。不要一开始就扫全库。
最后接监控和相册任务 先验证 telemetry 的 submitted/completed,再观察一个用户触发任务,分开记录 NPU 核心耗时和整轮任务耗时。

8. 怎么判断是不是全 NPU

我最后采用了几条硬条件,而不是看 UI 截图。你可以把它们做成脚本,让每次构建都自动跑。

  • 初始化日志显示识别 backend 为 Teflon,full NPU 标志为真,delegate 分区数为 1,图条目数与模型审计一致。
  • 目标 NPU scheduler 的 job run 和 job done 数量相等,设备标识没有跑到其他设备或软件路径。
  • 固定输入重复调用的输出摘要一致,CPU 参考和 NPU 输出的差异在约定的量化容差内。
  • 受控窗口前后的 fault、recover、hang、reset 计数没有增加。
  • RPC 返回 512 维 embedding,服务和数据库保持正常;这一步可以使用公开测试输入,不需要读取用户照片。
# 只看模式和分区,不把服务 active 当作完整验收
grep -E 'NPU_(DET|REC)_READY|REC_MODE' service.log

# 目标设备的硬件作业必须成对完成
submitted=$(read_stat submitted)
completed=$(read_stat completed)
test "$submitted" -eq "$completed"

# 识别 full NPU 必须真正关闭限制变量
test "${OES_REC_FULL_NPU:-0}" = 1
test -z "${ETNA_ML_CONV_ONLY+x}"

9. systemd 部署时我建议这样做

真正容易把修复弄丢的地方不是编译,而是部署。我的建议是把每个版本放在独立目录,配置只指向当前版本,切换前保留旧版本和旧配置。

  • 先判断 face 任务是否空闲,确认没有正在运行的子进程。
  • 备份旧 drop-in、旧适配器、旧人脸入口和旧模型摘要。
  • 把新模型和 delegate 安装到独立版本目录,先做 SHA256 校验,再改 systemd。
  • daemon-reload 后只重启必要服务,等待 ready 日志,再做固定输入 RPC。
  • 验证不通过时自动恢复旧 drop-in 和旧入口,重新 daemon-reload 和重启。
  • 把当前版本、构建提交、模型摘要、delegate 摘要和验证时间写到版本记录中。

不要在验证过程中同时升级内核、替换底层驱动、修改数据库或重建整个人脸库。一次只改一个边界,出了问题才知道是哪一层。


10. 如果你在自己的设备上遇到不同现象

现象 优先检查
delegate 分区为 0 TFLite dtype、算子版本、后端编译选项、模型是否真的量化
分区为 1 但 NPU 作业为 0 full NPU 环境是否传给子进程、Teflon 是否走了软件路径、scheduler 设备过滤是否正确
作业完成但输出恒定 输入量化、signed/unsigned 权重、zero point、权重布局和 cache flush
单层正确整图变差 第一处中间 tensor 漂移、bias correction、post shift 和输出 scale
监控显示 NPU 为 0 driver link、trace instance、统计文件新鲜度和 resmon 兼容作用域
重启后退回旧模型 systemd drop-in、wrapper、子进程环境和版本目录是否持久化

遇到这些情况时,先把最小复现模型、输入 dtype、量化元数据、delegate 分区数和一小段 scheduler 统计发出来。只给一张资源管理截图,通常不够判断问题在哪一层。


11. 速度应该怎样测

识别核心几十毫秒只是其中一段。正确的测速至少分四段:照片读取和解码、对齐和量化、NPU inference、结果写入和聚类。每段分别计时,再做单人脸、批量和整轮相册任务三种测试。

指标 怎么测 怎么看
NPU 核心耗时 在同一张 112×112 输入上重复调用,去掉数据库和 RPC 判断 delegate 和硬件作业本身
适配器耗时 包含预处理、量化、invoke 和输出转换 判断 Python/数据转换开销
单任务耗时 从 face 子进程开始到退出 包含调度、RPC、I/O 和数据库
整轮相册耗时 从任务创建到完成 反映用户体验,不能只归因于 NPU

12. 给想复现的人几句实话

别先找一个看起来相近的二进制库来替换。A311D、内核 NPU 版本、Teflon 构建和模型量化必须放在一起看。网上下载的 NPU 库即使能被动态链接器加载,也不代表它的权重编码、内存布局和内核 ABI 对得上。

也别因为一次输出能返回、一次分区数是 1 就宣布成功。真正有用的证据是:

  • 同一输入重复稳定;
  • CPU/NPU 对照在容差内;
  • 目标 NPU 的作业有完成事件;
  • 服务子进程加载了正确版本;
  • 监控在硬件作业期间有变化;
  • 重启后还能恢复。

如果你要把这套修复做成产品,最好把后端补丁、模型转换、适配器和监控一起纳入版本管理。临时环境变量和手工复制可以帮助排查,但不能替代正式的版本、摘要和回滚机制。


🔒
评论后可见
本条内容需要评论一次才能查看
去评论 ↓

总结

我这次的路线核心就一句话:

先证明硬件链路,再逐层修正模型和后端,最后才接服务和 UI。顺序反过来,很容易得到一个看起来正常、实际没有跑对的系统。

如果你也在 A311D 或类似 Etnaviv 接管的 OES 上做人脸识别 NPU,希望这篇整理能帮你少走一点弯路。

💬 评论区 0