CUDA 9中張量核(Tensor Cores)編程

CUDA 9中張量核(Tensor Cores)編程

Programming Tensor Cores in CUDA 9

一.概述

新的Volta GPU架構的一個重要特點是它的Tensor核,使Tesla V100加速器的峯值吞吐量是上一代Tesla P100的32位浮點吞吐量的12倍。Tensor內核使人工智能程序員能夠使用混合精度來獲得更高的吞吐量,而不犧牲精度。

Tensor核心已經在許多深度學習框架(包括Tensorflow、PyTorch、MXNet和Caffe2)中支持深度學習訓練,無論是在主版本中還是通過pull請求。有關在使用這些框架時啓用Tensor核心的更多信息,請參閱《混合精度訓練指南》。對於深度學習推理,最近的TensorRT 3版本也支持Tensor核心。

本文將展示如何使用CUDA庫在自己的應用程序中使用張量核,以及如何在CUDA C++設備代碼中直接編程。
在這裏插入圖片描述
二.什麼是張量核(Tensor Cores)?

特斯拉V100的張量核心是可編程的矩陣乘法和累加單元,可以提供多達125 Tensor tflop的訓練和推理應用。特斯拉V100
GPU包含640個Tensor Cores:8/SM。Tensor內核及其相關的數據路徑是定製的,以顯著提高浮點計算吞吐量,只需適當的區域和功耗。時鐘選通廣泛應用於最大限度地節省功耗。
在這裏插入圖片描述
每個張量核提供一個4x4x4矩陣處理數組,它執行操作D=a*B+C,其中a、B、C和D是4×4矩陣,如圖1所示。矩陣乘法輸入A和B是FP16矩陣,而累積矩陣C和D可以是FP16或FP32矩陣。
在這裏插入圖片描述
每個張量核執行64個浮點FMA混合精度操作每個時鐘(FP16輸入乘法與全精度積和FP32累加,如圖2所示)和8張量核在一個SM執行總共1024個浮點操作每個時鐘。與使用標準FP32操作的Pascal GP100相比,每SM深度學習應用程序的吞吐量顯著增加了8倍,因此Volta V100 GPU的吞吐量與Pascal P100 GPU相比總共增加了12倍。張量覈對FP16輸入數據進行運算,FP32累加。如圖2所示,對於4x4x4矩陣乘法,FP16乘法產生的全精度結果是在FP32運算中與給定點積中的其他乘積累積的結果。

三. CUDA庫中的張量核

使用Tensor核的兩個CUDA庫是cuBLAS和cuDNN。cuBLAS使用張量核加速GEMM計算(GEMM是矩陣-矩陣乘法的BLAS術語);cuDNN使用張量核加速卷積和遞歸神經網絡(RNNs)。

許多計算應用程序使用GEMM:信號處理、流體動力學等等。隨着這些應用程序的數據大小呈指數級增長,這些應用程序需要在處理速度上進行匹配。圖3中的混合精度GEMM性能圖顯示,張量核顯然滿足了這一需求。

提高卷積速度的需求同樣巨大;例如,深神經網絡(DNNs)使用了許多層卷積。人工智能研究人員每年都在設計越來越深的神經網絡;最深的神經網絡中的卷積層現在有幾十個。訓練DNNs需要卷積層在正向和反向傳播期間重複運行。

圖4中的卷積性能圖顯示,張量核滿足卷積性能的需要。

兩個性能圖表都顯示,特斯拉V100的張量核心提供了數倍於上一代特斯拉P100的性能。性能改進這一巨大的變化如何在計算領域工作:使交互性成爲可能,啓用“假設”方案研究,或減少服務器場使用。如果在應用程序中使用GEMM或卷積,請使用下面的簡單步驟來提高工作效率。

四.如何在立方體中使用張量核

通過對現有cuBLAS代碼進行一些更改,可以利用張量核。這些變化是在使用cuBLAS API時的小變化。

下面的示例代碼應用一些簡單的規則來指示cuBLAS應該使用張量核;這些規則在代碼後面顯式枚舉。

示例代碼

下面的代碼與以前的架構中用於調用cuBLAS中GEMM的通用代碼基本相同。

// First, create a cuBLAS handle:

cublasStatus_t cublasStat = cublasCreate(&handle);

// Set the math
mode to allow cuBLAS to use Tensor Cores:

cublasStat = cublasSetMathMode(handle,
CUBLAS_TENSOR_OP_MATH);

// Allocate and
initialize your matrices (only the A matrix is shown):

size_t matrixSizeA = (size_t)rowsA * colsA;

T_ELEM_IN **devPtrA = 0;

cudaMalloc((void**)&devPtrA[0], matrixSizeA * sizeof(devPtrA[0][0]));

T_ELEM_IN A = (T_ELEM_IN *)malloc(matrixSizeA * sizeof(A[0]));

memset( A, 0xFF, matrixSizeA* sizeof(A[0]));

status1 = cublasSetMatrix(rowsA, colsA, sizeof(A[0]), A, rowsA, devPtrA[i], rowsA);

// … allocate
and initialize B and C matrices (not shown) …

// Invoke the
GEMM, ensuring k, lda, ldb, and ldc are all multiples of 8,

// and m is a
multiple of 4:

cublasStat = cublasGemmEx(handle, transa, transb, m, n, k, alpha,

                      A, CUDA_R_16F, lda,

                      B, CUDA_R_16F, ldb,

beta, C, CUDA_R_16F, ldc, CUDA_R_32F, algo);

五.一些簡單的規則

cuBLAS用戶將注意到其現有cuBLAS GEMM代碼的一些變化:

例程必須是GEMM;目前,只有GEMM支持Tensor核心執行。

數學模式必須設置爲CUBLAS_TENSOR_OP_math。浮點數學不具有關聯性,因此張量核心數學例程的結果與類似的非張量核心數學例程的結果不完全等價。cuBLAS要求用戶“選擇”使用張量核。

k、lda、ldb和ldc都必須是8的倍數;m必須是4的倍數。張量核心數學例程以八個值的步驟遍歷輸入數據,因此矩陣的維數必須是八的倍數。

矩陣的輸入和輸出數據類型必須是半精度或單精度。(上面只顯示了CUDA_R_16F,但也支持CUDA_R_32F。)不滿足上述規則的GEMM將返回到非張量核心實現。 GEMM性能

如前所述,Tensor內核提供的GEMM性能是以前硬件的幾倍。圖3顯示了GP100(Pascal)和GV100(Volta)硬件的比較性能。
在這裏插入圖片描述
六.如何在cuDNN中使用張量核

在cuDNN中使用Tensor核也很容易,而且只涉及對現有代碼的微小更改。

示例代碼

在cuDNN中使用張量核的示例代碼可以在conv中找到_示例.cpp在cuDNN samples目錄中;複製了下面的一些摘錄。(cuDNN samples目錄與文檔打包在一起。)

// Create a cuDNN handle:

checkCudnnErr(cudnnCreate(&handle_));

// Create your tensor descriptors:

checkCudnnErr( cudnnCreateTensorDescriptor( &cudnnIdesc ));

checkCudnnErr( cudnnCreateFilterDescriptor( &cudnnFdesc ));

checkCudnnErr( cudnnCreateTensorDescriptor( &cudnnOdesc ));

checkCudnnErr( cudnnCreateConvolutionDescriptor( &cudnnConvDesc ));

// Set tensor dimensions as multiples of
eight (only the input tensor is shown here):

int dimA[] = {1, 8, 32, 32};

int strideA[] = {8192, 1024, 32, 1};

checkCudnnErr( cudnnSetTensorNdDescriptor(cudnnIdesc, getDataType(),

                                      convDim+2, dimA, strideA) );

// Allocate and initialize tensors (again,
only the input tensor is shown):

checkCudaErr( cudaMalloc((void**)&(devPtrI), (insize) * sizeof(devPtrI[0]) ));

hostI = (T_ELEM*)calloc (insize, sizeof(hostI[0]) );

initImage(hostI, insize);

checkCudaErr( cudaMemcpy(devPtrI, hostI, sizeof(hostI[0]) * insize, cudaMemcpyHostToDevice));

// Set the compute data type (below as
CUDNN_DATA_FLOAT):

checkCudnnErr( cudnnSetConvolutionNdDescriptor(cudnnConvDesc,

                                           convDim,

padA,

convstrideA,

dilationA,

CUDNN_CONVOLUTION,

CUDNN_DATA_FLOAT) );

// Set the math type to allow cuDNN to use
Tensor Cores:

checkCudnnErr( cudnnSetConvolutionMathType(cudnnConvDesc, CUDNN_TENSOR_OP_MATH) );

// Choose a supported algorithm:

cudnnConvolutionFwdAlgo_t algo = CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_PRECOMP_GEMM;

// Allocate your workspace:

checkCudnnErr( cudnnGetConvolutionForwardWorkspaceSize(handle_, cudnnIdesc,

               cudnnFdesc, cudnnConvDesc,

cudnnOdesc, algo, &workSpaceSize) );

if (workSpaceSize > 0) {

cudaMalloc(&workSpace, workSpaceSize);

}

// Invoke the convolution:

checkCudnnErr( cudnnConvolutionForward(handle_, (void*)(&alpha), cudnnIdesc, devPtrI,

cudnnFdesc, devPtrF, cudnnConvDesc, algo,

workSpace, workSpaceSize, (void*)(&beta),

cudnnOdesc, devPtrO) );

七.一些簡單的規則

注意一些與常用cuDNN用法不同的變化:

卷積算法必須是ALGO 1(前向的隱式預處理)。在將來的cuDNN版本中,除ALGO 1之外的其他卷積算法可能使用張量核。

數學類型必須設置爲CUDNN_TENSOR_OP_math。與cuBLAS一樣,張量核數學例程的結果與類似的非張量核數學例程的結果並不完全等價,因此cuDNN要求用戶“選擇”使用張量核。 輸入和輸出通道尺寸必須是8的倍數。同樣,在cuBLAS中,張量核心數學例程以8個值的步長遍歷輸入數據,因此輸入數據的維數必須是8的倍數。

卷積的輸入、濾波和輸出數據類型必須爲半精度。

不滿足上述規則的卷積將返回到非張量核心實現。

上面的示例代碼顯示了NCHW數據格式,請參見conv_示例.cppNHWC支持的樣本。 卷積性能

如前所述,張量核的卷積性能是以前硬件的幾倍。圖4顯示了GP100(Pascal)和GV100(Volta)硬件的比較性能。
在這裏插入圖片描述
八.在CUDA 9.0中對張量核的編程訪問

通過CUDA9.0訪問內核中的Tensor核是一個預覽功能。這意味着本節中描述的數據結構、api和代碼在將來的CUDA版本中可能會發生更改。

雖然CuBLAS和CUDNN覆蓋了張量核的許多潛在用途,但是也可以直接在CUDA C++中編程它們。張量核在CUDA 9.0中通過nvcuda::wmma命名空間中的一組函數和類型公開。它們允許將值加載或初始化爲張量核所需的特殊格式,執行矩陣乘法累加(MMA)步驟,並將值存儲回內存。在程序執行期間,多個張量核被一個完全扭曲同時使用。這允許warp在非常高的吞吐量下執行16x16x16mma(圖5)。
在這裏插入圖片描述
讓看一個簡單的例子,它展示瞭如何使用WMMA(Warp Matrix Multiply Accumulate)API執行矩陣乘法。請注意,這個示例並不是爲高性能而調整的,它主要用作API的演示。爲了獲得更好的性能,可以應用於此代碼的優化示例,請查看CUDA工具包中的cudatensorcoregem示例。爲了獲得最高的生產性能,應使用立方塊,如上所述。標題和命名空間

WMMA API包含在mma.h頭文件中。完整的名稱空間是nvcuda::wmma::*,但是在整個代碼中保持wmma顯式很有用,因此將只使用nvcuda名稱空間。
#include <mma.h>
using namespace nvcuda;
九.聲明和初始化

完整的GEMM規範允許算法處理a或b的轉置,並允許數據跨距大於矩陣中的跨距。爲了簡單起見,假設a和b都沒有被轉置,並且內存和矩陣的前導維數是相同的。 將採用的策略是讓一個warp負責輸出矩陣的一個16×16部分。通過使用二維網格和線程塊,可以有效地將曲面平鋪到二維輸出矩陣上。
// The only
dimensions currently supported by WMMA

const int WMMA_M = 16;

const int WMMA_N = 16;

const int WMMA_K = 16;

global void wmma_example(half *a, half *b, float *c,

                         int M, int N, int K, 

                         float alpha, float beta) 

{
// Leading dimensions. Packed with no transpositions.

int lda = M;

int ldb = K;

int ldc = M;  

// Tile using a 2D grid

int warpM = (blockIdx.x * blockDim.x + threadIdx.x) / warpSize;

int warpN = (blockIdx.y * blockDim.y + threadIdx.y);

在執行MMA操作之前,操作數矩陣必須在GPU的寄存器中表示。由於MMA是一個全曲速操作,這些寄存器分佈在曲速的線程中,每個線程持有整個矩陣的一個片段。各個矩陣參數與其片段之間的映射是不透明的,因此程序不應對此進行假設。在CUDA中,片段是一種模板類型,模板參數描述片段保存的矩陣(a、B或累加器)、整個WMMA操作的形狀、數據類型,以及對於a和B矩陣,數據是主要行還是主要列。最後一個參數可用於執行A或B矩陣的換位。這個例子沒有換位,所以兩個矩陣都是列主矩陣,這是GEMM的標準。
// Declare the
fragments

wmma::fragment<wmma::matrix_a, WMMA_M, WMMA_N, WMMA_K, half, wmma::col_major> a_frag;

wmma::fragment<wmma::matrix_b, WMMA_M, WMMA_N, WMMA_K, half, wmma::col_major> b_frag;

wmma::fragment<wmma::accumulator, WMMA_M, WMMA_N, WMMA_K, float> acc_frag;

wmma::fragment<wmma::accumulator, WMMA_M, WMMA_N, WMMA_K, float> c_frag;

wmma::fill_fragment(acc_frag, 0.0f);

初始化步驟的最後一部分是用零填充累加器片段。

內循環

用於GEMM的策略是計算每個曲面的輸出矩陣的一個平鋪。爲此,需要循環遍歷矩陣的行和列。這是沿着兩個矩陣的K維,並生成一個MxN輸出平鋪。load matrix函數從內存(在本例中是全局內存,儘管它可以是任何內存空間)獲取數據並將其放入片段中。加載的第三個參數是矩陣內存中的“前導維度”;加載的16×16平鋪在內存中是不連續的,因此函數需要知道連續列(或行,如果這些列是行的主要片段)之間的跨距。

MMA調用累積到位,因此第一個和最後一個參數都是先前初始化爲零的累加器片段。

// Loop over the K-dimension

for (int i = 0; i < K; i += WMMA_K) {

    int aRow = warpM * WMMA_M;

    int aCol = i;

    int bRow = i;

    int bCol = warpN * WMMA_N;

    // Bounds

checking

    if (aRow < M && aCol < K && bRow < K && bCol < N) {

        // Load the

inputs

        wmma::load_matrix_sync(a_frag, a + aRow + aCol * lda, lda);

        wmma::load_matrix_sync(b_frag, b + bRow + bCol * ldb, ldb);

        // Perform the

matrix multiplication

        wmma::mma_sync(acc_frag, a_frag, b_frag, acc_frag);

結束
acc_frag現在保存基於A和B的乘法的該曲面輸出平鋪的結果。完整的GEMM規範允許縮放該結果,並將其累積在適當的矩陣上。實現這種縮放的一種方法是對片段執行按元素的操作。雖然沒有定義從矩陣座標到線程的映射,但是元素操作不需要知道這個映射,所以仍然可以使用片段執行。因此,對片段執行縮放操作或將一個片段的內容添加到另一個片段是合法的,只要這兩個片段具有相同的模板參數。如果片段具有不同的模板參數,則結果未定義。利用這個特性,我們在C語言中加載現有的數據,並以正確的比例,用它累積到目前爲止的計算結果。

// Load in
current value of c, scale by beta, and add to result scaled by alpha

int cRow = warpM * WMMA_M;

int cCol = warpN * WMMA_N;



if (cRow < M && cCol < N) {

    wmma::load_matrix_sync(c_frag, c + cRow + cCol * ldc, ldc, wmma::mem_col_major);

    

    for(int i=0; i < c_frag.num_elements; i++) {

        c_frag.x[i] = alpha * acc_frag.x[i] + beta * c_frag.x[i];

    }

最後,將數據存儲到內存中。目標指針可以是GPU可見的任何內存空間,並且必須指定內存中的前導維度。還有一個選項可以指定輸出是寫入row還是column major。

    // Store the

output

    wmma::store_matrix_sync(c + cRow + cCol * ldc, c_frag, ldc, wmma::mem_col_major);

}

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章