1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        如何實(shí)現(xiàn)比PyTorch快6倍的Permute/Transpose算子?

        共 4399字,需瀏覽 9分鐘

         ·

        2021-11-06 15:48


        撰文 | 鄭澤康、柳俊丞、姚遲、郭冉

        無論是在統(tǒng)治NLP屆的Transformer,還是最近視覺領(lǐng)域的新秀Vision Transformer,我們都能在模型中看到Transpose/Permute算子的身影,特別是在多頭注意力機(jī)制(Multi-Head Attention)中,需要該算子來改變數(shù)據(jù)維度排布。

        顯然,作為一個(gè)被高頻使用的算子,其CUDA實(shí)現(xiàn)會(huì)影響到實(shí)際網(wǎng)絡(luò)的訓(xùn)練速度。本文會(huì)介紹OneFlow中優(yōu)化Permute Kernel的技巧,并跟PyTorch的Permute,原生的Copy操作進(jìn)行實(shí)驗(yàn)對(duì)比。結(jié)果表明,經(jīng)過深度優(yōu)化后的Permute操作在OneFlow上的速度和帶寬利用率遠(yuǎn)超PyTorch,帶寬利用率能夠接近原生Copy操作。

        1
        樸素的Permute實(shí)現(xiàn)

        Permute算子的作用是變換張量數(shù)據(jù)維度的順序,舉個(gè)例子:

        x?=?flow.randn(2,?3)
        y?=?x.permute(1,?0)
        y.shape?
        (3,?2)
        其實(shí)現(xiàn)原理也可以很容易理解,即輸出Tensor的第i維,對(duì)應(yīng)輸入Tensor的dims[i]維,上述例子中 permute 實(shí)現(xiàn)對(duì)應(yīng)的偽代碼如下:

        for?row?in?x.shape[0]:?
        ??for?col?in?x.shape[1]:?
        ????y[row][col]?=?x[col][row]

        但是實(shí)際情況與上面的偽代碼有出入,張量的Shape是數(shù)學(xué)上的概念,在物理設(shè)備上并不真實(shí)存在。

        在OneFlow中,張量的數(shù)據(jù)都是保存在一塊連續(xù)的內(nèi)存中,下圖分別從上層視角和底層視角描述了形狀為(2, 3)的張量的存儲(chǔ)方式:




        OneFlow的Permute實(shí)現(xiàn)原理為:
        • 通過當(dāng)前輸出的一維偏移量(offset)計(jì)算對(duì)應(yīng)的高維索引

        • 然后根據(jù)參數(shù)dims重新排列輸出索引,進(jìn)而得到輸入索引。

        • 將輸入索引轉(zhuǎn)換成輸入偏移量

        • 最后進(jìn)行數(shù)據(jù)移動(dòng),整個(gè)過程的示意圖如下:



        完成Permute后,輸出如下圖所示:


        整個(gè) permute 計(jì)算過程需要經(jīng)過多次一維偏移量offset和高維索引之間的轉(zhuǎn)換,為了避免一次次手工計(jì)算,OneFlow提供了一個(gè)工具類NdIndexOffsetHelper來方便做上述轉(zhuǎn)換。

        2
        NdIndexOffsetHelper

        NdIndexOffsetHelper的主體方法如下:

        • NdIndexToOffset方法把高維索引轉(zhuǎn)為一維偏移量

        • OffsetToNdIndex方法把一維偏移量轉(zhuǎn)為高維索引


        有了這么一個(gè)工具類,那我們就可以很輕松的寫出一版Naive Permute Kernel了,核函數(shù)如下:
        template<size_t?num_dims,?size_t?movement_size,?typename?IndexType>
        __global__?void?PermuteKernel(PermuteKernelParams?params)?{
        ??using?T?=?typename?std::aligned_storage::type;
        ??const?T*?src?=?reinterpret_cast<const?T*>(params.src);
        ??T*?dst?=?reinterpret_cast(params.dst);
        ??IndexType?src_index[num_dims];
        ??IndexType?dst_index[num_dims];
        ??CUDA_1D_KERNEL_LOOP_T(IndexType,?i,?params.count)?{
        ????params.dst_index_helper.OffsetToNdIndex(i,?dst_index);
        #pragma?unroll
        ????for?(size_t?dim?=?0;?dim???????src_index[params.permutation[dim]]?=?dst_index[dim];
        ????}
        ????IndexType?src_offset?=?params.src_index_helper.NdIndexToOffset(src_index);
        ????dst[i]?=?src[src_offset];
        ??}
        }

        • PermuteKernelParams是一個(gè)結(jié)構(gòu)體,里面有初始化好的NdIndexOffsetHelper(src和dst各一個(gè)),元素總數(shù)count還有變換后的維度順序permutation

        • 首先我們?nèi)〉卯?dāng)前處理輸出元素的高維索引dst_index,然后賦給經(jīng)過Permute后的輸入索引src_index

        • 將輸入索引轉(zhuǎn)換成一維偏移量src_offset,取到輸入元素并賦給對(duì)應(yīng)的輸出

        3
        常規(guī)情況的優(yōu)化

        這種樸素Permute Kernel的計(jì)算代價(jià)來源于坐標(biāo)換算,訪存開銷則來源于數(shù)據(jù)移動(dòng),針對(duì)這兩個(gè)角度我們引入以下優(yōu)化方案。

        1. IndexType靜態(tài)派發(fā)

        隨著深度學(xué)習(xí)模型越來越大,參與運(yùn)算元素的個(gè)數(shù)可能超過int32_t表示的范圍。并且在坐標(biāo)換算中,不同整數(shù)類型的除法運(yùn)算開銷不一樣。因此我們給核函數(shù)增加了一個(gè)模板參數(shù)IndexType用于指定索引的數(shù)據(jù)類型,根據(jù)參與Permute的元素個(gè)數(shù)來決定IndexTypeint32_t還是int64_t。

        2. 合并冗余維度

        在一些特殊情形下,Permute維度是可以進(jìn)行合并的,其規(guī)則如下:

        • 大小為1的維度可以直接去除

        • 連續(xù)排列的維度可以合并成一個(gè)維度


        針對(duì)第二條規(guī)則,我們考慮以下Permute情況:

        #?0,?1,?2,?3)?->?(2,?3,?0,?1)
        x?=?flow.randn(3,?4,?5,?6)
        y?=?x.permute(2,?3,?0,?1)
        y.shape?
        (5,?6,?3,?4)

        顯然這是一個(gè)四維的Permute情形,但這里第2,3維,第0,1維是一起Permute的,所以我們可以看成是一種二維的Permute情形:

        #?(0,?1,?2,?3)?->?((2,?3),?(0,?1))
        x?=?x.reshape(x.shape[0]*x.shape[1],?x.shape[2]*x.shape[3])
        y?=?x.permute(1,?0)
        y?=?y.reshape(x.shape[2],?x.shape[3],?x.shape[0],?x.shape[1])

        合并維度后,在利用NdIndexOffsetHelper根據(jù)偏移量計(jì)算索引時(shí),合并前需要計(jì)算成四維索引,而合并后我們只需計(jì)算成二維索引。相比合并前減少除法和乘法的次數(shù),進(jìn)而提升速度。

        3. 使用更大的訪問粒度

        細(xì)心的朋友們可能觀察到核函數(shù)中有一個(gè)模板參數(shù)size_t movement_size,它表示的是訪問元素的粒度。

        在Nvidia性能優(yōu)化博客increase Performance with Vectorized Memory Access中提到可以通過向量化內(nèi)存操作來提高CUDA Kernel性能,能夠減少指令數(shù),提高帶寬利用率。鏈接:https://developer.nvidia.com/blog/cuda-pro-tip-increase-performance-with-vectorized-memory-access/)

        我們?cè)O(shè)置訪問粒度的規(guī)則如下:

        • CUDA支持的訪問粒度為1B,2B,4B,8B,16B,粒度越大性能越好

        • 最后一個(gè)維度是作為整體來移動(dòng)的,即permutation[n-1]==x.dims[n-1],且大小是新訪問粒度的倍數(shù)

        • 保證數(shù)據(jù)指針滿足新訪問粒度的對(duì)齊要求


        針對(duì)規(guī)則2,對(duì)應(yīng)著以下Permute場(chǎng)景:
        (0, 1, 2, 3) -> (0, 2, 1, 3)
        其中最后一維并沒有變化,僅僅是第1,2維進(jìn)行交換,那么我們可以使用更大的訪問粒度來讀取數(shù)據(jù),再進(jìn)行Permute操作。代碼中通過GetMovementSize函數(shù)來確定訪問粒度的大小。

        我們使用Nsight Compute對(duì)PyTorch的Permute和原生Copy操作對(duì)比測(cè)試運(yùn)行時(shí)間和帶寬,測(cè)試結(jié)果如下:


        其中測(cè)試環(huán)境為NVIDIA A100 40GB,場(chǎng)景為(0, 1, 2)->(1, 0, 2),橫坐標(biāo)表示數(shù)據(jù)形狀及數(shù)據(jù)類型。測(cè)試數(shù)據(jù)覆蓋了16MB到128MB不同大小的數(shù)據(jù),數(shù)據(jù)類型包含fp32和half兩種類型。

        從上面兩張圖可以看到,OneFlow在大部分情況下都可以逼近甚至略高于Copy操作的帶寬。與PyTorch對(duì)比,在操作耗時(shí)上最少快1.24倍,最快能達(dá)1.4倍。
        這里Permute的帶寬比原生Copy還高一點(diǎn),是因?yàn)镃opy Kernel里沒有做unroll指令間并行優(yōu)化,而Permute Kernel內(nèi)部做了相關(guān)優(yōu)化,這里僅做參考。
        使用上面的兩個(gè)優(yōu)化技巧,OneFlow就能輕易做到比PyTorch的實(shí)現(xiàn)要快了。常規(guī)的Permute適用情況比較廣泛,也因此可能存在訪存不合并的情況。在一些特殊的場(chǎng)景下,我們可以通過合并訪存以提升帶寬利用率和速度,這就引出我們下個(gè)關(guān)于BatchTranspose優(yōu)化的話題。

        4
        BatchTranspose優(yōu)化

        BatchTranspose操作即矩陣轉(zhuǎn)置,僅交換矩陣最后的兩維,以下情況均符合BatchTranspose的定義,其中括號(hào)內(nèi)容表示維度的順序:

        (0,?1)?->?(1,?0)
        (0,?1,?2)?->?(0,?2,?1)

        在樸素的Permute方案中,對(duì)于最后一維作為整體移動(dòng)的情況下,已經(jīng)進(jìn)行充分的優(yōu)化。但實(shí)際場(chǎng)景中還存在矩陣轉(zhuǎn)置的情況,此時(shí)無法應(yīng)用第三條增大訪問粒度的優(yōu)化操作,并且不滿足訪存合并要求,導(dǎo)致性能不佳。以Pytorch為例,在數(shù)據(jù)大小為128MB情況下進(jìn)行BatchTranspose時(shí),因?yàn)槲春喜⒌脑L存導(dǎo)致實(shí)際讀取數(shù)據(jù)量遠(yuǎn)大于寫入數(shù)據(jù)量(7-8倍)。


        在英偉達(dá)性能優(yōu)化博客An Efficient Matrix Transpose in CUDA C/C++(https://developer.nvidia.com/blog/efficient-matrix-transpose-cuda-cc/)中,其做法是設(shè)置一塊Shared Memory,然后將一行數(shù)據(jù)讀取到Shared Memory,再按列順序?qū)hared Memory中的元素寫回到Global Memory中。得益于Shared Memory訪問粒度小的特性(Global Memory是32B,Shared Memory是4B),進(jìn)而避免Global Memory的訪存不連續(xù)的問題。

        Shared Memory相比Global Memory有15倍更高的帶寬,20-40倍更低的延遲,因此額外引入的讀寫開銷可以忽略不計(jì)。

        此外我們給Shared Memory多padding了一個(gè)元素,進(jìn)而讓以列順序訪問的元素能夠均勻分布在32個(gè)bank上,避免bank conflict。對(duì)應(yīng)的示意圖如下(其中灰色部分代表Padding元素):


        基于上述提到的點(diǎn)我們實(shí)現(xiàn)了一版BatchTranspose,代碼如下:

        template<size_t?num_dims,?size_t?movement_size,?size_t?tile_size,?typename?IndexType>
        __global__?void?BatchTransposeKernel(const?void*?src_ptr,?void*?dst_ptr,?IndexType?H,?IndexType?W,
        ?????????????????????????????????????IndexType?num_tile_rows,?IndexType?num_tile_cols,
        ?????????????????????????????????????int32_t?block_nums)
        ?
        {
        ??using?T?=?typename?std::aligned_storage::type;
        ??__shared__?T?tile[tile_size][tile_size?+?1];??//?To?avoid?bank?conflict.

        ??const?T*?src?=?reinterpret_cast<const?T*>(src_ptr);
        ??T*?dst?=?reinterpret_cast(dst_ptr);

        ??IndexType?batch_num_tile?=?num_tile_rows?*?num_tile_cols;
        ??for?(int?i?=?blockIdx.x,?step?=?gridDim.x;?i?????const?IndexType?batch_index?=?i?/?batch_num_tile;??//?the?index?of?batch.
        ????const?IndexType?flatten_index?=
        ????????i?-?batch_index?*?batch_num_tile;??
        ????const?IndexType?row_index?=?flatten_index?/?num_tile_cols;??//?the?row?index?of?tile?in?a?batch.
        ????const?IndexType?col_index?=
        ????????flatten_index
        ????????-?row_index
        ??????????????*?num_tile_cols;??//?the?col?index?of?tile?in?a?batch.
        ????const?IndexType?offset?=?batch_index?*?H?*?W;
        ????IndexType?x?=?col_index?*?tile_size?+?threadIdx.x;
        ????IndexType?y?=?row_index?*?tile_size?+?threadIdx.y;
        ????if?(x???????IndexType?y_range?=
        ??????????((tile_size?-?threadIdx.y)?#pragma?unroll
        ??????for?(int?i?=?0;?i?????????tile[threadIdx.y?+?i][threadIdx.x]?=?src[offset?+?(y?+?i)?*?W?+?x];
        ??????}
        ????}
        ????__syncthreads();
        ????x?=?row_index?*?tile_size?+?threadIdx.x;
        ????y?=?col_index?*?tile_size?+?threadIdx.y;
        ????if?(x???????IndexType?x_range?=
        ??????????((tile_size?-?threadIdx.y)?#pragma?unroll
        ??????//?`i?
        ??????for?(int?i?=?0;?i?????????dst[offset?+?(y?+?i)?*?H?+?x]?=?tile[threadIdx.x][threadIdx.y?+?i];
        ??????}
        ????}
        ????__syncthreads();
        ??}
        }

        其中BatchTranspose的優(yōu)化涉及以下兩點(diǎn):

        顯式展開循環(huán)

        在先前版本,我們的for循環(huán)寫法如下:

        #pragma?unroll
        for?(int?i?=?0;?threadIdx.y?+?i?????...
        }

        即便是加入了預(yù)編譯指令#pragma unroll,在Nsight Compute里的匯編代碼中,我們也只能看到兩條相關(guān)指令,也就意味著這部分循環(huán)并沒有展開。

        而for循環(huán)里的條件,我們可以化簡(jiǎn)并提取出來,如下代碼所示:

        IndexType?y_range?=?((tile_size?-?threadIdx.y)?#pragma?unroll
        for?(int?i?=?0;?i???...
        }

        此時(shí)對(duì)應(yīng)的匯編代碼顯示這部分的循環(huán)進(jìn)行了展開,在帶寬利用率和速度上有24%的提升。

        針對(duì)half2版本優(yōu)化

        特別的,針對(duì)half數(shù)據(jù)類型,且轉(zhuǎn)置維度均能被2整除的情況下,我們可以進(jìn)一步利用half2來合并。

        Shared Memory的一個(gè)bank寬度為4B,那么一個(gè)bank能塞下兩個(gè)half數(shù)據(jù),示意圖如下:


        那么加載到Shared Memory的時(shí)候,我們可以將兩個(gè)half數(shù)據(jù)合并為half2類型進(jìn)行加載

        但是取列元素的時(shí)候,因?yàn)樵胤植荚趦蓚€(gè)不同的bank上,不能合并成half2直接取。需要構(gòu)造一個(gè)臨時(shí)的half2對(duì)象,分別將兩個(gè)bank上的half元素存儲(chǔ)到該half2對(duì)象,再寫回到Global Memory里。對(duì)應(yīng)的代碼如下:

        template<size_t?num_dims,?size_t?tile_size,?typename?IndexType>
        __global__?void?BatchTransposeMovement2Kernel(const?void*?src_ptr,?void*?dst_ptr,?IndexType?rows,
        ??????????????????????????????????????????????IndexType?cols,?IndexType?num_tile_rows,
        ??????????????????????????????????????????????IndexType?num_tile_cols,?int32_t?block_nums)
        ?
        {
        ??static_assert(tile_size?%?2?==?0);
        ??using?T_MOV2?=?typename?std::aligned_storage<2,?2>::type;
        ??using?T_MOV4?=?typename?std::aligned_storage<4,?4>::type;

        ??const?T_MOV4*?src?=?reinterpret_cast<const?T_MOV4*>(src_ptr);
        ??T_MOV4*?dst?=?reinterpret_cast(dst_ptr);

        ??//?Use?union?structure?to?process?Load?and?Store.
        ??__shared__?union?{
        ????T_MOV2?tile_m2[tile_size][tile_size?+?2];??????//?half?[64][66]
        ????T_MOV4?tile_m4[tile_size][tile_size?/?2?+?1];??//?half2?[64][33]
        ??}?tile_mem;

        ??IndexType?batch_num_tile?=?num_tile_rows?*?num_tile_cols;
        ??for?(int?i?=?blockIdx.x,?step?=?gridDim.x;?i?????const?IndexType?batch_index?=?i?/?batch_num_tile;??//?the?index?of?batch.
        ????const?IndexType?flatten_index?=
        ????????i?-?batch_index?*?batch_num_tile;??//?the?flatten?index?of?tile?in?a?batch.

        ????const?IndexType?row_index?=?flatten_index?/?num_tile_cols;??//?the?row?index?of?tile?in?a?batch.
        ????const?IndexType?col_index?=
        ????????flatten_index
        ????????-?row_index
        ??????????????*?num_tile_cols;??//?equal?to?k?%?num_tile_cols.?the?col?index?of?tile?in?a?batch.
        ????const?IndexType?offset?=?batch_index?*?rows?*?cols;
        ????IndexType?x?=
        ????????col_index?*?tile_size?+?threadIdx.x?*?2;??//?cause?each?thread?process?a?half2?element,?we?need?to?multiply?2?for?threadIdx.x.
        ????IndexType?y?=?row_index?*?tile_size?+?threadIdx.y;
        ????if?(x???????//?each?thread?process?4?elements.
        ??????IndexType?y_range?=
        ??????????((tile_size?-?threadIdx.y)?#pragma?unroll
        ??????//?`i?
        ??????for?(int?i?=?0;?i?????????//?each?thread?load?a?half2.
        ????????tile_mem.tile_m4[threadIdx.y?+?i][threadIdx.x]?=?src[(offset?+?(y?+?i)?*?cols?+?x)?/?2];
        ??????}
        ????}
        ????__syncthreads();
        ????x?=?row_index?*?tile_size?+?threadIdx.x?*?2;??//?cause?each?thread?process?a?half2?element,?we?need?to?multiply?2?for?threadIdx.x.
        ????y?=?col_index?*?tile_size?+?threadIdx.y;
        ????if?(x???????IndexType?x_range?=
        ??????????((tile_size?-?threadIdx.y)?#pragma?unroll
        ??????//?`i?
        ??????for?(int?i?=?0;?i?????????/*
        ????????When?write?back?as?column,?it?cannot?be?stored?as?half2?directly.
        ????????So?we?split?as?2?half?elements,?and?write?back?separately.
        ????????*/

        ????????union?{
        ??????????T_MOV4?m4;
        ??????????T_MOV2?m2[2];
        ????????}?tmp_storage;
        ????????tmp_storage.m2[0]?=?tile_mem.tile_m2[threadIdx.x?*?2][threadIdx.y?+?i];
        ????????tmp_storage.m2[1]?=?tile_mem.tile_m2[threadIdx.x?*?2?+?1][threadIdx.y?+?i];
        ????????dst[(offset?+?(y?+?i)?*?rows?+?x)?/?2]?=?tmp_storage.m4;
        ??????}
        ????}
        ????__syncthreads();
        ??}
        }

        在前面相同的測(cè)試條件下,我們將測(cè)試場(chǎng)景設(shè)置為(0, 1, 2)->(0, 2, 1),測(cè)試結(jié)果如下:


        可以看到,OneFlow在大部分情況下,無論是計(jì)算耗時(shí),還是帶寬利用率都可以逼近原生Copy操作。在操作耗時(shí)上與PyTorch對(duì)比,fp32數(shù)據(jù)類型情況下最少快3倍,最快能達(dá)3.2倍。而half數(shù)據(jù)類型情況下OneFlow優(yōu)勢(shì)更為明顯,最快能達(dá)6.3倍。

        5
        未來優(yōu)化方向

        經(jīng)過我們實(shí)際測(cè)試,在坐標(biāo)換算過程中,整數(shù)除法的運(yùn)算開銷比較大。而市面上有很多優(yōu)秀的運(yùn)算庫如Eigen,lemire/fast_division都提供了基于int32,int64類型的快速除法,根據(jù)官方提供的benchmark測(cè)試結(jié)果,快速除法相較于標(biāo)準(zhǔn)除法能提升1-3倍性能。未來我們將探索合適的快速除法用于坐標(biāo)轉(zhuǎn)換中,進(jìn)一步提升運(yùn)算速度。

        6
        展望

        從本文和之前OneFlow發(fā)布的CUDA優(yōu)化文章中可以看到,在kernel優(yōu)化過程中有一些常見、通用的手段,如合并冗余以減少計(jì)算次數(shù)、調(diào)整訪問粒度以提高訪存效率。

        這些常見、通用的優(yōu)化手段,是有可能作為深度學(xué)習(xí)編譯器的組件被提煉出,來部分替代手工調(diào)優(yōu)工作。

        但是,自動(dòng)優(yōu)化邊界的確定、以及如何自動(dòng)優(yōu)化,都提出了比手工調(diào)優(yōu)更高的要求,據(jù)我們所知也還是一個(gè)半開放的問題。歡迎感興趣的同道,在OneFlow倉庫提issue討論、研發(fā)。

        參考資料

        https://developer.nvidia.com/blog/cuda-pro-tip-increase-performance-with-vectorized-memory-access/https://developer.nvidia.com/blog/efficient-matrix-transpose-cuda-cc/https://on-demand.gputechconf.com/gtc/2018/presentation/s81006-volta-architecture-and-performance-optimization.pdf

        題圖源自geralt, Pixabay

        其他人都在看
        點(diǎn)擊“閱讀原文,歡迎下載體驗(yàn)OneFlow新一代開源深度學(xué)習(xí)框架



        ??
        瀏覽 113
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            啪啪毛片 | h网站在线| www.17草 | 国语自产免费精品视频在 | 无码中文视频 | 插插插91 | 青青草怡红院 | 黄片操操 | 老司机伊人网 | 三级久久久 |