1. 程式人生 > >CUDA編程(十)使用Kahan's Summation Formula提高精度

CUDA編程(十)使用Kahan's Summation Formula提高精度

產生 trac include ase should print double類型 compile ids

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&#39;s Summation Formula提高精度