ysh329 / OpenCL-101

Learn OpenCL step by step.
123 stars 31 forks source link

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

Open ysh329 opened 3 years ago

ysh329 commented 3 years ago

OpenCL后端背景

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

这也是很多业务考虑使用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 commented 3 years ago

业务侧常见问题

支持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 commented 3 years ago

业务侧问题解决和思路

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

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 commented 3 years ago

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

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

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

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

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