作者​:昇腾实战派
知识地图​:https://blog.csdn.net/Lumos_Lovegood/article/details/161601003

背景概述

在深度学习分布式训练场景中,多线程多流并发下发算子是提升训练效率的常见手段。然而,当多个线程在不同设备(卡)上并发执行计算与通信任务时,由于各线程执行速度不一致,算子下发顺序可能出现交叉,进而引发PTA TaskQueue卡死或HCCL通信算子卡死等问题。本文基于实际开发中遇到的问题,详细分析该现象的根因,并给出有效的解决方案与最佳实践。

问题复现与现象

复现示例

使用以下命令启动多卡训练:

torchrun --nprorc-per-node=2 test.py

用例参考 https://gitcode.com/Ascend/pytorch/issues/4537
该示例可复现多线程多流场景下,由于不同线程、不同卡的执行快慢不同,导致算子下发顺序交叉,最终在PTA TaskQueue处卡死。

调试手段

  • HCCL调用日志:配置环境变量 export HCCL_ENTRY_LOG_ENABLE=1,在 ${home}/ascend/log/run/plog 目录下搜索 Entry- 关键字,观察HCCL算子下发日志。
  • PTA TaskQueue调用日志:配置环境变量 export TORCH_NPU_LOG=+dispatch,在打屏日志中观察PTA TaskQueue的调用情况。
  • 堆栈定位:使用 py-spygdb 工具观察代码堆栈位置,辅助定位卡死点。

根因分析

核心问题:TaskQueue串行化导致依赖交叉

在多线程场景下,如果使用单线程下发任务,Host侧的任务下发本身就是串行的,类似于TaskQueue的串行顺序,天然不会卡死。同样,如果使用单流,流上的任务下发也是串行的,也不会卡死。

TaskQueue带来的卡死问题,本质上是将原本多线程并发下发的任务放入一个TaskQueue中串行下发,而多线程多流任务之间本身存在依赖关系(如计算流与通信流之间的同步),当任务下发顺序交叉时,就会导致死锁。

具体表现

  • 关闭TaskQueue使用PER_STREAM_QUEUE(每个流独立一个队列)可以避免PTA侧的卡死,因为此时不再依赖全局串行下发顺序,每个流独立下发。
  • 但由于HCCL算子下发顺序仍可能不一致,即使PTA侧不再卡死,HCCL侧仍可能因下发顺序问题而卡死。

构造示例过程中的踩坑记录

坑1:线程与函数行为差异

现象:使用函数时,卡1的allreduce无法下发;使用线程时,allreduce可以正常下发。

定位原因:使用函数时,默认未明确设置device,导致任务在默认卡(卡0)上执行,阻塞了卡1的TaskQueue,使得后续allreduce无法下发。而使用线程时,若未明确set device,线程默认也在卡0上执行,但不会阻塞卡1的TaskQueue,因此allreduce可以下发。

结论:多线程场景下,务必明确指定每个线程的device,避免意外阻塞其他卡的TaskQueue。

坑2:未单独创建流导致计算流卡死

现象:在f3中未单独创建流时,无论是否关闭TaskQueue、是否开启PER_STREAM_QUEUE,卡1的allreduce均无法下发。

定位原因:allreduce虽然在HCCL流上执行,但会与计算流交互(如等待计算流上的record)。如果不单独创建一条流,会导致计算流卡死:

  • 关闭TaskQueue或开启TaskQueue时,allreduce下发前在计算流上的record无法下发。
  • 开启PER_STREAM_QUEUE后,record会在计算流的TaskQueue中卡住,而HCCL流上还有一个block等待该record,因此仍然卡死。

结论:通信算子(如allreduce)即使运行在独立的通信流上,也可能与计算流存在依赖关系。必须为通信任务单独创建流,避免与计算流共用流资源。

坑3:创建c tensor导致流同步

现象:在f3中创建c tensor后,TaskQueue中的allreduce都可以正常下发。

定位原因:创建c tensor时,数据从CPU拷贝到NPU,会触发流同步操作,导致后续add算子无法下发,从而不会阻塞TaskQueue,卡1的allreduce得以正常下发。

结论:流同步操作会阻塞当前流上的后续任务,但可能意外解除其他流的阻塞。依赖这种“巧合”来解决问题并不可靠,应通过正确的流管理来保证任务顺序。

解决方案

方案一:关闭TaskQueue

关闭TaskQueue后,算子下发不再依赖全局串行队列,每个流独立下发,可避免因下发顺序交叉导致的卡死。

方案二:使用PER_STREAM_QUEUE

开启PER_STREAM_QUEUE后,每个流拥有独立的队列,任务下发顺序与流内任务顺序一致,不再受其他流影响。

方案三:合理管理多线程多流

  • 每个线程明确指定device。
  • 为通信任务单独创建流,避免与计算流共用。
  • 确保流之间的依赖关系正确,避免循环等待。

最佳实践总结

  1. 保序原则:在多线程多流并发下发算子的场景中,务必保证算子下发顺序与依赖关系一致,避免交叉下发导致死锁。
  2. 流隔离:计算流与通信流应严格隔离,避免共用流资源。
  3. 调试手段:充分利用HCCL日志、PTA TaskQueue日志和堆栈工具,快速定位卡死点。
  4. 版本兼容:不同版本的框架和CANN包可能存在差异,遇到难以定位的问题时,可尝试切换版本验证。

通过以上分析与实践,可以有效避免多线程多流场景下的TaskQueue卡死问题,保障分布式训练任务的稳定性与性能。

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐