面向昇腾通算融合算子的最优Tiling搜索----以matmul_reduce_scatter为例
算子的最佳性能收到各种调优参数的影响,为了找到特定场景下最优的参数,实机暴力搜索得到的最优性能结果最为可靠。本文档主要记录了对matmul_reduce_scatter进行参数搜索的过程,方便后续对其他算子进行调优搜索工作。
简介
算子的最佳性能收到各种调优参数的影响,为了找到特定场景下最优的参数,实机暴力搜索得到的最优性能结果最为可靠。本文档主要记录了对matmul_reduce_scatter进行参数搜索的过程,方便后续对其他算子进行调优搜索工作。
步骤一:参数透传
使能参数透传以实现以下目标:
-
显示传入tilingdata内容而非算子tiling自己计算
-
增加算子输出[const aclTensor *T]以获得下文<算子内插桩计时>中打桩后的算子调优性能
具体操作步骤如下:
声明修改
aclnn调用采用二段式,先调用aclnn<op>GetWorkspaceSize在host侧计算该算子需要临时使用的显存空间,并配置相关tilingdata信息。然后调用aclnn<op>正式调用NPU执行算子操作。因此透传的参数需要在第一段接口传入,该步骤增加传入的调优参数声明,具体如下。
原本声明:
extern aclnnStatus aclnnInnerMatmulReduceScatterV3GetWorkspaceSize( const aclTensor *a, const aclTensor *b, const aclTensor *bias, const char *group, int64_t worldSize, int64_t epRankId, bool transposeX1, bool transposeX2, const aclTensor *c, uint64_t *workspaceSize, aclOpExecutor **executor);
修改后声明:
extern aclnnStatus aclnnInnerMatmulReduceScatterV3GetWorkspaceSize( const aclTensor *a, const aclTensor *b, const aclTensor *bias, const char *group, int64_t worldSize, int64_t epRankId, bool transposeX1, bool transposeX2, bool with_default_tiling, int64_t m0, int64_t swizzlCount, int64_t swizzlDirect, int64_t pValue, int64_t ubMoveNum, int64_t commNpuSplit, int64_t commDataSplit, const aclTensor *c, const aclTensor *T, uint64_t *workspaceSize, aclOpExecutor **executor);
其中增加的(此处仅供参考,透传参数自定义)有:
-
bool with_default_tiling标记是否使用传入的tiling参数 -
七个matmul_reduce_scatter的tiling参数:
int64_t m0, swizzlCount, swizzlDirect, pValue, ubMoveNUm, commNpuSplit, commDataSplit -
统计时间Tensor:
aclTensor * T
OpDef 修改
在完成了声明修改后,需要在具体实现中处理传入的参数。aclnn二段式调用的具体实现是由构建工程自动调用,因此需要定义传入参数的性质,让构建工程生成正确的函数定义。
matmul_reduce_scatter为例,在matmul_reduce_scatter_def.cpp中增加
#ifdef ENABLE_TIMING
this->Output("T")
.ParamType(REQUIRED)
.DataType({ge::DT_INT64,ge::DT_INT64})
.Format({ge::FORMAT_ND,ge::FORMAT_ND})
.UnknownShapeFormat({ge::FORMAT_ND,ge::FORMAT_ND});
#endif
this->Attr("group").AttrType(REQUIRED).String();
this->Attr("worldSize").AttrType(REQUIRED).Int();
this->Attr("rankId").AttrType(REQUIRED).Int();
this->Attr("transposeX1").AttrType(REQUIRED).Bool(false);
this->Attr("transposeX2").AttrType(REQUIRED).Bool(false);
this->Attr("with_default_tiling").AttrType(REQUIRED).Bool(true);
this->Attr("algor").AttrType(OPTIONAL).Int(0);
this->Attr("m0").AttrType(OPTIONAL).Int();
this->Attr("swizzlCount").AttrType(OPTIONAL).Int();
this->Attr("swizzlDirect").AttrType(OPTIONAL).Int();
this->Attr("pValue").AttrType(OPTIONAL).Int();
this->Attr("ubMoveNum").AttrType(OPTIONAL).Int();
this->Attr("commNpuSplit").AttrType(OPTIONAL).Int();
this->Attr("commDataSplit").AttrType(OPTIONAL).Int();
tiling参数检查和设置
透传的参数应该由tilingdata,由auto_gen的自动工程生成的代码由host拷贝到device。因此我们还需要设置tilingdata的数据结构,在host侧配置好需要tilingdata。
下面展示了tilingdata的数据结构,保存了我们需要的调优参数。它由auto_gen自动从host侧拷贝到device,算子侧可以直接通过该数据结构的tilingdata直接获取各个tiling参数的值。
struct CoCTiling {
int32_t algor = -1;
int32_t m0 = -1;
int32_t k0 = -1;
int32_t n0 = -1;
int32_t mLoop = -1;
int32_t kLoop = -1;
int32_t nLoop = -1;
int32_t swizzlCount = -1;
int32_t swizzlDirect = -1;
int32_t pValue = -1;
int32_t ubMoveNum = -1;
int32_t commNpuSplit = -1;
int32_t commDataSplit = -1;
int32_t lenPerLoop = -1;
};
struct MatmulReduceScatterV3TilingData {
Mc2InitTiling mc2InitTiling;
Mc2CcTiling mc2CcTiling;
MatmulReduceScatterV3Info matmulReduceScatterV3Info;
CoCTiling cocTiling;
};
修改发生在matmu_reduce_scatter_v3_tiling.cpp的MatmulReduceScatterV3TilingFuncImpl函数中。
通过以下方式获得tilingdata的指针
MatmulReduceScatterV3TilingData *tilingData = context->GetTilingData<MatmulReduceScatterV3TilingData>();
通过以下方式获得传入参数的具体值。这里ATTR_M0, ATTR_SWIZZLCOUNT,是这些属性的具体序号,对应“OpDef 修改”修改中代码,group为0, ATTR_M0为7, ATTR_SWIZZLCOUNT为8。
m0_can = *(attrs->GetAttrPointer<int>(ATTR_M0)); swizzlCount_can = *(attrs->GetAttrPointer<int>(ATTR_SWIZZLCOUNT)); swizzlDirect_can = *(attrs->GetAttrPointer<int>(ATTR_SWIZZLDIRECT)); pValue_can = *(attrs->GetAttrPointer<int>(ATTR_PVALUE)); ubMoveNum_can = *(attrs->GetAttrPointer<int>(ATTR_UBMOVENUM)); commNpuSplit_can = *(attrs->GetAttrPointer<int>(ATTR_COMMNPUSPLIT)); commDataSplit_can = *(attrs->GetAttrPointer<int>(ATTR_COMMDATASPLIT));
通过以下方式进行tilingdata赋值
tilingData->cocTiling.m0 = m0_can; tilingData->cocTiling.swizzlCount = swizzlCount_can; ...
Tensor T的形状和数据类型设置
在OpDef中定义了两类参数,一类是input/output Tensor,它们是Tensor对象,另一类是属性值,即调优参数使用的类型。 我们在进行内部计时时,使用一个Tensor T(output)将数据传出,它是第一类参数,需要额外配置这个tensor的形状,大小以及数据类型。具体如下:
保存时间统计的T是一个int64_t的向量,其长度为AscendTimer::N_TIMING_COUNTER 在matmul_reduce_scatter_v3_proto.cpp中: 设置形状
gert::Shape* t_shape = context->GetOutputShape(1); OPS_LOG_E_IF_NULL(context, t_shape, return GRAPH_FAILED); t_shape->SetDimNum(1); t_shape->SetDim(0, AscendTimer::N_TIMING_COUNTER);
设置数据类型
context->SetOutputDataType(1, ge::DT_INT64);
参数透传调用
在完成好上述的修改后,参数透传的代码修改理应正确配置。通过二段式调用,测试是否如预期调优参数通过调用接口影响了算子执行,以及数据信息outT是否正确保存了统计数据。
具体可参考以下代码片段:
// 调用第一阶段接口
ret = aclnnMatmulReduceScatterV3GetWorkspaceSize(
x1, x2, bias, hcomName, worldSize, args.rankId, with_default_tiling, tilingInfo.algor, tilingInfo.m0,
tilingInfo.swizzlCount, tilingInfo.swizzlDirect, tilingInfo.pValue, tilingInfo.ubMoveNum,
tilingInfo.commNpuSplit, tilingInfo.commDataSplit, out, outT, &workspaceSize, &executor);
CHECK_RET(ret == ACL_SUCCESS,
LOG_PRINT("[ERROR] aclnnMatmulReduceScatterV3GetWorkspaceSize failed. ret = %d \n", ret);
return ret);
// 根据第一阶段接口计算出的workspaceSize申请device内存
if (workspaceSize > 0) {
ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);
CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("[ERROR] aclrtMalloc workspace failed. ret = %d \n", ret);
return ret);
}
ret = HcclBarrier(args.hcclComm, args.stream); // 新增的全卡同步
CHECK_RET(ret == HCCL_SUCCESS, LOG_PRINT("[ERROR] mmReduceScatterV3 HcclBarrier failed. ret = %d \n", ret);
return ret);
// 调用第二阶段接口
ret = aclnnMatmulReduceScatterV3(workspaceAddr, workspaceSize, executor, args.stream);
CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("[ERROR] aclnnMatmulReduceScatterV3 failed. ret = %d \n", ret);
return ret);
// (固定写法)同步等待任务执行结束
ret = aclrtSynchronizeStreamWithTimeout(args.stream, 50000);
步骤二:参数搜索
该步骤通过运行单算子测试,为不同的tiling参数生成一个各个ai core统计时间的csv文件。 完整执行过程请参考matmul_reduce_scatter_v3-2目录下的README.md
这里的主要新增两个模块:
-
TilingSpace.hpp: 管理搜索的tiling参数空间
-
AscendTimer.hpp: 算子内插装计时(封装了host侧输出耗时csv的功能)
参数空间
TilingSpace.hpp管理的参数空间,是参数搜索的范围。通过设置每个参数的范围,自动生成所有参数组合(笛卡尔积)。
参数空间的使用十分方便,先设置需要搜索的各个参数范围
const AlgorithmParamSpace SSAlgoParamSpace = AlgorithmParamSpace::Builder("SmallShape", 1)
.setM0({128})
.setSwizzlCount({3})
.setSwizzlDirect({1})
.setPValue({1, 2, 4})
.setUbMoveNum({8, 16})
.setCommNpuSplit({1}) // 无用参数
.setCommDataSplit({16}) // 无用参数
.build();
然后通过笛卡儿积获得全部的参数信息
auto tilingInfos = SSAlgoParamSpace.get_all_tilings();
算子内插桩计时
该步骤是获得算子的运行时间,也可以使用msprof的方式获得,但是使用msprof需要写额外的脚本将不同tiling的数据和采集时间进行对应。 插桩计时的好处是,在测试的cpp文件中,调用算子前获得了设置了tiling配置,调用后,通过Tensor T传出了计时信息。可以在该测试文件中更方便地处理完数据,生成所需的tiling-运行时间的对应信息。
该模块需要同步在算子中include,并增加对应插桩。
使用前准备
具体使用,需要先设置枚举变量TimingConstants,用于自定义计时部分。这里第一个KERNEL_TIMING_IDX用来记录整个算子的执行时间,后面是我自定义不同步骤的计时。 N_TIMING_COUNTER_PER_CORE是计时器的上限个数,不能自定义超过32个,N_REAL_COUTER是实际使用的计时器个数,N_TIMING_COUNTER是计时数据的长度。(上面Tensor T的长度)
enum TimingConstants {
KERNEL_TIMING_IDX = 0,
AI_CORE_INIT,
AI_CORE_PROCESS,
AIV_PROCESS,
AIV_ZERO_INIT_IDX,
AIV_LOOP_OTHER_IDX,
AIV_SYNC_IDX,
AIV_DATA_COPY_IDX,
AIV_FINALIZE_IDX,
N_REAL_COUTER,
N_TIMING_COUNTER_PER_CORE = 32,
N_TIMING_COUNTER = N_TIMING_COUNTER_PER_CORE * 128
};
然后修改timingNameMap,它是用于不同计时器最终输出csv中的表头,如果不设置将打印unknown
// 定义枚举值到计时名称的映射表,在枚举后立即定义
static inline const std::unordered_map<int, std::string> &getTimingNameMap()
{
static const std::unordered_map<int, std::string> timingNameMap = {
{KERNEL_TIMING_IDX, "KERNEL_TIMING"}, {AI_CORE_INIT, "AI_CORE_INIT"},
{AI_CORE_PROCESS, "AI_CORE_PROCESS"}, {AIV_ZERO_INIT_IDX, "AIV_ZERO_INIT"},
{AIV_PROCESS, "AIV_PROCESS"}, {AIV_LOOP_OTHER_IDX, "AIV_LOOP_OTHER_IDX"},
{AIV_SYNC_IDX, "AIV_SYNC"}, {AIV_DATA_COPY_IDX, "AIV_DATA_COPY"},
{AIV_FINALIZE_IDX, "AIV_FINALIZE"}};
return timingNameMap;
}
device侧插桩
通过如下方法在device侧插桩计时。
// 通过写入的gm地址构造计时器 AscendTimer timer(t); // 初始化计时器对应索引的值为0,为后续累加做准备。 timer.zero(AscendTimer::KERNEL_TIMING_IDX); // tik()成员函数开始计时 timer.tik(); // 此处为待计时部分 // tok()成员函数结束计时,并将该段计时部分累加到对应索引位置。 timer.tok(AscendTimer::KERNEL_TIMING_IDX);
tok()函数结束自动调用tik()函数,分步骤计时时无需再次调用tik()。 只需开始调用一次tik(),后续调用不同索引位置的tok()即可。
host侧输出结果
如下方式将Tensor T的数据输出csv文件,其中outTDeviceAddr是T的gm地址。
auto output_csv_filename = get_time_filename(tilingInfo, args.rankId, worldSize, Op_Shape(args.m, args.k, args.n), "loop_" + std::to_string(repeatLoop) + "_"); auto time_result = AscendTimer::getTimeResult(&outTDeviceAddr); AscendTimer::writeTimeResultsToCsv(DEFAULT_OUTPUT_CSV_DIR + output_csv_filename, time_result);
输出loop_3__time_stats_rank_1_world_8_tiling_128_3_1_4_16_1_16__shape_128_1024_1024.csv示例(请忽略计时具体数值):
| group_id | core_type | sub_group_id | KERNEL_TIMING(us) | AI_CORE_INIT(us) | AI_CORE_PROCESS(us) | AIV_PROCESS(us) | AIV_ZERO_INIT(us) | AIV_LOOP_OTHER_IDX(us) | AIV_SYNC(us) | AIV_DATA_COPY(us) | AIV_FINALIZE(us) |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | AIC | 0 | 3 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | AIV | 0 | 883 | 4 | 879 | 879 | 8 | 0 | 866 | 4 | 0 |
| 0 | AIV | 1 | 884 | 4 | 880 | 880 | 8 | 0 | 870 | 0 | 1 |
| 1 | AIC | 0 | 2 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | AIV | 0 | 883 | 4 | 879 | 879 | 8 | 0 | 869 | 2 | 0 |
| 1 | AIV | 1 | 883 | 3 | 880 | 880 | 9 | 0 | 870 | 0 | 0 |
| 2 | AIC | 0 | 2 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | AIV | 0 | 883 | 4 | 879 | 879 | 9 | 0 | 867 | 4 | 0 |
| 2 | AIV | 1 | 884 | 4 | 880 | 880 | 9 | 0 | 870 | 0 | 1 |
| 3 | AIC | 0 | 3 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | AIV | 0 | 883 | 4 | 879 | 879 | 9 | 0 | 866 | 4 | 0 |
| 3 | AIV | 1 | 883 | 4 | 878 | 878 | 8 | 0 | 869 | 0 | 1 |
| 4 | AIC | 0 | 3 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | AIV | 0 | 882 | 4 | 878 | 877 | 8 | 0 | 865 | 4 | 0 |
这里文件名意义为第3次循环,8卡中1卡的数据,后面7个数字为tiling参数,最后三个数字为输入的shape。 KERNEL_TIMING项目是具体的时间。
步骤三:数据处理(可选)
该步骤是我对上步骤统计的csv文件进程脚本整合处理,生成更好对比的表格数据。具体由所需的数据,自定义脚本生成。
执行
python3 process_data.py ../result/result_search_tiling/ ./output.csv
将多个csv数据合并成一个数据。 具体过程大致为,选择8个卡中每次迭代中的最小的值(因为通算融合算子有同步等待,认为不同卡中最短时间为真实算子时间),然后对不同迭代的时间计算平均值为该tiling参数的执行时间。同时保留了方差和标准差,和统计循环次数,这里实际为20次,实际计算平均值时只有19次,因为舍去了第一次异常值(通常时间过大)。 output.csv示例,通过排序aic_mean或者aiv_mean找出最优参数。对于先计算后通信的融合算子,参考aiv_mean, 反之参考aic_mean。
| m0 | swizzlCount | swizzlDirect | pValue | ubMoveNum | commNpuSplit | commDataSplit | shape_m | shape_k | shape_n | aic_mean | aic_std | aic_var | aiv_mean | aiv_std | aiv_var | loop_count |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 128 | 3 | 1 | 2 | 16 | 1 | 16 | 128 | 1024 | 1024 | 3.421053 | 0.493728 | 0.243767 | 23.84211 | 0.58608 | 0.34349 | 19 |
| 128 | 3 | 1 | 1 | 16 | 1 | 16 | 128 | 1024 | 1024 | 3.368421 | 0.482376 | 0.232687 | 23.89474 | 0.787717 | 0.620499 | 19 |
| 128 | 3 | 1 | 1 | 8 | 1 | 16 | 128 | 1024 | 1024 | 3.368421 | 0.581335 | 0.33795 | 24.73684 | 0.713929 | 0.509695 | 19 |
| 128 | 3 | 1 | 4 | 16 | 1 | 16 | 128 | 1024 | 1024 | 3.473684 | 0.595458 | 0.354571 | 23.73684 | 0.63595 | 0.404432 | 19 |
| 128 | 3 | 1 | 2 | 8 | 1 | 16 | 128 | 1024 | 1024 | 3.473684 | 0.499307 | 0.249307 | 24.84211 | 0.488085 | 0.238227 | 19 |
| 128 | 3 | 1 | 4 | 8 | 1 | 16 | 128 | 1024 | 1024 | 3.263158 | 0.440347 | 0.193906 | 24.47368 | 0.499307 | 0.249307 | 19 |
更多推荐



所有评论(0)