泛化性较强一些基本思路(1)

遇事不决,默念口诀

Posted by Xiaofei on September 10, 2022

在这个系列里,打算总结一下作为一个程序员,在我日常遇到的许多典型场景下的基本思路,可以先按照这个手册来进行debug,避免走一些歪路

场景一:这个程序比较速度慢

这应该是一个现实中常见的场景。一般而言,程序比较慢的原因包括(以下可能是废话):

  • GPU瓶颈
  • CPU瓶颈
  • IO瓶颈

但是无论哪种,首先要利用profiling工具看一下是否能找到瓶颈,参见:Python Profiling

但是这一篇里,我们来说一些通用的可能检查项

GPU瓶颈

判断GPU瓶颈的方法很简单:watch nvidia-smi看GPU利用率,如果一直很高大概率就是GPU瓶颈,这里要注意波动情况,波动太大说明还是有cpu瓶颈或者io瓶颈在的

GPU利用率高基本上没啥好方法,能立刻想到的方向包括:

  1. 半精度:相当有效,直接速度提升一倍,需要注意的是最好做一下回归测试,拿一些例子跑一下,看一下不一致的比例是多少
  2. 算法优化:这个要具体问题具体分析,就不展开讲了,典型的比如
    1. 减少重复计算,大部分需要根据业务场景来设计,要评估某项举措是否只影响了极小的情况,如果是,那么才能继续推动
    2. 或者控制一些参数,比如beam-search的大小等等
    3. 分batch前先排序

CPU瓶颈

CPU的瓶颈也相对简单,直接htop看CPU利用率一直很高就行。

CPU的瓶颈原因就太多了,但是如果是预期内的慢操作,我们也不会太过惊讶,所以,我们这边列举一些常见的【将低复杂度操作写成高复杂度操作】的例子:

  1. 在list中执行in操作判断:in操作在list中是O(n)复杂度,在set中是O(1)复杂度(对于平衡树的是O(log n))

IO瓶颈

有的时候我没看GPU和CPU利用率其实不高,这时候就要考虑IO瓶颈。

IO瓶颈我认为是三种瓶颈里最不容易察觉的,坑太多,一不小心就掉进去了

这里给一些典型的场景:

  1. 反复申请内存:比如有一次我做一个jaccard similarity的举措,结果反复申请了一个矩阵,导致某个相似性矩阵反复创建
  2. 使用Triton的时候没有使用grpc方式而是使用了http方式:50%的性能损失
  3. 反复垃圾回收:比如在python里超大for循环,如果有tqdm,有的时候可能会发现【进度一卡一卡的】,这时候有可能是反复在进行垃圾回收
  4. GPU和CPU的反复数据交换:典型的比如在一个GPU向量上进行循环
  5. 赋值操作(可以用默认值代替):这个不太常见,比如,我要给一个矩阵的某些位置赋值,那么,如果要赋的值已经等于矩阵的默认值(典型的是0),那么跳过这个赋值,就可以起到一定的优化作用
  6. 多进程时反复创建大对象:比如用ProcessPoolExecutor.map的时候,传入的function里会创建一个大的对象
  7. 数据库一条条请求:能用batch就用batch吧(或者叫bulk、chunk,各位看官搜索的时候都可以试试)

场景三:docker build太慢了

三个思路:

  1. 直接先搞一个基础镜像,把比较慢的包或者其他步骤搞一下,然后基于这个基础镜像构建新的镜像
  2. 在上一个version的基础上进行构建,比如这一次我们执行docker build -t tmp .,那么下一次Dockerfile里可以写:FROM tmp:latest
  3. 如果只是代码修改,不是环境的修改,可以直接通过挂载目录的形式进行代码更新,不需要重新打镜像

场景二:镜像build失败了(卡住了)

一些思考如下:

  1. 最重要的是——看错误提示
  2. 基础镜像能不能访问:有的时候为了加速我们会使用之前创建好的本地镜像,看一下这次是否是本地镜像然而却没有创建
  3. apt-get install等是否需要互动:这个会造成卡住,比如需要输入y才能继续,这时候可以查一下怎么跳过,比如apt-get的话可以加-y
  4. 避免直接对着docker build进行debug:可以进入基础镜像内进行操作——先通过docker run进入基础镜像,对照着Dockerfile一步一步执行,看一下是哪一步出了错,解决后,继续进行下一步,直到整个Dockerfile的步骤直行通过,再进行整体的docker build。这样做可以防止浪费时间在已经搞定的步骤上(知道docker build有缓存,但是有的时候不靠谱啊)

场景四:单步调试

一些思考如下:

  1. 如果想要进入for循环里比较特殊的一个部分,可以通过加if然后输出的形式来做,具体而言,如果我想在for idx in range(100)这个循环里在idx=50时暂停,我需要的是

    1
    2
    3
    4
    
    for idx in range(100):
        if idx == 100:
            print("stop here")
        other_codes
    

    然后在第三行打断点

  2. 如果程序启动太慢,而又不确定问题在哪儿,要反复单步调试,这时候其实可以先初始化好(例如加载机器学习模型或者读取大文件),然后写一个死循环比如while 1来反复调用我们的功能性函数,过了断点位置只需要等待下一次循环重新进入即可,不需要重新启动程序