我的代码:https://github.com/BBuf/CUDA_LEARN_SAMPLES/tree/58b95d34b60c9b50c72d66a2d2b3b671337191bd
CUDA加速的非常核心的一个应用是对矩阵运算进行加速,所以这篇博客,我会记录利用CUDA写矩阵乘法加速的过程。
矩阵乘法 所谓矩阵乘法就是了AB=C,其中A为n*m的矩阵,B为m*q的矩阵,C为n*q的矩阵,矩阵乘法的代码如下:
const LL mod = 1e9+7;
struct Matrix{
LL a[2][2];
void set1(){
memset(a, 0, sizeof(a));
}
void set2(){
set1();
for(int i=0; i<2; i++) a[i][i]=1;
}
};
Matrix operator*(const Matrix &a, const Matrix &b){
Matrix res;
res.set1();
for(int i=0; i<2; i++){
for(int j=0; j<2; j++){
for(int k=0; k<2; k++){
res.a[i][j] = (res.a[i][j] + a.a[i][k]*b.a[k][j]%mod)%mod;
}
}
}
return res;
}
这里为了方便,接下来涉及的矩阵均为n*n的方形矩阵。
并行矩阵乘法核函数
//n * n Matrix Multiply A * B = C
__global__ static void MatrixMulCUDA(const float *a, const float *b, float *c, int n)
{
const int tid = threadIdx.x; //目前的thread是第几个thread
const int bid = blockIdx.x; //目前的thread是属于哪个block
const int idx = bid * THREAD_NUM + tid; //从bid和tid推出目前的thread应该计算的A矩阵的行数和B矩阵列数
const int row = idx / n;
const int col = idx % n;
if(row < n && col < n){
float sum = 0;
for(int i = 0; i < n; i++){
sum += a[row * n + i] * b[i * n + col];
}
c[row * n + col] = sum;
}
}
生成随机矩阵
//生成随机矩阵
void RandomMatrix(float *A, int n){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
A[i * n + j] = (float)rand() / MY_RAND_MAX + (float)rand() / (MY_RAND_MAX * MY_RAND_MAX);
}
}
}
矩阵运算
void MatrixMul(){
//定义矩阵
float *a, *b, *c, *d;
int n = MATRIX_SIZE;
a = (float*)malloc(sizeof(float)*n*n);
b = (float*)malloc(sizeof(float)*n*n);
c = (float*)malloc(sizeof(float)*n*n);
d = (float*)malloc(sizeof(float)*n*n);
srand(time(NULL));
RandomMatrix(a, n);
RandomMatrix(b, n);
//把数据复制到显卡内存中
float *cuda_a, *cuda_b, *cuda_c;
//cudaMalloc 取得一块显存内存
cudaMalloc((void**)&cuda_a, sizeof(float) * n * n);
cudaMalloc((void**)&cuda_b, sizeof(float) * n * n);
cudaMalloc((void**)&cuda_c, sizeof(float) * n * n);
//cudaMemcpy 将产生的矩阵复制到显卡内存中
//cudaMemcpyHostToDevice - 从内存复制到显卡内存
//cudaMemcpyDeviceToHost - 从显卡内存复制到内存
cudaMemcpy(cuda_a, a, sizeof(float)*n*n, cudaMemcpyHostToDevice);
cudaMemcpy(cuda_b, b, sizeof(float)*n*n, cudaMemcpyHostToDevice);
float time_elapsed=0;
cudaEvent_t start,stop;
cudaEventCreate(&start); //创建Event
cudaEventCreate(&stop);
cudaEventRecord( start,0); //记录当前时间
MatrixMulCUDA<<<blocks_num, THREAD_NUM, 0>>>(cuda_a, cuda_b, cuda_c, n);
cudaEventRecord( stop,0); //记录当前时间
cudaEventSynchronize(start); //Waits for an event to complete.
cudaEventSynchronize(stop); //Waits for an event to complete.Record之前的任务
//把结果从显示芯片复制回主内存
//cudaMemcpy 将结果从显存中复制回内存
cudaMemcpy(c, cuda_c, sizeof(float) * n * n, cudaMemcpyDeviceToHost);
cudaEventElapsedTime(&time_elapsed,start,stop); //计算时间差
cudaEventDestroy(start); //destory the event
cudaEventDestroy(stop);
//Free
cudaFree(cuda_a);
cudaFree(cuda_b);
cudaFree(cuda_c);
printf("Matrix multiply GPU time: %.10f\n", time_elapsed);
clock_t start_time = clock();
//CPU计算矩阵乘法
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
double temp = 0;
for(int k = 0; k < n; k++){
temp += a[i * n + k]* b[k * n + j];
}
d[i * n + j] = temp;
}
}
clock_t end_time = clock();
//验证正确性和准确性
float max_error = 0.0, average_error = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(d[i * n + j] != 0){
float err = fabs((c[i * n + j] - d[i * n + j]) / d[i * n + j]);
if(max_error < err) max_error = err;
average_error += err;
}
}
}
double cpu_time = (double)(end_time - start_time) / CLOCKS_PER_SEC * 1000.0;
printf("Matrix multiply CPU time: %.10f\n", cpu_time);
printf("Max error: %.10f Average error: %.10f\n", max_error, average_error / (n * n));
printf("%.10f\n", cpu_time/time_elapsed);
}
矩阵运算出现精度问题 由于在 CPU 上进行计算时,我们使用 double(即 64 bits 浮点数)来累进计算过程,而在 GPU 上则只能用 float(32 bits 浮点数)。在累加大量数字的时候,由于累加结果很快会变大,因此后面的数字很容易被舍去过多的位数。例如下面这段代码:
float a = 100998;
float b = 2.338;
a = a + b;
printf("the sum is %.10f\n", a);
输出是:101000.3359375000,容易看出有一个0.008被截断了,在1000次操作后一个整数8就没了,所以造成了很大的精度问题。
Kahan’s Summation Formula 为了一定程度上解决这个精度问题,我们需要记住这个损失的误差值,假设误差为temp= (a+b)-a-b, 在上面那个问题中 temp = -0.008,在下次计算的时候加和到下一个加数就可以一定程度的减小误差。
有一篇这个算法的好文章:https://www.cnblogs.com/kid551/p/6444520.html 这个算法的伪代码描述如下:
def KahanSum(input):
var sum = 0.0
var c = 0.0
for i = 1 to input.length do
var y = input[i] - c // Initially, c is zero; then it compensates previous accuracy.
var t = sum + y // low-order digits of y are lost
c = (t - sum) - y // recover the low-order digits of y, with negative symbol
sum = t
next i
return sum
在上述伪代码中,变量c表示的即是小数的补全部分compensation,更严格地说,应该是负的补全部分。随着这个补全部分的不断积累,当这些截断误差积累到一定量级,它们在求和的时候也就不会被截断了,从而能够相对好地控制整个求和过程的精度。然后把这个算法应用到我们的矩阵乘法核函数中。
//n * n Matrix Multiply A * B = C
__global__ static void MatrixMulCUDA(const float *a, const float *b, float *c, int n)
{
const int tid = threadIdx.x; //目前的thread是第几个thread
const int bid = blockIdx.x; //目前的thread是属于哪个block
const int idx = bid * THREAD_NUM + tid; //从bid和tid推出目前的thread应该计算的A矩阵的行数和B矩阵列数
const int row = idx / n;
const int col = idx % n;
if(row < n && col < n){
// float sum = 0;
// for(int i = 0; i < n; i++){
// sum += a[row * n + i] * b[i * n + col];
// }
float t = 0;
float y = 0;
for(int i = 0; i < n; i++){
float r;
y -= a[row * n + i] * b[i * n + col];
r = t - y;
y = (r - t) + y;
t = r;
}
c[row * n + col] = t;
}
}
然后不知道为什么,在我的GPU上的精度是没有提升的。。。