- 人工智能
- 深度学习
- 推理引擎
- 本地部署
- 嵌入式
- 物联网
【免费下载链接】tflite-micro
Infrastructure to enable deployment of ML models to low-power resource-constrained embedded targets (including microcontrollers and digital signal processors).
TFLite Micro(TFLM)在资源受限的嵌入式目标上部署机器学习模型时,默认采用"无动态内存、无持久状态"的设计哲学。但当模型需要跨多次Invoke保持状态(例如累加器、滑窗、状态循环神经网络)时,就需要用到本文档介绍的 Resource Variables(资源变量)机制:它通过VAR_HANDLE、ASSIGN_VARIABLE(即 ASSIGN_RESOURCE)与READ_VARIABLE三个算子,把可变的持久缓冲区挂载到解释器上,同时把该特性设计为可选编译/运行项以避免资源受限系统上的二进制体积膨胀。读完本文,你将掌握:资源变量的工厂 API 与接线方式、完整的生命周期规则(Prepare 阶段的三步模式)、三个算子在源码层的实际实现,以及如何从 TensorFlow 模型端生成并使用这类模型(含仓库内可直接运行的累加器示例)。
为什么需要资源变量:可选特性背后的设计取舍
VAR_HANDLE、ASSIGN_VARIABLE与READ_VARIABLE是 TensorFlow 侧tf.Variable经 TFLite 转换后落地的底层算子。常规 TFLM 模型每次Invoke都是"无状态"的——张量内存要么在 arena 中复用,要么在每次调用时重新计算。对于需要跨调用保留数值的应用(累加器、状态机、时序滤波等),TFLM 提供 Resource Variables 作为可选特性,官方文档明确指出:
This feature is optional in order to prevent binary bloat on resource constrained systems.
从代码结构看,这一"可选"体现在两个层面:
- 运行时必须显式创建
MicroResourceVariables对象并传入解释器,否则VAR_HANDLE、ASSIGN_VARIABLE、READ_VARIABLE三个算子在 Prepare/Eval 时都会打印错误并返回kTfLiteError(见 var_handle.cc、assign_variable.cc、read_variable.cc); - 解释器构造函数的资源变量参数默认为
nullptr(见 micro_interpreter.h),应用不传即完全关闭该功能,不产生相关代码路径的开销。
因此,是否启用资源变量完全由应用根据模型需求自行决定:模型里不含这三个算子时,无需创建资源变量对象;包含时,必须创建并正确传入,否则推理会直接失败。
API:MicroResourceVariables 工厂方法与核心接口
资源变量的入口类是tflite::MicroResourceVariables,声明位于 micro_resource_variable.h,实现位于 micro_resource_variable.cc。文档强调其工厂方法:
The MicroResourceVariables factory method takes a MicroAllocator and an int indicating the number of resource variables to support. This allows the application to choose the correct number of variables based on the model.
对应签名:
static MicroResourceVariables* Create(MicroAllocator* allocator, int num_variables);参数说明:
allocator:MicroAllocator指针,工厂方法通过allocator->AllocatePersistentBuffer(...)在 arena 的持久区分配MicroResourceVariables对象本体以及长度为num_variables的MicroResourceVariable数组(见 micro_resource_variable.cc)。由于分配发生在持久区,资源变量对象与缓冲区不会在每次Invoke之间被回收。num_variables:需要支持的最大资源变量数量。该值必须大于等于模型实际使用的变量个数——若超出,CreateIdIfNoneFound会打印"Failed to allocate resource variable. Maximum resource variable count (%d) reached."并返回-1(见 micro_resource_variable.cc)。如何确定数量:可在转换后查看模型内VAR_HANDLE算子个数,或按业务所需的持久状态数直接设定。
内部每个变量由MicroResourceVariable结构体描述(见 micro_resource_variable.h):
struct MicroResourceVariable { const char* container; // 容器名(可为 nullptr) const char* shared_name; // 共享名,与 container 组成唯一标识 void* resource_buffer; // 资源缓冲区指针(ASSIGN 阶段分配) size_t bytes; // 缓冲区字节数,用于校验读写尺寸 int8_t default_value; // 初始化默认值(量化 zero_point) };对外公开的核心方法(均可在 micro_resource_variable.cc 中对照阅读):
int CreateIdIfNoneFound(const char* container, const char* shared_name):按 container/shared_name 对查找已有变量,找到则复用其 ID;否则分配新 ID。注意"Some TFLite flatbuffers contain null container names to save space",因此查找逻辑对nullptrcontainer 做了兼容(见 micro_resource_variable.cc)。TfLiteStatus Read(int id, const TfLiteEvalTensor* tensor):把 ID 对应缓冲区的bytes字节拷入目标张量,同时用TFLITE_DCHECK(EvalTensorBytes(tensor) == variable.bytes)校验尺寸一致。TfLiteStatus Allocate(int id, TfLiteContext* context, const TfLiteTensor* tensor):若缓冲区尚未分配,则按输入张量tensor->bytes通过context->AllocatePersistentBuffer分配,并按量化 zero_point 初始化(memset(resource_buffer, default_value, bytes));已分配则直接返回kTfLiteOk。TfLiteStatus Assign(int id, size_t count_bytes, const void* input_buffer):把输入数据拷入已分配的缓冲区;若缓冲区未分配(即未先走 Allocate)会报错提示"Make sure to call AssignResourceVariable with a TfLiteTensor first."。TfLiteStatus ResetAll():将所有缓冲区重置为各自default_value(zero_point),供解释器Reset()时恢复初始状态。
一个值得注意的细节:缓冲区默认值取张量的量化zero_point(见 micro_resource_variable.cc),这样即使不做任何 ASSIGN,缓冲区也处于量化语义下的"零值"状态。
与解释器接线:如何把资源变量传给 MicroInterpreter
MicroResourceVariables::Create得到的指针需要传给MicroInterpreter。MicroInterpreter的两个构造函数都接受可选的MicroResourceVariables* resource_variables参数(默认nullptr,见 micro_interpreter.h),并在构造MicroInterpreterGraph时透传(见 micro_interpreter.cc)。
典型 C++ 使用方式:
#include "tensorflow/lite/micro/micro_resource_variable.h" #include "tensorflow/lite/micro/micro_interpreter.h" #include "tensorflow/lite/micro/micro_mutable_op_resolver.h" // 1. 创建 allocator(arena 需足够容纳资源变量对象 + 缓冲区) static tflite::MicroAllocator* allocator = ...; // 通常来自 MicroInterpreter // 2. 按模型需要的变量数创建资源变量(例如模型内含 1 个 tf.Variable) tflite::MicroResourceVariables* resource_variables = tflite::MicroResourceVariables::Create(allocator, /*num_variables=*/1); // 3. 注册三个算子 tflite::MicroMutableOpResolver<3> resolver; resolver.AddVarHandle(); resolver.AddAssignVariable(); resolver.AddReadVariable(); // 4. 构造解释器并传入资源变量 tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, arena_size, resource_variables);在解释器内部,MicroInterpreterGraph持有该指针,并通过GetResourceVariables()暴露给算子(见 micro_interpreter_graph.h);MicroGraph将其声明为纯虚接口(见 micro_graph.h)。三个算子的 Prepare/Eval 都是通过micro_context->graph().GetResourceVariables()拿到对象后调用对应方法的(见 var_handle.cc、assign_variable.cc、read_variable.cc)。
Python 侧(TFLM Python 运行时)则无需手工创建对象:tflite_micro.python.tflite_micro.runtime.Interpreter在内部完成资源变量的创建与传递,用户只需正常set_input/invoke/get_output/reset(见 resource_variables_test.py)。
生命周期:从创建到跨子图复用的完整规则
原文档用精炼的语言描述了资源变量的完整生命周期,这是理解该机制的核心,逐条展开如下:
1. 创建阶段:ID 即数组下标
当应用创建MicroResourceVariables时,它内部持有一个长度为 N 的MicroResourceVariable数组(N 即Create传入的num_variables)。该数组的下标就是 Resource ID:CreateIdIfNoneFound返回的新 ID 即num_resource_variables_++的自增下标(见 micro_resource_variable.cc)。ID 与(container, shared_name)一一映射,同一对名字只对应一个 ID。
2. VAR_HANDLE 首次 Prepare:保留资源 ID
On the first call to Prepare in the VAR_HANDLE op, a new resource ID is reserved and the resource ID value is referenced from within the output tensor of VAR_HANDLE.
在 var_handle.cc 的VarHandlePrepare中:
- 从算子参数
TfLiteVarHandleParams取出container与shared_name; - 调用
resources->CreateIdIfNoneFound(container, shared_name)保留(或复用)一个资源 ID; - 把 ID 存入该算子的
OpData(op_data->resource_id),并让输出张量output->data.i32指向op_data->resource_id。
也就是说,VAR_HANDLE的输出张量本身并不携带数据,而是携带一个指向资源 ID 的指针;后续ASSIGN_VARIABLE/READ_VARIABLE通过读取这个 ID 才知道操作哪个缓冲区。Eval 阶段(VarHandleEval)会再次把同一 ID 写入输出张量(见 var_handle.cc),保证每次推理输出一致。
3. ASSIGN_VARIABLE 首次 Prepare:按值张量尺寸分配缓冲区
On the first call to Prepare in ASSIGN_VARIABLE, the specified ID found in the input index tensor is updated based on the size of the input value tensor, and its resource buffer is allocated.
在 assign_variable.cc 的Prepare中:
- 校验输入个数为 2(
kInputVariableId=0为资源 ID,kInputValue=1为值张量)、输出个数为 0; - 校验资源 ID 张量类型为
kTfLiteResource或kTfLiteInt32且元素数为 1; - 通过
resources->Allocate(id, context, input_value)按值张量tensor->bytes分配缓冲区(仅首次生效,见Allocate中if (variable.resource_buffer == nullptr)分支)。
Eval 阶段则把值张量内容Assign进缓冲区(resources->Assign(input_id->data.i32[0], EvalTensorBytes(input_value), buffer),见 assign_variable.cc),即完成"写入变量"。
4. 后续调用:读写分配好的缓冲区
Future invocations of READ_VARIABLE and ASSIGN_VARIABLE read and write to and from the allocated resource buffer.
一旦缓冲区在首次 ASSIGN Prepare 时分配完成,之后的每次READ_VARIABLE调用resources->Read(id, output_value)把缓冲区内容拷入输出张量(read_variable.cc),每次ASSIGN_VARIABLE调用resources->Assign(...)把新值写入缓冲区。整个过程不再触碰内存分配,只是memcpy级别的读写(见 micro_resource_variable.cc),对实时性要求高的嵌入式场景非常友好。
5. 必须遵守的调用顺序与跨子图语义
文档给出了强约束的生命周期模式:
VAR_HANDLE Prepare() -> ASSIGN_VARIABLE Prepare() -> Other calls即必须先有VAR_HANDLE的 Prepare 保留 ID,再有ASSIGN_VARIABLE的 Prepare 分配缓冲区,之后才能安全地执行READ_VARIABLE/ASSIGN_VARIABLE的 Eval。反过来,若先执行 READ 而缓冲区尚未分配,Read中的TFLITE_DCHECK(variable.resource_buffer != nullptr)会直接触发断言失败。
文档还强调一个跨子图的重要特性:
Note that VAR_HANDLE Prepare() and ASSIGN_VARIABLE Prepare() may be called more that once, across multiple subgraphs. Only the first call to each will generate a new resource ID or allocate a resource buffer.
即这两个 Prepare 可能跨多个子图被多次调用(典型场景:带控制流IF/WHILE的模型,或子图共享变量的模型),但只有第一次调用会真正生成新 ID 或分配缓冲区。这由两个机制保证:
CreateIdIfNoneFound先FindId,已存在的(container, shared_name)直接返回旧 ID(见 micro_resource_variable.cc);Allocate仅在resource_buffer == nullptr时分配(见 micro_resource_variable.cc)。
此外,跨子图使用时还隐藏着一个时序细节:assign_variable.cc的 Prepare 注释指出,当input_resource_id_tensor->data.i32为nullptr时(变量用于另一个子图、ID 要到 Eval 时才有效),Prepare 阶段跳过Allocate,改由 Eval 阶段校验兜底(见 assign_variable.cc 及注释中的 b/277231654)。
从模型端落地:仓库内可运行的累加器示例
资源变量最直观的落地场景是"跨 Invoke 的持久状态"。仓库在 examples/recipes/ 目录提供了完整示例,包含模型生成脚本、预生成的 tflite 文件与端到端测试:
- resource_variables_lib.py:演示两种建模方式生成"累加器"模型——用
tf.Module+tf.Variable构造 concrete function 转换,或用 Keras 自定义Layer转换。模型核心逻辑为:输入布尔值选择加/减,输入值数组作为加减量,self._accum.assign_add(accum_val)/assign_sub(accum_val)更新tf.Variable,再read_value()返回累加结果。tf.Variable经转换后在 flatbuffer 中正是以VAR_HANDLE+ASSIGN_VARIABLE+READ_VARIABLE三个算子呈现。 - resource_variables.tflite:预生成的模型文件,可直接加载使用,无需本地 TensorFlow 环境。
- resource_variables_test.py:Python 侧端到端测试,验证了三种关键行为:
- 首次
invoke时,输入[True]+ 全 15.0 的数组,输出为全 15.0(初始 0 + 15); - 第二次
invoke时,输入[False]+ 全 9.0,输出为全 6.0(15 - 9)——证明变量值跨 Invoke 保留; - 调用
interpreter.reset()后重新累加 5.0,输出回到全 5.0——证明 reset 会重置资源缓冲区到初始值(对应ResetAll())。
- 首次
这是验证你对生命周期理解的最短路径:把该测试跑通,即可确认模型转换、解释器接线与资源变量机制全链路正确。
源码级验证:单元测试如何佐证生命周期规则
仓库还提供了针对MicroResourceVariables本身的单元测试 micro_resource_variable_test.cc,逐条印证了文档所述规则:
CreateVariables:用MicroAllocator::Create+Create(..., 4)创建 4 个槽位,验证不同(container, shared_name)组合会得到互不相同的 ID,而相同组合重复查询返回同一 ID(体现CreateIdIfNoneFound的幂等性,对应"仅首次生成新 ID")。CreateVariablesNullContainer:验证container == nullptr时查找与分配依然正确(对应"flatbuffer 中空 container 名省空间"的兼容逻辑)。AllocateResourceBuffers:分别用 42 字节、100 字节的TfLiteTensor调用Allocate,断言缓冲区按各自tensor->bytes精确分配(对应"按输入值张量尺寸分配")。VerifyAssignAndReadResourceBuffer:构造 32 个int32_t的 golden 数据,Assign后Read回读,逐元素断言相等——完整走通"分配 -> 写入 -> 读出"链路,对应"未来调用读/写已分配缓冲区"。
这些测试同时是使用MicroResourceVariablesAPI 的极佳范例:mock 一个只提供AllocatePersistentBuffer的TfLiteContext(见测试文件GetMockContext),即可在无完整解释器的环境中验证资源变量的读写行为。
关键限制与使用注意事项
结合文档与源码,使用资源变量时有几个必须注意的点:
- 缓冲区分区与数量上限:对象与缓冲区均来自
MicroAllocator的持久区(AllocatePersistentBuffer),arena 需要预留足够空间;同时Create传入的num_variables一旦不足,运行时会报错返回,因此应优先按模型内VAR_HANDLE数量精确配置。 - 严格的 Prepare 顺序:
VAR_HANDLE Prepare()必须在ASSIGN_VARIABLE Prepare()之前;对任一子图都成立。模型转换器通常已保证该顺序,但若手工拼装 flatbuffer 需自行遵守。 - 缓冲区初始化值是量化 zero_point 而非 0:见
Allocate中对TfLiteAffineQuantization的 zero_point 读取与memset初始化(micro_resource_variable.cc)。这是为了与量化语义一致,浮点模型则表现为 0.0。 - 未创建资源变量对象 = 算子直接报错:三个算子中任一在
GetResourceVariables()返回nullptr时都会MicroPrintf并返回kTfLiteError,且MicroInterpreterGraph在未传入时返回nullptr(见 micro_interpreter_graph.h 附近注释)。因此只要模型含这三个算子之一,就必须完成对象创建与传入。 kTfLiteResource类型张量:VAR_HANDLE输出与ASSIGN_VARIABLE/READ_VARIABLE的输入资源 ID 张量类型为kTfLiteResource(read_variable.cc的 Prepare 用TFLITE_DCHECK强制校验,read_variable.cc),普通推理代码不应直接读写该张量的数据内容。
结语
Resource Variables 是 TFLM 在"资源受限 + 有状态模型"这对矛盾之间给出的优雅解:它用"应用按模型显式配置数量、首次 Prepare 延迟分配、后续纯 memcpy 读写"的方式,把二进制开销与运行期内存都控制到最小,同时通过(container, shared_name)到数组下标的映射和"仅首调用生效"的语义,安全支撑了跨子图共享变量的复杂模型。理解本文的生命周期三步模式(VAR_HANDLE Prepare()→ASSIGN_VARIABLE Prepare()→ 其他调用),再结合 micro_resource_variable.cc 的实现与 examples/recipes/ 的累加器示例,即可在自己的嵌入式项目中放心地引入持久状态能力。
- 人工智能
- 深度学习
- 推理引擎
- 本地部署
- 嵌入式
- 物联网
【免费下载链接】tflite-micro
Infrastructure to enable deployment of ML models to low-power resource-constrained embedded targets (including microcontrollers and digital signal processors).
相关推荐
InsightFace:4 行代码跑通本地人脸检测与 1:1 人脸比对
InsightFace:4 行代码跑通本地人脸检测与 1:1 人脸比对 InsightFace 是开源 2D/3D 人脸分析工具箱,装好即具备检测、识别、对齐、
人工智能计算机视觉深度学习Flink 有状态流处理编程指南:Keyed State、算子状态与状态 TTL 完整实战
Flink 有状态流处理编程指南:Keyed State、算子状态与状态 TTL 完整实战 本指南基于当前仓库中的 Working with State 官方文
后端大数据流处理批处理TiXL 的 Loop 算子:用迭代变量实现批量绘图的完整指南
TiXL 的 Loop 算子:用迭代变量实现批量绘图的完整指南 本文围绕 TiXL 实时图形系统( Lib.flow 命名空间)中的 Loop 算子 展开,讲解
音视频图形学桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考