简介

算子的最佳性能收到各种调优参数的影响,为了找到特定场景下最优的参数,实机暴力搜索得到的最优性能结果最为可靠。本文档主要记录了对matmul_reduce_scatter进行参数搜索的过程,方便后续对其他算子进行调优搜索工作。

步骤一:参数透传

使能参数透传以实现以下目标:

  1. 显示传入tilingdata内容而非算子tiling自己计算

  2. 增加算子输出[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
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐