Skip to content

通过SIMT实现分支判断

说明 该性能优化建议适用于如下型号:

  • Ascend 950PR/Ascend 950DT

【优先级】高

【描述】基于SIMD编程模型实现的批量数据计算性能很高,但在算子实现逻辑中涉及分支判断时,基于SIMD的计算操作会变得相对复杂,导致性能下降。此时,可以考虑采用SIMT方式,因为SIMT编程更为灵活,更适合处理分支判断的场景。

【样例介绍】以floor_mod算子为例,算子功能为将输入x的每个元素除以输入y的对应元素,获取余数。该余数应与除数y具有相同的符号,且其绝对值应小于y的绝对值。在计算过程中,需要判断y中每个元素的符号以及余数与该元素绝对值的大小关系。完整的算子实现代码请参考SIMT与SIMD混合编程高性能优化样例

表1 样例规格

名称nameshapedata typeformat
算子输入x[8192, 8192]int32ND
y[8192, 8192]int32ND
算子输出z[8192, 8192]int32ND

SIMT线程层次结构为:

  • 核函数(Kernel)启动核数:64
  • 单次SIMT VF调用线程数:1024

【反例】

基于SIMD Reg矢量计算的floor_mod算子实现:对应样例中的场景1(SCENARIO_NUM=1)。该场景使用 asc_copy_gm2ub_alignasc_copy_ub2gm_align 完成GM与Unified Buffer(UB)之间的数据搬运,在UB上使用SIMD VF函数实现计算。由于SIMD无法直接通过普通if else语句表达逐元素分支判断,因此需要使用多个矢量计算API完成符号比较、条件组合和结果选择,相关代码如下。

C++
__simd_vf__ inline void floor_mod_simd(
    __ubuf__ int32_t* z_addr, __ubuf__ int32_t* x_addr, __ubuf__ int32_t* y_addr, const uint32_t count)
{
    constexpr uint32_t one_repeat_size = asc_get_vf_len() / sizeof(int32_t);
    uint16_t loop_times = ceil_div(count, one_repeat_size);
    vector_int32_t x_value;
    vector_int32_t y_value;
    vector_int32_t mod_value;
    vector_int32_t temp_value;
    vector_int32_t default_value;
    vector_int32_t sign_value;

    vector_bool mask;
    vector_bool select_mask;
    vector_bool adjust_mask;
    uint32_t mask_count = count;

    asc_duplicate_scalar(default_value, int32_t(-1));
    asc_duplicate_scalar(sign_value, static_cast<int32_t>(FMOD_B32_SIGN));

    for (uint16_t i = 0; i < loop_times; i++) {
        mask = asc_update_mask_b32(mask_count);
        asc_loadalign(x_value, x_addr + i * one_repeat_size);
        asc_loadalign(y_value, y_addr + i * one_repeat_size);

        asc_div(temp_value, x_value, y_value, mask);
        asc_mul(temp_value, y_value, temp_value, mask);
        asc_sub(mod_value, x_value, temp_value, mask);

        asc_ne_scalar(select_mask, y_value, int32_t(0), mask);
        asc_select(temp_value, mod_value, default_value, select_mask);

        asc_add(mod_value, temp_value, y_value, mask);
        asc_ne_scalar(adjust_mask, temp_value, int32_t(0), mask);
        asc_and(x_value, temp_value, sign_value, mask);
        asc_and(y_value, y_value, sign_value, mask);
        asc_ne(select_mask, x_value, y_value, mask);
        asc_and(adjust_mask, select_mask, adjust_mask, mask);
        asc_select(mod_value, mod_value, temp_value, adjust_mask);
        asc_storealign(z_addr + i * one_repeat_size, mod_value, mask);
    }
}

【正例】

基于SIMT的floor_mod算子实现:对应样例中的场景3(SCENARIO_NUM=3)。该场景同样使用搬运接口完成GM与UB之间的数据搬运,在UB上采用SIMT编程方式实现计算过程,通过if else语句完成分支判断,代码如下所示。

C++
__simt_vf__ inline void floor_mod_simt_contiguous(
    __ubuf__ int32_t* x, __ubuf__ int32_t* y, __ubuf__ int32_t* z, uint32_t input_total_length)
{
    for (uint32_t index = threadIdx.x; index < input_total_length; index += blockDim.x) {
        int32_t y_value = y[index];
        const int32_t rem = x[index] % y_value;
        bool signs_differ = ((rem < 0) != (y_value < 0));
        if (signs_differ && (rem != 0)) {
            z[index] = rem + y_value;
        } else {
            z[index] = rem;
        }
    }
}

【性能对比】

在核数相同、输入输出规格相同、均使用搬运接口完成GM与UB之间数据搬运的情况下,对比场景1和场景3的性能数据如下。

除Task Duration外,其余指标均为所有Thread Block上的平均值。

场景实现方式核数Task Duration(μs)aiv_vec_time(μs)aiv_vec_ratioaiv_mte2_time(μs)aiv_mte2_ratio
场景1SIMD Reg矢量计算64525.736509.9870.989217.3410.421
场景3SIMT连续访问UB64463.179301.4740.668437.0550.968

相比场景1,场景3使用SIMT直接表达floor_mod中的分支判断逻辑,aiv_vec_time从509.987μs降低至301.474μs,下降约40.9%,说明Vector计算侧耗时明显降低。端到端Task Duration从525.736μs降低至463.179μs,下降约11.9%,场景3的aiv_mte2_ratio达到0.968,主要瓶颈已转为MTE2 Bound,即算子性能的最大影响转变为GM到UB的搬运效率。

免责声明:本站内容由 asc-devkit 仓 master 分支自动编译生成,属于持续开发版本,可能存在缺陷,仅供预览与参考。如需稳定及商用资料,请查阅官方 昇腾社区