PyTorch高效编程实战指南

嵌入式技术

1335人已加入

描述

  1. 能用_all_gather_base的,不用all_gather

  output = torch.empty(input.numel() * world_size, dtype=input.dtype, device=input.device)

  torch.distributed._all_gather_base(output, input, group=xxx)

  vs.

  output_list = [

  torch.empty(input.numel(), dtype=input.dtype, device=input.device)

  for _ in range(world_size)

  ]

  torch.distributed.all_gather(output_list, input, group=xxx)

  output = torch.cat(output_list, dim=0)

  内存碎片更少,操作更少,性能/内存均有收益!

  2. 能用专有算子的,不用通用算子

  如 F.embedding vs. Index-select

  Megatron-LM master实现使用的Index-select算子,Index-select会涉及索引展开、内存复用等HostCPU逻辑,效率较低

  3. 对于生命周期较长的Tensors,可以共用contiguous buffer

  data = torch.zeros(global_size, dtype=xx, device=xx)

  start_idx = 0

  for i in range(len(item_list)):

  item_list[i] = data[start_idx:start_idx+item_list[i].numel()].view(item_list[i].shape)

  torch.cuda.empty_cache() # 清空原始已释放的item list数据

  CUDA内存池是对齐分配的,使用分散的block会带来内存碎片,同时对于相同操作,可以直接对contiguous buffer进行操作,减少了更多的算子下发,大块计算效率也会更高。

  4. 尽可能使用异步通信,提高计算/通信overlap

  comm_handle = torch.distributed.all_reduce(data, group=xxx, async_op=True)

  。.. # 省略若干计算代码

  comm_handle.wait()

  对应中间的计算就能够跟通信进行overlap,只要我们提前梳理好网络拓扑,完全是没问题的。

  5. 对于输入数据size频繁变化的场景,使用Expandable Segments

  PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True

  跟cudaMalloc直接分配Kernel可访问的内存地址不同,该机制操作的是虚拟内存空间(对应的物理内存地址不具备访问权限),可以通过驱动map更多的物理内存在已分配的block的后面,从而使得segments可向上扩展,一定程度上提高了cache match的效率,减少内存碎片。

  6. 在适当时机清空缓存可以大幅降低内存占用

  torch.cuda.empty_cache()

  在训练任务初始化时,经常会创建一些临时的设备Tensors,如果在训练任务开始时不及时清理,会造成内存池碎片化,最终导致内存占用增加。

  训练过程中,禁止使用torch.cuda.empty_cache(),除非切换不同任务(如train/eval切换),因为cache blocks释放会触发Stream Synchronize,开销较大。

  7. non-blocking H2D拷贝是安全的,可以无脑使用

  data = data.cuda(non_blocking=True)

  在后续对当前数据有依赖的地方会主动插入sync point,保证数据安全;在没有立即对数据产生依赖的场景,可以使得数据H2D拷贝和计算并行。

  8. 在CPU负载比较空的时候,还是要充分利用的

  如数据加载的时候可以尽量将部分操作放在CPU负载。当前Megatron master主干在这一块还是很有优化空间的。

  https://zhuanlan.zhihu.com/p/670569490

  但是尽量不要在网络中间插入to cpu操作,会触发同步,反而弄巧成拙。

  9. 加速通信算子内存释放,可以无脑使用

  10. 训练/推理过程中不要触及内存上限

  如果内存观测是在持续上下跳动,那就是触及了内存上限,虽然整体程序能正常run起来,这时候已经频繁触发了内存池回收,每一次block回收都会触发一次Stream Synchronize,虽然平均利用率看起来可能超过90%,但是整体性能会降低的非常多。

  11. 对于连续的ElementWise算子,可以使用NvFuser加速

  @torch.jit.script

  def bias_dropout_add(x_with_bias, residual, prob, training):

  x, bias = x_with_bias # unpack

  x = x + bias

  out = torch.nn.functional.dropout(x, p=prob, training=training)

  out = residual + out

  return out

  torch._C._jit_set_nvfuser_enabled(True)

  前反向过程可以通过NvFuser实时生成高效的融合Kernel,但是注意torch.jit.script装饰器下的所有操作必须能被TorchScript语法解释,不然还是不能work的(具体可以去看PyTorch官方文档的TorchScript语法介绍)。

  12. 模型运行过程中不要流同步阻塞算子下发

  虚拟内存

  D2H操作、内存回收、以及主动调用流同步(torch.cuda.synchronize())等都会阻塞算子下发(保证对应Stream清空),那么后续算子如果执行过快(比下发快),那就会造成GPU间隙,所以说这个下发越快越好、越多越好,上图这个曲线是越缓越好,下发即执行那就是性能随时都可能坑。

  13. 尽量使用TensorCore,避免使用CUDACore

  # 直接使用cumsum

  b = a.cumsum(dim=-1)

  # 使用矩阵计算替代

  a = torch.matmul(a.view(x, b, s), triu_matrix)

  c = a[:, :-1, -1].cumsum(-1)

  a[:, 1:, :].add_(c.unsqueeze(-1))

  a = a.view(x, b*s)

  上图的计算如果替换成矩阵计算,加速数十倍,在cumsum维度过高的情况下,开销是异常大的。所以在遇到类似场景,都尽量转换成矩阵计算,即使计算量增加很多,速度还是有巨大收益的。

  14. 集群通信需要寻找合适的bucket size

  对于分桶通信,最优bucket size往往跟集群规模相关,需要自适应修改,并不一定是越小越好,不然训练性能损失惨重。

  审核编辑:黄飞

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分