使用自定义逻辑流分配Pass定制并发
功能介绍
由于算子执行时间、核占用、带宽等执行时的因素影响,导致一套算法在不同硬件上并发效果不同,并且在编译阶段无法准确预估;目前并不存在一个完美的算法,在任意网络、任意硬件上都能达到最佳并发。
基于上述问题,引入该章节的特性,开放自定义逻辑流分配Pass的接入,用户可以结合图的拓扑结构、根据Profiling来分析具体网络在具体硬件上的硬件利用率,从而可以灵活地调整并发效果,一网一策,一卡一策,达到最优性能。使用自定义逻辑流分配Pass定制并发,一般有两种使用场景:
- 基于内置的逻辑流分配结果进行微调。
- 基于图结构开发自研的流分配算法。
相关概念:
- 逻辑流:与物理流对应的概念,在图上按某些条件(拓扑序、引擎归属)预分配的流,这条逻辑流上的任务将按先后顺序执行;本文讲述的流均为逻辑流。
- 流分配:指定某几个任务可以并行执行,可以提升硬件利用率,降低模型执行时间。
本章节以场景一“基于内置的逻辑流分配结果进行微调”为例,详细介绍如何使用自定义逻辑流分配Pass定制并发,整体流程为:
1 2 3 4 5 6 7 8 |
#include "register_custom_pass.h" // 用户自定义逻辑流分配函数 Status CustomStreamPassFunc(const ConstGraphPtr &graph, StreamPassContext &stream_context) { // 此处定义逻辑流分配行为 return GRAPH_SUCCESS; } // Pass注册,无需指定stage,默认在逻辑流分配阶段之后执行 REGISTER_CUSTOM_PASS("pass_name").CustomAllocateStreamPassFn(CustomStreamPassFunc); |
- register_custom_pass.h:存储在“CANN软件安装目录/latest/include/register/”目录下,包含该头文件,可使用Pass注册相关类,使用Pass注册相关接口。
- Status:成功返回ge::GRAPH_SUCCESS,返回其他全为失败。建议使用小于0的值作为返回的错误码,大于0的值可能会和框架已使用的错误码产生冲突。
- CustomStreamPassFunc:自定义Pass的执行函数,详情请参见回调函数CustomAllocateStreamPassFunc。
- graph:待分配逻辑流的图,类型为ConstGraphPtr。
- stream_context:StreamPassContext类对象,可参考StreamPassContext提供的方法。
- REGISTER_CUSTOM_PASS:注册自定义Pass,"pass_name"可任意命名,详情请参见REGISTER_CUSTOM_PASS。
- CustomAllocateStreamPassFn:注册自定义的逻辑流分配Pass执行函数,详情请参见CustomAllocateStreamPassFn。
开发示例
- 前提条件
结合下图结构和Profiling分析优化点:
其Profiling分析结果为:
由Profiling分析结果可知有并发条件(无数据依赖和控制依赖)的算子1和2为串行执行(在逻辑流分配以后,算子1和2被分配了同一个Stream ID),可以基于本节提供的功能,修改为并行执行。
有并发条件的算子若使用相同的计算资源,且存在资源的抢占和等待,并不总是能够获得并发收益,需要根据Profiling详细分析,此处仅以此为例介绍如何调整。
- 开发步骤
- 包含的头文件。
1 2 3
#include <iostream> // 自定义Pass接口头文件 #include "register_custom_pass.h"
- 开发自定义Pass,对节点1分配新的Stream ID。(如下代码仅为示例,不可执行)
1 2 3 4 5 6 7 8 9 10 11 12 13
#include <iostream> #include "register_custom_pass.h" graphStatus AllocateStreamPass(const ConstGraphPtr &graph, StreamPassContext &context) { for (const auto &node : graph->GetDirectNode()) { AscendString node_name; node.GetName(node_name); if (std::string(node_name.GetString()) == "Abs_1/unary_ops_composition_0_SquareAbs_1/unary_ops_composition_1_Abs") { context.SetStreamId(node, context.AllocateNextStreamId()); } } return SUCCESS; } REGISTER_CUSTOM_PASS("AllocateStreamPass").CustomAllocateStreamPassFn(AllocateStreamPass);
- 包含的头文件。
如何使用自定义Pass
完成上述自定义Passs后,本节简单介绍如何把用户自定义逻辑流分配函数编译成动态库插件方式,以便注册的Pass在逻辑流分配阶段之后被调用。
- 前提条件
- 程序编译:
- 参见样例使用指导,获取其中的CMakeLists.txt编辑脚本,并按照Sample中的目录结构,将用户自定义逻辑流分配函数AllocateStreamPass.cpp文件放在src目录下。
- 根据实际情况修改CMakeLists.txt文件中的如下信息:
- ASCEND_PATH:指定CANN软件安装后文件存储路径,以root安装用户为例,/usr/local/Ascend/ascend-toolkit/latest。
- target_include_directories:需要包含的头文件,对于本示例,无需修改。如果是用户自行开发的代码,当需要添加头文件时,在示例下方直接增加行即可,注意不要删除原有项目。如果网络中有自定义算子,请增加自定义算子的原型定义头文件。
- target_link_libraries:需要链接的库,对于本示例,无需修改。如果是用户自行开发的代码,当需要添加链接库时,在示例下方直接增加行即可,注意不要删除原有项目。
- 执行如下命令进行编译:
mkdir build && cd build cmake .. && make
编译结束后,在build目录下生成动态库文件libAllocateStreamPass.so。
- 将libAllocateStreamPass.so拷贝到${ASCEND_PATH}/opp/vendors/xxx/custom_fusion_passes/目录下。其中“xxx”为用户自定义目录。(支持设置软链接的方式;".so"文件对可执行用户,需要有可读权限)
多个"${ASCEND_PATH}/opp/vendors/xxx"目录按照文本序排序后遍历寻找"custom_fusion_passes/"子目录,单个子目录内的".so"按照文本序加载,非".so"结尾的文件在加载时跳过:
- xxx:有且仅有一层自定义目录。
- custom_fusion_passes:该目录下不能有子目录。
- 自定义Pass使用(支持但不限于如下几种入口编译模型文件)
如果要查看上述自定义Pass有没有生效,在编译模型前,需要dump图进行查看:在执行之前设置DUMP_GE_GRAPH环境变量,然后使用如下入口编译模型:
- 使用ATC工具进行模型转换。ATC工具使用方法请参见《ATC离线模型编译工具用户指南》。
- 编译Graph为离线模型。
- 编译并运行Graph。
结果验证

若一个动态shape模型中有可下沉的部分,框架内部会将模型拆分为动态调度和下沉调度(静态子图)两部分,其中下沉调度可能有多个小模型;因此在对一个动静混合模型做自定义流分配时,自定义Pass前后的dump图将会有多份:第一份对应根图,后面若干份对应模型中的静态模型,当希望通过dump图查看分配结果时,最好通过查看build图之前最后一次自定义流分配的dump图。
设置了dump环境变量后,程序执行完毕,会在当前路径生成ge_onnx*.pbtxt等图文件,用户可以获取如下两张图,然后使用Netron等可视化软件查看:
- ge_onnx_xxx_RunCustomPass_BeforeAssignLogicStream_xxx.pbtxt:Pass执行前的图,图结构请参见图1。
- ge_onnx_xxx_RunCustomPass_AfterAssignLogicStream_xxx.pbtxt:Pass执行后的图,图结构为:
从图中可以看出算子1被分配了新的Stream ID。
用户也可以基于模型编译完成的图,并结合Profiling,查看最终的效果:从图2查看,算子1前后产生了Send/Recv等流间同步算子;执行生成Profiling(图3),可知算子1、2在两条流上并发执行。Profiling操作请参见Profiling性能数据采集。