Skip to main content

10.2 TFLM 运行时与模型嵌入

本阶段涵盖两项内容:将 TensorFlow Lite Micro (TFLM) 运行时集成到 Q2390 / IQ2390 MCU 固件中,以及将预先构建的 int8 模型(在第 1 阶段生成)直接嵌入固件镜像。其中包括在 SDLLVM/RISC-V 上所需的、不易察觉的 C++ 运行时设置。

10.2.1 概述

集成包含五个不同部分:
  1. 获取 TFLM 源码 — TFLM 源码未随仓库提供(not vendored),必须手动获取
  2. 检查模型并验证算子支持 — 从模型中提取算子类型,并确认 TFLM 支持这些算子
  3. 配置 LLVM libc++ — SDLLVM 默认不配置 libc++;需要进行 5 项特定修复
  4. 创建推理模块 — 构建包含嵌入式模型 C 数组、推理框架(tflite_infer.cc)、Kconfig 头文件和 CMakeLists.txt 的模块
  5. 集成到固件构建中 — 在 main.c 中调用 tflite_infer_run(),并在 CMake 中注册该模块

10.2.2. 第 1 部分 — 获取 TFLite Micro 源码

路径约定: 在本阶段中,<wasp_proc> 指 MCU 固件工作区的根目录,即包含 zephyr/、modules/、config/ 等的目录。凡出现此处,请替换为您的实际路径(例如 /path/to/your_workspace/wasp_proc)。
TFLM 模块桩文件(CMakeLists.txt、Kconfig)位于 wasp_proc/zephyr/zephyr/modules/tflite-micro/,但实际的 C++ 源码树并不存在,必须手动克隆。 tflite-micro 项目在 west 清单(zephyr/zephyr/submanifests/optional.yaml)中定义于 optional 组,而 Zephyr 自身的 west.yml 通过 group-filter: [-optional] 显式禁用了该组。这意味着 west update tflite-micro 将无法工作,请改用 git clone。

10.2.2.1. 克隆源码

在 Docker 构建容器内:
修订版本直接从清单中读取,因此无需修改即可适用于任何 LPAICP 版本。由此产生的分离 HEAD(detached HEAD)状态是预期且正确的。

10.2.2.2. 验证


10.2.3. 第 2 部分 — 检查模型并验证算子支持

在编写任何固件代码之前,我们需要从模型中获取两项信息:
  1. 模型使用了哪些算子 — TFLM 运行时要求在运行推理之前显式注册模型使用的每个算子。如果缺少任何算子,固件将在启动时因无法解析算子而报错。此步骤可确定模型所需的确切算子集合,以便您在 第 5 部分 中集成推理框架时知道需要注册哪些算子。
  2. 输入/输出量化参数(scale + zero_point)— 如果模型采用 int8 量化,则浮点输入在传递给推理框架之前必须预先量化为 int8。

10.2.3.1. 选项 A — flatc(完整拓扑解码 — 推荐)

使用 TFLM schema 将模型二进制解码为人类可读的 JSON。可获得:所有张量的算子类型、张量名称、形状和量化参数。 步骤 1 — 运行 flatc 解码模型:
这会在当前目录中生成 cnn1d_minimal_int8.json。 步骤 2 — 从 JSON 中提取算子列表:
步骤 3 — 将每个算子与 1.3.3 中的支持算子列表进行交叉核对。

10.2.3.2. 选项 B — tf.lite.Interpreter(仅量化参数,无需 flatc)

在 flatc 不可用时使用。可获得输入/输出量化参数(scale、zero_point)。不会暴露算子类型 — 请使用选项 A 获取算子列表。
tf.lite.Interpreter 不会暴露算子类型 — 请使用选项 A(flatc)获取算子列表。

10.2.3.3. 此 TFLM 构建支持的所有算子

TFLM 所支持算子的权威来源是 micro_mutable_op_resolver.h — 该文件中声明的每个 Add*() 方法都对应所下载的确切修订版本所支持的一个算子。
发布标签 zephyr_20240627 的完整支持算子列表 — 共 114 个算子:
每个算子对应的 resolver 方法为 Add + 算子名称 — 例如 Conv2D → AddConv2D(),FullyConnected → AddFullyConnected()。

10.2.3.4. 参考 — 示例模型已确认的算子

cnn1d_minimal_int8.tflite 中的所有算子、对应的 TFLM 内核文件以及 resolver 方法: 如果某个算子未注册,您将看到以下错误之一: 编译时(方法名称错误或缺失):
运行时在 AllocateTensors() 阶段(算子未注册):

10.2.4. 第 3 部分 — 配置 LLVM libc++(5 步修复)

10.2.4.1. 默认方法为何失败

要使用 TFLM(以 C++ 编写),Zephyr 需要 C++ 标准库。自然的起点是启用 CONFIG_LIBCXX_LIBCPP=y。然而在 SDLLVM 上,这会触发一条破坏构建的 Kconfig 依赖链。该故障会表现为一个完全不相关的内核文件中的错误:

10.2.4.2. 4 步修复

10.2.4.2.1. 步骤 1 — 从 TFLM Kconfig 中移除 select REQUIRES_FULL_LIBCPP

文件:zephyr/zephyr/modules/tflite-micro/Kconfig 从 config TENSORFLOW_LITE_MICRO 内部移除 select REQUIRES_FULL_LIBCPP 这一行。这是依赖级联的根源 — 移除它可防止 Zephyr 在 SDLLVM 上触发已损坏的 REQUIRES_FULL_LIBC 路径。

10.2.4.2.2. 步骤 2 — 使用 EXTERNAL_MODULE_LIBCPP,而非 EXTERNAL_LIBCPP

此处无需操作 — 该设置包含在第 2.4.4 节创建的新文件中。

10.2.4.2.3. 步骤 3 — 通过 -I 而非 -isystem 注入 libc++ 包含路径

此处无需操作 — 该调用是第 2.4.3 节中添加的完整代码块的一部分。 -I 目录会先于任何 -isystem 目录被搜索。通过以 -I BEFORE 方式注入 libc++ 头文件,clang 会首先找到 libc++ 自身的 <stdio.h>/<string.h> 包装头文件,它们再通过 #include_next 链接到 musl — 这正是 C++ 翻译单元的正确解析顺序。

10.2.4.2.4. 步骤 4 — 使用 -DNDEBUG 编译 C++ 翻译单元

此处无需操作 — 该标志是第 2.4.3 节中添加的完整代码块的一部分。 musl 的 assert() 宏会展开为 __assert_fail(),而 picolibc 并不提供该函数。-DNDEBUG 会在 C++ 翻译单元中编译掉所有 assert 调用。其作用范围限定为 $<$<COMPILE_LANGUAGE:CXX>:-DNDEBUG>,因此 C 代码不受影响。

10.2.4.3. 完整的 CMake 配置代码块

文件:modules/hal/qcom/core/config/CMakeLists.txt(已有文件 — 在末尾添加此完整代码块)。该代码块以 CONFIG_EXTERNAL_MODULE_LIBCPP 为条件,因此仅在合并 TFLM Kconfig 片段时才会生效。

10.2.4.4. shikra_lpaicp_tflite.conf

10.2.4.4.1. 步骤 1 — 创建 shikra_lpaicp_tflite.conf

在 zephyr/kernel/config/shikra_lpaicp_tflite.conf 创建新文件:

10.2.4.4.2. 步骤 2 — 在 config.yml 中注册该文件

文件:zephyr/kernel/config/config.yml(已有文件 — 添加一个新条目):
为什么使用单独的文件,而不是添加到 shikra_lpaicp.conf? 现有条目使用正则表达式 (^|shikra_)lpaicp_.*,它同时匹配硬件目标(SHIKRA_LPAICP_TEST)和使用 Zephyr-SDK GCC 的 QEMU 仿真变体。将 TFLM 特定于 SDLLVM 的 libc++ 配置添加到 shikra_lpaicp.conf 会破坏基于 GCC 的 QEMU 构建。范围更窄的正则表达式 shikra_lpaicp_.* 仅匹配硬件目标。

10.2.5. 第 4 部分 — 创建推理模块

10.2.5.1. 步骤 1 — 生成 src/model_data.cpp 和 src/input_data.cpp

首先创建模块目录,然后将第 1 阶段生成的模型和输入文件转换为 C 字节数组。请在包含第 1 阶段源文件的目录中运行这些命令。
将 <wasp_proc> 替换为您的 wasp_proc MCU 代码库路径。 生成的文件如下所示:
模型数组上的 alignas(8): TFLM 的 FlatBuffers 解析器要求模型字节数组至少按 4 字节对齐。无论链接器将该符号放置在何处,alignas(8) 都能保证满足这一要求。

10.2.5.2. 步骤 2 — 创建 inc/tflite_infer.h

文件:modules/hal/qcom/core/tflite_infer/inc/tflite_infer.h(新文件)。

10.2.5.2.1. 调整 TFLI_ARENA_KB

TFLI_ARENA_KB 设置张量 arena 的大小 — 这是 TFLM 用于输入/输出张量、层间中间激活缓冲区及其内部分配器元数据的连续内存块。它在 tflite_infer_run() 开始时通过 k_malloc 从系统堆中分配。 取值不当时会出现的问题: 这两种故障都会导致 tflite_infer_run() 提前返回 — g_tfli_batches 保持为 0,所有延迟全局变量保持其初始值。 如何确定合适的值 — 使用 g_tfli_arena_used 进行测量:
  1. 将 TFLI_ARENA_KB 设置为一个充裕的初始值(例如 32)— 足够大以保证 AllocateTensors() 成功。
  2. 构建、烧写、启动,并在 T32 中运行 read_tflite_infer.cmm。
  3. 确认 g_tfli_status == 0 且 g_tfli_batches >= 1。
  4. 从 T32 Var.View 窗口读取 g_tfli_arena_used — 这是 TFLM 所需的确切字节数。
  5. 计算最小安全值并更新头文件:
  1. 重新构建并重新烧写。验证 g_tfli_arena_used 仍小于 g_tfli_arena_size。
参考模型的配置值: TFLI_ARENA_KB = 4(4,096 字节)。这是参考代码库中 cnn1d_minimal_int8.tflite 所使用的值。成功运行后,请在目标设备上测量 g_tfli_arena_used,以确认您的模型的使用情况。
TFLI_ARENA_KB 对 RAM 的影响: arena 在运行时从系统堆中分配 — 它不会直接增加静态镜像大小。但是,堆的后备缓冲区(kheap_buf__system_heap)是一个静态链接段,其大小由 CONFIG_HEAP_MEM_POOL_SIZE 决定(在 shikra_lpaicp.conf 中设置为 32768)。仅减小 TFLI_ARENA_KB 并不会减少静态 RAM。若要回收静态 RAM,还需相应降低 CONFIG_HEAP_MEM_POOL_SIZE — 确保新的大小能够覆盖 TFLI_ARENA_KB * 1024 + 16 以及其他所有 k_malloc 调用方的需求。

10.2.5.3. 步骤 3 — 创建 CMakeLists.txt

文件:modules/hal/qcom/core/tflite_infer/CMakeLists.txt(新文件)。以 CONFIG_TENSORFLOW_LITE_MICRO 为条件。

10.2.5.4. 步骤 4 — 创建 src/tflite_infer.cc

文件:modules/hal/qcom/core/tflite_infer/src/tflite_infer.cc(新文件)。每个批次运行 TFLI_ITERS 次计时的 Invoke() 调用(在一次预热之后),然后将每批次以微秒、QTMR tick 和 CPU 周期表示的最小/平均/最大延迟发布到 g_tfli_* volatile 全局变量中。
关于 __attribute__((retain)) 的说明: g_tfli_arena_size 和 g_tfli_iters 仅在初始化时写入,固件代码从不读取它们。如果没有 __attribute__((retain)),即使它们是 volatile,链接器的 --gc-sections 过程也会悄无声息地丢弃它们的 ELF 段 — volatile 可防止编译器将其优化掉,但无法防止链接器 GC。retain 属性会将该段标记为始终存活,从而确保这些符号对 T32 保持可见。

10.2.6. 第 5 部分 — 集成到固件构建中

10.2.6.1. 步骤 1 — 将 tflite_infer_run() 集成到 main.c 中

文件:modules/hal/qcom/core/main/src/main.c
放置位置: tflite_infer_run() 放在 CONFIG_QC_CLK_READY 代码块之后。如果该代码块不存在,则将其直接放在初始的 LOG_INF / printk 行之后,效果相同。

10.2.6.2. 步骤 2 — 在 CMake 中注册该模块

文件:modules/hal/qcom/core/config/CMakeLists.txt(已有文件 — 在 add_subdirectory_ifdef 代码块末尾添加一行):
将其放在最后一个现有的 add_subdirectory_ifdef 行之后:

10.2.7. RAM 说明

在添加 TFLM 之前,Shikra LPAICP 固件大约有 40 KB 的空闲 RAM。TFLM 的静态代码占用(3 个算子子集约 25 KB)、模型数据(约 2 KB)以及堆分配可能会使镜像接近 3 MB 的上限。 如果构建失败并出现:
请在 zephyr/kernel/config/config.yml 中注释掉 log.conf:
log.conf 会启用 Zephyr 的延迟日志子系统:一个 16 KB 的环形缓冲区以及一个具有 4 KB 栈的专用后台线程,共占用约 20 KB 静态 RAM。由于此固件使用 T32 RAM 控制台进行输出(无 UART),延迟日志线程没有输出路径。将其注释掉可回收约 20 KB,同时 shikra_lpaicp.conf 中的 CONFIG_LOG_MODE_MINIMAL=y 仍保持生效。

10.2.8. Kconfig 标志参考

为 TFLM 集成而添加或修改的所有 Kconfig 符号: 推理框架参数(TFLI_ARENA_KB、TFLI_ITERS、TFLI_BATCH_SLEEP_MS)是 inc/tflite_infer.h 中的 #define 常量。直接编辑该文件即可进行调整,无需重新编译其他任何文件。有关 arena 大小确定步骤,请参阅第 2.5.2.1 节。