Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

移动端OPENCL后端模型业务支持 #52

Open
ysh329 opened this issue Feb 21, 2021 · 3 comments
Open

移动端OPENCL后端模型业务支持 #52

ysh329 opened this issue Feb 21, 2021 · 3 comments

Comments

@ysh329
Copy link
Owner

ysh329 commented Feb 21, 2021

OpenCL后端背景

不少业务用OpenCL后端,是因为对其抱有一定的期望,包含不限于:

  • 获得比CPU更好的性能;
  • 减轻对CPU的负载,因为CPU往往还有业务/其它算法逻辑;
  • 利用上GPU的能力,因为业务没有GPU渲染方面的工作或者很少。比方在给定设备如RK3399等等。

这也是很多业务考虑使用GPU的原因。

后端业务支持流程

OpenCL后端支持模型的一大考量就是性能,一般来说,在ADB shell环境获取到的性能会比Android APP环境的性能要好,而且所有APP的使用条件是不绑定大核的,因此性能自然会弱一些<10%。所以,算法同学在训练模型时,首先的考虑便是结构的确定,见下面流程:

  1. 确定模型结构:算法同学训练的模型需要遵循一定的设计原则,匹配底层库的优化思路,避免未被优化地方,性能在部分结构上可能相差1~5倍;
  2. 预跑性能:确定结构,需要在APP环境打通完整业务逻辑流程,将预先设定的模型结构(可以是未被训练的)在APP里获取到执行时间,这个时间相比ADB shell环境更加可靠。该性能是否满足业务上线预期,再结合第3和第4点,这是一个循环
  3. 结构评估:在预跑性能时,可能在转换模型前或者运行期,遇到某些OP不支持或OP的某种情况不支持的情况,需要加以支持;
  4. 性能优化:得到APP或ADB shell性能。如框架开发更关注后者,可通过模型结构分析两个方面:
    1. 粗粒度:缺少GPU算子,结构冗余。可以加上GPU算子的实现,通过模型OP融合/消除策略将问题结构解决;
    2. 细粒度:Profiler该模型架构,得到网络中所有OP的执行时间:
      1. 对非Conv(即非计算密集型)算子:时间是否存在占比较大问题;
      2. 对Conv算子:模型中某些Conv算子是否走到通用的实现上,实现是否存在可优化的空间等等。
@ysh329 ysh329 changed the title 移动端业务对接 移动端OPENCL后端模型业务支持 Feb 21, 2021
@ysh329
Copy link
Owner Author

ysh329 commented Feb 22, 2021

业务侧常见问题

支持opencl后端的手机众多,从gpu厂商到型号都有较大差异,在对接过程中也遇到极大的困难和挑战,可以将问题归类如下:

OpenCL后端初始化:手机GPU特殊机型case

  1. cl::Context与KernelContext/CLContext:与Lite框架里的定义存在不同,Lite框架里有KernelContext为每个Kernel持有,但对OpenCL来说,一个设备一个CL::Context;
  2. libopencl.so:某些机型不具有libopencl.so;
  3. symbol:某些机型在libopencl.so里的符号不全,即lite框架里用到的符号在该手机的opencl动态库里没有。分为两种,必须和非必须;
  4. cl设备:某些机型即使可以找到libopencl.so且符号完整,但在初始化设备类型时即OEPNCL_DEVICE_TYPE,会出现初始化失败的情况,即获取到的设备类型既不是CPU也不是GPU。在一款较老的arm mali gpu上遇到过;
  5. 安卓系统限制:在某些小众安卓OS系统如魅族,其厂商在系统层面对APP开发者在调用系统库如libopencl.so时进行了屏蔽,能使用的系统库存在一个或多个白名单位于手机上,能否使用该系统库需要查看白名单上是否具有该系统库;
  6. 对OpenCL后端的初始化容错:框架层面的问题,对OpenCL的初始化应该及时容错,即使初始化失败也不能Segfault挂掉(这种必须要解决),而是应该分为必要的abort和非必要的abort。
    1. 非必要abort:即必须容错,是初始化的过程,如CPU+GPU库,但即使是跑CPU模型,也有一定的对GPU的初始化流程,该过程不能因为该设备对GPU存在支持问题,导致挂掉影响了CPU的计算流程,这部分代码需要一定的解耦,同时要禁用abort;
    2. 必要abort:当CPU+GPU库跑GPU模型时,已成功完成必要的OpenCL初始化(无论CPU还是GPU模型,都必须经过的),在再后续的OpenCL初始化时,对不支持的情况务必abort。这时,就是已经确定是要运行GPU,但是确实有OpenCL的问题,那就必须abort。
    3. 业务层面的容错:业务层面对框架代码必须考虑容错,try-catch捕获异常。

驱动

  1. OpenCL驱动API内存泄露:arm mali,某荣耀机型

Kernel计算异常

  1. Mali/Adreno实现差异:通常是标量矢量类型等,该报错在build program过程就会暴露;
  2. Mac和移动端差异:image2d sampler采样器在Normalized Coordinates的不同;
  3. 特殊机型(Adreno)矢量数组写入失败:寄存器限制;
  4. 缺少算子通用实现;
  5. 精度FP16误差大:不算bug,通过定位发现是逐层累计造成,类似int8导致的0.49999和0.50006问题,逐层累计,放大了误差。通过一套代码在FP32上运行,不存在精度问题。

用户使用

  1. 模型版本管理:模型中包含了模型转换工具opt的版本信息;
  2. cpu/gpu模型分不清:通过模型文件的明文信息可以确定cpu/gpu模型。

性能不符合预期

前文提到:

  1. 粗粒度:结构优化;
  2. 细粒度:逐个op Pofiler定位调优。

@ysh329
Copy link
Owner Author

ysh329 commented Feb 22, 2021

业务侧问题解决和思路

根据上面遇到的问题,除部分提到的解决思路外,大体解决方法有如下几类:

image

  1. 初始化问题
    1. 提供用户API——isOpenclBackendValid API
    2. 明确不同阶段挂的原因:
      1. isOpenclBackendValid API挂:找库、找符号、平台/设备/Context初始化等;
      2. Predictor创建挂:RuntimeProgram创建、KernelContext/CLContext(非cl::Context)赋值等;
      3. Predictor Run挂:LiteKernel/BuildProgram/OpenCL析构等等。
  2. 精度比对
    1. 依据ARM CPU的结算结果为正确基准,逐层比较精度Profiler;
    2. CL Kernel内:加打印printf对应某一线程的值,定位问题;
    3. FP16存在一定精度损失,可以使用set_opencl_precision跑FP32的;
  3. 性能问题(前文已提,简略)
    1. 结构(粗粒度)
    2. 逐OP(细粒度),性能Profiler

@ysh329
Copy link
Owner Author

ysh329 commented Feb 22, 2021

OpenCL Bug案例:解决和排查思路分析

案例1:不解决:driver内存泄露

  • 表象:业务反馈跑gpu模型出现持续性内存增长;
  • 排查:
    1. 用户API侧:加载模型、设置输入、获取输出、predictor执行分别for循环100w次,定位是predictor Run。涉及框架底层;
    2. 排除特殊op:尝试非业务模型、业务模型,均观察到有内存增长的情况。和业务模型的特殊op无关;
    3. 拆分原始模型为小模型:基于构建模型的脚本生成仅有conv、relu的一层小模型,转为部署模型,也存在内存增长情况。可能和io_copy、layout转换有关;
    4. 单元测试:使用conv/act的单元测试,该单元测试不包括io_copy和layout op转换,发现也存在内存增长;
    5. 进入conv实现:分为PrepareForRun、Run、ReInitWhenNeeded,分别放入100w次的for循环内进一步观察定位泄露点,定位到是Run部分,在对Run里的逐行代码分析,逐行放入100w次的for循环内,定位到是command_queue的enqueueNDRangeKernel在多次循环时有明显内存泄露情况;
    6. 观察是否是框架本身引入的问题:command_queue是实体非指针,观察其他框架tnn的调用方式,也是C++ OPENCL API,也存在泄露问题(需要多次运行),进一步观察其他框架(MNN/TFLite/TNN)也均有内存泄露问题;
    7. 确定该问题是gpu驱动bug,即该opencl的enqueueNDRangeKernel方法存在内存泄露。
  • 解决方法:尝试升级手机系统,得到解决

案例2:解决:特殊机型OpenCL兼容性case-by-case

  • 表象:找不到OpenCL库/库里符号不全/设备初始化失败/kernel类型不兼容/private memory写入失败/不支持FP16/…
  • 解决:
    • Host端问题:增加用户侧API,预判库/符号/设备/精度支持;
    • OpenCL Kernel问题:改写为兼容性的实现(部分机型float与float4不支持互操作/某gpu不支持private的矢量数组定义如float4 a[4]导致部分写入失败);
    • 精度问题:FP16精度低,增加精度设置API后,在一套kernel实现下,FP32精度下结果与ARM CPU一致。

案例3:解决:Conv随机中间结果

  • 表象:安卓adb shell环境始终正确且无法复现,安卓APP环境中间层结果随机次数出错,出错值固定,
  • 排查:
    1. 打印定位:检查conv输入、filter、param参数正确一致
    2. 单元测试定位:同规模无法复现;
    3. 怀疑opencl backend后端问题:中间过程排查了很多后端固有问题,如KernelContext的多Kernel持有问题等;
    4. 二分commit定位:仅半年的提交历史记录中,发现某次解决但后来只是减轻随机出错的次数,一直存在;
    5. 二分模型定位:基于原始模型的Paddle脚本二分模型,二分最小规模的三层模型;
    6. Kernel内打印定位:定位出随机出错的结果,逐步排查反推到conv op的激活函数值未初始化。
  • 原因:conv param的激活函数成员未给初始化值,导致该值计算偶现随机化,arm cpu未出现该问题,但在gpu上出现。

案例4:规避:gpu模型cpu yolo_box计算结果不对

  • 表象:在gpu模型中的cpu yolo_box算子计算结果不对,但cpu模型的yolo_box是正确的;
  • 排查:yolo_box输入参数kernel计算前打印,中间过程计算结果打印;
  • Cpu yolo_box单元测试同规模下无法复现;
  • 解决:规避,实现yolo_box的gpu kernel。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant