您當前的位置:首頁 > 攝影

CUDA 3:錯誤檢測

作者:由 aHiiLn 發表于 攝影時間:2022-12-19

編寫CUDA程式難免出現錯誤,編譯錯誤這種能在編譯過程中被編譯器捕抓的還好,如果是編譯期間沒有被發現而在執行時出現,這種錯誤更難排查。本文著重討論如何檢測執行時刻的錯誤。

一個檢測CUDA執行時錯誤的宏函式

之前提到的

記憶體分配函式cudaMalloc()、釋放記憶體函式cudaFree()以及傳輸資料的函式cudaMemcpy(),都有一個cudaError_t型別的返回值,代表一種錯誤資訊,只有返回cudaSuccess時才代表成功呼叫了cuda的API函式

。依這樣的原則可以寫一個頭檔案(error。cuh),其包含一個檢測執行時刻錯誤的宏函式(macro function):

#pragma once

#include

#define CHECK(call) \

do \

{ \

const cudaError_t error_code = call; \

if (error_code != cudaSuccess) \

{ \

printf(“CUDA Error:\n”); \

printf(“ File: %s\n”, __FILE__); \

printf(“ Line: %d\n”, __LINE__); \

printf(“ Error code: %d\n”, error_code); \

printf(“ Error text: %s\n”, \

cudaGetErrorString(error_code)); \

exit(1); \

} \

} while (0)

這段程式碼中第一行

#pragma once是一個預處理命令

,用於確保當前檔案在一個編譯單元中不被重複包含。宏函式的名為CHECK,引數

call是一個CUDA執行時API函式

;在定義宏時如果一行寫不下則需要

\在行末以表示續行

。此外

cudaGetErrorString()是一個將錯誤代號轉換為錯誤文字描述的API函式

。使用該宏函式時只要將一個CUDA執行時API當作引數傳入該宏函式即可。

檢查執行時API函式

將上面的錯誤檢測用於陣列相加很簡單,

只需要在指令碼開頭包含上述的標頭檔案並將CUDA執行時API函式都用宏函式CHECK包裝

即可:

#include

“error。cuh”

#include

#include

const

double

EPSILON

=

1。0e-15

const

double

a

=

1。23

const

double

b

=

2。34

const

double

c

=

3。57

void

__global__

add

const

double

*

x

const

double

*

y

double

*

z

const

int

N

);

void

check

const

double

*

z

const

int

N

);

int

main

void

{

const

int

N

=

100000000

const

int

M

=

sizeof

double

*

N

double

*

h_x

=

double

*

malloc

M

);

double

*

h_y

=

double

*

malloc

M

);

double

*

h_z

=

double

*

malloc

M

);

for

int

n

=

0

n

<

N

++

n

{

h_x

n

=

a

h_y

n

=

b

}

double

*

d_x

*

d_y

*

d_z

CHECK

cudaMalloc

((

void

**

&

d_x

M

));

CHECK

cudaMalloc

((

void

**

&

d_y

M

));

CHECK

cudaMalloc

((

void

**

&

d_z

M

));

CHECK

cudaMemcpy

d_x

h_x

M

cudaMemcpyDeviceToHost

));

CHECK

cudaMemcpy

d_y

h_y

M

cudaMemcpyDeviceToHost

));

const

int

block_size

=

128

const

int

grid_size

=

N

+

block_size

-

1

/

block_size

add

<<<

grid_size

block_size

>>>

d_x

d_y

d_z

N

);

CHECK

cudaMemcpy

h_z

d_z

M

cudaMemcpyDeviceToHost

));

check

h_z

N

);

free

h_x

);

free

h_y

);

free

h_z

);

CHECK

cudaFree

d_x

));

CHECK

cudaFree

d_y

));

CHECK

cudaFree

d_z

));

return

0

}

void

__global__

add

const

double

*

x

const

double

*

y

double

*

z

const

int

N

{

const

int

n

=

blockDim

x

*

blockIdx

x

+

threadIdx

x

if

n

<

N

{

z

n

=

x

n

+

y

n

];

}

}

void

check

const

double

*

z

const

int

N

{

bool

has_error

=

false

for

int

n

=

0

n

<

N

n

++

{

if

fabs

z

n

-

c

>

EPSILON

{

has_error

=

true

}

}

printf

“%s

\n

has_error

“Has errors”

“No errors”

);

}

編譯後執行得到輸出:

CUDA 3:錯誤檢測

可見宏函式捕抓到了執行時刻錯誤。上面這個意思是指令碼檔案中第30行的cudaMemcpy的引數有問題,因為我們故意將cudaMemcpyHostToDevice寫成了cudaMemcpyDeviceToHost。這樣我們就得到了有用的錯誤資訊。

檢查核函式

核函式不返回任何值,因此上述的檢測功能不適用於核函式。但可以在呼叫核函式後加上如下兩條語句:

CHECK

cudaGetLastError

());

CHECK

cudaDeviceSynchronize

());

其中

第一條語句的作用是捕抓第二個語句之前的最後一個錯誤,第二條語句的作用是同步主機於裝置

。之所以要同步主機與裝置,是因為

核函式的呼叫是非同步的,即主機發出調用核函式的命令後會立即執行後面的語句

,不會等待核函式執行完畢。需要注意的是,上述同步函式比較耗時,如果在程式的較內淺層迴圈呼叫的話會嚴重降低程式的效能。

為展示對核函式呼叫的檢查,我們稍改上面的程式碼:

#include

“error。cuh”

#include

#include

const

double

EPSILON

=

1。0e-15

const

double

a

=

1。23

const

double

b

=

2。34

const

double

c

=

3。57

void

__global__

add

const

double

*

x

const

double

*

y

double

*

z

const

int

N

);

void

check

const

double

*

z

const

int

N

);

int

main

void

{

const

int

N

=

100000000

const

int

M

=

sizeof

double

*

N

double

*

h_x

=

double

*

malloc

M

);

double

*

h_y

=

double

*

malloc

M

);

double

*

h_z

=

double

*

malloc

M

);

for

int

n

=

0

n

<

N

++

n

{

h_x

n

=

a

h_y

n

=

b

}

double

*

d_x

*

d_y

*

d_z

CHECK

cudaMalloc

((

void

**

&

d_x

M

));

CHECK

cudaMalloc

((

void

**

&

d_y

M

));

CHECK

cudaMalloc

((

void

**

&

d_z

M

));

CHECK

cudaMemcpy

d_x

h_x

M

cudaMemcpyHostToDevice

));

CHECK

cudaMemcpy

d_y

h_y

M

cudaMemcpyHostToDevice

));

const

int

block_size

=

1280

const

int

grid_size

=

N

+

block_size

-

1

/

block_size

add

<<<

grid_size

block_size

>>>

d_x

d_y

d_z

N

);

CHECK

cudaGetLastError

());

CHECK

cudaDeviceSynchronize

());

CHECK

cudaMemcpy

h_z

d_z

M

cudaMemcpyDeviceToHost

));

check

h_z

N

);

free

h_x

);

free

h_y

);

free

h_z

);

CHECK

cudaFree

d_x

));

CHECK

cudaFree

d_y

));

CHECK

cudaFree

d_z

));

return

0

}

void

__global__

add

const

double

*

x

const

double

*

y

double

*

z

const

int

N

{

const

int

n

=

blockDim

x

*

blockIdx

x

+

threadIdx

x

if

n

<

N

{

z

n

=

x

n

+

y

n

];

}

}

void

check

const

double

*

z

const

int

N

{

bool

has_error

=

false

for

int

n

=

0

n

<

N

n

++

{

if

fabs

z

n

-

c

>

EPSILON

{

has_error

=

true

}

}

printf

“%s

\n

has_error

“Has errors”

“No errors”

);

}

執行緒塊大小的最大值為1024,我們為了實驗錯誤檢測程式,將執行配置中的執行緒塊大小改成1280,然後編譯和執行:

CUDA 3:錯誤檢測

可以看到核函式不能成功地呼叫,並且我們成功捕抓到了核函式的錯誤。它告訴我們第36行出現了核函式地執行配置引數設定無效。如果去掉CHECK(cudaDeviceSynchronize())其實也能成功捕抓上述錯誤資訊。這是因為後面的資料傳輸CHECK(cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost))能夠起到一種隱式地同步主機與裝置的作用。一般情況下要獲得精確的錯誤資訊還需要

顯式的同步

。例如呼叫cudaDeviceSynchronize()函式或者臨時將環境變數CUDA_LAUNCH_BLOCKING的值設為1:

$export

CUDA_LAUNCH_BLOCKING

=

1

這樣核函式的呼叫就是同步的了,即主機呼叫一個核函式後必須等核函式執行完才能向下執行

用CUDA-MEMCHECK檢查記憶體錯誤

CUDA有一個名為CUDA-MEMCHECK的工具集,包括

memcheck、racecheck、initcheck、synccheck

四個工具,都可以由可執行檔案cuda-memcheck呼叫:

$cuda

-memcheck

-

-tool

memcheck

[options] app_name [options]

$cuda

-memcheck

-

-tool

racecheck

[options] app_name [options]

$cuda

-memcheck

-

-tool

initcheck

[options] app_name [options]

$cuda

-memcheck

-

-tool

synccheck

[options] app_name [options]

其中memcheck工具可以簡化成:

$cuda

-memcheck

[options] app_name [options]

標簽: check  double  函式  Error  __