「cuda C プログラミングの決定版ガイド」02 - メモリ管理とスレッド管理

一般的なCUDA プログラミング 構造は、5 つの主要なステップで構成されます。

  1. GPU メモリを割り当てます。
  2. CPU メモリからGPU メモリにデータをコピーします。
  3. CUDA カーネル関数を呼び出して、プログラムで指定された操作を完了します。
  4. データを GPU からCPU メモリにコピーして戻します。
  5. GPU メモリ領域を解放します。

まずはGPUメモリを割り当てる方法を見てみましょう。

目次

1. メモリ管理機能

1.1 独立したメモリ

1.2 データコピー

2.GPUメモリ構造

3.栗

3.1 純粋な C で記述する (CPU にのみ追加)

3.2 cuda書き込み(GPU上の追加)

3.2.1 スレッド階層

3.2.2 定義

3.2.3 同期の問題

3.2.4 カーネル機能

3.2.5 デバッグエラー

3.2.6 完全な cuda プログラム


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;
}

おすすめ

転載: blog.csdn.net/jizhidexiaoming/article/details/132010214