Ascend for PyTorch多线程多流场景下 TaskQueue卡死问题分析与解决方案
作者:昇腾实战派
知识地图: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-spy或gdb工具观察代码堆栈位置,辅助定位卡死点。
根因分析
核心问题: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。
- 为通信任务单独创建流,避免与计算流共用。
- 确保流之间的依赖关系正确,避免循环等待。
最佳实践总结
- 保序原则:在多线程多流并发下发算子的场景中,务必保证算子下发顺序与依赖关系一致,避免交叉下发导致死锁。
- 流隔离:计算流与通信流应严格隔离,避免共用流资源。
- 调试手段:充分利用HCCL日志、PTA TaskQueue日志和堆栈工具,快速定位卡死点。
- 版本兼容:不同版本的框架和CANN包可能存在差异,遇到难以定位的问题时,可尝试切换版本验证。
通过以上分析与实践,可以有效避免多线程多流场景下的TaskQueue卡死问题,保障分布式训练任务的稳定性与性能。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)