ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TFLite Micro 性能剖析(Profiling)实践指南:从算子级耗时定位到内核子例程瓶颈

TFLite Micro 性能剖析(Profiling)实践指南:从算子级耗时定位到内核子例程瓶颈 人工智能深度学习推理引擎本地部署嵌入式物联网【免费下载链接】tflite-microInfrastructure to enable deployment of ML models to low-power resource-constrained embedded targets (including microcontrollers and digital signal processors).项目地址https://gitcode.com/gh_mirrors/tf/tflite-micro点击查看免费下载导读本文围绕 TensorFlow Lite for MicrocontrollersTFLite Micro官方文档 profiling.md 展开系统讲解如何在资源受限的嵌入式目标微控制器、DSP上借助 TFLite Micro 内置的 Profiler 采集每次算子调用per-op invoke的耗时并进一步下钻到算子内核内部子例程定位性能瓶颈。读完本文你将掌握 MicroProfiler 的 API 用法、MicroInterpreter 的 per-op 剖析启用方式、子例程级剖析的手动埋点技巧以及如何结合仓库中的 keyword_benchmark 示例编写完整的基准测试程序。一、Profiling 概述TFLite Micro 的剖析能力从哪来TFLite Micro 是为低功耗、资源受限的嵌入式目标包括微控制器与数字信号处理器设计的推理框架。其 Profiler 的目标非常聚焦获取每次算子调用per-op invoke的耗时并从算子内核内部定位热点代码段为算子级优化与端到端基准测试提供量化依据。整个剖析能力的核心是 micro_profiler.h 中定义的tflite::MicroProfiler类以及由 micro_profiler_interface.h 定义的抽象接口tflite::MicroProfilerInterface。接口层只暴露两个纯虚方法class MicroProfilerInterface { public: virtual ~MicroProfilerInterface() {} virtual uint32_t BeginEvent(const char* tag) 0; // 标记事件开始返回事件句柄 virtual void EndEvent(uint32_t event_handle) 0; // 标记事件结束 };这种接口抽象意味着框架内部如 MicroInterpreter、压缩解压子系统只依赖MicroProfilerInterface编程而具体实现可以由用户替换——默认的MicroProfiler是大多数场景下的即用实现其源码位于 micro_profiler.cc。注意官方文档中提到的tflite::Profiler默认实现路径在本文所述版本中对应为 micro_profiler.cc 中的tflite::MicroProfiler它实现了BeginEvent/EndEvent两个方法可直接用于绝大多数场景。二、API 速览MicroProfiler 的核心接口MicroProfiler对外暴露以下关键接口详见 micro_profiler.h接口作用uint32_t BeginEvent(const char* tag)标记事件开始返回一个事件句柄供EndEvent配对使用tag的存活期必须超过 MicroProfiler 本身void EndEvent(uint32_t event_handle)标记事件结束调用者需保证每个句柄恰好调用一次重复调用以最后一次为准漏调则该事件耗时为 0void ClearEvents()清空当前已记录的所有事件uint32_t GetTotalTicks()返回所有事件 tick 数之和仅在事件互不重叠前一个事件结束时间 ≤ 下一个事件开始时间时有意义void Log()以人类可读形式打印每个事件的剖析信息took %u ticks (%d ms)void LogCsv()以 CSV 格式打印事件信息void LogTicksPerTagCsv()按 tag 聚合以 CSV 格式输出每个唯一 tag 的累计 tick 数及总 tick 数2.1 计时与事件存储的实现细节从 micro_profiler.cc 可以看到BeginEvent与EndEvent的底层实现uint32_t MicroProfiler::BeginEvent(const char* tag) { if (num_events_ kMaxEvents) { /* 达到上限时报错并断言 */ } tags_[num_events_] tag; start_ticks_[num_events_] GetCurrentTimeTicks(); end_ticks_[num_events_] start_ticks_[num_events_] - 1; return num_events_; } void MicroProfiler::EndEvent(uint32_t event_handle) { TFLITE_DCHECK(event_handle kMaxEvents); end_ticks_[event_handle] GetCurrentTimeTicks(); }值得注意的实现约束事件容量上限内部以静态数组存储kMaxEvents 4096micro_profiler.h事件数超过上限会触发断言失败需要更多事件时需自行增大该常量tick 的含义随平台变化GetCurrentTimeTicks()与ticks_per_second()由各目标平台实现见 micro_time.hTicksToMs(ticks)的换算公式为1000.0f * ticks / ticks_per_second()且对ticks_per_second为 0 的情况做了除零保护Log 系列输出默认裁剪Log()、LogCsv()、LogTicksPerTagCsv()的实现体都包裹在#if !defined(TF_LITE_STRIP_ERROR_STRINGS)内即发布构建定义该宏时输出为空平台适配分支LogCsv()中对HEXAGON/CMSIS_NN平台使用int格式化其余平台使用uint32_t说明 CSV 导出在不同 DSP/加速器后端上有细微差异。2.2 多轮调用的最佳实践ClearEvents官方文档特别强调跨多次 invoke 进行剖析的最佳实践是在两次调用之间调用ClearEvents()。这是因为事件数组是累加的若不清理后续Log()会打印历史累积的所有事件且GetTotalTicks()的总和会混入多轮数据。这一实践在 benchmark 示例中体现得非常典型见下文第五节每轮迭代前先profiler.ClearEvents()再执行一次RunSingleIteration()。三、Per-Op Profiling为整个推理过程按算子计时3.1 启用方式MicroInterpreter的构造函数带有可选的 profiler 参数micro_interpreter.hMicroInterpreter(const Model* model, const MicroOpResolver op_resolver, uint8_t* tensor_arena, size_t tensor_arena_size, MicroResourceVariables* resource_variables nullptr, MicroProfilerInterface* profiler nullptr, bool preserve_all_tensors false);启用 per-op 剖析只需两步构造一个MicroProfiler或实现MicroProfilerInterface的自定义剖析器实例将其作为 profiler 参数传给MicroInterpreter构造函数使用非 release 构建编译以关闭NDEBUG宏对 MicroInterpreter 内部ScopedOperatorProfile的裁剪。3.2 底层触发点Invoke 时的 ScopedMicroProfiler从源码结构看per-op 剖析的实际触发点在 micro_interpreter_graph.cc在每次算子invoke之前会创建ScopedMicroProfiler作用域对象并传入由OpNameFromRegistration(registration)得到的算子名// 该 ifdef 是必需的即使 ScopedMicroProfiler 在 -DTF_LITE_STRIP_ERROR_STRINGS 下是 no-op // 因为 OpNameFromRegistration 仅在带错误字符串的构建中定义。 #if !defined(TF_LITE_STRIP_ERROR_STRINGS) ScopedMicroProfiler scoped_profiler( OpNameFromRegistration(registration), reinterpret_castMicroProfilerInterface*(context_-profiler)); #endif TFLITE_DCHECK(registration-invoke); invoke_status registration-invoke(context_, node);ScopedMicroProfiler定义在 micro_profiler.h其构造函数调用BeginEvent(tag)析构函数调用EndEvent(event_handle)——利用 C RAII 机制保证无论算子执行路径如何事件都能成对闭合。这里揭示了三个关键事实release 构建的零开销设计当定义TF_LITE_STRIP_ERROR_STRINGS时ScopedMicroProfiler被替换为一个空的 no-op 类micro_profiler.h从而保证发布版本零剖析开销NDEBUG与TF_LITE_STRIP_ERROR_STRINGS的关系文档中“build with a non-release build to disable the NDEBUG define”所描述的行为在代码中实际由TF_LITE_STRIP_ERROR_STRINGS宏控制——因此编译时不要定义该宏才能让剖析代码生效并拿到带算子名的标签profiler 通过 context 传递MicroInterpreter::Init会把 profiler 写入context_.profilermicro_interpreter.cc算子注册的invoke回调执行期间框架就能通过 TFLiteContext 拿到剖析器。3.3 输出形式与解读调用profiler.Log()后输出形如OpName took 1234 ticks (2 ms).每条记录对应一次算子 invoke。若模型包含多个子图如if/while控制流各子图内的算子也会以同样机制被计时。若需要对多轮结果做统计分析可结合LogTicksPerTagCsv()按算子名聚合快速识别总耗时最高的算子。四、Subroutine Profiling深入算子内核内部的子例程剖析Per-op 剖析只能定位到“哪个算子慢”若要回答“算子内部哪段代码慢”需要子例程级剖析。官方文档给出两种方式从 TFLiteContext 直接使用 MicroProfiler算子实现内部可以通过TFLiteContext获取当前 profiler 引用直接调用BeginEvent/EndEvent或将其包装进ScopedProfile作用域对象利用 RAII 自动配对事件新建独立的 MicroProfiler在拿不到 TFLiteContext 的代码路径中例如非内核模块、工具函数直接创建一个新的MicroProfiler实例进行埋点。官方文档中提到的ScopedProfile位于lite/core/api/profiler.h文档内部链接路径为../../lite/core/api/profiler.h对应仓库根目录为 tensorflow/lite/core/api/profiler.h该头文件在当前仓库位于 tensorflow/lite/core/api 目录下其用法与ScopedMicroProfiler一致构造时开始事件、析构时结束事件。说明若目标构建树中未包含上述头文件路径可直接使用 micro_profiler.h 中的ScopedMicroProfiler达到相同效果——它同样接受(tag, MicroProfilerInterface*)两个参数。4.1 源码中的实际用例压缩解压子系统的剖析埋点仓库中有大量直接使用ScopedMicroProfiler埋点子例程的实例最集中的是张量压缩/解压子系统TFLM Compression。例如 decompress_common.ccvoid DecompressionState::DecompressToBufferWidth4_16(int8_t* buffer) { ScopedMicroProfiler scoped_profiler(__func__, micro_profiler_); ... }同样模式的埋点出现在 decode_state_lut.cc、decode_state_huffman.cc、decode_state_prune.cc 以及 Xtensa 平台特化实现 xtensa/decompress.cc 等文件中覆盖了 LUT 查找、Huffman 解码、Prune 解码等多种解压算法路径。这些子例程通过构造函数的micro_profiler_成员获得 profiler 引用——这正对应官方文档所说的“TFLiteContext 不可用时就近创建一个 MicroProfiler 传给子例程”的实践。4.2 组合使用alternate profiler 支持并行剖析除上述机制外MicroInterpreter还提供了SetAlternateProfiler()micro_interpreter.h允许设置第二个MicroProfilerInterface用于“在 kernel Eval 阶段与剖析子系统并行地进行剖析”当前主要用于张量解压子系统。这意味着你可以用主 profiler 记录算子耗时、用 alternate profiler 记录解压耗时从而实现同一轮推理中两个维度的剖析数据同时采集。五、端到端实战仿照 keyword_benchmark 编写剖析程序仓库中的 keyword_benchmark.cc 是 Profiler 集成方式的官方示范它把本文前述的所有知识点串成了一条完整的实战链路。5.1 构建 BenchmarkRunner 并注入 profilerKeywordBenchmarkRunner* CreateBenchmarkRunner(MicroProfiler* profiler) { KeywordOpResolver* op_resolver new (op_resolver_buffer) KeywordOpResolver(); op_resolver-AddFullyConnected(tflite::Register_FULLY_CONNECTED_INT8()); op_resolver-AddQuantize(); op_resolver-AddSoftmax(tflite::Register_SOFTMAX_INT8_INT16()); op_resolver-AddSvdf(tflite::Register_SVDF_INT8()); return new (benchmark_runner_buffer) KeywordBenchmarkRunner(g_keyword_scrambled_model_data, op_resolver, tensor_arena, kTensorArenaSize, profiler); }MicroBenchmarkRunner模板micro_benchmark.h内部创建RecordingMicroAllocator与RecordingMicroInterpreter并将 profiler 透传给解释器构造函数——因此该示例天然支持 per-op 剖析。注意注释中的生命周期要求model、op_resolver、tensor_arena、profiler 的存活期必须超过 BenchmarkRunner 对象这是嵌入式裸内存分配placement new场景下的典型约束。5.2 多轮迭代 ClearEvents GetTotalTicks 的规范模式void KeywordRunNIerations(int iterations, const char* tag, KeywordBenchmarkRunner benchmark_runner, MicroProfiler profiler) { int32_t ticks 0; for (int i 0; i iterations; i) { benchmark_runner.SetRandomInput(i); profiler.ClearEvents(); // 每轮前清空历史事件 benchmark_runner.RunSingleIteration(); // 一次 Invoke ticks profiler.GetTotalTicks(); // 累加本轮总 tick } MicroPrintf(%s took %d ticks (%d ms), tag, ticks, TicksToMs(ticks)); }这段代码同时体现了三个要点ClearEvents()位于每次 Invoke 之前保证GetTotalTicks()只统计当前轮次这正是官方文档强调的最佳实践RunSingleIteration()内部调用interpreter_.Invoke()见 micro_benchmark.h因此每轮都会触发所有算子的 ScopedMicroProfiler 记录TicksToMs()负责把平台相关 tick 换算为毫秒。5.3 main 函数中的手动埋点与日志输出int main(int argc, char** argv) { tflite::InitializeTarget(); tflite::MicroProfiler profiler; uint32_t event_handle profiler.BeginEvent(InitializeKeywordRunner); tflite::KeywordBenchmarkRunner* benchmark_runner CreateBenchmarkRunner(profiler); profiler.EndEvent(event_handle); profiler.Log(); // 打印初始化阶段耗时 ... tflite::KeywordRunNIerations(1, KeywordRunNIerations(1), *benchmark_runner, profiler); profiler.Log(); ... }main中展示了子例程级剖析的手动埋点范式用BeginEvent(InitializeKeywordRunner)标记初始化开始、EndEvent(event_handle)标记结束再通过Log()输出。这就是官方文档“Subroutine Profiling”一节所描述的BeginEvent/EndEvent直接调用方式。5.4 编译与运行要点目标构建请参考 benchmarks/BUILD 中keyword_benchmark目标的依赖micro_profiler、micro_framework、op_resolvers等若要看到 per-op 的算子名明细请使用非 release 构建不定义TF_LITE_STRIP_ERROR_STRINGS/ 不以 release 方式定义NDEBUGInitializeTarget()与TicksToMs的行为依赖目标平台对 micro_time.h 中ticks_per_second()、GetCurrentTimeTicks()的实现不同 MCU/DSP 上 tick 的实际物理含义不同跨平台对比耗时时应统一以毫秒TicksToMs换算后为准。六、常见问题与注意事项6.1 为什么 release 构建看不到剖析输出因为ScopedMicroProfiler在定义TF_LITE_STRIP_ERROR_STRINGS时是 no-opmicro_profiler.hLog()系列方法的主体也在该宏下被裁剪micro_profiler.cc。这是刻意设计保证发布版本零剖析开销。剖析时务必使用非 release 构建。6.2 事件上限 4096 不够怎么办MicroProfiler使用固定大小数组存储事件micro_profiler.h超过kMaxEvents会触发断言。模型算子很多或循环剖析时会逼近该上限对策包括增大kMaxEvents常量、或在多轮剖析之间及时调用ClearEvents()。6.3 剖析开销本身会影响耗时吗会。BeginEvent/EndEvent每次调用都要读取平台时钟GetCurrentTimeTicks()并在 debug 构建中维护 4096 项数组。因此剖析数据适用于相对比较哪个算子更慢、优化前后对比而非绝对精确的实时指标正式发布前应使用 release 构建重新验证端到端性能。七、总结TFLite Micro 的剖析能力分为三层接口层MicroProfilerInterface的BeginEvent/EndEvent、默认实现层MicroProfiler的 tick 记录、Log/LogCsv/LogTicksPerTagCsv输出、ClearEvents复位、集成层MicroInterpreter 构造参数注入、micro_interpreter_graph.cc中每次算子 invoke 前的ScopedMicroProfiler自动埋点、SetAlternateProfiler并行剖析。掌握这三层之后你可以从“整模型耗时”一路下钻到“单个算子耗时”再到“内核子例程耗时”配合 keyword_benchmark.cc 这样的基准框架在真实嵌入式目标上完成可复现的性能量化与瓶颈定位。进一步研究可阅读 micro_profiler.h、micro_profiler.cc、micro_profiler_interface.h 三个核心文件以及 micro_time.h 了解平台时钟抽象。赞分享人工智能深度学习推理引擎本地部署嵌入式物联网【免费下载链接】tflite-microInfrastructure to enable deployment of ML models to low-power resource-constrained embedded targets (including microcontrollers and digital signal processors).项目地址https://gitcode.com/gh_mirrors/tf/tflite-micro点击查看免费下载相关推荐PyTorch Lightning 中级性能剖析指南用 PyTorchProfiler 定位 PyTorch 算子级瓶颈PyTorch Lightning 中级性能剖析指南用 PyTorchProfiler 定位 PyTorch 算子级瓶颈 导读 本文面向已经掌握基础剖析如人工智能深度学习机器学习预训练分布式训练微调CANN triton-inference-server-ge-backend Profiling 采集指南从算子级数据采集到性能瓶颈定位CANN triton inference server ge backend Profiling 采集指南从算子级数据采集到性能瓶颈定位 Profiling模型推理服务人工智能后端CANNAscendPyTorch Lightning Profiler 完整指南从训练循环到算子级性能瓶颈定位PyTorch Lightning Profiler 完整指南从训练循环到算子级性能瓶颈定位 导读 本文围绕 PyTorch Lightning 内置的 Pr人工智能深度学习机器学习预训练分布式训练微调上一篇茉莉花插件为Zotero注入中文文献处理能力下一篇3分钟实现视频PPT智能提取告别手动截图的实用自动化方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表