开发者
资源

VF融合优化

【优先级】高

【描述】VF融合是将代码中多个VF函数融合成一个VF函数,有效提升性能。VF融合特性是优化特性,VF自动融合会借助Loop Fuse算法,将VF转换成Loop形态,然后将控制流等价(Control-Flow-Equivalent)的VF进行融合,最后将VF进行还原。编译器首先会做融合前的合法性检查,判断两个VF是否等价,Main侧中间代码是否能在VF内执行以及融合后是否可产生正收益(不会引起传参寄存器溢出、VF代码不会过大)等,如果满足VF融合条件,编译器会自动执行VF融合优化,为保证融合后的VF执行逻辑与语义与融合前一致,会在原来两个VF之间保守地插入同步指令,编译器还会尝试外提、合并融合后的VF中的指令,对VF代码进行优化。融合策略是能融尽融,用户按照符合融合的合法性检查的模式进行编码,可以增加VF融合的机会。

VF融合原理介绍

VF融合优化可分为三个阶段:VF浅度融合、VF深度融合和VF内自动同步:

VF浅度融合:编译器首先会分析两个VF的控制流是否等价,构建Cost Model分析是否有正向收益,如果满足VF融合条件,将VF外部的控制流融入到VF内,将VF外的Software Loop硬化成VF内的Hardware Loop,然后使能VF自动融合的基础能力,将两个VF融合成一个VF,为后续的VF深度融合提供基础。

VF深度融合:VF深度融合会继续对VF内的Hardware Loop进行融合,从而减少Hardware Loop的启动开销,并且极大地减少冗余的Load/Store操作,充分复用寄存器。

VF内自动同步:编译器会精准地插入必要的同步指令,删除冗余的同步指令,极大地释放了硬件OOO(Out of Order)能力。用户无需手动插入同步指令,极大地降低了用户的编码难度。

VF融合编写指导

  1. 多个VF函数自动融合:如果多个VF函数的控制流等价,且满足均为Hardware Loop循环,编译器会执行VF融合优化特性。

    【正例】VF函数DivVF和AddVF会被编译器融合成一个VF函数,并且能优化多余的Load/Store指令。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    template<typename T>
    __simd_vf__ inline void DivVF(__ubuf__ T* dstAddr, __ubuf__ T* srcAddr, uint32_t count, uint32_t repeatTime, uint32_t oneRepNum){
        AscendC::Reg::MaskReg mask;
        AscendC::Reg::RegTensor<T> reg0, reg1, reg2;
        constexpr float num = 1.0f;
        for(uint16_t j = 0; j < repeatTime; ++j){
            mask = AscendC::Reg::UpdateMask<T>(count);
            AscendC::Reg::LoadAlign(reg0, srcAddr + j * oneRepNum);
            AscendC::Reg::Duplicate(reg1, num, mask);
            AscendC::Reg::Div(reg2, reg1, reg0, mask);
            AscendC::Reg::StoreAlign(dstAddr + j * oneRepNum, reg2, mask);
        }
    }
    template<typename T>
    __simd_vf__ inline void AddVF(__ubuf__ T* dstAddr, __ubuf__ T* srcAddr, uint32_t count, uint32_t repeatTime, uint32_t oneRepNum){
        AscendC::Reg::MaskReg mask;
        AscendC::Reg::RegTensor<T> srcReg;
        AscendC::Reg::RegTensor<T> dstReg;
        constexpr float num = 1.0f;
        for(uint16_t j = 0; j < repeatTime; ++j){
            mask = AscendC::Reg::UpdateMask<T>(count);
            AscendC::Reg::LoadAlign(srcReg, srcAddr + j * oneRepNum);
            AscendC::Reg::Adds(dstReg, srcReg, num, mask);
            AscendC::Reg::StoreAlign(dstAddr + j * oneRepNum, dstReg, mask);
        }
    }
    template<typename T>
    class Kernel {
        public:
        __aicore__ inline Kernel() = default;
        __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, uint32_t count, AscendC::TPipe* pipeIn){
            // ... 
     
        }
        __aicore__ inline void CopyIn(){
            // ... 
        }
        __aicore__ inline void Compute(){
            AscendC::LocalTensor<T> xLocal = inQueueX.DeQue<T>();
            AscendC::LocalTensor<T> yLocal = outQueueY.AllocTensor<T>();
            AscendC::DataCopy(yLocal, xLocal, count);
            __ubuf__ T* srcAddr = reinterpret_cast<__ubuf__ T*>(xLocal.GetPhyAddr());
            __ubuf__ T* dstAddr = reinterpret_cast<__ubuf__ T*>(yLocal.GetPhyAddr());
            constexpr uint32_t oneRepNum = 256 / sizeof(T);
            uint32_t repeatTime =  count / oneRepNum;
            DivVF(dstAddr, srcAddr, count, repeatTime, oneRepNum);
            AddVF(dstAddr, dstAddr, count, repeatTime, oneRepNum);
            outQueueY.EnQue<T>(yLocal);
        }
        __aicore__ inline void CopyOut(){
            // ... 
        }
        __aicore__ inline void Process(){
            CopyIn();
            Compute();
            CopyOut();
        }
        private:
        AscendC::TPipe* pipe = nullptr;
        uint32_t count;
        AscendC::GlobalTensor<T> xGm;
        AscendC::GlobalTensor<T> yGm;
        AscendC::TQue<AscendC::TPosition::VECIN, 1> inQueueX;
        AscendC::TQue<AscendC::TPosition::VECOUT, 1> outQueueY;
    };
    
  2. 使用基础API连续计算模式:基础API实现对硬件能力的抽象,开放芯片的能力,保证完备性和兼容性。基础API根据对数据操作方法的不同,可以分为两大类:
    • 连续计算API:支持Tensor前n个数据计算。针对源操作数的连续n个数据进行计算并连续写入目的操作数,解决一维tensor的连续计算问题。
    • 高维切分API:支持Repeat和Stride。功能灵活的计算API,提供与Builtin API完全对等的编程能力,充分发挥硬件优势,支持对每个操作数的DataBlock Stride,Repeat Stride,Mask等参数的操作。

    在VF融合优化中,推荐使用基础API的连续计算模式编写算子,可以充分发挥出VF融合优化的能力,与高维切分API相比,连续计算API使得编译器能更好地分析VF融合优化,更加容易满足VF融合优化的条件,使用基础API的连续计算模式能写出性能更优的算子。

    【反例】使用基础API的高维切分模式编写算子,编译器在分析VF融合时受复杂的计算逻辑影响,无法对Add和Mul接口进行VF融合优化。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    template<typename T>
    class Kernel {
        public:
        // ...
        __aicore__ inline void Compute(){
            AscendC::LocalTensor<T> xLocal = inQueueX.DeQue<T>();
            AscendC::LocalTensor<T> yLocal = outQueueY.AllocTensor<T>();
            AscendC::DataCopy(yLocal, xLocal, inner * outter);
            uint64_t mask = 128;
            AscendC::Add(yLocal, xLocal, xLocal, mask, 4, { 1, 1, 1, 8, 8, 8 });
            AscendC::Mul(yLocal, yLocal, xLocal, mask, 4, { 1, 1, 1, 8, 8, 8 });
            outQueueY.EnQue<T>(yLocal);
        }
        // ...
    };
    
    【正例】使用基础API的连续计算模式,编译器分析Add和Mul函数后符合VF融合要求,将Add和Mul融合成一个VF函数。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    template<typename T>
    class Kernel {
        public:
        // ...
        __aicore__ inline void Compute(){
            AscendC::LocalTensor<T> xLocal = inQueueX.DeQue<T>();
            AscendC::LocalTensor<T> yLocal = outQueueY.AllocTensor<T>();
            AscendC::DataCopy(yLocal, xLocal, inner * outter);
            AscendC::Add(yLocal, xLocal, xLocal, count);
            AscendC::Mul(yLocal, yLocal, xLocal, count);
            outQueueY.EnQue<T>(yLocal);
        }
        // ...
    };