スパース ニューラル ネットワークの順伝播
仕事の背景と要件
この宿題は 2019 年の並列コンペの質問です。そのコンペは覚えていません。とにかく、問題の一般的な要件は次のとおりです: 60000 * 1024 の行列 A を左の乗算行列として使用し、120 個の疎行列 B を使用する1024 * 1024 [i]、0 ≤ i ≤ 119 0\le i\le 1190≤私≤119 を右の乗算行列として使用します。A と B[i] を乗算して行列 C を取得します。C の次元は 60000 * 1024 です。C 行列にバイアス項目を追加し、活性化関数 relu を使用して活性化します。活性化された C 行列は、次の乗算のための行列 A と B [i+1] 乗算します。つまり、ニューラル ネットワークは 1 回前方に伝播します。動作時間は gettimeofday(&time,NULL); で取得します。
元のコードは、3 層のループを使用する従来の行列乗算を使用しており、各内側のループで取得する行列の要素が計算されます。時間計算量は O(n 3 ) で、行列乗算の計算には約 10 分かかります。120回の計算はすごいですね。したがって、この割り当ての目的は、コードを可能な限り最適化し、行列の乗算とバイアス + レルの時間を短縮することです。先生は、前回のセッションで桜のキーボードを使って最速の最適化を行ったグループに褒美を与えましたが、今回のセッションで最適化が行われるかどうかはわかりませんが、いずれにせよ、私は最速ではありません。
最適化のアイデアのソース
今学期の宿題は合計 5 つあり、そのうち 4 つは行列の乗算の最適化に関するもので、最もパフォーマンスの良いアルゴリズムを選別して大きな課題に適用するだけです。
- 最初の割り当ては 2 つの正方行列を乗算することであり、正方行列のサイズは 100 ~ 2000 であり、100 は合計 20 セットのデータ テストのステップ サイズです。ループ順序を変更し、openmp と openblas を使用します。
上の図は、ループの順序を変更した結果を示しています。
上図はループの順序を変更して openmp を追加した結果ですが、openmp ステートメントの出力が間違っている順序が 2 つあるため、描画されていません。
上の写真は openblas の結果です。
void gemm_OpenBlas(double *A, double *B, double *C, int m, int k, int n)
{
enum CBLAS_ORDER order = CblasRowMajor;
enum CBLAS_TRANSPOSE transposeA = CblasNoTrans;
enum CBLAS_TRANSPOSE transposeB = CblasNoTrans;
double alpha = 1;
double beta = 1;
cblas_dgemm(order,transposeA,transposeB,m,n,k,alpha,A,k,B,n,beta,C,n);
}
gemm_OpenBlas(A, B, C_yours, m, k, n);
すぐ!2000 × 2000 2000\times20002000年×2000 年の 2 つの行列の乗算にはわずか 0.25 秒しかかかりません。
- 課題 3 は、CUDA 関数を使用した課題 1 の続きです。
//cublasSgemm
cublasHandle_t s;
cublasCreate(&s);
cublasSgemm('N', 'N', m, n, k, 1.0f, d_A, m, d_B, k, 0, d_C, m);
cublasDestroy(s);
//cublasSgemm_v2
cublasHandle_t s;
cublasCreate_v2(&s);
cublasSgemm_v2(s,CUBLAS_OP_T,CUBLAS_OP_T,m,n,k,&al,d_A,k,d_B,n,&ve,d_C,n);
cublasDestroy_v2(s);
//cblas_sgemm
void gemm_OpenBlas(float *A, float *B, float *C, int m, int k, int n) {
enum CBLAS_ORDER order = CblasRowMajor;
enum CBLAS_TRANSPOSE transposeA = CblasNoTrans;
enum CBLAS_TRANSPOSE transposeB = CblasNoTrans;
float alpha = 1;
float beta = 1;
cblas_sgemm(order,transposeA,transposeB,m,n,k,alpha,A,k,B,n,beta,C,n);
}
//cuda_shared
typedef struct {
int width;
int height;
int stride;
float *elements;
} Matrix;
__device__ float GetElement(const Matrix A, int row, int col) {
return A.elements[row * A.stride + col];
}
__device__ void SetElement(Matrix A, int row, int col,
float value) {
A.elements[row * A.stride + col] = value;
}
__device__ Matrix GetSubMatrix(Matrix A, int row, int col) {
Matrix Asub;
Asub.width
= BLOCK_SIZE;
Asub.height
= BLOCK_SIZE;
Asub.stride
= A.stride;
Asub.elements = &A.elements[A.stride * BLOCK_SIZE * row
+ BLOCK_SIZE * col];
return Asub;
}
#define ifin(r, c, rb, cb, mat_A) \
((c + cb*BLOCK_SIZE < mat_A.width) && (r+rb*BLOCK_SIZE < mat_A.height))
__global__ void MatMulKernel_SharedMemory(Matrix A, Matrix B, Matrix C) {
int blockRow = blockIdx.y;
int blockCol = blockIdx.x;
int row = threadIdx.y;
int col = threadIdx.x;
__shared__ float As[BLOCK_SIZE][BLOCK_SIZE];
__shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];
Matrix subC = GetSubMatrix(C, blockRow, blockCol);
int tot = A.width / BLOCK_SIZE + (A.width % BLOCK_SIZE != 0);
float CValue = 0;
for (int i = 0; i < tot; ++i) {
Matrix Asub = GetSubMatrix(A, blockRow, i);
Matrix Bsub = GetSubMatrix(B, i, blockCol);
//if (i * BLOCK_SIZE + col < A.width)
if(ifin(row,col,blockRow,i,A)) {
As[row][col] = GetElement(Asub, row, col);
} else As[row][col] = 0;
if(ifin(row,col,i,blockCol,B)) {
Bs[row][col] = GetElement(Bsub, row, col);
}else Bs[row][col] = 0;
__syncthreads();
for (int e = 0; e < BLOCK_SIZE; ++e)
CValue += As[row][e] * Bs[e][col];
__syncthreads();
if (ifin(row, col, blockRow, blockCol, C))
SetElement(subC, row, col, CValue);
}
}
void gemm_cuda_shared(float *A, float *B, float *C, int m, int k, int n,double *time_value) {
Matrix d_A;
d_A.width = d_A.stride = k;
d_A.height = m;
size_t size = k * m * sizeof(float);
cudaMalloc(&d_A.elements, size);
cudaMemcpy(d_A.elements, A, size, cudaMemcpyHostToDevice);
Matrix d_B;
d_B.width = d_B.stride = n;
d_B.height = k;
size = n * k * sizeof(float);
cudaMalloc(&d_B.elements, size);
cudaMemcpy(d_B.elements, B, size, cudaMemcpyHostToDevice);
Matrix d_C;
d_C.width = d_C.stride = n;
d_C.height = m;
size = n * m * sizeof(float);
cudaMalloc(&d_C.elements, size);
dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
dim3 dimGrid((n / dimBlock.x) + (n % dimBlock.x != 0), (m / dimBlock.y) + (m % dimBlock.y != 0));
timeval t1,t2;
*time_value = 0;
cudaDeviceSynchronize();
gettimeofday(&t1, nullptr);
MatMulKernel_SharedMemory<<<dimGrid, dimBlock>>>(d_A, d_B, d_C);
cudaDeviceSynchronize();
gettimeofday(&t2, nullptr);
*time_value+=(t2.tv_sec - t1.tv_sec) + (t2.tv_usec - t1.tv_usec) / 1000000.0;
cudaMemcpy(C, d_C.elements, size, cudaMemcpyDeviceToHost);
cudaFree(d_A.elements);
cudaFree(d_B.elements);
cudaFree(d_C.elements);
}
//cuda_global
__global__ void MatrixMulKernel(float *A, float *B, float *C, int m, int k, int n) {
// Each thread computes one element of C
// by accumulating results into Cvalue
float Cvalue = 0;
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if (row < m && col < n) {
for (int e = 0; e < k; ++e)
Cvalue += A[row * k + e]
* B[e * n + col];
C[row * n + col] = Cvalue;
}
}
void gemm_cuda(float *A, float *B, float *C, int m, int k, int n,double *time_value) {
float *d_A, *d_B, *d_C;
size_t size = m * k * sizeof(float);
cudaMalloc(&d_A, size);
cudaMemcpy(d_A, A, size, cudaMemcpyHostToDevice);
size = k * n * sizeof(float);
cudaMalloc(&d_B, size);
cudaMemcpy(d_B, B, size, cudaMemcpyHostToDevice);
size = m * n * sizeof(float);
cudaMalloc(&d_C, size);
dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
dim3 dimGrid((n / dimBlock.x) + (n % dimBlock.x != 0), (m / dimBlock.y) + (m % dimBlock.y != 0));
timeval t1,t2;
*time_value = 0;
cudaDeviceSynchronize();
gettimeofday(&t1, nullptr);
MatrixMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C, m, k, n);
cudaDeviceSynchronize();
gettimeofday(&t2, nullptr);
*time_value += (t2.tv_sec - t1.tv_sec) + (t2.tv_usec - t1.tv_usec) / 1000000.0;
cudaMemcpy(C, d_C, size, cudaMemcpyDeviceToHost);
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
}
最速は cublas_sgemm 2000 × 2000 2000 \times 2000です2000年×2000 年のマトリックスには約 0.0075 秒しかかかりませんでした。2 つ目は、cublas_sgemm_v2 に約 0.0175 秒かかることです。
- 4 番目の割り当ては、スパース正方行列とシン行列を乗算することで、正方行列を csr の保存形式に変換します。次に、csr と同じ方法で行列を乗算します。csr 法を使用すると計算時間は短くなりますが、初期段階での準備が多く、複数の行列を乗算する過程で行列形式の変換とメモリ割り当てが何度も必要になります。 csr の 3 つの配列の長さも異なるため、毎回メモリの再割り当てと解放が必要になり、時間がかかります。
csr 形式のスパース行列の乗算には上記の欠点がありますが、この方法はまだテストされています。以下はインターネット上にあるコードをつなぎ合わせたものです。原理はあまり明確ではありませんが、関数部分を貼り付け、その後関数部分を貼り付けます。変化と呼ばれます。マトリックスを csr ストレージ形式に変換した後、cusparseSpMM を使用します。
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <stdbool.h>
#include "mmio.h"
#include "mmiohighlevel.h"
#include <omp.h>
#define nthreads 1024
//#ifndef MATRIXMULTISAMPLES_CUSPARSE_SPMM_CUH
//#define MATRIXMULTISAMPLES_CUSPARSE_SPMM_CUH
#include <cusparse.h>
typedef struct
{
VALUE_TYPE *value;
int *columnindex;
int *rowpointer;
}SMatrix;
void dense2csr(int m, int n, VALUE_TYPE *C0, VALUE_TYPE *value,
int *columnindex, int *rowpointer){
VALUE_TYPE *d_C0;
size_t size = m * n * sizeof(VALUE_TYPE);
cudaMalloc(&d_C0, size);
cudaMemcpy(d_C0, C0, size,
cudaMemcpyHostToDevice);
size = m * sizeof(int);
int *nnzPerRow = 0;
cudaMalloc(&nnzPerRow, size);
int nzz = 0;
cusparseHandle_t handle = 0;
cusparseCreate(&handle);
cusparseMatDescr_t descrA = 0;
cusparseCreateMatDescr(&descrA);
cusparseSetMatType(descrA, CUSPARSE_MATRIX_TYPE_GENERAL);
cusparseSetMatIndexBase(descrA, CUSPARSE_INDEX_BASE_ZERO);
cusparseDirection_t dirA = CUSPARSE_DIRECTION_ROW;
cusparseSnnz(handle,dirA,m,n,descrA,
d_C0,m,nnzPerRow,&nzz);
float *csrValA;
size = nzz * sizeof(VALUE_TYPE);
cudaMalloc(&csrValA, size);
int *csrRowPtrA, *csrColIndA;
size = (m+1) * sizeof(int);
cudaMalloc(&csrRowPtrA, size);
size = nzz * sizeof(int);
cudaMalloc(&csrColIndA, size);
cusparseSdense2csr(handle,m,n,descrA,d_C0,
m,nnzPerRow,csrValA,csrRowPtrA,csrColIndA);
size = nzz * sizeof(VALUE_TYPE);
cudaMemcpy(value, csrValA, size,
cudaMemcpyDeviceToHost);
size = (m+1) * sizeof(int);
cudaMemcpy(rowpointer, csrRowPtrA, size,
cudaMemcpyDeviceToHost);
size = nzz * sizeof(int);
cudaMemcpy(columnindex, csrColIndA, size,
cudaMemcpyDeviceToHost);
cudaFree(d_C0);
cudaFree(csrValA);
cudaFree(csrRowPtrA);
cudaFree(csrColIndA);
}
void scan(int *array, int len)
{
int old, _new;
old = array[0];
array[0] = 0;
for (int i = 1; i < len; i++)
{
_new = array[i];
array[i] = old + array[i - 1];
old = _new;
}
}
void toRowIndx_(int line, int ld, VALUE_TYPE *val) {
VALUE_TYPE *temp = (VALUE_TYPE *) malloc(sizeof(VALUE_TYPE) * line * ld);
for (int i = 0; i < line; ++i) {
for (int j = 0; j < ld; ++j) {
temp[i * ld + j] = val[j * line + i];
}
}
memcpy(val, temp, sizeof(VALUE_TYPE) * line * ld);
free(temp);
}
void toColIndx_(int line, int ld, VALUE_TYPE *val) {
VALUE_TYPE *temp = (VALUE_TYPE *) malloc(sizeof(VALUE_TYPE) * line * ld);
for (int i = 0; i < ld; ++i) {
for (int j = 0; j < line; ++j) {
temp[i * line + j] = val[j * ld + i];
}
}
memcpy(val, temp, sizeof(VALUE_TYPE) * line * ld);
free(temp);
}
int main(int argc, char ** argv)
{
struct timeval t1, t2, t3, t4;
int size1 = 0;
int size2 = 0;
int *tc1;
int *tc2;
double bias = -0.3000;
int mA;
int nA;
int nnzA;
int isSymmetricA;
SMatrix A;
int mB;
int nB;
int nnzB;
int isSymmetricB;
SMatrix B[120];
int mC,nC;
omp_set_num_threads(nthreads);
// load matrix data from file
gettimeofday(&t3, NULL);
char filename1[]="sparse-images-1024.tsv";
mmio_info(&mA, &nA, &nnzA, &isSymmetricA, filename1);
A.value=(VALUE_TYPE*)malloc((nnzA)*sizeof(VALUE_TYPE));
A.columnindex=(int*)malloc((nnzA)*sizeof(int));
A.rowpointer=(int*)malloc((mA+1)*sizeof(int));
mmio_data(A.rowpointer, A.columnindex, A.value, filename1);
printf("input matrix A: ( %i, %i ) nnz = %i\n", mA, nA, nnzA);
/*
VALUE_TYPE *A0 = (VALUE_TYPE *)malloc(mA * nA * sizeof(VALUE_TYPE));
memset(A0, 0, sizeof(VALUE_TYPE) * mA * nA);
for (int i = 0; i < mA; i++)
{
for (int j = A.rowpointer[i]; j < A.rowpointer[i+1]; j++)
{
A0[i * nA + A.columnindex[j]] = A.value[j];
}
}
free(A.rowpointer);
free(A.columnindex);
free(A.value);
*/
char neuronfile1[] = "neuron1024/n1024-l";
char neuronfile2[] = ".tsv";
char filename3[60];
VALUE_TYPE *B0[120];
for (int k = 0; k < 120; k++)
{
char filenum[5];
int k1=k+1;
snprintf(filenum,sizeof(filenum),"%d",k1);
strcpy(filename3, neuronfile1);
strcat(filename3, filenum);
strcat(filename3, neuronfile2);
mmio_info(&mB, &nB, &nnzB, &isSymmetricB, filename3);
B[k].value=(VALUE_TYPE*)malloc((nnzB)*sizeof(VALUE_TYPE));
B[k].columnindex=(int*)malloc((nnzB)*sizeof(int));
B[k].rowpointer=(int*)malloc((mB+1)*sizeof(int));
mmio_data(B[k].rowpointer, B[k].columnindex, B[k].value, filename3);
B0[k] = (VALUE_TYPE *)malloc(mB * nB * sizeof(VALUE_TYPE));
memset(B0[k], 0, sizeof(VALUE_TYPE) * mB * nB);
for (int i = 0; i < mB; i++)
{
for (int j = B[k].rowpointer[i]; j < B[k].rowpointer[i+1]; j++)
{
B0[k][i * nB + B[k].columnindex[j]] = B[k].value[j];
}
}
free(B[k].rowpointer);
free(B[k].columnindex);
free(B[k].value);
}
gettimeofday(&t4,NULL);
double time_load = (t4.tv_sec - t3.tv_sec) * 1000.0 + (t4.tv_usec - t3.tv_usec) / 1000.0;
printf("Weight matrix load time: %f ms \n",time_load);
mC = mA;
nC = nB;
SMatrix C;
C.value=(VALUE_TYPE*)malloc((mA*nB)*sizeof(VALUE_TYPE));
C.columnindex=(int*)malloc((mA*nB)*sizeof(int));
C.rowpointer=(int*)malloc((mA+1)*sizeof(int));
VALUE_TYPE *C0 =(VALUE_TYPE *)malloc((mA*nB)*sizeof(VALUE_TYPE));
float *dRes;
VALUE_TYPE *d_B[120];
for (int k=0;k<120;k++)
{
size_t size = nA * nB * sizeof(VALUE_TYPE);
toColIndx_(nB,mB,B0[k]);
cudaMalloc(&d_B[k], size);
cudaMemcpy(d_B[k], B0[k], size,
cudaMemcpyHostToDevice);
}
gettimeofday(&t3, NULL);
for (int k = 0; k < 120; k++)
{
//memset(C0, 0, sizeof(VALUE_TYPE)*mC*nC);
int *dRow, *dCol;
size_t size = (mA + 1) * sizeof(int);
cudaMalloc(&dRow, size);
cudaMemcpy(dRow, A.rowpointer, size,
cudaMemcpyHostToDevice);
size = A.rowpointer[mA]* sizeof(int);
cudaMalloc(&dCol, size);
cudaMemcpy(dCol, A.columnindex, size,
cudaMemcpyHostToDevice);
VALUE_TYPE *dA, *dB0;
size = A.rowpointer[mA]* sizeof(VALUE_TYPE);
cudaMalloc(&dA, size);
cudaMemcpy(dA, A.value, size,
cudaMemcpyHostToDevice);
size = mB * nB* sizeof(VALUE_TYPE);
cudaMalloc(&dB0, size);
cudaMemcpy(dB0, d_B[k], size, cudaMemcpyDeviceToDevice);
size = mA* nA * sizeof(VALUE_TYPE);
cudaMalloc(&dRes, size);
cusparseHandle_t handle;
cusparseCreate(&handle);
cusparseOperation_t a = CUSPARSE_OPERATION_NON_TRANSPOSE;
cusparseOperation_t b = CUSPARSE_OPERATION_NON_TRANSPOSE;
VALUE_TYPE al = 1, be = 0;
cusparseSpMatDescr_t csrMtxA;
cusparseCreateCsr(&csrMtxA, (int64_t) mA, (int64_t) nA,
(int64_t) nnzA, dRow, dCol, dA, CUSPARSE_INDEX_32I, CUSPARSE_INDEX_32I,
CUSPARSE_INDEX_BASE_ZERO, CUDA_R_32F);
cusparseDnMatDescr_t dnsMtxB;
cusparseCreateDnMat(&dnsMtxB, (int64_t) mB, (int64_t) nB,
(int64_t) nB, dB0, CUDA_R_32F, CUSPARSE_ORDER_COL);
cusparseDnMatDescr_t dnsMtxC;
cusparseCreateDnMat(&dnsMtxC, (int64_t) mA, (int64_t) nA,
(int64_t) mA, dRes, CUDA_R_32F, CUSPARSE_ORDER_COL);
gettimeofday(&t1,NULL);
cusparseSpMM(handle, a, b, &al, csrMtxA, dnsMtxB, &be, dnsMtxC, CUDA_R_32F, CUSPARSE_MM_ALG_DEFAULT, NULL);
gettimeofday(&t2,NULL);
double time_gemm = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
gettimeofday(&t1,NULL);
gettimeofday(&t2,NULL);
gettimeofday(&t1,NULL);
cudaMemcpy(C0, dRes,(mC*nC)*sizeof(VALUE_TYPE) , cudaMemcpyDeviceToHost);
cudaFree(dCol);
cudaFree(dRow);
cudaFree(dRes);
cudaFree(dA);
cudaFree(dB0);
#pragma omp parallel for
for (int i = 0; i < mC*nC; i++)
{
C0[i] += bias;
if (C0[i] <= 0)
{
C0[i] = 0;
}
else if (C0[i] >= 32)
{
C0[i] = 32;
}
}
int *rowpointer, *columnindex;
VALUE_TYPE* value;
columnindex = (int*)malloc((mA*nA)*sizeof(int));
rowpointer = (int*)malloc((mA+1)*sizeof(int));
value = (VALUE_TYPE*)malloc(mA*nA*sizeof(VALUE_TYPE));
dense2csr(mA, nB, C0, value, columnindex, rowpointer);
A.rowpointer = (int*)malloc((mA+1)*sizeof(int));
A.columnindex = (int*)malloc(rowpointer[mA]*sizeof(int));
A.value = (VALUE_TYPE*)malloc(rowpointer[mA]*sizeof(VALUE_TYPE));
memcpy(A.rowpointer, rowpointer, (mA+1)*sizeof(int));
memcpy(A.columnindex, columnindex, rowpointer[mA]*sizeof(int));
memcpy(A.value, value, rowpointer[mA]*sizeof(VALUE_TYPE));
gettimeofday(&t2,NULL);
double time_biasrelu = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
printf("k = %d, GEMM time: %4.5f ms, Bias+ReLU time: %4.5f ms\n", k+1, time_gemm, time_biasrelu);
}
//cudaMemcpy(A0, d_C, (mC*nC)*sizeof(VALUE_TYPE),cudaMemcpyDeviceToHost);
gettimeofday(&t4,NULL);
double time_inference = (t4.tv_sec - t3.tv_sec) * 1000.0 + (t4.tv_usec - t3.tv_usec) / 1000.0;
printf("Inference time: %f ms \n",time_inference);
//free(C0);
VALUE_TYPE *A0 = (VALUE_TYPE *)malloc(mA * nA * sizeof(VALUE_TYPE));
memset(A0, 0, sizeof(VALUE_TYPE) * mA * nA);
for (int i = 0; i < mA; i++)
{
for (int j = A.rowpointer[i]; j < A.rowpointer[i+1]; j++)
{
A0[i * nA + A.columnindex[j]] = A.value[j];
}
}
// check results
printf("test\n");
FILE* fs;
fs=fopen("sparse-images-1024-1.tsv","w+");
for (int i = 0; i <mA; i++)
{
int sum =0;
for (int j = (i*nA); j < ((i+1)*nA); j++)
{
sum+=A0[j];
}
if(sum!=0)
{
fprintf(fs,"%d\n", i+1);
}
}
fclose(fs);
FILE* fp2=NULL;
fp2 = fopen("sparse-images-1024-1.tsv", "rb");
if (fp2 == NULL)
{
printf("Error:Open file fail!\n");
}
fseek(fp2, 0, SEEK_END);
size2 = ftell(fp2);
rewind(fp2);
tc2 = (int*)malloc(sizeof(int) * size2/4);
int readnum2 = fread(tc2, 4, size2/4, fp2);
fclose(fp2);
FILE* fp1;
fp1 = fopen("neuron1024-l120-categories.tsv", "rb");
if (fp1 == NULL)
{
printf("Error:Open file fail!\n");
}
fseek(fp1, 0, SEEK_END);
size1 = ftell(fp1);
rewind(fp1);
tc1 = (int*)malloc(sizeof(int) * size1/4);
int readnum1 = fread(tc1, 4, size1/4, fp1);
fclose(fp1);
int judge=0;
for(int i=0;i<size1/4;i++)
{
if(tc1[i]-tc2[i] != 0)
{
judge++;
}
}
printf("judge:%d\n",judge);
if (judge == 0) {
printf("CHALLENGE PASSED\n");
}
else
{
printf("CHALLENGE FAILED\n");
}
free(A0);
return 0;
}
この時間は約 24 秒ですが、bias と relu がカーネルを使用すると数秒速くなります。後で紹介するcublasSgemmと比べると、やはりかなり遅いです。
- 宿題 5 は、mpi を使用して行列の乗算を計算することです。私がこれを学んでいたとき、疫病のせいで学校から車で帰宅していたので、何気なく通りかかりました。放っておいてください。しかし、誰かがそれを使ってクラス最速の3秒を達成しました。
行列乗算の最適化のアイデア
最速の cublasSgemm を選択してください
cublasSgemm は、NV cublas ライブラリの行列乗算 API です。cublas の行列の格納は列優先であるため、cublasSgemm API のパラメーターの設定はより困難です。そのため、cublasSgemm のパラメーター設定から始めて、次に進みます。 cublasSgemm のパラメータを正しく設定した後、前のステップを実行します。
2 次元行列の行優先および列優先の格納
行列は論理的にはM行K列の2次元で表現されますが、メモリに格納する際は1次元に配置されます。上の図。
上図のように、行列をrow-firstで格納し、逆にcolumn-firstで読み込むとメタ行列転置の結果が得られますが、column-firstで格納してから読み出す場合も同様です。行先で。
cublasSgemm 関数のパラメーター
cublasStatus_t cublasSgemm(cublasHandle_t handle,
cublasOperation_t transa, cublasOperation_t transb,
int m, int n, int k,
const float *alpha,
const float *A, int lda,
const float *B, int ldb,
const float *beta,
float *C, int ldc)
この関数は行列間の乗算を実行します:
C = α op ( A ) op ( B ) + β CC=αop(A)op(B)+βCC=α o p ( A ) o p ( B )+βC
ここで、 α と β はスカラー、 A 、 B 、 C はそれぞれ、次元 op(A) m×k 、 op(B) k×n 、 C m×n の列優先形式で格納された行列です。また、行列 A については、
op ( A ) = { A transa = = CUBLAS _ OP _ NAT transa = = CUBLAS _ OP _ TAH transa = = CUBLAS _ OP _ C op(A)=\left\{ \begin{array}{rcl} A & & {transa == CUBLAS\_OP\_N}\\ A^T & & {transa == CUBLAS\_OP\_T}\\ A^H & & {transa == CUBLAS\_OP\_C} \end{array} \右。OP ( A ) _=⎩
⎨
⎧ああTあHトランザ_ _ _ _==キュブラス_OP_N _ _ _ _ _ _ _ _トランザ_ _ _ _==キュブラス_OP_T _ _ _ _ _ _ _ _トランザ_ _ _ _==キュブラス_OP_C _ _ _ _ _ _ _ _
op(B) は行列 B に対して同様に定義されます。
公式ドキュメントのパラメータは次のように説明されています。
そこから次の重要な点を得ることができます。
-
ドキュメントによると、cublasSgemm が C = alpha * op ( A ) * op ( B ) + beta * C の行列乗算と加算演算を完了したことがわかります。
-
ここで、alpha と beta はスカラー、ABC は列優先行列です。
-
transa のパラメータが CUBLAS_OP_N の場合、op(A) = A、CUBLAS_OP_T の場合、op(A)=A 転置します。CUBLAS_OP_C については、このジョブでは使用されませんので、心配する必要はありません。
-
transb のパラメータが CUBLAS_OP_N の場合は op(B) = B、CUBLAS_OP_T の場合は op(B)=B transpose
API の行列パラメータも ABC で表されるため、次の例の行列 AB と混同しないように、cublasSgemm のパラメータを次のように調整します。
- A は乗法左行列と呼ばれます
- B は乗法右行列と呼ばれます
- C は結果行列と呼ばれます
したがって、alpha =1 および beta =0 の場合、cublasSgemm は計算を完了します:
C = op ( A ) ∗ op ( B ) C = op (A) * op ( B)C=OP ( A ) _∗オプ( B ) _
C=AxB を解きます。
ここで、A は mA 行 nA 列 B は mB 行 nB 列であるため、C は mC 行 nC 列であり、6 つのパラメータには次の関係があります: { m A
= m C n A = m B n B = n C \ left\{ \begin{array}{rcl} mA = mC\\ nA = mB\\ nB=nC \end{array} \right。⎩
⎨
⎧mA _=mC _あ_=mB _nB _=nC _
cublasSgemmのtransaおよびtransbパラメータを使用しないでください
C/C++ プログラムの入力 A と B は行に格納されるため、cublas の場合、実際には A とBの転置行列 ATA^T を読み取ります。あTとBTB^TBT
線形代数の規則によれば、 C T = (A x B) T = B T x A Tであることがわかるため、cublasSgemm API のいくつかのパラメーターは次のように設定されます。
- cublasSgemm=CUBLAS_OP_N の transa および transb パラメータを設定します。
- 左行列を B に乗算しますT = パラメータを B に設定し、右行列を A に乗算しますT = パラメータを A に設定します
- 結果の行列の行数はC Tの行数です = パラメータは nC に設定されます
- 結果行列の列数はC Tの列数= パラメータは mC に設定されます
- 左行列の列と右行列の行を乗算 = パラメータを nA または mB に設定
- 乗算左行列 B の主次元を列から最初に読み取ります (つまり、 B Tの行数) = パラメータを nB に設定します
- 乗算右行列 A の主次元を最初に読み取ります (つまり、 A Tの行数) = nA に設定されたパラメータ
- 結果の行列は、主次元 (つまりC T の行数) = パラメータが nC に設定された状態でパラメータ C に保存されます。
cublasSgemm(handle,CUBLAS_OP_N,CUBLAS_OP_N,nC,mC,nA,&alpha,d_B,nB,d_A,nA,&beta,d_C,nC);
上記のパラメータに従って cublasSgemm API を呼び出し (行列 A はポインタ d_a に行ごとに保存され、行列 B はポインタ d_b に行ごとに保存され、行列 C の記憶領域ポインタ d_c に保存されます)、最後にストレージから読み取られます。結果行列の行ごとの空間 d_c は C=AxB の結果であり、cublasSgemm 全体の計算プロセスを次の図に示します。
cublasSgemmのtransaおよびtransbパラメータを使用する
C/C++ プログラムの入力 A と B は行に格納されるため、cublas の場合は実際に A と B の転置行列 AT と BT を読み取ります。
cublasSgemm の transa および transb パラメータを設定した後、行列演算を実行して A および B を取得する前に、読み取った AT および BT 行列を転置できます。
線形代数の規則によれば、C = A x B であることがわかるため、cublasSgemm API のいくつかのパラメーターは次のように設定されます。
transa および transb パラメータを cublasSgemm = CUBLAS_OP_T に設定し、行列演算を実行する前に読み取り行列の転置を実行します。
- 乗算の左側の行列は A = パラメータは A に設定され、乗算の右側の行列は B = パラメータは B に設定されます
- 結果行列の行数はCの行数 = パラメータはmCに設定されます
- 結果行列の列数はCの列数 = パラメータはnCに設定されます
- 左行列の列と右行列の行を乗算 = パラメータを nA に設定
- 乗算左行列 A の主次元を最初に読み取ります (つまり、 A Tの行数) = nA に設定されたパラメータ
- 乗算右行列 B の主次元を列から最初に読み取ります (つまり、 B Tの行数) = パラメータを nB に設定します
- 結果の行列は、主次元 (つまり、C の行数) = パラメータが mC に設定された状態でパラメータ C に保存されます。
cublasSgemm(handle,CUBLAS_OP_T,CUBLAS_OP_T, mC, nC, nA,&alpha,d_a, nA, d_b, nB,&beta, d_c, mC);
演算の結果、C を行から N 行 M 列の順に読み出すことは、C を転置して C Tを取得することと等価です。計算で transa および transb パラメーターを使用する場合、計算の最後に結果の行列を転置する必要があり、効率が低下します。そのため、この大規模な代入では、計算に transa および transb パラメーターを使用しない cublasSgemm ステートメントを使用します。
コードと結果の分析の最適化
コード
dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
dim3 dimGrid(ceil((float)nC / BLOCK_SIZE) , ceil((float)mC / BLOCK_SIZE) );
gettimeofday(&t3, NULL);
for (int k = 0; k < 120; k++)
{
memset(C0, 0, sizeof(VALUE_TYPE)*mC*nC);
VALUE_TYPE *d_A, *d_B, *d_C;
cudaMalloc((void**)&d_A, sizeof(VALUE_TYPE)*mA*nA);
cudaMalloc((void**)&d_B, sizeof(VALUE_TYPE)*mB*nB);
cudaMalloc((void**)&d_C, sizeof(VALUE_TYPE)*mC*nC);
cudaMemcpy(d_A, A0, sizeof(VALUE_TYPE)*mA*nA, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, B0[k], sizeof(VALUE_TYPE)*mB*nB, cudaMemcpyHostToDevice);
cublasHandle_t handle;
cublasCreate(&handle);
gettimeofday(&t1, NULL);
cublasSgemm(handle,CUBLAS_OP_N,CUBLAS_OP_N,nC,mC,nA,&alpha,d_B,mB,d_A,mB,&beta,d_C,nC);
//cublasSgemm_v2(s ,CUBLAS_OP_N,CUBLAS_OP_T,m, n, k, &al, d_A,k, d_B,n, &ve, d_C,n);
gettimeofday(&t2,NULL);
cublasDestroy(handle);
// cudaMemcpy(A0, d_C, sizeof(VALUE_TYPE)*mC*nC, cudaMemcpyDeviceToHost);
cudaFree(d_A);
cudaFree(d_B);
// cudaFree(d_C);
double time_gemm = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
gettimeofday(&t1, NULL);
RELUKernel<<<dimGrid, dimBlock>>>(d_C,nA);
gettimeofday(&t2,NULL);
double time_biasrelu = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
printf("k = %d, GEMM time: %4.5f ms, Bias+ReLU time: %4.5f ms\n",
k+1, time_gemm, time_biasrelu);
cudaMemcpy(A0, d_C, sizeof(VALUE_TYPE)*mC*nC, cudaMemcpyDeviceToHost);
cudaFree(d_C);
}
実行結果は次のとおりです。
前方パスが完了するまでに合計 16.5 秒かかります。
結果の分析と最適化
行列の乗算とbias+reluの計算にかかる時間のオーバーヘッドは、推論時間の16秒に比べて非常に小さいことがわかります。そのため、メモリ割り当てやビデオ メモリ アクセスなど、コードの他の側面の最適化に重点を置く必要があります。
各ループの開始時に、memset を使用して C0 を 0 に設定し、行列 d_A、d_B、d_C を定義します。cudaMalloc を使用してスペースを割り当ててから、cudaMemcpy を使用して行列 d_A、d_B をホストからデバイスにコピーします。次に、関数 cublasSgemm を呼び出し、最後に d_A、d_B を解放し、d_C をデバイスからホストにコピーしてから、d_C を解放します。このサイクルは終了します。
最適化できるオプション:
- 各サイクルの終わりに、d_C が A0 に渡され、A0 がアクセスされて、次のサイクルで d_A に渡されます。このステップの操作は次のように簡略化できます: d_A と d_C をサイクルの外側で宣言し、d_C を d_A に直接転送します。各サイクルの終わりに計算が終了します。その後、d_A と d_C は解放されなくなります。サイクルの終わりに解放します。
- d_B の場合、各計算では B から異なる行列をロードする必要がありますが、ループの外で宣言することもできます。
- 120 個の行列を乗算した後、ループの外で d_C 行列を A0 に渡し、d_C、d_A、および d_B を解放します。
最適化されたコード
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <stdbool.h>
#include <cublas.h>
#include "mmio.h"
#include "mmiohighlevel.h"
#define BLOCK_SIZE 32
typedef struct
{
VALUE_TYPE *value;
int *columnindex;
int *rowpointer;
}SMatrix;
__global__ void RELUKernel(VALUE_TYPE *C,int n)
{
VALUE_TYPE Cvalue;
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
C[row * n + col]=C[row * n + col]-0.3000;
Cvalue=C[row * n + col];
if (Cvalue<=0){
C[row * n + col]=0.0;}
else if(Cvalue>=32){
C[row * n + col]=32.0;
}
}
int main(int argc, char ** argv)
{
struct timeval t1, t2, t3, t4;
int size1 = 0;
int size2 = 0;
int *tc1;
int *tc2;
double bias = -0.3000;
const float alpha = 1.0, beta = 0.0;
int mA;
int nA;
int nnzA;
int isSymmetricA;
SMatrix A;
int mB;
int nB;
int nnzB;
int isSymmetricB;
SMatrix B[120];
int mC,nC;
int nnzC_golden = 0;
// load matrix data from file
gettimeofday(&t3, NULL);
char filename1[]="sparse-images-1024.tsv";
mmio_info(&mA, &nA, &nnzA, &isSymmetricA, filename1);
A.value=(VALUE_TYPE*)malloc((nnzA)*sizeof(VALUE_TYPE));
A.columnindex=(int*)malloc((nnzA)*sizeof(int));
A.rowpointer=(int*)malloc((mA+1)*sizeof(int));
mmio_data(A.rowpointer, A.columnindex, A.value, filename1);
printf("input matrix A: ( %i, %i ) nnz = %i\n", mA, nA, nnzA);
VALUE_TYPE *A0 = (VALUE_TYPE *)malloc(mA * nA * sizeof(VALUE_TYPE));
memset(A0, 0, sizeof(VALUE_TYPE) * mA * nA);
for (int i = 0; i < mA; i++)
{
for (int j = A.rowpointer[i]; j < A.rowpointer[i+1]; j++)
{
A0[i * nA + A.columnindex[j]] = A.value[j];
}
}
free(A.rowpointer);
free(A.columnindex);
free(A.value);
char neuronfile1[] = "neuron1024/n1024-l";
char neuronfile2[] = ".tsv";
char filename3[60];
VALUE_TYPE *B0[120];
for (int k = 0; k < 120; k++)
{
char filenum[5];
int k1=k+1;
snprintf(filenum,sizeof(filenum),"%d",k1);
strcpy(filename3, neuronfile1);
strcat(filename3, filenum);
strcat(filename3, neuronfile2);
mmio_info(&mB, &nB, &nnzB, &isSymmetricB, filename3);
B[k].value=(VALUE_TYPE*)malloc((nnzB)*sizeof(VALUE_TYPE));
B[k].columnindex=(int*)malloc((nnzB)*sizeof(int));
B[k].rowpointer=(int*)malloc((mB+1)*sizeof(int));
mmio_data(B[k].rowpointer, B[k].columnindex, B[k].value, filename3);
B0[k] = (VALUE_TYPE *)malloc(mB * nB * sizeof(VALUE_TYPE));
memset(B0[k], 0, sizeof(VALUE_TYPE) * mB * nB);
for (int i = 0; i < mB; i++)
{
for (int j = B[k].rowpointer[i]; j < B[k].rowpointer[i+1]; j++)
{
B0[k][i * nB + B[k].columnindex[j]] = B[k].value[j];
}
}
free(B[k].rowpointer);
free(B[k].columnindex);
free(B[k].value);
}
gettimeofday(&t4,NULL);
double time_load = (t4.tv_sec - t3.tv_sec) * 1000.0 + (t4.tv_usec - t3.tv_usec) / 1000.0;
printf("Weight matrix load time: %f ms \n",time_load);
mC = mA;
nC = nB;
VALUE_TYPE *C0 =(VALUE_TYPE *)malloc((mC*nC)*sizeof(VALUE_TYPE));
dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
dim3 dimGrid(ceil((float)nC / BLOCK_SIZE) , ceil((float)mC / BLOCK_SIZE) );
gettimeofday(&t3, NULL);
VALUE_TYPE *d_A, *d_B, *d_C;
cudaMalloc((void**)&d_A, sizeof(VALUE_TYPE)*mA*nA);
cudaMalloc((void**)&d_B, sizeof(VALUE_TYPE)*mB*nB);
cudaMalloc((void**)&d_C, sizeof(VALUE_TYPE)*mC*nC);
cudaMemcpy(d_A, A0, sizeof(VALUE_TYPE)*mA*nA, cudaMemcpyHostToDevice);
for (int k = 0; k < 120; k++)
{
cudaMemcpy(d_B, B0[k], sizeof(VALUE_TYPE)*mB*nB, cudaMemcpyHostToDevice);
free(B0[k]);
cublasHandle_t handle;
cublasCreate(&handle);
gettimeofday(&t1, NULL);
// cublasSgemm(handle,CUBLAS_OP_N,CUBLAS_OP_N,nC,mC,nA,&alpha,d_B,mB,d_A,mB,&beta,d_C,nC);
cublasSgemm(handle,CUBLAS_OP_N,CUBLAS_OP_N,nC,mC,nA,&alpha,d_B,nB,d_A,nA,&beta,d_C,nC);
// cublasSgemm(handle,CUBLAS_OP_T,CUBLAS_OP_T, mC, nC, nA,&alpha,d_a, nA, d_b, nB,&beta, d_c, mC);
gettimeofday(&t2,NULL);
cublasDestroy(handle);
double time_gemm = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
gettimeofday(&t1, NULL);
RELUKernel<<<dimGrid, dimBlock>>>(d_C,nA);
gettimeofday(&t2,NULL);
double time_biasrelu = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
printf("k = %d, GEMM time: %4.5f ms, Bias+ReLU time: %4.5f ms\n",
k+1, time_gemm, time_biasrelu);
cudaMemcpy(d_A, d_C, sizeof(VALUE_TYPE)*mC*nC, cudaMemcpyDeviceToDevice);
}
cudaMemcpy(A0, d_A, sizeof(VALUE_TYPE)*mC*nC, cudaMemcpyDeviceToHost);
gettimeofday(&t4,NULL);
double time_inference = (t4.tv_sec - t3.tv_sec) * 1000.0 + (t4.tv_usec - t3.tv_usec) / 1000.0;
printf("Inference time: %f ms \n",time_inference);
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
free(C0);
// check results
printf("test\n");
FILE* fs;
fs=fopen("sparse-images-1024-1.tsv","w+");
for (int i = 0; i <mA; i++)
{
int sum =0;
for (int j = (i*nA); j < ((i+1)*nA); j++)
{
sum+=A0[j];
}
if(sum!=0)
{
fprintf(fs,"%d\n", i+1);
}
}
fclose(fs);
FILE* fp2=NULL;
fp2 = fopen("sparse-images-1024-1.tsv", "rb");
if (fp2 == NULL)
{
printf("Error:Open file fail!\n");
}
fseek(fp2, 0, SEEK_END);
size2 = ftell(fp2);
rewind(fp2);
tc2 = (int*)malloc(sizeof(int) * size2/4);
int readnum2 = fread(tc2, 4, size2/4, fp2);
fclose(fp2);
FILE* fp1;
fp1 = fopen("neuron1024-l120-categories.tsv", "rb");
if (fp1 == NULL)
{
printf("Error:Open file fail!\n");
}
fseek(fp1, 0, SEEK_END);
size1 = ftell(fp1);
rewind(fp1);
tc1 = (int*)malloc(sizeof(int) * size1/4);
int readnum1 = fread(tc1, 4, size1/4, fp1);
fclose(fp1);
int judge=0;
for(int i=0;i<size1/4;i++)
{
if(tc1[i]-tc2[i] != 0)
{
judge++;
}
}
printf("judge:%d\n",judge);
if (judge == 0) {
printf("CHALLENGE PASSED\n");
}
else
{
printf("CHALLENGE FAILED\n");
}
free(A0);
return 0;
}
このコード部分は、前のセクションで説明したように最適化できる部分を実装しており、次は実行結果のスクリーンショットです
。メモリアクセスの最適化は元の方法よりも10秒速くなり、後述するrelu+biasの時間も約9秒速くなりました。それは信じられないです!
バイアス+レルの最適化
bias と relu は行列の各項目にバイアスをかけて、relu で有効化するというものですが、当然カーネル関数を使って各スレッドで並列処理することも可能です。加速の目的で。
ここでは、2 次元グリッドとスレッド ブロックが計算に使用されます。行インデックス row は、blockIdx.y * blockDim.y + threadIdx.y によって取得されます。blockIdx.y はスレッド ブロックの水平方向のインデックス、blockDim.y はスレッド ブロック内の水平方向のスレッドの数、threadIdx.y は特定のブロック内のスレッドの水平インデックス。Col の計算も同様です。
カーネル関数は次のとおりです。
dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
dim3 dimGrid(ceil((float)nC / BLOCK_SIZE) , ceil((float)mC / BLOCK_SIZE) );
__global__ void RELUKernel(VALUE_TYPE *C,int n)
{
VALUE_TYPE Cvalue;
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
C[row * n + col]=C[row * n + col]-0.3000;
Cvalue=C[row * n + col];
if (Cvalue<=0){
C[row * n + col]=0.0;}
else if(Cvalue>=32){
C[row * n + col]=32.0;
}
}
forループの時間:
カーネルの時間:
約36000倍向上!
結論は
最終的な最適化時間は約 6.7 秒です。