一般的なCUDA プログラミング 構造は、5 つの主要なステップで構成されます。
- GPU メモリを割り当てます。
- CPU メモリからGPU メモリにデータをコピーします。
- CUDA カーネル関数を呼び出して、プログラムで指定された操作を完了します。
- データを GPU からCPU メモリにコピーして戻します。
- GPU メモリ領域を解放します。
まずはGPUメモリを割り当てる方法を見てみましょう。
目次
1. メモリ管理機能
4種類のメモリ管理関数、目的と標準C言語を1対1対応可能。違いは、一方は cpu 上で割り当てと解放を管理し、もう一方は GPU 上で動作することです。
1.1 独立したメモリ
cudaError_t cudaMalloc (void** devPtr, size_t size)
デバイス側 (GPU) は、size bytesのリニア メモリを割り当てます。
1.2 データコピー
cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)
ここでのデータ コピーは、ホスト側とデバイス側の間でcount バイトのデータを転送するために使用されます。送信方向は種類によって指定されており、種類は以下の4種類があります。
cudaMemcpy 関数が戻り、転送操作が完了するまでホスト アプリケーションはブロックされるため、この関数は同期的に実行されます。
このうち、上記で返された cudaError_t は、読み取り可能なエラー メッセージとして解釈できます。
char* cudaGetErrorString(cudaError_t e)
この関数は C 言語の strerror に似ています。
2.GPUメモリ構造
GPU のメモリには、グローバル メモリと共有メモリの 2 つの主なタイプがあります。グローバル メモリはCPU のシステム メモリに似ており、共有メモリは CPUキャッシュに似ています。
3.栗
機能: 配列 a の数値を配列 b に加算し、配列 c に格納します。
3.1 純粋な C で記述する (CPU にのみ追加)
#include <time.h>
#include <stdlib.h> // srand
// cpu
void sumArraysOnHost(float* a, float* b, float* c, const int N)
{
for (int i = 0; i < N; i++)
{
c[i] = a[i] + b[i];
}
}
void initialData(float* p, const int N)
{
//generate different seed from random number
time_t t;
srand((unsigned int)time(&t)); // 生成种子
for (int i = 0; i < N; i++)
{
p[i] = (float)(rand() & 0xFF) / 10.0f; // 随机数
}
}
int main(void)
{
// 1 分配内存
int nElem = 1024;
size_t nBytes = nElem * sizeof(nElem);
float* h_a, * h_b, * h_c;
h_a = (float*)malloc(nBytes);
h_b = (float*)malloc(nBytes);
h_c = (float*)malloc(nBytes);
// 初始化
initialData(h_a, nElem);
initialData(h_b, nElem);
// 2 直接在cpu上相加
sumArraysOnHost(h_a, h_b, h_c, nElem);
// 3 释放内存
free(h_a);
free(h_b);
free(h_c);
return 0;
}
3.2 cuda書き込み(GPU上の追加)
加算演算を GPU に置きます。完全な典型的な cuda プログラミング構造を以下に示します。
3.2.1 スレッド階層
スレッド階層。グリッドには多数のブロックが含まれ、ブロックには多数のスレッドが含まれます。
カーネルの起動によって生成されたすべてのスレッドは、集合的にグリッドと呼ばれます。同じグリッド内のすべてのスレッドは、同じグローバル メモリ空間 (システム メモリに相当) を共有します。blockIdx (スレッド グリッド内のスレッド ブロックのインデックス)、threadIdx (ブロック内のスレッド インデックス)。
カーネル関数を実行するとき、CUDA ランタイムは座標変数 blockIdx および threadIdx (自動生成変数) を各スレッドに割り当てます。
3.2.2 定義
ブロックのサイズを定義し、ブロックとデータのサイズに基づいてグリッド サイズを計算します。たとえば、データが 6 つあるとします。
int nElem = 6;
// 定义一维数组线程块
dim3 block(3); // 块内有3个线程组
// 定义一维数组网格. 有3个块。即网格大小3是块大小3的倍数。
dim3 grid((nElem + block.x - 1) / block.x); // 为了保证倍数关系。(6+3-1)/3 = 3
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
__global__ void checkIndex(void) {
printf("blockIdx: (%d, %d, %d) threadIdx: (%d, %d, %d) \n"
"gridDim: (%d, %d, %d) blockDim: (%d, %d, %d) \n",
blockIdx.x, blockIdx.y, blockIdx.z,
threadIdx.x, threadIdx.y, threadIdx.z,
gridDim.x, gridDim.y, gridDim.z,
blockDim.x, blockDim.y, blockDim.z
);
}
int main(void)
{
int nElem = 6;
// 定义一维数组线程块
dim3 block(3); // 块内有3个线程组
// 定义一维数组网格. 有3个块。即网格大小3是块大小3的倍数。
dim3 grid((nElem + block.x - 1) / block.x); // (6+3-1)/3 = 3
// check grid and block dimension from the host side.
printf("host gridIdx: (%d, %d, %d) \n", grid.x, grid.y, grid.z);
printf("host blockIdx: (%d, %d, %d) \n", block.x, block.y, block.z);
// check grid and block dimension from the device side.
checkIndex << <grid, block >> > (); // <<<grid_dim,block_dim>>>
// reset device before you leavec
cudaDeviceReset();
return 0;
}
カーネル関数 checkIndex が実行されると、CUDA ランタイムが座標変数 blockIdx と threadIdx を各スレッドに割り当てること がわかります。
の、
(1) blockDim と GridDim は両方とも (3,1,1) です。
(2) blockIdx の (0,0,0) -> (2,0,0) -> (1,0,0).block の順序はランダムです。
(3) 異なるブロック内の threadIdx は常に (0,0,0) -> (1,0,0) -> (2,0,0) になります。
3.2.3 同期の問題
(1)すべてのカーネル関数呼び出しとホスト スレッドは非同期です。カーネル関数を呼び出した後は、カーネル関数の実行が完了するのを待たずに実行を継続します。
(2) 同期が必要な場合は、すべてのカーネル関数の実行が終了するまでホストに強制的に待機させるため、次のように設定します。
cudaError_t cudaDeviceSynchronize(void);
(3) また、一部の cuda API はホストとデバイス間で暗黙的に同期されます。たとえば、cudaMemcpy の場合、ホストは実行を続ける前にコピーが完了するまで待つ必要があります。
3.2.4 カーネル機能
カーネル関数はデバイス側で実行されるコードです。カーネル関数が呼び出されると、多くの異なる CUDA スレッドが同じ計算タスクを並行して実行します。
__global__ void kernel_name(argument list);
知らせ:
(1) カーネル関数は void 戻り型である必要があります。
(2) 関数の型修飾子 (修飾子) は、どこで実行するか、誰が呼び出されるかを決定します。
__device__ 修飾子と __host__ 修飾子を一緒に使用すると、ホスト側とデバイス側の両方で関数をコンパイルできます。
(3) CUDAカーネル機能の制限事項
すべてのカーネル関数に次の制限が適用されます。
デバイス メモリにのみアクセスできる
戻り値の型が void である必要がある
可変数の引数をサポートしない
静的変数をサポートしない
非同期動作を示す
// 主机端:纯c语言
void sumArraysOnHost(float* a, float* b, float* c, const int N)
{
for (int i = 0; i < N; i++)
c[i] = a[i] + b[i];
}
// 设备端:去掉了循环。内置的线程坐标变量替换了数组索引
__global__ void sumArraysOnDevice(float* a, float* b, float* c)
{
int i = threadIdx.x; // sumArraysOnDevice << <1, 32 >> > (a, b, c);
//int i = blockIdx.x; // sumArraysOnDevice << <32, 1>> > (a, b, c);
c[i] = a[i] + b[i];
}
// 调用方式:只有一个块,块内有32个线程。并发执行
sumArraysOnDevice << <1, 32 >> > (a, b, c);
// 强制用一个块和一个线程执行核函数,这模拟了串行执行程序。有助于调试和验证结果
sumArraysOnDevice << <1, 1 >> > (a, b, c);
グリッド内にブロックが 1 つだけあり、ブロック内に 32 個のスレッドがある場合は、threadIdx.x をインデックスとして使用できます。
グリッド内に 32 のブロックがあり、各ブロック内に 1 つのスレッドがある場合は、blockIdx.x をインデックスとして使用できます。
3.2.5 デバッグエラー
#define CHECK(call)
{
const cudaError_t error = call;
if (error != cudaSuccess)
{
printf("Error: %s: %d, ", __FILE__, __LINE__);
printf("code: %d, reason: %s\n", error, cudaGetErrorString(error));
exit(1);
}
}
CHECK(cudaMemCpy(d_c, gpuRef, nBytes, cudaMemcpyHostToDevice));
3.2.6 完全な cuda プログラム
#include "cuda_runtime.h"
#include "device_launch_parameters.h" // threadIdx
#include <stdio.h> // io
#include <time.h> // time_t
#include <stdlib.h> // rand
#include <memory.h> //memset
#define CHECK(call) \
{ \
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); \
} \
}
void checkResult(float* hostRef, float* deviceRef, const int N)
{
double eps = 1.0E-8;
int match = 1;
for (int i = 0; i < N; i++)
{
if (hostRef[i] - deviceRef[i] > eps)
{
match = 0;
printf("\nArrays do not match\n");
printf("host %5.2f gpu %5.2f at current %d\n", hostRef[i], deviceRef[i], i);
break;
}
}
if (match)
printf("Arrays match!\n");
}
void initialData(float* p, const int N)
{
//generate different seed from random number
time_t t;
srand((unsigned int)time(&t)); // 生成种子
for (int i = 0; i < N; i++)
{
p[i] = (float)(rand() & 0xFF) / 10.0f; // 随机数
}
}
__global__ void checkIndex(void) {
printf("blockIdx: (%d, %d, %d) threadIdx: (%d, %d, %d) \n"
"gridDim: (%d, %d, %d) blockDim: (%d, %d, %d) \n",
blockIdx.x, blockIdx.y, blockIdx.z,
threadIdx.x, threadIdx.y, threadIdx.z,
gridDim.x, gridDim.y, gridDim.z,
blockDim.x, blockDim.y, blockDim.z
);
}
// cpu
void sumArraysOnHost(float* a, float* b, float* c, const int N)
{
for (int i = 0; i < N; i++)
{
c[i] = a[i] + b[i];
}
}
// 设备端:去掉了循环
__global__ void sumArraysOnDevice(float* a, float* b, float* c, const int N)
{
int i = threadIdx.x;
c[i] = a[i] + b[i];
}
int main(void)
{
int device = 0;
cudaSetDevice(device); // 设置显卡号
// 1 分配内存
// host memory
int nElem = 32;
size_t nBytes = nElem * sizeof(nElem);
float* h_a, * h_b, * hostRef, *gpuRef;
h_a = (float*)malloc(nBytes);
h_b = (float*)malloc(nBytes);
hostRef = (float*)malloc(nBytes); // 主机端求得的结果
gpuRef = (float*)malloc(nBytes); // 设备端拷回的数据
// 初始化
initialData(h_a, nElem);
initialData(h_b, nElem);
memset(hostRef, 0, nBytes);
memset(hostRef, 0, nBytes);
// device memory
float* d_a, * d_b, * d_c;
cudaMalloc((float**)&d_a, nBytes);
cudaMalloc((float**)&d_b, nBytes);
cudaMalloc((float**)&d_c, nBytes);
// 2 transfer data from host to device
cudaMemcpy(d_a, h_a, nBytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, nBytes, cudaMemcpyHostToDevice);
// 3 在主机端调用设备端核函数
dim3 block(nElem);
dim3 grid(nElem / block.x);
sumArraysOnDevice<<<grid, block>>>(d_a, d_b, d_c, nElem);
// 4 transfer data from device to host
cudaMemcpy(gpuRef, d_c, nBytes, cudaMemcpyDeviceToHost);
//确认下结果
sumArraysOnHost(h_a, h_b, hostRef, nElem);
checkResult(hostRef, gpuRef, nElem);
// 5 释放内存
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
free(h_a);
free(h_b);
free(hostRef);
free(gpuRef);
return 0;
}