使用方法
参考tensorflow/lite/micro/examples/xxx 目录下的使用方法, 以hello_world为例,
文件hello_world_test.cc
1. 创建MicroErrorReporter object
tflite::MicroErrorReporter micro_error_reporter;
2. 有tflite model文件得到 tflite::Modle 结构体
const tflite::Model* model = ::tflite::GetModel(g_model);
3. 创建OpsResolver对象
这里是创建包含所有算子的对象
tflite::AllOpsResolver resolver;
如果是根据实际使用的算子创建OpsResolver, 使用
static tflite::MicroMutableOpResolver<5> micro_op_resolver;
micro_op_resolver.AddConv2D();
micro_op_resolver.AddDepthwiseConv2D();
micro_op_resolver.AddFullyConnected();
micro_op_resolver.AddMaxPool2D();
micro_op_resolver.AddSoftmax();
4. 提供一段连续的内存,用于model的内存(placement new)
uint8_t tensor_arena[tensor_arena_size];
5. 创建MicroInterpreter对象
MicroInterpreter(const Model* model, const MicroOpResolver& op_resolver,
uint8_t* tensor_arena, size_t tensor_arena_size,
ErrorReporter* error_reporter,
tflite::Profiler* profiler = nullptr);
构造函数的参数类型是基类,而创建对象的参数是派生类的引用或指针,实现了多态性
tflite::MicroInterpreter interpreter(
model, resolver, tensor_arena, tensor_arena_size, µ_error_reporter);
6. 为所有的Tensor/ScratchBuffer分配内存,为所有支撑的变量分配内存等
interpreter.AllocateTensors();
//因内存的大小是个超参数常量,可以通过使用情况调整下,如参考arena_used_bytes()
interpreter.arena_used_bytes();
7. 得到输入对应的Tensor
TfLiteTensor* input = interpreter.input(0);
检测input tensor的一些属性
TF_LITE_MICRO_EXPECT_EQ(2, input->dims->size);
// The value of each element gives the length of the corresponding tensor.
// We should expect two single element tensors (one is contained within the
// other).
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[1]);
// The input is a 32 bit floating point value
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, input->type);
8. Provide an input value赋值input tensor
input->data.f[0] = 0.;
9. 进行推断
interpreter.Invoke();
10. 得到推断结果
// Obtain a pointer to the output tensor and make sure it has the
// properties we expect. It should be the same as the input tensor.
TfLiteTensor* output = interpreter.output(0);
//检测推断结果的属性
TF_LITE_MICRO_EXPECT_EQ(2, output->dims->size);
TF_LITE_MICRO_EXPECT_EQ(1, output->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(1, output->dims->data[1]);
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, output->type);
// Obtain the output value from the tensor
float value = output->data.f[0];
按上面的步骤,分析下代码实现, 尽量明白一个 AIEngine reference的实现。
ErrorReporter
ErrorReport class 是用来输出log, 各个平台如PC/ 串口打印log的具体实现不同,使用虚函数继承是合理的,接口类Class ErrorReporter:lite/core/api/error_report.h
虚函数是virtual int Report(const char*, va_list args), 普通成员函数调用虚函数也得到了跨平台的接口。
对外的接口是个宏: 其中... 表示所有输入
#define TF_LITE_REPORT_ERROR(reporter, ...) \
do { \
static_cast<tflite::ErrorReporter*>(reporter)->Report(__VA_ARGS__); \
} while (false)
TF_LITE_REPORT_ERROR(error_reporter_,
"Tensor index %d", index);
展开为:
static_cast<tflite::ErrorReporter*>(reporter)->Report(Tensor index %d", index); 强制类型转换
static_cast<tflite::ErrorReporter*>(reporter) 把reporter转换为基类,调用函数Report(const char* format, ...) 而Report的实现又调用虚函数得到log的具体实现。
tflite::Model
Model文件包含的数据包括: subgraphs(子图) 每个子图中包括输入、输出的tensor索引号和tensors
model的buffer里保存的是tensor相关的数据,model的operator_codes里包括了每个算子的具体信息如op_code(操作码),算子的input/oupt(tensor的索引号),算子的custom/builtin data.
模型的量化信息,
OpResolver
算子决议,根据opcode找到具体的实现
算子决议的实现方法
调用AddBuiltin把opcode和对应的TfLiteRegistration, BuiltinParseFunction注册到数组中,最后通过GetRegistrationFromOpCode得到对应的TfLiteRegistration。
算子的添加
因为MicroMutableOpResolver是个模板类,算子的个数是个变量;上面又封装了一层,屏蔽掉TfLite中使用的算子的version信息;
static tflite::MicroMutableOpResolver<5> micro_op_resolver; // NOLINT
micro_op_resolver.AddConv2D();
micro_op_resolver.AddDepthwiseConv2D();
micro_op_resolver.AddFullyConnected();
micro_op_resolver.AddMaxPool2D();
micro_op_resolver.AddSoftmax();
TfLiteStatus AddConv2D() {
return AddBuiltin(BuiltinOperator_CONV_2D, Register_CONV_2D(), ParseConv2D);
}
TfLiteStatus AddBuiltin(tflite::BuiltinOperator op,
const TfLiteRegistration& registration,
MicroOpResolver::BuiltinParseFunction parser) {
registrations_[registrations_len_] = registration;
registrations_[registrations_len_].builtin_code = op;
registrations_len_++;
builtin_codes_[num_buitin_ops_] = op;
builtin_parsers_[num_buitin_ops_] = parser;
num_buitin_ops_++;
return kTfLiteOk;
}
算子的实现有三个部分: builtin_code, TfLiteRegistration, and parser三部分,builtin_code这个值和flatbuffer model里的是一致的(通过它做匹配),parese是从 flatbuffer model 里解析出 init_data.
TfLiteRegistration的函数在不同的时间点被调用, init 调用init_data 进行初始化操作, prepare分配在 invoke中会使用的buffer, invoke是算子的执行
TfLiteRegistration
AI reference 的终极目的是调用invoke 函数, invoke函数的实现只需要参数TfLiteConext and TfLiteNode, 而TfLiteContext 此时不能在分配内存,只能通过GetScratchBuffer获得内存
// Prepare is done, we're ready for Invoke. Memory allocation is no longer
// allowed. Kernels can only fetch scratch buffers via GetScratchBuffer.
context_.AllocatePersistentBuffer = nullptr; context_.RequestScratchBufferInArena = nullptr;
context_.GetScratchBuffer = context_helper_.GetScratchBuffer;
这么看invoke最重要的参数是TfLiteNode. 其中的inputs/outputs tensors提供了 invoke的输入和输出。
tf:Model中的算子在代码里表示
tf:Model中的算子在代码里表示NodeAndRegistration 或者说是算子的输入和算子的执行体
MicroInterpreter构造函数
MicroInterpreter::MicroInterpreter(const Model* model,
const MicroOpResolver& op_resolver,
uint8_t* tensor_arena,
size_t tensor_arena_size,
ErrorReporter* error_reporter,
tflite::Profiler* profiler)
: model_(model),
op_resolver_(op_resolver),
error_reporter_(error_reporter),
allocator_(*MicroAllocator::Create(tensor_arena, tensor_arena_size,
error_reporter)),
tensors_allocated_(false),
initialization_status_(kTfLiteError),
eval_tensors_(nullptr),
context_helper_(error_reporter_, &allocator_, model),
input_tensor_(nullptr),
output_tensor_(nullptr) {
Init(profiler);
}
初始化列表中创建了类: MicroAllocator, ContextHelper
MicroAllocator
MicroAllocator的作用是从arena中分配内存,具体实现是成员变量 class SimpleMemoryAllocator
这是代码最复杂的部分,下面会详述
ContextHelper
ContextHelper是为to encapsulate the implementation of APIs in Context.
TfLiteContext包含了不少函数指针,ContextHelper封装这些函数的实现,或者说提供了这些函数的实现。
context_.AllocatePersistentBuffer = context_helper_.AllocatePersistentBuffer;
context_.RequestScratchBufferInArena = context_helper_.RequestScratchBufferInArena;
context_.GetScratchBuffer = context_helper_.GetScratchBuffer;
class的static member function 作为callback 或者说类似C function
或者这么说 class的non static member function 不适合作为回调函数
class ContextHelper {
public:
// Functions that will be assigned to function pointers on TfLiteContext:
static void* AllocatePersistentBuffer(TfLiteContext* ctx, size_t bytes);
static TfLiteStatus RequestScratchBufferInArena(TfLiteContext* ctx,
size_t bytes,
int* buffer_idx);
static void* GetScratchBuffer(TfLiteContext* ctx, int buffer_idx);
static void ReportOpError(struct TfLiteContext* context, const char* format,
...);
static TfLiteTensor* GetTensor(const struct TfLiteContext* context,
int tensor_idx);
static TfLiteEvalTensor* GetEvalTensor(const struct TfLiteContext* context,
int tensor_idx);
}
static function 怎么访问 data member, member function
明确一点static funciton 类似普通的C 函数工作,不能展开为包含对象指针的函数,所以是没有办法访问 data member 和non static member function。
当可以通过其他的方法,如得到一个对象就可以访问data member 和 member function
void* ContextHelper::GetScratchBuffer(TfLiteContext* ctx, int buffer_idx) {
ContextHelper* helper = reinterpret_cast<ContextHelper*>(ctx->impl_);
ScratchBufferHandle* handle = helper->scratch_buffer_handles_ + buffer_idx;
return handle->data;
}
context_.impl_ = static_cast<void*>(&context_helper_);
MicroInterpreter::AllocateTensors()
AllocateTensors之所以这么复杂,是为了内存的复用,如算子1:tensor占用的内存,到算子3时如果不用了,这块内存就可以复用,怎样知道那些可以复用也就是内存 planner 的实现GreedyMemoryAllocator 贪婪算法。
AllocateTensors() 分为 4个阶段
1. allocate::StartModelModelAllocation(),
2. model init stage,
3. model prepare stage and
4. allocate::FinishModelAllocation()
1. StartModelModelAllocation
1.1] 为每个tensor分配eval_tensor;
1.2] 为每个op分配 node_and_registration, 为将要使用的算子分配 kMaxScratchBuffersPerOp (8)个 ScratchBufferRequest;
1.3] PrepareNodeAndRegistrationDataFromFlatbuffer遍历所有算子,调用GetRegistrationFromOpCode得到对应的 registration,
由registration中注册的parser函数从flatbuffer model 中得到 op 相关的custom/ builtin data, 并保存到对应的 TensorNode中
2. model init stage
2.1. 此时TfliteContext的内容,分配内存的方式只有AllocatPersistentBuffer,这也是合理的 init 只被调用一次,所以内存要是persistent的
2.2. init stage 主要是调用registration->init(context, init_data, init_data_size)
init函数的参数包括TfliteContext, init_data是node的实现者分配和使用的
registration->init()的返回值保存到 TensorNode的user_data成员中, init_data要和builtin_data区别开,builtin_data是算子的parse函数分配和赋值的(usually a structure defined in builtin_op_data.h)
3. model prepare state
3.1 此时TfliteContext的内容,分配内存的方式增加了RequestScratchBufferInArena,
3.2 registration->prepare(context, node)函数的参数是node, 函数调用中调用内存的方式应该是RequestScratchBufferInArena, 使用header buffer
中的ScratchBuffer, 然后调用FinishPrepareNodeAllocations关联 scratch buffer到 node
4. FinishModelAllocation
FinishModelAllocation 函数的功能是TfLiteEvaTensor 和Prepare stage阶段需要的内存诉求,怎样得到满足且分配的内存可以复用,因此引入了贪婪算法
去满足要求且尽量memory复用。这里不在详细解释这部分代码,可以看下
但一定要知道到此 内存策略已经生效,TfLiteEvaTensor 和 ScratchBufferHandle 内存地址成员已经赋值。
引入TfLiteEvalTensor
为了减少Memory的使用引入了TfLiteEvalTensor 保留了op必须使用的成员
SimpleMemoryAllocator
SimpleMemoryAllocator 管理一个线性数组(memory arena)
memory 分为3部分:
head(scratch buffer: GreedyMemoryPlanner策略的内存),
tail(persistent 部分,内容的生命周期 persistent),
temp(head, tail间的内存,使用后马上释放部分)
online/offline
memory 怎么plann是怎么得到?一种方法是offline 也就是 model文件的meta data里已经提供memory plann, 而online是 MicroInterpreter根据model 文件的内容 online进行计算得到memory planner
Tensors are allocated differently depending on the type of tensor.
Weight tensors are located in the flatbuffer, which is allocated by the application that calls TensorFlow Lite Micro.
EvalTensors are allocated in the tensor arena, either offline planned as specified in the flatbuffers metadata, or allocated during runtime by the memory planner (online planned). The tensor arena is allocated by MicroAllocator in TensorFlow Lite Micro, and the model buffer (represented by a .tflite-file) is allocated by the application using TensorFlow Lite Micro.
MicroInterpreter::input/output
怎样给model 输入数据进行推断,怎样得到推断输出?
1. 从Persistent buffer zone申请内存用于 TfLiteTensor
2. 关联TfLiteTensor到eval_tesnor, graph inputs等
3. 得到tensor 相关的data address
3.1 input->data
input->data.f[0] = 0.;
// Provide an input value
const float* ring_features_data = g_ring_micro_f9643d42_nohash_4_data;
TF_LITE_REPORT_ERROR(µ_error_reporter, "%d", input->bytes);
for (size_t i = 0; i < (input->bytes / sizeof(float)); ++i) {
input->data.f[i] = ring_features_data[i];
}
3.2 output->data
// Get the output from the model, and make sure it's the expected size and
// type.
output = interpreter.output(0);
TF_LITE_MICRO_EXPECT_EQ(2, output->dims->size);
TF_LITE_MICRO_EXPECT_EQ(1, output->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(4, output->dims->data[1]);
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, output->type);
// Make sure that the expected "Slope" score is higher than the other classes.
wing_score = output->data.f[kWingIndex];
ring_score = output->data.f[kRingIndex];
slope_score = output->data.f[kSlopeIndex];
negative_score = output->data.f[kNegativeIndex];
TF_LITE_MICRO_EXPECT_GT(slope_score, wing_score);
TF_LITE_MICRO_EXPECT_GT(slope_score, ring_score);
TF_LITE_MICRO_EXPECT_GT(slope_score, negative_score);
MicroInterpreter::invoke
最后进行reference的过程,看上去却很简单, 遍历operator, 分别调用每个registration的invoke 函数
总结
以TFLM AIEngine 的使用为线索,分析了AIEngine的整个框架,可以AIEngine的代码并不复杂,包括模型的解析,内存的planner 和算子的实现,其中工作量大部分是算子的实现,后面会分析几个算子的实现。
Todo:
comiple/target相关/macro/log/const/ 模式元素