CUDA編程(十)使用Kahan's Summation Formula提高精度
CUDA編程(十)
使用Kahan’s Summation Formula提高精度
上一次我們準備去並行一個矩陣乘法。然後我們在GPU上完畢了這個程序,當然是非常單純的把任務分配給各個線程。也沒有經過優化。終於我們看到,執行效率相當的低下,可是更重要的是出現了一個我們之前做整數立方和沒遇到的問題,那就是浮點數精度損失的問題。
關註GPU運算的精度問題:
在程序的最後。我們計算了精度誤差,發現最大相對誤差偏高,而一般理想上應該要低於 1e-6。
我們之前將評估CUDA程序的時候也提過了。精度是CUDA程序須要重點評估的一個點,那麽我們該怎樣解決問題呢?我們先分析一下原因。
出現精度問題的解決辦法:
事實上計算結果的誤差偏高的原因非常簡單。在 CPU 上進行計算時,我們使用 double(即 64 bits 浮點數)來累進計算過程。而在 GPU 上則僅僅能用 float(32 bits 浮點數)。
在累加大量數字的時候,由於累加結果非常快會變大。因此後面的數字非常easy被舍去過多的位數。
這裏可能說的不是非常清楚。看完以下這個樣例就清楚了。
浮點數的大數吃小數問題:
浮點數的精度:
大家應該非常清楚,浮點數在內存中是按科學計數法來存儲的,分為符號位,指數位。和尾數位。
float和double各段的位數各自是:
float:
1bit(符號位) 8bits(指數位) 23bits(尾數位)
double:
1bit(符號位) 11bits(指數位) 52bits(尾數位)
float和double的精度是由尾數的位數來決定的:
float: 2^23 = 8388608。一共七位,這意味著最多能有7位有效數字,但絕對能保證的為6位,也即float的精度為6~7位有效數字。
double: 2^52 = 4503599627370496,一共16位,同理。double的精度為15~16位。
大數吃小數:
float由於位數相較於double要短不少,所以非常easy出現大數吃小數的問題:
比方我們用兩個float相加:
#include <stdio.h>
int main()
{
float a = 100998;
float b = 2.338;
a = a + b;
printf("the sum is %f", a);
}
a+b 應該等於 101000.338,前面說了float的精度有6~7位,所以38可能會被截掉,3不一定,可是8必定會被截掉。我們能夠實際輸出一下看看:
結果是:the sum is 101000.335938
由於%f是輸出double類型。能夠看到轉換後8這位已經沒了,33是正常的。
從這裏能夠看到一個加法過程就沒了0.008,要是加1000次。一個整8就沒了。
這就是大數吃小數問題。
Kahan’s Summation Formula:
如今我們就要想辦法解決問題了,我們看到標題中這個看起來非常高大上的名字,這個也叫作kahan求和算法,我們接下來就要用kahan求和來避免這樣的精度損失的情況。
名字非常高大上,可是原理非常小兒科,小學生也知道,缺的我們想辦法再補回來:
所以我們用一個temp變量來記住損失掉的部分,等下次加法的時候再加回去就好了。
temp= (a+b)-a-b; 在上面那個問題中 temp = -0.008,在下次計算的時候加和到下一個加數就能夠一定程度的減小誤差。
Kahan’s Summation Formula偽代碼:
function KahanSum(input)
var sum = 0.0
var c = 0.0 //A running compensation for lost low-order bits.
for i = 1 to input.length do
y = input[i] - c //So far, so good: c is zero.
t = sum + y //Alas, sum is big, y small, so low-order digits of y are lost.
c = (t - sum) - y //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y)
sum = t //Algebraically, c should always be zero. Beware eagerly optimising compilers!
//Next time around, the lost low part will be added to y in a fresh attempt.
return sum
提高矩陣乘法的精度:
看著偽代碼比著葫蘆畫瓢還是比較簡單的,我們僅僅須要更改核函數中的加和部分就可以:
原版
//計算矩陣乘法
if (row < n && column < n)
{
float t = 0;
for (i = 0; i < n; i++)
{
t += a[row * n + i] * b[i * n + column];
}
c[row * n + column] = t;
}
改版
//計算矩陣乘法
if (row < n && column < n)
{
float t = 0;
float y = 0;
for (i = 0; i < n; i++)
{
float r;
y -= a[row * n + i] * b[i * n + column];
r = t - y;
y = (r - t) + y;
t = r;
}
c[row * n + column] = t;
}
完整程序:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//CUDA RunTime API
#include <cuda_runtime.h>
#define THREAD_NUM 256
#define MATRIX_SIZE 1000
const int blocks_num = MATRIX_SIZE*(MATRIX_SIZE + THREAD_NUM - 1) / THREAD_NUM;
//打印設備信息
void printDeviceProp(const cudaDeviceProp &prop)
{
printf("Device Name : %s.\n", prop.name);
printf("totalGlobalMem : %d.\n", prop.totalGlobalMem);
printf("sharedMemPerBlock : %d.\n", prop.sharedMemPerBlock);
printf("regsPerBlock : %d.\n", prop.regsPerBlock);
printf("warpSize : %d.\n", prop.warpSize);
printf("memPitch : %d.\n", prop.memPitch);
printf("maxThreadsPerBlock : %d.\n", prop.maxThreadsPerBlock);
printf("maxThreadsDim[0 - 2] : %d %d %d.\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
printf("maxGridSize[0 - 2] : %d %d %d.\n", prop.maxGridSize[0], prop.maxGridSize[1], prop.maxGridSize[2]);
printf("totalConstMem : %d.\n", prop.totalConstMem);
printf("major.minor : %d.%d.\n", prop.major, prop.minor);
printf("clockRate : %d.\n", prop.clockRate);
printf("textureAlignment : %d.\n", prop.textureAlignment);
printf("deviceOverlap : %d.\n", prop.deviceOverlap);
printf("multiProcessorCount : %d.\n", prop.multiProcessorCount);
}
//CUDA 初始化
bool InitCUDA()
{
int count;
//取得支持Cuda的裝置的數目
cudaGetDeviceCount(&count);
if (count == 0)
{
fprintf(stderr, "There is no device.\n");
return false;
}
int i;
for (i = 0; i < count; i++)
{
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, i);
//打印設備信息
printDeviceProp(prop);
if (cudaGetDeviceProperties(&prop, i) == cudaSuccess)
{
if (prop.major >= 1)
{
break;
}
}
}
if (i == count)
{
fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
return false;
}
cudaSetDevice(i);
return true;
}
//生成隨機矩陣
void matgen(float* a, int n)
{
int i, j;
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{
a[i * n + j] = (float)rand() / RAND_MAX + (float)rand() / (RAND_MAX * RAND_MAX);
}
}
}
// __global__ 函數 並行計算矩陣乘法
__global__ static void matMultCUDA(const float* a, const float* b, float* c, int n, clock_t* time)
{
//表示眼下的 thread 是第幾個 thread(由 0 開始計算)
const int tid = threadIdx.x;
//表示眼下的 thread 屬於第幾個 block(由 0 開始計算)
const int bid = blockIdx.x;
//從 bid 和 tid 計算出這個 thread 應該計算的 row 和 column
const int idx = bid * THREAD_NUM + tid;
const int row = idx / n;
const int column = idx % n;
int i;
//記錄運算開始的時間
clock_t start;
//僅僅在 thread 0(即 threadIdx.x = 0 的時候)進行記錄,每一個 block 都會記錄開始時間及結束時間
if (tid == 0) time[bid] = clock();
//計算矩陣乘法
if (row < n && column < n)
{
float t = 0;
//temp變量
float y = 0;
for (i = 0; i < n; i++)
{
float r;
y -= a[row * n + i] * b[i * n + column];
r = t - y;
y = (r - t) + y;
t = r;
}
c[row * n + column] = t;
}
//計算時間,記錄結果。僅僅在 thread 0(即 threadIdx.x = 0 的時候)進行,每一個 block 都會記錄開始時間及結束時間
if (tid == 0)
{
time[bid + blocks_num] = clock();
}
}
int main()
{
//CUDA 初始化
if (!InitCUDA()) return 0;
//定義矩陣
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(0);
//隨機生成矩陣
matgen(a, n);
matgen(b, n);
/*把數據拷貝到顯卡內存中*/
float *cuda_a, *cuda_b, *cuda_c;
clock_t* time;
//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);
cudaMalloc((void**)&time, sizeof(clock_t)* blocks_num * 2);
//cudaMemcpy 將產生的矩陣拷貝到顯卡內存中
//cudaMemcpyHostToDevice - 從內存拷貝到顯卡內存
//cudaMemcpyDeviceToHost - 從顯卡內存拷貝到內存
cudaMemcpy(cuda_a, a, sizeof(float)* n * n, cudaMemcpyHostToDevice);
cudaMemcpy(cuda_b, b, sizeof(float)* n * n, cudaMemcpyHostToDevice);
// 在CUDA 中執行函數 語法:函數名稱<<<block 數目, thread 數目, shared memory 大小>>>(參數...);
matMultCUDA << < blocks_num, THREAD_NUM, 0 >> >(cuda_a, cuda_b, cuda_c, n, time);
/*把結果從顯示芯片復制回主內存*/
clock_t time_use[blocks_num * 2];
//cudaMemcpy 將結果從顯存中復制回內存
cudaMemcpy(c, cuda_c, sizeof(float)* n * n, cudaMemcpyDeviceToHost);
cudaMemcpy(&time_use, time, sizeof(clock_t)* blocks_num * 2, cudaMemcpyDeviceToHost);
//Free
cudaFree(cuda_a);
cudaFree(cuda_b);
cudaFree(cuda_c);
cudaFree(time);
//把每一個 block 最早的開始時間。和最晚的結束時間相減。取得總執行時間
clock_t min_start, max_end;
min_start = time_use[0];
max_end = time_use[blocks_num];
for (int i = 1; i < blocks_num; i++)
{
if (min_start > time_use[i]) min_start = time_use[i];
if (max_end < time_use[i + blocks_num]) max_end = time_use[i + blocks_num];
}
//核函數執行時間
clock_t final_time = max_end - min_start;
//CPU矩陣乘法,存入矩陣d
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
double t = 0;
for (int k = 0; k < n; k++)
{
t += a[i * n + k] * b[k * n + j];
}
d[i * n + j] = t;
}
}
//驗證正確性與精確性
float max_err = 0;
float average_err = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (d[i * n + j] != 0)
{
//fabs求浮點數x的絕對值
float err = fabs((c[i * n + j] - d[i * n + j]) / d[i * n + j]);
if (max_err < err) max_err = err;
average_err += err;
}
}
}
printf("Max error: %g Average error: %g\n", max_err, average_err / (n * n));
printf("gputime: %d\n", final_time);
return 0;
}
執行結果:
我們看到結果還是效果還是非常不錯的,我們上次的結果是:
Max error:2.07589e-006
Average error :3.3492e-007
gpu time:189967999
而眼下的結果是:
Max error:1.19206e-007
Average error :7.70641e-010
gpu time:210779939
我們能夠看到準確度確實有了非常大的提升,當然效率還是一如既往地慢,只是我們至少把精度問題給攻克了。
總結:
之前我們用CUDA完畢了矩陣乘法,可是當然會存在非常多問題,除了速度問題。GPU浮點數運算的精度也非常差,本篇博客從出現誤差的原理(浮點數大數吃小數)分析,使用了Kahan’s Summation Formula在一定程度上攻克了CUDA運算float精度不夠的情況。接下來我們會著手去解決速度問題~
希望我的博客能幫助到大家~
參考資料:《深入淺出談CUDA》
CUDA編程(十)使用Kahan's Summation Formula提高精度